Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
39.45% covered (danger)
39.45%
185 / 469
7.41% covered (danger)
7.41%
2 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 3
sharing_register_post_for_share_counts
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
sharing_maybe_enqueue_scripts
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
90
sharing_add_footer
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
110
sharing_add_header
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
sharing_process_requests
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
42
get_sharing_buttons_customisation_url
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
sharing_display
64.84% covered (warning)
64.84%
83 / 128
0.00% covered (danger)
0.00%
0 / 1
229.03
get_base_recaptcha_lang_code
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
Sharing_Service
41.30% covered (danger)
41.30%
102 / 247
13.33% covered (danger)
13.33%
2 / 15
1728.70
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_all_services_blog
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 get_all_services
92.31% covered (success)
92.31%
24 / 26
0.00% covered (danger)
0.00%
0 / 1
6.02
 new_service
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
42
 delete_service
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 set_blog_services
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
2
 get_blog_services
92.31% covered (success)
92.31%
36 / 39
0.00% covered (danger)
0.00%
0 / 1
19.16
 get_service
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 set_global_options
72.50% covered (warning)
72.50%
29 / 40
0.00% covered (danger)
0.00%
0 / 1
21.32
 get_global_options
47.62% covered (danger)
47.62%
10 / 21
0.00% covered (danger)
0.00%
0 / 1
37.29
 set_service
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 get_total
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
 get_services_total
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 get_posts_total
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
Sharing_Service_Total
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 2
20
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 cmp
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
Sharing_Post_Total
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 2
12
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 cmp
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Utilities to register and interact with a sharing service.
4 *
5 * Sharing_Service gets info about a service.
6 * Sharing_Service_Total and Sharing_Post_Total get stats data.
7 *
8 * @package automattic/jetpack
9 *
10 * phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
11 */
12
13// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files.
14
15use Automattic\Jetpack\Assets;
16use Automattic\Jetpack\Sync\Settings;
17
18if ( ! defined( 'ABSPATH' ) ) {
19    exit( 0 );
20}
21
22require_once __DIR__ . '/sharing-sources.php';
23
24define( 'WP_SHARING_PLUGIN_VERSION', JETPACK__VERSION );
25
26/**
27 * Interact with a sharing service.
28 */
29class Sharing_Service {
30    /**
31     * Should the service be available globally?
32     *
33     * @var bool
34     */
35    private $global = false;
36
37    /**
38     * Default sharing label.
39     *
40     * @var string
41     */
42    public $default_sharing_label = '';
43
44    /**
45     * Initialize the sharing service.
46     * Only run this method once upon module loading.
47     *
48     * @return void
49     */
50    public static function init() {
51        add_filter( 'the_content', 'sharing_display', 19 );
52        add_filter( 'the_excerpt', 'sharing_display', 19 );
53    }
54
55    /**
56     * Constructor.
57     */
58    public function __construct() {
59        $this->default_sharing_label = __( 'Share this:', 'jetpack' );
60    }
61
62    /**
63     * Gets a generic list of all services, without any config
64     *
65     * @return array
66     */
67    public function get_all_services_blog() {
68        $options  = get_option( 'sharing-options' );
69        $all      = $this->get_all_services();
70        $services = array();
71
72        foreach ( $all as $id => $name ) {
73            if ( isset( $all[ $id ] ) ) {
74                $config = array();
75
76                // Pre-load custom modules otherwise they won't know who they are
77                if ( str_starts_with( $id, 'custom-' ) && is_array( $options[ $id ] ) ) {
78                    $config = $options[ $id ];
79                }
80
81                $services[ $id ] = new $all[ $id ]( $id, $config );
82            }
83        }
84
85        return $services;
86    }
87
88    /**
89     * Gets a list of all available service names and classes
90     *
91     * @param bool $include_custom Include custom sharing services.
92     *
93     * @return array
94     */
95    public function get_all_services( $include_custom = true ) {
96        // Default services
97        // if you update this list, please update the REST API tests
98        // in bin/tests/api/suites/SharingTest.php
99        $services = array(
100            'print'            => 'Share_Print',
101            'email'            => 'Share_Email',
102            'facebook'         => 'Share_Facebook',
103            'linkedin'         => 'Share_LinkedIn',
104            'reddit'           => 'Share_Reddit',
105            'twitter'          => 'Share_Twitter',
106            'tumblr'           => 'Share_Tumblr',
107            'pinterest'        => 'Share_Pinterest',
108            'pocket'           => 'Share_Pocket',
109            'telegram'         => 'Share_Telegram',
110            'threads'          => 'Share_Threads',
111            'jetpack-whatsapp' => 'Jetpack_Share_WhatsApp',
112            'mastodon'         => 'Share_Mastodon',
113            'nextdoor'         => 'Share_Nextdoor',
114            'x'                => 'Share_X',
115            'bluesky'          => 'Share_Bluesky',
116        );
117
118        if ( is_multisite() && is_plugin_active( 'press-this/press-this-plugin.php' ) ) {
119            $services['press-this'] = 'Share_PressThis';
120        }
121
122        if ( $include_custom ) {
123            // Add any custom services in
124            $options = $this->get_global_options();
125            if ( isset( $options['custom'] ) ) {
126                foreach ( $options['custom'] as $custom_id ) {
127                    $services[ $custom_id ] = 'Share_Custom';
128                }
129            }
130        }
131
132        /**
133         * Filters the list of available Sharing Services.
134         *
135         * @module sharedaddy
136         *
137         * @since 1.1.0
138         *
139         * @param array $services Array of all available Sharing Services.
140         */
141        return apply_filters( 'sharing_services', $services );
142    }
143
144    /**
145     * Save a new custom sharing service.
146     *
147     * @param string $label Service name.
148     * @param string $url   Service sharing URL.
149     * @param string $icon  Service icon.
150     *
151     * @return bool|Share_Custom
152     */
153    public function new_service( $label, $url, $icon ) {
154        // Validate.
155        $label = trim( wp_html_excerpt( wp_kses( $label, array() ), 30 ) );
156        $url   = trim( esc_url_raw( $url ) );
157        $icon  = trim( esc_url_raw( $icon ) );
158
159        if ( $label && $url && $icon ) {
160            $options = get_option( 'sharing-options' );
161            if ( ! is_array( $options ) ) {
162                $options = array();
163            }
164
165            $service_id = 'custom-' . time();
166
167            // Add a new custom service
168            $options['global']['custom'][] = $service_id;
169            if ( false !== $this->global ) {
170                $this->global['custom'][] = $service_id;
171            }
172
173            update_option( 'sharing-options', $options );
174
175            // Create a custom service and set the options for it
176            $service = new Share_Custom(
177                $service_id,
178                array(
179                    'name' => $label,
180                    'url'  => $url,
181                    'icon' => $icon,
182                )
183            );
184            $this->set_service( $service_id, $service );
185
186            // Return the service
187            return $service;
188        }
189
190        return false;
191    }
192
193    /**
194     * Delete a sharing service.
195     *
196     * @param string $service_id Service ID.
197     *
198     * @return bool
199     */
200    public function delete_service( $service_id ) {
201        $options = get_option( 'sharing-options' );
202        if ( isset( $options[ $service_id ] ) ) {
203            unset( $options[ $service_id ] );
204        }
205
206        $key = array_search( $service_id, $options['global']['custom'], true );
207        if ( $key !== false ) {
208            unset( $options['global']['custom'][ $key ] );
209        }
210
211        update_option( 'sharing-options', $options );
212        return true;
213    }
214
215    /**
216     * Save enabled sharing services.
217     *
218     * @param array $visible Visible sharing services.
219     * @param array $hidden  Hidden sharing services (available under a dropdown).
220     *
221     * @return bool
222     */
223    public function set_blog_services( array $visible, array $hidden ) {
224        $services = $this->get_all_services();
225        // Validate the services
226        $available = array_keys( $services );
227
228        // Only allow services that we have defined
229        $hidden  = array_intersect( $hidden, $available );
230        $visible = array_intersect( $visible, $available );
231
232        // Ensure we don't have the same ones in hidden and visible
233        $hidden = array_diff( $hidden, $visible );
234
235        /**
236         * Control the state of the list of sharing services.
237         *
238         * @module sharedaddy
239         *
240         * @since 1.1.0
241         *
242         * @param array $args {
243         *  Array of options describing the state of the sharing services.
244         *
245         *  @type array $services List of all available service names and classes.
246         *  @type array $available Validated list of all available service names and classes.
247         *  @type array $hidden List of services hidden behind a "More" button.
248         *  @type array $visible List of visible services.
249         *  @type array $this->get_blog_services() Array of Sharing Services currently enabled.
250         * }
251         */
252        do_action(
253            'sharing_get_services_state',
254            array(
255                'services'          => $services,
256                'available'         => $available,
257                'hidden'            => $hidden,
258                'visible'           => $visible,
259                'currently_enabled' => $this->get_blog_services(),
260            )
261        );
262
263        return update_option(
264            'sharing-services',
265            array(
266                'visible' => $visible,
267                'hidden'  => $hidden,
268            )
269        );
270    }
271
272    /**
273     * Get information about enabled sharing services on the site.
274     *
275     * @return array
276     */
277    public function get_blog_services() {
278        $options  = get_option( 'sharing-options' );
279        $enabled  = get_option( 'sharing-services' );
280        $services = $this->get_all_services();
281
282        /**
283         * Check if options exist and are well formatted.
284         * This avoids issues on sites with corrupted options.
285         *
286         * @see https://github.com/Automattic/jetpack/issues/6121
287         */
288        if ( ! is_array( $options ) || ! isset( $options['button_style'] ) || ! isset( $options['global'] ) ) {
289            $global_options = array( 'global' => $this->get_global_options() );
290            $options        = is_array( $options )
291                ? array_merge( $options, $global_options )
292                : $global_options;
293        }
294
295        $global = $options['global'];
296
297        // Default services
298        if ( ! is_array( $enabled ) ) {
299            $enabled = array(
300                'visible' => array(
301                    'facebook',
302                    'x',
303                ),
304                'hidden'  => array(),
305            );
306
307            /**
308             * Filters the list of default Sharing Services.
309             *
310             * @module sharedaddy
311             *
312             * @since 1.1.0
313             *
314             * @param array $enabled Array of default Sharing Services.
315             */
316            $enabled = apply_filters( 'sharing_default_services', $enabled );
317        }
318
319        // Cleanup after any filters that may have produced duplicate services
320        if ( isset( $enabled['visible'] ) && is_array( $enabled['visible'] ) ) {
321            $enabled['visible'] = array_unique( $enabled['visible'] );
322        } else {
323            $enabled['visible'] = array();
324        }
325
326        if ( isset( $enabled['hidden'] ) && is_array( $enabled['hidden'] ) ) {
327            $enabled['hidden'] = array_unique( $enabled['hidden'] );
328        } else {
329            $enabled['hidden'] = array();
330        }
331
332        // Form the enabled services
333        $blog = array(
334            'visible' => array(),
335            'hidden'  => array(),
336        );
337
338        foreach ( $blog as $area => $stuff ) {
339            foreach ( (array) $enabled[ $area ] as $service ) {
340                if ( isset( $services[ $service ] ) ) {
341                    if ( ! isset( $options[ $service ] ) || ! is_array( $options[ $service ] ) ) {
342                        $options[ $service ] = array();
343                    }
344                    $blog[ $area ][ $service ] = new $services[ $service ]( $service, array_merge( $global, $options[ $service ] ) );
345                }
346            }
347        }
348
349        /**
350         * Filters the list of enabled Sharing Services.
351         *
352         * @module sharedaddy
353         *
354         * @since 1.1.0
355         *
356         * @param array $blog Array of enabled Sharing Services.
357         */
358        $blog = apply_filters( 'sharing_services_enabled', $blog );
359
360        // Add CSS for NASCAR
361        if ( ( is_countable( $blog['visible'] ) && count( $blog['visible'] ) ) || ( is_countable( $blog['hidden'] ) && count( $blog['hidden'] ) ) ) {
362            add_filter( 'post_flair_block_css', 'post_flair_service_enabled_sharing' );
363        }
364
365        // Convenience for checking if a service is present
366        $blog['all'] = array_flip( array_merge( array_keys( $blog['visible'] ), array_keys( $blog['hidden'] ) ) );
367        return $blog;
368    }
369
370    /**
371     * Get information about a specific enabled sharing service.
372     *
373     * @param string $service_name Service name.
374     *
375     * @return bool|Sharing_Source
376     */
377    public function get_service( $service_name ) {
378        $services = $this->get_blog_services();
379
380        if ( isset( $services['visible'][ $service_name ] ) ) {
381            return $services['visible'][ $service_name ];
382        }
383
384        if ( isset( $services['hidden'][ $service_name ] ) ) {
385            return $services['hidden'][ $service_name ];
386        }
387
388        return false;
389    }
390
391    /**
392     * Update global sharing options.
393     *
394     * @param array $data Array of new sharing options to save.
395     */
396    public function set_global_options( $data ) {
397        $options = get_option( 'sharing-options' );
398
399        // No options yet.
400        if ( ! is_array( $options ) ) {
401            $options = array();
402        }
403
404        // Defaults.
405        $options['global'] = array(
406            'button_style'  => 'icon-text',
407            'sharing_label' => $this->default_sharing_label,
408            'open_links'    => 'same',
409            'show'          => ! isset( $options['global'] ) ? array( 'post', 'page' ) : array(),
410            'custom'        => isset( $options['global']['custom'] ) ? $options['global']['custom'] : array(),
411        );
412
413        /**
414         * Filters global sharing settings.
415         *
416         * @module sharedaddy
417         *
418         * @since 1.1.0
419         *
420         * @param array $options['global'] Array of global sharing settings.
421         */
422        $options['global'] = apply_filters( 'sharing_default_global', $options['global'] );
423
424        // Validate options and set from our data
425        if (
426            isset( $data['button_style'] )
427            && in_array( $data['button_style'], array( 'icon-text', 'icon', 'text', 'official' ), true )
428        ) {
429            $options['global']['button_style'] = $data['button_style'];
430        }
431
432        if ( isset( $data['sharing_label'] ) ) {
433            if ( $this->default_sharing_label === $data['sharing_label'] ) {
434                $options['global']['sharing_label'] = false;
435            } else {
436                $options['global']['sharing_label'] = trim( wp_kses( stripslashes( $data['sharing_label'] ), array() ) );
437            }
438        }
439
440        if (
441            isset( $data['open_links'] )
442            && in_array( $data['open_links'], array( 'new', 'same' ), true )
443        ) {
444            $options['global']['open_links'] = $data['open_links'];
445        }
446
447        $shows   = array_values( get_post_types( array( 'public' => true ) ) );
448        $shows[] = 'index';
449        if ( isset( $data['show'] ) ) {
450            if ( is_scalar( $data['show'] ) ) {
451                switch ( $data['show'] ) {
452                    case 'posts':
453                        $data['show'] = array( 'post', 'page' );
454                        break;
455                    case 'index':
456                        $data['show'] = array( 'index' );
457                        break;
458                    case 'posts-index':
459                        $data['show'] = array( 'post', 'page', 'index' );
460                        break;
461                }
462            }
463
464            $data['show'] = array_intersect( $data['show'], $shows );
465            if ( $data['show'] ) {
466                $options['global']['show'] = $data['show'];
467            }
468        }
469
470        update_option( 'sharing-options', $options );
471        return $options['global'];
472    }
473
474    /**
475     * Get global sharing options for the site.
476     *
477     * @return array
478     */
479    public function get_global_options() {
480        if ( $this->global === false ) {
481            $options = get_option( 'sharing-options' );
482
483            if ( is_array( $options ) && isset( $options['global'] ) && is_array( $options['global'] ) ) {
484                $this->global = $options['global'];
485            } else {
486                $this->global = $this->set_global_options( $options );
487            }
488        }
489
490        if ( ! isset( $this->global['show'] ) ) {
491            $this->global['show'] = array( 'post', 'page' );
492        } elseif ( is_scalar( $this->global['show'] ) ) {
493            switch ( $this->global['show'] ) {
494                case 'posts':
495                    $this->global['show'] = array( 'post', 'page' );
496                    break;
497                case 'index':
498                    $this->global['show'] = array( 'index' );
499                    break;
500                case 'posts-index':
501                    $this->global['show'] = array( 'post', 'page', 'index' );
502                    break;
503            }
504        }
505
506        if ( ! isset( $this->global['sharing_label'] ) || false === $this->global['sharing_label'] || $this->global['sharing_label'] === 'Share this:' ) {
507            $this->global['sharing_label'] = $this->default_sharing_label;
508        }
509
510        return $this->global;
511    }
512
513    /**
514     * Save a sharing service for use.
515     *
516     * @param int                     $id Sharing unique ID.
517     * @param Sharing_Advanced_Source $service Sharing service.
518     *
519     * @return void
520     */
521    public function set_service( $id, Sharing_Advanced_Source $service ) {
522        // Update the options for this service
523        $options = get_option( 'sharing-options' );
524
525        // No options yet
526        if ( ! is_array( $options ) ) {
527            $options = array();
528        }
529
530        /**
531         * Get the state of a sharing button.
532         *
533         * @module sharedaddy
534         *
535         * @since 1.1.0
536         *
537         * @param array $args {
538         *  State of a sharing button.
539         *
540         *  @type string $id Service ID.
541         *  @type array $options Array of all sharing options.
542         *  @type array $service Details about a service.
543         * }
544         */
545        do_action(
546            'sharing_get_button_state',
547            array(
548                'id'      => $id,
549                'options' => $options,
550                'service' => $service,
551            )
552        );
553
554        $options[ $id ] = $service->get_options();
555
556        update_option( 'sharing-options', array_filter( $options ) );
557    }
558
559    /**
560     * Get stats for a site, a post, or a sharing service.
561     * Soon to come to a .org plugin near you!
562     *
563     * @param string|bool $service_name Service name.
564     * @param int|bool    $post_id      Post ID.
565     * @param int|bool    $_blog_id     Blog ID.
566     *
567     * @return int
568     */
569    public function get_total( $service_name = false, $post_id = false, $_blog_id = false ) {
570        global $wpdb, $blog_id;
571        if ( ! $_blog_id ) {
572            $_blog_id = $blog_id;
573        }
574        if ( $service_name === false ) {
575            if ( $post_id > 0 ) {
576                // total number of shares for this post
577                $sql       = $wpdb->prepare( 'SELECT SUM( count ) FROM sharing_stats WHERE blog_id = %d AND post_id = %d', $_blog_id, $post_id );
578                $cache_key = "sharing_service_get_total_b{$_blog_id}_p{$post_id}";
579            } else {
580                // total number of shares for this blog
581                $sql       = $wpdb->prepare( 'SELECT SUM( count ) FROM sharing_stats WHERE blog_id = %d', $_blog_id );
582                $cache_key = "sharing_service_get_total_b{$_blog_id}";
583            }
584        } elseif ( $post_id > 0 ) {
585            $sql       = $wpdb->prepare( 'SELECT SUM( count ) FROM sharing_stats WHERE blog_id = %d AND post_id = %d AND share_service = %s', $_blog_id, $post_id, $service_name );
586            $cache_key = "sharing_service_get_total_b{$_blog_id}_p{$post_id}_s{$service_name}";
587        } else {
588            $sql       = $wpdb->prepare( 'SELECT SUM( count ) FROM sharing_stats WHERE blog_id = %d AND share_service = %s', $_blog_id, $service_name );
589            $cache_key = "sharing_service_get_total_b{$_blog_id}_s{$service_name}";
590        }
591
592        $ret = wp_cache_get( $cache_key, 'sharing' );
593        if ( $ret === false ) {
594            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery -- Prepared above.
595            $ret = (int) $wpdb->get_var( $sql );
596            wp_cache_set( $cache_key, $ret, 'sharing', 5 * MINUTE_IN_SECONDS );
597        }
598        return $ret;
599    }
600
601    /**
602     * Get total stats for a site, for all sharing services.
603     *
604     * @param int|bool $post_id Post ID.
605     *
606     * @return array
607     */
608    public function get_services_total( $post_id = false ) {
609        $totals   = array();
610        $services = $this->get_blog_services();
611
612        if ( ! empty( $services ) && isset( $services['all'] ) ) {
613            foreach ( $services['all'] as $key => $value ) {
614                $totals[ $key ] = new Sharing_Service_Total( $key, $this->get_total( $key, $post_id ) );
615            }
616        }
617        usort( $totals, array( 'Sharing_Service_Total', 'cmp' ) );
618
619        return $totals;
620    }
621
622    /**
623     * Get sharing stats for all posts on the site.
624     *
625     * @return array
626     */
627    public function get_posts_total() {
628        $totals = array();
629        global $wpdb, $blog_id;
630
631        $cache_key = "sharing_service_get_posts_total_{$blog_id}";
632        $my_data   = wp_cache_get( $cache_key, 'sharing' );
633        if ( $my_data === false ) {
634            $my_data = $wpdb->get_results( $wpdb->prepare( 'SELECT post_id as id, SUM( count ) as total FROM sharing_stats WHERE blog_id = %d GROUP BY post_id ORDER BY count DESC ', $blog_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
635            wp_cache_set( $cache_key, $my_data, 'sharing', 5 * MINUTE_IN_SECONDS );
636        }
637
638        if ( ! empty( $my_data ) ) {
639            foreach ( $my_data as $row ) {
640                $totals[] = new Sharing_Post_Total( $row->id, $row->total );
641            }
642        }
643
644        usort( $totals, array( 'Sharing_Post_Total', 'cmp' ) );
645
646        return $totals;
647    }
648}
649
650/**
651 * Get stats for a specific sharing service.
652 */
653class Sharing_Service_Total {
654    /**
655     * Sharing service ID.
656     *
657     * @var int
658     */
659    public $id = '';
660
661    /**
662     * Service name.
663     *
664     * @var string
665     */
666    public $name = '';
667
668    /**
669     * Sharing service name.
670     *
671     * @var string
672     */
673    public $service = '';
674
675    /**
676     * Total number of shares for this service.
677     *
678     * @var string
679     */
680    public $total = 0;
681
682    /**
683     * Constructor.
684     *
685     * @param int $id      Service ID.
686     * @param int $total   Total shares.
687     */
688    public function __construct( $id, $total ) {
689        $services      = new Sharing_Service();
690        $this->id      = esc_html( $id );
691        $this->service = $services->get_service( $id );
692        $this->total   = (int) $total;
693
694        if ( $this->service instanceof Sharing_Source ) {
695            $this->name = $this->service->get_name();
696        }
697    }
698
699    /**
700     * Compare total shares between 2 posts.
701     *
702     * @param object $a Sharing_Service_Total object.
703     * @param object $b Sharing_Service_Total object.
704     *
705     * @return int -1, 0, or 1 if $a is <, =, or > $b
706     */
707    public static function cmp( $a, $b ) {
708        if ( $a->total === $b->total ) {
709            return $b->name <=> $a->name;
710        }
711        return $b->total <=> $a->total;
712    }
713}
714
715/**
716 * Get sharing stats for a specific post.
717 */
718class Sharing_Post_Total {
719    /**
720     * Sharing service ID.
721     *
722     * @var int
723     */
724    public $id = 0;
725
726    /**
727     * Total shares.
728     *
729     * @var int
730     */
731    public $total = 0;
732
733    /**
734     * Post title.
735     *
736     * @var string
737     */
738    public $title = '';
739
740    /**
741     * Post permalink.
742     *
743     * @var string
744     */
745    public $url = '';
746
747    /**
748     * Constructor.
749     *
750     * @param int $id      Service ID.
751     * @param int $total   Total shares.
752     */
753    public function __construct( $id, $total ) {
754        $this->id    = (int) $id;
755        $this->total = (int) $total;
756        $this->title = get_the_title( $this->id );
757        $this->url   = get_permalink( $this->id );
758    }
759
760    /**
761     * Compare total shares between 2 posts.
762     *
763     * @param object $a Sharing_Post_Total object.
764     * @param object $b Sharing_Post_Total object.
765     *
766     * @return int -1, 0, or 1 if $a is <, =, or > $b
767     */
768    public static function cmp( $a, $b ) {
769        if ( $a->total === $b->total ) {
770            return $b->id <=> $a->id;
771        }
772        return $b->total <=> $a->total;
773    }
774}
775
776/**
777 * Populate sharing counts global with a post we want to count shares for.
778 *
779 * @param int $post_id Post ID.
780 *
781 * @return void
782 */
783function sharing_register_post_for_share_counts( $post_id ) {
784    global $jetpack_sharing_counts;
785
786    if ( ! isset( $jetpack_sharing_counts ) || ! is_array( $jetpack_sharing_counts ) ) {
787        $jetpack_sharing_counts = array();
788    }
789
790    $jetpack_sharing_counts[ (int) $post_id ] = get_permalink( $post_id );
791}
792
793/**
794 * Determine whether we should load sharing scripts or not.
795 *
796 * @return bool
797 */
798function sharing_maybe_enqueue_scripts() {
799    $sharer         = new Sharing_Service();
800    $global_options = $sharer->get_global_options();
801
802    $enqueue = false;
803    if ( is_singular() && in_array( get_post_type(), $global_options['show'], true ) ) {
804        $enqueue = true;
805    } elseif (
806        in_array( 'index', $global_options['show'], true )
807        && (
808            is_home()
809            || is_front_page()
810            || is_archive()
811            || is_search()
812            || in_array( get_post_type(), $global_options['show'], true )
813        )
814    ) {
815        $enqueue = true;
816    }
817
818    /**
819     * Filter to decide when sharing scripts should be enqueued.
820     *
821     * @module sharedaddy
822     *
823     * @since 3.2.0
824     *
825     * @param bool $enqueue Decide if the sharing scripts should be enqueued.
826     */
827    return (bool) apply_filters( 'sharing_enqueue_scripts', $enqueue );
828}
829
830/**
831 * Add sharing JavaScript to the footer of a page.
832 *
833 * @return void
834 */
835function sharing_add_footer() {
836    if (
837        class_exists( 'Jetpack_AMP_Support' )
838        && Jetpack_AMP_Support::is_amp_request()
839    ) {
840        return;
841    }
842
843    global $jetpack_sharing_counts;
844
845    if (
846        /**
847         * Filter all JavaScript output by the sharing module.
848         *
849         * @module sharedaddy
850         *
851         * @since 1.1.0
852         *
853         * @param bool true Control whether the sharing module should add any JavaScript to the site. Default to true.
854         */
855        apply_filters( 'sharing_js', true )
856        && sharing_maybe_enqueue_scripts()
857    ) {
858        if (
859            /**
860             * Filter the display of sharing counts next to the sharing buttons.
861             *
862             * @module sharedaddy
863             *
864             * @since 3.2.0
865             *
866             * @param bool true Control the display of counters next to the sharing buttons. Default to true.
867             */
868            apply_filters( 'jetpack_sharing_counts', true )
869            && is_array( $jetpack_sharing_counts )
870            && count( $jetpack_sharing_counts )
871        ) :
872            $sharing_post_urls = array_filter( $jetpack_sharing_counts );
873            if ( $sharing_post_urls ) :
874                ?>
875
876    <script type="text/javascript">
877        window.WPCOM_sharing_counts = <?php echo wp_json_encode( array_flip( $sharing_post_urls ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>;
878    </script>
879                <?php
880            endif;
881        endif;
882
883        wp_enqueue_script( 'sharing-js' );
884        $sharing_js_options = array(
885            'lang'            => get_base_recaptcha_lang_code(),
886            /** This filter is documented in modules/sharedaddy/sharing-service.php */
887            'counts'          => apply_filters( 'jetpack_sharing_counts', true ),
888            'is_stats_active' => Jetpack::is_module_active( 'stats' ),
889        );
890        wp_localize_script( 'sharing-js', 'sharing_js_options', $sharing_js_options );
891    }
892    $sharer  = new Sharing_Service();
893    $enabled = $sharer->get_blog_services();
894    foreach ( array_merge( $enabled['visible'], $enabled['hidden'] ) as $service ) {
895        $service->display_footer();
896    }
897}
898
899/**
900 * Enqueue sharing CSS in head.
901 *
902 * @return void
903 */
904function sharing_add_header() {
905    $sharer  = new Sharing_Service();
906    $enabled = $sharer->get_blog_services();
907
908    foreach ( array_merge( $enabled['visible'], $enabled['hidden'] ) as $service ) {
909        $service->display_header();
910    }
911
912    if ( is_countable( $enabled['all'] ) && ( count( $enabled['all'] ) > 0 ) && sharing_maybe_enqueue_scripts() ) {
913        wp_enqueue_style( 'sharedaddy', plugin_dir_url( __FILE__ ) . 'sharing.css', array(), JETPACK__VERSION );
914        wp_enqueue_style( 'social-logos' );
915    }
916}
917add_action( 'wp_head', 'sharing_add_header', 1 );
918
919/**
920 * Launch sharing requests on page load when a specific query string is used.
921 *
922 * @return void
923 */
924function sharing_process_requests() {
925    global $post;
926
927    // Only process if: single post and share=X defined
928    if ( ( is_page() || is_single() ) && isset( $_GET['share'] ) && is_string( $_GET['share'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
929        $sharer = new Sharing_Service();
930
931        $service = $sharer->get_service( sanitize_text_field( wp_unslash( $_GET['share'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
932        if ( $service ) {
933            $service->process_request( $post, $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
934        }
935    }
936}
937
938// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only checking for the data being present.
939if ( isset( $_GET['share'] ) ) {
940    add_action( 'template_redirect', 'sharing_process_requests', 9 );
941}
942
943/**
944 * Gets the url to customise the sharing buttons in WP-Admin.
945 *
946 * @return string the customisation URL.
947 */
948function get_sharing_buttons_customisation_url() {
949    return admin_url( 'options-general.php?page=sharing' );
950}
951
952/**
953 * Append sharing links to text.
954 *
955 * @param string $text The original text to append sharing links onto.
956 * @param bool   $echo Where to echo the text or return.
957 *
958 * @return string The original $text with, if conditions are met, the sharing links.
959 */
960function sharing_display( $text = '', $echo = false ) {
961    global $post, $wp_current_filter;
962
963    if ( Settings::is_syncing() ) {
964        return $text;
965    }
966
967    // We require the post to not be empty and be an actual WordPress post object. If it's not - we just return.
968    if ( empty( $post ) || ! $post instanceof \WP_Post ) {
969        return $text;
970    }
971
972    if ( ( is_preview() || is_admin() ) && ! ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
973        return $text;
974    }
975
976    // Prevent from rendering sharing buttons in block which is fetched from REST endpoint by editor
977    if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
978        return $text;
979    }
980
981    // Do not output sharing buttons for ActivityPub requests.
982    if (
983        function_exists( '\Activitypub\is_activitypub_request' )
984        && \Activitypub\is_activitypub_request()
985    ) {
986        return $text;
987    }
988
989    // Don't output flair on excerpts.
990    if ( in_array( 'get_the_excerpt', (array) $wp_current_filter, true ) ) {
991        return $text;
992    }
993
994    // Ensure we don't display sharing buttons on post excerpts that are hooked inside the post content
995    if ( in_array( 'the_excerpt', (array) $wp_current_filter, true ) &&
996        in_array( 'the_content', (array) $wp_current_filter, true ) ) {
997        return $text;
998    }
999
1000    // Don't allow flair to be added to the_content more than once (prevent infinite loops).
1001    $done = false;
1002    foreach ( $wp_current_filter as $filter ) {
1003        if ( 'the_content' === $filter ) {
1004            if ( $done ) {
1005                return $text;
1006            } else {
1007                $done = true;
1008            }
1009        }
1010    }
1011
1012    // check whether we are viewing the front page and whether the front page option is checked.
1013    $options         = get_option( 'sharing-options' );
1014    $display_options = null;
1015
1016    if ( is_array( $options ) ) {
1017        $display_options = $options['global']['show'];
1018    }
1019
1020    if ( is_front_page() && ( is_array( $display_options ) && ! in_array( 'index', $display_options, true ) ) ) {
1021        return $text;
1022    }
1023
1024    if ( is_attachment() && in_array( 'the_excerpt', (array) $wp_current_filter, true ) ) {
1025        // Many themes run the_excerpt() conditionally on an attachment page, then run the_content().
1026        // We only want to output the sharing buttons once.  Let's stick with the_content().
1027        return $text;
1028    }
1029
1030    $sharer = new Sharing_Service();
1031    $global = $sharer->get_global_options();
1032
1033    $show = false;
1034    if ( ! is_feed() ) {
1035        if ( is_singular() && in_array( get_post_type(), $global['show'], true ) ) {
1036            $show = true;
1037        } elseif ( in_array( 'index', $global['show'], true ) && ( is_home() || is_front_page() || is_archive() || is_search() || in_array( get_post_type(), $global['show'], true ) ) ) {
1038            $show = true;
1039        }
1040    }
1041
1042    /**
1043     * Filter to decide if sharing buttons should be displayed.
1044     *
1045     * @module sharedaddy
1046     *
1047     * @since 1.1.0
1048     *
1049     * @param bool $show Should the sharing buttons be displayed.
1050     * @param WP_Post $post The post to share.
1051     */
1052    $show = apply_filters( 'sharing_show', $show, $post );
1053
1054    // Disabled for this post?
1055    $switched_status = get_post_meta( $post->ID, 'sharing_disabled', false );
1056
1057    if ( ! empty( $switched_status ) ) {
1058        $show = false;
1059    }
1060
1061    // Is the post private?
1062    $post_status = get_post_status( $post->ID );
1063
1064    if ( 'private' === $post_status ) {
1065        $show = false;
1066    }
1067
1068    // Hide on password protected posts unless password is provided.
1069    if ( post_password_required( $post->ID ) ) {
1070            $show = false;
1071    }
1072
1073    /**
1074     * Filter the Sharing buttons' Ajax action name Jetpack checks for.
1075     * This allows the use of the buttons with your own Ajax implementation.
1076     *
1077     * @module sharedaddy
1078     *
1079     * @since 7.3.0
1080     *
1081     * @param string $sharing_ajax_action_name Name of the Sharing buttons' Ajax action.
1082     */
1083    $ajax_action = apply_filters( 'sharing_ajax_action', 'get_latest_posts' );
1084
1085    // Allow to be used in ajax requests for latest posts.
1086    if (
1087        defined( 'DOING_AJAX' )
1088        && DOING_AJAX
1089        && isset( $_REQUEST['action'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce handling happens within each custom implementation.
1090        && $ajax_action === $_REQUEST['action'] // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce handling happens within each custom implementation.
1091    ) {
1092        $show = true;
1093    }
1094
1095    $sharing_content = '';
1096    $enabled         = false;
1097
1098    if ( $show ) {
1099        /**
1100         * Filters the list of enabled Sharing Services.
1101         *
1102         * @module sharedaddy
1103         *
1104         * @since 2.2.3
1105         *
1106         * @param array $sharer->get_blog_services() Array of Sharing Services currently enabled.
1107         */
1108        $enabled = apply_filters( 'sharing_enabled', $sharer->get_blog_services() );
1109
1110        if ( is_countable( $enabled['all'] ) && ( count( $enabled['all'] ) > 0 ) ) {
1111            $dir = get_option( 'text_direction' );
1112
1113            // Wrapper.
1114            $sharing_content .= '<div class="sharedaddy sd-sharing-enabled"><div class="robots-nocontent sd-block sd-social sd-social-' . ( $global['button_style'] ?? 'icon-text' ) . ' sd-sharing">';
1115            if ( '' !== $global['sharing_label'] ) {
1116                $sharing_content .= sprintf(
1117                    /**
1118                     * Filter the sharing buttons' headline structure.
1119                     *
1120                     * @module sharedaddy
1121                     *
1122                     * @since 4.4.0
1123                     *
1124                     * @param string $sharing_headline Sharing headline structure.
1125                     * @param string $global['sharing_label'] Sharing title.
1126                     * @param string $sharing Module name.
1127                     */
1128                    apply_filters( 'jetpack_sharing_headline_html', '<h3 class="sd-title">%s</h3>', $global['sharing_label'], 'sharing' ),
1129                    esc_html( $global['sharing_label'] )
1130                );
1131            }
1132            $sharing_content .= '<div class="sd-content"><ul>';
1133
1134            // Visible items.
1135            $visible = '';
1136            foreach ( $enabled['visible'] as $service ) {
1137                $klasses = array( 'share-' . $service->get_class() );
1138                if ( $service->is_deprecated() ) {
1139                    if ( ! current_user_can( 'manage_options' ) ) {
1140                        continue;
1141                    }
1142                    $klasses[] = 'share-deprecated';
1143                }
1144                // Individual HTML for sharing service.
1145                $visible .= '<li class="' . implode( ' ', $klasses ) . '">' . $service->get_display( $post ) . '</li>';
1146            }
1147
1148            $parts         = array();
1149            $parts[]       = $visible;
1150            $count_hidden  = is_countable( $enabled['hidden'] ) ? count( $enabled['hidden'] ) : 0;
1151            $count_visible = is_countable( $enabled['visible'] ) ? count( $enabled['visible'] ) : 0;
1152            if ( $count_hidden > 0 ) {
1153                if ( $count_visible > 0 ) {
1154                    $expand = __( 'More', 'jetpack' );
1155                } else {
1156                    $expand = __( 'Share', 'jetpack' );
1157                }
1158                $parts[] = '<li><a href="#" class="sharing-anchor sd-button share-more"><span>' . $expand . '</span></a></li>';
1159            }
1160
1161            if ( 'rtl' === $dir ) {
1162                $parts = array_reverse( $parts );
1163            }
1164
1165            $sharing_content .= implode( '', $parts );
1166            $sharing_content .= '<li class="share-end"></li></ul>';
1167
1168            // Link to customization options if user can manage them.
1169            if ( current_user_can( 'manage_options' ) ) {
1170                $link_url = get_sharing_buttons_customisation_url();
1171                if ( ! empty( $link_url ) ) {
1172                    $link_text        = __( 'Customize buttons', 'jetpack' );
1173                    $sharing_content .= '<p class="share-customize-link"><a href="' . esc_url( $link_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $link_text ) . '</a></p>';
1174                }
1175            }
1176
1177            if ( $count_hidden > 0 ) {
1178                $sharing_content .= '<div class="sharing-hidden"><div class="inner" style="display: none;';
1179
1180                if ( $count_hidden === 1 ) {
1181                    $sharing_content .= 'width:150px;';
1182                }
1183
1184                $sharing_content .= '">';
1185
1186                if ( $count_hidden === 1 ) {
1187                    $sharing_content .= '<ul style="background-image:none;">';
1188                } else {
1189                    $sharing_content .= '<ul>';
1190                }
1191
1192                foreach ( $enabled['hidden'] as $service ) {
1193                    // Individual HTML for sharing service.
1194                    $klasses = array( 'share-' . $service->get_class() );
1195                    if ( $service->is_deprecated() ) {
1196                        if ( ! current_user_can( 'manage_options' ) ) {
1197                            continue;
1198                        }
1199                        $klasses[] = 'share-deprecated';
1200                    }
1201                    $sharing_content .= '<li class="' . implode( ' ', $klasses ) . '">';
1202                    $sharing_content .= $service->get_display( $post );
1203                    $sharing_content .= '</li>';
1204                }
1205
1206                // End of wrapper.
1207                $sharing_content .= '<li class="share-end"></li></ul></div></div>';
1208            }
1209
1210            $sharing_content .= '</div></div></div>';
1211
1212            // Register our JS.
1213            if ( defined( 'JETPACK__VERSION' ) ) {
1214                $ver = JETPACK__VERSION;
1215            } else {
1216                $ver = '20211226';
1217            }
1218
1219            // @todo: Investigate if we can load this JS in the footer instead.
1220            wp_register_script(
1221                'sharing-js',
1222                Assets::get_file_url_for_environment(
1223                    '_inc/build/sharedaddy/sharing.min.js',
1224                    'modules/sharedaddy/sharing.js'
1225                ),
1226                array(),
1227                $ver,
1228                false
1229            );
1230
1231            // Enqueue scripts for the footer.
1232            add_action( 'wp_footer', 'sharing_add_footer' );
1233        }
1234    }
1235
1236    /**
1237     * Filters the content markup of the Jetpack sharing links
1238     *
1239     * @module sharedaddy
1240     *
1241     * @since 3.8.0
1242     * @since 6.2.0 Started sending $enabled as a second parameter.
1243     *
1244     * @param string $sharing_content Content markup of the Jetpack sharing links
1245     * @param array  $enabled         Array of Sharing Services currently enabled.
1246     */
1247    $sharing_markup = apply_filters( 'jetpack_sharing_display_markup', $sharing_content, $enabled );
1248
1249    if ( $echo ) {
1250        echo $text . $sharing_markup; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1251    } else {
1252        return $text . $sharing_markup;
1253    }
1254}
1255
1256/**
1257 * Get reCAPTCHA language code based off the language code of the site.
1258 *
1259 * @return string
1260 */
1261function get_base_recaptcha_lang_code() {
1262    $base_recaptcha_lang_code_mapping = array(
1263        'en'    => 'en',
1264        'nl'    => 'nl',
1265        'fr'    => 'fr',
1266        'fr-be' => 'fr',
1267        'fr-ca' => 'fr',
1268        'fr-ch' => 'fr',
1269        'de'    => 'de',
1270        'pt'    => 'pt',
1271        'pt-br' => 'pt',
1272        'ru'    => 'ru',
1273        'es'    => 'es',
1274        'tr'    => 'tr',
1275    );
1276
1277    $blog_lang_code = get_bloginfo( 'language' );
1278    if ( isset( $base_recaptcha_lang_code_mapping[ $blog_lang_code ] ) ) {
1279        return $base_recaptcha_lang_code_mapping[ $blog_lang_code ];
1280    }
1281
1282    // if no base mapping is found return default 'en'
1283    return 'en';
1284}
1285
1286Sharing_Service::init();