Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.57% covered (warning)
67.57%
50 / 74
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Unavailable_Blocks
67.57% covered (warning)
67.57%
50 / 74
50.00% covered (danger)
50.00%
4 / 8
80.21
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_site_wide_notice
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 get_editor_data
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
7
 get_site_wide_reason
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 describe_inactive_modules
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 get_inactive_module_details
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
8.23
 get_shipped_blocks
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
5.39
 is_forced_off
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Explains, in the block editor, why a saved Jetpack block is unavailable.
4 *
5 * @package automattic/jetpack
6 */
7
8namespace Automattic\Jetpack\Plugin;
9
10use Automattic\Jetpack\Assets;
11use Automattic\Jetpack\Constants;
12use Automattic\Jetpack\Modules;
13use Automattic\Jetpack\Status;
14use Automattic\Jetpack\Status\Host;
15use Jetpack;
16use Jetpack_Gutenberg;
17use Jetpack_Modules_Overrides;
18
19/**
20 * Works out why Jetpack is not registering its blocks, so the editor can say so
21 * instead of showing core's generic "doesn't include support" warning.
22 */
23class Unavailable_Blocks {
24
25    const SCRIPT_HANDLE = 'jetpack-unavailable-blocks-notice';
26
27    /**
28     * Blocks that stay registered in the editor while the Blocks module is inactive.
29     */
30    const INDEPENDENT_BLOCKS = array( 'jetpack/contact-form' );
31
32    /**
33     * Top-level blocks that packages register, which the plugin's block manifest does not list.
34     */
35    const PACKAGE_BLOCKS = array(
36        'jetpack/contact-form',
37        'jetpack/podcast-episode',
38        'jetpack/paypal-payment-buttons',
39        'videopress/video',
40        'videopress/all-playlists',
41        'videopress/latest-videos-playlist',
42        'videopress/playlist',
43    );
44
45    /**
46     * Manifest blocks that a plan or site setting can also keep unregistered.
47     */
48    const CONDITIONAL_BLOCKS = array( 'jetpack/cookie-consent', 'jetpack/voice-to-content', 'jetpack/wordads' );
49
50    /**
51     * Hook into the block editor.
52     *
53     * @since $$next-version$$
54     */
55    public static function init() {
56        add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_site_wide_notice' ) );
57    }
58
59    /**
60     * Load the standalone notice when no Jetpack block is registered at all.
61     *
62     * Otherwise Jetpack's own editor bundle is on the page and carries the same notice.
63     *
64     * @since $$next-version$$
65     */
66    public static function enqueue_site_wide_notice() {
67        if ( ! wp_should_load_block_editor_scripts_and_styles() ) {
68            return;
69        }
70
71        $data = self::get_editor_data();
72        if ( null === $data || null === $data['reason'] ) {
73            return;
74        }
75
76        Assets::register_script(
77            self::SCRIPT_HANDLE,
78            Jetpack_Gutenberg::get_blocks_directory() . 'unavailable-blocks-notice.js',
79            JETPACK__PLUGIN_FILE,
80            array(
81                'textdomain' => 'jetpack',
82                'enqueue'    => true,
83            )
84        );
85        wp_add_inline_script(
86            self::SCRIPT_HANDLE,
87            'var Jetpack_Unavailable_Blocks = ' . wp_json_encode( $data, JSON_HEX_TAG | JSON_HEX_AMP ) . ';',
88            'before'
89        );
90    }
91
92    /**
93     * Get what the standalone notice needs when no Jetpack block from the Blocks module is registered.
94     *
95     * @since $$next-version$$
96     *
97     * @return array|null Null when there is nothing site-wide to explain.
98     */
99    public static function get_editor_data() {
100        // The mobile apps fetch editor assets over REST, where wp-admin links are no use.
101        if ( Constants::is_true( 'REST_REQUEST' ) ) {
102            return null;
103        }
104
105        $reason = self::get_site_wide_reason();
106        if ( null === $reason ) {
107            return null;
108        }
109
110        $is_disconnected = 'not_connected' === $reason;
111        $features        = array();
112
113        // The form registers without the Blocks module, so Forms itself can be the cause.
114        if ( ! ( new Modules() )->is_active( 'contact-form' ) ) {
115            $forms = self::get_inactive_module_details( 'contact-form' );
116            if ( $forms ) {
117                $features['jetpack/contact-form'] = $forms;
118            }
119        }
120
121        return array(
122            'reason'           => $reason,
123            'canFix'           => current_user_can( $is_disconnected ? 'jetpack_connect' : 'jetpack_manage_modules' ),
124            'fixUrl'           => admin_url( $is_disconnected ? 'admin.php?page=jetpack' : 'admin.php?page=jetpack#/writing' ),
125            'features'         => $features,
126            'canManageModules' => current_user_can( 'jetpack_manage_modules' ),
127            'modulesUrl'       => admin_url( 'admin.php?page=jetpack_modules' ),
128            'independent'      => self::INDEPENDENT_BLOCKS,
129            'shipped'          => self::get_shipped_blocks(),
130        );
131    }
132
133    /**
134     * Get the reason no Jetpack block from the Blocks module is registered, if there is one.
135     *
136     * @since $$next-version$$
137     *
138     * @return string|null `not_connected`, `blocks_module`, `disabled`, or null.
139     */
140    public static function get_site_wide_reason() {
141        if ( ! Jetpack::is_connection_ready() && ! ( new Status() )->is_offline_mode() ) {
142            return 'not_connected';
143        }
144
145        // Not Jetpack_Gutenberg::should_load(): a filter returning true cannot load an inactive module.
146        if ( ! ( new Modules() )->is_active( 'blocks' ) ) {
147            return self::is_forced_off( 'blocks' ) ? 'disabled' : 'blocks_module';
148        }
149
150        /** This filter is documented in class.jetpack-gutenberg.php */
151        if ( ! apply_filters( 'jetpack_gutenberg', true ) ) {
152            return 'disabled';
153        }
154
155        return null;
156    }
157
158    /**
159     * Add each inactive module's name and forced state to blocks that reported one when they bailed out.
160     *
161     * @since $$next-version$$
162     *
163     * @param array $availability Block availability, as from Jetpack_Gutenberg::get_availability().
164     * @return array The same availability, with module details filled in or dropped.
165     */
166    public static function describe_inactive_modules( $availability ) {
167        foreach ( (array) $availability as $slug => $entry ) {
168            if ( isset( $entry['details']['module'] ) && is_string( $entry['details']['module'] ) ) {
169                $availability[ $slug ]['details'] = self::get_inactive_module_details( $entry['details']['module'] );
170            }
171        }
172
173        return $availability;
174    }
175
176    /**
177     * Describe an inactive module for the editor's explanation of a block that needs it.
178     *
179     * @since $$next-version$$
180     *
181     * @param string $module Module slug.
182     * @return array Module slug, name, and whether it is forced off; empty when the editor should not name it.
183     */
184    public static function get_inactive_module_details( $module ) {
185        if ( ( new Host() )->is_wpcom_simple() ) {
186            return array();
187        }
188
189        $info = ( new Modules() )->get( $module );
190        if ( ! is_array( $info ) ) {
191            return array();
192        }
193
194        // Offline mode keeps connection-dependent modules off, so their toggle would not help.
195        if ( ! empty( $info['requires_connection'] ) && ( new Status() )->is_offline_mode() ) {
196            return array();
197        }
198
199        // Modules::get() returns the untranslated file header.
200        $i18n = function_exists( 'jetpack_get_module_i18n' ) ? jetpack_get_module_i18n( $module ) : array();
201
202        return array(
203            'module'      => $module,
204            'module_name' => ! empty( $i18n['name'] ) ? $i18n['name'] : ( ! empty( $info['name'] ) ? $info['name'] : $module ),
205            'forced'      => self::is_forced_off( $module ),
206        );
207    }
208
209    /**
210     * Get the top-level blocks this site would register with nothing turned off.
211     *
212     * @since $$next-version$$
213     *
214     * @return string[] Block names.
215     */
216    public static function get_shipped_blocks() {
217        $manifest_file = JETPACK__PLUGIN_DIR . '_inc/blocks/blocks-manifest.php';
218        $manifest      = file_exists( $manifest_file ) ? require $manifest_file : array();
219        $extensions    = Jetpack_Gutenberg::get_available_extensions();
220        $blocks        = self::PACKAGE_BLOCKS;
221
222        foreach ( (array) $manifest as $extension => $metadata ) {
223            if ( in_array( $extension, $extensions, true ) && ! empty( $metadata['name'] ) ) {
224                $blocks[] = $metadata['name'];
225            }
226        }
227
228        return array_values( array_diff( $blocks, self::CONDITIONAL_BLOCKS ) );
229    }
230
231    /**
232     * Whether a filter or the host's feature policy keeps a module inactive.
233     *
234     * @param string $module Module slug.
235     * @return bool
236     */
237    private static function is_forced_off( $module ) {
238        return 'inactive' === Jetpack_Modules_Overrides::instance()->get_module_override( $module );
239    }
240}