Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
7.59% |
59 / 777 |
|
9.09% |
7 / 77 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
7.59% |
59 / 777 |
|
9.09% |
7 / 77 |
54012.59 | |
0.00% |
0 / 1 |
registerExtension | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onResourceLoaderRegisterModules | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
onBeforePageDisplay | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
getAbuseFilter | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
initFlowExtension | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
resetFlowExtension | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onChangesListInitRows | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
onChangesListInsertArticleLink | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onOldChangesListRecentChangesLine | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
processRecentChangesLine | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
90 | |||
onEnhancedChangesList__getLogText | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
30 | |||
onEnhancedChangesListModifyLineData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onEnhancedChangesListModifyBlockLineData | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
modifyChangesListLine | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
isFlow | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onSpecialCheckUserGetLinksFromRow | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
onCheckUserFormatRow | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
getReplacementRowItems | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
onSkinTemplateNavigation__Universal | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
72 | |||
onArticle__MissingArticleConditions | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
onSpecialWatchlistGetNonRevisionTypes | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onUserGetReservedNames | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
onResourceLoaderGetConfigVars | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
onDeletedContributionsLineEnding | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
onContributionsLineEnding | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onApiFeedContributions__feedItem | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
getContributionsQuery | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
onDeletedContribsPager__reallyDoQuery | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onContribsPager__reallyDoQuery | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
onAbuseFilterBuilder | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
onAbuseFilterDeprecatedVariables | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
onAbuseFilterComputeVariable | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onAbortEmailNotification | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
onBeforeEchoEventInsert | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
3.33 | |||
onArticleEditUpdateNewTalk | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
isTalkpageManagerUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onEchoAbortEmailNotification | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onBeforeDisplayOrangeAlert | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
onInfoAction | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
onCheckUserInsertChangesRow | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
onIRCLineURL | |
60.00% |
9 / 15 |
|
0.00% |
0 / 1 |
5.02 | |||
onWhatLinksHereProps | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
onGetPreferences | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
handleWatchArticle | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 | |||
onWatchArticle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onUnwatchArticle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onMovePageIsValidMove | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
onMovePageCheckPermissions | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
onTitleSquidURLs | |
11.76% |
2 / 17 |
|
0.00% |
0 / 1 |
14.99 | |||
onWatchlistEditorBuildRemoveLine | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
onWatchlistEditorBeforeFormRender | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
onUserMergeAccountFields | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
onMergeAccountFromTo | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onIsLiquidThreadsPage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
12 | |||
onCategoryViewer__doCategoryQuery | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onCategoryViewer__generateLink | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
getTopicDeletionError | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
onArticleConfirmDelete | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onArticleDelete | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onArticleDeleteComplete | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
onRevisionUndeleted | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
onArticleUndelete | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onTitleMoveStarting | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
onPageMoveCompleting | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
onShowMissingArticle | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
onSearchableNamespaces | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isBetaFeatureAvailable | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
onGetBetaFeaturePreferences | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
onSaveUserOptions | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
90 | |||
onImportHandleToplevelXMLTag | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
210 | |||
onNukeGetNewPages | |
0.00% |
0 / 94 |
|
0.00% |
0 / 1 |
420 | |||
onNukeDeletePage | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
42 | |||
onChangesListSpecialPageQuery | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
onGetUserPermissionsErrors | |
42.86% |
6 / 14 |
|
0.00% |
0 / 1 |
24.11 | |||
getTermsOfUseMessages | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
getTermsOfUseMessagesParsed | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getTermsOfUseMessagesVersion | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | // phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName |
4 | |
5 | namespace Flow; |
6 | |
7 | use Article; |
8 | use ChangesList; |
9 | use Content; |
10 | use EnhancedChangesList; |
11 | use Exception; |
12 | use ExtensionRegistry; |
13 | use Flow\Collection\PostCollection; |
14 | use Flow\Data\Listener\RecentChangesListener; |
15 | use Flow\Exception\FlowException; |
16 | use Flow\Exception\InvalidInputException; |
17 | use Flow\Exception\PermissionException; |
18 | use Flow\Formatter\CheckUserQuery; |
19 | use Flow\Hooks\HookRunner; |
20 | use Flow\Import\OptInController; |
21 | use Flow\Model\UUID; |
22 | use Flow\SpamFilter\AbuseFilter; |
23 | use LogEntry; |
24 | use MediaWiki\Api\Hook\ApiFeedContributions__feedItemHook; |
25 | use MediaWiki\CheckUser\CheckUser\Pagers\AbstractCheckUserPager; |
26 | use MediaWiki\Config\Config; |
27 | use MediaWiki\Context\IContextSource; |
28 | use MediaWiki\Context\RequestContext; |
29 | use MediaWiki\Deferred\DeferredUpdates; |
30 | use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder; |
31 | use MediaWiki\Extension\BetaFeatures\BetaFeatures; |
32 | use MediaWiki\Extension\GuidedTour\GuidedTourLauncher; |
33 | use MediaWiki\Extension\Notifications\Hooks\BeforeDisplayOrangeAlertHook; |
34 | use MediaWiki\Extension\Notifications\Model\Event; |
35 | use MediaWiki\Feed\FeedItem; |
36 | use MediaWiki\Hook\AbortEmailNotificationHook; |
37 | use MediaWiki\Hook\CategoryViewer__doCategoryQueryHook; |
38 | use MediaWiki\Hook\CategoryViewer__generateLinkHook; |
39 | use MediaWiki\Hook\ChangesListInitRowsHook; |
40 | use MediaWiki\Hook\ChangesListInsertArticleLinkHook; |
41 | use MediaWiki\Hook\ContribsPager__reallyDoQueryHook; |
42 | use MediaWiki\Hook\ContributionsLineEndingHook; |
43 | use MediaWiki\Hook\DeletedContribsPager__reallyDoQueryHook; |
44 | use MediaWiki\Hook\DeletedContributionsLineEndingHook; |
45 | use MediaWiki\Hook\EnhancedChangesList__getLogTextHook; |
46 | use MediaWiki\Hook\EnhancedChangesListModifyBlockLineDataHook; |
47 | use MediaWiki\Hook\EnhancedChangesListModifyLineDataHook; |
48 | use MediaWiki\Hook\ImportHandleToplevelXMLTagHook; |
49 | use MediaWiki\Hook\InfoActionHook; |
50 | use MediaWiki\Hook\IRCLineURLHook; |
51 | use MediaWiki\Hook\MovePageCheckPermissionsHook; |
52 | use MediaWiki\Hook\MovePageIsValidMoveHook; |
53 | use MediaWiki\Hook\OldChangesListRecentChangesLineHook; |
54 | use MediaWiki\Hook\PageMoveCompletingHook; |
55 | use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook; |
56 | use MediaWiki\Hook\SpecialWatchlistGetNonRevisionTypesHook; |
57 | use MediaWiki\Hook\TitleMoveStartingHook; |
58 | use MediaWiki\Hook\TitleSquidURLsHook; |
59 | use MediaWiki\Hook\UnwatchArticleHook; |
60 | use MediaWiki\Hook\WatchArticleHook; |
61 | use MediaWiki\Hook\WatchlistEditorBeforeFormRenderHook; |
62 | use MediaWiki\Hook\WatchlistEditorBuildRemoveLineHook; |
63 | use MediaWiki\Hook\WhatLinksHerePropsHook; |
64 | use MediaWiki\Html\FormOptions; |
65 | use MediaWiki\Html\Html; |
66 | use MediaWiki\MediaWikiServices; |
67 | use MediaWiki\Message\Message; |
68 | use MediaWiki\Output\Hook\BeforePageDisplayHook; |
69 | use MediaWiki\Output\OutputPage; |
70 | use MediaWiki\Page\Hook\Article__MissingArticleConditionsHook; |
71 | use MediaWiki\Page\Hook\ArticleConfirmDeleteHook; |
72 | use MediaWiki\Page\Hook\ArticleDeleteCompleteHook; |
73 | use MediaWiki\Page\Hook\ArticleDeleteHook; |
74 | use MediaWiki\Page\Hook\ArticleUndeleteHook; |
75 | use MediaWiki\Page\Hook\RevisionUndeletedHook; |
76 | use MediaWiki\Page\Hook\ShowMissingArticleHook; |
77 | use MediaWiki\Pager\ContribsPager; |
78 | use MediaWiki\Pager\DeletedContribsPager; |
79 | use MediaWiki\Pager\IndexPager; |
80 | use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsHook; |
81 | use MediaWiki\Preferences\Hook\GetPreferencesHook; |
82 | use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook; |
83 | use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook; |
84 | use MediaWiki\ResourceLoader\ResourceLoader; |
85 | use MediaWiki\Revision\RevisionRecord; |
86 | use MediaWiki\Revision\SlotRecord; |
87 | use MediaWiki\Search\Hook\SearchableNamespacesHook; |
88 | use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageQueryHook; |
89 | use MediaWiki\Status\Status; |
90 | use MediaWiki\Storage\Hook\ArticleEditUpdateNewTalkHook; |
91 | use MediaWiki\Title\Title; |
92 | use MediaWiki\User\Hook\UserGetReservedNamesHook; |
93 | use MediaWiki\User\Options\Hook\SaveUserOptionsHook; |
94 | use MediaWiki\User\User; |
95 | use MediaWiki\User\UserIdentity; |
96 | use MediaWiki\WikiMap\WikiMap; |
97 | use MessageLocalizer; |
98 | use MWException; |
99 | use MWExceptionHandler; |
100 | use OldChangesList; |
101 | use RecentChange; |
102 | use Skin; |
103 | use SkinTemplate; |
104 | use stdClass; |
105 | use WikiImporter; |
106 | use Wikimedia\Rdbms\SelectQueryBuilder; |
107 | use WikiPage; |
108 | use XMLReader; |
109 | |
110 | class Hooks implements |
111 | ResourceLoaderRegisterModulesHook, |
112 | BeforePageDisplayHook, |
113 | GetPreferencesHook, |
114 | OldChangesListRecentChangesLineHook, |
115 | ChangesListInsertArticleLinkHook, |
116 | ChangesListInitRowsHook, |
117 | EnhancedChangesList__getLogTextHook, |
118 | EnhancedChangesListModifyLineDataHook, |
119 | EnhancedChangesListModifyBlockLineDataHook, |
120 | ChangesListSpecialPageQueryHook, |
121 | SkinTemplateNavigation__UniversalHook, |
122 | Article__MissingArticleConditionsHook, |
123 | SpecialWatchlistGetNonRevisionTypesHook, |
124 | UserGetReservedNamesHook, |
125 | ResourceLoaderGetConfigVarsHook, |
126 | ContribsPager__reallyDoQueryHook, |
127 | DeletedContribsPager__reallyDoQueryHook, |
128 | ContributionsLineEndingHook, |
129 | DeletedContributionsLineEndingHook, |
130 | ApiFeedContributions__feedItemHook, |
131 | AbortEmailNotificationHook, |
132 | BeforeDisplayOrangeAlertHook, |
133 | ArticleEditUpdateNewTalkHook, |
134 | InfoActionHook, |
135 | IRCLineURLHook, |
136 | WhatLinksHerePropsHook, |
137 | ShowMissingArticleHook, |
138 | WatchArticleHook, |
139 | UnwatchArticleHook, |
140 | MovePageCheckPermissionsHook, |
141 | MovePageIsValidMoveHook, |
142 | TitleMoveStartingHook, |
143 | PageMoveCompletingHook, |
144 | TitleSquidURLsHook, |
145 | WatchlistEditorBuildRemoveLineHook, |
146 | WatchlistEditorBeforeFormRenderHook, |
147 | CategoryViewer__doCategoryQueryHook, |
148 | CategoryViewer__generateLinkHook, |
149 | ArticleConfirmDeleteHook, |
150 | ArticleDeleteHook, |
151 | ArticleDeleteCompleteHook, |
152 | RevisionUndeletedHook, |
153 | ArticleUndeleteHook, |
154 | SearchableNamespacesHook, |
155 | ImportHandleToplevelXMLTagHook, |
156 | SaveUserOptionsHook, |
157 | GetUserPermissionsErrorsHook |
158 | { |
159 | /** |
160 | * @var AbuseFilter|null Initialized during extension initialization |
161 | */ |
162 | protected static $abuseFilter; |
163 | |
164 | public static function registerExtension() { |
165 | require_once dirname( __DIR__ ) . '/defines.php'; |
166 | } |
167 | |
168 | public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void { |
169 | // Register a dummy supportCheck module in case VE isn't loaded, as we attempt |
170 | // to load this module unconditionally on load. |
171 | if ( !$resourceLoader->isModuleRegistered( 'ext.visualEditor.supportCheck' ) ) { |
172 | $resourceLoader->register( 'ext.visualEditor.supportCheck', [] ); |
173 | } |
174 | |
175 | if ( ExtensionRegistry::getInstance()->isLoaded( 'GuidedTour' ) ) { |
176 | $resourceLoader->register( 'ext.guidedTour.tour.flowOptIn', [ |
177 | 'localBasePath' => dirname( __DIR__ ) . '/modules', |
178 | 'remoteExtPath' => 'Flow/modules', |
179 | 'scripts' => 'tours/flowOptIn.js', |
180 | 'styles' => 'tours/flowOptIn.less', |
181 | 'messages' => [ |
182 | "flow-guidedtour-optin-welcome", |
183 | "flow-guidedtour-optin-welcome-description", |
184 | "flow-guidedtour-optin-find-old-conversations", |
185 | "flow-guidedtour-optin-find-old-conversations-description", |
186 | "flow-guidedtour-optin-feedback", |
187 | "flow-guidedtour-optin-feedback-description" |
188 | ], |
189 | 'dependencies' => 'ext.guidedTour', |
190 | ] ); |
191 | } |
192 | } |
193 | |
194 | public function onBeforePageDisplay( $out, $skin ): void { |
195 | $title = $skin->getTitle(); |
196 | |
197 | // Register guided tour if needed |
198 | if ( |
199 | // Check that the cookie for Flow opt-in tour exists |
200 | $out->getRequest()->getCookie( 'Flow_optIn_guidedTour' ) && |
201 | // Check that the user is on their own talk page |
202 | $out->getUser()->getTalkPage()->equals( $title ) && |
203 | // Check that we are on a flow board |
204 | $title->getContentModel() === CONTENT_MODEL_FLOW_BOARD && |
205 | // Check that guided tour exists |
206 | ExtensionRegistry::getInstance()->isLoaded( 'GuidedTour' ) |
207 | ) { |
208 | // Activate tour |
209 | GuidedTourLauncher::launchTourByCookie( 'flowOptIn', 'newTopic' ); |
210 | |
211 | // Destroy Flow cookie |
212 | $out->getRequest()->response()->setCookie( 'Flow_optIn_guidedTour', '', time() - 3600 ); |
213 | } |
214 | } |
215 | |
216 | /** |
217 | * Initialized during extension initialization rather than |
218 | * in container so that non-flow pages don't load the container. |
219 | * |
220 | * @return AbuseFilter |
221 | */ |
222 | public static function getAbuseFilter() { |
223 | if ( self::$abuseFilter === null ) { |
224 | global $wgFlowAbuseFilterGroup, |
225 | $wgFlowAbuseFilterEmergencyDisableThreshold, |
226 | $wgFlowAbuseFilterEmergencyDisableCount, |
227 | $wgFlowAbuseFilterEmergencyDisableAge; |
228 | |
229 | self::$abuseFilter = new AbuseFilter( $wgFlowAbuseFilterGroup ); |
230 | self::$abuseFilter->setup( [ |
231 | 'threshold' => $wgFlowAbuseFilterEmergencyDisableThreshold, |
232 | 'count' => $wgFlowAbuseFilterEmergencyDisableCount, |
233 | 'age' => $wgFlowAbuseFilterEmergencyDisableAge, |
234 | ] ); |
235 | } |
236 | return self::$abuseFilter; |
237 | } |
238 | |
239 | /** |
240 | * Initialize Flow extension with necessary data, this function is invoked |
241 | * from $wgExtensionFunctions |
242 | */ |
243 | public static function initFlowExtension() { |
244 | global $wgFlowAbuseFilterGroup; |
245 | |
246 | // necessary to provide flow options in abuse filter on-wiki pages |
247 | if ( $wgFlowAbuseFilterGroup ) { |
248 | self::getAbuseFilter(); |
249 | } |
250 | } |
251 | |
252 | /** |
253 | * Reset anything that happened in self::initFlowExtension for |
254 | * unit tests |
255 | */ |
256 | public static function resetFlowExtension() { |
257 | self::$abuseFilter = null; |
258 | } |
259 | |
260 | /** |
261 | * Loads RecentChanges list metadata into a temporary cache for later use. |
262 | * |
263 | * @param ChangesList $changesList |
264 | * @param array $rows |
265 | */ |
266 | public function onChangesListInitRows( $changesList, $rows ) { |
267 | if ( !( $changesList instanceof OldChangesList || $changesList instanceof EnhancedChangesList ) ) { |
268 | return; |
269 | } |
270 | |
271 | set_error_handler( new RecoverableErrorHandler, -1 ); |
272 | try { |
273 | /** @var Formatter\ChangesListQuery $query */ |
274 | $query = Container::get( 'query.changeslist' ); |
275 | $query->loadMetadataBatch( |
276 | $rows, |
277 | $changesList->isWatchlist() |
278 | ); |
279 | } catch ( Exception $e ) { |
280 | MWExceptionHandler::logException( $e ); |
281 | } finally { |
282 | restore_error_handler(); |
283 | } |
284 | } |
285 | |
286 | /** |
287 | * Updates the given Flow topic line in an enhanced changes list (grouped RecentChanges). |
288 | * |
289 | * @param ChangesList $changesList |
290 | * @param string &$articlelink |
291 | * @param string &$s |
292 | * @param RecentChange $rc |
293 | * @param bool $unpatrolled |
294 | * @param bool $isWatchlist |
295 | * @return bool |
296 | */ |
297 | public function onChangesListInsertArticleLink( |
298 | $changesList, |
299 | &$articlelink, |
300 | &$s, |
301 | $rc, |
302 | $unpatrolled, |
303 | $isWatchlist |
304 | ) { |
305 | if ( !( $changesList instanceof EnhancedChangesList ) ) { |
306 | // This method is only to update EnhancedChangesList. |
307 | // onOldChangesListRecentChangesLine allows updating OldChangesList, |
308 | // and supports adding wrapper classes. |
309 | return true; |
310 | } |
311 | $classes = null; // avoid pass-by-reference error |
312 | return self::processRecentChangesLine( $changesList, $articlelink, $rc, $classes, true ); |
313 | } |
314 | |
315 | /** |
316 | * Updates a Flow line in the old changes list (standard RecentChanges). |
317 | * |
318 | * @param ChangesList $changesList |
319 | * @param string &$s |
320 | * @param RecentChange $rc |
321 | * @param array &$classes |
322 | * @param array &$attribs |
323 | * @return bool |
324 | */ |
325 | public function onOldChangesListRecentChangesLine( |
326 | $changesList, |
327 | &$s, |
328 | $rc, |
329 | &$classes, |
330 | &$attribs |
331 | ) { |
332 | return self::processRecentChangesLine( $changesList, $s, $rc, $classes ); |
333 | } |
334 | |
335 | /** |
336 | * Does the actual work for onOldChangesListRecentChangesLine and |
337 | * onChangesListInsertArticleLink hooks. Either updates an entire |
338 | * line with meta info (old changes), or simply updates the link to |
339 | * the topic (enhanced). |
340 | * |
341 | * @param ChangesList $changesList |
342 | * @param string &$s |
343 | * @param RecentChange $rc |
344 | * @param array|null &$classes |
345 | * @param bool $topicOnly |
346 | * @return bool |
347 | */ |
348 | protected static function processRecentChangesLine( |
349 | ChangesList $changesList, |
350 | &$s, |
351 | RecentChange $rc, |
352 | &$classes = null, |
353 | $topicOnly = false |
354 | ) { |
355 | $source = $rc->getAttribute( 'rc_source' ); |
356 | if ( $source === null ) { |
357 | $rcType = (int)$rc->getAttribute( 'rc_type' ); |
358 | if ( $rcType !== RC_FLOW ) { |
359 | return true; |
360 | } |
361 | } elseif ( $source !== RecentChangesListener::SRC_FLOW ) { |
362 | return true; |
363 | } |
364 | |
365 | set_error_handler( new RecoverableErrorHandler, -1 ); |
366 | try { |
367 | /** @var Formatter\ChangesListQuery $query */ |
368 | $query = Container::get( 'query.changeslist' ); |
369 | |
370 | $row = $query->getResult( $changesList, $rc, $changesList->isWatchlist() ); |
371 | if ( $row === false ) { |
372 | restore_error_handler(); |
373 | return false; |
374 | } |
375 | |
376 | /** @var Formatter\ChangesListFormatter $formatter */ |
377 | $formatter = Container::get( 'formatter.changeslist' ); |
378 | $line = $formatter->format( $row, $changesList, $topicOnly ); |
379 | } catch ( PermissionException $pe ) { |
380 | // It is expected that some rows won't be formatted because the current user |
381 | // doesn't have permission to see some of the data they contain. |
382 | return false; |
383 | } catch ( Exception $e ) { |
384 | wfDebugLog( 'Flow', __METHOD__ . ': Exception formatting rc ' . |
385 | $rc->getAttribute( 'rc_id' ) . ' ' . $e ); |
386 | MWExceptionHandler::logException( $e ); |
387 | return false; |
388 | } finally { |
389 | restore_error_handler(); |
390 | } |
391 | |
392 | if ( $line === false ) { |
393 | return false; |
394 | } |
395 | |
396 | if ( is_array( $classes ) ) { |
397 | // Add the flow class to <li> |
398 | $classes[] = 'flow-recentchanges-line'; |
399 | $classes[] = 'mw-changeslist-src-mw-edit'; |
400 | } |
401 | // Update the line markup |
402 | $s = $line; |
403 | |
404 | return true; |
405 | } |
406 | |
407 | /** |
408 | * Alter the enhanced RC links: (n changes | history) |
409 | * The default diff links are incorrect! |
410 | * |
411 | * @param EnhancedChangesList $changesList |
412 | * @param array &$links |
413 | * @param RecentChange[] $block |
414 | * @return bool |
415 | */ |
416 | public function onEnhancedChangesList__getLogText( $changesList, &$links, $block ) { |
417 | $rc = $block[0]; |
418 | |
419 | // quit if non-flow |
420 | // FIXME: It could be that $rc is a non-Flow change (e.g. Wikidata), but $block still |
421 | // contains Flow changes. In that case we should probably process those? |
422 | if ( !self::isFlow( $rc ) ) { |
423 | return true; |
424 | } |
425 | |
426 | set_error_handler( new RecoverableErrorHandler, -1 ); |
427 | try { |
428 | /** @var Formatter\ChangesListQuery $query */ |
429 | $query = Container::get( 'query.changeslist' ); |
430 | |
431 | $row = $query->getResult( $changesList, $rc, $changesList->isWatchlist() ); |
432 | if ( $row === false ) { |
433 | restore_error_handler(); |
434 | return false; |
435 | } |
436 | |
437 | /** @var Formatter\ChangesListFormatter $formatter */ |
438 | $formatter = Container::get( 'formatter.changeslist' ); |
439 | $logTextLinks = $formatter->getLogTextLinks( $row, $changesList, $block, $links ); |
440 | } catch ( Exception $e ) { |
441 | wfDebugLog( 'Flow', __METHOD__ . ': Exception formatting rc logtext ' . |
442 | $rc->getAttribute( 'rc_id' ) . ' ' . $e ); |
443 | MWExceptionHandler::logException( $e ); |
444 | return false; |
445 | } finally { |
446 | restore_error_handler(); |
447 | } |
448 | |
449 | if ( $logTextLinks === false ) { |
450 | return false; |
451 | } |
452 | |
453 | $links = $logTextLinks; |
454 | return true; |
455 | } |
456 | |
457 | /** |
458 | * @param EnhancedChangesList $changesList |
459 | * @param array &$data |
460 | * @param RecentChange[] $block |
461 | * @param RecentChange $rc |
462 | * @param string[] &$classes |
463 | * @param string[] &$attribs |
464 | * @return bool |
465 | */ |
466 | public function onEnhancedChangesListModifyLineData( $changesList, &$data, $block, $rc, &$classes, &$attribs ) { |
467 | return static::modifyChangesListLine( $changesList, $data, $rc, $classes ); |
468 | } |
469 | |
470 | /** |
471 | * @param EnhancedChangesList $changesList |
472 | * @param array &$data |
473 | * @param RecentChange $rc |
474 | * @return bool |
475 | */ |
476 | public function onEnhancedChangesListModifyBlockLineData( $changesList, &$data, $rc ) { |
477 | $classes = []; |
478 | return static::modifyChangesListLine( $changesList, $data, $rc, $classes ); |
479 | } |
480 | |
481 | /** |
482 | * @param ChangesList $changesList |
483 | * @param array &$data |
484 | * @param RecentChange $rc |
485 | * @param string[] &$classes |
486 | * @return bool |
487 | */ |
488 | private static function modifyChangesListLine( $changesList, &$data, $rc, &$classes ) { |
489 | // quit if non-flow |
490 | if ( !self::isFlow( $rc ) ) { |
491 | return true; |
492 | } |
493 | |
494 | $query = Container::get( 'query.changeslist' ); |
495 | $row = $query->getResult( $changesList, $rc, $changesList->isWatchlist() ); |
496 | if ( $row === false ) { |
497 | return false; |
498 | } |
499 | |
500 | /** @var Formatter\ChangesListFormatter $formatter */ |
501 | $formatter = Container::get( 'formatter.changeslist' ); |
502 | try { |
503 | $data['timestampLink'] = $formatter->getTimestampLink( $row, $changesList ); |
504 | $data['recentChangesFlags'] = array_merge( |
505 | $data['recentChangesFlags'], |
506 | $formatter->getFlags( $row, $changesList ) |
507 | ); |
508 | $classes[] = 'mw-changeslist-src-mw-edit'; |
509 | } catch ( PermissionException $e ) { |
510 | return false; |
511 | } |
512 | |
513 | return true; |
514 | } |
515 | |
516 | /** |
517 | * Checks if the given recent change entry is from Flow |
518 | * @param RecentChange $rc |
519 | * @return bool |
520 | */ |
521 | private static function isFlow( $rc ) { |
522 | $source = $rc->getAttribute( 'rc_source' ); |
523 | if ( $source === null ) { |
524 | $rcType = (int)$rc->getAttribute( 'rc_type' ); |
525 | return $rcType === RC_FLOW; |
526 | } else { |
527 | return $source === RecentChangesListener::SRC_FLOW; |
528 | } |
529 | } |
530 | |
531 | public static function onSpecialCheckUserGetLinksFromRow( AbstractCheckUserPager $pager, $row, &$links ) { |
532 | // TODO: Replace accesses to $row properties with the prefix "cuc_" to |
533 | // remove the need for this aliasing. |
534 | if ( isset( $row->type ) ) { |
535 | $row->cuc_type = $row->type; |
536 | } |
537 | if ( isset( $row->comment_text ) ) { |
538 | $row->cuc_comment_text = $row->comment_text; |
539 | $row->cuc_comment_data = $row->comment_data; |
540 | } |
541 | |
542 | if ( $row->cuc_type != RC_FLOW ) { |
543 | return; |
544 | } |
545 | |
546 | $replacement = self::getReplacementRowItems( $pager->getContext(), $row ); |
547 | |
548 | if ( $replacement === null ) { |
549 | // some sort of failure, but this is a RC_FLOW so blank out hist/diff links |
550 | // which aren't correct |
551 | $links['history'] = ''; |
552 | $links['diff'] = ''; |
553 | } else { |
554 | $links = $replacement; |
555 | } |
556 | } |
557 | |
558 | public static function onCheckUserFormatRow( IContextSource $context, $row, &$rowItems ) { |
559 | // TODO: Replace accesses to $row properties with the prefix "cuc_" to |
560 | // remove the need for this aliasing. |
561 | if ( isset( $row->type ) ) { |
562 | $row->cuc_type = $row->type; |
563 | } |
564 | if ( isset( $row->comment_text ) ) { |
565 | $row->cuc_comment_text = $row->comment_text; |
566 | $row->cuc_comment_data = $row->comment_data; |
567 | } |
568 | |
569 | if ( $row->cuc_type != RC_FLOW ) { |
570 | return; |
571 | } |
572 | |
573 | $replacement = self::getReplacementRowItems( $context, $row ); |
574 | |
575 | // These links are incorrect for Flow |
576 | $rowItems['links']['diffLink'] = ''; |
577 | $rowItems['links']['historyLink'] = ''; |
578 | |
579 | if ( $replacement !== null ) { |
580 | array_unshift( $rowItems['links'], $replacement['links'] ); |
581 | $rowItems['info']['titleLink'] = $replacement['title']; |
582 | } |
583 | } |
584 | |
585 | /** |
586 | * @param IContextSource $context |
587 | * @param stdClass $row |
588 | * @return array|null |
589 | */ |
590 | private static function getReplacementRowItems( IContextSource $context, $row ): ?array { |
591 | set_error_handler( new RecoverableErrorHandler, -1 ); |
592 | $replacement = null; |
593 | try { |
594 | /** @var CheckUserQuery $query */ |
595 | $query = Container::get( 'query.checkuser' ); |
596 | // @todo: create hook to allow batch-loading this data, instead of doing piecemeal like this |
597 | $query->loadMetadataBatch( [ $row ] ); |
598 | $row = $query->getResult( $row ); |
599 | if ( $row !== false ) { |
600 | /** @var Formatter\CheckUserFormatter $formatter */ |
601 | $formatter = Container::get( 'formatter.checkuser' ); |
602 | $replacement = $formatter->format( $row, $context ); |
603 | } |
604 | } catch ( Exception $e ) { |
605 | wfDebugLog( 'Flow', __METHOD__ . ': Exception formatting cu ' . json_encode( $row ) . ' ' . $e ); |
606 | MWExceptionHandler::logException( $e ); |
607 | } finally { |
608 | restore_error_handler(); |
609 | } |
610 | |
611 | return $replacement; |
612 | } |
613 | |
614 | /** |
615 | * Regular talk page "Create source" and "Add topic" links are quite useless |
616 | * in the context of Flow boards. Let's get rid of them. |
617 | * |
618 | * @param SkinTemplate $template |
619 | * @param array &$links |
620 | */ |
621 | public function onSkinTemplateNavigation__Universal( $template, &$links ): void { |
622 | global $wgFlowCoreActionWhitelist, |
623 | $wgMFPageActions; |
624 | |
625 | $title = $template->getTitle(); |
626 | |
627 | // if Flow is enabled on this talk page, overrule talk page red link |
628 | if ( $title->getContentModel() === CONTENT_MODEL_FLOW_BOARD ) { |
629 | // Turn off page actions in MobileFrontend. |
630 | // FIXME: Find more elegant standard way of doing this. |
631 | $wgMFPageActions = []; |
632 | |
633 | // watch star & delete links are inside the topic itself |
634 | if ( $title->getNamespace() === NS_TOPIC ) { |
635 | unset( $links['actions']['watch'] ); |
636 | unset( $links['actions']['unwatch'] ); |
637 | unset( $links['actions']['delete'] ); |
638 | } |
639 | |
640 | // hide all views unless whitelisted |
641 | foreach ( $links['views'] as $action => $data ) { |
642 | if ( !in_array( $action, $wgFlowCoreActionWhitelist ) ) { |
643 | unset( $links['views'][$action] ); |
644 | } |
645 | } |
646 | |
647 | // hide all actions unless whitelisted |
648 | foreach ( $links['actions'] as $action => $data ) { |
649 | if ( !in_array( $action, $wgFlowCoreActionWhitelist ) ) { |
650 | unset( $links['actions'][$action] ); |
651 | } |
652 | } |
653 | |
654 | if ( isset( $links['namespaces']['topic_talk'] ) ) { |
655 | // hide discussion page in Topic namespace(which is already discussion) |
656 | unset( $links['namespaces']['topic_talk'] ); |
657 | unset( $links['associated-pages']['topic_talk'] ); |
658 | // hide protection (topic protection is done via moderation) |
659 | unset( $links['actions']['protect'] ); |
660 | // topic pages are also not movable |
661 | unset( $links['actions']['move'] ); |
662 | } |
663 | } |
664 | } |
665 | |
666 | /** |
667 | * When a (talk) page does not exist, one of the checks being performed is |
668 | * to see if the page had once existed but was removed. In doing so, the |
669 | * deletion & move log is checked. |
670 | * |
671 | * In theory, a Flow board could overtake a non-existing talk page. If that |
672 | * board is later removed, this will be run to see if a message can be |
673 | * displayed to inform the user if the page has been deleted/moved. |
674 | * |
675 | * Since, in Flow, we also write (topic, post, ...) deletion to the deletion |
676 | * log, we don't want those to appear, since they're not actually actions |
677 | * related to that talk page (rather: they were actions on the board) |
678 | * |
679 | * @param array &$conds Array of conditions |
680 | * @param array $logTypes Array of log types |
681 | */ |
682 | public function onArticle__MissingArticleConditions( &$conds, $logTypes ) { |
683 | global $wgLogActionsHandlers; |
684 | /** @var FlowActions $actions */ |
685 | $actions = Container::get( 'flow_actions' ); |
686 | |
687 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
688 | |
689 | foreach ( $actions->getActions() as $action ) { |
690 | foreach ( $logTypes as $logType ) { |
691 | // Check if Flow actions are defined for the requested log types |
692 | // and make sure they're ignored. |
693 | if ( isset( $wgLogActionsHandlers["$logType/flow-$action"] ) ) { |
694 | $conds[] = $dbr->expr( 'log_action', '!=', "flow-$action" ); |
695 | } |
696 | } |
697 | } |
698 | } |
699 | |
700 | /** |
701 | * Adds Flow entries to watchlists |
702 | * |
703 | * @param array &$types Type array to modify |
704 | */ |
705 | public function onSpecialWatchlistGetNonRevisionTypes( &$types ) { |
706 | $types[] = RC_FLOW; |
707 | } |
708 | |
709 | /** |
710 | * Make sure no user can register a flow-*-usertext username, to avoid |
711 | * confusion with a real user when we print e.g. "Suppressed" instead of a |
712 | * username. Additionally reserve the username used to add a revision on |
713 | * taking over a page. |
714 | * |
715 | * @param array &$names |
716 | */ |
717 | public function onUserGetReservedNames( &$names ) { |
718 | $permissions = Model\AbstractRevision::$perms; |
719 | foreach ( $permissions as $permission ) { |
720 | $names[] = "msg:flow-$permission-usertext"; |
721 | } |
722 | |
723 | // Reserve the bot account we use during content model changes & LQT conversion |
724 | $names[] = FLOW_TALK_PAGE_MANAGER_USER; |
725 | } |
726 | |
727 | /** |
728 | * Static variables that do not vary by request; delivered through startup module |
729 | * @param array &$vars |
730 | * @param string $skin |
731 | * @param Config $config |
732 | */ |
733 | public function onResourceLoaderGetConfigVars( array &$vars, $skin, Config $config ): void { |
734 | global $wgFlowAjaxTimeout; |
735 | |
736 | $vars['wgFlowMaxTopicLength'] = Model\PostRevision::MAX_TOPIC_LENGTH; |
737 | $vars['wgFlowMentionTemplate'] = wfMessage( 'flow-ve-mention-template-title' )->inContentLanguage()->plain(); |
738 | $vars['wgFlowAjaxTimeout'] = $wgFlowAjaxTimeout; |
739 | } |
740 | |
741 | /** |
742 | * Intercept contribution entries and format those belonging to Flow |
743 | * |
744 | * @param IContextSource $pager |
745 | * @param string &$ret The HTML line |
746 | * @param stdClass $row The data for this line |
747 | * @param array &$classes the classes to add to the surrounding <li> |
748 | * @param array &$attribs |
749 | * @return bool |
750 | */ |
751 | public function onDeletedContributionsLineEnding( $pager, &$ret, $row, &$classes, &$attribs ) { |
752 | if ( !$row instanceof Formatter\FormatterRow ) { |
753 | return true; |
754 | } |
755 | |
756 | set_error_handler( new RecoverableErrorHandler, -1 ); |
757 | try { |
758 | /** @var Formatter\ContributionsFormatter $formatter */ |
759 | $formatter = Container::get( 'formatter.contributions' ); |
760 | $line = $formatter->format( $row, $pager ); |
761 | } catch ( PermissionException $e ) { |
762 | $line = false; |
763 | } catch ( Exception $e ) { |
764 | wfDebugLog( 'Flow', __METHOD__ . ': Failed formatting contribution ' . |
765 | json_encode( $row ) . ': ' . $e->getMessage() ); |
766 | MWExceptionHandler::logException( $e ); |
767 | $line = false; |
768 | } finally { |
769 | restore_error_handler(); |
770 | } |
771 | |
772 | if ( $line === false ) { |
773 | return false; |
774 | } |
775 | |
776 | $classes[] = 'mw-flow-contribution'; |
777 | $ret = $line; |
778 | |
779 | // If we output one or more lines of contributions entries we also need to include |
780 | // the javascript that hooks into moderation actions. |
781 | $pager->getOutput()->addModules( [ 'ext.flow.contributions' ] ); |
782 | $pager->getOutput()->addModuleStyles( [ 'ext.flow.contributions.styles' ] ); |
783 | |
784 | return true; |
785 | } |
786 | |
787 | /** |
788 | * Intercept contribution entries and format those belonging to Flow |
789 | * |
790 | * @param IContextSource $pager |
791 | * @param string &$ret The HTML line |
792 | * @param stdClass $row The data for this line |
793 | * @param array &$classes the classes to add to the surrounding <li> |
794 | * @param array &$attribs |
795 | * @return bool |
796 | */ |
797 | public function onContributionsLineEnding( $pager, &$ret, $row, &$classes, &$attribs ) { |
798 | return static::onDeletedContributionsLineEnding( $pager, $ret, $row, $classes, $attribs ); |
799 | } |
800 | |
801 | /** |
802 | * Convert flow contributions entries into FeedItem instances |
803 | * for ApiFeedContributions |
804 | * |
805 | * @param object $row Single row of data from ContribsPager |
806 | * @param IContextSource $ctx The context to creat the feed item within |
807 | * @param FeedItem|null &$feedItem Return value holder for created feed item. |
808 | * @return bool |
809 | */ |
810 | public function onApiFeedContributions__feedItem( $row, $ctx, &$feedItem ) { |
811 | if ( !$row instanceof Formatter\FormatterRow ) { |
812 | return true; |
813 | } |
814 | |
815 | set_error_handler( new RecoverableErrorHandler, -1 ); |
816 | try { |
817 | /** @var Formatter\FeedItemFormatter $formatter */ |
818 | $formatter = Container::get( 'formatter.contributions.feeditem' ); |
819 | $result = $formatter->format( $row, $ctx ); |
820 | } catch ( PermissionException $e ) { |
821 | return false; |
822 | } catch ( Exception $e ) { |
823 | wfDebugLog( 'Flow', __METHOD__ . ': Failed formatting contribution ' . |
824 | json_encode( $row ) . ': ' . $e->getMessage() ); |
825 | MWExceptionHandler::logException( $e ); |
826 | return false; |
827 | } finally { |
828 | restore_error_handler(); |
829 | } |
830 | |
831 | if ( $result instanceof FeedItem ) { |
832 | $feedItem = $result; |
833 | return true; |
834 | } else { |
835 | // If we failed to render a flow row, cancel it. This could be |
836 | // either permissions or bugs. |
837 | return false; |
838 | } |
839 | } |
840 | |
841 | /** |
842 | * Gets Flow contributions for contributions-related special pages |
843 | * |
844 | * @see onDeletedContributionsQuery |
845 | * @see onContributionsQuery |
846 | * |
847 | * @param array &$data |
848 | * @param IndexPager $pager |
849 | * @param string $offset |
850 | * @param int $limit |
851 | * @param bool $descending |
852 | * @param array $rangeOffsets Query range, in the format of [ startOffset, endOffset ] |
853 | * @return bool |
854 | */ |
855 | private static function getContributionsQuery( &$data, $pager, $offset, $limit, $descending, $rangeOffsets = [] ) { |
856 | if ( |
857 | !( $pager instanceof ContribsPager ) && |
858 | !( $pager instanceof DeletedContribsPager ) |
859 | ) { |
860 | return false; |
861 | } |
862 | |
863 | set_error_handler( new RecoverableErrorHandler, -1 ); |
864 | try { |
865 | /** @var Formatter\ContributionsQuery $query */ |
866 | $query = Container::get( 'query.contributions' ); |
867 | $results = $query->getResults( $pager, $offset, $limit, $descending, $rangeOffsets ); |
868 | } catch ( Exception $e ) { |
869 | wfDebugLog( 'Flow', __METHOD__ . ': Failed contributions query' ); |
870 | MWExceptionHandler::logException( $e ); |
871 | $results = false; |
872 | } finally { |
873 | restore_error_handler(); |
874 | } |
875 | |
876 | if ( $results === false ) { |
877 | return false; |
878 | } |
879 | |
880 | $data[] = $results; |
881 | |
882 | return true; |
883 | } |
884 | |
885 | /** |
886 | * Adds Flow contributions to the DeletedContributions special page |
887 | * |
888 | * @inheritDoc |
889 | */ |
890 | public function onDeletedContribsPager__reallyDoQuery( &$data, $pager, $offset, $limit, $descending ) { |
891 | return self::getContributionsQuery( $data, $pager, $offset, $limit, $descending, [ $pager->getEndOffset() ] ); |
892 | } |
893 | |
894 | /** |
895 | * Adds Flow contributions to the Contributions special page |
896 | * |
897 | * @inheritDoc |
898 | */ |
899 | public function onContribsPager__reallyDoQuery( &$data, $pager, $offset, $limit, $descending ) { |
900 | // Flow has nothing to do with the tag filter, so ignore tag searches |
901 | if ( $pager->getTagFilter() != false ) { |
902 | return true; |
903 | } |
904 | |
905 | return static::getContributionsQuery( $data, $pager, $offset, $limit, $descending, $pager->getRangeOffsets() ); |
906 | } |
907 | |
908 | /** |
909 | * Define and add descriptions for board-related variables |
910 | * @param array &$realValues |
911 | */ |
912 | public static function onAbuseFilterBuilder( &$realValues ) { |
913 | $realValues['vars'] += [ |
914 | 'board_id' => 'board-id', |
915 | 'board_namespace' => 'board-namespace', |
916 | 'board_title' => 'board-title', |
917 | 'board_prefixedtitle' => 'board-prefixedtitle', |
918 | ]; |
919 | } |
920 | |
921 | /** |
922 | * Add our deprecated variables |
923 | * @param array &$deprecatedVars |
924 | */ |
925 | public static function onAbuseFilterDeprecatedVariables( &$deprecatedVars ) { |
926 | $deprecatedVars += [ |
927 | 'board_articleid' => 'board_id', |
928 | 'board_text' => 'board_title', |
929 | 'board_prefixedtext' => 'board_prefixedtitle', |
930 | ]; |
931 | } |
932 | |
933 | /** |
934 | * Adds lazy-load methods for AbstractRevision objects. |
935 | * |
936 | * @param string $method Method to generate the variable |
937 | * @param VariableHolder $vars |
938 | * @param array $parameters Parameters with data to compute the value |
939 | * @param mixed &$result Result of the computation |
940 | * @return bool |
941 | */ |
942 | public static function onAbuseFilterComputeVariable( |
943 | $method, |
944 | VariableHolder $vars, |
945 | $parameters, |
946 | &$result |
947 | ) { |
948 | // fetch all lazy-load methods |
949 | $methods = self::getAbuseFilter()->lazyLoadMethods(); |
950 | |
951 | // method isn't known here |
952 | if ( !isset( $methods[$method] ) ) { |
953 | return true; |
954 | } |
955 | |
956 | // fetch variable result from lazy-load method |
957 | $result = $methods[$method]( $vars, $parameters ); |
958 | return false; |
959 | } |
960 | |
961 | /** |
962 | * Abort notifications regarding occupied pages coming from the RecentChange class. |
963 | * Flow has its own notifications through Echo. |
964 | * |
965 | * Also don't notify for actions made by the talk page manager. |
966 | * |
967 | * @param User $editor |
968 | * @param Title $title |
969 | * @param RecentChange $rc |
970 | * @return bool false to abort email notification |
971 | */ |
972 | public function onAbortEmailNotification( $editor, $title, $rc ) { |
973 | if ( $title->getContentModel() === CONTENT_MODEL_FLOW_BOARD ) { |
974 | // Since we are aborting the notification we need to manually update the watchlist |
975 | $config = RequestContext::getMain()->getConfig(); |
976 | if ( $config->get( 'EnotifWatchlist' ) || $config->get( 'ShowUpdatedMarker' ) ) { |
977 | MediaWikiServices::getInstance()->getWatchedItemStore()->updateNotificationTimestamp( |
978 | $editor, |
979 | $title, |
980 | wfTimestampNow() |
981 | ); |
982 | } |
983 | return false; |
984 | } |
985 | |
986 | if ( !$editor instanceof UserIdentity ) { |
987 | return true; |
988 | } |
989 | |
990 | if ( self::isTalkpageManagerUser( $editor ) ) { |
991 | return false; |
992 | } |
993 | |
994 | return true; |
995 | } |
996 | |
997 | /** |
998 | * Suppress all Echo notifications generated by the Talk page manager user |
999 | * |
1000 | * @param Event $event |
1001 | * @return bool |
1002 | */ |
1003 | public static function onBeforeEchoEventInsert( Event $event ) { |
1004 | $agent = $event->getAgent(); |
1005 | |
1006 | if ( $agent === null ) { |
1007 | return true; |
1008 | } |
1009 | |
1010 | if ( self::isTalkpageManagerUser( $agent ) ) { |
1011 | return false; |
1012 | } |
1013 | |
1014 | return true; |
1015 | } |
1016 | |
1017 | /** |
1018 | * Suppress the 'You have new messages!' indication when a change to a |
1019 | * user talk page is done by the talk page manager user. |
1020 | * |
1021 | * @param WikiPage $page |
1022 | * @param User $recipient |
1023 | * @return bool |
1024 | */ |
1025 | public function onArticleEditUpdateNewTalk( $page, $recipient ) { |
1026 | $user = User::newFromId( $page->getUser( RevisionRecord::RAW ) ); |
1027 | |
1028 | if ( self::isTalkpageManagerUser( $user ) ) { |
1029 | return false; |
1030 | } |
1031 | |
1032 | return true; |
1033 | } |
1034 | |
1035 | /** |
1036 | * @param UserIdentity $user |
1037 | * @return bool |
1038 | */ |
1039 | private static function isTalkpageManagerUser( UserIdentity $user ) { |
1040 | return $user->getName() === FLOW_TALK_PAGE_MANAGER_USER; |
1041 | } |
1042 | |
1043 | /** |
1044 | * Don't send email notifications that are imported from LiquidThreads. It will |
1045 | * still be in their web notifications (if enabled), but they will never be |
1046 | * notified via email (regardless of batching settings) for this particular |
1047 | * notification. |
1048 | * @param User $user |
1049 | * @param Event $event |
1050 | * @return bool |
1051 | */ |
1052 | public static function onEchoAbortEmailNotification( User $user, Event $event ) { |
1053 | $extra = $event->getExtra(); |
1054 | return !isset( $extra['lqtThreadId'] ); |
1055 | } |
1056 | |
1057 | /** |
1058 | * Hides the orange alert indicating 'You have a new message' |
1059 | * when the user reads flow-topic replies. |
1060 | * |
1061 | * @param User $user |
1062 | * @param Title $title |
1063 | * @return bool true to show the alert, false to hide(abort) the alert |
1064 | */ |
1065 | public function onBeforeDisplayOrangeAlert( User $user, Title $title ) { |
1066 | if ( $title->getNamespace() === NS_TOPIC ) { |
1067 | /** @var Data\ObjectManager $storage */ |
1068 | $storage = Container::get( 'storage.workflow' ); |
1069 | $uuid = WorkflowLoaderFactory::uuidFromTitle( $title ); |
1070 | /** @var Model\Workflow $workflow */ |
1071 | $workflow = $storage->get( $uuid ); |
1072 | if ( $workflow && $user->getTalkPage()->equals( $workflow->getOwnerTitle() ) ) { |
1073 | return false; |
1074 | } |
1075 | } |
1076 | |
1077 | return true; |
1078 | } |
1079 | |
1080 | public function onInfoAction( $ctx, &$pageinfo ) { |
1081 | if ( $ctx->getTitle()->getContentModel() !== CONTENT_MODEL_FLOW_BOARD ) { |
1082 | return; |
1083 | } |
1084 | |
1085 | // All of the info in this section is wrong for Flow pages, |
1086 | // so we'll just remove it. |
1087 | unset( $pageinfo['header-edits'] ); |
1088 | |
1089 | // These keys are wrong on Flow pages, so we'll remove them |
1090 | static $badMessageKeys = [ 'pageinfo-length' ]; |
1091 | |
1092 | foreach ( $pageinfo['header-basic'] as $num => $val ) { |
1093 | if ( $val[0] instanceof Message && in_array( $val[0]->getKey(), $badMessageKeys ) ) { |
1094 | unset( $pageinfo['header-basic'][$num] ); |
1095 | } |
1096 | } |
1097 | } |
1098 | |
1099 | /** |
1100 | * Provide detail about the Flow edit for checkusers using Special:CheckUser / Special:Investigate |
1101 | * |
1102 | * @param string &$ip |
1103 | * @param string|false &$xff |
1104 | * @param array &$row The row to be inserted (before defaults are applied) |
1105 | * @param UserIdentity $user |
1106 | * @param ?RecentChange $rc If triggered by a RecentChange, then this is the associated |
1107 | * RecentChange object. Null if not triggered by a RecentChange. |
1108 | */ |
1109 | public static function onCheckUserInsertChangesRow( |
1110 | string &$ip, |
1111 | &$xff, |
1112 | array &$row, |
1113 | UserIdentity $user, |
1114 | ?RecentChange $rc |
1115 | ) { |
1116 | if ( $rc === null || $rc->getAttribute( 'rc_source' ) !== RecentChangesListener::SRC_FLOW ) { |
1117 | return; |
1118 | } |
1119 | |
1120 | $params = unserialize( $rc->getAttribute( 'rc_params' ) ); |
1121 | $change = $params['flow-workflow-change']; |
1122 | |
1123 | // don't forget to increase the version number when data format changes |
1124 | $comment = CheckUserQuery::VERSION_PREFIX; |
1125 | $comment .= ',' . $change['action']; |
1126 | $comment .= ',' . $change['workflow']; |
1127 | $comment .= ',' . $change['revision']; |
1128 | |
1129 | $row['cuc_comment'] = $comment; |
1130 | } |
1131 | |
1132 | public function onIRCLineURL( &$url, &$query, $rc ) { |
1133 | if ( $rc->getAttribute( 'rc_source' ) !== RecentChangesListener::SRC_FLOW ) { |
1134 | return; |
1135 | } |
1136 | |
1137 | set_error_handler( new RecoverableErrorHandler, -1 ); |
1138 | $result = null; |
1139 | try { |
1140 | /** @var Formatter\IRCLineUrlFormatter $formatter */ |
1141 | $formatter = Container::get( 'formatter.irclineurl' ); |
1142 | $result = $formatter->format( $rc ); |
1143 | } catch ( Exception $e ) { |
1144 | $result = null; |
1145 | wfDebugLog( 'Flow', __METHOD__ . ': Failed formatting rc ' . |
1146 | $rc->getAttribute( 'rc_id' ) . ': ' . $e->getMessage() ); |
1147 | MWExceptionHandler::logException( $e ); |
1148 | } finally { |
1149 | restore_error_handler(); |
1150 | } |
1151 | |
1152 | if ( $result !== null ) { |
1153 | $url = $result; |
1154 | $query = ''; |
1155 | } |
1156 | } |
1157 | |
1158 | public function onWhatLinksHereProps( $row, $title, $target, &$props ) { |
1159 | set_error_handler( new RecoverableErrorHandler, -1 ); |
1160 | try { |
1161 | /** @var ReferenceClarifier $clarifier */ |
1162 | $clarifier = Container::get( 'reference.clarifier' ); |
1163 | $newProps = $clarifier->getWhatLinksHereProps( $row, $title, $target ); |
1164 | |
1165 | $props = array_merge( $props, $newProps ); |
1166 | } catch ( Exception $e ) { |
1167 | wfDebugLog( 'Flow', sprintf( |
1168 | '%s: Failed formatting WhatLinksHere for %s to %s', |
1169 | __METHOD__, |
1170 | $title->getFullText(), |
1171 | $target->getFullText() |
1172 | ) ); |
1173 | MWExceptionHandler::logException( $e ); |
1174 | } finally { |
1175 | restore_error_handler(); |
1176 | } |
1177 | } |
1178 | |
1179 | /** |
1180 | * Add topiclist sortby to preferences. |
1181 | * |
1182 | * @param User $user |
1183 | * @param array &$preferences |
1184 | */ |
1185 | public function onGetPreferences( $user, &$preferences ) { |
1186 | $preferences['flow-topiclist-sortby'] = [ |
1187 | 'type' => 'api', |
1188 | ]; |
1189 | |
1190 | $preferences['flow-editor'] = [ |
1191 | 'type' => 'api' |
1192 | ]; |
1193 | |
1194 | $preferences['flow-side-rail-state'] = [ |
1195 | 'type' => 'api' |
1196 | ]; |
1197 | |
1198 | if ( ExtensionRegistry::getInstance()->isLoaded( 'VisualEditor' ) ) { |
1199 | $preferences['flow-visualeditor'] = [ |
1200 | 'type' => 'toggle', |
1201 | 'label-message' => 'flow-preference-visualeditor', |
1202 | 'section' => 'editing/editor', |
1203 | ]; |
1204 | } |
1205 | } |
1206 | |
1207 | /** |
1208 | * @param User $user |
1209 | * @param WikiPage $page |
1210 | * @param Status &$status |
1211 | * @return bool |
1212 | */ |
1213 | public static function handleWatchArticle( $user, WikiPage $page, &$status ) { |
1214 | $title = $page->getTitle(); |
1215 | if ( $title->getNamespace() == NS_TOPIC ) { |
1216 | // @todo - use !$title->exists()? |
1217 | /** @var Data\ManagerGroup $storage */ |
1218 | $storage = Container::get( 'storage' ); |
1219 | $found = $storage->find( |
1220 | 'PostRevision', |
1221 | [ 'rev_type_id' => strtolower( $title->getDBkey() ) ], |
1222 | [ 'sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1 ] |
1223 | ); |
1224 | if ( !$found ) { |
1225 | return false; |
1226 | } |
1227 | $post = reset( $found ); |
1228 | if ( !$post->isTopicTitle() ) { |
1229 | return false; |
1230 | } |
1231 | } |
1232 | return true; |
1233 | } |
1234 | |
1235 | /** |
1236 | * Don't watch a non-existing flow topic |
1237 | * |
1238 | * @param User $user |
1239 | * @param WikiPage $page |
1240 | * @param Status &$status |
1241 | * @param string|null $expiry |
1242 | * @return bool |
1243 | */ |
1244 | public function onWatchArticle( $user, $page, &$status, $expiry ) { |
1245 | return self::handleWatchArticle( $user, $page, $status ); |
1246 | } |
1247 | |
1248 | /** |
1249 | * Don't unwatch a non-existing flow topic |
1250 | * |
1251 | * @param User $user |
1252 | * @param WikiPage $page |
1253 | * @param Status &$status |
1254 | * @return bool |
1255 | */ |
1256 | public function onUnwatchArticle( $user, $page, &$status ) { |
1257 | return self::handleWatchArticle( $user, $page, $status ); |
1258 | } |
1259 | |
1260 | /** |
1261 | * Checks whether this is a valid move technically. MovePageIsValidMove should not |
1262 | * be affected by the specific user, or user permissions. |
1263 | * |
1264 | * Those are handled in onMovePageCheckPermissions, called later. |
1265 | * |
1266 | * @param Title $oldTitle |
1267 | * @param Title $newTitle |
1268 | * @param Status $status Status to update with any technical issues |
1269 | * |
1270 | * @return bool true to continue, false to abort the hook |
1271 | */ |
1272 | public function onMovePageIsValidMove( $oldTitle, $newTitle, $status ) { |
1273 | // We only care about moving Flow boards, and *not* moving Flow topics |
1274 | // (but both are CONTENT_MODEL_FLOW_BOARD) |
1275 | if ( $oldTitle->getContentModel() !== CONTENT_MODEL_FLOW_BOARD ) { |
1276 | return true; |
1277 | } |
1278 | |
1279 | // Pages within the Topic namespace are not movable |
1280 | // This is also enforced by the namespace configuration in extension.json. |
1281 | if ( $oldTitle->getNamespace() === NS_TOPIC ) { |
1282 | $status->fatal( 'flow-error-move-topic' ); |
1283 | return false; |
1284 | } |
1285 | |
1286 | /** @var OccupationController $occupationController */ |
1287 | $occupationController = MediaWikiServices::getInstance()->getService( 'FlowTalkpageManager' ); |
1288 | $flowStatus = $occupationController->checkIfCreationIsPossible( $newTitle, /*mustNotExist*/ true ); |
1289 | $status->merge( $flowStatus ); |
1290 | |
1291 | return true; |
1292 | } |
1293 | |
1294 | /** |
1295 | * Checks whether user has permission to move the board. |
1296 | * |
1297 | * Technical restrictions are handled in onMovePageIsValidMove, called earlier. |
1298 | * |
1299 | * @param Title $oldTitle |
1300 | * @param Title $newTitle |
1301 | * @param User $user User doing the move |
1302 | * @param string $reason Reason for the move |
1303 | * @param Status $status Status updated with any permissions issue |
1304 | */ |
1305 | public function onMovePageCheckPermissions( |
1306 | $oldTitle, |
1307 | $newTitle, |
1308 | $user, |
1309 | $reason, |
1310 | $status |
1311 | ) { |
1312 | // Only affect moves if the source has Flow content model |
1313 | if ( $oldTitle->getContentModel() !== CONTENT_MODEL_FLOW_BOARD ) { |
1314 | return; |
1315 | } |
1316 | |
1317 | /** @var OccupationController $occupationController */ |
1318 | $occupationController = MediaWikiServices::getInstance()->getService( 'FlowTalkpageManager' ); |
1319 | $permissionStatus = $occupationController->checkIfUserHasPermission( |
1320 | $newTitle, |
1321 | $user |
1322 | ); |
1323 | $status->merge( $permissionStatus ); |
1324 | } |
1325 | |
1326 | /** |
1327 | * @param Title $title |
1328 | * @param string[] &$urls |
1329 | */ |
1330 | public function onTitleSquidURLs( $title, &$urls ) { |
1331 | if ( $title->getNamespace() !== NS_TOPIC ) { |
1332 | return; |
1333 | } |
1334 | try { |
1335 | $uuid = WorkflowLoaderFactory::uuidFromTitle( $title ); |
1336 | } catch ( InvalidInputException $e ) { |
1337 | wfDebugLog( 'Flow', __METHOD__ . ': Invalid title ' . $title->getPrefixedText() ); |
1338 | return; |
1339 | } |
1340 | /** @var Data\ManagerGroup $storage */ |
1341 | $storage = Container::get( 'storage' ); |
1342 | $workflow = $storage->get( 'Workflow', $uuid ); |
1343 | if ( !$workflow instanceof Model\Workflow ) { |
1344 | wfDebugLog( 'Flow', __METHOD__ . ': Title for non-existent Workflow ' . |
1345 | $title->getPrefixedText() ); |
1346 | return; |
1347 | } |
1348 | |
1349 | $htmlCache = MediaWikiServices::getInstance()->getHtmlCacheUpdater(); |
1350 | $urls = array_merge( |
1351 | $urls, |
1352 | $htmlCache->getUrls( $workflow->getOwnerTitle() ) |
1353 | ); |
1354 | } |
1355 | |
1356 | /** |
1357 | * @param array &$tools Extra links |
1358 | * @param Title $title |
1359 | * @param bool $redirect Whether the page is a redirect |
1360 | * @param Skin $skin |
1361 | * @param string &$link |
1362 | */ |
1363 | public function onWatchlistEditorBuildRemoveLine( |
1364 | &$tools, |
1365 | $title, |
1366 | $redirect, |
1367 | $skin, |
1368 | &$link |
1369 | ) { |
1370 | if ( $title->getNamespace() !== NS_TOPIC ) { |
1371 | // Leave all non Flow topics alone! |
1372 | return; |
1373 | } |
1374 | |
1375 | /* |
1376 | * Link to talk page is no applicable for Flow topics |
1377 | * Note that key 'talk' doesn't exist prior to |
1378 | * https://gerrit.wikimedia.org/r/#/c/156522/, so on old MW's, the link |
1379 | * to talk page will still be present. |
1380 | */ |
1381 | unset( $tools['talk'] ); |
1382 | |
1383 | if ( !$link ) { |
1384 | /* |
1385 | * https://gerrit.wikimedia.org/r/#/c/156118/ adds argument $link. |
1386 | * Prior to that patch, it was impossible to change the link, so |
1387 | * let's quit early if it doesn't exist. |
1388 | */ |
1389 | return; |
1390 | } |
1391 | |
1392 | try { |
1393 | // Find the title text of this specific topic |
1394 | $uuid = WorkflowLoaderFactory::uuidFromTitle( $title ); |
1395 | $collection = PostCollection::newFromId( $uuid ); |
1396 | $revision = $collection->getLastRevision(); |
1397 | } catch ( Exception $e ) { |
1398 | wfWarn( __METHOD__ . ': Failed to locate revision for: ' . $title->getDBkey() ); |
1399 | return; |
1400 | } |
1401 | |
1402 | $content = $revision->getContent( 'topic-title-plaintext' ); |
1403 | $link = MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( $title, $content ); |
1404 | } |
1405 | |
1406 | /** |
1407 | * @param array &$watchlistInfo Watchlisted pages |
1408 | */ |
1409 | public function onWatchlistEditorBeforeFormRender( &$watchlistInfo ) { |
1410 | if ( !isset( $watchlistInfo[NS_TOPIC] ) ) { |
1411 | // No topics watchlisted |
1412 | return; |
1413 | } |
1414 | |
1415 | $ids = array_keys( $watchlistInfo[NS_TOPIC] ); |
1416 | |
1417 | // build array of queries to be executed all at once |
1418 | $queries = []; |
1419 | foreach ( $ids as $id ) { |
1420 | try { |
1421 | $uuid = WorkflowLoaderFactory::uuidFromTitlePair( NS_TOPIC, $id ); |
1422 | $queries[] = [ 'rev_type_id' => $uuid ]; |
1423 | } catch ( Exception $e ) { |
1424 | // invalid id |
1425 | unset( $watchlistInfo[NS_TOPIC][$id] ); |
1426 | } |
1427 | } |
1428 | |
1429 | /** @var Data\ManagerGroup $storage */ |
1430 | $storage = Container::get( 'storage' ); |
1431 | |
1432 | /* |
1433 | * Now, finally find all requested topics - this will be stored in |
1434 | * local cache so subsequent calls (in onWatchlistEditorBuildRemoveLine) |
1435 | * will just find these in memory, instead of doing a bunch of network |
1436 | * requests. |
1437 | */ |
1438 | $storage->findMulti( |
1439 | 'PostRevision', |
1440 | $queries, |
1441 | [ 'sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1 ] |
1442 | ); |
1443 | } |
1444 | |
1445 | /** |
1446 | * For integration with the UserMerge extension. Provides the database and |
1447 | * sets of table/column pairs to update user id's within. |
1448 | * |
1449 | * @param array &$updateFields |
1450 | */ |
1451 | public static function onUserMergeAccountFields( &$updateFields ) { |
1452 | /** @var Data\Utils\UserMerger $merger */ |
1453 | $merger = Container::get( 'user_merger' ); |
1454 | foreach ( $merger->getAccountFields() as $row ) { |
1455 | $updateFields[] = $row; |
1456 | } |
1457 | } |
1458 | |
1459 | /** |
1460 | * Finalize the merge by purging any cached value that contained $oldUser |
1461 | * @param User &$oldUser |
1462 | * @param User &$newUser |
1463 | */ |
1464 | public static function onMergeAccountFromTo( User &$oldUser, User &$newUser ) { |
1465 | /** @var Data\Utils\UserMerger $merger */ |
1466 | $merger = Container::get( 'user_merger' ); |
1467 | $merger->finalizeMerge( $oldUser->getId(), $newUser->getId() ); |
1468 | } |
1469 | |
1470 | /** |
1471 | * Gives precedence to Flow over LQT. |
1472 | * @param Title $title |
1473 | * @param bool &$isLqtPage |
1474 | */ |
1475 | public static function onIsLiquidThreadsPage( Title $title, &$isLqtPage ) { |
1476 | if ( $isLqtPage && $title->getContentModel() === CONTENT_MODEL_FLOW_BOARD ) { |
1477 | $isLqtPage = false; |
1478 | } |
1479 | } |
1480 | |
1481 | public function onCategoryViewer__doCategoryQuery( $type, $res ) { |
1482 | if ( $type !== 'page' ) { |
1483 | return; |
1484 | } |
1485 | |
1486 | /** @var Formatter\CategoryViewerQuery $query */ |
1487 | $query = Container::get( 'query.categoryviewer' ); |
1488 | $query->loadMetadataBatch( $res ); |
1489 | } |
1490 | |
1491 | public function onCategoryViewer__generateLink( $type, $title, $html, &$link ) { |
1492 | if ( $type !== 'page' || $title->getNamespace() !== NS_TOPIC ) { |
1493 | return; |
1494 | } |
1495 | $uuid = UUID::create( strtolower( $title->getDBkey() ) ); |
1496 | if ( !$uuid ) { |
1497 | return; |
1498 | } |
1499 | /** @var Formatter\CategoryViewerQuery $query */ |
1500 | $query = Container::get( 'query.categoryviewer' ); |
1501 | $row = $query->getResult( $uuid ); |
1502 | /** @var Formatter\CategoryViewerFormatter $formatter */ |
1503 | $formatter = Container::get( 'formatter.categoryviewer' ); |
1504 | $result = $formatter->format( $row ); |
1505 | if ( $result ) { |
1506 | $link = $result; |
1507 | } |
1508 | } |
1509 | |
1510 | /** |
1511 | * Gets error HTML for attempted NS_TOPIC deletion using core interface |
1512 | * |
1513 | * @param Title $title Topic title they are attempting to delete |
1514 | * @return string Error html |
1515 | */ |
1516 | protected static function getTopicDeletionError( Title $title ) { |
1517 | $error = wfMessage( 'flow-error-core-topic-deletion', $title->getFullURL() )->parse(); |
1518 | $wrappedError = Html::rawElement( 'span', [ |
1519 | 'class' => 'plainlinks', |
1520 | ], $error ); |
1521 | return $wrappedError; |
1522 | } |
1523 | |
1524 | // This should block them from wasting their time filling the form, but it won't |
1525 | // without a core change. However, it does show the message. |
1526 | |
1527 | /** |
1528 | * Shows an error message when the user visits the deletion form if the page is in |
1529 | * the Topic namespace. |
1530 | * |
1531 | * @param Article $article Page the user requested to delete |
1532 | * @param OutputPage $output Output page |
1533 | * @param string &$reason Pre-filled reason given for deletion (note, this could |
1534 | * be used to customize this for boards and/or topics later) |
1535 | * @return bool False if it is a Topic; otherwise, true |
1536 | */ |
1537 | public function onArticleConfirmDelete( $article, $output, &$reason ) { |
1538 | $title = $article->getTitle(); |
1539 | if ( $title->inNamespace( NS_TOPIC ) ) { |
1540 | $output->addHTML( self::getTopicDeletionError( $title ) ); |
1541 | return false; |
1542 | } |
1543 | |
1544 | return true; |
1545 | } |
1546 | |
1547 | /** |
1548 | * Blocks topics from being deleted using the core deletion process, since it |
1549 | * doesn't work. |
1550 | * |
1551 | * @param WikiPage $article Page the user requested to delete |
1552 | * @param User $user User who requested to delete the article |
1553 | * @param string &$reason Reason given for deletion |
1554 | * @param string &$error Error explaining why we are not allowing the deletion |
1555 | * @param Status &$status |
1556 | * @param bool $suppress |
1557 | * @return bool False if it is a Topic (to block it); otherwise, true |
1558 | */ |
1559 | public function onArticleDelete( WikiPage $article, User $user, &$reason, &$error, Status &$status, $suppress ) { |
1560 | $title = $article->getTitle(); |
1561 | if ( $title->inNamespace( NS_TOPIC ) ) { |
1562 | $error = self::getTopicDeletionError( $title ); |
1563 | return false; |
1564 | } |
1565 | |
1566 | return true; |
1567 | } |
1568 | |
1569 | /** |
1570 | * Evicts topics from Squid/Varnish when the board is deleted. |
1571 | * We do permission checks for this scenario, but since the topic isn't deleted |
1572 | * at the core level, we need to evict it from Varnish ourselves. |
1573 | * |
1574 | * @param WikiPage $article Deleted article |
1575 | * @param User $user User that deleted article |
1576 | * @param string $reason Reason given |
1577 | * @param int $articleId Article ID of deleted article |
1578 | * @param Content|null $content Content that was deleted, or null on error |
1579 | * @param LogEntry $logEntry Log entry for deletion |
1580 | * @param int $archivedRevisionCount |
1581 | */ |
1582 | public function onArticleDeleteComplete( |
1583 | $article, |
1584 | $user, |
1585 | $reason, |
1586 | $articleId, |
1587 | $content, |
1588 | $logEntry, |
1589 | $archivedRevisionCount |
1590 | ) { |
1591 | $title = $article->getTitle(); |
1592 | |
1593 | // Topics use the same content model, but can't be deleted at the core |
1594 | // level currently. |
1595 | if ( $content !== null && |
1596 | $title->getNamespace() !== NS_TOPIC && |
1597 | $title->getContentModel() === CONTENT_MODEL_FLOW_BOARD ) { |
1598 | $storage = Container::get( 'storage' ); |
1599 | |
1600 | DeferredUpdates::addCallableUpdate( static function () use ( $storage, $articleId ) { |
1601 | /** @var Model\Workflow[] $workflows */ |
1602 | $workflows = $storage->find( 'Workflow', [ |
1603 | 'workflow_wiki' => WikiMap::getCurrentWikiId(), |
1604 | 'workflow_page_id' => $articleId, |
1605 | ] ); |
1606 | if ( !$workflows ) { |
1607 | return; |
1608 | } |
1609 | |
1610 | $topicTitles = []; |
1611 | foreach ( $workflows as $workflow ) { |
1612 | if ( $workflow->getType() === 'topic' ) { |
1613 | $topicTitles[] = $workflow->getArticleTitle(); |
1614 | } |
1615 | } |
1616 | |
1617 | $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater(); |
1618 | $hcu->purgeTitleUrls( $topicTitles, $hcu::PURGE_INTENT_TXROUND_REFLECTED ); |
1619 | } ); |
1620 | } |
1621 | } |
1622 | |
1623 | /** |
1624 | * @param RevisionRecord $revisionRecord Revision just undeleted |
1625 | * @param ?int $oldPageId Old page ID stored with that revision when it was in the archive table |
1626 | */ |
1627 | public function onRevisionUndeleted( $revisionRecord, $oldPageId ) { |
1628 | $contentModel = $revisionRecord->getSlot( |
1629 | SlotRecord::MAIN, RevisionRecord::RAW |
1630 | )->getModel(); |
1631 | if ( $contentModel === CONTENT_MODEL_FLOW_BOARD ) { |
1632 | // complete hack to make sure that when the page is saved to new |
1633 | // location and rendered it doesn't throw an error about the wrong title |
1634 | Container::get( 'factory.loader.workflow' )->pageMoveInProgress(); |
1635 | |
1636 | $title = Title::newFromLinkTarget( $revisionRecord->getPageAsLinkTarget() ); |
1637 | |
1638 | // Reassociate the Flow board associated with this undeleted revision. |
1639 | $boardMover = Container::get( 'board_mover' ); |
1640 | $boardMover->move( intval( $oldPageId ), $title ); |
1641 | } |
1642 | } |
1643 | |
1644 | /** |
1645 | * @param Title $title Title corresponding to the article restored |
1646 | * @param bool $create Whether or not the restoration caused the page to be created (i.e. it didn't exist before). |
1647 | * @param string $comment The comment associated with the undeletion. |
1648 | * @param int $oldPageId ID of page previously deleted (from archive table) |
1649 | * @param array $restoredPages |
1650 | */ |
1651 | public function onArticleUndelete( $title, $create, $comment, $oldPageId, $restoredPages ) { |
1652 | // Avoid CI errors when other ArticleUndelete implementations are present, see: T356704 |
1653 | if ( defined( 'MW_PHPUNIT_TEST' ) ) { |
1654 | return; |
1655 | } |
1656 | $boardMover = Container::get( 'board_mover' ); |
1657 | $boardMover->commit(); |
1658 | } |
1659 | |
1660 | /** |
1661 | * Occurs at the beginning of the MovePage process (just after the startAtomic). |
1662 | * |
1663 | * Perhaps ContentModel should be extended to be notified about moves explicitly. |
1664 | * @param Title $oldTitle |
1665 | * @param Title $newTitle |
1666 | * @param User $user |
1667 | */ |
1668 | public function onTitleMoveStarting( $oldTitle, $newTitle, $user ) { |
1669 | if ( $oldTitle->getContentModel() === CONTENT_MODEL_FLOW_BOARD ) { |
1670 | // $newTitle doesn't yet exist, but after the move it'll still have |
1671 | // the same ID $oldTitle used to have |
1672 | // Since we don't want to wait until after the page has been moved |
1673 | // to start preparing relevant Flow moves, I'll make it reflect the |
1674 | // correct ID already |
1675 | $bogusTitle = clone $newTitle; |
1676 | $bogusTitle->resetArticleID( $oldTitle->getArticleID() ); |
1677 | |
1678 | // This is only safe because we have called |
1679 | // checkIfCreationIsPossible and (usually) checkIfUserHasPermission. |
1680 | Container::get( 'occupation_controller' )->forceAllowCreation( $bogusTitle ); |
1681 | // complete hack to make sure that when the page is saved to new |
1682 | // location and rendered it doesn't throw an error about the wrong title |
1683 | Container::get( 'factory.loader.workflow' )->pageMoveInProgress(); |
1684 | // open a database transaction and prepare everything for the move, but |
1685 | // don't commit yet. That is done below in self::onPageMoveCompleting |
1686 | $boardMover = Container::get( 'board_mover' ); |
1687 | $boardMover->move( $oldTitle->getArticleID(), $bogusTitle ); |
1688 | } |
1689 | } |
1690 | |
1691 | public function onPageMoveCompleting( |
1692 | $oldTitle, |
1693 | $newTitle, |
1694 | $user, |
1695 | $pageid, |
1696 | $redirid, |
1697 | $reason, |
1698 | $revisionRecord |
1699 | ) { |
1700 | $newTitle = Title::newFromLinkTarget( $newTitle ); |
1701 | if ( $newTitle->getContentModel() === CONTENT_MODEL_FLOW_BOARD ) { |
1702 | Container::get( 'board_mover' )->commit(); |
1703 | } |
1704 | } |
1705 | |
1706 | public function onShowMissingArticle( $article ) { |
1707 | if ( $article->getPage()->getContentModel() !== CONTENT_MODEL_FLOW_BOARD ) { |
1708 | return true; |
1709 | } |
1710 | |
1711 | if ( $article->getTitle()->getNamespace() === NS_TOPIC ) { |
1712 | // @todo pretty message about invalid workflow |
1713 | throw new FlowException( 'Non-existent topic' ); |
1714 | } |
1715 | |
1716 | $services = MediaWikiServices::getInstance(); |
1717 | $emptyContent = $services->getContentHandlerFactory() |
1718 | ->getContentHandler( CONTENT_MODEL_FLOW_BOARD )->makeEmptyContent(); |
1719 | $contentRenderer = $services->getContentRenderer(); |
1720 | $parserOutput = $contentRenderer->getParserOutput( $emptyContent, $article->getTitle() ); |
1721 | $article->getContext()->getOutput()->addParserOutput( $parserOutput ); |
1722 | |
1723 | return false; |
1724 | } |
1725 | |
1726 | /** |
1727 | * Excludes NS_TOPIC from the list of searchable namespaces |
1728 | * |
1729 | * @param array &$namespaces Associative array mapping namespace index |
1730 | * to name |
1731 | */ |
1732 | public function onSearchableNamespaces( &$namespaces ) { |
1733 | unset( $namespaces[NS_TOPIC] ); |
1734 | } |
1735 | |
1736 | /** |
1737 | * @return bool |
1738 | */ |
1739 | private static function isBetaFeatureAvailable() { |
1740 | if ( !ExtensionRegistry::getInstance()->isLoaded( 'BetaFeatures' ) ) { |
1741 | return false; |
1742 | } |
1743 | |
1744 | $config = RequestContext::getMain()->getConfig(); |
1745 | $betaFeaturesAllowList = $config->get( 'BetaFeaturesAllowList' ); |
1746 | |
1747 | return $config->get( 'FlowEnableOptInBetaFeature' ) |
1748 | && ( |
1749 | !is_array( $betaFeaturesAllowList ) |
1750 | || in_array( BETA_FEATURE_FLOW_USER_TALK_PAGE, $betaFeaturesAllowList ) |
1751 | ); |
1752 | } |
1753 | |
1754 | /** |
1755 | * @param User $user |
1756 | * @param array &$prefs |
1757 | */ |
1758 | public function onGetBetaFeaturePreferences( $user, &$prefs ) { |
1759 | global $wgExtensionAssetsPath; |
1760 | |
1761 | if ( !self::isBetaFeatureAvailable() ) { |
1762 | return; |
1763 | } |
1764 | // Do not allow users to opt-in for Flow as preliminary sunset step |
1765 | if ( !BetaFeatures::isFeatureEnabled( $user, BETA_FEATURE_FLOW_USER_TALK_PAGE ) ) { |
1766 | return; |
1767 | } |
1768 | |
1769 | $prefs[BETA_FEATURE_FLOW_USER_TALK_PAGE] = [ |
1770 | // The first two are message keys |
1771 | 'label-message' => 'flow-talk-page-beta-feature-message', |
1772 | 'desc-message' => 'flow-talk-page-beta-feature-description', |
1773 | 'screenshot' => [ |
1774 | 'ltr' => "$wgExtensionAssetsPath/Flow/images/betafeature-flow-ltr.svg", |
1775 | 'rtl' => "$wgExtensionAssetsPath/Flow/images/betafeature-flow-rtl.svg", |
1776 | ], |
1777 | 'info-link' => 'https://www.mediawiki.org/wiki/Flow', |
1778 | 'discussion-link' => 'https://www.mediawiki.org/wiki/Talk:Flow', |
1779 | 'exempt-from-auto-enrollment' => true, |
1780 | ]; |
1781 | } |
1782 | |
1783 | /** |
1784 | * @param UserIdentity $user |
1785 | * @param array &$modifiedOptions |
1786 | * @param array $originalOptions |
1787 | */ |
1788 | public function onSaveUserOptions( UserIdentity $user, array &$modifiedOptions, array $originalOptions ) { |
1789 | if ( !self::isBetaFeatureAvailable() ) { |
1790 | return; |
1791 | } |
1792 | |
1793 | // Short circuit, it's fine because this beta feature is exempted from auto-enroll. |
1794 | if ( !array_key_exists( BETA_FEATURE_FLOW_USER_TALK_PAGE, $modifiedOptions ) ) { |
1795 | return; |
1796 | } |
1797 | |
1798 | $before = BetaFeatures::isFeatureEnabled( $user, BETA_FEATURE_FLOW_USER_TALK_PAGE, $originalOptions ); |
1799 | $after = BetaFeatures::isFeatureEnabled( $user, BETA_FEATURE_FLOW_USER_TALK_PAGE ); |
1800 | $action = null; |
1801 | |
1802 | $optInController = Container::get( 'controller.opt_in' ); |
1803 | $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $user ); |
1804 | if ( !$before && $after ) { |
1805 | $action = OptInController::ENABLE; |
1806 | // Check if the user had a flow board |
1807 | if ( !$optInController->hasFlowBoardArchive( $user ) ) { |
1808 | // Enable the guided tour by setting the cookie |
1809 | RequestContext::getMain()->getRequest()->response()->setCookie( 'Flow_optIn_guidedTour', '1' ); |
1810 | } |
1811 | } elseif ( $before && !$after ) { |
1812 | $action = OptInController::DISABLE; |
1813 | } |
1814 | |
1815 | if ( $action ) { |
1816 | $optInController->initiateChange( $action, $user->getTalkPage(), $user ); |
1817 | } |
1818 | } |
1819 | |
1820 | /** |
1821 | * @param WikiImporter $importer |
1822 | * @return bool |
1823 | */ |
1824 | public function onImportHandleToplevelXMLTag( $importer ) { |
1825 | // only init Flow's importer once, then re-use it |
1826 | static $flowImporter = null; |
1827 | if ( $flowImporter === null ) { |
1828 | // importer can be dry-run (= parse, but don't store), but we can only |
1829 | // derive that from mPageOutCallback. I'll set a new value (which will |
1830 | // return the existing value) to see if it's in dry-run mode (= null) |
1831 | $callback = $importer->setPageOutCallback( null ); |
1832 | // restore previous mPageOutCallback value |
1833 | $importer->setPageOutCallback( $callback ); |
1834 | |
1835 | $flowImporter = new Dump\Importer( $importer ); |
1836 | if ( $callback !== null ) { |
1837 | // not in dry-run mode |
1838 | $flowImporter->setStorage( Container::get( 'storage' ) ); |
1839 | } |
1840 | } |
1841 | |
1842 | $reader = $importer->getReader(); |
1843 | $tag = $reader->localName; |
1844 | $type = $reader->nodeType; |
1845 | |
1846 | if ( $tag == 'board' ) { |
1847 | if ( $type === XMLReader::ELEMENT ) { |
1848 | $flowImporter->handleBoard(); |
1849 | } |
1850 | return false; |
1851 | } elseif ( $tag == 'description' ) { |
1852 | if ( $type === XMLReader::ELEMENT ) { |
1853 | $flowImporter->handleHeader(); |
1854 | } |
1855 | return false; |
1856 | } elseif ( $tag == 'topic' ) { |
1857 | if ( $type === XMLReader::ELEMENT ) { |
1858 | $flowImporter->handleTopic(); |
1859 | } |
1860 | return false; |
1861 | } elseif ( $tag == 'post' ) { |
1862 | if ( $type === XMLReader::ELEMENT ) { |
1863 | $flowImporter->handlePost(); |
1864 | } |
1865 | return false; |
1866 | } elseif ( $tag == 'summary' ) { |
1867 | if ( $type === XMLReader::ELEMENT ) { |
1868 | $flowImporter->handleSummary(); |
1869 | } |
1870 | return false; |
1871 | } elseif ( $tag == 'children' ) { |
1872 | return false; |
1873 | } |
1874 | |
1875 | return true; |
1876 | } |
1877 | |
1878 | public static function onNukeGetNewPages( $username, $pattern, $namespace, $limit, &$pages ) { |
1879 | if ( $namespace && $namespace !== NS_TOPIC ) { |
1880 | // not interested in any Topics |
1881 | return true; |
1882 | } |
1883 | |
1884 | // Remove any pre-existing Topic pages. |
1885 | // They are coming from the recentchanges table. |
1886 | // Most likely the filters were not applied correctly. |
1887 | $pages = array_filter( $pages, static function ( $entry ) { |
1888 | /** @var Title $title */ |
1889 | $title = $entry[0]; |
1890 | return $title->getNamespace() !== NS_TOPIC; |
1891 | } ); |
1892 | |
1893 | if ( $pattern ) { |
1894 | // pattern is not supported |
1895 | return true; |
1896 | } |
1897 | |
1898 | if ( !MediaWikiServices::getInstance()->getPermissionManager() |
1899 | ->userHasRight( RequestContext::getMain()->getUser(), 'flow-delete' ) |
1900 | ) { |
1901 | // there's no point adding topics since the current user won't be allowed to delete them |
1902 | return true; |
1903 | } |
1904 | |
1905 | // how many are we allowed to retrieve now |
1906 | $newLimit = $limit - count( $pages ); |
1907 | |
1908 | // we can't add anything |
1909 | if ( $newLimit < 1 ) { |
1910 | return true; |
1911 | } |
1912 | |
1913 | /** @var DbFactory $dbFactory */ |
1914 | $dbFactory = Container::get( 'db.factory' ); |
1915 | $dbr = $dbFactory->getDB( DB_REPLICA ); |
1916 | |
1917 | // if a username is specified, search only for that user |
1918 | $userWhere = []; |
1919 | if ( $username ) { |
1920 | $user = User::newFromName( $username ); |
1921 | if ( $user && $user->isRegistered() ) { |
1922 | $userWhere = [ 'tree_orig_user_id' => $user->getId() ]; |
1923 | } else { |
1924 | $userWhere = [ 'tree_orig_user_ip' => $username ]; |
1925 | } |
1926 | } |
1927 | |
1928 | // limit results to the range of RC |
1929 | global $wgRCMaxAge; |
1930 | $rcTimeLimit = UUID::getComparisonUUID( strtotime( "-$wgRCMaxAge seconds" ) ); |
1931 | |
1932 | // get latest revision id for each topic |
1933 | $result = $dbr->newSelectQueryBuilder() |
1934 | ->select( [ |
1935 | 'revId' => 'MAX(r.rev_id)', |
1936 | 'userIp' => "tree_orig_user_ip", |
1937 | 'userId' => "tree_orig_user_id", |
1938 | ] ) |
1939 | ->from( 'flow_revision', 'r' ) |
1940 | ->join( 'flow_tree_revision', null, 'r.rev_type_id=tree_rev_descendant_id' ) |
1941 | ->join( 'flow_workflow', null, 'r.rev_type_id=workflow_id' ) |
1942 | ->where( [ |
1943 | 'tree_parent_id' => null, |
1944 | 'r.rev_type' => 'post', |
1945 | 'workflow_wiki' => WikiMap::getCurrentWikiId(), |
1946 | $dbr->expr( 'workflow_id', '>', $rcTimeLimit->getBinary() ) |
1947 | ] ) |
1948 | ->andWhere( $userWhere ) |
1949 | ->groupBy( 'r.rev_type_id' ) |
1950 | ->caller( __METHOD__ ) |
1951 | ->fetchResultSet(); |
1952 | |
1953 | if ( $result->numRows() < 1 ) { |
1954 | return true; |
1955 | } |
1956 | |
1957 | $revIds = []; |
1958 | foreach ( $result as $r ) { |
1959 | $revIds[$r->revId] = [ 'userIp' => $r->userIp, 'userId' => $r->userId, 'name' => false ]; |
1960 | } |
1961 | |
1962 | // get non-moderated revisions (but include hidden ones for T180607) |
1963 | $result = $dbr->newSelectQueryBuilder() |
1964 | ->select( [ |
1965 | 'topicId' => 'rev_type_id', |
1966 | 'revId' => 'rev_id' |
1967 | ] ) |
1968 | ->from( 'flow_revision' ) |
1969 | ->where( [ |
1970 | 'rev_mod_state' => [ '', 'hide' ], |
1971 | 'rev_id' => array_keys( $revIds ) |
1972 | ] ) |
1973 | ->limit( $newLimit ) |
1974 | ->orderBy( 'rev_type_id', SelectQueryBuilder::SORT_DESC ) |
1975 | ->caller( __METHOD__ ) |
1976 | ->fetchResultSet(); |
1977 | |
1978 | // all topics previously found appear to be moderated |
1979 | if ( $result->numRows() < 1 ) { |
1980 | return true; |
1981 | } |
1982 | |
1983 | // keep only the relevant topics in [topicId => userInfo] format |
1984 | $limitedRevIds = []; |
1985 | foreach ( $result as $r ) { |
1986 | $limitedRevIds[$r->topicId] = $revIds[$r->revId]; |
1987 | } |
1988 | |
1989 | // fill usernames if no $username filter was specified |
1990 | if ( !$username ) { |
1991 | $userIds = array_column( array_values( $limitedRevIds ), 'userId' ); |
1992 | $userIds = array_filter( $userIds ); |
1993 | |
1994 | $userMap = []; |
1995 | if ( $userIds ) { |
1996 | $wikiDbr = $dbFactory->getWikiDB( DB_REPLICA ); |
1997 | $result = $wikiDbr->newSelectQueryBuilder() |
1998 | ->select( [ 'user_id', 'user_name' ] ) |
1999 | ->from( 'user' ) |
2000 | ->where( [ 'user_id' => array_values( $userIds ) ] ) |
2001 | ->caller( __METHOD__ ) |
2002 | ->fetchResultSet(); |
2003 | foreach ( $result as $r ) { |
2004 | $userMap[$r->user_id] = $r->user_name; |
2005 | } |
2006 | } |
2007 | |
2008 | // set name in userInfo structure |
2009 | foreach ( $limitedRevIds as $topicId => &$userInfo ) { |
2010 | if ( $userInfo['userIp'] ) { |
2011 | $userInfo['name'] = $userInfo['userIp']; |
2012 | } elseif ( $userInfo['userId'] ) { |
2013 | $userInfo['name'] = $userMap[$userInfo['userId']]; |
2014 | } else { |
2015 | $userInfo['name'] = false; |
2016 | $topicIdAlpha = UUID::create( $topicId )->getAlphadecimal(); |
2017 | wfLogWarning( __METHOD__ . ": Cannot find user information for topic {$topicIdAlpha}" ); |
2018 | } |
2019 | } |
2020 | } |
2021 | |
2022 | // add results to the list of pages to nuke |
2023 | foreach ( $limitedRevIds as $topicId => $userInfo ) { |
2024 | $pages[] = [ |
2025 | Title::makeTitle( NS_TOPIC, UUID::create( $topicId )->getAlphadecimal() ), |
2026 | $userInfo['name'] |
2027 | ]; |
2028 | } |
2029 | |
2030 | return true; |
2031 | } |
2032 | |
2033 | public static function onNukeDeletePage( Title $title, $reason, &$deletionResult ) { |
2034 | if ( $title->getNamespace() !== NS_TOPIC ) { |
2035 | // we don't handle it |
2036 | return true; |
2037 | } |
2038 | |
2039 | $action = 'moderate-topic'; |
2040 | $params = [ |
2041 | 'topic' => [ |
2042 | 'moderationState' => 'delete', |
2043 | 'reason' => $reason, |
2044 | 'page' => $title->getPrefixedText() |
2045 | ], |
2046 | ]; |
2047 | |
2048 | /** @var WorkflowLoaderFactory $factory */ |
2049 | $factory = Container::get( 'factory.loader.workflow' ); |
2050 | |
2051 | $workflowId = WorkflowLoaderFactory::uuidFromTitle( $title ); |
2052 | /** @var WorkflowLoader $loader */ |
2053 | $loader = $factory->createWorkflowLoader( $title, $workflowId ); |
2054 | |
2055 | $blocks = $loader->getBlocks(); |
2056 | |
2057 | $blocksToCommit = $loader->handleSubmit( |
2058 | RequestContext::getMain(), |
2059 | $action, |
2060 | $params |
2061 | ); |
2062 | |
2063 | $result = true; |
2064 | $errors = []; |
2065 | foreach ( $blocks as $block ) { |
2066 | if ( $block->hasErrors() ) { |
2067 | $result = false; |
2068 | $errorKeys = $block->getErrors(); |
2069 | foreach ( $errorKeys as $errorKey ) { |
2070 | $errors[] = $block->getErrorMessage( $errorKey ); |
2071 | } |
2072 | } |
2073 | } |
2074 | |
2075 | if ( $result ) { |
2076 | $loader->commit( $blocksToCommit ); |
2077 | $deletionResult = true; |
2078 | } else { |
2079 | $deletionResult = false; |
2080 | $msg = "Failed to delete {$title->getPrefixedText()}. Errors: " . implode( '. ', $errors ); |
2081 | wfLogWarning( $msg ); |
2082 | } |
2083 | |
2084 | // we've handled the deletion, abort the hook |
2085 | return false; |
2086 | } |
2087 | |
2088 | /** |
2089 | * Filter out all Flow changes when hidepageedits=1 |
2090 | * |
2091 | * @param string $name |
2092 | * @param array &$tables |
2093 | * @param array &$fields |
2094 | * @param array &$conds |
2095 | * @param array &$query_options |
2096 | * @param array &$join_conds |
2097 | * @param FormOptions $opts |
2098 | */ |
2099 | public function onChangesListSpecialPageQuery( |
2100 | $name, &$tables, &$fields, &$conds, |
2101 | &$query_options, &$join_conds, $opts |
2102 | ) { |
2103 | try { |
2104 | $hidePageEdits = $opts->getValue( 'hidepageedits' ); |
2105 | } catch ( MWException $e ) { |
2106 | // If not set, assume they should be hidden. |
2107 | $hidePageEdits = true; |
2108 | } |
2109 | if ( $hidePageEdits ) { |
2110 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
2111 | $conds[] = $dbr->expr( 'rc_type', '!=', RC_FLOW ); |
2112 | } |
2113 | } |
2114 | |
2115 | /** |
2116 | * @inheritDoc |
2117 | * @return false|void |
2118 | */ |
2119 | public function onGetUserPermissionsErrors( $title, $user, $action, &$result ) { |
2120 | global $wgFlowReadOnly; |
2121 | |
2122 | $tempUserConfig = MediaWikiServices::getInstance()->getTempUserConfig(); |
2123 | // Flow has no support for temp accounts. If temp accounts are |
2124 | // known on the wiki, don't let anonymous users edit, and |
2125 | // don't let temporary users edit either. |
2126 | if ( |
2127 | $tempUserConfig->isKnown() && !$user->isNamed() && |
2128 | $title->getContentModel() === CONTENT_MODEL_FLOW_BOARD && |
2129 | $action !== 'read' |
2130 | ) { |
2131 | $result = 'flow-error-protected-readonly'; |
2132 | return false; |
2133 | } |
2134 | |
2135 | if ( !$wgFlowReadOnly ) { |
2136 | return; |
2137 | } |
2138 | |
2139 | // Deny all actions related to Flow pages, and deny all flow-create-board actions, |
2140 | // but allow read and delete/undelete |
2141 | $allowedActions = [ 'read', 'delete', 'undelete' ]; |
2142 | if ( |
2143 | $action === 'flow-create-board' || |
2144 | ( |
2145 | $title->getContentModel() === CONTENT_MODEL_FLOW_BOARD && |
2146 | !in_array( $action, $allowedActions ) |
2147 | ) |
2148 | ) { |
2149 | $result = 'flow-error-protected-readonly'; |
2150 | return false; |
2151 | } |
2152 | } |
2153 | |
2154 | /** |
2155 | * Return information about terms-of-use messages. |
2156 | * |
2157 | * @param MessageLocalizer $context |
2158 | * @param Config $config |
2159 | * @return array Map from internal name to array of parameters for MessageLocalizer::msg() |
2160 | * @phan-return non-empty-array[] |
2161 | */ |
2162 | private static function getTermsOfUseMessages( |
2163 | MessageLocalizer $context, Config $config |
2164 | ): array { |
2165 | $messages = [ |
2166 | 'new-topic' => [ 'flow-terms-of-use-new-topic' ], |
2167 | 'reply' => [ 'flow-terms-of-use-reply' ], |
2168 | 'edit' => [ 'flow-terms-of-use-edit' ], |
2169 | 'summarize' => [ 'flow-terms-of-use-summarize' ], |
2170 | 'lock-topic' => [ 'flow-terms-of-use-lock-topic' ], |
2171 | 'unlock-topic' => [ 'flow-terms-of-use-unlock-topic' ], |
2172 | ]; |
2173 | |
2174 | $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ); |
2175 | $hookRunner->onFlowTermsOfUseMessages( $messages, $context, $config ); |
2176 | |
2177 | return $messages; |
2178 | } |
2179 | |
2180 | /** |
2181 | * Return parsed terms-of-use messages, for use in a ResourceLoader module. |
2182 | * |
2183 | * @param MessageLocalizer $context |
2184 | * @param Config $config |
2185 | * @return array |
2186 | */ |
2187 | public static function getTermsOfUseMessagesParsed( |
2188 | MessageLocalizer $context, Config $config |
2189 | ): array { |
2190 | $messages = self::getTermsOfUseMessages( $context, $config ); |
2191 | foreach ( $messages as &$msg ) { |
2192 | $msg = $context->msg( ...$msg )->parse(); |
2193 | } |
2194 | return $messages; |
2195 | } |
2196 | |
2197 | /** |
2198 | * Return information about terms-of-use messages, for use in a ResourceLoader module as |
2199 | * 'versionCallback'. This is to avoid calling the parser from version invalidation code. |
2200 | * |
2201 | * @param MessageLocalizer $context |
2202 | * @param Config $config |
2203 | * @return array |
2204 | */ |
2205 | public static function getTermsOfUseMessagesVersion( |
2206 | MessageLocalizer $context, Config $config |
2207 | ): array { |
2208 | $messages = self::getTermsOfUseMessages( $context, $config ); |
2209 | foreach ( $messages as &$msg ) { |
2210 | $message = $context->msg( ...$msg ); |
2211 | $msg = [ |
2212 | // Include the text of the message, in case the canonical translation changes |
2213 | $message->plain(), |
2214 | // Include the page touched time, in case the on-wiki override is invalidated |
2215 | Title::makeTitle( NS_MEDIAWIKI, ucfirst( $message->getKey() ) )->getTouched(), |
2216 | ]; |
2217 | } |
2218 | return $messages; |
2219 | } |
2220 | } |