Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.70% covered (success)
90.70%
39 / 43
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Analytics_Dashboard
90.70% covered (success)
90.70%
39 / 43
62.50% covered (warning)
62.50%
5 / 8
23.43
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%
15 / 15
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        if ( ! self::widget_contract_is_supported() ) {
96            return;
97        }
98
99        register_widget_types_from_manifest(
100            self::get_widget_manifest(),
101            array(
102                'textdomain'    => self::TEXTDOMAIN,
103                'i18n_manifest' => add_query_arg( 'ver', Package_Version::PACKAGE_VERSION, plugins_url( 'i18n-manifest.json', self::build_dir() . '/build.php' ) ),
104                'former_names'  => array( self::TOP_VIDEOS_TYPE => self::TOP_VIDEOS_FORMER_NAMES ),
105                'plan_features' => array(
106                    self::TOP_VIDEOS_TYPE   => 'videos',
107                    self::VIDEO_PLAYER_TYPE => 'videos',
108                ),
109            ),
110            $registry
111        );
112    }
113
114    /**
115     * Seed the Top videos instance into the Traffic section's default layout.
116     *
117     * Order 8 keeps the place the dashboard seeded it in before the widget moved here. A layout
118     * that already holds the instance, under this name or a former one, is left alone; and one
119     * seeded on a site where the type never registers loses it to the dashboard's own policy.
120     *
121     * @param mixed  $layout     The section's default widget instances; anything but an array passes through.
122     * @param string $section_id Namespaced section identifier.
123     * @return mixed The layout, with the instance appended when it is an array.
124     */
125    public static function add_default_layout_instance( $layout, $section_id ) {
126        if ( self::TRAFFIC_SECTION_ID !== $section_id || ! is_array( $layout ) ) {
127            return $layout;
128        }
129
130        foreach ( $layout as $item ) {
131            if ( ! is_array( $item ) ) {
132                continue;
133            }
134            if ( self::TOP_VIDEOS_INSTANCE_UUID === ( $item['uuid'] ?? null ) || self::is_top_videos_type( $item['type'] ?? null ) ) {
135                return $layout;
136            }
137        }
138
139        $layout[] = get_dashboard_default_widget_instance( self::TOP_VIDEOS_INSTANCE_UUID, self::TOP_VIDEOS_TYPE, 8, 1, DASHBOARD_TILE_LIST );
140
141        return $layout;
142    }
143
144    /**
145     * Whether a widget type name is the Top videos type, current or former.
146     *
147     * @param mixed $type A widget type name.
148     * @return bool
149     */
150    private static function is_top_videos_type( $type ) {
151        return is_string( $type ) && ( self::TOP_VIDEOS_TYPE === $type || in_array( $type, self::TOP_VIDEOS_FORMER_NAMES, true ) );
152    }
153
154    /**
155     * Whether the dashboard's widget contract is one this build works against.
156     *
157     * Undefined is a request that never loaded the dashboard's widget types, such as the sections
158     * REST route: the widgets wait for one that does.
159     *
160     * @return bool
161     */
162    private static function widget_contract_is_supported() {
163        if ( ! defined( 'Automattic\\Jetpack\\PremiumAnalytics\\WIDGET_API_VERSION' ) ) {
164            return false;
165        }
166
167        $version = \Automattic\Jetpack\PremiumAnalytics\WIDGET_API_VERSION;
168
169        return version_compare( $version, self::MIN_WIDGET_API_VERSION, '>=' ) && version_compare( $version, '3', '<' );
170    }
171
172    /**
173     * The widget manifest the package registers from: what its build generated, none without a build.
174     *
175     * @return array[] Manifest entries, as `jetpack_videopress_get_registered_widget_modules()` returns them.
176     */
177    private static function get_widget_manifest() {
178        self::load_build();
179        if ( ! function_exists( 'jetpack_videopress_get_registered_widget_modules' ) ) {
180            return array();
181        }
182
183        return (array) jetpack_videopress_get_registered_widget_modules();
184    }
185
186    /**
187     * Load the generated build once. Guarded by symbol, not by path: on WordPress.com two copies of
188     * the package can share a request.
189     *
190     * @return void
191     */
192    private static function load_build() {
193        if ( function_exists( 'jetpack_videopress_get_registered_widget_modules' ) ) {
194            return;
195        }
196        $loader = self::build_dir() . '/build.php';
197        if ( file_exists( $loader ) ) {
198            require_once $loader;
199        }
200    }
201
202    /**
203     * Directory of the wp-build output.
204     *
205     * @return string
206     */
207    private static function build_dir() {
208        return dirname( __DIR__ ) . '/build';
209    }
210}