table = Schema::table( 'forensix_activity' ); } /** * Inserts an entry. * * @param array $entry Fields. * @phpstan-param array{user_id: int, app: string, action: string, object_type: string, object_id: string, summary: string, before: mixed, after: mixed, after_hash: string, status: string, undo_of: int} $entry */ public function insert( array $entry ): int { global $wpdb; $wpdb->insert( $this->table, array( 'user_id' => $entry['user_id'], 'app' => $entry['app'], 'action' => $entry['action'], 'object_type' => $entry['object_type'], 'object_id' => $entry['object_id'], 'summary' => $entry['summary'], 'before_data' => (string) wp_json_encode( $entry['before'] ), 'after_data' => (string) wp_json_encode( $entry['after'] ), 'after_hash' => $entry['after_hash'], 'status' => $entry['status'], 'undo_of' => $entry['undo_of'] > 0 ? $entry['undo_of'] : null, 'created_at' => gmdate( 'Y-m-d H:i:s' ), ), array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s' ) ); return (int) $wpdb->insert_id; } /** * Loads an entry. * * @param int $id Entry id. */ public function find( int $id ): ?ActivityRecord { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $this->table, $id ), ARRAY_A ); return is_array( $row ) ? self::record( $row ) : null; } /** * Newest entries first. * * @param array $filters Filters. * @param int $limit Max rows. * @param int $offset Rows to skip. * @phpstan-param array{app?: string, object_type?: string, object_id?: string} $filters * @return ActivityRecord[] */ public function recent( array $filters, int $limit, int $offset ): array { global $wpdb; $where = array( '1=1' ); $params = array( $this->table ); foreach ( array( 'app', 'object_type', 'object_id' ) as $column ) { if ( isset( $filters[ $column ] ) ) { $where[] = "{$column} = %s"; $params[] = $filters[ $column ]; } } $params[] = max( 1, min( 500, $limit ) ); $params[] = max( 0, $offset ); // The WHERE clause only contains fixed column names and placeholders. $sql = 'SELECT * FROM %i WHERE ' . implode( ' AND ', $where ) . ' ORDER BY id DESC LIMIT %d OFFSET %d'; $rows = $wpdb->get_results( $wpdb->prepare( $sql, $params ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared; see comment above. return array_map( array( self::class, 'record' ), is_array( $rows ) ? $rows : array() ); } /** * Changes an entry's status. * * @param int $id Entry id. * @param string $status New status. */ public function set_status( int $id, string $status ): void { global $wpdb; $wpdb->update( $this->table, array( 'status' => $status ), array( 'id' => $id ), array( '%s' ), array( '%d' ) ); } /** * Deletes all entries of an app. * * @param string $app App id. */ public function delete_for_app( string $app ): void { global $wpdb; $wpdb->delete( $this->table, array( 'app' => $app ), array( '%s' ) ); } /** * Deletes entries older than a number of days. * * @param int $days Age in days. */ public function purge_older_than( int $days ): void { global $wpdb; $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE created_at < %s', $this->table, gmdate( 'Y-m-d H:i:s', time() - $days * DAY_IN_SECONDS ) ) ); } /** * Builds a record from a row. * * @param array $row Database row. */ private static function record( array $row ): ActivityRecord { return new ActivityRecord( array( 'id' => (int) $row['id'], 'user_id' => (int) $row['user_id'], 'app' => (string) $row['app'], 'action' => (string) $row['action'], 'object_type' => (string) $row['object_type'], 'object_id' => (string) $row['object_id'], 'summary' => (string) $row['summary'], 'before' => is_string( $row['before_data'] ) ? json_decode( $row['before_data'], true ) : null, 'after' => is_string( $row['after_data'] ) ? json_decode( $row['after_data'], true ) : null, 'after_hash' => (string) $row['after_hash'], 'status' => (string) $row['status'], 'undo_of' => (int) $row['undo_of'], 'created_at' => (string) $row['created_at'], ) ); } }