Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialHideBanners
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 4
156
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
56
 setHideCookie
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 setP3P
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
4use MediaWiki\Registration\ExtensionRegistry;
5use MediaWiki\SpecialPage\SpecialPage;
6use MediaWiki\SpecialPage\UnlistedSpecialPage;
7
8/**
9 * Unlisted Special Page which sets a cookie for hiding banners across all languages of a project.
10 * This is typically used on donation thank-you pages so that users who have donated will no longer
11 * see fundrasing banners.
12 */
13class SpecialHideBanners extends UnlistedSpecialPage {
14    // Cache this blank response for a day or so (60 * 60 * 24 s.)
15    private const CACHE_EXPIRY = 86400;
16    // Hard-coded upper limit of 10 years for the user-provided …&duration=… parameter
17    private const MAX_COOKIE_DURATION = 10 * 365 * 86400;
18    private const P3P_SUBPAGE = 'P3P';
19
20    public function __construct() {
21        parent::__construct( 'HideBanners' );
22    }
23
24    /** @inheritDoc */
25    public function execute( $par ) {
26        $config = $this->getConfig();
27        // Handle /P3P subpage with explanation of invalid P3P header
28        if ( ( strval( $par ) === self::P3P_SUBPAGE ) &&
29            !$config->get( 'CentralNoticeHideBannersP3P' )
30        ) {
31            $this->setHeaders();
32            $this->getOutput()->addWikiMsg( 'centralnotice-specialhidebanners-p3p' );
33            return;
34        }
35
36        $reason = $this->getRequest()->getText( 'reason', 'donate' );
37
38        // No duration parameter for a custom reason is not expected; we have a
39        // fallback value, but we log that this happened.
40        $duration = $this->getRequest()->getInt( 'duration' );
41        if ( $duration <= 0 || $duration > self::MAX_COOKIE_DURATION ) {
42            $noticeCookieDurations = $config->get( 'NoticeCookieDurations' );
43            if ( isset( $noticeCookieDurations[$reason] ) ) {
44                $duration = $noticeCookieDurations[$reason];
45            } else {
46                $duration = $config->get( 'CentralNoticeFallbackHideCookieDuration' );
47                wfLogWarning( 'Missing or invalid duration for hide cookie reason "'
48                    . $reason . '".' );
49            }
50        }
51
52        $category = $this->getRequest()->getText( 'category', 'fundraising' );
53        $category = Banner::sanitizeRenderedCategory( $category );
54        $this->setHideCookie( $category, $duration, $reason );
55        $this->setP3P();
56
57        $this->getOutput()->disable();
58        wfResetOutputBuffers();
59
60        header( 'Content-Type: image/png' );
61        header( 'access-control-allow-origin: *' );
62
63        if ( !$this->getUser()->isRegistered() ) {
64            $expiry = self::CACHE_EXPIRY;
65            header( "Cache-Control: public, s-maxage={$expiry}, max-age=0" );
66        }
67    }
68
69    /**
70     * Set the cookie for hiding fundraising banners.
71     * @param string $category
72     * @param int $duration
73     * @param string $reason
74     */
75    private function setHideCookie( $category, $duration, $reason ) {
76        $created = time();
77        $exp = $created + $duration;
78        $value = [
79            'v' => 1,
80            'created' => $created,
81            'reason' => $reason
82        ];
83
84        if ( ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ) ) {
85            $cookieDomain = CentralAuthUser::getCookieDomain();
86        } else {
87            $cookieDomain = $this->getConfig()->get( 'NoticeCookieDomain' );
88        }
89        setcookie(
90            "centralnotice_hide_{$category}",
91            json_encode( $value ),
92            [
93                'expires' => $exp,
94                'path' => '/',
95                'domain' => $cookieDomain,
96                'secure' => true,
97                'httponly' => false,
98                'samesite' => 'None',
99            ]
100        );
101    }
102
103    /**
104     * Set an invalid P3P policy header to make IE accept third-party hide cookies.
105     */
106    private function setP3P() {
107        $centralNoticeHideBannersP3P = $this->getConfig()->get( 'CentralNoticeHideBannersP3P' );
108
109        if ( !$centralNoticeHideBannersP3P ) {
110            $url = SpecialPage::getTitleFor(
111                'HideBanners', self::P3P_SUBPAGE )
112                ->getCanonicalURL();
113
114            $p3p = "CP=\"This is not a P3P policy! See $url for more info.\"";
115
116        } else {
117            $p3p = $centralNoticeHideBannersP3P;
118        }
119
120        header( "P3P: $p3p", true );
121    }
122}