database = $database; $this->table = Schema::tables( $database )['assertions']; } public function upsert( int $run_id, string $assertion_key, string $state, string $comment_masked, int $actor_id, string $asserted_at ): void { $this->validate_identity( $run_id, $assertion_key, $actor_id ); if ( ! in_array( $state, self::STATES, true ) || strlen( $comment_masked ) > 500 || 1 !== preg_match( '/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/D', $asserted_at ) ) { throw new InvalidArgumentException( 'manual_assertion_invalid' ); } // The table is selected from the compiled Schema allowlist and every value is parameterized. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $result = $this->database->query( $this->database->prepare( "INSERT INTO {$this->table} (run_id, assertion_key, state, comment_masked, actor_id, asserted_at) VALUES (%d, %s, %s, %s, %d, %s) ON DUPLICATE KEY UPDATE state = VALUES(state), comment_masked = VALUES(comment_masked), actor_id = VALUES(actor_id), asserted_at = VALUES(asserted_at)", $run_id, $assertion_key, $state, $comment_masked, $actor_id, $asserted_at ) ); if ( false === $result ) { throw new StorageException( 'manual_assertion_save_failed' ); } } public function delete( int $run_id, string $assertion_key, int $actor_id ): void { $this->validate_identity( $run_id, $assertion_key, $actor_id ); $result = $this->database->delete( $this->table, array( 'run_id' => $run_id, 'assertion_key' => $assertion_key, ), array( '%d', '%s' ) ); if ( false === $result ) { throw new StorageException( 'manual_assertion_delete_failed' ); } } /** * @return list */ public function list_for_run( int $run_id ): array { if ( $run_id < 1 ) { throw new InvalidArgumentException( 'invalid_run_id' ); } // Table name is selected from the compiled Schema allowlist. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $rows = $this->database->get_results( $this->database->prepare( "SELECT assertion_key, state, comment_masked, asserted_at FROM {$this->table} WHERE run_id = %d ORDER BY assertion_key ASC LIMIT 80", $run_id ), ARRAY_A ); if ( ! is_array( $rows ) ) { throw new StorageException( 'manual_assertion_read_failed' ); } $out = array(); foreach ( $rows as $row ) { if ( ! is_array( $row ) || ! isset( $row['assertion_key'], $row['state'], $row['comment_masked'], $row['asserted_at'] ) || ! in_array( $row['state'], self::STATES, true ) ) { throw new StorageException( 'manual_assertion_invalid' ); } $out[] = array( 'assertion_key' => (string) $row['assertion_key'], 'state' => (string) $row['state'], 'comment_masked' => (string) $row['comment_masked'], 'asserted_at' => (string) $row['asserted_at'], ); } return $out; } private function validate_identity( int $run_id, string $assertion_key, int $actor_id ): void { if ( $run_id < 1 || $actor_id < 1 || strlen( $assertion_key ) > 96 || 1 !== preg_match( '/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/D', $assertion_key ) ) { throw new InvalidArgumentException( 'manual_assertion_identity_invalid' ); } } }