Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 14
930
0.00% covered (danger)
0.00%
0 / 1
 getInstance
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 onEditFilterMergedContent
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onPageSaveComplete
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 onEditPageBeforeEditButtons
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onEditPage__showEditForm_fields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onEmailUserForm
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onEmailUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onAPIGetAllowedParams
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onAuthChangeFormFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 confirmEditSetup
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 onTitleReadWhitelist
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 onFancyCaptchaSetup
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 onAlternateEditPreview
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
42
 onResourceLoaderRegisterModules
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
4
5namespace MediaWiki\Extension\ConfirmEdit;
6
7use ApiBase;
8use Content;
9use ExtensionRegistry;
10use HTMLForm;
11use IContextSource;
12use MailAddress;
13use MediaWiki\Api\Hook\APIGetAllowedParamsHook;
14use MediaWiki\EditPage\EditPage;
15use MediaWiki\Extension\ConfirmEdit\SimpleCaptcha\SimpleCaptcha;
16use MediaWiki\Hook\AlternateEditPreviewHook;
17use MediaWiki\Hook\EditFilterMergedContentHook;
18use MediaWiki\Hook\EditPage__showEditForm_fieldsHook;
19use MediaWiki\Hook\EditPageBeforeEditButtonsHook;
20use MediaWiki\Hook\EmailUserFormHook;
21use MediaWiki\Hook\EmailUserHook;
22use MediaWiki\Html\Html;
23use MediaWiki\MediaWikiServices;
24use MediaWiki\Output\OutputPage;
25use MediaWiki\Permissions\Hook\TitleReadWhitelistHook;
26use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook;
27use MediaWiki\ResourceLoader\ResourceLoader;
28use MediaWiki\Revision\RevisionRecord;
29use MediaWiki\SpecialPage\Hook\AuthChangeFormFieldsHook;
30use MediaWiki\SpecialPage\SpecialPage;
31use MediaWiki\Status\Status;
32use MediaWiki\Storage\EditResult;
33use MediaWiki\Storage\Hook\PageSaveCompleteHook;
34use MediaWiki\Title\Title;
35use MediaWiki\User\User;
36use MediaWiki\User\UserIdentity;
37use MessageSpecifier;
38use ParserOutput;
39use Wikimedia\IPUtils;
40use WikiPage;
41
42class Hooks implements
43    AlternateEditPreviewHook,
44    EditPageBeforeEditButtonsHook,
45    EmailUserFormHook,
46    EmailUserHook,
47    TitleReadWhitelistHook,
48    ResourceLoaderRegisterModulesHook,
49    PageSaveCompleteHook,
50    EditPage__showEditForm_fieldsHook,
51    EditFilterMergedContentHook,
52    APIGetAllowedParamsHook,
53    AuthChangeFormFieldsHook
54{
55
56    protected static $instanceCreated = false;
57
58    /**
59     * Get the global Captcha instance
60     *
61     * @return SimpleCaptcha
62     */
63    public static function getInstance() {
64        global $wgCaptcha, $wgCaptchaClass;
65
66        if ( !static::$instanceCreated ) {
67            static::$instanceCreated = true;
68            $class = $wgCaptchaClass ?: SimpleCaptcha::class;
69            $wgCaptcha = new $class;
70        }
71
72        return $wgCaptcha;
73    }
74
75    /**
76     * @param IContextSource $context
77     * @param Content $content
78     * @param Status $status
79     * @param string $summary
80     * @param User $user
81     * @param bool $minorEdit
82     * @return bool
83     */
84    public function onEditFilterMergedContent( IContextSource $context, Content $content, Status $status,
85        $summary, User $user, $minorEdit
86    ) {
87        return self::getInstance()->confirmEditMerged( $context, $content, $status, $summary,
88            $user, $minorEdit );
89    }
90
91    /**
92     * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageSaveComplete
93     *
94     * @param WikiPage $wikiPage
95     * @param UserIdentity $user
96     * @param string $summary
97     * @param int $flags
98     * @param RevisionRecord $revisionRecord
99     * @param EditResult $editResult
100     * @return bool|void
101     */
102    public function onPageSaveComplete(
103        $wikiPage,
104        $user,
105        $summary,
106        $flags,
107        $revisionRecord,
108        $editResult
109    ) {
110        $title = $wikiPage->getTitle();
111        if ( $title->getText() === 'Captcha-ip-whitelist' && $title->getNamespace() === NS_MEDIAWIKI ) {
112            $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
113            $cache->delete( $cache->makeKey( 'confirmedit', 'ipwhitelist' ) );
114        }
115
116        return true;
117    }
118
119    /**
120     * @see https://www.mediawiki.org/wiki/Manual:Hooks/EditPageBeforeEditButtons
121     *
122     * @param EditPage $editpage Current EditPage object
123     * @param array &$buttons Array of edit buttons, "Save", "Preview", "Live", and "Diff"
124     * @param int &$tabindex HTML tabindex of the last edit check/button
125     */
126    public function onEditPageBeforeEditButtons( $editpage, &$buttons, &$tabindex ) {
127        self::getInstance()->editShowCaptcha( $editpage );
128    }
129
130    /**
131     * @param EditPage $editPage
132     * @param OutputPage $out
133     */
134    public function onEditPage__showEditForm_fields( $editPage, $out ) {
135        self::getInstance()->showEditFormFields( $editPage, $out );
136    }
137
138    /**
139     * @see https://www.mediawiki.org/wiki/Manual:Hooks/EmailUserForm
140     *
141     * @param HTMLForm &$form HTMLForm object
142     * @return bool|void True or no return value to continue or false to abort
143     */
144    public function onEmailUserForm( &$form ) {
145        return self::getInstance()->injectEmailUser( $form );
146    }
147
148    /**
149     * @see https://www.mediawiki.org/wiki/Manual:Hooks/EmailUser
150     *
151     * @param MailAddress &$to MailAddress object of receiving user
152     * @param MailAddress &$from MailAddress object of sending user
153     * @param string &$subject subject of the mail
154     * @param string &$text text of the mail
155     * @param bool|Status|MessageSpecifier|array &$error Out-param for an error.
156     *   Should be set to a Status object or boolean false.
157     * @return bool|void True or no return value to continue or false to abort
158     */
159    public function onEmailUser( &$to, &$from, &$subject, &$text, &$error ) {
160        return self::getInstance()->confirmEmailUser( $from, $to, $subject, $text, $error );
161    }
162
163    /**
164     * APIGetAllowedParams hook handler
165     * Default $flags to 1 for backwards-compatible behavior
166     * @param ApiBase $module
167     * @param array &$params
168     * @param int $flags
169     * @return bool
170     */
171    public function onAPIGetAllowedParams( $module, &$params, $flags ) {
172        return self::getInstance()->apiGetAllowedParams( $module, $params, $flags );
173    }
174
175    /**
176     * @param array $requests
177     * @param array $fieldInfo
178     * @param array &$formDescriptor
179     * @param string $action
180     */
181    public function onAuthChangeFormFields(
182        $requests, $fieldInfo, &$formDescriptor, $action
183    ) {
184        self::getInstance()->onAuthChangeFormFields( $requests, $fieldInfo, $formDescriptor, $action );
185    }
186
187    public static function confirmEditSetup() {
188        global $wgCaptchaTriggers;
189
190        // There is no need to run (core) tests with enabled ConfirmEdit - bug T44145
191        if ( defined( 'MW_PHPUNIT_TEST' ) || defined( 'MW_QUIBBLE_CI' ) ) {
192            $wgCaptchaTriggers = array_fill_keys( array_keys( $wgCaptchaTriggers ), false );
193        }
194    }
195
196    /**
197     * @see https://www.mediawiki.org/wiki/Manual:Hooks/TitleReadWhitelist
198     *
199     * @param Title $title
200     * @param User $user
201     * @param bool &$whitelisted
202     * @return bool|void
203     */
204    public function onTitleReadWhitelist( $title, $user, &$whitelisted ) {
205        $image = SpecialPage::getTitleFor( 'Captcha', 'image' );
206        $help = SpecialPage::getTitleFor( 'Captcha', 'help' );
207        if ( $title->equals( $image ) || $title->equals( $help ) ) {
208            $whitelisted = true;
209        }
210    }
211
212    /**
213     *
214     * Callback for extension.json of FancyCaptcha to set a default captcha directory,
215     * which depends on wgUploadDirectory
216     */
217    public static function onFancyCaptchaSetup() {
218        global $wgCaptchaDirectory, $wgUploadDirectory;
219        if ( !$wgCaptchaDirectory ) {
220            $wgCaptchaDirectory = "$wgUploadDirectory/captcha";
221        }
222    }
223
224    /**
225     * @see https://www.mediawiki.org/wiki/Manual:Hooks/AlternateEditPreview
226     *
227     * @param EditPage $editPage
228     * @param Content &$content
229     * @param string &$previewHTML
230     * @param ParserOutput &$parserOutput
231     * @return bool|void
232     */
233    public function onAlternateEditPreview( $editPage, &$content, &$previewHTML,
234        &$parserOutput
235    ) {
236        $title = $editPage->getTitle();
237        $exceptionTitle = Title::makeTitle( NS_MEDIAWIKI, 'Captcha-ip-whitelist' );
238
239        if ( !$title->equals( $exceptionTitle ) ) {
240            return true;
241        }
242
243        $ctx = $editPage->getArticle()->getContext();
244        $out = $ctx->getOutput();
245        $lang = $ctx->getLanguage();
246
247        $lines = explode( "\n", $content->getNativeData() );
248        $previewHTML .= Html::warningBox(
249                $ctx->msg( 'confirmedit-preview-description' )->parse()
250            ) .
251            Html::openElement(
252                'table',
253                [ 'class' => 'wikitable sortable' ]
254            ) .
255            Html::openElement( 'thead' ) .
256            Html::element( 'th', [], $ctx->msg( 'confirmedit-preview-line' )->text() ) .
257            Html::element( 'th', [], $ctx->msg( 'confirmedit-preview-content' )->text() ) .
258            Html::element( 'th', [], $ctx->msg( 'confirmedit-preview-validity' )->text() ) .
259            Html::closeElement( 'thead' );
260
261        foreach ( $lines as $count => $line ) {
262            $ip = trim( $line );
263            if ( $ip === '' || strpos( $ip, '#' ) !== false ) {
264                continue;
265            }
266            if ( IPUtils::isIPAddress( $ip ) ) {
267                $validity = $ctx->msg( 'confirmedit-preview-valid' )->escaped();
268                $css = 'valid';
269            } else {
270                $validity = $ctx->msg( 'confirmedit-preview-invalid' )->escaped();
271                $css = 'notvalid';
272            }
273            $previewHTML .= Html::openElement( 'tr' ) .
274                Html::element(
275                    'td',
276                    [],
277                    $lang->formatNum( $count + 1 )
278                ) .
279                Html::element(
280                    'td',
281                    [],
282                    // IPv6 max length: 8 groups * 4 digits + 7 delimiter = 39
283                    // + 11 chars for safety
284                    $lang->truncateForVisual( $ip, 50 )
285                ) .
286                Html::rawElement(
287                    'td',
288                    // possible values:
289                    // mw-confirmedit-ip-valid
290                    // mw-confirmedit-ip-notvalid
291                    [ 'class' => 'mw-confirmedit-ip-' . $css ],
292                    $validity
293                ) .
294                Html::closeElement( 'tr' );
295        }
296        $previewHTML .= Html::closeElement( 'table' );
297        $out->addModuleStyles( 'ext.confirmEdit.editPreview.ipwhitelist.styles' );
298
299        return false;
300    }
301
302    /**
303     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderRegisterModules
304     *
305     * @param ResourceLoader $resourceLoader
306     * @return void
307     */
308    public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void {
309        $extensionRegistry = ExtensionRegistry::getInstance();
310        $messages = [];
311
312        $messages[] = 'colon-separator';
313        $messages[] = 'captcha-edit';
314        $messages[] = 'captcha-label';
315
316        if ( $extensionRegistry->isLoaded( 'QuestyCaptcha' ) ) {
317            $messages[] = 'questycaptcha-edit';
318        }
319
320        if ( $extensionRegistry->isLoaded( 'FancyCaptcha' ) ) {
321            $messages[] = 'fancycaptcha-edit';
322            $messages[] = 'fancycaptcha-reload-text';
323            $messages[] = 'fancycaptcha-imgcaptcha-ph';
324        }
325
326        $resourceLoader->register( [
327            'ext.confirmEdit.CaptchaInputWidget' => [
328                'localBasePath' => dirname( __DIR__ ),
329                'remoteExtPath' => 'ConfirmEdit',
330                'scripts' => 'resources/libs/ext.confirmEdit.CaptchaInputWidget.js',
331                'styles' => 'resources/libs/ext.confirmEdit.CaptchaInputWidget.less',
332                'messages' => $messages,
333                'dependencies' => 'oojs-ui-core',
334            ]
335        ] );
336    }
337
338}
339
340class_alias( Hooks::class, 'ConfirmEditHooks' );