Index: trunk/phase3/includes/api/ApiEditPage.php |
— | — | @@ -270,11 +270,11 @@ |
271 | 271 | $oldRequest = $wgRequest; |
272 | 272 | $wgRequest = $req; |
273 | 273 | |
274 | | - $retval = $ep->internalAttemptSave( $result, $wgUser->isAllowed( 'bot' ) && $params['bot'] ); |
| 274 | + $status = $ep->internalAttemptSave( $result, $wgUser->isAllowed( 'bot' ) && $params['bot'] ); |
275 | 275 | $wgRequest = $oldRequest; |
276 | 276 | global $wgMaxArticleSize; |
277 | 277 | |
278 | | - switch( $retval ) { |
| 278 | + switch( $status->value ) { |
279 | 279 | case EditPage::AS_HOOK_ERROR: |
280 | 280 | case EditPage::AS_HOOK_ERROR_EXPECTED: |
281 | 281 | $this->dieUsageMsg( 'hookaborted' ); |
— | — | @@ -353,15 +353,12 @@ |
354 | 354 | $this->dieUsageMsg( 'summaryrequired' ); |
355 | 355 | |
356 | 356 | case EditPage::AS_END: |
357 | | - // This usually means some kind of race condition |
358 | | - // or DB weirdness occurred. Fall through to throw an unknown |
359 | | - // error. |
360 | | - |
361 | | - // This needs fixing higher up, as EditPage::internalAttemptSave |
362 | | - // should return the Status object, so that specific error |
363 | | - // conditions can be returned |
| 357 | + // $status came from WikiPage::doEdit() |
| 358 | + $errors = $status->getErrorsArray(); |
| 359 | + $this->dieUsageMsg( $errors[0] ); // TODO: Add new errors to message map |
| 360 | + break; |
364 | 361 | default: |
365 | | - $this->dieUsageMsg( array( 'unknownerror', $retval ) ); |
| 362 | + $this->dieUsageMsg( array( 'unknownerror', $status->value ) ); |
366 | 363 | } |
367 | 364 | $apiResult->addValue( null, $this->getModuleName(), $r ); |
368 | 365 | } |
Index: trunk/phase3/includes/api/ApiBase.php |
— | — | @@ -1164,6 +1164,12 @@ |
1165 | 1165 | 'emptynewsection' => array( 'code' => 'emptynewsection', 'info' => 'Creating empty new sections is not possible.' ), |
1166 | 1166 | 'revwrongpage' => array( 'code' => 'revwrongpage', 'info' => "r\$1 is not a revision of ``\$2''" ), |
1167 | 1167 | 'undo-failure' => array( 'code' => 'undofailure', 'info' => 'Undo failed due to conflicting intermediate edits' ), |
| 1168 | + |
| 1169 | + // Messages from WikiPage::doEit() |
| 1170 | + 'edit-hook-aborted' => array( 'code' => 'edit-hook-aborted', 'info' => "Your edit was aborted by an ArticleSave hook" ), |
| 1171 | + 'edit-gone-missing' => array( 'code' => 'edit-gone-missing', 'info' => "The page you tried to edit doesn't seem to exist anymore" ), |
| 1172 | + 'edit-conflict' => array( 'code' => 'editconflict', 'info' => "Edit conflict detected" ), |
| 1173 | + 'edit-already-exists' => array( 'code' => 'edit-already-exists', 'info' => "It seems the page you tried to create already exist" ), |
1168 | 1174 | |
1169 | 1175 | // uploadMsgs |
1170 | 1176 | 'invalid-session-key' => array( 'code' => 'invalid-session-key', 'info' => 'Not a valid session key' ), |
Index: trunk/phase3/includes/EditPage.php |
— | — | @@ -853,32 +853,42 @@ |
854 | 854 | * @param $result |
855 | 855 | * @param $bot bool |
856 | 856 | * |
857 | | - * @return int one of the constants describing the result |
| 857 | + * @return Status object, possibly with a message, but always with one of the AS_* constants in $status->value, |
| 858 | + * |
| 859 | + * FIXME: This interface is TERRIBLE, but hard to get rid of due to various error display idiosyncrasies. There are |
| 860 | + * also lots of cases where error metadata is set in the object and retrieved later instead of being returned, e.g. |
| 861 | + * AS_CONTENT_TOO_BIG and AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some time. |
858 | 862 | */ |
859 | 863 | function internalAttemptSave( &$result, $bot = false ) { |
860 | 864 | global $wgFilterCallback, $wgUser, $wgRequest, $wgParser; |
861 | 865 | global $wgMaxArticleSize; |
| 866 | + |
| 867 | + $status = Status::newGood(); |
862 | 868 | |
863 | 869 | wfProfileIn( __METHOD__ ); |
864 | 870 | wfProfileIn( __METHOD__ . '-checks' ); |
865 | 871 | |
866 | 872 | if ( !wfRunHooks( 'EditPage::attemptSave', array( $this ) ) ) { |
867 | 873 | wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" ); |
| 874 | + $status->fatal( 'hookaborted' ); |
| 875 | + $status->value = self::AS_HOOK_ERROR; |
868 | 876 | wfProfileOut( __METHOD__ . '-checks' ); |
869 | 877 | wfProfileOut( __METHOD__ ); |
870 | | - return self::AS_HOOK_ERROR; |
| 878 | + return $status; |
871 | 879 | } |
872 | 880 | |
873 | 881 | # Check image redirect |
874 | 882 | if ( $this->mTitle->getNamespace() == NS_FILE && |
875 | 883 | Title::newFromRedirect( $this->textbox1 ) instanceof Title && |
876 | 884 | !$wgUser->isAllowed( 'upload' ) ) { |
877 | | - $isAnon = $wgUser->isAnon(); |
| 885 | + $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; |
| 886 | + $status->setResult( false, $code ); |
878 | 887 | |
879 | 888 | wfProfileOut( __METHOD__ . '-checks' ); |
| 889 | + |
880 | 890 | wfProfileOut( __METHOD__ ); |
881 | 891 | |
882 | | - return $isAnon ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; |
| 892 | + return $status; |
883 | 893 | } |
884 | 894 | |
885 | 895 | # Check for spam |
— | — | @@ -892,71 +902,88 @@ |
893 | 903 | $pdbk = $this->mTitle->getPrefixedDBkey(); |
894 | 904 | $match = str_replace( "\n", '', $match ); |
895 | 905 | wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" ); |
| 906 | + $status->fatal( 'spamprotectionmatch', $match ); |
| 907 | + $status->value = self::AS_SPAM_ERROR; |
896 | 908 | wfProfileOut( __METHOD__ . '-checks' ); |
897 | 909 | wfProfileOut( __METHOD__ ); |
898 | | - return self::AS_SPAM_ERROR; |
| 910 | + return $status; |
899 | 911 | } |
900 | 912 | if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section, $this->hookError, $this->summary ) ) { |
901 | 913 | # Error messages or other handling should be performed by the filter function |
| 914 | + $status->setResult( false, self::AS_FILTERING ); |
902 | 915 | wfProfileOut( __METHOD__ . '-checks' ); |
903 | 916 | wfProfileOut( __METHOD__ ); |
904 | | - return self::AS_FILTERING; |
| 917 | + return $status; |
905 | 918 | } |
906 | 919 | if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ) ) ) { |
907 | 920 | # Error messages etc. could be handled within the hook... |
| 921 | + $status->fatal( 'hookaborted' ); |
| 922 | + $status->value = self::AS_HOOK_ERROR; |
908 | 923 | wfProfileOut( __METHOD__ . '-checks' ); |
909 | 924 | wfProfileOut( __METHOD__ ); |
910 | | - return self::AS_HOOK_ERROR; |
| 925 | + return $status; |
911 | 926 | } elseif ( $this->hookError != '' ) { |
912 | 927 | # ...or the hook could be expecting us to produce an error |
| 928 | + $status->fatal( 'hookaborted' ); |
| 929 | + $status->value = self::AS_HOOK_ERROR_EXPECTED; |
913 | 930 | wfProfileOut( __METHOD__ . '-checks' ); |
914 | 931 | wfProfileOut( __METHOD__ ); |
915 | | - return self::AS_HOOK_ERROR_EXPECTED; |
| 932 | + return $status; |
916 | 933 | } |
917 | 934 | if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) { |
918 | 935 | # Check block state against master, thus 'false'. |
| 936 | + $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER ); |
919 | 937 | wfProfileOut( __METHOD__ . '-checks' ); |
920 | 938 | wfProfileOut( __METHOD__ ); |
921 | | - return self::AS_BLOCKED_PAGE_FOR_USER; |
| 939 | + return $status; |
922 | 940 | } |
923 | 941 | $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 ); |
924 | 942 | if ( $this->kblength > $wgMaxArticleSize ) { |
925 | 943 | // Error will be displayed by showEditForm() |
926 | 944 | $this->tooBig = true; |
| 945 | + $status->setResult( false, self::AS_CONTENT_TOO_BIG ); |
927 | 946 | wfProfileOut( __METHOD__ . '-checks' ); |
928 | 947 | wfProfileOut( __METHOD__ ); |
929 | | - return self::AS_CONTENT_TOO_BIG; |
| 948 | + return $status; |
930 | 949 | } |
931 | 950 | |
932 | 951 | if ( !$wgUser->isAllowed( 'edit' ) ) { |
933 | 952 | if ( $wgUser->isAnon() ) { |
| 953 | + $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON ); |
934 | 954 | wfProfileOut( __METHOD__ . '-checks' ); |
935 | 955 | wfProfileOut( __METHOD__ ); |
936 | | - return self::AS_READ_ONLY_PAGE_ANON; |
| 956 | + return $status; |
937 | 957 | } else { |
| 958 | + $status->fatal( 'readonlytext' ); |
| 959 | + $status->value = self::AS_READ_ONLY_PAGE_LOGGED; |
938 | 960 | wfProfileOut( __METHOD__ . '-checks' ); |
939 | 961 | wfProfileOut( __METHOD__ ); |
940 | | - return self::AS_READ_ONLY_PAGE_LOGGED; |
| 962 | + return $status; |
941 | 963 | } |
942 | 964 | } |
943 | 965 | |
944 | 966 | if ( wfReadOnly() ) { |
| 967 | + $status->fatal( 'readonlytext' ); |
| 968 | + $status->value = self::AS_READ_ONLY_PAGE; |
945 | 969 | wfProfileOut( __METHOD__ . '-checks' ); |
946 | 970 | wfProfileOut( __METHOD__ ); |
947 | | - return self::AS_READ_ONLY_PAGE; |
| 971 | + return $status; |
948 | 972 | } |
949 | 973 | if ( $wgUser->pingLimiter() ) { |
| 974 | + $status->fatal( 'actionthrottledtext' ); |
| 975 | + $status->value = self::AS_RATE_LIMITED; |
950 | 976 | wfProfileOut( __METHOD__ . '-checks' ); |
951 | 977 | wfProfileOut( __METHOD__ ); |
952 | | - return self::AS_RATE_LIMITED; |
| 978 | + return $status; |
953 | 979 | } |
954 | 980 | |
955 | 981 | # If the article has been deleted while editing, don't save it without |
956 | 982 | # confirmation |
957 | 983 | if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) { |
| 984 | + $status->setResult( false, self::AS_ARTICLE_WAS_DELETED ); |
958 | 985 | wfProfileOut( __METHOD__ . '-checks' ); |
959 | 986 | wfProfileOut( __METHOD__ ); |
960 | | - return self::AS_ARTICLE_WAS_DELETED; |
| 987 | + return $status; |
961 | 988 | } |
962 | 989 | |
963 | 990 | wfProfileOut( __METHOD__ . '-checks' ); |
— | — | @@ -968,34 +995,43 @@ |
969 | 996 | if ( $new ) { |
970 | 997 | // Late check for create permission, just in case *PARANOIA* |
971 | 998 | if ( !$this->mTitle->userCan( 'create' ) ) { |
| 999 | + $status->fatal( 'nocreatetext' ); |
| 1000 | + $status->value = self::AS_NO_CREATE_PERMISSION; |
972 | 1001 | wfDebug( __METHOD__ . ": no create permission\n" ); |
973 | 1002 | wfProfileOut( __METHOD__ ); |
974 | | - return self::AS_NO_CREATE_PERMISSION; |
| 1003 | + return $status; |
975 | 1004 | } |
976 | 1005 | |
977 | 1006 | # Don't save a new article if it's blank. |
978 | 1007 | if ( $this->textbox1 == '' ) { |
| 1008 | + $status->setResult( false, self::AS_BLANK_ARTICLE ); |
979 | 1009 | wfProfileOut( __METHOD__ ); |
980 | | - return self::AS_BLANK_ARTICLE; |
| 1010 | + return $status; |
981 | 1011 | } |
982 | 1012 | |
983 | 1013 | // Run post-section-merge edit filter |
984 | 1014 | if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) { |
985 | 1015 | # Error messages etc. could be handled within the hook... |
| 1016 | + $status->fatal( 'hookaborted' ); |
| 1017 | + $status->value = self::AS_HOOK_ERROR; |
986 | 1018 | wfProfileOut( __METHOD__ ); |
987 | | - return self::AS_HOOK_ERROR; |
| 1019 | + return $status; |
988 | 1020 | } elseif ( $this->hookError != '' ) { |
989 | 1021 | # ...or the hook could be expecting us to produce an error |
| 1022 | + $status->fatal( 'hookaborted' ); |
| 1023 | + $status->value = self::AS_HOOK_ERROR_EXPECTED; |
990 | 1024 | wfProfileOut( __METHOD__ ); |
991 | | - return self::AS_HOOK_ERROR_EXPECTED; |
| 1025 | + return $status; |
992 | 1026 | } |
993 | 1027 | |
994 | 1028 | # Handle the user preference to force summaries here. Check if it's not a redirect. |
995 | 1029 | if ( !$this->allowBlankSummary && !Title::newFromRedirect( $this->textbox1 ) ) { |
996 | 1030 | if ( md5( $this->summary ) == $this->autoSumm ) { |
997 | 1031 | $this->missingSummary = true; |
| 1032 | + $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh |
| 1033 | + $status->value = self::AS_SUMMARY_NEEDED; |
998 | 1034 | wfProfileOut( __METHOD__ ); |
999 | | - return self::AS_SUMMARY_NEEDED; |
| 1035 | + return $status; |
1000 | 1036 | } |
1001 | 1037 | } |
1002 | 1038 | |
— | — | @@ -1004,7 +1040,7 @@ |
1005 | 1041 | $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text; |
1006 | 1042 | } |
1007 | 1043 | |
1008 | | - $retval = self::AS_SUCCESS_NEW_ARTICLE; |
| 1044 | + $status->value = self::AS_SUCCESS_NEW_ARTICLE; |
1009 | 1045 | |
1010 | 1046 | } else { |
1011 | 1047 | |
— | — | @@ -1061,19 +1097,24 @@ |
1062 | 1098 | } |
1063 | 1099 | |
1064 | 1100 | if ( $this->isConflict ) { |
| 1101 | + $status->setResult( false, self::AS_CONFLICT_DETECTED ); |
1065 | 1102 | wfProfileOut( __METHOD__ ); |
1066 | | - return self::AS_CONFLICT_DETECTED; |
| 1103 | + return $status; |
1067 | 1104 | } |
1068 | 1105 | |
1069 | 1106 | // Run post-section-merge edit filter |
1070 | 1107 | if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) { |
1071 | 1108 | # Error messages etc. could be handled within the hook... |
| 1109 | + $status->fatal( 'hookaborted' ); |
| 1110 | + $status->value = self::AS_HOOK_ERROR; |
1072 | 1111 | wfProfileOut( __METHOD__ ); |
1073 | | - return self::AS_HOOK_ERROR; |
| 1112 | + return $status; |
1074 | 1113 | } elseif ( $this->hookError != '' ) { |
1075 | 1114 | # ...or the hook could be expecting us to produce an error |
| 1115 | + $status->fatal( 'hookaborted' ); |
| 1116 | + $status->value = self::AS_HOOK_ERROR_EXPECTED; |
1076 | 1117 | wfProfileOut( __METHOD__ ); |
1077 | | - return self::AS_HOOK_ERROR_EXPECTED; |
| 1118 | + return $status; |
1078 | 1119 | } |
1079 | 1120 | |
1080 | 1121 | # Handle the user preference to force summaries here, but not for null edits |
— | — | @@ -1092,8 +1133,10 @@ |
1093 | 1134 | if ( $this->section == 'new' && !$this->allowBlankSummary ) { |
1094 | 1135 | if ( trim( $this->summary ) == '' ) { |
1095 | 1136 | $this->missingSummary = true; |
| 1137 | + $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh |
| 1138 | + $status->value = self::AS_SUMMARY_NEEDED; |
1096 | 1139 | wfProfileOut( __METHOD__ ); |
1097 | | - return self::AS_SUMMARY_NEEDED; |
| 1140 | + return $status; |
1098 | 1141 | } |
1099 | 1142 | } |
1100 | 1143 | |
— | — | @@ -1103,9 +1146,11 @@ |
1104 | 1147 | if ( $this->section == 'new' ) { |
1105 | 1148 | if ( $this->textbox1 == '' ) { |
1106 | 1149 | $this->missingComment = true; |
| 1150 | + $status->fatal( 'missingcommenttext' ); |
| 1151 | + $status->value = self::AS_TEXTBOX_EMPTY; |
1107 | 1152 | wfProfileOut( __METHOD__ . '-sectionanchor' ); |
1108 | 1153 | wfProfileOut( __METHOD__ ); |
1109 | | - return self::AS_TEXTBOX_EMPTY; |
| 1154 | + return $status; |
1110 | 1155 | } |
1111 | 1156 | if ( $this->summary != '' ) { |
1112 | 1157 | $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); |
— | — | @@ -1135,15 +1180,16 @@ |
1136 | 1181 | $this->textbox1 = $text; |
1137 | 1182 | $this->section = ''; |
1138 | 1183 | |
1139 | | - $retval = self::AS_SUCCESS_UPDATE; |
| 1184 | + $status->value = self::AS_SUCCESS_UPDATE; |
1140 | 1185 | } |
1141 | 1186 | |
1142 | 1187 | // Check for length errors again now that the section is merged in |
1143 | 1188 | $this->kblength = (int)( strlen( $text ) / 1024 ); |
1144 | 1189 | if ( $this->kblength > $wgMaxArticleSize ) { |
1145 | 1190 | $this->tooBig = true; |
| 1191 | + $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED ); |
1146 | 1192 | wfProfileOut( __METHOD__ ); |
1147 | | - return self::AS_MAX_ARTICLE_SIZE_EXCEEDED; |
| 1193 | + return $status; |
1148 | 1194 | } |
1149 | 1195 | |
1150 | 1196 | $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | |
— | — | @@ -1151,17 +1197,18 @@ |
1152 | 1198 | ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) | |
1153 | 1199 | ( $bot ? EDIT_FORCE_BOT : 0 ); |
1154 | 1200 | |
1155 | | - $status = $this->mArticle->doEdit( $text, $this->summary, $flags ); |
| 1201 | + $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags ); |
1156 | 1202 | |
1157 | | - if ( $status->isOK() ) { |
| 1203 | + if ( $doEditStatus->isOK() ) { |
1158 | 1204 | $result['redirect'] = Title::newFromRedirect( $text ) !== null; |
1159 | 1205 | $this->commitWatch(); |
1160 | 1206 | wfProfileOut( __METHOD__ ); |
1161 | | - return $retval; |
| 1207 | + return $status; |
1162 | 1208 | } else { |
1163 | 1209 | $this->isConflict = true; |
| 1210 | + $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares |
1164 | 1211 | wfProfileOut( __METHOD__ ); |
1165 | | - return self::AS_END; |
| 1212 | + return $doEditStatus; |
1166 | 1213 | } |
1167 | 1214 | } |
1168 | 1215 | |
— | — | @@ -2833,13 +2880,14 @@ |
2834 | 2881 | $resultDetails = false; |
2835 | 2882 | # Allow bots to exempt some edits from bot flagging |
2836 | 2883 | $bot = $wgUser->isAllowed( 'bot' ) && $this->bot; |
2837 | | - $value = $this->internalAttemptSave( $resultDetails, $bot ); |
| 2884 | + $status = $this->internalAttemptSave( $resultDetails, $bot ); |
| 2885 | + // FIXME: once the interface for internalAttemptSave() is made nicer, this should use the message in $status |
2838 | 2886 | |
2839 | | - if ( $value == self::AS_SUCCESS_UPDATE || $value == self::AS_SUCCESS_NEW_ARTICLE ) { |
| 2887 | + if ( $status->value == self::AS_SUCCESS_UPDATE || $status->value == self::AS_SUCCESS_NEW_ARTICLE ) { |
2840 | 2888 | $this->didSave = true; |
2841 | 2889 | } |
2842 | 2890 | |
2843 | | - switch ( $value ) { |
| 2891 | + switch ( $status->value ) { |
2844 | 2892 | case self::AS_HOOK_ERROR_EXPECTED: |
2845 | 2893 | case self::AS_CONTENT_TOO_BIG: |
2846 | 2894 | case self::AS_ARTICLE_WAS_DELETED: |