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']