From 5a58d8c39ccb76290fa242e98e51e003ebdd4165 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Thu, 16 Mar 2023 21:43:16 +0100 Subject: [PATCH] Harden and improve "Download Mod from Url" parser (#8924) --- .../jsons/translations/template.properties | 3 +- .../unciv/ui/screens/pickerscreens/GitHub.kt | 58 ++++++++++++------- .../pickerscreens/ModManagementScreen.kt | 11 +++- 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 21de6cb3e5..2e86e48232 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -1642,7 +1642,8 @@ Download [modName] = Update [modName] = Could not download mod list = Download mod from URL = -Please enter the mod repository -or- archive zip url: = +Please enter the mod repository -or- archive zip -or- branch url: = +Invalid link! = Paste from clipboard = Download = Done! = diff --git a/core/src/com/unciv/ui/screens/pickerscreens/GitHub.kt b/core/src/com/unciv/ui/screens/pickerscreens/GitHub.kt index 1ed814bc6d..f6ad117834 100644 --- a/core/src/com/unciv/ui/screens/pickerscreens/GitHub.kt +++ b/core/src/com/unciv/ui/screens/pickerscreens/GitHub.kt @@ -323,36 +323,50 @@ object Github { //var has_wiki = false // a wiki could mean proper documentation for the mod? /** - * Initialize `this` with an url, extracting all possible fields from it. + * Initialize `this` with an url, extracting all possible fields from it + * (html_url, author, repoName, branchName). * - * Allows basic repo url or complete 'zip' url from github's code->download zip menu + * Allow url formats: + * * Basic repo url: + * https://github.com/author/repoName + * * or complete 'zip' url from github's code->download zip menu: + * https://github.com/author/repoName/archive/refs/heads/branchName.zip + * * or the branch url same as one navigates to on github through the "branches" menu: + * https://github.com/author/repoName/tree/branchName * - * @return `this` to allow chaining + * In the case of the basic repo url, an API query is sent to determine the default branch. + * Other url forms will not go online. + * + * @return `this` to allow chaining, `null` for invalid links or any other failures */ - fun parseUrl(url: String): Repo { - // Allow url formats - // https://github.com/author/repoName - // or - // https://github.com/author/repoName/archive/refs/heads/branchName.zip - // and extract author, repoName, branchName + fun parseUrl(url: String): Repo? { + fun processMatch(matchResult: MatchResult): Repo { + html_url = matchResult.groups[1]!!.value + owner.login = matchResult.groups[2]!!.value + name = matchResult.groups[3]!!.value + default_branch = matchResult.groups[4]!!.value + return this + } html_url = url default_branch = "master" val matchZip = Regex("""^(.*/(.*)/(.*))/archive/(?:.*/)?([^.]+).zip$""").matchEntire(url) - if (matchZip != null && matchZip.groups.size > 3) { - html_url = matchZip.groups[1]!!.value - owner.login = matchZip.groups[2]!!.value - name = matchZip.groups[3]!!.value - default_branch = matchZip.groups[4]!!.value - } else { - val matchRepo = Regex("""^.*/(.*)/(.*)/?$""").matchEntire(url) - if (matchRepo != null && matchRepo.groups.size > 2) { - val repoString = download("https://api.github.com/repos/${matchRepo.groups[1]!!.value}/${matchRepo.groups[2]!!.value}")!! - .bufferedReader().readText() - return json().fromJson(Repo::class.java, repoString) - } + if (matchZip != null && matchZip.groups.size > 4) + return processMatch(matchZip) + + val matchBranch = Regex("""^(.*/(.*)/(.*))/tree/([^/]+)$""").matchEntire(url) + if (matchBranch != null && matchBranch.groups.size > 4) + return processMatch(matchBranch) + + val matchRepo = Regex("""^.*//.*/(.+)/(.+)/?$""").matchEntire(url) + if (matchRepo != null && matchRepo.groups.size > 2) { + // Query API if we got the first URL format to get the correct default branch + val response = download("https://api.github.com/repos/${matchRepo.groups[1]!!.value}/${matchRepo.groups[2]!!.value}") + ?: return null + val repoString = response.bufferedReader().readText() + return json().fromJson(Repo::class.java, repoString) } - return this + return null } } diff --git a/core/src/com/unciv/ui/screens/pickerscreens/ModManagementScreen.kt b/core/src/com/unciv/ui/screens/pickerscreens/ModManagementScreen.kt index 90a0106b09..d1fc4b9210 100644 --- a/core/src/com/unciv/ui/screens/pickerscreens/ModManagementScreen.kt +++ b/core/src/com/unciv/ui/screens/pickerscreens/ModManagementScreen.kt @@ -400,8 +400,8 @@ class ModManagementScreen( val downloadButton = "Download mod from URL".toTextButton() downloadButton.onClick { val popup = Popup(this) - popup.addGoodSizedLabel("Please enter the mod repository -or- archive zip url:").row() - val textField = UncivTextField.create("") + popup.addGoodSizedLabel("Please enter the mod repository -or- archive zip -or- branch url:").row() + val textField = UncivTextField.create("").apply { maxLength = 666 } popup.add(textField).width(stage.width / 2).row() val pasteLinkButton = "Paste from clipboard".toTextButton() pasteLinkButton.onClick { @@ -412,7 +412,12 @@ class ModManagementScreen( actualDownloadButton.onClick { actualDownloadButton.setText("Downloading...".tr()) actualDownloadButton.disable() - downloadMod(Github.Repo().parseUrl(textField.text)) { popup.close() } + val repo = Github.Repo().parseUrl(textField.text) + if (repo == null) { + ToastPopup("Invalid link!", this@ModManagementScreen) + popup.close() // Re-enabling button would be nice, but Toast doesn't work over other Popups + } else + downloadMod(repo) { popup.close() } } popup.add(actualDownloadButton).row() popup.addCloseButton()