Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.49% covered (warning)
58.49%
31 / 53
59.26% covered (warning)
59.26%
16 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
Environment
58.49% covered (warning)
58.49%
31 / 53
59.26% covered (warning)
59.26%
16 / 27
271.35
0.00% covered (danger)
0.00%
0 / 1
 is_block_theme
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_simple_site
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sharing_module_running
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 legacy_sharing_switched_off
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 legacy_sharing_supported
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 settings_screen_supported
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 jetpack_blocks_load
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 likes_supported
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 is_connected
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_connection_ready
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 is_offline_mode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 likes_module_running
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 legacy_likes_switched_off
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 comment_likes_module_running
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 comment_likes_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 comment_likes_follow_likes_settings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 likes_settings_in_use
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 twitter_site_tag_used
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 module_active
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 sharing_block_registered
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 like_block_registered
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 single_template_editor_url
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 page_template_editor_url
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 saved_template_content
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 template_editor_url
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 block_template
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 block_is_registered
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Reads the site state that Settings > Sharing branches on.
4 *
5 * @package automattic/jetpack-sharing-likes
6 */
7
8declare( strict_types = 1 );
9
10namespace Automattic\Jetpack\Sharing_Likes\Settings;
11
12use Automattic\Jetpack\Connection\Manager as Connection_Manager;
13use Automattic\Jetpack\Modules;
14use Automattic\Jetpack\Sharing_Likes\Block_Names;
15use Automattic\Jetpack\Status;
16use Automattic\Jetpack\Status\Host;
17
18/**
19 * The facts Settings > Sharing branches on, read in one place so the sections
20 * and the state resolver stay free of environment lookups.
21 */
22final class Environment {
23
24    /**
25     * Memoised Site Editor URL, since resolving the template hits the theme's files.
26     *
27     * @var string|null
28     */
29    private static $single_template_editor_url = null;
30
31    /**
32     * Memoised Site Editor URL for the page template, for the same reason.
33     *
34     * @var string|null
35     */
36    private static $page_template_editor_url = null;
37
38    /**
39     * Memoised templates, by blog and template ID, since building one applies block hooks.
40     *
41     * @var array<string, \WP_Block_Template|null>
42     */
43    private static $block_templates = array();
44
45    /**
46     * Whether the active theme is a block theme.
47     */
48    public static function is_block_theme(): bool {
49        return wp_is_block_theme();
50    }
51
52    /**
53     * Whether this is a WordPress.com Simple site.
54     */
55    public static function is_simple_site(): bool {
56        return ( new Host() )->is_wpcom_simple();
57    }
58
59    /**
60     * Whether sharing buttons can still produce output.
61     */
62    public static function sharing_module_running(): bool {
63        return self::legacy_sharing_supported() && self::module_active( 'sharedaddy' );
64    }
65
66    /**
67     * Whether every sharing service has been removed, so sharedaddy renders nothing.
68     *
69     * An absent option is not that: `Sharing_Service::get_blog_services()` falls
70     * back to its default services when the option was never saved.
71     */
72    public static function legacy_sharing_switched_off(): bool {
73        $services = get_option( 'sharing-services' );
74
75        if ( ! is_array( $services ) ) {
76            return false;
77        }
78
79        return empty( $services['visible'] ) && empty( $services['hidden'] );
80    }
81
82    /**
83     * Whether this site can have legacy sharing buttons at all.
84     *
85     * The module needs no connection, but `Jetpack::load_modules()` includes
86     * nothing on a site that is neither connected nor in offline mode, and
87     * `Modules::activate()` refuses there too: an active module renders no
88     * buttons, and offering to turn it on would do nothing. Simple loads
89     * sharedaddy from `post-flair.php` without either.
90     */
91    public static function legacy_sharing_supported(): bool {
92        return self::is_simple_site() || self::is_connected() || self::is_offline_mode();
93    }
94
95    /**
96     * Whether Settings > Sharing has anything to offer this site.
97     *
98     * Neither the legacy features nor their blocks load on a site that is neither
99     * connected nor in offline mode. Checking sharing alone is enough: any site
100     * that can have Like buttons (connected, or Simple) passes that check too.
101     */
102    public static function settings_screen_supported(): bool {
103        return self::legacy_sharing_supported();
104    }
105
106    /**
107     * Whether Jetpack's blocks load on this site, as `Jetpack_Gutenberg::should_load()` decides:
108     * a ready connection or offline mode, then the Blocks module, which its `jetpack_gutenberg` filter can override.
109     *
110     * True on Simple without asking for modules or a connection, both of which it lacks.
111     */
112    public static function jetpack_blocks_load(): bool {
113        if ( ! self::is_simple_site() && ! self::is_connection_ready() && ! self::is_offline_mode() ) {
114            return false;
115        }
116
117        /** This filter is documented in projects/plugins/jetpack/class.jetpack-gutenberg.php */
118        return (bool) apply_filters( 'jetpack_gutenberg', self::module_active( 'blocks' ) );
119    }
120
121    /**
122     * Whether this site can have Like buttons at all.
123     *
124     * The Likes module declares `Requires Connection: Yes`, so without a
125     * WordPress.com connection it cannot be activated and its buttons cannot
126     * render: the widget is keyed on the blog ID a connection provides.
127     * Offline mode keeps a connected site's tokens, but `Jetpack::load_modules()`
128     * skips every module that requires a connection until it ends.
129     */
130    public static function likes_supported(): bool {
131        return self::is_simple_site() || ( self::is_connected() && ! self::is_offline_mode() );
132    }
133
134    /**
135     * Whether the site holds a WordPress.com connection.
136     */
137    private static function is_connected(): bool {
138        return ( new Connection_Manager( 'jetpack' ) )->is_connected();
139    }
140
141    /**
142     * Whether the connection is ready for Jetpack's modules and blocks, as `Jetpack::is_connection_ready()` decides.
143     *
144     * WordPress.com's Atomic hosting filters it off on a site whose connection has no owner.
145     */
146    private static function is_connection_ready(): bool {
147        $manager = new Connection_Manager( 'jetpack' );
148
149        /** This filter is documented in projects/plugins/jetpack/class.jetpack.php */
150        return (bool) apply_filters( 'jetpack_is_connection_ready', $manager->is_connected(), $manager );
151    }
152
153    /**
154     * Whether the site is in offline mode, where it cannot connect to WordPress.com either.
155     */
156    public static function is_offline_mode(): bool {
157        return ( new Status() )->is_offline_mode();
158    }
159
160    /**
161     * Whether Like buttons can still produce output.
162     *
163     * The module can be left active in the options through a disconnect,
164     * which is why support is checked too.
165     */
166    public static function likes_module_running(): bool {
167        return self::likes_supported() && self::module_active( 'likes' );
168    }
169
170    /**
171     * Whether Simple's posts show neither a Like nor a Reblog button, bar those that opted in individually.
172     *
173     * Both count because the legacy widget renders for either. Jetpack and Atomic never
174     * render Reblog, and switch to the block by turning the module off instead.
175     */
176    public static function legacy_likes_switched_off(): bool {
177        return self::is_simple_site()
178            && ! Likes_Options::likes_enabled_sitewide()
179            && ! Likes_Options::reblogs_enabled_sitewide();
180    }
181
182    /**
183     * Whether comments can still be liked.
184     *
185     * Comment Likes is its own module, but it reads the Likes settings: both
186     * `disabled_likes` and the shared placement gate `is_likes_visible()`. So a
187     * site running it without the Likes module still needs those controls.
188     */
189    public static function comment_likes_module_running(): bool {
190        return self::likes_supported() && self::module_active( 'comment-likes' );
191    }
192
193    /**
194     * Whether Comment Likes are switched on for this site.
195     *
196     * - On WordPress.com Simple, which has no modules: the `jetpack_comment_likes_enabled` option.
197     * - On Atomic and self-hosted Jetpack sites: the Comment Likes module, which never reads that option.
198     */
199    public static function comment_likes_enabled(): bool {
200        return self::is_simple_site() ? Likes_Options::comment_likes_enabled() : self::comment_likes_module_running();
201    }
202
203    /**
204     * Whether Comment Likes read the Likes settings: the sitewide default and placement. Simple's do not.
205     */
206    public static function comment_likes_follow_likes_settings(): bool {
207        return ! self::is_simple_site() && self::comment_likes_module_running();
208    }
209
210    /**
211     * Whether anything on this site still reads the Likes settings.
212     *
213     * Both features are gated on `disabled_likes` and the shared placement, so
214     * either one running makes the Likes placement defaults the ones that apply.
215     */
216    public static function likes_settings_in_use(): bool {
217        return self::likes_module_running() || self::comment_likes_module_running();
218    }
219
220    /**
221     * Whether anything on this site reads the Twitter Site Tag option.
222     *
223     * Twitter Cards read it, and through them the Sharing Buttons block's X `via`.
224     * Simple's Twitter Cards read `twitter_via`, which wpcom serves and saves this option from.
225     */
226    public static function twitter_site_tag_used(): bool {
227        /** This filter is documented in projects/plugins/jetpack/class.jetpack.php */
228        return ! apply_filters( 'jetpack_disable_twitter_cards', false );
229    }
230
231    /**
232     * Whether a module is active.
233     *
234     * Simple has no modules: wpcom loads both features on every request, so
235     * every module counts as active there whatever the option holds.
236     *
237     * @param string $slug Module slug.
238     */
239    private static function module_active( string $slug ): bool {
240        return self::is_simple_site() || ( new Modules() )->is_active( $slug );
241    }
242
243    /**
244     * Whether the Sharing Buttons block is available to offer as an alternative.
245     */
246    public static function sharing_block_registered(): bool {
247        return self::block_is_registered( Block_Names::SHARING_BUTTONS );
248    }
249
250    /**
251     * Whether the Like block is available to offer as an alternative.
252     */
253    public static function like_block_registered(): bool {
254        return self::block_is_registered( Block_Names::LIKE );
255    }
256
257    /**
258     * Site Editor URL for the active theme's single post template.
259     *
260     * Empty when the theme has no `single` template, which callers treat as
261     * "offer no Site Editor link" rather than linking somewhere that 404s: a
262     * block theme shipping only `index.html` is the case that hits this.
263     */
264    public static function single_template_editor_url(): string {
265        if ( null === self::$single_template_editor_url ) {
266            self::$single_template_editor_url = self::template_editor_url( 'single' );
267        }
268
269        return self::$single_template_editor_url;
270    }
271
272    /**
273     * Site Editor URL for the active theme's page template, empty when it has none.
274     *
275     * @since 0.4.0
276     */
277    public static function page_template_editor_url(): string {
278        if ( null === self::$page_template_editor_url ) {
279            self::$page_template_editor_url = self::template_editor_url( 'page' );
280        }
281
282        return self::$page_template_editor_url;
283    }
284
285    /**
286     * Content of one of the active theme's templates as saved in the Site Editor; null while the theme's file serves it.
287     *
288     * Read from the post, since `get_block_template()` returns the content with block hooks already applied.
289     *
290     * @since 0.6.0
291     *
292     * @param string $slug Template slug.
293     */
294    public static function saved_template_content( string $slug ): ?string {
295        if ( ! self::is_block_theme() ) {
296            return null;
297        }
298
299        $template = self::block_template( $slug );
300        $post     = $template && $template->wp_id ? get_post( $template->wp_id ) : null;
301
302        return $post instanceof \WP_Post ? $post->post_content : null;
303    }
304
305    /**
306     * Site Editor URL for one of the active theme's templates, empty when it has none.
307     *
308     * @param string $slug Template slug.
309     */
310    private static function template_editor_url( string $slug ): string {
311        if ( ! self::block_template( $slug ) ) {
312            return '';
313        }
314
315        // `add_query_arg()` does not encode, and the Site Editor router wants `p` percent-encoded.
316        return admin_url( 'site-editor.php?p=' . rawurlencode( '/wp_template/' . get_stylesheet() . '//' . $slug ) . '&canvas=edit' );
317    }
318
319    /**
320     * One of the active theme's templates, as `get_block_template()` resolves it.
321     *
322     * @param string $slug Template slug.
323     */
324    private static function block_template( string $slug ): ?\WP_Block_Template {
325        $stylesheet = get_stylesheet();
326
327        if ( ! $stylesheet ) {
328            return null;
329        }
330
331        $id  = $stylesheet . '//' . $slug;
332        $key = get_current_blog_id() . ':' . $id;
333
334        if ( ! array_key_exists( $key, self::$block_templates ) ) {
335            $template                      = get_block_template( $id );
336            self::$block_templates[ $key ] = $template instanceof \WP_Block_Template ? $template : null;
337        }
338
339        return self::$block_templates[ $key ];
340    }
341
342    /**
343     * Whether a block is registered, and so available to add to a template.
344     *
345     * Registration happens on `init`, well before this screen renders.
346     *
347     * @param string $name Fully qualified block name.
348     */
349    private static function block_is_registered( string $name ): bool {
350        return \WP_Block_Type_Registry::get_instance()->is_registered( $name );
351    }
352}