registry = $registry; $this->writer = $writer; $this->io = $io; } /** * Lists every feature and whether it is on. * * ## OPTIONS * * [--group=] * : Only features in this group: site_lock, appearance, admin, email or content. * * [--enabled] * : Only features that are on. * * [--fields=] * : Comma-separated columns. Available: id, group, label, enabled, state. * * [--format=] * : Output format. * --- * default: table * options: * - table * - json * - csv * - yaml * --- * * ## EXAMPLES * * $ wp adminkeep feature list * $ wp adminkeep feature list --enabled --fields=id --format=csv * * @subcommand list * * @param array $args Positional arguments. * @param array $assoc_args Flags. * @return void */ public function list_( $args, $assoc_args ) { $only_group = isset( $assoc_args['group'] ) ? (string) $assoc_args['group'] : ''; $only_enabled = ! empty( $assoc_args['enabled'] ); $rows = array(); foreach ( $this->registry->grouped() as $group => $features ) { if ( '' !== $only_group && $group !== $only_group ) { continue; } foreach ( $features as $id => $feature ) { $on = Settings::is_enabled( $id ); if ( $only_enabled && ! $on ) { continue; } $rows[] = array( 'id' => $id, 'group' => $group, 'label' => $feature->label(), 'enabled' => $on ? 'on' : 'off', 'state' => $this->state_label( $feature, $on ), ); } } $fields = isset( $assoc_args['fields'] ) ? array_values( array_filter( array_map( 'trim', explode( ',', (string) $assoc_args['fields'] ) ) ) ) : array( 'id', 'group', 'label', 'enabled', 'state' ); $this->io->items( isset( $assoc_args['format'] ) ? (string) $assoc_args['format'] : 'table', $rows, $fields ); } /** * Turns one or more features on. * * Only the on/off switch changes; the feature's settings are kept. * Takes effect from the next request, the same as saving the * settings screen. * * ## OPTIONS * * ... * : One or more feature ids. See `wp adminkeep feature list`. * * ## EXAMPLES * * $ wp adminkeep feature enable disable_comments * $ wp adminkeep feature enable disable_xmlrpc disable_file_edit * * @param array $args Feature ids. * @param array $assoc_args Flags. * @return void */ public function enable( $args, $assoc_args ) { $this->switch_features( $args, true ); } /** * Turns one or more features off. * * Only the on/off switch changes; the feature's settings are kept for when it comes back on. * * ## OPTIONS * * ... * : One or more feature ids. See `wp adminkeep feature list`. * * ## EXAMPLES * * $ wp adminkeep feature disable disable_comments * * # The way out of Installation Lockdown from a shell. * $ wp adminkeep feature disable install_lock * * @param array $args Feature ids. * @param array $assoc_args Flags. * @return void */ public function disable( $args, $assoc_args ) { $this->switch_features( $args, false ); } /** * Shows whether a feature is on, and its settings. * * One row per setting: its key, current value, type, and what * `wp adminkeep feature set` accepts for it. This is the reference * for a feature's keys; nothing else needs reading. * * ## OPTIONS * * * : Feature id. See `wp adminkeep feature list`. * * [--format=] * : Output format. * --- * default: table * options: * - table * - json * - csv * - yaml * --- * * ## EXAMPLES * * $ wp adminkeep feature get order * * @param array $args Feature id. * @param array $assoc_args Flags. * @return void */ public function get( $args, $assoc_args ) { $feature = $this->feature_or_halt( $args[0] ); $format = isset( $assoc_args['format'] ) ? (string) $assoc_args['format'] : 'table'; $native = in_array( $format, array( 'json', 'yaml' ), true ); $values = array_merge( array( 'enabled' => Settings::is_enabled( $feature->id() ) ), array_intersect_key( $this->registry->resolve_settings( $feature ), $this->settable( $feature ) ) ); $schema = $this->settable( $feature ); $rows = array(); foreach ( $values as $key => $value ) { $spec = isset( $schema[ $key ] ) ? $schema[ $key ] : array( 'type' => 'bool' ); $rows[] = array( 'key' => $key, 'value' => $native ? $value : $this->as_text( $value ), 'type' => isset( $spec['type'] ) ? $spec['type'] : 'string', 'accepts' => 'enabled' === $key ? sprintf( 'not set here; use: wp adminkeep feature enable %1$s / disable %1$s', $feature->id() ) : $this->accepts( $spec ), ); } $this->io->items( $format, $rows, array( 'key', 'value', 'type', 'accepts' ) ); } /** * Changes one or more of a feature's settings. Everything not named is kept. * * Values: true/false for a switch, a whole number, plain text, or a * comma-separated list (a list replaces what is stored; an empty * value empties it). This does not switch the feature on; use * `wp adminkeep feature enable` for that. * * ## OPTIONS * * * : Feature id. * * ... * : One or more settings, as key=value pairs. `wp adminkeep feature get ` lists the keys. * * ## EXAMPLES * * # Manual ordering for posts and pages. * $ wp adminkeep feature set order post_types=post,page new_post_position=top * * # Keep replaced media files for two weeks. * $ wp adminkeep feature set replace_media retain_days=14 * * @param array $args Feature id, then key=value pairs. * @param array $assoc_args Flags. * @return void */ public function set( $args, $assoc_args ) { $feature = $this->feature_or_halt( array_shift( $args ) ); $schema = $feature->settings_schema(); $settable = $this->settable( $feature ); if ( empty( $settable ) ) { $message = sprintf( '%s has no settings. It can only be switched on or off.', $feature->id() ); // smtp, header_footer_code and contact_form do have settings, but they live in their // own command groups. if ( 'smtp' === $feature->id() ) { $message .= ' Its settings are under: wp adminkeep smtp set'; } elseif ( 'header_footer_code' === $feature->id() ) { $message .= ' Its code is under: wp adminkeep code set'; } elseif ( 'contact_form' === $feature->id() ) { $message .= ' Its settings are under: wp adminkeep contact-form set'; } $this->io->error( $message ); } if ( empty( $args ) ) { $this->io->error( sprintf( 'Give at least one key=value pair. Keys: %s.', implode( ', ', array_keys( $settable ) ) ) ); } $changes = array(); // Parse everything before writing anything. foreach ( $args as $pair ) { $split = ValueParser::split( $pair ); if ( is_wp_error( $split ) ) { $this->io->error( $split->get_error_message() ); } list( $key, $raw ) = $split; if ( isset( $schema[ $key ] ) && ! isset( $settable[ $key ] ) ) { $this->io->error( sprintf( '"%s" is managed by Adminkeep and cannot be set.', $key ) ); } if ( ! isset( $settable[ $key ] ) ) { $message = sprintf( 'Unknown setting "%s". Keys: %s.', $key, implode( ', ', array_keys( $settable ) ) ); // Custom CSS has a setting of its own, but the CSS itself lives in its own command. if ( 'custom_css' === $feature->id() ) { $message .= ' The CSS itself is under: wp adminkeep css set'; } $this->io->error( $message ); } $value = ValueParser::parse( $raw, $settable[ $key ] ); if ( is_wp_error( $value ) ) { $this->io->error( sprintf( '%s: %s', $key, $value->get_error_message() ) ); } $this->warn_unknown_post_types( $settable[ $key ], $value ); $changes[ $key ] = $value; } $this->writer->patch( $feature, $changes ); $message = sprintf( 'Updated %s: %s.', $feature->id(), implode( ', ', array_keys( $changes ) ) ); if ( ! Settings::is_enabled( $feature->id() ) ) { $message .= sprintf( ' The feature is off; turn it on with: wp adminkeep feature enable %s', $feature->id() ); } $this->io->success( $message ); } /** * Switch several features, or none of them. * * @param string[] $ids Feature ids. * @param bool $on Target state. * @return void */ private function switch_features( array $ids, $on ) { $features = array(); // Resolve every id before writing anything: one bad id must not leave a half-applied call. foreach ( $ids as $id ) { $features[] = $this->feature_or_halt( $id ); } $word = $on ? 'on' : 'off'; foreach ( $features as $feature ) { if ( Settings::is_enabled( $feature->id() ) === $on ) { $this->io->success( sprintf( '%s is already %s.', $feature->id(), $word ) ); continue; } $this->writer->set_enabled( $feature, $on ); $this->io->success( sprintf( '%s is now %s.', $feature->id(), $word ) ); if ( $on && $feature instanceof TwoFactor ) { $this->report_recovery_link(); } } } /** * Say where Two-Factor Login's recovery link went. * * Switching the feature on emails the link (the writer tells the feature, see * TwoFactor::enabled_changed()). From a shell there is usually no current user, so it goes to * the site's administration address, and the person at the terminal has no settings screen in * front of them to read that from. The address is printed whole: anyone who can run this * command can read it from the database anyway. * * @return void */ private function report_recovery_link() { $record = RecoveryLink::record(); if ( null !== $record && $record['sent'] ) { $this->io->line( sprintf( 'A recovery link was emailed to %s. Keep that email: the link turns Two-Factor Login off without logging in.', $record['email'] ) ); return; } $this->io->warning( 'The recovery link could not be emailed. This site may not be sending email; fix that before relying on Two-Factor Login.' ); } /** * The feature with this id, or an error listing the valid ids. * * @param string $id Feature id. * @return Feature */ private function feature_or_halt( $id ) { $feature = $this->registry->get( (string) $id ); if ( null === $feature ) { $this->io->error( sprintf( 'Unknown feature "%s". Valid ids: %s.', $id, implode( ', ', array_keys( $this->registry->all() ) ) ) ); } return $feature; } /** * The chip the settings screen shows for a feature: empty when it is off or declares none. * * @param Feature $feature Feature. * @param bool $on Whether it is on. * @return string */ private function state_label( Feature $feature, $on ) { if ( ! $on || ! method_exists( $feature, 'state_label' ) ) { return ''; } return (string) $feature->state_label(); } /** * The schema entries a person may set: everything except machine-managed fields. * * @param Feature $feature Feature. * @return array */ private function settable( Feature $feature ) { return array_filter( $feature->settings_schema(), static function ( $spec ) { return ! ( isset( $spec['ui'] ) && 'status' === $spec['ui'] ); } ); } /** * What `set` takes for one setting, in words a person or a script can act on. * * @param array $spec Schema entry. * @return string */ private function accepts( array $spec ) { $type = isset( $spec['type'] ) ? $spec['type'] : 'string'; $ui = isset( $spec['ui'] ) ? $spec['ui'] : ''; switch ( $type ) { case 'bool': return 'true, false'; case 'int': return 'a whole number'; case 'array': return in_array( $ui, array( 'post_types', 'post_types_flat' ), true ) ? 'comma-separated post types, e.g. post,page' : 'comma-separated list'; case 'string': default: return isset( $spec['choices'] ) && is_array( $spec['choices'] ) ? implode( ', ', $spec['choices'] ) : 'text'; } } /** * A value as one table cell. * * @param mixed $value Value. * @return string */ private function as_text( $value ) { if ( is_bool( $value ) ) { return $value ? 'true' : 'false'; } return is_array( $value ) ? implode( ',', $value ) : (string) $value; } /** * Warn, without refusing, about post types this run cannot see. * * Not an error: the plugin that registers the post type may simply not be loaded here. * * @param array $spec Schema entry. * @param mixed $value Parsed value. * @return void */ private function warn_unknown_post_types( array $spec, $value ) { $ui = isset( $spec['ui'] ) ? $spec['ui'] : ''; if ( ! is_array( $value ) || ! in_array( $ui, array( 'post_types', 'post_types_flat' ), true ) ) { return; } foreach ( $value as $slug ) { if ( ! post_type_exists( $slug ) ) { $this->io->warning( sprintf( '"%s" is not a registered post type right now. Saved anyway.', $slug ) ); } } } }