MediaWiki REL1_30
EditPage.php
Go to the documentation of this file.
1<?php
25use Wikimedia\ScopedCallback;
26
42class EditPage {
46 const UNICODE_CHECK = 'ℳ𝒲β™₯π“Šπ“ƒπ’Ύπ’Έβ„΄π’Ήβ„―';
47
51 const AS_SUCCESS_UPDATE = 200;
52
57
61 const AS_HOOK_ERROR = 210;
62
67
72
76 const AS_CONTENT_TOO_BIG = 216;
77
82
87
91 const AS_READ_ONLY_PAGE = 220;
92
96 const AS_RATE_LIMITED = 221;
97
103
109
113 const AS_BLANK_ARTICLE = 224;
114
119
124 const AS_SUMMARY_NEEDED = 226;
125
129 const AS_TEXTBOX_EMPTY = 228;
130
135
139 const AS_END = 231;
140
144 const AS_SPAM_ERROR = 232;
145
150
155
161
166 const AS_SELF_REDIRECT = 236;
167
173
177 const AS_PARSE_ERROR = 240;
178
184
189
193 const EDITFORM_ID = 'editform';
194
199 const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
200
215
220 public $mArticle;
222 private $page;
223
228 public $mTitle;
229
231 private $mContextTitle = null;
232
234 public $action = 'submit';
235
237 public $isConflict = false;
238
243 public $isCssJsSubpage = false;
244
249 public $isCssSubpage = false;
250
255 public $isJsSubpage = false;
256
261 public $isWrongCaseCssJsPage = false;
262
264 public $isNew = false;
265
268
270 public $formtype;
271
274
277
279 public $mTokenOk = false;
280
282 public $mTokenOkExceptSuffix = false;
283
285 public $mTriedSave = false;
286
288 public $incompleteForm = false;
289
291 public $tooBig = false;
292
294 public $missingComment = false;
295
297 public $missingSummary = false;
298
300 public $allowBlankSummary = false;
301
303 protected $blankArticle = false;
304
306 protected $allowBlankArticle = false;
307
309 protected $selfRedirect = false;
310
312 protected $allowSelfRedirect = false;
313
315 public $autoSumm = '';
316
318 public $hookError = '';
319
322
324 public $hasPresetSummary = false;
325
327 public $mBaseRevision = false;
328
330 public $mShowSummaryField = true;
331
332 # Form values
333
335 public $save = false;
336
338 public $preview = false;
339
341 public $diff = false;
342
344 public $minoredit = false;
345
347 public $watchthis = false;
348
350 public $recreate = false;
351
353 public $textbox1 = '';
354
356 public $textbox2 = '';
357
359 public $summary = '';
360
362 public $nosummary = false;
363
365 public $edittime = '';
366
368 private $editRevId = null;
369
371 public $section = '';
372
374 public $sectiontitle = '';
375
377 public $starttime = '';
378
380 public $oldid = 0;
381
383 public $parentRevId = 0;
384
386 public $editintro = '';
387
389 public $scrolltop = null;
390
392 public $bot = true;
393
396
398 public $contentFormat = null;
399
401 private $changeTags = null;
402
403 # Placeholders for text injection by hooks (must be HTML)
404 # extensions should take care to _append_ to the present value
405
407 public $editFormPageTop = '';
408 public $editFormTextTop = '';
415 public $mPreloadContent = null;
416
417 /* $didSave should be set to true whenever an article was successfully altered. */
418 public $didSave = false;
419 public $undidRev = 0;
420
421 public $suppressIntro = false;
422
424 protected $edit;
425
427 protected $contentLength = false;
428
432 private $enableApiEditOverride = false;
433
437 protected $context;
438
442 private $isOldRev = false;
443
448
452 public function __construct( Article $article ) {
453 $this->mArticle = $article;
454 $this->page = $article->getPage(); // model object
455 $this->mTitle = $article->getTitle();
456 $this->context = $article->getContext();
457
458 $this->contentModel = $this->mTitle->getContentModel();
459
460 $handler = ContentHandler::getForModelID( $this->contentModel );
461 $this->contentFormat = $handler->getDefaultFormat();
462 }
463
467 public function getArticle() {
468 return $this->mArticle;
469 }
470
475 public function getContext() {
476 return $this->context;
477 }
478
483 public function getTitle() {
484 return $this->mTitle;
485 }
486
492 public function setContextTitle( $title ) {
493 $this->mContextTitle = $title;
494 }
495
503 public function getContextTitle() {
504 if ( is_null( $this->mContextTitle ) ) {
506 'GlobalTitleFail',
507 __METHOD__ . ' called by ' . wfGetAllCallers( 5 ) . ' with no title set.'
508 );
510 return $wgTitle;
511 } else {
513 }
514 }
515
521 public function isOouiEnabled() {
522 return true;
523 }
524
532 public function isSupportedContentModel( $modelId ) {
533 return $this->enableApiEditOverride === true ||
534 ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
535 }
536
543 public function setApiEditOverride( $enableOverride ) {
544 $this->enableApiEditOverride = $enableOverride;
545 }
546
550 public function submit() {
551 wfDeprecated( __METHOD__, '1.29' );
552 $this->edit();
553 }
554
566 public function edit() {
567 // Allow extensions to modify/prevent this form or submission
568 if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
569 return;
570 }
571
572 wfDebug( __METHOD__ . ": enter\n" );
573
574 $request = $this->context->getRequest();
575 // If they used redlink=1 and the page exists, redirect to the main article
576 if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
577 $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
578 return;
579 }
580
581 $this->importFormData( $request );
582 $this->firsttime = false;
583
584 if ( wfReadOnly() && $this->save ) {
585 // Force preview
586 $this->save = false;
587 $this->preview = true;
588 }
589
590 if ( $this->save ) {
591 $this->formtype = 'save';
592 } elseif ( $this->preview ) {
593 $this->formtype = 'preview';
594 } elseif ( $this->diff ) {
595 $this->formtype = 'diff';
596 } else { # First time through
597 $this->firsttime = true;
598 if ( $this->previewOnOpen() ) {
599 $this->formtype = 'preview';
600 } else {
601 $this->formtype = 'initial';
602 }
603 }
604
605 $permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
606 if ( $permErrors ) {
607 wfDebug( __METHOD__ . ": User can't edit\n" );
608 // Auto-block user's IP if the account was "hard" blocked
609 if ( !wfReadOnly() ) {
610 DeferredUpdates::addCallableUpdate( function () {
611 $this->context->getUser()->spreadAnyEditBlock();
612 } );
613 }
614 $this->displayPermissionsError( $permErrors );
615
616 return;
617 }
618
619 $revision = $this->mArticle->getRevisionFetched();
620 // Disallow editing revisions with content models different from the current one
621 // Undo edits being an exception in order to allow reverting content model changes.
622 if ( $revision
623 && $revision->getContentModel() !== $this->contentModel
624 ) {
625 $prevRev = null;
626 if ( $this->undidRev ) {
627 $undidRevObj = Revision::newFromId( $this->undidRev );
628 $prevRev = $undidRevObj ? $undidRevObj->getPrevious() : null;
629 }
630 if ( !$this->undidRev
631 || !$prevRev
632 || $prevRev->getContentModel() !== $this->contentModel
633 ) {
635 $this->getContentObject(),
636 $this->context->msg(
637 'contentmodelediterror',
638 $revision->getContentModel(),
639 $this->contentModel
640 )->plain()
641 );
642 return;
643 }
644 }
645
646 $this->isConflict = false;
647 // css / js subpages of user pages get a special treatment
648 // The following member variables are deprecated since 1.30,
649 // the functions should be used instead.
650 $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
651 $this->isCssSubpage = $this->mTitle->isCssSubpage();
652 $this->isJsSubpage = $this->mTitle->isJsSubpage();
654
655 # Show applicable editing introductions
656 if ( $this->formtype == 'initial' || $this->firsttime ) {
657 $this->showIntro();
658 }
659
660 # Attempt submission here. This will check for edit conflicts,
661 # and redundantly check for locked database, blocked IPs, etc.
662 # that edit() already checked just in case someone tries to sneak
663 # in the back door with a hand-edited submission URL.
664
665 if ( 'save' == $this->formtype ) {
666 $resultDetails = null;
667 $status = $this->attemptSave( $resultDetails );
668 if ( !$this->handleStatus( $status, $resultDetails ) ) {
669 return;
670 }
671 }
672
673 # First time through: get contents, set time for conflict
674 # checking, etc.
675 if ( 'initial' == $this->formtype || $this->firsttime ) {
676 if ( $this->initialiseForm() === false ) {
677 $this->noSuchSectionPage();
678 return;
679 }
680
681 if ( !$this->mTitle->getArticleID() ) {
682 Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
683 } else {
684 Hooks::run( 'EditFormInitialText', [ $this ] );
685 }
686
687 }
688
689 $this->showEditForm();
690 }
691
696 protected function getEditPermissionErrors( $rigor = 'secure' ) {
697 $user = $this->context->getUser();
698 $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
699 # Can this title be created?
700 if ( !$this->mTitle->exists() ) {
701 $permErrors = array_merge(
702 $permErrors,
704 $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
705 $permErrors
706 )
707 );
708 }
709 # Ignore some permissions errors when a user is just previewing/viewing diffs
710 $remove = [];
711 foreach ( $permErrors as $error ) {
712 if ( ( $this->preview || $this->diff )
713 && (
714 $error[0] == 'blockedtext' ||
715 $error[0] == 'autoblockedtext' ||
716 $error[0] == 'systemblockedtext'
717 )
718 ) {
719 $remove[] = $error;
720 }
721 }
722 $permErrors = wfArrayDiff2( $permErrors, $remove );
723
724 return $permErrors;
725 }
726
740 protected function displayPermissionsError( array $permErrors ) {
741 $out = $this->context->getOutput();
742 if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
743 // The edit page was reached via a red link.
744 // Redirect to the article page and let them click the edit tab if
745 // they really want a permission error.
746 $out->redirect( $this->mTitle->getFullURL() );
747 return;
748 }
749
750 $content = $this->getContentObject();
751
752 # Use the normal message if there's nothing to display
753 if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
754 $action = $this->mTitle->exists() ? 'edit' :
755 ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
756 throw new PermissionsError( $action, $permErrors );
757 }
758
760 $content,
761 $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
762 );
763 }
764
770 protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
771 $out = $this->context->getOutput();
772 Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
773
774 $out->setRobotPolicy( 'noindex,nofollow' );
775 $out->setPageTitle( $this->context->msg(
776 'viewsource-title',
777 $this->getContextTitle()->getPrefixedText()
778 ) );
779 $out->addBacklinkSubtitle( $this->getContextTitle() );
780 $out->addHTML( $this->editFormPageTop );
781 $out->addHTML( $this->editFormTextTop );
782
783 if ( $errorMessage !== '' ) {
784 $out->addWikiText( $errorMessage );
785 $out->addHTML( "<hr />\n" );
786 }
787
788 # If the user made changes, preserve them when showing the markup
789 # (This happens when a user is blocked during edit, for instance)
790 if ( !$this->firsttime ) {
791 $text = $this->textbox1;
792 $out->addWikiMsg( 'viewyourtext' );
793 } else {
794 try {
795 $text = $this->toEditText( $content );
796 } catch ( MWException $e ) {
797 # Serialize using the default format if the content model is not supported
798 # (e.g. for an old revision with a different model)
799 $text = $content->serialize();
800 }
801 $out->addWikiMsg( 'viewsourcetext' );
802 }
803
804 $out->addHTML( $this->editFormTextBeforeContent );
805 $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
806 $out->addHTML( $this->editFormTextAfterContent );
807
808 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
809
810 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
811
812 $out->addHTML( $this->editFormTextBottom );
813 if ( $this->mTitle->exists() ) {
814 $out->returnToMain( null, $this->mTitle );
815 }
816 }
817
823 protected function previewOnOpen() {
824 $previewOnOpenNamespaces = $this->context->getConfig()->get( 'PreviewOnOpenNamespaces' );
825 $request = $this->context->getRequest();
826 if ( $request->getVal( 'preview' ) == 'yes' ) {
827 // Explicit override from request
828 return true;
829 } elseif ( $request->getVal( 'preview' ) == 'no' ) {
830 // Explicit override from request
831 return false;
832 } elseif ( $this->section == 'new' ) {
833 // Nothing *to* preview for new sections
834 return false;
835 } elseif ( ( $request->getVal( 'preload' ) !== null || $this->mTitle->exists() )
836 && $this->context->getUser()->getOption( 'previewonfirst' )
837 ) {
838 // Standard preference behavior
839 return true;
840 } elseif ( !$this->mTitle->exists()
841 && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
842 && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
843 ) {
844 // Categories are special
845 return true;
846 } else {
847 return false;
848 }
849 }
850
857 protected function isWrongCaseCssJsPage() {
858 if ( $this->mTitle->isCssJsSubpage() ) {
859 $name = $this->mTitle->getSkinFromCssJsSubpage();
860 $skins = array_merge(
861 array_keys( Skin::getSkinNames() ),
862 [ 'common' ]
863 );
864 return !in_array( $name, $skins )
865 && in_array( strtolower( $name ), $skins );
866 } else {
867 return false;
868 }
869 }
870
878 protected function isSectionEditSupported() {
879 $contentHandler = ContentHandler::getForTitle( $this->mTitle );
880 return $contentHandler->supportsSections();
881 }
882
888 public function importFormData( &$request ) {
889 # Section edit can come from either the form or a link
890 $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
891
892 if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
893 throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
894 }
895
896 $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
897
898 if ( $request->wasPosted() ) {
899 # These fields need to be checked for encoding.
900 # Also remove trailing whitespace, but don't remove _initial_
901 # whitespace from the text boxes. This may be significant formatting.
902 $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
903 if ( !$request->getCheck( 'wpTextbox2' ) ) {
904 // Skip this if wpTextbox2 has input, it indicates that we came
905 // from a conflict page with raw page text, not a custom form
906 // modified by subclasses
908 if ( $textbox1 !== null ) {
909 $this->textbox1 = $textbox1;
910 }
911 }
912
913 $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
914
915 $this->summary = $request->getText( 'wpSummary' );
916
917 # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
918 # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
919 # section titles.
920 $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
921
922 # Treat sectiontitle the same way as summary.
923 # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
924 # currently doing double duty as both edit summary and section title. Right now this
925 # is just to allow API edits to work around this limitation, but this should be
926 # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312).
927 $this->sectiontitle = $request->getText( 'wpSectionTitle' );
928 $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
929
930 $this->edittime = $request->getVal( 'wpEdittime' );
931 $this->editRevId = $request->getIntOrNull( 'editRevId' );
932 $this->starttime = $request->getVal( 'wpStarttime' );
933
934 $undidRev = $request->getInt( 'wpUndidRevision' );
935 if ( $undidRev ) {
936 $this->undidRev = $undidRev;
937 }
938
939 $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
940
941 if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
942 // wpTextbox1 field is missing, possibly due to being "too big"
943 // according to some filter rules such as Suhosin's setting for
944 // suhosin.request.max_value_length (d'oh)
945 $this->incompleteForm = true;
946 } else {
947 // If we receive the last parameter of the request, we can fairly
948 // claim the POST request has not been truncated.
949
950 // TODO: softened the check for cutover. Once we determine
951 // that it is safe, we should complete the transition by
952 // removing the "edittime" clause.
953 $this->incompleteForm = ( !$request->getVal( 'wpUltimateParam' )
954 && is_null( $this->edittime ) );
955 }
956 if ( $this->incompleteForm ) {
957 # If the form is incomplete, force to preview.
958 wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
959 wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" );
960 $this->preview = true;
961 } else {
962 $this->preview = $request->getCheck( 'wpPreview' );
963 $this->diff = $request->getCheck( 'wpDiff' );
964
965 // Remember whether a save was requested, so we can indicate
966 // if we forced preview due to session failure.
967 $this->mTriedSave = !$this->preview;
968
969 if ( $this->tokenOk( $request ) ) {
970 # Some browsers will not report any submit button
971 # if the user hits enter in the comment box.
972 # The unmarked state will be assumed to be a save,
973 # if the form seems otherwise complete.
974 wfDebug( __METHOD__ . ": Passed token check.\n" );
975 } elseif ( $this->diff ) {
976 # Failed token check, but only requested "Show Changes".
977 wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
978 } else {
979 # Page might be a hack attempt posted from
980 # an external site. Preview instead of saving.
981 wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
982 $this->preview = true;
983 }
984 }
985 $this->save = !$this->preview && !$this->diff;
986 if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
987 $this->edittime = null;
988 }
989
990 if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
991 $this->starttime = null;
992 }
993
994 $this->recreate = $request->getCheck( 'wpRecreate' );
995
996 $this->minoredit = $request->getCheck( 'wpMinoredit' );
997 $this->watchthis = $request->getCheck( 'wpWatchthis' );
998
999 $user = $this->context->getUser();
1000 # Don't force edit summaries when a user is editing their own user or talk page
1001 if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
1002 && $this->mTitle->getText() == $user->getName()
1003 ) {
1004 $this->allowBlankSummary = true;
1005 } else {
1006 $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1007 || !$user->getOption( 'forceeditsummary' );
1008 }
1009
1010 $this->autoSumm = $request->getText( 'wpAutoSummary' );
1011
1012 $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1013 $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
1014
1015 $changeTags = $request->getVal( 'wpChangeTags' );
1016 if ( is_null( $changeTags ) || $changeTags === '' ) {
1017 $this->changeTags = [];
1018 } else {
1019 $this->changeTags = array_filter( array_map( 'trim', explode( ',',
1020 $changeTags ) ) );
1021 }
1022 } else {
1023 # Not a posted form? Start with nothing.
1024 wfDebug( __METHOD__ . ": Not a posted form.\n" );
1025 $this->textbox1 = '';
1026 $this->summary = '';
1027 $this->sectiontitle = '';
1028 $this->edittime = '';
1029 $this->editRevId = null;
1030 $this->starttime = wfTimestampNow();
1031 $this->edit = false;
1032 $this->preview = false;
1033 $this->save = false;
1034 $this->diff = false;
1035 $this->minoredit = false;
1036 // Watch may be overridden by request parameters
1037 $this->watchthis = $request->getBool( 'watchthis', false );
1038 $this->recreate = false;
1039
1040 // When creating a new section, we can preload a section title by passing it as the
1041 // preloadtitle parameter in the URL (T15100)
1042 if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1043 $this->sectiontitle = $request->getVal( 'preloadtitle' );
1044 // Once wpSummary isn't being use for setting section titles, we should delete this.
1045 $this->summary = $request->getVal( 'preloadtitle' );
1046 } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) {
1047 $this->summary = $request->getText( 'summary' );
1048 if ( $this->summary !== '' ) {
1049 $this->hasPresetSummary = true;
1050 }
1051 }
1052
1053 if ( $request->getVal( 'minor' ) ) {
1054 $this->minoredit = true;
1055 }
1056 }
1057
1058 $this->oldid = $request->getInt( 'oldid' );
1059 $this->parentRevId = $request->getInt( 'parentRevId' );
1060
1061 $this->bot = $request->getBool( 'bot', true );
1062 $this->nosummary = $request->getBool( 'nosummary' );
1063
1064 // May be overridden by revision.
1065 $this->contentModel = $request->getText( 'model', $this->contentModel );
1066 // May be overridden by revision.
1067 $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1068
1069 try {
1070 $handler = ContentHandler::getForModelID( $this->contentModel );
1072 throw new ErrorPageError(
1073 'editpage-invalidcontentmodel-title',
1074 'editpage-invalidcontentmodel-text',
1075 [ wfEscapeWikiText( $this->contentModel ) ]
1076 );
1077 }
1078
1079 if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1080 throw new ErrorPageError(
1081 'editpage-notsupportedcontentformat-title',
1082 'editpage-notsupportedcontentformat-text',
1083 [
1084 wfEscapeWikiText( $this->contentFormat ),
1085 wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1086 ]
1087 );
1088 }
1089
1096 $this->editintro = $request->getText( 'editintro',
1097 // Custom edit intro for new sections
1098 $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1099
1100 // Allow extensions to modify form data
1101 Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1102 }
1103
1113 protected function importContentFormData( &$request ) {
1114 return; // Don't do anything, EditPage already extracted wpTextbox1
1115 }
1116
1122 public function initialiseForm() {
1123 $this->edittime = $this->page->getTimestamp();
1124 $this->editRevId = $this->page->getLatest();
1125
1126 $content = $this->getContentObject( false ); # TODO: track content object?!
1127 if ( $content === false ) {
1128 return false;
1129 }
1130 $this->textbox1 = $this->toEditText( $content );
1131
1132 $user = $this->context->getUser();
1133 // activate checkboxes if user wants them to be always active
1134 # Sort out the "watch" checkbox
1135 if ( $user->getOption( 'watchdefault' ) ) {
1136 # Watch all edits
1137 $this->watchthis = true;
1138 } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1139 # Watch creations
1140 $this->watchthis = true;
1141 } elseif ( $user->isWatched( $this->mTitle ) ) {
1142 # Already watched
1143 $this->watchthis = true;
1144 }
1145 if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1146 $this->minoredit = true;
1147 }
1148 if ( $this->textbox1 === false ) {
1149 return false;
1150 }
1151 return true;
1152 }
1153
1161 protected function getContentObject( $def_content = null ) {
1163
1164 $content = false;
1165
1166 $user = $this->context->getUser();
1167 $request = $this->context->getRequest();
1168 // For message page not locally set, use the i18n message.
1169 // For other non-existent articles, use preload text if any.
1170 if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1171 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1172 # If this is a system message, get the default text.
1173 $msg = $this->mTitle->getDefaultMessageText();
1174
1175 $content = $this->toEditContent( $msg );
1176 }
1177 if ( $content === false ) {
1178 # If requested, preload some text.
1179 $preload = $request->getVal( 'preload',
1180 // Custom preload text for new sections
1181 $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1182 $params = $request->getArray( 'preloadparams', [] );
1183
1184 $content = $this->getPreloadedContent( $preload, $params );
1185 }
1186 // For existing pages, get text based on "undo" or section parameters.
1187 } else {
1188 if ( $this->section != '' ) {
1189 // Get section edit text (returns $def_text for invalid sections)
1190 $orig = $this->getOriginalContent( $user );
1191 $content = $orig ? $orig->getSection( $this->section ) : null;
1192
1193 if ( !$content ) {
1194 $content = $def_content;
1195 }
1196 } else {
1197 $undoafter = $request->getInt( 'undoafter' );
1198 $undo = $request->getInt( 'undo' );
1199
1200 if ( $undo > 0 && $undoafter > 0 ) {
1201 $undorev = Revision::newFromId( $undo );
1202 $oldrev = Revision::newFromId( $undoafter );
1203
1204 # Sanity check, make sure it's the right page,
1205 # the revisions exist and they were not deleted.
1206 # Otherwise, $content will be left as-is.
1207 if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1208 !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1209 !$oldrev->isDeleted( Revision::DELETED_TEXT )
1210 ) {
1211 $content = $this->page->getUndoContent( $undorev, $oldrev );
1212
1213 if ( $content === false ) {
1214 # Warn the user that something went wrong
1215 $undoMsg = 'failure';
1216 } else {
1217 $oldContent = $this->page->getContent( Revision::RAW );
1218 $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
1219 $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
1220 if ( $newContent->getModel() !== $oldContent->getModel() ) {
1221 // The undo may change content
1222 // model if its reverting the top
1223 // edit. This can result in
1224 // mismatched content model/format.
1225 $this->contentModel = $newContent->getModel();
1226 $this->contentFormat = $oldrev->getContentFormat();
1227 }
1228
1229 if ( $newContent->equals( $oldContent ) ) {
1230 # Tell the user that the undo results in no change,
1231 # i.e. the revisions were already undone.
1232 $undoMsg = 'nochange';
1233 $content = false;
1234 } else {
1235 # Inform the user of our success and set an automatic edit summary
1236 $undoMsg = 'success';
1237
1238 # If we just undid one rev, use an autosummary
1239 $firstrev = $oldrev->getNext();
1240 if ( $firstrev && $firstrev->getId() == $undo ) {
1241 $userText = $undorev->getUserText();
1242 if ( $userText === '' ) {
1243 $undoSummary = $this->context->msg(
1244 'undo-summary-username-hidden',
1245 $undo
1246 )->inContentLanguage()->text();
1247 } else {
1248 $undoSummary = $this->context->msg(
1249 'undo-summary',
1250 $undo,
1251 $userText
1252 )->inContentLanguage()->text();
1253 }
1254 if ( $this->summary === '' ) {
1255 $this->summary = $undoSummary;
1256 } else {
1257 $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1258 ->inContentLanguage()->text() . $this->summary;
1259 }
1260 $this->undidRev = $undo;
1261 }
1262 $this->formtype = 'diff';
1263 }
1264 }
1265 } else {
1266 // Failed basic sanity checks.
1267 // Older revisions may have been removed since the link
1268 // was created, or we may simply have got bogus input.
1269 $undoMsg = 'norev';
1270 }
1271
1272 $out = $this->context->getOutput();
1273 // Messages: undo-success, undo-failure, undo-norev, undo-nochange
1274 $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1275 $this->editFormPageTop .= $out->parse( "<div class=\"{$class}\">" .
1276 $this->context->msg( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
1277 }
1278
1279 if ( $content === false ) {
1280 $content = $this->getOriginalContent( $user );
1281 }
1282 }
1283 }
1284
1285 return $content;
1286 }
1287
1303 private function getOriginalContent( User $user ) {
1304 if ( $this->section == 'new' ) {
1305 return $this->getCurrentContent();
1306 }
1307 $revision = $this->mArticle->getRevisionFetched();
1308 if ( $revision === null ) {
1309 $handler = ContentHandler::getForModelID( $this->contentModel );
1310 return $handler->makeEmptyContent();
1311 }
1312 $content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1313 return $content;
1314 }
1315
1328 public function getParentRevId() {
1329 if ( $this->parentRevId ) {
1330 return $this->parentRevId;
1331 } else {
1332 return $this->mArticle->getRevIdFetched();
1333 }
1334 }
1335
1344 protected function getCurrentContent() {
1345 $rev = $this->page->getRevision();
1346 $content = $rev ? $rev->getContent( Revision::RAW ) : null;
1347
1348 if ( $content === false || $content === null ) {
1349 $handler = ContentHandler::getForModelID( $this->contentModel );
1350 return $handler->makeEmptyContent();
1351 } elseif ( !$this->undidRev ) {
1352 // Content models should always be the same since we error
1353 // out if they are different before this point (in ->edit()).
1354 // The exception being, during an undo, the current revision might
1355 // differ from the prior revision.
1356 $logger = LoggerFactory::getInstance( 'editpage' );
1357 if ( $this->contentModel !== $rev->getContentModel() ) {
1358 $logger->warning( "Overriding content model from current edit {prev} to {new}", [
1359 'prev' => $this->contentModel,
1360 'new' => $rev->getContentModel(),
1361 'title' => $this->getTitle()->getPrefixedDBkey(),
1362 'method' => __METHOD__
1363 ] );
1364 $this->contentModel = $rev->getContentModel();
1365 }
1366
1367 // Given that the content models should match, the current selected
1368 // format should be supported.
1369 if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1370 $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1371
1372 'prev' => $this->contentFormat,
1373 'new' => $rev->getContentFormat(),
1374 'title' => $this->getTitle()->getPrefixedDBkey(),
1375 'method' => __METHOD__
1376 ] );
1377 $this->contentFormat = $rev->getContentFormat();
1378 }
1379 }
1380 return $content;
1381 }
1382
1390 public function setPreloadedContent( Content $content ) {
1391 $this->mPreloadContent = $content;
1392 }
1393
1405 protected function getPreloadedContent( $preload, $params = [] ) {
1406 if ( !empty( $this->mPreloadContent ) ) {
1408 }
1409
1410 $handler = ContentHandler::getForModelID( $this->contentModel );
1411
1412 if ( $preload === '' ) {
1413 return $handler->makeEmptyContent();
1414 }
1415
1416 $user = $this->context->getUser();
1417 $title = Title::newFromText( $preload );
1418 # Check for existence to avoid getting MediaWiki:Noarticletext
1419 if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) {
1420 // TODO: somehow show a warning to the user!
1421 return $handler->makeEmptyContent();
1422 }
1423
1425 if ( $page->isRedirect() ) {
1427 # Same as before
1428 if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) {
1429 // TODO: somehow show a warning to the user!
1430 return $handler->makeEmptyContent();
1431 }
1433 }
1434
1435 $parserOptions = ParserOptions::newFromUser( $user );
1436 $content = $page->getContent( Revision::RAW );
1437
1438 if ( !$content ) {
1439 // TODO: somehow show a warning to the user!
1440 return $handler->makeEmptyContent();
1441 }
1442
1443 if ( $content->getModel() !== $handler->getModelID() ) {
1444 $converted = $content->convert( $handler->getModelID() );
1445
1446 if ( !$converted ) {
1447 // TODO: somehow show a warning to the user!
1448 wfDebug( "Attempt to preload incompatible content: " .
1449 "can't convert " . $content->getModel() .
1450 " to " . $handler->getModelID() );
1451
1452 return $handler->makeEmptyContent();
1453 }
1454
1455 $content = $converted;
1456 }
1457
1458 return $content->preloadTransform( $title, $parserOptions, $params );
1459 }
1460
1468 public function tokenOk( &$request ) {
1469 $token = $request->getVal( 'wpEditToken' );
1470 $user = $this->context->getUser();
1471 $this->mTokenOk = $user->matchEditToken( $token );
1472 $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
1473 return $this->mTokenOk;
1474 }
1475
1490 protected function setPostEditCookie( $statusValue ) {
1491 $revisionId = $this->page->getLatest();
1492 $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1493
1494 $val = 'saved';
1495 if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1496 $val = 'created';
1497 } elseif ( $this->oldid ) {
1498 $val = 'restored';
1499 }
1500
1501 $response = $this->context->getRequest()->response();
1502 $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1503 }
1504
1511 public function attemptSave( &$resultDetails = false ) {
1512 # Allow bots to exempt some edits from bot flagging
1513 $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot;
1514 $status = $this->internalAttemptSave( $resultDetails, $bot );
1515
1516 Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1517
1518 return $status;
1519 }
1520
1524 private function incrementResolvedConflicts() {
1525 if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1526 return;
1527 }
1528
1529 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1530 $stats->increment( 'edit.failures.conflict.resolved' );
1531 }
1532
1542 private function handleStatus( Status $status, $resultDetails ) {
1547 if ( $status->value == self::AS_SUCCESS_UPDATE
1548 || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1549 ) {
1551
1552 $this->didSave = true;
1553 if ( !$resultDetails['nullEdit'] ) {
1554 $this->setPostEditCookie( $status->value );
1555 }
1556 }
1557
1558 $out = $this->context->getOutput();
1559
1560 // "wpExtraQueryRedirect" is a hidden input to modify
1561 // after save URL and is not used by actual edit form
1562 $request = $this->context->getRequest();
1563 $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1564
1565 switch ( $status->value ) {
1573 case self::AS_END:
1576 return true;
1577
1579 return false;
1580
1584 $out->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
1585 return true;
1586
1588 $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1589 if ( $extraQueryRedirect ) {
1590 if ( $query === '' ) {
1591 $query = $extraQueryRedirect;
1592 } else {
1593 $query = $query . '&' . $extraQueryRedirect;
1594 }
1595 }
1596 $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
1597 $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1598 return false;
1599
1601 $extraQuery = '';
1602 $sectionanchor = $resultDetails['sectionanchor'];
1603
1604 // Give extensions a chance to modify URL query on update
1605 Hooks::run(
1606 'ArticleUpdateBeforeRedirect',
1607 [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1608 );
1609
1610 if ( $resultDetails['redirect'] ) {
1611 if ( $extraQuery == '' ) {
1612 $extraQuery = 'redirect=no';
1613 } else {
1614 $extraQuery = 'redirect=no&' . $extraQuery;
1615 }
1616 }
1617 if ( $extraQueryRedirect ) {
1618 if ( $extraQuery === '' ) {
1619 $extraQuery = $extraQueryRedirect;
1620 } else {
1621 $extraQuery = $extraQuery . '&' . $extraQueryRedirect;
1622 }
1623 }
1624
1625 $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1626 return false;
1627
1629 $this->spamPageWithContent( $resultDetails['spam'] );
1630 return false;
1631
1633 throw new UserBlockedError( $this->context->getUser()->getBlock() );
1634
1637 throw new PermissionsError( 'upload' );
1638
1641 throw new PermissionsError( 'edit' );
1642
1644 throw new ReadOnlyError;
1645
1647 throw new ThrottledError();
1648
1650 $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1651 throw new PermissionsError( $permission );
1652
1654 throw new PermissionsError( 'editcontentmodel' );
1655
1656 default:
1657 // We don't recognize $status->value. The only way that can happen
1658 // is if an extension hook aborted from inside ArticleSave.
1659 // Render the status object into $this->hookError
1660 // FIXME this sucks, we should just use the Status object throughout
1661 $this->hookError = '<div class="error">' ."\n" . $status->getWikiText() .
1662 '</div>';
1663 return true;
1664 }
1665 }
1666
1676 protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1677 // Run old style post-section-merge edit filter
1678 if ( $this->hookError != '' ) {
1679 # ...or the hook could be expecting us to produce an error
1680 $status->fatal( 'hookaborted' );
1682 return false;
1683 }
1684
1685 // Run new style post-section-merge edit filter
1686 if ( !Hooks::run( 'EditFilterMergedContent',
1687 [ $this->context, $content, $status, $this->summary,
1688 $user, $this->minoredit ] )
1689 ) {
1690 # Error messages etc. could be handled within the hook...
1691 if ( $status->isGood() ) {
1692 $status->fatal( 'hookaborted' );
1693 // Not setting $this->hookError here is a hack to allow the hook
1694 // to cause a return to the edit page without $this->hookError
1695 // being set. This is used by ConfirmEdit to display a captcha
1696 // without any error message cruft.
1697 } else {
1698 $this->hookError = $status->getWikiText();
1699 }
1700 // Use the existing $status->value if the hook set it
1701 if ( !$status->value ) {
1703 }
1704 return false;
1705 } elseif ( !$status->isOK() ) {
1706 # ...or the hook could be expecting us to produce an error
1707 // FIXME this sucks, we should just use the Status object throughout
1708 $this->hookError = $status->getWikiText();
1709 $status->fatal( 'hookaborted' );
1711 return false;
1712 }
1713
1714 return true;
1715 }
1716
1723 private function newSectionSummary( &$sectionanchor = null ) {
1725
1726 if ( $this->sectiontitle !== '' ) {
1727 $sectionanchor = $this->guessSectionName( $this->sectiontitle );
1728 // If no edit summary was specified, create one automatically from the section
1729 // title and have it link to the new section. Otherwise, respect the summary as
1730 // passed.
1731 if ( $this->summary === '' ) {
1732 $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1733 return $this->context->msg( 'newsectionsummary' )
1734 ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
1735 }
1736 } elseif ( $this->summary !== '' ) {
1737 $sectionanchor = $this->guessSectionName( $this->summary );
1738 # This is a new section, so create a link to the new section
1739 # in the revision summary.
1740 $cleanSummary = $wgParser->stripSectionName( $this->summary );
1741 return $this->context->msg( 'newsectionsummary' )
1742 ->rawParams( $cleanSummary )->inContentLanguage()->text();
1743 }
1744 return $this->summary;
1745 }
1746
1771 public function internalAttemptSave( &$result, $bot = false ) {
1772 $status = Status::newGood();
1773 $user = $this->context->getUser();
1774
1775 if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1776 wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1777 $status->fatal( 'hookaborted' );
1779 return $status;
1780 }
1781
1782 if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
1783 $status->fatal( 'unicode-support-fail' );
1785 return $status;
1786 }
1787
1788 $request = $this->context->getRequest();
1789 $spam = $request->getText( 'wpAntispam' );
1790 if ( $spam !== '' ) {
1791 wfDebugLog(
1792 'SimpleAntiSpam',
1793 $user->getName() .
1794 ' editing "' .
1795 $this->mTitle->getPrefixedText() .
1796 '" submitted bogus field "' .
1797 $spam .
1798 '"'
1799 );
1800 $status->fatal( 'spamprotectionmatch', false );
1802 return $status;
1803 }
1804
1805 try {
1806 # Construct Content object
1807 $textbox_content = $this->toEditContent( $this->textbox1 );
1808 } catch ( MWContentSerializationException $ex ) {
1809 $status->fatal(
1810 'content-failed-to-parse',
1811 $this->contentModel,
1812 $this->contentFormat,
1813 $ex->getMessage()
1814 );
1816 return $status;
1817 }
1818
1819 # Check image redirect
1820 if ( $this->mTitle->getNamespace() == NS_FILE &&
1821 $textbox_content->isRedirect() &&
1822 !$user->isAllowed( 'upload' )
1823 ) {
1825 $status->setResult( false, $code );
1826
1827 return $status;
1828 }
1829
1830 # Check for spam
1831 $match = self::matchSummarySpamRegex( $this->summary );
1832 if ( $match === false && $this->section == 'new' ) {
1833 # $wgSpamRegex is enforced on this new heading/summary because, unlike
1834 # regular summaries, it is added to the actual wikitext.
1835 if ( $this->sectiontitle !== '' ) {
1836 # This branch is taken when the API is used with the 'sectiontitle' parameter.
1837 $match = self::matchSpamRegex( $this->sectiontitle );
1838 } else {
1839 # This branch is taken when the "Add Topic" user interface is used, or the API
1840 # is used with the 'summary' parameter.
1841 $match = self::matchSpamRegex( $this->summary );
1842 }
1843 }
1844 if ( $match === false ) {
1845 $match = self::matchSpamRegex( $this->textbox1 );
1846 }
1847 if ( $match !== false ) {
1848 $result['spam'] = $match;
1849 $ip = $request->getIP();
1850 $pdbk = $this->mTitle->getPrefixedDBkey();
1851 $match = str_replace( "\n", '', $match );
1852 wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1853 $status->fatal( 'spamprotectionmatch', $match );
1855 return $status;
1856 }
1857 if ( !Hooks::run(
1858 'EditFilter',
1859 [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1860 ) {
1861 # Error messages etc. could be handled within the hook...
1862 $status->fatal( 'hookaborted' );
1864 return $status;
1865 } elseif ( $this->hookError != '' ) {
1866 # ...or the hook could be expecting us to produce an error
1867 $status->fatal( 'hookaborted' );
1869 return $status;
1870 }
1871
1872 if ( $user->isBlockedFrom( $this->mTitle, false ) ) {
1873 // Auto-block user's IP if the account was "hard" blocked
1874 if ( !wfReadOnly() ) {
1875 $user->spreadAnyEditBlock();
1876 }
1877 # Check block state against master, thus 'false'.
1878 $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1879 return $status;
1880 }
1881
1882 $this->contentLength = strlen( $this->textbox1 );
1883 $config = $this->context->getConfig();
1884 $maxArticleSize = $config->get( 'MaxArticleSize' );
1885 if ( $this->contentLength > $maxArticleSize * 1024 ) {
1886 // Error will be displayed by showEditForm()
1887 $this->tooBig = true;
1888 $status->setResult( false, self::AS_CONTENT_TOO_BIG );
1889 return $status;
1890 }
1891
1892 if ( !$user->isAllowed( 'edit' ) ) {
1893 if ( $user->isAnon() ) {
1894 $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1895 return $status;
1896 } else {
1897 $status->fatal( 'readonlytext' );
1899 return $status;
1900 }
1901 }
1902
1903 $changingContentModel = false;
1904 if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1905 if ( !$config->get( 'ContentHandlerUseDB' ) ) {
1906 $status->fatal( 'editpage-cannot-use-custom-model' );
1908 return $status;
1909 } elseif ( !$user->isAllowed( 'editcontentmodel' ) ) {
1910 $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1911 return $status;
1912 }
1913 // Make sure the user can edit the page under the new content model too
1914 $titleWithNewContentModel = clone $this->mTitle;
1915 $titleWithNewContentModel->setContentModel( $this->contentModel );
1916 if ( !$titleWithNewContentModel->userCan( 'editcontentmodel', $user )
1917 || !$titleWithNewContentModel->userCan( 'edit', $user )
1918 ) {
1919 $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1920 return $status;
1921 }
1922
1923 $changingContentModel = true;
1924 $oldContentModel = $this->mTitle->getContentModel();
1925 }
1926
1927 if ( $this->changeTags ) {
1928 $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
1929 $this->changeTags, $user );
1930 if ( !$changeTagsStatus->isOK() ) {
1931 $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
1932 return $changeTagsStatus;
1933 }
1934 }
1935
1936 if ( wfReadOnly() ) {
1937 $status->fatal( 'readonlytext' );
1939 return $status;
1940 }
1941 if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 )
1942 || ( $changingContentModel && $user->pingLimiter( 'editcontentmodel' ) )
1943 ) {
1944 $status->fatal( 'actionthrottledtext' );
1946 return $status;
1947 }
1948
1949 # If the article has been deleted while editing, don't save it without
1950 # confirmation
1951 if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
1952 $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
1953 return $status;
1954 }
1955
1956 # Load the page data from the master. If anything changes in the meantime,
1957 # we detect it by using page_latest like a token in a 1 try compare-and-swap.
1958 $this->page->loadPageData( 'fromdbmaster' );
1959 $new = !$this->page->exists();
1960
1961 if ( $new ) {
1962 // Late check for create permission, just in case *PARANOIA*
1963 if ( !$this->mTitle->userCan( 'create', $user ) ) {
1964 $status->fatal( 'nocreatetext' );
1966 wfDebug( __METHOD__ . ": no create permission\n" );
1967 return $status;
1968 }
1969
1970 // Don't save a new page if it's blank or if it's a MediaWiki:
1971 // message with content equivalent to default (allow empty pages
1972 // in this case to disable messages, see T52124)
1973 $defaultMessageText = $this->mTitle->getDefaultMessageText();
1974 if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
1975 $defaultText = $defaultMessageText;
1976 } else {
1977 $defaultText = '';
1978 }
1979
1980 if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
1981 $this->blankArticle = true;
1982 $status->fatal( 'blankarticle' );
1983 $status->setResult( false, self::AS_BLANK_ARTICLE );
1984 return $status;
1985 }
1986
1987 if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
1988 return $status;
1989 }
1990
1991 $content = $textbox_content;
1992
1993 $result['sectionanchor'] = '';
1994 if ( $this->section == 'new' ) {
1995 if ( $this->sectiontitle !== '' ) {
1996 // Insert the section title above the content.
1997 $content = $content->addSectionHeader( $this->sectiontitle );
1998 } elseif ( $this->summary !== '' ) {
1999 // Insert the section title above the content.
2000 $content = $content->addSectionHeader( $this->summary );
2001 }
2002 $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
2003 }
2004
2006
2007 } else { # not $new
2008
2009 # Article exists. Check for edit conflict.
2010
2011 $this->page->clear(); # Force reload of dates, etc.
2012 $timestamp = $this->page->getTimestamp();
2013 $latest = $this->page->getLatest();
2014
2015 wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
2016
2017 // Check editRevId if set, which handles same-second timestamp collisions
2018 if ( $timestamp != $this->edittime
2019 || ( $this->editRevId !== null && $this->editRevId != $latest )
2020 ) {
2021 $this->isConflict = true;
2022 if ( $this->section == 'new' ) {
2023 if ( $this->page->getUserText() == $user->getName() &&
2024 $this->page->getComment() == $this->newSectionSummary()
2025 ) {
2026 // Probably a duplicate submission of a new comment.
2027 // This can happen when CDN resends a request after
2028 // a timeout but the first one actually went through.
2029 wfDebug( __METHOD__
2030 . ": duplicate new section submission; trigger edit conflict!\n" );
2031 } else {
2032 // New comment; suppress conflict.
2033 $this->isConflict = false;
2034 wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
2035 }
2036 } elseif ( $this->section == ''
2038 DB_MASTER, $this->mTitle->getArticleID(),
2039 $user->getId(), $this->edittime
2040 )
2041 ) {
2042 # Suppress edit conflict with self, except for section edits where merging is required.
2043 wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
2044 $this->isConflict = false;
2045 }
2046 }
2047
2048 // If sectiontitle is set, use it, otherwise use the summary as the section title.
2049 if ( $this->sectiontitle !== '' ) {
2050 $sectionTitle = $this->sectiontitle;
2051 } else {
2052 $sectionTitle = $this->summary;
2053 }
2054
2055 $content = null;
2056
2057 if ( $this->isConflict ) {
2058 wfDebug( __METHOD__
2059 . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2060 . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
2061 // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2062 // ...or disable section editing for non-current revisions (not exposed anyway).
2063 if ( $this->editRevId !== null ) {
2064 $content = $this->page->replaceSectionAtRev(
2065 $this->section,
2066 $textbox_content,
2067 $sectionTitle,
2068 $this->editRevId
2069 );
2070 } else {
2071 $content = $this->page->replaceSectionContent(
2072 $this->section,
2073 $textbox_content,
2074 $sectionTitle,
2075 $this->edittime
2076 );
2077 }
2078 } else {
2079 wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
2080 $content = $this->page->replaceSectionContent(
2081 $this->section,
2082 $textbox_content,
2083 $sectionTitle
2084 );
2085 }
2086
2087 if ( is_null( $content ) ) {
2088 wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
2089 $this->isConflict = true;
2090 $content = $textbox_content; // do not try to merge here!
2091 } elseif ( $this->isConflict ) {
2092 # Attempt merge
2093 if ( $this->mergeChangesIntoContent( $content ) ) {
2094 // Successful merge! Maybe we should tell the user the good news?
2095 $this->isConflict = false;
2096 wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
2097 } else {
2098 $this->section = '';
2099 $this->textbox1 = ContentHandler::getContentText( $content );
2100 wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
2101 }
2102 }
2103
2104 if ( $this->isConflict ) {
2105 $status->setResult( false, self::AS_CONFLICT_DETECTED );
2106 return $status;
2107 }
2108
2109 if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
2110 return $status;
2111 }
2112
2113 if ( $this->section == 'new' ) {
2114 // Handle the user preference to force summaries here
2115 if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2116 $this->missingSummary = true;
2117 $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2119 return $status;
2120 }
2121
2122 // Do not allow the user to post an empty comment
2123 if ( $this->textbox1 == '' ) {
2124 $this->missingComment = true;
2125 $status->fatal( 'missingcommenttext' );
2127 return $status;
2128 }
2129 } elseif ( !$this->allowBlankSummary
2130 && !$content->equals( $this->getOriginalContent( $user ) )
2131 && !$content->isRedirect()
2132 && md5( $this->summary ) == $this->autoSumm
2133 ) {
2134 $this->missingSummary = true;
2135 $status->fatal( 'missingsummary' );
2137 return $status;
2138 }
2139
2140 # All's well
2141 $sectionanchor = '';
2142 if ( $this->section == 'new' ) {
2143 $this->summary = $this->newSectionSummary( $sectionanchor );
2144 } elseif ( $this->section != '' ) {
2145 # Try to get a section anchor from the section source, redirect
2146 # to edited section if header found.
2147 # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2148 # for duplicate heading checking and maybe parsing.
2149 $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2150 # We can't deal with anchors, includes, html etc in the header for now,
2151 # headline would need to be parsed to improve this.
2152 if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2153 $sectionanchor = $this->guessSectionName( $matches[2] );
2154 }
2155 }
2156 $result['sectionanchor'] = $sectionanchor;
2157
2158 // Save errors may fall down to the edit form, but we've now
2159 // merged the section into full text. Clear the section field
2160 // so that later submission of conflict forms won't try to
2161 // replace that into a duplicated mess.
2162 $this->textbox1 = $this->toEditText( $content );
2163 $this->section = '';
2164
2166 }
2167
2168 if ( !$this->allowSelfRedirect
2169 && $content->isRedirect()
2170 && $content->getRedirectTarget()->equals( $this->getTitle() )
2171 ) {
2172 // If the page already redirects to itself, don't warn.
2173 $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2174 if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2175 $this->selfRedirect = true;
2176 $status->fatal( 'selfredirect' );
2178 return $status;
2179 }
2180 }
2181
2182 // Check for length errors again now that the section is merged in
2183 $this->contentLength = strlen( $this->toEditText( $content ) );
2184 if ( $this->contentLength > $maxArticleSize * 1024 ) {
2185 $this->tooBig = true;
2186 $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2187 return $status;
2188 }
2189
2191 ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2192 ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2193 ( $bot ? EDIT_FORCE_BOT : 0 );
2194
2195 $doEditStatus = $this->page->doEditContent(
2196 $content,
2197 $this->summary,
2198 $flags,
2199 false,
2200 $user,
2201 $content->getDefaultFormat(),
2204 );
2205
2206 if ( !$doEditStatus->isOK() ) {
2207 // Failure from doEdit()
2208 // Show the edit conflict page for certain recognized errors from doEdit(),
2209 // but don't show it for errors from extension hooks
2210 $errors = $doEditStatus->getErrorsArray();
2211 if ( in_array( $errors[0][0],
2212 [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2213 ) {
2214 $this->isConflict = true;
2215 // Destroys data doEdit() put in $status->value but who cares
2216 $doEditStatus->value = self::AS_END;
2217 }
2218 return $doEditStatus;
2219 }
2220
2221 $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2222 if ( $result['nullEdit'] ) {
2223 // We don't know if it was a null edit until now, so increment here
2224 $user->pingLimiter( 'linkpurge' );
2225 }
2226 $result['redirect'] = $content->isRedirect();
2227
2228 $this->updateWatchlist();
2229
2230 // If the content model changed, add a log entry
2231 if ( $changingContentModel ) {
2233 $user,
2234 $new ? false : $oldContentModel,
2237 );
2238 }
2239
2240 return $status;
2241 }
2242
2249 protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2250 $new = $oldModel === false;
2251 $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2252 $log->setPerformer( $user );
2253 $log->setTarget( $this->mTitle );
2254 $log->setComment( $reason );
2255 $log->setParameters( [
2256 '4::oldmodel' => $oldModel,
2257 '5::newmodel' => $newModel
2258 ] );
2259 $logid = $log->insert();
2260 $log->publish( $logid );
2261 }
2262
2266 protected function updateWatchlist() {
2267 $user = $this->context->getUser();
2268 if ( !$user->isLoggedIn() ) {
2269 return;
2270 }
2271
2273 $watch = $this->watchthis;
2274 // Do this in its own transaction to reduce contention...
2275 DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2276 if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2277 return; // nothing to change
2278 }
2280 } );
2281 }
2282
2294 private function mergeChangesIntoContent( &$editContent ) {
2295 $db = wfGetDB( DB_MASTER );
2296
2297 // This is the revision the editor started from
2298 $baseRevision = $this->getBaseRevision();
2299 $baseContent = $baseRevision ? $baseRevision->getContent() : null;
2300
2301 if ( is_null( $baseContent ) ) {
2302 return false;
2303 }
2304
2305 // The current state, we want to merge updates into it
2306 $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2307 $currentContent = $currentRevision ? $currentRevision->getContent() : null;
2308
2309 if ( is_null( $currentContent ) ) {
2310 return false;
2311 }
2312
2313 $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2314
2315 $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2316
2317 if ( $result ) {
2318 $editContent = $result;
2319 // Update parentRevId to what we just merged.
2320 $this->parentRevId = $currentRevision->getId();
2321 return true;
2322 }
2323
2324 return false;
2325 }
2326
2332 public function getBaseRevision() {
2333 if ( !$this->mBaseRevision ) {
2334 $db = wfGetDB( DB_MASTER );
2335 $this->mBaseRevision = $this->editRevId
2336 ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2337 : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2338 }
2339 return $this->mBaseRevision;
2340 }
2341
2349 public static function matchSpamRegex( $text ) {
2351 // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2352 $regexes = (array)$wgSpamRegex;
2353 return self::matchSpamRegexInternal( $text, $regexes );
2354 }
2355
2363 public static function matchSummarySpamRegex( $text ) {
2365 $regexes = (array)$wgSummarySpamRegex;
2366 return self::matchSpamRegexInternal( $text, $regexes );
2367 }
2368
2374 protected static function matchSpamRegexInternal( $text, $regexes ) {
2375 foreach ( $regexes as $regex ) {
2376 $matches = [];
2377 if ( preg_match( $regex, $text, $matches ) ) {
2378 return $matches[0];
2379 }
2380 }
2381 return false;
2382 }
2383
2384 public function setHeaders() {
2385 $out = $this->context->getOutput();
2386
2387 $out->addModules( 'mediawiki.action.edit' );
2388 $out->addModuleStyles( 'mediawiki.action.edit.styles' );
2389
2390 $user = $this->context->getUser();
2391 if ( $user->getOption( 'showtoolbar' ) ) {
2392 // The addition of default buttons is handled by getEditToolbar() which
2393 // has its own dependency on this module. The call here ensures the module
2394 // is loaded in time (it has position "top") for other modules to register
2395 // buttons (e.g. extensions, gadgets, user scripts).
2396 $out->addModules( 'mediawiki.toolbar' );
2397 }
2398
2399 if ( $user->getOption( 'uselivepreview' ) ) {
2400 $out->addModules( 'mediawiki.action.edit.preview' );
2401 }
2402
2403 if ( $user->getOption( 'useeditwarning' ) ) {
2404 $out->addModules( 'mediawiki.action.edit.editWarning' );
2405 }
2406
2407 # Enabled article-related sidebar, toplinks, etc.
2408 $out->setArticleRelated( true );
2409
2410 $contextTitle = $this->getContextTitle();
2411 if ( $this->isConflict ) {
2412 $msg = 'editconflict';
2413 } elseif ( $contextTitle->exists() && $this->section != '' ) {
2414 $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2415 } else {
2416 $msg = $contextTitle->exists()
2417 || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2418 && $contextTitle->getDefaultMessageText() !== false
2419 )
2420 ? 'editing'
2421 : 'creating';
2422 }
2423
2424 # Use the title defined by DISPLAYTITLE magic word when present
2425 # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2426 # setPageTitle() treats the input as wikitext, which should be safe in either case.
2427 $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2428 if ( $displayTitle === false ) {
2429 $displayTitle = $contextTitle->getPrefixedText();
2430 }
2431 $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2432 # Transmit the name of the message to JavaScript for live preview
2433 # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2434 $out->addJsConfigVars( [
2435 'wgEditMessage' => $msg,
2436 'wgAjaxEditStash' => $this->context->getConfig()->get( 'AjaxEditStash' ),
2437 ] );
2438 }
2439
2443 protected function showIntro() {
2444 if ( $this->suppressIntro ) {
2445 return;
2446 }
2447
2448 $out = $this->context->getOutput();
2449 $namespace = $this->mTitle->getNamespace();
2450
2451 if ( $namespace == NS_MEDIAWIKI ) {
2452 # Show a warning if editing an interface message
2453 $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2454 # If this is a default message (but not css or js),
2455 # show a hint that it is translatable on translatewiki.net
2456 if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2457 && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2458 ) {
2459 $defaultMessageText = $this->mTitle->getDefaultMessageText();
2460 if ( $defaultMessageText !== false ) {
2461 $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2462 'translateinterface' );
2463 }
2464 }
2465 } elseif ( $namespace == NS_FILE ) {
2466 # Show a hint to shared repo
2467 $file = wfFindFile( $this->mTitle );
2468 if ( $file && !$file->isLocal() ) {
2469 $descUrl = $file->getDescriptionUrl();
2470 # there must be a description url to show a hint to shared repo
2471 if ( $descUrl ) {
2472 if ( !$this->mTitle->exists() ) {
2473 $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2474 'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2475 ] );
2476 } else {
2477 $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2478 'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2479 ] );
2480 }
2481 }
2482 }
2483 }
2484
2485 # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2486 # Show log extract when the user is currently blocked
2487 if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2488 $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2489 $user = User::newFromName( $username, false /* allow IP users */ );
2490 $ip = User::isIP( $username );
2491 $block = Block::newFromTarget( $user, $user );
2492 if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2493 $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2494 [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2495 } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
2496 # Show log extract if the user is currently blocked
2498 $out,
2499 'block',
2500 MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2501 '',
2502 [
2503 'lim' => 1,
2504 'showIfEmpty' => false,
2505 'msgKey' => [
2506 'blocked-notice-logextract',
2507 $user->getName() # Support GENDER in notice
2508 ]
2509 ]
2510 );
2511 }
2512 }
2513 # Try to add a custom edit intro, or use the standard one if this is not possible.
2514 if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2516 $this->context->msg( 'helppage' )->inContentLanguage()->text()
2517 ) );
2518 if ( $this->context->getUser()->isLoggedIn() ) {
2519 $out->wrapWikiMsg(
2520 // Suppress the external link icon, consider the help url an internal one
2521 "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2522 [
2523 'newarticletext',
2524 $helpLink
2525 ]
2526 );
2527 } else {
2528 $out->wrapWikiMsg(
2529 // Suppress the external link icon, consider the help url an internal one
2530 "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2531 [
2532 'newarticletextanon',
2533 $helpLink
2534 ]
2535 );
2536 }
2537 }
2538 # Give a notice if the user is editing a deleted/moved page...
2539 if ( !$this->mTitle->exists() ) {
2540 $dbr = wfGetDB( DB_REPLICA );
2541
2542 LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2543 '',
2544 [
2545 'lim' => 10,
2546 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2547 'showIfEmpty' => false,
2548 'msgKey' => [ 'recreate-moveddeleted-warn' ]
2549 ]
2550 );
2551 }
2552 }
2553
2559 protected function showCustomIntro() {
2560 if ( $this->editintro ) {
2561 $title = Title::newFromText( $this->editintro );
2562 if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2563 // Added using template syntax, to take <noinclude>'s into account.
2564 $this->context->getOutput()->addWikiTextTitleTidy(
2565 '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2566 $this->mTitle
2567 );
2568 return true;
2569 }
2570 }
2571 return false;
2572 }
2573
2592 protected function toEditText( $content ) {
2593 if ( $content === null || $content === false || is_string( $content ) ) {
2594 return $content;
2595 }
2596
2597 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2598 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2599 }
2600
2601 return $content->serialize( $this->contentFormat );
2602 }
2603
2620 protected function toEditContent( $text ) {
2621 if ( $text === false || $text === null ) {
2622 return $text;
2623 }
2624
2625 $content = ContentHandler::makeContent( $text, $this->getTitle(),
2626 $this->contentModel, $this->contentFormat );
2627
2628 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2629 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2630 }
2631
2632 return $content;
2633 }
2634
2643 public function showEditForm( $formCallback = null ) {
2644 # need to parse the preview early so that we know which templates are used,
2645 # otherwise users with "show preview after edit box" will get a blank list
2646 # we parse this near the beginning so that setHeaders can do the title
2647 # setting work instead of leaving it in getPreviewText
2648 $previewOutput = '';
2649 if ( $this->formtype == 'preview' ) {
2650 $previewOutput = $this->getPreviewText();
2651 }
2652
2653 $out = $this->context->getOutput();
2654
2655 // Avoid PHP 7.1 warning of passing $this by reference
2656 $editPage = $this;
2657 Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$out ] );
2658
2659 $this->setHeaders();
2660
2661 $this->addTalkPageText();
2662 $this->addEditNotices();
2663
2664 if ( !$this->isConflict &&
2665 $this->section != '' &&
2666 !$this->isSectionEditSupported() ) {
2667 // We use $this->section to much before this and getVal('wgSection') directly in other places
2668 // at this point we can't reset $this->section to '' to fallback to non-section editing.
2669 // Someone is welcome to try refactoring though
2670 $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2671 return;
2672 }
2673
2674 $this->showHeader();
2675
2676 $out->addHTML( $this->editFormPageTop );
2677
2678 $user = $this->context->getUser();
2679 if ( $user->getOption( 'previewontop' ) ) {
2680 $this->displayPreviewArea( $previewOutput, true );
2681 }
2682
2683 $out->addHTML( $this->editFormTextTop );
2684
2685 $showToolbar = true;
2686 if ( $this->wasDeletedSinceLastEdit() ) {
2687 if ( $this->formtype == 'save' ) {
2688 // Hide the toolbar and edit area, user can click preview to get it back
2689 // Add an confirmation checkbox and explanation.
2690 $showToolbar = false;
2691 } else {
2692 $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2693 'deletedwhileediting' );
2694 }
2695 }
2696
2697 // @todo add EditForm plugin interface and use it here!
2698 // search for textarea1 and textarea2, and allow EditForm to override all uses.
2699 $out->addHTML( Html::openElement(
2700 'form',
2701 [
2702 'class' => 'mw-editform',
2703 'id' => self::EDITFORM_ID,
2704 'name' => self::EDITFORM_ID,
2705 'method' => 'post',
2706 'action' => $this->getActionURL( $this->getContextTitle() ),
2707 'enctype' => 'multipart/form-data'
2708 ]
2709 ) );
2710
2711 if ( is_callable( $formCallback ) ) {
2712 wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2713 call_user_func_array( $formCallback, [ &$out ] );
2714 }
2715
2716 // Add a check for Unicode support
2717 $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2718
2719 // Add an empty field to trip up spambots
2720 $out->addHTML(
2721 Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2722 . Html::rawElement(
2723 'label',
2724 [ 'for' => 'wpAntispam' ],
2725 $this->context->msg( 'simpleantispam-label' )->parse()
2726 )
2727 . Xml::element(
2728 'input',
2729 [
2730 'type' => 'text',
2731 'name' => 'wpAntispam',
2732 'id' => 'wpAntispam',
2733 'value' => ''
2734 ]
2735 )
2736 . Xml::closeElement( 'div' )
2737 );
2738
2739 // Avoid PHP 7.1 warning of passing $this by reference
2740 $editPage = $this;
2741 Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$out ] );
2742
2743 // Put these up at the top to ensure they aren't lost on early form submission
2744 $this->showFormBeforeText();
2745
2746 if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2747 $username = $this->lastDelete->user_name;
2748 $comment = CommentStore::newKey( 'log_comment' )->getComment( $this->lastDelete )->text;
2749
2750 // It is better to not parse the comment at all than to have templates expanded in the middle
2751 // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2752 $key = $comment === ''
2753 ? 'confirmrecreate-noreason'
2754 : 'confirmrecreate';
2755 $out->addHTML(
2756 '<div class="mw-confirm-recreate">' .
2757 $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2758 Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2759 [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2760 ) .
2761 '</div>'
2762 );
2763 }
2764
2765 # When the summary is hidden, also hide them on preview/show changes
2766 if ( $this->nosummary ) {
2767 $out->addHTML( Html::hidden( 'nosummary', true ) );
2768 }
2769
2770 # If a blank edit summary was previously provided, and the appropriate
2771 # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2772 # user being bounced back more than once in the event that a summary
2773 # is not required.
2774 # ####
2775 # For a bit more sophisticated detection of blank summaries, hash the
2776 # automatic one and pass that in the hidden field wpAutoSummary.
2777 if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2778 $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2779 }
2780
2781 if ( $this->undidRev ) {
2782 $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2783 }
2784
2785 if ( $this->selfRedirect ) {
2786 $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2787 }
2788
2789 if ( $this->hasPresetSummary ) {
2790 // If a summary has been preset using &summary= we don't want to prompt for
2791 // a different summary. Only prompt for a summary if the summary is blanked.
2792 // (T19416)
2793 $this->autoSumm = md5( '' );
2794 }
2795
2796 $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2797 $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2798
2799 $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2800 $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2801
2802 $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2803 $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
2804
2805 $out->enableOOUI();
2806
2807 if ( $this->section == 'new' ) {
2808 $this->showSummaryInput( true, $this->summary );
2809 $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2810 }
2811
2812 $out->addHTML( $this->editFormTextBeforeContent );
2813
2814 if ( !$this->mTitle->isCssJsSubpage() && $showToolbar && $user->getOption( 'showtoolbar' ) ) {
2815 $out->addHTML( self::getEditToolbar( $this->mTitle ) );
2816 }
2817
2818 if ( $this->blankArticle ) {
2819 $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2820 }
2821
2822 if ( $this->isConflict ) {
2823 // In an edit conflict bypass the overridable content form method
2824 // and fallback to the raw wpTextbox1 since editconflicts can't be
2825 // resolved between page source edits and custom ui edits using the
2826 // custom edit ui.
2827 $this->textbox2 = $this->textbox1;
2828
2829 $content = $this->getCurrentContent();
2830 $this->textbox1 = $this->toEditText( $content );
2831
2832 $this->showTextbox1();
2833 } else {
2834 $this->showContentForm();
2835 }
2836
2837 $out->addHTML( $this->editFormTextAfterContent );
2838
2839 $this->showStandardInputs();
2840
2841 $this->showFormAfterText();
2842
2843 $this->showTosSummary();
2844
2845 $this->showEditTools();
2846
2847 $out->addHTML( $this->editFormTextAfterTools . "\n" );
2848
2849 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
2850
2851 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2852 Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2853
2854 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2855 self::getPreviewLimitReport( $this->mParserOutput ) ) );
2856
2857 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2858
2859 if ( $this->isConflict ) {
2860 try {
2861 $this->showConflict();
2862 } catch ( MWContentSerializationException $ex ) {
2863 // this can't really happen, but be nice if it does.
2864 $msg = $this->context->msg(
2865 'content-failed-to-parse',
2866 $this->contentModel,
2867 $this->contentFormat,
2868 $ex->getMessage()
2869 );
2870 $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2871 }
2872 }
2873
2874 // Set a hidden field so JS knows what edit form mode we are in
2875 if ( $this->isConflict ) {
2876 $mode = 'conflict';
2877 } elseif ( $this->preview ) {
2878 $mode = 'preview';
2879 } elseif ( $this->diff ) {
2880 $mode = 'diff';
2881 } else {
2882 $mode = 'text';
2883 }
2884 $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
2885
2886 // Marker for detecting truncated form data. This must be the last
2887 // parameter sent in order to be of use, so do not move me.
2888 $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2889 $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2890
2891 if ( !$user->getOption( 'previewontop' ) ) {
2892 $this->displayPreviewArea( $previewOutput, false );
2893 }
2894 }
2895
2903 public function makeTemplatesOnThisPageList( array $templates ) {
2904 $templateListFormatter = new TemplatesOnThisPageFormatter(
2905 $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
2906 );
2907
2908 // preview if preview, else section if section, else false
2909 $type = false;
2910 if ( $this->preview ) {
2911 $type = 'preview';
2912 } elseif ( $this->section != '' ) {
2913 $type = 'section';
2914 }
2915
2916 return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2917 $templateListFormatter->format( $templates, $type )
2918 );
2919 }
2920
2927 public static function extractSectionTitle( $text ) {
2928 preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2929 if ( !empty( $matches[2] ) ) {
2931 return $wgParser->stripSectionName( trim( $matches[2] ) );
2932 } else {
2933 return false;
2934 }
2935 }
2936
2937 protected function showHeader() {
2938 $out = $this->context->getOutput();
2939 $user = $this->context->getUser();
2940 if ( $this->isConflict ) {
2942 $this->editRevId = $this->page->getLatest();
2943 } else {
2944 if ( $this->section != '' && $this->section != 'new' ) {
2945 if ( !$this->summary && !$this->preview && !$this->diff ) {
2946 $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2947 if ( $sectionTitle !== false ) {
2948 $this->summary = "/* $sectionTitle */ ";
2949 }
2950 }
2951 }
2952
2953 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
2954
2955 if ( $this->missingComment ) {
2956 $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
2957 }
2958
2959 if ( $this->missingSummary && $this->section != 'new' ) {
2960 $out->wrapWikiMsg(
2961 "<div id='mw-missingsummary'>\n$1\n</div>",
2962 [ 'missingsummary', $buttonLabel ]
2963 );
2964 }
2965
2966 if ( $this->missingSummary && $this->section == 'new' ) {
2967 $out->wrapWikiMsg(
2968 "<div id='mw-missingcommentheader'>\n$1\n</div>",
2969 [ 'missingcommentheader', $buttonLabel ]
2970 );
2971 }
2972
2973 if ( $this->blankArticle ) {
2974 $out->wrapWikiMsg(
2975 "<div id='mw-blankarticle'>\n$1\n</div>",
2976 [ 'blankarticle', $buttonLabel ]
2977 );
2978 }
2979
2980 if ( $this->selfRedirect ) {
2981 $out->wrapWikiMsg(
2982 "<div id='mw-selfredirect'>\n$1\n</div>",
2983 [ 'selfredirect', $buttonLabel ]
2984 );
2985 }
2986
2987 if ( $this->hookError !== '' ) {
2988 $out->addWikiText( $this->hookError );
2989 }
2990
2991 if ( $this->section != 'new' ) {
2992 $revision = $this->mArticle->getRevisionFetched();
2993 if ( $revision ) {
2994 // Let sysop know that this will make private content public if saved
2995
2996 if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) {
2997 $out->wrapWikiMsg(
2998 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2999 'rev-deleted-text-permission'
3000 );
3001 } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
3002 $out->wrapWikiMsg(
3003 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3004 'rev-deleted-text-view'
3005 );
3006 }
3007
3008 if ( !$revision->isCurrent() ) {
3009 $this->mArticle->setOldSubtitle( $revision->getId() );
3010 $out->addWikiMsg( 'editingold' );
3011 $this->isOldRev = true;
3012 }
3013 } elseif ( $this->mTitle->exists() ) {
3014 // Something went wrong
3015
3016 $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
3017 [ 'missing-revision', $this->oldid ] );
3018 }
3019 }
3020 }
3021
3022 if ( wfReadOnly() ) {
3023 $out->wrapWikiMsg(
3024 "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3025 [ 'readonlywarning', wfReadOnlyReason() ]
3026 );
3027 } elseif ( $user->isAnon() ) {
3028 if ( $this->formtype != 'preview' ) {
3029 $out->wrapWikiMsg(
3030 "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3031 [ 'anoneditwarning',
3032 // Log-in link
3033 SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
3034 'returnto' => $this->getTitle()->getPrefixedDBkey()
3035 ] ),
3036 // Sign-up link
3037 SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
3038 'returnto' => $this->getTitle()->getPrefixedDBkey()
3039 ] )
3040 ]
3041 );
3042 } else {
3043 $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3044 'anonpreviewwarning'
3045 );
3046 }
3047 } else {
3048 if ( $this->mTitle->isCssJsSubpage() ) {
3049 # Check the skin exists
3050 if ( $this->isWrongCaseCssJsPage() ) {
3051 $out->wrapWikiMsg(
3052 "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
3053 [ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
3054 );
3055 }
3056 if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
3057 $isCssSubpage = $this->mTitle->isCssSubpage();
3058 $out->wrapWikiMsg( '<div class="mw-usercssjspublic">$1</div>',
3059 $isCssSubpage ? 'usercssispublic' : 'userjsispublic'
3060 );
3061 if ( $this->formtype !== 'preview' ) {
3062 $config = $this->context->getConfig();
3063 if ( $isCssSubpage && $config->get( 'AllowUserCss' ) ) {
3064 $out->wrapWikiMsg(
3065 "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3066 [ 'usercssyoucanpreview' ]
3067 );
3068 }
3069
3070 if ( $this->mTitle->isJsSubpage() && $config->get( 'AllowUserJs' ) ) {
3071 $out->wrapWikiMsg(
3072 "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3073 [ 'userjsyoucanpreview' ]
3074 );
3075 }
3076 }
3077 }
3078 }
3079 }
3080
3082
3083 $this->addLongPageWarningHeader();
3084
3085 # Add header copyright warning
3087 }
3088
3096 private function getSummaryInputAttributes( array $inputAttrs = null ) {
3097 // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
3098 return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3099 'id' => 'wpSummary',
3100 'name' => 'wpSummary',
3101 'maxlength' => '200',
3102 'tabindex' => 1,
3103 'size' => 60,
3104 'spellcheck' => 'true',
3105 ];
3106 }
3107
3122 public function getSummaryInput( $summary = "", $labelText = null,
3123 $inputAttrs = null, $spanLabelAttrs = null
3124 ) {
3125 wfDeprecated( __METHOD__, '1.30' );
3126 $inputAttrs = $this->getSummaryInputAttributes( $inputAttrs );
3127 $inputAttrs += Linker::tooltipAndAccesskeyAttribs( 'summary' );
3128
3129 $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
3130 'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
3131 'id' => "wpSummaryLabel"
3132 ];
3133
3134 $label = null;
3135 if ( $labelText ) {
3136 $label = Xml::tags(
3137 'label',
3138 $inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null,
3139 $labelText
3140 );
3141 $label = Xml::tags( 'span', $spanLabelAttrs, $label );
3142 }
3143
3144 $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
3145
3146 return [ $label, $input ];
3147 }
3148
3159 function getSummaryInputOOUI( $summary = "", $labelText = null, $inputAttrs = null ) {
3160 wfDeprecated( __METHOD__, '1.30' );
3161 $this->getSummaryInputWidget( $summary, $labelText, $inputAttrs );
3162 }
3163
3173 function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) {
3174 $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3175 $this->getSummaryInputAttributes( $inputAttrs )
3176 );
3177 $inputAttrs += [
3178 'title' => Linker::titleAttrib( 'summary' ),
3179 'accessKey' => Linker::accesskey( 'summary' ),
3180 ];
3181
3182 // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3183 $inputAttrs['inputId'] = $inputAttrs['id'];
3184 $inputAttrs['id'] = 'wpSummaryWidget';
3185
3186 return new OOUI\FieldLayout(
3187 new OOUI\TextInputWidget( [
3188 'value' => $summary,
3189 'infusable' => true,
3190 ] + $inputAttrs ),
3191 [
3192 'label' => new OOUI\HtmlSnippet( $labelText ),
3193 'align' => 'top',
3194 'id' => 'wpSummaryLabel',
3195 'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3196 ]
3197 );
3198 }
3199
3206 protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3207 # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3208 $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3209 if ( $isSubjectPreview ) {
3210 if ( $this->nosummary ) {
3211 return;
3212 }
3213 } else {
3214 if ( !$this->mShowSummaryField ) {
3215 return;
3216 }
3217 }
3218
3219 $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3220 $this->context->getOutput()->addHTML( $this->getSummaryInputWidget(
3221 $summary,
3222 $labelText,
3223 [ 'class' => $summaryClass ]
3224 ) );
3225 }
3226
3234 protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3235 // avoid spaces in preview, gets always trimmed on save
3236 $summary = trim( $summary );
3237 if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3238 return "";
3239 }
3240
3242
3243 if ( $isSubjectPreview ) {
3244 $summary = $this->context->msg( 'newsectionsummary' )
3245 ->rawParams( $wgParser->stripSectionName( $summary ) )
3246 ->inContentLanguage()->text();
3247 }
3248
3249 $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3250
3251 $summary = $this->context->msg( $message )->parse()
3252 . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3253 return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3254 }
3255
3256 protected function showFormBeforeText() {
3257 $out = $this->context->getOutput();
3258 $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3259 $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3260 $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3261 $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3262 $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3263 }
3264
3265 protected function showFormAfterText() {
3278 $this->context->getOutput()->addHTML(
3279 "\n" .
3280 Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3281 "\n"
3282 );
3283 }
3284
3293 protected function showContentForm() {
3294 $this->showTextbox1();
3295 }
3296
3305 protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3306 if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3307 $attribs = [ 'style' => 'display:none;' ];
3308 } else {
3309 $classes = []; // Textarea CSS
3310 if ( $this->mTitle->isProtected( 'edit' ) &&
3311 MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
3312 ) {
3313 # Is the title semi-protected?
3314 if ( $this->mTitle->isSemiProtected() ) {
3315 $classes[] = 'mw-textarea-sprotected';
3316 } else {
3317 # Then it must be protected based on static groups (regular)
3318 $classes[] = 'mw-textarea-protected';
3319 }
3320 # Is the title cascade-protected?
3321 if ( $this->mTitle->isCascadeProtected() ) {
3322 $classes[] = 'mw-textarea-cprotected';
3323 }
3324 }
3325 # Is an old revision being edited?
3326 if ( $this->isOldRev ) {
3327 $classes[] = 'mw-textarea-oldrev';
3328 }
3329
3330 $attribs = [ 'tabindex' => 1 ];
3331
3332 if ( is_array( $customAttribs ) ) {
3334 }
3335
3336 if ( count( $classes ) ) {
3337 if ( isset( $attribs['class'] ) ) {
3338 $classes[] = $attribs['class'];
3339 }
3340 $attribs['class'] = implode( ' ', $classes );
3341 }
3342 }
3343
3344 $this->showTextbox(
3345 $textoverride !== null ? $textoverride : $this->textbox1,
3346 'wpTextbox1',
3347 $attribs
3348 );
3349 }
3350
3351 protected function showTextbox2() {
3352 $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3353 }
3354
3355 protected function showTextbox( $text, $name, $customAttribs = [] ) {
3356 $wikitext = $this->addNewLineAtEnd( $text );
3357
3358 $attribs = $this->buildTextboxAttribs( $name, $customAttribs, $this->context->getUser() );
3359
3360 $this->context->getOutput()->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
3361 }
3362
3363 protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3364 $classes = [];
3365 if ( $isOnTop ) {
3366 $classes[] = 'ontop';
3367 }
3368
3369 $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3370
3371 if ( $this->formtype != 'preview' ) {
3372 $attribs['style'] = 'display: none;';
3373 }
3374
3375 $out = $this->context->getOutput();
3376 $out->addHTML( Xml::openElement( 'div', $attribs ) );
3377
3378 if ( $this->formtype == 'preview' ) {
3379 $this->showPreview( $previewOutput );
3380 } else {
3381 // Empty content container for LivePreview
3382 $pageViewLang = $this->mTitle->getPageViewLanguage();
3383 $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3384 'class' => 'mw-content-' . $pageViewLang->getDir() ];
3385 $out->addHTML( Html::rawElement( 'div', $attribs ) );
3386 }
3387
3388 $out->addHTML( '</div>' );
3389
3390 if ( $this->formtype == 'diff' ) {
3391 try {
3392 $this->showDiff();
3393 } catch ( MWContentSerializationException $ex ) {
3394 $msg = $this->context->msg(
3395 'content-failed-to-parse',
3396 $this->contentModel,
3397 $this->contentFormat,
3398 $ex->getMessage()
3399 );
3400 $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3401 }
3402 }
3403 }
3404
3411 protected function showPreview( $text ) {
3412 if ( $this->mArticle instanceof CategoryPage ) {
3413 $this->mArticle->openShowCategory();
3414 }
3415 # This hook seems slightly odd here, but makes things more
3416 # consistent for extensions.
3417 $out = $this->context->getOutput();
3418 Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
3419 $out->addHTML( $text );
3420 if ( $this->mArticle instanceof CategoryPage ) {
3421 $this->mArticle->closeShowCategory();
3422 }
3423 }
3424
3432 public function showDiff() {
3434
3435 $oldtitlemsg = 'currentrev';
3436 # if message does not exist, show diff against the preloaded default
3437 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3438 $oldtext = $this->mTitle->getDefaultMessageText();
3439 if ( $oldtext !== false ) {
3440 $oldtitlemsg = 'defaultmessagetext';
3441 $oldContent = $this->toEditContent( $oldtext );
3442 } else {
3443 $oldContent = null;
3444 }
3445 } else {
3446 $oldContent = $this->getCurrentContent();
3447 }
3448
3449 $textboxContent = $this->toEditContent( $this->textbox1 );
3450 if ( $this->editRevId !== null ) {
3451 $newContent = $this->page->replaceSectionAtRev(
3452 $this->section, $textboxContent, $this->summary, $this->editRevId
3453 );
3454 } else {
3455 $newContent = $this->page->replaceSectionContent(
3456 $this->section, $textboxContent, $this->summary, $this->edittime
3457 );
3458 }
3459
3460 if ( $newContent ) {
3461 Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3462
3463 $user = $this->context->getUser();
3464 $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
3465 $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
3466 }
3467
3468 if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3469 $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3470 $newtitle = $this->context->msg( 'yourtext' )->parse();
3471
3472 if ( !$oldContent ) {
3473 $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3474 }
3475
3476 if ( !$newContent ) {
3477 $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3478 }
3479
3480 $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3481 $de->setContent( $oldContent, $newContent );
3482
3483 $difftext = $de->getDiff( $oldtitle, $newtitle );
3484 $de->showDiffStyle();
3485 } else {
3486 $difftext = '';
3487 }
3488
3489 $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3490 }
3491
3495 protected function showHeaderCopyrightWarning() {
3496 $msg = 'editpage-head-copy-warn';
3497 if ( !$this->context->msg( $msg )->isDisabled() ) {
3498 $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3499 'editpage-head-copy-warn' );
3500 }
3501 }
3502
3511 protected function showTosSummary() {
3512 $msg = 'editpage-tos-summary';
3513 Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3514 if ( !$this->context->msg( $msg )->isDisabled() ) {
3515 $out = $this->context->getOutput();
3516 $out->addHTML( '<div class="mw-tos-summary">' );
3517 $out->addWikiMsg( $msg );
3518 $out->addHTML( '</div>' );
3519 }
3520 }
3521
3526 protected function showEditTools() {
3527 $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3528 $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3529 '</div>' );
3530 }
3531
3538 protected function getCopywarn() {
3539 return self::getCopyrightWarning( $this->mTitle );
3540 }
3541
3550 public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3552 if ( $wgRightsText ) {
3553 $copywarnMsg = [ 'copyrightwarning',
3554 '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3555 $wgRightsText ];
3556 } else {
3557 $copywarnMsg = [ 'copyrightwarning2',
3558 '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3559 }
3560 // Allow for site and per-namespace customization of contribution/copyright notice.
3561 Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3562
3563 $msg = call_user_func_array( 'wfMessage', $copywarnMsg )->title( $title );
3564 if ( $langcode ) {
3565 $msg->inLanguage( $langcode );
3566 }
3567 return "<div id=\"editpage-copywarn\">\n" .
3568 $msg->$format() . "\n</div>";
3569 }
3570
3578 public static function getPreviewLimitReport( $output ) {
3579 if ( !$output || !$output->getLimitReportData() ) {
3580 return '';
3581 }
3582
3583 $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3584 wfMessage( 'limitreport-title' )->parseAsBlock()
3585 );
3586
3587 // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3588 $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3589
3590 $limitReport .= Html::openElement( 'table', [
3591 'class' => 'preview-limit-report wikitable'
3592 ] ) .
3593 Html::openElement( 'tbody' );
3594
3595 foreach ( $output->getLimitReportData() as $key => $value ) {
3596 if ( Hooks::run( 'ParserLimitReportFormat',
3597 [ $key, &$value, &$limitReport, true, true ]
3598 ) ) {
3599 $keyMsg = wfMessage( $key );
3600 $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3601 if ( !$valueMsg->exists() ) {
3602 $valueMsg = new RawMessage( '$1' );
3603 }
3604 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3605 $limitReport .= Html::openElement( 'tr' ) .
3606 Html::rawElement( 'th', null, $keyMsg->parse() ) .
3607 Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) .
3608 Html::closeElement( 'tr' );
3609 }
3610 }
3611 }
3612
3613 $limitReport .= Html::closeElement( 'tbody' ) .
3614 Html::closeElement( 'table' ) .
3615 Html::closeElement( 'div' );
3616
3617 return $limitReport;
3618 }
3619
3620 protected function showStandardInputs( &$tabindex = 2 ) {
3621 $out = $this->context->getOutput();
3622 $out->addHTML( "<div class='editOptions'>\n" );
3623
3624 if ( $this->section != 'new' ) {
3625 $this->showSummaryInput( false, $this->summary );
3626 $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3627 }
3628
3629 $checkboxes = $this->getCheckboxesWidget(
3630 $tabindex,
3631 [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3632 );
3633 $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] );
3634
3635 $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3636
3637 // Show copyright warning.
3638 $out->addWikiText( $this->getCopywarn() );
3639 $out->addHTML( $this->editFormTextAfterWarn );
3640
3641 $out->addHTML( "<div class='editButtons'>\n" );
3642 $out->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
3643
3644 $cancel = $this->getCancelLink();
3645 if ( $cancel !== '' ) {
3646 $cancel .= Html::element( 'span',
3647 [ 'class' => 'mw-editButtons-pipe-separator' ],
3648 $this->context->msg( 'pipe-separator' )->text() );
3649 }
3650
3651 $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3652 $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3653 $edithelp =
3654 Html::linkButton(
3655 $this->context->msg( 'edithelp' )->text(),
3656 [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3657 [ 'mw-ui-quiet' ]
3658 ) .
3659 $this->context->msg( 'word-separator' )->escaped() .
3660 $this->context->msg( 'newwindow' )->parse();
3661
3662 $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3663 $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3664 $out->addHTML( "</div><!-- editButtons -->\n" );
3665
3666 Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
3667
3668 $out->addHTML( "</div><!-- editOptions -->\n" );
3669 }
3670
3675 protected function showConflict() {
3676 $out = $this->context->getOutput();
3677 // Avoid PHP 7.1 warning of passing $this by reference
3678 $editPage = $this;
3679 if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$out ] ) ) {
3680 $this->incrementConflictStats();
3681
3682 $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3683
3684 $content1 = $this->toEditContent( $this->textbox1 );
3685 $content2 = $this->toEditContent( $this->textbox2 );
3686
3687 $handler = ContentHandler::getForModelID( $this->contentModel );
3688 $de = $handler->createDifferenceEngine( $this->context );
3689 $de->setContent( $content2, $content1 );
3690 $de->showDiff(
3691 $this->context->msg( 'yourtext' )->parse(),
3692 $this->context->msg( 'storedversion' )->text()
3693 );
3694
3695 $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3696 $this->showTextbox2();
3697 }
3698 }
3699
3700 protected function incrementConflictStats() {
3701 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
3702 $stats->increment( 'edit.failures.conflict' );
3703 // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
3704 if (
3705 $this->mTitle->getNamespace() >= NS_MAIN &&
3706 $this->mTitle->getNamespace() <= NS_CATEGORY_TALK
3707 ) {
3708 $stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() );
3709 }
3710 }
3711
3715 public function getCancelLink() {
3716 $cancelParams = [];
3717 if ( !$this->isConflict && $this->oldid > 0 ) {
3718 $cancelParams['oldid'] = $this->oldid;
3719 } elseif ( $this->getContextTitle()->isRedirect() ) {
3720 $cancelParams['redirect'] = 'no';
3721 }
3722
3723 return new OOUI\ButtonWidget( [
3724 'id' => 'mw-editform-cancel',
3725 'href' => $this->getContextTitle()->getLinkUrl( $cancelParams ),
3726 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3727 'framed' => false,
3728 'infusable' => true,
3729 'flags' => 'destructive',
3730 ] );
3731 }
3732
3742 protected function getActionURL( Title $title ) {
3743 return $title->getLocalURL( [ 'action' => $this->action ] );
3744 }
3745
3753 protected function wasDeletedSinceLastEdit() {
3754 if ( $this->deletedSinceEdit !== null ) {
3756 }
3757
3758 $this->deletedSinceEdit = false;
3759
3760 if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3761 $this->lastDelete = $this->getLastDelete();
3762 if ( $this->lastDelete ) {
3763 $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3764 if ( $deleteTime > $this->starttime ) {
3765 $this->deletedSinceEdit = true;
3766 }
3767 }
3768 }
3769
3771 }
3772
3776 protected function getLastDelete() {
3777 $dbr = wfGetDB( DB_REPLICA );
3778 $commentQuery = CommentStore::newKey( 'log_comment' )->getJoin();
3779 $data = $dbr->selectRow(
3780 [ 'logging', 'user' ] + $commentQuery['tables'],
3781 [
3782 'log_type',
3783 'log_action',
3784 'log_timestamp',
3785 'log_user',
3786 'log_namespace',
3787 'log_title',
3788 'log_params',
3789 'log_deleted',
3790 'user_name'
3791 ] + $commentQuery['fields'], [
3792 'log_namespace' => $this->mTitle->getNamespace(),
3793 'log_title' => $this->mTitle->getDBkey(),
3794 'log_type' => 'delete',
3795 'log_action' => 'delete',
3796 'user_id=log_user'
3797 ],
3798 __METHOD__,
3799 [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
3800 [
3801 'user' => [ 'JOIN', 'user_id=log_user' ],
3802 ] + $commentQuery['joins']
3803 );
3804 // Quick paranoid permission checks...
3805 if ( is_object( $data ) ) {
3806 if ( $data->log_deleted & LogPage::DELETED_USER ) {
3807 $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
3808 }
3809
3810 if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3811 $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
3812 $data->log_comment_data = null;
3813 }
3814 }
3815
3816 return $data;
3817 }
3818
3824 public function getPreviewText() {
3825 $out = $this->context->getOutput();
3826 $config = $this->context->getConfig();
3827
3828 if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) {
3829 // Could be an offsite preview attempt. This is very unsafe if
3830 // HTML is enabled, as it could be an attack.
3831 $parsedNote = '';
3832 if ( $this->textbox1 !== '' ) {
3833 // Do not put big scary notice, if previewing the empty
3834 // string, which happens when you initially edit
3835 // a category page, due to automatic preview-on-open.
3836 $parsedNote = $out->parse( "<div class='previewnote'>" .
3837 $this->context->msg( 'session_fail_preview_html' )->text() . "</div>",
3838 true, /* interface */true );
3839 }
3840 $this->incrementEditFailureStats( 'session_loss' );
3841 return $parsedNote;
3842 }
3843
3844 $note = '';
3845
3846 try {
3847 $content = $this->toEditContent( $this->textbox1 );
3848
3849 $previewHTML = '';
3850 if ( !Hooks::run(
3851 'AlternateEditPreview',
3852 [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3853 ) {
3854 return $previewHTML;
3855 }
3856
3857 # provide a anchor link to the editform
3858 $continueEditing = '<span class="mw-continue-editing">' .
3859 '[[#' . self::EDITFORM_ID . '|' .
3860 $this->context->getLanguage()->getArrow() . ' ' .
3861 $this->context->msg( 'continue-editing' )->text() . ']]</span>';
3862 if ( $this->mTriedSave && !$this->mTokenOk ) {
3863 if ( $this->mTokenOkExceptSuffix ) {
3864 $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
3865 $this->incrementEditFailureStats( 'bad_token' );
3866 } else {
3867 $note = $this->context->msg( 'session_fail_preview' )->plain();
3868 $this->incrementEditFailureStats( 'session_loss' );
3869 }
3870 } elseif ( $this->incompleteForm ) {
3871 $note = $this->context->msg( 'edit_form_incomplete' )->plain();
3872 if ( $this->mTriedSave ) {
3873 $this->incrementEditFailureStats( 'incomplete_form' );
3874 }
3875 } else {
3876 $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
3877 }
3878
3879 # don't parse non-wikitext pages, show message about preview
3880 if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
3881 if ( $this->mTitle->isCssJsSubpage() ) {
3882 $level = 'user';
3883 } elseif ( $this->mTitle->isCssOrJsPage() ) {
3884 $level = 'site';
3885 } else {
3886 $level = false;
3887 }
3888
3889 if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3890 $format = 'css';
3891 if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
3892 $format = false;
3893 }
3894 } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3895 $format = 'js';
3896 if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
3897 $format = false;
3898 }
3899 } else {
3900 $format = false;
3901 }
3902
3903 # Used messages to make sure grep find them:
3904 # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
3905 if ( $level && $format ) {
3906 $note = "<div id='mw-{$level}{$format}preview'>" .
3907 $this->context->msg( "{$level}{$format}preview" )->text() .
3908 ' ' . $continueEditing . "</div>";
3909 }
3910 }
3911
3912 # If we're adding a comment, we need to show the
3913 # summary as the headline
3914 if ( $this->section === "new" && $this->summary !== "" ) {
3915 $content = $content->addSectionHeader( $this->summary );
3916 }
3917
3918 $hook_args = [ $this, &$content ];
3919 Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3920
3921 $parserResult = $this->doPreviewParse( $content );
3922 $parserOutput = $parserResult['parserOutput'];
3923 $previewHTML = $parserResult['html'];
3924 $this->mParserOutput = $parserOutput;
3925 $out->addParserOutputMetadata( $parserOutput );
3926
3927 if ( count( $parserOutput->getWarnings() ) ) {
3928 $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3929 }
3930
3931 } catch ( MWContentSerializationException $ex ) {
3932 $m = $this->context->msg(
3933 'content-failed-to-parse',
3934 $this->contentModel,
3935 $this->contentFormat,
3936 $ex->getMessage()
3937 );
3938 $note .= "\n\n" . $m->parse();
3939 $previewHTML = '';
3940 }
3941
3942 if ( $this->isConflict ) {
3943 $conflict = '<h2 id="mw-previewconflict">'
3944 . $this->context->msg( 'previewconflict' )->escaped() . "</h2>\n";
3945 } else {
3946 $conflict = '<hr />';
3947 }
3948
3949 $previewhead = "<div class='previewnote'>\n" .
3950 '<h2 id="mw-previewheader">' . $this->context->msg( 'preview' )->escaped() . "</h2>" .
3951 $out->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3952
3953 $pageViewLang = $this->mTitle->getPageViewLanguage();
3954 $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3955 'class' => 'mw-content-' . $pageViewLang->getDir() ];
3956 $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3957
3958 return $previewhead . $previewHTML . $this->previewTextAfterContent;
3959 }
3960
3961 private function incrementEditFailureStats( $failureType ) {
3962 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
3963 $stats->increment( 'edit.failures.' . $failureType );
3964 }
3965
3970 protected function getPreviewParserOptions() {
3971 $parserOptions = $this->page->makeParserOptions( $this->context );
3972 $parserOptions->setIsPreview( true );
3973 $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3974 $parserOptions->enableLimitReport();
3975 return $parserOptions;
3976 }
3977
3987 protected function doPreviewParse( Content $content ) {
3988 $user = $this->context->getUser();
3989 $parserOptions = $this->getPreviewParserOptions();
3990 $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
3991 $scopedCallback = $parserOptions->setupFakeRevision(
3992 $this->mTitle, $pstContent, $user );
3993 $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
3994 ScopedCallback::consume( $scopedCallback );
3995 $parserOutput->setEditSectionTokens( false ); // no section edit links
3996 return [
3997 'parserOutput' => $parserOutput,
3998 'html' => $parserOutput->getText() ];
3999 }
4000
4004 public function getTemplates() {
4005 if ( $this->preview || $this->section != '' ) {
4006 $templates = [];
4007 if ( !isset( $this->mParserOutput ) ) {
4008 return $templates;
4009 }
4010 foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
4011 foreach ( array_keys( $template ) as $dbk ) {
4012 $templates[] = Title::makeTitle( $ns, $dbk );
4013 }
4014 }
4015 return $templates;
4016 } else {
4017 return $this->mTitle->getTemplateLinksFrom();
4018 }
4019 }
4020
4028 public static function getEditToolbar( $title = null ) {
4031
4032 $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
4033 $showSignature = true;
4034 if ( $title ) {
4035 $showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
4036 }
4037
4047 $toolarray = [
4048 [
4049 'id' => 'mw-editbutton-bold',
4050 'open' => '\'\'\'',
4051 'close' => '\'\'\'',
4052 'sample' => wfMessage( 'bold_sample' )->text(),
4053 'tip' => wfMessage( 'bold_tip' )->text(),
4054 ],
4055 [
4056 'id' => 'mw-editbutton-italic',
4057 'open' => '\'\'',
4058 'close' => '\'\'',
4059 'sample' => wfMessage( 'italic_sample' )->text(),
4060 'tip' => wfMessage( 'italic_tip' )->text(),
4061 ],
4062 [
4063 'id' => 'mw-editbutton-link',
4064 'open' => '[[',
4065 'close' => ']]',
4066 'sample' => wfMessage( 'link_sample' )->text(),
4067 'tip' => wfMessage( 'link_tip' )->text(),
4068 ],
4069 [
4070 'id' => 'mw-editbutton-extlink',
4071 'open' => '[',
4072 'close' => ']',
4073 'sample' => wfMessage( 'extlink_sample' )->text(),
4074 'tip' => wfMessage( 'extlink_tip' )->text(),
4075 ],
4076 [
4077 'id' => 'mw-editbutton-headline',
4078 'open' => "\n== ",
4079 'close' => " ==\n",
4080 'sample' => wfMessage( 'headline_sample' )->text(),
4081 'tip' => wfMessage( 'headline_tip' )->text(),
4082 ],
4083 $imagesAvailable ? [
4084 'id' => 'mw-editbutton-image',
4085 'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
4086 'close' => ']]',
4087 'sample' => wfMessage( 'image_sample' )->text(),
4088 'tip' => wfMessage( 'image_tip' )->text(),
4089 ] : false,
4090 $imagesAvailable ? [
4091 'id' => 'mw-editbutton-media',
4092 'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
4093 'close' => ']]',
4094 'sample' => wfMessage( 'media_sample' )->text(),
4095 'tip' => wfMessage( 'media_tip' )->text(),
4096 ] : false,
4097 [
4098 'id' => 'mw-editbutton-nowiki',
4099 'open' => "<nowiki>",
4100 'close' => "</nowiki>",
4101 'sample' => wfMessage( 'nowiki_sample' )->text(),
4102 'tip' => wfMessage( 'nowiki_tip' )->text(),
4103 ],
4104 $showSignature ? [
4105 'id' => 'mw-editbutton-signature',
4106 'open' => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(),
4107 'close' => '',
4108 'sample' => '',
4109 'tip' => wfMessage( 'sig_tip' )->text(),
4110 ] : false,
4111 [
4112 'id' => 'mw-editbutton-hr',
4113 'open' => "\n----\n",
4114 'close' => '',
4115 'sample' => '',
4116 'tip' => wfMessage( 'hr_tip' )->text(),
4117 ]
4118 ];
4119
4120 $script = 'mw.loader.using("mediawiki.toolbar", function () {';
4121 foreach ( $toolarray as $tool ) {
4122 if ( !$tool ) {
4123 continue;
4124 }
4125
4126 $params = [
4127 // Images are defined in ResourceLoaderEditToolbarModule
4128 false,
4129 // Note that we use the tip both for the ALT tag and the TITLE tag of the image.
4130 // Older browsers show a "speedtip" type message only for ALT.
4131 // Ideally these should be different, realistically they
4132 // probably don't need to be.
4133 $tool['tip'],
4134 $tool['open'],
4135 $tool['close'],
4136 $tool['sample'],
4137 $tool['id'],
4138 ];
4139
4140 $script .= Xml::encodeJsCall(
4141 'mw.toolbar.addButton',
4142 $params,
4144 );
4145 }
4146
4147 $script .= '});';
4148
4149 $toolbar = '<div id="toolbar"></div>';
4150
4151 if ( Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
4152 // Only add the old toolbar cruft to the page payload if the toolbar has not
4153 // been over-written by a hook caller
4154 $wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
4155 };
4156
4157 return $toolbar;
4158 }
4159
4178 public function getCheckboxesDefinition( $checked ) {
4179 $checkboxes = [];
4180
4181 $user = $this->context->getUser();
4182 // don't show the minor edit checkbox if it's a new page or section
4183 if ( !$this->isNew && $user->isAllowed( 'minoredit' ) ) {
4184 $checkboxes['wpMinoredit'] = [
4185 'id' => 'wpMinoredit',
4186 'label-message' => 'minoredit',
4187 // Uses messages: tooltip-minoredit, accesskey-minoredit
4188 'tooltip' => 'minoredit',
4189 'label-id' => 'mw-editpage-minoredit',
4190 'legacy-name' => 'minor',
4191 'default' => $checked['minor'],
4192 ];
4193 }
4194
4195 if ( $user->isLoggedIn() ) {
4196 $checkboxes['wpWatchthis'] = [
4197 'id' => 'wpWatchthis',
4198 'label-message' => 'watchthis',
4199 // Uses messages: tooltip-watch, accesskey-watch
4200 'tooltip' => 'watch',
4201 'label-id' => 'mw-editpage-watch',
4202 'legacy-name' => 'watch',
4203 'default' => $checked['watch'],
4204 ];
4205 }
4206
4207 $editPage = $this;
4208 Hooks::run( 'EditPageGetCheckboxesDefinition', [ $editPage, &$checkboxes ] );
4209
4210 return $checkboxes;
4211 }
4212
4222 public function getCheckboxes( &$tabindex, $checked ) {
4224
4225 $checkboxes = [];
4226 $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4227
4228 // Backwards-compatibility for the EditPageBeforeEditChecks hook
4229 if ( !$this->isNew ) {
4230 $checkboxes['minor'] = '';
4231 }
4232 $checkboxes['watch'] = '';
4233
4234 foreach ( $checkboxesDef as $name => $options ) {
4235 $legacyName = isset( $options['legacy-name'] ) ? $options['legacy-name'] : $name;
4236 $label = $this->context->msg( $options['label-message'] )->parse();
4237 $attribs = [
4238 'tabindex' => ++$tabindex,
4239 'id' => $options['id'],
4240 ];
4241 $labelAttribs = [
4242 'for' => $options['id'],
4243 ];
4244 if ( isset( $options['tooltip'] ) ) {
4245 $attribs['accesskey'] = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4246 $labelAttribs['title'] = Linker::titleAttrib( $options['tooltip'], 'withaccess' );
4247 }
4248 if ( isset( $options['title-message'] ) ) {
4249 $labelAttribs['title'] = $this->context->msg( $options['title-message'] )->text();
4250 }
4251 if ( isset( $options['label-id'] ) ) {
4252 $labelAttribs['id'] = $options['label-id'];
4253 }
4254 $checkboxHtml =
4255 Xml::check( $name, $options['default'], $attribs ) .
4256 '&#160;' .
4257 Xml::tags( 'label', $labelAttribs, $label );
4258
4260 $checkboxHtml = Html::rawElement( 'div', [ 'class' => 'mw-ui-checkbox' ], $checkboxHtml );
4261 }
4262
4263 $checkboxes[ $legacyName ] = $checkboxHtml;
4264 }
4265
4266 // Avoid PHP 7.1 warning of passing $this by reference
4267 $editPage = $this;
4268 Hooks::run( 'EditPageBeforeEditChecks', [ &$editPage, &$checkboxes, &$tabindex ], '1.29' );
4269 return $checkboxes;
4270 }
4271
4283 public function getCheckboxesOOUI( &$tabindex, $checked ) {
4284 return $this->getCheckboxesWidget( $tabindex, $checked );
4285 }
4286
4297 public function getCheckboxesWidget( &$tabindex, $checked ) {
4298 $checkboxes = [];
4299 $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4300
4301 foreach ( $checkboxesDef as $name => $options ) {
4302 $legacyName = isset( $options['legacy-name'] ) ? $options['legacy-name'] : $name;
4303
4304 $title = null;
4305 $accesskey = null;
4306 if ( isset( $options['tooltip'] ) ) {
4307 $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4308 $title = Linker::titleAttrib( $options['tooltip'] );
4309 }
4310 if ( isset( $options['title-message'] ) ) {
4311 $title = $this->context->msg( $options['title-message'] )->text();
4312 }
4313
4314 $checkboxes[ $legacyName ] = new OOUI\FieldLayout(
4315 new OOUI\CheckboxInputWidget( [
4316 'tabIndex' => ++$tabindex,
4317 'accessKey' => $accesskey,
4318 'id' => $options['id'] . 'Widget',
4319 'inputId' => $options['id'],
4320 'name' => $name,
4321 'selected' => $options['default'],
4322 'infusable' => true,
4323 ] ),
4324 [
4325 'align' => 'inline',
4326 'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4327 'title' => $title,
4328 'id' => isset( $options['label-id'] ) ? $options['label-id'] : null,
4329 ]
4330 );
4331 }
4332
4333 // Backwards-compatibility hack to run the EditPageBeforeEditChecks hook. It's important,
4334 // people have used it for the weirdest things completely unrelated to checkboxes...
4335 // And if we're gonna run it, might as well allow its legacy checkboxes to be shown.
4336 $legacyCheckboxes = [];
4337 if ( !$this->isNew ) {
4338 $legacyCheckboxes['minor'] = '';
4339 }
4340 $legacyCheckboxes['watch'] = '';
4341 // Copy new-style checkboxes into an old-style structure
4342 foreach ( $checkboxes as $name => $oouiLayout ) {
4343 $legacyCheckboxes[$name] = (string)$oouiLayout;
4344 }
4345 // Avoid PHP 7.1 warning of passing $this by reference
4346 $ep = $this;
4347 Hooks::run( 'EditPageBeforeEditChecks', [ &$ep, &$legacyCheckboxes, &$tabindex ], '1.29' );
4348 // Copy back any additional old-style checkboxes into the new-style structure
4349 foreach ( $legacyCheckboxes as $name => $html ) {
4350 if ( $html && !isset( $checkboxes[$name] ) ) {
4351 $checkboxes[$name] = new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $html ) ] );
4352 }
4353 }
4354
4355 return $checkboxes;
4356 }
4357
4364 protected function getSubmitButtonLabel() {
4365 $labelAsPublish =
4366 $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4367
4368 // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4369 $newPage = !$this->mTitle->exists();
4370
4371 if ( $labelAsPublish ) {
4372 $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4373 } else {
4374 $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4375 }
4376
4377 return $buttonLabelKey;
4378 }
4379
4388 public function getEditButtons( &$tabindex ) {
4389 $buttons = [];
4390
4391 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4392
4393 $attribs = [
4394 'name' => 'wpSave',
4395 'tabindex' => ++$tabindex,
4396 ];
4397
4398 $saveConfig = OOUI\Element::configFromHtmlAttributes( $attribs );
4399 $buttons['save'] = new OOUI\ButtonInputWidget( [
4400 'id' => 'wpSaveWidget',
4401 'inputId' => 'wpSave',
4402 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4403 'useInputTag' => true,
4404 'flags' => [ 'constructive', 'primary' ],
4405 'label' => $buttonLabel,
4406 'infusable' => true,
4407 'type' => 'submit',
4408 'title' => Linker::titleAttrib( 'save' ),
4409 'accessKey' => Linker::accesskey( 'save' ),
4410 ] + $saveConfig );
4411
4412 $attribs = [
4413 'name' => 'wpPreview',
4414 'tabindex' => ++$tabindex,
4415 ];
4416
4417 $previewConfig = OOUI\Element::configFromHtmlAttributes( $attribs );
4418 $buttons['preview'] = new OOUI\ButtonInputWidget( [
4419 'id' => 'wpPreviewWidget',
4420 'inputId' => 'wpPreview',
4421 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4422 'useInputTag' => true,
4423 'label' => $this->context->msg( 'showpreview' )->text(),
4424 'infusable' => true,
4425 'type' => 'submit',
4426 'title' => Linker::titleAttrib( 'preview' ),
4427 'accessKey' => Linker::accesskey( 'preview' ),
4428 ] + $previewConfig );
4429
4430 $attribs = [
4431 'name' => 'wpDiff',
4432 'tabindex' => ++$tabindex,
4433 ];
4434
4435 $diffConfig = OOUI\Element::configFromHtmlAttributes( $attribs );
4436 $buttons['diff'] = new OOUI\ButtonInputWidget( [
4437 'id' => 'wpDiffWidget',
4438 'inputId' => 'wpDiff',
4439 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4440 'useInputTag' => true,
4441 'label' => $this->context->msg( 'showdiff' )->text(),
4442 'infusable' => true,
4443 'type' => 'submit',
4444 'title' => Linker::titleAttrib( 'diff' ),
4445 'accessKey' => Linker::accesskey( 'diff' ),
4446 ] + $diffConfig );
4447
4448 // Avoid PHP 7.1 warning of passing $this by reference
4449 $editPage = $this;
4450 Hooks::run( 'EditPageBeforeEditButtons', [ &$editPage, &$buttons, &$tabindex ] );
4451
4452 return $buttons;
4453 }
4454
4459 public function noSuchSectionPage() {
4460 $out = $this->context->getOutput();
4461 $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4462
4463 $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4464
4465 // Avoid PHP 7.1 warning of passing $this by reference
4466 $editPage = $this;
4467 Hooks::run( 'EditPageNoSuchSection', [ &$editPage, &$res ] );
4468 $out->addHTML( $res );
4469
4470 $out->returnToMain( false, $this->mTitle );
4471 }
4472
4478 public function spamPageWithContent( $match = false ) {
4479 $this->textbox2 = $this->textbox1;
4480
4481 if ( is_array( $match ) ) {
4482 $match = $this->context->getLanguage()->listToText( $match );
4483 }
4484 $out = $this->context->getOutput();
4485 $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4486
4487 $out->addHTML( '<div id="spamprotected">' );
4488 $out->addWikiMsg( 'spamprotectiontext' );
4489 if ( $match ) {
4490 $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4491 }
4492 $out->addHTML( '</div>' );
4493
4494 $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4495 $this->showDiff();
4496
4497 $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4498 $this->showTextbox2();
4499
4500 $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4501 }
4502
4513 protected function safeUnicodeInput( $request, $field ) {
4514 return rtrim( $request->getText( $field ) );
4515 }
4516
4526 protected function safeUnicodeOutput( $text ) {
4527 return $text;
4528 }
4529
4533 protected function addEditNotices() {
4534 $out = $this->context->getOutput();
4535 $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4536 if ( count( $editNotices ) ) {
4537 $out->addHTML( implode( "\n", $editNotices ) );
4538 } else {
4539 $msg = $this->context->msg( 'editnotice-notext' );
4540 if ( !$msg->isDisabled() ) {
4541 $out->addHTML(
4542 '<div class="mw-editnotice-notext">'
4543 . $msg->parseAsBlock()
4544 . '</div>'
4545 );
4546 }
4547 }
4548 }
4549
4553 protected function addTalkPageText() {
4554 if ( $this->mTitle->isTalkPage() ) {
4555 $this->context->getOutput()->addWikiMsg( 'talkpagetext' );
4556 }
4557 }
4558
4562 protected function addLongPageWarningHeader() {
4563 if ( $this->contentLength === false ) {
4564 $this->contentLength = strlen( $this->textbox1 );
4565 }
4566
4567 $out = $this->context->getOutput();
4568 $lang = $this->context->getLanguage();
4569 $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' );
4570 if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4571 $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4572 [
4573 'longpageerror',
4574 $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4575 $lang->formatNum( $maxArticleSize )
4576 ]
4577 );
4578 } else {
4579 if ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
4580 $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
4581 [
4582 'longpage-hint',
4583 $lang->formatSize( strlen( $this->textbox1 ) ),
4584 strlen( $this->textbox1 )
4585 ]
4586 );
4587 }
4588 }
4589 }
4590
4594 protected function addPageProtectionWarningHeaders() {
4595 $out = $this->context->getOutput();
4596 if ( $this->mTitle->isProtected( 'edit' ) &&
4597 MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
4598 ) {
4599 # Is the title semi-protected?
4600 if ( $this->mTitle->isSemiProtected() ) {
4601 $noticeMsg = 'semiprotectedpagewarning';
4602 } else {
4603 # Then it must be protected based on static groups (regular)
4604 $noticeMsg = 'protectedpagewarning';
4605 }
4606 LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4607 [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4608 }
4609 if ( $this->mTitle->isCascadeProtected() ) {
4610 # Is this page under cascading protection from some source pages?
4612 list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4613 $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
4614 $cascadeSourcesCount = count( $cascadeSources );
4615 if ( $cascadeSourcesCount > 0 ) {
4616 # Explain, and list the titles responsible
4617 foreach ( $cascadeSources as $page ) {
4618 $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4619 }
4620 }
4621 $notice .= '</div>';
4622 $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4623 }
4624 if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4625 LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4626 [ 'lim' => 1,
4627 'showIfEmpty' => false,
4628 'msgKey' => [ 'titleprotectedwarning' ],
4629 'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4630 }
4631 }
4632
4638 $out->wrapWikiMsg(
4639 "<div class='mw-explainconflict'>\n$1\n</div>",
4640 [ 'explainconflict', $this->context->msg( $this->getSubmitButtonLabel() )->text() ]
4641 );
4642 }
4643
4651 protected function buildTextboxAttribs( $name, array $customAttribs, User $user ) {
4653 'accesskey' => ',',
4654 'id' => $name,
4655 'cols' => 80,
4656 'rows' => 25,
4657 // Avoid PHP notices when appending preferences
4658 // (appending allows customAttribs['style'] to still work).
4659 'style' => ''
4660 ];
4661
4662 // The following classes can be used here:
4663 // * mw-editfont-default
4664 // * mw-editfont-monospace
4665 // * mw-editfont-sans-serif
4666 // * mw-editfont-serif
4667 $class = 'mw-editfont-' . $user->getOption( 'editfont' );
4668
4669 if ( isset( $attribs['class'] ) ) {
4670 if ( is_string( $attribs['class'] ) ) {
4671 $attribs['class'] .= ' ' . $class;
4672 } elseif ( is_array( $attribs['class'] ) ) {
4673 $attribs['class'][] = $class;
4674 }
4675 } else {
4676 $attribs['class'] = $class;
4677 }
4678
4679 $pageLang = $this->mTitle->getPageLanguage();
4680 $attribs['lang'] = $pageLang->getHtmlCode();
4681 $attribs['dir'] = $pageLang->getDir();
4682
4683 return $attribs;
4684 }
4685
4691 protected function addNewLineAtEnd( $wikitext ) {
4692 if ( strval( $wikitext ) !== '' ) {
4693 // Ensure there's a newline at the end, otherwise adding lines
4694 // is awkward.
4695 // But don't add a newline if the text is empty, or Firefox in XHTML
4696 // mode will show an extra newline. A bit annoying.
4697 $wikitext .= "\n";
4698 return $wikitext;
4699 }
4700 return $wikitext;
4701 }
4702
4713 private function guessSectionName( $text ) {
4715
4716 // Detect Microsoft browsers
4717 $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
4718 if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
4719 // ...and redirect them to legacy encoding, if available
4720 return $wgParser->guessLegacySectionNameFromWikiText( $text );
4721 }
4722 // Meanwhile, real browsers get real anchors
4723 return $wgParser->guessSectionNameFromWikiText( $text );
4724 }
4725}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
target page
$wgSummarySpamRegex
Same as the above except for edit summaries.
$wgRightsText
If either $wgRightsUrl or $wgRightsPage is specified then this variable gives the text for the link.
$wgSpamRegex
Edits matching these regular expressions in body text will be recognised as spam and rejected automat...
$wgEnableUploads
Uploads have to be specially set up to be secure.
$wgUseMediaWikiUIEverywhere
Temporary variable that applies MediaWiki UI wherever it can be supported.
$wgForeignFileRepos
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfReadOnly()
Check whether the wiki is in read-only mode.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfFindFile( $title, $options=[])
Find a file.
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
wfGetAllCallers( $limit=3)
Return a string consisting of callers in the stack.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
$wgOut
Definition Setup.php:827
$wgParser
Definition Setup.php:832
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:1112
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 newKey( $key)
Static constructor for easier chaining.
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.
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:374
getActionURL(Title $title)
Returns the URL to use in the form's action attribute.
isOouiEnabled()
Check if the edit page is using OOUI controls.
Definition EditPage.php:521
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:276
showTextbox( $text, $name, $customAttribs=[])
string $hookError
Definition EditPage.php:318
attemptSave(&$resultDetails=false)
Attempt submission.
$editFormTextAfterTools
Definition EditPage.php:411
isSectionEditSupported()
Returns whether section editing is supported for the current page.
Definition EditPage.php:878
getCheckboxesWidget(&$tabindex, $checked)
Returns an array of checkboxes for the edit form, including 'minor' and 'watch' checkboxes and any ot...
WikiPage $page
Definition EditPage.php:222
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:166
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:398
bool $allowBlankSummary
Definition EditPage.php:300
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:740
bool $firsttime
Definition EditPage.php:273
bool $isCssSubpage
Definition EditPage.php:249
const AS_CANNOT_USE_CUSTOM_MODEL
Status: when changing the content model is disallowed due to $wgContentHandlerUseDB being false.
Definition EditPage.php:183
runPostMergeFilters(Content $content, Status $status, User $user)
Run hooks that can filter edits just before they get saved.
bool $bot
Definition EditPage.php:392
showPreview( $text)
Append preview output to OutputPage.
const AS_UNICODE_NOT_SUPPORTED
Status: edit rejected because browser doesn't support Unicode.
Definition EditPage.php:188
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition EditPage.php:66
addTalkPageText()
const AS_READ_ONLY_PAGE_ANON
Status: this anonymous user is not allowed to edit this page.
Definition EditPage.php:81
string $textbox2
Definition EditPage.php:356
bool $tooBig
Definition EditPage.php:291
safeUnicodeOutput( $text)
Filter an output field through a Unicode armoring process if it is going to an old browser with known...
getCheckboxesDefinition( $checked)
Return an array of checkbox definitions.
showEditTools()
Inserts optional text shown below edit and upload forms.
isSupportedContentModel( $modelId)
Returns if the given content model is editable.
Definition EditPage.php:532
$editFormTextAfterContent
Definition EditPage.php:413
$editFormTextBottom
Definition EditPage.php:412
int $editRevId
Definition EditPage.php:368
$editFormTextBeforeContent
Definition EditPage.php:409
string $contentModel
Definition EditPage.php:395
bool $deletedSinceEdit
Definition EditPage.php:267
__construct(Article $article)
Definition EditPage.php:452
getEditPermissionErrors( $rigor='secure')
Definition EditPage.php:696
showStandardInputs(&$tabindex=2)
toEditContent( $text)
Turns the given text into a Content object by unserializing it.
int $oldid
Definition EditPage.php:380
const POST_EDIT_COOKIE_KEY_PREFIX
Prefix of key for cookie used to pass post-edit state.
Definition EditPage.php:199
const AS_MAX_ARTICLE_SIZE_EXCEEDED
Status: article is too big (> $wgMaxArticleSize), after merging in the new section.
Definition EditPage.php:134
const AS_CONFLICT_DETECTED
Status: (non-resolvable) edit conflict.
Definition EditPage.php:118
addExplainConflictHeader(OutputPage $out)
$editFormTextAfterWarn
Definition EditPage.php:410
isWrongCaseCssJsPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition EditPage.php:857
bool $mTokenOk
Definition EditPage.php:279
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:144
wasDeletedSinceLastEdit()
Check if a page was deleted while the user was editing it, before submit.
string null $unicodeCheck
What the user submitted in the 'wpUnicodeCheck' field.
Definition EditPage.php:447
const AS_READ_ONLY_PAGE_LOGGED
Status: this logged in user is not allowed to edit this page.
Definition EditPage.php:86
bool $mShowSummaryField
Definition EditPage.php:330
showFormAfterText()
bool $recreate
Definition EditPage.php:350
int $parentRevId
Definition EditPage.php:383
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:324
const AS_BLANK_ARTICLE
Status: user tried to create a blank page and wpIgnoreBlankArticle == false.
Definition EditPage.php:113
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:888
static extractSectionTitle( $text)
Extract the section title from current section text, if any.
bool $missingSummary
Definition EditPage.php:297
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:228
bool int $contentLength
Definition EditPage.php:427
const AS_PARSE_ERROR
Status: can't parse content.
Definition EditPage.php:177
bool $blankArticle
Definition EditPage.php:303
noSuchSectionPage()
Creates a basic error page which informs the user that they have attempted to edit a nonexistent sect...
bool $mTriedSave
Definition EditPage.php:285
incrementResolvedConflicts()
Log when a page was successfully saved after the edit conflict view.
showConflict()
Show an edit conflict.
guessSectionName( $text)
Turns section name wikitext into anchors for use in HTTP redirects.
const AS_READ_ONLY_PAGE
Status: wiki is in readonly mode (wfReadOnly() == true)
Definition EditPage.php:91
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:371
ParserOutput $mParserOutput
Definition EditPage.php:321
previewOnOpen()
Should we show a preview when the edit form is first shown?
Definition EditPage.php:823
displayPreviewArea( $previewOutput, $isOnTop=false)
doPreviewParse(Content $content)
Parse the page for a preview.
string $formtype
Definition EditPage.php:270
bool $allowBlankArticle
Definition EditPage.php:306
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing.
Definition EditPage.php:543
internalAttemptSave(&$result, $bot=false)
Attempt submission (no UI)
bool $isWrongCaseCssJsPage
Definition EditPage.php:261
string $summary
Definition EditPage.php:359
getSubmitButtonLabel()
Get the message key of the label for the button to save the page.
getSummaryInputWidget( $summary="", $labelText=null, $inputAttrs=null)
Builds a standard summary input with a label.
bool $save
Definition EditPage.php:335
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition EditPage.php:76
bool $diff
Definition EditPage.php:341
getCurrentContent()
Get the current content of the page.
bool $selfRedirect
Definition EditPage.php:309
string $textbox1
Definition EditPage.php:353
addContentModelChangeLogEntry(User $user, $oldModel, $newModel, $reason)
const EDITFORM_ID
HTML id and name for the beginning of the edit form.
Definition EditPage.php:193
const AS_CHANGE_TAG_ERROR
Status: an error relating to change tagging.
Definition EditPage.php:172
string $editFormPageTop
Before even the preview.
Definition EditPage.php:407
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
Definition EditPage.php:71
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:160
getCopywarn()
Get the copyright warning.
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition EditPage.php:214
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:503
toEditText( $content)
Gets an editable textual representation of $content.
null Title $mContextTitle
Definition EditPage.php:231
string $autoSumm
Definition EditPage.php:315
bool $isOldRev
Whether an old revision is edited.
Definition EditPage.php:442
const AS_IMAGE_REDIRECT_LOGGED
Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
Definition EditPage.php:154
const AS_END
Status: WikiPage::doEdit() was unsuccessful.
Definition EditPage.php:139
const AS_ARTICLE_WAS_DELETED
Status: article was deleted while editing and param wpRecreate == false or form was not posted.
Definition EditPage.php:102
const AS_TEXTBOX_EMPTY
Status: user tried to create a new section without content.
Definition EditPage.php:129
bool $mTokenOkExceptSuffix
Definition EditPage.php:282
newSectionSummary(&$sectionanchor=null)
Return the summary to be used for a new section.
const AS_SUCCESS_UPDATE
Status: Article successfully updated.
Definition EditPage.php:51
getBaseRevision()
bool $isCssJsSubpage
Definition EditPage.php:243
string $starttime
Definition EditPage.php:377
bool $isNew
New page or new section.
Definition EditPage.php:264
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:220
getSummaryInputOOUI( $summary="", $labelText=null, $inputAttrs=null)
Builds a standard summary input with a label.
bool $allowSelfRedirect
Definition EditPage.php:312
const AS_IMAGE_REDIRECT_ANON
Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
Definition EditPage.php:149
getContentObject( $def_content=null)
bool $isJsSubpage
Definition EditPage.php:255
bool $isConflict
Definition EditPage.php:237
null $scrolltop
Definition EditPage.php:389
getLastDelete()
string $action
Definition EditPage.php:234
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:566
string $editintro
Definition EditPage.php:386
bool $enableApiEditOverride
Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing.
Definition EditPage.php:432
displayViewSourcePage(Content $content, $errorMessage='')
Display a read-only View Source page.
Definition EditPage.php:770
bool $watchthis
Definition EditPage.php:347
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:108
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:344
$previewTextAfterContent
Definition EditPage.php:414
getSummaryPreview( $isSubjectPreview, $summary="")
bool $nosummary
Definition EditPage.php:362
bool $incompleteForm
Definition EditPage.php:288
const AS_SUCCESS_NEW_ARTICLE
Status: Article successfully created.
Definition EditPage.php:56
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:294
string $edittime
Definition EditPage.php:365
showTosSummary()
Give a chance for site and per-namespace customizations of terms of service summary link that might e...
const UNICODE_CHECK
Used for Unicode support checks.
Definition EditPage.php:46
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:424
incrementConflictStats()
getCheckboxesOOUI(&$tabindex, $checked)
Returns an array of checkboxes for the edit form, including 'minor' and 'watch' checkboxes and any ot...
IContextSource $context
Definition EditPage.php:437
Revision bool $mBaseRevision
Definition EditPage.php:327
bool $preview
Definition EditPage.php:338
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 OutputPage.
null array $changeTags
Definition EditPage.php:401
const AS_RATE_LIMITED
Status: rate limiter for action 'edit' was tripped.
Definition EditPage.php:96
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
Definition EditPage.php:61
setContextTitle( $title)
Set the context Title object.
Definition EditPage.php:492
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:124
An error page which can definitely be safely rendered using the OutputPage.
static titleAttrib( $name, $options=null, array $msgParams=[])
Given the id of an interface element, constructs the appropriate title attribute from the system mess...
Definition Linker.php:1951
static accesskey( $name)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition Linker.php:1994
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:1445
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[])
Returns the attributes for the tooltip and access key.
Definition Linker.php:2111
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition Linker.php:1900
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:400
setPerformer(User $performer)
Set the user that performed the action being logged.
Definition LogEntry.php:495
PSR-3 logger instance factory.
MediaWikiServices is the service locator for the application scope of MediaWiki.
This class should be covered by a general architecture document which does not exist as of January 20...
Show an error when a user tries to do something they do not have the necessary permissions for.
Variant of the Message class.
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
static inDebugMode()
Determine whether debug mode was requested Order of priority is 1) request param, 2) cookie,...
static makeInlineScript( $script)
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:284
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition Revision.php:309
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:1161
static getSkinNames()
Fetch the set of available skins.
Definition Skin.php:51
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:1026
Show an error when the user tries to do something whilst blocked.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
static doWatchOrUnwatch( $watch, Title $title, User $user)
Watch or unwatch a page.
Class representing a MediaWiki article and history.
Definition WikiPage.php:37
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition WikiPage.php:121
getRedirectTarget()
If this page is a redirect, get its target.
Definition WikiPage.php:835
getContent( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition WikiPage.php:666
isRedirect()
Tests if the article content represents a redirect.
Definition WikiPage.php:492
per default it will return the text for text based content
$res
Definition database.txt:21
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a save
Definition deferred.txt:5
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add etc
Definition design.txt:19
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition design.txt:57
when a variable name is used in a it is silently declared as a new local masking the global
Definition design.txt:95
This 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:157
const EDIT_UPDATE
Definition Defines.php:154
const NS_USER
Definition Defines.php:67
const CONTENT_MODEL_CSS
Definition Defines.php:238
const NS_FILE
Definition Defines.php:71
const NS_MAIN
Definition Defines.php:65
const NS_MEDIAWIKI
Definition Defines.php:73
const NS_CATEGORY_TALK
Definition Defines.php:80
const NS_MEDIA
Definition Defines.php:53
const NS_USER_TALK
Definition Defines.php:68
const EDIT_MINOR
Definition Defines.php:155
const CONTENT_MODEL_JAVASCRIPT
Definition Defines.php:237
const EDIT_AUTOSUMMARY
Definition Defines.php:159
const EDIT_NEW
Definition Defines.php:153
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action or null $user:User who performed the tagging when the tagging is subsequent to the action or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1245
the array() calling protocol came about after MediaWiki 1.4rc1.
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on $request
Definition hooks.txt:2775
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:1963
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set but before ordinary actions take place $output
Definition hooks.txt:2225
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition hooks.txt:181
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping $template
Definition hooks.txt:829
also included in $newHeader if any indicating whether we should show just the diff
Definition hooks.txt:1249
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:1971
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:863
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function array $article
Definition hooks.txt:77
null means default & $customAttribs
Definition hooks.txt:1965
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:962
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:1581
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition hooks.txt:2805
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:1976
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:1422
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:862
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:1983
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:783
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing & $attribs
Definition hooks.txt:1984
this hook is for auditing only $response
Definition hooks.txt:781
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:901
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:1610
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:1760
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
returning false will NOT prevent logging $e
Definition hooks.txt:2146
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
Base interface for content objects.
Definition Content.php:34
preSaveTransform(Title $title, User $user, ParserOptions $parserOptions)
Returns a Content object with pre-save transformations applied (or this object if no transformations ...
serialize( $format=null)
Convenience method for serializing this Content object.
Interface for objects which can provide a MediaWiki context on request.
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
if(!isset( $args[0])) $lang