'POST', 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'args' => array( 'optin' => array( 'type' => 'boolean', 'required' => true, ), 'email' => array( 'type' => 'string' ), ), 'callback' => array( $this, 'handle' ), ) ); register_rest_route( 'aim-transparency/v1', '/directory-token', array( 'methods' => 'GET', 'permission_callback' => '__return_true', 'callback' => function () { $d = get_option( self::OPTION, array() ); $pending = ! empty( $d['token_time'] ) && ( time() - (int) $d['token_time'] ) < 15 * MINUTE_IN_SECONDS; $token = ( $pending && ! empty( $d['optin'] ) && ! empty( $d['token'] ) ) ? $d['token'] : null; return rest_ensure_response( array( 'token' => $token ) ); }, ) ); } /** * Return the current opt-in state for the dashboard. * * @since 1.0.0 * @return array Associative array with opt-in flag, contact email and site URL. */ public function state() { $d = get_option( self::OPTION, array() ); return array( 'optin' => ! empty( $d['optin'] ), 'email' => ! empty( $d['email'] ) ? $d['email'] : wp_get_current_user()->user_email, 'url' => home_url(), ); } /** * Process an opt-in or opt-out request from the dashboard. * * @since 1.0.0 * @param WP_REST_Request $request The REST request carrying the opt-in flag and contact email. * @return WP_REST_Response Response containing the success flag and the updated directory state. */ public function handle( $request ) { $optin = (bool) $request->get_param( 'optin' ); $email = sanitize_email( (string) $request->get_param( 'email' ) ); $existing = get_option( self::OPTION, array() ); if ( $optin ) { $token = wp_generate_password( 24, false ); $data = array( 'optin' => 1, 'email' => is_email( $email ) ? $email : '', 'consent_version' => self::CONSENT_VERSION, 'consent_time' => current_time( 'mysql' ), 'token' => $token, 'token_time' => time(), ); update_option( self::OPTION, $data, false ); $this->push( 'register', $data ); } else { self::request_removal( isset( $existing['token'] ) ? $existing['token'] : '' ); delete_option( self::OPTION ); } return rest_ensure_response( array( 'ok' => true, 'directory' => $this->state(), ) ); } /** * Send the registration request to the directory (non-blocking, best-effort). * * Removal has its own method, request_removal(), because uninstall.php needs it without an * instance and without the rest of the plugin loaded. * * @since 1.0.0 * @param string $action The directory action to perform. Only 'register' is sent from here. * @param array $consent The local consent record supplying the email, consent metadata and token. * @return void */ protected function push( $action, $consent ) { $endpoint = self::endpoint(); if ( ! $endpoint ) { return; } $payload = array( 'action' => $action, 'site_url' => home_url(), 'site_name' => get_bloginfo( 'name' ), 'plugin_version' => AICL_VERSION, 'wp_version' => get_bloginfo( 'version' ), 'flagged_count' => AICL_Plugin::flagged_count(), 'email' => isset( $consent['email'] ) ? $consent['email'] : '', 'consent_version' => isset( $consent['consent_version'] ) ? $consent['consent_version'] : '', 'consent_time' => isset( $consent['consent_time'] ) ? $consent['consent_time'] : '', 'token' => isset( $consent['token'] ) ? $consent['token'] : '', 'token_url' => add_query_arg( 'rest_route', '/aim-transparency/v1/directory-token', home_url( '/' ) ), ); wp_remote_post( $endpoint, array( 'timeout' => 5, 'blocking' => false, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $payload ), ) ); } /** * Ask the directory to delist this site. * * Shared by the dashboard opt-out and by uninstall.php, so there is one copy of the endpoint and * of the request. It is static and needs nothing but WordPress, because uninstall runs with the * plugin inactive and loads this file on its own. * * Both callers delete the token immediately afterwards, so nothing ever retries a removal. The * request is therefore blocking. With the cURL transport WordPress waits for the exchange either * way; without cURL a non-blocking request closes the socket as soon as it is written. * * The directory's remove branch reads only the action, the site URL and the token, so nothing * else is sent. * * @since 2.2.3 * @param string $token The proof-of-control token from the local consent record. * @return void */ public static function request_removal( $token ) { $endpoint = self::endpoint(); if ( ! $endpoint || '' === (string) $token ) { return; } wp_remote_post( $endpoint, array( 'timeout' => 5, 'blocking' => true, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'action' => 'remove', 'site_url' => home_url(), 'token' => (string) $token, ) ), ) ); } /** * The directory endpoint, after the filter. * * @since 2.2.3 * @return string The endpoint URL, or an empty string when the directory has been switched off. */ protected static function endpoint() { /** * Filter the AI Transparency Directory endpoint URL. * * Return an empty string to stop the plugin contacting the directory at all. * * @since 1.0.0 * @param string $endpoint The directory registration endpoint URL. */ return (string) apply_filters( 'aicl_directory_endpoint', self::ENDPOINT ); } }