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