Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/google-site-kit/includes/Modules/Analytics_4.php

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
1 + <?php
2 + /**
3 + * Class Google\Site_Kit\Modules\Analytics_4
4 + *
5 + * @package Google\Site_Kit
6 + * @copyright 2021 Google LLC
7 + * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
8 + * @link https://sitekit.withgoogle.com
9 + */
10 +
11 + // phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded
12 +
13 + namespace Google\Site_Kit\Modules;
14 +
15 + use Exception;
16 + use Google\Site_Kit\Context;
17 + use Google\Site_Kit\Core\Assets\Asset;
18 + use Google\Site_Kit\Core\Assets\Assets;
19 + use Google\Site_Kit\Core\Assets\Script;
20 + use Google\Site_Kit\Core\Authentication\Authentication;
21 + use Google\Site_Kit\Core\Authentication\Clients\Google_Site_Kit_Client;
22 + use Google\Site_Kit\Core\Dismissals\Dismissed_Items;
23 + use Google\Site_Kit\Core\Modules\Analytics_4\Tag_Matchers;
24 + use Google\Site_Kit\Core\Modules\Module;
25 + use Google\Site_Kit\Core\Modules\Module_Settings;
26 + use Google\Site_Kit\Core\Modules\Module_With_Activation;
27 + use Google\Site_Kit\Core\Modules\Module_With_Deactivation;
28 + use Google\Site_Kit\Core\Modules\Module_With_Debug_Fields;
29 + use Google\Site_Kit\Core\Modules\Module_With_Assets;
30 + use Google\Site_Kit\Core\Modules\Module_With_Assets_Trait;
31 + use Google\Site_Kit\Core\Modules\Module_With_Data_Available_State;
32 + use Google\Site_Kit\Core\Modules\Module_With_Data_Available_State_Trait;
33 + use Google\Site_Kit\Core\Modules\Module_With_Inline_Data;
34 + use Google\Site_Kit\Core\Modules\Module_With_Inline_Data_Trait;
35 + use Google\Site_Kit\Core\Modules\Module_With_Scopes;
36 + use Google\Site_Kit\Core\Modules\Module_With_Scopes_Trait;
37 + use Google\Site_Kit\Core\Modules\Module_With_Settings;
38 + use Google\Site_Kit\Core\Modules\Module_With_Settings_Trait;
39 + use Google\Site_Kit\Core\Modules\Module_With_Owner;
40 + use Google\Site_Kit\Core\Modules\Module_With_Owner_Trait;
41 + use Google\Site_Kit\Core\Modules\Module_With_Service_Entity;
42 + use Google\Site_Kit\Core\Permissions\Permissions;
43 + use Google\Site_Kit\Core\Modules\Module_With_Tag;
44 + use Google\Site_Kit\Core\Modules\Module_With_Tag_Trait;
45 + use Google\Site_Kit\Core\Modules\Tags\Module_Tag_Matchers;
46 + use Google\Site_Kit\Core\REST_API\Exception\Invalid_Datapoint_Exception;
47 + use Google\Site_Kit\Core\REST_API\Data_Request;
48 + use Google\Site_Kit\Core\REST_API\Exception\Invalid_Param_Exception;
49 + use Google\Site_Kit\Core\REST_API\Exception\Missing_Required_Param_Exception;
50 + use Google\Site_Kit\Core\Site_Health\Debug_Data;
51 + use Google\Site_Kit\Core\Storage\Options;
52 + use Google\Site_Kit\Core\Storage\User_Options;
53 + use Google\Site_Kit\Core\Tags\Guards\Tag_Environment_Type_Guard;
54 + use Google\Site_Kit\Core\Tags\Guards\Tag_Verify_Guard;
55 + use Google\Site_Kit\Core\Util\BC_Functions;
56 + use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
57 + use Google\Site_Kit\Core\Util\Sort;
58 + use Google\Site_Kit\Core\Util\URL;
59 + use Google\Site_Kit\Modules\AdSense\Settings as AdSense_Settings;
60 + use Google\Site_Kit\Modules\Analytics_4\Account_Ticket;
61 + use Google\Site_Kit\Modules\Analytics_4\Advanced_Tracking;
62 + use Google\Site_Kit\Modules\Analytics_4\AMP_Tag;
63 + use Google\Site_Kit\Modules\Analytics_4\Custom_Dimensions_Data_Available;
64 + use Google\Site_Kit\Modules\Analytics_4\Datapoints\Create_Account_Ticket;
65 + use Google\Site_Kit\Modules\Analytics_4\Datapoints\Create_Property;
66 + use Google\Site_Kit\Modules\Analytics_4\Datapoints\Create_Webdatastream;
67 + use Google\Site_Kit\Modules\Analytics_4\Synchronize_Property;
68 + use Google\Site_Kit\Modules\Analytics_4\Synchronize_AdSenseLinked;
69 + use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\AccountProvisioningService;
70 + use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\EnhancedMeasurementSettingsModel;
71 + use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\PropertiesAdSenseLinksService;
72 + use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\PropertiesAudiencesService;
73 + use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\PropertiesEnhancedMeasurementService;
74 + use Google\Site_Kit\Modules\Analytics_4\Report\Request as Analytics_4_Report_Request;
75 + use Google\Site_Kit\Modules\Analytics_4\Report\Response as Analytics_4_Report_Response;
76 + use Google\Site_Kit\Modules\Analytics_4\Resource_Data_Availability_Date;
77 + use Google\Site_Kit\Modules\Analytics_4\Settings;
78 + use Google\Site_Kit\Modules\Analytics_4\Synchronize_AdsLinked;
79 + use Google\Site_Kit\Modules\Analytics_4\Tag_Guard;
80 + use Google\Site_Kit\Modules\Analytics_4\Tag_Interface;
81 + use Google\Site_Kit\Modules\Analytics_4\Web_Tag;
82 + use Google\Site_Kit_Dependencies\Google\Model as Google_Model;
83 + use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData as Google_Service_AnalyticsData;
84 + use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportRequest as Google_Service_AnalyticsData_RunReportRequest;
85 + use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\DateRange as Google_Service_AnalyticsData_DateRange;
86 + use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Dimension as Google_Service_AnalyticsData_Dimension;
87 + use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Metric as Google_Service_AnalyticsData_Metric;
88 + use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin as Google_Service_GoogleAnalyticsAdmin;
89 + use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1alphaAudience;
90 + use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaCustomDimension;
91 + use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaDataStream;
92 + use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaDataStreamWebStreamData;
93 + use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaListDataStreamsResponse;
94 + use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaProperty as Google_Service_GoogleAnalyticsAdmin_GoogleAnalyticsAdminV1betaProperty;
95 + use Google\Site_Kit_Dependencies\Google\Service\TagManager as Google_Service_TagManager;
96 + use Google\Site_Kit_Dependencies\Google_Service_TagManager_Container;
97 + use Google\Site_Kit_Dependencies\Psr\Http\Message\RequestInterface;
98 + use Google\Site_Kit\Core\REST_API\REST_Routes;
99 + use Google\Site_Kit\Core\Tracking\Feature_Metrics_Trait;
100 + use Google\Site_Kit\Core\Tracking\Provides_Feature_Metrics;
101 + use Google\Site_Kit\Core\Util\Feature_Flags;
102 + use Google\Site_Kit\Modules\Analytics_4\Audience_Settings;
103 + use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Cron;
104 + use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Events_Sync;
105 + use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_New_Badge_Events_Sync;
106 + use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Provider;
107 + use Google\Site_Kit\Modules\Analytics_4\Reset_Audiences;
108 + use stdClass;
109 + use WP_Error;
110 + use WP_Post;
111 +
112 + /**
113 + * Class representing the Analytics 4 module.
114 + *
115 + * @since 1.30.0
116 + * @access private
117 + * @ignore
118 + */
119 + final class Analytics_4 extends Module implements Module_With_Inline_Data, Module_With_Scopes, Module_With_Settings, Module_With_Debug_Fields, Module_With_Owner, Module_With_Assets, Module_With_Service_Entity, Module_With_Activation, Module_With_Deactivation, Module_With_Data_Available_State, Module_With_Tag, Provides_Feature_Metrics {
120 +
121 + use Method_Proxy_Trait;
122 + use Module_With_Assets_Trait;
123 + use Module_With_Owner_Trait;
124 + use Module_With_Scopes_Trait;
125 + use Module_With_Settings_Trait;
126 + use Module_With_Data_Available_State_Trait;
127 + use Module_With_Tag_Trait;
128 + use Module_With_Inline_Data_Trait;
129 + use Feature_Metrics_Trait;
130 +
131 + const PROVISION_ACCOUNT_TICKET_ID = 'googlesitekit_analytics_provision_account_ticket_id';
132 +
133 + const READONLY_SCOPE = 'https://www.googleapis.com/auth/analytics.readonly';
134 + const EDIT_SCOPE = 'https://www.googleapis.com/auth/analytics.edit';
135 +
136 + /**
137 + * Module slug name.
138 + */
139 + const MODULE_SLUG = 'analytics-4';
140 +
141 + /**
142 + * Prefix used to fetch custom dimensions in reports.
143 + */
144 + const CUSTOM_EVENT_PREFIX = 'customEvent:';
145 +
146 + /**
147 + * Custom dimensions tracked by Site Kit.
148 + */
149 + const CUSTOM_DIMENSION_POST_AUTHOR = 'googlesitekit_post_author';
150 + const CUSTOM_DIMENSION_POST_CATEGORIES = 'googlesitekit_post_categories';
151 +
152 + /**
153 + * Weights for audience types when sorting audiences in the selection panel
154 + * and within the dashboard widget.
155 + */
156 + const AUDIENCE_TYPE_SORT_ORDER = array(
157 + 'USER_AUDIENCE' => 0,
158 + 'SITE_KIT_AUDIENCE' => 1,
159 + 'DEFAULT_AUDIENCE' => 2,
160 + );
161 +
162 + /**
163 + * Custom_Dimensions_Data_Available instance.
164 + *
165 + * @since 1.113.0
166 + * @var Custom_Dimensions_Data_Available
167 + */
168 + protected $custom_dimensions_data_available;
169 +
170 + /**
171 + * Reset_Audiences instance.
172 + *
173 + * @since 1.137.0
174 + * @var Reset_Audiences
175 + */
176 + protected $reset_audiences;
177 +
178 + /**
179 + * Resource_Data_Availability_Date instance.
180 + *
181 + * @since 1.127.0
182 + * @var Resource_Data_Availability_Date
183 + */
184 + protected $resource_data_availability_date;
185 +
186 + /**
187 + * Audience_Settings instance.
188 + *
189 + * @since 1.148.0
190 + *
191 + * @var Audience_Settings
192 + */
193 + protected $audience_settings;
194 +
195 + /**
196 + * Constructor.
197 + *
198 + * @since 1.113.0
199 + *
200 + * @param Context $context Plugin context.
201 + * @param Options $options Optional. Option API instance. Default is a new instance.
202 + * @param User_Options $user_options Optional. User Option API instance. Default is a new instance.
203 + * @param Authentication $authentication Optional. Authentication instance. Default is a new instance.
204 + * @param Assets $assets Optional. Assets API instance. Default is a new instance.
205 + */
206 + public function __construct(
207 + Context $context,
208 + ?Options $options = null,
209 + ?User_Options $user_options = null,
210 + ?Authentication $authentication = null,
211 + ?Assets $assets = null
212 + ) {
213 + parent::__construct( $context, $options, $user_options, $authentication, $assets );
214 + $this->custom_dimensions_data_available = new Custom_Dimensions_Data_Available( $this->transients );
215 + $this->reset_audiences = new Reset_Audiences( $this->user_options );
216 + $this->audience_settings = new Audience_Settings( $this->options );
217 + $this->resource_data_availability_date = new Resource_Data_Availability_Date( $this->transients, $this->get_settings(), $this->audience_settings );
218 + }
219 +
220 + /**
221 + * Registers functionality through WordPress hooks.
222 + *
223 + * @since 1.30.0
224 + * @since 1.101.0 Added a filter hook to add the required `https://www.googleapis.com/auth/tagmanager.readonly` scope for GTE support.
225 + */
226 + public function register() {
227 + $this->register_scopes_hook();
228 +
229 + $this->register_inline_data();
230 +
231 + $this->register_feature_metrics();
232 +
233 + $synchronize_property = new Synchronize_Property(
234 + $this,
235 + $this->user_options
236 + );
237 + $synchronize_property->register();
238 +
239 + $synchronize_adsense_linked = new Synchronize_AdSenseLinked(
240 + $this,
241 + $this->user_options,
242 + $this->options
243 + );
244 + $synchronize_adsense_linked->register();
245 +
246 + $synchronize_ads_linked = new Synchronize_AdsLinked(
247 + $this,
248 + $this->user_options
249 + );
250 + $synchronize_ads_linked->register();
251 +
252 + $conversion_reporting_provider = new Conversion_Reporting_Provider(
253 + $this->context,
254 + $this->settings,
255 + $this->user_options,
256 + $this
257 + );
258 + $conversion_reporting_provider->register();
259 +
260 + $this->audience_settings->register();
261 +
262 + ( new Advanced_Tracking( $this->context ) )->register();
263 +
264 + add_action( 'admin_init', array( $synchronize_property, 'maybe_schedule_synchronize_property' ) );
265 + add_action( 'admin_init', array( $synchronize_adsense_linked, 'maybe_schedule_synchronize_adsense_linked' ) );
266 + add_action( 'load-toplevel_page_googlesitekit-dashboard', array( $synchronize_ads_linked, 'maybe_schedule_synchronize_ads_linked' ) );
267 + add_action( 'admin_init', $this->get_method_proxy( 'handle_provisioning_callback' ) );
268 +
269 + // For non-AMP and AMP.
270 + add_action( 'wp_head', $this->get_method_proxy( 'print_tracking_opt_out' ), 0 );
271 + // For Web Stories plugin.
272 + add_action( 'web_stories_story_head', $this->get_method_proxy( 'print_tracking_opt_out' ), 0 );
273 +
274 + // Analytics 4 tag placement logic.
275 + add_action( 'template_redirect', array( $this, 'register_tag' ) );
276 +
277 + $this->audience_settings->on_change(
278 + function ( $old_value, $new_value ) {
279 + // Ensure that the resource data availability dates for `availableAudiences` that no longer exist are reset.
280 + $old_available_audiences = $old_value['availableAudiences'];
281 + if ( $old_available_audiences ) {
282 + $old_available_audience_names = array_map(
283 + function ( $audience ) {
284 + return $audience['name'];
285 + },
286 + $old_available_audiences
287 + );
288 +
289 + $new_available_audiences = $new_value['availableAudiences'] ?? array();
290 + $new_available_audience_names = array_map(
291 + function ( $audience ) {
292 + return $audience['name'];
293 + },
294 + $new_available_audiences
295 + );
296 +
297 + $unavailable_audience_names = array_diff( $old_available_audience_names, $new_available_audience_names );
298 +
299 + foreach ( $unavailable_audience_names as $unavailable_audience_name ) {
300 + $this->resource_data_availability_date->reset_resource_date( $unavailable_audience_name, Resource_Data_Availability_Date::RESOURCE_TYPE_AUDIENCE );
301 + }
302 + }
303 + }
304 + );
305 +
306 + $this->get_settings()->on_change(
307 + function ( $old_value, $new_value ) {
308 + // Ensure that the data available state is reset when the property ID or measurement ID changes.
309 + if ( $old_value['propertyID'] !== $new_value['propertyID'] || $old_value['measurementID'] !== $new_value['measurementID'] ) {
310 + $this->reset_data_available();
311 + $this->custom_dimensions_data_available->reset_data_available();
312 +
313 + $audience_settings = $this->audience_settings->get();
314 + $available_audiences = $audience_settings['availableAudiences'] ?? array();
315 +
316 + $available_audience_names = array_map(
317 + function ( $audience ) {
318 + return $audience['name'];
319 + },
320 + $available_audiences
321 + );
322 +
323 + $this->resource_data_availability_date->reset_all_resource_dates( $available_audience_names, $old_value['propertyID'] );
324 + }
325 +
326 + // Reset property specific settings when propertyID changes.
327 + if ( $old_value['propertyID'] !== $new_value['propertyID'] ) {
328 + $this->get_settings()->merge(
329 + array(
330 + 'adSenseLinked' => false,
331 + 'adSenseLinkedLastSyncedAt' => 0,
332 + 'adsLinked' => false,
333 + 'adsLinkedLastSyncedAt' => 0,
334 + 'detectedEvents' => array(),
335 + )
336 + );
337 +
338 + $this->audience_settings->delete();
339 +
340 + if ( ! empty( $new_value['propertyID'] ) ) {
341 + do_action( Synchronize_AdSenseLinked::CRON_SYNCHRONIZE_ADSENSE_LINKED );
342 +
343 + // Reset event detection and new badge events.
344 + $this->transients->delete( Conversion_Reporting_Events_Sync::DETECTED_EVENTS_TRANSIENT );
345 + $this->transients->delete( Conversion_Reporting_Events_Sync::LOST_EVENTS_TRANSIENT );
346 + $this->transients->delete( Conversion_Reporting_New_Badge_Events_Sync::NEW_EVENTS_BADGE_TRANSIENT );
347 +
348 + $this->transients->set( Conversion_Reporting_New_Badge_Events_Sync::SKIP_NEW_BADGE_TRANSIENT, 1 );
349 +
350 + do_action( Conversion_Reporting_Cron::CRON_ACTION );
351 + }
352 +
353 + // Reset audience specific settings.
354 + $this->reset_audiences->reset_audience_data();
355 + }
356 + }
357 + );
358 +
359 + // Check if the property ID has changed and reset applicable settings to null.
360 + //
361 + // This is not done using the `get_settings()->merge` method because
362 + // `Module_Settings::merge` doesn't support setting a value to `null`.
363 + add_filter(
364 + 'pre_update_option_googlesitekit_analytics-4_settings',
365 + function ( $new_value, $old_value ) {
366 + if ( $new_value['propertyID'] !== $old_value['propertyID'] ) {
367 + $new_value['availableCustomDimensions'] = null;
368 + }
369 +
370 + return $new_value;
371 + },
372 + 10,
373 + 2
374 + );
375 +
376 + add_filter(
377 + 'googlesitekit_auth_scopes',
378 + function ( array $scopes ) {
379 + $oauth_client = $this->authentication->get_oauth_client();
380 +
381 + $needs_tagmanager_scope = false;
382 +
383 + $refined_scopes = $this->get_refined_scopes( $scopes );
384 +
385 + if ( $oauth_client->has_sufficient_scopes(
386 + array_merge(
387 + $refined_scopes,
388 + array(
389 + 'https://www.googleapis.com/auth/tagmanager.readonly',
390 + ),
391 + )
392 + ) ) {
393 + $needs_tagmanager_scope = true;
394 +
395 + // Ensure the Tag Manager scope is not added as a required scope in the case where the user has
396 + // granted the Analytics scope but not the Tag Manager scope, in order to allow the GTE-specific
397 + // Unsatisfied Scopes notification to be displayed without the Additional Permissions Required
398 + // modal also appearing.
399 + } elseif ( ! $oauth_client->has_sufficient_scopes(
400 + $refined_scopes
401 + ) ) {
402 + $needs_tagmanager_scope = true;
403 + }
404 +
405 + if ( $needs_tagmanager_scope ) {
406 + $refined_scopes[] = 'https://www.googleapis.com/auth/tagmanager.readonly';
407 + }
408 +
409 + return $refined_scopes;
410 + }
411 + );
412 +
413 + add_filter( 'googlesitekit_allow_tracking_disabled', $this->get_method_proxy( 'filter_analytics_allow_tracking_disabled' ) );
414 +
415 + // This hook adds the "Set up Google Analytics" step to the Site Kit
416 + // setup flow.
417 + //
418 + // This filter is documented in
419 + // Core\Authentication\Google_Proxy::get_metadata_fields.
420 + add_filter(
421 + 'googlesitekit_proxy_setup_mode',
422 + function ( $original_mode ) {
423 + return ! $this->is_connected()
424 + ? 'analytics-step'
425 + : $original_mode;
426 + }
427 + );
428 +
429 + // Preload the path to avoid layout shift for audience setup CTA banner.
430 + add_filter(
431 + 'googlesitekit_apifetch_preload_paths',
432 + function ( $routes ) {
433 + return array_merge(
434 + $routes,
435 + array(
436 + '/' . REST_Routes::REST_ROOT . '/modules/analytics-4/data/audience-settings',
437 + )
438 + );
439 + }
440 + );
441 +
442 + add_filter(
443 + 'googlesitekit_ads_measurement_connection_checks',
444 + function ( $checks ) {
445 + $checks[] = array( $this, 'check_ads_measurement_connection' );
446 + return $checks;
447 + },
448 + 20
449 + );
450 + }
451 +
452 + /**
453 + * Checks if the Analytics 4 module is connected and contributing to Ads measurement.
454 + *
455 + * Verifies connection status and settings to determine if Ads-related configurations
456 + * (AdSense linked or Google Tag Container with AW- destination IDs) exist.
457 + *
458 + * @since 1.151.0
459 + *
460 + * @return bool True if Analytics 4 is connected and configured for Ads measurement; false otherwise.
461 + */
462 + public function check_ads_measurement_connection() {
463 + if ( ! $this->is_connected() ) {
464 + return false;
465 + }
466 + $settings = $this->get_settings()->get();
467 +
468 + if ( $settings['adsLinked'] ) {
469 + return true;
470 + }
471 +
472 + foreach ( (array) $settings['googleTagContainerDestinationIDs'] as $destination_id ) {
473 + if ( 0 === stripos( $destination_id, 'AW-' ) ) {
474 + return true;
475 + }
476 + }
477 +
478 + return false;
479 + }
480 +
481 + /**
482 + * Gets required Google OAuth scopes for the module.
483 + *
484 + * @since 1.30.0
485 + *
486 + * @return array List of Google OAuth scopes.
487 + */
488 + public function get_scopes() {
489 + return array( self::READONLY_SCOPE );
490 + }
491 +
492 + /**
493 + * Checks whether the module is connected.
494 + *
495 + * A module being connected means that all steps required as part of its activation are completed.
496 + *
497 + * @since 1.30.0
498 + *
499 + * @return bool True if module is connected, false otherwise.
500 + */
501 + public function is_connected() {
502 + $required_keys = array(
503 + 'accountID',
504 + 'propertyID',
505 + 'webDataStreamID',
506 + 'measurementID',
507 + );
508 +
509 + $options = $this->get_settings()->get();
510 + foreach ( $required_keys as $required_key ) {
511 + if ( empty( $options[ $required_key ] ) ) {
512 + return false;
513 + }
514 + }
515 +
516 + return parent::is_connected();
517 + }
518 +
519 + /**
520 + * Cleans up when the module is activated.
521 + *
522 + * @since 1.107.0
523 + */
524 + public function on_activation() {
525 + $dismissed_items = new Dismissed_Items( $this->user_options );
526 + $dismissed_items->remove( 'key-metrics-connect-ga4-cta-widget' );
527 + }
528 +
529 + /**
530 + * Cleans up when the module is deactivated.
531 + *
532 + * @since 1.30.0
533 + */
534 + public function on_deactivation() {
535 + // We need to reset the resource data availability dates before deleting the settings.
536 + // This is because the property ID and the audience resource names are pulled from settings.
537 + $this->resource_data_availability_date->reset_all_resource_dates();
538 + $this->get_settings()->delete();
539 + $this->reset_data_available();
540 + $this->custom_dimensions_data_available->reset_data_available();
541 + $this->reset_audiences->reset_audience_data();
542 + $this->audience_settings->delete();
543 + }
544 +
545 + /**
546 + * Checks whether the AdSense module is connected.
547 + *
548 + * @since 1.121.0
549 + *
550 + * @return bool True if AdSense is connected, false otherwise.
551 + */
552 + private function is_adsense_connected() {
553 + $adsense_settings = ( new AdSense_Settings( $this->options ) )->get();
554 +
555 + if ( empty( $adsense_settings['accountSetupComplete'] ) || empty( $adsense_settings['siteSetupComplete'] ) ) {
556 + return false;
557 + }
558 +
559 + return true;
560 + }
561 +
562 + /**
563 + * Gets an array of debug field definitions.
564 + *
565 + * @since 1.30.0
566 + *
567 + * @return array
568 + */
569 + public function get_debug_fields() {
570 + $settings = $this->get_settings()->get();
571 +
572 + $debug_fields = array(
573 + 'analytics_4_account_id' => array(
574 + 'label' => __( 'Analytics: Account ID', 'google-site-kit' ),
575 + 'value' => $settings['accountID'],
576 + 'debug' => Debug_Data::redact_debug_value( $settings['accountID'] ),
577 + ),
578 + 'analytics_4_property_id' => array(
579 + 'label' => __( 'Analytics: Property ID', 'google-site-kit' ),
580 + 'value' => $settings['propertyID'],
581 + 'debug' => Debug_Data::redact_debug_value( $settings['propertyID'], 7 ),
582 + ),
583 + 'analytics_4_web_data_stream_id' => array(
584 + 'label' => __( 'Analytics: Web data stream ID', 'google-site-kit' ),
585 + 'value' => $settings['webDataStreamID'],
586 + 'debug' => Debug_Data::redact_debug_value( $settings['webDataStreamID'] ),
587 + ),
588 + 'analytics_4_measurement_id' => array(
589 + 'label' => __( 'Analytics: Measurement ID', 'google-site-kit' ),
590 + 'value' => $settings['measurementID'],
591 + 'debug' => Debug_Data::redact_debug_value( $settings['measurementID'] ),
592 + ),
593 + 'analytics_4_use_snippet' => array(
594 + 'label' => __( 'Analytics: Snippet placed', 'google-site-kit' ),
595 + 'value' => $settings['useSnippet'] ? __( 'Yes', 'google-site-kit' ) : __( 'No', 'google-site-kit' ),
596 + 'debug' => $settings['useSnippet'] ? 'yes' : 'no',
597 + ),
598 + 'analytics_4_available_custom_dimensions' => array(
599 + 'label' => __( 'Analytics: Available Custom Dimensions', 'google-site-kit' ),
600 + 'value' => empty( $settings['availableCustomDimensions'] )
601 + ? __( 'None', 'google-site-kit' )
602 + : join(
603 + /* translators: used between list items, there is a space after the comma */
604 + __( ', ', 'google-site-kit' ),
605 + $settings['availableCustomDimensions']
606 + ),
607 + 'debug' => empty( $settings['availableCustomDimensions'] )
608 + ? 'none'
609 + : join( ', ', $settings['availableCustomDimensions'] ),
610 + ),
611 + 'analytics_4_ads_linked' => array(
612 + 'label' => __( 'Analytics: Ads Linked', 'google-site-kit' ),
613 + 'value' => $settings['adsLinked'] ? __( 'Connected', 'google-site-kit' ) : __( 'Not connected', 'google-site-kit' ),
614 + 'debug' => $settings['adsLinked'],
615 + ),
616 + 'analytics_4_ads_linked_last_synced_at' => array(
617 + 'label' => __( 'Analytics: Ads Linked Last Synced At', 'google-site-kit' ),
618 + 'value' => $settings['adsLinkedLastSyncedAt'] ? gmdate( 'Y-m-d H:i:s', $settings['adsLinkedLastSyncedAt'] ) : __( 'Never synced', 'google-site-kit' ),
619 + 'debug' => $settings['adsLinkedLastSyncedAt'],
620 + ),
621 + );
622 +
623 + if ( $this->is_adsense_connected() ) {
624 + $debug_fields['analytics_4_adsense_linked'] = array(
625 + 'label' => __( 'Analytics: AdSense Linked', 'google-site-kit' ),
626 + 'value' => $settings['adSenseLinked'] ? __( 'Connected', 'google-site-kit' ) : __( 'Not connected', 'google-site-kit' ),
627 + 'debug' => Debug_Data::redact_debug_value( $settings['adSenseLinked'] ),
628 + );
629 +
630 + $debug_fields['analytics_4_adsense_linked_last_synced_at'] = array(
631 + 'label' => __( 'Analytics: AdSense Linked Last Synced At', 'google-site-kit' ),
632 + 'value' => $settings['adSenseLinkedLastSyncedAt'] ? gmdate( 'Y-m-d H:i:s', $settings['adSenseLinkedLastSyncedAt'] ) : __( 'Never synced', 'google-site-kit' ),
633 + 'debug' => Debug_Data::redact_debug_value( $settings['adSenseLinkedLastSyncedAt'] ),
634 + );
635 + }
636 +
637 + // Return the SITE_KIT_AUDIENCE audiences.
638 + $available_audiences = $this->audience_settings->get()['availableAudiences'] ?? array();
639 + $site_kit_audiences = $this->get_site_kit_audiences( $available_audiences );
640 +
641 + $debug_fields['analytics_4_site_kit_audiences'] = array(
642 + 'label' => __( 'Analytics: Site created audiences', 'google-site-kit' ),
643 + 'value' => empty( $site_kit_audiences )
644 + ? __( 'None', 'google-site-kit' )
645 + : join(
646 + /* translators: used between list items, there is a space after the comma */
647 + __( ', ', 'google-site-kit' ),
648 + $site_kit_audiences
649 + ),
650 + 'debug' => empty( $site_kit_audiences )
651 + ? 'none'
652 + : join( ', ', $site_kit_audiences ),
653 + );
654 +
655 + return $debug_fields;
656 + }
657 +
658 + /**
659 + * Gets an array of internal feature metrics.
660 + *
661 + * @since 1.163.0
662 + *
663 + * @return array
664 + */
665 + public function get_feature_metrics() {
666 + $settings = $this->get_settings()->get();
667 +
668 + return array(
669 + 'audseg_setup_completed' => (bool) $this->audience_settings->get()['audienceSegmentationSetupCompletedBy'],
670 + 'audseg_audience_count' => count( $this->audience_settings->get()['availableAudiences'] ?? array() ),
671 + 'analytics_adsense_linked' => $this->is_adsense_connected() && $settings['adSenseLinked'],
672 + );
673 + }
674 +
675 + /**
676 + * Gets map of datapoint to definition data for each.
677 + *
678 + * @since 1.30.0
679 + *
680 + * @return array Map of datapoints to their definitions.
681 + */
682 + protected function get_datapoint_definitions() {
683 + $datapoints = array(
684 + 'GET:account-summaries' => array( 'service' => 'analyticsadmin' ),
685 + 'GET:accounts' => array( 'service' => 'analyticsadmin' ),
686 + 'GET:ads-links' => array( 'service' => 'analyticsadmin' ),
687 + 'GET:adsense-links' => array( 'service' => 'analyticsadsenselinks' ),
688 + 'GET:container-lookup' => array(
689 + 'service' => 'tagmanager',
690 + 'scopes' => array(
691 + 'https://www.googleapis.com/auth/tagmanager.readonly',
692 + ),
693 + ),
694 + 'GET:container-destinations' => array(
695 + 'service' => 'tagmanager',
696 + 'scopes' => array(
697 + 'https://www.googleapis.com/auth/tagmanager.readonly',
698 + ),
699 + ),
700 + 'GET:key-events' => array(
701 + 'service' => 'analyticsadmin',
702 + 'shareable' => true,
703 + ),
704 + 'POST:create-account-ticket' => new Create_Account_Ticket(
705 + array(
706 + 'credentials' => $this->authentication->credentials()->get(),
707 + 'provisioning_redirect_uri' => $this->get_provisioning_redirect_uri(),
708 + 'service' => function () {
709 + return $this->get_service( 'analyticsprovisioning' );
710 + },
711 + 'scopes' => array( self::EDIT_SCOPE ),
712 + 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create a new Analytics account on your behalf.', 'google-site-kit' ),
713 + ),
714 + ),
715 + 'GET:google-tag-settings' => array(
716 + 'service' => 'tagmanager',
717 + 'scopes' => array(
718 + 'https://www.googleapis.com/auth/tagmanager.readonly',
719 + ),
720 + ),
721 + 'POST:create-property' => new Create_Property(
722 + array(
723 + 'reference_site_url' => $this->context->get_reference_site_url(),
724 + 'service' => function () {
725 + return $this->get_service( 'analyticsadmin' );
726 + },
727 + 'scopes' => array( self::EDIT_SCOPE ),
728 + 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create a new Analytics property on your behalf.', 'google-site-kit' ),
729 + )
730 + ),
731 + 'POST:create-webdatastream' => new Create_Webdatastream(
732 + array(
733 + 'reference_site_url' => $this->context->get_reference_site_url(),
734 + 'service' => function () {
735 + return $this->get_service( 'analyticsadmin' );
736 + },
737 + 'scopes' => array( self::EDIT_SCOPE ),
738 + 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create a new Analytics web data stream for this site on your behalf.', 'google-site-kit' ),
739 + )
740 + ),
741 + 'GET:properties' => array( 'service' => 'analyticsadmin' ),
742 + 'GET:property' => array( 'service' => 'analyticsadmin' ),
743 + 'GET:has-property-access' => array( 'service' => 'analyticsdata' ),
744 + 'GET:report' => array(
745 + 'service' => 'analyticsdata',
746 + 'shareable' => true,
747 + ),
748 + 'GET:batch-report' => array(
749 + 'service' => 'analyticsdata',
750 + 'shareable' => true,
751 + ),
752 + 'GET:webdatastreams' => array( 'service' => 'analyticsadmin' ),
753 + 'GET:webdatastreams-batch' => array( 'service' => 'analyticsadmin' ),
754 + 'GET:enhanced-measurement-settings' => array( 'service' => 'analyticsenhancedmeasurement' ),
755 + 'POST:enhanced-measurement-settings' => array(
756 + 'service' => 'analyticsenhancedmeasurement',
757 + 'scopes' => array( self::EDIT_SCOPE ),
758 + 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to update enhanced measurement settings for this Analytics web data stream on your behalf.', 'google-site-kit' ),
759 + ),
760 + 'POST:create-custom-dimension' => array(
761 + 'service' => 'analyticsdata',
762 + 'scopes' => array( self::EDIT_SCOPE ),
763 + 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create a new Analytics custom dimension on your behalf.', 'google-site-kit' ),
764 + ),
765 + 'POST:sync-custom-dimensions' => array(
766 + 'service' => 'analyticsadmin',
767 + ),
768 + 'POST:custom-dimension-data-available' => array(
769 + 'service' => '',
770 + ),
771 + 'POST:set-google-tag-id-mismatch' => array(
772 + 'service' => '',
773 + ),
774 + 'POST:set-is-web-data-stream-unavailable' => array(
775 + 'service' => '',
776 + ),
777 + 'POST:create-audience' => array(
778 + 'service' => 'analyticsaudiences',
779 + 'scopes' => array( self::EDIT_SCOPE ),
780 + 'request_scopes_message' => __( 'You’ll need to grant Site Kit permission to create new audiences for your Analytics property on your behalf.', 'google-site-kit' ),
781 + ),
782 + 'POST:save-resource-data-availability-date' => array(
783 + 'service' => '',
784 + ),
785 + 'POST:sync-audiences' => array(
786 + 'service' => 'analyticsaudiences',
787 + 'shareable' => true,
788 + ),
789 + 'GET:audience-settings' => array(
790 + 'service' => '',
791 + 'shareable' => true,
792 + ),
793 + 'POST:save-audience-settings' => array(
794 + 'service' => '',
795 + ),
796 + );
797 +
798 + return $datapoints;
799 + }
800 +
801 + /**
802 + * Creates a new property for provided account.
803 + *
804 + * @since 1.35.0
805 + * @since 1.98.0 Added `$options` parameter.
806 + *
807 + * @param string $account_id Account ID.
808 + * @param array $options {
809 + * Property options.
810 + *
811 + * @type string $displayName Display name.
812 + * @type string $timezone Timezone.
813 + * }
814 + * @return Google_Service_GoogleAnalyticsAdmin_GoogleAnalyticsAdminV1betaProperty A new property.
815 + */
816 + private function create_property( $account_id, $options = array() ) {
817 + if ( ! empty( $options['displayName'] ) ) {
818 + $display_name = sanitize_text_field( $options['displayName'] );
819 + } else {
820 + $display_name = URL::parse( $this->context->get_reference_site_url(), PHP_URL_HOST );
821 + }
822 +
823 + if ( ! empty( $options['timezone'] ) ) {
824 + $timezone = $options['timezone'];
825 + } else {
826 + $timezone = get_option( 'timezone_string' ) ?: 'UTC';
827 + }
828 +
829 + $property = new Google_Service_GoogleAnalyticsAdmin_GoogleAnalyticsAdminV1betaProperty();
830 + $property->setParent( self::normalize_account_id( $account_id ) );
831 + $property->setDisplayName( $display_name );
832 + $property->setTimeZone( $timezone );
833 +
834 + return $this->get_service( 'analyticsadmin' )->properties->create( $property );
835 + }
836 +
837 + /**
838 + * Creates a new web data stream for provided property.
839 + *
840 + * @since 1.35.0
841 + * @since 1.98.0 Added `$options` parameter.
842 + *
843 + * @param string $property_id Property ID.
844 + * @param array $options {
845 + * Web data stream options.
846 + *
847 + * @type string $displayName Display name.
848 + * }
849 + * @return GoogleAnalyticsAdminV1betaDataStream A new web data stream.
850 + */
851 + private function create_webdatastream( $property_id, $options = array() ) {
852 + $site_url = $this->context->get_reference_site_url();
853 +
854 + if ( ! empty( $options['displayName'] ) ) {
855 + $display_name = sanitize_text_field( $options['displayName'] );
856 + } else {
857 + $display_name = URL::parse( $site_url, PHP_URL_HOST );
858 + }
859 +
860 + $data = new GoogleAnalyticsAdminV1betaDataStreamWebStreamData();
861 + $data->setDefaultUri( $site_url );
862 +
863 + $datastream = new GoogleAnalyticsAdminV1betaDataStream();
864 + $datastream->setDisplayName( $display_name );
865 + $datastream->setType( 'WEB_DATA_STREAM' );
866 + $datastream->setWebStreamData( $data );
867 +
868 + /* @var Google_Service_GoogleAnalyticsAdmin $analyticsadmin phpcs:ignore Squiz.PHP.CommentedOutCode.Found */
869 + $analyticsadmin = $this->get_service( 'analyticsadmin' );
870 +
871 + return $analyticsadmin
872 + ->properties_dataStreams // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
873 + ->create(
874 + self::normalize_property_id( $property_id ),
875 + $datastream
876 + );
877 + }
878 +
879 + /**
880 + * Outputs the user tracking opt-out script.
881 + *
882 + * This script opts out of all Google Analytics tracking, for all measurement IDs, regardless of implementation.
883 + * E.g. via Tag Manager, etc.
884 + *
885 + * @since 1.5.0
886 + * @since 1.121.0 Migrated from the Analytics (UA) class and adapted to only work for GA4 properties.
887 + * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/user-opt-out
888 + */
889 + private function print_tracking_opt_out() {
890 + $settings = $this->get_settings()->get();
891 + $account_id = $settings['accountID'];
892 + $property_id = $settings['propertyID'];
893 +
894 + if ( ! $this->is_tracking_disabled() ) {
895 + return;
896 + }
897 +
898 + if ( $this->context->is_amp() ) : ?>
899 + <!-- <?php esc_html_e( 'Google Analytics AMP opt-out snippet added by Site Kit', 'google-site-kit' ); ?> -->
900 + <meta name="ga-opt-out" content="" id="__gaOptOutExtension">
901 + <!-- <?php esc_html_e( 'End Google Analytics AMP opt-out snippet added by Site Kit', 'google-site-kit' ); ?> -->
902 + <?php else : ?>
903 + <!-- <?php esc_html_e( 'Google Analytics opt-out snippet added by Site Kit', 'google-site-kit' ); ?> -->
904 + <?php
905 + // Opt-out should always use the measurement ID, even when using a GT tag.
906 + $tag_id = $this->get_measurement_id();
907 + if ( ! empty( $tag_id ) ) {
908 + BC_Functions::wp_print_inline_script_tag( sprintf( 'window["ga-disable-%s"] = true;', esc_attr( $tag_id ) ) );
909 + }
910 + ?>
911 + <?php do_action( 'googlesitekit_analytics_tracking_opt_out', $property_id, $account_id ); ?>
912 + <!-- <?php esc_html_e( 'End Google Analytics opt-out snippet added by Site Kit', 'google-site-kit' ); ?> -->
913 + <?php
914 + endif;
915 + }
916 +
917 + /**
918 + * Checks whether or not tracking snippet should be contextually disabled for this request.
919 + *
920 + * @since 1.1.0
921 + * @since 1.121.0 Migrated here from the Analytics (UA) class.
922 + *
923 + * @return bool
924 + */
925 + protected function is_tracking_disabled() {
926 + $settings = $this->get_settings()->get();
927 +
928 + // This filter is documented in Tag_Manager::filter_analytics_allow_tracking_disabled.
929 + if ( ! apply_filters( 'googlesitekit_allow_tracking_disabled', $settings['useSnippet'] ) ) {
930 + return false;
931 + }
932 +
933 + $disable_logged_in_users = in_array( 'loggedinUsers', $settings['trackingDisabled'], true ) && is_user_logged_in();
934 + $disable_content_creators = in_array( 'contentCreators', $settings['trackingDisabled'], true ) && current_user_can( 'edit_posts' );
935 +
936 + $disabled = $disable_logged_in_users || $disable_content_creators;
937 +
938 + /**
939 + * Filters whether or not the Analytics tracking snippet is output for the current request.
940 + *
941 + * @since 1.1.0
942 + *
943 + * @param $disabled bool Whether to disable tracking or not.
944 + */
945 + return (bool) apply_filters( 'googlesitekit_analytics_tracking_disabled', $disabled );
946 + }
947 +
948 + /**
949 + * Handles the provisioning callback after the user completes the terms of service.
950 + *
951 + * @since 1.9.0
952 + * @since 1.98.0 Extended to handle callback from Admin API (no UA entities).
953 + * @since 1.121.0 Migrated method from original Analytics class to Analytics_4 class.
954 + */
955 + protected function handle_provisioning_callback() {
956 + if ( defined( 'WP_CLI' ) && WP_CLI ) {
957 + return;
958 + }
959 +
960 + if ( ! current_user_can( Permissions::MANAGE_OPTIONS ) ) {
961 + return;
962 + }
963 +
964 + $input = $this->context->input();
965 +
966 + if ( ! $input->filter( INPUT_GET, 'gatoscallback' ) ) {
967 + return;
968 + }
969 +
970 + // First check that the accountTicketId matches one stored for the user.
971 + // This is always provided, even in the event of an error.
972 + $account_ticket_id = htmlspecialchars( $input->filter( INPUT_GET, 'accountTicketId' ) );
973 + // The create-account-ticket request stores the created account ticket in a transient before
974 + // sending the user off to the terms of service page.
975 + $account_ticket_transient_key = self::PROVISION_ACCOUNT_TICKET_ID . '::' . get_current_user_id();
976 + $account_ticket_params = $this->transients->get( $account_ticket_transient_key );
977 + $account_ticket = new Account_Ticket( $account_ticket_params );
978 +
979 + // Backwards compat for previous storage type which stored ID only.
980 + if ( is_scalar( $account_ticket_params ) ) {
981 + $account_ticket->set_id( $account_ticket_params );
982 + }
983 +
984 + if ( $account_ticket->get_id() !== $account_ticket_id ) {
985 + wp_safe_redirect(
986 + $this->context->admin_url( 'dashboard', array( 'error_code' => 'account_ticket_id_mismatch' ) )
987 + );
988 + exit;
989 + }
990 +
991 + // At this point, the accountTicketId is a match and params are loaded, so we can safely delete the transient.
992 + $this->transients->delete( $account_ticket_transient_key );
993 +
994 + // Next, check for a returned error.
995 + $error = $input->filter( INPUT_GET, 'error' );
996 + if ( ! empty( $error ) ) {
997 + wp_safe_redirect(
998 + $this->context->admin_url( 'dashboard', array( 'error_code' => htmlspecialchars( $error ) ) )
999 + );
1000 + exit;
1001 + }
1002 +
1003 + $account_id = htmlspecialchars( $input->filter( INPUT_GET, 'accountId' ) );
1004 +
1005 + if ( empty( $account_id ) ) {
1006 + wp_safe_redirect(
1007 + $this->context->admin_url( 'dashboard', array( 'error_code' => 'callback_missing_parameter' ) )
1008 + );
1009 + exit;
1010 + }
1011 +
1012 + $new_settings = array();
1013 +
1014 + // At this point, account creation was successful.
1015 + $new_settings['accountID'] = $account_id;
1016 +
1017 + $this->get_settings()->merge( $new_settings );
1018 +
1019 + $this->provision_property_webdatastream( $account_id, $account_ticket );
1020 +
1021 + if ( Feature_Flags::enabled( 'setupFlowRefresh' ) ) {
1022 + $show_progress = (bool) $input->filter( INPUT_GET, 'show_progress' );
1023 +
1024 + wp_safe_redirect(
1025 + $this->context->admin_url(
1026 + 'key-metrics-setup',
1027 + array(
1028 + 'showProgress' => $show_progress ? 'true' : null,
1029 + )
1030 + )
1031 + );
1032 + exit;
1033 + }
1034 +
1035 + wp_safe_redirect(
1036 + $this->context->admin_url(
1037 + 'dashboard',
1038 + array(
1039 + 'notification' => 'authentication_success',
1040 + 'slug' => 'analytics-4',
1041 + )
1042 + )
1043 + );
1044 + exit;
1045 + }
1046 +
1047 + /**
1048 + * Provisions new GA4 property and web data stream for provided account.
1049 + *
1050 + * @since 1.35.0
1051 + * @since 1.98.0 Added $account_ticket.
1052 + *
1053 + * @param string $account_id Account ID.
1054 + * @param Account_Ticket $account_ticket Account ticket instance.
1055 + */
1056 + private function provision_property_webdatastream( $account_id, $account_ticket ) {
1057 + // Reset the current GA4 settings.
1058 + $this->get_settings()->merge(
1059 + array(
1060 + 'propertyID' => '',
1061 + 'webDataStreamID' => '',
1062 + 'measurementID' => '',
1063 + )
1064 + );
1065 +
1066 + $property = $this->create_property(
1067 + $account_id,
1068 + array(
1069 + 'displayName' => $account_ticket->get_property_name(),
1070 + 'timezone' => $account_ticket->get_timezone(),
1071 + )
1072 + );
1073 + $property = self::filter_property_with_ids( $property );
1074 +
1075 + if ( empty( $property->_id ) ) {
1076 + return;
1077 + }
1078 +
1079 + $create_time = isset( $property->createTime ) ? $property->createTime : ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1080 + $create_time_ms = 0;
1081 + if ( $create_time ) {
1082 + $create_time_ms = Synchronize_Property::convert_time_to_unix_ms( $create_time );
1083 + }
1084 +
1085 + $this->get_settings()->merge(
1086 + array(
1087 + 'propertyID' => $property->_id,
1088 + 'propertyCreateTime' => $create_time_ms,
1089 + )
1090 + );
1091 +
1092 + $web_datastream = $this->create_webdatastream(
1093 + $property->_id,
1094 + array(
1095 + 'displayName' => $account_ticket->get_data_stream_name(),
1096 + )
1097 + );
1098 + $web_datastream = self::filter_webdatastream_with_ids( $web_datastream );
1099 +
1100 + if ( empty( $web_datastream->_id ) ) {
1101 + return;
1102 + }
1103 +
1104 + $measurement_id = $web_datastream->webStreamData->measurementId; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1105 +
1106 + $this->get_settings()->merge(
1107 + array(
1108 + 'webDataStreamID' => $web_datastream->_id,
1109 + 'measurementID' => $measurement_id,
1110 + )
1111 + );
1112 +
1113 + if ( $account_ticket->get_enhanced_measurement_stream_enabled() ) {
1114 + $this->set_data(
1115 + 'enhanced-measurement-settings',
1116 + array(
1117 + 'propertyID' => $property->_id,
1118 + 'webDataStreamID' => $web_datastream->_id,
1119 + 'enhancedMeasurementSettings' => array(
1120 + // We can hardcode this to `true` here due to the conditional invocation.
1121 + 'streamEnabled' => true,
1122 + ),
1123 + )
1124 + );
1125 + }
1126 +
1127 + $this->sync_google_tag_settings();
1128 + }
1129 +
1130 + /**
1131 + * Syncs Google tag settings for the currently configured measurementID.
1132 + *
1133 + * @since 1.102.0
1134 + */
1135 + protected function sync_google_tag_settings() {
1136 + $settings = $this->get_settings();
1137 + $measurement_id = $settings->get()['measurementID'];
1138 +
1139 + if ( ! $measurement_id ) {
1140 + return;
1141 + }
1142 +
1143 + $google_tag_settings = $this->get_data( 'google-tag-settings', array( 'measurementID' => $measurement_id ) );
1144 +
1145 + if ( is_wp_error( $google_tag_settings ) ) {
1146 + return;
1147 + }
1148 +
1149 + $settings->merge( $google_tag_settings );
1150 + }
1151 +
1152 + /**
1153 + * Creates a request object for the given datapoint.
1154 + *
1155 + * @since 1.30.0
1156 + *
1157 + * @param Data_Request $data Data request object.
1158 + * @return RequestInterface|callable|WP_Error Request object or callable on success, or WP_Error on failure.
1159 + *
1160 + * @throws Invalid_Datapoint_Exception Thrown if the datapoint does not exist.
1161 + * @throws Invalid_Param_Exception Thrown if a parameter is invalid.
1162 + * @throws Missing_Required_Param_Exception Thrown if a required parameter is missing or empty.
1163 + *
1164 + * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
1165 + */
1166 + protected function create_data_request( Data_Request $data ) {
1167 + switch ( "{$data->method}:{$data->datapoint}" ) {
1168 + case 'GET:accounts':
1169 + return $this->get_service( 'analyticsadmin' )->accounts->listAccounts();
1170 + case 'GET:account-summaries':
1171 + return $this->get_service( 'analyticsadmin' )->accountSummaries->listAccountSummaries(
1172 + array(
1173 + 'pageSize' => 200,
1174 + 'pageToken' => $data['pageToken'],
1175 + )
1176 + );
1177 + case 'GET:ads-links':
1178 + if ( empty( $data['propertyID'] ) ) {
1179 + throw new Missing_Required_Param_Exception( 'propertyID' );
1180 + }
1181 +
1182 + $parent = self::normalize_property_id( $data['propertyID'] );
1183 +
1184 + return $this->get_service( 'analyticsadmin' )->properties_googleAdsLinks->listPropertiesGoogleAdsLinks( $parent );
1185 + case 'GET:adsense-links':
1186 + if ( empty( $data['propertyID'] ) ) {
1187 + throw new Missing_Required_Param_Exception( 'propertyID' );
1188 + }
1189 +
1190 + $parent = self::normalize_property_id( $data['propertyID'] );
1191 +
1192 + return $this->get_analyticsadsenselinks_service()->properties_adSenseLinks->listPropertiesAdSenseLinks( $parent );
1193 + case 'POST:create-audience':
1194 + $settings = $this->get_settings()->get();
1195 + if ( ! isset( $settings['propertyID'] ) ) {
1196 + return new WP_Error(
1197 + 'missing_required_setting',
1198 + __( 'No connected Google Analytics property ID.', 'google-site-kit' ),
1199 + array( 'status' => 500 )
1200 + );
1201 + }
1202 +
1203 + if ( ! isset( $data['audience'] ) ) {
1204 + throw new Missing_Required_Param_Exception( 'audience' );
1205 + }
1206 +
1207 + $property_id = $settings['propertyID'];
1208 + $audience = $data['audience'];
1209 +
1210 + $fields = array(
1211 + 'displayName',
1212 + 'description',
1213 + 'membershipDurationDays',
1214 + 'eventTrigger',
1215 + 'exclusionDurationMode',
1216 + 'filterClauses',
1217 + );
1218 +
1219 + $invalid_keys = array_diff( array_keys( $audience ), $fields );
1220 +
1221 + if ( ! empty( $invalid_keys ) ) {
1222 + return new WP_Error(
1223 + 'invalid_property_name',
1224 + /* translators: %s: Invalid property names */
1225 + sprintf( __( 'Invalid properties in audience: %s.', 'google-site-kit' ), implode( ', ', $invalid_keys ) ),
1226 + array( 'status' => 400 )
1227 + );
1228 + }
1229 +
1230 + $property_id = self::normalize_property_id( $property_id );
1231 +
1232 + $post_body = new GoogleAnalyticsAdminV1alphaAudience( $audience );
1233 +
1234 + $analyticsadmin = $this->get_analyticsaudiences_service();
1235 +
1236 + return $analyticsadmin
1237 + ->properties_audiences
1238 + ->create(
1239 + $property_id,
1240 + $post_body
1241 + );
1242 + case 'GET:properties':
1243 + if ( ! isset( $data['accountID'] ) ) {
1244 + return new WP_Error(
1245 + 'missing_required_param',
1246 + /* translators: %s: Missing parameter name */
1247 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ),
1248 + array( 'status' => 400 )
1249 + );
1250 + }
1251 +
1252 + return $this->get_service( 'analyticsadmin' )->properties->listProperties(
1253 + array(
1254 + 'filter' => 'parent:' . self::normalize_account_id( $data['accountID'] ),
1255 + 'pageSize' => 200,
1256 + )
1257 + );
1258 + case 'GET:property':
1259 + if ( ! isset( $data['propertyID'] ) ) {
1260 + return new WP_Error(
1261 + 'missing_required_param',
1262 + /* translators: %s: Missing parameter name */
1263 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ),
1264 + array( 'status' => 400 )
1265 + );
1266 + }
1267 +
1268 + return $this->get_service( 'analyticsadmin' )->properties->get( self::normalize_property_id( $data['propertyID'] ) );
1269 + case 'GET:has-property-access':
1270 + if ( ! isset( $data['propertyID'] ) ) {
1271 + throw new Missing_Required_Param_Exception( 'propertyID' );
1272 + }
1273 +
1274 + // A simple way to check for property access is to attempt a minimal report request.
1275 + // If the user does not have access, this will return a 403 error.
1276 + $request = new Google_Service_AnalyticsData_RunReportRequest();
1277 + $request->setDimensions( array( new Google_Service_AnalyticsData_Dimension( array( 'name' => 'date' ) ) ) );
1278 + $request->setMetrics( array( new Google_Service_AnalyticsData_Metric( array( 'name' => 'sessions' ) ) ) );
1279 + $request->setDateRanges(
1280 + array(
1281 + new Google_Service_AnalyticsData_DateRange(
1282 + array(
1283 + 'start_date' => 'yesterday',
1284 + 'end_date' => 'today',
1285 + )
1286 + ),
1287 + )
1288 + );
1289 + $request->setLimit( 0 );
1290 +
1291 + return $this->get_analyticsdata_service()->properties->runReport( $data['propertyID'], $request );
1292 + case 'GET:report':
1293 + if ( empty( $data['metrics'] ) ) {
1294 + return new WP_Error(
1295 + 'missing_required_param',
1296 + /* translators: %s: Missing parameter name */
1297 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'metrics' ),
1298 + array( 'status' => 400 )
1299 + );
1300 + }
1301 +
1302 + $settings = $this->get_settings()->get();
1303 + if ( empty( $settings['propertyID'] ) ) {
1304 + return new WP_Error(
1305 + 'missing_required_setting',
1306 + __( 'No connected Google Analytics property ID.', 'google-site-kit' ),
1307 + array( 'status' => 500 )
1308 + );
1309 + }
1310 +
1311 + $report = new Analytics_4_Report_Request( $this->context );
1312 + $request = $report->create_request( $data, $this->is_shared_data_request( $data ) );
1313 + if ( is_wp_error( $request ) ) {
1314 + return $request;
1315 + }
1316 +
1317 + $property_id = self::normalize_property_id( $settings['propertyID'] );
1318 + $request->setProperty( $property_id );
1319 +
1320 + return $this->get_analyticsdata_service()->properties->runReport( $property_id, $request );
1321 +
1322 + case 'GET:batch-report':
1323 + if ( empty( $data['requests'] ) ) {
1324 + return new WP_Error(
1325 + 'missing_required_param',
1326 + /* translators: %s: Missing parameter name */
1327 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'requests' ),
1328 + array( 'status' => 400 )
1329 + );
1330 + }
1331 +
1332 + if ( ! is_array( $data['requests'] ) || count( $data['requests'] ) > 5 ) {
1333 + return new WP_Error(
1334 + 'invalid_batch_size',
1335 + __( 'Batch report requests must be an array with 1-5 requests.', 'google-site-kit' ),
1336 + array( 'status' => 400 )
1337 + );
1338 + }
1339 +
1340 + $settings = $this->get_settings()->get();
1341 + if ( empty( $settings['propertyID'] ) ) {
1342 + return new WP_Error(
1343 + 'missing_required_setting',
1344 + __( 'No connected Google Analytics property ID.', 'google-site-kit' ),
1345 + array( 'status' => 500 )
1346 + );
1347 + }
1348 +
1349 + $batch_requests = array();
1350 + $report = new Analytics_4_Report_Request( $this->context );
1351 +
1352 + foreach ( $data['requests'] as $request_data ) {
1353 + $data_request = new Data_Request( 'GET', 'modules', $this->slug, 'report', $request_data );
1354 + $request = $report->create_request(
1355 + $data_request,
1356 + $this->is_shared_data_request( $data_request )
1357 + );
1358 + if ( is_wp_error( $request ) ) {
1359 + return $request;
1360 + }
1361 + $batch_requests[] = $request;
1362 + }
1363 +
1364 + $property_id = self::normalize_property_id( $settings['propertyID'] );
1365 +
1366 + $batch_request = new Google_Service_AnalyticsData\BatchRunReportsRequest();
1367 + $batch_request->setRequests( $batch_requests );
1368 +
1369 + return $this->get_analyticsdata_service()->properties->batchRunReports(
1370 + $property_id,
1371 + $batch_request
1372 + );
1373 +
1374 + case 'GET:enhanced-measurement-settings':
1375 + if ( ! isset( $data['propertyID'] ) ) {
1376 + return new WP_Error(
1377 + 'missing_required_param',
1378 + /* translators: %s: Missing parameter name */
1379 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ),
1380 + array( 'status' => 400 )
1381 + );
1382 + }
1383 +
1384 + if ( ! isset( $data['webDataStreamID'] ) ) {
1385 + return new WP_Error(
1386 + 'missing_required_param',
1387 + /* translators: %s: Missing parameter name */
1388 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'webDataStreamID' ),
1389 + array( 'status' => 400 )
1390 + );
1391 + }
1392 +
1393 + $name = self::normalize_property_id(
1394 + $data['propertyID']
1395 + ) . '/dataStreams/' . $data['webDataStreamID'] . '/enhancedMeasurementSettings';
1396 +
1397 + $analyticsadmin = $this->get_analyticsenhancedmeasurements_service();
1398 +
1399 + return $analyticsadmin
1400 + ->properties_enhancedMeasurements // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1401 + ->getEnhancedMeasurementSettings( $name );
1402 + case 'POST:enhanced-measurement-settings':
1403 + if ( ! isset( $data['propertyID'] ) ) {
1404 + return new WP_Error(
1405 + 'missing_required_param',
1406 + /* translators: %s: Missing parameter name */
1407 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ),
1408 + array( 'status' => 400 )
1409 + );
1410 + }
1411 +
1412 + if ( ! isset( $data['webDataStreamID'] ) ) {
1413 + return new WP_Error(
1414 + 'missing_required_param',
1415 + /* translators: %s: Missing parameter name */
1416 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'webDataStreamID' ),
1417 + array( 'status' => 400 )
1418 + );
1419 + }
1420 +
1421 + if ( ! isset( $data['enhancedMeasurementSettings'] ) ) {
1422 + return new WP_Error(
1423 + 'missing_required_param',
1424 + /* translators: %s: Missing parameter name */
1425 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'enhancedMeasurementSettings' ),
1426 + array( 'status' => 400 )
1427 + );
1428 + }
1429 +
1430 + $enhanced_measurement_settings = $data['enhancedMeasurementSettings'];
1431 +
1432 + $fields = array(
1433 + 'name',
1434 + 'streamEnabled',
1435 + 'scrollsEnabled',
1436 + 'outboundClicksEnabled',
1437 + 'siteSearchEnabled',
1438 + 'videoEngagementEnabled',
1439 + 'fileDownloadsEnabled',
1440 + 'pageChangesEnabled',
1441 + 'formInteractionsEnabled',
1442 + 'searchQueryParameter',
1443 + 'uriQueryParameter',
1444 + );
1445 +
1446 + $invalid_keys = array_diff( array_keys( $enhanced_measurement_settings ), $fields );
1447 +
1448 + if ( ! empty( $invalid_keys ) ) {
1449 + return new WP_Error(
1450 + 'invalid_property_name',
1451 + /* translators: %s: Invalid property names */
1452 + sprintf( __( 'Invalid properties in enhancedMeasurementSettings: %s.', 'google-site-kit' ), implode( ', ', $invalid_keys ) ),
1453 + array( 'status' => 400 )
1454 + );
1455 + }
1456 +
1457 + $name = self::normalize_property_id(
1458 + $data['propertyID']
1459 + ) . '/dataStreams/' . $data['webDataStreamID'] . '/enhancedMeasurementSettings';
1460 +
1461 + $post_body = new EnhancedMeasurementSettingsModel( $data['enhancedMeasurementSettings'] );
1462 +
1463 + $analyticsadmin = $this->get_analyticsenhancedmeasurements_service();
1464 +
1465 + return $analyticsadmin
1466 + ->properties_enhancedMeasurements // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1467 + ->updateEnhancedMeasurementSettings(
1468 + $name,
1469 + $post_body,
1470 + array(
1471 + 'updateMask' => 'streamEnabled', // Only allow updating the streamEnabled field for now.
1472 + )
1473 + );
1474 + case 'POST:create-custom-dimension':
1475 + if ( ! isset( $data['propertyID'] ) ) {
1476 + return new WP_Error(
1477 + 'missing_required_param',
1478 + /* translators: %s: Missing parameter name */
1479 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ),
1480 + array( 'status' => 400 )
1481 + );
1482 + }
1483 +
1484 + if ( ! isset( $data['customDimension'] ) ) {
1485 + return new WP_Error(
1486 + 'missing_required_param',
1487 + /* translators: %s: Missing parameter name */
1488 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'customDimension' ),
1489 + array( 'status' => 400 )
1490 + );
1491 + }
1492 +
1493 + $custom_dimension_data = $data['customDimension'];
1494 +
1495 + $fields = array(
1496 + 'parameterName',
1497 + 'displayName',
1498 + 'description',
1499 + 'scope',
1500 + 'disallowAdsPersonalization',
1501 + );
1502 +
1503 + $invalid_keys = array_diff( array_keys( $custom_dimension_data ), $fields );
1504 +
1505 + if ( ! empty( $invalid_keys ) ) {
1506 + return new WP_Error(
1507 + 'invalid_property_name',
1508 + /* translators: %s: Invalid property names */
1509 + sprintf( __( 'Invalid properties in customDimension: %s.', 'google-site-kit' ), implode( ', ', $invalid_keys ) ),
1510 + array( 'status' => 400 )
1511 + );
1512 + }
1513 +
1514 + // Define the valid `DimensionScope` enum values.
1515 + $valid_scopes = array( 'EVENT', 'USER', 'ITEM' );
1516 +
1517 + // If the scope field is not set, default to `EVENT`.
1518 + // Otherwise, validate against the enum values.
1519 + if ( ! isset( $custom_dimension_data['scope'] ) ) {
1520 + $custom_dimension_data['scope'] = 'EVENT';
1521 + } elseif ( ! in_array( $custom_dimension_data['scope'], $valid_scopes, true ) ) {
1522 + return new WP_Error(
1523 + 'invalid_scope',
1524 + /* translators: %s: Invalid scope */
1525 + sprintf( __( 'Invalid scope: %s.', 'google-site-kit' ), $custom_dimension_data['scope'] ),
1526 + array( 'status' => 400 )
1527 + );
1528 + }
1529 +
1530 + $custom_dimension = new GoogleAnalyticsAdminV1betaCustomDimension();
1531 + $custom_dimension->setParameterName( $custom_dimension_data['parameterName'] );
1532 + $custom_dimension->setDisplayName( $custom_dimension_data['displayName'] );
1533 + $custom_dimension->setScope( $custom_dimension_data['scope'] );
1534 +
1535 + if ( isset( $custom_dimension_data['description'] ) ) {
1536 + $custom_dimension->setDescription( $custom_dimension_data['description'] );
1537 + }
1538 +
1539 + if ( isset( $custom_dimension_data['disallowAdsPersonalization'] ) ) {
1540 + $custom_dimension->setDisallowAdsPersonalization( $custom_dimension_data['disallowAdsPersonalization'] );
1541 + }
1542 +
1543 + $analyticsadmin = $this->get_service( 'analyticsadmin' );
1544 +
1545 + return $analyticsadmin
1546 + ->properties_customDimensions // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1547 + ->create(
1548 + self::normalize_property_id( $data['propertyID'] ),
1549 + $custom_dimension
1550 + );
1551 +
1552 + case 'GET:audience-settings':
1553 + return function () {
1554 + $settings = $this->audience_settings->get();
1555 + return current_user_can( Permissions::MANAGE_OPTIONS ) ? $settings : array_intersect_key( $settings, array_flip( $this->audience_settings->get_view_only_keys() ) );
1556 + };
1557 +
1558 + case 'POST:save-audience-settings':
1559 + if ( ! current_user_can( Permissions::MANAGE_OPTIONS ) ) {
1560 + return new WP_Error(
1561 + 'forbidden',
1562 + __( 'User does not have permission to save audience settings.', 'google-site-kit' ),
1563 + array( 'status' => 403 )
1564 + );
1565 + }
1566 +
1567 + $settings = $data['settings'];
1568 +
1569 + if (
1570 + isset( $settings['audienceSegmentationSetupCompletedBy'] ) &&
1571 + ! is_int( $settings['audienceSegmentationSetupCompletedBy'] )
1572 + ) {
1573 + throw new Invalid_Param_Exception( 'audienceSegmentationSetupCompletedBy' );
1574 + }
1575 +
1576 + return function () use ( $settings ) {
1577 + $new_settings = array();
1578 +
1579 + if ( isset( $settings['audienceSegmentationSetupCompletedBy'] ) ) {
1580 + $new_settings['audienceSegmentationSetupCompletedBy'] = $settings['audienceSegmentationSetupCompletedBy'];
1581 + }
1582 +
1583 + $settings = $this->audience_settings->merge( $new_settings );
1584 +
1585 + return $settings;
1586 + };
1587 +
1588 + case 'POST:sync-audiences':
1589 + if ( ! $this->authentication->is_authenticated() ) {
1590 + return new WP_Error(
1591 + 'forbidden',
1592 + __( 'User must be authenticated to sync audiences.', 'google-site-kit' ),
1593 + array( 'status' => 403 )
1594 + );
1595 + }
1596 +
1597 + $settings = $this->get_settings()->get();
1598 + if ( empty( $settings['propertyID'] ) ) {
1599 + return new WP_Error(
1600 + 'missing_required_setting',
1601 + __( 'No connected Google Analytics property ID.', 'google-site-kit' ),
1602 + array( 'status' => 500 )
1603 + );
1604 + }
1605 +
1606 + $analyticsadmin = $this->get_analyticsaudiences_service();
1607 + $property_id = self::normalize_property_id( $settings['propertyID'] );
1608 +
1609 + return $analyticsadmin
1610 + ->properties_audiences
1611 + ->listPropertiesAudiences( $property_id );
1612 + case 'POST:sync-custom-dimensions':
1613 + $settings = $this->get_settings()->get();
1614 + if ( empty( $settings['propertyID'] ) ) {
1615 + return new WP_Error(
1616 + 'missing_required_setting',
1617 + __( 'No connected Google Analytics property ID.', 'google-site-kit' ),
1618 + array( 'status' => 500 )
1619 + );
1620 + }
1621 +
1622 + $analyticsadmin = $this->get_service( 'analyticsadmin' );
1623 +
1624 + return $analyticsadmin
1625 + ->properties_customDimensions // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1626 + ->listPropertiesCustomDimensions( self::normalize_property_id( $settings['propertyID'] ) );
1627 + case 'POST:custom-dimension-data-available':
1628 + if ( ! isset( $data['customDimension'] ) ) {
1629 + return new WP_Error(
1630 + 'missing_required_param',
1631 + /* translators: %s: Missing parameter name */
1632 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'customDimension' ),
1633 + array( 'status' => 400 )
1634 + );
1635 + }
1636 +
1637 + if ( ! $this->custom_dimensions_data_available->is_valid_custom_dimension( $data['customDimension'] ) ) {
1638 + return new WP_Error(
1639 + 'invalid_custom_dimension_slug',
1640 + /* translators: %s: Invalid custom dimension slug */
1641 + sprintf( __( 'Invalid custom dimension slug: %s.', 'google-site-kit' ), $data['customDimension'] ),
1642 + array( 'status' => 400 )
1643 + );
1644 + }
1645 +
1646 + return function () use ( $data ) {
1647 + return $this->custom_dimensions_data_available->set_data_available( $data['customDimension'] );
1648 + };
1649 + case 'POST:save-resource-data-availability-date':
1650 + if ( ! isset( $data['resourceType'] ) ) {
1651 + throw new Missing_Required_Param_Exception( 'resourceType' );
1652 + }
1653 +
1654 + if ( ! isset( $data['resourceSlug'] ) ) {
1655 + throw new Missing_Required_Param_Exception( 'resourceSlug' );
1656 + }
1657 +
1658 + if ( ! isset( $data['date'] ) ) {
1659 + throw new Missing_Required_Param_Exception( 'date' );
1660 + }
1661 +
1662 + if ( ! $this->resource_data_availability_date->is_valid_resource_type( $data['resourceType'] ) ) {
1663 + throw new Invalid_Param_Exception( 'resourceType' );
1664 + }
1665 +
1666 + if ( ! $this->resource_data_availability_date->is_valid_resource_slug( $data['resourceSlug'], $data['resourceType'] ) ) {
1667 + throw new Invalid_Param_Exception( 'resourceSlug' );
1668 + }
1669 +
1670 + if ( ! is_int( $data['date'] ) ) {
1671 + throw new Invalid_Param_Exception( 'date' );
1672 + }
1673 +
1674 + return function () use ( $data ) {
1675 + return $this->resource_data_availability_date->set_resource_date( $data['resourceSlug'], $data['resourceType'], $data['date'] );
1676 + };
1677 + case 'GET:webdatastreams':
1678 + if ( ! isset( $data['propertyID'] ) ) {
1679 + return new WP_Error(
1680 + 'missing_required_param',
1681 + /* translators: %s: Missing parameter name */
1682 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyID' ),
1683 + array( 'status' => 400 )
1684 + );
1685 + }
1686 +
1687 + $analyticsadmin = $this->get_service( 'analyticsadmin' );
1688 +
1689 + return $analyticsadmin
1690 + ->properties_dataStreams // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1691 + ->listPropertiesDataStreams(
1692 + self::normalize_property_id( $data['propertyID'] )
1693 + );
1694 + case 'GET:webdatastreams-batch':
1695 + if ( ! isset( $data['propertyIDs'] ) ) {
1696 + return new WP_Error(
1697 + 'missing_required_param',
1698 + /* translators: %s: Missing parameter name */
1699 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'propertyIDs' ),
1700 + array( 'status' => 400 )
1701 + );
1702 + }
1703 +
1704 + if ( ! is_array( $data['propertyIDs'] ) || count( $data['propertyIDs'] ) > 10 ) {
1705 + return new WP_Error(
1706 + 'rest_invalid_param',
1707 + /* translators: %s: List of invalid parameters. */
1708 + sprintf( __( 'Invalid parameter(s): %s', 'google-site-kit' ), 'propertyIDs' ),
1709 + array( 'status' => 400 )
1710 + );
1711 + }
1712 +
1713 + $analyticsadmin = $this->get_service( 'analyticsadmin' );
1714 + $batch_request = $analyticsadmin->createBatch();
1715 +
1716 + foreach ( $data['propertyIDs'] as $property_id ) {
1717 + $batch_request->add(
1718 + $analyticsadmin
1719 + ->properties_dataStreams // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1720 + ->listPropertiesDataStreams(
1721 + self::normalize_property_id( $property_id )
1722 + )
1723 + );
1724 + }
1725 +
1726 + return function () use ( $batch_request ) {
1727 + return $batch_request->execute();
1728 + };
1729 + case 'GET:container-lookup':
1730 + if ( ! isset( $data['destinationID'] ) ) {
1731 + return new WP_Error(
1732 + 'missing_required_param',
1733 + /* translators: %s: Missing parameter name */
1734 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'destinationID' ),
1735 + array( 'status' => 400 )
1736 + );
1737 + }
1738 +
1739 + return $this->get_tagmanager_service()->accounts_containers->lookup( array( 'destinationId' => $data['destinationID'] ) );
1740 + case 'GET:container-destinations':
1741 + if ( ! isset( $data['accountID'] ) ) {
1742 + return new WP_Error(
1743 + 'missing_required_param',
1744 + /* translators: %s: Missing parameter name */
1745 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'accountID' ),
1746 + array( 'status' => 400 )
1747 + );
1748 + }
1749 + if ( ! isset( $data['containerID'] ) ) {
1750 + return new WP_Error(
1751 + 'missing_required_param',
1752 + /* translators: %s: Missing parameter name */
1753 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'containerID' ),
1754 + array( 'status' => 400 )
1755 + );
1756 + }
1757 +
1758 + return $this->get_tagmanager_service()->accounts_containers_destinations->listAccountsContainersDestinations(
1759 + "accounts/{$data['accountID']}/containers/{$data['containerID']}"
1760 + );
1761 + case 'GET:google-tag-settings':
1762 + if ( ! isset( $data['measurementID'] ) ) {
1763 + return new WP_Error(
1764 + 'missing_required_param',
1765 + /* translators: %s: Missing parameter name */
1766 + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'measurementID' ),
1767 + array( 'status' => 400 )
1768 + );
1769 + }
1770 +
1771 + return $this->get_tagmanager_service()->accounts_containers->lookup( array( 'destinationId' => $data['measurementID'] ) );
1772 + case 'GET:key-events':
1773 + $settings = $this->get_settings()->get();
1774 + if ( empty( $settings['propertyID'] ) ) {
1775 + return new WP_Error(
1776 + 'missing_required_setting',
1777 + __( 'No connected Google Analytics property ID.', 'google-site-kit' ),
1778 + array( 'status' => 500 )
1779 + );
1780 + }
1781 +
1782 + $analyticsadmin = $this->get_service( 'analyticsadmin' );
1783 + $property_id = self::normalize_property_id( $settings['propertyID'] );
1784 +
1785 + return $analyticsadmin
1786 + ->properties_keyEvents // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1787 + ->listPropertiesKeyEvents( $property_id );
1788 + case 'POST:set-google-tag-id-mismatch':
1789 + if ( ! isset( $data['hasMismatchedTag'] ) ) {
1790 + throw new Missing_Required_Param_Exception( 'hasMismatchedTag' );
1791 + }
1792 +
1793 + if ( false === $data['hasMismatchedTag'] ) {
1794 + return function () {
1795 + $this->transients->delete( 'googlesitekit_inline_tag_id_mismatch' );
1796 + return false;
1797 + };
1798 + }
1799 +
1800 + return function () use ( $data ) {
1801 + $this->transients->set( 'googlesitekit_inline_tag_id_mismatch', $data['hasMismatchedTag'] );
1802 + return $data['hasMismatchedTag'];
1803 + };
1804 + case 'POST:set-is-web-data-stream-unavailable':
1805 + if ( ! isset( $data['isWebDataStreamUnavailable'] ) ) {
1806 + throw new Missing_Required_Param_Exception( 'isWebDataStreamUnavailable' );
1807 + }
1808 +
1809 + if ( true === $data['isWebDataStreamUnavailable'] ) {
1810 + return function () {
1811 + $settings = $this->get_settings()->get();
1812 + $transient_key = 'googlesitekit_web_data_stream_unavailable_' . $settings['webDataStreamID'];
1813 + $this->transients->set( $transient_key, true );
1814 + return true;
1815 + };
1816 + }
1817 +
1818 + return function () {
1819 + $settings = $this->get_settings()->get();
1820 + $transient_key = 'googlesitekit_web_data_stream_unavailable_' . $settings['webDataStreamID'];
1821 + $this->transients->delete( $transient_key );
1822 + return false;
1823 + };
1824 + }
1825 +
1826 + return parent::create_data_request( $data );
1827 + }
1828 +
1829 + /**
1830 + * Parses a response for the given datapoint.
1831 + *
1832 + * @since 1.30.0
1833 + *
1834 + * @param Data_Request $data Data request object.
1835 + * @param mixed $response Request response.
1836 + *
1837 + * @return mixed Parsed response data on success, or WP_Error on failure.
1838 + */
1839 + protected function parse_data_response( Data_Request $data, $response ) {
1840 + switch ( "{$data->method}:{$data->datapoint}" ) {
1841 + case 'GET:accounts':
1842 + return array_map( array( self::class, 'filter_account_with_ids' ), $response->getAccounts() );
1843 + case 'GET:ads-links':
1844 + return (array) $response->getGoogleAdsLinks();
1845 + case 'GET:adsense-links':
1846 + return (array) $response->getAdsenseLinks();
1847 + case 'GET:properties':
1848 + return Sort::case_insensitive_list_sort(
1849 + array_map( array( self::class, 'filter_property_with_ids' ), $response->getProperties() ),
1850 + 'displayName'
1851 + );
1852 + case 'GET:property':
1853 + return self::filter_property_with_ids( $response );
1854 + case 'GET:webdatastreams':
1855 + /* @var GoogleAnalyticsAdminV1betaListDataStreamsResponse $response phpcs:ignore Squiz.PHP.CommentedOutCode.Found */
1856 + $webdatastreams = self::filter_web_datastreams( $response->getDataStreams() );
1857 + return array_map( array( self::class, 'filter_webdatastream_with_ids' ), $webdatastreams );
1858 + case 'GET:webdatastreams-batch':
1859 + return self::parse_webdatastreams_batch( $response );
1860 + case 'GET:container-destinations':
1861 + return (array) $response->getDestination();
1862 + case 'GET:google-tag-settings':
1863 + return $this->get_google_tag_settings_for_measurement_id( $response, $data['measurementID'] );
1864 + case 'GET:key-events':
1865 + return (array) $response->getKeyEvents();
1866 + case 'GET:report':
1867 + $report = new Analytics_4_Report_Response( $this->context );
1868 + return $report->parse_response( $data, $response );
1869 + case 'POST:sync-audiences':
1870 + $audiences = $this->set_available_audiences( $response->getAudiences() );
1871 + return $audiences;
1872 + case 'POST:sync-custom-dimensions':
1873 + if ( is_wp_error( $response ) ) {
1874 + return $response;
1875 + }
1876 +
1877 + $custom_dimensions = wp_list_pluck( $response->getCustomDimensions(), 'parameterName' );
1878 + $matching_dimensions = array_values(
1879 + array_filter(
1880 + $custom_dimensions,
1881 + function ( $dimension ) {
1882 + return strpos( $dimension, 'googlesitekit_' ) === 0;
1883 + }
1884 + )
1885 + );
1886 + $this->get_settings()->merge(
1887 + array(
1888 + 'availableCustomDimensions' => $matching_dimensions,
1889 + )
1890 + );
1891 +
1892 + // Reset the data available state for custom dimensions that are no longer available.
1893 + $missing_custom_dimensions_with_data_available = array_diff(
1894 + array_keys(
1895 + // Only compare against custom dimensions that have data available.
1896 + array_filter(
1897 + $this->custom_dimensions_data_available->get_data_availability()
1898 + )
1899 + ),
1900 + $matching_dimensions
1901 + );
1902 +
1903 + if ( count( $missing_custom_dimensions_with_data_available ) > 0 ) {
1904 + $this->custom_dimensions_data_available->reset_data_available(
1905 + $missing_custom_dimensions_with_data_available
1906 + );
1907 + }
1908 +
1909 + return $matching_dimensions;
1910 + }
1911 +
1912 + return parent::parse_data_response( $data, $response );
1913 + }
1914 +
1915 + /**
1916 + * Gets the configured TagManager service instance.
1917 + *
1918 + * @since 1.92.0
1919 + *
1920 + * @return Google_Service_TagManager instance.
1921 + * @throws Exception Thrown if the module did not correctly set up the service.
1922 + */
1923 + private function get_tagmanager_service() {
1924 + return $this->get_service( 'tagmanager' );
1925 + }
1926 +
1927 + /**
1928 + * Sets up information about the module.
1929 + *
1930 + * @since 1.30.0
1931 + * @since 1.123.0 Updated to include in the module setup.
1932 + *
1933 + * @return array Associative array of module info.
1934 + */
1935 + protected function setup_info() {
1936 + return array(
1937 + 'slug' => self::MODULE_SLUG,
1938 + 'name' => _x( 'Analytics', 'Service name', 'google-site-kit' ),
1939 + 'description' => __( 'Get a deeper understanding of your customers. Google Analytics gives you the free tools you need to analyze data for your business in one place.', 'google-site-kit' ),
1940 + 'homepage' => __( 'https://analytics.google.com/analytics/web', 'google-site-kit' ),
1941 + );
1942 + }
1943 +
1944 + /**
1945 + * Gets the configured Analytics Data service object instance.
1946 + *
1947 + * @since 1.93.0
1948 + *
1949 + * @return Google_Service_AnalyticsData The Analytics Data API service.
1950 + */
1951 + protected function get_analyticsdata_service() {
1952 + return $this->get_service( 'analyticsdata' );
1953 + }
1954 +
1955 + /**
1956 + * Gets the configured Analytics Data service object instance.
1957 + *
1958 + * @since 1.110.0
1959 + *
1960 + * @return PropertiesEnhancedMeasurementService The Analytics Admin API service.
1961 + */
1962 + protected function get_analyticsenhancedmeasurements_service() {
1963 + return $this->get_service( 'analyticsenhancedmeasurement' );
1964 + }
1965 +
1966 + /**
1967 + * Gets the configured Analytics Admin service object instance that includes `adSenseLinks` related methods.
1968 + *
1969 + * @since 1.120.0
1970 + *
1971 + * @return PropertiesAdSenseLinksService The Analytics Admin API service.
1972 + */
1973 + protected function get_analyticsadsenselinks_service() {
1974 + return $this->get_service( 'analyticsadsenselinks' );
1975 + }
1976 +
1977 + /**
1978 + * Gets the configured Analytics Data service object instance.
1979 + *
1980 + * @since 1.120.0
1981 + *
1982 + * @return PropertiesAudiencesService The Analytics Admin API service.
1983 + */
1984 + protected function get_analyticsaudiences_service() {
1985 + return $this->get_service( 'analyticsaudiences' );
1986 + }
1987 +
1988 + /**
1989 + * Sets up the Google services the module should use.
1990 + *
1991 + * This method is invoked once by {@see Module::get_service()} to lazily set up the services when one is requested
1992 + * for the first time.
1993 + *
1994 + * @since 1.30.0
1995 + *
1996 + * @param Google_Site_Kit_Client $client Google client instance.
1997 + * @return array Google services as $identifier => $service_instance pairs. Every $service_instance must be an
1998 + * instance of Google_Service.
1999 + */
2000 + protected function setup_services( Google_Site_Kit_Client $client ) {
2001 + $google_proxy = $this->authentication->get_google_proxy();
2002 +
2003 + return array(
2004 + 'analyticsadmin' => new Google_Service_GoogleAnalyticsAdmin( $client ),
2005 + 'analyticsdata' => new Google_Service_AnalyticsData( $client ),
2006 + 'analyticsprovisioning' => new AccountProvisioningService( $client, $google_proxy->url() ),
2007 + 'analyticsenhancedmeasurement' => new PropertiesEnhancedMeasurementService( $client ),
2008 + 'analyticsaudiences' => new PropertiesAudiencesService( $client ),
2009 + 'analyticsadsenselinks' => new PropertiesAdSenseLinksService( $client ),
2010 + 'tagmanager' => new Google_Service_TagManager( $client ),
2011 + );
2012 + }
2013 +
2014 + /**
2015 + * Sets up the module's settings instance.
2016 + *
2017 + * @since 1.30.0
2018 + *
2019 + * @return Module_Settings
2020 + */
2021 + protected function setup_settings() {
2022 + return new Settings( $this->options );
2023 + }
2024 +
2025 + /**
2026 + * Sets up the module's assets to register.
2027 + *
2028 + * @since 1.31.0
2029 + *
2030 + * @return Asset[] List of Asset objects.
2031 + */
2032 + protected function setup_assets() {
2033 + $base_url = $this->context->url( 'dist/assets/' );
2034 +
2035 + return array(
2036 + new Script(
2037 + 'googlesitekit-modules-analytics-4',
2038 + array(
2039 + 'src' => $base_url . 'js/googlesitekit-modules-analytics-4.js',
2040 + 'dependencies' => array(
2041 + 'googlesitekit-vendor',
2042 + 'googlesitekit-api',
2043 + 'googlesitekit-data',
2044 + 'googlesitekit-modules',
2045 + 'googlesitekit-notifications',
2046 + 'googlesitekit-datastore-site',
2047 + 'googlesitekit-datastore-user',
2048 + 'googlesitekit-datastore-forms',
2049 + 'googlesitekit-components',
2050 + 'googlesitekit-modules-data',
2051 + ),
2052 + )
2053 + ),
2054 + );
2055 + }
2056 +
2057 + /**
2058 + * Gets the provisioning redirect URI that listens for the Terms of Service redirect.
2059 + *
2060 + * @since 1.98.0
2061 + *
2062 + * @return string Provisioning redirect URI.
2063 + */
2064 + private function get_provisioning_redirect_uri() {
2065 + return $this->authentication->get_google_proxy()
2066 + ->get_site_fields()['analytics_redirect_uri'];
2067 + }
2068 +
2069 + /**
2070 + * Registers the Analytics 4 tag.
2071 + *
2072 + * @since 1.31.0
2073 + * @since 1.104.0 Added support for AMP tag.
2074 + * @since 1.119.0 Made method public.
2075 + */
2076 + public function register_tag() {
2077 + $tag = $this->context->is_amp()
2078 + ? new AMP_Tag( $this->get_measurement_id(), self::MODULE_SLUG ) // AMP currently only works with the measurement ID.
2079 + : new Web_Tag( $this->get_tag_id(), self::MODULE_SLUG );
2080 +
2081 + if ( $tag->is_tag_blocked() ) {
2082 + return;
2083 + }
2084 +
2085 + $tag->use_guard( new Tag_Verify_Guard( $this->context->input() ) );
2086 + $tag->use_guard( new Tag_Guard( $this->get_settings() ) );
2087 + $tag->use_guard( new Tag_Environment_Type_Guard() );
2088 +
2089 + if ( ! $tag->can_register() ) {
2090 + return;
2091 + }
2092 +
2093 + $home_domain = URL::parse( $this->context->get_canonical_home_url(), PHP_URL_HOST );
2094 + $tag->set_home_domain( $home_domain );
2095 +
2096 + $custom_dimensions_data = $this->get_custom_dimensions_data();
2097 + if ( ! empty( $custom_dimensions_data ) && $tag instanceof Tag_Interface ) {
2098 + $tag->set_custom_dimensions( $custom_dimensions_data );
2099 + }
2100 +
2101 + $tag->register();
2102 + }
2103 +
2104 + /**
2105 + * Returns the Module_Tag_Matchers instance.
2106 + *
2107 + * @since 1.119.0
2108 + *
2109 + * @return Module_Tag_Matchers Module_Tag_Matchers instance.
2110 + */
2111 + public function get_tag_matchers() {
2112 + return new Tag_Matchers();
2113 + }
2114 +
2115 + /**
2116 + * Gets custom dimensions data based on available custom dimensions.
2117 + *
2118 + * @since 1.113.0
2119 + *
2120 + * @return array An associated array of custom dimensions data.
2121 + */
2122 + private function get_custom_dimensions_data() {
2123 + if ( ! is_singular() ) {
2124 + return array();
2125 + }
2126 +
2127 + $settings = $this->get_settings()->get();
2128 + if ( empty( $settings['availableCustomDimensions'] ) ) {
2129 + return array();
2130 + }
2131 +
2132 + /**
2133 + * Filters the allowed post types for custom dimensions tracking.
2134 + *
2135 + * @since 1.113.0
2136 + *
2137 + * @param array $allowed_post_types The array of allowed post types.
2138 + */
2139 + $allowed_post_types = apply_filters( 'googlesitekit_custom_dimension_valid_post_types', array( 'post' ) );
2140 +
2141 + $data = array();
2142 + $post = get_queried_object();
2143 +
2144 + if ( ! $post instanceof WP_Post ) {
2145 + return $data;
2146 + }
2147 +
2148 + if ( in_array( 'googlesitekit_post_type', $settings['availableCustomDimensions'], true ) ) {
2149 + $data['googlesitekit_post_type'] = $post->post_type;
2150 + }
2151 +
2152 + if ( is_singular( $allowed_post_types ) ) {
2153 + foreach ( $settings['availableCustomDimensions'] as $custom_dimension ) {
2154 + switch ( $custom_dimension ) {
2155 + case 'googlesitekit_post_author':
2156 + $author = get_userdata( $post->post_author );
2157 +
2158 + if ( $author ) {
2159 + $data[ $custom_dimension ] = $author->display_name ? $author->display_name : $author->user_login;
2160 + }
2161 +
2162 + break;
2163 + case 'googlesitekit_post_categories':
2164 + $categories = get_the_category( $post->ID );
2165 +
2166 + if ( ! empty( $categories ) ) {
2167 + $category_names = wp_list_pluck( $categories, 'name' );
2168 +
2169 + $data[ $custom_dimension ] = implode( '; ', $category_names );
2170 + }
2171 +
2172 + break;
2173 + case 'googlesitekit_post_date':
2174 + $data[ $custom_dimension ] = get_the_date( 'Ymd', $post );
2175 + break;
2176 + }
2177 + }
2178 + }
2179 +
2180 + return $data;
2181 + }
2182 +
2183 + /**
2184 + * Parses account ID, adds it to the model object and returns updated model.
2185 + *
2186 + * @since 1.31.0
2187 + *
2188 + * @param Google_Model $account Account model.
2189 + * @param string $id_key Attribute name that contains account id.
2190 + * @return stdClass Updated model with _id attribute.
2191 + */
2192 + public static function filter_account_with_ids( $account, $id_key = 'name' ) {
2193 + $obj = $account->toSimpleObject();
2194 +
2195 + $matches = array();
2196 + if ( preg_match( '#accounts/([^/]+)#', $account[ $id_key ], $matches ) ) {
2197 + $obj->_id = $matches[1];
2198 + }
2199 +
2200 + return $obj;
2201 + }
2202 +
2203 + /**
2204 + * Parses account and property IDs, adds it to the model object and returns updated model.
2205 + *
2206 + * @since 1.31.0
2207 + *
2208 + * @param Google_Model $property Property model.
2209 + * @param string $id_key Attribute name that contains property id.
2210 + * @return stdClass Updated model with _id and _accountID attributes.
2211 + */
2212 + public static function filter_property_with_ids( $property, $id_key = 'name' ) {
2213 + $obj = $property->toSimpleObject();
2214 +
2215 + $matches = array();
2216 + if ( preg_match( '#properties/([^/]+)#', $property[ $id_key ] ?? '', $matches ) ) {
2217 + $obj->_id = $matches[1];
2218 + }
2219 +
2220 + $matches = array();
2221 + if ( preg_match( '#accounts/([^/]+)#', $property['parent'] ?? '', $matches ) ) {
2222 + $obj->_accountID = $matches[1]; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
2223 + }
2224 +
2225 + return $obj;
2226 + }
2227 +
2228 + /**
2229 + * Parses property and web datastream IDs, adds it to the model object and returns updated model.
2230 + *
2231 + * @since 1.31.0
2232 + *
2233 + * @param Google_Model $webdatastream Web datastream model.
2234 + * @return stdClass Updated model with _id and _propertyID attributes.
2235 + */
2236 + public static function filter_webdatastream_with_ids( $webdatastream ) {
2237 + $obj = $webdatastream->toSimpleObject();
2238 +
2239 + $matches = array();
2240 + if ( preg_match( '#properties/([^/]+)/dataStreams/([^/]+)#', $webdatastream['name'], $matches ) ) {
2241 + $obj->_id = $matches[2];
2242 + $obj->_propertyID = $matches[1]; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
2243 + }
2244 +
2245 + return $obj;
2246 + }
2247 +
2248 + /**
2249 + * Filters a list of data stream objects and returns only web data streams.
2250 + *
2251 + * @since 1.49.1
2252 + *
2253 + * @param GoogleAnalyticsAdminV1betaDataStream[] $datastreams Data streams to filter.
2254 + * @return GoogleAnalyticsAdminV1betaDataStream[] Web data streams.
2255 + */
2256 + public static function filter_web_datastreams( array $datastreams ) {
2257 + return array_filter(
2258 + $datastreams,
2259 + function ( GoogleAnalyticsAdminV1betaDataStream $datastream ) {
2260 + return $datastream->getType() === 'WEB_DATA_STREAM';
2261 + }
2262 + );
2263 + }
2264 +
2265 + /**
2266 + * Parses a response, adding the _id and _propertyID params and converting to an array keyed by the propertyID and web datastream IDs.
2267 + *
2268 + * @since 1.39.0
2269 + *
2270 + * @param GoogleAnalyticsAdminV1betaListDataStreamsResponse[] $batch_response Array of GoogleAnalyticsAdminV1betaListWebDataStreamsResponse objects.
2271 + * @return stdClass[] Array of models containing _id and _propertyID attributes, keyed by the propertyID.
2272 + */
2273 + public static function parse_webdatastreams_batch( $batch_response ) {
2274 + $mapped = array();
2275 +
2276 + foreach ( $batch_response as $response ) {
2277 + if ( $response instanceof Exception ) {
2278 + continue;
2279 + }
2280 +
2281 + $webdatastreams = self::filter_web_datastreams( $response->getDataStreams() );
2282 +
2283 + foreach ( $webdatastreams as $webdatastream ) {
2284 + $value = self::filter_webdatastream_with_ids( $webdatastream );
2285 + $key = $value->_propertyID; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
2286 + $mapped[ $key ] = isset( $mapped[ $key ] ) ? $mapped[ $key ] : array();
2287 + $mapped[ $key ][] = $value;
2288 + }
2289 + }
2290 +
2291 + return $mapped;
2292 + }
2293 +
2294 + /**
2295 + * Normalizes account ID and returns it.
2296 + *
2297 + * @since 1.31.0
2298 + *
2299 + * @param string $account_id Account ID.
2300 + * @return string Updated account ID with "accounts/" prefix.
2301 + */
2302 + public static function normalize_account_id( $account_id ) {
2303 + return 'accounts/' . $account_id;
2304 + }
2305 +
2306 + /**
2307 + * Normalizes property ID and returns it.
2308 + *
2309 + * @since 1.31.0
2310 + *
2311 + * @param string $property_id Property ID.
2312 + * @return string Updated property ID with "properties/" prefix.
2313 + */
2314 + public static function normalize_property_id( $property_id ) {
2315 + return 'properties/' . $property_id;
2316 + }
2317 +
2318 + /**
2319 + * Checks if the current user has access to the current configured service entity.
2320 + *
2321 + * @since 1.70.0
2322 + *
2323 + * @return boolean|WP_Error
2324 + */
2325 + public function check_service_entity_access() {
2326 + $settings = $this->get_settings()->get();
2327 +
2328 + if ( empty( $settings['propertyID'] ) ) {
2329 + return new WP_Error(
2330 + 'missing_required_setting',
2331 + __( 'No connected Google Analytics property ID.', 'google-site-kit' ),
2332 + array( 'status' => 500 )
2333 + );
2334 + }
2335 +
2336 + return $this->has_property_access( $settings['propertyID'] );
2337 + }
2338 +
2339 + /**
2340 + * Checks if the current user has access to the given property ID.
2341 + *
2342 + * @since 1.163.0
2343 + *
2344 + * @param string $property_id Property ID to check access for.
2345 + * @return boolean|WP_Error True if the user has access, false if not, or WP_Error on any other error.
2346 + */
2347 + public function has_property_access( $property_id ) {
2348 + $request = $this->get_data( 'has-property-access', array( 'propertyID' => $property_id ) );
2349 +
2350 + if ( is_wp_error( $request ) ) {
2351 + // A 403 error implies that the user does not have access to the service entity.
2352 + if ( $request->get_error_code() === 403 ) {
2353 + return false;
2354 + }
2355 +
2356 + return $request;
2357 + }
2358 +
2359 + return true;
2360 + }
2361 +
2362 + /**
2363 + * Gets the Google Tag Settings for the given measurement ID.
2364 + *
2365 + * @since 1.94.0
2366 + *
2367 + * @param Google_Service_TagManager_Container $container Tag Manager container.
2368 + * @param string $measurement_id Measurement ID.
2369 + * @return array Google Tag Settings.
2370 + */
2371 + protected function get_google_tag_settings_for_measurement_id( $container, $measurement_id ) {
2372 + return array(
2373 + 'googleTagAccountID' => $container->getAccountId(),
2374 + 'googleTagContainerID' => $container->getContainerId(),
2375 + 'googleTagID' => $this->determine_google_tag_id_from_tag_ids( $container->getTagIds(), $measurement_id ),
2376 + );
2377 + }
2378 +
2379 + /**
2380 + * Determines Google Tag ID from the given Tag IDs.
2381 + *
2382 + * @since 1.94.0
2383 + *
2384 + * @param array $tag_ids Tag IDs.
2385 + * @param string $measurement_id Measurement ID.
2386 + * @return string Google Tag ID.
2387 + */
2388 + private function determine_google_tag_id_from_tag_ids( $tag_ids, $measurement_id ) {
2389 + // If there is only one tag id in the array, return it.
2390 + if ( count( $tag_ids ) === 1 ) {
2391 + return $tag_ids[0];
2392 + }
2393 +
2394 + // If there are multiple tags, return the first one that starts with `GT-`.
2395 + foreach ( $tag_ids as $tag_id ) {
2396 + if ( substr( $tag_id, 0, 3 ) === 'GT-' ) { // strlen( 'GT-' ) === 3.
2397 + return $tag_id;
2398 + }
2399 + }
2400 +
2401 + // Otherwise, return the `$measurement_id` if it is in the array.
2402 + if ( in_array( $measurement_id, $tag_ids, true ) ) {
2403 + return $measurement_id;
2404 + }
2405 +
2406 + // Otherwise, return the first one that starts with `G-`.
2407 + foreach ( $tag_ids as $tag_id ) {
2408 + if ( substr( $tag_id, 0, 2 ) === 'G-' ) { // strlen( 'G-' ) === 2.
2409 + return $tag_id;
2410 + }
2411 + }
2412 +
2413 + // If none of the above, return the first one.
2414 + return $tag_ids[0];
2415 + }
2416 +
2417 + /**
2418 + * Gets the Google Analytics 4 tag ID.
2419 + *
2420 + * @since 1.96.0
2421 + *
2422 + * @return string Google Analytics 4 tag ID.
2423 + */
2424 + private function get_tag_id() {
2425 + $settings = $this->get_settings()->get();
2426 +
2427 + if ( ! empty( $settings['googleTagID'] ) ) {
2428 + return $settings['googleTagID'];
2429 + }
2430 + return $settings['measurementID'];
2431 + }
2432 +
2433 + /**
2434 + * Gets the currently configured measurement ID.
2435 + *
2436 + * @since 1.104.0
2437 + *
2438 + * @return string Google Analytics 4 measurement ID.
2439 + */
2440 + protected function get_measurement_id() {
2441 + $settings = $this->get_settings()->get();
2442 +
2443 + return $settings['measurementID'];
2444 + }
2445 +
2446 + /**
2447 + * Populates custom dimension data to pass to JS via _googlesitekitModulesData.
2448 + *
2449 + * @since 1.113.0
2450 + * @since 1.158.0 Renamed method to `get_inline_custom_dimensions_data()`, and modified it to return a new array rather than populating a passed filter value.
2451 + *
2452 + * @return array Inline modules data.
2453 + */
2454 + private function get_inline_custom_dimensions_data() {
2455 + if ( $this->is_connected() ) {
2456 + return array(
2457 + 'customDimensionsDataAvailable' => $this->custom_dimensions_data_available->get_data_availability(),
2458 + );
2459 + }
2460 + }
2461 +
2462 + /**
2463 + * Populates tag ID mismatch value to pass to JS via _googlesitekitModulesData.
2464 + *
2465 + * @since 1.130.0
2466 + * @since 1.158.0 Renamed method to `get_inline_tag_id_mismatch()`, and modified it to return a new array rather than populating a passed filter value.
2467 + *
2468 + * @return array Inline modules data.
2469 + */
2470 + private function get_inline_tag_id_mismatch() {
2471 + if ( $this->is_connected() ) {
2472 + $tag_id_mismatch = $this->transients->get( 'googlesitekit_inline_tag_id_mismatch' );
2473 +
2474 + return array(
2475 + 'tagIDMismatch' => $tag_id_mismatch,
2476 + );
2477 + }
2478 +
2479 + return array();
2480 + }
2481 +
2482 + /**
2483 + * Populates resource availability dates data to pass to JS via _googlesitekitModulesData.
2484 + *
2485 + * @since 1.127.0
2486 + * @since 1.158.0 Renamed method to `get_inline_resource_availability_dates_data()`, and modified it to return a new array rather than populating a passed filter value.
2487 + *
2488 + * @return array Inline modules data.
2489 + */
2490 + private function get_inline_resource_availability_dates_data() {
2491 + if ( $this->is_connected() ) {
2492 + return array(
2493 + 'resourceAvailabilityDates' => $this->resource_data_availability_date->get_all_resource_dates(),
2494 + );
2495 + }
2496 +
2497 + return array();
2498 + }
2499 +
2500 + /**
2501 + * Filters whether or not the option to exclude certain users from tracking should be displayed.
2502 + *
2503 + * If the Analytics-4 module is enabled, and the snippet is enabled, then the option to exclude
2504 + * the option to exclude certain users from tracking should be displayed.
2505 + *
2506 + * @since 1.101.0
2507 + *
2508 + * @param bool $allowed Whether to allow tracking exclusion.
2509 + * @return bool Filtered value.
2510 + */
2511 + private function filter_analytics_allow_tracking_disabled( $allowed ) {
2512 + if ( $allowed ) {
2513 + return $allowed;
2514 + }
2515 +
2516 + if ( $this->get_settings()->get()['useSnippet'] ) {
2517 + return true;
2518 + }
2519 +
2520 + return $allowed;
2521 + }
2522 +
2523 + /**
2524 + * Sets and returns available audiences.
2525 + *
2526 + * @since 1.126.0
2527 + *
2528 + * @param GoogleAnalyticsAdminV1alphaAudience[] $audiences The audiences to set.
2529 + * @return array The available audiences.
2530 + */
2531 + private function set_available_audiences( $audiences ) {
2532 + $available_audiences = array_map(
2533 + function ( GoogleAnalyticsAdminV1alphaAudience $audience ) {
2534 + $display_name = $audience->getDisplayName();
2535 + $audience_item = array(
2536 + 'name' => $audience->getName(),
2537 + 'displayName' => ( 'All Users' === $display_name ) ? 'All visitors' : $display_name,
2538 + 'description' => $audience->getDescription(),
2539 + );
2540 +
2541 + $audience_slug = $this->get_audience_slug( $audience );
2542 + $audience_type = $this->get_audience_type( $audience_slug );
2543 +
2544 + $audience_item['audienceType'] = $audience_type;
2545 + $audience_item['audienceSlug'] = $audience_slug;
2546 +
2547 + return $audience_item;
2548 + },
2549 + $audiences
2550 + );
2551 +
2552 + usort(
2553 + $available_audiences,
2554 + function ( $audience_a, $audience_b ) use ( $available_audiences ) {
2555 + $audience_index_a = array_search( $audience_a, $available_audiences, true );
2556 + $audience_index_b = array_search( $audience_b, $available_audiences, true );
2557 +
2558 + if ( false === $audience_index_a || false === $audience_index_b ) {
2559 + return 0;
2560 + }
2561 +
2562 + $audience_a = $available_audiences[ $audience_index_a ];
2563 + $audience_b = $available_audiences[ $audience_index_b ];
2564 +
2565 + $audience_type_a = $audience_a['audienceType'];
2566 + $audience_type_b = $audience_b['audienceType'];
2567 +
2568 + if ( $audience_type_a === $audience_type_b ) {
2569 + if ( 'SITE_KIT_AUDIENCE' === $audience_type_b ) {
2570 + return 'new-visitors' === $audience_a['audienceSlug'] ? -1 : 1;
2571 + }
2572 +
2573 + return $audience_index_a - $audience_index_b;
2574 + }
2575 +
2576 + $weight_a = self::AUDIENCE_TYPE_SORT_ORDER[ $audience_type_a ];
2577 + $weight_b = self::AUDIENCE_TYPE_SORT_ORDER[ $audience_type_b ];
2578 +
2579 + if ( $weight_a === $weight_b ) {
2580 + return $audience_index_a - $audience_index_b;
2581 + }
2582 +
2583 + return $weight_a - $weight_b;
2584 + }
2585 + );
2586 +
2587 + $this->audience_settings->merge(
2588 + array(
2589 + 'availableAudiences' => $available_audiences,
2590 + 'availableAudiencesLastSyncedAt' => time(),
2591 + )
2592 + );
2593 +
2594 + return $available_audiences;
2595 + }
2596 +
2597 + /**
2598 + * Gets the audience slug.
2599 + *
2600 + * @since 1.126.0
2601 + *
2602 + * @param GoogleAnalyticsAdminV1alphaAudience $audience The audience object.
2603 + * @return string The audience slug.
2604 + */
2605 + private function get_audience_slug( GoogleAnalyticsAdminV1alphaAudience $audience ) {
2606 + $display_name = $audience->getDisplayName();
2607 +
2608 + if ( 'All Users' === $display_name ) {
2609 + return 'all-users';
2610 + }
2611 +
2612 + if ( 'Purchasers' === $display_name ) {
2613 + return 'purchasers';
2614 + }
2615 +
2616 + $filter_clauses = $audience->getFilterClauses();
2617 +
2618 + if ( $filter_clauses ) {
2619 + if ( $this->has_audience_site_kit_identifier(
2620 + $filter_clauses,
2621 + 'new_visitors'
2622 + ) ) {
2623 + return 'new-visitors';
2624 + }
2625 +
2626 + if ( $this->has_audience_site_kit_identifier(
2627 + $filter_clauses,
2628 + 'returning_visitors'
2629 + ) ) {
2630 + return 'returning-visitors';
2631 + }
2632 + }
2633 +
2634 + // Return an empty string for user defined audiences.
2635 + return '';
2636 + }
2637 +
2638 + /**
2639 + * Gets the audience type based on the audience slug.
2640 + *
2641 + * @since 1.126.0
2642 + *
2643 + * @param string $audience_slug The audience slug.
2644 + * @return string The audience type.
2645 + */
2646 + private function get_audience_type( $audience_slug ) {
2647 + if ( ! $audience_slug ) {
2648 + return 'USER_AUDIENCE';
2649 + }
2650 +
2651 + switch ( $audience_slug ) {
2652 + case 'all-users':
2653 + case 'purchasers':
2654 + return 'DEFAULT_AUDIENCE';
2655 + case 'new-visitors':
2656 + case 'returning-visitors':
2657 + return 'SITE_KIT_AUDIENCE';
2658 + }
2659 + }
2660 +
2661 + /**
2662 + * Checks if an audience Site Kit identifier
2663 + * (e.g. `created_by_googlesitekit:new_visitors`) exists in a nested array or object.
2664 + *
2665 + * @since 1.126.0
2666 + *
2667 + * @param array|object $data The array or object to search.
2668 + * @param mixed $identifier The identifier to search for.
2669 + * @return bool True if the value exists, false otherwise.
2670 + */
2671 + private function has_audience_site_kit_identifier( $data, $identifier ) {
2672 + if ( is_array( $data ) || is_object( $data ) ) {
2673 + foreach ( $data as $key => $value ) {
2674 + if ( is_array( $value ) || is_object( $value ) ) {
2675 + // Recursively search the nested structure.
2676 + if ( $this->has_audience_site_kit_identifier( $value, $identifier ) ) {
2677 + return true;
2678 + }
2679 + } elseif (
2680 + 'fieldName' === $key &&
2681 + 'groupId' === $value &&
2682 + isset( $data['stringFilter'] ) &&
2683 + "created_by_googlesitekit:{$identifier}" === $data['stringFilter']['value']
2684 + ) {
2685 + return true;
2686 + }
2687 + }
2688 + }
2689 +
2690 + return false;
2691 + }
2692 +
2693 + /**
2694 + * Returns the Site Kit-created audience display names from the passed list of audiences.
2695 + *
2696 + * @since 1.129.0
2697 + *
2698 + * @param array $audiences List of audiences.
2699 + *
2700 + * @return array List of Site Kit-created audience display names.
2701 + */
2702 + private function get_site_kit_audiences( $audiences ) {
2703 + // Ensure that audiences are available, otherwise return an empty array.
2704 + if ( empty( $audiences ) || ! is_array( $audiences ) ) {
2705 + return array();
2706 + }
2707 +
2708 + $site_kit_audiences = array_filter( $audiences, fn ( $audience ) => ! empty( $audience['audienceType'] ) && ( 'SITE_KIT_AUDIENCE' === $audience['audienceType'] ) );
2709 +
2710 + if ( empty( $site_kit_audiences ) ) {
2711 + return array();
2712 + }
2713 +
2714 + return wp_list_pluck( $site_kit_audiences, 'displayName' );
2715 + }
2716 +
2717 + /**
2718 + * Populates conversion reporting event data to pass to JS via _googlesitekitModulesData.
2719 + *
2720 + * @since 1.139.0
2721 + * @since 1.158.0 Renamed method to `get_inline_conversion_reporting_events_detection()`, and modified it to return a new array rather than populating a passed filter value.
2722 + *
2723 + * @return array Inline modules data.
2724 + */
2725 + private function get_inline_conversion_reporting_events_detection() {
2726 + if ( ! $this->is_connected() ) {
2727 + return array();
2728 + }
2729 +
2730 + $detected_events = $this->transients->get( Conversion_Reporting_Events_Sync::DETECTED_EVENTS_TRANSIENT );
2731 + $lost_events = $this->transients->get( Conversion_Reporting_Events_Sync::LOST_EVENTS_TRANSIENT );
2732 + $new_events_badge = $this->transients->get( Conversion_Reporting_New_Badge_Events_Sync::NEW_EVENTS_BADGE_TRANSIENT );
2733 +
2734 + return array(
2735 + 'newEvents' => is_array( $detected_events ) ? $detected_events : array(),
2736 + 'lostEvents' => is_array( $lost_events ) ? $lost_events : array(),
2737 + 'newBadgeEvents' => is_array( $new_events_badge ) ? $new_events_badge['events'] : array(),
2738 + );
2739 + }
2740 +
2741 + /**
2742 + * Refines the requested scopes based on the current authentication and connection state.
2743 + *
2744 + * Specifically, the `EDIT_SCOPE` is only added if the user is not yet authenticated,
2745 + * or if they are authenticated and have already granted the scope, or if the module
2746 + * is not yet connected (i.e. during setup).
2747 + *
2748 + * @since 1.163.0
2749 + *
2750 + * @param string[] $scopes Array of requested scopes.
2751 + * @return string[] Refined array of requested scopes.
2752 + */
2753 + private function get_refined_scopes( $scopes = array() ) {
2754 + if ( ! Feature_Flags::enabled( 'setupFlowRefresh' ) ) {
2755 + return $scopes;
2756 + }
2757 +
2758 + if ( ! $this->authentication->is_authenticated() ) {
2759 + $scopes[] = self::EDIT_SCOPE;
2760 + return $scopes;
2761 + }
2762 +
2763 + $oauth_client = $this->authentication->get_oauth_client();
2764 + $granted_scopes = $oauth_client->get_granted_scopes();
2765 + $is_in_setup_process = ! $this->is_connected();
2766 +
2767 + if ( in_array( self::EDIT_SCOPE, $granted_scopes, true ) || $is_in_setup_process ) {
2768 + $scopes[] = self::EDIT_SCOPE;
2769 + }
2770 +
2771 + return $scopes;
2772 + }
2773 +
2774 + /**
2775 + * Gets required inline data for the module.
2776 + *
2777 + * @since 1.158.0
2778 + * @since 1.160.0 Include $modules_data parameter to match the interface.
2779 + *
2780 + * @param array $modules_data Inline modules data.
2781 + * @return array An array of the module's inline data.
2782 + */
2783 + public function get_inline_data( $modules_data ) {
2784 + if ( ! $this->is_connected() ) {
2785 + return $modules_data;
2786 + }
2787 +
2788 + $inline_data = array();
2789 +
2790 + // Web data stream availability data.
2791 + $settings = $this->get_settings()->get();
2792 + $transient_key = 'googlesitekit_web_data_stream_unavailable_' . $settings['webDataStreamID'];
2793 + $is_web_data_stream_unavailable = $this->transients->get( $transient_key );
2794 + $inline_data['isWebDataStreamUnavailable'] = (bool) $is_web_data_stream_unavailable;
2795 +
2796 + $inline_data = array_merge(
2797 + $inline_data,
2798 + $this->get_inline_custom_dimensions_data(),
2799 + $this->get_inline_tag_id_mismatch(),
2800 + $this->get_inline_resource_availability_dates_data(),
2801 + $this->get_inline_conversion_reporting_events_detection()
2802 + );
2803 +
2804 + $modules_data[ self::MODULE_SLUG ] = $inline_data;
2805 + return $modules_data;
2806 + }
2807 + }
2808 +