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