MediaWiki REL1_33
EditPage.php
Go to the documentation of this file.
1<?php
27use Wikimedia\ScopedCallback;
28
44class EditPage {
48 const UNICODE_CHECK = 'ℳ𝒲β™₯π“Šπ“ƒπ’Ύπ’Έβ„΄π’Ήβ„―';
49
53 const AS_SUCCESS_UPDATE = 200;
54
59
63 const AS_HOOK_ERROR = 210;
64
69
74
78 const AS_CONTENT_TOO_BIG = 216;
79
84
89
93 const AS_READ_ONLY_PAGE = 220;
94
98 const AS_RATE_LIMITED = 221;
99
105
111
115 const AS_BLANK_ARTICLE = 224;
116
121
126 const AS_SUMMARY_NEEDED = 226;
127
131 const AS_TEXTBOX_EMPTY = 228;
132
137
141 const AS_END = 231;
142
146 const AS_SPAM_ERROR = 232;
147
152
157
163
168 const AS_SELF_REDIRECT = 236;
169
175
179 const AS_PARSE_ERROR = 240;
180
186
191
195 const EDITFORM_ID = 'editform';
196
201 const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
202
217
222 public $mArticle;
224 private $page;
225
230 public $mTitle;
231
233 private $mContextTitle = null;
234
236 public $action = 'submit';
237
242 public $isConflict = false;
243
245 public $isNew = false;
246
249
251 public $formtype;
252
258
261
263 public $mTokenOk = false;
264
266 public $mTokenOkExceptSuffix = false;
267
269 public $mTriedSave = false;
270
272 public $incompleteForm = false;
273
275 public $tooBig = false;
276
278 public $missingComment = false;
279
281 public $missingSummary = false;
282
284 public $allowBlankSummary = false;
285
287 protected $blankArticle = false;
288
290 protected $allowBlankArticle = false;
291
293 protected $selfRedirect = false;
294
296 protected $allowSelfRedirect = false;
297
299 public $autoSumm = '';
300
302 public $hookError = '';
303
306
308 public $hasPresetSummary = false;
309
311 public $mBaseRevision = false;
312
314 public $mShowSummaryField = true;
315
316 # Form values
317
319 public $save = false;
320
322 public $preview = false;
323
325 public $diff = false;
326
328 public $minoredit = false;
329
331 public $watchthis = false;
332
334 public $recreate = false;
335
339 public $textbox1 = '';
340
342 public $textbox2 = '';
343
345 public $summary = '';
346
350 public $nosummary = false;
351
356 public $edittime = '';
357
369 private $editRevId = null;
370
372 public $section = '';
373
375 public $sectiontitle = '';
376
380 public $starttime = '';
381
387 public $oldid = 0;
388
394 public $parentRevId = 0;
395
397 public $editintro = '';
398
400 public $scrolltop = null;
401
403 public $bot = true;
404
407
409 public $contentFormat = null;
410
412 private $changeTags = null;
413
414 # Placeholders for text injection by hooks (must be HTML)
415 # extensions should take care to _append_ to the present value
416
418 public $editFormPageTop = '';
419 public $editFormTextTop = '';
426 public $mPreloadContent = null;
427
428 /* $didSave should be set to true whenever an article was successfully altered. */
429 public $didSave = false;
430 public $undidRev = 0;
431
432 public $suppressIntro = false;
433
435 protected $edit;
436
438 protected $contentLength = false;
439
443 private $enableApiEditOverride = false;
444
448 protected $context;
449
453 private $isOldRev = false;
454
459
466
471
475 public function __construct( Article $article ) {
476 $this->mArticle = $article;
477 $this->page = $article->getPage(); // model object
478 $this->mTitle = $article->getTitle();
479
480 // Make sure the local context is in sync with other member variables.
481 // Particularly make sure everything is using the same WikiPage instance.
482 // This should probably be the case in Article as well, but it's
483 // particularly important for EditPage, to make use of the in-place caching
484 // facility in WikiPage::prepareContentForEdit.
485 $this->context = new DerivativeContext( $article->getContext() );
486 $this->context->setWikiPage( $this->page );
487 $this->context->setTitle( $this->mTitle );
488
489 $this->contentModel = $this->mTitle->getContentModel();
490
491 $handler = ContentHandler::getForModelID( $this->contentModel );
492 $this->contentFormat = $handler->getDefaultFormat();
493 $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ];
494 }
495
499 public function getArticle() {
500 return $this->mArticle;
501 }
502
507 public function getContext() {
508 return $this->context;
509 }
510
515 public function getTitle() {
516 return $this->mTitle;
517 }
518
524 public function setContextTitle( $title ) {
525 $this->mContextTitle = $title;
526 }
527
536 public function getContextTitle() {
537 if ( is_null( $this->mContextTitle ) ) {
538 wfDeprecated( __METHOD__ . ' called with no title set', '1.32' );
539 global $wgTitle;
540 return $wgTitle;
541 } else {
543 }
544 }
545
553 public function isSupportedContentModel( $modelId ) {
554 return $this->enableApiEditOverride === true ||
555 ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
556 }
557
564 public function setApiEditOverride( $enableOverride ) {
565 $this->enableApiEditOverride = $enableOverride;
566 }
567
571 public function submit() {
572 wfDeprecated( __METHOD__, '1.29' );
573 $this->edit();
574 }
575
587 public function edit() {
588 // Allow extensions to modify/prevent this form or submission
589 if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
590 return;
591 }
592
593 wfDebug( __METHOD__ . ": enter\n" );
594
595 $request = $this->context->getRequest();
596 // If they used redlink=1 and the page exists, redirect to the main article
597 if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
598 $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
599 return;
600 }
601
602 $this->importFormData( $request );
603 $this->firsttime = false;
604
605 if ( wfReadOnly() && $this->save ) {
606 // Force preview
607 $this->save = false;
608 $this->preview = true;
609 }
610
611 if ( $this->save ) {
612 $this->formtype = 'save';
613 } elseif ( $this->preview ) {
614 $this->formtype = 'preview';
615 } elseif ( $this->diff ) {
616 $this->formtype = 'diff';
617 } else { # First time through
618 $this->firsttime = true;
619 if ( $this->previewOnOpen() ) {
620 $this->formtype = 'preview';
621 } else {
622 $this->formtype = 'initial';
623 }
624 }
625
626 $permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
627 if ( $permErrors ) {
628 wfDebug( __METHOD__ . ": User can't edit\n" );
629
630 if ( $this->context->getUser()->getBlock() ) {
631 // track block with a cookie if it doesn't exists already
632 $this->context->getUser()->trackBlockWithCookie();
633
634 // Auto-block user's IP if the account was "hard" blocked
635 if ( !wfReadOnly() ) {
636 DeferredUpdates::addCallableUpdate( function () {
637 $this->context->getUser()->spreadAnyEditBlock();
638 } );
639 }
640 }
641 $this->displayPermissionsError( $permErrors );
642
643 return;
644 }
645
646 $revision = $this->mArticle->getRevisionFetched();
647 // Disallow editing revisions with content models different from the current one
648 // Undo edits being an exception in order to allow reverting content model changes.
649 if ( $revision
650 && $revision->getContentModel() !== $this->contentModel
651 ) {
652 $prevRev = null;
653 if ( $this->undidRev ) {
654 $undidRevObj = Revision::newFromId( $this->undidRev );
655 $prevRev = $undidRevObj ? $undidRevObj->getPrevious() : null;
656 }
657 if ( !$this->undidRev
658 || !$prevRev
659 || $prevRev->getContentModel() !== $this->contentModel
660 ) {
662 $this->getContentObject(),
663 $this->context->msg(
664 'contentmodelediterror',
665 $revision->getContentModel(),
666 $this->contentModel
667 )->plain()
668 );
669 return;
670 }
671 }
672
673 $this->isConflict = false;
674
675 # Show applicable editing introductions
676 if ( $this->formtype == 'initial' || $this->firsttime ) {
677 $this->showIntro();
678 }
679
680 # Attempt submission here. This will check for edit conflicts,
681 # and redundantly check for locked database, blocked IPs, etc.
682 # that edit() already checked just in case someone tries to sneak
683 # in the back door with a hand-edited submission URL.
684
685 if ( $this->formtype == 'save' ) {
686 $resultDetails = null;
687 $status = $this->attemptSave( $resultDetails );
688 if ( !$this->handleStatus( $status, $resultDetails ) ) {
689 return;
690 }
691 }
692
693 # First time through: get contents, set time for conflict
694 # checking, etc.
695 if ( $this->formtype == 'initial' || $this->firsttime ) {
696 if ( $this->initialiseForm() === false ) {
697 $out = $this->context->getOutput();
698 if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it
699 $this->noSuchSectionPage();
700 }
701 return;
702 }
703
704 if ( !$this->mTitle->getArticleID() ) {
705 Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
706 } else {
707 Hooks::run( 'EditFormInitialText', [ $this ] );
708 }
709
710 }
711
712 $this->showEditForm();
713 }
714
719 protected function getEditPermissionErrors( $rigor = 'secure' ) {
720 $user = $this->context->getUser();
721 $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
722 # Can this title be created?
723 if ( !$this->mTitle->exists() ) {
724 $permErrors = array_merge(
725 $permErrors,
727 $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
728 $permErrors
729 )
730 );
731 }
732 # Ignore some permissions errors when a user is just previewing/viewing diffs
733 $remove = [];
734 foreach ( $permErrors as $error ) {
735 if ( ( $this->preview || $this->diff )
736 && (
737 $error[0] == 'blockedtext' ||
738 $error[0] == 'autoblockedtext' ||
739 $error[0] == 'systemblockedtext'
740 )
741 ) {
742 $remove[] = $error;
743 }
744 }
745 $permErrors = wfArrayDiff2( $permErrors, $remove );
746
747 return $permErrors;
748 }
749
763 protected function displayPermissionsError( array $permErrors ) {
764 $out = $this->context->getOutput();
765 if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
766 // The edit page was reached via a red link.
767 // Redirect to the article page and let them click the edit tab if
768 // they really want a permission error.
769 $out->redirect( $this->mTitle->getFullURL() );
770 return;
771 }
772
773 $content = $this->getContentObject();
774
775 # Use the normal message if there's nothing to display
776 if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
777 $action = $this->mTitle->exists() ? 'edit' :
778 ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
779 throw new PermissionsError( $action, $permErrors );
780 }
781
783 $content,
784 $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
785 );
786 }
787
793 protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
794 $out = $this->context->getOutput();
795 Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
796
797 $out->setRobotPolicy( 'noindex,nofollow' );
798 $out->setPageTitle( $this->context->msg(
799 'viewsource-title',
800 $this->getContextTitle()->getPrefixedText()
801 ) );
802 $out->addBacklinkSubtitle( $this->getContextTitle() );
803 $out->addHTML( $this->editFormPageTop );
804 $out->addHTML( $this->editFormTextTop );
805
806 if ( $errorMessage !== '' ) {
807 $out->addWikiTextAsInterface( $errorMessage );
808 $out->addHTML( "<hr />\n" );
809 }
810
811 # If the user made changes, preserve them when showing the markup
812 # (This happens when a user is blocked during edit, for instance)
813 if ( !$this->firsttime ) {
814 $text = $this->textbox1;
815 $out->addWikiMsg( 'viewyourtext' );
816 } else {
817 try {
818 $text = $this->toEditText( $content );
819 } catch ( MWException $e ) {
820 # Serialize using the default format if the content model is not supported
821 # (e.g. for an old revision with a different model)
822 $text = $content->serialize();
823 }
824 $out->addWikiMsg( 'viewsourcetext' );
825 }
826
827 $out->addHTML( $this->editFormTextBeforeContent );
828 $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
829 $out->addHTML( $this->editFormTextAfterContent );
830
831 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
832
833 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
834
835 $out->addHTML( $this->editFormTextBottom );
836 if ( $this->mTitle->exists() ) {
837 $out->returnToMain( null, $this->mTitle );
838 }
839 }
840
846 protected function previewOnOpen() {
847 $config = $this->context->getConfig();
848 $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
849 $request = $this->context->getRequest();
850 if ( $config->get( 'RawHtml' ) ) {
851 // If raw HTML is enabled, disable preview on open
852 // since it has to be posted with a token for
853 // security reasons
854 return false;
855 }
856 if ( $request->getVal( 'preview' ) == 'yes' ) {
857 // Explicit override from request
858 return true;
859 } elseif ( $request->getVal( 'preview' ) == 'no' ) {
860 // Explicit override from request
861 return false;
862 } elseif ( $this->section == 'new' ) {
863 // Nothing *to* preview for new sections
864 return false;
865 } elseif ( ( $request->getCheck( 'preload' ) || $this->mTitle->exists() )
866 && $this->context->getUser()->getOption( 'previewonfirst' )
867 ) {
868 // Standard preference behavior
869 return true;
870 } elseif ( !$this->mTitle->exists()
871 && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
872 && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
873 ) {
874 // Categories are special
875 return true;
876 } else {
877 return false;
878 }
879 }
880
887 protected function isWrongCaseUserConfigPage() {
888 if ( $this->mTitle->isUserConfigPage() ) {
889 $name = $this->mTitle->getSkinFromConfigSubpage();
890 $skins = array_merge(
891 array_keys( Skin::getSkinNames() ),
892 [ 'common' ]
893 );
894 return !in_array( $name, $skins )
895 && in_array( strtolower( $name ), $skins );
896 } else {
897 return false;
898 }
899 }
900
908 protected function isSectionEditSupported() {
909 $contentHandler = ContentHandler::getForTitle( $this->mTitle );
910 return $contentHandler->supportsSections();
911 }
912
918 public function importFormData( &$request ) {
919 # Section edit can come from either the form or a link
920 $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
921
922 if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
923 throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
924 }
925
926 $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
927
928 if ( $request->wasPosted() ) {
929 # These fields need to be checked for encoding.
930 # Also remove trailing whitespace, but don't remove _initial_
931 # whitespace from the text boxes. This may be significant formatting.
932 $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
933 if ( !$request->getCheck( 'wpTextbox2' ) ) {
934 // Skip this if wpTextbox2 has input, it indicates that we came
935 // from a conflict page with raw page text, not a custom form
936 // modified by subclasses
938 if ( $textbox1 !== null ) {
939 $this->textbox1 = $textbox1;
940 }
941 }
942
943 $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
944
945 $this->summary = $request->getText( 'wpSummary' );
946
947 # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
948 # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
949 # section titles.
950 $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
951
952 # Treat sectiontitle the same way as summary.
953 # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
954 # currently doing double duty as both edit summary and section title. Right now this
955 # is just to allow API edits to work around this limitation, but this should be
956 # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
957 $this->sectiontitle = $request->getText( 'wpSectionTitle' );
958 $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
959
960 $this->edittime = $request->getVal( 'wpEdittime' );
961 $this->editRevId = $request->getIntOrNull( 'editRevId' );
962 $this->starttime = $request->getVal( 'wpStarttime' );
963
964 $undidRev = $request->getInt( 'wpUndidRevision' );
965 if ( $undidRev ) {
966 $this->undidRev = $undidRev;
967 }
968
969 $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
970
971 if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
972 // wpTextbox1 field is missing, possibly due to being "too big"
973 // according to some filter rules such as Suhosin's setting for
974 // suhosin.request.max_value_length (d'oh)
975 $this->incompleteForm = true;
976 } else {
977 // If we receive the last parameter of the request, we can fairly
978 // claim the POST request has not been truncated.
979 $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
980 }
981 if ( $this->incompleteForm ) {
982 # If the form is incomplete, force to preview.
983 wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
984 wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) . "\n" );
985 $this->preview = true;
986 } else {
987 $this->preview = $request->getCheck( 'wpPreview' );
988 $this->diff = $request->getCheck( 'wpDiff' );
989
990 // Remember whether a save was requested, so we can indicate
991 // if we forced preview due to session failure.
992 $this->mTriedSave = !$this->preview;
993
994 if ( $this->tokenOk( $request ) ) {
995 # Some browsers will not report any submit button
996 # if the user hits enter in the comment box.
997 # The unmarked state will be assumed to be a save,
998 # if the form seems otherwise complete.
999 wfDebug( __METHOD__ . ": Passed token check.\n" );
1000 } elseif ( $this->diff ) {
1001 # Failed token check, but only requested "Show Changes".
1002 wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
1003 } else {
1004 # Page might be a hack attempt posted from
1005 # an external site. Preview instead of saving.
1006 wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
1007 $this->preview = true;
1008 }
1009 }
1010 $this->save = !$this->preview && !$this->diff;
1011 if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
1012 $this->edittime = null;
1013 }
1014
1015 if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1016 $this->starttime = null;
1017 }
1018
1019 $this->recreate = $request->getCheck( 'wpRecreate' );
1020
1021 $this->minoredit = $request->getCheck( 'wpMinoredit' );
1022 $this->watchthis = $request->getCheck( 'wpWatchthis' );
1023
1024 $user = $this->context->getUser();
1025 # Don't force edit summaries when a user is editing their own user or talk page
1026 if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
1027 && $this->mTitle->getText() == $user->getName()
1028 ) {
1029 $this->allowBlankSummary = true;
1030 } else {
1031 $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1032 || !$user->getOption( 'forceeditsummary' );
1033 }
1034
1035 $this->autoSumm = $request->getText( 'wpAutoSummary' );
1036
1037 $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1038 $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
1039
1040 $changeTags = $request->getVal( 'wpChangeTags' );
1041 if ( is_null( $changeTags ) || $changeTags === '' ) {
1042 $this->changeTags = [];
1043 } else {
1044 $this->changeTags = array_filter( array_map( 'trim', explode( ',',
1045 $changeTags ) ) );
1046 }
1047 } else {
1048 # Not a posted form? Start with nothing.
1049 wfDebug( __METHOD__ . ": Not a posted form.\n" );
1050 $this->textbox1 = '';
1051 $this->summary = '';
1052 $this->sectiontitle = '';
1053 $this->edittime = '';
1054 $this->editRevId = null;
1055 $this->starttime = wfTimestampNow();
1056 $this->edit = false;
1057 $this->preview = false;
1058 $this->save = false;
1059 $this->diff = false;
1060 $this->minoredit = false;
1061 // Watch may be overridden by request parameters
1062 $this->watchthis = $request->getBool( 'watchthis', false );
1063 $this->recreate = false;
1064
1065 // When creating a new section, we can preload a section title by passing it as the
1066 // preloadtitle parameter in the URL (T15100)
1067 if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1068 $this->sectiontitle = $request->getVal( 'preloadtitle' );
1069 // Once wpSummary isn't being use for setting section titles, we should delete this.
1070 $this->summary = $request->getVal( 'preloadtitle' );
1071 } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) !== '' ) {
1072 $this->summary = $request->getText( 'summary' );
1073 if ( $this->summary !== '' ) {
1074 $this->hasPresetSummary = true;
1075 }
1076 }
1077
1078 if ( $request->getVal( 'minor' ) ) {
1079 $this->minoredit = true;
1080 }
1081 }
1082
1083 $this->oldid = $request->getInt( 'oldid' );
1084 $this->parentRevId = $request->getInt( 'parentRevId' );
1085
1086 $this->bot = $request->getBool( 'bot', true );
1087 $this->nosummary = $request->getBool( 'nosummary' );
1088
1089 // May be overridden by revision.
1090 $this->contentModel = $request->getText( 'model', $this->contentModel );
1091 // May be overridden by revision.
1092 $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1093
1094 try {
1095 $handler = ContentHandler::getForModelID( $this->contentModel );
1097 throw new ErrorPageError(
1098 'editpage-invalidcontentmodel-title',
1099 'editpage-invalidcontentmodel-text',
1100 [ wfEscapeWikiText( $this->contentModel ) ]
1101 );
1102 }
1103
1104 if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1105 throw new ErrorPageError(
1106 'editpage-notsupportedcontentformat-title',
1107 'editpage-notsupportedcontentformat-text',
1108 [
1109 wfEscapeWikiText( $this->contentFormat ),
1110 wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1111 ]
1112 );
1113 }
1114
1121 $this->editintro = $request->getText( 'editintro',
1122 // Custom edit intro for new sections
1123 $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1124
1125 // Allow extensions to modify form data
1126 Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1127 }
1128
1138 protected function importContentFormData( &$request ) {
1139 return; // Don't do anything, EditPage already extracted wpTextbox1
1140 }
1141
1147 public function initialiseForm() {
1148 $this->edittime = $this->page->getTimestamp();
1149 $this->editRevId = $this->page->getLatest();
1150
1151 $content = $this->getContentObject( false ); # TODO: track content object?!
1152 if ( $content === false ) {
1153 return false;
1154 }
1155 $this->textbox1 = $this->toEditText( $content );
1156
1157 $user = $this->context->getUser();
1158 // activate checkboxes if user wants them to be always active
1159 # Sort out the "watch" checkbox
1160 if ( $user->getOption( 'watchdefault' ) ) {
1161 # Watch all edits
1162 $this->watchthis = true;
1163 } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1164 # Watch creations
1165 $this->watchthis = true;
1166 } elseif ( $user->isWatched( $this->mTitle ) ) {
1167 # Already watched
1168 $this->watchthis = true;
1169 }
1170 if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1171 $this->minoredit = true;
1172 }
1173 if ( $this->textbox1 === false ) {
1174 return false;
1175 }
1176 return true;
1177 }
1178
1186 protected function getContentObject( $def_content = null ) {
1187 $content = false;
1188
1189 $user = $this->context->getUser();
1190 $request = $this->context->getRequest();
1191 // For message page not locally set, use the i18n message.
1192 // For other non-existent articles, use preload text if any.
1193 if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1194 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1195 # If this is a system message, get the default text.
1196 $msg = $this->mTitle->getDefaultMessageText();
1197
1198 $content = $this->toEditContent( $msg );
1199 }
1200 if ( $content === false ) {
1201 # If requested, preload some text.
1202 $preload = $request->getVal( 'preload',
1203 // Custom preload text for new sections
1204 $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1205 $params = $request->getArray( 'preloadparams', [] );
1206
1207 $content = $this->getPreloadedContent( $preload, $params );
1208 }
1209 // For existing pages, get text based on "undo" or section parameters.
1210 } elseif ( $this->section != '' ) {
1211 // Get section edit text (returns $def_text for invalid sections)
1212 $orig = $this->getOriginalContent( $user );
1213 $content = $orig ? $orig->getSection( $this->section ) : null;
1214
1215 if ( !$content ) {
1216 $content = $def_content;
1217 }
1218 } else {
1219 $undoafter = $request->getInt( 'undoafter' );
1220 $undo = $request->getInt( 'undo' );
1221
1222 if ( $undo > 0 && $undoafter > 0 ) {
1223 $undorev = Revision::newFromId( $undo );
1224 $oldrev = Revision::newFromId( $undoafter );
1225 $undoMsg = null;
1226
1227 # Sanity check, make sure it's the right page,
1228 # the revisions exist and they were not deleted.
1229 # Otherwise, $content will be left as-is.
1230 if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1231 !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1232 !$oldrev->isDeleted( Revision::DELETED_TEXT )
1233 ) {
1234 if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1235 || !$this->isSupportedContentModel( $oldrev->getContentModel() )
1236 ) {
1237 // Hack for undo while EditPage can't handle multi-slot editing
1238 $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [
1239 'action' => 'mcrundo',
1240 'undo' => $undo,
1241 'undoafter' => $undoafter,
1242 ] ) );
1243 return false;
1244 } else {
1245 $content = $this->page->getUndoContent( $undorev, $oldrev );
1246
1247 if ( $content === false ) {
1248 # Warn the user that something went wrong
1249 $undoMsg = 'failure';
1250 }
1251 }
1252
1253 if ( $undoMsg === null ) {
1254 $oldContent = $this->page->getContent( Revision::RAW );
1255 $popts = ParserOptions::newFromUserAndLang(
1256 $user, MediaWikiServices::getInstance()->getContentLanguage() );
1257 $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
1258 if ( $newContent->getModel() !== $oldContent->getModel() ) {
1259 // The undo may change content
1260 // model if its reverting the top
1261 // edit. This can result in
1262 // mismatched content model/format.
1263 $this->contentModel = $newContent->getModel();
1264 $this->contentFormat = $oldrev->getContentFormat();
1265 }
1266
1267 if ( $newContent->equals( $oldContent ) ) {
1268 # Tell the user that the undo results in no change,
1269 # i.e. the revisions were already undone.
1270 $undoMsg = 'nochange';
1271 $content = false;
1272 } else {
1273 # Inform the user of our success and set an automatic edit summary
1274 $undoMsg = 'success';
1275
1276 # If we just undid one rev, use an autosummary
1277 $firstrev = $oldrev->getNext();
1278 if ( $firstrev && $firstrev->getId() == $undo ) {
1279 $userText = $undorev->getUserText();
1280 if ( $userText === '' ) {
1281 $undoSummary = $this->context->msg(
1282 'undo-summary-username-hidden',
1283 $undo
1284 )->inContentLanguage()->text();
1285 } else {
1286 $undoSummary = $this->context->msg(
1287 'undo-summary',
1288 $undo,
1289 $userText
1290 )->inContentLanguage()->text();
1291 }
1292 if ( $this->summary === '' ) {
1293 $this->summary = $undoSummary;
1294 } else {
1295 $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1296 ->inContentLanguage()->text() . $this->summary;
1297 }
1298 $this->undidRev = $undo;
1299 }
1300 $this->formtype = 'diff';
1301 }
1302 }
1303 } else {
1304 // Failed basic sanity checks.
1305 // Older revisions may have been removed since the link
1306 // was created, or we may simply have got bogus input.
1307 $undoMsg = 'norev';
1308 }
1309
1310 $out = $this->context->getOutput();
1311 // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1312 // undo-nochange.
1313 $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1314 $this->editFormPageTop .= Html::rawElement(
1315 'div', [ 'class' => $class ],
1316 $out->parseAsInterface(
1317 $this->context->msg( 'undo-' . $undoMsg )->plain()
1318 )
1319 );
1320 }
1321
1322 if ( $content === false ) {
1323 // Hack for restoring old revisions while EditPage
1324 // can't handle multi-slot editing.
1325
1326 $curRevision = $this->page->getRevision();
1327 $oldRevision = $this->mArticle->getRevisionFetched();
1328
1329 if ( $curRevision
1330 && $oldRevision
1331 && $curRevision->getId() !== $oldRevision->getId()
1332 && ( WikiPage::hasDifferencesOutsideMainSlot( $oldRevision, $curRevision )
1333 || !$this->isSupportedContentModel( $oldRevision->getContentModel() ) )
1334 ) {
1335 $this->context->getOutput()->redirect(
1336 $this->mTitle->getFullURL(
1337 [
1338 'action' => 'mcrrestore',
1339 'restore' => $oldRevision->getId(),
1340 ]
1341 )
1342 );
1343
1344 return false;
1345 }
1346 }
1347
1348 if ( $content === false ) {
1349 $content = $this->getOriginalContent( $user );
1350 }
1351 }
1352
1353 return $content;
1354 }
1355
1371 private function getOriginalContent( User $user ) {
1372 if ( $this->section == 'new' ) {
1373 return $this->getCurrentContent();
1374 }
1375 $revision = $this->mArticle->getRevisionFetched();
1376 if ( $revision === null ) {
1377 $handler = ContentHandler::getForModelID( $this->contentModel );
1378 return $handler->makeEmptyContent();
1379 }
1380 $content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1381 return $content;
1382 }
1383
1396 public function getParentRevId() {
1397 if ( $this->parentRevId ) {
1398 return $this->parentRevId;
1399 } else {
1400 return $this->mArticle->getRevIdFetched();
1401 }
1402 }
1403
1412 protected function getCurrentContent() {
1413 $rev = $this->page->getRevision();
1414 $content = $rev ? $rev->getContent( Revision::RAW ) : null;
1415
1416 if ( $content === false || $content === null ) {
1417 $handler = ContentHandler::getForModelID( $this->contentModel );
1418 return $handler->makeEmptyContent();
1419 } elseif ( !$this->undidRev ) {
1420 // Content models should always be the same since we error
1421 // out if they are different before this point (in ->edit()).
1422 // The exception being, during an undo, the current revision might
1423 // differ from the prior revision.
1424 $logger = LoggerFactory::getInstance( 'editpage' );
1425 if ( $this->contentModel !== $rev->getContentModel() ) {
1426 $logger->warning( "Overriding content model from current edit {prev} to {new}", [
1427 'prev' => $this->contentModel,
1428 'new' => $rev->getContentModel(),
1429 'title' => $this->getTitle()->getPrefixedDBkey(),
1430 'method' => __METHOD__
1431 ] );
1432 $this->contentModel = $rev->getContentModel();
1433 }
1434
1435 // Given that the content models should match, the current selected
1436 // format should be supported.
1437 if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1438 $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1439
1440 'prev' => $this->contentFormat,
1441 'new' => $rev->getContentFormat(),
1442 'title' => $this->getTitle()->getPrefixedDBkey(),
1443 'method' => __METHOD__
1444 ] );
1445 $this->contentFormat = $rev->getContentFormat();
1446 }
1447 }
1448 return $content;
1449 }
1450
1459 $this->mPreloadContent = $content;
1460 }
1461
1473 protected function getPreloadedContent( $preload, $params = [] ) {
1474 if ( !empty( $this->mPreloadContent ) ) {
1476 }
1477
1478 $handler = ContentHandler::getForModelID( $this->contentModel );
1479
1480 if ( $preload === '' ) {
1481 return $handler->makeEmptyContent();
1482 }
1483
1484 $user = $this->context->getUser();
1485 $title = Title::newFromText( $preload );
1486 # Check for existence to avoid getting MediaWiki:Noarticletext
1487 if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) {
1488 // TODO: somehow show a warning to the user!
1489 return $handler->makeEmptyContent();
1490 }
1491
1492 $page = WikiPage::factory( $title );
1493 if ( $page->isRedirect() ) {
1495 # Same as before
1496 if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) {
1497 // TODO: somehow show a warning to the user!
1498 return $handler->makeEmptyContent();
1499 }
1500 $page = WikiPage::factory( $title );
1501 }
1502
1503 $parserOptions = ParserOptions::newFromUser( $user );
1505
1506 if ( !$content ) {
1507 // TODO: somehow show a warning to the user!
1508 return $handler->makeEmptyContent();
1509 }
1510
1511 if ( $content->getModel() !== $handler->getModelID() ) {
1512 $converted = $content->convert( $handler->getModelID() );
1513
1514 if ( !$converted ) {
1515 // TODO: somehow show a warning to the user!
1516 wfDebug( "Attempt to preload incompatible content: " .
1517 "can't convert " . $content->getModel() .
1518 " to " . $handler->getModelID() );
1519
1520 return $handler->makeEmptyContent();
1521 }
1522
1523 $content = $converted;
1524 }
1525
1526 return $content->preloadTransform( $title, $parserOptions, $params );
1527 }
1528
1536 public function tokenOk( &$request ) {
1537 $token = $request->getVal( 'wpEditToken' );
1538 $user = $this->context->getUser();
1539 $this->mTokenOk = $user->matchEditToken( $token );
1540 $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
1541 return $this->mTokenOk;
1542 }
1543
1558 protected function setPostEditCookie( $statusValue ) {
1559 $revisionId = $this->page->getLatest();
1560 $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1561
1562 $val = 'saved';
1563 if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1564 $val = 'created';
1565 } elseif ( $this->oldid ) {
1566 $val = 'restored';
1567 }
1568
1569 $response = $this->context->getRequest()->response();
1570 $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1571 }
1572
1579 public function attemptSave( &$resultDetails = false ) {
1580 // TODO: MCR: treat $this->minoredit like $this->bot and check isAllowed( 'minoredit' )!
1581 // Also, add $this->autopatrol like $this->bot and check isAllowed( 'autopatrol' )!
1582 // This is needed since PageUpdater no longer checks these rights!
1583
1584 // Allow bots to exempt some edits from bot flagging
1585 $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot;
1586 $status = $this->internalAttemptSave( $resultDetails, $bot );
1587
1588 Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1589
1590 return $status;
1591 }
1592
1596 private function incrementResolvedConflicts() {
1597 if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1598 return;
1599 }
1600
1601 $this->getEditConflictHelper()->incrementResolvedStats();
1602 }
1603
1613 private function handleStatus( Status $status, $resultDetails ) {
1618 if ( $status->value == self::AS_SUCCESS_UPDATE
1619 || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1620 ) {
1622
1623 $this->didSave = true;
1624 if ( !$resultDetails['nullEdit'] ) {
1625 $this->setPostEditCookie( $status->value );
1626 }
1627 }
1628
1629 $out = $this->context->getOutput();
1630
1631 // "wpExtraQueryRedirect" is a hidden input to modify
1632 // after save URL and is not used by actual edit form
1633 $request = $this->context->getRequest();
1634 $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1635
1636 switch ( $status->value ) {
1644 case self::AS_END:
1647 return true;
1648
1650 return false;
1651
1655 $out->wrapWikiTextAsInterface( 'error', $status->getWikiText() );
1656 return true;
1657
1659 $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1660 if ( $extraQueryRedirect ) {
1661 if ( $query !== '' ) {
1662 $query .= '&';
1663 }
1664 $query .= $extraQueryRedirect;
1665 }
1666 $anchor = $resultDetails['sectionanchor'] ?? '';
1667 $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1668 return false;
1669
1671 $extraQuery = '';
1672 $sectionanchor = $resultDetails['sectionanchor'];
1673
1674 // Give extensions a chance to modify URL query on update
1675 Hooks::run(
1676 'ArticleUpdateBeforeRedirect',
1677 [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1678 );
1679
1680 if ( $resultDetails['redirect'] ) {
1681 if ( $extraQuery !== '' ) {
1682 $extraQuery = '&' . $extraQuery;
1683 }
1684 $extraQuery = 'redirect=no' . $extraQuery;
1685 }
1686 if ( $extraQueryRedirect ) {
1687 if ( $extraQuery !== '' ) {
1688 $extraQuery .= '&';
1689 }
1690 $extraQuery .= $extraQueryRedirect;
1691 }
1692
1693 $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1694 return false;
1695
1697 $this->spamPageWithContent( $resultDetails['spam'] );
1698 return false;
1699
1701 throw new UserBlockedError( $this->context->getUser()->getBlock() );
1702
1705 throw new PermissionsError( 'upload' );
1706
1709 throw new PermissionsError( 'edit' );
1710
1712 throw new ReadOnlyError;
1713
1715 throw new ThrottledError();
1716
1718 $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1719 throw new PermissionsError( $permission );
1720
1722 throw new PermissionsError( 'editcontentmodel' );
1723
1724 default:
1725 // We don't recognize $status->value. The only way that can happen
1726 // is if an extension hook aborted from inside ArticleSave.
1727 // Render the status object into $this->hookError
1728 // FIXME this sucks, we should just use the Status object throughout
1729 $this->hookError = '<div class="error">' . "\n" . $status->getWikiText() .
1730 '</div>';
1731 return true;
1732 }
1733 }
1734
1744 protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1745 // Run old style post-section-merge edit filter
1746 if ( $this->hookError != '' ) {
1747 # ...or the hook could be expecting us to produce an error
1748 $status->fatal( 'hookaborted' );
1750 return false;
1751 }
1752
1753 // Run new style post-section-merge edit filter
1754 if ( !Hooks::run( 'EditFilterMergedContent',
1755 [ $this->context, $content, $status, $this->summary,
1756 $user, $this->minoredit ] )
1757 ) {
1758 # Error messages etc. could be handled within the hook...
1759 if ( $status->isGood() ) {
1760 $status->fatal( 'hookaborted' );
1761 // Not setting $this->hookError here is a hack to allow the hook
1762 // to cause a return to the edit page without $this->hookError
1763 // being set. This is used by ConfirmEdit to display a captcha
1764 // without any error message cruft.
1765 } else {
1766 $this->hookError = $this->formatStatusErrors( $status );
1767 }
1768 // Use the existing $status->value if the hook set it
1769 if ( !$status->value ) {
1771 }
1772 return false;
1773 } elseif ( !$status->isOK() ) {
1774 # ...or the hook could be expecting us to produce an error
1775 // FIXME this sucks, we should just use the Status object throughout
1776 $this->hookError = $this->formatStatusErrors( $status );
1777 $status->fatal( 'hookaborted' );
1779 return false;
1780 }
1781
1782 return true;
1783 }
1784
1791 private function formatStatusErrors( Status $status ) {
1792 $errmsg = $status->getWikiText(
1793 'edit-error-short',
1794 'edit-error-long',
1795 $this->context->getLanguage()
1796 );
1797 return <<<ERROR
1798<div class="errorbox">
1799{$errmsg}
1800</div>
1801<br clear="all" />
1802ERROR;
1803 }
1804
1811 private function newSectionSummary( &$sectionanchor = null ) {
1812 global $wgParser;
1813
1814 if ( $this->sectiontitle !== '' ) {
1815 $sectionanchor = $this->guessSectionName( $this->sectiontitle );
1816 // If no edit summary was specified, create one automatically from the section
1817 // title and have it link to the new section. Otherwise, respect the summary as
1818 // passed.
1819 if ( $this->summary === '' ) {
1820 $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1821 return $this->context->msg( 'newsectionsummary' )
1822 ->plaintextParams( $cleanSectionTitle )->inContentLanguage()->text();
1823 }
1824 } elseif ( $this->summary !== '' ) {
1825 $sectionanchor = $this->guessSectionName( $this->summary );
1826 # This is a new section, so create a link to the new section
1827 # in the revision summary.
1828 $cleanSummary = $wgParser->stripSectionName( $this->summary );
1829 return $this->context->msg( 'newsectionsummary' )
1830 ->plaintextParams( $cleanSummary )->inContentLanguage()->text();
1831 }
1832 return $this->summary;
1833 }
1834
1859 public function internalAttemptSave( &$result, $bot = false ) {
1860 $status = Status::newGood();
1861 $user = $this->context->getUser();
1862
1863 if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1864 wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1865 $status->fatal( 'hookaborted' );
1867 return $status;
1868 }
1869
1870 if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
1871 $status->fatal( 'unicode-support-fail' );
1873 return $status;
1874 }
1875
1876 $request = $this->context->getRequest();
1877 $spam = $request->getText( 'wpAntispam' );
1878 if ( $spam !== '' ) {
1879 wfDebugLog(
1880 'SimpleAntiSpam',
1881 $user->getName() .
1882 ' editing "' .
1883 $this->mTitle->getPrefixedText() .
1884 '" submitted bogus field "' .
1885 $spam .
1886 '"'
1887 );
1888 $status->fatal( 'spamprotectionmatch', false );
1890 return $status;
1891 }
1892
1893 try {
1894 # Construct Content object
1895 $textbox_content = $this->toEditContent( $this->textbox1 );
1896 } catch ( MWContentSerializationException $ex ) {
1897 $status->fatal(
1898 'content-failed-to-parse',
1899 $this->contentModel,
1900 $this->contentFormat,
1901 $ex->getMessage()
1902 );
1904 return $status;
1905 }
1906
1907 # Check image redirect
1908 if ( $this->mTitle->getNamespace() == NS_FILE &&
1909 $textbox_content->isRedirect() &&
1910 !$user->isAllowed( 'upload' )
1911 ) {
1913 $status->setResult( false, $code );
1914
1915 return $status;
1916 }
1917
1918 # Check for spam
1919 $match = self::matchSummarySpamRegex( $this->summary );
1920 if ( $match === false && $this->section == 'new' ) {
1921 # $wgSpamRegex is enforced on this new heading/summary because, unlike
1922 # regular summaries, it is added to the actual wikitext.
1923 if ( $this->sectiontitle !== '' ) {
1924 # This branch is taken when the API is used with the 'sectiontitle' parameter.
1925 $match = self::matchSpamRegex( $this->sectiontitle );
1926 } else {
1927 # This branch is taken when the "Add Topic" user interface is used, or the API
1928 # is used with the 'summary' parameter.
1929 $match = self::matchSpamRegex( $this->summary );
1930 }
1931 }
1932 if ( $match === false ) {
1933 $match = self::matchSpamRegex( $this->textbox1 );
1934 }
1935 if ( $match !== false ) {
1936 $result['spam'] = $match;
1937 $ip = $request->getIP();
1938 $pdbk = $this->mTitle->getPrefixedDBkey();
1939 $match = str_replace( "\n", '', $match );
1940 wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1941 $status->fatal( 'spamprotectionmatch', $match );
1943 return $status;
1944 }
1945 if ( !Hooks::run(
1946 'EditFilter',
1947 [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1948 ) {
1949 # Error messages etc. could be handled within the hook...
1950 $status->fatal( 'hookaborted' );
1952 return $status;
1953 } elseif ( $this->hookError != '' ) {
1954 # ...or the hook could be expecting us to produce an error
1955 $status->fatal( 'hookaborted' );
1957 return $status;
1958 }
1959
1960 if ( $user->isBlockedFrom( $this->mTitle ) ) {
1961 // Auto-block user's IP if the account was "hard" blocked
1962 if ( !wfReadOnly() ) {
1963 $user->spreadAnyEditBlock();
1964 }
1965 # Check block state against master, thus 'false'.
1966 $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1967 return $status;
1968 }
1969
1970 $this->contentLength = strlen( $this->textbox1 );
1971 $config = $this->context->getConfig();
1972 $maxArticleSize = $config->get( 'MaxArticleSize' );
1973 if ( $this->contentLength > $maxArticleSize * 1024 ) {
1974 // Error will be displayed by showEditForm()
1975 $this->tooBig = true;
1976 $status->setResult( false, self::AS_CONTENT_TOO_BIG );
1977 return $status;
1978 }
1979
1980 if ( !$user->isAllowed( 'edit' ) ) {
1981 if ( $user->isAnon() ) {
1982 $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1983 return $status;
1984 } else {
1985 $status->fatal( 'readonlytext' );
1987 return $status;
1988 }
1989 }
1990
1991 $changingContentModel = false;
1992 if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1993 if ( !$config->get( 'ContentHandlerUseDB' ) ) {
1994 $status->fatal( 'editpage-cannot-use-custom-model' );
1996 return $status;
1997 } elseif ( !$user->isAllowed( 'editcontentmodel' ) ) {
1998 $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1999 return $status;
2000 }
2001 // Make sure the user can edit the page under the new content model too
2002 $titleWithNewContentModel = clone $this->mTitle;
2003 $titleWithNewContentModel->setContentModel( $this->contentModel );
2004 if ( !$titleWithNewContentModel->userCan( 'editcontentmodel', $user )
2005 || !$titleWithNewContentModel->userCan( 'edit', $user )
2006 ) {
2007 $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2008 return $status;
2009 }
2010
2011 $changingContentModel = true;
2012 $oldContentModel = $this->mTitle->getContentModel();
2013 }
2014
2015 if ( $this->changeTags ) {
2016 $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
2017 $this->changeTags, $user );
2018 if ( !$changeTagsStatus->isOK() ) {
2019 $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
2020 return $changeTagsStatus;
2021 }
2022 }
2023
2024 if ( wfReadOnly() ) {
2025 $status->fatal( 'readonlytext' );
2027 return $status;
2028 }
2029 if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 )
2030 || ( $changingContentModel && $user->pingLimiter( 'editcontentmodel' ) )
2031 ) {
2032 $status->fatal( 'actionthrottledtext' );
2034 return $status;
2035 }
2036
2037 # If the article has been deleted while editing, don't save it without
2038 # confirmation
2039 if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
2040 $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
2041 return $status;
2042 }
2043
2044 # Load the page data from the master. If anything changes in the meantime,
2045 # we detect it by using page_latest like a token in a 1 try compare-and-swap.
2046 $this->page->loadPageData( 'fromdbmaster' );
2047 $new = !$this->page->exists();
2048
2049 if ( $new ) {
2050 // Late check for create permission, just in case *PARANOIA*
2051 if ( !$this->mTitle->userCan( 'create', $user ) ) {
2052 $status->fatal( 'nocreatetext' );
2054 wfDebug( __METHOD__ . ": no create permission\n" );
2055 return $status;
2056 }
2057
2058 // Don't save a new page if it's blank or if it's a MediaWiki:
2059 // message with content equivalent to default (allow empty pages
2060 // in this case to disable messages, see T52124)
2061 $defaultMessageText = $this->mTitle->getDefaultMessageText();
2062 if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
2063 $defaultText = $defaultMessageText;
2064 } else {
2065 $defaultText = '';
2066 }
2067
2068 if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
2069 $this->blankArticle = true;
2070 $status->fatal( 'blankarticle' );
2071 $status->setResult( false, self::AS_BLANK_ARTICLE );
2072 return $status;
2073 }
2074
2075 if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
2076 return $status;
2077 }
2078
2079 $content = $textbox_content;
2080
2081 $result['sectionanchor'] = '';
2082 if ( $this->section == 'new' ) {
2083 if ( $this->sectiontitle !== '' ) {
2084 // Insert the section title above the content.
2085 $content = $content->addSectionHeader( $this->sectiontitle );
2086 } elseif ( $this->summary !== '' ) {
2087 // Insert the section title above the content.
2088 $content = $content->addSectionHeader( $this->summary );
2089 }
2090 $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
2091 }
2092
2094
2095 } else { # not $new
2096
2097 # Article exists. Check for edit conflict.
2098
2099 $this->page->clear(); # Force reload of dates, etc.
2100 $timestamp = $this->page->getTimestamp();
2101 $latest = $this->page->getLatest();
2102
2103 wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
2104
2105 // An edit conflict is detected if the current revision is different from the
2106 // revision that was current when editing was initiated on the client.
2107 // This is checked based on the timestamp and revision ID.
2108 // TODO: the timestamp based check can probably go away now.
2109 if ( $timestamp != $this->edittime
2110 || ( $this->editRevId !== null && $this->editRevId != $latest )
2111 ) {
2112 $this->isConflict = true;
2113 if ( $this->section == 'new' ) {
2114 if ( $this->page->getUserText() == $user->getName() &&
2115 $this->page->getComment() == $this->newSectionSummary()
2116 ) {
2117 // Probably a duplicate submission of a new comment.
2118 // This can happen when CDN resends a request after
2119 // a timeout but the first one actually went through.
2120 wfDebug( __METHOD__
2121 . ": duplicate new section submission; trigger edit conflict!\n" );
2122 } else {
2123 // New comment; suppress conflict.
2124 $this->isConflict = false;
2125 wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
2126 }
2127 } elseif ( $this->section == ''
2129 DB_MASTER, $this->mTitle->getArticleID(),
2130 $user->getId(), $this->edittime
2131 )
2132 ) {
2133 # Suppress edit conflict with self, except for section edits where merging is required.
2134 wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
2135 $this->isConflict = false;
2136 }
2137 }
2138
2139 // If sectiontitle is set, use it, otherwise use the summary as the section title.
2140 if ( $this->sectiontitle !== '' ) {
2141 $sectionTitle = $this->sectiontitle;
2142 } else {
2143 $sectionTitle = $this->summary;
2144 }
2145
2146 $content = null;
2147
2148 if ( $this->isConflict ) {
2149 wfDebug( __METHOD__
2150 . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2151 . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
2152 // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2153 // ...or disable section editing for non-current revisions (not exposed anyway).
2154 if ( $this->editRevId !== null ) {
2155 $content = $this->page->replaceSectionAtRev(
2156 $this->section,
2157 $textbox_content,
2158 $sectionTitle,
2159 $this->editRevId
2160 );
2161 } else {
2162 $content = $this->page->replaceSectionContent(
2163 $this->section,
2164 $textbox_content,
2165 $sectionTitle,
2166 $this->edittime
2167 );
2168 }
2169 } else {
2170 wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
2171 $content = $this->page->replaceSectionContent(
2172 $this->section,
2173 $textbox_content,
2174 $sectionTitle
2175 );
2176 }
2177
2178 if ( is_null( $content ) ) {
2179 wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
2180 $this->isConflict = true;
2181 $content = $textbox_content; // do not try to merge here!
2182 } elseif ( $this->isConflict ) {
2183 # Attempt merge
2184 if ( $this->mergeChangesIntoContent( $content ) ) {
2185 // Successful merge! Maybe we should tell the user the good news?
2186 $this->isConflict = false;
2187 wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
2188 } else {
2189 $this->section = '';
2190 $this->textbox1 = ContentHandler::getContentText( $content );
2191 wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
2192 }
2193 }
2194
2195 if ( $this->isConflict ) {
2196 $status->setResult( false, self::AS_CONFLICT_DETECTED );
2197 return $status;
2198 }
2199
2200 if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
2201 return $status;
2202 }
2203
2204 if ( $this->section == 'new' ) {
2205 // Handle the user preference to force summaries here
2206 if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2207 $this->missingSummary = true;
2208 $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2210 return $status;
2211 }
2212
2213 // Do not allow the user to post an empty comment
2214 if ( $this->textbox1 == '' ) {
2215 $this->missingComment = true;
2216 $status->fatal( 'missingcommenttext' );
2218 return $status;
2219 }
2220 } elseif ( !$this->allowBlankSummary
2221 && !$content->equals( $this->getOriginalContent( $user ) )
2222 && !$content->isRedirect()
2223 && md5( $this->summary ) == $this->autoSumm
2224 ) {
2225 $this->missingSummary = true;
2226 $status->fatal( 'missingsummary' );
2228 return $status;
2229 }
2230
2231 # All's well
2232 $sectionanchor = '';
2233 if ( $this->section == 'new' ) {
2234 $this->summary = $this->newSectionSummary( $sectionanchor );
2235 } elseif ( $this->section != '' ) {
2236 # Try to get a section anchor from the section source, redirect
2237 # to edited section if header found.
2238 # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2239 # for duplicate heading checking and maybe parsing.
2240 $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2241 # We can't deal with anchors, includes, html etc in the header for now,
2242 # headline would need to be parsed to improve this.
2243 if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2244 $sectionanchor = $this->guessSectionName( $matches[2] );
2245 }
2246 }
2247 $result['sectionanchor'] = $sectionanchor;
2248
2249 // Save errors may fall down to the edit form, but we've now
2250 // merged the section into full text. Clear the section field
2251 // so that later submission of conflict forms won't try to
2252 // replace that into a duplicated mess.
2253 $this->textbox1 = $this->toEditText( $content );
2254 $this->section = '';
2255
2257 }
2258
2259 if ( !$this->allowSelfRedirect
2260 && $content->isRedirect()
2261 && $content->getRedirectTarget()->equals( $this->getTitle() )
2262 ) {
2263 // If the page already redirects to itself, don't warn.
2264 $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2265 if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2266 $this->selfRedirect = true;
2267 $status->fatal( 'selfredirect' );
2269 return $status;
2270 }
2271 }
2272
2273 // Check for length errors again now that the section is merged in
2274 $this->contentLength = strlen( $this->toEditText( $content ) );
2275 if ( $this->contentLength > $maxArticleSize * 1024 ) {
2276 $this->tooBig = true;
2277 $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2278 return $status;
2279 }
2280
2281 $flags = EDIT_AUTOSUMMARY |
2282 ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2283 ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2284 ( $bot ? EDIT_FORCE_BOT : 0 );
2285
2286 $doEditStatus = $this->page->doEditContent(
2287 $content,
2288 $this->summary,
2289 $flags,
2290 false,
2291 $user,
2292 $content->getDefaultFormat(),
2295 );
2296
2297 if ( !$doEditStatus->isOK() ) {
2298 // Failure from doEdit()
2299 // Show the edit conflict page for certain recognized errors from doEdit(),
2300 // but don't show it for errors from extension hooks
2301 $errors = $doEditStatus->getErrorsArray();
2302 if ( in_array( $errors[0][0],
2303 [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2304 ) {
2305 $this->isConflict = true;
2306 // Destroys data doEdit() put in $status->value but who cares
2307 $doEditStatus->value = self::AS_END;
2308 }
2309 return $doEditStatus;
2310 }
2311
2312 $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2313 if ( $result['nullEdit'] ) {
2314 // We don't know if it was a null edit until now, so increment here
2315 $user->pingLimiter( 'linkpurge' );
2316 }
2317 $result['redirect'] = $content->isRedirect();
2318
2319 $this->updateWatchlist();
2320
2321 // If the content model changed, add a log entry
2322 if ( $changingContentModel ) {
2324 $user,
2325 $new ? false : $oldContentModel,
2328 );
2329 }
2330
2331 return $status;
2332 }
2333
2340 protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2341 $new = $oldModel === false;
2342 $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2343 $log->setPerformer( $user );
2344 $log->setTarget( $this->mTitle );
2345 $log->setComment( $reason );
2346 $log->setParameters( [
2347 '4::oldmodel' => $oldModel,
2348 '5::newmodel' => $newModel
2349 ] );
2350 $logid = $log->insert();
2351 $log->publish( $logid );
2352 }
2353
2357 protected function updateWatchlist() {
2358 $user = $this->context->getUser();
2359 if ( !$user->isLoggedIn() ) {
2360 return;
2361 }
2362
2364 $watch = $this->watchthis;
2365 // Do this in its own transaction to reduce contention...
2366 DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2367 if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2368 return; // nothing to change
2369 }
2371 } );
2372 }
2373
2385 private function mergeChangesIntoContent( &$editContent ) {
2386 $db = wfGetDB( DB_MASTER );
2387
2388 // This is the revision that was current at the time editing was initiated on the client,
2389 // even if the edit was based on an old revision.
2390 $baseRevision = $this->getBaseRevision();
2391 $baseContent = $baseRevision ? $baseRevision->getContent() : null;
2392
2393 if ( is_null( $baseContent ) ) {
2394 return false;
2395 }
2396
2397 // The current state, we want to merge updates into it
2398 $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2399 $currentContent = $currentRevision ? $currentRevision->getContent() : null;
2400
2401 if ( is_null( $currentContent ) ) {
2402 return false;
2403 }
2404
2405 $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2406
2407 $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2408
2409 if ( $result ) {
2410 $editContent = $result;
2411 // Update parentRevId to what we just merged.
2412 $this->parentRevId = $currentRevision->getId();
2413 return true;
2414 }
2415
2416 return false;
2417 }
2418
2431 public function getBaseRevision() {
2432 if ( !$this->mBaseRevision ) {
2433 $db = wfGetDB( DB_MASTER );
2434 $this->mBaseRevision = $this->editRevId
2435 ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2436 : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2437 }
2438 return $this->mBaseRevision;
2439 }
2440
2448 public static function matchSpamRegex( $text ) {
2449 global $wgSpamRegex;
2450 // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2451 $regexes = (array)$wgSpamRegex;
2452 return self::matchSpamRegexInternal( $text, $regexes );
2453 }
2454
2462 public static function matchSummarySpamRegex( $text ) {
2463 global $wgSummarySpamRegex;
2464 $regexes = (array)$wgSummarySpamRegex;
2465 return self::matchSpamRegexInternal( $text, $regexes );
2466 }
2467
2473 protected static function matchSpamRegexInternal( $text, $regexes ) {
2474 foreach ( $regexes as $regex ) {
2475 $matches = [];
2476 if ( preg_match( $regex, $text, $matches ) ) {
2477 return $matches[0];
2478 }
2479 }
2480 return false;
2481 }
2482
2483 public function setHeaders() {
2484 $out = $this->context->getOutput();
2485
2486 $out->addModules( 'mediawiki.action.edit' );
2487 $out->addModuleStyles( 'mediawiki.action.edit.styles' );
2488 $out->addModuleStyles( 'mediawiki.editfont.styles' );
2489
2490 $user = $this->context->getUser();
2491
2492 if ( $user->getOption( 'uselivepreview' ) ) {
2493 $out->addModules( 'mediawiki.action.edit.preview' );
2494 }
2495
2496 if ( $user->getOption( 'useeditwarning' ) ) {
2497 $out->addModules( 'mediawiki.action.edit.editWarning' );
2498 }
2499
2500 # Enabled article-related sidebar, toplinks, etc.
2501 $out->setArticleRelated( true );
2502
2503 $contextTitle = $this->getContextTitle();
2504 if ( $this->isConflict ) {
2505 $msg = 'editconflict';
2506 } elseif ( $contextTitle->exists() && $this->section != '' ) {
2507 $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2508 } else {
2509 $msg = $contextTitle->exists()
2510 || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2511 && $contextTitle->getDefaultMessageText() !== false
2512 )
2513 ? 'editing'
2514 : 'creating';
2515 }
2516
2517 # Use the title defined by DISPLAYTITLE magic word when present
2518 # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2519 # setPageTitle() treats the input as wikitext, which should be safe in either case.
2520 $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2521 if ( $displayTitle === false ) {
2522 $displayTitle = $contextTitle->getPrefixedText();
2523 } else {
2524 $out->setDisplayTitle( $displayTitle );
2525 }
2526 $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2527
2528 $config = $this->context->getConfig();
2529
2530 # Transmit the name of the message to JavaScript for live preview
2531 # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2532 $out->addJsConfigVars( [
2533 'wgEditMessage' => $msg,
2534 'wgAjaxEditStash' => $config->get( 'AjaxEditStash' ),
2535 ] );
2536
2537 // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2538 // editors, etc.
2539 $out->addJsConfigVars(
2540 'wgEditSubmitButtonLabelPublish',
2541 $config->get( 'EditSubmitButtonLabelPublish' )
2542 );
2543 }
2544
2548 protected function showIntro() {
2549 if ( $this->suppressIntro ) {
2550 return;
2551 }
2552
2553 $out = $this->context->getOutput();
2554 $namespace = $this->mTitle->getNamespace();
2555
2556 if ( $namespace == NS_MEDIAWIKI ) {
2557 # Show a warning if editing an interface message
2558 $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2559 # If this is a default message (but not css, json, or js),
2560 # show a hint that it is translatable on translatewiki.net
2561 if (
2562 !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2563 && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON )
2564 && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2565 ) {
2566 $defaultMessageText = $this->mTitle->getDefaultMessageText();
2567 if ( $defaultMessageText !== false ) {
2568 $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2569 'translateinterface' );
2570 }
2571 }
2572 } elseif ( $namespace == NS_FILE ) {
2573 # Show a hint to shared repo
2574 $file = wfFindFile( $this->mTitle );
2575 if ( $file && !$file->isLocal() ) {
2576 $descUrl = $file->getDescriptionUrl();
2577 # there must be a description url to show a hint to shared repo
2578 if ( $descUrl ) {
2579 if ( !$this->mTitle->exists() ) {
2580 $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2581 'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2582 ] );
2583 } else {
2584 $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2585 'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2586 ] );
2587 }
2588 }
2589 }
2590 }
2591
2592 # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2593 # Show log extract when the user is currently blocked
2594 if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2595 $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2596 $user = User::newFromName( $username, false /* allow IP users */ );
2597 $ip = User::isIP( $username );
2598 $block = Block::newFromTarget( $user, $user );
2599 if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2600 $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2601 [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2602 } elseif (
2603 !is_null( $block ) &&
2604 $block->getType() != Block::TYPE_AUTO &&
2605 ( $block->isSitewide() || $user->isBlockedFrom( $this->mTitle ) )
2606 ) {
2607 // Show log extract if the user is sitewide blocked or is partially
2608 // blocked and not allowed to edit their user page or user talk page
2610 $out,
2611 'block',
2612 MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2613 '',
2614 [
2615 'lim' => 1,
2616 'showIfEmpty' => false,
2617 'msgKey' => [
2618 'blocked-notice-logextract',
2619 $user->getName() # Support GENDER in notice
2620 ]
2621 ]
2622 );
2623 }
2624 }
2625 # Try to add a custom edit intro, or use the standard one if this is not possible.
2626 if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2627 $helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
2628 $this->context->msg( 'helppage' )->inContentLanguage()->text()
2629 ) );
2630 if ( $this->context->getUser()->isLoggedIn() ) {
2631 $out->wrapWikiMsg(
2632 // Suppress the external link icon, consider the help url an internal one
2633 "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2634 [
2635 'newarticletext',
2636 $helpLink
2637 ]
2638 );
2639 } else {
2640 $out->wrapWikiMsg(
2641 // Suppress the external link icon, consider the help url an internal one
2642 "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2643 [
2644 'newarticletextanon',
2645 $helpLink
2646 ]
2647 );
2648 }
2649 }
2650 # Give a notice if the user is editing a deleted/moved page...
2651 if ( !$this->mTitle->exists() ) {
2652 $dbr = wfGetDB( DB_REPLICA );
2653
2654 LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2655 '',
2656 [
2657 'lim' => 10,
2658 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2659 'showIfEmpty' => false,
2660 'msgKey' => [ 'recreate-moveddeleted-warn' ]
2661 ]
2662 );
2663 }
2664 }
2665
2671 protected function showCustomIntro() {
2672 if ( $this->editintro ) {
2673 $title = Title::newFromText( $this->editintro );
2674 if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2675 // Added using template syntax, to take <noinclude>'s into account.
2676 $this->context->getOutput()->addWikiTextAsContent(
2677 '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2678 /*linestart*/true,
2679 $this->mTitle
2680 );
2681 return true;
2682 }
2683 }
2684 return false;
2685 }
2686
2705 protected function toEditText( $content ) {
2706 if ( $content === null || $content === false || is_string( $content ) ) {
2707 return $content;
2708 }
2709
2710 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2711 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2712 }
2713
2714 return $content->serialize( $this->contentFormat );
2715 }
2716
2733 protected function toEditContent( $text ) {
2734 if ( $text === false || $text === null ) {
2735 return $text;
2736 }
2737
2738 $content = ContentHandler::makeContent( $text, $this->getTitle(),
2739 $this->contentModel, $this->contentFormat );
2740
2741 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2742 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2743 }
2744
2745 return $content;
2746 }
2747
2756 public function showEditForm( $formCallback = null ) {
2757 # need to parse the preview early so that we know which templates are used,
2758 # otherwise users with "show preview after edit box" will get a blank list
2759 # we parse this near the beginning so that setHeaders can do the title
2760 # setting work instead of leaving it in getPreviewText
2761 $previewOutput = '';
2762 if ( $this->formtype == 'preview' ) {
2763 $previewOutput = $this->getPreviewText();
2764 }
2765
2766 $out = $this->context->getOutput();
2767
2768 // Avoid PHP 7.1 warning of passing $this by reference
2769 $editPage = $this;
2770 Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$out ] );
2771
2772 $this->setHeaders();
2773
2774 $this->addTalkPageText();
2775 $this->addEditNotices();
2776
2777 if ( !$this->isConflict &&
2778 $this->section != '' &&
2779 !$this->isSectionEditSupported() ) {
2780 // We use $this->section to much before this and getVal('wgSection') directly in other places
2781 // at this point we can't reset $this->section to '' to fallback to non-section editing.
2782 // Someone is welcome to try refactoring though
2783 $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2784 return;
2785 }
2786
2787 $this->showHeader();
2788
2789 $out->addHTML( $this->editFormPageTop );
2790
2791 $user = $this->context->getUser();
2792 if ( $user->getOption( 'previewontop' ) ) {
2793 $this->displayPreviewArea( $previewOutput, true );
2794 }
2795
2796 $out->addHTML( $this->editFormTextTop );
2797
2798 if ( $this->wasDeletedSinceLastEdit() && $this->formtype !== 'save' ) {
2799 $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2800 'deletedwhileediting' );
2801 }
2802
2803 // @todo add EditForm plugin interface and use it here!
2804 // search for textarea1 and textarea2, and allow EditForm to override all uses.
2805 $out->addHTML( Html::openElement(
2806 'form',
2807 [
2808 'class' => 'mw-editform',
2809 'id' => self::EDITFORM_ID,
2810 'name' => self::EDITFORM_ID,
2811 'method' => 'post',
2812 'action' => $this->getActionURL( $this->getContextTitle() ),
2813 'enctype' => 'multipart/form-data'
2814 ]
2815 ) );
2816
2817 if ( is_callable( $formCallback ) ) {
2818 wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2819 call_user_func_array( $formCallback, [ &$out ] );
2820 }
2821
2822 // Add a check for Unicode support
2823 $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2824
2825 // Add an empty field to trip up spambots
2826 $out->addHTML(
2827 Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2828 . Html::rawElement(
2829 'label',
2830 [ 'for' => 'wpAntispam' ],
2831 $this->context->msg( 'simpleantispam-label' )->parse()
2832 )
2833 . Xml::element(
2834 'input',
2835 [
2836 'type' => 'text',
2837 'name' => 'wpAntispam',
2838 'id' => 'wpAntispam',
2839 'value' => ''
2840 ]
2841 )
2842 . Xml::closeElement( 'div' )
2843 );
2844
2845 // Avoid PHP 7.1 warning of passing $this by reference
2846 $editPage = $this;
2847 Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$out ] );
2848
2849 // Put these up at the top to ensure they aren't lost on early form submission
2850 $this->showFormBeforeText();
2851
2852 if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
2853 $username = $this->lastDelete->user_name;
2854 $comment = CommentStore::getStore()
2855 ->getComment( 'log_comment', $this->lastDelete )->text;
2856
2857 // It is better to not parse the comment at all than to have templates expanded in the middle
2858 // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2859 $key = $comment === ''
2860 ? 'confirmrecreate-noreason'
2861 : 'confirmrecreate';
2862 $out->addHTML(
2863 '<div class="mw-confirm-recreate">' .
2864 $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2865 Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2866 [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2867 ) .
2868 '</div>'
2869 );
2870 }
2871
2872 # When the summary is hidden, also hide them on preview/show changes
2873 if ( $this->nosummary ) {
2874 $out->addHTML( Html::hidden( 'nosummary', true ) );
2875 }
2876
2877 # If a blank edit summary was previously provided, and the appropriate
2878 # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2879 # user being bounced back more than once in the event that a summary
2880 # is not required.
2881 # ####
2882 # For a bit more sophisticated detection of blank summaries, hash the
2883 # automatic one and pass that in the hidden field wpAutoSummary.
2884 if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2885 $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2886 }
2887
2888 if ( $this->undidRev ) {
2889 $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2890 }
2891
2892 if ( $this->selfRedirect ) {
2893 $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2894 }
2895
2896 if ( $this->hasPresetSummary ) {
2897 // If a summary has been preset using &summary= we don't want to prompt for
2898 // a different summary. Only prompt for a summary if the summary is blanked.
2899 // (T19416)
2900 $this->autoSumm = md5( '' );
2901 }
2902
2903 $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
2904 $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2905
2906 $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2907 $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2908
2909 $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2910 $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
2911
2912 $out->enableOOUI();
2913
2914 if ( $this->section == 'new' ) {
2915 $this->showSummaryInput( true, $this->summary );
2916 $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2917 }
2918
2919 $out->addHTML( $this->editFormTextBeforeContent );
2920 if ( $this->isConflict ) {
2921 // In an edit conflict, we turn textbox2 into the user's text,
2922 // and textbox1 into the stored version
2923 $this->textbox2 = $this->textbox1;
2924
2925 $content = $this->getCurrentContent();
2926 $this->textbox1 = $this->toEditText( $content );
2927
2929 $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 );
2930 $editConflictHelper->setContentModel( $this->contentModel );
2931 $editConflictHelper->setContentFormat( $this->contentFormat );
2933 }
2934
2935 if ( !$this->mTitle->isUserConfigPage() ) {
2936 $out->addHTML( self::getEditToolbar( $this->mTitle ) );
2937 }
2938
2939 if ( $this->blankArticle ) {
2940 $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2941 }
2942
2943 if ( $this->isConflict ) {
2944 // In an edit conflict bypass the overridable content form method
2945 // and fallback to the raw wpTextbox1 since editconflicts can't be
2946 // resolved between page source edits and custom ui edits using the
2947 // custom edit ui.
2948 $conflictTextBoxAttribs = [];
2949 if ( $this->wasDeletedSinceLastEdit() ) {
2950 $conflictTextBoxAttribs['style'] = 'display:none;';
2951 } elseif ( $this->isOldRev ) {
2952 $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
2953 }
2954
2955 $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
2957 } else {
2958 $this->showContentForm();
2959 }
2960
2961 $out->addHTML( $this->editFormTextAfterContent );
2962
2963 $this->showStandardInputs();
2964
2965 $this->showFormAfterText();
2966
2967 $this->showTosSummary();
2968
2969 $this->showEditTools();
2970
2971 $out->addHTML( $this->editFormTextAfterTools . "\n" );
2972
2973 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
2974
2975 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2976 Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2977
2978 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2979 self::getPreviewLimitReport( $this->mParserOutput ) ) );
2980
2981 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2982
2983 if ( $this->isConflict ) {
2984 try {
2985 $this->showConflict();
2986 } catch ( MWContentSerializationException $ex ) {
2987 // this can't really happen, but be nice if it does.
2988 $msg = $this->context->msg(
2989 'content-failed-to-parse',
2990 $this->contentModel,
2991 $this->contentFormat,
2992 $ex->getMessage()
2993 );
2994 $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
2995 }
2996 }
2997
2998 // Set a hidden field so JS knows what edit form mode we are in
2999 if ( $this->isConflict ) {
3000 $mode = 'conflict';
3001 } elseif ( $this->preview ) {
3002 $mode = 'preview';
3003 } elseif ( $this->diff ) {
3004 $mode = 'diff';
3005 } else {
3006 $mode = 'text';
3007 }
3008 $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
3009
3010 // Marker for detecting truncated form data. This must be the last
3011 // parameter sent in order to be of use, so do not move me.
3012 $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
3013 $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
3014
3015 if ( !$user->getOption( 'previewontop' ) ) {
3016 $this->displayPreviewArea( $previewOutput, false );
3017 }
3018 }
3019
3027 public function makeTemplatesOnThisPageList( array $templates ) {
3028 $templateListFormatter = new TemplatesOnThisPageFormatter(
3029 $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
3030 );
3031
3032 // preview if preview, else section if section, else false
3033 $type = false;
3034 if ( $this->preview ) {
3035 $type = 'preview';
3036 } elseif ( $this->section != '' ) {
3037 $type = 'section';
3038 }
3039
3040 return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
3041 $templateListFormatter->format( $templates, $type )
3042 );
3043 }
3044
3051 public static function extractSectionTitle( $text ) {
3052 preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
3053 if ( !empty( $matches[2] ) ) {
3054 global $wgParser;
3055 return $wgParser->stripSectionName( trim( $matches[2] ) );
3056 } else {
3057 return false;
3058 }
3059 }
3060
3061 protected function showHeader() {
3062 $out = $this->context->getOutput();
3063 $user = $this->context->getUser();
3064 if ( $this->isConflict ) {
3066 $this->editRevId = $this->page->getLatest();
3067 } else {
3068 if ( $this->section != '' && $this->section != 'new' && !$this->summary &&
3069 !$this->preview && !$this->diff
3070 ) {
3071 $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
3072 if ( $sectionTitle !== false ) {
3073 $this->summary = "/* $sectionTitle */ ";
3074 }
3075 }
3076
3077 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
3078
3079 if ( $this->missingComment ) {
3080 $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
3081 }
3082
3083 if ( $this->missingSummary && $this->section != 'new' ) {
3084 $out->wrapWikiMsg(
3085 "<div id='mw-missingsummary'>\n$1\n</div>",
3086 [ 'missingsummary', $buttonLabel ]
3087 );
3088 }
3089
3090 if ( $this->missingSummary && $this->section == 'new' ) {
3091 $out->wrapWikiMsg(
3092 "<div id='mw-missingcommentheader'>\n$1\n</div>",
3093 [ 'missingcommentheader', $buttonLabel ]
3094 );
3095 }
3096
3097 if ( $this->blankArticle ) {
3098 $out->wrapWikiMsg(
3099 "<div id='mw-blankarticle'>\n$1\n</div>",
3100 [ 'blankarticle', $buttonLabel ]
3101 );
3102 }
3103
3104 if ( $this->selfRedirect ) {
3105 $out->wrapWikiMsg(
3106 "<div id='mw-selfredirect'>\n$1\n</div>",
3107 [ 'selfredirect', $buttonLabel ]
3108 );
3109 }
3110
3111 if ( $this->hookError !== '' ) {
3112 $out->addWikiTextAsInterface( $this->hookError );
3113 }
3114
3115 if ( $this->section != 'new' ) {
3116 $revision = $this->mArticle->getRevisionFetched();
3117 if ( $revision ) {
3118 // Let sysop know that this will make private content public if saved
3119
3120 if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) {
3121 $out->wrapWikiMsg(
3122 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3123 'rev-deleted-text-permission'
3124 );
3125 } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
3126 $out->wrapWikiMsg(
3127 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3128 'rev-deleted-text-view'
3129 );
3130 }
3131
3132 if ( !$revision->isCurrent() ) {
3133 $this->mArticle->setOldSubtitle( $revision->getId() );
3134 $out->wrapWikiMsg(
3135 Html::warningBox( "\n$1\n" ),
3136 'editingold'
3137 );
3138 $this->isOldRev = true;
3139 }
3140 } elseif ( $this->mTitle->exists() ) {
3141 // Something went wrong
3142
3143 $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
3144 [ 'missing-revision', $this->oldid ] );
3145 }
3146 }
3147 }
3148
3149 if ( wfReadOnly() ) {
3150 $out->wrapWikiMsg(
3151 "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3152 [ 'readonlywarning', wfReadOnlyReason() ]
3153 );
3154 } elseif ( $user->isAnon() ) {
3155 if ( $this->formtype != 'preview' ) {
3156 $returntoquery = array_diff_key(
3157 $this->context->getRequest()->getValues(),
3158 [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
3159 );
3160 $out->wrapWikiMsg(
3161 "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3162 [ 'anoneditwarning',
3163 // Log-in link
3164 SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
3165 'returnto' => $this->getTitle()->getPrefixedDBkey(),
3166 'returntoquery' => wfArrayToCgi( $returntoquery ),
3167 ] ),
3168 // Sign-up link
3169 SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
3170 'returnto' => $this->getTitle()->getPrefixedDBkey(),
3171 'returntoquery' => wfArrayToCgi( $returntoquery ),
3172 ] )
3173 ]
3174 );
3175 } else {
3176 $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3177 'anonpreviewwarning'
3178 );
3179 }
3180 } elseif ( $this->mTitle->isUserConfigPage() ) {
3181 # Check the skin exists
3182 if ( $this->isWrongCaseUserConfigPage() ) {
3183 $out->wrapWikiMsg(
3184 "<div class='error' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
3185 [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
3186 );
3187 }
3188 if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
3189 $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
3190 $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
3191 $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
3192
3193 $warning = $isUserCssConfig
3194 ? 'usercssispublic'
3195 : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' );
3196
3197 $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning );
3198
3199 if ( $this->formtype !== 'preview' ) {
3200 $config = $this->context->getConfig();
3201 if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) {
3202 $out->wrapWikiMsg(
3203 "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3204 [ 'usercssyoucanpreview' ]
3205 );
3206 } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
3207 $out->wrapWikiMsg(
3208 "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
3209 [ 'userjsonyoucanpreview' ]
3210 );
3211 } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) {
3212 $out->wrapWikiMsg(
3213 "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3214 [ 'userjsyoucanpreview' ]
3215 );
3216 }
3217 }
3218 }
3219 }
3220
3222
3223 $this->addLongPageWarningHeader();
3224
3225 # Add header copyright warning
3227 }
3228
3236 private function getSummaryInputAttributes( array $inputAttrs = null ) {
3237 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3238 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3239 // Unicode codepoints.
3240 return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3241 'id' => 'wpSummary',
3242 'name' => 'wpSummary',
3243 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
3244 'tabindex' => 1,
3245 'size' => 60,
3246 'spellcheck' => 'true',
3247 ];
3248 }
3249
3259 function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) {
3260 $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3261 $this->getSummaryInputAttributes( $inputAttrs )
3262 );
3263 $inputAttrs += [
3264 'title' => Linker::titleAttrib( 'summary' ),
3265 'accessKey' => Linker::accesskey( 'summary' ),
3266 ];
3267
3268 // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3269 $inputAttrs['inputId'] = $inputAttrs['id'];
3270 $inputAttrs['id'] = 'wpSummaryWidget';
3271
3272 return new OOUI\FieldLayout(
3273 new OOUI\TextInputWidget( [
3274 'value' => $summary,
3275 'infusable' => true,
3276 ] + $inputAttrs ),
3277 [
3278 'label' => new OOUI\HtmlSnippet( $labelText ),
3279 'align' => 'top',
3280 'id' => 'wpSummaryLabel',
3281 'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3282 ]
3283 );
3284 }
3285
3292 protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3293 # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3294 $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3295 if ( $isSubjectPreview ) {
3296 if ( $this->nosummary ) {
3297 return;
3298 }
3299 } elseif ( !$this->mShowSummaryField ) {
3300 return;
3301 }
3302
3303 $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3304 $this->context->getOutput()->addHTML( $this->getSummaryInputWidget(
3305 $summary,
3306 $labelText,
3307 [ 'class' => $summaryClass ]
3308 ) );
3309 }
3310
3318 protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3319 // avoid spaces in preview, gets always trimmed on save
3320 $summary = trim( $summary );
3321 if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3322 return "";
3323 }
3324
3325 global $wgParser;
3326
3327 if ( $isSubjectPreview ) {
3328 $summary = $this->context->msg( 'newsectionsummary' )
3329 ->rawParams( $wgParser->stripSectionName( $summary ) )
3330 ->inContentLanguage()->text();
3331 }
3332
3333 $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3334
3335 $summary = $this->context->msg( $message )->parse()
3336 . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3337 return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3338 }
3339
3340 protected function showFormBeforeText() {
3341 $out = $this->context->getOutput();
3342 $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3343 $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3344 $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3345 $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3346 $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3347 }
3348
3349 protected function showFormAfterText() {
3362 $this->context->getOutput()->addHTML(
3363 "\n" .
3364 Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3365 "\n"
3366 );
3367 }
3368
3377 protected function showContentForm() {
3378 $this->showTextbox1();
3379 }
3380
3389 protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3390 if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3391 $attribs = [ 'style' => 'display:none;' ];
3392 } else {
3393 $builder = new TextboxBuilder();
3394 $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3395
3396 # Is an old revision being edited?
3397 if ( $this->isOldRev ) {
3398 $classes[] = 'mw-textarea-oldrev';
3399 }
3400
3401 $attribs = [ 'tabindex' => 1 ];
3402
3403 if ( is_array( $customAttribs ) ) {
3405 }
3406
3407 $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
3408 }
3409
3410 $this->showTextbox(
3411 $textoverride ?? $this->textbox1,
3412 'wpTextbox1',
3413 $attribs
3414 );
3415 }
3416
3417 protected function showTextbox2() {
3418 $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3419 }
3420
3421 protected function showTextbox( $text, $name, $customAttribs = [] ) {
3422 $builder = new TextboxBuilder();
3423 $attribs = $builder->buildTextboxAttribs(
3424 $name,
3426 $this->context->getUser(),
3427 $this->mTitle
3428 );
3429
3430 $this->context->getOutput()->addHTML(
3431 Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3432 );
3433 }
3434
3435 protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3436 $classes = [];
3437 if ( $isOnTop ) {
3438 $classes[] = 'ontop';
3439 }
3440
3441 $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3442
3443 if ( $this->formtype != 'preview' ) {
3444 $attribs['style'] = 'display: none;';
3445 }
3446
3447 $out = $this->context->getOutput();
3448 $out->addHTML( Xml::openElement( 'div', $attribs ) );
3449
3450 if ( $this->formtype == 'preview' ) {
3451 $this->showPreview( $previewOutput );
3452 } else {
3453 // Empty content container for LivePreview
3454 $pageViewLang = $this->mTitle->getPageViewLanguage();
3455 $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3456 'class' => 'mw-content-' . $pageViewLang->getDir() ];
3457 $out->addHTML( Html::rawElement( 'div', $attribs ) );
3458 }
3459
3460 $out->addHTML( '</div>' );
3461
3462 if ( $this->formtype == 'diff' ) {
3463 try {
3464 $this->showDiff();
3465 } catch ( MWContentSerializationException $ex ) {
3466 $msg = $this->context->msg(
3467 'content-failed-to-parse',
3468 $this->contentModel,
3469 $this->contentFormat,
3470 $ex->getMessage()
3471 );
3472 $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3473 }
3474 }
3475 }
3476
3483 protected function showPreview( $text ) {
3484 if ( $this->mArticle instanceof CategoryPage ) {
3485 $this->mArticle->openShowCategory();
3486 }
3487 # This hook seems slightly odd here, but makes things more
3488 # consistent for extensions.
3489 $out = $this->context->getOutput();
3490 Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
3491 $out->addHTML( $text );
3492 if ( $this->mArticle instanceof CategoryPage ) {
3493 $this->mArticle->closeShowCategory();
3494 }
3495 }
3496
3504 public function showDiff() {
3505 $oldtitlemsg = 'currentrev';
3506 # if message does not exist, show diff against the preloaded default
3507 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3508 $oldtext = $this->mTitle->getDefaultMessageText();
3509 if ( $oldtext !== false ) {
3510 $oldtitlemsg = 'defaultmessagetext';
3511 $oldContent = $this->toEditContent( $oldtext );
3512 } else {
3513 $oldContent = null;
3514 }
3515 } else {
3516 $oldContent = $this->getCurrentContent();
3517 }
3518
3519 $textboxContent = $this->toEditContent( $this->textbox1 );
3520 if ( $this->editRevId !== null ) {
3521 $newContent = $this->page->replaceSectionAtRev(
3522 $this->section, $textboxContent, $this->summary, $this->editRevId
3523 );
3524 } else {
3525 $newContent = $this->page->replaceSectionContent(
3526 $this->section, $textboxContent, $this->summary, $this->edittime
3527 );
3528 }
3529
3530 if ( $newContent ) {
3531 Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3532
3533 $user = $this->context->getUser();
3534 $popts = ParserOptions::newFromUserAndLang( $user,
3535 MediaWikiServices::getInstance()->getContentLanguage() );
3536 $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
3537 }
3538
3539 if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3540 $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3541 $newtitle = $this->context->msg( 'yourtext' )->parse();
3542
3543 if ( !$oldContent ) {
3544 $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3545 }
3546
3547 if ( !$newContent ) {
3548 $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3549 }
3550
3551 $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3552 $de->setContent( $oldContent, $newContent );
3553
3554 $difftext = $de->getDiff( $oldtitle, $newtitle );
3555 $de->showDiffStyle();
3556 } else {
3557 $difftext = '';
3558 }
3559
3560 $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3561 }
3562
3566 protected function showHeaderCopyrightWarning() {
3567 $msg = 'editpage-head-copy-warn';
3568 if ( !$this->context->msg( $msg )->isDisabled() ) {
3569 $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3570 'editpage-head-copy-warn' );
3571 }
3572 }
3573
3582 protected function showTosSummary() {
3583 $msg = 'editpage-tos-summary';
3584 Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3585 if ( !$this->context->msg( $msg )->isDisabled() ) {
3586 $out = $this->context->getOutput();
3587 $out->addHTML( '<div class="mw-tos-summary">' );
3588 $out->addWikiMsg( $msg );
3589 $out->addHTML( '</div>' );
3590 }
3591 }
3592
3597 protected function showEditTools() {
3598 $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3599 $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3600 '</div>' );
3601 }
3602
3609 protected function getCopywarn() {
3610 return self::getCopyrightWarning( $this->mTitle );
3611 }
3612
3621 public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3622 global $wgRightsText;
3623 if ( $wgRightsText ) {
3624 $copywarnMsg = [ 'copyrightwarning',
3625 '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3626 $wgRightsText ];
3627 } else {
3628 $copywarnMsg = [ 'copyrightwarning2',
3629 '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3630 }
3631 // Allow for site and per-namespace customization of contribution/copyright notice.
3632 Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3633
3634 $msg = wfMessage( ...$copywarnMsg )->title( $title );
3635 if ( $langcode ) {
3636 $msg->inLanguage( $langcode );
3637 }
3638 return "<div id=\"editpage-copywarn\">\n" .
3639 $msg->$format() . "\n</div>";
3640 }
3641
3649 public static function getPreviewLimitReport( ParserOutput $output = null ) {
3650 global $wgLang;
3651
3652 if ( !$output || !$output->getLimitReportData() ) {
3653 return '';
3654 }
3655
3656 $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3657 wfMessage( 'limitreport-title' )->parseAsBlock()
3658 );
3659
3660 // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3661 $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3662
3663 $limitReport .= Html::openElement( 'table', [
3664 'class' => 'preview-limit-report wikitable'
3665 ] ) .
3666 Html::openElement( 'tbody' );
3667
3668 foreach ( $output->getLimitReportData() as $key => $value ) {
3669 if ( Hooks::run( 'ParserLimitReportFormat',
3670 [ $key, &$value, &$limitReport, true, true ]
3671 ) ) {
3672 $keyMsg = wfMessage( $key );
3673 $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3674 if ( !$valueMsg->exists() ) {
3675 $valueMsg = new RawMessage( '$1' );
3676 }
3677 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3678 $limitReport .= Html::openElement( 'tr' ) .
3679 Html::rawElement( 'th', null, $keyMsg->parse() ) .
3680 Html::rawElement( 'td', null,
3681 $wgLang->formatNum( $valueMsg->params( $value )->parse() )
3682 ) .
3683 Html::closeElement( 'tr' );
3684 }
3685 }
3686 }
3687
3688 $limitReport .= Html::closeElement( 'tbody' ) .
3689 Html::closeElement( 'table' ) .
3690 Html::closeElement( 'div' );
3691
3692 return $limitReport;
3693 }
3694
3695 protected function showStandardInputs( &$tabindex = 2 ) {
3696 $out = $this->context->getOutput();
3697 $out->addHTML( "<div class='editOptions'>\n" );
3698
3699 if ( $this->section != 'new' ) {
3700 $this->showSummaryInput( false, $this->summary );
3701 $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3702 }
3703
3704 $checkboxes = $this->getCheckboxesWidget(
3705 $tabindex,
3706 [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3707 );
3708 $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] );
3709
3710 $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3711
3712 // Show copyright warning.
3713 $out->addWikiTextAsInterface( $this->getCopywarn() );
3714 $out->addHTML( $this->editFormTextAfterWarn );
3715
3716 $out->addHTML( "<div class='editButtons'>\n" );
3717 $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3718
3719 $cancel = $this->getCancelLink();
3720
3721 $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3722 $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3723 $edithelp =
3724 Html::linkButton(
3725 $this->context->msg( 'edithelp' )->text(),
3726 [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3727 [ 'mw-ui-quiet' ]
3728 ) .
3729 $this->context->msg( 'word-separator' )->escaped() .
3730 $this->context->msg( 'newwindow' )->parse();
3731
3732 $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3733 $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3734 $out->addHTML( "</div><!-- editButtons -->\n" );
3735
3736 Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
3737
3738 $out->addHTML( "</div><!-- editOptions -->\n" );
3739 }
3740
3745 protected function showConflict() {
3746 $out = $this->context->getOutput();
3747 // Avoid PHP 7.1 warning of passing $this by reference
3748 $editPage = $this;
3749 if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$out ] ) ) {
3750 $this->incrementConflictStats();
3751
3752 $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3753 }
3754 }
3755
3756 protected function incrementConflictStats() {
3757 $this->getEditConflictHelper()->incrementConflictStats();
3758 }
3759
3763 public function getCancelLink() {
3764 $cancelParams = [];
3765 if ( !$this->isConflict && $this->oldid > 0 ) {
3766 $cancelParams['oldid'] = $this->oldid;
3767 } elseif ( $this->getContextTitle()->isRedirect() ) {
3768 $cancelParams['redirect'] = 'no';
3769 }
3770
3771 return new OOUI\ButtonWidget( [
3772 'id' => 'mw-editform-cancel',
3773 'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3774 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3775 'framed' => false,
3776 'infusable' => true,
3777 'flags' => 'destructive',
3778 ] );
3779 }
3780
3790 protected function getActionURL( Title $title ) {
3791 return $title->getLocalURL( [ 'action' => $this->action ] );
3792 }
3793
3801 protected function wasDeletedSinceLastEdit() {
3802 if ( $this->deletedSinceEdit !== null ) {
3804 }
3805
3806 $this->deletedSinceEdit = false;
3807
3808 if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3809 $this->lastDelete = $this->getLastDelete();
3810 if ( $this->lastDelete ) {
3811 $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3812 if ( $deleteTime > $this->starttime ) {
3813 $this->deletedSinceEdit = true;
3814 }
3815 }
3816 }
3817
3819 }
3820
3826 protected function getLastDelete() {
3827 $dbr = wfGetDB( DB_REPLICA );
3828 $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
3829 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
3830 $data = $dbr->selectRow(
3831 array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
3832 [
3833 'log_type',
3834 'log_action',
3835 'log_timestamp',
3836 'log_namespace',
3837 'log_title',
3838 'log_params',
3839 'log_deleted',
3840 'user_name'
3841 ] + $commentQuery['fields'] + $actorQuery['fields'],
3842 [
3843 'log_namespace' => $this->mTitle->getNamespace(),
3844 'log_title' => $this->mTitle->getDBkey(),
3845 'log_type' => 'delete',
3846 'log_action' => 'delete',
3847 ],
3848 __METHOD__,
3849 [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
3850 [
3851 'user' => [ 'JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
3852 ] + $commentQuery['joins'] + $actorQuery['joins']
3853 );
3854 // Quick paranoid permission checks...
3855 if ( is_object( $data ) ) {
3856 if ( $data->log_deleted & LogPage::DELETED_USER ) {
3857 $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
3858 }
3859
3860 if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3861 $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
3862 $data->log_comment_data = null;
3863 }
3864 }
3865
3866 return $data;
3867 }
3868
3874 public function getPreviewText() {
3875 $out = $this->context->getOutput();
3876 $config = $this->context->getConfig();
3877
3878 if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) {
3879 // Could be an offsite preview attempt. This is very unsafe if
3880 // HTML is enabled, as it could be an attack.
3881 $parsedNote = '';
3882 if ( $this->textbox1 !== '' ) {
3883 // Do not put big scary notice, if previewing the empty
3884 // string, which happens when you initially edit
3885 // a category page, due to automatic preview-on-open.
3886 $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
3887 $out->parseAsInterface(
3888 $this->context->msg( 'session_fail_preview_html' )->plain()
3889 ) );
3890 }
3891 $this->incrementEditFailureStats( 'session_loss' );
3892 return $parsedNote;
3893 }
3894
3895 $note = '';
3896
3897 try {
3898 $content = $this->toEditContent( $this->textbox1 );
3899
3900 $previewHTML = '';
3901 if ( !Hooks::run(
3902 'AlternateEditPreview',
3903 [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3904 ) {
3905 return $previewHTML;
3906 }
3907
3908 # provide a anchor link to the editform
3909 $continueEditing = '<span class="mw-continue-editing">' .
3910 '[[#' . self::EDITFORM_ID . '|' .
3911 $this->context->getLanguage()->getArrow() . ' ' .
3912 $this->context->msg( 'continue-editing' )->text() . ']]</span>';
3913 if ( $this->mTriedSave && !$this->mTokenOk ) {
3914 if ( $this->mTokenOkExceptSuffix ) {
3915 $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
3916 $this->incrementEditFailureStats( 'bad_token' );
3917 } else {
3918 $note = $this->context->msg( 'session_fail_preview' )->plain();
3919 $this->incrementEditFailureStats( 'session_loss' );
3920 }
3921 } elseif ( $this->incompleteForm ) {
3922 $note = $this->context->msg( 'edit_form_incomplete' )->plain();
3923 if ( $this->mTriedSave ) {
3924 $this->incrementEditFailureStats( 'incomplete_form' );
3925 }
3926 } else {
3927 $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
3928 }
3929
3930 # don't parse non-wikitext pages, show message about preview
3931 if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
3932 if ( $this->mTitle->isUserConfigPage() ) {
3933 $level = 'user';
3934 } elseif ( $this->mTitle->isSiteConfigPage() ) {
3935 $level = 'site';
3936 } else {
3937 $level = false;
3938 }
3939
3940 if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3941 $format = 'css';
3942 if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
3943 $format = false;
3944 }
3945 } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) {
3946 $format = 'json';
3947 if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
3948 $format = false;
3949 }
3950 } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3951 $format = 'js';
3952 if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
3953 $format = false;
3954 }
3955 } else {
3956 $format = false;
3957 }
3958
3959 # Used messages to make sure grep find them:
3960 # Messages: usercsspreview, userjsonpreview, userjspreview,
3961 # sitecsspreview, sitejsonpreview, sitejspreview
3962 if ( $level && $format ) {
3963 $note = "<div id='mw-{$level}{$format}preview'>" .
3964 $this->context->msg( "{$level}{$format}preview" )->plain() .
3965 ' ' . $continueEditing . "</div>";
3966 }
3967 }
3968
3969 # If we're adding a comment, we need to show the
3970 # summary as the headline
3971 if ( $this->section === "new" && $this->summary !== "" ) {
3972 $content = $content->addSectionHeader( $this->summary );
3973 }
3974
3975 $hook_args = [ $this, &$content ];
3976 Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3977
3978 $parserResult = $this->doPreviewParse( $content );
3979 $parserOutput = $parserResult['parserOutput'];
3980 $previewHTML = $parserResult['html'];
3981 $this->mParserOutput = $parserOutput;
3982 $out->addParserOutputMetadata( $parserOutput );
3983 if ( $out->userCanPreview() ) {
3984 $out->addContentOverride( $this->getTitle(), $content );
3985 }
3986
3987 if ( count( $parserOutput->getWarnings() ) ) {
3988 $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3989 }
3990
3991 } catch ( MWContentSerializationException $ex ) {
3992 $m = $this->context->msg(
3993 'content-failed-to-parse',
3994 $this->contentModel,
3995 $this->contentFormat,
3996 $ex->getMessage()
3997 );
3998 $note .= "\n\n" . $m->plain(); # gets parsed down below
3999 $previewHTML = '';
4000 }
4001
4002 if ( $this->isConflict ) {
4003 $conflict = Html::rawElement(
4004 'h2', [ 'id' => 'mw-previewconflict' ],
4005 $this->context->msg( 'previewconflict' )->escaped()
4006 );
4007 } else {
4008 $conflict = '<hr />';
4009 }
4010
4011 $previewhead = Html::rawElement(
4012 'div', [ 'class' => 'previewnote' ],
4013 Html::rawElement(
4014 'h2', [ 'id' => 'mw-previewheader' ],
4015 $this->context->msg( 'preview' )->escaped()
4016 ) .
4017 $out->parseAsInterface( $note ) . $conflict
4018 );
4019
4020 $pageViewLang = $this->mTitle->getPageViewLanguage();
4021 $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
4022 'class' => 'mw-content-' . $pageViewLang->getDir() ];
4023 $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
4024
4025 return $previewhead . $previewHTML . $this->previewTextAfterContent;
4026 }
4027
4028 private function incrementEditFailureStats( $failureType ) {
4029 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
4030 $stats->increment( 'edit.failures.' . $failureType );
4031 }
4032
4037 protected function getPreviewParserOptions() {
4038 $parserOptions = $this->page->makeParserOptions( $this->context );
4039 $parserOptions->setIsPreview( true );
4040 $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
4041 $parserOptions->enableLimitReport();
4042
4043 // XXX: we could call $parserOptions->setCurrentRevisionCallback here to force the
4044 // current revision to be null during PST, until setupFakeRevision is called on
4045 // the ParserOptions. Currently, we rely on Parser::getRevisionObject() to ignore
4046 // existing revisions in preview mode.
4047
4048 return $parserOptions;
4049 }
4050
4060 protected function doPreviewParse( Content $content ) {
4061 $user = $this->context->getUser();
4062 $parserOptions = $this->getPreviewParserOptions();
4063
4064 // NOTE: preSaveTransform doesn't have a fake revision to operate on.
4065 // Parser::getRevisionObject() will return null in preview mode,
4066 // causing the context user to be used for {{subst:REVISIONUSER}}.
4067 // XXX: Alternatively, we could also call setupFakeRevision() a second time:
4068 // once before PST with $content, and then after PST with $pstContent.
4069 $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
4070 $scopedCallback = $parserOptions->setupFakeRevision( $this->mTitle, $pstContent, $user );
4071 $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
4072 ScopedCallback::consume( $scopedCallback );
4073 return [
4074 'parserOutput' => $parserOutput,
4075 'html' => $parserOutput->getText( [
4076 'enableSectionEditLinks' => false
4077 ] )
4078 ];
4079 }
4080
4084 public function getTemplates() {
4085 if ( $this->preview || $this->section != '' ) {
4086 $templates = [];
4087 if ( !isset( $this->mParserOutput ) ) {
4088 return $templates;
4089 }
4090 foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
4091 foreach ( array_keys( $template ) as $dbk ) {
4092 $templates[] = Title::makeTitle( $ns, $dbk );
4093 }
4094 }
4095 return $templates;
4096 } else {
4097 return $this->mTitle->getTemplateLinksFrom();
4098 }
4099 }
4100
4107 public static function getEditToolbar( $title = null ) {
4108 $startingToolbar = '<div id="toolbar"></div>';
4109 $toolbar = $startingToolbar;
4110
4111 if ( !Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
4112 return null;
4113 };
4114 // Don't add a pointless `<div>` to the page unless a hook caller populated it
4115 return ( $toolbar === $startingToolbar ) ? null : $toolbar;
4116 }
4117
4136 public function getCheckboxesDefinition( $checked ) {
4137 $checkboxes = [];
4138
4139 $user = $this->context->getUser();
4140 // don't show the minor edit checkbox if it's a new page or section
4141 if ( !$this->isNew && $user->isAllowed( 'minoredit' ) ) {
4142 $checkboxes['wpMinoredit'] = [
4143 'id' => 'wpMinoredit',
4144 'label-message' => 'minoredit',
4145 // Uses messages: tooltip-minoredit, accesskey-minoredit
4146 'tooltip' => 'minoredit',
4147 'label-id' => 'mw-editpage-minoredit',
4148 'legacy-name' => 'minor',
4149 'default' => $checked['minor'],
4150 ];
4151 }
4152
4153 if ( $user->isLoggedIn() ) {
4154 $checkboxes['wpWatchthis'] = [
4155 'id' => 'wpWatchthis',
4156 'label-message' => 'watchthis',
4157 // Uses messages: tooltip-watch, accesskey-watch
4158 'tooltip' => 'watch',
4159 'label-id' => 'mw-editpage-watch',
4160 'legacy-name' => 'watch',
4161 'default' => $checked['watch'],
4162 ];
4163 }
4164
4165 $editPage = $this;
4166 Hooks::run( 'EditPageGetCheckboxesDefinition', [ $editPage, &$checkboxes ] );
4167
4168 return $checkboxes;
4169 }
4170
4181 public function getCheckboxesWidget( &$tabindex, $checked ) {
4182 $checkboxes = [];
4183 $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4184
4185 foreach ( $checkboxesDef as $name => $options ) {
4186 $legacyName = $options['legacy-name'] ?? $name;
4187
4188 $title = null;
4189 $accesskey = null;
4190 if ( isset( $options['tooltip'] ) ) {
4191 $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4192 $title = Linker::titleAttrib( $options['tooltip'] );
4193 }
4194 if ( isset( $options['title-message'] ) ) {
4195 $title = $this->context->msg( $options['title-message'] )->text();
4196 }
4197
4198 $checkboxes[ $legacyName ] = new OOUI\FieldLayout(
4199 new OOUI\CheckboxInputWidget( [
4200 'tabIndex' => ++$tabindex,
4201 'accessKey' => $accesskey,
4202 'id' => $options['id'] . 'Widget',
4203 'inputId' => $options['id'],
4204 'name' => $name,
4205 'selected' => $options['default'],
4206 'infusable' => true,
4207 ] ),
4208 [
4209 'align' => 'inline',
4210 'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4211 'title' => $title,
4212 'id' => $options['label-id'] ?? null,
4213 ]
4214 );
4215 }
4216
4217 return $checkboxes;
4218 }
4219
4226 protected function getSubmitButtonLabel() {
4227 $labelAsPublish =
4228 $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4229
4230 // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4231 $newPage = !$this->mTitle->exists();
4232
4233 if ( $labelAsPublish ) {
4234 $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4235 } else {
4236 $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4237 }
4238
4239 return $buttonLabelKey;
4240 }
4241
4250 public function getEditButtons( &$tabindex ) {
4251 $buttons = [];
4252
4253 $labelAsPublish =
4254 $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4255
4256 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4257 $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4258
4259 $buttons['save'] = new OOUI\ButtonInputWidget( [
4260 'name' => 'wpSave',
4261 'tabIndex' => ++$tabindex,
4262 'id' => 'wpSaveWidget',
4263 'inputId' => 'wpSave',
4264 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4265 'useInputTag' => true,
4266 'flags' => [ 'progressive', 'primary' ],
4267 'label' => $buttonLabel,
4268 'infusable' => true,
4269 'type' => 'submit',
4270 // Messages used: tooltip-save, tooltip-publish
4271 'title' => Linker::titleAttrib( $buttonTooltip ),
4272 // Messages used: accesskey-save, accesskey-publish
4273 'accessKey' => Linker::accesskey( $buttonTooltip ),
4274 ] );
4275
4276 $buttons['preview'] = new OOUI\ButtonInputWidget( [
4277 'name' => 'wpPreview',
4278 'tabIndex' => ++$tabindex,
4279 'id' => 'wpPreviewWidget',
4280 'inputId' => 'wpPreview',
4281 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4282 'useInputTag' => true,
4283 'label' => $this->context->msg( 'showpreview' )->text(),
4284 'infusable' => true,
4285 'type' => 'submit',
4286 // Message used: tooltip-preview
4287 'title' => Linker::titleAttrib( 'preview' ),
4288 // Message used: accesskey-preview
4289 'accessKey' => Linker::accesskey( 'preview' ),
4290 ] );
4291
4292 $buttons['diff'] = new OOUI\ButtonInputWidget( [
4293 'name' => 'wpDiff',
4294 'tabIndex' => ++$tabindex,
4295 'id' => 'wpDiffWidget',
4296 'inputId' => 'wpDiff',
4297 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4298 'useInputTag' => true,
4299 'label' => $this->context->msg( 'showdiff' )->text(),
4300 'infusable' => true,
4301 'type' => 'submit',
4302 // Message used: tooltip-diff
4303 'title' => Linker::titleAttrib( 'diff' ),
4304 // Message used: accesskey-diff
4305 'accessKey' => Linker::accesskey( 'diff' ),
4306 ] );
4307
4308 // Avoid PHP 7.1 warning of passing $this by reference
4309 $editPage = $this;
4310 Hooks::run( 'EditPageBeforeEditButtons', [ &$editPage, &$buttons, &$tabindex ] );
4311
4312 return $buttons;
4313 }
4314
4319 public function noSuchSectionPage() {
4320 $out = $this->context->getOutput();
4321 $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4322
4323 $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4324
4325 // Avoid PHP 7.1 warning of passing $this by reference
4326 $editPage = $this;
4327 Hooks::run( 'EditPageNoSuchSection', [ &$editPage, &$res ] );
4328 $out->addHTML( $res );
4329
4330 $out->returnToMain( false, $this->mTitle );
4331 }
4332
4338 public function spamPageWithContent( $match = false ) {
4339 $this->textbox2 = $this->textbox1;
4340
4341 if ( is_array( $match ) ) {
4342 $match = $this->context->getLanguage()->listToText( $match );
4343 }
4344 $out = $this->context->getOutput();
4345 $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4346
4347 $out->addHTML( '<div id="spamprotected">' );
4348 $out->addWikiMsg( 'spamprotectiontext' );
4349 if ( $match ) {
4350 $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4351 }
4352 $out->addHTML( '</div>' );
4353
4354 $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4355 $this->showDiff();
4356
4357 $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4358 $this->showTextbox2();
4359
4360 $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4361 }
4362
4373 protected function safeUnicodeInput( $request, $field ) {
4374 return rtrim( $request->getText( $field ) );
4375 }
4376
4386 protected function safeUnicodeOutput( $text ) {
4387 return $text;
4388 }
4389
4393 protected function addEditNotices() {
4394 $out = $this->context->getOutput();
4395 $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4396 if ( count( $editNotices ) ) {
4397 $out->addHTML( implode( "\n", $editNotices ) );
4398 } else {
4399 $msg = $this->context->msg( 'editnotice-notext' );
4400 if ( !$msg->isDisabled() ) {
4401 $out->addHTML(
4402 '<div class="mw-editnotice-notext">'
4403 . $msg->parseAsBlock()
4404 . '</div>'
4405 );
4406 }
4407 }
4408 }
4409
4413 protected function addTalkPageText() {
4414 if ( $this->mTitle->isTalkPage() ) {
4415 $this->context->getOutput()->addWikiMsg( 'talkpagetext' );
4416 }
4417 }
4418
4422 protected function addLongPageWarningHeader() {
4423 if ( $this->contentLength === false ) {
4424 $this->contentLength = strlen( $this->textbox1 );
4425 }
4426
4427 $out = $this->context->getOutput();
4428 $lang = $this->context->getLanguage();
4429 $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' );
4430 if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4431 $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4432 [
4433 'longpageerror',
4434 $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4435 $lang->formatNum( $maxArticleSize )
4436 ]
4437 );
4438 } elseif ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
4439 $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
4440 [
4441 'longpage-hint',
4442 $lang->formatSize( strlen( $this->textbox1 ) ),
4443 strlen( $this->textbox1 )
4444 ]
4445 );
4446 }
4447 }
4448
4452 protected function addPageProtectionWarningHeaders() {
4453 $out = $this->context->getOutput();
4454 if ( $this->mTitle->isProtected( 'edit' ) &&
4455 MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
4456 ) {
4457 # Is the title semi-protected?
4458 if ( $this->mTitle->isSemiProtected() ) {
4459 $noticeMsg = 'semiprotectedpagewarning';
4460 } else {
4461 # Then it must be protected based on static groups (regular)
4462 $noticeMsg = 'protectedpagewarning';
4463 }
4464 LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4465 [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4466 }
4467 if ( $this->mTitle->isCascadeProtected() ) {
4468 # Is this page under cascading protection from some source pages?
4470 list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4471 $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
4472 $cascadeSourcesCount = count( $cascadeSources );
4473 if ( $cascadeSourcesCount > 0 ) {
4474 # Explain, and list the titles responsible
4475 foreach ( $cascadeSources as $page ) {
4476 $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4477 }
4478 }
4479 $notice .= '</div>';
4480 $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4481 }
4482 if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4483 LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4484 [ 'lim' => 1,
4485 'showIfEmpty' => false,
4486 'msgKey' => [ 'titleprotectedwarning' ],
4487 'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4488 }
4489 }
4490
4496 $out->addHTML(
4497 $this->getEditConflictHelper()->getExplainHeader()
4498 );
4499 }
4500
4508 protected function buildTextboxAttribs( $name, array $customAttribs, User $user ) {
4509 return ( new TextboxBuilder() )->buildTextboxAttribs(
4510 $name, $customAttribs, $user, $this->mTitle
4511 );
4512 }
4513
4519 protected function addNewLineAtEnd( $wikitext ) {
4520 return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext );
4521 }
4522
4533 private function guessSectionName( $text ) {
4534 global $wgParser;
4535
4536 // Detect Microsoft browsers
4537 $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
4538 if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
4539 // ...and redirect them to legacy encoding, if available
4540 return $wgParser->guessLegacySectionNameFromWikiText( $text );
4541 }
4542 // Meanwhile, real browsers get real anchors
4543 $name = $wgParser->guessSectionNameFromWikiText( $text );
4544 // With one little caveat: per T216029, fragments in HTTP redirects need to be urlencoded,
4545 // otherwise Chrome double-escapes the rest of the URL.
4546 return '#' . urlencode( mb_substr( $name, 1 ) );
4547 }
4548
4555 public function setEditConflictHelperFactory( callable $factory ) {
4556 $this->editConflictHelperFactory = $factory;
4557 $this->editConflictHelper = null;
4558 }
4559
4563 private function getEditConflictHelper() {
4564 if ( !$this->editConflictHelper ) {
4565 $this->editConflictHelper = call_user_func(
4566 $this->editConflictHelperFactory,
4567 $this->getSubmitButtonLabel()
4568 );
4569 }
4570
4572 }
4573
4578 private function newTextConflictHelper( $submitButtonLabel ) {
4579 return new TextConflictHelper(
4580 $this->getTitle(),
4581 $this->getContext()->getOutput(),
4582 MediaWikiServices::getInstance()->getStatsdDataFactory(),
4583 $submitButtonLabel
4584 );
4585 }
4586}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
See &</td >< td > &Fill in a specific reason below(for example, citing particular pages that were vandalized).</td >< td >
target page
$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.
wfFindFile( $title, $options=[])
Find a file.
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.
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.
$wgParser
Definition Setup.php:886
$wgLang
Definition Setup.php:875
if(! $wgRequest->checkUrlExtension()) if(isset( $_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !='') $wgTitle
Definition api.php:57
Class for viewing MediaWiki article and history.
Definition Article.php:37
const TYPE_AUTO
Definition Block.php:99
static newFromTarget( $specificTarget, $vagueTarget=null, $fromMaster=false)
Given a target and the target's type, get an existing Block object if possible.
Definition Block.php:1403
Special handling for category description pages, showing pages, subcategories and file that belong to...
static canAddTagsAccompanyingChange(array $tags, User $user=null)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
getContext()
Get the base IContextSource object.
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:44
getPreviewParserOptions()
Get parser options for a preview.
string $sectiontitle
Definition EditPage.php:375
getActionURL(Title $title)
Returns the URL to use in the form's action attribute.
getEditConflictHelper()
safeUnicodeInput( $request, $field)
Filter an input field through a Unicode de-armoring process if it came from an old browser with known...
bool stdClass $lastDelete
Definition EditPage.php:260
showTextbox( $text, $name, $customAttribs=[])
string $hookError
Definition EditPage.php:302
attemptSave(&$resultDetails=false)
Attempt submission.
$editFormTextAfterTools
Definition EditPage.php:422
isSectionEditSupported()
Returns whether section editing is supported for the current page.
Definition EditPage.php:908
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:224
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:168
getOriginalContent(User $user)
Get the content of the wanted revision, without section extraction.
getCancelLink()
null string $contentFormat
Definition EditPage.php:409
bool $allowBlankSummary
Definition EditPage.php:284
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:763
bool $firsttime
True the first time the edit form is rendered, false after re-rendering with diff,...
Definition EditPage.php:257
const AS_CANNOT_USE_CUSTOM_MODEL
Status: when changing the content model is disallowed due to $wgContentHandlerUseDB being false.
Definition EditPage.php:185
runPostMergeFilters(Content $content, Status $status, User $user)
Run hooks that can filter edits just before they get saved.
bool $bot
Definition EditPage.php:403
showPreview( $text)
Append preview output to OutputPage.
const AS_UNICODE_NOT_SUPPORTED
Status: edit rejected because browser doesn't support Unicode.
Definition EditPage.php:190
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition EditPage.php:68
addTalkPageText()
const AS_READ_ONLY_PAGE_ANON
Status: this anonymous user is not allowed to edit this page.
Definition EditPage.php:83
string $textbox2
Definition EditPage.php:342
bool $tooBig
Definition EditPage.php:275
safeUnicodeOutput( $text)
Filter an output field through a Unicode armoring process if it is going to an old browser with known...
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:553
$editFormTextAfterContent
Definition EditPage.php:424
$editFormTextBottom
Definition EditPage.php:423
int $editRevId
Revision ID of the latest revision of the page when editing was initiated on the client.
Definition EditPage.php:369
$editFormTextBeforeContent
Definition EditPage.php:420
string $contentModel
Definition EditPage.php:406
bool $deletedSinceEdit
Definition EditPage.php:248
__construct(Article $article)
Definition EditPage.php:475
getEditPermissionErrors( $rigor='secure')
Definition EditPage.php:719
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:387
const POST_EDIT_COOKIE_KEY_PREFIX
Prefix of key for cookie used to pass post-edit state.
Definition EditPage.php:201
const AS_MAX_ARTICLE_SIZE_EXCEEDED
Status: article is too big (> $wgMaxArticleSize), after merging in the new section.
Definition EditPage.php:136
const AS_CONFLICT_DETECTED
Status: (non-resolvable) edit conflict.
Definition EditPage.php:120
addExplainConflictHeader(OutputPage $out)
$editFormTextAfterWarn
Definition EditPage.php:421
bool $mTokenOk
Definition EditPage.php:263
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:146
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:458
const AS_READ_ONLY_PAGE_LOGGED
Status: this logged in user is not allowed to edit this page.
Definition EditPage.php:88
bool $mShowSummaryField
Definition EditPage.php:314
showFormAfterText()
bool $recreate
Definition EditPage.php:334
callable $editConflictHelperFactory
Factory function to create an edit conflict helper.
Definition EditPage.php:465
int $parentRevId
Revision ID the edit is based on, adjusted when an edit conflict is resolved.
Definition EditPage.php:394
isWrongCaseUserConfigPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition EditPage.php:887
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:308
const AS_BLANK_ARTICLE
Status: user tried to create a blank page and wpIgnoreBlankArticle == false.
Definition EditPage.php:115
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:918
static extractSectionTitle( $text)
Extract the section title from current section text, if any.
bool $missingSummary
Definition EditPage.php:281
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:230
Revision bool null $mBaseRevision
A revision object corresponding to $this->editRevId.
Definition EditPage.php:311
bool int $contentLength
Definition EditPage.php:438
const AS_PARSE_ERROR
Status: can't parse content.
Definition EditPage.php:179
bool $blankArticle
Definition EditPage.php:287
noSuchSectionPage()
Creates a basic error page which informs the user that they have attempted to edit a nonexistent sect...
bool $mTriedSave
Definition EditPage.php:269
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:93
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:372
ParserOutput $mParserOutput
Definition EditPage.php:305
previewOnOpen()
Should we show a preview when the edit form is first shown?
Definition EditPage.php:846
displayPreviewArea( $previewOutput, $isOnTop=false)
doPreviewParse(Content $content)
Parse the page for a preview.
string $formtype
Definition EditPage.php:251
bool $allowBlankArticle
Definition EditPage.php:290
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing.
Definition EditPage.php:564
internalAttemptSave(&$result, $bot=false)
Attempt submission (no UI)
string $summary
Definition EditPage.php:345
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:319
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition EditPage.php:78
bool $diff
Definition EditPage.php:325
showStandardInputs(&$tabindex=2)
getCurrentContent()
Get the current content of the page.
bool $selfRedirect
Definition EditPage.php:293
string $textbox1
Page content input field.
Definition EditPage.php:339
addContentModelChangeLogEntry(User $user, $oldModel, $newModel, $reason)
const EDITFORM_ID
HTML id and name for the beginning of the edit form.
Definition EditPage.php:195
const AS_CHANGE_TAG_ERROR
Status: an error relating to change tagging.
Definition EditPage.php:174
string $editFormPageTop
Before even the preview.
Definition EditPage.php:418
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
Definition EditPage.php:73
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:162
getCopywarn()
Get the copyright warning.
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition EditPage.php:216
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:536
toEditText( $content)
Gets an editable textual representation of $content.
null Title $mContextTitle
Definition EditPage.php:233
string $autoSumm
Definition EditPage.php:299
bool $isOldRev
Whether an old revision is edited.
Definition EditPage.php:453
const AS_IMAGE_REDIRECT_LOGGED
Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
Definition EditPage.php:156
const AS_END
Status: WikiPage::doEdit() was unsuccessful.
Definition EditPage.php:141
const AS_ARTICLE_WAS_DELETED
Status: article was deleted while editing and param wpRecreate == false or form was not posted.
Definition EditPage.php:104
const AS_TEXTBOX_EMPTY
Status: user tried to create a new section without content.
Definition EditPage.php:131
bool $mTokenOkExceptSuffix
Definition EditPage.php:266
newSectionSummary(&$sectionanchor=null)
Return the summary to be used for a new section.
const AS_SUCCESS_UPDATE
Status: Article successfully updated.
Definition EditPage.php:53
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:380
bool $isNew
New page or new section.
Definition EditPage.php:245
showIntro()
Show all applicable editing introductions.
static getEditToolbar( $title=null)
Allow extensions to provide a toolbar.
Article $mArticle
Definition EditPage.php:222
bool $allowSelfRedirect
Definition EditPage.php:296
const AS_IMAGE_REDIRECT_ANON
Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
Definition EditPage.php:151
getContentObject( $def_content=null)
bool $isConflict
Whether an edit conflict needs to be resolved.
Definition EditPage.php:242
null $scrolltop
Definition EditPage.php:400
getLastDelete()
Get the last log record of this page being deleted, if ever.
string $action
Definition EditPage.php:236
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:587
string $editintro
Definition EditPage.php:397
newTextConflictHelper( $submitButtonLabel)
bool $enableApiEditOverride
Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing.
Definition EditPage.php:443
displayViewSourcePage(Content $content, $errorMessage='')
Display a read-only View Source page.
Definition EditPage.php:793
bool $watchthis
Definition EditPage.php:331
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:110
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:328
$previewTextAfterContent
Definition EditPage.php:425
getSummaryPreview( $isSubjectPreview, $summary="")
bool $nosummary
If true, hide the summary field.
Definition EditPage.php:350
bool $incompleteForm
Definition EditPage.php:272
const AS_SUCCESS_NEW_ARTICLE
Status: Article successfully created.
Definition EditPage.php:58
showSummaryInput( $isSubjectPreview, $summary="")
mergeChangesIntoContent(&$editContent)
Attempts to do 3-way merge of edit content with a base revision and current content,...
bool $missingComment
Definition EditPage.php:278
string $edittime
Timestamp of the latest revision of the page when editing was initiated on the client.
Definition EditPage.php:356
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:48
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:435
incrementConflictStats()
IContextSource $context
Definition EditPage.php:448
bool $preview
Definition EditPage.php:322
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:470
showEditForm( $formCallback=null)
Send the edit form and related headers to OutputPage.
null array $changeTags
Definition EditPage.php:412
const AS_RATE_LIMITED
Status: rate limiter for action 'edit' was tripped.
Definition EditPage.php:98
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
Definition EditPage.php:63
setContextTitle( $title)
Set the context Title object.
Definition EditPage.php:524
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:126
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:1965
static accesskey( $name)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition Linker.php:2013
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition Linker.php:1930
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:1480
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.
Definition LogEntry.php:441
setPerformer(UserIdentity $performer)
Set the user that performed the action being logged.
Definition LogEntry.php:536
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.
This class should be covered by a general architecture document which does not exist as of January 20...
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:277
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition Revision.php:295
const DELETED_TEXT
Definition Revision.php:46
static userWasLastToEdit( $db, $pageId, $userId, $since)
Check if no edits were made by other users since the time a user started editing the page.
const RAW
Definition Revision.php:56
const FOR_THIS_USER
Definition Revision.php:55
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:118
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:40
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:40
setContentModel( $model)
Set a proposed content model for the page for permissions checking.
Definition Title.php:1044
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:48
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:585
const IGNORE_USER_RIGHTS
Definition User.php:78
static isIP( $name)
Does the string match an anonymous IP address?
Definition User.php:967
static doWatchOrUnwatch( $watch, Title $title, User $user)
Watch or unwatch a page.
Class representing a MediaWiki article and history.
Definition WikiPage.php:45
getRedirectTarget()
If this page is a redirect, get its target.
Definition WikiPage.php:995
getContent( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition WikiPage.php:816
isRedirect()
Tests if the article content represents a redirect.
Definition WikiPage.php:630
per default it will return the text for text based content
$res
Definition database.txt:21
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a save
Definition deferred.txt:5
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
globals txt Globals are evil The original MediaWiki code relied on globals for processing context far too often MediaWiki development since then has been a story of slowly moving context out of global variables and into objects Storing processing context in object member variables allows those objects to be reused in a much more flexible way Consider the elegance of
database rows
Definition globals.txt:10
const EDIT_FORCE_BOT
Definition Defines.php:165
const EDIT_UPDATE
Definition Defines.php:162
const NS_USER
Definition Defines.php:75
const CONTENT_MODEL_CSS
Definition Defines.php:246
const NS_FILE
Definition Defines.php:79
const NS_MEDIAWIKI
Definition Defines.php:81
const CONTENT_MODEL_JSON
Definition Defines.php:248
const NS_USER_TALK
Definition Defines.php:76
const EDIT_MINOR
Definition Defines.php:163
const CONTENT_MODEL_JAVASCRIPT
Definition Defines.php:245
const EDIT_AUTOSUMMARY
Definition Defines.php:167
const EDIT_NEW
Definition Defines.php:161
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on $request
Definition hooks.txt:2843
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping $template
Definition hooks.txt:822
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImgAuthModifyHeaders':Executed just before a file is streamed to a user via img_auth.php, allowing headers to be modified beforehand. $title:LinkTarget object & $headers:HTTP headers(name=> value, names are case insensitive). Two headers get special handling:If-Modified-Since(value must be a valid HTTP date) and Range(must be of the form "bytes=(\d*-\d*)") will be honored when streaming the file. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED since 1.28! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition hooks.txt:1991
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition hooks.txt:855
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition hooks.txt:894
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1266
also included in $newHeader if any indicating whether we should show just the diff
Definition hooks.txt:1272
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:1999
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going and make changes or fix bugs In we can take all the code that deals with the little used title reversing etc
Definition hooks.txt:91
null means default & $customAttribs
Definition hooks.txt:1993
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:955
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition hooks.txt:783
null for the local wiki Added in
Definition hooks.txt:1588
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition hooks.txt:856
in this case you re responsible for computing and outputting the entire conflict i the difference between revisions and your text headers and sections and Diff & $tabindex
Definition hooks.txt:1436
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:271
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:782
return true to allow those checks to and false if checking is done remove or add to the links of a group of changes in EnhancedChangesList Hook subscribers can return false to omit this line from recentchanges use this to change the tables headers change it to an object instance and return false override the list derivative used the name of the old file & $article
Definition hooks.txt:1580
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing & $attribs
Definition hooks.txt:2012
this hook is for auditing only $response
Definition hooks.txt:780
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition hooks.txt:1617
return true to allow those checks to and false if checking is done & $user
Definition hooks.txt:1510
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title e g db for database replication lag or jobqueue for job queue size converted to pseudo seconds It is possible to add more fields and they will be returned to the user in the API response after the basic globals have been set but before ordinary actions take place $output
Definition hooks.txt:2272
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1779
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
returning false will NOT prevent logging $e
Definition hooks.txt:2175
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
Base interface for content objects.
Definition Content.php:34
Interface for objects which can provide a MediaWiki context on request.
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$content
The First
Definition primes.txt:1
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Definition router.php:42
$params
if(!isset( $args[0])) $lang