settings()->register(); ( new Upgrader() )->maybe_upgrade(); $this->capabilities()->register(); ( new OAuthReturn( fn(): OAuthFlow => $this->oauth() ) )->register(); ( new Routes( $this ) )->register(); $this->health()->register_site_health(); // The posts_column warm-up handler runs on its own wp-cron request, // so it is registered unconditionally rather than only in is_admin(). $posts_column = new Admin\PostsColumn( $this ); $posts_column->register_cron(); // So does the Clarity snapshot sync. The closure keeps boot() from // building ClaritySync (and its TokenStore) on every request. add_action( ClaritySync::CRON_HOOK, fn() => $this->clarity()->run_scheduled() ); // Saving a post (anywhere) forgets its cached Clarity page titles. add_action( 'post_updated', array( Insights::class, 'forget_post_titles' ), 10, 3 ); add_action( 'save_post', array( Insights::class, 'forget_post_titles' ), 10, 2 ); if ( is_admin() ) { ( new Admin\Menu( $this ) )->register(); $this->assets()->register(); $posts_column->register(); ( new Admin\EditorPanel( $this ) )->register(); ( new Admin\DashboardWidget( $this ) )->register(); ( new Admin\PrivacyPolicy( $this->settings() ) )->register(); } else { ( new TagRenderer( $this->settings(), $this->gate() ) )->register(); } add_filter( 'wp_consent_api_registered_' . plugin_basename( TAGSMITH_FILE ), '__return_true' ); // Last, so everything above is in place and every integration can rely on it. ( new Loader( $this->integrations() ) )->load(); /** * Fires once the plugin has booted, on plugins_loaded. Add-ons hook * here to read settings or register their own services through the * Plugin getters. * * @since 2.0.0 * * @param Plugin $plugin The plugin service locator. */ do_action( 'tagsmith_loaded', $this ); } /** * The typed settings store. */ public function settings(): Settings { if ( null === $this->settings ) { $this->settings = new Settings(); } return $this->settings; } /** * The shared tracking rules (environment, request type, visitor): the * tags, Health and any other front-end output ask here, so they follow * the same exclusions. */ public function gate(): Gate { if ( null === $this->gate ) { $this->gate = new Gate( $this->settings() ); } return $this->gate; } /** * What the server knows about a visitor's marketing consent: whether a * consent banner is set up, and whether it allows marketing. */ public function server_consent(): ServerConsent { if ( null === $this->server_consent ) { $this->server_consent = new ServerConsent( $this->settings() ); } return $this->server_consent; } /** * The manager's pixel preview link and its no-cache step. */ public function pixel_preview(): PreviewMode { if ( null === $this->pixel_preview ) { $this->pixel_preview = new PreviewMode(); } return $this->pixel_preview; } /** * The short-lived "a form was sent" signal the form listeners feed. */ public function lead_signal(): LeadSignal { if ( null === $this->lead_signal ) { $this->lead_signal = new LeadSignal( $this->settings(), $this->gate(), $this->server_consent(), $this->pixels_on(), $this->lead_event_on(), $this->pixel_preview() ); } return $this->lead_signal; } /** * The integrations the Loader registers, in order: the ad pixel renderer, * the lead signal and the four form plugin listeners. * * When they are asked: the Loader runs once, at the end of boot(), on * plugins_loaded. By then every active plugin's main file has run, so a * condition may ask whether a function, class or constant a plugin * defines exists, and may read the settings. The query has not been * parsed and the user is not set, so a condition must never use a * conditional tag (is_singular(), is_search()) or the current user; an * integration asks anything that depends on the request itself, from a * hook that fires after `wp` (the renderer does, on wp_enqueue_scripts). * * The integrations share their condition objects, so the Loader asks each * question once. * * @return list */ public function integrations(): array { $lead = $this->lead_signal(); $pixels_on = $this->pixels_on(); $lead_on = $this->lead_event_on(); // The form listeners also run in AJAX and REST requests, so unlike the renderer they do not wait for the front end. return array( new PixelRenderer( $this->settings(), $this->gate(), $this->pixel_preview(), $lead, array( new FrontEnd(), $pixels_on ) ), $lead, new WpForms( $lead, new WpFormsActive(), $pixels_on, $lead_on ), new GravityForms( $lead, new GravityFormsActive(), $pixels_on, $lead_on ), new FluentForms( $lead, new FluentFormsActive(), $pixels_on, $lead_on ), new ElementorPro( $lead, new ElementorProActive(), $pixels_on, $lead_on ), ); } /** * The condition "an ad pixel is on", shared by the integrations. */ private function pixels_on(): PixelsOn { if ( null === $this->pixels_on ) { $this->pixels_on = new PixelsOn( $this->settings() ); } return $this->pixels_on; } /** * The condition "a pixel that is on sends leads", shared likewise. */ private function lead_event_on(): LeadEventOn { if ( null === $this->lead_event_on ) { $this->lead_event_on = new LeadEventOn( $this->settings() ); } return $this->lead_event_on; } /** * The report-viewing capability mapping. */ public function capabilities(): Capabilities { if ( null === $this->capabilities ) { $this->capabilities = new Capabilities( $this->settings() ); } return $this->capabilities; } /** * The encrypted-at-rest token store. */ public function tokens(): TokenStore { if ( null === $this->tokens ) { $this->tokens = new TokenStore(); } return $this->tokens; } /** * The Google connection state. */ public function connection(): Connection { if ( null === $this->connection ) { $this->connection = new Connection( $this->tokens() ); } return $this->connection; } /** * The HTTP client for Google and relay calls. */ public function http(): HttpClient { if ( null === $this->http ) { $this->http = new HttpClient(); } return $this->http; } /** * The Google OAuth flow (relay and BYO). */ public function oauth(): OAuthFlow { if ( null === $this->oauth ) { $this->oauth = new OAuthFlow( $this->settings(), $this->connection(), $this->http(), self::relay_url(), $this->tokens() ); } return $this->oauth; } /** * The Google Analytics Admin API client (accounts, properties, streams). */ public function admin_api(): AdminApi { if ( null === $this->admin_api ) { $this->admin_api = new AdminApi( $this->http(), $this->oauth() ); } return $this->admin_api; } /** * The Google Analytics Data API client (reports). */ public function data_api(): DataApi { if ( null === $this->data_api ) { $this->data_api = new DataApi( $this->http(), $this->oauth() ); } return $this->data_api; } /** * The Google Search Console API client. */ public function search_console(): SearchConsoleApi { if ( null === $this->search_console ) { $this->search_console = new SearchConsoleApi( $this->http(), $this->oauth() ); } return $this->search_console; } /** * The report definitions registry. */ public function registry(): ReportRegistry { if ( null === $this->registry ) { $this->registry = new ReportRegistry(); } return $this->registry; } /** * The report runner (cached Google reports). */ public function reports(): ReportRunner { if ( null === $this->reports ) { $this->reports = new ReportRunner( $this->settings(), $this->registry(), $this->data_api(), $this->search_console(), $this->connection() ); } return $this->reports; } /** * The diagnostics service (connection, tag, conflicts, environment, * consent), also registered with Site Health. Built with closures for * OAuthFlow/AdminApi/SearchConsoleApi so boot() never constructs * OAuthFlow eagerly (it needs TokenStore, which needs the auth salts). */ public function health(): HealthCheck { if ( null === $this->health ) { $this->health = new HealthCheck( $this->settings(), $this->connection(), fn(): OAuthFlow => $this->oauth(), fn(): AdminApi => $this->admin_api(), fn(): SearchConsoleApi => $this->search_console(), $this->gate(), $this->server_consent() ); } return $this->health; } /** * The admin asset enqueuer, also the single source of the * `window.tagsmithConfig` payload every Tagsmith JS entry * (dashboard, settings, editor, widget) reads. */ public function assets(): Assets { if ( null === $this->assets ) { $this->assets = new Assets( $this ); } return $this->assets; } /** * The local table of Clarity Data Export snapshots. */ public function clarity_store(): SnapshotStore { if ( null === $this->clarity_store ) { $this->clarity_store = new SnapshotStore(); } return $this->clarity_store; } /** * The Clarity Data Export sync: token (write-only), status, budget and * the twice-daily cron. */ public function clarity(): ClaritySync { if ( null === $this->clarity ) { $this->clarity = new ClaritySync( $this->tokens(), new ClarityApi(), $this->clarity_store(), $this->settings() ); } return $this->clarity; } /** * Whether this is the demo (the Playground preview, which defines * TAGSMITH_DEMO): it shows fake data and never loads an ad pixel. */ public static function is_demo(): bool { return defined( 'TAGSMITH_DEMO' ) && TAGSMITH_DEMO; } /** * The URL of the Tagsmith relay service, used for the "relay" * connection mode. Overridable via the TAGSMITH_RELAY_URL constant, * then the tagsmith_relay_url filter. */ public static function relay_url(): string { if ( defined( 'TAGSMITH_RELAY_URL' ) ) { return untrailingslashit( TAGSMITH_RELAY_URL ); } /** * Filters the URL of the relay that handles the Google sign-in. * Not applied when the TAGSMITH_RELAY_URL constant is defined. * * @since 2.0.0 * * @param string $url The relay URL. Default is the relay run by the plugin's author. */ return untrailingslashit( apply_filters( 'tagsmith_relay_url', self::DEFAULT_RELAY_URL ) ); } /** * Whether the relay in use is the one run by the plugin's author. Copy * that says who runs the relay is only true then. */ public static function relay_is_default(): bool { return self::relay_url() === self::DEFAULT_RELAY_URL; } /** * The domain Google names on its "Google hasn't verified this app" * screen ("Go to (unsafe)"): the home page domain of the OAuth * app behind the relay. It is not the relay's own host, so it is kept * apart from relay_url(). The relay pins the same value * (UNVERIFIED_APP_DOMAIN in relay/src/confirm.ts). * * That domain is the author's only with the author's relay. With another * relay the filter starts from an empty string, and only a host name it * returns is used; anything else leaves no domain, and the Connect step * then says nothing about Google's warning. */ public static function oauth_app_domain(): string { /** * Filters the domain the Connect step names for Google's * unverified-app screen. With your own relay, return your OAuth app's * home domain here (a host name, without a scheme, a path or a * port). When nothing valid is returned, the note is not shown. * * @since 2.0.0 * * @param string $domain The OAuth app's home page domain: the author's with the default relay, otherwise an empty string. */ $domain = apply_filters( 'tagsmith_oauth_app_domain', self::relay_is_default() ? 'shivaatripathi.com' : '' ); if ( ! is_string( $domain ) ) { return ''; } $domain = strtolower( $domain ); return 1 === preg_match( '/^(?=.{1,253}$)([a-z0-9-]+\.)+[a-z]{2,}$/D', $domain ) ? $domain : ''; } }