\WP_REST_Server::READABLE, 'callback' => array( $this, 'get_settings' ), 'permission_callback' => array( $this, 'can_manage' ), 'args' => array(), ), array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $this, 'update_settings' ), 'permission_callback' => array( $this, 'can_manage' ), 'args' => self::settings_args(), ), ) ); } /** * GET /settings. */ public function get_settings(): \WP_REST_Response { return new \WP_REST_Response( $this->payload() ); } /** * POST /settings: updates only the keys present in the request body. * * @param \WP_REST_Request $request Request. */ public function update_settings( \WP_REST_Request $request ): \WP_REST_Response { $body = array_merge( (array) $request->get_body_params(), (array) $request->get_json_params() ); $changes = array_intersect_key( $body, Settings::defaults() ); $settings = $this->plugin->settings(); $before = (string) $settings->get( 'property_id' ); $after = $settings->update( $changes ); if ( $before !== (string) $after['property_id'] ) { $this->plugin->reports()->clear_cache(); } return new \WP_REST_Response( $this->payload() ); } /** * The settings response body. `clarity` is the Data Export summary: * {has_token, table_ready, status} (the token itself is write-only). * * @return array */ private function payload(): array { return array( 'settings' => $this->plugin->settings()->all(), 'connection' => $this->plugin->connection()->summary(), 'redirect_uri' => OAuthFlow::return_url(), 'relay_url' => Plugin::relay_url(), 'environment' => wp_get_environment_type(), 'roles' => array_map( 'translate_user_role', wp_roles()->get_names() ), 'post_type_options' => self::post_type_options(), 'clarity' => $this->plugin->clarity()->summary(), ); } /** * Public post types that can get per-post stats, slug => label, for the * Access tab. * * @return array */ private static function post_type_options(): array { $options = array(); foreach ( get_post_types( array( 'public' => true ), 'objects' ) as $slug => $type ) { if ( 'attachment' === $slug ) { continue; } $options[ (string) $slug ] = (string) $type->labels->name; } return $options; } /** * Arg schema for every settings key. Values that pass the schema are * still sanitized by Settings::update(). * * @return array> */ private static function settings_args(): array { $bool = array( 'type' => 'boolean' ); $slug_list = array( 'type' => 'array', 'maxItems' => 100, 'items' => array( 'type' => 'string', 'maxLength' => 64, ), ); $text = array( 'type' => 'string', 'maxLength' => 200, ); $url = array( 'type' => 'string', 'maxLength' => 2048, ); $schemas = array( 'measurement_id' => array( 'type' => 'string', 'pattern' => '^([Gg]-[A-Za-z0-9]{4,16})?$', ), 'tracking_enabled' => $bool, 'exclude_roles' => $slug_list, 'track_non_production' => $bool, 'consent_mode' => array( 'type' => 'string', 'enum' => array( 'off', 'basic', 'advanced' ), ), 'consent_region_default' => array( 'type' => 'string', 'enum' => array( 'eea_denied', 'all_denied', 'all_granted' ), ), 'property_id' => array( 'type' => 'string', 'pattern' => '^(properties/[0-9]+)?$', ), 'property_name' => $text, 'account_name' => $text, 'stream_id' => array( 'type' => 'string', 'pattern' => '^(properties/[0-9]+/dataStreams/[0-9]+)?$', ), 'stream_default_uri' => $url, 'gsc_enabled' => $bool, 'gsc_site' => $url, 'report_roles' => $slug_list, 'post_types' => $slug_list, 'delete_on_uninstall' => $bool, 'onboarded' => $bool, 'clarity_enabled' => $bool, 'clarity_project_id' => array( 'type' => 'string', 'pattern' => '^\s*([A-Za-z0-9]{6,16})?\s*$', ), 'clarity_wp_tags' => $bool, 'pixel_events' => array( 'type' => 'object', 'additionalProperties' => false, 'properties' => array_fill_keys( Platforms::CHOICES, $bool ), ), 'pixels' => self::pixels_schema( $bool ), ); $args = array(); foreach ( $schemas as $key => $schema ) { $args[ $key ] = self::arg( $schema ); } return $args; } /** * The schema of the ad pixel settings, built from the platform registry: * one object per platform, with its ID and conversion fields checked * against the registry's patterns. WordPress answers 400 and names the * field (pixels[tiktok][id]) when one does not match. Unknown platforms * and fields are refused. * * @param array $boolean The boolean schema. * @return array */ private static function pixels_schema( array $boolean ): array { $platforms = array(); foreach ( Platforms::all() as $key => $platform ) { $properties = array( 'enabled' => $boolean, 'id' => array( 'type' => 'string', 'pattern' => Platforms::rest_pattern( $key, 'id' ), ), ); if ( null !== $platform['conversion'] ) { $properties['conversions'] = array( 'type' => 'object', 'additionalProperties' => false, 'properties' => array_fill_keys( $platform['conversion']['events'], array( 'type' => 'string', 'pattern' => Platforms::rest_pattern( $key, 'conversion' ), ) ), ); } // null follows the shared events; otherwise all four are given. $properties['custom_events'] = array( 'type' => array( 'object', 'null' ), 'additionalProperties' => false, 'required' => Platforms::CHOICES, 'properties' => array_fill_keys( Platforms::CHOICES, $boolean ), ); $platforms[ $key ] = array( 'type' => 'object', 'additionalProperties' => false, 'properties' => $properties, ); } return array( 'type' => 'object', 'additionalProperties' => false, 'properties' => $platforms, ); } }