Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,33 @@ class PlaylistDataManager {
return exists
}

func manualPlaylistUUIDs(for episodesUUIDs: [String], dbQueue: PCDBQueue) -> [String] {
var uuids: [String] = []
let episodesUUIDsString = DataHelper.convertArrayToInString(episodesUUIDs)
dbQueue.read { db in
do {
let query = """
SELECT playlist_uuid
FROM \(DataManager.playlistEpisodeTableName)
WHERE episodeUuid IN (\(episodesUUIDsString))
GROUP BY playlist_uuid
HAVING COUNT(DISTINCT episodeUuid) = \(episodesUUIDs.count)
"""
let resultSet = try db.executeQuery(query, values: [])
Comment on lines +190 to +201

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manualPlaylistUUIDs(for episodesUUIDs: [String]) builds a SQL IN clause by interpolating the result of DataHelper.convertArrayToInString(episodesUUIDs) directly into the query string, which is vulnerable to SQL injection if any episodeUuid contains quotes or SQL metacharacters. Since episode.uuid values are populated from server JSON (episodeJson["uuid"]) and then passed here (e.g. via episodes.map { $0.uuid }), an attacker controlling server responses or intercepting traffic could inject arbitrary SQL and read or modify data in the local database. Instead of string-concatenating the IN list, construct the query using ? placeholders for each UUID and pass the episode UUIDs via the values array so the database layer performs proper escaping/binding.

Suggested change
var uuids: [String] = []
//let episodesUUIDS = episodeUUIDs.joined(separator: ",")
dbQueue.read { db in
do {
let query = """
SELECT playlist_uuid
FROM \(DataManager.playlistEpisodeTableName)
WHERE episodeUuid IN (\(DataHelper.convertArrayToInString(episodesUUIDs)))
GROUP BY playlist_uuid
HAVING COUNT(DISTINCT episodeUuid) = \(episodesUUIDs.count);
"""
let resultSet = try db.executeQuery(query, values: [])
// If no episode UUIDs are provided, there can be no matching playlists.
guard !episodesUUIDs.isEmpty else { return [] }
var uuids: [String] = []
dbQueue.read { db in
do {
let placeholders = Array(repeating: "?", count: episodesUUIDs.count).joined(separator: ", ")
let query = """
SELECT playlist_uuid
FROM \(DataManager.playlistEpisodeTableName)
WHERE episodeUuid IN (\(placeholders))
GROUP BY playlist_uuid
HAVING COUNT(DISTINCT episodeUuid) = ?
"""
var values: [Any] = episodesUUIDs
values.append(episodesUUIDs.count)
let resultSet = try db.executeQuery(query, values: values)

Copilot uses AI. Check for mistakes.
defer { resultSet.close() }

while resultSet.next() {
if let uuid = resultSet.string(forColumn: "playlist_uuid") {
uuids.append(uuid)
}
}
} catch {
FileLog.shared.addMessage("PlaylistDataManager.manualPlaylistUUIDs error: \(error)")
}
}
return uuids
}
Comment on lines +189 to +214

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing empty array guard. The function should handle empty arrays gracefully by returning early. When an empty array is passed to convertArrayToInString, it produces an empty string wrapped in quotes (''), which results in invalid SQL syntax. Other similar functions in the codebase (e.g., UserEpisodeDataManager.delete at line 546, PlaylistDataManager.deleteEpisodes at line 297) use a guard statement to check for empty arrays before proceeding with database operations.

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +214

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the new function. The PlaylistDataManagerTests file contains comprehensive test coverage for other PlaylistDataManager methods, but this new function that returns playlists containing all episodes from an array lacks test coverage. Consider adding tests to verify the behavior with multiple episodes, empty arrays, episodes that are only in some playlists, and episodes that share common playlists.

Copilot uses AI. Check for mistakes.

func manualPlaylistUUIDs(for episodeUUID: String, dbQueue: PCDBQueue) -> [String] {
var uuids: [String] = []
dbQueue.read { db in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,10 @@ public class DataManager {
playlistManager.manualPlaylistUUIDs(for: episodeUUID, dbQueue: dbQueue)
}

public func manualPlaylistUUIDs(for episodeUUIDs: [String]) -> [String] {
playlistManager.manualPlaylistUUIDs(for: episodeUUIDs, dbQueue: dbQueue)
}

public func playlistContainsPodcast(podcastUuid: String, includeDeleted: Bool = false) -> Bool {
playlistManager.playlistContainsPodcast(podcastUuid: podcastUuid, includeDeleted: includeDeleted, dbQueue: dbQueue)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ class ManualPlaylistsChooserViewController: PCViewController {
let uuids = dataManager.manualPlaylistUUIDs(for: episode.uuid)
initialSelectedPlaylists = Set(uuids)
} else {
initialSelectedPlaylists = []
let uuids = dataManager.manualPlaylistUUIDs(for: episodes.map {$0.uuid})
initialSelectedPlaylists = Set(uuids)
}
newSelectedPlaylists = initialSelectedPlaylists
}
Expand Down