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