MediaWiki REL1_31
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
239 public $isConflict = false;
240
242 public $isNew = false;
243
246
248 public $formtype;
249
252
255
257 public $mTokenOk = false;
258
260 public $mTokenOkExceptSuffix = false;
261
263 public $mTriedSave = false;
264
266 public $incompleteForm = false;
267
269 public $tooBig = false;
270
272 public $missingComment = false;
273
275 public $missingSummary = false;
276
278 public $allowBlankSummary = false;
279
281 protected $blankArticle = false;
282
284 protected $allowBlankArticle = false;
285
287 protected $selfRedirect = false;
288
290 protected $allowSelfRedirect = false;
291
293 public $autoSumm = '';
294
296 public $hookError = '';
297
300
302 public $hasPresetSummary = false;
303
305 public $mBaseRevision = false;
306
308 public $mShowSummaryField = true;
309
310 # Form values
311
313 public $save = false;
314
316 public $preview = false;
317
319 public $diff = false;
320
322 public $minoredit = false;
323
325 public $watchthis = false;
326
328 public $recreate = false;
329
331 public $textbox1 = '';
332
334 public $textbox2 = '';
335
337 public $summary = '';
338
340 public $nosummary = false;
341
343 public $edittime = '';
344
346 private $editRevId = null;
347
349 public $section = '';
350
352 public $sectiontitle = '';
353
355 public $starttime = '';
356
358 public $oldid = 0;
359
361 public $parentRevId = 0;
362
364 public $editintro = '';
365
367 public $scrolltop = null;
368
370 public $bot = true;
371
374
376 public $contentFormat = null;
377
379 private $changeTags = null;
380
381 # Placeholders for text injection by hooks (must be HTML)
382 # extensions should take care to _append_ to the present value
383
385 public $editFormPageTop = '';
386 public $editFormTextTop = '';
393 public $mPreloadContent = null;
394
395 /* $didSave should be set to true whenever an article was successfully altered. */
396 public $didSave = false;
397 public $undidRev = 0;
398
399 public $suppressIntro = false;
400
402 protected $edit;
403
405 protected $contentLength = false;
406
410 private $enableApiEditOverride = false;
411
415 protected $context;
416
420 private $isOldRev = false;
421
426
433
438
442 public function __construct( Article $article ) {
443 $this->mArticle = $article;
444 $this->page = $article->getPage(); // model object
445 $this->mTitle = $article->getTitle();
446 $this->context = $article->getContext();
447
448 $this->contentModel = $this->mTitle->getContentModel();
449
450 $handler = ContentHandler::getForModelID( $this->contentModel );
451 $this->contentFormat = $handler->getDefaultFormat();
452 $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ];
453 }
454
458 public function getArticle() {
459 return $this->mArticle;
460 }
461
466 public function getContext() {
467 return $this->context;
468 }
469
474 public function getTitle() {
475 return $this->mTitle;
476 }
477
483 public function setContextTitle( $title ) {
484 $this->mContextTitle = $title;
485 }
486
494 public function getContextTitle() {
495 if ( is_null( $this->mContextTitle ) ) {
497 'GlobalTitleFail',
498 __METHOD__ . ' called by ' . wfGetAllCallers( 5 ) . ' with no title set.'
499 );
501 return $wgTitle;
502 } else {
504 }
505 }
506
512 public function isOouiEnabled() {
513 wfDeprecated( __METHOD__, '1.30' );
514 return true;
515 }
516
524 public function isSupportedContentModel( $modelId ) {
525 return $this->enableApiEditOverride === true ||
526 ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
527 }
528
535 public function setApiEditOverride( $enableOverride ) {
536 $this->enableApiEditOverride = $enableOverride;
537 }
538
542 public function submit() {
543 wfDeprecated( __METHOD__, '1.29' );
544 $this->edit();
545 }
546
558 public function edit() {
559 // Allow extensions to modify/prevent this form or submission
560 if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
561 return;
562 }
563
564 wfDebug( __METHOD__ . ": enter\n" );
565
566 $request = $this->context->getRequest();
567 // If they used redlink=1 and the page exists, redirect to the main article
568 if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
569 $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
570 return;
571 }
572
573 $this->importFormData( $request );
574 $this->firsttime = false;
575
576 if ( wfReadOnly() && $this->save ) {
577 // Force preview
578 $this->save = false;
579 $this->preview = true;
580 }
581
582 if ( $this->save ) {
583 $this->formtype = 'save';
584 } elseif ( $this->preview ) {
585 $this->formtype = 'preview';
586 } elseif ( $this->diff ) {
587 $this->formtype = 'diff';
588 } else { # First time through
589 $this->firsttime = true;
590 if ( $this->previewOnOpen() ) {
591 $this->formtype = 'preview';
592 } else {
593 $this->formtype = 'initial';
594 }
595 }
596
597 $permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
598 if ( $permErrors ) {
599 wfDebug( __METHOD__ . ": User can't edit\n" );
600 // Auto-block user's IP if the account was "hard" blocked
601 if ( !wfReadOnly() ) {
602 DeferredUpdates::addCallableUpdate( function () {
603 $this->context->getUser()->spreadAnyEditBlock();
604 } );
605 }
606 $this->displayPermissionsError( $permErrors );
607
608 return;
609 }
610
611 $revision = $this->mArticle->getRevisionFetched();
612 // Disallow editing revisions with content models different from the current one
613 // Undo edits being an exception in order to allow reverting content model changes.
614 if ( $revision
615 && $revision->getContentModel() !== $this->contentModel
616 ) {
617 $prevRev = null;
618 if ( $this->undidRev ) {
619 $undidRevObj = Revision::newFromId( $this->undidRev );
620 $prevRev = $undidRevObj ? $undidRevObj->getPrevious() : null;
621 }
622 if ( !$this->undidRev
623 || !$prevRev
624 || $prevRev->getContentModel() !== $this->contentModel
625 ) {
627 $this->getContentObject(),
628 $this->context->msg(
629 'contentmodelediterror',
630 $revision->getContentModel(),
631 $this->contentModel
632 )->plain()
633 );
634 return;
635 }
636 }
637
638 $this->isConflict = false;
639
640 # Show applicable editing introductions
641 if ( $this->formtype == 'initial' || $this->firsttime ) {
642 $this->showIntro();
643 }
644
645 # Attempt submission here. This will check for edit conflicts,
646 # and redundantly check for locked database, blocked IPs, etc.
647 # that edit() already checked just in case someone tries to sneak
648 # in the back door with a hand-edited submission URL.
649
650 if ( 'save' == $this->formtype ) {
651 $resultDetails = null;
652 $status = $this->attemptSave( $resultDetails );
653 if ( !$this->handleStatus( $status, $resultDetails ) ) {
654 return;
655 }
656 }
657
658 # First time through: get contents, set time for conflict
659 # checking, etc.
660 if ( 'initial' == $this->formtype || $this->firsttime ) {
661 if ( $this->initialiseForm() === false ) {
662 $this->noSuchSectionPage();
663 return;
664 }
665
666 if ( !$this->mTitle->getArticleID() ) {
667 Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
668 } else {
669 Hooks::run( 'EditFormInitialText', [ $this ] );
670 }
671
672 }
673
674 $this->showEditForm();
675 }
676
681 protected function getEditPermissionErrors( $rigor = 'secure' ) {
682 $user = $this->context->getUser();
683 $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
684 # Can this title be created?
685 if ( !$this->mTitle->exists() ) {
686 $permErrors = array_merge(
687 $permErrors,
689 $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
690 $permErrors
691 )
692 );
693 }
694 # Ignore some permissions errors when a user is just previewing/viewing diffs
695 $remove = [];
696 foreach ( $permErrors as $error ) {
697 if ( ( $this->preview || $this->diff )
698 && (
699 $error[0] == 'blockedtext' ||
700 $error[0] == 'autoblockedtext' ||
701 $error[0] == 'systemblockedtext'
702 )
703 ) {
704 $remove[] = $error;
705 }
706 }
707 $permErrors = wfArrayDiff2( $permErrors, $remove );
708
709 return $permErrors;
710 }
711
725 protected function displayPermissionsError( array $permErrors ) {
726 $out = $this->context->getOutput();
727 if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
728 // The edit page was reached via a red link.
729 // Redirect to the article page and let them click the edit tab if
730 // they really want a permission error.
731 $out->redirect( $this->mTitle->getFullURL() );
732 return;
733 }
734
735 $content = $this->getContentObject();
736
737 # Use the normal message if there's nothing to display
738 if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
739 $action = $this->mTitle->exists() ? 'edit' :
740 ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
741 throw new PermissionsError( $action, $permErrors );
742 }
743
745 $content,
746 $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
747 );
748 }
749
755 protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
756 $out = $this->context->getOutput();
757 Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
758
759 $out->setRobotPolicy( 'noindex,nofollow' );
760 $out->setPageTitle( $this->context->msg(
761 'viewsource-title',
762 $this->getContextTitle()->getPrefixedText()
763 ) );
764 $out->addBacklinkSubtitle( $this->getContextTitle() );
765 $out->addHTML( $this->editFormPageTop );
766 $out->addHTML( $this->editFormTextTop );
767
768 if ( $errorMessage !== '' ) {
769 $out->addWikiText( $errorMessage );
770 $out->addHTML( "<hr />\n" );
771 }
772
773 # If the user made changes, preserve them when showing the markup
774 # (This happens when a user is blocked during edit, for instance)
775 if ( !$this->firsttime ) {
776 $text = $this->textbox1;
777 $out->addWikiMsg( 'viewyourtext' );
778 } else {
779 try {
780 $text = $this->toEditText( $content );
781 } catch ( MWException $e ) {
782 # Serialize using the default format if the content model is not supported
783 # (e.g. for an old revision with a different model)
784 $text = $content->serialize();
785 }
786 $out->addWikiMsg( 'viewsourcetext' );
787 }
788
789 $out->addHTML( $this->editFormTextBeforeContent );
790 $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
791 $out->addHTML( $this->editFormTextAfterContent );
792
793 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
794
795 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
796
797 $out->addHTML( $this->editFormTextBottom );
798 if ( $this->mTitle->exists() ) {
799 $out->returnToMain( null, $this->mTitle );
800 }
801 }
802
808 protected function previewOnOpen() {
809 $config = $this->context->getConfig();
810 $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
811 $request = $this->context->getRequest();
812 if ( $config->get( 'RawHtml' ) ) {
813 // If raw HTML is enabled, disable preview on open
814 // since it has to be posted with a token for
815 // security reasons
816 return false;
817 }
818 if ( $request->getVal( 'preview' ) == 'yes' ) {
819 // Explicit override from request
820 return true;
821 } elseif ( $request->getVal( 'preview' ) == 'no' ) {
822 // Explicit override from request
823 return false;
824 } elseif ( $this->section == 'new' ) {
825 // Nothing *to* preview for new sections
826 return false;
827 } elseif ( ( $request->getVal( 'preload' ) !== null || $this->mTitle->exists() )
828 && $this->context->getUser()->getOption( 'previewonfirst' )
829 ) {
830 // Standard preference behavior
831 return true;
832 } elseif ( !$this->mTitle->exists()
833 && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
834 && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
835 ) {
836 // Categories are special
837 return true;
838 } else {
839 return false;
840 }
841 }
842
849 protected function isWrongCaseUserConfigPage() {
850 if ( $this->mTitle->isUserConfigPage() ) {
851 $name = $this->mTitle->getSkinFromConfigSubpage();
852 $skins = array_merge(
853 array_keys( Skin::getSkinNames() ),
854 [ 'common' ]
855 );
856 return !in_array( $name, $skins )
857 && in_array( strtolower( $name ), $skins );
858 } else {
859 return false;
860 }
861 }
862
870 protected function isSectionEditSupported() {
871 $contentHandler = ContentHandler::getForTitle( $this->mTitle );
872 return $contentHandler->supportsSections();
873 }
874
880 public function importFormData( &$request ) {
881 # Section edit can come from either the form or a link
882 $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
883
884 if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
885 throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
886 }
887
888 $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
889
890 if ( $request->wasPosted() ) {
891 # These fields need to be checked for encoding.
892 # Also remove trailing whitespace, but don't remove _initial_
893 # whitespace from the text boxes. This may be significant formatting.
894 $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
895 if ( !$request->getCheck( 'wpTextbox2' ) ) {
896 // Skip this if wpTextbox2 has input, it indicates that we came
897 // from a conflict page with raw page text, not a custom form
898 // modified by subclasses
900 if ( $textbox1 !== null ) {
901 $this->textbox1 = $textbox1;
902 }
903 }
904
905 $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
906
907 $this->summary = $request->getText( 'wpSummary' );
908
909 # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
910 # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
911 # section titles.
912 $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
913
914 # Treat sectiontitle the same way as summary.
915 # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
916 # currently doing double duty as both edit summary and section title. Right now this
917 # is just to allow API edits to work around this limitation, but this should be
918 # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312).
919 $this->sectiontitle = $request->getText( 'wpSectionTitle' );
920 $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
921
922 $this->edittime = $request->getVal( 'wpEdittime' );
923 $this->editRevId = $request->getIntOrNull( 'editRevId' );
924 $this->starttime = $request->getVal( 'wpStarttime' );
925
926 $undidRev = $request->getInt( 'wpUndidRevision' );
927 if ( $undidRev ) {
928 $this->undidRev = $undidRev;
929 }
930
931 $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
932
933 if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
934 // wpTextbox1 field is missing, possibly due to being "too big"
935 // according to some filter rules such as Suhosin's setting for
936 // suhosin.request.max_value_length (d'oh)
937 $this->incompleteForm = true;
938 } else {
939 // If we receive the last parameter of the request, we can fairly
940 // claim the POST request has not been truncated.
941
942 // TODO: softened the check for cutover. Once we determine
943 // that it is safe, we should complete the transition by
944 // removing the "edittime" clause.
945 $this->incompleteForm = ( !$request->getVal( 'wpUltimateParam' )
946 && is_null( $this->edittime ) );
947 }
948 if ( $this->incompleteForm ) {
949 # If the form is incomplete, force to preview.
950 wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
951 wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" );
952 $this->preview = true;
953 } else {
954 $this->preview = $request->getCheck( 'wpPreview' );
955 $this->diff = $request->getCheck( 'wpDiff' );
956
957 // Remember whether a save was requested, so we can indicate
958 // if we forced preview due to session failure.
959 $this->mTriedSave = !$this->preview;
960
961 if ( $this->tokenOk( $request ) ) {
962 # Some browsers will not report any submit button
963 # if the user hits enter in the comment box.
964 # The unmarked state will be assumed to be a save,
965 # if the form seems otherwise complete.
966 wfDebug( __METHOD__ . ": Passed token check.\n" );
967 } elseif ( $this->diff ) {
968 # Failed token check, but only requested "Show Changes".
969 wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
970 } else {
971 # Page might be a hack attempt posted from
972 # an external site. Preview instead of saving.
973 wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
974 $this->preview = true;
975 }
976 }
977 $this->save = !$this->preview && !$this->diff;
978 if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
979 $this->edittime = null;
980 }
981
982 if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
983 $this->starttime = null;
984 }
985
986 $this->recreate = $request->getCheck( 'wpRecreate' );
987
988 $this->minoredit = $request->getCheck( 'wpMinoredit' );
989 $this->watchthis = $request->getCheck( 'wpWatchthis' );
990
991 $user = $this->context->getUser();
992 # Don't force edit summaries when a user is editing their own user or talk page
993 if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
994 && $this->mTitle->getText() == $user->getName()
995 ) {
996 $this->allowBlankSummary = true;
997 } else {
998 $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
999 || !$user->getOption( 'forceeditsummary' );
1000 }
1001
1002 $this->autoSumm = $request->getText( 'wpAutoSummary' );
1003
1004 $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1005 $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
1006
1007 $changeTags = $request->getVal( 'wpChangeTags' );
1008 if ( is_null( $changeTags ) || $changeTags === '' ) {
1009 $this->changeTags = [];
1010 } else {
1011 $this->changeTags = array_filter( array_map( 'trim', explode( ',',
1012 $changeTags ) ) );
1013 }
1014 } else {
1015 # Not a posted form? Start with nothing.
1016 wfDebug( __METHOD__ . ": Not a posted form.\n" );
1017 $this->textbox1 = '';
1018 $this->summary = '';
1019 $this->sectiontitle = '';
1020 $this->edittime = '';
1021 $this->editRevId = null;
1022 $this->starttime = wfTimestampNow();
1023 $this->edit = false;
1024 $this->preview = false;
1025 $this->save = false;
1026 $this->diff = false;
1027 $this->minoredit = false;
1028 // Watch may be overridden by request parameters
1029 $this->watchthis = $request->getBool( 'watchthis', false );
1030 $this->recreate = false;
1031
1032 // When creating a new section, we can preload a section title by passing it as the
1033 // preloadtitle parameter in the URL (T15100)
1034 if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1035 $this->sectiontitle = $request->getVal( 'preloadtitle' );
1036 // Once wpSummary isn't being use for setting section titles, we should delete this.
1037 $this->summary = $request->getVal( 'preloadtitle' );
1038 } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) {
1039 $this->summary = $request->getText( 'summary' );
1040 if ( $this->summary !== '' ) {
1041 $this->hasPresetSummary = true;
1042 }
1043 }
1044
1045 if ( $request->getVal( 'minor' ) ) {
1046 $this->minoredit = true;
1047 }
1048 }
1049
1050 $this->oldid = $request->getInt( 'oldid' );
1051 $this->parentRevId = $request->getInt( 'parentRevId' );
1052
1053 $this->bot = $request->getBool( 'bot', true );
1054 $this->nosummary = $request->getBool( 'nosummary' );
1055
1056 // May be overridden by revision.
1057 $this->contentModel = $request->getText( 'model', $this->contentModel );
1058 // May be overridden by revision.
1059 $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1060
1061 try {
1062 $handler = ContentHandler::getForModelID( $this->contentModel );
1064 throw new ErrorPageError(
1065 'editpage-invalidcontentmodel-title',
1066 'editpage-invalidcontentmodel-text',
1067 [ wfEscapeWikiText( $this->contentModel ) ]
1068 );
1069 }
1070
1071 if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1072 throw new ErrorPageError(
1073 'editpage-notsupportedcontentformat-title',
1074 'editpage-notsupportedcontentformat-text',
1075 [
1076 wfEscapeWikiText( $this->contentFormat ),
1077 wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1078 ]
1079 );
1080 }
1081
1088 $this->editintro = $request->getText( 'editintro',
1089 // Custom edit intro for new sections
1090 $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1091
1092 // Allow extensions to modify form data
1093 Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1094 }
1095
1105 protected function importContentFormData( &$request ) {
1106 return; // Don't do anything, EditPage already extracted wpTextbox1
1107 }
1108
1114 public function initialiseForm() {
1115 $this->edittime = $this->page->getTimestamp();
1116 $this->editRevId = $this->page->getLatest();
1117
1118 $content = $this->getContentObject( false ); # TODO: track content object?!
1119 if ( $content === false ) {
1120 return false;
1121 }
1122 $this->textbox1 = $this->toEditText( $content );
1123
1124 $user = $this->context->getUser();
1125 // activate checkboxes if user wants them to be always active
1126 # Sort out the "watch" checkbox
1127 if ( $user->getOption( 'watchdefault' ) ) {
1128 # Watch all edits
1129 $this->watchthis = true;
1130 } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1131 # Watch creations
1132 $this->watchthis = true;
1133 } elseif ( $user->isWatched( $this->mTitle ) ) {
1134 # Already watched
1135 $this->watchthis = true;
1136 }
1137 if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1138 $this->minoredit = true;
1139 }
1140 if ( $this->textbox1 === false ) {
1141 return false;
1142 }
1143 return true;
1144 }
1145
1153 protected function getContentObject( $def_content = null ) {
1155
1156 $content = false;
1157
1158 $user = $this->context->getUser();
1159 $request = $this->context->getRequest();
1160 // For message page not locally set, use the i18n message.
1161 // For other non-existent articles, use preload text if any.
1162 if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1163 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1164 # If this is a system message, get the default text.
1165 $msg = $this->mTitle->getDefaultMessageText();
1166
1167 $content = $this->toEditContent( $msg );
1168 }
1169 if ( $content === false ) {
1170 # If requested, preload some text.
1171 $preload = $request->getVal( 'preload',
1172 // Custom preload text for new sections
1173 $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1174 $params = $request->getArray( 'preloadparams', [] );
1175
1176 $content = $this->getPreloadedContent( $preload, $params );
1177 }
1178 // For existing pages, get text based on "undo" or section parameters.
1179 } else {
1180 if ( $this->section != '' ) {
1181 // Get section edit text (returns $def_text for invalid sections)
1182 $orig = $this->getOriginalContent( $user );
1183 $content = $orig ? $orig->getSection( $this->section ) : null;
1184
1185 if ( !$content ) {
1186 $content = $def_content;
1187 }
1188 } else {
1189 $undoafter = $request->getInt( 'undoafter' );
1190 $undo = $request->getInt( 'undo' );
1191
1192 if ( $undo > 0 && $undoafter > 0 ) {
1193 $undorev = Revision::newFromId( $undo );
1194 $oldrev = Revision::newFromId( $undoafter );
1195
1196 # Sanity check, make sure it's the right page,
1197 # the revisions exist and they were not deleted.
1198 # Otherwise, $content will be left as-is.
1199 if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1200 !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1201 !$oldrev->isDeleted( Revision::DELETED_TEXT )
1202 ) {
1203 $content = $this->page->getUndoContent( $undorev, $oldrev );
1204
1205 if ( $content === false ) {
1206 # Warn the user that something went wrong
1207 $undoMsg = 'failure';
1208 } else {
1209 $oldContent = $this->page->getContent( Revision::RAW );
1210 $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
1211 $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
1212 if ( $newContent->getModel() !== $oldContent->getModel() ) {
1213 // The undo may change content
1214 // model if its reverting the top
1215 // edit. This can result in
1216 // mismatched content model/format.
1217 $this->contentModel = $newContent->getModel();
1218 $this->contentFormat = $oldrev->getContentFormat();
1219 }
1220
1221 if ( $newContent->equals( $oldContent ) ) {
1222 # Tell the user that the undo results in no change,
1223 # i.e. the revisions were already undone.
1224 $undoMsg = 'nochange';
1225 $content = false;
1226 } else {
1227 # Inform the user of our success and set an automatic edit summary
1228 $undoMsg = 'success';
1229
1230 # If we just undid one rev, use an autosummary
1231 $firstrev = $oldrev->getNext();
1232 if ( $firstrev && $firstrev->getId() == $undo ) {
1233 $userText = $undorev->getUserText();
1234 if ( $userText === '' ) {
1235 $undoSummary = $this->context->msg(
1236 'undo-summary-username-hidden',
1237 $undo
1238 )->inContentLanguage()->text();
1239 } else {
1240 $undoSummary = $this->context->msg(
1241 'undo-summary',
1242 $undo,
1243 $userText
1244 )->inContentLanguage()->text();
1245 }
1246 if ( $this->summary === '' ) {
1247 $this->summary = $undoSummary;
1248 } else {
1249 $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1250 ->inContentLanguage()->text() . $this->summary;
1251 }
1252 $this->undidRev = $undo;
1253 }
1254 $this->formtype = 'diff';
1255 }
1256 }
1257 } else {
1258 // Failed basic sanity checks.
1259 // Older revisions may have been removed since the link
1260 // was created, or we may simply have got bogus input.
1261 $undoMsg = 'norev';
1262 }
1263
1264 $out = $this->context->getOutput();
1265 // Messages: undo-success, undo-failure, undo-norev, undo-nochange
1266 $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1267 $this->editFormPageTop .= $out->parse( "<div class=\"{$class}\">" .
1268 $this->context->msg( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
1269 }
1270
1271 if ( $content === false ) {
1272 $content = $this->getOriginalContent( $user );
1273 }
1274 }
1275 }
1276
1277 return $content;
1278 }
1279
1295 private function getOriginalContent( User $user ) {
1296 if ( $this->section == 'new' ) {
1297 return $this->getCurrentContent();
1298 }
1299 $revision = $this->mArticle->getRevisionFetched();
1300 if ( $revision === null ) {
1301 $handler = ContentHandler::getForModelID( $this->contentModel );
1302 return $handler->makeEmptyContent();
1303 }
1304 $content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1305 return $content;
1306 }
1307
1320 public function getParentRevId() {
1321 if ( $this->parentRevId ) {
1322 return $this->parentRevId;
1323 } else {
1324 return $this->mArticle->getRevIdFetched();
1325 }
1326 }
1327
1336 protected function getCurrentContent() {
1337 $rev = $this->page->getRevision();
1338 $content = $rev ? $rev->getContent( Revision::RAW ) : null;
1339
1340 if ( $content === false || $content === null ) {
1341 $handler = ContentHandler::getForModelID( $this->contentModel );
1342 return $handler->makeEmptyContent();
1343 } elseif ( !$this->undidRev ) {
1344 // Content models should always be the same since we error
1345 // out if they are different before this point (in ->edit()).
1346 // The exception being, during an undo, the current revision might
1347 // differ from the prior revision.
1348 $logger = LoggerFactory::getInstance( 'editpage' );
1349 if ( $this->contentModel !== $rev->getContentModel() ) {
1350 $logger->warning( "Overriding content model from current edit {prev} to {new}", [
1351 'prev' => $this->contentModel,
1352 'new' => $rev->getContentModel(),
1353 'title' => $this->getTitle()->getPrefixedDBkey(),
1354 'method' => __METHOD__
1355 ] );
1356 $this->contentModel = $rev->getContentModel();
1357 }
1358
1359 // Given that the content models should match, the current selected
1360 // format should be supported.
1361 if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1362 $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1363
1364 'prev' => $this->contentFormat,
1365 'new' => $rev->getContentFormat(),
1366 'title' => $this->getTitle()->getPrefixedDBkey(),
1367 'method' => __METHOD__
1368 ] );
1369 $this->contentFormat = $rev->getContentFormat();
1370 }
1371 }
1372 return $content;
1373 }
1374
1382 public function setPreloadedContent( Content $content ) {
1383 $this->mPreloadContent = $content;
1384 }
1385
1397 protected function getPreloadedContent( $preload, $params = [] ) {
1398 if ( !empty( $this->mPreloadContent ) ) {
1400 }
1401
1402 $handler = ContentHandler::getForModelID( $this->contentModel );
1403
1404 if ( $preload === '' ) {
1405 return $handler->makeEmptyContent();
1406 }
1407
1408 $user = $this->context->getUser();
1409 $title = Title::newFromText( $preload );
1410 # Check for existence to avoid getting MediaWiki:Noarticletext
1411 if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) {
1412 // TODO: somehow show a warning to the user!
1413 return $handler->makeEmptyContent();
1414 }
1415
1416 $page = WikiPage::factory( $title );
1417 if ( $page->isRedirect() ) {
1419 # Same as before
1420 if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) {
1421 // TODO: somehow show a warning to the user!
1422 return $handler->makeEmptyContent();
1423 }
1424 $page = WikiPage::factory( $title );
1425 }
1426
1427 $parserOptions = ParserOptions::newFromUser( $user );
1428 $content = $page->getContent( Revision::RAW );
1429
1430 if ( !$content ) {
1431 // TODO: somehow show a warning to the user!
1432 return $handler->makeEmptyContent();
1433 }
1434
1435 if ( $content->getModel() !== $handler->getModelID() ) {
1436 $converted = $content->convert( $handler->getModelID() );
1437
1438 if ( !$converted ) {
1439 // TODO: somehow show a warning to the user!
1440 wfDebug( "Attempt to preload incompatible content: " .
1441 "can't convert " . $content->getModel() .
1442 " to " . $handler->getModelID() );
1443
1444 return $handler->makeEmptyContent();
1445 }
1446
1447 $content = $converted;
1448 }
1449
1450 return $content->preloadTransform( $title, $parserOptions, $params );
1451 }
1452
1460 public function tokenOk( &$request ) {
1461 $token = $request->getVal( 'wpEditToken' );
1462 $user = $this->context->getUser();
1463 $this->mTokenOk = $user->matchEditToken( $token );
1464 $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
1465 return $this->mTokenOk;
1466 }
1467
1482 protected function setPostEditCookie( $statusValue ) {
1483 $revisionId = $this->page->getLatest();
1484 $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1485
1486 $val = 'saved';
1487 if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1488 $val = 'created';
1489 } elseif ( $this->oldid ) {
1490 $val = 'restored';
1491 }
1492
1493 $response = $this->context->getRequest()->response();
1494 $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1495 }
1496
1503 public function attemptSave( &$resultDetails = false ) {
1504 # Allow bots to exempt some edits from bot flagging
1505 $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot;
1506 $status = $this->internalAttemptSave( $resultDetails, $bot );
1507
1508 Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1509
1510 return $status;
1511 }
1512
1516 private function incrementResolvedConflicts() {
1517 if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1518 return;
1519 }
1520
1521 $this->getEditConflictHelper()->incrementResolvedStats();
1522 }
1523
1533 private function handleStatus( Status $status, $resultDetails ) {
1538 if ( $status->value == self::AS_SUCCESS_UPDATE
1539 || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1540 ) {
1542
1543 $this->didSave = true;
1544 if ( !$resultDetails['nullEdit'] ) {
1545 $this->setPostEditCookie( $status->value );
1546 }
1547 }
1548
1549 $out = $this->context->getOutput();
1550
1551 // "wpExtraQueryRedirect" is a hidden input to modify
1552 // after save URL and is not used by actual edit form
1553 $request = $this->context->getRequest();
1554 $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1555
1556 switch ( $status->value ) {
1564 case self::AS_END:
1567 return true;
1568
1570 return false;
1571
1575 $out->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
1576 return true;
1577
1579 $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1580 if ( $extraQueryRedirect ) {
1581 if ( $query === '' ) {
1582 $query = $extraQueryRedirect;
1583 } else {
1584 $query = $query . '&' . $extraQueryRedirect;
1585 }
1586 }
1587 $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
1588 $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1589 return false;
1590
1592 $extraQuery = '';
1593 $sectionanchor = $resultDetails['sectionanchor'];
1594
1595 // Give extensions a chance to modify URL query on update
1596 Hooks::run(
1597 'ArticleUpdateBeforeRedirect',
1598 [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1599 );
1600
1601 if ( $resultDetails['redirect'] ) {
1602 if ( $extraQuery == '' ) {
1603 $extraQuery = 'redirect=no';
1604 } else {
1605 $extraQuery = 'redirect=no&' . $extraQuery;
1606 }
1607 }
1608 if ( $extraQueryRedirect ) {
1609 if ( $extraQuery === '' ) {
1610 $extraQuery = $extraQueryRedirect;
1611 } else {
1612 $extraQuery = $extraQuery . '&' . $extraQueryRedirect;
1613 }
1614 }
1615
1616 $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1617 return false;
1618
1620 $this->spamPageWithContent( $resultDetails['spam'] );
1621 return false;
1622
1624 throw new UserBlockedError( $this->context->getUser()->getBlock() );
1625
1628 throw new PermissionsError( 'upload' );
1629
1632 throw new PermissionsError( 'edit' );
1633
1635 throw new ReadOnlyError;
1636
1638 throw new ThrottledError();
1639
1641 $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1642 throw new PermissionsError( $permission );
1643
1645 throw new PermissionsError( 'editcontentmodel' );
1646
1647 default:
1648 // We don't recognize $status->value. The only way that can happen
1649 // is if an extension hook aborted from inside ArticleSave.
1650 // Render the status object into $this->hookError
1651 // FIXME this sucks, we should just use the Status object throughout
1652 $this->hookError = '<div class="error">' ."\n" . $status->getWikiText() .
1653 '</div>';
1654 return true;
1655 }
1656 }
1657
1667 protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1668 // Run old style post-section-merge edit filter
1669 if ( $this->hookError != '' ) {
1670 # ...or the hook could be expecting us to produce an error
1671 $status->fatal( 'hookaborted' );
1673 return false;
1674 }
1675
1676 // Run new style post-section-merge edit filter
1677 if ( !Hooks::run( 'EditFilterMergedContent',
1678 [ $this->context, $content, $status, $this->summary,
1679 $user, $this->minoredit ] )
1680 ) {
1681 # Error messages etc. could be handled within the hook...
1682 if ( $status->isGood() ) {
1683 $status->fatal( 'hookaborted' );
1684 // Not setting $this->hookError here is a hack to allow the hook
1685 // to cause a return to the edit page without $this->hookError
1686 // being set. This is used by ConfirmEdit to display a captcha
1687 // without any error message cruft.
1688 } else {
1689 $this->hookError = $this->formatStatusErrors( $status );
1690 }
1691 // Use the existing $status->value if the hook set it
1692 if ( !$status->value ) {
1694 }
1695 return false;
1696 } elseif ( !$status->isOK() ) {
1697 # ...or the hook could be expecting us to produce an error
1698 // FIXME this sucks, we should just use the Status object throughout
1699 $this->hookError = $this->formatStatusErrors( $status );
1700 $status->fatal( 'hookaborted' );
1702 return false;
1703 }
1704
1705 return true;
1706 }
1707
1714 private function formatStatusErrors( Status $status ) {
1715 $errmsg = $status->getWikiText(
1716 'edit-error-short',
1717 'edit-error-long',
1718 $this->context->getLanguage()
1719 );
1720 return <<<ERROR
1721<div class="errorbox">
1722{$errmsg}
1723</div>
1724<br clear="all" />
1725ERROR;
1726 }
1727
1734 private function newSectionSummary( &$sectionanchor = null ) {
1736
1737 if ( $this->sectiontitle !== '' ) {
1738 $sectionanchor = $this->guessSectionName( $this->sectiontitle );
1739 // If no edit summary was specified, create one automatically from the section
1740 // title and have it link to the new section. Otherwise, respect the summary as
1741 // passed.
1742 if ( $this->summary === '' ) {
1743 $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1744 return $this->context->msg( 'newsectionsummary' )
1745 ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
1746 }
1747 } elseif ( $this->summary !== '' ) {
1748 $sectionanchor = $this->guessSectionName( $this->summary );
1749 # This is a new section, so create a link to the new section
1750 # in the revision summary.
1751 $cleanSummary = $wgParser->stripSectionName( $this->summary );
1752 return $this->context->msg( 'newsectionsummary' )
1753 ->rawParams( $cleanSummary )->inContentLanguage()->text();
1754 }
1755 return $this->summary;
1756 }
1757
1782 public function internalAttemptSave( &$result, $bot = false ) {
1783 $status = Status::newGood();
1784 $user = $this->context->getUser();
1785
1786 if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1787 wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1788 $status->fatal( 'hookaborted' );
1790 return $status;
1791 }
1792
1793 if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
1794 $status->fatal( 'unicode-support-fail' );
1796 return $status;
1797 }
1798
1799 $request = $this->context->getRequest();
1800 $spam = $request->getText( 'wpAntispam' );
1801 if ( $spam !== '' ) {
1802 wfDebugLog(
1803 'SimpleAntiSpam',
1804 $user->getName() .
1805 ' editing "' .
1806 $this->mTitle->getPrefixedText() .
1807 '" submitted bogus field "' .
1808 $spam .
1809 '"'
1810 );
1811 $status->fatal( 'spamprotectionmatch', false );
1813 return $status;
1814 }
1815
1816 try {
1817 # Construct Content object
1818 $textbox_content = $this->toEditContent( $this->textbox1 );
1819 } catch ( MWContentSerializationException $ex ) {
1820 $status->fatal(
1821 'content-failed-to-parse',
1822 $this->contentModel,
1823 $this->contentFormat,
1824 $ex->getMessage()
1825 );
1827 return $status;
1828 }
1829
1830 # Check image redirect
1831 if ( $this->mTitle->getNamespace() == NS_FILE &&
1832 $textbox_content->isRedirect() &&
1833 !$user->isAllowed( 'upload' )
1834 ) {
1836 $status->setResult( false, $code );
1837
1838 return $status;
1839 }
1840
1841 # Check for spam
1842 $match = self::matchSummarySpamRegex( $this->summary );
1843 if ( $match === false && $this->section == 'new' ) {
1844 # $wgSpamRegex is enforced on this new heading/summary because, unlike
1845 # regular summaries, it is added to the actual wikitext.
1846 if ( $this->sectiontitle !== '' ) {
1847 # This branch is taken when the API is used with the 'sectiontitle' parameter.
1848 $match = self::matchSpamRegex( $this->sectiontitle );
1849 } else {
1850 # This branch is taken when the "Add Topic" user interface is used, or the API
1851 # is used with the 'summary' parameter.
1852 $match = self::matchSpamRegex( $this->summary );
1853 }
1854 }
1855 if ( $match === false ) {
1856 $match = self::matchSpamRegex( $this->textbox1 );
1857 }
1858 if ( $match !== false ) {
1859 $result['spam'] = $match;
1860 $ip = $request->getIP();
1861 $pdbk = $this->mTitle->getPrefixedDBkey();
1862 $match = str_replace( "\n", '', $match );
1863 wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1864 $status->fatal( 'spamprotectionmatch', $match );
1866 return $status;
1867 }
1868 if ( !Hooks::run(
1869 'EditFilter',
1870 [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1871 ) {
1872 # Error messages etc. could be handled within the hook...
1873 $status->fatal( 'hookaborted' );
1875 return $status;
1876 } elseif ( $this->hookError != '' ) {
1877 # ...or the hook could be expecting us to produce an error
1878 $status->fatal( 'hookaborted' );
1880 return $status;
1881 }
1882
1883 if ( $user->isBlockedFrom( $this->mTitle, false ) ) {
1884 // Auto-block user's IP if the account was "hard" blocked
1885 if ( !wfReadOnly() ) {
1886 $user->spreadAnyEditBlock();
1887 }
1888 # Check block state against master, thus 'false'.
1889 $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1890 return $status;
1891 }
1892
1893 $this->contentLength = strlen( $this->textbox1 );
1894 $config = $this->context->getConfig();
1895 $maxArticleSize = $config->get( 'MaxArticleSize' );
1896 if ( $this->contentLength > $maxArticleSize * 1024 ) {
1897 // Error will be displayed by showEditForm()
1898 $this->tooBig = true;
1899 $status->setResult( false, self::AS_CONTENT_TOO_BIG );
1900 return $status;
1901 }
1902
1903 if ( !$user->isAllowed( 'edit' ) ) {
1904 if ( $user->isAnon() ) {
1905 $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1906 return $status;
1907 } else {
1908 $status->fatal( 'readonlytext' );
1910 return $status;
1911 }
1912 }
1913
1914 $changingContentModel = false;
1915 if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1916 if ( !$config->get( 'ContentHandlerUseDB' ) ) {
1917 $status->fatal( 'editpage-cannot-use-custom-model' );
1919 return $status;
1920 } elseif ( !$user->isAllowed( 'editcontentmodel' ) ) {
1921 $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1922 return $status;
1923 }
1924 // Make sure the user can edit the page under the new content model too
1925 $titleWithNewContentModel = clone $this->mTitle;
1926 $titleWithNewContentModel->setContentModel( $this->contentModel );
1927 if ( !$titleWithNewContentModel->userCan( 'editcontentmodel', $user )
1928 || !$titleWithNewContentModel->userCan( 'edit', $user )
1929 ) {
1930 $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1931 return $status;
1932 }
1933
1934 $changingContentModel = true;
1935 $oldContentModel = $this->mTitle->getContentModel();
1936 }
1937
1938 if ( $this->changeTags ) {
1939 $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
1940 $this->changeTags, $user );
1941 if ( !$changeTagsStatus->isOK() ) {
1942 $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
1943 return $changeTagsStatus;
1944 }
1945 }
1946
1947 if ( wfReadOnly() ) {
1948 $status->fatal( 'readonlytext' );
1950 return $status;
1951 }
1952 if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 )
1953 || ( $changingContentModel && $user->pingLimiter( 'editcontentmodel' ) )
1954 ) {
1955 $status->fatal( 'actionthrottledtext' );
1957 return $status;
1958 }
1959
1960 # If the article has been deleted while editing, don't save it without
1961 # confirmation
1962 if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
1963 $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
1964 return $status;
1965 }
1966
1967 # Load the page data from the master. If anything changes in the meantime,
1968 # we detect it by using page_latest like a token in a 1 try compare-and-swap.
1969 $this->page->loadPageData( 'fromdbmaster' );
1970 $new = !$this->page->exists();
1971
1972 if ( $new ) {
1973 // Late check for create permission, just in case *PARANOIA*
1974 if ( !$this->mTitle->userCan( 'create', $user ) ) {
1975 $status->fatal( 'nocreatetext' );
1977 wfDebug( __METHOD__ . ": no create permission\n" );
1978 return $status;
1979 }
1980
1981 // Don't save a new page if it's blank or if it's a MediaWiki:
1982 // message with content equivalent to default (allow empty pages
1983 // in this case to disable messages, see T52124)
1984 $defaultMessageText = $this->mTitle->getDefaultMessageText();
1985 if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
1986 $defaultText = $defaultMessageText;
1987 } else {
1988 $defaultText = '';
1989 }
1990
1991 if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
1992 $this->blankArticle = true;
1993 $status->fatal( 'blankarticle' );
1994 $status->setResult( false, self::AS_BLANK_ARTICLE );
1995 return $status;
1996 }
1997
1998 if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
1999 return $status;
2000 }
2001
2002 $content = $textbox_content;
2003
2004 $result['sectionanchor'] = '';
2005 if ( $this->section == 'new' ) {
2006 if ( $this->sectiontitle !== '' ) {
2007 // Insert the section title above the content.
2008 $content = $content->addSectionHeader( $this->sectiontitle );
2009 } elseif ( $this->summary !== '' ) {
2010 // Insert the section title above the content.
2011 $content = $content->addSectionHeader( $this->summary );
2012 }
2013 $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
2014 }
2015
2017
2018 } else { # not $new
2019
2020 # Article exists. Check for edit conflict.
2021
2022 $this->page->clear(); # Force reload of dates, etc.
2023 $timestamp = $this->page->getTimestamp();
2024 $latest = $this->page->getLatest();
2025
2026 wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
2027
2028 // Check editRevId if set, which handles same-second timestamp collisions
2029 if ( $timestamp != $this->edittime
2030 || ( $this->editRevId !== null && $this->editRevId != $latest )
2031 ) {
2032 $this->isConflict = true;
2033 if ( $this->section == 'new' ) {
2034 if ( $this->page->getUserText() == $user->getName() &&
2035 $this->page->getComment() == $this->newSectionSummary()
2036 ) {
2037 // Probably a duplicate submission of a new comment.
2038 // This can happen when CDN resends a request after
2039 // a timeout but the first one actually went through.
2040 wfDebug( __METHOD__
2041 . ": duplicate new section submission; trigger edit conflict!\n" );
2042 } else {
2043 // New comment; suppress conflict.
2044 $this->isConflict = false;
2045 wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
2046 }
2047 } elseif ( $this->section == ''
2048 && Revision::userWasLastToEdit(
2049 DB_MASTER, $this->mTitle->getArticleID(),
2050 $user->getId(), $this->edittime
2051 )
2052 ) {
2053 # Suppress edit conflict with self, except for section edits where merging is required.
2054 wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
2055 $this->isConflict = false;
2056 }
2057 }
2058
2059 // If sectiontitle is set, use it, otherwise use the summary as the section title.
2060 if ( $this->sectiontitle !== '' ) {
2061 $sectionTitle = $this->sectiontitle;
2062 } else {
2063 $sectionTitle = $this->summary;
2064 }
2065
2066 $content = null;
2067
2068 if ( $this->isConflict ) {
2069 wfDebug( __METHOD__
2070 . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2071 . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
2072 // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2073 // ...or disable section editing for non-current revisions (not exposed anyway).
2074 if ( $this->editRevId !== null ) {
2075 $content = $this->page->replaceSectionAtRev(
2076 $this->section,
2077 $textbox_content,
2078 $sectionTitle,
2079 $this->editRevId
2080 );
2081 } else {
2082 $content = $this->page->replaceSectionContent(
2083 $this->section,
2084 $textbox_content,
2085 $sectionTitle,
2086 $this->edittime
2087 );
2088 }
2089 } else {
2090 wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
2091 $content = $this->page->replaceSectionContent(
2092 $this->section,
2093 $textbox_content,
2094 $sectionTitle
2095 );
2096 }
2097
2098 if ( is_null( $content ) ) {
2099 wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
2100 $this->isConflict = true;
2101 $content = $textbox_content; // do not try to merge here!
2102 } elseif ( $this->isConflict ) {
2103 # Attempt merge
2104 if ( $this->mergeChangesIntoContent( $content ) ) {
2105 // Successful merge! Maybe we should tell the user the good news?
2106 $this->isConflict = false;
2107 wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
2108 } else {
2109 $this->section = '';
2110 $this->textbox1 = ContentHandler::getContentText( $content );
2111 wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
2112 }
2113 }
2114
2115 if ( $this->isConflict ) {
2116 $status->setResult( false, self::AS_CONFLICT_DETECTED );
2117 return $status;
2118 }
2119
2120 if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
2121 return $status;
2122 }
2123
2124 if ( $this->section == 'new' ) {
2125 // Handle the user preference to force summaries here
2126 if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2127 $this->missingSummary = true;
2128 $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2130 return $status;
2131 }
2132
2133 // Do not allow the user to post an empty comment
2134 if ( $this->textbox1 == '' ) {
2135 $this->missingComment = true;
2136 $status->fatal( 'missingcommenttext' );
2138 return $status;
2139 }
2140 } elseif ( !$this->allowBlankSummary
2141 && !$content->equals( $this->getOriginalContent( $user ) )
2142 && !$content->isRedirect()
2143 && md5( $this->summary ) == $this->autoSumm
2144 ) {
2145 $this->missingSummary = true;
2146 $status->fatal( 'missingsummary' );
2148 return $status;
2149 }
2150
2151 # All's well
2152 $sectionanchor = '';
2153 if ( $this->section == 'new' ) {
2154 $this->summary = $this->newSectionSummary( $sectionanchor );
2155 } elseif ( $this->section != '' ) {
2156 # Try to get a section anchor from the section source, redirect
2157 # to edited section if header found.
2158 # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2159 # for duplicate heading checking and maybe parsing.
2160 $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2161 # We can't deal with anchors, includes, html etc in the header for now,
2162 # headline would need to be parsed to improve this.
2163 if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2164 $sectionanchor = $this->guessSectionName( $matches[2] );
2165 }
2166 }
2167 $result['sectionanchor'] = $sectionanchor;
2168
2169 // Save errors may fall down to the edit form, but we've now
2170 // merged the section into full text. Clear the section field
2171 // so that later submission of conflict forms won't try to
2172 // replace that into a duplicated mess.
2173 $this->textbox1 = $this->toEditText( $content );
2174 $this->section = '';
2175
2177 }
2178
2179 if ( !$this->allowSelfRedirect
2180 && $content->isRedirect()
2181 && $content->getRedirectTarget()->equals( $this->getTitle() )
2182 ) {
2183 // If the page already redirects to itself, don't warn.
2184 $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2185 if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2186 $this->selfRedirect = true;
2187 $status->fatal( 'selfredirect' );
2189 return $status;
2190 }
2191 }
2192
2193 // Check for length errors again now that the section is merged in
2194 $this->contentLength = strlen( $this->toEditText( $content ) );
2195 if ( $this->contentLength > $maxArticleSize * 1024 ) {
2196 $this->tooBig = true;
2197 $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2198 return $status;
2199 }
2200
2201 $flags = EDIT_AUTOSUMMARY |
2202 ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2203 ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2204 ( $bot ? EDIT_FORCE_BOT : 0 );
2205
2206 $doEditStatus = $this->page->doEditContent(
2207 $content,
2208 $this->summary,
2209 $flags,
2210 false,
2211 $user,
2212 $content->getDefaultFormat(),
2215 );
2216
2217 if ( !$doEditStatus->isOK() ) {
2218 // Failure from doEdit()
2219 // Show the edit conflict page for certain recognized errors from doEdit(),
2220 // but don't show it for errors from extension hooks
2221 $errors = $doEditStatus->getErrorsArray();
2222 if ( in_array( $errors[0][0],
2223 [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2224 ) {
2225 $this->isConflict = true;
2226 // Destroys data doEdit() put in $status->value but who cares
2227 $doEditStatus->value = self::AS_END;
2228 }
2229 return $doEditStatus;
2230 }
2231
2232 $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2233 if ( $result['nullEdit'] ) {
2234 // We don't know if it was a null edit until now, so increment here
2235 $user->pingLimiter( 'linkpurge' );
2236 }
2237 $result['redirect'] = $content->isRedirect();
2238
2239 $this->updateWatchlist();
2240
2241 // If the content model changed, add a log entry
2242 if ( $changingContentModel ) {
2244 $user,
2245 $new ? false : $oldContentModel,
2248 );
2249 }
2250
2251 return $status;
2252 }
2253
2260 protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2261 $new = $oldModel === false;
2262 $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2263 $log->setPerformer( $user );
2264 $log->setTarget( $this->mTitle );
2265 $log->setComment( $reason );
2266 $log->setParameters( [
2267 '4::oldmodel' => $oldModel,
2268 '5::newmodel' => $newModel
2269 ] );
2270 $logid = $log->insert();
2271 $log->publish( $logid );
2272 }
2273
2277 protected function updateWatchlist() {
2278 $user = $this->context->getUser();
2279 if ( !$user->isLoggedIn() ) {
2280 return;
2281 }
2282
2284 $watch = $this->watchthis;
2285 // Do this in its own transaction to reduce contention...
2286 DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2287 if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2288 return; // nothing to change
2289 }
2291 } );
2292 }
2293
2305 private function mergeChangesIntoContent( &$editContent ) {
2306 $db = wfGetDB( DB_MASTER );
2307
2308 // This is the revision the editor started from
2309 $baseRevision = $this->getBaseRevision();
2310 $baseContent = $baseRevision ? $baseRevision->getContent() : null;
2311
2312 if ( is_null( $baseContent ) ) {
2313 return false;
2314 }
2315
2316 // The current state, we want to merge updates into it
2317 $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2318 $currentContent = $currentRevision ? $currentRevision->getContent() : null;
2319
2320 if ( is_null( $currentContent ) ) {
2321 return false;
2322 }
2323
2324 $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2325
2326 $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2327
2328 if ( $result ) {
2329 $editContent = $result;
2330 // Update parentRevId to what we just merged.
2331 $this->parentRevId = $currentRevision->getId();
2332 return true;
2333 }
2334
2335 return false;
2336 }
2337
2343 public function getBaseRevision() {
2344 if ( !$this->mBaseRevision ) {
2345 $db = wfGetDB( DB_MASTER );
2346 $this->mBaseRevision = $this->editRevId
2347 ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2348 : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2349 }
2350 return $this->mBaseRevision;
2351 }
2352
2360 public static function matchSpamRegex( $text ) {
2362 // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2363 $regexes = (array)$wgSpamRegex;
2364 return self::matchSpamRegexInternal( $text, $regexes );
2365 }
2366
2374 public static function matchSummarySpamRegex( $text ) {
2376 $regexes = (array)$wgSummarySpamRegex;
2377 return self::matchSpamRegexInternal( $text, $regexes );
2378 }
2379
2385 protected static function matchSpamRegexInternal( $text, $regexes ) {
2386 foreach ( $regexes as $regex ) {
2387 $matches = [];
2388 if ( preg_match( $regex, $text, $matches ) ) {
2389 return $matches[0];
2390 }
2391 }
2392 return false;
2393 }
2394
2395 public function setHeaders() {
2396 $out = $this->context->getOutput();
2397
2398 $out->addModules( 'mediawiki.action.edit' );
2399 $out->addModuleStyles( 'mediawiki.action.edit.styles' );
2400 $out->addModuleStyles( 'mediawiki.editfont.styles' );
2401
2402 $user = $this->context->getUser();
2403 if ( $user->getOption( 'showtoolbar' ) ) {
2404 // The addition of default buttons is handled by getEditToolbar() which
2405 // has its own dependency on this module. The call here ensures the module
2406 // is loaded in time (it has position "top") for other modules to register
2407 // buttons (e.g. extensions, gadgets, user scripts).
2408 $out->addModules( 'mediawiki.toolbar' );
2409 }
2410
2411 if ( $user->getOption( 'uselivepreview' ) ) {
2412 $out->addModules( 'mediawiki.action.edit.preview' );
2413 }
2414
2415 if ( $user->getOption( 'useeditwarning' ) ) {
2416 $out->addModules( 'mediawiki.action.edit.editWarning' );
2417 }
2418
2419 # Enabled article-related sidebar, toplinks, etc.
2420 $out->setArticleRelated( true );
2421
2422 $contextTitle = $this->getContextTitle();
2423 if ( $this->isConflict ) {
2424 $msg = 'editconflict';
2425 } elseif ( $contextTitle->exists() && $this->section != '' ) {
2426 $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2427 } else {
2428 $msg = $contextTitle->exists()
2429 || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2430 && $contextTitle->getDefaultMessageText() !== false
2431 )
2432 ? 'editing'
2433 : 'creating';
2434 }
2435
2436 # Use the title defined by DISPLAYTITLE magic word when present
2437 # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2438 # setPageTitle() treats the input as wikitext, which should be safe in either case.
2439 $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2440 if ( $displayTitle === false ) {
2441 $displayTitle = $contextTitle->getPrefixedText();
2442 }
2443 $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2444
2445 $config = $this->context->getConfig();
2446
2447 # Transmit the name of the message to JavaScript for live preview
2448 # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2449 $out->addJsConfigVars( [
2450 'wgEditMessage' => $msg,
2451 'wgAjaxEditStash' => $config->get( 'AjaxEditStash' ),
2452 ] );
2453
2454 // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2455 // editors, etc.
2456 $out->addJsConfigVars(
2457 'wgEditSubmitButtonLabelPublish',
2458 $config->get( 'EditSubmitButtonLabelPublish' )
2459 );
2460 }
2461
2465 protected function showIntro() {
2466 if ( $this->suppressIntro ) {
2467 return;
2468 }
2469
2470 $out = $this->context->getOutput();
2471 $namespace = $this->mTitle->getNamespace();
2472
2473 if ( $namespace == NS_MEDIAWIKI ) {
2474 # Show a warning if editing an interface message
2475 $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2476 # If this is a default message (but not css, json, or js),
2477 # show a hint that it is translatable on translatewiki.net
2478 if (
2479 !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2480 && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON )
2481 && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2482 ) {
2483 $defaultMessageText = $this->mTitle->getDefaultMessageText();
2484 if ( $defaultMessageText !== false ) {
2485 $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2486 'translateinterface' );
2487 }
2488 }
2489 } elseif ( $namespace == NS_FILE ) {
2490 # Show a hint to shared repo
2491 $file = wfFindFile( $this->mTitle );
2492 if ( $file && !$file->isLocal() ) {
2493 $descUrl = $file->getDescriptionUrl();
2494 # there must be a description url to show a hint to shared repo
2495 if ( $descUrl ) {
2496 if ( !$this->mTitle->exists() ) {
2497 $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2498 'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2499 ] );
2500 } else {
2501 $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2502 'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2503 ] );
2504 }
2505 }
2506 }
2507 }
2508
2509 # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2510 # Show log extract when the user is currently blocked
2511 if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2512 $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2513 $user = User::newFromName( $username, false /* allow IP users */ );
2514 $ip = User::isIP( $username );
2515 $block = Block::newFromTarget( $user, $user );
2516
2517 $userExists = ( $user && $user->isLoggedIn() );
2518 if ( $userExists && $user->isHidden() && !$this->context->getUser()->isAllowed( 'hideuser' ) ) {
2519 // If the user exists, but is hidden, and the viewer cannot see hidden
2520 // users, pretend like they don't exist at all. See T120883
2521 $userExists = false;
2522 }
2523
2524 if ( !$userExists && !$ip ) { # User does not exist
2525 $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2526 [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2527 } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
2528 # Show log extract if the user is currently blocked
2530 $out,
2531 'block',
2532 MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2533 '',
2534 [
2535 'lim' => 1,
2536 'showIfEmpty' => false,
2537 'msgKey' => [
2538 'blocked-notice-logextract',
2539 $user->getName() # Support GENDER in notice
2540 ]
2541 ]
2542 );
2543 }
2544 }
2545 # Try to add a custom edit intro, or use the standard one if this is not possible.
2546 if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2547 $helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
2548 $this->context->msg( 'helppage' )->inContentLanguage()->text()
2549 ) );
2550 if ( $this->context->getUser()->isLoggedIn() ) {
2551 $out->wrapWikiMsg(
2552 // Suppress the external link icon, consider the help url an internal one
2553 "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2554 [
2555 'newarticletext',
2556 $helpLink
2557 ]
2558 );
2559 } else {
2560 $out->wrapWikiMsg(
2561 // Suppress the external link icon, consider the help url an internal one
2562 "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2563 [
2564 'newarticletextanon',
2565 $helpLink
2566 ]
2567 );
2568 }
2569 }
2570 # Give a notice if the user is editing a deleted/moved page...
2571 if ( !$this->mTitle->exists() ) {
2572 $dbr = wfGetDB( DB_REPLICA );
2573
2574 LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2575 '',
2576 [
2577 'lim' => 10,
2578 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2579 'showIfEmpty' => false,
2580 'msgKey' => [ 'recreate-moveddeleted-warn' ]
2581 ]
2582 );
2583 }
2584 }
2585
2591 protected function showCustomIntro() {
2592 if ( $this->editintro ) {
2593 $title = Title::newFromText( $this->editintro );
2594 if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2595 // Added using template syntax, to take <noinclude>'s into account.
2596 $this->context->getOutput()->addWikiTextTitleTidy(
2597 '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2598 $this->mTitle
2599 );
2600 return true;
2601 }
2602 }
2603 return false;
2604 }
2605
2624 protected function toEditText( $content ) {
2625 if ( $content === null || $content === false || is_string( $content ) ) {
2626 return $content;
2627 }
2628
2629 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2630 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2631 }
2632
2633 return $content->serialize( $this->contentFormat );
2634 }
2635
2652 protected function toEditContent( $text ) {
2653 if ( $text === false || $text === null ) {
2654 return $text;
2655 }
2656
2657 $content = ContentHandler::makeContent( $text, $this->getTitle(),
2658 $this->contentModel, $this->contentFormat );
2659
2660 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2661 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2662 }
2663
2664 return $content;
2665 }
2666
2675 public function showEditForm( $formCallback = null ) {
2676 # need to parse the preview early so that we know which templates are used,
2677 # otherwise users with "show preview after edit box" will get a blank list
2678 # we parse this near the beginning so that setHeaders can do the title
2679 # setting work instead of leaving it in getPreviewText
2680 $previewOutput = '';
2681 if ( $this->formtype == 'preview' ) {
2682 $previewOutput = $this->getPreviewText();
2683 }
2684
2685 $out = $this->context->getOutput();
2686
2687 // Avoid PHP 7.1 warning of passing $this by reference
2688 $editPage = $this;
2689 Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$out ] );
2690
2691 $this->setHeaders();
2692
2693 $this->addTalkPageText();
2694 $this->addEditNotices();
2695
2696 if ( !$this->isConflict &&
2697 $this->section != '' &&
2698 !$this->isSectionEditSupported() ) {
2699 // We use $this->section to much before this and getVal('wgSection') directly in other places
2700 // at this point we can't reset $this->section to '' to fallback to non-section editing.
2701 // Someone is welcome to try refactoring though
2702 $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2703 return;
2704 }
2705
2706 $this->showHeader();
2707
2708 $out->addHTML( $this->editFormPageTop );
2709
2710 $user = $this->context->getUser();
2711 if ( $user->getOption( 'previewontop' ) ) {
2712 $this->displayPreviewArea( $previewOutput, true );
2713 }
2714
2715 $out->addHTML( $this->editFormTextTop );
2716
2717 $showToolbar = true;
2718 if ( $this->wasDeletedSinceLastEdit() ) {
2719 if ( $this->formtype == 'save' ) {
2720 // Hide the toolbar and edit area, user can click preview to get it back
2721 // Add an confirmation checkbox and explanation.
2722 $showToolbar = false;
2723 } else {
2724 $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2725 'deletedwhileediting' );
2726 }
2727 }
2728
2729 // @todo add EditForm plugin interface and use it here!
2730 // search for textarea1 and textarea2, and allow EditForm to override all uses.
2731 $out->addHTML( Html::openElement(
2732 'form',
2733 [
2734 'class' => 'mw-editform',
2735 'id' => self::EDITFORM_ID,
2736 'name' => self::EDITFORM_ID,
2737 'method' => 'post',
2738 'action' => $this->getActionURL( $this->getContextTitle() ),
2739 'enctype' => 'multipart/form-data'
2740 ]
2741 ) );
2742
2743 if ( is_callable( $formCallback ) ) {
2744 wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2745 call_user_func_array( $formCallback, [ &$out ] );
2746 }
2747
2748 // Add a check for Unicode support
2749 $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2750
2751 // Add an empty field to trip up spambots
2752 $out->addHTML(
2753 Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2754 . Html::rawElement(
2755 'label',
2756 [ 'for' => 'wpAntispam' ],
2757 $this->context->msg( 'simpleantispam-label' )->parse()
2758 )
2759 . Xml::element(
2760 'input',
2761 [
2762 'type' => 'text',
2763 'name' => 'wpAntispam',
2764 'id' => 'wpAntispam',
2765 'value' => ''
2766 ]
2767 )
2768 . Xml::closeElement( 'div' )
2769 );
2770
2771 // Avoid PHP 7.1 warning of passing $this by reference
2772 $editPage = $this;
2773 Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$out ] );
2774
2775 // Put these up at the top to ensure they aren't lost on early form submission
2776 $this->showFormBeforeText();
2777
2778 if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2779 $username = $this->lastDelete->user_name;
2780 $comment = CommentStore::getStore()
2781 ->getComment( 'log_comment', $this->lastDelete )->text;
2782
2783 // It is better to not parse the comment at all than to have templates expanded in the middle
2784 // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2785 $key = $comment === ''
2786 ? 'confirmrecreate-noreason'
2787 : 'confirmrecreate';
2788 $out->addHTML(
2789 '<div class="mw-confirm-recreate">' .
2790 $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2791 Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2792 [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2793 ) .
2794 '</div>'
2795 );
2796 }
2797
2798 # When the summary is hidden, also hide them on preview/show changes
2799 if ( $this->nosummary ) {
2800 $out->addHTML( Html::hidden( 'nosummary', true ) );
2801 }
2802
2803 # If a blank edit summary was previously provided, and the appropriate
2804 # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2805 # user being bounced back more than once in the event that a summary
2806 # is not required.
2807 # ####
2808 # For a bit more sophisticated detection of blank summaries, hash the
2809 # automatic one and pass that in the hidden field wpAutoSummary.
2810 if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2811 $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2812 }
2813
2814 if ( $this->undidRev ) {
2815 $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2816 }
2817
2818 if ( $this->selfRedirect ) {
2819 $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2820 }
2821
2822 if ( $this->hasPresetSummary ) {
2823 // If a summary has been preset using &summary= we don't want to prompt for
2824 // a different summary. Only prompt for a summary if the summary is blanked.
2825 // (T19416)
2826 $this->autoSumm = md5( '' );
2827 }
2828
2829 $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2830 $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2831
2832 $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2833 $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2834
2835 $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2836 $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
2837
2838 $out->enableOOUI();
2839
2840 if ( $this->section == 'new' ) {
2841 $this->showSummaryInput( true, $this->summary );
2842 $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2843 }
2844
2845 $out->addHTML( $this->editFormTextBeforeContent );
2846 if ( $this->isConflict ) {
2847 // In an edit conflict, we turn textbox2 into the user's text,
2848 // and textbox1 into the stored version
2849 $this->textbox2 = $this->textbox1;
2850
2851 $content = $this->getCurrentContent();
2852 $this->textbox1 = $this->toEditText( $content );
2853
2855 $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 );
2856 $editConflictHelper->setContentModel( $this->contentModel );
2857 $editConflictHelper->setContentFormat( $this->contentFormat );
2859 }
2860
2861 if ( !$this->mTitle->isUserConfigPage() && $showToolbar && $user->getOption( 'showtoolbar' ) ) {
2862 $out->addHTML( self::getEditToolbar( $this->mTitle ) );
2863 }
2864
2865 if ( $this->blankArticle ) {
2866 $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2867 }
2868
2869 if ( $this->isConflict ) {
2870 // In an edit conflict bypass the overridable content form method
2871 // and fallback to the raw wpTextbox1 since editconflicts can't be
2872 // resolved between page source edits and custom ui edits using the
2873 // custom edit ui.
2874 $conflictTextBoxAttribs = [];
2875 if ( $this->wasDeletedSinceLastEdit() ) {
2876 $conflictTextBoxAttribs['style'] = 'display:none;';
2877 } elseif ( $this->isOldRev ) {
2878 $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
2879 }
2880
2881 $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
2883 } else {
2884 $this->showContentForm();
2885 }
2886
2887 $out->addHTML( $this->editFormTextAfterContent );
2888
2889 $this->showStandardInputs();
2890
2891 $this->showFormAfterText();
2892
2893 $this->showTosSummary();
2894
2895 $this->showEditTools();
2896
2897 $out->addHTML( $this->editFormTextAfterTools . "\n" );
2898
2899 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
2900
2901 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2902 Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2903
2904 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2905 self::getPreviewLimitReport( $this->mParserOutput ) ) );
2906
2907 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2908
2909 if ( $this->isConflict ) {
2910 try {
2911 $this->showConflict();
2912 } catch ( MWContentSerializationException $ex ) {
2913 // this can't really happen, but be nice if it does.
2914 $msg = $this->context->msg(
2915 'content-failed-to-parse',
2916 $this->contentModel,
2917 $this->contentFormat,
2918 $ex->getMessage()
2919 );
2920 $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2921 }
2922 }
2923
2924 // Set a hidden field so JS knows what edit form mode we are in
2925 if ( $this->isConflict ) {
2926 $mode = 'conflict';
2927 } elseif ( $this->preview ) {
2928 $mode = 'preview';
2929 } elseif ( $this->diff ) {
2930 $mode = 'diff';
2931 } else {
2932 $mode = 'text';
2933 }
2934 $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
2935
2936 // Marker for detecting truncated form data. This must be the last
2937 // parameter sent in order to be of use, so do not move me.
2938 $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2939 $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2940
2941 if ( !$user->getOption( 'previewontop' ) ) {
2942 $this->displayPreviewArea( $previewOutput, false );
2943 }
2944 }
2945
2953 public function makeTemplatesOnThisPageList( array $templates ) {
2954 $templateListFormatter = new TemplatesOnThisPageFormatter(
2955 $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
2956 );
2957
2958 // preview if preview, else section if section, else false
2959 $type = false;
2960 if ( $this->preview ) {
2961 $type = 'preview';
2962 } elseif ( $this->section != '' ) {
2963 $type = 'section';
2964 }
2965
2966 return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2967 $templateListFormatter->format( $templates, $type )
2968 );
2969 }
2970
2977 public static function extractSectionTitle( $text ) {
2978 preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2979 if ( !empty( $matches[2] ) ) {
2981 return $wgParser->stripSectionName( trim( $matches[2] ) );
2982 } else {
2983 return false;
2984 }
2985 }
2986
2987 protected function showHeader() {
2988 $out = $this->context->getOutput();
2989 $user = $this->context->getUser();
2990 if ( $this->isConflict ) {
2992 $this->editRevId = $this->page->getLatest();
2993 } else {
2994 if ( $this->section != '' && $this->section != 'new' ) {
2995 if ( !$this->summary && !$this->preview && !$this->diff ) {
2996 $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2997 if ( $sectionTitle !== false ) {
2998 $this->summary = "/* $sectionTitle */ ";
2999 }
3000 }
3001 }
3002
3003 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
3004
3005 if ( $this->missingComment ) {
3006 $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
3007 }
3008
3009 if ( $this->missingSummary && $this->section != 'new' ) {
3010 $out->wrapWikiMsg(
3011 "<div id='mw-missingsummary'>\n$1\n</div>",
3012 [ 'missingsummary', $buttonLabel ]
3013 );
3014 }
3015
3016 if ( $this->missingSummary && $this->section == 'new' ) {
3017 $out->wrapWikiMsg(
3018 "<div id='mw-missingcommentheader'>\n$1\n</div>",
3019 [ 'missingcommentheader', $buttonLabel ]
3020 );
3021 }
3022
3023 if ( $this->blankArticle ) {
3024 $out->wrapWikiMsg(
3025 "<div id='mw-blankarticle'>\n$1\n</div>",
3026 [ 'blankarticle', $buttonLabel ]
3027 );
3028 }
3029
3030 if ( $this->selfRedirect ) {
3031 $out->wrapWikiMsg(
3032 "<div id='mw-selfredirect'>\n$1\n</div>",
3033 [ 'selfredirect', $buttonLabel ]
3034 );
3035 }
3036
3037 if ( $this->hookError !== '' ) {
3038 $out->addWikiText( $this->hookError );
3039 }
3040
3041 if ( $this->section != 'new' ) {
3042 $revision = $this->mArticle->getRevisionFetched();
3043 if ( $revision ) {
3044 // Let sysop know that this will make private content public if saved
3045
3046 if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) {
3047 $out->wrapWikiMsg(
3048 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3049 'rev-deleted-text-permission'
3050 );
3051 } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
3052 $out->wrapWikiMsg(
3053 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3054 'rev-deleted-text-view'
3055 );
3056 }
3057
3058 if ( !$revision->isCurrent() ) {
3059 $this->mArticle->setOldSubtitle( $revision->getId() );
3060 $out->addWikiMsg( 'editingold' );
3061 $this->isOldRev = true;
3062 }
3063 } elseif ( $this->mTitle->exists() ) {
3064 // Something went wrong
3065
3066 $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
3067 [ 'missing-revision', $this->oldid ] );
3068 }
3069 }
3070 }
3071
3072 if ( wfReadOnly() ) {
3073 $out->wrapWikiMsg(
3074 "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3075 [ 'readonlywarning', wfReadOnlyReason() ]
3076 );
3077 } elseif ( $user->isAnon() ) {
3078 if ( $this->formtype != 'preview' ) {
3079 $out->wrapWikiMsg(
3080 "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3081 [ 'anoneditwarning',
3082 // Log-in link
3083 SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
3084 'returnto' => $this->getTitle()->getPrefixedDBkey()
3085 ] ),
3086 // Sign-up link
3087 SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
3088 'returnto' => $this->getTitle()->getPrefixedDBkey()
3089 ] )
3090 ]
3091 );
3092 } else {
3093 $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3094 'anonpreviewwarning'
3095 );
3096 }
3097 } else {
3098 if ( $this->mTitle->isUserConfigPage() ) {
3099 # Check the skin exists
3100 if ( $this->isWrongCaseUserConfigPage() ) {
3101 $out->wrapWikiMsg(
3102 "<div class='error' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
3103 [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
3104 );
3105 }
3106 if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
3107 $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
3108 $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
3109 $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
3110
3111 $warning = $isUserCssConfig
3112 ? 'usercssispublic'
3113 : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' );
3114
3115 $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning );
3116
3117 if ( $this->formtype !== 'preview' ) {
3118 $config = $this->context->getConfig();
3119 if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) {
3120 $out->wrapWikiMsg(
3121 "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3122 [ 'usercssyoucanpreview' ]
3123 );
3124 } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
3125 $out->wrapWikiMsg(
3126 "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
3127 [ 'userjsonyoucanpreview' ]
3128 );
3129 } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) {
3130 $out->wrapWikiMsg(
3131 "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3132 [ 'userjsyoucanpreview' ]
3133 );
3134 }
3135 }
3136 }
3137 }
3138 }
3139
3141
3142 $this->addLongPageWarningHeader();
3143
3144 # Add header copyright warning
3146 }
3147
3155 private function getSummaryInputAttributes( array $inputAttrs = null ) {
3156 $conf = $this->context->getConfig();
3157 $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
3158 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3159 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3160 // Unicode codepoints (or 255 UTF-8 bytes for old schema).
3161 return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3162 'id' => 'wpSummary',
3163 'name' => 'wpSummary',
3164 'maxlength' => $oldCommentSchema ? 200 : CommentStore::COMMENT_CHARACTER_LIMIT,
3165 'tabindex' => 1,
3166 'size' => 60,
3167 'spellcheck' => 'true',
3168 ];
3169 }
3170
3180 function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) {
3181 $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3182 $this->getSummaryInputAttributes( $inputAttrs )
3183 );
3184 $inputAttrs += [
3185 'title' => Linker::titleAttrib( 'summary' ),
3186 'accessKey' => Linker::accesskey( 'summary' ),
3187 ];
3188
3189 // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3190 $inputAttrs['inputId'] = $inputAttrs['id'];
3191 $inputAttrs['id'] = 'wpSummaryWidget';
3192
3193 return new OOUI\FieldLayout(
3194 new OOUI\TextInputWidget( [
3195 'value' => $summary,
3196 'infusable' => true,
3197 ] + $inputAttrs ),
3198 [
3199 'label' => new OOUI\HtmlSnippet( $labelText ),
3200 'align' => 'top',
3201 'id' => 'wpSummaryLabel',
3202 'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3203 ]
3204 );
3205 }
3206
3213 protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3214 # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3215 $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3216 if ( $isSubjectPreview ) {
3217 if ( $this->nosummary ) {
3218 return;
3219 }
3220 } else {
3221 if ( !$this->mShowSummaryField ) {
3222 return;
3223 }
3224 }
3225
3226 $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3227 $this->context->getOutput()->addHTML( $this->getSummaryInputWidget(
3228 $summary,
3229 $labelText,
3230 [ 'class' => $summaryClass ]
3231 ) );
3232 }
3233
3241 protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3242 // avoid spaces in preview, gets always trimmed on save
3243 $summary = trim( $summary );
3244 if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3245 return "";
3246 }
3247
3249
3250 if ( $isSubjectPreview ) {
3251 $summary = $this->context->msg( 'newsectionsummary' )
3252 ->rawParams( $wgParser->stripSectionName( $summary ) )
3253 ->inContentLanguage()->text();
3254 }
3255
3256 $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3257
3258 $summary = $this->context->msg( $message )->parse()
3259 . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3260 return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3261 }
3262
3263 protected function showFormBeforeText() {
3264 $out = $this->context->getOutput();
3265 $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3266 $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3267 $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3268 $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3269 $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3270 }
3271
3272 protected function showFormAfterText() {
3285 $this->context->getOutput()->addHTML(
3286 "\n" .
3287 Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3288 "\n"
3289 );
3290 }
3291
3300 protected function showContentForm() {
3301 $this->showTextbox1();
3302 }
3303
3312 protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3313 if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3314 $attribs = [ 'style' => 'display:none;' ];
3315 } else {
3316 $builder = new TextboxBuilder();
3317 $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3318
3319 # Is an old revision being edited?
3320 if ( $this->isOldRev ) {
3321 $classes[] = 'mw-textarea-oldrev';
3322 }
3323
3324 $attribs = [ 'tabindex' => 1 ];
3325
3326 if ( is_array( $customAttribs ) ) {
3328 }
3329
3330 $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
3331 }
3332
3333 $this->showTextbox(
3334 $textoverride !== null ? $textoverride : $this->textbox1,
3335 'wpTextbox1',
3336 $attribs
3337 );
3338 }
3339
3340 protected function showTextbox2() {
3341 $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3342 }
3343
3344 protected function showTextbox( $text, $name, $customAttribs = [] ) {
3345 $builder = new TextboxBuilder();
3346 $attribs = $builder->buildTextboxAttribs(
3347 $name,
3349 $this->context->getUser(),
3350 $this->mTitle
3351 );
3352
3353 $this->context->getOutput()->addHTML(
3354 Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3355 );
3356 }
3357
3358 protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3359 $classes = [];
3360 if ( $isOnTop ) {
3361 $classes[] = 'ontop';
3362 }
3363
3364 $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3365
3366 if ( $this->formtype != 'preview' ) {
3367 $attribs['style'] = 'display: none;';
3368 }
3369
3370 $out = $this->context->getOutput();
3371 $out->addHTML( Xml::openElement( 'div', $attribs ) );
3372
3373 if ( $this->formtype == 'preview' ) {
3374 $this->showPreview( $previewOutput );
3375 } else {
3376 // Empty content container for LivePreview
3377 $pageViewLang = $this->mTitle->getPageViewLanguage();
3378 $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3379 'class' => 'mw-content-' . $pageViewLang->getDir() ];
3380 $out->addHTML( Html::rawElement( 'div', $attribs ) );
3381 }
3382
3383 $out->addHTML( '</div>' );
3384
3385 if ( $this->formtype == 'diff' ) {
3386 try {
3387 $this->showDiff();
3388 } catch ( MWContentSerializationException $ex ) {
3389 $msg = $this->context->msg(
3390 'content-failed-to-parse',
3391 $this->contentModel,
3392 $this->contentFormat,
3393 $ex->getMessage()
3394 );
3395 $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3396 }
3397 }
3398 }
3399
3406 protected function showPreview( $text ) {
3407 if ( $this->mArticle instanceof CategoryPage ) {
3408 $this->mArticle->openShowCategory();
3409 }
3410 # This hook seems slightly odd here, but makes things more
3411 # consistent for extensions.
3412 $out = $this->context->getOutput();
3413 Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
3414 $out->addHTML( $text );
3415 if ( $this->mArticle instanceof CategoryPage ) {
3416 $this->mArticle->closeShowCategory();
3417 }
3418 }
3419
3427 public function showDiff() {
3429
3430 $oldtitlemsg = 'currentrev';
3431 # if message does not exist, show diff against the preloaded default
3432 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3433 $oldtext = $this->mTitle->getDefaultMessageText();
3434 if ( $oldtext !== false ) {
3435 $oldtitlemsg = 'defaultmessagetext';
3436 $oldContent = $this->toEditContent( $oldtext );
3437 } else {
3438 $oldContent = null;
3439 }
3440 } else {
3441 $oldContent = $this->getCurrentContent();
3442 }
3443
3444 $textboxContent = $this->toEditContent( $this->textbox1 );
3445 if ( $this->editRevId !== null ) {
3446 $newContent = $this->page->replaceSectionAtRev(
3447 $this->section, $textboxContent, $this->summary, $this->editRevId
3448 );
3449 } else {
3450 $newContent = $this->page->replaceSectionContent(
3451 $this->section, $textboxContent, $this->summary, $this->edittime
3452 );
3453 }
3454
3455 if ( $newContent ) {
3456 Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3457
3458 $user = $this->context->getUser();
3459 $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
3460 $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
3461 }
3462
3463 if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3464 $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3465 $newtitle = $this->context->msg( 'yourtext' )->parse();
3466
3467 if ( !$oldContent ) {
3468 $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3469 }
3470
3471 if ( !$newContent ) {
3472 $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3473 }
3474
3475 $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3476 $de->setContent( $oldContent, $newContent );
3477
3478 $difftext = $de->getDiff( $oldtitle, $newtitle );
3479 $de->showDiffStyle();
3480 } else {
3481 $difftext = '';
3482 }
3483
3484 $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3485 }
3486
3490 protected function showHeaderCopyrightWarning() {
3491 $msg = 'editpage-head-copy-warn';
3492 if ( !$this->context->msg( $msg )->isDisabled() ) {
3493 $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3494 'editpage-head-copy-warn' );
3495 }
3496 }
3497
3506 protected function showTosSummary() {
3507 $msg = 'editpage-tos-summary';
3508 Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3509 if ( !$this->context->msg( $msg )->isDisabled() ) {
3510 $out = $this->context->getOutput();
3511 $out->addHTML( '<div class="mw-tos-summary">' );
3512 $out->addWikiMsg( $msg );
3513 $out->addHTML( '</div>' );
3514 }
3515 }
3516
3521 protected function showEditTools() {
3522 $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3523 $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3524 '</div>' );
3525 }
3526
3533 protected function getCopywarn() {
3534 return self::getCopyrightWarning( $this->mTitle );
3535 }
3536
3545 public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3547 if ( $wgRightsText ) {
3548 $copywarnMsg = [ 'copyrightwarning',
3549 '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3550 $wgRightsText ];
3551 } else {
3552 $copywarnMsg = [ 'copyrightwarning2',
3553 '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3554 }
3555 // Allow for site and per-namespace customization of contribution/copyright notice.
3556 Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3557
3558 $msg = call_user_func_array( 'wfMessage', $copywarnMsg )->title( $title );
3559 if ( $langcode ) {
3560 $msg->inLanguage( $langcode );
3561 }
3562 return "<div id=\"editpage-copywarn\">\n" .
3563 $msg->$format() . "\n</div>";
3564 }
3565
3573 public static function getPreviewLimitReport( $output ) {
3575
3576 if ( !$output || !$output->getLimitReportData() ) {
3577 return '';
3578 }
3579
3580 $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3581 wfMessage( 'limitreport-title' )->parseAsBlock()
3582 );
3583
3584 // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3585 $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3586
3587 $limitReport .= Html::openElement( 'table', [
3588 'class' => 'preview-limit-report wikitable'
3589 ] ) .
3590 Html::openElement( 'tbody' );
3591
3592 foreach ( $output->getLimitReportData() as $key => $value ) {
3593 if ( Hooks::run( 'ParserLimitReportFormat',
3594 [ $key, &$value, &$limitReport, true, true ]
3595 ) ) {
3596 $keyMsg = wfMessage( $key );
3597 $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3598 if ( !$valueMsg->exists() ) {
3599 $valueMsg = new RawMessage( '$1' );
3600 }
3601 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3602 $limitReport .= Html::openElement( 'tr' ) .
3603 Html::rawElement( 'th', null, $keyMsg->parse() ) .
3604 Html::rawElement( 'td', null,
3605 $wgLang->formatNum( $valueMsg->params( $value )->parse() )
3606 ) .
3607 Html::closeElement( 'tr' );
3608 }
3609 }
3610 }
3611
3612 $limitReport .= Html::closeElement( 'tbody' ) .
3613 Html::closeElement( 'table' ) .
3614 Html::closeElement( 'div' );
3615
3616 return $limitReport;
3617 }
3618
3619 protected function showStandardInputs( &$tabindex = 2 ) {
3620 $out = $this->context->getOutput();
3621 $out->addHTML( "<div class='editOptions'>\n" );
3622
3623 if ( $this->section != 'new' ) {
3624 $this->showSummaryInput( false, $this->summary );
3625 $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3626 }
3627
3628 $checkboxes = $this->getCheckboxesWidget(
3629 $tabindex,
3630 [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3631 );
3632 $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] );
3633
3634 $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3635
3636 // Show copyright warning.
3637 $out->addWikiText( $this->getCopywarn() );
3638 $out->addHTML( $this->editFormTextAfterWarn );
3639
3640 $out->addHTML( "<div class='editButtons'>\n" );
3641 $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3642
3643 $cancel = $this->getCancelLink();
3644
3645 $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3646 $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3647 $edithelp =
3648 Html::linkButton(
3649 $this->context->msg( 'edithelp' )->text(),
3650 [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3651 [ 'mw-ui-quiet' ]
3652 ) .
3653 $this->context->msg( 'word-separator' )->escaped() .
3654 $this->context->msg( 'newwindow' )->parse();
3655
3656 $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3657 $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3658 $out->addHTML( "</div><!-- editButtons -->\n" );
3659
3660 Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
3661
3662 $out->addHTML( "</div><!-- editOptions -->\n" );
3663 }
3664
3669 protected function showConflict() {
3670 $out = $this->context->getOutput();
3671 // Avoid PHP 7.1 warning of passing $this by reference
3672 $editPage = $this;
3673 if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$out ] ) ) {
3674 $this->incrementConflictStats();
3675
3676 $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3677 }
3678 }
3679
3680 protected function incrementConflictStats() {
3681 $this->getEditConflictHelper()->incrementConflictStats();
3682 }
3683
3687 public function getCancelLink() {
3688 $cancelParams = [];
3689 if ( !$this->isConflict && $this->oldid > 0 ) {
3690 $cancelParams['oldid'] = $this->oldid;
3691 } elseif ( $this->getContextTitle()->isRedirect() ) {
3692 $cancelParams['redirect'] = 'no';
3693 }
3694
3695 return new OOUI\ButtonWidget( [
3696 'id' => 'mw-editform-cancel',
3697 'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3698 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3699 'framed' => false,
3700 'infusable' => true,
3701 'flags' => 'destructive',
3702 ] );
3703 }
3704
3714 protected function getActionURL( Title $title ) {
3715 return $title->getLocalURL( [ 'action' => $this->action ] );
3716 }
3717
3725 protected function wasDeletedSinceLastEdit() {
3726 if ( $this->deletedSinceEdit !== null ) {
3728 }
3729
3730 $this->deletedSinceEdit = false;
3731
3732 if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3733 $this->lastDelete = $this->getLastDelete();
3734 if ( $this->lastDelete ) {
3735 $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3736 if ( $deleteTime > $this->starttime ) {
3737 $this->deletedSinceEdit = true;
3738 }
3739 }
3740 }
3741
3743 }
3744
3748 protected function getLastDelete() {
3749 $dbr = wfGetDB( DB_REPLICA );
3750 $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
3751 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
3752 $data = $dbr->selectRow(
3753 array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
3754 [
3755 'log_type',
3756 'log_action',
3757 'log_timestamp',
3758 'log_namespace',
3759 'log_title',
3760 'log_params',
3761 'log_deleted',
3762 'user_name'
3763 ] + $commentQuery['fields'] + $actorQuery['fields'],
3764 [
3765 'log_namespace' => $this->mTitle->getNamespace(),
3766 'log_title' => $this->mTitle->getDBkey(),
3767 'log_type' => 'delete',
3768 'log_action' => 'delete',
3769 ],
3770 __METHOD__,
3771 [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
3772 [
3773 'user' => [ 'JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
3774 ] + $commentQuery['joins'] + $actorQuery['joins']
3775 );
3776 // Quick paranoid permission checks...
3777 if ( is_object( $data ) ) {
3778 if ( $data->log_deleted & LogPage::DELETED_USER ) {
3779 $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
3780 }
3781
3782 if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3783 $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
3784 $data->log_comment_data = null;
3785 }
3786 }
3787
3788 return $data;
3789 }
3790
3796 public function getPreviewText() {
3797 $out = $this->context->getOutput();
3798 $config = $this->context->getConfig();
3799
3800 if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) {
3801 // Could be an offsite preview attempt. This is very unsafe if
3802 // HTML is enabled, as it could be an attack.
3803 $parsedNote = '';
3804 if ( $this->textbox1 !== '' ) {
3805 // Do not put big scary notice, if previewing the empty
3806 // string, which happens when you initially edit
3807 // a category page, due to automatic preview-on-open.
3808 $parsedNote = $out->parse( "<div class='previewnote'>" .
3809 $this->context->msg( 'session_fail_preview_html' )->text() . "</div>",
3810 true, /* interface */true );
3811 }
3812 $this->incrementEditFailureStats( 'session_loss' );
3813 return $parsedNote;
3814 }
3815
3816 $note = '';
3817
3818 try {
3819 $content = $this->toEditContent( $this->textbox1 );
3820
3821 $previewHTML = '';
3822 if ( !Hooks::run(
3823 'AlternateEditPreview',
3824 [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3825 ) {
3826 return $previewHTML;
3827 }
3828
3829 # provide a anchor link to the editform
3830 $continueEditing = '<span class="mw-continue-editing">' .
3831 '[[#' . self::EDITFORM_ID . '|' .
3832 $this->context->getLanguage()->getArrow() . ' ' .
3833 $this->context->msg( 'continue-editing' )->text() . ']]</span>';
3834 if ( $this->mTriedSave && !$this->mTokenOk ) {
3835 if ( $this->mTokenOkExceptSuffix ) {
3836 $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
3837 $this->incrementEditFailureStats( 'bad_token' );
3838 } else {
3839 $note = $this->context->msg( 'session_fail_preview' )->plain();
3840 $this->incrementEditFailureStats( 'session_loss' );
3841 }
3842 } elseif ( $this->incompleteForm ) {
3843 $note = $this->context->msg( 'edit_form_incomplete' )->plain();
3844 if ( $this->mTriedSave ) {
3845 $this->incrementEditFailureStats( 'incomplete_form' );
3846 }
3847 } else {
3848 $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
3849 }
3850
3851 # don't parse non-wikitext pages, show message about preview
3852 if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
3853 if ( $this->mTitle->isUserConfigPage() ) {
3854 $level = 'user';
3855 } elseif ( $this->mTitle->isSiteConfigPage() ) {
3856 $level = 'site';
3857 } else {
3858 $level = false;
3859 }
3860
3861 if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3862 $format = 'css';
3863 if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
3864 $format = false;
3865 }
3866 } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) {
3867 $format = 'json';
3868 if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
3869 $format = false;
3870 }
3871 } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3872 $format = 'js';
3873 if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
3874 $format = false;
3875 }
3876 } else {
3877 $format = false;
3878 }
3879
3880 # Used messages to make sure grep find them:
3881 # Messages: usercsspreview, userjsonpreview, userjspreview,
3882 # sitecsspreview, sitejsonpreview, sitejspreview
3883 if ( $level && $format ) {
3884 $note = "<div id='mw-{$level}{$format}preview'>" .
3885 $this->context->msg( "{$level}{$format}preview" )->text() .
3886 ' ' . $continueEditing . "</div>";
3887 }
3888 }
3889
3890 # If we're adding a comment, we need to show the
3891 # summary as the headline
3892 if ( $this->section === "new" && $this->summary !== "" ) {
3893 $content = $content->addSectionHeader( $this->summary );
3894 }
3895
3896 $hook_args = [ $this, &$content ];
3897 Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3898
3899 $parserResult = $this->doPreviewParse( $content );
3900 $parserOutput = $parserResult['parserOutput'];
3901 $previewHTML = $parserResult['html'];
3902 $this->mParserOutput = $parserOutput;
3903 $out->addParserOutputMetadata( $parserOutput );
3904
3905 if ( count( $parserOutput->getWarnings() ) ) {
3906 $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3907 }
3908
3909 } catch ( MWContentSerializationException $ex ) {
3910 $m = $this->context->msg(
3911 'content-failed-to-parse',
3912 $this->contentModel,
3913 $this->contentFormat,
3914 $ex->getMessage()
3915 );
3916 $note .= "\n\n" . $m->parse();
3917 $previewHTML = '';
3918 }
3919
3920 if ( $this->isConflict ) {
3921 $conflict = '<h2 id="mw-previewconflict">'
3922 . $this->context->msg( 'previewconflict' )->escaped() . "</h2>\n";
3923 } else {
3924 $conflict = '<hr />';
3925 }
3926
3927 $previewhead = "<div class='previewnote'>\n" .
3928 '<h2 id="mw-previewheader">' . $this->context->msg( 'preview' )->escaped() . "</h2>" .
3929 $out->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3930
3931 $pageViewLang = $this->mTitle->getPageViewLanguage();
3932 $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3933 'class' => 'mw-content-' . $pageViewLang->getDir() ];
3934 $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3935
3936 return $previewhead . $previewHTML . $this->previewTextAfterContent;
3937 }
3938
3939 private function incrementEditFailureStats( $failureType ) {
3940 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
3941 $stats->increment( 'edit.failures.' . $failureType );
3942 }
3943
3948 protected function getPreviewParserOptions() {
3949 $parserOptions = $this->page->makeParserOptions( $this->context );
3950 $parserOptions->setIsPreview( true );
3951 $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3952 $parserOptions->enableLimitReport();
3953 return $parserOptions;
3954 }
3955
3965 protected function doPreviewParse( Content $content ) {
3966 $user = $this->context->getUser();
3967 $parserOptions = $this->getPreviewParserOptions();
3968 $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
3969 $scopedCallback = $parserOptions->setupFakeRevision(
3970 $this->mTitle, $pstContent, $user );
3971 $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
3972 ScopedCallback::consume( $scopedCallback );
3973 return [
3974 'parserOutput' => $parserOutput,
3975 'html' => $parserOutput->getText( [
3976 'enableSectionEditLinks' => false
3977 ] )
3978 ];
3979 }
3980
3984 public function getTemplates() {
3985 if ( $this->preview || $this->section != '' ) {
3986 $templates = [];
3987 if ( !isset( $this->mParserOutput ) ) {
3988 return $templates;
3989 }
3990 foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
3991 foreach ( array_keys( $template ) as $dbk ) {
3992 $templates[] = Title::makeTitle( $ns, $dbk );
3993 }
3994 }
3995 return $templates;
3996 } else {
3997 return $this->mTitle->getTemplateLinksFrom();
3998 }
3999 }
4000
4008 public static function getEditToolbar( $title = null ) {
4011
4012 $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
4013 $showSignature = true;
4014 if ( $title ) {
4015 $showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
4016 }
4017
4027 $toolarray = [
4028 [
4029 'id' => 'mw-editbutton-bold',
4030 'open' => '\'\'\'',
4031 'close' => '\'\'\'',
4032 'sample' => wfMessage( 'bold_sample' )->text(),
4033 'tip' => wfMessage( 'bold_tip' )->text(),
4034 ],
4035 [
4036 'id' => 'mw-editbutton-italic',
4037 'open' => '\'\'',
4038 'close' => '\'\'',
4039 'sample' => wfMessage( 'italic_sample' )->text(),
4040 'tip' => wfMessage( 'italic_tip' )->text(),
4041 ],
4042 [
4043 'id' => 'mw-editbutton-link',
4044 'open' => '[[',
4045 'close' => ']]',
4046 'sample' => wfMessage( 'link_sample' )->text(),
4047 'tip' => wfMessage( 'link_tip' )->text(),
4048 ],
4049 [
4050 'id' => 'mw-editbutton-extlink',
4051 'open' => '[',
4052 'close' => ']',
4053 'sample' => wfMessage( 'extlink_sample' )->text(),
4054 'tip' => wfMessage( 'extlink_tip' )->text(),
4055 ],
4056 [
4057 'id' => 'mw-editbutton-headline',
4058 'open' => "\n== ",
4059 'close' => " ==\n",
4060 'sample' => wfMessage( 'headline_sample' )->text(),
4061 'tip' => wfMessage( 'headline_tip' )->text(),
4062 ],
4063 $imagesAvailable ? [
4064 'id' => 'mw-editbutton-image',
4065 'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
4066 'close' => ']]',
4067 'sample' => wfMessage( 'image_sample' )->text(),
4068 'tip' => wfMessage( 'image_tip' )->text(),
4069 ] : false,
4070 $imagesAvailable ? [
4071 'id' => 'mw-editbutton-media',
4072 'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
4073 'close' => ']]',
4074 'sample' => wfMessage( 'media_sample' )->text(),
4075 'tip' => wfMessage( 'media_tip' )->text(),
4076 ] : false,
4077 [
4078 'id' => 'mw-editbutton-nowiki',
4079 'open' => "<nowiki>",
4080 'close' => "</nowiki>",
4081 'sample' => wfMessage( 'nowiki_sample' )->text(),
4082 'tip' => wfMessage( 'nowiki_tip' )->text(),
4083 ],
4084 $showSignature ? [
4085 'id' => 'mw-editbutton-signature',
4086 'open' => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(),
4087 'close' => '',
4088 'sample' => '',
4089 'tip' => wfMessage( 'sig_tip' )->text(),
4090 ] : false,
4091 [
4092 'id' => 'mw-editbutton-hr',
4093 'open' => "\n----\n",
4094 'close' => '',
4095 'sample' => '',
4096 'tip' => wfMessage( 'hr_tip' )->text(),
4097 ]
4098 ];
4099
4100 $script = 'mw.loader.using("mediawiki.toolbar", function () {';
4101 foreach ( $toolarray as $tool ) {
4102 if ( !$tool ) {
4103 continue;
4104 }
4105
4106 $params = [
4107 // Images are defined in ResourceLoaderEditToolbarModule
4108 false,
4109 // Note that we use the tip both for the ALT tag and the TITLE tag of the image.
4110 // Older browsers show a "speedtip" type message only for ALT.
4111 // Ideally these should be different, realistically they
4112 // probably don't need to be.
4113 $tool['tip'],
4114 $tool['open'],
4115 $tool['close'],
4116 $tool['sample'],
4117 $tool['id'],
4118 ];
4119
4120 $script .= Xml::encodeJsCall(
4121 'mw.toolbar.addButton',
4122 $params,
4124 );
4125 }
4126
4127 $script .= '});';
4128
4129 $toolbar = '<div id="toolbar"></div>';
4130
4131 if ( Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
4132 // Only add the old toolbar cruft to the page payload if the toolbar has not
4133 // been over-written by a hook caller
4134 $wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
4135 };
4136
4137 return $toolbar;
4138 }
4139
4158 public function getCheckboxesDefinition( $checked ) {
4159 $checkboxes = [];
4160
4161 $user = $this->context->getUser();
4162 // don't show the minor edit checkbox if it's a new page or section
4163 if ( !$this->isNew && $user->isAllowed( 'minoredit' ) ) {
4164 $checkboxes['wpMinoredit'] = [
4165 'id' => 'wpMinoredit',
4166 'label-message' => 'minoredit',
4167 // Uses messages: tooltip-minoredit, accesskey-minoredit
4168 'tooltip' => 'minoredit',
4169 'label-id' => 'mw-editpage-minoredit',
4170 'legacy-name' => 'minor',
4171 'default' => $checked['minor'],
4172 ];
4173 }
4174
4175 if ( $user->isLoggedIn() ) {
4176 $checkboxes['wpWatchthis'] = [
4177 'id' => 'wpWatchthis',
4178 'label-message' => 'watchthis',
4179 // Uses messages: tooltip-watch, accesskey-watch
4180 'tooltip' => 'watch',
4181 'label-id' => 'mw-editpage-watch',
4182 'legacy-name' => 'watch',
4183 'default' => $checked['watch'],
4184 ];
4185 }
4186
4187 $editPage = $this;
4188 Hooks::run( 'EditPageGetCheckboxesDefinition', [ $editPage, &$checkboxes ] );
4189
4190 return $checkboxes;
4191 }
4192
4203 public function getCheckboxesWidget( &$tabindex, $checked ) {
4204 $checkboxes = [];
4205 $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4206
4207 foreach ( $checkboxesDef as $name => $options ) {
4208 $legacyName = isset( $options['legacy-name'] ) ? $options['legacy-name'] : $name;
4209
4210 $title = null;
4211 $accesskey = null;
4212 if ( isset( $options['tooltip'] ) ) {
4213 $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4214 $title = Linker::titleAttrib( $options['tooltip'] );
4215 }
4216 if ( isset( $options['title-message'] ) ) {
4217 $title = $this->context->msg( $options['title-message'] )->text();
4218 }
4219
4220 $checkboxes[ $legacyName ] = new OOUI\FieldLayout(
4221 new OOUI\CheckboxInputWidget( [
4222 'tabIndex' => ++$tabindex,
4223 'accessKey' => $accesskey,
4224 'id' => $options['id'] . 'Widget',
4225 'inputId' => $options['id'],
4226 'name' => $name,
4227 'selected' => $options['default'],
4228 'infusable' => true,
4229 ] ),
4230 [
4231 'align' => 'inline',
4232 'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4233 'title' => $title,
4234 'id' => isset( $options['label-id'] ) ? $options['label-id'] : null,
4235 ]
4236 );
4237 }
4238
4239 // Backwards-compatibility hack to run the EditPageBeforeEditChecks hook. It's important,
4240 // people have used it for the weirdest things completely unrelated to checkboxes...
4241 // And if we're gonna run it, might as well allow its legacy checkboxes to be shown.
4242 $legacyCheckboxes = [];
4243 if ( !$this->isNew ) {
4244 $legacyCheckboxes['minor'] = '';
4245 }
4246 $legacyCheckboxes['watch'] = '';
4247 // Copy new-style checkboxes into an old-style structure
4248 foreach ( $checkboxes as $name => $oouiLayout ) {
4249 $legacyCheckboxes[$name] = (string)$oouiLayout;
4250 }
4251 // Avoid PHP 7.1 warning of passing $this by reference
4252 $ep = $this;
4253 Hooks::run( 'EditPageBeforeEditChecks', [ &$ep, &$legacyCheckboxes, &$tabindex ], '1.29' );
4254 // Copy back any additional old-style checkboxes into the new-style structure
4255 foreach ( $legacyCheckboxes as $name => $html ) {
4256 if ( $html && !isset( $checkboxes[$name] ) ) {
4257 $checkboxes[$name] = new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $html ) ] );
4258 }
4259 }
4260
4261 return $checkboxes;
4262 }
4263
4270 protected function getSubmitButtonLabel() {
4271 $labelAsPublish =
4272 $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4273
4274 // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4275 $newPage = !$this->mTitle->exists();
4276
4277 if ( $labelAsPublish ) {
4278 $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4279 } else {
4280 $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4281 }
4282
4283 return $buttonLabelKey;
4284 }
4285
4294 public function getEditButtons( &$tabindex ) {
4295 $buttons = [];
4296
4297 $labelAsPublish =
4298 $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4299
4300 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4301 $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4302
4303 $buttons['save'] = new OOUI\ButtonInputWidget( [
4304 'name' => 'wpSave',
4305 'tabIndex' => ++$tabindex,
4306 'id' => 'wpSaveWidget',
4307 'inputId' => 'wpSave',
4308 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4309 'useInputTag' => true,
4310 'flags' => [ 'progressive', 'primary' ],
4311 'label' => $buttonLabel,
4312 'infusable' => true,
4313 'type' => 'submit',
4314 // Messages used: tooltip-save, tooltip-publish
4315 'title' => Linker::titleAttrib( $buttonTooltip ),
4316 // Messages used: accesskey-save, accesskey-publish
4317 'accessKey' => Linker::accesskey( $buttonTooltip ),
4318 ] );
4319
4320 $buttons['preview'] = new OOUI\ButtonInputWidget( [
4321 'name' => 'wpPreview',
4322 'tabIndex' => ++$tabindex,
4323 'id' => 'wpPreviewWidget',
4324 'inputId' => 'wpPreview',
4325 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4326 'useInputTag' => true,
4327 'label' => $this->context->msg( 'showpreview' )->text(),
4328 'infusable' => true,
4329 'type' => 'submit',
4330 // Message used: tooltip-preview
4331 'title' => Linker::titleAttrib( 'preview' ),
4332 // Message used: accesskey-preview
4333 'accessKey' => Linker::accesskey( 'preview' ),
4334 ] );
4335
4336 $buttons['diff'] = new OOUI\ButtonInputWidget( [
4337 'name' => 'wpDiff',
4338 'tabIndex' => ++$tabindex,
4339 'id' => 'wpDiffWidget',
4340 'inputId' => 'wpDiff',
4341 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4342 'useInputTag' => true,
4343 'label' => $this->context->msg( 'showdiff' )->text(),
4344 'infusable' => true,
4345 'type' => 'submit',
4346 // Message used: tooltip-diff
4347 'title' => Linker::titleAttrib( 'diff' ),
4348 // Message used: accesskey-diff
4349 'accessKey' => Linker::accesskey( 'diff' ),
4350 ] );
4351
4352 // Avoid PHP 7.1 warning of passing $this by reference
4353 $editPage = $this;
4354 Hooks::run( 'EditPageBeforeEditButtons', [ &$editPage, &$buttons, &$tabindex ] );
4355
4356 return $buttons;
4357 }
4358
4363 public function noSuchSectionPage() {
4364 $out = $this->context->getOutput();
4365 $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4366
4367 $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4368
4369 // Avoid PHP 7.1 warning of passing $this by reference
4370 $editPage = $this;
4371 Hooks::run( 'EditPageNoSuchSection', [ &$editPage, &$res ] );
4372 $out->addHTML( $res );
4373
4374 $out->returnToMain( false, $this->mTitle );
4375 }
4376
4382 public function spamPageWithContent( $match = false ) {
4383 $this->textbox2 = $this->textbox1;
4384
4385 if ( is_array( $match ) ) {
4386 $match = $this->context->getLanguage()->listToText( $match );
4387 }
4388 $out = $this->context->getOutput();
4389 $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4390
4391 $out->addHTML( '<div id="spamprotected">' );
4392 $out->addWikiMsg( 'spamprotectiontext' );
4393 if ( $match ) {
4394 $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4395 }
4396 $out->addHTML( '</div>' );
4397
4398 $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4399 $this->showDiff();
4400
4401 $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4402 $this->showTextbox2();
4403
4404 $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4405 }
4406
4417 protected function safeUnicodeInput( $request, $field ) {
4418 return rtrim( $request->getText( $field ) );
4419 }
4420
4430 protected function safeUnicodeOutput( $text ) {
4431 return $text;
4432 }
4433
4437 protected function addEditNotices() {
4438 $out = $this->context->getOutput();
4439 $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4440 if ( count( $editNotices ) ) {
4441 $out->addHTML( implode( "\n", $editNotices ) );
4442 } else {
4443 $msg = $this->context->msg( 'editnotice-notext' );
4444 if ( !$msg->isDisabled() ) {
4445 $out->addHTML(
4446 '<div class="mw-editnotice-notext">'
4447 . $msg->parseAsBlock()
4448 . '</div>'
4449 );
4450 }
4451 }
4452 }
4453
4457 protected function addTalkPageText() {
4458 if ( $this->mTitle->isTalkPage() ) {
4459 $this->context->getOutput()->addWikiMsg( 'talkpagetext' );
4460 }
4461 }
4462
4466 protected function addLongPageWarningHeader() {
4467 if ( $this->contentLength === false ) {
4468 $this->contentLength = strlen( $this->textbox1 );
4469 }
4470
4471 $out = $this->context->getOutput();
4472 $lang = $this->context->getLanguage();
4473 $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' );
4474 if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4475 $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4476 [
4477 'longpageerror',
4478 $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4479 $lang->formatNum( $maxArticleSize )
4480 ]
4481 );
4482 } else {
4483 if ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
4484 $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
4485 [
4486 'longpage-hint',
4487 $lang->formatSize( strlen( $this->textbox1 ) ),
4488 strlen( $this->textbox1 )
4489 ]
4490 );
4491 }
4492 }
4493 }
4494
4498 protected function addPageProtectionWarningHeaders() {
4499 $out = $this->context->getOutput();
4500 if ( $this->mTitle->isProtected( 'edit' ) &&
4501 MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
4502 ) {
4503 # Is the title semi-protected?
4504 if ( $this->mTitle->isSemiProtected() ) {
4505 $noticeMsg = 'semiprotectedpagewarning';
4506 } else {
4507 # Then it must be protected based on static groups (regular)
4508 $noticeMsg = 'protectedpagewarning';
4509 }
4510 LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4511 [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4512 }
4513 if ( $this->mTitle->isCascadeProtected() ) {
4514 # Is this page under cascading protection from some source pages?
4516 list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4517 $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
4518 $cascadeSourcesCount = count( $cascadeSources );
4519 if ( $cascadeSourcesCount > 0 ) {
4520 # Explain, and list the titles responsible
4521 foreach ( $cascadeSources as $page ) {
4522 $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4523 }
4524 }
4525 $notice .= '</div>';
4526 $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4527 }
4528 if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4529 LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4530 [ 'lim' => 1,
4531 'showIfEmpty' => false,
4532 'msgKey' => [ 'titleprotectedwarning' ],
4533 'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4534 }
4535 }
4536
4542 $out->addHTML(
4543 $this->getEditConflictHelper()->getExplainHeader()
4544 );
4545 }
4546
4554 protected function buildTextboxAttribs( $name, array $customAttribs, User $user ) {
4555 return ( new TextboxBuilder() )->buildTextboxAttribs(
4556 $name, $customAttribs, $user, $this->mTitle
4557 );
4558 }
4559
4565 protected function addNewLineAtEnd( $wikitext ) {
4566 return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext );
4567 }
4568
4579 private function guessSectionName( $text ) {
4581
4582 // Detect Microsoft browsers
4583 $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
4584 if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
4585 // ...and redirect them to legacy encoding, if available
4586 return $wgParser->guessLegacySectionNameFromWikiText( $text );
4587 }
4588 // Meanwhile, real browsers get real anchors
4589 $name = $wgParser->guessSectionNameFromWikiText( $text );
4590 // With one little caveat: per T216029, fragments in HTTP redirects need to be urlencoded,
4591 // otherwise Chrome double-escapes the rest of the URL.
4592 return '#' . urlencode( mb_substr( $name, 1 ) );
4593 }
4594
4601 public function setEditConflictHelperFactory( callable $factory ) {
4602 $this->editConflictHelperFactory = $factory;
4603 $this->editConflictHelper = null;
4604 }
4605
4609 private function getEditConflictHelper() {
4610 if ( !$this->editConflictHelper ) {
4611 $this->editConflictHelper = call_user_func(
4612 $this->editConflictHelperFactory,
4613 $this->getSubmitButtonLabel()
4614 );
4615 }
4616
4618 }
4619
4624 private function newTextConflictHelper( $submitButtonLabel ) {
4625 return new TextConflictHelper(
4626 $this->getTitle(),
4627 $this->getContext()->getOutput(),
4628 MediaWikiServices::getInstance()->getStatsdDataFactory(),
4629 $submitButtonLabel
4630 );
4631 }
4632}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
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...
$wgEnableUploads
Uploads have to be specially set up to be secure.
$wgForeignFileRepos
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.
wfGetAllCallers( $limit=3)
Return a string consisting of callers in the stack.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
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.
$wgOut
Definition Setup.php:912
$wgParser
Definition Setup.php:917
if(! $wgRequest->checkUrlExtension()) if(isset($_SERVER[ 'PATH_INFO']) &&$_SERVER[ 'PATH_INFO'] !='') if(! $wgEnableAPI) $wgTitle
Definition api.php:68
Class for viewing MediaWiki article and history.
Definition Article.php:35
const TYPE_AUTO
Definition Block.php:86
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:1173
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...
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:352
getActionURL(Title $title)
Returns the URL to use in the form's action attribute.
getEditConflictHelper()
isOouiEnabled()
Check if the edit page is using OOUI controls.
Definition EditPage.php:512
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:254
showTextbox( $text, $name, $customAttribs=[])
string $hookError
Definition EditPage.php:296
attemptSave(&$resultDetails=false)
Attempt submission.
$editFormTextAfterTools
Definition EditPage.php:389
isSectionEditSupported()
Returns whether section editing is supported for the current page.
Definition EditPage.php:870
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.
static getPreviewLimitReport( $output)
Get the Limit report for page previews.
getCancelLink()
null string $contentFormat
Definition EditPage.php:376
bool $allowBlankSummary
Definition EditPage.php:278
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:725
bool $firsttime
Definition EditPage.php:251
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:370
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:334
bool $tooBig
Definition EditPage.php:269
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:524
$editFormTextAfterContent
Definition EditPage.php:391
$editFormTextBottom
Definition EditPage.php:390
int $editRevId
Definition EditPage.php:346
$editFormTextBeforeContent
Definition EditPage.php:387
string $contentModel
Definition EditPage.php:373
bool $deletedSinceEdit
Definition EditPage.php:245
__construct(Article $article)
Definition EditPage.php:442
getEditPermissionErrors( $rigor='secure')
Definition EditPage.php:681
showStandardInputs(&$tabindex=2)
toEditContent( $text)
Turns the given text into a Content object by unserializing it.
int $oldid
Definition EditPage.php:358
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:388
bool $mTokenOk
Definition EditPage.php:257
getSummaryInputAttributes(array $inputAttrs=null)
Helper function for summary input functions, which returns the neccessary 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:425
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:308
showFormAfterText()
bool $recreate
Definition EditPage.php:328
callable $editConflictHelperFactory
Factory function to create an edit conflict helper.
Definition EditPage.php:432
int $parentRevId
Definition EditPage.php:361
isWrongCaseUserConfigPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition EditPage.php:849
initialiseForm()
Initialise form fields in the object Called on the first invocation, e.g.
bool $hasPresetSummary
Has a summary been preset using GET parameter &summary= ?
Definition EditPage.php:302
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:880
static extractSectionTitle( $text)
Extract the section title from current section text, if any.
bool $missingSummary
Definition EditPage.php:275
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
Definition EditPage.php:305
bool int $contentLength
Definition EditPage.php:405
const AS_PARSE_ERROR
Status: can't parse content.
Definition EditPage.php:179
bool $blankArticle
Definition EditPage.php:281
noSuchSectionPage()
Creates a basic error page which informs the user that they have attempted to edit a nonexistent sect...
bool $mTriedSave
Definition EditPage.php:263
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:349
ParserOutput $mParserOutput
Definition EditPage.php:299
previewOnOpen()
Should we show a preview when the edit form is first shown?
Definition EditPage.php:808
displayPreviewArea( $previewOutput, $isOnTop=false)
doPreviewParse(Content $content)
Parse the page for a preview.
string $formtype
Definition EditPage.php:248
bool $allowBlankArticle
Definition EditPage.php:284
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing.
Definition EditPage.php:535
internalAttemptSave(&$result, $bot=false)
Attempt submission (no UI)
string $summary
Definition EditPage.php:337
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:313
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition EditPage.php:78
bool $diff
Definition EditPage.php:319
getCurrentContent()
Get the current content of the page.
bool $selfRedirect
Definition EditPage.php:287
string $textbox1
Definition EditPage.php:331
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:385
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:494
toEditText( $content)
Gets an editable textual representation of $content.
null Title $mContextTitle
Definition EditPage.php:233
string $autoSumm
Definition EditPage.php:293
bool $isOldRev
Whether an old revision is edited.
Definition EditPage.php:420
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:260
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()
string $starttime
Definition EditPage.php:355
bool $isNew
New page or new section.
Definition EditPage.php:242
showIntro()
Show all applicable editing introductions.
static getEditToolbar( $title=null)
Shows a bulletin board style toolbar for common editing functions.
Article $mArticle
Definition EditPage.php:222
bool $allowSelfRedirect
Definition EditPage.php:290
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
Definition EditPage.php:239
null $scrolltop
Definition EditPage.php:367
getLastDelete()
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:558
string $editintro
Definition EditPage.php:364
newTextConflictHelper( $submitButtonLabel)
bool $enableApiEditOverride
Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing.
Definition EditPage.php:410
displayViewSourcePage(Content $content, $errorMessage='')
Display a read-only View Source page.
Definition EditPage.php:755
bool $watchthis
Definition EditPage.php:325
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:322
$previewTextAfterContent
Definition EditPage.php:392
getSummaryPreview( $isSubjectPreview, $summary="")
bool $nosummary
Definition EditPage.php:340
bool $incompleteForm
Definition EditPage.php:266
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:272
string $edittime
Definition EditPage.php:343
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:402
incrementConflictStats()
IContextSource $context
Definition EditPage.php:415
bool $preview
Definition EditPage.php:316
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:437
showEditForm( $formCallback=null)
Send the edit form and related headers to OutputPage.
null array $changeTags
Definition EditPage.php:379
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:483
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:1969
static accesskey( $name)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition Linker.php:2017
static commentBlock( $comment, $title=null, $local=false, $wikiId=null)
Wrap a comment in standard punctuation and formatting if it's non-empty, otherwise return empty strin...
Definition Linker.php:1455
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition Linker.php:1917
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
const DELETED_USER
Definition LogPage.php:34
const DELETED_COMMENT
Definition LogPage.php:33
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 log entries manually, to inject them into the database.
Definition LogEntry.php:432
setPerformer(User $performer)
Set the user that performed the action being logged.
Definition LogEntry.php:527
Helper for displaying edit conflicts in text content models to users.
getEditConflictMainTextBox( $customAttribs=[])
HTML to build the textbox1 on edit conflicts.
getEditFormHtmlAfterContent()
Content to go in the edit form after textbox1.
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 inDebugMode()
Determine whether debug mode was requested Order of priority is 1) request param, 2) cookie,...
static makeInlineScript( $script)
Returns an HTML script tag that runs given JS code after startup and base modules.
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:39
setContentModel( $model)
Set a proposed content model for the page for permissions checking.
Definition Title.php:1018
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:53
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:591
const IGNORE_USER_RIGHTS
Definition User.php:90
static isIP( $name)
Does the string match an anonymous IP address?
Definition User.php:943
static doWatchOrUnwatch( $watch, Title $title, User $user)
Watch or unwatch a page.
Class representing a MediaWiki article and history.
Definition WikiPage.php:37
getRedirectTarget()
If this page is a redirect, get its target.
Definition WikiPage.php:887
getContent( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition WikiPage.php:718
isRedirect()
Tests if the article content represents a redirect.
Definition WikiPage.php:544
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
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add etc
Definition design.txt:19
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition design.txt:57
when a variable name is used in a it is silently declared as a new local masking the global
Definition design.txt:95
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as $wgLang
Definition design.txt:56
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
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:166
const EDIT_UPDATE
Definition Defines.php:163
const NS_USER
Definition Defines.php:76
const CONTENT_MODEL_CSS
Definition Defines.php:247
const NS_FILE
Definition Defines.php:80
const NS_MEDIAWIKI
Definition Defines.php:82
const CONTENT_MODEL_JSON
Definition Defines.php:249
const NS_MEDIA
Definition Defines.php:62
const NS_USER_TALK
Definition Defines.php:77
const MIGRATION_OLD
Definition Defines.php:302
const EDIT_MINOR
Definition Defines.php:164
const CONTENT_MODEL_JAVASCRIPT
Definition Defines.php:246
const EDIT_AUTOSUMMARY
Definition Defines.php:168
const EDIT_NEW
Definition Defines.php:162
the array() calling protocol came about after MediaWiki 1.4rc1.
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:2806
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. 'LanguageGetMagic':DEPRECATED! Use $magicWords in a file listed in $wgExtensionMessagesFiles instead. Use this to define synonyms of magic words depending of the language & $magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED! Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead. Use to define aliases of special pages names depending of the language & $specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links: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! 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:1993
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set but before ordinary actions take place $output
Definition hooks.txt:2255
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition hooks.txt:181
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping $template
Definition hooks.txt:831
also included in $newHeader if any indicating whether we should show just the diff
Definition hooks.txt:1259
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:2001
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping 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:865
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function array $article
Definition hooks.txt:77
null means default & $customAttribs
Definition hooks.txt:1995
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:964
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 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
null for the local wiki Added in
Definition hooks.txt:1591
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition hooks.txt:2006
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:1432
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping 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:864
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 & $html
Definition hooks.txt:2013
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:785
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. '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:1255
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
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:2014
this hook is for auditing only $response
Definition hooks.txt:783
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping 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:903
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:1620
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:1777
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
returning false will NOT prevent logging $e
Definition hooks.txt:2176
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
preSaveTransform(Title $title, User $user, ParserOptions $parserOptions)
Returns a Content object with pre-save transformations applied (or this object if no transformations ...
serialize( $format=null)
Convenience method for serializing this Content object.
Interface for objects which can provide a MediaWiki context on request.
The First
Definition primes.txt:1
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:29
$params
if(!isset( $args[0])) $lang