Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
WordAds_California_Privacy
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 14
552
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_scripts
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
12
 init_ajax_actions
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 do_not_sell_link_shortcode
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_optout_link_text
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 build_iab_privacy_string
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 get_cookie_name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_cookie_domain
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 get_optout_cookie_string
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_optin_cookie_string
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_optout_cookie
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_optin_cookie
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 handle_optout_request
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 handle_optout_markup
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * CCPA Class
4 *
5 * @package automattic/jetpack
6 */
7
8use Automattic\Jetpack\Assets;
9
10if ( ! defined( 'ABSPATH' ) ) {
11    exit( 0 );
12}
13
14/**
15 * Class WordAds_California_Privacy
16 *
17 * Implementation of [California Consumer Privacy Act] (https://leginfo.legislature.ca.gov/faces/codes_displayText.xhtml?lawCode=CIV&division=3.&title=1.81.5.&part=4.&chapter=&article=) as applicable to WordAds.
18 * Includes:
19 * - Do Not Sell or Share My Personal Information shortcode and widget.
20 * - Modal notice to toggle opt-in/opt-out.
21 * - Cookie handling. Implements IAB usprivacy cookie specifications.
22 * - Client side geo-detection of California visitors by IP address. Avoids issues with page caching.
23 */
24class WordAds_California_Privacy {
25
26    /**
27     * Initializes required scripts and shortcode.
28     */
29    public static function init() {
30        // Initialize shortcode.
31        add_shortcode( 'ccpa-do-not-sell-link', array( __CLASS__, 'do_not_sell_link_shortcode' ) );
32        add_shortcode( 'privacy-do-not-sell-link', array( __CLASS__, 'do_not_sell_link_shortcode' ) );
33    }
34
35    /**
36     * Enqueue required CCPA JavaScript on the frontend.
37     */
38    public static function enqueue_scripts() {
39        wp_enqueue_script(
40            'wordads_ccpa',
41            Assets::get_file_url_for_environment(
42                '_inc/build/wordads/js/wordads-ccpa.min.js',
43                'modules/wordads/js/wordads-ccpa.js'
44            ),
45            array(),
46            JETPACK__VERSION,
47            true
48        );
49
50        wp_localize_script(
51            'wordads_ccpa',
52            'ccpaSettings',
53            array(
54                'defaultOptInCookieString'  => esc_html( self::get_optin_cookie_string() ),
55                'defaultOptOutCookieString' => esc_html( self::get_optout_cookie_string() ),
56                'ccpaCssUrl'                => esc_url( Assets::get_file_url_for_environment( '/css/wordads-ccpa.min.css', '/css/wordads-ccpa.css' ) . '?ver=' . JETPACK__VERSION ),
57                'ajaxUrl'                   => esc_url( admin_url( 'admin-ajax.php' ) ),
58                'ajaxNonce'                 => wp_create_nonce( 'ccpa_optout' ),
59                'forceApplies'              => is_user_logged_in() && current_user_can( 'manage_options' ) ? 'true' : 'false',
60                'strings'                   => array(
61                    'pleaseWait' => esc_html__( 'Please Wait', 'jetpack' ),
62                ),
63            )
64        );
65    }
66
67    /**
68     * Initializes handlers for admin AJAX.
69     */
70    public static function init_ajax_actions() {
71        add_action( 'wp_ajax_privacy_optout', array( __CLASS__, 'handle_optout_request' ) );
72        add_action( 'wp_ajax_nopriv_privacy_optout', array( __CLASS__, 'handle_optout_request' ) );
73
74        add_action( 'wp_ajax_privacy_optout_markup', array( __CLASS__, 'handle_optout_markup' ) );
75        add_action( 'wp_ajax_nopriv_privacy_optout_markup', array( __CLASS__, 'handle_optout_markup' ) );
76    }
77
78    /**
79     * Outputs [privacy-do-not-sell-link] shortcode markup.
80     *
81     * @return string The generated shortcode markup.
82     */
83    public static function do_not_sell_link_shortcode() {
84
85        // If in the customizer always display the link.
86        if ( is_customize_preview() ) {
87            return '<a href="#" class="ccpa-do-not-sell">' . self::get_optout_link_text() . '</a>';
88        }
89
90        // Load required scripts if the shortcode/widget is loaded on the page.
91        self::enqueue_scripts();
92
93        return '<a href="#" class="ccpa-do-not-sell" style="display: none;">' . self::get_optout_link_text() . '</a>';
94    }
95
96    /**
97     * Gets the text used to link to the opt-out page. By law must read 'Do Not Sell or Share My Personal Information'.
98     *
99     * @return mixed|string|void The text of the opt-out link.
100     */
101    private static function get_optout_link_text() {
102        return __( 'Do Not Sell or Share My Personal Information', 'jetpack' );
103    }
104
105    /**
106     * Builds the value of the opt-out cookie.
107     * Format matches spec of [IAB U.S. Privacy String](https://github.com/InteractiveAdvertisingBureau/USPrivacy/blob/master/CCPA/US%20Privacy%20String.md).
108     *
109     * @param bool $optout True if setting an opt-out cookie.
110     *
111     * @return string The value to be stored in the opt-out cookie.
112     */
113    private static function build_iab_privacy_string( $optout ) {
114        $values = array(
115            '1', // Specification version.
116            'Y', // Explicit notice to opt-out provided.
117            $optout ? 'Y' : 'N', // Opt-out of data sale.
118            'N', // Signatory to IAB Limited Service Provider Agreement.
119        );
120
121        return implode( $values );
122    }
123
124    /**
125     * Gets the name to be used for the opt-out cookie.
126     * Name matches spec of [IAB U.S. Privacy String](https://github.com/InteractiveAdvertisingBureau/USPrivacy/blob/master/CCPA/US%20Privacy%20String.md).
127     *
128     * @return string The name of the opt-out cookie.
129     */
130    private static function get_cookie_name() {
131        return 'usprivacy';
132    }
133
134    /**
135     * Gets the domain to be used for the opt-out cookie.
136     * Use the site's custom domain, or if the site has a wordpress.com subdomain, use .wordpress.com to share the cookie.
137     *
138     * @return string The domain to set for the opt-out cookie.
139     */
140    public static function get_cookie_domain() {
141        $host = 'localhost';
142
143        if ( isset( $_SERVER['HTTP_HOST'] ) ) {
144            $host = filter_var( wp_unslash( $_SERVER['HTTP_HOST'] ) );
145        }
146
147        return '.wordpress.com' === substr( $host, -strlen( '.wordpress.com' ) ) ? '.wordpress.com' : '.' . $host;
148    }
149
150    /**
151     * Gets the value to be used when an opt-out cookie is set.
152     *
153     * @return string The value to store in the opt-out cookie.
154     */
155    private static function get_optout_cookie_string() {
156        return self::build_iab_privacy_string( true );
157    }
158
159    /**
160     * Gets the value to be used when an opt-in cookie is set.
161     *
162     * @return string The value to store in the opt-in cookie.
163     */
164    private static function get_optin_cookie_string() {
165        return self::build_iab_privacy_string( false );
166    }
167
168    /**
169     * Sets a cookie in the HTTP response to opt-out visitors from data sales.
170     *
171     * @return bool True if the cookie could be set.
172     */
173    private static function set_optout_cookie() {
174        return setcookie( self::get_cookie_name(), self::get_optout_cookie_string(), time() + ( 5 * YEAR_IN_SECONDS ), '/', self::get_cookie_domain(), is_ssl(), false ); // phpcs:ignore Jetpack.Functions.SetCookie -- Want this accessible.
175    }
176
177    /**
178     * Sets a cookie in the HTTP response to opt-in visitors from data sales.
179     *
180     * @return bool True if the cookie could be set.
181     */
182    private static function set_optin_cookie() {
183        return setcookie( self::get_cookie_name(), self::get_optin_cookie_string(), time() + YEAR_IN_SECONDS, '/', self::get_cookie_domain(), is_ssl(), false ); // phpcs:ignore Jetpack.Functions.SetCookie -- Want this accessible.
184    }
185
186    /**
187     * Handler for opt-in/opt-out AJAX request.
188     */
189    public static function handle_optout_request() {
190        check_ajax_referer( 'ccpa_optout', 'security' );
191
192        $optout = isset( $_POST['optout'] ) && 'true' === $_POST['optout'];
193        $optout ? self::set_optout_cookie() : self::set_optin_cookie();
194
195        // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
196        wp_send_json_success( $optout, null, JSON_UNESCAPED_SLASHES );
197    }
198
199    /**
200     * Handler for modal popup notice markup.
201     */
202    public static function handle_optout_markup() {
203        check_ajax_referer( 'ccpa_optout', 'security' );
204
205        header( 'Content-Type: text/html; charset=utf-8' );
206        $policy_url = get_option( 'wordads_ccpa_privacy_policy_url' );
207
208        $default_disclosure = sprintf(
209            '<p>%s</p>
210            <p>%s</p>
211            <p><strong>%s</strong></p>
212            <p>%s</p>
213            <p>%s</p>
214            <p>%s</p>',
215            esc_html__( 'If you are a resident of certain US states, you have the right to opt out of the "sale" of your "personal information" under your state\'s privacy laws.', 'jetpack' ),
216            esc_html__( 'This site operates an ads program in partnership with third-party vendors who help place ads. Advertising cookies enable these ads partners to serve ads, to personalize those ads based on information like visits to this site and other sites on the internet, and to understand how users engage with those ads. Cookies collect certain information as part of the ads program, and we provide the following categories of information to third-party advertising partners: online identifiers and internet or other network or device activity (such as unique identifiers, cookie information, and IP address), and geolocation data (approximate location information from your IP address). This type of sharing with ads partners may be considered a "sale" of personal information under your state\'s privacy laws.', 'jetpack' ),
217            esc_html__( 'We never share information that identifies you personally, like your name or email address, as part of the advertising program.', 'jetpack' ),
218            esc_html__( 'If you\'d prefer not to see ads that are personalized based on information from your visits to this site, you can opt-out by toggling the "Do Not Sell or Share My Personal Information" switch below to the On position.', 'jetpack' ),
219            esc_html__( 'This opt-out is managed through cookies, so if you delete cookies, your browser is set to delete cookies automatically after a certain length of time, or if you visit this site with a different browser, you\'ll need to make this selection again.', 'jetpack' ),
220            esc_html__( 'After you opt-out you may still see ads, including personalized ones, on this site and other sites - they just won\'t be personalized based on information from your visits to this site.', 'jetpack' )
221        );
222
223        /**
224         * Filter on the default CCPA disclosure text.
225         *
226         * @see https://jetpack.com/support/ads/
227         *
228         * @module wordads
229         *
230         * @since 8.7.0
231         *
232         * @param string Default CCPA disclosure for WordAds.
233         */
234        $disclosure = apply_filters( 'wordads_ccpa_disclosure', $default_disclosure );
235        ?>
236            <div id="ccpa-modal" class="cleanslate">
237                <div class="components-modal__screen-overlay">
238                    <div tabindex="0"></div>
239                    <div role="dialog" aria-labelledby="dialog_label" aria-modal="true" class="components-modal__frame">
240                        <div class="components-modal__content ccpa-settings">
241                            <div class="components-modal__header">
242                                <div class="components-modal__header-heading-container">
243                                    <h1 id="dialog_label" class="components-modal__header-heading"><?php esc_html_e( 'Do Not Sell or Share My Personal Information', 'jetpack' ); ?></h1>
244                                </div>
245                                <button type="button" aria-label="<?php esc_html_e( 'Close dialog', 'jetpack' ); ?>" class="components-button components-icon-button">
246                                    <svg aria-hidden="true" role="img" focusable="false" class="dashicon dashicons-no-alt" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
247                                        <path d="M14.95 6.46L11.41 10l3.54 3.54-1.41 1.41L10 11.42l-3.53 3.53-1.42-1.42L8.58 10 5.05 6.47l1.42-1.42L10 8.58l3.54-3.53z"></path>
248                                    </svg>
249                                </button>
250                            </div>
251                            <div class="ccpa-settings__intro-txt"><?php echo wp_kses( $disclosure, wp_kses_allowed_html( 'post' ) ); ?></div>
252                            <div class="components-modal__footer">
253                                <div role="form" class="ccpa-setting">
254                                    <label>
255                                        <span class="ccpa-setting__header"><?php esc_html_e( 'Do Not Sell or Share My Personal Information', 'jetpack' ); ?></span>
256                                        <span class="ccpa-setting__toggle components-form-toggle">
257                                            <input id="ccpa-opt-out" class="components-form-toggle__input opt-out" type="checkbox" value="false" autofocus />
258                                            <span class="components-form-toggle__track"></span>
259                                            <span class="components-form-toggle__thumb"></span>
260                                            <svg class="components-form-toggle__on" width="2" height="6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 6" role="img" aria-hidden="true" focusable="false"><path d="M0 0h2v6H0z"></path></svg>
261                                            <svg class="components-form-toggle__off" width="6" height="6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 6 6" role="img" aria-hidden="true" focusable="false"><path d="M3 1.5c.8 0 1.5.7 1.5 1.5S3.8 4.5 3 4.5 1.5 3.8 1.5 3 2.2 1.5 3 1.5M3 0C1.3 0 0 1.3 0 3s1.3 3 3 3 3-1.3 3-3-1.3-3-3-3z"></path></svg>
262                                        </span>
263                                        <span class="ccpa-setting__toggle-text ccpa-setting__toggle-text-off"><?php esc_html_e( 'Off', 'jetpack' ); ?></span>
264                                        <span class="ccpa-setting__toggle-text ccpa-setting__toggle-text-on"><?php esc_html_e( 'On', 'jetpack' ); ?></span>
265                                    </label>
266                                </div>
267                                <div class="components-modal__footer-bottom">
268                                    <button class="components-button is-button is-primary"><?php esc_html_e( 'Close', 'jetpack' ); ?></button>
269                                    <?php
270                                    if ( $policy_url ) {
271                                        printf(
272                                            '<a href="%s" class="ccpa-privacy">%s</a>',
273                                            esc_url( $policy_url ),
274                                            esc_html__( 'View Privacy Policy', 'jetpack' )
275                                        );
276                                    }
277                                    ?>
278                                </div>
279                            </div>
280                        </div>
281                    </div>
282                </div>
283            </div>
284        <?php
285
286        wp_die();
287    }
288}