$ok, 'changed' => $changed, 'message' => $message, 'warnings' => $warnings, 'slug' => $slug, ]; } /** * Why an operation is refused because of a wp-config.php constant, or ''. * Mirrors the settings screen: the login slug is locked by both constants, * the module toggle only by AO_DISABLE_CUSTOM_LOGIN, and the redirect * target by neither. * * @param string $operation 'slug' (set, generate --set, reset), 'toggle' (enable, disable) or 'redirect'. * * @return string */ public function get_lock( string $operation ): string { $source = ao_get_effective_login_slug( $this->get_options() )['source']; if ( 'constant-disable' === $source && in_array( $operation, [ 'slug', 'toggle' ], true ) ) { return __( 'Locked by AO_DISABLE_CUSTOM_LOGIN in wp-config.php. Remove the constant to change this.', 'admin-optimizer' ); } if ( 'constant-override' === $source && 'slug' === $operation ) { return __( 'Locked by AO_CUSTOM_LOGIN_URL in wp-config.php. Remove the constant to change the login URL.', 'admin-optimizer' ); } return ''; } /** * Everything `get` shows. * * @return array */ public function get_info(): array { $options = $this->get_options(); $effective = ao_get_effective_login_slug( $options ); $target = ao_get_login_redirect_target( $options ); $enabled = $effective['enabled'] && Module_Registry::is_enabled( Custom_Login_Url::MODULE_ID ); $pending = Custom_Login_Url_Safe_Save::get_pending( $options ); $status = 'n/a'; if ( '' !== $effective['error'] ) { // An AO_CUSTOM_LOGIN_URL the validator blocked: the module is off and the default login works. $status = 'block: ' . ( Custom_Login_Url_Validator::get_messages()[ $effective['error'] ] ?? $effective['error'] ); } elseif ( '' !== $effective['slug'] ) { $check = Custom_Login_Url_Validator::validate( $effective['slug'], [ 'redirection_slug' => $target['slug'] ] ); $status = '' === $check['message'] ? $check['status'] : $check['status'] . ': ' . $check['message']; } return [ 'slug' => $effective['slug'], 'source' => $effective['source'], 'enabled' => $enabled, 'url' => $enabled ? Custom_Login_Url::build_url( $effective['slug'] ) : site_url( 'wp-login.php', 'login' ), 'redirect_mode' => $target['mode'], 'redirect_target' => $target['slug'], 'email_on_change' => ! isset( $options['email_on_change'] ) || ! empty( $options['email_on_change'] ), 'slug_status' => $status, 'pending_revert_at' => null === $pending ? '' : gmdate( 'Y-m-d\TH:i:s\Z', $pending['expires'] ), ]; } /** * Write the option and prove the new value is what reads back, so a * persistent object cache cannot leave a stale copy behind. * * @param array $options New option value. * * @return bool */ private function save_options( array $options ): bool { Custom_Login_Url_Safe_Save::write_options( $options ); if ( $this->get_options() !== $options ) { wp_cache_delete( 'alloptions', 'options' ); wp_cache_delete( Custom_Login_Url::OPTION_NAME, 'options' ); } return $this->get_options() === $options; } /** * Warning shown when a change cannot take effect yet because the module is off. * * @return string[] */ private function disabled_warnings(): array { if ( Module_Registry::is_enabled( Custom_Login_Url::MODULE_ID ) ) { return []; } return [ __( 'The Custom Login URL module is disabled, so this does not take effect until you run "wp ao login-url enable".', 'admin-optimizer' ) ]; } /** * Email the admin about a change, when asked and a real change happened. * * @param string $slug New stored slug ('' for a reset). * * @return string[] Warnings. */ private function notify( string $slug ): array { if ( Custom_Login_Url::send_login_url_email( $slug ) ) { return []; } return [ __( 'The email could not be sent. Check that the admin email is valid and that your site can send email.', 'admin-optimizer' ) ]; } /** * Set the login slug: the same normalization and validation as the settings screen. * * @param string $input Candidate slug. * @param bool $force Accept a warn-level slug. Never bypasses a block. * @param bool $notify Email the admin when the slug changed. * * @return array */ public function set_slug( string $input, bool $force = false, bool $notify = false ): array { // --- 1. Constants win. --- $lock = $this->get_lock( 'slug' ); if ( '' !== $lock ) { return $this->result( false, $lock ); } $options = $this->get_options(); $target = ao_get_login_redirect_target( $options ); $stored_slug = isset( $options['custom_slug'] ) ? strtolower( (string) $options['custom_slug'] ) : ''; $candidate = strtolower( trim( trim( $input ), '/' ) ); // --- 2. Unchanged: nothing to validate, save or email. --- if ( '' !== $candidate && $candidate === $stored_slug ) { return $this->result( true, __( 'No change: the login URL is already set to this slug.', 'admin-optimizer' ), false, [], $stored_slug ); } // --- 3. Validate. A block always fails; a warn needs --force. --- $check = Custom_Login_Url_Validator::validate( $candidate, [ 'redirection_slug' => $target['slug'] ] ); if ( Custom_Login_Url_Validator::STATUS_BLOCK === $check['status'] ) { return $this->result( false, $check['message'] ); } $warnings = []; if ( Custom_Login_Url_Validator::STATUS_WARN === $check['status'] ) { if ( ! $force ) { return $this->result( false, $check['message'] . ' ' . __( 'Use --force to use this slug anyway.', 'admin-optimizer' ) ); } $warnings[] = $check['message']; } // --- 4. Save, verify, notify. --- // A change made here is never put on probation: any pending safe-save is dropped, the slug is kept. $options['custom_slug'] = $check['slug']; unset( $options[ Custom_Login_Url_Safe_Save::PENDING_KEY ] ); if ( ! $this->save_options( $options ) ) { return $this->result( false, __( 'The login URL could not be saved.', 'admin-optimizer' ) ); } Custom_Login_Url_Safe_Save::unschedule(); $warnings = array_merge( $warnings, $this->disabled_warnings() ); if ( $notify ) { $warnings = array_merge( $warnings, $this->notify( $check['slug'] ) ); } return $this->result( true, __( 'Login URL updated.', 'admin-optimizer' ), true, $warnings, $check['slug'] ); } /** * Generate candidate slugs: three distinct words from the shared list, * picked with random_int(), each passing the validator. * * @param int $count How many candidates. * * @return array{ok:bool,message:string,slugs:string[]} */ public function generate_candidates( int $count = 1 ): array { if ( $count < 1 || $count > self::MAX_GENERATE_COUNT ) { return [ 'ok' => false, /* translators: %d: the largest allowed count. */ 'message' => sprintf( __( '--count must be between 1 and %d.', 'admin-optimizer' ), self::MAX_GENERATE_COUNT ), 'slugs' => [], ]; } $words = self::get_words(); $max = count( $words ) - 1; $target = ao_get_login_redirect_target( $this->get_options() ); $slugs = []; $limit = self::MAX_ATTEMPTS * $count; for ( $attempt = 0; $attempt < $limit && ! isset( $slugs[ $count - 1 ] ); $attempt++ ) { $picked = []; $needed = 3; while ( $needed > 0 ) { $word = $words[ random_int( 0, $max ) ]; if ( ! in_array( $word, $picked, true ) ) { $picked[] = $word; --$needed; } } $slug = implode( '-', $picked ); $check = Custom_Login_Url_Validator::validate( $slug, [ 'redirection_slug' => $target['slug'] ] ); if ( Custom_Login_Url_Validator::STATUS_OK === $check['status'] && ! in_array( $slug, $slugs, true ) ) { $slugs[] = $slug; } } if ( count( $slugs ) < $count ) { return [ 'ok' => false, 'message' => __( 'Could not generate a valid login URL. Try again.', 'admin-optimizer' ), 'slugs' => [], ]; } return [ 'ok' => true, 'message' => '', 'slugs' => $slugs, ]; } /** * Clear the stored slug. The module needs a slug to intercept anything, so an * empty one leaves wp-login.php and /wp-admin on their defaults; the module * toggle and every other setting stay as they are. * * @param bool $notify Email the admin when something was cleared. * * @return array */ public function reset( bool $notify = false ): array { $lock = $this->get_lock( 'slug' ); if ( '' !== $lock ) { return $this->result( false, $lock ); } $options = $this->get_options(); if ( empty( $options['custom_slug'] ) ) { return $this->result( true, __( 'No change: no custom login URL is stored.', 'admin-optimizer' ) ); } $options['custom_slug'] = ''; unset( $options[ Custom_Login_Url_Safe_Save::PENDING_KEY ] ); if ( ! $this->save_options( $options ) ) { return $this->result( false, __( 'The login URL could not be reset.', 'admin-optimizer' ) ); } Custom_Login_Url_Safe_Save::unschedule(); $warnings = $notify ? $this->notify( '' ) : []; return $this->result( true, __( 'Custom login URL cleared. The default wp-login.php and /wp-admin work again.', 'admin-optimizer' ), true, $warnings ); } /** * Turn the module on or off through the Module_Registry toggle. Never * touches the stored settings. * * @param bool $enabled Desired state. * * @return array */ public function set_module_enabled( bool $enabled ): array { $lock = $this->get_lock( 'toggle' ); if ( '' !== $lock ) { return $this->result( false, $lock ); } if ( Module_Registry::is_enabled( Custom_Login_Url::MODULE_ID ) === $enabled ) { return $this->result( true, $enabled ? __( 'No change: the module is already enabled.', 'admin-optimizer' ) : __( 'No change: the module is already disabled.', 'admin-optimizer' ) ); } $changed = Module_Registry::set_enabled( Custom_Login_Url::MODULE_ID, $enabled ); if ( is_wp_error( $changed ) ) { return $this->result( false, $changed->get_error_message() ); } // Toggling the module drops any pending safe-save (the slug stays as it is). Custom_Login_Url_Safe_Save::clear(); $warnings = []; if ( $enabled && '' === (string) ( $this->get_options()['custom_slug'] ?? '' ) ) { $warnings[] = __( 'No login URL is stored yet, so nothing changes until you run "wp ao login-url set".', 'admin-optimizer' ); } return $this->result( true, $enabled ? __( 'Module enabled.', 'admin-optimizer' ) : __( 'Module disabled.', 'admin-optimizer' ), true, $warnings ); } /** * Set the redirect target for logged-out visitors of wp-login.php and wp-admin. * * @param string $mode '404', 'home' or 'custom'. * @param string|null $slug Slug for 'custom' only. * * @return array */ public function set_redirect( string $mode, ?string $slug = null ): array { if ( ! in_array( $mode, [ '404', 'home', 'custom' ], true ) ) { return $this->result( false, __( 'The mode must be 404, home or custom.', 'admin-optimizer' ) ); } if ( 'custom' === $mode && ( null === $slug || '' === trim( $slug ) ) ) { return $this->result( false, __( 'The custom mode needs a slug: wp ao login-url redirect custom .', 'admin-optimizer' ) ); } if ( 'custom' !== $mode && null !== $slug ) { return $this->result( false, __( 'Only the custom mode takes a slug.', 'admin-optimizer' ) ); } $options = $this->get_options(); $effective = ao_get_effective_login_slug( $options ); $current = ao_get_login_redirect_target( $options ); $warnings = []; if ( 'custom' === $mode ) { // Same sanitizing and validation as the settings screen, against the login slug in force. $slug = sanitize_title_with_dashes( (string) $slug ); $check = Custom_Login_Url_Validator::validate( $slug, [ 'purpose' => 'redirect', 'login_slug' => $effective['slug'], ] ); if ( Custom_Login_Url_Validator::STATUS_BLOCK === $check['status'] ) { return $this->result( false, $check['message'] ); } $slug = $check['slug']; if ( $current['mode'] === $mode && $current['slug'] === $slug ) { return $this->result( true, __( 'No change: the redirect target is already set to this.', 'admin-optimizer' ) ); } $options['redirection_slug'] = $slug; } elseif ( $current['mode'] === $mode ) { return $this->result( true, __( 'No change: the redirect mode is already set to this.', 'admin-optimizer' ) ); } $options['redirect_mode'] = $mode; if ( ! $this->save_options( $options ) ) { return $this->result( false, __( 'The redirect target could not be saved.', 'admin-optimizer' ) ); } if ( 'constant-disable' === $effective['source'] ) { $warnings[] = __( 'AO_DISABLE_CUSTOM_LOGIN is active, so this is stored but has no effect until the constant is removed.', 'admin-optimizer' ); } else { $warnings = $this->disabled_warnings(); } return $this->result( true, __( 'Redirect target updated.', 'admin-optimizer' ), true, $warnings ); } }