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