@@ -114,6 +116,7 @@ import { copyLink, downloadTool, openLink } from "./utilities";
import FormMessage from "components/Form/FormMessage";
import ToolFooter from "components/Tool/ToolFooter";
import ToolHelp from "components/Tool/ToolHelp";
+import ToolSourceMenuItem from "components/Tool/ToolSourceMenuItem";
import Webhooks from "mvc/webhooks";
import { addFavorite, removeFavorite } from "components/Tool/services";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
@@ -128,6 +131,7 @@ export default {
FormMessage,
ToolFooter,
ToolHelp,
+ ToolSourceMenuItem,
},
props: {
id: {
diff --git a/client/src/components/Tool/ToolSource.vue b/client/src/components/Tool/ToolSource.vue
new file mode 100644
index 000000000000..13c7e52348ba
--- /dev/null
+++ b/client/src/components/Tool/ToolSource.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
diff --git a/client/src/components/Tool/ToolSourceMenuItem.vue b/client/src/components/Tool/ToolSourceMenuItem.vue
new file mode 100644
index 000000000000..fef9964be07f
--- /dev/null
+++ b/client/src/components/Tool/ToolSourceMenuItem.vue
@@ -0,0 +1,39 @@
+
+
+
+
+ View Tool source
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/providers/SingleQueryProvider.js b/client/src/components/providers/SingleQueryProvider.js
index 76ec0cbb41d1..98b33727d57c 100644
--- a/client/src/components/providers/SingleQueryProvider.js
+++ b/client/src/components/providers/SingleQueryProvider.js
@@ -2,9 +2,9 @@ import hash from "object-hash";
/**
* Builds a provider that gets its result from a single promise-based query function and
- * caches the result of lookup for subsequent instantitations.
+ * caches the result of lookup for subsequent instantiations.
*
- * @param {Function} lookup async function that loads the result, paramters will be an object
+ * @param {Function} lookup async function that loads the result, parameters will be an object
* whose properties are the attributes assigned to the provider component
* @return {VueComponentOptions} Vue component options definition
*/
diff --git a/client/src/components/providers/ToolSourceProvider.js b/client/src/components/providers/ToolSourceProvider.js
new file mode 100644
index 000000000000..c3861c91fedf
--- /dev/null
+++ b/client/src/components/providers/ToolSourceProvider.js
@@ -0,0 +1,24 @@
+import axios from "axios";
+import { getAppRoot } from "onload/loadConfig";
+import { SingleQueryProvider } from "components/providers/SingleQueryProvider";
+import { rethrowSimple } from "utils/simple-error";
+import beautify from "xml-beautifier";
+
+async function toolSource({ id }) {
+ const url = `${getAppRoot()}api/tools/${id}/raw_tool_source`;
+ try {
+ const { data, headers } = await axios.get(url);
+ const result = {};
+ result.language = headers.language;
+ if (headers.language === "xml") {
+ result.source = beautify(data);
+ } else {
+ result.source = data;
+ }
+ return result;
+ } catch (e) {
+ rethrowSimple(e);
+ }
+}
+
+export const ToolSourceProvider = SingleQueryProvider(toolSource);
diff --git a/client/yarn.lock b/client/yarn.lock
index 9d62093ed6dc..7673892f8047 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -2729,6 +2729,14 @@ babel-preset-jest@^27.2.0:
babel-plugin-jest-hoist "^27.2.0"
babel-preset-current-node-syntax "^1.0.0"
+babel-runtime@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+ integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
+ dependencies:
+ core-js "^2.4.0"
+ regenerator-runtime "^0.11.0"
+
babel-walk@3.0.0-canary-5:
version "3.0.0-canary-5"
resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11"
@@ -3831,6 +3839,11 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.18.3.tgz#7eed77dcce1445ab68fd68715856633e2fb3b90c"
integrity sha512-qfskyO/KjtbYn09bn1IPkuhHl5PlJ6IzJ9s9sraJ1EqcuGyLGKzhSM1cY0zgyL9hx42eulQLZ6WaeK5ycJCkqw==
+core-js@^2.4.0:
+ version "2.6.12"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
+ integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
+
core-js@^3.19.1:
version "3.19.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.19.1.tgz#f6f173cae23e73a7d88fa23b6e9da329276c6641"
@@ -10070,7 +10083,7 @@ pretty@^2.0.0:
extend-shallow "^2.0.1"
js-beautify "^1.6.12"
-prismjs@^1.17.1, prismjs@^1.23.0:
+prismjs@^1.17.1, prismjs@^1.23.0, prismjs@^1.6.0:
version "1.25.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756"
integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==
@@ -10736,6 +10749,11 @@ regenerate@^1.4.0, regenerate@^1.4.2:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
+regenerator-runtime@^0.11.0:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+ integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
+
regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7, regenerator-runtime@^0.13.9:
version "0.13.9"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
@@ -10867,7 +10885,7 @@ repeat-element@^1.1.2:
resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9"
integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==
-repeat-string@^1.0.0, repeat-string@^1.6.1:
+repeat-string@1.6.1, repeat-string@^1.0.0, repeat-string@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
@@ -12933,6 +12951,13 @@ vue-observe-visibility@^1.0.0:
resolved "https://registry.yarnpkg.com/vue-observe-visibility/-/vue-observe-visibility-1.0.0.tgz#17cf1b2caf74022f0f3c95371468ddf2b9573152"
integrity sha512-s5TFh3s3h3Mhd3jaz3zGzkVHKHnc/0C/gNr30olO99+yw2hl3WBhK3ng3/f9OF+qkW4+l7GkmwfAzDAcY3lCFg==
+vue-prismjs@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/vue-prismjs/-/vue-prismjs-1.2.0.tgz#b137f5ed958685ce1fd55ca6b068289f3359b8b7"
+ integrity sha512-1mICbMknMiKw4mfM1AU7vkFT3VX4Cq8ySyDU7IBr9tYW1fzxIPkfLoERkRMbQLBc+J74K3FIiuuZ+WfBjO3SCQ==
+ dependencies:
+ prismjs "^1.6.0"
+
vue-router@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.5.2.tgz#5f55e3f251970e36c3e8d88a7cd2d67a350ade5c"
@@ -13544,6 +13569,13 @@ ws@~7.4.2:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
+xml-beautifier@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/xml-beautifier/-/xml-beautifier-0.5.0.tgz#476fd7118b972c2ec5863babca6812934d34bb56"
+ integrity sha512-QG/qiHeolHUd1tAtM+5zHxTzDprb8qvhmIYUYV1E9QK/jTFlrAa1Mz7QQqJPeqc3uuFAGzTOhjvbdx2hOP6bHw==
+ dependencies:
+ repeat-string "1.6.1"
+
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
diff --git a/doc/source/admin/galaxy_options.rst b/doc/source/admin/galaxy_options.rst
index 105f905c112e..2347d8b882ba 100644
--- a/doc/source/admin/galaxy_options.rst
+++ b/doc/source/admin/galaxy_options.rst
@@ -3908,6 +3908,20 @@
:Type: bool
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+``enable_tool_source_display``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+:Description:
+ This option allows users to view the tool wrapper source code.
+ This is safe to enable if you have not hardcoded any secrets in
+ any of the tool wrappers installed on this Galaxy server. If you
+ have only installed tool wrappers from public tool sheds and
+ tools shipped with Galaxy there you can enable this option.
+:Default: ``false``
+:Type: bool
+
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
``job_metrics_config_file``
~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/lib/galaxy/config/sample/galaxy.yml.sample b/lib/galaxy/config/sample/galaxy.yml.sample
index 83fe7890909b..7d62df5f30f0 100644
--- a/lib/galaxy/config/sample/galaxy.yml.sample
+++ b/lib/galaxy/config/sample/galaxy.yml.sample
@@ -1912,6 +1912,13 @@ galaxy:
# see dataset paths.
#expose_dataset_path: false
+ # This option allows users to view the tool wrapper source code. This
+ # is safe to enable if you have not hardcoded any secrets in any of
+ # the tool wrappers installed on this Galaxy server. If you have only
+ # installed tool wrappers from public tool sheds and tools shipped
+ # with Galaxy there you can enable this option.
+ #enable_tool_source_display: false
+
# XML config file that contains the job metric collection
# configuration.
# The value of this option will be resolved with respect to
diff --git a/lib/galaxy/managers/configuration.py b/lib/galaxy/managers/configuration.py
index 27427d896069..0134e4498f6a 100644
--- a/lib/galaxy/managers/configuration.py
+++ b/lib/galaxy/managers/configuration.py
@@ -208,6 +208,7 @@ def _config_is_truthy(item, key, **context):
'upload_from_form_button': _use_config,
'release_doc_base_url': _use_config,
'expose_user_email': _use_config,
+ 'enable_tool_source_display': _use_config,
'user_library_import_dir_available': lambda item, key, **context: bool(item.get('user_library_import_dir')),
'welcome_directory': _use_config,
}
diff --git a/lib/galaxy/tool_util/parser/cwl.py b/lib/galaxy/tool_util/parser/cwl.py
index 0ebb4a48261c..59f1036072ed 100644
--- a/lib/galaxy/tool_util/parser/cwl.py
+++ b/lib/galaxy/tool_util/parser/cwl.py
@@ -21,6 +21,8 @@
class CwlToolSource(ToolSource):
+ language = 'yaml'
+
def __init__(self, tool_file, strict_cwl_validation=True):
self._cwl_tool_file = tool_file
self._id, _ = os.path.splitext(os.path.basename(tool_file))
diff --git a/lib/galaxy/tool_util/parser/interface.py b/lib/galaxy/tool_util/parser/interface.py
index b82154bdc960..a4f8c6ea9b56 100644
--- a/lib/galaxy/tool_util/parser/interface.py
+++ b/lib/galaxy/tool_util/parser/interface.py
@@ -19,6 +19,7 @@ class ToolSource(metaclass=ABCMeta):
information from.
"""
default_is_multi_byte = False
+ language: str
@abstractmethod
def parse_id(self):
diff --git a/lib/galaxy/tool_util/parser/xml.py b/lib/galaxy/tool_util/parser/xml.py
index dbcf380e763d..ff16177864ae 100644
--- a/lib/galaxy/tool_util/parser/xml.py
+++ b/lib/galaxy/tool_util/parser/xml.py
@@ -50,6 +50,8 @@ class XmlToolSource(ToolSource):
""" Responsible for parsing a tool from classic Galaxy representation.
"""
+ language = 'xml'
+
def __init__(self, xml_tree, source_path=None, macro_paths=None):
self.xml_tree = xml_tree
self.root = xml_tree.getroot()
diff --git a/lib/galaxy/tool_util/parser/yaml.py b/lib/galaxy/tool_util/parser/yaml.py
index 7499cab21cbe..b0f76cf9a261 100644
--- a/lib/galaxy/tool_util/parser/yaml.py
+++ b/lib/galaxy/tool_util/parser/yaml.py
@@ -23,6 +23,8 @@
class YamlToolSource(ToolSource):
+ language = 'yaml'
+
def __init__(self, root_dict, source_path=None):
self.root_dict = root_dict
self._source_path = source_path
diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py
index 4fbc0294f79d..f26eebe6b3f7 100644
--- a/lib/galaxy/webapps/galaxy/api/tools.py
+++ b/lib/galaxy/webapps/galaxy/api/tools.py
@@ -18,6 +18,7 @@
expose_api_anonymous_and_sessionless,
expose_api_raw_anonymous_and_sessionless,
)
+from galaxy.web.framework.decorators import expose_api_raw
from galaxy.webapps.base.controller import UsesVisualizationMixin
from galaxy.webapps.base.webapp import GalaxyWebTransaction
from . import BaseGalaxyAPIController, depends
@@ -490,6 +491,15 @@ def download(self, trans: GalaxyWebTransaction, id, **kwds):
trans.response.headers["Content-Disposition"] = f'attachment; filename="{id}.tgz"'
return download_file
+ @expose_api_raw
+ def raw_tool_source(self, trans: GalaxyWebTransaction, id, **kwds):
+ """Returns tool source. ``language`` is included in the response header."""
+ if not trans.app.config.enable_tool_source_display and not trans.user_is_admin:
+ raise exceptions.InsufficientPermissionsException("Only administrators may display tool sources on this Galaxy server.")
+ tool = self._get_tool(id, user=trans.user, tool_version=kwds.get('tool_version'))
+ trans.response.headers['language'] = tool.tool_source.language
+ return tool.tool_source.to_string()
+
@expose_api_anonymous
def fetch(self, trans: GalaxyWebTransaction, payload, **kwd):
"""Adapt clean API to tool-constrained API.
diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py
index 79fba83aa888..5e5cb7006884 100644
--- a/lib/galaxy/webapps/galaxy/buildapp.py
+++ b/lib/galaxy/webapps/galaxy/buildapp.py
@@ -434,6 +434,7 @@ def populate_api_routes(webapp, app):
webapp.mapper.connect('/api/tools/{tool_id:.+?}/convert', action='conversion', controller="tools", conditions=dict(method=["POST"]))
webapp.mapper.connect('/api/tools/{id:.+?}/xrefs', action='xrefs', controller="tools")
webapp.mapper.connect('/api/tools/{id:.+?}/download', action='download', controller="tools")
+ webapp.mapper.connect('/api/tools/{id:.+?}/raw_tool_source', action='raw_tool_source', controller="tools")
webapp.mapper.connect('/api/tools/{id:.+?}/requirements', action='requirements', controller="tools")
webapp.mapper.connect('/api/tools/{id:.+?}/install_dependencies', action='install_dependencies', controller="tools", conditions=dict(method=["POST"]))
webapp.mapper.connect('/api/tools/{id:.+?}/dependencies', action='install_dependencies', controller="tools", conditions=dict(method=["POST"]))
diff --git a/lib/galaxy/webapps/galaxy/config_schema.yml b/lib/galaxy/webapps/galaxy/config_schema.yml
index 2292290f9cea..1913950b43aa 100644
--- a/lib/galaxy/webapps/galaxy/config_schema.yml
+++ b/lib/galaxy/webapps/galaxy/config_schema.yml
@@ -2821,6 +2821,17 @@ mapping:
Details" option in the history. This option also exposes the command line to
non-administrative users. Administrators can always see dataset paths.
+ enable_tool_source_display:
+ type: bool
+ default: false
+ required: false
+ desc: |
+ This option allows users to view the tool wrapper source code. This is
+ safe to enable if you have not hardcoded any secrets in any of the tool
+ wrappers installed on this Galaxy server. If you have only installed tool
+ wrappers from public tool sheds and tools shipped with Galaxy there you
+ can enable this option.
+
job_metrics_config_file:
type: str
default: job_metrics_conf.xml
diff --git a/lib/galaxy_test/api/test_tools.py b/lib/galaxy_test/api/test_tools.py
index 85d7b91c0210..7454db58f546 100644
--- a/lib/galaxy_test/api/test_tools.py
+++ b/lib/galaxy_test/api/test_tools.py
@@ -1195,6 +1195,17 @@ def test_show_dynamic_tools(self):
assert found_id
assert found_uuid
+ def test_show_tool_source_admin(self):
+ response = self._get("tools/cat1/raw_tool_source", admin=True)
+ response.raise_for_status()
+ assert "Concatenate datasets" in response.text
+ assert response.headers["language"] == 'xml'
+
+ def test_show_tool_source_denied(self):
+ with self._different_user(anon=True):
+ response = self._get("tools/cat1/raw_tool_source")
+ assert response.status_code == 403
+
def test_tool_deactivate(self):
# Create tool.
tool_response = self.dataset_populator.create_tool(MINIMAL_TOOL_NO_ID)