Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
39.34% covered (danger)
39.34%
48 / 122
12.50% covered (danger)
12.50%
2 / 16
CRAP
n/a
0 / 0
wpcomsh_get_wpcom_themes_service_instance
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
wpcomsh_remove_dot_org_themes_based_on_plan
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_keep_only_wpcom_themes
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
wpcomsh_popular_wpcom_themes_api_result
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
wpcomsh_latest_wpcom_themes_api_result
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
wpcomsh_block_themes_wpcom_themes_api_result
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
wpcomsh_search_wpcom_themes_api_result
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
wpcomsh_search_prefers_wporg
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
wpcomsh_feature_filter_wpcom_themes_api_result
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
wpcomsh_theme_information_wpcom_themes_api_result
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_remove_symlink_wp_error
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_theme_install_by_symlink
30.43% covered (danger)
30.43%
7 / 23
0.00% covered (danger)
0.00%
0 / 1
36.27
wpcomsh_theme_install_by_download
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
8.70
wpcomsh_get_marketplace_theme_download_link
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
6.07
wpcomsh_delete_managed_wpcom_theme
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_include_themes_creation_date
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Registers a filter for the themes API result to add WPCom themes.
4 *
5 * @package wpcom-themes
6 */
7
8/**
9 * Loads the WPCom themes service instance.
10 *
11 * @return WPCom_Themes_Service
12 */
13function wpcomsh_get_wpcom_themes_service_instance(): WPCom_Themes_Service {
14    static $instance;
15    if ( ! $instance ) {
16        // Load dependencies.
17        require_once __DIR__ . '/includes/class-wpcom-themes-merger.php';
18        require_once __DIR__ . '/includes/class-wpcom-themes-service.php';
19
20        // Resolve and Inject dependencies.
21        $mapper               = new WPCom_Themes_Mapper();
22        $merger               = new WPCom_Themes_Merger();
23        $cache                = new WPCom_Themes_Cache();
24        $api                  = new WPCom_Themes_Api( $cache );
25        $wpcom_themes_service = new WPCom_Themes_Service( $api, $mapper, $merger );
26
27        $instance = $wpcom_themes_service;
28    }
29
30    return $instance;
31}
32
33/**
34 * Perform necessary checks to remove dot org themes based on the plan and other features.
35 *
36 * @param mixed $res The result object.
37 *
38 * @return mixed|stdClass
39 */
40function wpcomsh_remove_dot_org_themes_based_on_plan( $res ) {
41    if ( wpcom_site_has_feature( WPCOM_Features::COMMUNITY_THEMES ) ) {
42        return $res;
43    }
44
45    return wpcomsh_keep_only_wpcom_themes( $res );
46}
47add_filter( 'themes_api_result', 'wpcomsh_remove_dot_org_themes_based_on_plan', 0, 1 );
48
49/**
50 * Drops the themes that came from WordPress.org out of a themes API result.
51 *
52 * @param mixed $res The result object.
53 *
54 * @return mixed|stdClass
55 */
56function wpcomsh_keep_only_wpcom_themes( $res ) {
57    // A single theme's details, rather than a list to filter.
58    if ( ! isset( $res->themes ) || ! is_array( $res->themes ) ) {
59        return $res;
60    }
61
62    $ours = array_values(
63        array_filter(
64            $res->themes,
65            function ( $theme ) {
66                return ! empty( $theme->is_wpcom_theme );
67            }
68        )
69    );
70
71    // Our own themes are already the ones this site may install, and the total counts every page
72    // rather than this one, so a result made of them is left as it is.
73    if ( count( $ours ) === count( $res->themes ) ) {
74        return $res;
75    }
76
77    /*
78     * The rest came from WordPress.org, which this plan excludes. Clearing the total as well lets
79     * the filters that run next set it from their own list, which is what they expect.
80     */
81    $res->themes = $ours;
82
83    if ( isset( $res->info['results'] ) ) {
84        $res->info['results'] = count( $ours );
85    }
86
87    return $res;
88}
89
90/**
91 * Process the themes API result and add the recommended WPCom themes to the Popular tab.
92 *
93 * @param mixed  $res    The result object.
94 * @param string $action The action.
95 * @param mixed  $args   The arguments.
96 *
97 * @return mixed|stdClass
98 */
99function wpcomsh_popular_wpcom_themes_api_result( $res, string $action, $args ) {
100    // Pre-requisites checks.
101    $browse = $args->browse ?? '';
102    if ( 'query_themes' !== $action || 'popular' !== $browse ) {
103        return $res;
104    }
105
106    $wpcom_themes_service = wpcomsh_get_wpcom_themes_service_instance();
107
108    // Add results to the resulting array.
109    return $wpcom_themes_service->filter_themes_api_result_recommended( $res );
110}
111add_filter( 'themes_api_result', 'wpcomsh_popular_wpcom_themes_api_result', 1, 3 );
112
113/**
114 * Process the themes API result and add the latest WPCom themes to the Latest tab.
115 *
116 * @param mixed  $res    The result object.
117 * @param string $action The action.
118 * @param mixed  $args   The arguments.
119 *
120 * @return mixed|stdClass
121 */
122function wpcomsh_latest_wpcom_themes_api_result( $res, string $action, $args ) {
123    // Pre-requisites checks.
124    $browse = $args->browse ?? '';
125    if ( 'query_themes' !== $action || 'new' !== $browse ) {
126        return $res;
127    }
128
129    $wpcom_themes_service = wpcomsh_get_wpcom_themes_service_instance();
130
131    // Add results to the resulting array.
132    return $wpcom_themes_service->filter_themes_api_result_latest( $res );
133}
134add_filter( 'themes_api_result', 'wpcomsh_latest_wpcom_themes_api_result', 1, 3 );
135
136/**
137 * Process the themes API result and add the WPCom block themes to the Block themes tab.
138 *
139 * @param mixed  $res    The result object.
140 * @param string $action The action.
141 * @param mixed  $args   The arguments.
142 *
143 * @return mixed|stdClass
144 */
145function wpcomsh_block_themes_wpcom_themes_api_result( $res, string $action, $args ) {
146    // Pre-requisites checks.
147    $tag = $args->tag ?? '';
148    if ( 'query_themes' !== $action || 'full-site-editing' !== $tag ) {
149        return $res;
150    }
151
152    $wpcom_themes_service = wpcomsh_get_wpcom_themes_service_instance();
153
154    // Add results to the resulting array.
155    return $wpcom_themes_service->filter_themes_api_result_block_themes( $res );
156}
157add_filter( 'themes_api_result', 'wpcomsh_block_themes_wpcom_themes_api_result', 1, 3 );
158
159/**
160 * Process the themes API result and add WPCom themes when using the search.
161 *
162 * @param mixed  $res    The result object.
163 * @param string $action The action.
164 * @param mixed  $args   The arguments.
165 *
166 * @return mixed|stdClass
167 */
168function wpcomsh_search_wpcom_themes_api_result( $res, string $action, $args ) {
169    // Pre-requisites checks.
170    $search = $args->search ?? '';
171    if ( 'query_themes' !== $action || '' === $search ) {
172        return $res;
173    }
174
175    $wpcom_themes_service = wpcomsh_get_wpcom_themes_service_instance();
176
177    // Add results to the resulting array.
178    return $wpcom_themes_service->filter_themes_api_result_search( $res, $search, wpcomsh_search_prefers_wporg() );
179}
180add_filter( 'themes_api_result', 'wpcomsh_search_wpcom_themes_api_result', 1, 3 );
181
182/**
183 * Whether search shows WordPress.org's copy of a theme both directories list, rather than ours.
184 *
185 * Only behind the Marketplace tab's flag, and only where the plan shows WordPress.org's themes:
186 * elsewhere ours is the only copy the search can show.
187 *
188 * @return bool
189 */
190function wpcomsh_search_prefers_wporg(): bool {
191    return function_exists( 'wpcom_themes_tab_enabled' )
192        && wpcom_themes_tab_enabled()
193        && wpcom_site_has_feature( WPCOM_Features::COMMUNITY_THEMES );
194}
195
196/**
197 * Process the themes API result and add WPCom themes when using the filter feature.
198 *
199 * @param mixed  $res    The result object.
200 * @param string $action The action.
201 * @param mixed  $args   The arguments.
202 *
203 * @return mixed|stdClass
204 */
205function wpcomsh_feature_filter_wpcom_themes_api_result( $res, string $action, $args ) {
206    // Pre-requisites checks.
207    $tags = $args->tag ?? array();
208    if ( 'query_themes' !== $action || ! $tags ) {
209        return $res;
210    }
211
212    if ( ! is_array( $tags ) ) {
213        $tags = array( $tags );
214    }
215
216    $wpcom_themes_service = wpcomsh_get_wpcom_themes_service_instance();
217
218    // Add results to the resulting array.
219    return $wpcom_themes_service->filter_themes_api_result_feature_filter( $res, $tags );
220}
221add_filter( 'themes_api_result', 'wpcomsh_feature_filter_wpcom_themes_api_result', 1, 3 );
222
223/**
224 * Process the themes API result theme_information for WPCom themes if needed.
225 *
226 * @param mixed  $res    The result object.
227 * @param string $action The action.
228 * @param mixed  $args   The arguments.
229 *
230 * @return mixed|stdClass|WP_Error|null
231 */
232function wpcomsh_theme_information_wpcom_themes_api_result( $res, string $action, $args ) {
233    // Pre-requisites checks.
234    if ( 'theme_information' !== $action ) {
235        return $res;
236    }
237
238    $wpcom_themes_service = wpcomsh_get_wpcom_themes_service_instance();
239
240    return $wpcom_themes_service->get_theme( $args->slug ) ?? $res;
241}
242add_filter( 'themes_api_result', 'wpcomsh_theme_information_wpcom_themes_api_result', 1, 3 );
243
244/**
245 * Remove the WP_Error object from the WP_Error object.
246 *
247 * @param string   $code    The error code.
248 * @param string   $message The error message.
249 * @param mixed    $data    The error data.
250 * @param WP_Error $error   The WP_Error object.
251 *
252 * @return void
253 */
254function wpcomsh_remove_symlink_wp_error( $code, $message, $data, WP_Error $error ) {
255    if ( 'wpcomsh_theme_install_symlink' === $code ) {
256        $error->remove( 'wpcomsh_theme_install_symlink' );
257    }
258}
259
260/**
261 * Install WPCom themes by creating a symlink instead of downloading the theme package.
262 *
263 * @param mixed  $reply    The result object.
264 * @param string $package  The package to install.
265 * @param mixed  $upgrader The upgrader component.
266 *
267 * @return bool|mixed|WP_Error
268 */
269function wpcomsh_theme_install_by_symlink( $reply, $package, $upgrader ) {
270    // Pre-requisites checks. A WP.com theme's package is its slug; a URL or file path is some other theme.
271    if ( ! $package || ! is_string( $package ) || str_contains( $package, '/' ) || ! $upgrader instanceof Theme_Upgrader ) {
272        return $reply;
273    }
274
275    $wpcom_themes_service = wpcomsh_get_wpcom_themes_service_instance();
276    $wpcom_theme          = $wpcom_themes_service->get_theme( $package );
277
278    if ( ! $wpcom_theme ) {
279        return $reply;
280    }
281
282    $theme_type = wpcomsh_get_wpcom_theme_type( $wpcom_theme->slug );
283
284    // WP Cloud doesn't host it, so wpcomsh_theme_install_by_download() gets it instead.
285    if ( ! $theme_type ) {
286        return $reply;
287    }
288
289    $upgrader->skin->feedback( 'installing_package' );
290    $result = wpcomsh_install_managed_theme( $wpcom_theme->slug, $theme_type );
291
292    if ( is_wp_error( $result ) && $result->get_error_code() !== 'wpcom_theme_already_installed' ) {
293        $upgrader->skin->feedback( 'process_failed' );
294        // The internal state and return values are WP_Errors.
295        $upgrader->result = $result;
296
297        return $result;
298    }
299
300    // Set the result to the theme slug to indicate success and so that the skin can reference it.
301    $result           = array(
302        'destination_name' => $wpcom_theme->slug,
303    );
304    $upgrader->result = $result;
305    // Skin uses both the upgrader and its own result which should be the same.
306    $upgrader->skin->result = $result;
307    $upgrader->skin->feedback( 'process_success' );
308
309    // Symlink errors are not really errors so let's remove them. Some clients inspect the skin errors to determine the success of the operation.
310    add_action( 'wp_error_added', 'wpcomsh_remove_symlink_wp_error', 0, 4 );
311
312    // We need to return a WP_Error to short circuit the installation process.
313    return new WP_Error( 'wpcomsh_theme_install_symlink' );
314}
315add_filter( 'upgrader_pre_download', 'wpcomsh_theme_install_by_symlink', 0, 4 );
316
317/**
318 * Install WPCom themes WP Cloud doesn't host by handing core their download, which it unpacks and installs.
319 *
320 * @param mixed  $reply    The result object.
321 * @param string $package  The package to install.
322 * @param mixed  $upgrader The upgrader component.
323 *
324 * @return mixed|string|WP_Error The downloaded package's path, or why there is none.
325 */
326function wpcomsh_theme_install_by_download( $reply, $package, $upgrader ) {
327    if ( false !== $reply || ! $package || ! is_string( $package ) || str_contains( $package, '/' ) || ! $upgrader instanceof Theme_Upgrader ) {
328        return $reply;
329    }
330
331    $wpcom_theme = wpcomsh_get_wpcom_themes_service_instance()->get_theme( $package );
332    if ( ! $wpcom_theme ) {
333        return $reply;
334    }
335
336    $download_link = wpcomsh_get_marketplace_theme_download_link( $wpcom_theme->slug );
337    if ( '' === $download_link ) {
338        return new WP_Error( 'wpcom_theme_not_installable', __( 'This theme could not be installed.', 'wpcomsh' ) );
339    }
340
341    return download_url( $download_link );
342}
343add_filter( 'upgrader_pre_download', 'wpcomsh_theme_install_by_download', 1, 4 );
344
345/**
346 * Where to download a marketplace theme this site owns, which WordPress.com lists only for owned themes.
347 *
348 * @param string $slug Theme slug.
349 *
350 * @return string The download link, empty when WordPress.com offers none for this site.
351 */
352function wpcomsh_get_marketplace_theme_download_link( string $slug ): string {
353    $response = Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_blog( '/marketplace/themes/updates', '2', array(), null, 'wpcom' );
354    if ( is_wp_error( $response ) ) {
355        return '';
356    }
357
358    $body = json_decode( wp_remote_retrieve_body( $response ), true );
359    foreach ( $body['updates'] ?? array() as $update ) {
360        if ( is_array( $update ) && ( $update['slug'] ?? '' ) === $slug && ! empty( $update['download_link'] ) ) {
361            return (string) $update['download_link'];
362        }
363    }
364
365    return '';
366}
367
368/**
369 * Remove the symlink created for the WPCom theme when the theme is deleted.
370 *
371 * @param string $stylesheet The theme stylesheet.
372 *
373 * @return void Return early if the theme is not a WPCom theme.
374 */
375function wpcomsh_delete_managed_wpcom_theme( string $stylesheet ) {
376    if ( wpcomsh_is_theme_symlinked( $stylesheet ) ) {
377        wpcomsh_jetpack_wpcom_theme_delete( false, $stylesheet );
378    }
379}
380add_action( 'delete_theme', 'wpcomsh_delete_managed_wpcom_theme' );
381
382/**
383 * Include the creation date in the themes API response.
384 *
385 * @param array $args The arguments.
386 *
387 * @return array The arguments with the creation time included.
388 */
389function wpcomsh_include_themes_creation_date( $args ) {
390    $args['fields']['creation_time'] = true;
391
392    return $args;
393}
394add_filter( 'install_themes_table_api_args_new', 'wpcomsh_include_themes_creation_date' );
395add_filter( 'install_themes_table_api_args_search', 'wpcomsh_include_themes_creation_date' );