*/ public static function defaults(): array { return array( 'keep_data_on_uninstall' => true, 'setup_done' => false, // White label: an agency's name on the menu and the header, and no // vendor line in the footer. The plugin still says who it is on // the Plugins screen — this is presentation, not concealment. 'brand_name' => '', 'hide_footer' => false, // How long the plugin keeps its own logs. Days; 0 = keep. 'retention_crawler_days' => 90, 'retention_ai_usage_days' => 365, 'retention_404_days' => 90, ); } public function id(): string { return 'settings'; } public function register( Container $container ): void {} public function boot( Container $container ): void { add_action( 'rest_api_init', function () use ( $container ): void { $this->routes( $container->get( Options::class ) ); } ); } private function routes( Options $options ): void { register_rest_route( self::NAMESPACE, '/settings/general', array( array( 'methods' => 'GET', 'callback' => fn() => $this->payload( $options ), 'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ), ), array( 'methods' => 'PUT', 'callback' => function ( WP_REST_Request $request ) use ( $options ) { $values = array(); if ( $request->has_param( 'keep_data_on_uninstall' ) ) { $values['keep_data_on_uninstall'] = (bool) $request->get_param( 'keep_data_on_uninstall' ); } foreach ( array( 'hide_footer' ) as $flag ) { if ( $request->has_param( $flag ) ) { $values[ $flag ] = (bool) $request->get_param( $flag ); } } if ( $request->has_param( 'brand_name' ) ) { $values['brand_name'] = mb_substr( sanitize_text_field( (string) $request->get_param( 'brand_name' ) ), 0, 40 ); } foreach ( array( 'retention_crawler_days', 'retention_ai_usage_days', 'retention_404_days' ) as $days ) { if ( $request->has_param( $days ) ) { $values[ $days ] = max( 0, min( 3650, (int) $request->get_param( $days ) ) ); } } if ( ! empty( $values ) ) { $options->set( 'core', $values, self::defaults() ); } return $this->payload( $options ); }, 'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ), 'args' => array( 'keep_data_on_uninstall' => array( 'type' => 'boolean' ), 'brand_name' => array( 'type' => 'string' ), 'hide_footer' => array( 'type' => 'boolean' ), 'retention_crawler_days' => array( 'type' => 'integer' ), 'retention_ai_usage_days' => array( 'type' => 'integer' ), 'retention_404_days' => array( 'type' => 'integer' ), ), ), ) ); register_rest_route( self::NAMESPACE, '/settings/modules', array( 'methods' => 'PUT', 'callback' => function ( WP_REST_Request $request ) use ( $options ) { $requested = array_map( 'sanitize_key', (array) $request->get_param( 'disabled' ) ); $catalog = ModuleCatalog::all(); // Only known, optional module ids may be disabled — an // essential module smuggled into the list is dropped here. $disabled = array_values( array_filter( array_unique( $requested ), static fn( string $id ) => isset( $catalog[ $id ] ) && $catalog[ $id ]['optional'] ) ); // A module that executes code comes on only through a // request that says so. The check is on the transition // (off → on), never on the state: a site that already runs // snippets must not be asked again every time it switches // something else, and the acknowledgement is not stored — // switching it off and on again asks again. $current = (array) get_option( 'airanksuite_disabled_modules', array() ); foreach ( $current as $id ) { if ( ModuleCatalog::needs_acknowledgement( (string) $id ) && ! in_array( $id, $disabled, true ) && true !== rest_sanitize_boolean( $request->get_param( 'acknowledge_' . $id ) ) ) { return new WP_Error( 'airanksuite_module_acknowledge', sprintf( /* translators: %s: module name. */ __( '%s runs code on this site. Confirm that you understand this before switching it on.', 'ai-rank-suite' ), $catalog[ $id ]['label'] ?? (string) $id ), array( 'status' => 400, 'module' => (string) $id ) ); } } update_option( 'airanksuite_disabled_modules', $disabled, false ); return $this->payload( $options ); }, 'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ), 'args' => array( 'disabled' => array( 'type' => 'array', 'required' => true, ), ), ) ); register_rest_route( self::NAMESPACE, '/settings/environment', array( 'methods' => 'GET', 'callback' => static fn() => new WP_REST_Response( Services\Environment::payload() ), 'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ), ) ); register_rest_route( self::NAMESPACE, '/setup/status', array( 'methods' => 'GET', 'callback' => static function () { $container = \AiRankSuite\Kernel::instance()->container(); try { $ai = $container->get( \AiRankSuite\Modules\Ai\Services\AiClient::class )->has_key(); } catch ( \Throwable $e ) { $ai = false; // AI module disabled — its service is not registered. } $gsc = is_array( get_option( 'airanksuite_gsc_tokens' ) ); $indexnow = ! empty( $container->get( Options::class )->get( 'indexnow', array( 'enabled' => false ) )['enabled'] ); $visibility = $container->get( Options::class )->get( 'ai_visibility', array( 'prompts' => array() ) ); return new WP_REST_Response( array( 'ai' => $ai, 'google' => $gsc, 'indexnow' => $indexnow, 'visibility_prompts' => count( (array) ( $visibility['prompts'] ?? array() ) ), 'other_seo_plugin' => \AiRankSuite\Modules\Seo\SeoModule::conflict(), // The ordered checklist: what to do, in which order, // and which parts are optional. Every state is read // from a fact the plugin already stores — never from // "the user clicked next". 'checklist' => Services\Checklist::steps(), ) ); }, 'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ), ) ); register_rest_route( self::NAMESPACE, '/core/onboarding', array( 'methods' => 'POST', 'callback' => static function ( WP_REST_Request $request ) use ( $options ) { $options->set( 'core', array( 'setup_done' => (bool) $request->get_param( 'done' ) ), self::defaults() ); return new WP_REST_Response( $options->get( 'core', self::defaults() ) ); }, 'permission_callback' => static fn(): bool => current_user_can( 'manage_options' ), 'args' => array( 'done' => array( 'type' => 'boolean', 'default' => true, ), ), ) ); } private function payload( Options $options ): WP_REST_Response { $disabled = get_option( 'airanksuite_disabled_modules', array() ); $modules = array(); foreach ( ModuleCatalog::all() as $id => $meta ) { $modules[] = array( 'id' => $id, 'label' => $meta['label'], 'desc' => $meta['desc'], 'optional' => $meta['optional'], 'group' => $meta['group'], 'enabled' => ! in_array( $id, (array) $disabled, true ), // The UI shows the warning and the confirm step for exactly the // modules the route will refuse without one — one list, server-side. 'acknowledge' => ModuleCatalog::needs_acknowledgement( $id ), ); } return new WP_REST_Response( array_merge( $options->get( 'core', self::defaults() ), array( 'modules' => $modules, 'active' => \AiRankSuite\Kernel::instance()->moduleIds(), ) ) ); } }