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