Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
27.78% covered (danger)
27.78%
30 / 108
16.67% covered (danger)
16.67%
1 / 6
CRAP
n/a
0 / 0
wpcom_themes_tracks_get_theme_props
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
56
wpcom_themes_tracks_get_origin
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
13
wpcom_themes_tracks_switch_theme
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_themes_tracks_install_source_and_type
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
132
wpcom_themes_tracks_install_theme
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
72
wpcom_themes_tracks_install_wpcom_theme
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * WordPress.com Theme Tracking
4 *
5 * Add Tracks events to the wp-admin theme screens.
6 *
7 * @package automattic/jetpack-mu-wpcom
8 */
9
10use Automattic\Jetpack\Constants;
11use Automattic\Jetpack\Jetpack_Mu_Wpcom\Common;
12
13/**
14 * Get theme properties for Tracks event.
15 *
16 * @param WP_Theme $theme  Theme object.
17 * @param string   $prefix Prefix for the property keys.
18 * @return array Associative array of theme properties.
19 */
20function wpcom_themes_tracks_get_theme_props( $theme, $prefix = '' ) {
21    if ( $prefix !== '' ) {
22        $prefix .= '_';
23    }
24
25    $props = array(
26        $prefix . 'theme'                => $theme->get( 'Name' ),
27        $prefix . 'theme_stylesheet'     => $theme->get_stylesheet(),
28        $prefix . 'theme_tier'           => null,
29        $prefix . 'theme_is_block_theme' => $theme->is_block_theme(),
30        $prefix . 'theme_is_retired'     => false,
31    );
32
33    // Simple sites
34    if ( defined( 'IS_WPCOM' ) && IS_WPCOM && class_exists( 'WPCom_Themes' ) ) {
35        // @phan-suppress-next-line PhanUndeclaredClassMethod
36        $props[ $prefix . 'theme_tier' ] = WPCom_Themes::get_theme_tier( $theme->get_stylesheet() );
37        // @phan-suppress-next-line PhanUndeclaredClassMethod
38        $props[ $prefix . 'theme_is_retired' ] = WPCom_Themes::is_retired( $theme->get_stylesheet() );
39
40        return $props;
41    }
42
43    // Atomic sites
44    $props[ $prefix . 'theme_tier' ] = 'community';
45
46    if ( function_exists( 'wpcomsh_get_wpcom_themes_service_instance' ) ) {
47        $wpcom_themes_service = wpcomsh_get_wpcom_themes_service_instance();
48        $theme_data           = $wpcom_themes_service->get_theme( $theme->get_stylesheet() );
49
50        if ( $theme_data !== null ) {
51            $props[ $prefix . 'theme_tier' ]       = $theme_data->theme_tier ?? null;
52            $props[ $prefix . 'theme_is_retired' ] = $theme_data->is_retired ?? false;
53        }
54    }
55
56    return $props;
57}
58
59/**
60 * Work out which surface a theme install or switch started from.
61 *
62 * @return string One of marketplace-tab, add-themes, themes, customizer, api, cli or other.
63 */
64function wpcom_themes_tracks_get_origin() {
65    if ( Constants::is_true( 'WP_CLI' ) ) {
66        return 'cli';
67    }
68
69    if ( Constants::is_true( 'REST_API_REQUEST' ) ) {
70        return 'api';
71    }
72
73    // This only labels the event and authorizes nothing, so nonces are not checked.
74    // phpcs:disable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
75    $marker      = $_REQUEST['wpcom_themes_origin'] ?? '';
76    $post_action = $_POST['action'] ?? '';
77    $customize   = $_POST['wp_customize'] ?? '';
78    $get_action  = $_GET['action'] ?? '';
79    // phpcs:enable WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
80
81    // An array arrives when `_wp_http_referer[]` is sent.
82    $referer      = wp_get_raw_referer();
83    $referer      = is_string( $referer ) ? $referer : '';
84    $referer_path = (string) wp_parse_url( $referer, PHP_URL_PATH );
85
86    if ( $marker === 'marketplace-tab' ) {
87        return 'marketplace-tab';
88    }
89
90    if ( substr( $referer_path, -strlen( 'theme-install.php' ) ) === 'theme-install.php' ) {
91        wp_parse_str( (string) wp_parse_url( $referer, PHP_URL_QUERY ), $referer_query );
92
93        if ( ( $referer_query['browse'] ?? '' ) === 'wpcom' ) {
94            return 'marketplace-tab';
95        }
96    }
97
98    if ( $post_action === 'customize_save' || $customize === 'on' ) {
99        return 'customizer';
100    }
101
102    if ( $post_action === 'install-theme' ) {
103        return 'add-themes';
104    }
105
106    if ( $get_action === 'activate' ) {
107        if ( substr( $referer_path, -strlen( 'theme-install.php' ) ) === 'theme-install.php' ) {
108            return 'add-themes';
109        }
110
111        if ( substr( $referer_path, -strlen( 'themes.php' ) ) === 'themes.php' ) {
112            return 'themes';
113        }
114    }
115
116    return 'other';
117}
118
119/**
120 * Record a theme switch.
121 *
122 * @todo There is already a theme switch event for Simple sites. It should be removed in favor of this one.
123 *
124 * @param string   $new_theme_name New theme name.
125 * @param WP_Theme $new_theme      New theme object.
126 * @param WP_Theme $old_theme      Old theme object.
127 * @return void
128 */
129function wpcom_themes_tracks_switch_theme( $new_theme_name, $new_theme, $old_theme ) {
130    if ( ! is_wpcom_user() ) {
131        return;
132    }
133
134    $old_theme_props = wpcom_themes_tracks_get_theme_props( $old_theme, 'old' );
135    $new_theme_props = wpcom_themes_tracks_get_theme_props( $new_theme, 'new' );
136
137    $event_props = array_merge(
138        array( 'blog_id' => get_wpcom_blog_id() ),
139        $old_theme_props,
140        $new_theme_props,
141        array( 'origin' => wpcom_themes_tracks_get_origin() )
142    );
143
144    Common\wpcom_record_tracks_event( 'wpcom_theme_switch', $event_props );
145}
146add_action( 'switch_theme', 'wpcom_themes_tracks_switch_theme', 12, 3 );
147
148/**
149 * Work out the source and type of a theme install or update.
150 *
151 * @param string $hook_action The `action` from the upgrader hook data.
152 * @return array{source: string, type: string}
153 */
154function wpcom_themes_tracks_install_source_and_type( $hook_action ) {
155    $install_props = array(
156        'source' => '', // Intentionally left empty for debugging purposes.
157        'type'   => 'upload',
158    );
159
160    // Trying to distinguish between a manual upload vs a standard install or upgrade. It's not security sensitive.
161    // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
162    $action    = $_GET['action'] ?? '';
163    $overwrite = $_GET['overwrite'] ?? '';
164    // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
165
166    if ( $hook_action === 'install' && ! $action ) {
167        // Install theme via wp-admin UI
168        $install_props['source'] = 'wp-admin';
169        $install_props['type']   = 'install';
170    } elseif ( $hook_action === 'install' && $action === 'upload-theme' && ! $overwrite ) {
171        // Upload a theme via wp-admin (new theme)
172        $install_props['source'] = 'wp-admin';
173        $install_props['type']   = 'upload';
174    } elseif ( $hook_action === 'install' && $action === 'upload-theme' && $overwrite === 'update-theme' ) {
175        // Upload a theme via wp-admin (existing theme)
176        $install_props['source'] = 'wp-admin';
177        $install_props['type']   = 'update';
178    } elseif ( $hook_action === 'update' && $action === 'do-theme-upgrade' ) {
179        // Update theme via update-core.php
180        $install_props['source'] = 'wp-admin';
181        $install_props['type']   = 'bulk-update';
182    }
183
184    return $install_props;
185}
186
187/**
188 * Record a theme install via wp-admin.
189 *
190 * @param Theme_Upgrader $upgrader   Upgrader instance.
191 * @param array          $hook_extra Upgrade data.
192 * @return void
193 */
194function wpcom_themes_tracks_install_theme( $upgrader, $hook_extra ) {
195    if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
196        // Simple sites can't install themes with wp-admin so should never reach here,
197        // but just in case, we exit early.
198        return;
199    }
200
201    if ( ! isset( $hook_extra['type'] ) || $hook_extra['type'] !== 'theme' || ! isset( $hook_extra['action'] ) ) {
202        return;
203    }
204
205    if ( ! is_wpcom_user() ) {
206        return;
207    }
208
209    $theme_slug = $upgrader->result['destination_name'] ?? '';
210
211    if ( ! $theme_slug ) {
212        return;
213    }
214
215    $theme       = wp_get_theme( $theme_slug );
216    $theme_props = wpcom_themes_tracks_get_theme_props( $theme );
217
218    $event_props = array_merge(
219        array( 'blog_id' => get_wpcom_blog_id() ),
220        $theme_props,
221        wpcom_themes_tracks_install_source_and_type( $hook_extra['action'] ),
222        array( 'origin' => wpcom_themes_tracks_get_origin() )
223    );
224
225    Common\wpcom_record_tracks_event( 'wpcom_theme_install', $event_props );
226}
227add_action( 'upgrader_process_complete', 'wpcom_themes_tracks_install_theme', 10, 2 );
228
229/**
230 * Record the install of a WP.com theme, which is symlinked instead of going through the upgrader.
231 *
232 * @param string $theme_slug Theme slug.
233 * @return void
234 */
235function wpcom_themes_tracks_install_wpcom_theme( $theme_slug ) {
236    if ( Constants::is_true( 'IS_WPCOM' ) || ! is_wpcom_user() ) {
237        return;
238    }
239
240    $event_props = array_merge(
241        array( 'blog_id' => get_wpcom_blog_id() ),
242        wpcom_themes_tracks_get_theme_props( wp_get_theme( $theme_slug ) ),
243        wpcom_themes_tracks_install_source_and_type( 'install' ),
244        array( 'origin' => wpcom_themes_tracks_get_origin() )
245    );
246
247    Common\wpcom_record_tracks_event( 'wpcom_theme_install', $event_props );
248}
249add_action( 'wpcomsh_wpcom_theme_installed', 'wpcom_themes_tracks_install_wpcom_theme' );