Skip to content
Merged
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
12 changes: 6 additions & 6 deletions Flowlight.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2196,7 +2196,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 60;
CURRENT_PROJECT_VERSION = 61;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -2211,7 +2211,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.15.13;
MARKETING_VERSION = 0.15.14;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -2257,7 +2257,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 60;
CURRENT_PROJECT_VERSION = 61;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 38RJUJHKZS;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -2272,7 +2272,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.15.13;
MARKETING_VERSION = 0.15.14;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -2337,7 +2337,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 60;
CURRENT_PROJECT_VERSION = 61;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -2358,7 +2358,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.15.13;
MARKETING_VERSION = 0.15.14;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
Expand Down
6 changes: 6 additions & 0 deletions Flowlight/App/DemoData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import Foundation
enum DemoData {
static var isEnabled: Bool { UserDefaults.standard.bool(forKey: "FLDemo") }
static var showsPackReview: Bool { UserDefaults.standard.bool(forKey: "FLDemoPackReview") }
static var pluginReviewURL: URL? {
guard isEnabled,
let path = UserDefaults.standard.string(forKey: "FLDemoPluginReview"),
!path.isEmpty else { return nil }
return URL(fileURLWithPath: path)
}

static var databaseURL: URL {
let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].appendingPathComponent("Flowlight")
Expand Down
2 changes: 2 additions & 0 deletions Flowlight/Localization/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1691,6 +1691,8 @@
"Channel" = "Channel";
"Package digest" = "Package digest";
"Script digest" = "Script digest";
"JavaScript source" = "JavaScript-Quelltext";
"Plugin scripts cannot contain invisible Unicode formatting controls." = "Plugin-Skripte dürfen keine unsichtbaren Unicode-Formatierungszeichen enthalten.";
"Permissions" = "Permissions";
"Install" = "Install";
"Not declared" = "Not declared";
Expand Down
2 changes: 2 additions & 0 deletions Flowlight/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1690,6 +1690,8 @@
"Channel" = "Channel";
"Package digest" = "Package digest";
"Script digest" = "Script digest";
"JavaScript source" = "JavaScript source";
"Plugin scripts cannot contain invisible Unicode formatting controls." = "Plugin scripts cannot contain invisible Unicode formatting controls.";
"Permissions" = "Permissions";
"Install" = "Install";
"Not declared" = "Not declared";
Expand Down
2 changes: 2 additions & 0 deletions Flowlight/Localization/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1691,6 +1691,8 @@
"Channel" = "Channel";
"Package digest" = "Package digest";
"Script digest" = "Script digest";
"JavaScript source" = "Código fuente JavaScript";
"Plugin scripts cannot contain invisible Unicode formatting controls." = "Los scripts de plugins no pueden contener controles de formato Unicode invisibles.";
"Permissions" = "Permissions";
"Install" = "Install";
"Not declared" = "Not declared";
Expand Down
2 changes: 2 additions & 0 deletions Flowlight/Localization/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1691,6 +1691,8 @@
"Channel" = "Channel";
"Package digest" = "Package digest";
"Script digest" = "Script digest";
"JavaScript source" = "Code source JavaScript";
"Plugin scripts cannot contain invisible Unicode formatting controls." = "Les scripts de plugins ne peuvent pas contenir de contrôles de formatage Unicode invisibles.";
"Permissions" = "Permissions";
"Install" = "Install";
"Not declared" = "Not declared";
Expand Down
2 changes: 2 additions & 0 deletions Flowlight/Localization/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1691,6 +1691,8 @@
"Channel" = "Channel";
"Package digest" = "Package digest";
"Script digest" = "Script digest";
"JavaScript source" = "Codice sorgente JavaScript";
"Plugin scripts cannot contain invisible Unicode formatting controls." = "Gli script dei plugin non possono contenere controlli di formattazione Unicode invisibili.";
"Permissions" = "Permissions";
"Install" = "Install";
"Not declared" = "Not declared";
Expand Down
2 changes: 2 additions & 0 deletions Flowlight/Localization/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1691,6 +1691,8 @@
"Channel" = "Channel";
"Package digest" = "Package digest";
"Script digest" = "Script digest";
"JavaScript source" = "JavaScript ソースコード";
"Plugin scripts cannot contain invisible Unicode formatting controls." = "プラグインスクリプトに不可視の Unicode 書式制御文字を含めることはできません。";
"Permissions" = "Permissions";
"Install" = "Install";
"Not declared" = "Not declared";
Expand Down
2 changes: 2 additions & 0 deletions Flowlight/Localization/ko.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1690,6 +1690,8 @@
"Channel" = "Channel";
"Package digest" = "Package digest";
"Script digest" = "Script digest";
"JavaScript source" = "JavaScript 소스 코드";
"Plugin scripts cannot contain invisible Unicode formatting controls." = "플러그인 스크립트에는 보이지 않는 유니코드 서식 제어 문자를 포함할 수 없습니다.";
"Permissions" = "Permissions";
"Install" = "Install";
"Not declared" = "Not declared";
Expand Down
2 changes: 2 additions & 0 deletions Flowlight/Localization/pt-PT.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1690,6 +1690,8 @@
"Channel" = "Channel";
"Package digest" = "Package digest";
"Script digest" = "Script digest";
"JavaScript source" = "Código-fonte JavaScript";
"Plugin scripts cannot contain invisible Unicode formatting controls." = "Os scripts de plugins não podem conter controlos de formatação Unicode invisíveis.";
"Permissions" = "Permissions";
"Install" = "Install";
"Not declared" = "Not declared";
Expand Down
2 changes: 2 additions & 0 deletions Flowlight/Localization/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1690,6 +1690,8 @@
"Channel" = "Channel";
"Package digest" = "待翻译:Package digest";
"Script digest" = "待翻译:Script digest";
"JavaScript source" = "JavaScript 源代码";
"Plugin scripts cannot contain invisible Unicode formatting controls." = "插件脚本不能包含不可见的 Unicode 格式控制字符。";
"Permissions" = "Permissions";
"Install" = "Install";
"Not declared" = "待翻译:Not declared";
Expand Down
2 changes: 2 additions & 0 deletions Flowlight/Localization/zh-Hant.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1690,6 +1690,8 @@
"Channel" = "Channel";
"Package digest" = "Package digest";
"Script digest" = "Script digest";
"JavaScript source" = "JavaScript 原始碼";
"Plugin scripts cannot contain invisible Unicode formatting controls." = "外掛程式指令碼不能包含不可見的 Unicode 格式控制字元。";
"Permissions" = "Permissions";
"Install" = "Install";
"Not declared" = "Not declared";
Expand Down
8 changes: 8 additions & 0 deletions Flowlight/Plugins/PluginPackageVerifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ enum PluginPackageError: LocalizedError, Equatable {
case reservedBuiltInID
case invalidID
case scriptTooLarge
case unsafeScriptFormatting
case unsupportedPackage
case unknownSigner(String)
case wrongPublisher
Expand All @@ -16,6 +17,7 @@ enum PluginPackageError: LocalizedError, Equatable {
case .reservedBuiltInID: return L("Installed plugin IDs cannot reuse a built-in plugin ID.")
case .invalidID: return L("Plugin IDs must contain only letters, numbers, dots, underscores, and hyphens.")
case .scriptTooLarge: return L("Plugin scripts must be 128 KB or smaller.")
case .unsafeScriptFormatting: return L("Plugin scripts cannot contain invisible Unicode formatting controls.")
case .unsupportedPackage: return L("This plugin package format is not supported.")
case .unknownSigner(let key): return L("The plugin package was signed by an unknown key: %@.", key)
case .wrongPublisher: return L("The plugin package signature does not match the declared publisher.")
Expand All @@ -42,6 +44,7 @@ enum PluginPackageVerifier {
var manifest = package.manifest
try validateBase(manifest)
manifest.source = .installed
manifest.configuration = [:]
if manifest.capabilities.isEmpty { manifest.capabilities = [.annotate] }
manifest.permissions = normalizedPermissions(manifest)
if manifest.privacy.categories.isEmpty { manifest.privacy.categories = [.metadataOnly] }
Expand Down Expand Up @@ -117,6 +120,11 @@ enum PluginPackageVerifier {
guard manifest.id.range(of: #"^[A-Za-z0-9_.-]+$"#, options: .regularExpression) != nil else { throw PluginPackageError.invalidID }
guard !PluginEngine.builtInManifests.contains(where: { $0.id == manifest.id }) else { throw PluginPackageError.reservedBuiltInID }
guard manifest.script.lengthOfBytes(using: .utf8) <= PluginScriptRunner.maxScriptBytes else { throw PluginPackageError.scriptTooLarge }
guard !manifest.script.unicodeScalars.contains(where: {
$0.properties.generalCategory == .format
}) else {
throw PluginPackageError.unsafeScriptFormatting
}
}

private static func normalizedPermissions(_ manifest: PluginManifest) -> [PluginPermission] {
Expand Down
10 changes: 10 additions & 0 deletions Flowlight/UI/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ struct ContentView: View {
@EnvironmentObject var updater: UpdateChecker
@Environment(\.openWindow) private var openWindow
@Environment(\.dismissWindow) private var dismissWindow
@State private var demoPluginReview: PluginPackageReview?

var body: some View {
NavigationSplitView {
Expand Down Expand Up @@ -99,6 +100,15 @@ struct ContentView: View {
}
}
.id(localization.revision)
.task {
guard demoPluginReview == nil, let url = DemoData.pluginReviewURL else { return }
if let data = try? Data(contentsOf: url) {
demoPluginReview = try? monitor.plugins.reviewPackage(data: data)
}
}
.sheet(item: $demoPluginReview) { review in
PluginPackageReviewSheet(review: review) { _ in demoPluginReview = nil }
}
#if !APP_STORE
.onReceive(NotificationCenter.default.publisher(for: .flowlightOpenUpdate)) { _ in openWindow(id: "update") }
#endif
Expand Down
118 changes: 80 additions & 38 deletions Flowlight/UI/PluginSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,64 +187,98 @@ private struct PluginSettingsRow: View {
}
}

private struct PluginPackageReviewSheet: View {
struct PluginPackageReviewSheet: View {
var review: PluginPackageReview
var close: (Bool) -> Void

var body: some View {
VStack(alignment: .leading, spacing: 14) {
Text(L("Review Plugin Package")).font(.headline)
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 6) {
Text(review.manifest.name).font(.callout.bold())
PluginPill(review.signatureStatus.title)
PluginPill(review.manifest.kind.title)
if !review.manifest.script.isEmpty { PluginPill(L("JavaScript")) }
}
Text(review.manifest.description).font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
Text(review.manifest.privacySummary).font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
}
Divider()
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 6) {
reviewRow(L("Plugin ID"), review.manifest.id)
reviewRow(L("Version"), review.manifest.version)
reviewRow(L("Publisher"), review.manifest.publisher.title)
reviewRow(L("Provider"), review.manifest.guardrailProvider.title)
reviewRow(L("Channel"), review.manifest.provenance.distributionChannel.title)
reviewRow(L("Package digest"), review.packageDigest.shortDigest)
reviewRow(L("Script digest"), review.scriptDigest.shortDigest)
}
GroupBox(L("Permissions")) {
FlowPillLine(items: review.manifest.permissions.map(\.title))
.frame(maxWidth: .infinity, alignment: .leading)
}
GroupBox(L("Privacy")) {
FlowPillLine(items: review.manifest.privacy.categories.map(\.title))
.frame(maxWidth: .infinity, alignment: .leading)
}
if !review.warnings.isEmpty {
VStack(alignment: .leading, spacing: 4) {
ForEach(review.warnings, id: \.self) { warning in
Text(warning).font(.caption).foregroundStyle(.orange).fixedSize(horizontal: false, vertical: true)
ScrollView {
VStack(alignment: .leading, spacing: 14) {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 6) {
Text(review.manifest.name).font(.callout.bold())
PluginPill(review.signatureStatus.title)
PluginPill(review.manifest.kind.title)
if !review.manifest.script.isEmpty { PluginPill(L("JavaScript")) }
}
Text(review.manifest.description).font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
Text(review.manifest.privacySummary).font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
}
Divider()
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 6) {
reviewRow(L("Plugin ID"), review.manifest.id)
reviewRow(L("Version"), review.manifest.version)
reviewRow(L("Publisher"), review.manifest.publisher.title)
reviewRow(L("Provider"), review.manifest.guardrailProvider.title)
reviewRow(L("Channel"), review.manifest.provenance.distributionChannel.title)
}
digestRow(L("Package digest"), review.packageDigest)
digestRow(L("Script digest"), review.scriptDigest)
GroupBox(L("Permissions")) {
FlowPillLine(items: review.manifest.permissions.map(\.title))
.frame(maxWidth: .infinity, alignment: .leading)
}
GroupBox(L("Privacy")) {
FlowPillLine(items: review.manifest.privacy.categories.map(\.title))
.frame(maxWidth: .infinity, alignment: .leading)
}
if !review.manifest.script.isEmpty {
VStack(alignment: .leading, spacing: 8) {
Text(L("JavaScript source")).font(.caption.bold()).foregroundStyle(.secondary)
ScrollView([.horizontal, .vertical]) {
Text(review.manifest.script)
.font(.caption.monospaced())
.textSelection(.enabled)
.fixedSize(horizontal: true, vertical: true)
.frame(maxWidth: .infinity, alignment: .topLeading)
}
.frame(height: 180)
.padding(8)
.background(.quaternary.opacity(0.35), in: RoundedRectangle(cornerRadius: 6))
}
}
if !review.warnings.isEmpty {
VStack(alignment: .leading, spacing: 4) {
ForEach(review.warnings, id: \.self) { warning in
Text(warning).font(.caption).foregroundStyle(.orange).fixedSize(horizontal: false, vertical: true)
}
}
}
Text(L("Plugins are advisory. Installing this package does not let it block, rewrite, mock, transform, save settings, upload, or export traffic."))
.font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
Text(L("Plugins are advisory. Installing this package does not let it block, rewrite, mock, transform, save settings, upload, or export traffic."))
.font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
HStack {
Spacer()
Button(L("Cancel")) { close(false) }
Button(L("Install")) { close(true) }.keyboardShortcut(.defaultAction)
}
}
.padding(20)
.frame(width: 620)
.frame(width: 680, height: 650)
}

private func reviewRow(_ key: String, _ value: String) -> some View {
GridRow {
Text(key).font(.caption).foregroundStyle(.secondary)
Text(value.isEmpty ? L("Not declared") : value).font(.caption).textSelection(.enabled)
Text(value.isEmpty ? L("Not declared") : value)
.font(.caption)
.textSelection(.enabled)
.fixedSize(horizontal: false, vertical: true)
}
}

private func digestRow(_ key: String, _ value: String) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text(key).font(.caption).foregroundStyle(.secondary)
Text(value.isEmpty ? L("Not declared") : value.digestGroups)
.font(.caption.monospaced())
.textSelection(.enabled)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
Expand All @@ -262,7 +296,7 @@ private struct WrappingHStack<Item: Hashable, Content: View>: View {
var content: (Item) -> Content

var body: some View {
HStack(spacing: 6) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 110), spacing: 6)], alignment: .leading, spacing: 6) {
ForEach(items, id: \.self) { item in content(item) }
}
}
Expand Down Expand Up @@ -358,4 +392,12 @@ private extension String {
guard count > 12 else { return self }
return String(prefix(12))
}

var digestGroups: String {
stride(from: 0, to: count, by: 16).map { offset in
let start = index(startIndex, offsetBy: offset)
let end = index(start, offsetBy: Swift.min(16, count - offset))
return String(self[start..<end])
}.joined(separator: " ")
}
}
Loading
Loading