Escape unicode in all URLs from API

This commit is contained in:
Justin Mazzocchi 2021-03-28 23:04:14 -07:00
parent 0ca2bf9653
commit 9552305a78
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
37 changed files with 136 additions and 107 deletions

View file

@ -16,10 +16,10 @@ struct AccountRecord: ContentDatabaseRecord, Hashable {
let statusesCount: Int
let note: HTML
let url: String
let avatar: URL
let avatarStatic: URL
let header: URL
let headerStatic: URL
let avatar: UnicodeURL
let avatarStatic: UnicodeURL
let header: UnicodeURL
let headerStatic: UnicodeURL
let fields: [Account.Field]
let emojis: [Emoji]
let bot: Bool

View file

@ -7,7 +7,7 @@ import Mastodon
struct FeaturedTagRecord: ContentDatabaseRecord, Hashable {
let id: FeaturedTag.Id
let name: String
let url: URL
let url: UnicodeURL
let statusesCount: Int
let lastStatusAt: Date
let accountId: Account.Id

View file

@ -8,8 +8,8 @@ struct IdentityProofRecord: ContentDatabaseRecord, Hashable {
let accountId: Account.Id
let provider: String
let providerUsername: String
let profileUrl: URL
let proofUrl: URL
let profileUrl: UnicodeURL
let proofUrl: UnicodeURL
let updatedAt: Date
}

View file

@ -17,7 +17,7 @@ struct InstanceRecord: ContentDatabaseRecord, Hashable {
let invitesEnabled: Bool
let urls: Instance.URLs
let stats: Instance.Stats
let thumbnail: URL?
let thumbnail: UnicodeURL?
let contactAccountId: Account.Id?
let maxTootChars: Int?
}

View file

@ -21,9 +21,9 @@ public extension Identity {
struct Instance: Codable, Hashable {
public let uri: String
public let streamingAPI: URL
public let streamingAPI: UnicodeURL
public let title: String
public let thumbnail: URL?
public let thumbnail: UnicodeURL?
public let version: String
public let maxTootChars: Int?
}
@ -34,10 +34,10 @@ public extension Identity {
public let username: String
public let displayName: String
public let url: String
public let avatar: URL
public let avatarStatic: URL
public let header: URL
public let headerStatic: URL
public let avatar: UnicodeURL
public let avatarStatic: UnicodeURL
public let header: UnicodeURL
public let headerStatic: UnicodeURL
public let emojis: [Emoji]
public let followRequestCount: Int
}
@ -59,7 +59,7 @@ public extension Identity {
return instance?.title ?? url.host ?? url.absoluteString
}
var image: URL? { account?.avatar ?? instance?.thumbnail }
var image: URL? { (account?.avatar ?? instance?.thumbnail)?.url }
}
public extension Identity.Preferences {

View file

@ -89,15 +89,14 @@ extension CollectionItem {
private extension Account {
func mediaPrefetchURLs(identityContext: IdentityContext) -> Set<URL> {
var urls = Set(emojis.compactMap {
identityContext.appPreferences.animateCustomEmojis ? $0.url : $0.staticUrl
}
.compactMap(URL.init(string:)))
var urls = Set(emojis.map {
(identityContext.appPreferences.animateCustomEmojis ? $0.url : $0.staticUrl).url
})
if identityContext.appPreferences.animateAvatars == .everywhere {
urls.insert(avatar)
urls.insert(avatar.url)
} else {
urls.insert(avatarStatic)
urls.insert(avatarStatic.url)
}
return urls
@ -107,10 +106,9 @@ private extension Account {
private extension Status {
func mediaPrefetchURLs(identityContext: IdentityContext) -> Set<URL> {
displayStatus.account.mediaPrefetchURLs(identityContext: identityContext)
.union(displayStatus.mediaAttachments.compactMap(\.previewUrl))
.union(displayStatus.emojis.compactMap {
identityContext.appPreferences.animateCustomEmojis ? $0.url : $0.staticUrl
}
.compactMap(URL.init(string:)))
.union(displayStatus.mediaAttachments.compactMap(\.previewUrl?.url))
.union(displayStatus.emojis.map {
(identityContext.appPreferences.animateCustomEmojis ? $0.url : $0.staticUrl).url
})
}
}

View file

@ -12,15 +12,12 @@ extension NSMutableAttributedString {
while let tokenRange = string.range(of: token) {
let attachment = AnimatedTextAttachment()
let imageURL: URL?
let imageURL: URL
if identityContext.appPreferences.animateCustomEmojis,
let urlString = emoji.url {
imageURL = URL(stringEscapingPath: urlString)
} else if let staticURLString = emoji.staticUrl {
imageURL = URL(stringEscapingPath: staticURLString)
if identityContext.appPreferences.animateCustomEmojis {
imageURL = emoji.url.url
} else {
imageURL = nil
imageURL = emoji.staticUrl.url
}
attachment.imageView.sd_setImage(with: imageURL) { image, _, _, _ in

View file

@ -14,10 +14,10 @@ public final class Account: Codable, Identifiable {
public let statusesCount: Int
public let note: HTML
public let url: String
public let avatar: URL
public let avatarStatic: URL
public let header: URL
public let headerStatic: URL
public let avatar: UnicodeURL
public let avatarStatic: UnicodeURL
public let header: UnicodeURL
public let headerStatic: UnicodeURL
public let fields: [Field]
public let emojis: [Emoji]
@DecodableDefault.False public private(set) var bot: Bool
@ -36,10 +36,10 @@ public final class Account: Codable, Identifiable {
statusesCount: Int,
note: HTML,
url: String,
avatar: URL,
avatarStatic: URL,
header: URL,
headerStatic: URL,
avatar: UnicodeURL,
avatarStatic: UnicodeURL,
header: UnicodeURL,
headerStatic: UnicodeURL,
fields: [Account.Field],
emojis: [Emoji],
bot: Bool,

View file

@ -6,6 +6,6 @@ public struct AnnouncementReaction: Codable, Hashable {
public let name: String
public let count: Int
public let me: Bool
public let url: URL?
public let staticUrl: URL?
public let url: UnicodeURL?
public let staticUrl: UnicodeURL?
}

View file

@ -34,9 +34,9 @@ public struct Attachment: Codable, Hashable {
public let id: Id
public let type: AttachmentType
public let url: URL
public let remoteUrl: URL?
public let previewUrl: URL?
public let url: UnicodeURL
public let remoteUrl: UnicodeURL?
public let previewUrl: UnicodeURL?
public let meta: Meta?
public let description: String?
public let blurhash: String?

View file

@ -9,7 +9,7 @@ public struct Card: Codable, Hashable {
public static var unknownCase: Self { .unknown }
}
public let url: URL
public let url: UnicodeURL
public let title: String
public let description: String
public let type: CardType
@ -20,6 +20,6 @@ public struct Card: Codable, Hashable {
public let html: String?
public let width: Int?
public let height: Int?
public let image: URL?
public let image: UnicodeURL?
public let embedUrl: String?
}

View file

@ -4,8 +4,8 @@ import Foundation
public struct Emoji: Codable, Hashable {
public let shortcode: String
public let staticUrl: String?
public let url: String?
public let staticUrl: UnicodeURL
public let url: UnicodeURL
public let visibleInPicker: Bool
public let category: String?
}

View file

@ -5,11 +5,11 @@ import Foundation
public struct FeaturedTag: Codable, Hashable {
public let id: Id
public let name: String
public let url: URL
public let url: UnicodeURL
public let statusesCount: Int
public let lastStatusAt: Date
public init(id: FeaturedTag.Id, name: String, url: URL, statusesCount: Int, lastStatusAt: Date) {
public init(id: FeaturedTag.Id, name: String, url: UnicodeURL, statusesCount: Int, lastStatusAt: Date) {
self.id = id
self.name = name
self.url = url

View file

@ -5,11 +5,15 @@ import Foundation
public struct IdentityProof: Codable, Hashable {
public let provider: String
public let providerUsername: String
public let profileUrl: URL
public let proofUrl: URL
public let profileUrl: UnicodeURL
public let proofUrl: UnicodeURL
public let updatedAt: Date
public init(provider: String, providerUsername: String, profileUrl: URL, proofUrl: URL, updatedAt: Date) {
public init(provider: String,
providerUsername: String,
profileUrl: UnicodeURL,
proofUrl: UnicodeURL,
updatedAt: Date) {
self.provider = provider
self.providerUsername = providerUsername
self.profileUrl = profileUrl

View file

@ -4,7 +4,7 @@ import Foundation
public struct Instance: Codable, Hashable {
public struct URLs: Codable, Hashable {
public let streamingApi: URL
public let streamingApi: UnicodeURL
}
public struct Stats: Codable, Hashable {
@ -25,7 +25,7 @@ public struct Instance: Codable, Hashable {
@DecodableDefault.False public private(set) var invitesEnabled: Bool
public let urls: URLs
public let stats: Stats
public let thumbnail: URL?
public let thumbnail: UnicodeURL?
public let contactAccount: Account?
public let maxTootChars: Int?
@ -37,7 +37,7 @@ public struct Instance: Codable, Hashable {
version: String,
urls: Instance.URLs,
stats: Instance.Stats,
thumbnail: URL?,
thumbnail: UnicodeURL?,
contactAccount: Account?,
maxTootChars: Int?) {
self.uri = uri

View file

@ -3,7 +3,7 @@
import Foundation
public struct Mention: Codable, Hashable {
public let url: URL
public let url: UnicodeURL
public let username: String
public let acct: String
public let id: Account.Id

View file

@ -6,7 +6,7 @@ public struct PushNotification: Codable {
public let accessToken: String
public let body: String
public let title: String
public let icon: URL
public let icon: UnicodeURL
public let notificationId: Int
public let notificationType: MastodonNotification.NotificationType
public let preferredLocale: String

View file

@ -13,7 +13,7 @@ public struct PushSubscription: Codable {
@DecodableDefault.True public var status: Bool
}
public let endpoint: URL
public let endpoint: UnicodeURL
public let alerts: Alerts
public let serverKey: String
}

View file

@ -4,7 +4,7 @@ import Foundation
public struct Tag: Codable, Hashable {
public let name: String
public let url: URL
public let url: UnicodeURL
public let history: [History]?
}

View file

@ -0,0 +1,37 @@
// Copyright © 2021 Metabolist. All rights reserved.
import Foundation
public struct UnicodeURL: Hashable {
public let raw: String
public let url: URL
}
extension UnicodeURL: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
raw = try container.decode(String.self)
if let url = URL(string: raw) {
self.url = url
} else if let escaped = raw.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
let colonUnescaped = escaped.replacingOccurrences(
of: "%3A",
with: ":",
range: escaped.range(of: "%3A"))
guard let url = URL(string: colonUnescaped) else { throw URLError(.badURL) }
self.url = url
} else {
throw URLError(.badURL)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(raw)
}
}

View file

@ -54,7 +54,7 @@ final class NotificationService: UNNotificationServiceExtension {
bestAttemptContent.subtitle = handle
}
Self.attachment(imageURL: pushNotification.icon)
Self.attachment(imageURL: pushNotification.icon.url)
.map { [$0] }
.replaceError(with: [])
.handleEvents(receiveOutput: { bestAttemptContent.attachments = $0 })

View file

@ -122,7 +122,7 @@ public extension NavigationService {
private extension NavigationService {
func tag(url: URL) -> String? {
if status?.tags.first(where: { $0.url.path.lowercased() == url.path.lowercased() }) != nil {
if status?.tags.first(where: { $0.url.url.path.lowercased() == url.path.lowercased() }) != nil {
return url.lastPathComponent
} else if
mastodonAPIClient.instanceURL.host == url.host {
@ -133,7 +133,9 @@ private extension NavigationService {
}
func accountId(url: URL) -> String? {
if let mentionId = status?.mentions.first(where: { $0.url.path.lowercased() == url.path.lowercased() })?.id {
if let mentionId = status?.mentions.first(where: {
$0.url.url.path.lowercased() == url.path.lowercased()
})?.id {
return mentionId
} else if
mastodonAPIClient.instanceURL.host == url.host {

View file

@ -262,7 +262,7 @@ private extension AddIdentityViewController {
if let instance = instance {
self.instanceTitleLabel.text = instance.title
self.instanceURLLabel.text = instance.uri
self.instanceImageView.sd_setImage(with: instance.thumbnail)
self.instanceImageView.sd_setImage(with: instance.thumbnail?.url)
self.instanceStackView.isHidden_stackViewSafe = false
if instance.registrations {

View file

@ -52,9 +52,9 @@ final class EditAttachmentViewController: UIViewController {
let player: AVPlayer
if viewModel.attachment.type == .video {
player = PlayerCache.shared.player(url: viewModel.attachment.url)
player = PlayerCache.shared.player(url: viewModel.attachment.url.url)
} else {
player = AVPlayer(url: viewModel.attachment.url)
player = AVPlayer(url: viewModel.attachment.url.url)
}
player.isMuted = false
@ -188,7 +188,7 @@ private extension EditAttachmentViewController {
func detectTextFromPicture() {
SDWebImageManager.shared.loadImage(
with: viewModel.attachment.url,
with: viewModel.attachment.url.url,
options: [],
progress: nil) { image, _, _, _, _, _ in
guard let cgImage = image?.cgImage else { return }

View file

@ -127,7 +127,7 @@ final class ImageViewController: UIViewController {
playerView.isHidden = true
let placeholderImage: UIImage?
let cachedImageKey = viewModel.attachment.previewUrl?.absoluteString
let cachedImageKey = viewModel.attachment.previewUrl?.url.absoluteString
let cachedImage = SDImageCache.shared.imageFromCache(forKey: cachedImageKey)
if cachedImage != nil {
@ -139,7 +139,7 @@ final class ImageViewController: UIViewController {
placeholderImage = nil
}
imageView.sd_setImage(with: viewModel.attachment.url,
imageView.sd_setImage(with: viewModel.attachment.url.url,
placeholderImage: placeholderImage) { _, error, _, _ in
if error != nil {
let alertItem = AlertItem(error: ImageError.unableToLoad)
@ -150,7 +150,7 @@ final class ImageViewController: UIViewController {
case .gifv:
playerView.tag = viewModel.tag
imageView.isHidden = true
let player = PlayerCache.shared.player(url: viewModel.attachment.url)
let player = PlayerCache.shared.player(url: viewModel.attachment.url.url)
player.isMuted = true

View file

@ -589,9 +589,9 @@ private extension TableViewController {
let player: AVPlayer
if attachmentViewModel.attachment.type == .video {
player = PlayerCache.shared.player(url: attachmentViewModel.attachment.url)
player = PlayerCache.shared.player(url: attachmentViewModel.attachment.url.url)
} else {
player = AVPlayer(url: attachmentViewModel.attachment.url)
player = AVPlayer(url: attachmentViewModel.attachment.url.url)
}
playerViewController.delegate = self

View file

@ -29,9 +29,9 @@ public extension AccountViewModel {
var headerURL: URL {
if identityContext.appPreferences.animateHeaders {
return accountService.account.header
return accountService.account.header.url
} else {
return accountService.account.headerStatic
return accountService.account.headerStatic.url
}
}
@ -66,9 +66,9 @@ public extension AccountViewModel {
func avatarURL(profile: Bool = false) -> URL {
if identityContext.appPreferences.animateAvatars == .everywhere
|| (identityContext.appPreferences.animateAvatars == .profiles && profile) {
return accountService.account.avatar
return accountService.account.avatar.url
} else {
return accountService.account.avatarStatic
return accountService.account.avatarStatic.url
}
}

View file

@ -12,11 +12,11 @@ public struct CardViewModel {
}
public extension CardViewModel {
var url: URL { card.url }
var url: URL { card.url.url }
var title: String { card.title }
var description: String { card.description }
var imageURL: URL? { card.image }
var imageURL: URL? { card.image?.url }
}

View file

@ -18,13 +18,13 @@ public extension EmojiViewModel {
var system: Bool { emoji.system }
var url: String? {
var url: URL? {
guard case let .custom(emoji, _) = emoji else { return nil }
if identityContext.appPreferences.animateCustomEmojis {
return emoji.url
return emoji.url.url
} else {
return emoji.staticUrl
return emoji.staticUrl.url
}
}
}

View file

@ -83,17 +83,17 @@ public extension StatusViewModel {
var avatarURL: URL {
if identityContext.appPreferences.animateAvatars == .everywhere {
return statusService.status.displayStatus.account.avatar
return statusService.status.displayStatus.account.avatar.url
} else {
return statusService.status.displayStatus.account.avatarStatic
return statusService.status.displayStatus.account.avatarStatic.url
}
}
var rebloggerAvatarURL: URL {
if identityContext.appPreferences.animateAvatars == .everywhere {
return statusService.status.account.avatar
return statusService.status.account.avatar.url
} else {
return statusService.status.account.avatarStatic
return statusService.status.account.avatarStatic.url
}
}
@ -351,7 +351,7 @@ public extension StatusViewModel {
func attachmentSelected(viewModel: AttachmentViewModel) {
if viewModel.attachment.type == .unknown, let remoteUrl = viewModel.attachment.remoteUrl {
urlSelected(remoteUrl)
urlSelected(remoteUrl.url)
} else {
eventsSubject.send(Just(.attachment(viewModel, self)).setFailureType(to: Error.self).eraseToAnyPublisher())
}

View file

@ -64,7 +64,7 @@ final class AttachmentView: UIView {
extension AttachmentView {
func play() {
let player = PlayerCache.shared.player(url: viewModel.attachment.url)
let player = PlayerCache.shared.player(url: viewModel.attachment.url.url)
playerCancellable = NotificationCenter.default.publisher(
for: .AVPlayerItemDidPlayToEndTime,
@ -180,7 +180,7 @@ private extension AttachmentView {
}
imageView.sd_setImage(
with: viewModel.attachment.previewUrl,
with: viewModel.attachment.previewUrl?.url,
placeholderImage: placeholderImage) { [weak self] _, _, _, _ in
self?.layoutSubviews()
}

View file

@ -199,7 +199,7 @@ private extension CompositionView {
? $0.identity.account?.avatar
: $0.identity.account?.avatarStatic
self.avatarImageView.sd_setImage(with: avatarURL)
self.avatarImageView.sd_setImage(with: avatarURL?.url)
self.changeIdentityButton.accessibilityLabel = $0.identity.handle
self.changeIdentityButton.accessibilityHint =
NSLocalizedString("compose.change-identity-button.accessibility-hint", comment: "")

View file

@ -76,16 +76,7 @@ private extension EmojiView {
emojiLabel.isHidden = true
emojiLabel.text = nil
imageView.isHidden = false
let url: URL?
if let urlString = emojiConfiguration.viewModel.url {
url = URL(stringEscapingPath: urlString)
} else {
url = nil
}
imageView.sd_setImage(with: url)
imageView.sd_setImage(with: emojiConfiguration.viewModel.url)
}
accessibilityLabel = emojiConfiguration.viewModel.name

View file

@ -73,7 +73,7 @@ private extension AutocompleteItemView {
switch autocompleteItemConfiguration.item {
case let .account(account):
let appPreferences = autocompleteItemConfiguration.identityContext.appPreferences
let avatarURL = appPreferences.animateAvatars == .everywhere ? account.avatar : account.avatarStatic
let avatarURL = (appPreferences.animateAvatars == .everywhere ? account.avatar : account.avatarStatic).url
imageView.sd_setImage(with: avatarURL)
imageView.isHidden = false

View file

@ -77,7 +77,7 @@ private extension InstanceView {
func applyInstanceConfiguration() {
let viewModel = instanceConfiguration.viewModel
imageView.sd_setImage(with: viewModel.instance.thumbnail)
imageView.sd_setImage(with: viewModel.instance.thumbnail?.url)
imageView.autoPlayAnimatedImage = !UIAccessibility.isReduceMotionEnabled
titleLabel.text = viewModel.instance.title

View file

@ -138,7 +138,7 @@ private extension EditThumbnailView {
previewImageView.contentMode = .scaleAspectFill
previewImageView.clipsToBounds = true
previewImageView.layer.cornerRadius = .defaultCornerRadius
previewImageView.sd_setImage(with: viewModel.attachment.previewUrl)
previewImageView.sd_setImage(with: viewModel.attachment.previewUrl?.url)
switch viewModel.attachment.type {
case .image:
@ -151,10 +151,10 @@ private extension EditThumbnailView {
placeholderImage = nil
}
imageView.sd_setImage(with: viewModel.attachment.previewUrl, placeholderImage: placeholderImage)
imageView.sd_setImage(with: viewModel.attachment.previewUrl?.url, placeholderImage: placeholderImage)
case .gifv:
imageView.isHidden = true
let player = PlayerCache.shared.player(url: viewModel.attachment.url)
let player = PlayerCache.shared.player(url: viewModel.attachment.url.url)
player.isMuted = true

View file

@ -69,7 +69,7 @@ private extension SecondaryNavigationTitleView {
? viewModel.identityContext.identity.account?.avatar
: viewModel.identityContext.identity.account?.avatarStatic
avatarImageView.sd_setImage(with: avatarURL)
avatarImageView.sd_setImage(with: avatarURL?.url)
if let displayName = viewModel.identityContext.identity.account?.displayName,
!displayName.isEmpty {