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.2';
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: the widgets name their icons in `widget.json`,
51     * which the dashboard resolves from 1.6.0.
52     */
53    const MIN_WIDGET_API_VERSION = '1.6.0';
54
55    /**
56     * Text domain of the widget metadata and of the built widget bundles.
57     */
58    const TEXTDOMAIN = 'jetpack-ads-pkg';
59
60    /**
61     * Hook both registrants on the dashboard's registry actions.
62     *
63     * Priority 20, after the dashboard package's own registrants: an older package that still
64     * registers the section itself is found by slug and left alone.
65     *
66     * @return void
67     */
68    public static function init() {
69        add_action( self::REGISTER_SECTIONS_ACTION, array( __CLASS__, 'register_section' ), 20 );
70        add_action( self::REGISTER_WIDGET_TYPES_ACTION, array( __CLASS__, 'register_widget_types' ), 20 );
71    }
72
73    /**
74     * Register the Ads section unless another owner already holds the `ads` slug.
75     *
76     * Also skipped when the dashboard's widget contract moved past this build: a tab whose every
77     * widget is unavailable is worse than no tab.
78     *
79     * @param object $registry The section registry being hydrated.
80     * @return void
81     */
82    public static function register_section( $registry ) {
83        if ( self::widget_contract_moved_on() || self::dashboard_has_section_slug( $registry, DASHBOARD_NAME, 'ads' ) ) {
84            return;
85        }
86
87        register_dashboard_section(
88            DASHBOARD_NAME,
89            self::SECTION_ID,
90            array(
91                'label'               => __( 'Ads', 'jetpack-ads-pkg' ),
92                'title'               => __( 'Ads performance', 'jetpack-ads-pkg' ),
93                'order'               => 50,
94                'is_available'        => array( Capabilities::class, 'current_user_can_view_ad_reports' ),
95                // Only the chart supports dates, so it owns the control. No Ads widget
96                // supports comparison.
97                'date_filter_options' => array(
98                    'with_date_comparison'     => false,
99                    'with_header_date_control' => false,
100                ),
101                'default_layout'      => array( __CLASS__, 'get_default_layout' ),
102            )
103        );
104    }
105
106    /**
107     * The default layout of the Ads section: chart on top, balance, then earnings history.
108     *
109     * @return array[] Widget instances, as `get_dashboard_default_widget_instance()` builds them.
110     */
111    public static function get_default_layout() {
112        return array(
113            get_dashboard_default_widget_instance( 'default-wordads-chart-tabs-widget-instance', self::CHART_TABS_TYPE, 0, 3, 2 ),
114            get_dashboard_default_widget_instance( 'default-wordads-highlights-widget-instance', self::HIGHLIGHTS_TYPE, 1, 3, 1 ),
115            get_dashboard_default_widget_instance( 'default-wordads-earnings-history-widget-instance', self::EARNINGS_HISTORY_TYPE, 2, 1, 2 ),
116        );
117    }
118
119    /**
120     * Register the widget types from the package's build manifest.
121     *
122     * Loads the generated build first: after `init` it registers the widget script modules on the
123     * spot, so they reach the page import map the same request.
124     *
125     * @param object $registry The widget type registry being hydrated.
126     * @return void
127     */
128    public static function register_widget_types( $registry ) {
129        if ( ! self::widget_contract_is_supported() ) {
130            return;
131        }
132
133        self::load_build();
134        if ( ! function_exists( 'jetpack_ads_get_registered_widget_modules' ) ) {
135            return;
136        }
137
138        register_widget_types_from_manifest(
139            jetpack_ads_get_registered_widget_modules(),
140            array(
141                'textdomain'    => self::TEXTDOMAIN,
142                'i18n_manifest' => add_query_arg( 'ver', self::PACKAGE_VERSION, plugins_url( 'i18n-manifest.json', self::build_dir() . '/build.php' ) ),
143            ),
144            $registry
145        );
146    }
147
148    /**
149     * Whether the dashboard's widget contract is one the widgets were built against: at least the
150     * minimum, and not the next major.
151     *
152     * @return bool
153     */
154    private static function widget_contract_is_supported() {
155        if ( ! defined( 'Automattic\\Jetpack\\PremiumAnalytics\\WIDGET_API_VERSION' ) ) {
156            return false;
157        }
158
159        $version = \Automattic\Jetpack\PremiumAnalytics\WIDGET_API_VERSION;
160
161        return version_compare( $version, self::MIN_WIDGET_API_VERSION, '>=' ) && version_compare( $version, '2', '<' );
162    }
163
164    /**
165     * Whether the dashboard's widget contract moved to a major this package was not built against.
166     *
167     * Undefined is not a mismatch: the sections REST route hydrates the section registry before
168     * the dashboard loads the file that defines the version.
169     *
170     * @return bool
171     */
172    private static function widget_contract_moved_on() {
173        return defined( 'Automattic\\Jetpack\\PremiumAnalytics\\WIDGET_API_VERSION' )
174            && version_compare( \Automattic\Jetpack\PremiumAnalytics\WIDGET_API_VERSION, '2', '>=' );
175    }
176
177    /**
178     * Load the generated build once. Guarded by symbol, not by path: on WordPress.com Simple two
179     * copies of the package can share a request.
180     *
181     * @return void
182     */
183    private static function load_build() {
184        if ( function_exists( 'jetpack_ads_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
202    /**
203     * Whether a section with the slug is registered on the dashboard.
204     *
205     * Reads the registry through the slug lookup when the package offers it, and through the full
206     * list otherwise: the package and this one ship on different cadences.
207     *
208     * @param object $registry       The registry being hydrated.
209     * @param string $dashboard_name Dashboard identifier.
210     * @param string $slug           Section slug.
211     * @return bool
212     */
213    private static function dashboard_has_section_slug( $registry, $dashboard_name, $slug ) {
214        if ( method_exists( $registry, 'get_registered_by_slug' ) ) {
215            return null !== $registry->get_registered_by_slug( $dashboard_name, $slug );
216        }
217
218        foreach ( $registry->get_all_registered( $dashboard_name ) as $section ) {
219            if ( $section->slug === $slug ) {
220                return true;
221            }
222        }
223
224        return false;
225    }
226}