expiry). Several can be active at once (cron and a click at the same time). function trackum_challenges() { $all = get_transient( 'trackum_challenges' ); $now = time(); return is_array( $all ) ? array_filter( $all, function ( $expires ) use ( $now ) { return $expires > $now; } ) : array(); } add_action( 'rest_api_init', function () { // The Trackum service asks this to verify that a connect request really comes from this website. // Public on purpose: the service is not logged in. It sends the code it received and only gets back // whether that code is active; active codes are never listed. A confirmed code is used up at once. register_rest_route( 'trackum/v1', '/pruefung', array( 'methods' => 'GET', 'permission_callback' => '__return_true', 'args' => array( 'pruefzahl' => array( 'type' => 'string', 'required' => true, 'validate_callback' => function ( $value ) { return is_string( $value ) && 1 === preg_match( '/^[A-Za-z0-9]{20,64}\z/', $value ); }, ), ), 'callback' => function ( WP_REST_Request $request ) { $code = (string) $request->get_param( 'pruefzahl' ); $challenges = trackum_challenges(); $valid = false; foreach ( array_keys( $challenges ) as $known ) { if ( hash_equals( (string) $known, $code ) ) { $valid = true; unset( $challenges[ $known ] ); set_transient( 'trackum_challenges', $challenges, 2 * MINUTE_IN_SECONDS ); break; } } $response = new WP_REST_Response( array( 'gueltig' => $valid ) ); $response->header( 'Cache-Control', 'no-store, max-age=0' ); return $response; }, ) ); // Used by "Connect WordPress": Trackum hands over the container of this order directly (administrators only). register_rest_route( 'trackum/v1', '/container', array( 'methods' => 'POST', 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'args' => array( 'gtm' => array( 'type' => 'string', 'required' => true, 'validate_callback' => function ( $value ) { return trackum_valid_id( $value ); }, ), ), 'callback' => function ( WP_REST_Request $request ) { $ok = trackum_set_container( $request->get_param( 'gtm' ) ); return array( 'gtm' => trackum_container_id(), 'pending' => ! $ok && (bool) get_option( 'trackum_pending' ) ); }, ) ); // Used by "Connect WordPress" in Trackum right after installing the plugin (administrators only). register_rest_route( 'trackum/v1', '/verbinden', array( 'methods' => 'POST', 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'callback' => function () { trackum_allow(); $ok = trackum_connect(); return array( 'gtm' => $ok ? trackum_container_id() : '', 'meldung' => $ok ? '' : trackum_status_message() ); }, ) ); } ); function trackum_connect() { $challenge = wp_generate_password( 40, false, false ); $challenges = trackum_challenges(); $challenges[ $challenge ] = time() + 2 * MINUTE_IN_SECONDS; set_transient( 'trackum_challenges', $challenges, 2 * MINUTE_IN_SECONDS ); $response = wp_remote_post( TRACKUM_SERVICE . '/api/wp/verbinden', array( 'timeout' => 10, 'limit_response_size' => 4096, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'site' => home_url( '/' ), 'pruefzahl' => $challenge ) ), ) ); $challenges = trackum_challenges(); unset( $challenges[ $challenge ] ); set_transient( 'trackum_challenges', $challenges, 2 * MINUTE_IN_SECONDS ); if ( is_wp_error( $response ) ) { update_option( 'trackum_status', 'unreachable', false ); return false; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); $id = ( is_array( $data ) && isset( $data['gtm'] ) ) ? $data['gtm'] : null; if ( 200 === wp_remote_retrieve_response_code( $response ) && trackum_valid_id( $id ) ) { return trackum_set_container( $id ); } $codes = array( 'kein_auftrag' => 'no_order', 'pruefung' => 'not_reachable_back', 'zu_oft' => 'too_many' ); $error = ( is_array( $data ) && isset( $data['fehler'] ) && is_string( $data['fehler'] ) && isset( $codes[ $data['fehler'] ] ) ) ? $codes[ $data['fehler'] ] : 'failed'; update_option( 'trackum_status', $error, false ); return false; } // Store a container ID. Never replaces a working container silently: a different ID waits for an administrator. function trackum_set_container( $id ) { if ( ! trackum_valid_id( $id ) ) { return false; } $current = trackum_container_id(); if ( $current && $current !== $id ) { update_option( 'trackum_pending', $id, false ); update_option( 'trackum_status', 'pending', false ); return false; } update_option( 'trackum_container', $id, true ); delete_option( 'trackum_status' ); delete_option( 'trackum_pending' ); delete_option( 'trackum_retry_since' ); wp_clear_scheduled_hook( 'trackum_retry' ); return true; } function trackum_status_message() { switch ( get_option( 'trackum_status' ) ) { case 'no_order': return __( 'There is no Trackum order with a Tag Manager container for this website yet. As soon as there is one, the plugin connects itself (it retries every hour for 7 days).', 'trackum' ); case 'gave_up': return __( 'No Trackum order was found within 7 days, so the plugin stopped asking. Click “Connect now” to try again, or enter your container ID by hand below.', 'trackum' ); case 'not_reachable_back': return __( 'Trackum could not reach your website. Is a security plugin blocking the WordPress REST API?', 'trackum' ); case 'too_many': return __( 'Too many attempts. Please try again in an hour.', 'trackum' ); case 'unreachable': return __( 'Could not reach the Trackum service. Please try again later.', 'trackum' ); case 'invalid_id': return __( 'That is not a valid container ID. It starts with GTM- (for example GTM-AB12CD3).', 'trackum' ); case 'pending': return __( 'Trackum suggests a different container. Please check and confirm it below.', 'trackum' ); case 'failed': return __( 'Connecting did not work. Please try again later.', 'trackum' ); default: return __( 'Click “Connect now” to fetch your container from Trackum.', 'trackum' ); } } // After the administrator connected once: keep trying every hour until connected, for at most 7 days (then only "Connect now" // starts again). Scheduled lazily so it also works on every site of a multisite network. define( 'TRACKUM_RETRY_DAYS', 7 ); function trackum_retry_expired() { $since = (int) get_option( 'trackum_retry_since' ); return $since && time() - $since > TRACKUM_RETRY_DAYS * DAY_IN_SECONDS; } function trackum_schedule_retry() { if ( ! trackum_allowed() || trackum_container_id() ) { return; } if ( ! get_option( 'trackum_retry_since' ) ) { update_option( 'trackum_retry_since', time(), false ); // also for sites updated from 1.5 while still waiting } if ( ! trackum_retry_expired() && ! wp_next_scheduled( 'trackum_retry' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', 'trackum_retry' ); } } register_deactivation_hook( __FILE__, function () { wp_clear_scheduled_hook( 'trackum_retry' ); } ); add_action( 'trackum_retry', function () { if ( ! trackum_allowed() || trackum_container_id() ) { return; } if ( trackum_retry_expired() ) { wp_clear_scheduled_hook( 'trackum_retry' ); update_option( 'trackum_status', 'gave_up', false ); return; } trackum_connect(); } ); add_action( 'admin_init', function () { if ( current_user_can( 'manage_options' ) && ! wp_doing_ajax() ) { trackum_schedule_retry(); } } ); /* ---------- Output the Tag Manager code on the website ---------- */ function trackum_container_id() { $id = get_option( 'trackum_container' ); return trackum_valid_id( $id ) ? $id : ''; } // Strict privacy mode (only with the Trackum cookie banner): instead of the Tag Manager code, one script from the Trackum // service. Until your tracking is live it loads Google Tag Manager as usual; afterwards it shows the cookie banner and loads // Google Tag Manager only after a visitor agrees. Nothing is sent to Google before that. function trackum_strict() { return (bool) get_option( 'trackum_strict' ); } add_action( 'wp_head', function () { $id = trackum_container_id(); if ( ! $id ) { return; } if ( trackum_strict() ) { echo "\n"; wp_print_script_tag( array( 'src' => esc_url( TRACKUM_SERVICE . '/b/' . $id . '.js?streng=1' ), 'async' => true, 'id' => 'trackum-banner-js' ) ); return; } $code = "(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer'," . wp_json_encode( $id ) . ');'; echo "\n"; wp_print_inline_script_tag( $code, array( 'id' => 'trackum-gtm' ) ); }, 1 ); add_action( 'wp_body_open', function () { $id = trackum_container_id(); if ( ! $id || trackum_strict() ) { return; } echo '' . "\n"; }, 1 ); /* ---------- Settings page: Settings → Trackum ---------- */ add_action( 'admin_menu', function () { add_options_page( 'Trackum', 'Trackum', 'manage_options', 'trackum', 'trackum_settings_page' ); } ); add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), function ( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'trackum' ) . '' ); return $links; } ); // Notice at the top of the admin area while not connected or while a change waits for confirmation. add_action( 'admin_notices', function () { $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! current_user_can( 'manage_options' ) || 'trackum' === $page || ( trackum_container_id() && ! get_option( 'trackum_pending' ) ) ) { return; } $link = '' . esc_html__( 'Take a look', 'trackum' ) . ''; /* translators: %s: link to the Trackum settings page */ $text = trackum_container_id() ? __( 'Trackum: a container change is waiting for your confirmation. %s', 'trackum' ) : __( 'Trackum: not connected yet. %s', 'trackum' ); printf( '
%s