insert( Tables::changes(), array( 'created_at' => current_time( 'mysql', true ), 'user_id' => get_current_user_id(), 'fixer' => substr( (string) $fixer, 0, 50 ), 'object_type' => substr( (string) $object_type, 0, 20 ), 'object_id' => (int) $object_id, 'summary' => mb_substr( (string) $summary, 0, 500 ), 'state_before' => null === $before ? null : wp_json_encode( $before ), 'state_after' => null === $after ? null : wp_json_encode( $after ), 'status' => 'applied', ), array( '%s', '%d', '%s', '%s', '%d', '%s', '%s', '%s', '%s' ) ); return (int) $wpdb->insert_id; } /** * Stores the verification result of a change. * * @param int $id Change ID. * @param bool $ok Whether verification passed. * @param string $note Explanation. */ public function set_verified( $id, $ok, $note ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->update( Tables::changes(), array( 'verified' => $ok ? 1 : -1, 'verify_note' => mb_substr( (string) $note, 0, 500 ), ), array( 'id' => (int) $id ), array( '%d', '%s' ), array( '%d' ) ); } /** * Puts a label in front of a change's summary, e.g. to mark automatic repairs. * * @param int $id Change ID. * @param string $prefix Label including trailing space. */ public function prefix_summary( $id, $prefix ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->query( $wpdb->prepare( 'UPDATE %i SET summary = LEFT( CONCAT( %s, summary ), 500 ) WHERE id = %d', Tables::changes(), (string) $prefix, (int) $id ) ); } /** * Marks a change as rolled back. * * @param int $id Change ID. */ public function set_rolled_back( $id ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->update( Tables::changes(), array( 'status' => 'rolled_back' ), array( 'id' => (int) $id ), array( '%s' ), array( '%d' ) ); } /** * One change with decoded states. * * @param int $id Change ID. * @return array|null */ public function get( $id ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', Tables::changes(), (int) $id ), ARRAY_A ); return $row ? $this->decode( $row ) : null; } /** * Most recent changes, newest first. * * @param int $limit Rows. * @param int $offset Offset. * @return array */ public function recent( $limit = 50, $offset = 0 ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY id DESC LIMIT %d OFFSET %d', Tables::changes(), (int) $limit, (int) $offset ), ARRAY_A ); return array_map( array( $this, 'decode' ), $rows ); } /** * Most recent changes made by some fixers, newest first. * * @param string[] $fixers Fixer ids. * @param int $limit Rows. * @param int $offset Offset. * @return array */ public function recent_for( array $fixers, $limit = 50, $offset = 0 ) { global $wpdb; if ( ! $fixers ) { return array(); } $in = implode( ',', array_fill( 0, count( $fixers ), '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- one %s per fixer, built above. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE fixer IN ({$in}) ORDER BY id DESC LIMIT %d OFFSET %d", ...array_merge( array( Tables::changes() ), array_values( $fixers ), array( (int) $limit, (int) $offset ) ) ), ARRAY_A ); return array_map( array( $this, 'decode' ), $rows ); } /** * Number of changes made by some fixers. * * @param string[] $fixers Fixer ids. * @return int */ public function count_for( array $fixers ) { global $wpdb; if ( ! $fixers ) { return 0; } $in = implode( ',', array_fill( 0, count( $fixers ), '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- one %s per fixer, built above. return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE fixer IN ({$in})", ...array_merge( array( Tables::changes() ), array_values( $fixers ) ) ) ); } /** * Total number of changes. * * @return int */ public function count() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', Tables::changes() ) ); } /** * Deletes changes older than the retention period. * * @param int $days Days to keep. * @return int Rows deleted. */ public function prune( $days ) { global $wpdb; $cutoff = gmdate( 'Y-m-d H:i:s', time() - max( 1, (int) $days ) * DAY_IN_SECONDS ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery return (int) $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE created_at < %s', Tables::changes(), $cutoff ) ); } /** * Decodes JSON states. * * @param array $row Raw row. * @return array */ private function decode( array $row ) { $row['state_before'] = null === $row['state_before'] ? null : json_decode( $row['state_before'], true ); $row['state_after'] = null === $row['state_after'] ? null : json_decode( $row['state_after'], true ); $row['id'] = (int) $row['id']; $row['object_id'] = (int) $row['object_id']; $row['verified'] = (int) $row['verified']; return $row; } }