namespace, $this->base, array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check', ), ), ) ); register_rest_route( $this->namespace, $this->base, array( array( 'methods' => \WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check', ), ), ) ); } /** * Check if a given request has access to get items * * @param $request * * @return bool|\WP_Error */ public function get_items_permissions_check( $request ) { return current_user_can( 'manage_options' ); } /** * Get all extensions * * @param $request * * @return WP_HTTP_Response */ public function get_items( $request ) { $type = ! empty( $request['type'] ) ? sanitize_key( $request['type'] ) : 'addon'; $extensions = array_values( array_filter( Plugin_Manager::extensions(), function ( $extension ) use ( $type ) { return $extension['type'] === $type; } ) ); $extensions = array_map( function ( $extension ) { $extension['status'] = 'install'; if ( SdkPluginManager::is_installed( $extension['slug'] ) ) { $extension['status'] = 'activate'; } if ( SdkPluginManager::is_activated( $extension['slug'] ) ) { $extension['status'] = 'deactivate'; } return $extension; }, $extensions ); return $this->response( $extensions ); } /** * Check if a given request has access to get items * * @param $request * * @return bool|\WP_Error */ public function update_item_permissions_check( $request ) { return current_user_can( 'manage_options' ); } /** * Enable or disable extension * * @param $request * * @return WP_HTTP_Response */ public function update_item( $request ) { $input_data = json_decode( $request->get_body(), true ); $name = $input_data['name']; $status = $input_data['status']; $statuses = array( 'off', 'on', 'install', 'activate', 'deactivate' ); $extensions = Plugin_Manager::extensions(); if ( ! $name ) { return $this->error( __( 'Please enter extension name', 'booktics' ) ); } if ( ! $status ) { return $this->error( __( 'Please enter status', 'booktics' ) ); } if ( ! in_array( $status, $statuses ) ) { return $this->error( __( 'Please enter status on/off', 'booktics' ) ); } if ( ! isset( $extensions[ $name ] ) ) { return $this->error( __( 'Invalid extension', 'booktics' ) ); } $extension = $extensions[ $name ]; // Strict, so a "1" or "true" string never counts as agreement. $aisentic_consent = 'aisentic' === $name && true === ( $input_data['consent'] ?? null ); $aisentic_email = ''; // The Ask AI setup dialog sends an edited email. Reject a bad one before // anything is installed. if ( $aisentic_consent && isset( $input_data['email'] ) ) { $aisentic_email = sanitize_email( (string) $input_data['email'] ); if ( ! is_email( $aisentic_email ) ) { return $this->error( __( 'Enter a valid email address.', 'booktics' ), 400 ); } } if ( 'addon' === $extension['type'] ) { $result = null; if ( in_array( $status, array( 'install', 'activate' ) ) ) { $result = Plugin_Manager::install_plugin( $extension['slug'] ); if ( $result ) { $result = Plugin_Manager::activate_plugin( $extension['slug'] ); } } if ( 'deactivate' === $status && Plugin_Manager::is_activated( $extension['slug'] ) ) { $result = Plugin_Manager::deactivate_plugin( $extension['slug'] ); } if ( is_wp_error( $result ) ) { return $this->error( __( 'Extension installation error', 'booktics' ) ); } if ( ! $result ) { // translators: %s is the action performed on the extension (install, activate, deactivate, etc.) return $this->error( sprintf( __( 'Extension couldn\'t %s', 'booktics' ), $status ) ); } } if ( 'plugin' === $extension['type'] ) { $result = null; // Only people who could do this by hand from the Plugins screen. if ( 'install' === $status && ! current_user_can( 'install_plugins' ) ) { return $this->error( __( 'You are not allowed to install plugins.', 'booktics' ), 403 ); } if ( 'install' !== $status && ! current_user_can( 'activate_plugins' ) ) { return $this->error( __( 'You are not allowed to manage plugins.', 'booktics' ), 403 ); } if ( 'install' === $status ) { // Prefer the extension's own download_link (e.g. GitHub release) so a // non-wordpress.org URL is not shadowed by the SDK's hardcoded wp.org zip. $result = ! empty( $extension['download_link'] ) ? Plugin_Manager::install_plugin( $extension['slug'] ) : SdkPluginManager::install_plugin( $extension['slug'] ); if ( $result ) { $result = SdkPluginManager::activate_plugin( $extension['slug'] ); } } elseif ( 'activate' === $status ) { $result = SdkPluginManager::activate_plugin( $extension['slug'] ); } elseif ( 'deactivate' === $status ) { $result = SdkPluginManager::deactivate_plugin( $extension['slug'] ); } if ( is_wp_error( $result ) ) { return $this->error( __( 'Plugin operation error', 'booktics' ) ); } if ( false === $result ) { // translators: %s is the action performed on the plugin (install, activate, deactivate, etc.) return $this->error( sprintf( __( 'Plugin couldn\'t %s', 'booktics' ), $status ) ); } } $response = Plugin_Manager::extensions()[ $name ]; // Nothing reaches Aisentic's provider on activation alone. The handshake only // runs when the caller ticked the consent box and the plugin really is active. if ( 'aisentic' === $name && in_array( $status, array( 'install', 'activate' ), true ) && $aisentic_consent && SdkPluginManager::is_activated( $extension['slug'] ) ) { // Snapshot before the handshake so the caller can tell a fresh // registration (tokens just granted) from re-activating a site that // was already connected (no new tokens). $was_registered = booktics_aisentic_is_registered(); $this->register_aisentic_site( $aisentic_email, sanitize_text_field( (string) ( $input_data['account_name'] ?? '' ) ) ); $is_registered = booktics_aisentic_is_registered(); $response['aisentic_registered'] = $is_registered; // True only when this request is what connected the site, so the // "150K tokens added" message never fires on a plain re-activation. $response['aisentic_newly_registered'] = $is_registered && ! $was_registered; } $response['status'] = 'install'; if ( SdkPluginManager::is_installed( $response['slug'] ) ) { $response['status'] = 'activate'; } if ( SdkPluginManager::is_activated( $response['slug'] ) ) { $response['status'] = 'deactivate'; } return $this->response( $response, __( 'Successfully updated', 'booktics' ) ); } /** * Record consent and hand the site identity to Aisentic. * * Only ever called from behind the consent gate in update_item(). The action is * fire and forget: Aisentic owns the provider call, so a provider outage cannot * fail the activation the user actually asked for. * * @param string $email Email the user confirmed in the Ask AI dialog. Empty uses the business email. * @param string $name Account name sent by the Ask AI dialog. Empty uses the business name. * * @return void */ private function register_aisentic_site( string $email = '', string $name = '' ): void { // Older Aisentic builds ship no listener, so the handshake would go nowhere. if ( ! class_exists( 'Aisentic\Api\Services\Registration_Service' ) ) { return; } $identity = booktics_aisentic_identity(); if ( $email ) { $identity['email'] = $email; } if ( $name ) { $identity['name'] = $name; } // Fail closed. An account cannot be minted without a real address. if ( empty( $identity['email'] ) ) { return; } update_option( 'booktics_aisentic_consent', array( 'agreed' => true, 'time' => gmdate( 'c' ), 'user_id' => get_current_user_id(), 'email' => $identity['email'], ) ); /** * Fires once the admin has opted in to sharing site details with Aisentic. * * @param string $name Business name, falling back to the site title. * @param string $email Business email, falling back to the admin email. * @param string $site_url Site URL. */ do_action( 'booktics/aisentic/register_site', $identity['name'], $identity['email'], $identity['site_url'] ); } }