Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
70.67% covered (warning)
70.67%
53 / 75
77.78% covered (warning)
77.78%
7 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
TwoColConflictHooks
70.67% covered (warning)
70.67%
53 / 75
77.78% covered (warning)
77.78%
7 / 9
50.23
0.00% covered (danger)
0.00%
0 / 1
 newFromGlobalState
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onAlternateEdit
34.38% covered (danger)
34.38%
11 / 32
0.00% covered (danger)
0.00%
0 / 1
5.54
 onEditPage__showEditForm_initial
n/a
0 / 0
n/a
0 / 0
2
 onEditPage__showEditForm_fields
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 onEditPageBeforeConflictDiff
n/a
0 / 0
n/a
0 / 0
8
 onEditPageBeforeEditButtons
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 onGetBetaFeaturePreferences
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doGetBetaFeaturePreferences
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 onGetPreferences
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 onLoadUserOptions
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
1<?php
2
3// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
4
5namespace TwoColConflict\Hooks;
6
7use MediaWiki\EditPage\EditPage;
8use MediaWiki\Extension\EventLogging\EventLogging;
9use MediaWiki\Hook\AlternateEditHook;
10use MediaWiki\Hook\EditPage__showEditForm_fieldsHook;
11use MediaWiki\Hook\EditPage__showEditForm_initialHook;
12use MediaWiki\Hook\EditPageBeforeConflictDiffHook;
13use MediaWiki\Hook\EditPageBeforeEditButtonsHook;
14use MediaWiki\MainConfigNames;
15use MediaWiki\MediaWikiServices;
16use MediaWiki\Output\OutputPage;
17use MediaWiki\Preferences\Hook\GetPreferencesHook;
18use MediaWiki\Registration\ExtensionRegistry;
19use MediaWiki\User\Options\Hook\LoadUserOptionsHook;
20use MediaWiki\User\User;
21use MediaWiki\User\UserIdentity;
22use OOUI\ButtonInputWidget;
23use TwoColConflict\ConflictFormValidator;
24use TwoColConflict\Html\CoreUiHintHtml;
25use TwoColConflict\SplitTwoColConflictHelper;
26use TwoColConflict\TalkPageConflict\ResolutionSuggester;
27use TwoColConflict\TwoColConflictContext;
28
29/**
30 * Hook handlers for the TwoColConflict extension.
31 *
32 * @license GPL-2.0-or-later
33 */
34class 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}