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