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