MediaWiki REL1_29
EditPage.php
Go to the documentation of this file.
1<?php
25use Wikimedia\ScopedCallback;
26
42class EditPage {
46 const AS_SUCCESS_UPDATE = 200;
47
52
56 const AS_HOOK_ERROR = 210;
57
62
67
71 const AS_CONTENT_TOO_BIG = 216;
72
77
82
86 const AS_READ_ONLY_PAGE = 220;
87
91 const AS_RATE_LIMITED = 221;
92
98
104
108 const AS_BLANK_ARTICLE = 224;
109
114
119 const AS_SUMMARY_NEEDED = 226;
120
124 const AS_TEXTBOX_EMPTY = 228;
125
130
134 const AS_END = 231;
135
139 const AS_SPAM_ERROR = 232;
140
145
150
156
161 const AS_SELF_REDIRECT = 236;
162
168
172 const AS_PARSE_ERROR = 240;
173
179
183 const EDITFORM_ID = 'editform';
184
189 const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
190
205
207 public $mArticle;
209 private $page;
210
212 public $mTitle;
213
215 private $mContextTitle = null;
216
218 public $action = 'submit';
219
221 public $isConflict = false;
222
224 public $isCssJsSubpage = false;
225
227 public $isCssSubpage = false;
228
230 public $isJsSubpage = false;
231
233 public $isWrongCaseCssJsPage = false;
234
236 public $isNew = false;
237
240
242 public $formtype;
243
246
249
251 public $mTokenOk = false;
252
254 public $mTokenOkExceptSuffix = false;
255
257 public $mTriedSave = false;
258
260 public $incompleteForm = false;
261
263 public $tooBig = false;
264
266 public $missingComment = false;
267
269 public $missingSummary = false;
270
272 public $allowBlankSummary = false;
273
275 protected $blankArticle = false;
276
278 protected $allowBlankArticle = false;
279
281 protected $selfRedirect = false;
282
284 protected $allowSelfRedirect = false;
285
287 public $autoSumm = '';
288
290 public $hookError = '';
291
294
296 public $hasPresetSummary = false;
297
299 public $mBaseRevision = false;
300
302 public $mShowSummaryField = true;
303
304 # Form values
305
307 public $save = false;
308
310 public $preview = false;
311
313 public $diff = false;
314
316 public $minoredit = false;
317
319 public $watchthis = false;
320
322 public $recreate = false;
323
325 public $textbox1 = '';
326
328 public $textbox2 = '';
329
331 public $summary = '';
332
334 public $nosummary = false;
335
337 public $edittime = '';
338
340 private $editRevId = null;
341
343 public $section = '';
344
346 public $sectiontitle = '';
347
349 public $starttime = '';
350
352 public $oldid = 0;
353
355 public $parentRevId = 0;
356
358 public $editintro = '';
359
361 public $scrolltop = null;
362
364 public $bot = true;
365
368
370 public $contentFormat = null;
371
373 private $changeTags = null;
374
375 # Placeholders for text injection by hooks (must be HTML)
376 # extensions should take care to _append_ to the present value
377
379 public $editFormPageTop = '';
380 public $editFormTextTop = '';
387 public $mPreloadContent = null;
388
389 /* $didSave should be set to true whenever an article was successfully altered. */
390 public $didSave = false;
391 public $undidRev = 0;
392
393 public $suppressIntro = false;
394
396 protected $edit;
397
399 protected $contentLength = false;
400
404 private $enableApiEditOverride = false;
405
409 protected $context;
410
414 private $isOldRev = false;
415
419 private $oouiEnabled = false;
420
424 public function __construct( Article $article ) {
426
427 $this->mArticle = $article;
428 $this->page = $article->getPage(); // model object
429 $this->mTitle = $article->getTitle();
430 $this->context = $article->getContext();
431
432 $this->contentModel = $this->mTitle->getContentModel();
433
434 $handler = ContentHandler::getForModelID( $this->contentModel );
435 $this->contentFormat = $handler->getDefaultFormat();
436
437 $this->oouiEnabled = $wgOOUIEditPage;
438 }
439
443 public function getArticle() {
444 return $this->mArticle;
445 }
446
451 public function getContext() {
452 return $this->context;
453 }
454
459 public function getTitle() {
460 return $this->mTitle;
461 }
462
468 public function setContextTitle( $title ) {
469 $this->mContextTitle = $title;
470 }
471
479 public function getContextTitle() {
480 if ( is_null( $this->mContextTitle ) ) {
482 return $wgTitle;
483 } else {
485 }
486 }
487
492 public function isOouiEnabled() {
493 return $this->oouiEnabled;
494 }
495
503 public function isSupportedContentModel( $modelId ) {
504 return $this->enableApiEditOverride === true ||
505 ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
506 }
507
514 public function setApiEditOverride( $enableOverride ) {
515 $this->enableApiEditOverride = $enableOverride;
516 }
517
521 public function submit() {
522 $this->edit();
523 }
524
536 public function edit() {
538 // Allow extensions to modify/prevent this form or submission
539 if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
540 return;
541 }
542
543 wfDebug( __METHOD__ . ": enter\n" );
544
545 // If they used redlink=1 and the page exists, redirect to the main article
546 if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
547 $wgOut->redirect( $this->mTitle->getFullURL() );
548 return;
549 }
550
551 $this->importFormData( $wgRequest );
552 $this->firsttime = false;
553
554 if ( wfReadOnly() && $this->save ) {
555 // Force preview
556 $this->save = false;
557 $this->preview = true;
558 }
559
560 if ( $this->save ) {
561 $this->formtype = 'save';
562 } elseif ( $this->preview ) {
563 $this->formtype = 'preview';
564 } elseif ( $this->diff ) {
565 $this->formtype = 'diff';
566 } else { # First time through
567 $this->firsttime = true;
568 if ( $this->previewOnOpen() ) {
569 $this->formtype = 'preview';
570 } else {
571 $this->formtype = 'initial';
572 }
573 }
574
575 $permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
576 if ( $permErrors ) {
577 wfDebug( __METHOD__ . ": User can't edit\n" );
578 // Auto-block user's IP if the account was "hard" blocked
579 if ( !wfReadOnly() ) {
580 $user = $wgUser;
581 DeferredUpdates::addCallableUpdate( function () use ( $user ) {
582 $user->spreadAnyEditBlock();
583 } );
584 }
585 $this->displayPermissionsError( $permErrors );
586
587 return;
588 }
589
590 $revision = $this->mArticle->getRevisionFetched();
591 // Disallow editing revisions with content models different from the current one
592 // Undo edits being an exception in order to allow reverting content model changes.
593 if ( $revision
594 && $revision->getContentModel() !== $this->contentModel
595 ) {
596 $prevRev = null;
597 if ( $this->undidRev ) {
598 $undidRevObj = Revision::newFromId( $this->undidRev );
599 $prevRev = $undidRevObj ? $undidRevObj->getPrevious() : null;
600 }
601 if ( !$this->undidRev
602 || !$prevRev
603 || $prevRev->getContentModel() !== $this->contentModel
604 ) {
606 $this->getContentObject(),
607 $this->context->msg(
608 'contentmodelediterror',
609 $revision->getContentModel(),
610 $this->contentModel
611 )->plain()
612 );
613 return;
614 }
615 }
616
617 $this->isConflict = false;
618 // css / js subpages of user pages get a special treatment
619 $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
620 $this->isCssSubpage = $this->mTitle->isCssSubpage();
621 $this->isJsSubpage = $this->mTitle->isJsSubpage();
622 // @todo FIXME: Silly assignment.
624
625 # Show applicable editing introductions
626 if ( $this->formtype == 'initial' || $this->firsttime ) {
627 $this->showIntro();
628 }
629
630 # Attempt submission here. This will check for edit conflicts,
631 # and redundantly check for locked database, blocked IPs, etc.
632 # that edit() already checked just in case someone tries to sneak
633 # in the back door with a hand-edited submission URL.
634
635 if ( 'save' == $this->formtype ) {
636 $resultDetails = null;
637 $status = $this->attemptSave( $resultDetails );
638 if ( !$this->handleStatus( $status, $resultDetails ) ) {
639 return;
640 }
641 }
642
643 # First time through: get contents, set time for conflict
644 # checking, etc.
645 if ( 'initial' == $this->formtype || $this->firsttime ) {
646 if ( $this->initialiseForm() === false ) {
647 $this->noSuchSectionPage();
648 return;
649 }
650
651 if ( !$this->mTitle->getArticleID() ) {
652 Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
653 } else {
654 Hooks::run( 'EditFormInitialText', [ $this ] );
655 }
656
657 }
658
659 $this->showEditForm();
660 }
661
666 protected function getEditPermissionErrors( $rigor = 'secure' ) {
668
669 $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser, $rigor );
670 # Can this title be created?
671 if ( !$this->mTitle->exists() ) {
672 $permErrors = array_merge(
673 $permErrors,
675 $this->mTitle->getUserPermissionsErrors( 'create', $wgUser, $rigor ),
676 $permErrors
677 )
678 );
679 }
680 # Ignore some permissions errors when a user is just previewing/viewing diffs
681 $remove = [];
682 foreach ( $permErrors as $error ) {
683 if ( ( $this->preview || $this->diff )
684 && (
685 $error[0] == 'blockedtext' ||
686 $error[0] == 'autoblockedtext' ||
687 $error[0] == 'systemblockedtext'
688 )
689 ) {
690 $remove[] = $error;
691 }
692 }
693 $permErrors = wfArrayDiff2( $permErrors, $remove );
694
695 return $permErrors;
696 }
697
711 protected function displayPermissionsError( array $permErrors ) {
713
714 if ( $wgRequest->getBool( 'redlink' ) ) {
715 // The edit page was reached via a red link.
716 // Redirect to the article page and let them click the edit tab if
717 // they really want a permission error.
718 $wgOut->redirect( $this->mTitle->getFullURL() );
719 return;
720 }
721
722 $content = $this->getContentObject();
723
724 # Use the normal message if there's nothing to display
725 if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
726 $action = $this->mTitle->exists() ? 'edit' :
727 ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
728 throw new PermissionsError( $action, $permErrors );
729 }
730
732 $content,
733 $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' )
734 );
735 }
736
742 protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
744
745 Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$wgOut ] );
746
747 $wgOut->setRobotPolicy( 'noindex,nofollow' );
748 $wgOut->setPageTitle( $this->context->msg(
749 'viewsource-title',
750 $this->getContextTitle()->getPrefixedText()
751 ) );
752 $wgOut->addBacklinkSubtitle( $this->getContextTitle() );
753 $wgOut->addHTML( $this->editFormPageTop );
754 $wgOut->addHTML( $this->editFormTextTop );
755
756 if ( $errorMessage !== '' ) {
757 $wgOut->addWikiText( $errorMessage );
758 $wgOut->addHTML( "<hr />\n" );
759 }
760
761 # If the user made changes, preserve them when showing the markup
762 # (This happens when a user is blocked during edit, for instance)
763 if ( !$this->firsttime ) {
764 $text = $this->textbox1;
765 $wgOut->addWikiMsg( 'viewyourtext' );
766 } else {
767 try {
768 $text = $this->toEditText( $content );
769 } catch ( MWException $e ) {
770 # Serialize using the default format if the content model is not supported
771 # (e.g. for an old revision with a different model)
772 $text = $content->serialize();
773 }
774 $wgOut->addWikiMsg( 'viewsourcetext' );
775 }
776
777 $wgOut->addHTML( $this->editFormTextBeforeContent );
778 $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
779 $wgOut->addHTML( $this->editFormTextAfterContent );
780
781 $wgOut->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
782
783 $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
784
785 $wgOut->addHTML( $this->editFormTextBottom );
786 if ( $this->mTitle->exists() ) {
787 $wgOut->returnToMain( null, $this->mTitle );
788 }
789 }
790
796 protected function previewOnOpen() {
798 if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
799 // Explicit override from request
800 return true;
801 } elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) {
802 // Explicit override from request
803 return false;
804 } elseif ( $this->section == 'new' ) {
805 // Nothing *to* preview for new sections
806 return false;
807 } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() )
808 && $wgUser->getOption( 'previewonfirst' )
809 ) {
810 // Standard preference behavior
811 return true;
812 } elseif ( !$this->mTitle->exists()
813 && isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] )
814 && $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()]
815 ) {
816 // Categories are special
817 return true;
818 } else {
819 return false;
820 }
821 }
822
829 protected function isWrongCaseCssJsPage() {
830 if ( $this->mTitle->isCssJsSubpage() ) {
831 $name = $this->mTitle->getSkinFromCssJsSubpage();
832 $skins = array_merge(
833 array_keys( Skin::getSkinNames() ),
834 [ 'common' ]
835 );
836 return !in_array( $name, $skins )
837 && in_array( strtolower( $name ), $skins );
838 } else {
839 return false;
840 }
841 }
842
850 protected function isSectionEditSupported() {
851 $contentHandler = ContentHandler::getForTitle( $this->mTitle );
852 return $contentHandler->supportsSections();
853 }
854
860 public function importFormData( &$request ) {
862
863 # Allow users to change the mode for testing
864 $this->oouiEnabled = $request->getFuzzyBool( 'ooui', $this->oouiEnabled );
865
866 # Section edit can come from either the form or a link
867 $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
868
869 if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
870 throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
871 }
872
873 $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
874
875 if ( $request->wasPosted() ) {
876 # These fields need to be checked for encoding.
877 # Also remove trailing whitespace, but don't remove _initial_
878 # whitespace from the text boxes. This may be significant formatting.
879 $this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' );
880 if ( !$request->getCheck( 'wpTextbox2' ) ) {
881 // Skip this if wpTextbox2 has input, it indicates that we came
882 // from a conflict page with raw page text, not a custom form
883 // modified by subclasses
885 if ( $textbox1 !== null ) {
886 $this->textbox1 = $textbox1;
887 }
888 }
889
890 # Truncate for whole multibyte characters
891 $this->summary = $wgContLang->truncate( $request->getText( 'wpSummary' ), 255 );
892
893 # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
894 # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
895 # section titles.
896 $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
897
898 # Treat sectiontitle the same way as summary.
899 # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
900 # currently doing double duty as both edit summary and section title. Right now this
901 # is just to allow API edits to work around this limitation, but this should be
902 # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312).
903 $this->sectiontitle = $wgContLang->truncate( $request->getText( 'wpSectionTitle' ), 255 );
904 $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
905
906 $this->edittime = $request->getVal( 'wpEdittime' );
907 $this->editRevId = $request->getIntOrNull( 'editRevId' );
908 $this->starttime = $request->getVal( 'wpStarttime' );
909
910 $undidRev = $request->getInt( 'wpUndidRevision' );
911 if ( $undidRev ) {
912 $this->undidRev = $undidRev;
913 }
914
915 $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
916
917 if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
918 // wpTextbox1 field is missing, possibly due to being "too big"
919 // according to some filter rules such as Suhosin's setting for
920 // suhosin.request.max_value_length (d'oh)
921 $this->incompleteForm = true;
922 } else {
923 // If we receive the last parameter of the request, we can fairly
924 // claim the POST request has not been truncated.
925
926 // TODO: softened the check for cutover. Once we determine
927 // that it is safe, we should complete the transition by
928 // removing the "edittime" clause.
929 $this->incompleteForm = ( !$request->getVal( 'wpUltimateParam' )
930 && is_null( $this->edittime ) );
931 }
932 if ( $this->incompleteForm ) {
933 # If the form is incomplete, force to preview.
934 wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
935 wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" );
936 $this->preview = true;
937 } else {
938 $this->preview = $request->getCheck( 'wpPreview' );
939 $this->diff = $request->getCheck( 'wpDiff' );
940
941 // Remember whether a save was requested, so we can indicate
942 // if we forced preview due to session failure.
943 $this->mTriedSave = !$this->preview;
944
945 if ( $this->tokenOk( $request ) ) {
946 # Some browsers will not report any submit button
947 # if the user hits enter in the comment box.
948 # The unmarked state will be assumed to be a save,
949 # if the form seems otherwise complete.
950 wfDebug( __METHOD__ . ": Passed token check.\n" );
951 } elseif ( $this->diff ) {
952 # Failed token check, but only requested "Show Changes".
953 wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
954 } else {
955 # Page might be a hack attempt posted from
956 # an external site. Preview instead of saving.
957 wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
958 $this->preview = true;
959 }
960 }
961 $this->save = !$this->preview && !$this->diff;
962 if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
963 $this->edittime = null;
964 }
965
966 if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
967 $this->starttime = null;
968 }
969
970 $this->recreate = $request->getCheck( 'wpRecreate' );
971
972 $this->minoredit = $request->getCheck( 'wpMinoredit' );
973 $this->watchthis = $request->getCheck( 'wpWatchthis' );
974
975 # Don't force edit summaries when a user is editing their own user or talk page
976 if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
977 && $this->mTitle->getText() == $wgUser->getName()
978 ) {
979 $this->allowBlankSummary = true;
980 } else {
981 $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
982 || !$wgUser->getOption( 'forceeditsummary' );
983 }
984
985 $this->autoSumm = $request->getText( 'wpAutoSummary' );
986
987 $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
988 $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
989
990 $changeTags = $request->getVal( 'wpChangeTags' );
991 if ( is_null( $changeTags ) || $changeTags === '' ) {
992 $this->changeTags = [];
993 } else {
994 $this->changeTags = array_filter( array_map( 'trim', explode( ',',
995 $changeTags ) ) );
996 }
997 } else {
998 # Not a posted form? Start with nothing.
999 wfDebug( __METHOD__ . ": Not a posted form.\n" );
1000 $this->textbox1 = '';
1001 $this->summary = '';
1002 $this->sectiontitle = '';
1003 $this->edittime = '';
1004 $this->editRevId = null;
1005 $this->starttime = wfTimestampNow();
1006 $this->edit = false;
1007 $this->preview = false;
1008 $this->save = false;
1009 $this->diff = false;
1010 $this->minoredit = false;
1011 // Watch may be overridden by request parameters
1012 $this->watchthis = $request->getBool( 'watchthis', false );
1013 $this->recreate = false;
1014
1015 // When creating a new section, we can preload a section title by passing it as the
1016 // preloadtitle parameter in the URL (T15100)
1017 if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1018 $this->sectiontitle = $request->getVal( 'preloadtitle' );
1019 // Once wpSummary isn't being use for setting section titles, we should delete this.
1020 $this->summary = $request->getVal( 'preloadtitle' );
1021 } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) {
1022 $this->summary = $request->getText( 'summary' );
1023 if ( $this->summary !== '' ) {
1024 $this->hasPresetSummary = true;
1025 }
1026 }
1027
1028 if ( $request->getVal( 'minor' ) ) {
1029 $this->minoredit = true;
1030 }
1031 }
1032
1033 $this->oldid = $request->getInt( 'oldid' );
1034 $this->parentRevId = $request->getInt( 'parentRevId' );
1035
1036 $this->bot = $request->getBool( 'bot', true );
1037 $this->nosummary = $request->getBool( 'nosummary' );
1038
1039 // May be overridden by revision.
1040 $this->contentModel = $request->getText( 'model', $this->contentModel );
1041 // May be overridden by revision.
1042 $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1043
1044 try {
1045 $handler = ContentHandler::getForModelID( $this->contentModel );
1047 throw new ErrorPageError(
1048 'editpage-invalidcontentmodel-title',
1049 'editpage-invalidcontentmodel-text',
1050 [ wfEscapeWikiText( $this->contentModel ) ]
1051 );
1052 }
1053
1054 if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1055 throw new ErrorPageError(
1056 'editpage-notsupportedcontentformat-title',
1057 'editpage-notsupportedcontentformat-text',
1058 [
1059 wfEscapeWikiText( $this->contentFormat ),
1060 wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1061 ]
1062 );
1063 }
1064
1071 $this->editintro = $request->getText( 'editintro',
1072 // Custom edit intro for new sections
1073 $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1074
1075 // Allow extensions to modify form data
1076 Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1077 }
1078
1088 protected function importContentFormData( &$request ) {
1089 return; // Don't do anything, EditPage already extracted wpTextbox1
1090 }
1091
1097 public function initialiseForm() {
1099 $this->edittime = $this->page->getTimestamp();
1100 $this->editRevId = $this->page->getLatest();
1101
1102 $content = $this->getContentObject( false ); # TODO: track content object?!
1103 if ( $content === false ) {
1104 return false;
1105 }
1106 $this->textbox1 = $this->toEditText( $content );
1107
1108 // activate checkboxes if user wants them to be always active
1109 # Sort out the "watch" checkbox
1110 if ( $wgUser->getOption( 'watchdefault' ) ) {
1111 # Watch all edits
1112 $this->watchthis = true;
1113 } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1114 # Watch creations
1115 $this->watchthis = true;
1116 } elseif ( $wgUser->isWatched( $this->mTitle ) ) {
1117 # Already watched
1118 $this->watchthis = true;
1119 }
1120 if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) {
1121 $this->minoredit = true;
1122 }
1123 if ( $this->textbox1 === false ) {
1124 return false;
1125 }
1126 return true;
1127 }
1128
1136 protected function getContentObject( $def_content = null ) {
1138
1139 $content = false;
1140
1141 // For message page not locally set, use the i18n message.
1142 // For other non-existent articles, use preload text if any.
1143 if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1144 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1145 # If this is a system message, get the default text.
1146 $msg = $this->mTitle->getDefaultMessageText();
1147
1148 $content = $this->toEditContent( $msg );
1149 }
1150 if ( $content === false ) {
1151 # If requested, preload some text.
1152 $preload = $wgRequest->getVal( 'preload',
1153 // Custom preload text for new sections
1154 $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1155 $params = $wgRequest->getArray( 'preloadparams', [] );
1156
1157 $content = $this->getPreloadedContent( $preload, $params );
1158 }
1159 // For existing pages, get text based on "undo" or section parameters.
1160 } else {
1161 if ( $this->section != '' ) {
1162 // Get section edit text (returns $def_text for invalid sections)
1163 $orig = $this->getOriginalContent( $wgUser );
1164 $content = $orig ? $orig->getSection( $this->section ) : null;
1165
1166 if ( !$content ) {
1167 $content = $def_content;
1168 }
1169 } else {
1170 $undoafter = $wgRequest->getInt( 'undoafter' );
1171 $undo = $wgRequest->getInt( 'undo' );
1172
1173 if ( $undo > 0 && $undoafter > 0 ) {
1174 $undorev = Revision::newFromId( $undo );
1175 $oldrev = Revision::newFromId( $undoafter );
1176
1177 # Sanity check, make sure it's the right page,
1178 # the revisions exist and they were not deleted.
1179 # Otherwise, $content will be left as-is.
1180 if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1181 !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1182 !$oldrev->isDeleted( Revision::DELETED_TEXT )
1183 ) {
1184 $content = $this->page->getUndoContent( $undorev, $oldrev );
1185
1186 if ( $content === false ) {
1187 # Warn the user that something went wrong
1188 $undoMsg = 'failure';
1189 } else {
1190 $oldContent = $this->page->getContent( Revision::RAW );
1192 $newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
1193 if ( $newContent->getModel() !== $oldContent->getModel() ) {
1194 // The undo may change content
1195 // model if its reverting the top
1196 // edit. This can result in
1197 // mismatched content model/format.
1198 $this->contentModel = $newContent->getModel();
1199 $this->contentFormat = $oldrev->getContentFormat();
1200 }
1201
1202 if ( $newContent->equals( $oldContent ) ) {
1203 # Tell the user that the undo results in no change,
1204 # i.e. the revisions were already undone.
1205 $undoMsg = 'nochange';
1206 $content = false;
1207 } else {
1208 # Inform the user of our success and set an automatic edit summary
1209 $undoMsg = 'success';
1210
1211 # If we just undid one rev, use an autosummary
1212 $firstrev = $oldrev->getNext();
1213 if ( $firstrev && $firstrev->getId() == $undo ) {
1214 $userText = $undorev->getUserText();
1215 if ( $userText === '' ) {
1216 $undoSummary = $this->context->msg(
1217 'undo-summary-username-hidden',
1218 $undo
1219 )->inContentLanguage()->text();
1220 } else {
1221 $undoSummary = $this->context->msg(
1222 'undo-summary',
1223 $undo,
1224 $userText
1225 )->inContentLanguage()->text();
1226 }
1227 if ( $this->summary === '' ) {
1228 $this->summary = $undoSummary;
1229 } else {
1230 $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1231 ->inContentLanguage()->text() . $this->summary;
1232 }
1233 $this->undidRev = $undo;
1234 }
1235 $this->formtype = 'diff';
1236 }
1237 }
1238 } else {
1239 // Failed basic sanity checks.
1240 // Older revisions may have been removed since the link
1241 // was created, or we may simply have got bogus input.
1242 $undoMsg = 'norev';
1243 }
1244
1245 // Messages: undo-success, undo-failure, undo-norev, undo-nochange
1246 $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1247 $this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
1248 $this->context->msg( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
1249 }
1250
1251 if ( $content === false ) {
1252 $content = $this->getOriginalContent( $wgUser );
1253 }
1254 }
1255 }
1256
1257 return $content;
1258 }
1259
1275 private function getOriginalContent( User $user ) {
1276 if ( $this->section == 'new' ) {
1277 return $this->getCurrentContent();
1278 }
1279 $revision = $this->mArticle->getRevisionFetched();
1280 if ( $revision === null ) {
1281 $handler = ContentHandler::getForModelID( $this->contentModel );
1282 return $handler->makeEmptyContent();
1283 }
1284 $content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1285 return $content;
1286 }
1287
1300 public function getParentRevId() {
1301 if ( $this->parentRevId ) {
1302 return $this->parentRevId;
1303 } else {
1304 return $this->mArticle->getRevIdFetched();
1305 }
1306 }
1307
1316 protected function getCurrentContent() {
1317 $rev = $this->page->getRevision();
1318 $content = $rev ? $rev->getContent( Revision::RAW ) : null;
1319
1320 if ( $content === false || $content === null ) {
1321 $handler = ContentHandler::getForModelID( $this->contentModel );
1322 return $handler->makeEmptyContent();
1323 } elseif ( !$this->undidRev ) {
1324 // Content models should always be the same since we error
1325 // out if they are different before this point (in ->edit()).
1326 // The exception being, during an undo, the current revision might
1327 // differ from the prior revision.
1328 $logger = LoggerFactory::getInstance( 'editpage' );
1329 if ( $this->contentModel !== $rev->getContentModel() ) {
1330 $logger->warning( "Overriding content model from current edit {prev} to {new}", [
1331 'prev' => $this->contentModel,
1332 'new' => $rev->getContentModel(),
1333 'title' => $this->getTitle()->getPrefixedDBkey(),
1334 'method' => __METHOD__
1335 ] );
1336 $this->contentModel = $rev->getContentModel();
1337 }
1338
1339 // Given that the content models should match, the current selected
1340 // format should be supported.
1341 if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1342 $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1343
1344 'prev' => $this->contentFormat,
1345 'new' => $rev->getContentFormat(),
1346 'title' => $this->getTitle()->getPrefixedDBkey(),
1347 'method' => __METHOD__
1348 ] );
1349 $this->contentFormat = $rev->getContentFormat();
1350 }
1351 }
1352 return $content;
1353 }
1354
1363 $this->mPreloadContent = $content;
1364 }
1365
1377 protected function getPreloadedContent( $preload, $params = [] ) {
1379
1380 if ( !empty( $this->mPreloadContent ) ) {
1382 }
1383
1384 $handler = ContentHandler::getForModelID( $this->contentModel );
1385
1386 if ( $preload === '' ) {
1387 return $handler->makeEmptyContent();
1388 }
1389
1390 $title = Title::newFromText( $preload );
1391 # Check for existence to avoid getting MediaWiki:Noarticletext
1392 if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1393 // TODO: somehow show a warning to the user!
1394 return $handler->makeEmptyContent();
1395 }
1396
1398 if ( $page->isRedirect() ) {
1400 # Same as before
1401 if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1402 // TODO: somehow show a warning to the user!
1403 return $handler->makeEmptyContent();
1404 }
1406 }
1407
1408 $parserOptions = ParserOptions::newFromUser( $wgUser );
1410
1411 if ( !$content ) {
1412 // TODO: somehow show a warning to the user!
1413 return $handler->makeEmptyContent();
1414 }
1415
1416 if ( $content->getModel() !== $handler->getModelID() ) {
1417 $converted = $content->convert( $handler->getModelID() );
1418
1419 if ( !$converted ) {
1420 // TODO: somehow show a warning to the user!
1421 wfDebug( "Attempt to preload incompatible content: " .
1422 "can't convert " . $content->getModel() .
1423 " to " . $handler->getModelID() );
1424
1425 return $handler->makeEmptyContent();
1426 }
1427
1428 $content = $converted;
1429 }
1430
1431 return $content->preloadTransform( $title, $parserOptions, $params );
1432 }
1433
1441 public function tokenOk( &$request ) {
1443 $token = $request->getVal( 'wpEditToken' );
1444 $this->mTokenOk = $wgUser->matchEditToken( $token );
1445 $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
1446 return $this->mTokenOk;
1447 }
1448
1465 protected function setPostEditCookie( $statusValue ) {
1466 $revisionId = $this->page->getLatest();
1467 $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1468
1469 $val = 'saved';
1470 if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1471 $val = 'created';
1472 } elseif ( $this->oldid ) {
1473 $val = 'restored';
1474 }
1475
1476 $response = RequestContext::getMain()->getRequest()->response();
1477 $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, [
1478 'httpOnly' => false,
1479 ] );
1480 }
1481
1488 public function attemptSave( &$resultDetails = false ) {
1490
1491 # Allow bots to exempt some edits from bot flagging
1492 $bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
1493 $status = $this->internalAttemptSave( $resultDetails, $bot );
1494
1495 Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1496
1497 return $status;
1498 }
1499
1509 private function handleStatus( Status $status, $resultDetails ) {
1511
1516 if ( $status->value == self::AS_SUCCESS_UPDATE
1517 || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1518 ) {
1519 $this->didSave = true;
1520 if ( !$resultDetails['nullEdit'] ) {
1521 $this->setPostEditCookie( $status->value );
1522 }
1523 }
1524
1525 // "wpExtraQueryRedirect" is a hidden input to modify
1526 // after save URL and is not used by actual edit form
1528 $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1529
1530 switch ( $status->value ) {
1538 case self::AS_END:
1541 return true;
1542
1544 return false;
1545
1548 $wgOut->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
1549 return true;
1550
1552 $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1553 if ( $extraQueryRedirect ) {
1554 if ( $query === '' ) {
1555 $query = $extraQueryRedirect;
1556 } else {
1557 $query = $query . '&' . $extraQueryRedirect;
1558 }
1559 }
1560 $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
1561 $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1562 return false;
1563
1565 $extraQuery = '';
1566 $sectionanchor = $resultDetails['sectionanchor'];
1567
1568 // Give extensions a chance to modify URL query on update
1569 Hooks::run(
1570 'ArticleUpdateBeforeRedirect',
1571 [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1572 );
1573
1574 if ( $resultDetails['redirect'] ) {
1575 if ( $extraQuery == '' ) {
1576 $extraQuery = 'redirect=no';
1577 } else {
1578 $extraQuery = 'redirect=no&' . $extraQuery;
1579 }
1580 }
1581 if ( $extraQueryRedirect ) {
1582 if ( $extraQuery === '' ) {
1583 $extraQuery = $extraQueryRedirect;
1584 } else {
1585 $extraQuery = $extraQuery . '&' . $extraQueryRedirect;
1586 }
1587 }
1588
1589 $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1590 return false;
1591
1593 $this->spamPageWithContent( $resultDetails['spam'] );
1594 return false;
1595
1597 throw new UserBlockedError( $wgUser->getBlock() );
1598
1601 throw new PermissionsError( 'upload' );
1602
1605 throw new PermissionsError( 'edit' );
1606
1608 throw new ReadOnlyError;
1609
1611 throw new ThrottledError();
1612
1614 $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1615 throw new PermissionsError( $permission );
1616
1618 throw new PermissionsError( 'editcontentmodel' );
1619
1620 default:
1621 // We don't recognize $status->value. The only way that can happen
1622 // is if an extension hook aborted from inside ArticleSave.
1623 // Render the status object into $this->hookError
1624 // FIXME this sucks, we should just use the Status object throughout
1625 $this->hookError = '<div class="error">' ."\n" . $status->getWikiText() .
1626 '</div>';
1627 return true;
1628 }
1629 }
1630
1640 protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1641 // Run old style post-section-merge edit filter
1642 if ( $this->hookError != '' ) {
1643 # ...or the hook could be expecting us to produce an error
1644 $status->fatal( 'hookaborted' );
1646 return false;
1647 }
1648
1649 // Run new style post-section-merge edit filter
1650 if ( !Hooks::run( 'EditFilterMergedContent',
1651 [ $this->mArticle->getContext(), $content, $status, $this->summary,
1652 $user, $this->minoredit ] )
1653 ) {
1654 # Error messages etc. could be handled within the hook...
1655 if ( $status->isGood() ) {
1656 $status->fatal( 'hookaborted' );
1657 // Not setting $this->hookError here is a hack to allow the hook
1658 // to cause a return to the edit page without $this->hookError
1659 // being set. This is used by ConfirmEdit to display a captcha
1660 // without any error message cruft.
1661 } else {
1662 $this->hookError = $status->getWikiText();
1663 }
1664 // Use the existing $status->value if the hook set it
1665 if ( !$status->value ) {
1667 }
1668 return false;
1669 } elseif ( !$status->isOK() ) {
1670 # ...or the hook could be expecting us to produce an error
1671 // FIXME this sucks, we should just use the Status object throughout
1672 $this->hookError = $status->getWikiText();
1673 $status->fatal( 'hookaborted' );
1675 return false;
1676 }
1677
1678 return true;
1679 }
1680
1687 private function newSectionSummary( &$sectionanchor = null ) {
1689
1690 if ( $this->sectiontitle !== '' ) {
1691 $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
1692 // If no edit summary was specified, create one automatically from the section
1693 // title and have it link to the new section. Otherwise, respect the summary as
1694 // passed.
1695 if ( $this->summary === '' ) {
1696 $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1697 return $this->context->msg( 'newsectionsummary' )
1698 ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
1699 }
1700 } elseif ( $this->summary !== '' ) {
1701 $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
1702 # This is a new section, so create a link to the new section
1703 # in the revision summary.
1704 $cleanSummary = $wgParser->stripSectionName( $this->summary );
1705 return $this->context->msg( 'newsectionsummary' )
1706 ->rawParams( $cleanSummary )->inContentLanguage()->text();
1707 }
1708 return $this->summary;
1709 }
1710
1735 public function internalAttemptSave( &$result, $bot = false ) {
1738
1739 $status = Status::newGood();
1740
1741 if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1742 wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1743 $status->fatal( 'hookaborted' );
1745 return $status;
1746 }
1747
1748 $spam = $wgRequest->getText( 'wpAntispam' );
1749 if ( $spam !== '' ) {
1750 wfDebugLog(
1751 'SimpleAntiSpam',
1752 $wgUser->getName() .
1753 ' editing "' .
1754 $this->mTitle->getPrefixedText() .
1755 '" submitted bogus field "' .
1756 $spam .
1757 '"'
1758 );
1759 $status->fatal( 'spamprotectionmatch', false );
1761 return $status;
1762 }
1763
1764 try {
1765 # Construct Content object
1766 $textbox_content = $this->toEditContent( $this->textbox1 );
1767 } catch ( MWContentSerializationException $ex ) {
1768 $status->fatal(
1769 'content-failed-to-parse',
1770 $this->contentModel,
1771 $this->contentFormat,
1772 $ex->getMessage()
1773 );
1775 return $status;
1776 }
1777
1778 # Check image redirect
1779 if ( $this->mTitle->getNamespace() == NS_FILE &&
1780 $textbox_content->isRedirect() &&
1781 !$wgUser->isAllowed( 'upload' )
1782 ) {
1784 $status->setResult( false, $code );
1785
1786 return $status;
1787 }
1788
1789 # Check for spam
1790 $match = self::matchSummarySpamRegex( $this->summary );
1791 if ( $match === false && $this->section == 'new' ) {
1792 # $wgSpamRegex is enforced on this new heading/summary because, unlike
1793 # regular summaries, it is added to the actual wikitext.
1794 if ( $this->sectiontitle !== '' ) {
1795 # This branch is taken when the API is used with the 'sectiontitle' parameter.
1796 $match = self::matchSpamRegex( $this->sectiontitle );
1797 } else {
1798 # This branch is taken when the "Add Topic" user interface is used, or the API
1799 # is used with the 'summary' parameter.
1800 $match = self::matchSpamRegex( $this->summary );
1801 }
1802 }
1803 if ( $match === false ) {
1804 $match = self::matchSpamRegex( $this->textbox1 );
1805 }
1806 if ( $match !== false ) {
1807 $result['spam'] = $match;
1808 $ip = $wgRequest->getIP();
1809 $pdbk = $this->mTitle->getPrefixedDBkey();
1810 $match = str_replace( "\n", '', $match );
1811 wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1812 $status->fatal( 'spamprotectionmatch', $match );
1814 return $status;
1815 }
1816 if ( !Hooks::run(
1817 'EditFilter',
1818 [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1819 ) {
1820 # Error messages etc. could be handled within the hook...
1821 $status->fatal( 'hookaborted' );
1823 return $status;
1824 } elseif ( $this->hookError != '' ) {
1825 # ...or the hook could be expecting us to produce an error
1826 $status->fatal( 'hookaborted' );
1828 return $status;
1829 }
1830
1831 if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
1832 // Auto-block user's IP if the account was "hard" blocked
1833 if ( !wfReadOnly() ) {
1834 $wgUser->spreadAnyEditBlock();
1835 }
1836 # Check block state against master, thus 'false'.
1837 $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1838 return $status;
1839 }
1840
1841 $this->contentLength = strlen( $this->textbox1 );
1842 if ( $this->contentLength > $wgMaxArticleSize * 1024 ) {
1843 // Error will be displayed by showEditForm()
1844 $this->tooBig = true;
1845 $status->setResult( false, self::AS_CONTENT_TOO_BIG );
1846 return $status;
1847 }
1848
1849 if ( !$wgUser->isAllowed( 'edit' ) ) {
1850 if ( $wgUser->isAnon() ) {
1851 $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1852 return $status;
1853 } else {
1854 $status->fatal( 'readonlytext' );
1856 return $status;
1857 }
1858 }
1859
1860 $changingContentModel = false;
1861 if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1862 if ( !$wgContentHandlerUseDB ) {
1863 $status->fatal( 'editpage-cannot-use-custom-model' );
1865 return $status;
1866 } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) {
1867 $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1868 return $status;
1869 }
1870 // Make sure the user can edit the page under the new content model too
1871 $titleWithNewContentModel = clone $this->mTitle;
1872 $titleWithNewContentModel->setContentModel( $this->contentModel );
1873 if ( !$titleWithNewContentModel->userCan( 'editcontentmodel', $wgUser )
1874 || !$titleWithNewContentModel->userCan( 'edit', $wgUser )
1875 ) {
1876 $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1877 return $status;
1878 }
1879
1880 $changingContentModel = true;
1881 $oldContentModel = $this->mTitle->getContentModel();
1882 }
1883
1884 if ( $this->changeTags ) {
1885 $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
1886 $this->changeTags, $wgUser );
1887 if ( !$changeTagsStatus->isOK() ) {
1888 $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
1889 return $changeTagsStatus;
1890 }
1891 }
1892
1893 if ( wfReadOnly() ) {
1894 $status->fatal( 'readonlytext' );
1896 return $status;
1897 }
1898 if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 )
1899 || ( $changingContentModel && $wgUser->pingLimiter( 'editcontentmodel' ) )
1900 ) {
1901 $status->fatal( 'actionthrottledtext' );
1903 return $status;
1904 }
1905
1906 # If the article has been deleted while editing, don't save it without
1907 # confirmation
1908 if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
1909 $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
1910 return $status;
1911 }
1912
1913 # Load the page data from the master. If anything changes in the meantime,
1914 # we detect it by using page_latest like a token in a 1 try compare-and-swap.
1915 $this->page->loadPageData( 'fromdbmaster' );
1916 $new = !$this->page->exists();
1917
1918 if ( $new ) {
1919 // Late check for create permission, just in case *PARANOIA*
1920 if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
1921 $status->fatal( 'nocreatetext' );
1923 wfDebug( __METHOD__ . ": no create permission\n" );
1924 return $status;
1925 }
1926
1927 // Don't save a new page if it's blank or if it's a MediaWiki:
1928 // message with content equivalent to default (allow empty pages
1929 // in this case to disable messages, see T52124)
1930 $defaultMessageText = $this->mTitle->getDefaultMessageText();
1931 if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
1932 $defaultText = $defaultMessageText;
1933 } else {
1934 $defaultText = '';
1935 }
1936
1937 if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
1938 $this->blankArticle = true;
1939 $status->fatal( 'blankarticle' );
1940 $status->setResult( false, self::AS_BLANK_ARTICLE );
1941 return $status;
1942 }
1943
1944 if ( !$this->runPostMergeFilters( $textbox_content, $status, $wgUser ) ) {
1945 return $status;
1946 }
1947
1948 $content = $textbox_content;
1949
1950 $result['sectionanchor'] = '';
1951 if ( $this->section == 'new' ) {
1952 if ( $this->sectiontitle !== '' ) {
1953 // Insert the section title above the content.
1954 $content = $content->addSectionHeader( $this->sectiontitle );
1955 } elseif ( $this->summary !== '' ) {
1956 // Insert the section title above the content.
1957 $content = $content->addSectionHeader( $this->summary );
1958 }
1959 $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
1960 }
1961
1963
1964 } else { # not $new
1965
1966 # Article exists. Check for edit conflict.
1967
1968 $this->page->clear(); # Force reload of dates, etc.
1969 $timestamp = $this->page->getTimestamp();
1970 $latest = $this->page->getLatest();
1971
1972 wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
1973
1974 // Check editRevId if set, which handles same-second timestamp collisions
1975 if ( $timestamp != $this->edittime
1976 || ( $this->editRevId !== null && $this->editRevId != $latest )
1977 ) {
1978 $this->isConflict = true;
1979 if ( $this->section == 'new' ) {
1980 if ( $this->page->getUserText() == $wgUser->getName() &&
1981 $this->page->getComment() == $this->newSectionSummary()
1982 ) {
1983 // Probably a duplicate submission of a new comment.
1984 // This can happen when CDN resends a request after
1985 // a timeout but the first one actually went through.
1986 wfDebug( __METHOD__
1987 . ": duplicate new section submission; trigger edit conflict!\n" );
1988 } else {
1989 // New comment; suppress conflict.
1990 $this->isConflict = false;
1991 wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
1992 }
1993 } elseif ( $this->section == ''
1995 DB_MASTER, $this->mTitle->getArticleID(),
1996 $wgUser->getId(), $this->edittime
1997 )
1998 ) {
1999 # Suppress edit conflict with self, except for section edits where merging is required.
2000 wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
2001 $this->isConflict = false;
2002 }
2003 }
2004
2005 // If sectiontitle is set, use it, otherwise use the summary as the section title.
2006 if ( $this->sectiontitle !== '' ) {
2007 $sectionTitle = $this->sectiontitle;
2008 } else {
2009 $sectionTitle = $this->summary;
2010 }
2011
2012 $content = null;
2013
2014 if ( $this->isConflict ) {
2015 wfDebug( __METHOD__
2016 . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2017 . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
2018 // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2019 // ...or disable section editing for non-current revisions (not exposed anyway).
2020 if ( $this->editRevId !== null ) {
2021 $content = $this->page->replaceSectionAtRev(
2022 $this->section,
2023 $textbox_content,
2024 $sectionTitle,
2025 $this->editRevId
2026 );
2027 } else {
2028 $content = $this->page->replaceSectionContent(
2029 $this->section,
2030 $textbox_content,
2031 $sectionTitle,
2032 $this->edittime
2033 );
2034 }
2035 } else {
2036 wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
2037 $content = $this->page->replaceSectionContent(
2038 $this->section,
2039 $textbox_content,
2040 $sectionTitle
2041 );
2042 }
2043
2044 if ( is_null( $content ) ) {
2045 wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
2046 $this->isConflict = true;
2047 $content = $textbox_content; // do not try to merge here!
2048 } elseif ( $this->isConflict ) {
2049 # Attempt merge
2050 if ( $this->mergeChangesIntoContent( $content ) ) {
2051 // Successful merge! Maybe we should tell the user the good news?
2052 $this->isConflict = false;
2053 wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
2054 } else {
2055 $this->section = '';
2056 $this->textbox1 = ContentHandler::getContentText( $content );
2057 wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
2058 }
2059 }
2060
2061 if ( $this->isConflict ) {
2062 $status->setResult( false, self::AS_CONFLICT_DETECTED );
2063 return $status;
2064 }
2065
2066 if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) {
2067 return $status;
2068 }
2069
2070 if ( $this->section == 'new' ) {
2071 // Handle the user preference to force summaries here
2072 if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2073 $this->missingSummary = true;
2074 $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2076 return $status;
2077 }
2078
2079 // Do not allow the user to post an empty comment
2080 if ( $this->textbox1 == '' ) {
2081 $this->missingComment = true;
2082 $status->fatal( 'missingcommenttext' );
2084 return $status;
2085 }
2086 } elseif ( !$this->allowBlankSummary
2087 && !$content->equals( $this->getOriginalContent( $wgUser ) )
2088 && !$content->isRedirect()
2089 && md5( $this->summary ) == $this->autoSumm
2090 ) {
2091 $this->missingSummary = true;
2092 $status->fatal( 'missingsummary' );
2094 return $status;
2095 }
2096
2097 # All's well
2098 $sectionanchor = '';
2099 if ( $this->section == 'new' ) {
2100 $this->summary = $this->newSectionSummary( $sectionanchor );
2101 } elseif ( $this->section != '' ) {
2102 # Try to get a section anchor from the section source, redirect
2103 # to edited section if header found.
2104 # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2105 # for duplicate heading checking and maybe parsing.
2106 $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2107 # We can't deal with anchors, includes, html etc in the header for now,
2108 # headline would need to be parsed to improve this.
2109 if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2110 $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
2111 }
2112 }
2113 $result['sectionanchor'] = $sectionanchor;
2114
2115 // Save errors may fall down to the edit form, but we've now
2116 // merged the section into full text. Clear the section field
2117 // so that later submission of conflict forms won't try to
2118 // replace that into a duplicated mess.
2119 $this->textbox1 = $this->toEditText( $content );
2120 $this->section = '';
2121
2123 }
2124
2125 if ( !$this->allowSelfRedirect
2126 && $content->isRedirect()
2127 && $content->getRedirectTarget()->equals( $this->getTitle() )
2128 ) {
2129 // If the page already redirects to itself, don't warn.
2130 $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2131 if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2132 $this->selfRedirect = true;
2133 $status->fatal( 'selfredirect' );
2135 return $status;
2136 }
2137 }
2138
2139 // Check for length errors again now that the section is merged in
2140 $this->contentLength = strlen( $this->toEditText( $content ) );
2141 if ( $this->contentLength > $wgMaxArticleSize * 1024 ) {
2142 $this->tooBig = true;
2143 $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2144 return $status;
2145 }
2146
2148 ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2149 ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2150 ( $bot ? EDIT_FORCE_BOT : 0 );
2151
2152 $doEditStatus = $this->page->doEditContent(
2153 $content,
2154 $this->summary,
2155 $flags,
2156 false,
2157 $wgUser,
2158 $content->getDefaultFormat(),
2161 );
2162
2163 if ( !$doEditStatus->isOK() ) {
2164 // Failure from doEdit()
2165 // Show the edit conflict page for certain recognized errors from doEdit(),
2166 // but don't show it for errors from extension hooks
2167 $errors = $doEditStatus->getErrorsArray();
2168 if ( in_array( $errors[0][0],
2169 [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2170 ) {
2171 $this->isConflict = true;
2172 // Destroys data doEdit() put in $status->value but who cares
2173 $doEditStatus->value = self::AS_END;
2174 }
2175 return $doEditStatus;
2176 }
2177
2178 $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2179 if ( $result['nullEdit'] ) {
2180 // We don't know if it was a null edit until now, so increment here
2181 $wgUser->pingLimiter( 'linkpurge' );
2182 }
2183 $result['redirect'] = $content->isRedirect();
2184
2185 $this->updateWatchlist();
2186
2187 // If the content model changed, add a log entry
2188 if ( $changingContentModel ) {
2190 $wgUser,
2191 $new ? false : $oldContentModel,
2194 );
2195 }
2196
2197 return $status;
2198 }
2199
2206 protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2207 $new = $oldModel === false;
2208 $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2209 $log->setPerformer( $user );
2210 $log->setTarget( $this->mTitle );
2211 $log->setComment( $reason );
2212 $log->setParameters( [
2213 '4::oldmodel' => $oldModel,
2214 '5::newmodel' => $newModel
2215 ] );
2216 $logid = $log->insert();
2217 $log->publish( $logid );
2218 }
2219
2223 protected function updateWatchlist() {
2225
2226 if ( !$wgUser->isLoggedIn() ) {
2227 return;
2228 }
2229
2230 $user = $wgUser;
2232 $watch = $this->watchthis;
2233 // Do this in its own transaction to reduce contention...
2234 DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2235 if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2236 return; // nothing to change
2237 }
2239 } );
2240 }
2241
2253 private function mergeChangesIntoContent( &$editContent ) {
2254 $db = wfGetDB( DB_MASTER );
2255
2256 // This is the revision the editor started from
2257 $baseRevision = $this->getBaseRevision();
2258 $baseContent = $baseRevision ? $baseRevision->getContent() : null;
2259
2260 if ( is_null( $baseContent ) ) {
2261 return false;
2262 }
2263
2264 // The current state, we want to merge updates into it
2265 $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2266 $currentContent = $currentRevision ? $currentRevision->getContent() : null;
2267
2268 if ( is_null( $currentContent ) ) {
2269 return false;
2270 }
2271
2272 $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2273
2274 $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2275
2276 if ( $result ) {
2277 $editContent = $result;
2278 // Update parentRevId to what we just merged.
2279 $this->parentRevId = $currentRevision->getId();
2280 return true;
2281 }
2282
2283 return false;
2284 }
2285
2291 public function getBaseRevision() {
2292 if ( !$this->mBaseRevision ) {
2293 $db = wfGetDB( DB_MASTER );
2294 $this->mBaseRevision = $this->editRevId
2295 ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2296 : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2297 }
2298 return $this->mBaseRevision;
2299 }
2300
2308 public static function matchSpamRegex( $text ) {
2310 // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2311 $regexes = (array)$wgSpamRegex;
2312 return self::matchSpamRegexInternal( $text, $regexes );
2313 }
2314
2322 public static function matchSummarySpamRegex( $text ) {
2324 $regexes = (array)$wgSummarySpamRegex;
2325 return self::matchSpamRegexInternal( $text, $regexes );
2326 }
2327
2333 protected static function matchSpamRegexInternal( $text, $regexes ) {
2334 foreach ( $regexes as $regex ) {
2335 $matches = [];
2336 if ( preg_match( $regex, $text, $matches ) ) {
2337 return $matches[0];
2338 }
2339 }
2340 return false;
2341 }
2342
2343 public function setHeaders() {
2345
2346 $wgOut->addModules( 'mediawiki.action.edit' );
2347 $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
2348
2349 if ( $wgUser->getOption( 'showtoolbar' ) ) {
2350 // The addition of default buttons is handled by getEditToolbar() which
2351 // has its own dependency on this module. The call here ensures the module
2352 // is loaded in time (it has position "top") for other modules to register
2353 // buttons (e.g. extensions, gadgets, user scripts).
2354 $wgOut->addModules( 'mediawiki.toolbar' );
2355 }
2356
2357 if ( $wgUser->getOption( 'uselivepreview' ) ) {
2358 $wgOut->addModules( 'mediawiki.action.edit.preview' );
2359 }
2360
2361 if ( $wgUser->getOption( 'useeditwarning' ) ) {
2362 $wgOut->addModules( 'mediawiki.action.edit.editWarning' );
2363 }
2364
2365 # Enabled article-related sidebar, toplinks, etc.
2366 $wgOut->setArticleRelated( true );
2367
2368 $contextTitle = $this->getContextTitle();
2369 if ( $this->isConflict ) {
2370 $msg = 'editconflict';
2371 } elseif ( $contextTitle->exists() && $this->section != '' ) {
2372 $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2373 } else {
2374 $msg = $contextTitle->exists()
2375 || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2376 && $contextTitle->getDefaultMessageText() !== false
2377 )
2378 ? 'editing'
2379 : 'creating';
2380 }
2381
2382 # Use the title defined by DISPLAYTITLE magic word when present
2383 # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2384 # setPageTitle() treats the input as wikitext, which should be safe in either case.
2385 $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2386 if ( $displayTitle === false ) {
2387 $displayTitle = $contextTitle->getPrefixedText();
2388 }
2389 $wgOut->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2390 # Transmit the name of the message to JavaScript for live preview
2391 # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2392 $wgOut->addJsConfigVars( [
2393 'wgEditMessage' => $msg,
2394 'wgAjaxEditStash' => $wgAjaxEditStash,
2395 ] );
2396 }
2397
2401 protected function showIntro() {
2403 if ( $this->suppressIntro ) {
2404 return;
2405 }
2406
2407 $namespace = $this->mTitle->getNamespace();
2408
2409 if ( $namespace == NS_MEDIAWIKI ) {
2410 # Show a warning if editing an interface message
2411 $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2412 # If this is a default message (but not css or js),
2413 # show a hint that it is translatable on translatewiki.net
2414 if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2415 && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2416 ) {
2417 $defaultMessageText = $this->mTitle->getDefaultMessageText();
2418 if ( $defaultMessageText !== false ) {
2419 $wgOut->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2420 'translateinterface' );
2421 }
2422 }
2423 } elseif ( $namespace == NS_FILE ) {
2424 # Show a hint to shared repo
2425 $file = wfFindFile( $this->mTitle );
2426 if ( $file && !$file->isLocal() ) {
2427 $descUrl = $file->getDescriptionUrl();
2428 # there must be a description url to show a hint to shared repo
2429 if ( $descUrl ) {
2430 if ( !$this->mTitle->exists() ) {
2431 $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2432 'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2433 ] );
2434 } else {
2435 $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2436 'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2437 ] );
2438 }
2439 }
2440 }
2441 }
2442
2443 # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2444 # Show log extract when the user is currently blocked
2445 if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2446 $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2447 $user = User::newFromName( $username, false /* allow IP users */ );
2448 $ip = User::isIP( $username );
2449 $block = Block::newFromTarget( $user, $user );
2450 if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2451 $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2452 [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2453 } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
2454 # Show log extract if the user is currently blocked
2456 $wgOut,
2457 'block',
2458 MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2459 '',
2460 [
2461 'lim' => 1,
2462 'showIfEmpty' => false,
2463 'msgKey' => [
2464 'blocked-notice-logextract',
2465 $user->getName() # Support GENDER in notice
2466 ]
2467 ]
2468 );
2469 }
2470 }
2471 # Try to add a custom edit intro, or use the standard one if this is not possible.
2472 if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2474 $this->context->msg( 'helppage' )->inContentLanguage()->text()
2475 ) );
2476 if ( $wgUser->isLoggedIn() ) {
2477 $wgOut->wrapWikiMsg(
2478 // Suppress the external link icon, consider the help url an internal one
2479 "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2480 [
2481 'newarticletext',
2482 $helpLink
2483 ]
2484 );
2485 } else {
2486 $wgOut->wrapWikiMsg(
2487 // Suppress the external link icon, consider the help url an internal one
2488 "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2489 [
2490 'newarticletextanon',
2491 $helpLink
2492 ]
2493 );
2494 }
2495 }
2496 # Give a notice if the user is editing a deleted/moved page...
2497 if ( !$this->mTitle->exists() ) {
2498 $dbr = wfGetDB( DB_REPLICA );
2499
2500 LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle,
2501 '',
2502 [
2503 'lim' => 10,
2504 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2505 'showIfEmpty' => false,
2506 'msgKey' => [ 'recreate-moveddeleted-warn' ]
2507 ]
2508 );
2509 }
2510 }
2511
2517 protected function showCustomIntro() {
2518 if ( $this->editintro ) {
2519 $title = Title::newFromText( $this->editintro );
2520 if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2521 global $wgOut;
2522 // Added using template syntax, to take <noinclude>'s into account.
2523 $wgOut->addWikiTextTitleTidy(
2524 '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2525 $this->mTitle
2526 );
2527 return true;
2528 }
2529 }
2530 return false;
2531 }
2532
2551 protected function toEditText( $content ) {
2552 if ( $content === null || $content === false || is_string( $content ) ) {
2553 return $content;
2554 }
2555
2556 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2557 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2558 }
2559
2560 return $content->serialize( $this->contentFormat );
2561 }
2562
2579 protected function toEditContent( $text ) {
2580 if ( $text === false || $text === null ) {
2581 return $text;
2582 }
2583
2585 $this->contentModel, $this->contentFormat );
2586
2587 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2588 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2589 }
2590
2591 return $content;
2592 }
2593
2602 public function showEditForm( $formCallback = null ) {
2604
2605 # need to parse the preview early so that we know which templates are used,
2606 # otherwise users with "show preview after edit box" will get a blank list
2607 # we parse this near the beginning so that setHeaders can do the title
2608 # setting work instead of leaving it in getPreviewText
2609 $previewOutput = '';
2610 if ( $this->formtype == 'preview' ) {
2611 $previewOutput = $this->getPreviewText();
2612 }
2613
2614 // Avoid PHP 7.1 warning of passing $this by reference
2615 $editPage = $this;
2616 Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$wgOut ] );
2617
2618 $this->setHeaders();
2619
2620 $this->addTalkPageText();
2621 $this->addEditNotices();
2622
2623 if ( !$this->isConflict &&
2624 $this->section != '' &&
2625 !$this->isSectionEditSupported() ) {
2626 // We use $this->section to much before this and getVal('wgSection') directly in other places
2627 // at this point we can't reset $this->section to '' to fallback to non-section editing.
2628 // Someone is welcome to try refactoring though
2629 $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2630 return;
2631 }
2632
2633 $this->showHeader();
2634
2635 $wgOut->addHTML( $this->editFormPageTop );
2636
2637 if ( $wgUser->getOption( 'previewontop' ) ) {
2638 $this->displayPreviewArea( $previewOutput, true );
2639 }
2640
2641 $wgOut->addHTML( $this->editFormTextTop );
2642
2643 $showToolbar = true;
2644 if ( $this->wasDeletedSinceLastEdit() ) {
2645 if ( $this->formtype == 'save' ) {
2646 // Hide the toolbar and edit area, user can click preview to get it back
2647 // Add an confirmation checkbox and explanation.
2648 $showToolbar = false;
2649 } else {
2650 $wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2651 'deletedwhileediting' );
2652 }
2653 }
2654
2655 // @todo add EditForm plugin interface and use it here!
2656 // search for textarea1 and textarea2, and allow EditForm to override all uses.
2657 $wgOut->addHTML( Html::openElement(
2658 'form',
2659 [
2660 'class' => $this->oouiEnabled ? 'mw-editform-ooui' : 'mw-editform-legacy',
2661 'id' => self::EDITFORM_ID,
2662 'name' => self::EDITFORM_ID,
2663 'method' => 'post',
2664 'action' => $this->getActionURL( $this->getContextTitle() ),
2665 'enctype' => 'multipart/form-data'
2666 ]
2667 ) );
2668
2669 if ( is_callable( $formCallback ) ) {
2670 wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2671 call_user_func_array( $formCallback, [ &$wgOut ] );
2672 }
2673
2674 // Add an empty field to trip up spambots
2675 $wgOut->addHTML(
2676 Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2677 . Html::rawElement(
2678 'label',
2679 [ 'for' => 'wpAntispam' ],
2680 $this->context->msg( 'simpleantispam-label' )->parse()
2681 )
2682 . Xml::element(
2683 'input',
2684 [
2685 'type' => 'text',
2686 'name' => 'wpAntispam',
2687 'id' => 'wpAntispam',
2688 'value' => ''
2689 ]
2690 )
2691 . Xml::closeElement( 'div' )
2692 );
2693
2694 // Avoid PHP 7.1 warning of passing $this by reference
2695 $editPage = $this;
2696 Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$wgOut ] );
2697
2698 // Put these up at the top to ensure they aren't lost on early form submission
2699 $this->showFormBeforeText();
2700
2701 if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2702 $username = $this->lastDelete->user_name;
2703 $comment = $this->lastDelete->log_comment;
2704
2705 // It is better to not parse the comment at all than to have templates expanded in the middle
2706 // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2707 $key = $comment === ''
2708 ? 'confirmrecreate-noreason'
2709 : 'confirmrecreate';
2710 $wgOut->addHTML(
2711 '<div class="mw-confirm-recreate">' .
2712 $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2713 Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2714 [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2715 ) .
2716 '</div>'
2717 );
2718 }
2719
2720 # When the summary is hidden, also hide them on preview/show changes
2721 if ( $this->nosummary ) {
2722 $wgOut->addHTML( Html::hidden( 'nosummary', true ) );
2723 }
2724
2725 # If a blank edit summary was previously provided, and the appropriate
2726 # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2727 # user being bounced back more than once in the event that a summary
2728 # is not required.
2729 # ####
2730 # For a bit more sophisticated detection of blank summaries, hash the
2731 # automatic one and pass that in the hidden field wpAutoSummary.
2732 if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2733 $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2734 }
2735
2736 if ( $this->undidRev ) {
2737 $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2738 }
2739
2740 if ( $this->selfRedirect ) {
2741 $wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2742 }
2743
2744 if ( $this->hasPresetSummary ) {
2745 // If a summary has been preset using &summary= we don't want to prompt for
2746 // a different summary. Only prompt for a summary if the summary is blanked.
2747 // (T19416)
2748 $this->autoSumm = md5( '' );
2749 }
2750
2751 $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2752 $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2753
2754 $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2755 $wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2756
2757 $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2758 $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
2759
2760 // following functions will need OOUI, enable it only once; here.
2761 if ( $this->oouiEnabled ) {
2762 $wgOut->enableOOUI();
2763 }
2764
2765 if ( $this->section == 'new' ) {
2766 $this->showSummaryInput( true, $this->summary );
2767 $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2768 }
2769
2770 $wgOut->addHTML( $this->editFormTextBeforeContent );
2771
2772 if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
2773 $wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
2774 }
2775
2776 if ( $this->blankArticle ) {
2777 $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2778 }
2779
2780 if ( $this->isConflict ) {
2781 // In an edit conflict bypass the overridable content form method
2782 // and fallback to the raw wpTextbox1 since editconflicts can't be
2783 // resolved between page source edits and custom ui edits using the
2784 // custom edit ui.
2785 $this->textbox2 = $this->textbox1;
2786
2787 $content = $this->getCurrentContent();
2788 $this->textbox1 = $this->toEditText( $content );
2789
2790 $this->showTextbox1();
2791 } else {
2792 $this->showContentForm();
2793 }
2794
2795 $wgOut->addHTML( $this->editFormTextAfterContent );
2796
2797 $this->showStandardInputs();
2798
2799 $this->showFormAfterText();
2800
2801 $this->showTosSummary();
2802
2803 $this->showEditTools();
2804
2805 $wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
2806
2807 $wgOut->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
2808
2809 $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2810 Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2811
2812 $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2813 self::getPreviewLimitReport( $this->mParserOutput ) ) );
2814
2815 $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2816
2817 if ( $this->isConflict ) {
2818 try {
2819 $this->showConflict();
2820 } catch ( MWContentSerializationException $ex ) {
2821 // this can't really happen, but be nice if it does.
2822 $msg = $this->context->msg(
2823 'content-failed-to-parse',
2824 $this->contentModel,
2825 $this->contentFormat,
2826 $ex->getMessage()
2827 );
2828 $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2829 }
2830 }
2831
2832 // Set a hidden field so JS knows what edit form mode we are in
2833 if ( $this->isConflict ) {
2834 $mode = 'conflict';
2835 } elseif ( $this->preview ) {
2836 $mode = 'preview';
2837 } elseif ( $this->diff ) {
2838 $mode = 'diff';
2839 } else {
2840 $mode = 'text';
2841 }
2842 $wgOut->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
2843
2844 // Marker for detecting truncated form data. This must be the last
2845 // parameter sent in order to be of use, so do not move me.
2846 $wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2847 $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2848
2849 if ( !$wgUser->getOption( 'previewontop' ) ) {
2850 $this->displayPreviewArea( $previewOutput, false );
2851 }
2852 }
2853
2861 public function makeTemplatesOnThisPageList( array $templates ) {
2862 $templateListFormatter = new TemplatesOnThisPageFormatter(
2863 $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
2864 );
2865
2866 // preview if preview, else section if section, else false
2867 $type = false;
2868 if ( $this->preview ) {
2869 $type = 'preview';
2870 } elseif ( $this->section != '' ) {
2871 $type = 'section';
2872 }
2873
2874 return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2875 $templateListFormatter->format( $templates, $type )
2876 );
2877 }
2878
2885 public static function extractSectionTitle( $text ) {
2886 preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2887 if ( !empty( $matches[2] ) ) {
2889 return $wgParser->stripSectionName( trim( $matches[2] ) );
2890 } else {
2891 return false;
2892 }
2893 }
2894
2895 protected function showHeader() {
2898
2899 if ( $this->isConflict ) {
2900 $this->addExplainConflictHeader( $wgOut );
2901 $this->editRevId = $this->page->getLatest();
2902 } else {
2903 if ( $this->section != '' && $this->section != 'new' ) {
2904 if ( !$this->summary && !$this->preview && !$this->diff ) {
2905 $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2906 if ( $sectionTitle !== false ) {
2907 $this->summary = "/* $sectionTitle */ ";
2908 }
2909 }
2910 }
2911
2912 if ( $this->missingComment ) {
2913 $wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
2914 }
2915
2916 if ( $this->missingSummary && $this->section != 'new' ) {
2917 $wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
2918 }
2919
2920 if ( $this->missingSummary && $this->section == 'new' ) {
2921 $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
2922 }
2923
2924 if ( $this->blankArticle ) {
2925 $wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
2926 }
2927
2928 if ( $this->selfRedirect ) {
2929 $wgOut->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
2930 }
2931
2932 if ( $this->hookError !== '' ) {
2933 $wgOut->addWikiText( $this->hookError );
2934 }
2935
2936 if ( !$this->checkUnicodeCompliantBrowser() ) {
2937 $wgOut->addWikiMsg( 'nonunicodebrowser' );
2938 }
2939
2940 if ( $this->section != 'new' ) {
2941 $revision = $this->mArticle->getRevisionFetched();
2942 if ( $revision ) {
2943 // Let sysop know that this will make private content public if saved
2944
2945 if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
2946 $wgOut->wrapWikiMsg(
2947 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2948 'rev-deleted-text-permission'
2949 );
2950 } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
2951 $wgOut->wrapWikiMsg(
2952 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2953 'rev-deleted-text-view'
2954 );
2955 }
2956
2957 if ( !$revision->isCurrent() ) {
2958 $this->mArticle->setOldSubtitle( $revision->getId() );
2959 $wgOut->addWikiMsg( 'editingold' );
2960 $this->isOldRev = true;
2961 }
2962 } elseif ( $this->mTitle->exists() ) {
2963 // Something went wrong
2964
2965 $wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
2966 [ 'missing-revision', $this->oldid ] );
2967 }
2968 }
2969 }
2970
2971 if ( wfReadOnly() ) {
2972 $wgOut->wrapWikiMsg(
2973 "<div id=\"mw-read-only-warning\">\n$1\n</div>",
2974 [ 'readonlywarning', wfReadOnlyReason() ]
2975 );
2976 } elseif ( $wgUser->isAnon() ) {
2977 if ( $this->formtype != 'preview' ) {
2978 $wgOut->wrapWikiMsg(
2979 "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
2980 [ 'anoneditwarning',
2981 // Log-in link
2982 SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
2983 'returnto' => $this->getTitle()->getPrefixedDBkey()
2984 ] ),
2985 // Sign-up link
2986 SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
2987 'returnto' => $this->getTitle()->getPrefixedDBkey()
2988 ] )
2989 ]
2990 );
2991 } else {
2992 $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
2993 'anonpreviewwarning'
2994 );
2995 }
2996 } else {
2997 if ( $this->isCssJsSubpage ) {
2998 # Check the skin exists
2999 if ( $this->isWrongCaseCssJsPage ) {
3000 $wgOut->wrapWikiMsg(
3001 "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
3002 [ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
3003 );
3004 }
3005 if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
3006 $wgOut->wrapWikiMsg( '<div class="mw-usercssjspublic">$1</div>',
3007 $this->isCssSubpage ? 'usercssispublic' : 'userjsispublic'
3008 );
3009 if ( $this->formtype !== 'preview' ) {
3010 if ( $this->isCssSubpage && $wgAllowUserCss ) {
3011 $wgOut->wrapWikiMsg(
3012 "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3013 [ 'usercssyoucanpreview' ]
3014 );
3015 }
3016
3017 if ( $this->isJsSubpage && $wgAllowUserJs ) {
3018 $wgOut->wrapWikiMsg(
3019 "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3020 [ 'userjsyoucanpreview' ]
3021 );
3022 }
3023 }
3024 }
3025 }
3026 }
3027
3029
3030 $this->addLongPageWarningHeader();
3031
3032 # Add header copyright warning
3034 }
3035
3043 private function getSummaryInputAttributes( array $inputAttrs = null ) {
3044 // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
3045 return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3046 'id' => 'wpSummary',
3047 'name' => 'wpSummary',
3048 'maxlength' => '200',
3049 'tabindex' => '1',
3050 'size' => 60,
3051 'spellcheck' => 'true',
3052 ] + Linker::tooltipAndAccesskeyAttribs( 'summary' );
3053 }
3054
3069 public function getSummaryInput( $summary = "", $labelText = null,
3070 $inputAttrs = null, $spanLabelAttrs = null
3071 ) {
3072 $inputAttrs = $this->getSummaryInputAttributes( $inputAttrs );
3073
3074 $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
3075 'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
3076 'id' => "wpSummaryLabel"
3077 ];
3078
3079 $label = null;
3080 if ( $labelText ) {
3081 $label = Xml::tags(
3082 'label',
3083 $inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null,
3084 $labelText
3085 );
3086 $label = Xml::tags( 'span', $spanLabelAttrs, $label );
3087 }
3088
3089 $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
3090
3091 return [ $label, $input ];
3092 }
3093
3104 function getSummaryInputOOUI( $summary = "", $labelText = null, $inputAttrs = null ) {
3105 $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3106 $this->getSummaryInputAttributes( $inputAttrs )
3107 );
3108
3109 return new OOUI\FieldLayout(
3110 new OOUI\TextInputWidget( [
3111 'value' => $summary,
3112 'infusable' => true,
3113 ] + $inputAttrs ),
3114 [
3115 'label' => new OOUI\HtmlSnippet( $labelText ),
3116 'align' => 'top',
3117 'id' => 'wpSummaryLabel',
3118 'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3119 ]
3120 );
3121 }
3122
3129 protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3130 global $wgOut;
3131
3132 # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3133 $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3134 if ( $isSubjectPreview ) {
3135 if ( $this->nosummary ) {
3136 return;
3137 }
3138 } else {
3139 if ( !$this->mShowSummaryField ) {
3140 return;
3141 }
3142 }
3143
3144 $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3145 if ( $this->oouiEnabled ) {
3146 $wgOut->addHTML( $this->getSummaryInputOOUI(
3147 $summary,
3148 $labelText,
3149 [ 'class' => $summaryClass ]
3150 ) );
3151 } else {
3152 list( $label, $input ) = $this->getSummaryInput(
3153 $summary,
3154 $labelText,
3155 [ 'class' => $summaryClass ]
3156 );
3157 $wgOut->addHTML( "{$label} {$input}" );
3158 }
3159
3160 }
3161
3169 protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3170 // avoid spaces in preview, gets always trimmed on save
3171 $summary = trim( $summary );
3172 if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3173 return "";
3174 }
3175
3177
3178 if ( $isSubjectPreview ) {
3179 $summary = $this->context->msg( 'newsectionsummary' )
3180 ->rawParams( $wgParser->stripSectionName( $summary ) )
3181 ->inContentLanguage()->text();
3182 }
3183
3184 $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3185
3186 $summary = $this->context->msg( $message )->parse()
3187 . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3188 return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3189 }
3190
3191 protected function showFormBeforeText() {
3192 global $wgOut;
3193 $section = htmlspecialchars( $this->section );
3194 $wgOut->addHTML( <<<HTML
3195<input type='hidden' value="{$section}" name="wpSection"/>
3196<input type='hidden' value="{$this->starttime}" name="wpStarttime" />
3197<input type='hidden' value="{$this->edittime}" name="wpEdittime" />
3198<input type='hidden' value="{$this->editRevId}" name="editRevId" />
3199<input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
3200
3201HTML
3202 );
3203 if ( !$this->checkUnicodeCompliantBrowser() ) {
3204 $wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
3205 }
3206 }
3207
3208 protected function showFormAfterText() {
3222 $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
3223 }
3224
3233 protected function showContentForm() {
3234 $this->showTextbox1();
3235 }
3236
3245 protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3246 if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3247 $attribs = [ 'style' => 'display:none;' ];
3248 } else {
3249 $classes = []; // Textarea CSS
3250 if ( $this->mTitle->isProtected( 'edit' ) &&
3251 MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
3252 ) {
3253 # Is the title semi-protected?
3254 if ( $this->mTitle->isSemiProtected() ) {
3255 $classes[] = 'mw-textarea-sprotected';
3256 } else {
3257 # Then it must be protected based on static groups (regular)
3258 $classes[] = 'mw-textarea-protected';
3259 }
3260 # Is the title cascade-protected?
3261 if ( $this->mTitle->isCascadeProtected() ) {
3262 $classes[] = 'mw-textarea-cprotected';
3263 }
3264 }
3265 # Is an old revision being edited?
3266 if ( $this->isOldRev ) {
3267 $classes[] = 'mw-textarea-oldrev';
3268 }
3269
3270 $attribs = [ 'tabindex' => 1 ];
3271
3272 if ( is_array( $customAttribs ) ) {
3274 }
3275
3276 if ( count( $classes ) ) {
3277 if ( isset( $attribs['class'] ) ) {
3278 $classes[] = $attribs['class'];
3279 }
3280 $attribs['class'] = implode( ' ', $classes );
3281 }
3282 }
3283
3284 $this->showTextbox(
3285 $textoverride !== null ? $textoverride : $this->textbox1,
3286 'wpTextbox1',
3287 $attribs
3288 );
3289 }
3290
3291 protected function showTextbox2() {
3292 $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3293 }
3294
3295 protected function showTextbox( $text, $name, $customAttribs = [] ) {
3297
3298 $wikitext = $this->safeUnicodeOutput( $text );
3299 $wikitext = $this->addNewLineAtEnd( $wikitext );
3300
3302
3303 $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
3304 }
3305
3306 protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3307 global $wgOut;
3308 $classes = [];
3309 if ( $isOnTop ) {
3310 $classes[] = 'ontop';
3311 }
3312
3313 $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3314
3315 if ( $this->formtype != 'preview' ) {
3316 $attribs['style'] = 'display: none;';
3317 }
3318
3319 $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
3320
3321 if ( $this->formtype == 'preview' ) {
3322 $this->showPreview( $previewOutput );
3323 } else {
3324 // Empty content container for LivePreview
3325 $pageViewLang = $this->mTitle->getPageViewLanguage();
3326 $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3327 'class' => 'mw-content-' . $pageViewLang->getDir() ];
3328 $wgOut->addHTML( Html::rawElement( 'div', $attribs ) );
3329 }
3330
3331 $wgOut->addHTML( '</div>' );
3332
3333 if ( $this->formtype == 'diff' ) {
3334 try {
3335 $this->showDiff();
3336 } catch ( MWContentSerializationException $ex ) {
3337 $msg = $this->context->msg(
3338 'content-failed-to-parse',
3339 $this->contentModel,
3340 $this->contentFormat,
3341 $ex->getMessage()
3342 );
3343 $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3344 }
3345 }
3346 }
3347
3354 protected function showPreview( $text ) {
3355 global $wgOut;
3356 if ( $this->mArticle instanceof CategoryPage ) {
3357 $this->mArticle->openShowCategory();
3358 }
3359 # This hook seems slightly odd here, but makes things more
3360 # consistent for extensions.
3361 Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] );
3362 $wgOut->addHTML( $text );
3363 if ( $this->mArticle instanceof CategoryPage ) {
3364 $this->mArticle->closeShowCategory();
3365 }
3366 }
3367
3375 public function showDiff() {
3377
3378 $oldtitlemsg = 'currentrev';
3379 # if message does not exist, show diff against the preloaded default
3380 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3381 $oldtext = $this->mTitle->getDefaultMessageText();
3382 if ( $oldtext !== false ) {
3383 $oldtitlemsg = 'defaultmessagetext';
3384 $oldContent = $this->toEditContent( $oldtext );
3385 } else {
3386 $oldContent = null;
3387 }
3388 } else {
3389 $oldContent = $this->getCurrentContent();
3390 }
3391
3392 $textboxContent = $this->toEditContent( $this->textbox1 );
3393 if ( $this->editRevId !== null ) {
3394 $newContent = $this->page->replaceSectionAtRev(
3395 $this->section, $textboxContent, $this->summary, $this->editRevId
3396 );
3397 } else {
3398 $newContent = $this->page->replaceSectionContent(
3399 $this->section, $textboxContent, $this->summary, $this->edittime
3400 );
3401 }
3402
3403 if ( $newContent ) {
3404 Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3405
3407 $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
3408 }
3409
3410 if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3411 $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3412 $newtitle = $this->context->msg( 'yourtext' )->parse();
3413
3414 if ( !$oldContent ) {
3415 $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3416 }
3417
3418 if ( !$newContent ) {
3419 $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3420 }
3421
3422 $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
3423 $de->setContent( $oldContent, $newContent );
3424
3425 $difftext = $de->getDiff( $oldtitle, $newtitle );
3426 $de->showDiffStyle();
3427 } else {
3428 $difftext = '';
3429 }
3430
3431 $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3432 }
3433
3437 protected function showHeaderCopyrightWarning() {
3438 $msg = 'editpage-head-copy-warn';
3439 if ( !$this->context->msg( $msg )->isDisabled() ) {
3440 global $wgOut;
3441 $wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3442 'editpage-head-copy-warn' );
3443 }
3444 }
3445
3454 protected function showTosSummary() {
3455 $msg = 'editpage-tos-summary';
3456 Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3457 if ( !$this->context->msg( $msg )->isDisabled() ) {
3458 global $wgOut;
3459 $wgOut->addHTML( '<div class="mw-tos-summary">' );
3460 $wgOut->addWikiMsg( $msg );
3461 $wgOut->addHTML( '</div>' );
3462 }
3463 }
3464
3465 protected function showEditTools() {
3466 global $wgOut;
3467 $wgOut->addHTML( '<div class="mw-editTools">' .
3468 $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3469 '</div>' );
3470 }
3471
3478 protected function getCopywarn() {
3479 return self::getCopyrightWarning( $this->mTitle );
3480 }
3481
3490 public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3492 if ( $wgRightsText ) {
3493 $copywarnMsg = [ 'copyrightwarning',
3494 '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3495 $wgRightsText ];
3496 } else {
3497 $copywarnMsg = [ 'copyrightwarning2',
3498 '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3499 }
3500 // Allow for site and per-namespace customization of contribution/copyright notice.
3501 Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3502
3503 $msg = call_user_func_array( 'wfMessage', $copywarnMsg )->title( $title );
3504 if ( $langcode ) {
3505 $msg->inLanguage( $langcode );
3506 }
3507 return "<div id=\"editpage-copywarn\">\n" .
3508 $msg->$format() . "\n</div>";
3509 }
3510
3518 public static function getPreviewLimitReport( $output ) {
3519 if ( !$output || !$output->getLimitReportData() ) {
3520 return '';
3521 }
3522
3523 $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3524 wfMessage( 'limitreport-title' )->parseAsBlock()
3525 );
3526
3527 // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3528 $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3529
3530 $limitReport .= Html::openElement( 'table', [
3531 'class' => 'preview-limit-report wikitable'
3532 ] ) .
3533 Html::openElement( 'tbody' );
3534
3535 foreach ( $output->getLimitReportData() as $key => $value ) {
3536 if ( Hooks::run( 'ParserLimitReportFormat',
3537 [ $key, &$value, &$limitReport, true, true ]
3538 ) ) {
3539 $keyMsg = wfMessage( $key );
3540 $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3541 if ( !$valueMsg->exists() ) {
3542 $valueMsg = new RawMessage( '$1' );
3543 }
3544 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3545 $limitReport .= Html::openElement( 'tr' ) .
3546 Html::rawElement( 'th', null, $keyMsg->parse() ) .
3547 Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) .
3548 Html::closeElement( 'tr' );
3549 }
3550 }
3551 }
3552
3553 $limitReport .= Html::closeElement( 'tbody' ) .
3554 Html::closeElement( 'table' ) .
3555 Html::closeElement( 'div' );
3556
3557 return $limitReport;
3558 }
3559
3560 protected function showStandardInputs( &$tabindex = 2 ) {
3561 global $wgOut;
3562 $wgOut->addHTML( "<div class='editOptions'>\n" );
3563
3564 if ( $this->section != 'new' ) {
3565 $this->showSummaryInput( false, $this->summary );
3566 $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3567 }
3568
3569 if ( $this->oouiEnabled ) {
3570 $checkboxes = $this->getCheckboxesOOUI(
3571 $tabindex,
3572 [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3573 );
3574 $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] );
3575 } else {
3576 $checkboxes = $this->getCheckboxes(
3577 $tabindex,
3578 [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3579 );
3580 $checkboxesHTML = implode( $checkboxes, "\n" );
3581 }
3582
3583 $wgOut->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3584
3585 // Show copyright warning.
3586 $wgOut->addWikiText( $this->getCopywarn() );
3587 $wgOut->addHTML( $this->editFormTextAfterWarn );
3588
3589 $wgOut->addHTML( "<div class='editButtons'>\n" );
3590 $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
3591
3592 $cancel = $this->getCancelLink();
3593 if ( $cancel !== '' ) {
3594 $cancel .= Html::element( 'span',
3595 [ 'class' => 'mw-editButtons-pipe-separator' ],
3596 $this->context->msg( 'pipe-separator' )->text() );
3597 }
3598
3599 $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3600 $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3601 $edithelp =
3602 Html::linkButton(
3603 $this->context->msg( 'edithelp' )->text(),
3604 [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3605 [ 'mw-ui-quiet' ]
3606 ) .
3607 $this->context->msg( 'word-separator' )->escaped() .
3608 $this->context->msg( 'newwindow' )->parse();
3609
3610 $wgOut->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3611 $wgOut->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3612 $wgOut->addHTML( "</div><!-- editButtons -->\n" );
3613
3614 Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] );
3615
3616 $wgOut->addHTML( "</div><!-- editOptions -->\n" );
3617 }
3618
3623 protected function showConflict() {
3624 global $wgOut;
3625
3626 // Avoid PHP 7.1 warning of passing $this by reference
3627 $editPage = $this;
3628 if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$wgOut ] ) ) {
3629 $this->incrementConflictStats();
3630
3631 $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3632
3633 $content1 = $this->toEditContent( $this->textbox1 );
3634 $content2 = $this->toEditContent( $this->textbox2 );
3635
3636 $handler = ContentHandler::getForModelID( $this->contentModel );
3637 $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
3638 $de->setContent( $content2, $content1 );
3639 $de->showDiff(
3640 $this->context->msg( 'yourtext' )->parse(),
3641 $this->context->msg( 'storedversion' )->text()
3642 );
3643
3644 $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3645 $this->showTextbox2();
3646 }
3647 }
3648
3649 protected function incrementConflictStats() {
3650 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
3651 $stats->increment( 'edit.failures.conflict' );
3652 // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
3653 if (
3654 $this->mTitle->getNamespace() >= NS_MAIN &&
3655 $this->mTitle->getNamespace() <= NS_CATEGORY_TALK
3656 ) {
3657 $stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() );
3658 }
3659 }
3660
3664 public function getCancelLink() {
3665 $cancelParams = [];
3666 if ( !$this->isConflict && $this->oldid > 0 ) {
3667 $cancelParams['oldid'] = $this->oldid;
3668 } elseif ( $this->getContextTitle()->isRedirect() ) {
3669 $cancelParams['redirect'] = 'no';
3670 }
3671 if ( $this->oouiEnabled ) {
3672 return new OOUI\ButtonWidget( [
3673 'id' => 'mw-editform-cancel',
3674 'href' => $this->getContextTitle()->getLinkUrl( $cancelParams ),
3675 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3676 'framed' => false,
3677 'infusable' => true,
3678 'flags' => 'destructive',
3679 ] );
3680 } else {
3681 return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
3682 $this->getContextTitle(),
3683 new HtmlArmor( $this->context->msg( 'cancel' )->parse() ),
3684 Html::buttonAttributes( [ 'id' => 'mw-editform-cancel' ], [ 'mw-ui-quiet' ] ),
3685 $cancelParams
3686 );
3687 }
3688 }
3689
3699 protected function getActionURL( Title $title ) {
3700 return $title->getLocalURL( [ 'action' => $this->action ] );
3701 }
3702
3710 protected function wasDeletedSinceLastEdit() {
3711 if ( $this->deletedSinceEdit !== null ) {
3713 }
3714
3715 $this->deletedSinceEdit = false;
3716
3717 if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3718 $this->lastDelete = $this->getLastDelete();
3719 if ( $this->lastDelete ) {
3720 $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3721 if ( $deleteTime > $this->starttime ) {
3722 $this->deletedSinceEdit = true;
3723 }
3724 }
3725 }
3726
3728 }
3729
3733 protected function getLastDelete() {
3734 $dbr = wfGetDB( DB_REPLICA );
3735 $data = $dbr->selectRow(
3736 [ 'logging', 'user' ],
3737 [
3738 'log_type',
3739 'log_action',
3740 'log_timestamp',
3741 'log_user',
3742 'log_namespace',
3743 'log_title',
3744 'log_comment',
3745 'log_params',
3746 'log_deleted',
3747 'user_name'
3748 ], [
3749 'log_namespace' => $this->mTitle->getNamespace(),
3750 'log_title' => $this->mTitle->getDBkey(),
3751 'log_type' => 'delete',
3752 'log_action' => 'delete',
3753 'user_id=log_user'
3754 ],
3755 __METHOD__,
3756 [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ]
3757 );
3758 // Quick paranoid permission checks...
3759 if ( is_object( $data ) ) {
3760 if ( $data->log_deleted & LogPage::DELETED_USER ) {
3761 $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
3762 }
3763
3764 if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3765 $data->log_comment = $this->context->msg( 'rev-deleted-comment' )->escaped();
3766 }
3767 }
3768
3769 return $data;
3770 }
3771
3777 public function getPreviewText() {
3780
3781 if ( $wgRawHtml && !$this->mTokenOk ) {
3782 // Could be an offsite preview attempt. This is very unsafe if
3783 // HTML is enabled, as it could be an attack.
3784 $parsedNote = '';
3785 if ( $this->textbox1 !== '' ) {
3786 // Do not put big scary notice, if previewing the empty
3787 // string, which happens when you initially edit
3788 // a category page, due to automatic preview-on-open.
3789 $parsedNote = $wgOut->parse( "<div class='previewnote'>" .
3790 $this->context->msg( 'session_fail_preview_html' )->text() . "</div>",
3791 true, /* interface */true );
3792 }
3793 $this->incrementEditFailureStats( 'session_loss' );
3794 return $parsedNote;
3795 }
3796
3797 $note = '';
3798
3799 try {
3800 $content = $this->toEditContent( $this->textbox1 );
3801
3802 $previewHTML = '';
3803 if ( !Hooks::run(
3804 'AlternateEditPreview',
3805 [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3806 ) {
3807 return $previewHTML;
3808 }
3809
3810 # provide a anchor link to the editform
3811 $continueEditing = '<span class="mw-continue-editing">' .
3812 '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
3813 $this->context->msg( 'continue-editing' )->text() . ']]</span>';
3814 if ( $this->mTriedSave && !$this->mTokenOk ) {
3815 if ( $this->mTokenOkExceptSuffix ) {
3816 $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
3817 $this->incrementEditFailureStats( 'bad_token' );
3818 } else {
3819 $note = $this->context->msg( 'session_fail_preview' )->plain();
3820 $this->incrementEditFailureStats( 'session_loss' );
3821 }
3822 } elseif ( $this->incompleteForm ) {
3823 $note = $this->context->msg( 'edit_form_incomplete' )->plain();
3824 if ( $this->mTriedSave ) {
3825 $this->incrementEditFailureStats( 'incomplete_form' );
3826 }
3827 } else {
3828 $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
3829 }
3830
3831 # don't parse non-wikitext pages, show message about preview
3832 if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
3833 if ( $this->mTitle->isCssJsSubpage() ) {
3834 $level = 'user';
3835 } elseif ( $this->mTitle->isCssOrJsPage() ) {
3836 $level = 'site';
3837 } else {
3838 $level = false;
3839 }
3840
3841 if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3842 $format = 'css';
3843 if ( $level === 'user' && !$wgAllowUserCss ) {
3844 $format = false;
3845 }
3846 } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3847 $format = 'js';
3848 if ( $level === 'user' && !$wgAllowUserJs ) {
3849 $format = false;
3850 }
3851 } else {
3852 $format = false;
3853 }
3854
3855 # Used messages to make sure grep find them:
3856 # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
3857 if ( $level && $format ) {
3858 $note = "<div id='mw-{$level}{$format}preview'>" .
3859 $this->context->msg( "{$level}{$format}preview" )->text() .
3860 ' ' . $continueEditing . "</div>";
3861 }
3862 }
3863
3864 # If we're adding a comment, we need to show the
3865 # summary as the headline
3866 if ( $this->section === "new" && $this->summary !== "" ) {
3867 $content = $content->addSectionHeader( $this->summary );
3868 }
3869
3870 $hook_args = [ $this, &$content ];
3871 Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3872
3873 $parserResult = $this->doPreviewParse( $content );
3874 $parserOutput = $parserResult['parserOutput'];
3875 $previewHTML = $parserResult['html'];
3876 $this->mParserOutput = $parserOutput;
3877 $wgOut->addParserOutputMetadata( $parserOutput );
3878
3879 if ( count( $parserOutput->getWarnings() ) ) {
3880 $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3881 }
3882
3883 } catch ( MWContentSerializationException $ex ) {
3884 $m = $this->context->msg(
3885 'content-failed-to-parse',
3886 $this->contentModel,
3887 $this->contentFormat,
3888 $ex->getMessage()
3889 );
3890 $note .= "\n\n" . $m->parse();
3891 $previewHTML = '';
3892 }
3893
3894 if ( $this->isConflict ) {
3895 $conflict = '<h2 id="mw-previewconflict">'
3896 . $this->context->msg( 'previewconflict' )->escaped() . "</h2>\n";
3897 } else {
3898 $conflict = '<hr />';
3899 }
3900
3901 $previewhead = "<div class='previewnote'>\n" .
3902 '<h2 id="mw-previewheader">' . $this->context->msg( 'preview' )->escaped() . "</h2>" .
3903 $wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3904
3905 $pageViewLang = $this->mTitle->getPageViewLanguage();
3906 $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3907 'class' => 'mw-content-' . $pageViewLang->getDir() ];
3908 $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3909
3910 return $previewhead . $previewHTML . $this->previewTextAfterContent;
3911 }
3912
3913 private function incrementEditFailureStats( $failureType ) {
3914 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
3915 $stats->increment( 'edit.failures.' . $failureType );
3916 }
3917
3922 protected function getPreviewParserOptions() {
3923 $parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
3924 $parserOptions->setIsPreview( true );
3925 $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3926 $parserOptions->enableLimitReport();
3927 return $parserOptions;
3928 }
3929
3939 protected function doPreviewParse( Content $content ) {
3941 $parserOptions = $this->getPreviewParserOptions();
3942 $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
3943 $scopedCallback = $parserOptions->setupFakeRevision(
3944 $this->mTitle, $pstContent, $wgUser );
3945 $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
3946 ScopedCallback::consume( $scopedCallback );
3947 $parserOutput->setEditSectionTokens( false ); // no section edit links
3948 return [
3949 'parserOutput' => $parserOutput,
3950 'html' => $parserOutput->getText() ];
3951 }
3952
3956 public function getTemplates() {
3957 if ( $this->preview || $this->section != '' ) {
3958 $templates = [];
3959 if ( !isset( $this->mParserOutput ) ) {
3960 return $templates;
3961 }
3962 foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
3963 foreach ( array_keys( $template ) as $dbk ) {
3964 $templates[] = Title::makeTitle( $ns, $dbk );
3965 }
3966 }
3967 return $templates;
3968 } else {
3969 return $this->mTitle->getTemplateLinksFrom();
3970 }
3971 }
3972
3980 public static function getEditToolbar( $title = null ) {
3983
3984 $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
3985 $showSignature = true;
3986 if ( $title ) {
3987 $showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
3988 }
3989
3999 $toolarray = [
4000 [
4001 'id' => 'mw-editbutton-bold',
4002 'open' => '\'\'\'',
4003 'close' => '\'\'\'',
4004 'sample' => wfMessage( 'bold_sample' )->text(),
4005 'tip' => wfMessage( 'bold_tip' )->text(),
4006 ],
4007 [
4008 'id' => 'mw-editbutton-italic',
4009 'open' => '\'\'',
4010 'close' => '\'\'',
4011 'sample' => wfMessage( 'italic_sample' )->text(),
4012 'tip' => wfMessage( 'italic_tip' )->text(),
4013 ],
4014 [
4015 'id' => 'mw-editbutton-link',
4016 'open' => '[[',
4017 'close' => ']]',
4018 'sample' => wfMessage( 'link_sample' )->text(),
4019 'tip' => wfMessage( 'link_tip' )->text(),
4020 ],
4021 [
4022 'id' => 'mw-editbutton-extlink',
4023 'open' => '[',
4024 'close' => ']',
4025 'sample' => wfMessage( 'extlink_sample' )->text(),
4026 'tip' => wfMessage( 'extlink_tip' )->text(),
4027 ],
4028 [
4029 'id' => 'mw-editbutton-headline',
4030 'open' => "\n== ",
4031 'close' => " ==\n",
4032 'sample' => wfMessage( 'headline_sample' )->text(),
4033 'tip' => wfMessage( 'headline_tip' )->text(),
4034 ],
4035 $imagesAvailable ? [
4036 'id' => 'mw-editbutton-image',
4037 'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
4038 'close' => ']]',
4039 'sample' => wfMessage( 'image_sample' )->text(),
4040 'tip' => wfMessage( 'image_tip' )->text(),
4041 ] : false,
4042 $imagesAvailable ? [
4043 'id' => 'mw-editbutton-media',
4044 'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
4045 'close' => ']]',
4046 'sample' => wfMessage( 'media_sample' )->text(),
4047 'tip' => wfMessage( 'media_tip' )->text(),
4048 ] : false,
4049 [
4050 'id' => 'mw-editbutton-nowiki',
4051 'open' => "<nowiki>",
4052 'close' => "</nowiki>",
4053 'sample' => wfMessage( 'nowiki_sample' )->text(),
4054 'tip' => wfMessage( 'nowiki_tip' )->text(),
4055 ],
4056 $showSignature ? [
4057 'id' => 'mw-editbutton-signature',
4058 'open' => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(),
4059 'close' => '',
4060 'sample' => '',
4061 'tip' => wfMessage( 'sig_tip' )->text(),
4062 ] : false,
4063 [
4064 'id' => 'mw-editbutton-hr',
4065 'open' => "\n----\n",
4066 'close' => '',
4067 'sample' => '',
4068 'tip' => wfMessage( 'hr_tip' )->text(),
4069 ]
4070 ];
4071
4072 $script = 'mw.loader.using("mediawiki.toolbar", function () {';
4073 foreach ( $toolarray as $tool ) {
4074 if ( !$tool ) {
4075 continue;
4076 }
4077
4078 $params = [
4079 // Images are defined in ResourceLoaderEditToolbarModule
4080 false,
4081 // Note that we use the tip both for the ALT tag and the TITLE tag of the image.
4082 // Older browsers show a "speedtip" type message only for ALT.
4083 // Ideally these should be different, realistically they
4084 // probably don't need to be.
4085 $tool['tip'],
4086 $tool['open'],
4087 $tool['close'],
4088 $tool['sample'],
4089 $tool['id'],
4090 ];
4091
4092 $script .= Xml::encodeJsCall(
4093 'mw.toolbar.addButton',
4094 $params,
4096 );
4097 }
4098
4099 $script .= '});';
4100
4101 $toolbar = '<div id="toolbar"></div>';
4102
4103 if ( Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
4104 // Only add the old toolbar cruft to the page payload if the toolbar has not
4105 // been over-written by a hook caller
4106 $wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
4107 };
4108
4109 return $toolbar;
4110 }
4111
4130 protected function getCheckboxesDefinition( $checked ) {
4132 $checkboxes = [];
4133
4134 // don't show the minor edit checkbox if it's a new page or section
4135 if ( !$this->isNew && $wgUser->isAllowed( 'minoredit' ) ) {
4136 $checkboxes['wpMinoredit'] = [
4137 'id' => 'wpMinoredit',
4138 'label-message' => 'minoredit',
4139 // Uses messages: tooltip-minoredit, accesskey-minoredit
4140 'tooltip' => 'minoredit',
4141 'label-id' => 'mw-editpage-minoredit',
4142 'legacy-name' => 'minor',
4143 'default' => $checked['minor'],
4144 ];
4145 }
4146
4147 if ( $wgUser->isLoggedIn() ) {
4148 $checkboxes['wpWatchthis'] = [
4149 'id' => 'wpWatchthis',
4150 'label-message' => 'watchthis',
4151 // Uses messages: tooltip-watch, accesskey-watch
4152 'tooltip' => 'watch',
4153 'label-id' => 'mw-editpage-watch',
4154 'legacy-name' => 'watch',
4155 'default' => $checked['watch'],
4156 ];
4157 }
4158
4159 $editPage = $this;
4160 Hooks::run( 'EditPageGetCheckboxesDefinition', [ $editPage, &$checkboxes ] );
4161
4162 return $checkboxes;
4163 }
4164
4173 public function getCheckboxes( &$tabindex, $checked ) {
4175
4176 $checkboxes = [];
4177 $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4178
4179 // Backwards-compatibility for the EditPageBeforeEditChecks hook
4180 if ( !$this->isNew ) {
4181 $checkboxes['minor'] = '';
4182 }
4183 $checkboxes['watch'] = '';
4184
4185 foreach ( $checkboxesDef as $name => $options ) {
4186 $legacyName = isset( $options['legacy-name'] ) ? $options['legacy-name'] : $name;
4187 $label = $this->context->msg( $options['label-message'] )->parse();
4188 $attribs = [
4189 'tabindex' => ++$tabindex,
4190 'id' => $options['id'],
4191 ];
4192 $labelAttribs = [
4193 'for' => $options['id'],
4194 ];
4195 if ( isset( $options['tooltip'] ) ) {
4196 $attribs['accesskey'] = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4197 $labelAttribs['title'] = Linker::titleAttrib( $options['tooltip'], 'withaccess' );
4198 }
4199 if ( isset( $options['title-message'] ) ) {
4200 $labelAttribs['title'] = $this->context->msg( $options['title-message'] )->text();
4201 }
4202 if ( isset( $options['label-id'] ) ) {
4203 $labelAttribs['id'] = $options['label-id'];
4204 }
4205 $checkboxHtml =
4206 Xml::check( $name, $options['default'], $attribs ) .
4207 '&#160;' .
4208 Xml::tags( 'label', $labelAttribs, $label );
4209
4211 $checkboxHtml = Html::rawElement( 'div', [ 'class' => 'mw-ui-checkbox' ], $checkboxHtml );
4212 }
4213
4214 $checkboxes[ $legacyName ] = $checkboxHtml;
4215 }
4216
4217 // Avoid PHP 7.1 warning of passing $this by reference
4218 $editPage = $this;
4219 Hooks::run( 'EditPageBeforeEditChecks', [ &$editPage, &$checkboxes, &$tabindex ], '1.29' );
4220 return $checkboxes;
4221 }
4222
4233 public function getCheckboxesOOUI( &$tabindex, $checked ) {
4234 $checkboxes = [];
4235 $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4236
4237 $origTabindex = $tabindex;
4238
4239 foreach ( $checkboxesDef as $name => $options ) {
4240 $legacyName = isset( $options['legacy-name'] ) ? $options['legacy-name'] : $name;
4241
4242 $title = null;
4243 $accesskey = null;
4244 if ( isset( $options['tooltip'] ) ) {
4245 $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4246 $title = Linker::titleAttrib( $options['tooltip'], 'withaccess' );
4247 }
4248 if ( isset( $options['title-message'] ) ) {
4249 $title = $this->context->msg( $options['title-message'] )->text();
4250 }
4251 if ( isset( $options['label-id'] ) ) {
4252 $labelAttribs['id'] = $options['label-id'];
4253 }
4254
4255 $checkboxes[ $legacyName ] = new OOUI\FieldLayout(
4256 new OOUI\CheckboxInputWidget( [
4257 'tabIndex' => ++$tabindex,
4258 'accessKey' => $accesskey,
4259 'id' => $options['id'],
4260 'name' => $name,
4261 'selected' => $options['default'],
4262 'infusable' => true,
4263 ] ),
4264 [
4265 'align' => 'inline',
4266 'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4267 'title' => $title,
4268 'id' => isset( $options['label-id'] ) ? $options['label-id'] : null,
4269 ]
4270 );
4271 }
4272
4273 // Backwards-compatibility hack to run the EditPageBeforeEditChecks hook. It's important,
4274 // people have used it for the weirdest things completely unrelated to checkboxes...
4275 // And if we're gonna run it, might as well allow its legacy checkboxes to be shown.
4276 $legacyCheckboxes = $this->getCheckboxes( $origTabindex, $checked );
4277 foreach ( $legacyCheckboxes as $name => $html ) {
4278 if ( $html && !isset( $checkboxes[$name] ) ) {
4279 $checkboxes[$name] = new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $html ) ] );
4280 }
4281 }
4282
4283 return $checkboxes;
4284 }
4285
4294 public function getEditButtons( &$tabindex ) {
4295 $buttons = [];
4296
4297 $labelAsPublish =
4298 $this->mArticle->getContext()->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4299
4300 // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4301 if ( $labelAsPublish ) {
4302 $buttonLabelKey = !$this->mTitle->exists() ? 'publishpage' : 'publishchanges';
4303 } else {
4304 $buttonLabelKey = !$this->mTitle->exists() ? 'savearticle' : 'savechanges';
4305 }
4306 $attribs = [
4307 'id' => 'wpSave',
4308 'name' => 'wpSave',
4309 'tabindex' => ++$tabindex,
4311
4312 if ( $this->oouiEnabled ) {
4313 $saveConfig = OOUI\Element::configFromHtmlAttributes( $attribs );
4314 $buttons['save'] = new OOUI\ButtonInputWidget( [
4315 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4316 'useInputTag' => true,
4317 'flags' => [ 'constructive', 'primary' ],
4318 'label' => $this->context->msg( $buttonLabelKey )->text(),
4319 'infusable' => true,
4320 'type' => 'submit',
4321 ] + $saveConfig );
4322 } else {
4323 $buttons['save'] = Html::submitButton(
4324 $this->context->msg( $buttonLabelKey )->text(),
4325 $attribs,
4326 [ 'mw-ui-progressive' ]
4327 );
4328 }
4329
4330 $attribs = [
4331 'id' => 'wpPreview',
4332 'name' => 'wpPreview',
4333 'tabindex' => ++$tabindex,
4334 ] + Linker::tooltipAndAccesskeyAttribs( 'preview' );
4335 if ( $this->oouiEnabled ) {
4336 $previewConfig = OOUI\Element::configFromHtmlAttributes( $attribs );
4337 $buttons['preview'] = new OOUI\ButtonInputWidget( [
4338 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4339 'useInputTag' => true,
4340 'label' => $this->context->msg( 'showpreview' )->text(),
4341 'infusable' => true,
4342 'type' => 'submit'
4343 ] + $previewConfig );
4344 } else {
4345 $buttons['preview'] = Html::submitButton(
4346 $this->context->msg( 'showpreview' )->text(),
4347 $attribs
4348 );
4349 }
4350 $attribs = [
4351 'id' => 'wpDiff',
4352 'name' => 'wpDiff',
4353 'tabindex' => ++$tabindex,
4355 if ( $this->oouiEnabled ) {
4356 $diffConfig = OOUI\Element::configFromHtmlAttributes( $attribs );
4357 $buttons['diff'] = new OOUI\ButtonInputWidget( [
4358 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4359 'useInputTag' => true,
4360 'label' => $this->context->msg( 'showdiff' )->text(),
4361 'infusable' => true,
4362 'type' => 'submit',
4363 ] + $diffConfig );
4364 } else {
4365 $buttons['diff'] = Html::submitButton(
4366 $this->context->msg( 'showdiff' )->text(),
4367 $attribs
4368 );
4369 }
4370
4371 // Avoid PHP 7.1 warning of passing $this by reference
4372 $editPage = $this;
4373 Hooks::run( 'EditPageBeforeEditButtons', [ &$editPage, &$buttons, &$tabindex ] );
4374
4375 return $buttons;
4376 }
4377
4382 public function noSuchSectionPage() {
4383 global $wgOut;
4384
4385 $wgOut->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4386
4387 $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4388
4389 // Avoid PHP 7.1 warning of passing $this by reference
4390 $editPage = $this;
4391 Hooks::run( 'EditPageNoSuchSection', [ &$editPage, &$res ] );
4392 $wgOut->addHTML( $res );
4393
4394 $wgOut->returnToMain( false, $this->mTitle );
4395 }
4396
4402 public function spamPageWithContent( $match = false ) {
4404 $this->textbox2 = $this->textbox1;
4405
4406 if ( is_array( $match ) ) {
4407 $match = $wgLang->listToText( $match );
4408 }
4409 $wgOut->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4410
4411 $wgOut->addHTML( '<div id="spamprotected">' );
4412 $wgOut->addWikiMsg( 'spamprotectiontext' );
4413 if ( $match ) {
4414 $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4415 }
4416 $wgOut->addHTML( '</div>' );
4417
4418 $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4419 $this->showDiff();
4420
4421 $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4422 $this->showTextbox2();
4423
4424 $wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4425 }
4426
4433 private function checkUnicodeCompliantBrowser() {
4435
4436 $currentbrowser = $wgRequest->getHeader( 'User-Agent' );
4437 if ( $currentbrowser === false ) {
4438 // No User-Agent header sent? Trust it by default...
4439 return true;
4440 }
4441
4442 foreach ( $wgBrowserBlackList as $browser ) {
4443 if ( preg_match( $browser, $currentbrowser ) ) {
4444 return false;
4445 }
4446 }
4447 return true;
4448 }
4449
4458 protected function safeUnicodeInput( $request, $field ) {
4459 $text = rtrim( $request->getText( $field ) );
4460 return $request->getBool( 'safemode' )
4461 ? $this->unmakeSafe( $text )
4462 : $text;
4463 }
4464
4472 protected function safeUnicodeOutput( $text ) {
4473 return $this->checkUnicodeCompliantBrowser()
4474 ? $text
4475 : $this->makeSafe( $text );
4476 }
4477
4490 private function makeSafe( $invalue ) {
4491 // Armor existing references for reversibility.
4492 $invalue = strtr( $invalue, [ "&#x" => "&#x0" ] );
4493
4494 $bytesleft = 0;
4495 $result = "";
4496 $working = 0;
4497 $valueLength = strlen( $invalue );
4498 for ( $i = 0; $i < $valueLength; $i++ ) {
4499 $bytevalue = ord( $invalue[$i] );
4500 if ( $bytevalue <= 0x7F ) { // 0xxx xxxx
4501 $result .= chr( $bytevalue );
4502 $bytesleft = 0;
4503 } elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx
4504 $working = $working << 6;
4505 $working += ( $bytevalue & 0x3F );
4506 $bytesleft--;
4507 if ( $bytesleft <= 0 ) {
4508 $result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
4509 }
4510 } elseif ( $bytevalue <= 0xDF ) { // 110x xxxx
4511 $working = $bytevalue & 0x1F;
4512 $bytesleft = 1;
4513 } elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx
4514 $working = $bytevalue & 0x0F;
4515 $bytesleft = 2;
4516 } else { // 1111 0xxx
4517 $working = $bytevalue & 0x07;
4518 $bytesleft = 3;
4519 }
4520 }
4521 return $result;
4522 }
4523
4532 private function unmakeSafe( $invalue ) {
4533 $result = "";
4534 $valueLength = strlen( $invalue );
4535 for ( $i = 0; $i < $valueLength; $i++ ) {
4536 if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) {
4537 $i += 3;
4538 $hexstring = "";
4539 do {
4540 $hexstring .= $invalue[$i];
4541 $i++;
4542 } while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
4543
4544 // Do some sanity checks. These aren't needed for reversibility,
4545 // but should help keep the breakage down if the editor
4546 // breaks one of the entities whilst editing.
4547 if ( ( substr( $invalue, $i, 1 ) == ";" ) && ( strlen( $hexstring ) <= 6 ) ) {
4548 $codepoint = hexdec( $hexstring );
4549 $result .= UtfNormal\Utils::codepointToUtf8( $codepoint );
4550 } else {
4551 $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
4552 }
4553 } else {
4554 $result .= substr( $invalue, $i, 1 );
4555 }
4556 }
4557 // reverse the transform that we made for reversibility reasons.
4558 return strtr( $result, [ "&#x0" => "&#x" ] );
4559 }
4560
4564 protected function addEditNotices() {
4565 global $wgOut;
4566
4567 $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4568 if ( count( $editNotices ) ) {
4569 $wgOut->addHTML( implode( "\n", $editNotices ) );
4570 } else {
4571 $msg = $this->context->msg( 'editnotice-notext' );
4572 if ( !$msg->isDisabled() ) {
4573 $wgOut->addHTML(
4574 '<div class="mw-editnotice-notext">'
4575 . $msg->parseAsBlock()
4576 . '</div>'
4577 );
4578 }
4579 }
4580 }
4581
4585 protected function addTalkPageText() {
4586 global $wgOut;
4587
4588 if ( $this->mTitle->isTalkPage() ) {
4589 $wgOut->addWikiMsg( 'talkpagetext' );
4590 }
4591 }
4592
4596 protected function addLongPageWarningHeader() {
4598
4599 if ( $this->contentLength === false ) {
4600 $this->contentLength = strlen( $this->textbox1 );
4601 }
4602
4603 if ( $this->tooBig || $this->contentLength > $wgMaxArticleSize * 1024 ) {
4604 $wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4605 [
4606 'longpageerror',
4607 $wgLang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4608 $wgLang->formatNum( $wgMaxArticleSize )
4609 ]
4610 );
4611 } else {
4612 if ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
4613 $wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
4614 [
4615 'longpage-hint',
4616 $wgLang->formatSize( strlen( $this->textbox1 ) ),
4617 strlen( $this->textbox1 )
4618 ]
4619 );
4620 }
4621 }
4622 }
4623
4627 protected function addPageProtectionWarningHeaders() {
4628 global $wgOut;
4629
4630 if ( $this->mTitle->isProtected( 'edit' ) &&
4631 MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
4632 ) {
4633 # Is the title semi-protected?
4634 if ( $this->mTitle->isSemiProtected() ) {
4635 $noticeMsg = 'semiprotectedpagewarning';
4636 } else {
4637 # Then it must be protected based on static groups (regular)
4638 $noticeMsg = 'protectedpagewarning';
4639 }
4640 LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
4641 [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4642 }
4643 if ( $this->mTitle->isCascadeProtected() ) {
4644 # Is this page under cascading protection from some source pages?
4646 list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4647 $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
4648 $cascadeSourcesCount = count( $cascadeSources );
4649 if ( $cascadeSourcesCount > 0 ) {
4650 # Explain, and list the titles responsible
4651 foreach ( $cascadeSources as $page ) {
4652 $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4653 }
4654 }
4655 $notice .= '</div>';
4656 $wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4657 }
4658 if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4659 LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
4660 [ 'lim' => 1,
4661 'showIfEmpty' => false,
4662 'msgKey' => [ 'titleprotectedwarning' ],
4663 'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4664 }
4665 }
4666
4672 $out->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
4673 }
4674
4682 protected function buildTextboxAttribs( $name, array $customAttribs, User $user ) {
4684 'accesskey' => ',',
4685 'id' => $name,
4686 'cols' => 80,
4687 'rows' => 25,
4688 // Avoid PHP notices when appending preferences
4689 // (appending allows customAttribs['style'] to still work).
4690 'style' => ''
4691 ];
4692
4693 // The following classes can be used here:
4694 // * mw-editfont-default
4695 // * mw-editfont-monospace
4696 // * mw-editfont-sans-serif
4697 // * mw-editfont-serif
4698 $class = 'mw-editfont-' . $user->getOption( 'editfont' );
4699
4700 if ( isset( $attribs['class'] ) ) {
4701 if ( is_string( $attribs['class'] ) ) {
4702 $attribs['class'] .= ' ' . $class;
4703 } elseif ( is_array( $attribs['class'] ) ) {
4704 $attribs['class'][] = $class;
4705 }
4706 } else {
4707 $attribs['class'] = $class;
4708 }
4709
4710 $pageLang = $this->mTitle->getPageLanguage();
4711 $attribs['lang'] = $pageLang->getHtmlCode();
4712 $attribs['dir'] = $pageLang->getDir();
4713
4714 return $attribs;
4715 }
4716
4722 protected function addNewLineAtEnd( $wikitext ) {
4723 if ( strval( $wikitext ) !== '' ) {
4724 // Ensure there's a newline at the end, otherwise adding lines
4725 // is awkward.
4726 // But don't add a newline if the text is empty, or Firefox in XHTML
4727 // mode will show an extra newline. A bit annoying.
4728 $wikitext .= "\n";
4729 return $wikitext;
4730 }
4731 return $wikitext;
4732 }
4733}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
$wgPreviewOnOpenNamespaces
Which namespaces have special treatment where they should be preview-on-open Internally only Category...
$wgAllowUserJs
Allow user Javascript page? This enables a lot of neat customizations, but may increase security risk...
$wgMaxArticleSize
Maximum article size in kilobytes.
$wgBrowserBlackList
Browser Blacklist for unicode non compliant browsers.
$wgRawHtml
Allow raw, unchecked HTML in "<html>...</html>" sections.
$wgAllowUserCss
Allow user Cascading Style Sheets (CSS)? This enables a lot of neat customizations,...
$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.
$wgAjaxEditStash
Have clients send edits to be prepared when filling in edit summaries.
$wgUseMediaWikiUIEverywhere
Temporary variable that applies MediaWiki UI wherever it can be supported.
$wgForeignFileRepos
$wgContentHandlerUseDB
Set to false to disable use of the database fields introduced by the ContentHandler facility.
$wgOOUIEditPage
Temporary variable that determines whether the EditPage class should use OOjs UI or not.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfReadOnly()
Check whether the wiki is in read-only mode.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfFindFile( $title, $options=[])
Find a file.
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
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,...
$wgUser
Definition Setup.php:781
$wgOut
Definition Setup.php:791
$wgParser
Definition Setup.php:796
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:639
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:1113
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...
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
static getForTitle(Title $title)
Returns the appropriate ContentHandler singleton for the given title.
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
static getContentText(Content $content=null)
Convenience function for getting flat text from a Content object.
getRequest()
Get the WebRequest object.
msg()
Get a Message object with context set Parameters are the same as wfMessage()
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition EditPage.php:42
getPreviewParserOptions()
Get parser options for a preview.
getCheckboxes(&$tabindex, $checked)
Returns an array of html code of the following checkboxes old style: minor and watch.
string $sectiontitle
Definition EditPage.php:346
checkUnicodeCompliantBrowser()
Check if the browser is on a blacklist of user-agents known to mangle UTF-8 data on form submission.
getActionURL(Title $title)
Returns the URL to use in the form's action attribute.
makeSafe( $invalue)
A number of web browsers are known to corrupt non-ASCII characters in a UTF-8 text editing environmen...
isOouiEnabled()
Check if the edit page is using OOUI controls.
Definition EditPage.php:492
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:248
showTextbox( $text, $name, $customAttribs=[])
string $hookError
Definition EditPage.php:290
attemptSave(&$resultDetails=false)
Attempt submission.
$editFormTextAfterTools
Definition EditPage.php:383
isSectionEditSupported()
Returns whether section editing is supported for the current page.
Definition EditPage.php:850
WikiPage $page
Definition EditPage.php:209
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:161
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:370
bool $allowBlankSummary
Definition EditPage.php:272
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:711
bool $firsttime
Definition EditPage.php:245
bool $isCssSubpage
Definition EditPage.php:227
const AS_CANNOT_USE_CUSTOM_MODEL
Status: when changing the content model is disallowed due to $wgContentHandlerUseDB being false.
Definition EditPage.php:178
runPostMergeFilters(Content $content, Status $status, User $user)
Run hooks that can filter edits just before they get saved.
bool $bot
Definition EditPage.php:364
showPreview( $text)
Append preview output to $wgOut.
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition EditPage.php:61
addTalkPageText()
const AS_READ_ONLY_PAGE_ANON
Status: this anonymous user is not allowed to edit this page.
Definition EditPage.php:76
string $textbox2
Definition EditPage.php:328
bool $tooBig
Definition EditPage.php:263
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()
isSupportedContentModel( $modelId)
Returns if the given content model is editable.
Definition EditPage.php:503
$editFormTextAfterContent
Definition EditPage.php:385
$editFormTextBottom
Definition EditPage.php:384
$editFormTextBeforeContent
Definition EditPage.php:381
string $contentModel
Definition EditPage.php:367
bool $deletedSinceEdit
Definition EditPage.php:239
__construct(Article $article)
Definition EditPage.php:424
getEditPermissionErrors( $rigor='secure')
Definition EditPage.php:666
showStandardInputs(&$tabindex=2)
toEditContent( $text)
Turns the given text into a Content object by unserializing it.
int $oldid
Definition EditPage.php:352
const POST_EDIT_COOKIE_KEY_PREFIX
Prefix of key for cookie used to pass post-edit state.
Definition EditPage.php:189
const AS_MAX_ARTICLE_SIZE_EXCEEDED
Status: article is too big (> $wgMaxArticleSize), after merging in the new section.
Definition EditPage.php:129
integer $editRevId
Definition EditPage.php:340
const AS_CONFLICT_DETECTED
Status: (non-resolvable) edit conflict.
Definition EditPage.php:113
addExplainConflictHeader(OutputPage $out)
$editFormTextAfterWarn
Definition EditPage.php:382
isWrongCaseCssJsPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition EditPage.php:829
bool $mTokenOk
Definition EditPage.php:251
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:139
wasDeletedSinceLastEdit()
Check if a page was deleted while the user was editing it, before submit.
const AS_READ_ONLY_PAGE_LOGGED
Status: this logged in user is not allowed to edit this page.
Definition EditPage.php:81
bool $mShowSummaryField
Definition EditPage.php:302
showFormAfterText()
bool $recreate
Definition EditPage.php:322
int $parentRevId
Definition EditPage.php:355
bool $oouiEnabled
Whether OOUI should be enabled here.
Definition EditPage.php:419
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:296
const AS_BLANK_ARTICLE
Status: user tried to create a blank page and wpIgnoreBlankArticle == false.
Definition EditPage.php:108
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:860
static extractSectionTitle( $text)
Extract the section title from current section text, if any.
bool $missingSummary
Definition EditPage.php:269
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:212
bool int $contentLength
Definition EditPage.php:399
const AS_PARSE_ERROR
Status: can't parse content.
Definition EditPage.php:172
bool $blankArticle
Definition EditPage.php:275
noSuchSectionPage()
Creates a basic error page which informs the user that they have attempted to edit a nonexistent sect...
bool $mTriedSave
Definition EditPage.php:257
showConflict()
Show an edit conflict.
const AS_READ_ONLY_PAGE
Status: wiki is in readonly mode (wfReadOnly() == true)
Definition EditPage.php:86
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:343
ParserOutput $mParserOutput
Definition EditPage.php:293
previewOnOpen()
Should we show a preview when the edit form is first shown?
Definition EditPage.php:796
displayPreviewArea( $previewOutput, $isOnTop=false)
doPreviewParse(Content $content)
Parse the page for a preview.
string $formtype
Definition EditPage.php:242
bool $allowBlankArticle
Definition EditPage.php:278
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing.
Definition EditPage.php:514
internalAttemptSave(&$result, $bot=false)
Attempt submission (no UI)
bool $isWrongCaseCssJsPage
Definition EditPage.php:233
string $summary
Definition EditPage.php:331
bool $save
Definition EditPage.php:307
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition EditPage.php:71
bool $diff
Definition EditPage.php:313
getCurrentContent()
Get the current content of the page.
bool $selfRedirect
Definition EditPage.php:281
string $textbox1
Definition EditPage.php:325
addContentModelChangeLogEntry(User $user, $oldModel, $newModel, $reason)
const EDITFORM_ID
HTML id and name for the beginning of the edit form.
Definition EditPage.php:183
const AS_CHANGE_TAG_ERROR
Status: an error relating to change tagging.
Definition EditPage.php:167
string $editFormPageTop
Before even the preview.
Definition EditPage.php:379
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
Definition EditPage.php:66
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:155
getCopywarn()
Get the copyright warning.
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition EditPage.php:204
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:479
toEditText( $content)
Gets an editable textual representation of $content.
null Title $mContextTitle
Definition EditPage.php:215
string $autoSumm
Definition EditPage.php:287
bool $isOldRev
Whether an old revision is edited.
Definition EditPage.php:414
const AS_IMAGE_REDIRECT_LOGGED
Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
Definition EditPage.php:149
const AS_END
Status: WikiPage::doEdit() was unsuccessful.
Definition EditPage.php:134
const AS_ARTICLE_WAS_DELETED
Status: article was deleted while editing and param wpRecreate == false or form was not posted.
Definition EditPage.php:97
const AS_TEXTBOX_EMPTY
Status: user tried to create a new section without content.
Definition EditPage.php:124
bool $mTokenOkExceptSuffix
Definition EditPage.php:254
newSectionSummary(&$sectionanchor=null)
Return the summary to be used for a new section.
const AS_SUCCESS_UPDATE
Status: Article successfully updated.
Definition EditPage.php:46
getBaseRevision()
bool $isCssJsSubpage
Definition EditPage.php:224
string $starttime
Definition EditPage.php:349
bool $isNew
New page or new section.
Definition EditPage.php:236
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:207
getSummaryInputOOUI( $summary="", $labelText=null, $inputAttrs=null)
Same as self::getSummaryInput, but uses OOUI, instead of plain HTML.
bool $allowSelfRedirect
Definition EditPage.php:284
const AS_IMAGE_REDIRECT_ANON
Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
Definition EditPage.php:144
getContentObject( $def_content=null)
bool $isJsSubpage
Definition EditPage.php:230
bool $isConflict
Definition EditPage.php:221
null $scrolltop
Definition EditPage.php:361
getLastDelete()
string $action
Definition EditPage.php:218
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:536
string $editintro
Definition EditPage.php:358
bool $enableApiEditOverride
Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing.
Definition EditPage.php:404
displayViewSourcePage(Content $content, $errorMessage='')
Display a read-only View Source page.
Definition EditPage.php:742
bool $watchthis
Definition EditPage.php:319
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:103
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:316
$previewTextAfterContent
Definition EditPage.php:386
getSummaryPreview( $isSubjectPreview, $summary="")
unmakeSafe( $invalue)
Reverse the previously applied transliteration of non-ASCII characters back to UTF-8.
bool $nosummary
Definition EditPage.php:334
bool $incompleteForm
Definition EditPage.php:260
const AS_SUCCESS_NEW_ARTICLE
Status: Article successfully created.
Definition EditPage.php:51
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:266
string $edittime
Definition EditPage.php:337
showTosSummary()
Give a chance for site and per-namespace customizations of terms of service summary link that might e...
getPreviewText()
Get the rendered text for previewing.
getSummaryInput( $summary="", $labelText=null, $inputAttrs=null, $spanLabelAttrs=null)
Standard summary input and label (wgSummary), abstracted so EditPage subclasses may reorganize the fo...
showContentForm()
Subpage overridable method for printing the form for page content editing By default this simply outp...
bool $edit
Definition EditPage.php:396
incrementConflictStats()
getCheckboxesOOUI(&$tabindex, $checked)
Returns an array of html code of the following checkboxes: minor and watch.
IContextSource $context
Definition EditPage.php:409
Revision bool $mBaseRevision
Definition EditPage.php:299
bool $preview
Definition EditPage.php:310
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)
showEditForm( $formCallback=null)
Send the edit form and related headers to $wgOut.
null array $changeTags
Definition EditPage.php:373
const AS_RATE_LIMITED
Status: rate limiter for action 'edit' was tripped.
Definition EditPage.php:91
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
Definition EditPage.php:56
setContextTitle( $title)
Set the context Title object.
Definition EditPage.php:468
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:119
An error page which can definitely be safely rendered using the OutputPage.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:28
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:1938
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:1439
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[])
Returns the attributes for the tooltip and access key.
Definition Linker.php:2098
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition Linker.php:1886
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:396
setPerformer(User $performer)
Set the user that performed the action being logged.
Definition LogEntry.php:493
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...
static newFromUser( $user)
Get a ParserOptions object from a given user.
static newFromUserAndLang(User $user, Language $lang)
Get a ParserOptions object from a given user and language.
Show an error when a user tries to do something they do not have the necessary permissions for.
Variant of the Message class.
Definition Message.php:1379
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
static getMain()
Static methods.
static inDebugMode()
Determine whether debug mode was requested Order of priority is 1) request param, 2) cookie,...
static makeInlineScript( $script)
Construct an inline script tag with given JS code.
static loadFromTitle( $db, $title, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
Definition Revision.php:282
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition Revision.php:307
const DELETED_TEXT
Definition Revision.php:90
static userWasLastToEdit( $db, $pageId, $userId, $since)
Check if no edits were made by other users since the time a user started editing the page.
const RAW
Definition Revision.php:100
const FOR_THIS_USER
Definition Revision.php:99
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:116
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition Skin.php:1132
static getSkinNames()
Fetch the set of available skins.
Definition Skin.php:49
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
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:972
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:50
static doWatchOrUnwatch( $watch, Title $title, User $user)
Watch or unwatch a page.
Class representing a MediaWiki article and history.
Definition WikiPage.php:36
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition WikiPage.php:120
getRedirectTarget()
If this page is a redirect, get its target.
Definition WikiPage.php:832
getContent( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition WikiPage.php:663
isRedirect()
Tests if the article content represents a redirect.
Definition WikiPage.php:489
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
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 name
Definition design.txt:12
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:154
const EDIT_UPDATE
Definition Defines.php:151
const NS_USER
Definition Defines.php:64
const CONTENT_MODEL_CSS
Definition Defines.php:235
const NS_FILE
Definition Defines.php:68
const NS_MAIN
Definition Defines.php:62
const NS_MEDIAWIKI
Definition Defines.php:70
const NS_CATEGORY_TALK
Definition Defines.php:77
const NS_MEDIA
Definition Defines.php:50
const NS_USER_TALK
Definition Defines.php:65
const EDIT_MINOR
Definition Defines.php:152
const CONTENT_MODEL_JAVASCRIPT
Definition Defines.php:234
const EDIT_AUTOSUMMARY
Definition Defines.php:156
const EDIT_NEW
Definition Defines.php:150
the array() calling protocol came about after MediaWiki 1.4rc1.
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context the output can only depend on parameters provided to this hook not on global state indicating whether full HTML should be generated If generation of HTML may be but other information should still be present in the ParserOutput object & $output
Definition hooks.txt:1108
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context $parserOutput
Definition hooks.txt:1096
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:249
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED! Use $magicWords in a file listed in $wgExtensionMessagesFiles instead. Use this to define synonyms of magic words depending of the language & $magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED! Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead. Use to define aliases of special pages names depending of the language & $specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links: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:1954
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
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context $options
Definition hooks.txt:1102
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context the output can only depend on parameters provided to this hook not on global state indicating whether full HTML should be generated If generation of HTML may be but other information should still be present in the ParserOutput object to manipulate or replace but no entry for that model exists in $wgContentHandlers please use GetContentModels hook to make them known to core if desired 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 inclusive false for true for descending in case the handler function wants to provide a converted Content object Note that $result getContentModel() must return $toModel. 'CustomEditor' also included in $newHeader if any indicating whether we should show just the diff
Definition hooks.txt:1236
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content $content
Definition hooks.txt:1100
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
null means default & $customAttribs
Definition hooks.txt:1956
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:1572
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition hooks.txt:2753
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:1967
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached my talk my contributions etc etc otherwise the built in rate limiting checks are if enabled allows for interception of redirect as a string mapping parameter names to values & $type
Definition hooks.txt:2604
error also a ContextSource you ll probably need to make sure the header is varied on $request
Definition hooks.txt:2723
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:1409
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:1974
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:785
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:304
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:1975
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached my talk page
Definition hooks.txt:2579
this hook is for auditing only $response
Definition hooks.txt:783
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function array $article
Definition hooks.txt:78
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition hooks.txt:1049
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:1601
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:1751
processing should stop and the error should be shown to the user * false
Definition hooks.txt:189
returning false will NOT prevent logging $e
Definition hooks.txt:2127
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
Base interface for content objects.
Definition Content.php:34
Interface for objects which can provide a MediaWiki context on request.
This document describes the state of Postgres support in and is fairly well maintained The main code is very well while extensions are very hit and miss it is probably the most supported database after MySQL Much of the work in making MediaWiki database agnostic came about through the work of creating Postgres as and are nearing end of but without copying over all the usage comments General notes on the but these can almost always be programmed around *Although Postgres has a true BOOLEAN type
Definition postgres.txt:36
if(is_array($mode)) switch( $mode) $input
The First
Definition primes.txt:1
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26
$params