{from, to}). Each records who and when. * * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; results are request-specific. */ final class Entry_Notes { public const NOTE = 'note'; public const EDIT = 'edit'; /** Longest note kept, in characters. */ public const MAX_LENGTH = 5000; /** * Adds a note to an entry. * * @param int $entry_id Entry ID. * @param string $text Note text (plain; tags are stripped, line breaks kept). * @param int $user_id Author. * @return int Note ID, or 0 when the text is empty or the insert failed. */ public static function add( int $entry_id, string $text, int $user_id ): int { $text = mb_substr( trim( sanitize_textarea_field( $text ) ), 0, self::MAX_LENGTH ); if ( '' === $text || $entry_id <= 0 ) { return 0; } return self::insert( $entry_id, self::NOTE, $text, $user_id ); } /** * Records a change to an entry's values. * * @param int $entry_id Entry ID. * @param array $changes Field name => old and new value. * @param int $user_id Who changed it. */ public static function add_edit( int $entry_id, array $changes, int $user_id ): int { if ( array() === $changes ) { return 0; } return self::insert( $entry_id, self::EDIT, (string) wp_json_encode( $changes ), $user_id ); } /** * Inserts a row. * * @param int $entry_id Entry ID. * @param string $type note|edit. * @param string $content Content. * @param int $user_id User. */ private static function insert( int $entry_id, string $type, string $content, int $user_id ): int { global $wpdb; $ok = $wpdb->insert( Database::table( 'entry_notes' ), array( 'entry_id' => $entry_id, 'user_id' => max( 0, $user_id ), 'type' => $type, 'content' => $content, 'created_at' => current_time( 'mysql', true ), ), array( '%d', '%d', '%s', '%s', '%s' ) ); return false === $ok ? 0 : (int) $wpdb->insert_id; } /** * An entry's notes or change records, oldest first. * * @param int $entry_id Entry ID. * @param string $type note|edit. * @return array Rows with id, entry_id, user_id, type, created_at and * content (string for notes; field => {from, to} array for edits). */ public static function for_entry( int $entry_id, string $type = self::NOTE ): array { global $wpdb; $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE entry_id = %d AND type = %s ORDER BY id', Database::table( 'entry_notes' ), $entry_id, $type ) ); $out = array(); foreach ( is_array( $rows ) ? $rows : array() as $row ) { if ( self::EDIT === $type ) { $row->content = self::changes( (string) $row->content ); } $out[] = $row; } return $out; } /** * Decodes a change record. * * @param string $json Stored JSON. * @return array */ private static function changes( string $json ): array { $data = json_decode( $json, true ); $out = array(); foreach ( is_array( $data ) ? $data : array() as $name => $change ) { if ( is_array( $change ) ) { $out[ (string) $name ] = array( 'from' => (string) ( $change['from'] ?? '' ), 'to' => (string) ( $change['to'] ?? '' ), ); } } return $out; } /** * One note row, or null. * * @param int $note_id Note ID. */ public static function get( int $note_id ): ?object { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', Database::table( 'entry_notes' ), $note_id ) ); return is_object( $row ) ? $row : null; } /** * Deletes a note. Change records are never deleted on their own; they go * with the entry. * * @param int $note_id Note ID. * @return bool Whether a note was deleted. */ public static function delete( int $note_id ): bool { global $wpdb; return (bool) $wpdb->delete( Database::table( 'entry_notes' ), array( 'id' => $note_id, 'type' => self::NOTE, ), array( '%d', '%s' ) ); } /** * Deletes everything recorded for an entry (when the entry is deleted). * * @param int $entry_id Entry ID. */ public static function delete_for_entry( int $entry_id ): void { global $wpdb; $wpdb->delete( Database::table( 'entry_notes' ), array( 'entry_id' => $entry_id ), array( '%d' ) ); } /** * Whether the current user may delete a note: anyone who can edit entries * may delete their own notes; deleting someone else's needs the * delete_tflexforms_entries capability. * * @param object $note Note row. */ public static function current_user_can_delete( object $note ): bool { if ( self::NOTE !== $note->type || ! current_user_can( 'edit_tflexforms_entries' ) ) { return false; } return get_current_user_id() === (int) $note->user_id || current_user_can( 'delete_tflexforms_entries' ); } }