Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.45% covered (warning)
85.45%
47 / 55
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Analytics_Dashboard
85.45% covered (warning)
85.45%
47 / 55
66.67% covered (warning)
66.67%
6 / 9
22.36
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 register_section
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 get_default_layout
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 register_widget_types
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 widget_contract_is_supported
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 widget_contract_moved_on
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 load_build
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 build_dir
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dashboard_has_section_slug
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
8.74
1<?php
2/**
3 * The Ads section and widgets of the Premium Analytics dashboard.
4 *
5 * @package automattic/jetpack-ads
6 */
7
8namespace Automattic\Jetpack\WordAds;
9
10use Automattic\Jetpack\PremiumAnalytics\Capabilities;
11use function Automattic\Jetpack\PremiumAnalytics\get_dashboard_default_widget_instance;
12use function Automattic\Jetpack\PremiumAnalytics\register_dashboard_section;
13use function Automattic\Jetpack\PremiumAnalytics\register_widget_types_from_manifest;
14use const Automattic\Jetpack\PremiumAnalytics\DASHBOARD_NAME;
15
16/**
17 * Registers the Ads section, its default layout and its widget types on the Premium Analytics
18 * dashboard.
19 *
20 * The package decides nothing about who gets Ads: the WordAds module of the Jetpack plugin calls
21 * `init()` outside the WordPress.com platform, jetpack-mu-wpcom calls the registrants on Simple
22 * and Atomic where the plan includes WordAds and the site has it on. Everything registers when the dashboard's registries
23 * hydrate, so a call on a site without the dashboard is inert.
24 *
25 * @since 0.1.0
26 */
27class Analytics_Dashboard {
28
29    const PACKAGE_VERSION = '0.1.4';
30
31    /**
32     * Namespaced section identifier. Its slug, `ads`, keys the section's URL and stored layouts.
33     */
34    const SECTION_ID = 'wordads/ads';
35
36    /**
37     * Widget type names the package builds, from `widgets/*\/widget.json`.
38     */
39    const CHART_TABS_TYPE       = 'wordads/chart-tabs';
40    const HIGHLIGHTS_TYPE       = 'wordads/highlights';
41    const EARNINGS_HISTORY_TYPE = 'wordads/earnings-history';
42
43    /**
44     * Registry actions of the dashboard package.
45     */
46    const REGISTER_SECTIONS_ACTION     = 'jetpack_premium_analytics_register_dashboard_sections';
47    const REGISTER_WIDGET_TYPES_ACTION = 'jetpack_premium_analytics_register_widget_types';
48
49    /**
50     * Lowest widget contract the build works against: 2.0.0, the start of the major it is built for.
51     */
52    const MIN_WIDGET_API_VERSION = '2.0.0';
53
54    /**
55     * Text domain of the widget metadata and of the built widget bundles.
56     */
57    const TEXTDOMAIN = 'jetpack-ads-pkg';
58
59    /**
60     * Hook both registrants on the dashboard's registry actions.
61     *
62     * Priority 20, after the dashboard package's own registrants: an older package that still
63     * registers the section itself is found by slug and left alone.
64     *
65     * @return void
66     */
67    public static function init() {
68        add_action( self::REGISTER_SECTIONS_ACTION, array( __CLASS__, 'register_section' ), 20 );
69        add_action( self::REGISTER_WIDGET_TYPES_ACTION, array( __CLASS__, 'register_widget_types' ), 20 );
70    }
71
72    /**
73     * Register the Ads section unless another owner already holds the `ads` slug.
74     *
75     * Also skipped when the dashboard's widget contract moved past this build: a tab whose every
76     * widget is unavailable is worse than no tab.
77     *
78     * @param object $registry The section registry being hydrated.
79     * @return void
80     */
81    public static function register_section( $registry ) {
82        if ( self::widget_contract_moved_on() || self::dashboard_has_section_slug( $registry, DASHBOARD_NAME, 'ads' ) ) {
83            return;
84        }
85
86        register_dashboard_section(
87            DASHBOARD_NAME,
88            self::SECTION_ID,
89            array(
90                'label'               => __( 'Ads', 'jetpack-ads-pkg' ),
91                'title'               => __( 'Ads performance', 'jetpack-ads-pkg' ),
92                'order'               => 50,
93                'is_available'        => array( Capabilities::class, 'current_user_can_view_ad_reports' ),
94                // Only the chart supports dates, so it owns the control. No Ads widget
95                // supports comparison.
96                'date_filter_options' => array(
97                    'with_date_comparison'     => false,
98                    'with_header_date_control' => false,
99                ),
100                'default_layout'      => array( __CLASS__, 'get_default_layout' ),
101            )
102        );
103    }
104
105    /**
106     * The default layout of the Ads section: chart on top, balance, then earnings history.
107     *
108     * @return array[] Widget instances, as `get_dashboard_default_widget_instance()` builds them.
109     */
110    public static function get_default_layout() {
111        return array(
112            get_dashboard_default_widget_instance( 'default-wordads-chart-tabs-widget-instance', self::CHART_TABS_TYPE, 0, 3, 2 ),
113            get_dashboard_default_widget_instance( 'default-wordads-highlights-widget-instance', self::HIGHLIGHTS_TYPE, 1, 3, 1 ),
114            get_dashboard_default_widget_instance( 'default-wordads-earnings-history-widget-instance', self::EARNINGS_HISTORY_TYPE, 2, 1, 2 ),
115        );
116    }
117
118    /**
119     * Register the widget types from the package's build manifest.
120     *
121     * Loads the generated build first: after `init` it registers the widget script modules on the
122     * spot, so they reach the page import map the same request.
123     *
124     * @param object $registry The widget type registry being hydrated.
125     * @return void
126     */
127    public static function register_widget_types( $registry ) {
128        if ( ! self::widget_contract_is_supported() ) {
129            return;
130        }
131
132        self::load_build();
133        if ( ! function_exists( 'jetpack_ads_get_registered_widget_modules' ) ) {
134            return;
135        }
136
137        register_widget_types_from_manifest(
138            jetpack_ads_get_registered_widget_modules(),
139            array(
140                'textdomain'    => self::TEXTDOMAIN,
141                'i18n_manifest' => add_query_arg( 'ver', self::PACKAGE_VERSION, plugins_url( 'i18n-manifest.json', self::build_dir() . '/build.php' ) ),
142            ),
143            $registry
144        );
145    }
146
147    /**
148     * Whether the dashboard's widget contract is one the widgets were built against: at least the
149     * minimum, and not the next major.
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, '3', '<' );
161    }
162
163    /**
164     * Whether the dashboard's widget contract moved to a major this package was not built against.
165     *
166     * Undefined is not a mismatch: the sections REST route hydrates the section registry before
167     * the dashboard loads the file that defines the version.
168     *
169     * @return bool
170     */
171    private static function widget_contract_moved_on() {
172        return defined( 'Automattic\\Jetpack\\PremiumAnalytics\\WIDGET_API_VERSION' )
173            && version_compare( \Automattic\Jetpack\PremiumAnalytics\WIDGET_API_VERSION, '3', '>=' );
174    }
175
176    /**
177     * Load the generated build once. Guarded by symbol, not by path: on WordPress.com Simple two
178     * copies of the package can share a request.
179     *
180     * @return void
181     */
182    private static function load_build() {
183        if ( function_exists( 'jetpack_ads_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
201    /**
202     * Whether a section with the slug is registered on the dashboard.
203     *
204     * Reads the registry through the slug lookup when the package offers it, and through the full
205     * list otherwise: the package and this one ship on different cadences.
206     *
207     * @param object $registry       The registry being hydrated.
208     * @param string $dashboard_name Dashboard identifier.
209     * @param string $slug           Section slug.
210     * @return bool
211     */
212    private static function dashboard_has_section_slug( $registry, $dashboard_name, $slug ) {
213        if ( method_exists( $registry, 'get_registered_by_slug' ) ) {
214            return null !== $registry->get_registered_by_slug( $dashboard_name, $slug );
215        }
216
217        foreach ( $registry->get_all_registered( $dashboard_name ) as $section ) {
218            if ( $section->slug === $slug ) {
219                return true;
220            }
221        }
222
223        return false;
224    }
225}