get_api_namespace(), self::ROUTE, [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_known_services' ], 'permission_callback' => [ $this, 'validate_permission' ], ] ); register_rest_route( $this->get_api_namespace(), self::ROUTE . '/(?P[a-z0-9-]+)', [ [ 'methods' => WP_REST_Server::CREATABLE, // POST: install. 'callback' => [ $this, 'install_service' ], 'permission_callback' => [ $this, 'validate_permission' ], ], [ 'methods' => WP_REST_Server::DELETABLE, // DELETE: uninstall. 'callback' => [ $this, 'uninstall_service' ], 'permission_callback' => [ $this, 'validate_permission' ], ], ] ); } /** * Full catalog + installed + detected state for the library. * * @param \WP_REST_Request> $request Request. * @since 1.3.0 * @return WP_REST_Response { services:[], installed:[], detected:[] }. */ public function get_known_services( $request ): WP_REST_Response { $catalog = Services_Source::get_instance()->get_catalog(); $installed = Installed_Services::get_instance()->get_installed(); $services = []; foreach ( $catalog as $slug => $service ) { if ( ! is_string( $slug ) || ! is_array( $service ) ) { continue; } // Patterns are matchers, not cookies: install() will not add them, so the // library card must not count or list them either, or it promises seven // cookies for Google Analytics and adds five. $cookies = array_values( array_filter( (array) ( $service['cookies'] ?? [] ), static fn( $cookie ): bool => is_array( $cookie ) && ! Cookie_Identity::is_pattern( (string) ( $cookie['name'] ?? '' ) ) ) ); $summary = Services_Source::pattern_summary( $service ); $services[] = array_merge( [ 'slug' => $slug, 'label' => (string) ( $service['label'] ?? $slug ), 'description' => (string) ( $service['description'] ?? '' ), 'category' => (string) ( $service['category'] ?? 'uncategorized' ), 'pro' => ! empty( $service['pro'] ), 'cookieCount' => count( $cookies ), 'cookies' => $cookies, 'resources' => Services_Source::pattern_lists( $service ), ], $summary ); } return new WP_REST_Response( [ 'services' => $services, 'installed' => array_values( $installed ), 'detected' => $this->detected_slugs( array_keys( $catalog ) ), ], 200 ); } /** * Install a known service: declare its cookies + record the registry entry. * Pro services require an active Pro license (enforced here, not just in UI). * * @param \WP_REST_Request> $request Request (slug URL param). * @since 1.3.0 * @return WP_REST_Response */ public function install_service( $request ): WP_REST_Response { $slug = (string) $request->get_param( 'slug' ); // Shared with the known-services ability so the Pro gate has one // definition and cannot be bypassed through the other path. $blocked = ( new KnownServicesService() )->check_installable( $slug ); if ( $blocked !== null ) { return new WP_REST_Response( [ 'success' => false, 'code' => $blocked['code'], 'message' => $blocked['message'], ], $blocked['code'] === 'unknown_service' ? 404 : 403 ); } $result = Installed_Services::get_instance()->install( $slug ); return new WP_REST_Response( $result, empty( $result['success'] ) ? 400 : 200 ); } /** * Uninstall a known service: remove its declared cookies + registry entry. * * @param \WP_REST_Request> $request Request (slug URL param). * @since 1.3.0 * @return WP_REST_Response */ public function uninstall_service( $request ): WP_REST_Response { $slug = (string) $request->get_param( 'slug' ); $result = Installed_Services::get_instance()->uninstall( $slug ); return new WP_REST_Response( $result, 200 ); } /** * Catalog slugs detected in the most recent scan's resources. * * @param array $slugs Catalog slugs to test. * @since 1.3.0 * @return array */ private function detected_slugs( array $slugs ): array { $resources = (array) get_option( SURECOOKIE_SCANNED_RESOURCES_OPTION, [] ); $matcher = Service_Matcher::get_instance(); $urls = $matcher->collect_resource_urls( [ $resources ] ); return array_values( $matcher->match_services( $slugs, $urls ) ); } }