Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
28.44% covered (danger)
28.44%
31 / 109
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%
28 / 28
100.00% covered (success)
100.00%
1 / 1
15
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    wp_parse_str( (string) wp_parse_url( $referer, PHP_URL_QUERY ), $referer_query );
86
87    // On Simple, Add Themes is WordPress.com's own screen on themes.php.
88    $from_add_themes = substr( $referer_path, -strlen( 'theme-install.php' ) ) === 'theme-install.php'
89        || ( substr( $referer_path, -strlen( '/themes.php' ) ) === '/themes.php' && ( $referer_query['page'] ?? '' ) === 'wpcom-install-theme' );
90
91    if ( $marker === 'marketplace-tab' ) {
92        return 'marketplace-tab';
93    }
94
95    if ( $from_add_themes && ( $referer_query['browse'] ?? '' ) === 'wpcom' ) {
96        return 'marketplace-tab';
97    }
98
99    if ( $post_action === 'customize_save' || $customize === 'on' ) {
100        return 'customizer';
101    }
102
103    if ( $post_action === 'install-theme' ) {
104        return 'add-themes';
105    }
106
107    if ( $get_action === 'activate' ) {
108        if ( $from_add_themes ) {
109            return 'add-themes';
110        }
111
112        if ( substr( $referer_path, -strlen( 'themes.php' ) ) === 'themes.php' ) {
113            return 'themes';
114        }
115    }
116
117    return 'other';
118}
119
120/**
121 * Record a theme switch.
122 *
123 * @todo There is already a theme switch event for Simple sites. It should be removed in favor of this one.
124 *
125 * @param string   $new_theme_name New theme name.
126 * @param WP_Theme $new_theme      New theme object.
127 * @param WP_Theme $old_theme      Old theme object.
128 * @return void
129 */
130function wpcom_themes_tracks_switch_theme( $new_theme_name, $new_theme, $old_theme ) {
131    if ( ! is_wpcom_user() ) {
132        return;
133    }
134
135    $old_theme_props = wpcom_themes_tracks_get_theme_props( $old_theme, 'old' );
136    $new_theme_props = wpcom_themes_tracks_get_theme_props( $new_theme, 'new' );
137
138    $event_props = array_merge(
139        array( 'blog_id' => get_wpcom_blog_id() ),
140        $old_theme_props,
141        $new_theme_props,
142        array( 'origin' => wpcom_themes_tracks_get_origin() )
143    );
144
145    Common\wpcom_record_tracks_event( 'wpcom_theme_switch', $event_props );
146}
147add_action( 'switch_theme', 'wpcom_themes_tracks_switch_theme', 12, 3 );
148
149/**
150 * Work out the source and type of a theme install or update.
151 *
152 * @param string $hook_action The `action` from the upgrader hook data.
153 * @return array{source: string, type: string}
154 */
155function wpcom_themes_tracks_install_source_and_type( $hook_action ) {
156    $install_props = array(
157        'source' => '', // Intentionally left empty for debugging purposes.
158        'type'   => 'upload',
159    );
160
161    // Trying to distinguish between a manual upload vs a standard install or upgrade. It's not security sensitive.
162    // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
163    $action    = $_GET['action'] ?? '';
164    $overwrite = $_GET['overwrite'] ?? '';
165    // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
166
167    if ( $hook_action === 'install' && ! $action ) {
168        // Install theme via wp-admin UI
169        $install_props['source'] = 'wp-admin';
170        $install_props['type']   = 'install';
171    } elseif ( $hook_action === 'install' && $action === 'upload-theme' && ! $overwrite ) {
172        // Upload a theme via wp-admin (new theme)
173        $install_props['source'] = 'wp-admin';
174        $install_props['type']   = 'upload';
175    } elseif ( $hook_action === 'install' && $action === 'upload-theme' && $overwrite === 'update-theme' ) {
176        // Upload a theme via wp-admin (existing theme)
177        $install_props['source'] = 'wp-admin';
178        $install_props['type']   = 'update';
179    } elseif ( $hook_action === 'update' && $action === 'do-theme-upgrade' ) {
180        // Update theme via update-core.php
181        $install_props['source'] = 'wp-admin';
182        $install_props['type']   = 'bulk-update';
183    }
184
185    return $install_props;
186}
187
188/**
189 * Record a theme install via wp-admin.
190 *
191 * @param Theme_Upgrader $upgrader   Upgrader instance.
192 * @param array          $hook_extra Upgrade data.
193 * @return void
194 */
195function wpcom_themes_tracks_install_theme( $upgrader, $hook_extra ) {
196    if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
197        // Simple sites can't install themes with wp-admin so should never reach here,
198        // but just in case, we exit early.
199        return;
200    }
201
202    if ( ! isset( $hook_extra['type'] ) || $hook_extra['type'] !== 'theme' || ! isset( $hook_extra['action'] ) ) {
203        return;
204    }
205
206    if ( ! is_wpcom_user() ) {
207        return;
208    }
209
210    $theme_slug = $upgrader->result['destination_name'] ?? '';
211
212    if ( ! $theme_slug ) {
213        return;
214    }
215
216    $theme       = wp_get_theme( $theme_slug );
217    $theme_props = wpcom_themes_tracks_get_theme_props( $theme );
218
219    $event_props = array_merge(
220        array( 'blog_id' => get_wpcom_blog_id() ),
221        $theme_props,
222        wpcom_themes_tracks_install_source_and_type( $hook_extra['action'] ),
223        array( 'origin' => wpcom_themes_tracks_get_origin() )
224    );
225
226    Common\wpcom_record_tracks_event( 'wpcom_theme_install', $event_props );
227}
228add_action( 'upgrader_process_complete', 'wpcom_themes_tracks_install_theme', 10, 2 );
229
230/**
231 * Record the install of a WP.com theme, which is symlinked instead of going through the upgrader.
232 *
233 * @param string $theme_slug Theme slug.
234 * @return void
235 */
236function wpcom_themes_tracks_install_wpcom_theme( $theme_slug ) {
237    if ( Constants::is_true( 'IS_WPCOM' ) || ! is_wpcom_user() ) {
238        return;
239    }
240
241    $event_props = array_merge(
242        array( 'blog_id' => get_wpcom_blog_id() ),
243        wpcom_themes_tracks_get_theme_props( wp_get_theme( $theme_slug ) ),
244        wpcom_themes_tracks_install_source_and_type( 'install' ),
245        array( 'origin' => wpcom_themes_tracks_get_origin() )
246    );
247
248    Common\wpcom_record_tracks_event( 'wpcom_theme_install', $event_props );
249}
250add_action( 'wpcomsh_wpcom_theme_installed', 'wpcom_themes_tracks_install_wpcom_theme' );