Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
ClientHints
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
5 / 5
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onSpecialPageBeforeExecute
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 onBeforePageDisplay
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 getClientHintsHeaderString
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getEmptyClientHintsHeaderString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\CheckUser\HookHandler;
4
5use MediaWiki\Config\Config;
6use MediaWiki\Hook\BeforePageDisplayHook;
7use MediaWiki\SpecialPage\Hook\SpecialPageBeforeExecuteHook;
8
9/**
10 * HookHandler for entry points related to requesting User-Agent Client Hints data.
11 */
12class ClientHints implements SpecialPageBeforeExecuteHook, BeforePageDisplayHook {
13
14    private Config $config;
15
16    /**
17     * @param Config $config
18     */
19    public function __construct( Config $config ) {
20        $this->config = $config;
21    }
22
23    /** @inheritDoc */
24    public function onSpecialPageBeforeExecute( $special, $subPage ) {
25        if ( !$this->config->get( 'CheckUserClientHintsEnabled' ) ) {
26            return;
27        }
28
29        $request = $special->getRequest();
30        if ( $request->wasPosted() ) {
31            // It's too late to ask for client hints when a user is POST'ing a form.
32            if ( $this->config->get( 'CheckUserClientHintsUnsetHeaderWhenPossible' ) ) {
33                $request->response()->header( $this->getEmptyClientHintsHeaderString() );
34            }
35            return;
36        }
37
38        if ( in_array( $special->getName(), $this->config->get( 'CheckUserClientHintsSpecialPages' ) ) ) {
39            $request->response()->header( $this->getClientHintsHeaderString() );
40        } elseif ( $this->config->get( 'CheckUserClientHintsUnsetHeaderWhenPossible' ) ) {
41            $request->response()->header( $this->getEmptyClientHintsHeaderString() );
42        }
43    }
44
45    /** @inheritDoc */
46    public function onBeforePageDisplay( $out, $skin ): void {
47        // We handle special pages in BeforeSpecialPageBeforeExecute.
48        if ( $out->getTitle()->isSpecialPage() ||
49            // ClientHints is globally disabled
50            !$this->config->get( 'CheckUserClientHintsEnabled' )
51        ) {
52            return;
53        }
54
55        $out->addJsConfigVars( [
56            // Roundabout way to ensure we have a list of values like "architecture", "bitness"
57            // etc for use with the client-side JS API. Make sure we get 1) just the values
58            // from the configuration, 2) filter out any empty entries, 3) convert to a list
59            'wgCheckUserClientHintsHeadersJsApi' => array_values( array_filter( array_values(
60                $this->config->get( 'CheckUserClientHintsHeaders' )
61            ) ) ),
62        ] );
63        $out->addModules( 'ext.checkUser.clientHints' );
64
65        if ( $this->config->get( 'CheckUserClientHintsUnsetHeaderWhenPossible' ) ) {
66            $request = $out->getRequest();
67            $request->response()->header( $this->getEmptyClientHintsHeaderString() );
68        }
69    }
70
71    /**
72     * Get the list of headers to use with Accept-CH.
73     *
74     * @return string
75     */
76    private function getClientHintsHeaderString(): string {
77        $headers = implode(
78            ', ',
79            array_filter( array_keys( $this->config->get( 'CheckUserClientHintsHeaders' ) ) )
80        );
81        return "Accept-CH: $headers";
82    }
83
84    /**
85     * Get an Accept-CH header string to tell the client to stop sending client-hint data.
86     *
87     * @return string
88     */
89    private function getEmptyClientHintsHeaderString(): string {
90        return "Accept-CH: ";
91    }
92
93}