From c7a7740bfe32923ecdd263148ffea1af1e4ae854 Mon Sep 17 00:00:00 2001 From: x3rt Date: Thu, 28 Aug 2025 16:25:26 -0600 Subject: [PATCH] fix: image uploader setting import (#6387) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 2 +- src/util/ImageUploader.cpp | 71 +++++++++++++++++-- .../ImageUploader/Export/headers.json | 5 +- .../ImageUploader/Import/delete-url.json | 27 +++++++ .../ImageUploader/Import/headers.json | 2 +- .../ImageUploader/Import/jsonpath.json | 27 +++++++ .../Import/piped-jsonpath-bad.json | 27 +++++++ .../ImageUploader/Import/piped-jsonpath.json | 27 +++++++ 8 files changed, 178 insertions(+), 10 deletions(-) create mode 100644 tests/snapshots/ImageUploader/Import/delete-url.json create mode 100644 tests/snapshots/ImageUploader/Import/jsonpath.json create mode 100644 tests/snapshots/ImageUploader/Import/piped-jsonpath-bad.json create mode 100644 tests/snapshots/ImageUploader/Import/piped-jsonpath.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b1205ad..31ef1efc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ - Minor: Made commands searchable using the Settings dialog search bar. (#5891) - Minor: Added hotkey Action for opening account selector. (#6192) - Minor: Add a setting to change the emote and badge thumbnail size. (#6126) -- Minor: Add an import and export button to image uploader settings. (#6284, #6394) +- Minor: Add an import and export button to image uploader settings. (#6284, #6394, #6387) - Minor: Message character length label is now monospaced. (#6366) - Minor: Add a setting under Moderation -> Logs to customize the timestamp used for chat logs. (#6338) - Minor: Add a setting under Moderation -> Logs to use server timestamp from the message instead of the local clock time for logging. (#6346) diff --git a/src/util/ImageUploader.cpp b/src/util/ImageUploader.cpp index 9020ecef..c9b26826 100644 --- a/src/util/ImageUploader.cpp +++ b/src/util/ImageUploader.cpp @@ -4,11 +4,68 @@ #include #include +#include namespace chatterino::imageuploader::detail { namespace { +/** + * @brief Converts ShareX-style tokens throughout the URL to the expected format. + * + * Examples: + * - https://example.com/{json:foo.bar[1]}/{json:foo.bar[2]} + * -> https://example.com/{foo.bar.1}/{foo.bar.2} + * - {json:{response}|foo.bar[1]} + * -> {foo.bar.1} + * + * @see https://github.com/ShareX/ShareX/blob/8c0fac3bdf6c6ecb9756584096332175f290072c/ShareX.UploadersLib/CustomUploader/ShareXSyntaxParser.cs + * @see https://github.com/ShareX/ShareX/blob/8c0fac3bdf6c6ecb9756584096332175f290072c/ShareX.UploadersLib/CustomUploader/Functions/CustomUploaderFunctionJson.cs + */ +QString parseUrl(const QString &url) +{ + if (url.isEmpty() || url.compare("{response}", Qt::CaseInsensitive) == 0) + { + return {}; + } + + static const QRegularExpression tokenRegex( + R"(\{json:([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}*)", + QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression arrayRegex(R"(\[(\d+)\])"); + + QString out; + qsizetype last = 0; + bool changed = false; + + auto it = tokenRegex.globalMatch(url); + while (it.hasNext()) + { + auto match = it.next(); + + out += url.mid(last, match.capturedStart() - last); + + QString inner = match.captured(1).trimmed(); + + // Split by last '|' to support {json:input|jsonPath} + // We assume that the preceding parameter was `{response}` as other ShareX functions are not supported + qsizetype pipeIndex = inner.lastIndexOf('|'); + if (pipeIndex != -1) + { + inner = inner.mid(pipeIndex + 1).trimmed(); + } + + inner.replace(arrayRegex, R"(.\1)"); + + out += '{' + inner + '}'; + last = match.capturedEnd(); + changed = true; + } + + out += url.mid(last); + return changed ? out : QString(url); +} + QStringList parseHeaders(const QJsonObject &headersObj) { QStringList headerLines; @@ -41,7 +98,7 @@ QJsonObject exportSettings(const Settings &s) if (!headers.isEmpty()) { QJsonObject headersObj; - QStringList headerLines = headers.split('\n', Qt::SkipEmptyParts); + QStringList headerLines = headers.split(';', Qt::SkipEmptyParts); for (const QString &line : headerLines) { QStringList parts = line.split(':', Qt::SkipEmptyParts); @@ -74,12 +131,13 @@ bool importSettings(const QJsonObject &settingsObj, Settings &s) s.imageUploaderUrl = settingsObj["RequestURL"].toString(); s.imageUploaderFormField = settingsObj["FileFormName"].toString(); - s.imageUploaderLink = settingsObj["URL"].toString(); + s.imageUploaderLink = parseUrl(settingsObj["URL"].toString()); if (settingsObj.contains("DeletionURL") && settingsObj["DeletionURL"].isString()) { - s.imageUploaderDeletionLink = settingsObj["DeletionURL"].toString(); + s.imageUploaderDeletionLink = + parseUrl(settingsObj["DeletionURL"].toString()); } if (settingsObj.contains("Headers") && settingsObj["Headers"].isObject()) @@ -87,7 +145,7 @@ bool importSettings(const QJsonObject &settingsObj, Settings &s) QStringList headers = parseHeaders(settingsObj["Headers"].toObject()); if (!headers.isEmpty()) { - s.imageUploaderHeaders = headers.join('\n'); + s.imageUploaderHeaders = headers.join(';'); } } @@ -124,9 +182,10 @@ ExpectedStr validateImportJson(const QString &clipboardText) return nonstd::make_unexpected("JSON must contain the 'Version' key"); } - if (!settingsObj.contains("Name")) + if (!settingsObj.contains("RequestURL")) { - return nonstd::make_unexpected("JSON must contain the 'Name' key"); + return nonstd::make_unexpected( + "JSON must contain the 'RequestURL' key"); } return settingsObj; diff --git a/tests/snapshots/ImageUploader/Export/headers.json b/tests/snapshots/ImageUploader/Export/headers.json index c8a70468..0ab08b5b 100644 --- a/tests/snapshots/ImageUploader/Export/headers.json +++ b/tests/snapshots/ImageUploader/Export/headers.json @@ -11,8 +11,9 @@ "DeletionURL": "{more}", "FileFormName": "file", "Headers": { - "My-Header": "Foo ; Bar : Baz ; KeyOnly", - "X-My-Header": "1" + "Bar": "Baz", + "KeyOnly\nX-My-Header": "1", + "My-Header": "Foo" }, "Name": "Chatterino Image Uploader Settings", "RequestMethod": "POST", diff --git a/tests/snapshots/ImageUploader/Import/delete-url.json b/tests/snapshots/ImageUploader/Import/delete-url.json new file mode 100644 index 00000000..58779eca --- /dev/null +++ b/tests/snapshots/ImageUploader/Import/delete-url.json @@ -0,0 +1,27 @@ +{ + "input": { + "Body": "MultipartFormData", + "DeletionURL": "{json:delete}", + "DestinationType": "ImageUploader, FileUploader", + "ErrorMessage": "{json:message}", + "FileFormName": "file", + "Headers": { + "X-bing": "bong", + "X-foo": "bar" + }, + "Parameters": { + "urlfoo": "urlbar" + }, + "RequestMethod": "POST", + "RequestURL": "https://kappa.lol/api/upload", + "URL": "{json:link}", + "Version": "14.0.1" + }, + "output": { + "deletionLink": "{delete}", + "formField": "file", + "headers": "X-bing: bong;X-foo: bar", + "link": "{link}", + "url": "https://kappa.lol/api/upload" + } +} diff --git a/tests/snapshots/ImageUploader/Import/headers.json b/tests/snapshots/ImageUploader/Import/headers.json index b2f9b537..ca4ef2a6 100644 --- a/tests/snapshots/ImageUploader/Import/headers.json +++ b/tests/snapshots/ImageUploader/Import/headers.json @@ -17,7 +17,7 @@ "output": { "deletionLink": "{more}", "formField": "file", - "headers": "Another: header\nMy-Header: Foo ; Bar : Baz ; KeyOnly\nX-My-Header: 1", + "headers": "Another: header;My-Header: Foo ; Bar : Baz ; KeyOnly;X-My-Header: 1", "link": "foo{bar}baz", "url": "https://example.com" } diff --git a/tests/snapshots/ImageUploader/Import/jsonpath.json b/tests/snapshots/ImageUploader/Import/jsonpath.json new file mode 100644 index 00000000..dfb9bd67 --- /dev/null +++ b/tests/snapshots/ImageUploader/Import/jsonpath.json @@ -0,0 +1,27 @@ +{ + "input": { + "Body": "MultipartFormData", + "DeletionURL": "{json:delete}", + "DestinationType": "ImageUploader, FileUploader", + "ErrorMessage": "{json:message}", + "FileFormName": "file", + "Headers": { + "X-bing": "bong", + "X-foo": "bar" + }, + "Parameters": { + "urlfoo": "urlbar" + }, + "RequestMethod": "POST", + "RequestURL": "https://kappa.lol/api/upload", + "URL": "{json:files[0].url}", + "Version": "14.0.1" + }, + "output": { + "deletionLink": "{delete}", + "formField": "file", + "headers": "X-bing: bong;X-foo: bar", + "link": "{files.0.url}", + "url": "https://kappa.lol/api/upload" + } +} diff --git a/tests/snapshots/ImageUploader/Import/piped-jsonpath-bad.json b/tests/snapshots/ImageUploader/Import/piped-jsonpath-bad.json new file mode 100644 index 00000000..8fa31b00 --- /dev/null +++ b/tests/snapshots/ImageUploader/Import/piped-jsonpath-bad.json @@ -0,0 +1,27 @@ +{ + "input": { + "Body": "MultipartFormData", + "DeletionURL": "{json:delete}", + "DestinationType": "ImageUploader, FileUploader", + "ErrorMessage": "{json:message}", + "FileFormName": "file", + "Headers": { + "X-bing": "bong", + "X-foo": "bar" + }, + "Parameters": { + "urlfoo": "urlbar" + }, + "RequestMethod": "POST", + "RequestURL": "https://kappa.lol/api/upload", + "URL": "{json:response|}", + "Version": "14.0.1" + }, + "output": { + "deletionLink": "{delete}", + "formField": "file", + "headers": "X-bing: bong;X-foo: bar", + "link": "{}", + "url": "https://kappa.lol/api/upload" + } +} diff --git a/tests/snapshots/ImageUploader/Import/piped-jsonpath.json b/tests/snapshots/ImageUploader/Import/piped-jsonpath.json new file mode 100644 index 00000000..7d110854 --- /dev/null +++ b/tests/snapshots/ImageUploader/Import/piped-jsonpath.json @@ -0,0 +1,27 @@ +{ + "input": { + "Body": "MultipartFormData", + "DeletionURL": "{json:delete}", + "DestinationType": "ImageUploader, FileUploader", + "ErrorMessage": "{json:message}", + "FileFormName": "file", + "Headers": { + "X-bing": "bong", + "X-foo": "bar" + }, + "Parameters": { + "urlfoo": "urlbar" + }, + "RequestMethod": "POST", + "RequestURL": "https://kappa.lol/api/upload", + "URL": "{json:response|files[0].url}", + "Version": "14.0.1" + }, + "output": { + "deletionLink": "{delete}", + "formField": "file", + "headers": "X-bing: bong;X-foo: bar", + "link": "{files.0.url}", + "url": "https://kappa.lol/api/upload" + } +}