Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.49% covered (success)
91.49%
43 / 47
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Analytics_Dashboard
91.49% covered (success)
91.49%
43 / 47
66.67% covered (warning)
66.67%
6 / 9
25.39
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 register_widget_types
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 add_error_reporting_origin
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 add_default_layout_instance
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
7.07
 is_top_videos_type
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 widget_contract_is_supported
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 get_widget_manifest
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 load_build
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 build_dir
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * VideoPress widgets of the Premium Analytics dashboard.
4 *
5 * @package automattic/jetpack-videopress
6 */
7
8namespace Automattic\Jetpack\VideoPress;
9
10use Automattic\Jetpack\Status\Host;
11use function Automattic\Jetpack\PremiumAnalytics\get_dashboard_default_widget_instance;
12use function Automattic\Jetpack\PremiumAnalytics\register_widget_types_from_manifest;
13use const Automattic\Jetpack\PremiumAnalytics\DASHBOARD_TILE_LIST;
14
15/**
16 * Registers the package's widget types on the Premium Analytics dashboard and seeds Top videos
17 * into the Traffic section's default layout.
18 *
19 * The package decides nothing about who gets the widgets: `Initializer` calls `init()` where
20 * VideoPress is active outside the WordPress.com platform, jetpack-mu-wpcom calls the registrants
21 * on Simple and Atomic where the plan includes VideoPress. Everything registers when the
22 * dashboard's widget registry hydrates, so a call on a site without the dashboard is inert.
23 *
24 * @since 0.55.0
25 */
26class Analytics_Dashboard {
27
28    /**
29     * Widget type name the package builds, from `widgets/top-videos/widget.json`.
30     */
31    const TOP_VIDEOS_TYPE = 'videopress/top-videos';
32
33    /**
34     * Widget type name the package builds, from `widgets/video-player/widget.json`.
35     */
36    const VIDEO_PLAYER_TYPE = 'videopress/video-player';
37
38    /**
39     * Names the widget type registered under before it moved here. A persisted layout that still
40     * carries one resolves to TOP_VIDEOS_TYPE.
41     */
42    const TOP_VIDEOS_FORMER_NAMES = array( 'jpa/videopress' );
43
44    /**
45     * The dashboard section whose default layout seeds the widget, and the seeded instance.
46     */
47    const TRAFFIC_SECTION_ID       = 'analytics/traffic';
48    const TOP_VIDEOS_INSTANCE_UUID = 'default-videopress-widget-instance';
49
50    /**
51     * Registry action and default-layout filter of the dashboard package.
52     */
53    const REGISTER_WIDGET_TYPES_ACTION = 'jetpack_premium_analytics_register_widget_types';
54    const DEFAULT_LAYOUT_FILTER        = 'jetpack_premium_analytics_dashboard_default_layout';
55
56    /**
57     * Text domain of the widget metadata and of the built widget bundle.
58     */
59    const TEXTDOMAIN = 'jetpack-videopress-pkg';
60
61    /**
62     * Lowest widget contract the build works against: Top videos renders the `@wordpress/ui`
63     * primitives the SDK shares, which the dashboard ships from 2.1.0.
64     */
65    const MIN_WIDGET_API_VERSION = '2.1.0';
66
67    /**
68     * Hook the registrant on the dashboard's registry action and the seed on its layout filter.
69     *
70     * Priority 20 for the registrant, after the dashboard package's own; priority 10 for the seed,
71     * before the dashboard drops the instances of unregistered types at 100.
72     *
73     * @return void
74     */
75    public static function init() {
76        // Simple and Atomic decide by plan feature, from jetpack-mu-wpcom.
77        if ( ( new Host() )->is_wpcom_platform() ) {
78            return;
79        }
80
81        add_action( self::REGISTER_WIDGET_TYPES_ACTION, array( __CLASS__, 'register_widget_types' ), 20 );
82        add_filter( self::DEFAULT_LAYOUT_FILTER, array( __CLASS__, 'add_default_layout_instance' ), 10, 2 );
83    }
84
85    /**
86     * Register the widget types from the package's build manifest.
87     *
88     * Loads the generated build first: after `init` it registers the widget script modules on the
89     * spot, so they reach the page import map the same request.
90     *
91     * @param object $registry The widget type registry being hydrated.
92     * @return void
93     */
94    public static function register_widget_types( $registry ) {
95        add_filter( 'jetpack_premium_analytics_error_reporting_script_origins', array( __CLASS__, 'add_error_reporting_origin' ) );
96
97        if ( ! self::widget_contract_is_supported() ) {
98            return;
99        }
100
101        register_widget_types_from_manifest(
102            self::get_widget_manifest(),
103            array(
104                'textdomain'    => self::TEXTDOMAIN,
105                'i18n_manifest' => add_query_arg( 'ver', Package_Version::PACKAGE_VERSION, plugins_url( 'i18n-manifest.json', self::build_dir() . '/build.php' ) ),
106                'former_names'  => array( self::TOP_VIDEOS_TYPE => self::TOP_VIDEOS_FORMER_NAMES ),
107                'plan_features' => array(
108                    self::TOP_VIDEOS_TYPE   => 'videos',
109                    self::VIDEO_PLAYER_TYPE => 'videos',
110                ),
111            ),
112            $registry
113        );
114    }
115
116    /**
117     * Append this package's build base URL to the dashboard's error reporting origins.
118     *
119     * @param string[] $origins Absolute URL prefixes, each ending in a slash.
120     * @return string[]
121     */
122    public static function add_error_reporting_origin( $origins ) {
123        $origins   = is_array( $origins ) ? $origins : array();
124        $origins[] = trailingslashit( plugins_url( '', self::build_dir() . '/build.php' ) );
125
126        return $origins;
127    }
128
129    /**
130     * Seed the Top videos instance into the Traffic section's default layout.
131     *
132     * Order 8 keeps the place the dashboard seeded it in before the widget moved here. A layout
133     * that already holds the instance, under this name or a former one, is left alone; and one
134     * seeded on a site where the type never registers loses it to the dashboard's own policy.
135     *
136     * @param mixed  $layout     The section's default widget instances; anything but an array passes through.
137     * @param string $section_id Namespaced section identifier.
138     * @return mixed The layout, with the instance appended when it is an array.
139     */
140    public static function add_default_layout_instance( $layout, $section_id ) {
141        if ( self::TRAFFIC_SECTION_ID !== $section_id || ! is_array( $layout ) ) {
142            return $layout;
143        }
144
145        foreach ( $layout as $item ) {
146            if ( ! is_array( $item ) ) {
147                continue;
148            }
149            if ( self::TOP_VIDEOS_INSTANCE_UUID === ( $item['uuid'] ?? null ) || self::is_top_videos_type( $item['type'] ?? null ) ) {
150                return $layout;
151            }
152        }
153
154        $layout[] = get_dashboard_default_widget_instance( self::TOP_VIDEOS_INSTANCE_UUID, self::TOP_VIDEOS_TYPE, 8, 1, DASHBOARD_TILE_LIST );
155
156        return $layout;
157    }
158
159    /**
160     * Whether a widget type name is the Top videos type, current or former.
161     *
162     * @param mixed $type A widget type name.
163     * @return bool
164     */
165    private static function is_top_videos_type( $type ) {
166        return is_string( $type ) && ( self::TOP_VIDEOS_TYPE === $type || in_array( $type, self::TOP_VIDEOS_FORMER_NAMES, true ) );
167    }
168
169    /**
170     * Whether the dashboard's widget contract is one this build works against.
171     *
172     * Undefined is a request that never loaded the dashboard's widget types, such as the sections
173     * REST route: the widgets wait for one that does.
174     *
175     * @return bool
176     */
177    private static function widget_contract_is_supported() {
178        if ( ! defined( 'Automattic\\Jetpack\\PremiumAnalytics\\WIDGET_API_VERSION' ) ) {
179            return false;
180        }
181
182        $version = \Automattic\Jetpack\PremiumAnalytics\WIDGET_API_VERSION;
183
184        return version_compare( $version, self::MIN_WIDGET_API_VERSION, '>=' ) && version_compare( $version, '3', '<' );
185    }
186
187    /**
188     * The widget manifest the package registers from: what its build generated, none without a build.
189     *
190     * @return array[] Manifest entries, as `jetpack_videopress_get_registered_widget_modules()` returns them.
191     */
192    private static function get_widget_manifest() {
193        self::load_build();
194        if ( ! function_exists( 'jetpack_videopress_get_registered_widget_modules' ) ) {
195            return array();
196        }
197
198        return (array) jetpack_videopress_get_registered_widget_modules();
199    }
200
201    /**
202     * Load the generated build once. Guarded by symbol, not by path: on WordPress.com two copies of
203     * the package can share a request.
204     *
205     * @return void
206     */
207    private static function load_build() {
208        if ( function_exists( 'jetpack_videopress_get_registered_widget_modules' ) ) {
209            return;
210        }
211        $loader = self::build_dir() . '/build.php';
212        if ( file_exists( $loader ) ) {
213            require_once $loader;
214        }
215    }
216
217    /**
218     * Directory of the wp-build output.
219     *
220     * @return string
221     */
222    private static function build_dir() {
223        return dirname( __DIR__ ) . '/build';
224    }
225}