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