*/ private $fixers = array(); /** * Constructor. * * @param ChangeLog $log Change log. * @param Fixer[] $fixers Fixers. */ public function __construct( ChangeLog $log, array $fixers ) { $this->log = $log; foreach ( $fixers as $fixer ) { $this->fixers[ $fixer->id() ] = $fixer; } } /** * All fixers. * * @return array */ public function fixers() { return $this->fixers; } /** * Fixers that offer repairs (have candidates), for the Repair screen. * * @return array */ public function repair_fixers() { return array_filter( $this->fixers, function ( Fixer $fixer ) { return (bool) $fixer->issue_ids(); } ); } /** * Fixer for an issue id, or null. * * @param string $issue_id Issue id. * @return Fixer|null */ public function fixer_for_issue( $issue_id ) { foreach ( $this->fixers as $fixer ) { if ( in_array( $issue_id, $fixer->issue_ids(), true ) ) { return $fixer; } } return null; } /** * Applies a fixer to selected candidates. * * @param string $fixer_id Fixer id. * @param string[] $keys Candidate keys. * @param bool $confirmed Whether the user confirmed a destructive repair. * @return array|WP_Error Results keyed by candidate key. */ public function apply( $fixer_id, array $keys, $confirmed = false ) { if ( ! isset( $this->fixers[ $fixer_id ] ) ) { return new WP_Error( 'unknown', __( 'Unknown repair.', 'orbitstem-seo' ) ); } $fixer = $this->fixers[ $fixer_id ]; if ( $fixer->is_destructive() && ! $confirmed ) { return new WP_Error( 'confirm', __( 'This repair removes content. Tick the confirmation box to continue.', 'orbitstem-seo' ) ); } $results = array(); foreach ( array_slice( array_unique( $keys ), 0, self::MAX_PER_REQUEST ) as $key ) { $results[ $key ] = $this->apply_one( $fixer, (string) $key ); } delete_transient( Issues::SUMMARY_TRANSIENT ); return $results; } /** * Applies one candidate. * * @param Fixer $fixer Fixer. * @param string $key Candidate key. * @return array{ok:bool,message:string,change_id:int} */ private function apply_one( Fixer $fixer, $key ) { $item = $fixer->find( $key ); if ( ! $item ) { return $this->result( false, __( 'Skipped: this no longer needs repair (it changed since the preview).', 'orbitstem-seo' ) ); } if ( ! $fixer->can_apply( $item ) ) { return $this->result( false, __( 'Skipped: you do not have permission to change this item.', 'orbitstem-seo' ) ); } $applied = $fixer->apply( $item ); if ( is_wp_error( $applied ) ) { return $this->result( false, $applied->get_error_message() ); } $change_id = $this->log->record( $fixer->id(), $item['object_type'], $item['object_id'], $applied['summary'], $applied['before'], $applied['after'] ); $verify = $fixer->verify( $this->log->get( $change_id ) ); $this->log->set_verified( $change_id, $verify['ok'], $verify['note'] ); return $this->result( true, $applied['summary'] . ( $verify['ok'] ? '' : ' (' . $verify['note'] . ')' ), $change_id, $verify['ok'] ); } /** * Rolls back one logged change. * * @param int $change_id Change ID. * @return true|WP_Error */ public function rollback( $change_id ) { $change = $this->log->get( $change_id ); if ( ! $change ) { return new WP_Error( 'missing', __( 'That change is not in the log.', 'orbitstem-seo' ) ); } if ( 'applied' !== $change['status'] ) { return new WP_Error( 'state', __( 'That change was already rolled back.', 'orbitstem-seo' ) ); } if ( ! isset( $this->fixers[ $change['fixer'] ] ) ) { return new WP_Error( 'unknown', __( 'This kind of change cannot be rolled back here.', 'orbitstem-seo' ) ); } $result = $this->fixers[ $change['fixer'] ]->rollback( $change ); if ( is_wp_error( $result ) ) { return $result; } $this->log->set_rolled_back( $change_id ); delete_transient( Issues::SUMMARY_TRANSIENT ); return true; } /** * Result row. * * @param bool $ok Success. * @param string $message Message. * @param int $change_id Change ID. * @param bool $verified Whether verification passed. * @return array */ private function result( $ok, $message, $change_id = 0, $verified = false ) { return array( 'ok' => $ok, 'message' => $message, 'change_id' => $change_id, 'verified' => $verified, ); } }