Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
70.67% |
53 / 75 |
|
77.78% |
7 / 9 |
CRAP | |
0.00% |
0 / 1 |
TwoColConflictHooks | |
70.67% |
53 / 75 |
|
77.78% |
7 / 9 |
50.23 | |
0.00% |
0 / 1 |
newFromGlobalState | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onAlternateEdit | |
34.38% |
11 / 32 |
|
0.00% |
0 / 1 |
5.54 | |||
onEditPage__showEditForm_initial | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
onEditPage__showEditForm_fields | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
onEditPageBeforeConflictDiff | n/a |
0 / 0 |
n/a |
0 / 0 |
8 | |||||
onEditPageBeforeEditButtons | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
onGetBetaFeaturePreferences | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doGetBetaFeaturePreferences | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
onGetPreferences | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
onLoadUserOptions | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 |
1 | <?php |
2 | |
3 | // phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName |
4 | |
5 | namespace TwoColConflict\Hooks; |
6 | |
7 | use MediaWiki\EditPage\EditPage; |
8 | use MediaWiki\Extension\EventLogging\EventLogging; |
9 | use MediaWiki\Hook\AlternateEditHook; |
10 | use MediaWiki\Hook\EditPage__showEditForm_fieldsHook; |
11 | use MediaWiki\Hook\EditPage__showEditForm_initialHook; |
12 | use MediaWiki\Hook\EditPageBeforeConflictDiffHook; |
13 | use MediaWiki\Hook\EditPageBeforeEditButtonsHook; |
14 | use MediaWiki\MainConfigNames; |
15 | use MediaWiki\MediaWikiServices; |
16 | use MediaWiki\Output\OutputPage; |
17 | use MediaWiki\Preferences\Hook\GetPreferencesHook; |
18 | use MediaWiki\Registration\ExtensionRegistry; |
19 | use MediaWiki\User\Options\Hook\LoadUserOptionsHook; |
20 | use MediaWiki\User\User; |
21 | use MediaWiki\User\UserIdentity; |
22 | use OOUI\ButtonInputWidget; |
23 | use TwoColConflict\ConflictFormValidator; |
24 | use TwoColConflict\Html\CoreUiHintHtml; |
25 | use TwoColConflict\SplitTwoColConflictHelper; |
26 | use TwoColConflict\TalkPageConflict\ResolutionSuggester; |
27 | use TwoColConflict\TwoColConflictContext; |
28 | |
29 | /** |
30 | * Hook handlers for the TwoColConflict extension. |
31 | * |
32 | * @license GPL-2.0-or-later |
33 | */ |
34 | class TwoColConflictHooks implements |
35 | GetPreferencesHook, |
36 | LoadUserOptionsHook, |
37 | AlternateEditHook, |
38 | EditPageBeforeConflictDiffHook, |
39 | EditPageBeforeEditButtonsHook, |
40 | EditPage__showEditForm_initialHook, |
41 | EditPage__showEditForm_fieldsHook |
42 | { |
43 | |
44 | private TwoColConflictContext $twoColContext; |
45 | |
46 | private static function newFromGlobalState(): self { |
47 | return new self( MediaWikiServices::getInstance()->getService( 'TwoColConflictContext' ) ); |
48 | } |
49 | |
50 | public function __construct( TwoColConflictContext $twoColContext ) { |
51 | $this->twoColContext = $twoColContext; |
52 | } |
53 | |
54 | /** |
55 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/AlternateEdit |
56 | * |
57 | * @param EditPage $editPage |
58 | */ |
59 | public function onAlternateEdit( $editPage ) { |
60 | $context = $editPage->getContext(); |
61 | |
62 | // Skip out if the feature is disabled |
63 | if ( !$this->twoColContext->shouldTwoColConflictBeShown( |
64 | $context->getUser(), |
65 | $context->getTitle() |
66 | ) ) { |
67 | return; |
68 | } |
69 | |
70 | $editPage->setEditConflictHelperFactory( function ( $submitButtonLabel ) use ( $editPage ) { |
71 | $services = MediaWikiServices::getInstance(); |
72 | $context = $editPage->getContext(); |
73 | $baseRevision = $editPage->getExpectedParentRevision(); |
74 | $title = $context->getTitle(); |
75 | $wikiPage = $services->getWikiPageFactory()->newFromTitle( $title ); |
76 | |
77 | return new SplitTwoColConflictHelper( |
78 | $title, |
79 | $context->getOutput(), |
80 | $services->getStatsFactory(), |
81 | $submitButtonLabel, |
82 | $services->getContentHandlerFactory(), |
83 | $this->twoColContext, |
84 | new ResolutionSuggester( |
85 | $baseRevision, |
86 | $wikiPage->getContentHandler()->getDefaultFormat() |
87 | ), |
88 | $services->getCommentFormatter(), |
89 | $services->getMainObjectStash(), |
90 | $editPage->summary, |
91 | $services->getUserOptionsLookup()->getOption( $context->getUser(), 'editfont' ) |
92 | ); |
93 | } ); |
94 | |
95 | $request = $context->getRequest(); |
96 | if ( !( new ConflictFormValidator() )->validateRequest( $request ) ) { |
97 | // Mark the conflict as *not* being resolved to trigger it again. This works because |
98 | // EditPage uses editRevId to decide if it's even possible to run into a conflict. |
99 | // If editRevId reflects the most recent revision, it can't be a conflict (again), |
100 | // and the user's input is stored, even if it reverts everything. |
101 | // Warning, this is particularly fragile! This assumes EditPage was not reading the |
102 | // WebRequest values before! |
103 | $request->setVal( 'editRevId', $request->getInt( 'parentRevId' ) ); |
104 | } |
105 | } |
106 | |
107 | /** |
108 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/EditPage::showEditForm:initial |
109 | * @codeCoverageIgnore this is only for logging, not a user-facing feature |
110 | * |
111 | * @param EditPage $editPage |
112 | * @param OutputPage $outputPage |
113 | */ |
114 | public function onEditPage__showEditForm_initial( |
115 | $editPage, |
116 | $outputPage |
117 | ) { |
118 | // What the script does is only used for logging in doEditPageBeforeConflictDiff below |
119 | if ( ExtensionRegistry::getInstance()->isLoaded( 'EventLogging' ) ) { |
120 | $outputPage->addModules( 'ext.TwoColConflict.JSCheck' ); |
121 | } |
122 | } |
123 | |
124 | /** |
125 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/EditPage::showEditForm:fields |
126 | * |
127 | * @param EditPage $editPage |
128 | * @param OutputPage $outputPage |
129 | */ |
130 | public function onEditPage__showEditForm_fields( |
131 | $editPage, |
132 | $outputPage |
133 | ) { |
134 | // TODO remove this hint when we're sure people are aware of the new feature |
135 | if ( $editPage->isConflict && |
136 | $this->twoColContext->shouldCoreHintBeShown( $outputPage->getUser() ) |
137 | ) { |
138 | $outputPage->enableOOUI(); |
139 | $outputPage->addModuleStyles( 'ext.TwoColConflict.SplitCss' ); |
140 | $outputPage->addModules( 'ext.TwoColConflict.SplitJs' ); |
141 | $outputPage->addHTML( ( new CoreUiHintHtml( $outputPage->getContext() ) )->getHtml() ); |
142 | } |
143 | } |
144 | |
145 | /** |
146 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/EditPageBeforeConflictDiff |
147 | * @codeCoverageIgnore this is only for logging, not a user-facing feature |
148 | * |
149 | * @param EditPage $editPage |
150 | * @param OutputPage $outputPage |
151 | */ |
152 | public function onEditPageBeforeConflictDiff( |
153 | $editPage, |
154 | $outputPage |
155 | ) { |
156 | $context = $editPage->getContext(); |
157 | $title = $context->getTitle(); |
158 | $request = $context->getRequest(); |
159 | if ( $context->getConfig()->get( 'TwoColConflictTrackingOversample' ) ) { |
160 | $request->setVal( 'editingStatsOversample', true ); |
161 | } |
162 | |
163 | if ( ExtensionRegistry::getInstance()->isLoaded( 'EventLogging' ) ) { |
164 | $user = $outputPage->getUser(); |
165 | $baseRevision = $editPage->getExpectedParentRevision(); |
166 | $revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); |
167 | $latestRevision = $revisionStore->getKnownCurrentRevision( $title ); |
168 | |
169 | EventLogging::logEvent( |
170 | 'TwoColConflictConflict', |
171 | -1, |
172 | [ |
173 | 'twoColConflictShown' => $this->twoColContext->shouldTwoColConflictBeShown( |
174 | $user, |
175 | $context->getTitle() |
176 | ), |
177 | 'isAnon' => !$user->isNamed(), |
178 | 'editCount' => (int)$user->getEditCount(), |
179 | 'pageNs' => $context->getTitle()->getNamespace(), |
180 | 'baseRevisionId' => $baseRevision ? $baseRevision->getId() : 0, |
181 | 'latestRevisionId' => $latestRevision ? $latestRevision->getId() : 0, |
182 | // Previously we tried a 3-way-merge with the unsaved content and tracked some |
183 | // not so sensitive metrics here, but this was expensive and fragile |
184 | 'conflictChunks' => -1, |
185 | 'conflictChars' => -1, |
186 | 'startTime' => $editPage->starttime ?: '', |
187 | 'editTime' => $editPage->edittime ?: '', |
188 | 'pageTitle' => $context->getTitle()->getText(), |
189 | 'hasJavascript' => $request->getBool( 'mw-twocolconflict-js' ) |
190 | || $request->getBool( 'veswitched' ), |
191 | ] |
192 | ); |
193 | } |
194 | } |
195 | |
196 | /** |
197 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/EditPageBeforeEditButtons |
198 | * |
199 | * @param EditPage $editPage |
200 | * @param ButtonInputWidget[] &$buttons |
201 | * @param int &$tabindex |
202 | */ |
203 | public function onEditPageBeforeEditButtons( |
204 | $editPage, |
205 | &$buttons, |
206 | &$tabindex |
207 | ) { |
208 | $context = $editPage->getContext(); |
209 | if ( $this->twoColContext->shouldTwoColConflictBeShown( |
210 | $context->getUser(), |
211 | $context->getTitle() |
212 | ) && |
213 | $editPage->isConflict === true |
214 | ) { |
215 | unset( $buttons['diff'] ); |
216 | // T230152 |
217 | if ( isset( $buttons['preview'] ) ) { |
218 | $buttons['preview']->setDisabled( true ); |
219 | } |
220 | } |
221 | } |
222 | |
223 | /** |
224 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetBetaFeaturePreferences |
225 | * |
226 | * @param User $user |
227 | * @param array[] &$prefs |
228 | */ |
229 | public static function onGetBetaFeaturePreferences( $user, array &$prefs ) { |
230 | self::newFromGlobalState()->doGetBetaFeaturePreferences( $prefs ); |
231 | } |
232 | |
233 | /** |
234 | * @param array[] &$prefs |
235 | */ |
236 | private function doGetBetaFeaturePreferences( array &$prefs ): void { |
237 | if ( $this->twoColContext->isUsedAsBetaFeature() ) { |
238 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
239 | $path = $config->get( MainConfigNames::ExtensionAssetsPath ); |
240 | $prefs[TwoColConflictContext::BETA_PREFERENCE_NAME] = [ |
241 | 'label-message' => 'twocolconflict-beta-feature-message', |
242 | 'desc-message' => 'twocolconflict-beta-feature-description', |
243 | 'screenshot' => [ |
244 | 'ltr' => "$path/TwoColConflict/resources/TwoColConflict-beta-features-ltr.svg", |
245 | 'rtl' => "$path/TwoColConflict/resources/TwoColConflict-beta-features-rtl.svg", |
246 | ], |
247 | 'info-link' |
248 | => 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Two_Column_Edit_Conflict_View', |
249 | 'discussion-link' |
250 | => 'https://www.mediawiki.org/wiki/Help_talk:Two_Column_Edit_Conflict_View', |
251 | ]; |
252 | } |
253 | } |
254 | |
255 | /** |
256 | * @param User $user |
257 | * @param array[] &$preferences |
258 | */ |
259 | public function onGetPreferences( $user, &$preferences ) { |
260 | if ( $this->twoColContext->isUsedAsBetaFeature() ) { |
261 | return; |
262 | } |
263 | |
264 | $preferences[TwoColConflictContext::ENABLED_PREFERENCE] = [ |
265 | 'type' => 'toggle', |
266 | 'label-message' => 'twocolconflict-preference-enabled', |
267 | 'section' => 'editing/advancedediting', |
268 | ]; |
269 | } |
270 | |
271 | public function onLoadUserOptions( UserIdentity $user, array &$options ): void { |
272 | if ( $this->twoColContext->isUsedAsBetaFeature() ) { |
273 | return; |
274 | } |
275 | |
276 | // Drop obsolete option from the database. The original plan was to migrate the Beta opt-in |
277 | // to the later opt-out. This is not possible. Every user who changed some option will also |
278 | // have this option set. Impossible to know if the Beta feature was intentionally disabled. |
279 | unset( $options[TwoColConflictContext::BETA_PREFERENCE_NAME] ); |
280 | } |
281 | |
282 | } |