From 652bf1438e5881bf4e659ef7da6549948bc1f7b4 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 2 Apr 2020 14:29:27 -0400 Subject: [PATCH] Fixed unit tests --- app/admin.py | 62 ++++++++++++++++++++++++++++++++++++-- app/plugins/functions.py | 23 ++++++++++---- app/plugins/plugin_base.py | 10 ++++++ app/plugins/views.py | 7 +++-- app/tests/test_plugins.py | 25 +++++++++++---- package.json | 2 +- plugins/test/plugin.py | 4 +-- 7 files changed, 112 insertions(+), 21 deletions(-) diff --git a/app/admin.py b/app/admin.py index 15f8fc46..95558270 100644 --- a/app/admin.py +++ b/app/admin.py @@ -1,3 +1,8 @@ +import os +import tempfile +import zipfile +import shutil + from django.conf.urls import url from django.contrib import admin from django.contrib import messages @@ -9,10 +14,13 @@ from guardian.admin import GuardedModelAdmin from app.models import PluginDatum from app.models import Preset from app.models import Plugin -from app.plugins import get_plugin_by_name, enable_plugin, disable_plugin, delete_plugin +from app.plugins import get_plugin_by_name, enable_plugin, disable_plugin, delete_plugin, valid_plugin, \ + get_plugins_persistent_path, clear_plugins_cache, init_plugins from .models import Project, Task, ImageUpload, Setting, Theme from django import forms from codemirror2.widgets import CodeMirrorEditor +from webodm import settings +from django.core.files.uploadedfile import InMemoryUploadedFile admin.site.register(Project, GuardedModelAdmin) @@ -144,8 +152,56 @@ class PluginAdmin(admin.ModelAdmin): return HttpResponseRedirect(reverse('admin:app_plugin_changelist')) def plugin_upload(self, request, *args, **kwargs): - messages.info(request, "YAY") - # TODO + # messages.info(request, "YAY") + file = request.FILES.get('file') + if file is not None: + # Save to tmp dir + tmp_zip_path = tempfile.mktemp('plugin.zip', dir=settings.MEDIA_TMP) + tmp_extract_path = tempfile.mkdtemp('plugin', dir=settings.MEDIA_TMP) + + try: + with open(tmp_zip_path, 'wb+') as fd: + if isinstance(file, InMemoryUploadedFile): + for chunk in file.chunks(): + fd.write(chunk) + else: + with open(file.temporary_file_path(), 'rb') as f: + shutil.copyfileobj(f, fd) + + # Extract + with zipfile.ZipFile(tmp_zip_path, "r") as zip_h: + zip_h.extractall(tmp_extract_path) + + # Validate + folders = os.listdir(tmp_extract_path) + if len(folders) != 1: + raise ValueError("The plugin has more than 1 root directory (it should have only one)") + + plugin_name = folders[0] + plugin_path = os.path.join(tmp_extract_path, plugin_name) + if not valid_plugin(plugin_path): + raise ValueError("This doesn't look like a plugin. Are plugin.py and manifest.json in the proper place?") + + if os.path.exists(get_plugins_persistent_path(plugin_name)): + raise ValueError("A plugin with the name {} already exist. Please remove it before uploading one with the same name.".format(plugin_name)) + + # Move + shutil.move(plugin_path, get_plugins_persistent_path()) + + # Initialize + clear_plugins_cache() + init_plugins() + + messages.info(request, "Plugin added successfully") + except Exception as e: + messages.warning(request, "Cannot load plugin: {}".format(str(e))) + if os.path.exists(tmp_zip_path): + os.remove(tmp_zip_path) + if os.path.exists(tmp_extract_path): + shutil.rmtree(tmp_extract_path) + else: + messages.error(request, "You need to upload a zip file") + return HttpResponseRedirect(reverse('admin:app_plugin_changelist')) diff --git a/app/plugins/functions.py b/app/plugins/functions.py index f1281844..f94ef219 100644 --- a/app/plugins/functions.py +++ b/app/plugins/functions.py @@ -7,6 +7,8 @@ import platform import django import json + +import shutil from django.conf.urls import url from functools import reduce from string import Template @@ -149,6 +151,10 @@ def register_plugins(): disable_plugin(plugin.get_name()) logger.warning("Cannot register {}: {}".format(plugin, str(e))) +def valid_plugin(plugin_path): + pluginpy_path = os.path.join(plugin_path, "plugin.py") + manifest_path = os.path.join(plugin_path, "manifest.json") + return os.path.isfile(manifest_path) and os.path.isfile(pluginpy_path) plugins = None def get_plugins(): @@ -163,14 +169,15 @@ def get_plugins(): plugins = [] for plugins_path in plugins_paths: - for dir in [d for d in os.listdir(plugins_path) if os.path.isdir(plugins_path)]: + if not os.path.isdir(plugins_path): + continue + + for dir in os.listdir(plugins_path): # Each plugin must have a manifest.json and a plugin.py plugin_path = os.path.join(plugins_path, dir) - pluginpy_path = os.path.join(plugin_path, "plugin.py") - manifest_path = os.path.join(plugin_path, "manifest.json") # Do not load test plugin unless we're in test mode - if os.path.basename(plugin_path) == 'test' and not settings.TESTING: + if os.path.basename(plugin_path).endswith('test') and not settings.TESTING: continue # Ignore .gitignore @@ -178,7 +185,7 @@ def get_plugins(): continue # Check plugin required files - if not os.path.isfile(manifest_path) or not os.path.isfile(pluginpy_path): + if not valid_plugin(plugin_path): continue # Instantiate the plugin @@ -194,6 +201,7 @@ def get_plugins(): manifest = plugin.get_manifest() if 'webodmMinVersion' in manifest: min_version = manifest['webodmMinVersion'] + manifest_path = os.path.join(plugin_path, "manifest.json") if versionToInt(min_version) > versionToInt(settings.VERSION): logger.warning( @@ -305,7 +313,10 @@ def disable_plugin(plugin_name): Plugin.objects.get(pk=plugin_name).disable() def delete_plugin(plugin_name): - Plugin.objects.get(pk=plugin_name).disable() + Plugin.objects.get(pk=plugin_name).delete() + if os.path.exists(get_plugins_persistent_path(plugin_name)): + shutil.rmtree(get_plugins_persistent_path(plugin_name)) + clear_plugins_cache() def get_site_settings(): return Setting.objects.first() diff --git a/app/plugins/plugin_base.py b/app/plugins/plugin_base.py index a3578eaa..7b21c62c 100644 --- a/app/plugins/plugin_base.py +++ b/app/plugins/plugin_base.py @@ -188,6 +188,16 @@ class PluginBase(ABC): """ return [] + def serve_public_assets(self, request): + """ + Should be overriden by plugins that want to control which users + have access to the public assets. By default anyone can access them, + including anonymous users. + :param request: HTTP request + :return: boolean (whether the plugin's public assets should be exposed for this request) + """ + return True + def get_dynamic_script(self, script_path, callback = None, **template_args): """ Retrieves a view handler that serves a dynamic script from diff --git a/app/plugins/views.py b/app/plugins/views.py index aca945db..cea6e624 100644 --- a/app/plugins/views.py +++ b/app/plugins/views.py @@ -8,10 +8,12 @@ from django.http import HttpResponse, Http404 from .functions import get_plugin_by_name from django.conf.urls import url from django.views.static import serve +from urllib.parse import urlparse def try_resolve_url(request, url): - res = url.resolve(request.get_full_path()) + o = urlparse(request.get_full_path()) + res = url.resolve(o.path) if res: return res else: @@ -28,12 +30,11 @@ def app_view_handler(request, plugin_name=None): mount_point.view, *mount_point.args, **mount_point.kwargs)) - if view: return view(request, *args, **kwargs) # Try public assets - if os.path.exists(plugin.get_path("public")): + if os.path.exists(plugin.get_path("public")) and plugin.serve_public_assets(request): view, args, kwargs = try_resolve_url(request, url('^/plugins/{}/(.*)'.format(plugin_name), serve, {'document_root': plugin.get_path("public")})) diff --git a/app/tests/test_plugins.py b/app/tests/test_plugins.py index 651887f5..2d402896 100644 --- a/app/tests/test_plugins.py +++ b/app/tests/test_plugins.py @@ -29,6 +29,22 @@ class TestPlugins(BootTestCase): def test_core_plugins(self): client = Client() + # We cannot access public files core plugins (plugin is disabled) + res = client.get('/plugins/test/file.txt') + self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) + + # Cannot access mount point (plugin is disabled) + res = client.get('/plugins/test/app_mountpoint/') + self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) + + # No python packages have been installed (plugin is disabled) + self.assertFalse(os.path.exists(get_plugins_persistent_path("test", "site-packages"))) + + enable_plugin("test") + + # Python packages have been installed + self.assertTrue(os.path.exists(get_plugins_persistent_path("test", "site-packages"))) + # We can access public files core plugins (without auth) res = client.get('/plugins/test/file.txt') self.assertEqual(res.status_code, status.HTTP_200_OK) @@ -38,13 +54,10 @@ class TestPlugins(BootTestCase): self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertTemplateUsed(res, 'plugins/test/templates/app.html') - # No python packages have been installed (plugin is disabled) - self.assertFalse(os.path.exists(get_plugins_persistent_path("test", "site-packages"))) - - enable_plugin("test") - # Form was rendered correctly - self.assertContains(res, '', count=1, status_code=200, html=True) + self.assertContains(res, + '', + count=1, status_code=200, html=True) # It uses regex properly res = client.get('/plugins/test/app_mountpoint/a') diff --git a/package.json b/package.json index 2a187436..741b210a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "WebODM", - "version": "1.3.4", + "version": "1.3.5", "description": "User-friendly, extendable application and API for processing aerial imagery.", "main": "index.js", "scripts": { diff --git a/plugins/test/plugin.py b/plugins/test/plugin.py index 6bdbf4af..816bdf4c 100644 --- a/plugins/test/plugin.py +++ b/plugins/test/plugin.py @@ -22,10 +22,10 @@ class Plugin(PluginBase): return [Menu("Test", self.public_url("menu_url/"), "test-icon")] def include_js_files(self): - return ['test.js'] + return ['test.js'] def include_css_files(self): - return ['test.css'] + return ['test.css'] def build_jsx_components(self): return ['component.jsx']