Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.18% covered (warning)
59.18%
374 / 632
29.03% covered (danger)
29.03%
9 / 31
CRAP
0.00% covered (danger)
0.00%
0 / 1
Action_Bar
59.18% covered (warning)
59.18%
374 / 632
29.03% covered (danger)
29.03%
9 / 31
2645.92
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 load
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
2.00
 enqueue_scripts
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 1
812
 footer
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 get_post_stats_url
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
2.00
 is_vip
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 localized_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_folded
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 set_folded
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 logged_out_follow_disabled
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 has_enough_posts
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 flush_published_posts_count
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
 switch_to_user_locale
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 restore_locale
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 email_default
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 icon_markup
96.43% covered (success)
96.43%
27 / 28
0.00% covered (danger)
0.00%
0 / 1
3
 icon
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 menu_item
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
4
 menu_group
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 blavatar
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
5.67
 site_title
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 follow_links
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 html
74.53% covered (warning)
74.53%
240 / 322
0.00% covered (danger)
0.00%
0 / 1
178.48
 can_reblog
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
7.46
 gdpr_applies
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
6.84
 bump_stat
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
20
 fold
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 unfold
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 ajax_stats
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 settings_field
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 settings_field_display
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * WordPress.com Action Bar.
4 *
5 * The floating bar in the bottom corner of a site's front end. Visitors can subscribe, comment,
6 * reblog and report from it; site members get Edit and Stats shortcuts.
7 *
8 * @package automattic/jetpack-newsletter
9 */
10
11namespace Automattic\Jetpack\Newsletter;
12
13use Automattic\Jetpack\Assets;
14use Automattic\Jetpack\Status;
15use Automattic\Jetpack\Status\Host;
16
17/**
18 * The floating Action Bar on the front end of WordPress.com Simple sites. Ported from wpcom `wp-content/mu-plugins/actionbar.php`.
19 */
20class Action_Bar {
21    /**
22     * Transient caching whether the site has published enough posts to show Subscribe.
23     *
24     * @var string
25     */
26    const ENOUGH_POSTS_TRANSIENT = 'jetpack_action_bar_has_enough_posts';
27
28    /**
29     * Whether the class has been initialized.
30     *
31     * @var bool
32     */
33    private static $initialized = false;
34
35    /**
36     * Queue the Action Bar to load once all plugins are available.
37     *
38     * Simple only for now. Yields to the copy wpcom still ships in mu-plugins, so the two never load together.
39     *
40     * @since 0.17.0
41     */
42    public static function init() {
43        if ( self::$initialized ) {
44            return;
45        }
46        self::$initialized = true;
47
48        if ( ! ( new Host() )->is_wpcom_simple() ) {
49            return;
50        }
51
52        // A callback added to the hook that is currently running never fires.
53        if ( did_action( 'plugins_loaded' ) ) {
54            self::load();
55        } else {
56            add_action( 'plugins_loaded', array( __CLASS__, 'load' ) );
57        }
58    }
59
60    /**
61     * Register the bar's hooks unless wpcom's mu-plugin copy is loaded.
62     *
63     * @since 0.17.0
64     */
65    public static function load() {
66        if ( function_exists( 'wpcom_actionbar_enqueue_scripts' ) ) {
67            return;
68        }
69
70        add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 101 );
71        add_action( 'admin_init', array( __CLASS__, 'settings_field' ) );
72        add_action( 'transition_post_status', array( __CLASS__, 'flush_published_posts_count' ), 10, 3 );
73
74        add_action( 'wp_ajax_fold_actionbar', array( __CLASS__, 'fold' ) );
75        add_action( 'wp_ajax_nopriv_fold_actionbar', array( __CLASS__, 'fold' ) );
76        add_action( 'wp_ajax_unfold_actionbar', array( __CLASS__, 'unfold' ) );
77        add_action( 'wp_ajax_nopriv_unfold_actionbar', array( __CLASS__, 'unfold' ) );
78        add_action( 'wp_ajax_actionbar_stats', array( __CLASS__, 'ajax_stats' ) );
79        add_action( 'wp_ajax_nopriv_actionbar_stats', array( __CLASS__, 'ajax_stats' ) );
80    }
81
82    /**
83     * Decide whether the bar renders on this request and, if so, queue its data and footer output.
84     */
85    public static function enqueue_scripts() {
86        if ( get_option( 'wpcom_hide_action_bar' ) ) {
87            return;
88        }
89
90        $current_user = wp_get_current_user();
91
92        /**
93         * Filters whether the Action Bar loads on this request.
94         *
95         * WordPress.com hooks this to keep the bar off its internal sites and off sites marked deleted, spam, archived, or parked.
96         *
97         * @since 0.17.0
98         *
99         * @param bool $enabled Whether to load the bar. Default true.
100         */
101        if ( ! apply_filters( 'jetpack_action_bar_enabled', true ) ) {
102            return;
103        }
104
105        $site_id = get_current_blog_id();
106
107        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only checks on preview query args.
108        // Don't show on theme previews and block patterns source sites.
109        $is_theme_demo = function_exists( 'wpcom_is_theme_demo_site' ) && wpcom_is_theme_demo_site();
110        // @phan-suppress-next-line PhanUndeclaredFunction -- Defined by jetpack-mu-wpcom, which is not a dependency; guarded by function_exists().
111        $is_pattern_source = function_exists( 'wpcom_has_blog_sticker' ) && wpcom_has_blog_sticker( 'block-patterns-source-site', $site_id );
112        if ( isset( $_GET['theme'] ) || $is_theme_demo || $is_pattern_source ) {
113            return;
114        }
115
116        // Don't show on Customizer previews.
117        if ( isset( $_GET['customize_theme'] ) || isset( $_GET['customize_changeset_uuid'] ) ) {
118            return;
119        }
120        // phpcs:enable
121
122        // Don't show on WordPress mobile apps.
123        $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
124        if ( $user_agent && preg_match( '/wp-(android|iphone)/', $user_agent ) ) {
125            return;
126        }
127
128        // Don't show on Landpack blogs, unless user is a member of the blog.
129        if ( defined( 'WPCOM_LANDPACK_BLOG_IDS' ) && in_array( $site_id, (array) WPCOM_LANDPACK_BLOG_IDS, true ) && ! is_user_member_of_blog( $current_user->ID, $site_id ) ) {
130            return;
131        }
132
133        // Render this in the user's language.
134        self::switch_to_user_locale();
135
136        $status_message    = false;
137        $status_subscribed = false;
138
139        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Status flag set by the subscribe.wordpress.com redirect.
140        $blogsub = isset( $_GET['blogsub'] ) ? sanitize_key( wp_unslash( $_GET['blogsub'] ) ) : '';
141        switch ( $blogsub ) {
142            case 'confirming':
143                $status_subscribed = true;
144                $status_message    = '<h3>' . __( 'Thanks', 'jetpack-newsletter' ) . '</h3>';
145                $status_message   .= '<div>' .
146                    wp_kses(
147                        sprintf(
148                            /* translators: %s is the URL of the support contact page. */
149                            __( 'You’ll get an email with a link to confirm your subscription. If it doesn’t arrive, please <a href="%s">contact us</a>.', 'jetpack-newsletter' ),
150                            esc_url( self::localized_url( 'https://wordpress.com/support/contact/' ) )
151                        ),
152                        array(
153                            'a' => array(
154                                'href' => array(),
155                            ),
156                        )
157                    ) .
158                    '</div>';
159                break;
160            case 'subscribed':
161                $status_subscribed = true;
162                $status_message    = '<div>' . __( 'You’re already subscribed to this site!', 'jetpack-newsletter' ) . '</div>';
163                break;
164            case 'flooded':
165                $status_message =
166                    '<div>' .
167                    sprintf(
168                        /* translators: %s is a link with its text (Subscription Manager) translated separately */
169                        __( 'You already have several pending email subscriptions. Approve or delete a few through your %s before attempting to subscribe to more blogs.', 'jetpack-newsletter' ),
170                        '<a href="https://subscribe.wordpress.com/" target="_blank" rel="noopener noreferrer">' . __( 'Subscription Manager', 'jetpack-newsletter' ) . '</a>'
171                    ) .
172                    '</div>';
173                break;
174            case 'pending':
175                $status_subscribed = true;
176                $status_message    = '<div>' . __( 'You already have a pending subscription, we just sent you another email, click the link or <a href="https://en.support.wordpress.com/contact/">contact us</a> if you don’t get it', 'jetpack-newsletter' ) . '</div>';
177                break;
178            case 'confirmed':
179                $status_subscribed = true;
180                $status_message    = '<div>' . __( 'Congrats, you’re subscribed! You’ll get an email with the details of your subscription and an unsubscribe link', 'jetpack-newsletter' ) . '</div>';
181                break;
182        }
183
184        // VIP: Disable functionality on sites that have logged_out follow set to false.
185        if ( ! is_user_logged_in() && self::logged_out_follow_disabled() ) {
186            self::restore_locale();
187            return;
188        }
189
190        $http_host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
191
192        // Data to pass to the JS.
193        $actionbar_info = array(
194            'siteID'           => $site_id,
195            'postID'           => is_singular() ? get_the_ID() : 0,
196            'siteURL'          => get_option( 'home' ),
197            'xhrURL'           => esc_url_raw( ( is_ssl() ? 'https://' : 'http://' ) . $http_host . '/wp-admin/admin-ajax.php' ),
198            'nonce'            => wp_create_nonce( 'manage_subscription' ),
199            'isLoggedIn'       => is_user_logged_in(),
200            'statusMessage'    => $status_message,
201            'statusSubscribed' => $status_subscribed,
202            'subsEmailDefault' => self::email_default( $current_user ),
203            'proxyScriptUrl'   => 'https://s0.wp.com/wp-content/js/wpcom-proxy-request.js?ver=20211021',
204        );
205
206        if ( is_singular() ) {
207            $actionbar_info['shortlink'] = wp_get_shortlink( get_the_ID() );
208        }
209
210        $actionbar_info['i18n'] = array(
211            'followedText'    => __( 'New posts from this site will now appear in your <a href="https://wordpress.com/reader">Reader</a>', 'jetpack-newsletter' ),
212            'foldBar'         => __( 'Collapse this bar', 'jetpack-newsletter' ),
213            'unfoldBar'       => __( 'Expand this bar', 'jetpack-newsletter' ),
214            'shortLinkCopied' => __( 'Copied to clipboard.', 'jetpack-newsletter' ),
215        );
216
217        // Switch back to site language.
218        self::restore_locale();
219
220        // An alias handle with no src, so wp_localize_script() prints the data without a script tag.
221        // phpcs:disable WordPress.WP.EnqueuedResourceParameters.MissingVersion
222        wp_register_script( 'wpcom-actionbar-placeholder', '', array(), null, false );
223        wp_localize_script( 'wpcom-actionbar-placeholder', 'actionbardata', $actionbar_info );
224        wp_enqueue_script( 'wpcom-actionbar-placeholder' );
225        // phpcs:enable
226
227        // Defer loading the actionbar resources.
228        add_action( 'wp_footer', array( __CLASS__, 'footer' ) );
229    }
230
231    /**
232     * Print the bar's markup, then a loader that appends its CSS and JS after DOMContentLoaded.
233     */
234    public static function footer() {
235        $is_rtl = function_exists( 'wpcom_is_locale_rtl' ) ? wpcom_is_locale_rtl( get_user_locale() ) : is_rtl();
236        self::html( $is_rtl );
237
238        $asset_path = dirname( __DIR__ ) . '/build/action-bar.asset.php';
239        $asset_file = file_exists( $asset_path ) ? include $asset_path : array();
240        $version    = is_array( $asset_file ) && ! empty( $asset_file['version'] ) ? $asset_file['version'] : Settings::PACKAGE_VERSION;
241
242        // One stylesheet for both directions: it uses logical properties and the bar carries its own dir attribute.
243        $css_url = wp_json_encode( add_query_arg( 'ver', $version, plugins_url( '../build/action-bar.css', __FILE__ ) ), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT );
244        $js_url  = wp_json_encode( add_query_arg( 'ver', $version, plugins_url( '../build/action-bar.js', __FILE__ ) ), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT );
245
246        wp_print_inline_script_tag(
247            'window.addEventListener( "DOMContentLoaded", function () {
248                var link = document.createElement( "link" );
249                link.href = ' . $css_url . ';
250                link.rel = "stylesheet";
251                document.head.appendChild( link );
252
253                var script = document.createElement( "script" );
254                script.src = ' . $js_url . ';
255                document.body.appendChild( script );
256            } );'
257        );
258    }
259
260    /**
261     * Where the bar's post stats link goes.
262     *
263     * @param int    $post_id           The post.
264     * @param int    $site_id           The site's blog ID.
265     * @param string $site_slug         The site slug in Calypso URLs.
266     * @param bool   $use_calypso_links Whether the site links to Calypso instead of wp-admin.
267     * @return string
268     */
269    private static function get_post_stats_url( $post_id, $site_id, $site_slug, $use_calypso_links ) {
270        $url = $use_calypso_links
271            ? sprintf( 'https://wordpress.com/stats/post/%d/%s', $post_id, $site_slug )
272            : admin_url( sprintf( 'admin.php?page=stats#!/stats/post/%d/%d', $post_id, $site_id ) );
273
274        /** This filter is documented in projects/packages/stats-admin/src/class-admin-bar.php */
275        return apply_filters(
276            'jetpack_stats_url',
277            $url,
278            array(
279                'view' => 'post',
280                'id'   => $post_id,
281            )
282        );
283    }
284
285    /**
286     * Whether this is a WordPress.com VIP site. False anywhere the wpcom helper is missing.
287     *
288     * @return bool
289     */
290    private static function is_vip() {
291        return function_exists( 'wpcom_is_vip' ) && wpcom_is_vip();
292    }
293
294    /**
295     * Localize a WordPress.com URL for the current user where wpcom can; otherwise return it as is.
296     *
297     * @param string $url A wordpress.com URL.
298     * @return string
299     */
300    private static function localized_url( $url ) {
301        return function_exists( 'localized_wpcom_url' ) ? localized_wpcom_url( $url ) : $url;
302    }
303
304    /**
305     * Whether the user collapsed the bar. A wpcom user attribute on Simple, user meta elsewhere.
306     *
307     * @param int $user_id User ID.
308     * @return bool
309     */
310    private static function is_folded( $user_id ) {
311        if ( function_exists( 'get_user_attribute' ) ) {
312            return (bool) get_user_attribute( $user_id, 'is_actionbar_folded' );
313        }
314        return (bool) get_user_meta( $user_id, 'is_actionbar_folded', true );
315    }
316
317    /**
318     * Remember or forget that the user collapsed the bar.
319     *
320     * @param int  $user_id User ID.
321     * @param bool $folded  Whether the bar is collapsed.
322     */
323    private static function set_folded( $user_id, $folded ) {
324        if ( function_exists( 'update_user_attribute' ) && function_exists( 'delete_user_attribute' ) ) {
325            if ( $folded ) {
326                update_user_attribute( $user_id, 'is_actionbar_folded', 1 );
327            } else {
328                delete_user_attribute( $user_id, 'is_actionbar_folded' );
329            }
330            return;
331        }
332        if ( $folded ) {
333            update_user_meta( $user_id, 'is_actionbar_folded', 1 );
334        } else {
335            delete_user_meta( $user_id, 'is_actionbar_folded' );
336        }
337    }
338
339    /**
340     * Whether logged-out visitors should get no bar and no follow actions.
341     *
342     * @return bool
343     */
344    private static function logged_out_follow_disabled() {
345        $settings = get_option( 'subscription_options' );
346        $disabled = self::is_vip() && ( ! isset( $settings['loggedoutfollow'] ) || 'off' === $settings['loggedoutfollow'] );
347
348        /**
349         * Filters whether logged-out visitors get the bar and its follow actions.
350         *
351         * @since 0.17.0
352         *
353         * @param bool $disabled Whether to disable. Defaults to true on VIP sites with logged-out follow off.
354         */
355        return (bool) apply_filters( 'wpcom_disable_logged_out_follow', $disabled );
356    }
357
358    /**
359     * Whether the site has published enough posts for a Subscribe button to make sense.
360     *
361     * @return bool
362     */
363    private static function has_enough_posts() {
364        $has_enough_posts = get_transient( self::ENOUGH_POSTS_TRANSIENT );
365        if ( false === $has_enough_posts ) {
366            // Stored as 1/0: a cached false would read as a cache miss.
367            $has_enough_posts = (int) wp_count_posts( 'post' )->publish >= 2 ? 1 : 0;
368            set_transient( self::ENOUGH_POSTS_TRANSIENT, $has_enough_posts, DAY_IN_SECONDS );
369        }
370
371        return (bool) $has_enough_posts;
372    }
373
374    /**
375     * Clear the cached Subscribe answer when a post enters or leaves the published state.
376     *
377     * @since 0.17.2
378     *
379     * @param string         $new_status New post status.
380     * @param string         $old_status Old post status.
381     * @param \WP_Post|mixed $post Post object.
382     */
383    public static function flush_published_posts_count( $new_status, $old_status, $post ) {
384        if ( ! $post instanceof \WP_Post ) {
385            // Some callers fire the action without a populated post object (e.g. failed get_post lookups).
386            return;
387        }
388
389        if (
390            'post' === $post->post_type
391            && $new_status !== $old_status
392            && ( 'publish' === $new_status || 'publish' === $old_status )
393        ) {
394            delete_transient( self::ENOUGH_POSTS_TRANSIENT );
395        }
396    }
397
398    /**
399     * Switch to the current user's locale, if logged in.
400     */
401    private static function switch_to_user_locale() {
402        if ( ! is_user_logged_in() ) {
403            return;
404        }
405        if ( function_exists( 'wpcom_switch_to_user_locale' ) ) {
406            wpcom_switch_to_user_locale( get_current_user_id() );
407        } else {
408            switch_to_user_locale( get_current_user_id() );
409        }
410    }
411
412    /**
413     * Undo switch_to_user_locale().
414     */
415    private static function restore_locale() {
416        if ( ! is_user_logged_in() ) {
417            return;
418        }
419        if ( function_exists( 'wpcom_restore_current_locale' ) ) {
420            wpcom_restore_current_locale();
421        } else {
422            restore_previous_locale();
423        }
424    }
425
426    /**
427     * The email delivery frequency a new subscription defaults to for this user.
428     *
429     * @param \WP_User $current_user The current user.
430     * @return string One of instantly, daily, weekly, never.
431     */
432    private static function email_default( $current_user ) {
433        if ( ! empty( $current_user->subs_email_default ) ) {
434            return $current_user->subs_email_default;
435        }
436        if ( function_exists( 'wpcom_subs_get_subscription_delivery_email_default' ) ) {
437            return wpcom_subs_get_subscription_delivery_email_default();
438        }
439        return 'instantly';
440    }
441
442    /**
443     * Markup for one of the WordPress icons the bar uses, inlined from @wordpress/icons.
444     *
445     * Icons inherit their color from CSS `color`, like the library's own Icon component.
446     *
447     * @param string $name Icon name from the WordPress icon library.
448     * @param int    $size Rendered width and height in pixels.
449     * @return string Empty for an unknown name.
450     */
451    private static function icon_markup( $name, $size = 24 ) {
452        $fill_icons = array(
453            'bell'            => '<path fill-rule="evenodd" clip-rule="evenodd" d="M17 11.5c0 1.353.17 2.368.976 3 .266.209.602.376 1.024.5v1H5v-1c.422-.124.757-.291 1.024-.5.806-.632.976-1.647.976-3V9c0-2.8 2.2-5 5-5s5 2.2 5 5v2.5ZM15.5 9v2.5c0 .93.066 1.98.515 2.897l.053.103H7.932a4.018 4.018 0 0 0 .053-.103c.449-.917.515-1.967.515-2.897V9c0-1.972 1.528-3.5 3.5-3.5s3.5 1.528 3.5 3.5Zm-5.492 9.008c0-.176.023-.346.065-.508h3.854A1.996 1.996 0 0 1 12 20c-1.1 0-1.992-.892-1.992-1.992Z"/>',
454            'copy'            => '<path fill-rule="evenodd" clip-rule="evenodd" d="M5 4.5h11a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5H5a.5.5 0 0 1-.5-.5V5a.5.5 0 0 1 .5-.5ZM3 5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm17 3v10.75c0 .69-.56 1.25-1.25 1.25H6v1.5h12.75a2.75 2.75 0 0 0 2.75-2.75V8H20Z"/>',
455            'external'        => '<path d="M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"/>',
456            'comment'         => '<path d="M18 4H6c-1.1 0-2 .9-2 2v12.9c0 .6.5 1.1 1.1 1.1.3 0 .5-.1.8-.3L8.5 17H18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm.5 11c0 .3-.2.5-.5.5H7.9l-2.4 2.4V6c0-.3.2-.5.5-.5h12c.3 0 .5.2.5.5v9z"/>',
457            'more-horizontal' => '<path d="M11 13h2v-2h-2v2zm-6 0h2v-2H5v2zm12-2v2h2v-2h-2z"/>',
458            'pencil'          => '<path d="m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"/>',
459            'reusable-block'  => '<path d="M7 7.2h8.2L13.5 9l1.1 1.1 3.6-3.6-3.5-4-1.1 1 1.9 2.3H7c-.9 0-1.7.3-2.3.9-1.4 1.5-1.4 4.2-1.4 5.6v.2h1.5v-.3c0-1.1 0-3.5 1-4.5.3-.3.7-.5 1.2-.5zm13.8 4V11h-1.5v.3c0 1.1 0 3.5-1 4.5-.3.3-.7.5-1.3.5H8.8l1.7-1.7-1.1-1.1L5.9 17l3.5 4 1.1-1-1.9-2.3H17c.9 0 1.7-.3 2.3-.9 1.5-1.4 1.5-4.2 1.5-5.6z"/>',
460            'shield'          => '<path fill-rule="evenodd" clip-rule="evenodd" d="M12 3.176l6.75 3.068v4.574c0 3.9-2.504 7.59-6.035 8.755a2.283 2.283 0 01-1.43 0c-3.53-1.164-6.035-4.856-6.035-8.755V6.244L12 3.176zM6.75 7.21v3.608c0 3.313 2.145 6.388 5.005 7.33.159.053.331.053.49 0 2.86-.942 5.005-4.017 5.005-7.33V7.21L12 4.824 6.75 7.21z"/>',
461        );
462        // These are drawn with strokes, not fills, in the library.
463        $stroke_icons = array(
464            'chart-bar' => '<path d="M6.75 20V10M12 20V5M17.25 20V14" vector-effect="non-scaling-stroke"/>',
465            'check'     => '<path d="M7 12L10 15L17 8" vector-effect="non-scaling-stroke"/>',
466        );
467
468        if ( isset( $fill_icons[ $name ] ) ) {
469            $attributes = 'fill="currentColor"';
470            $markup     = $fill_icons[ $name ];
471        } elseif ( isset( $stroke_icons[ $name ] ) ) {
472            $attributes = 'style="fill: none" stroke="currentColor" stroke-width="1.5"';
473            $markup     = $stroke_icons[ $name ];
474        } else {
475            return '';
476        }
477
478        return sprintf(
479            '<svg class="actnbr-icon actnbr-icon-%1$s" width="%2$d" height="%2$d" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" %3$s>%4$s</svg>',
480            esc_attr( $name ),
481            (int) $size,
482            $attributes,
483            $markup
484        );
485    }
486
487    /**
488     * Print one of the bar's icons.
489     *
490     * @param string $name Icon name from the WordPress icon library.
491     * @param int    $size Rendered width and height in pixels.
492     */
493    private static function icon( $name, $size = 24 ) {
494        echo self::icon_markup( $name, $size ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static SVG markup, name escaped in the builder.
495    }
496
497    /**
498     * Markup for one item of the â‹¯ menu.
499     *
500     * @param array $args Item arguments: href, label, class (stats hook), icon (right-edge icon name), blank (open in a new tab), before (trusted markup before the label), role (defaults to menuitem).
501     * @return string
502     */
503    private static function menu_item( $args ) {
504        $args = wp_parse_args(
505            $args,
506            array(
507                'href'   => '',
508                'label'  => '',
509                'class'  => '',
510                'icon'   => '',
511                'blank'  => false,
512                'before' => '',
513                'role'   => 'menuitem',
514            )
515        );
516
517        return sprintf(
518            '<a%1$s class="actnbr-menu__item %2$s" href="%3$s"%4$s>%5$s<span class="actnbr-menu__label">%6$s</span>%7$s</a>',
519            $args['role'] ? ' role="' . esc_attr( $args['role'] ) . '"' : '',
520            esc_attr( $args['class'] ),
521            esc_url( $args['href'] ),
522            $args['blank'] ? ' target="_blank" rel="noopener noreferrer"' : '',
523            $args['before'], // Caller-built markup, escaped at the source.
524            esc_html( $args['label'] ),
525            $args['icon'] ? '<span class="actnbr-menu__icon">' . self::icon_markup( $args['icon'], 18 ) . '</span>' : ''
526        );
527    }
528
529    /**
530     * Print a menu group if it has any items.
531     *
532     * @param string[] $items Rendered items; empty strings are skipped.
533     */
534    private static function menu_group( array $items ) {
535        $items = array_filter( $items );
536        if ( ! $items ) {
537            return;
538        }
539        echo '<div class="actnbr-menu__group" role="group">' . implode( '', $items ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Items are escaped where built.
540    }
541
542    /**
543     * The site's blavatar image markup, or an empty string.
544     *
545     * @return string
546     */
547    private static function blavatar() {
548        if ( ! function_exists( 'get_blavatar' ) ) {
549            return '';
550        }
551        $blavatar_img = get_blavatar( get_option( 'siteurl' ), 50, Assets::staticize_subdomain( 'https://en.wordpress.com/i/logo/wpcom-gray-white.png' ) ); // phpcs:ignore WPCOM.I18nRules.LocalizedUrl.UnlocalizedUrl
552        if ( str_starts_with( $blavatar_img, '<img alt' ) ) {
553            $blavatar_img = "<img loading='lazy' alt" . substr( $blavatar_img, 8 );
554        }
555        return $blavatar_img;
556    }
557
558    /**
559     * Print the site title row at the top of the Subscribe popover.
560     *
561     * @param string $site_url  Site home URL.
562     * @param string $site_name Site title.
563     * @param string $blavatar  Blavatar image markup, or empty.
564     */
565    private static function site_title( $site_url, $site_name, $blavatar ) {
566        $item = self::menu_item(
567            array(
568                'href'   => $site_url,
569                'label'  => $site_name,
570                'class'  => 'actnbr-sitename',
571                'before' => $blavatar,
572                'role'   => '',
573            )
574        );
575        echo '<div class="actnbr-panel__group">' . $item . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped where built.
576    }
577
578    /**
579     * Print the Subscribe and Subscribed links; only one is visible at a time.
580     *
581     * @param bool $is_following Whether the current user already subscribes to this site.
582     */
583    private static function follow_links( $is_following ) {
584        ?>
585            <a class="actnbr-action actnbr-actn-follow <?php echo $is_following ? ' no-display' : ''; ?>" href="" role="button" aria-haspopup="dialog" aria-expanded="false">
586                <?php self::icon( 'bell', 20 ); ?>
587                <span><?php esc_html_e( 'Subscribe', 'jetpack-newsletter' ); ?></span>
588            </a>
589            <a class="actnbr-action actnbr-actn-following <?php echo $is_following ? '' : ' no-display'; ?>" href="" role="button">
590                <?php self::icon( 'check', 20 ); ?>
591                <span><?php esc_html_e( 'Subscribed', 'jetpack-newsletter' ); ?></span>
592            </a>
593        <?php
594    }
595
596    /**
597     * Print the bar's markup. Hidden inline until the JS reveals it.
598     *
599     * @param bool $is_rtl Whether to render right-to-left.
600     */
601    private static function html( $is_rtl ) {
602        $current_user = wp_get_current_user();
603        $site_id      = get_current_blog_id();
604
605        global $current_blog;
606
607        $is_logged_in = is_user_logged_in();
608        $is_member    = $is_logged_in && is_user_member_of_blog( $current_user->ID, $site_id );
609
610        // Render this in the user's language.
611        self::switch_to_user_locale();
612
613        $is_suspended = function_exists( 'is_suspended' ) && is_suspended( $site_id );
614
615        /*
616         * Follow actions are dropped only on a "static" site (front page set, under two posts) viewed by
617         * someone who is not a member. Deleted, spam and archived sites never reach this function.
618         */
619        $can_follow = true;
620        if (
621            ! $is_suspended &&
622            ! in_array( $site_id, (array) apply_filters( 'loggedout_follow_disabled_blog_id', array( 1 ) ), true ) &&
623            ! apply_filters( 'loggedout_follow_disabled', false ) &&
624            ! self::logged_out_follow_disabled() &&
625            ( ! $is_member || function_exists( 'is_automattician' ) && is_automattician( $current_user->ID ) ) &&
626            ( 'page' === get_option( 'show_on_front' ) && get_option( 'post_count' ) < 2 )
627        ) {
628            $can_follow = false;
629        }
630
631        $login_url = add_query_arg( 'redirect_to', get_permalink(), 'https://wordpress.com/log-in' );
632        $login_url = add_query_arg( 'signup_flow', 'account', $login_url );
633        if (
634            ! empty( $current_blog->primary_redirect )
635            && ! str_contains( $current_blog->primary_redirect, '.wordpress.com' )
636        ) {
637            // phpcs:ignore WPCOM.I18nRules.LocalizedUrl.UnlocalizedUrl
638            $redirect_to = add_query_arg( 'back', rawurlencode( get_permalink() ? get_permalink() : home_url() ), 'https://r-login.wordpress.com/remote-login.php?action=link' );
639            $login_url   = add_query_arg( 'redirect_to', rawurlencode( $redirect_to ), 'https://wordpress.com/log-in' );
640        }
641
642        $site_name          = get_option( 'blogname' );
643        $site_url           = get_option( 'home' );
644        $site_host          = wp_parse_url( $site_url, PHP_URL_HOST );
645        $site_slug          = ( new Status() )->get_site_suffix();
646        $can_customize_site = $is_member && current_user_can( 'edit_theme_options' );
647        $subscription_id    = function_exists( 'wpcom_subs_is_subscribed' ) ? wpcom_subs_is_subscribed(
648            array(
649                'user_id' => get_current_user_id(),
650                'blog_id' => $site_id,
651            )
652        ) : false;
653        $is_following       = $subscription_id ? true : false;
654        $signup_url         = 'https://wordpress.com/start/';
655        $theme_slug         = get_stylesheet();
656        $theme_url          = function_exists( 'wpcom_get_theme_showcase_url' ) ? wpcom_get_theme_showcase_url( $theme_slug ) : 'https://wordpress.com/theme/' . $theme_slug;
657        $is_singular        = false;
658        $is_folded          = $is_logged_in && self::is_folded( $current_user->ID );
659        $feed_id            = false;
660        if ( class_exists( 'FeedBag' ) ) {
661            $feed_id = \FeedBag::get_feed_id_for_blog_id( $site_id );
662        }
663        $gdpr_applies = self::gdpr_applies();
664
665        // Fall back to the site's domain if the title is empty.
666        if ( empty( $site_name ) ) {
667            $site_name = function_exists( 'get_primary_redirect' ) ? get_primary_redirect() : $site_host;
668        }
669
670        $post_id       = 0;
671        $shortlink     = '';
672        $edit_link     = '';
673        $stats_link    = '';
674        $can_edit_post = false;
675        if ( is_singular() ) {
676            $is_singular   = true;
677            $post_id       = get_the_ID();
678            $shortlink     = wp_get_shortlink( $post_id );
679            $can_edit_post = $is_member && current_user_can( 'edit_post', $post_id );
680
681            /*
682             * Use the wp admin editor for VIPs (since they have custom editor
683             * plugins), or for super admins who are not members of the blog (until
684             * user-switching is implemented in Calypso).
685             */
686            $edit_link = add_query_arg(
687                array(
688                    'post'   => $post_id,
689                    'action' => 'edit',
690                ),
691                admin_url( 'post.php' )
692            );
693
694            $post_type = get_post_type();
695
696            // @phan-suppress-next-line PhanUndeclaredFunction -- Defined by jetpack-mu-wpcom, which is not a dependency; guarded by function_exists().
697            $should_use_calypso_links = empty( $post_type ) || function_exists( 'wpcom_should_disable_calypso_links' ) && ! wpcom_should_disable_calypso_links( 'edit.php?post_type=' . $post_type );
698
699            if ( $should_use_calypso_links && ! self::is_vip() && ( ! is_super_admin() || $is_member ) ) {
700                $path_prefix = null;
701                if ( in_array( $post_type, array( 'post', 'page' ), true ) ) {
702                    $path_prefix = $post_type;
703                } elseif ( in_array( $post_type, apply_filters( 'rest_api_allowed_post_types', array( 'post', 'page', 'revision' ) ), true ) ) {
704                    $path_prefix = sprintf( 'edit/%s', $post_type );
705                }
706
707                if ( $path_prefix ) {
708                    $edit_link = sprintf( 'https://wordpress.com/%s/%s/%d', $path_prefix, $site_slug, $post_id );
709                }
710            }
711
712            $stats_link = self::get_post_stats_url( $post_id, $site_id, $site_slug, $should_use_calypso_links );
713        }
714
715        $referer = '';
716        if ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
717            $referer = ( is_ssl() ? 'https' : 'http' ) . '://' . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) . sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
718        }
719
720        $can_comment           = is_single() && ! post_password_required( $post_id ) && comments_open( $post_id );
721        $can_reblog            = is_single() && self::can_reblog( $site_id, $post_id );
722        $can_edit_current_view = $can_edit_post || $can_customize_site;
723        $show_follow           = $can_follow && ! $can_edit_current_view && self::has_enough_posts();
724
725        $followers = '';
726        if ( $show_follow && ! $is_logged_in ) {
727            $subscribers_total = function_exists( 'wpcom_subs_total_for_blog' ) ? wpcom_subs_total_for_blog() : 0;
728            if ( ! empty( $subscribers_total ) && $subscribers_total > 24 ) {
729                /* translators: %s: number of subscribers */
730                $followers = sprintf( _n( 'Join %s other subscriber', 'Join %s other subscribers', $subscribers_total, 'jetpack-newsletter' ), number_format_i18n( $subscribers_total ) );
731            }
732        }
733
734        $blavatar = self::blavatar();
735
736        $classes = 'actnbr-' . str_replace( '/', '-', $theme_slug );
737        if ( $is_folded ) {
738            $classes .= ' actnbr-folded';
739        }
740
741        $dir = $is_rtl ? 'rtl' : 'ltr';
742
743        ?>
744            <div id="actionbar" dir="<?php echo esc_attr( $dir ); ?>" style="display: none;"
745                class="<?php echo esc_attr( $classes ); ?>" role="region" aria-label="<?php esc_attr_e( 'Site actions', 'jetpack-newsletter' ); ?>">
746            <span class="actnbr-live" aria-live="polite"></span>
747            <ul>
748                <?php
749                if ( $can_edit_post ) {
750                    ?>
751                        <li class="actnbr-btn actnbr-edit">
752                            <a href="<?php echo esc_url( $edit_link ); ?>">
753                                <?php self::icon( 'pencil', 20 ); ?>
754                                <span><?php esc_html_e( 'Edit', 'jetpack-newsletter' ); ?></span>
755                            </a>
756                        </li>
757                        <li class="actnbr-btn actnbr-stats">
758                            <a href="<?php echo esc_url( $stats_link ); ?>">
759                                <?php self::icon( 'chart-bar', 20 ); ?>
760                                <span><?php esc_html_e( 'Stats', 'jetpack-newsletter' ); ?></span>
761                            </a>
762                        </li>
763                    <?php
764                }
765
766                if ( $can_comment && ! $can_edit_current_view ) {
767                    ?>
768                        <li class="actnbr-btn">
769                            <a class="actnbr-action actnbr-actn-comment" href="<?php echo esc_url( get_comments_link( $post_id ) ); ?>">
770                                <?php self::icon( 'comment', 20 ); ?>
771                                <span><?php esc_html_e( 'Comment', 'jetpack-newsletter' ); ?>
772                            </span>
773                            </a>
774                        </li>
775                    <?php
776                }
777
778                if ( $can_reblog && ! $can_edit_current_view ) {
779                    ?>
780                        <li class="actnbr-btn">
781                            <a class="actnbr-action actnbr-actn-reblog" href="" role="button">
782                                <?php self::icon( 'reusable-block', 20 ); ?><span><?php esc_html_e( 'Reblog', 'jetpack-newsletter' ); ?></span>
783                            </a>
784                        </li>
785                    <?php
786                }
787
788                if ( $show_follow ) {
789                    ?>
790                        <li class="actnbr-btn actnbr-hidden">
791                            <?php self::follow_links( $is_following ); ?>
792                            <div class="actnbr-popover actnbr-panel actnbr-notice" id="follow-bubble" role="dialog" aria-label="<?php echo esc_attr( sprintf( /* translators: %s: site name */ __( 'Subscribe to %s', 'jetpack-newsletter' ), $site_name ) ); ?>">
793                                <div class="actnbr-follow-bubble">
794                                <?php self::site_title( $site_url, $site_name, $blavatar ); ?>
795                                <?php
796                                if ( $is_logged_in ) {
797                                    ?>
798                                    <div class="actnbr-panel__group actnbr-site-settings">
799                                        <div class="actnbr-message no-display" aria-live="polite"></div>
800                                        <div class="actnbr-site-settings__setting">
801                                            <span class="actnbr-site-settings__toggle">
802                                                <input class="actnbr-site-settings__toggle__input" id="toggle-input-notify-posts" type="checkbox" role="switch" />
803                                                <span class="actnbr-site-settings__toggle__track"></span>
804                                                <span class="actnbr-site-settings__toggle__thumb"></span>
805                                            </span>
806                                            <label for="toggle-input-notify-posts" class="components-toggle-control__label">
807                                                <?php esc_html_e( 'Notify me of new posts', 'jetpack-newsletter' ); ?>
808                                            </label>
809                                        </div>
810                                        <p class="actnbr-site-settings__details">
811                                            <?php esc_html_e( 'Receive web and mobile notifications for new posts from this site.', 'jetpack-newsletter' ); ?>
812                                        </p>
813                                        <div class="actnbr-site-settings__setting">
814                                            <span class="actnbr-site-settings__toggle">
815                                                <input class="actnbr-site-settings__toggle__input" id="toggle-input-email-posts" type="checkbox" role="switch" />
816                                                <span class="actnbr-site-settings__toggle__track"></span>
817                                                <span class="actnbr-site-settings__toggle__thumb"></span>
818                                            </span>
819                                            <label for="toggle-input-email-posts" class="components-toggle-control__label">
820                                                <?php esc_html_e( 'Email me new posts', 'jetpack-newsletter' ); ?>
821                                            </label>
822                                        </div>
823                                        <div class="actnbr-site-settings__details" id="email-new-posts-details">
824                                            <ul class="segmented-control" role="radiogroup" aria-label="<?php esc_attr_e( 'Email frequency', 'jetpack-newsletter' ); ?>">
825                                                <li class="segmented-control__item">
826                                                    <a class="segmented-control__link frequency-instantly" data-frequency="instantly" role="radio" aria-checked="false" tabindex="0"><?php esc_html_e( 'Instantly', 'jetpack-newsletter' ); ?></a>
827                                                </li>
828                                                <li class="segmented-control__item">
829                                                    <a class="segmented-control__link frequency-daily" data-frequency="daily" role="radio" aria-checked="false" tabindex="0"><?php esc_html_e( 'Daily', 'jetpack-newsletter' ); ?></a>
830                                                </li>
831                                                <li class="segmented-control__item">
832                                                    <a class="segmented-control__link frequency-weekly" data-frequency="weekly" role="radio" aria-checked="false" tabindex="0"><?php esc_html_e( 'Weekly', 'jetpack-newsletter' ); ?></a>
833                                                </li>
834                                            </ul>
835                                        </div>
836                                        <div class="actnbr-site-settings__setting">
837                                            <span class="actnbr-site-settings__toggle">
838                                                <input class="actnbr-site-settings__toggle__input" id="toggle-input-email-comments" type="checkbox" role="switch" />
839                                                <span class="actnbr-site-settings__toggle__track"></span>
840                                                <span class="actnbr-site-settings__toggle__thumb"></span>
841                                            </span>
842                                            <label for="toggle-input-email-comments" class="components-toggle-control__label">
843                                                <?php esc_html_e( 'Email me new comments', 'jetpack-newsletter' ); ?>
844                                            </label>
845                                        </div>
846                                    </div>
847                                    <?php
848                                } else {
849                                    ?>
850                                    <div class="actnbr-panel__group">
851                                        <div class="actnbr-message no-display" aria-live="polite"></div>
852                                        <form method="post" action="https://subscribe.wordpress.com" accept-charset="utf-8" class="no-display">
853                                            <?php
854                                            if ( $followers ) {
855                                                ?>
856                                                <div class="actnbr-follow-count"><?php echo esc_html( $followers ); ?></div>
857                                                <?php
858                                            }
859                                            ?>
860                                            <input type="email" name="email" placeholder="<?php esc_attr_e( 'Want updates? Enter your email', 'jetpack-newsletter' ); ?>" class="actnbr-email-field" aria-label="<?php esc_attr_e( 'Email address', 'jetpack-newsletter' ); ?>" />
861                                            <input type="hidden" name="action" value="subscribe" />
862                                            <input type="hidden" name="blog_id" value="<?php echo esc_attr( (string) $site_id ); ?>" />
863                                            <input type="hidden" name="source" value="<?php echo esc_url( $referer ); ?>" />
864                                            <input type="hidden" name="sub-type" value="actionbar-follow" />
865                                            <?php wp_nonce_field( 'blogsub_subscribe_' . $site_id, '_wpnonce', false, true ); ?>
866                                            <button type="submit" class="actnbr-button"><?php esc_html_e( 'Sign me up', 'jetpack-newsletter' ); ?></button>
867                                        </form>
868                                        <p class="actnbr-login-nudge">
869                                            <?php
870                                            echo wp_kses(
871                                                /* translators: %s is a URL */
872                                                sprintf( __( 'Have a WordPress.com account? <a href="%s">Log in now.</a>', 'jetpack-newsletter' ), esc_url( $login_url ) ),
873                                                array(
874                                                    'a' => array(
875                                                        'href' => array(),
876                                                    ),
877                                                )
878                                            );
879                                            ?>
880                                        </p>
881                                    </div>
882                                    <?php
883                                }
884                                ?>
885                                </div>
886                            </div>
887                        </li>
888                    <?php
889                }
890
891                /*
892                 * The privacy/GDPR button is special as it relies on window.__tcfapi being in the browser window object as well.
893                 * no-display is removed by JS if window.__tcfapi is present.
894                 */
895                if ( $gdpr_applies ) {
896                    ?>
897                        <li class="actnbr-btn no-display" onclick="javascript:__tcfapi( 'showUi' );">
898                            <a class="actnbr-action actnbr-actn-privacy" href="#" role="button">
899                                <?php self::icon( 'shield', 20 ); ?>
900                                <span><?php esc_html_e( 'Privacy', 'jetpack-newsletter' ); ?>
901                            </span>
902                            </a>
903                        </li>
904                    <?php
905                }
906                ?>
907                <li class="actnbr-ellipsis actnbr-hidden">
908                    <button type="button" class="actnbr-more-toggle" aria-haspopup="true" aria-expanded="false" aria-label="<?php esc_attr_e( 'More options', 'jetpack-newsletter' ); ?>">
909                        <?php self::icon( 'more-horizontal', 24 ); ?>
910                    </button>
911                    <div class="actnbr-popover actnbr-menu" role="menu" aria-label="<?php esc_attr_e( 'Site options', 'jetpack-newsletter' ); ?>">
912                        <?php
913                        // Site.
914                        self::menu_group(
915                            array(
916                                self::menu_item(
917                                    array(
918                                        'href'   => $site_url,
919                                        'label'  => $site_name,
920                                        'class'  => 'actnbr-sitename',
921                                        'before' => $blavatar,
922                                    )
923                                ),
924                            )
925                        );
926
927                        // This post or site.
928                        $items = array();
929                        if ( $is_singular ) {
930                            $items[] = sprintf(
931                                '<a role="menuitem" class="actnbr-menu__item actnbr-shortlink" href="%1$s"><span class="actnbr-menu__label actnbr-shortlink__text">%2$s</span><span class="actnbr-menu__icon actnbr-shortlink__icon">%3$s</span><span class="actnbr-menu__icon actnbr-shortlink__icon-copied">%4$s</span></a>',
932                                esc_url( $shortlink ),
933                                esc_html__( 'Copy shortlink', 'jetpack-newsletter' ),
934                                self::icon_markup( 'copy', 18 ),
935                                self::icon_markup( 'check', 18 )
936                            );
937                        }
938                        if ( $can_follow && $is_singular ) {
939                            $items[] = self::menu_item(
940                                array(
941                                    'href'  => 'https://wordpress.com/reader/blogs/' . (int) $site_id . '/posts/' . (int) $post_id,
942                                    'label' => __( 'View post in Reader', 'jetpack-newsletter' ),
943                                    'class' => 'actnbr-reader',
944                                )
945                            );
946                        }
947                        if ( $can_follow && ! $is_singular ) {
948                            $items[] = self::menu_item(
949                                array(
950                                    'href'  => 'https://wordpress.com/reader/' . ( $feed_id ? 'feeds/' . (int) $feed_id : 'blogs/' . (int) $site_id ),
951                                    'label' => __( 'View site in Reader', 'jetpack-newsletter' ),
952                                    'class' => 'actnbr-reader',
953                                )
954                            );
955                        }
956                        if ( $is_logged_in && ! $can_customize_site ) {
957                            $items[] = self::menu_item(
958                                array(
959                                    'href'  => $theme_url,
960                                    'label' => __( 'Get theme', 'jetpack-newsletter' ) . ': ' . wp_get_theme()->get( 'Name' ),
961                                    'class' => 'actnbr-theme',
962                                )
963                            );
964                        }
965                        self::menu_group( $items );
966
967                        // Account.
968                        $items = array();
969                        if ( $is_logged_in && $is_following ) {
970                            $items[] = self::menu_item(
971                                array(
972                                    'href'  => 'https://wordpress.com/read/subscriptions/' . (int) $subscription_id,
973                                    'label' => __( 'Manage subscription', 'jetpack-newsletter' ),
974                                    'class' => 'actnbr-follows',
975                                )
976                            );
977                        }
978                        if ( $is_logged_in && ! $is_following ) {
979                            $items[] = self::menu_item(
980                                array(
981                                    'href'  => 'https://wordpress.com/read/subscriptions?s=' . rawurlencode( (string) $site_host ),
982                                    'label' => __( 'Manage subscriptions', 'jetpack-newsletter' ),
983                                    'class' => 'actnbr-follows',
984                                )
985                            );
986                        }
987                        if ( ! $is_logged_in ) {
988                            $items[] = self::menu_item(
989                                array(
990                                    'href'  => 'https://subscribe.wordpress.com/',
991                                    'label' => __( 'Manage subscriptions', 'jetpack-newsletter' ),
992                                    'class' => 'actnbr-subs',
993                                )
994                            );
995                            $items[] = self::menu_item(
996                                array(
997                                    'href'  => $signup_url,
998                                    'label' => __( 'Sign up', 'jetpack-newsletter' ),
999                                    'class' => 'actnbr-signup',
1000                                )
1001                            );
1002                            $items[] = self::menu_item(
1003                                array(
1004                                    'href'  => $login_url,
1005                                    'label' => __( 'Log in', 'jetpack-newsletter' ),
1006                                    'class' => 'actnbr-login',
1007                                )
1008                            );
1009                        }
1010                        self::menu_group( $items );
1011
1012                        // Report.
1013                        $items = array();
1014                        if ( ! $can_customize_site ) {
1015                            $report_url = add_query_arg(
1016                                'report_url',
1017                                $is_singular ? get_permalink( $post_id ) : $site_url,
1018                                // phpcs:ignore WPCOM.I18nRules.LocalizedUrl.UnlocalizedUrl
1019                                'https://wordpress.com/abuse/'
1020                            );
1021                            $items[] = self::menu_item(
1022                                array(
1023                                    'href'  => $report_url,
1024                                    'label' => __( 'Report this content', 'jetpack-newsletter' ),
1025                                    'class' => 'flb-report',
1026                                    'icon'  => 'external',
1027                                    'blank' => true,
1028                                )
1029                            );
1030                        }
1031                        self::menu_group( $items );
1032
1033                        // Bar.
1034                        $items = array();
1035                        if ( $is_logged_in || $can_follow ) {
1036                            $items[] = self::menu_item(
1037                                array(
1038                                    'href'  => '',
1039                                    'label' => $is_folded ? __( 'Expand this bar', 'jetpack-newsletter' ) : __( 'Collapse this bar', 'jetpack-newsletter' ),
1040                                    'class' => 'actnbr-fold',
1041                                )
1042                            );
1043                        }
1044                        if ( current_user_can( 'manage_options' ) ) {
1045                            $items[] = self::menu_item(
1046                                array(
1047                                    'href'  => self::localized_url( 'https://wordpress.com/support/action-bar/#show-or-hide-the-action-bar' ),
1048                                    'label' => __( 'Turn off this bar', 'jetpack-newsletter' ),
1049                                    'class' => 'actnbr-turn-off',
1050                                    'icon'  => 'external',
1051                                    'blank' => true,
1052                                )
1053                            );
1054                        }
1055                        self::menu_group( $items );
1056                        ?>
1057                    </div>
1058                </li>
1059            </ul>
1060        </div>
1061        <?php
1062
1063        // Switch back to site language.
1064        self::restore_locale();
1065    }
1066
1067    /**
1068     * Whether the post can be reblogged and the viewer is allowed to reblog.
1069     *
1070     * @param int $site_id Blog ID.
1071     * @param int $post_id Post ID.
1072     * @return bool
1073     */
1074    private static function can_reblog( $site_id, $post_id ) {
1075        if ( ! function_exists( 'wpr_can_reblog_post' ) || ! function_exists( 'wpcom_can_user_make_a_reblog' ) ) {
1076            return false;
1077        }
1078        $post_ok = (bool) wpr_can_reblog_post( $site_id, $post_id );
1079        $user_ok = (bool) wpcom_can_user_make_a_reblog();
1080        return $post_ok && $user_ok;
1081    }
1082
1083    /**
1084     * Whether the WordAds consent manager is on and the visitor is in a GDPR region.
1085     *
1086     * @return bool
1087     */
1088    private static function gdpr_applies() {
1089        if ( ! class_exists( 'WordAds_Consent_Management_Provider' ) && defined( 'WP_CONTENT_DIR' ) ) {
1090            $provider_file = WP_CONTENT_DIR . '/blog-plugins/wordads-classes/class-wordads-consent-management-provider.php';
1091            if ( file_exists( $provider_file ) ) {
1092                require_once $provider_file;
1093            }
1094        }
1095        if ( ! class_exists( 'WordAds_Consent_Management_Provider' ) ) {
1096            return false;
1097        }
1098        // @phan-suppress-next-line PhanUndeclaredClassMethod -- wpcom-only class, guarded by class_exists above.
1099        return \WordAds_Consent_Management_Provider::is_feature_enabled() && \WordAds_Consent_Management_Provider::does_gdpr_apply();
1100    }
1101
1102    /**
1103     * Bump one of the whitelisted action bar stats.
1104     *
1105     * Logged-in clicks land in the `actionbar` MC stat, logged-out clicks in `actionbar_logged_out`.
1106     *
1107     * @param string $stat_value The stat to bump.
1108     */
1109    private static function bump_stat( $stat_value ) {
1110        $whitelist = array(
1111            'clicked_login_link',
1112            'clicked_login_nudge',
1113            'clicked_manage_subs_link',
1114            'clicked_signup_link',
1115            'clicked_site_title',
1116            'clicked_stats',
1117            'copied_shortlink',
1118            'customized',
1119            'edited',
1120            'expanded',
1121            'explored_theme',
1122            'folded',
1123            'followed',
1124            'managed_following',
1125            'privacy_clicked',
1126            'reported_content',
1127            'show_follow_form',
1128            'show_more_menu',
1129            'submit_follow_form',
1130            'unfollowed',
1131            'view_reader',
1132            'comment_clicked',
1133        );
1134
1135        if ( ! in_array( $stat_value, $whitelist, true ) ) {
1136            return;
1137        }
1138
1139        if ( ! function_exists( 'bump_stats_extras' ) ) {
1140            return;
1141        }
1142
1143        $stat_name = 'actionbar';
1144
1145        if ( ! is_user_logged_in() ) {
1146            $stat_name .= '_logged_out';
1147        }
1148
1149        bump_stats_extras( $stat_name, $stat_value );
1150    }
1151
1152    /*
1153     * The three ajax handlers below deliberately skip nonce verification, as the wpcom original did.
1154     * Batcache can serve a logged-in visitor a page whose localized nonce was minted for user 0, so a
1155     * check would reject the request and the folded state would never save. The writes are limited to
1156     * the caller's own fold flag and anonymous counters.
1157     */
1158
1159    /**
1160     * Ajax: remember that the user collapsed the bar.
1161     *
1162     * @return never
1163     */
1164    public static function fold() {
1165        self::bump_stat( 'folded' );
1166
1167        if ( is_user_logged_in() ) {
1168            self::set_folded( get_current_user_id(), true );
1169        }
1170
1171        die;
1172    }
1173
1174    /**
1175     * Ajax: forget that the user collapsed the bar.
1176     *
1177     * @return never
1178     */
1179    public static function unfold() {
1180        self::bump_stat( 'expanded' );
1181
1182        if ( is_user_logged_in() ) {
1183            self::set_folded( get_current_user_id(), false );
1184        }
1185
1186        die;
1187    }
1188
1189    /**
1190     * Ajax: bump a click stat sent by the JS.
1191     *
1192     * @return never
1193     */
1194    public static function ajax_stats() {
1195        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Whitelisted counter bump; see the note above.
1196        $stat_value = isset( $_REQUEST['stat'] ) ? sanitize_key( wp_unslash( $_REQUEST['stat'] ) ) : '';
1197
1198        self::bump_stat( $stat_value );
1199
1200        die;
1201    }
1202
1203    /**
1204     * Add the Action Bar visibility setting to the General settings page.
1205     *
1206     * Stored in the `wpcom_hide_action_bar` option.
1207     */
1208    public static function settings_field() {
1209        add_settings_field( 'wpcom_hide_action_bar', __( 'Action Bar visibility', 'jetpack-newsletter' ), array( __CLASS__, 'settings_field_display' ), 'general', 'default', array( 'label_for' => 'wpcom_hide_action_bar' ) );
1210
1211        register_setting( 'general', 'wpcom_hide_action_bar' );
1212    }
1213
1214    /**
1215     * Render the `wpcom_hide_action_bar` checkbox.
1216     */
1217    public static function settings_field_display() {
1218        ?>
1219        <input type="checkbox" id="wpcom_hide_action_bar" name="wpcom_hide_action_bar" value="1" <?php checked( 1, get_option( 'wpcom_hide_action_bar' ) ); ?> />
1220
1221        <?php esc_html_e( 'Hide the Action Bar on the front end of the site.', 'jetpack-newsletter' ); ?>
1222
1223        <p class="description"><a href="<?php echo esc_url( self::localized_url( 'https://wordpress.com/support/action-bar/' ) ); ?>" data-target="wpcom-help-center"><?php esc_html_e( 'Learn more about the Action Bar', 'jetpack-newsletter' ); ?></a>.</p>
1224        <?php
1225    }
1226}