Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.75% covered (warning)
79.75%
63 / 79
77.78% covered (warning)
77.78%
7 / 9
CRAP
n/a
0 / 0
wpcom_themes_tab_register_flag
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_themes_tab_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
wpcom_themes_tab_enqueue_script
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
wpcom_themes_tab_is_default_view
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
wpcom_themes_tab_redirect_to_default_view
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_themes_tab_api
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
wpcom_themes_tab_mapper
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
wpcom_themes_tab_catalog
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
wpcom_themes_tab_serve_themes_api
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
11
1<?php
2/**
3 * Lists WordPress.com themes as a tab on the core Add Themes screen.
4 *
5 * @package automattic/jetpack-mu-wpcom
6 */
7
8use Automattic\Jetpack\Feature_Flags\Feature_Flags;
9use Automattic\Jetpack\Jetpack_Mu_Wpcom;
10
11/**
12 * The tab's `data-sort` value, which core's theme.js sends to themes_api() as `browse`.
13 */
14const WPCOM_THEMES_TAB = 'wpcom';
15
16/**
17 * Feature flag gating the tab.
18 */
19const WPCOM_THEMES_TAB_FLAG = 'wpcom-themes-marketplace-tab';
20
21/**
22 * Themes per request, which is also the grid's page size.
23 */
24const WPCOM_THEMES_TAB_PER_PAGE = 100;
25
26/**
27 * Registers the feature flag.
28 *
29 * @return void
30 */
31function wpcom_themes_tab_register_flag() {
32    Feature_Flags::register(
33        WPCOM_THEMES_TAB_FLAG,
34        array(
35            'default'     => false,
36            'description' => 'Show WordPress.com themes as a tab on the Add Themes screen.',
37            'owner'       => 'jetpack-mu-wpcom',
38        )
39    );
40}
41wpcom_themes_tab_register_flag();
42
43/**
44 * Whether to show the tab on this site.
45 *
46 * Atomic only for now: Simple has no Add Themes screen to add a tab to, and this package ships
47 * there too, so the host is checked rather than left to the screen's absence.
48 *
49 * @return bool
50 */
51function wpcom_themes_tab_enabled() {
52    return is_woa_site() && Feature_Flags::is_enabled( WPCOM_THEMES_TAB_FLAG );
53}
54
55/**
56 * Loads the script that adds the tab.
57 *
58 * Core hard-codes this screen's tabs with no filter (trac #65979), so the script inserts ours
59 * into the markup before core's theme.js reads it on DOM ready.
60 *
61 * @return void
62 */
63function wpcom_themes_tab_enqueue_script() {
64    if ( ! wpcom_themes_tab_enabled() ) {
65        return;
66    }
67
68    wp_enqueue_script(
69        'wpcom-themes-tab',
70        plugins_url( 'js/themes-tab.js', __FILE__ ),
71        array(),
72        Jetpack_Mu_Wpcom::PACKAGE_VERSION,
73        array( 'in_footer' => true )
74    );
75    wp_localize_script(
76        'wpcom-themes-tab',
77        'wpcomThemesTab',
78        array(
79            'sort'  => WPCOM_THEMES_TAB,
80            'label' => _x( 'Marketplace', 'Theme Installer', 'jetpack-mu-wpcom' ),
81        )
82    );
83}
84add_action( 'load-theme-install.php', 'wpcom_themes_tab_enqueue_script' );
85
86/**
87 * Whether a request to the Add Themes screen should land on this tab.
88 *
89 * @param array $query The request's query args.
90 * @return bool
91 */
92function wpcom_themes_tab_is_default_view( array $query ) {
93    if ( ! wpcom_themes_tab_enabled() ) {
94        return false;
95    }
96
97    // An explicit view, a search, an upload or a preview is left as asked for.
98    return ! array_intersect( array( 'browse', 'theme', 'search', 'upload' ), array_keys( $query ) );
99}
100
101/**
102 * Sends a bare Add Themes request to this tab.
103 *
104 * Core's theme.js routes a bare `theme-install.php` to `popular` and rewrites the URL to match, so
105 * the view has to be chosen before that runs rather than by ordering the tabs.
106 *
107 * @return void
108 */
109function wpcom_themes_tab_redirect_to_default_view() {
110    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reads which view was asked for, nothing more.
111    if ( ! wpcom_themes_tab_is_default_view( $_GET ) ) {
112        return;
113    }
114
115    wp_safe_redirect( admin_url( 'theme-install.php?browse=' . WPCOM_THEMES_TAB ) );
116    exit;
117}
118add_action( 'load-theme-install.php', 'wpcom_themes_tab_redirect_to_default_view' );
119/**
120 * The shared WordPress.com themes reader, which wpcomsh also uses for core's own tabs.
121 *
122 * @return WPCom_Themes_Api
123 */
124function wpcom_themes_tab_api() {
125    static $api;
126    if ( ! $api ) {
127        $api = new WPCom_Themes_Api( new WPCom_Themes_Cache() );
128    }
129
130    return $api;
131}
132
133/**
134 * The shared mapper, which shapes a WordPress.com theme the way core's grid reads it.
135 *
136 * @return WPCom_Themes_Mapper
137 */
138function wpcom_themes_tab_mapper() {
139    static $mapper;
140    if ( ! $mapper ) {
141        $mapper = new WPCom_Themes_Mapper();
142    }
143
144    return $mapper;
145}
146
147/**
148 * Every theme the tab lists, in the order WordPress.com returns them.
149 *
150 * The shared reader fetches and caches the whole catalog, so the tab pages through it here rather
151 * than asking for a page at a time.
152 *
153 * @return array<stdClass>
154 */
155function wpcom_themes_tab_catalog() {
156    return array_values(
157        array_filter(
158            wpcom_themes_tab_api()->fetch_all_non_delisted_themes(),
159            static function ( $theme ) {
160                return is_object( $theme ) && ! empty( $theme->id ) && is_string( $theme->id );
161            }
162        )
163    );
164}
165
166/**
167 * Answers the tab's query in place of the WordPress.org themes API.
168 *
169 * @param false|object|array $result The result object or array. Default false.
170 * @param string             $action The type of information being requested.
171 * @param object             $args   Theme API arguments.
172 * @return false|object|array
173 */
174function wpcom_themes_tab_serve_themes_api( $result, $action, $args ) {
175    if ( false !== $result || 'query_themes' !== $action || ! wpcom_themes_tab_enabled() ) {
176        return $result;
177    }
178
179    // A `?theme=<slug>` preview link, loaded directly, looks its theme up by slug rather than by tab.
180    if ( ! isset( $args->browse ) && isset( $args->theme ) && is_string( $args->theme ) ) {
181        $theme = wpcom_themes_tab_api()->fetch_theme( $args->theme );
182
183        return null === $theme ? $result : (object) array(
184            'info'   => array(
185                'page'    => 1,
186                'pages'   => 1,
187                'results' => 1,
188            ),
189            'themes' => array( wpcom_themes_tab_mapper()->map_wpcom_to_wporg( $theme ) ),
190        );
191    }
192
193    if ( ! isset( $args->browse ) || WPCOM_THEMES_TAB !== $args->browse ) {
194        return $result;
195    }
196
197    $themes = wpcom_themes_tab_catalog();
198    $page   = isset( $args->page ) ? max( 1, (int) $args->page ) : 1;
199    $mapper = wpcom_themes_tab_mapper();
200
201    // Only the page core asked for is mapped: mapping reads each theme from disk to say whether it
202    // is installed.
203    $shown = array_slice( $themes, ( $page - 1 ) * WPCOM_THEMES_TAB_PER_PAGE, WPCOM_THEMES_TAB_PER_PAGE );
204
205    return (object) array(
206        'info'   => array(
207            'page'    => $page,
208            'pages'   => (int) ceil( count( $themes ) / WPCOM_THEMES_TAB_PER_PAGE ),
209            'results' => count( $themes ),
210        ),
211        'themes' => array_values( array_map( array( $mapper, 'map_wpcom_to_wporg' ), $shown ) ),
212    );
213}
214add_filter( 'themes_api', 'wpcom_themes_tab_serve_themes_api', 10, 3 );