\d+)', [ 'methods' => 'GET', 'callback' => [self::class, 'read'], 'permission_callback' => [self::class, 'permission'], ]); register_rest_route('pro-calendar-wp/v1', '/geocoding', [ 'methods' => 'POST', 'callback' => [self::class, 'search'], 'permission_callback' => [self::class, 'permission'], ]); } public static function permission(\WP_REST_Request $request): bool { $id = absint($request['id'] ?? $request['post_id'] ?? 0); return $id > 0 && in_array(get_post_type($id), ['pcwp_event', 'pcwp_venue', 'pcwp_series'], true) && current_user_can('edit_post', $id); } public static function read(\WP_REST_Request $request): \WP_REST_Response { $id = absint($request['id']); $venues = []; foreach (Location::venues($id) as $venueId) { if (get_post_status($venueId) === 'publish' || current_user_can('edit_post', $venueId)) { $venues[] = ['id' => $venueId, 'title' => get_the_title($venueId), 'location' => Location::stored($venueId)]; } } return new \WP_REST_Response(['stored' => Location::stored($id), 'resolved' => Location::resolve($id), 'venues' => $venues, 'showMap' => (bool) get_post_meta($id, '_pcwp_show_map', true)]); } public static function search(\WP_REST_Request $request): \WP_REST_Response|\WP_Error { $query = sanitize_text_field((string) $request['query']); if (mb_strlen($query) < 3 || mb_strlen($query) > 250) { return new \WP_Error('pcwp_geocoding_query', __('Enter between 3 and 250 characters.', 'proeventswp'), ['status' => 400]); } if (get_option('pcwp_geocoding_provider', '') !== 'geoapify') { return new \WP_Error('pcwp_geocoding_disabled', __('Configure address lookup in Maps settings or place the pin manually.', 'proeventswp'), ['status' => 503]); } $rateKey = 'pcwp_geo_rate_' . get_current_user_id(); $rate = (int) get_transient($rateKey); if ($rate >= 30) { return new \WP_Error('pcwp_geocoding_rate', __('Too many lookups. Please wait a minute.', 'proeventswp'), ['status' => 429]); } set_transient($rateKey, $rate + 1, 60); $language = strtolower(substr(sanitize_key((string) ($request['language'] ?? get_user_locale())), 0, 2)); $country = (string) get_option('pcwp_geocoding_country', ''); $key = GeocodingKey::get(); if ($key === '') { return new \WP_Error('pcwp_geocoding_key', __('Configure a geocoding API key in Maps settings.', 'proeventswp'), ['status' => 503]); } // One bounded cache per editor, without persisting raw address query text. $cacheKey = 'pcwp_geo_cache_' . get_current_user_id(); $fingerprint = hash('sha256', $query . $language . $country . $key); $cache = get_transient($cacheKey); if (is_array($cache) && ($cache['fingerprint'] ?? '') === $fingerprint) { return new \WP_REST_Response($cache['results']); } try { $provider = apply_filters('pcwp_geocoder', new GeoapifyGeocoder($key, $language, $country)); if (! $provider instanceof GeocoderInterface) { throw new \RuntimeException('Address lookup provider is unavailable.'); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are data; REST encodes them and admin callers escape output. } $results = $provider->search($query); set_transient($cacheKey, ['fingerprint' => $fingerprint, 'results' => $results], 300); return new \WP_REST_Response($results); } catch (\RuntimeException $error) { $message = isset($provider) && $provider instanceof GeoapifyGeocoder ? $error->getMessage() : __('Address lookup is temporarily unavailable. You can still save the address.', 'proeventswp'); return new \WP_Error('pcwp_geocoding_failed', $message, ['status' => 502]); } catch (\Throwable) { return new \WP_Error('pcwp_geocoding_failed', __('Address lookup is temporarily unavailable. You can still save the address.', 'proeventswp'), ['status' => 502]); } } }