Merge pull request #846 from pierotofy/extplugins

Plugins refactoring
pull/697/head
Piero Toffanin 2020-04-02 19:11:54 -04:00 zatwierdzone przez GitHub
commit 195463f1f0
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
18 zmienionych plików z 449 dodań i 113 usunięć

Wyświetl plik

@ -2,7 +2,7 @@
[![Build Status](https://travis-ci.org/OpenDroneMap/WebODM.svg?branch=master)](https://travis-ci.org/OpenDroneMap/WebODM) [![GitHub version](https://badge.fury.io/gh/OpenDroneMap%2FWebODM.svg)](https://badge.fury.io/gh/OpenDroneMap%2FWebODM)
A user-friendly, extendable application and [API](http://docs.webodm.org) for drone image processing. Generate georeferenced maps, point clouds, elevation models and textured 3D models from aerial images. It supports multiple engines for processing, currently [ODM](https://github.com/OpenDroneMap/ODM) and [MicMac](https://github.com/dronemapper-io/NodeMICMAC/) (experimental).
A user-friendly, extendable application and [API](http://docs.webodm.org) for drone image processing. Generate georeferenced maps, point clouds, elevation models and textured 3D models from aerial images. It supports multiple engines for processing, currently [ODM](https://github.com/OpenDroneMap/ODM) and [MicMac](https://github.com/dronemapper-io/NodeMICMAC/).
![image](https://user-images.githubusercontent.com/1951843/73680798-efc02680-468a-11ea-9ae5-55e51427c6f1.png)
@ -35,6 +35,14 @@ A user-friendly, extendable application and [API](http://docs.webodm.org) for dr
## Getting Started
Windows and macOS users can purchase an automated [installer](https://www.opendronemap.org/webodm/download#installer), which makes the installation process easier.
You can also run WebODM from a Live USB/DVD. See [LiveODM](https://www.opendronemap.org/liveodm/).
Windows users looking for a UI-only native installation should also check [webodm.net](https://webodm.net).
To install WebODM manually, these steps should get you up and running:
* Install the following applications (if they are not installed already):
- [Git](https://git-scm.com/downloads)
- [Docker](https://www.docker.com/)
@ -76,12 +84,6 @@ To update WebODM to the latest version use:
We recommend that you read the [Docker Documentation](https://docs.docker.com/) to familiarize with the application lifecycle, setup and teardown, or for more advanced uses. Look at the contents of the webodm.sh script to understand what commands are used to launch WebODM.
Windows and macOS users can purchase an automated [installer](https://www.opendronemap.org/webodm/download#installer), which makes the installation process easier.
You can also run WebODM from a Live USB/DVD. See [LiveODM](https://www.opendronemap.org/liveodm/).
Windows users looking for a UI-only native installation should also check [webodm.net](https://webodm.net).
### Manage Processing Nodes
WebODM can be linked to one or more processing nodes that speak the [NodeODM API](https://github.com/OpenDroneMap/NodeODM/blob/master/docs/index.adoc), such as [NodeODM](https://github.com/OpenDroneMap/NodeODM), [NodeMICMAC](https://github.com/dronemapper-io/NodeMICMAC/) or [ClusterODM](https://github.com/OpenDroneMap/ClusterODM). The default configuration includes a "node-odm-1" processing node which runs on the same machine as WebODM, just to help you get started. As you become more familiar with WebODM, you might want to install processing nodes on separate machines.

Wyświetl plik

@ -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
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)
@ -74,6 +82,7 @@ admin.site.register(PluginDatum, admin.ModelAdmin)
class PluginAdmin(admin.ModelAdmin):
list_display = ("name", "description", "version", "author", "enabled", "plugin_actions")
readonly_fields = ("name", )
change_list_template = "admin/change_list_plugin.html"
def has_add_permission(self, request):
return False
@ -105,6 +114,16 @@ class PluginAdmin(admin.ModelAdmin):
self.admin_site.admin_view(self.plugin_disable),
name='plugin-disable',
),
url(
r'^(?P<plugin_name>.+)/delete/$',
self.admin_site.admin_view(self.plugin_delete),
name='plugin-delete',
),
url(
r'^actions/upload/$',
self.admin_site.admin_view(self.plugin_upload),
name='plugin-upload',
),
]
return custom_urls + urls
@ -124,14 +143,81 @@ class PluginAdmin(admin.ModelAdmin):
return HttpResponseRedirect(reverse('admin:app_plugin_changelist'))
def plugin_delete(self, request, plugin_name, *args, **kwargs):
try:
delete_plugin(plugin_name)
except Exception as e:
messages.warning(request, "Cannot delete plugin {}: {}".format(plugin_name, str(e)))
return HttpResponseRedirect(reverse('admin:app_plugin_changelist'))
def plugin_upload(self, request, *args, **kwargs):
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'))
def plugin_actions(self, obj):
plugin = get_plugin_by_name(obj.name, only_active=False)
return format_html(
'<a class="button" href="{}" {}>Disable</a>&nbsp;'
'<a class="button" href="{}" {}>Enable</a>',
'<a class="button" href="{}" {}>Enable</a>'
+ ('&nbsp;<a class="button" href="{}" onclick="return confirm(\'Are you sure you want to delete {}?\')"><i class="fa fa-trash"></i></a>' if not plugin.is_persistent() else '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;')
,
reverse('admin:plugin-disable', args=[obj.pk]) if obj.enabled else '#',
'disabled' if not obj.enabled else '',
reverse('admin:plugin-enable', args=[obj.pk]) if not obj.enabled else '#',
'disabled' if obj.enabled else '',
reverse('admin:plugin-delete', args=[obj.pk]),
obj.name
)
plugin_actions.short_description = 'Actions'

Wyświetl plik

@ -1,6 +1,6 @@
from django.core.exceptions import ObjectDoesNotExist, SuspiciousFileOperation
from rest_framework import exceptions
import os, zipfile
import os
from app import models

Wyświetl plik

@ -19,7 +19,8 @@ from app import models, pending_actions
from nodeodm import status_codes
from nodeodm.models import ProcessingNode
from worker import tasks as worker_tasks
from .common import get_and_check_project, path_traversal_check
from .common import get_and_check_project
from app.security import path_traversal_check
def flatten_files(request_files):

Wyświetl plik

@ -1,7 +1,7 @@
from django.conf.urls import url, include
from app.api.presets import PresetViewSet
from app.plugins import get_api_url_patterns
from app.plugins.views import api_view_handler
from .projects import ProjectViewSet
from .tasks import TaskViewSet, TaskDownloads, TaskAssets, TaskAssetsImport
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
@ -49,6 +49,6 @@ urlpatterns = [
url(r'^auth/', include('rest_framework.urls')),
url(r'^token-auth/', obtain_jwt_token),
]
urlpatterns += get_api_url_patterns()
url(r'^plugins/(?P<plugin_name>[^/.]+)/(.*)$', api_view_handler)
]

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -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
@ -16,6 +18,7 @@ from django.http import HttpResponse
from app.models import Plugin
from app.models import Setting
from django.conf import settings
from app.security import path_traversal_check
logger = logging.getLogger('app.logger')
@ -59,7 +62,7 @@ def sync_plugin_db():
defaults={'enabled': not disabled},
)
if created:
logger.info("Added [{}] plugin to database".format(plugin.get_name()))
logger.info("Added [{}] plugin to database".format(plugin))
def clear_plugins_cache():
@ -149,40 +152,11 @@ def register_plugins():
disable_plugin(plugin.get_name())
logger.warning("Cannot register {}: {}".format(plugin, str(e)))
def get_app_url_patterns():
"""
@return the patterns to expose the /public directory of each plugin (if needed) and
each mount point
"""
url_patterns = []
for plugin in get_plugins():
for mount_point in plugin.app_mount_points():
url_patterns.append(url('^plugins/{}/{}'.format(plugin.get_name(), mount_point.url),
mount_point.view,
*mount_point.args,
**mount_point.kwargs))
if plugin.path_exists("public"):
url_patterns.append(url('^plugins/{}/(.*)'.format(plugin.get_name()),
django.views.static.serve,
{'document_root': plugin.get_path("public")}))
return url_patterns
def get_api_url_patterns():
"""
@return the patterns to expose the plugin API mount points (if any)
"""
url_patterns = []
for plugin in get_plugins():
for mount_point in plugin.api_mount_points():
url_patterns.append(url('^plugins/{}/{}'.format(plugin.get_name(), mount_point.url),
mount_point.view,
*mount_point.args,
**mount_point.kwargs))
return url_patterns
def valid_plugin(plugin_path):
initpy_path = os.path.join(plugin_path, "__init__.py")
pluginpy_path = os.path.join(plugin_path, "plugin.py")
manifest_path = os.path.join(plugin_path, "manifest.json")
return os.path.isfile(initpy_path) and os.path.isfile(manifest_path) and os.path.isfile(pluginpy_path)
plugins = None
def get_plugins():
@ -193,51 +167,66 @@ def get_plugins():
global plugins
if plugins != None: return plugins
plugins_path = get_plugins_path()
plugins_paths = get_plugins_paths()
plugins = []
for dir in [d for d in os.listdir(plugins_path) if os.path.isdir(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:
for plugins_path in plugins_paths:
if not os.path.isdir(plugins_path):
continue
# Ignore .gitignore
if os.path.basename(plugin_path) == '.gitignore':
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)
# Check plugin required files
if not os.path.isfile(manifest_path) or not os.path.isfile(pluginpy_path):
logger.warning("Found invalid plugin in {}".format(plugin_path))
continue
# Instantiate the plugin
try:
module = importlib.import_module("plugins.{}".format(dir))
plugin = (getattr(module, "Plugin"))()
# Check version
manifest = plugin.get_manifest()
if 'webodmMinVersion' in manifest:
min_version = manifest['webodmMinVersion']
if versionToInt(min_version) > versionToInt(settings.VERSION):
logger.warning(
"In {} webodmMinVersion is set to {} but WebODM version is {}. Plugin will not be loaded. Update WebODM.".format(
manifest_path, min_version, settings.VERSION))
continue
# Skip plugins in blacklist
if plugin.get_name() in settings.PLUGINS_BLACKLIST:
# Do not load test plugin unless we're in test mode
if os.path.basename(plugin_path).endswith('test') and not settings.TESTING:
continue
plugins.append(plugin)
except Exception as e:
logger.warning("Failed to instantiate plugin {}: {}".format(dir, e))
# Ignore .gitignore
if os.path.basename(plugin_path) == '.gitignore':
continue
# Check plugin required files
if not valid_plugin(plugin_path):
continue
# Instantiate the plugin
try:
try:
if settings.TESTING:
module = importlib.import_module("app.media_test.plugins.{}".format(dir))
else:
module = importlib.import_module("app.media.plugins.{}".format(dir))
plugin = (getattr(module, "Plugin"))()
except (ModuleNotFoundError, AttributeError) as e:
module = importlib.import_module("plugins.{}".format(dir))
plugin = (getattr(module, "Plugin"))()
# Check version
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(
"In {} webodmMinVersion is set to {} but WebODM version is {}. Plugin will not be loaded. Update WebODM.".format(
manifest_path, min_version, settings.VERSION))
continue
# Skip plugins in blacklist
if plugin.get_name() in settings.PLUGINS_BLACKLIST:
continue
# Skip plugins already added
if plugin.get_name() in [p.get_name() for p in plugins]:
logger.warning("Duplicate plugin name found in {}, skipping".format(plugin_path))
continue
plugins.append(plugin)
except Exception as e:
logger.warning("Failed to instantiate plugin {}: {}".format(dir, e))
return plugins
@ -281,20 +270,27 @@ def get_current_plugin():
"""
caller_filename = traceback.extract_stack()[-2][0]
relp = os.path.relpath(caller_filename, get_plugins_path())
parts = relp.split(os.sep)
if len(parts) > 0:
plugin_name = parts[0]
return get_plugin_by_name(plugin_name, only_active=False)
for p in get_plugins_paths():
relp = os.path.relpath(caller_filename, p)
if ".." in relp:
continue
parts = relp.split(os.sep)
if len(parts) > 0:
plugin_name = parts[0]
return get_plugin_by_name(plugin_name, only_active=False)
return None
def get_plugins_path():
def get_plugins_paths():
current_path = os.path.dirname(os.path.realpath(__file__))
return os.path.abspath(os.path.join(current_path, "..", "..", "plugins"))
return [
os.path.abspath(get_plugins_persistent_path()),
os.path.abspath(os.path.join(current_path, "..", "..", "plugins")),
]
def get_plugins_persistent_path(*paths):
return os.path.join(settings.MEDIA_ROOT, "plugins", *paths)
return path_traversal_check(os.path.join(settings.MEDIA_ROOT, "plugins", *paths), os.path.join(settings.MEDIA_ROOT, "plugins"))
def get_dynamic_script_handler(script_path, callback=None, **kwargs):
def handleRequest(request):
@ -322,6 +318,12 @@ def enable_plugin(plugin_name):
def disable_plugin(plugin_name):
Plugin.objects.get(pk=plugin_name).disable()
def delete_plugin(plugin_name):
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()

Wyświetl plik

@ -1,4 +1,3 @@
import importlib
import json
import logging, os, sys, subprocess
from abc import ABC
@ -120,12 +119,22 @@ class PluginBase(ABC):
"""
return "/plugins/{}/{}".format(self.get_name(), path)
def is_persistent(self):
"""
:return: whether this plugin is persistent (stored in the /plugins directory,
instead of /app/media/plugins which are transient)
"""
return ".." in os.path.relpath(self.get_path(), get_plugins_persistent_path())
def template_path(self, path):
"""
:param path: unix-style path
:return: path used to reference Django templates for a plugin
"""
return "plugins/{}/templates/{}".format(self.get_name(), path)
if self.is_persistent():
return "plugins/{}/templates/{}".format(self.get_name(), path)
else:
return "app/media/plugins/{}/templates/{}".format(self.get_name(), path)
def path_exists(self, path):
return os.path.exists(self.get_path(path))
@ -179,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

Wyświetl plik

@ -1,5 +1,8 @@
// Magic to include node_modules of root WebODM's directory
process.env.NODE_PATH = "../../../node_modules";
const fs = require('fs');
let webodmRoot = "../../../"; // Assuming plugins/<name>/public
if (!fs.existsSync(webodmRoot + "webodm.sh")) webodmRoot = "../../../../../"; // Assuming app/media/plugins/<name>/public
process.env.NODE_PATH = webodmRoot + "node_modules";
require("module").Module._initPaths();
let path = require("path");
@ -69,7 +72,7 @@ module.exports = {
modules: ['node_modules', 'bower_components'],
extensions: ['.js', '.jsx'],
alias: {
webodm: path.resolve(__dirname, '../../../app/static/app/js')
webodm: path.resolve(__dirname, webodmRoot + 'app/static/app/js')
}
},

Wyświetl plik

@ -1,3 +1,61 @@
import os
from app.api.tasks import TaskNestedView as TaskView
from app.api.workers import CheckTask as CheckTask
from app.api.workers import GetTaskResult as GetTaskResult
from app.api.workers import GetTaskResult as GetTaskResult
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):
o = urlparse(request.get_full_path())
res = url.resolve(o.path)
if res:
return res
else:
return (None, None, None)
def app_view_handler(request, plugin_name=None):
plugin = get_plugin_by_name(plugin_name) # TODO: this pings the server, which might be bad for performance with very large amount of files
if plugin is None:
raise Http404("Plugin not found")
# Try mountpoints first
for mount_point in plugin.app_mount_points():
view, args, kwargs = try_resolve_url(request, url(r'^/plugins/{}/{}'.format(plugin_name, mount_point.url),
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")) 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")}))
if view:
return view(request, *args, **kwargs)
raise Http404("No valid routes")
def api_view_handler(request, plugin_name=None):
plugin = get_plugin_by_name(plugin_name) # TODO: this pings the server, which might be bad for performance with very large amount of files
if plugin is None:
raise Http404("Plugin not found")
for mount_point in plugin.api_mount_points():
view, args, kwargs = try_resolve_url(request, url(r'^/api/plugins/{}/{}'.format(plugin_name, mount_point.url),
mount_point.view,
*mount_point.args,
**mount_point.kwargs))
if view:
return view(request, *args, **kwargs)
raise Http404("No valid routes")

12
app/security.py 100644
Wyświetl plik

@ -0,0 +1,12 @@
from django.core.exceptions import SuspiciousFileOperation
import os
def path_traversal_check(unsafe_path, known_safe_path):
known_safe_path = os.path.abspath(known_safe_path)
unsafe_path = os.path.abspath(unsafe_path)
if (os.path.commonprefix([known_safe_path, unsafe_path]) != known_safe_path):
raise SuspiciousFileOperation("{} is not safe".format(unsafe_path))
# Passes the check
return unsafe_path

Wyświetl plik

@ -0,0 +1,29 @@
{% extends "admin/change_list.html" %}
{% block content_title %}
<style type="text/css">
.plugin-upload{
float: right;
margin-left: 8px;
margin-bottom: 12px;
}
.plugin-upload input[type="file"]{
display: none;
}
</style>
<div class="plugin-upload">
<form name="form" enctype="multipart/form-data"
action="actions/upload/" id="plugin-upload-form" method = "POST" >{% csrf_token %}
<input type="file"
name="file"
id="plugin-upload-file"
onchange="document.getElementById('plugin-upload-form').submit();"
accept=".zip"/>
<button type="submit" class="btn btn-sm btn-primary" onclick="document.getElementById('plugin-upload-file').click(); return false;">
<i class="glyphicon glyphicon-upload"></i> Load Plugin (.zip)
</button>
</form>
</div>
<h1>Manage Plugins</h1>
<div style="clear: both;"></div>
{% endblock %}

Wyświetl plik

@ -2,6 +2,7 @@ import os
import shutil
import sys
from django.contrib.auth.models import User
from django.test import Client
from rest_framework import status
@ -29,6 +30,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 +55,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, '<input type="text" name="testField" class="form-control" required id="id_testField" />', count=1, status_code=200, html=True)
self.assertContains(res,
'<input type="text" name="testField" class="form-control" required id="id_testField" />',
count=1, status_code=200, html=True)
# It uses regex properly
res = client.get('/plugins/test/app_mountpoint/a')
@ -66,6 +80,9 @@ class TestPlugins(BootTestCase):
test_plugin = get_plugin_by_name("test")
self.assertTrue(os.path.exists(test_plugin.get_path("public/node_modules")))
# This is a persistent plugin
self.assertTrue(test_plugin.is_persistent())
# A webpack file and build directory have been created as a
# result of the build_jsx_components directive
self.assertTrue(os.path.exists(test_plugin.get_path("public/webpack.config.js")))
@ -304,4 +321,112 @@ class TestPlugins(BootTestCase):
self.assertEqual(p.get_manifest()['author'], "Piero Toffanin")
def test_plugin_loading(self):
c = Client()
plugin_file = open("app/fixtures/testabc_plugin.zip", 'rb')
bad_dir_plugin_file = open("app/fixtures/bad_dir_plugin.zip", 'rb')
missing_manifest_plugin_file = open("app/fixtures/missing_manifest_plugin.zip", 'rb')
# Cannot upload new plugins anonymously
res = c.post('/admin/app/plugin/actions/upload/', {'file': plugin_file}, follow=True)
self.assertRedirects(res, '/admin/login/?next=/admin/app/plugin/actions/upload/')
self.assertFalse(os.path.exists(get_plugins_persistent_path("testabc")))
plugin_file.seek(0)
# Cannot upload plugins as a normal user
c.login(username='testuser', password='test1234')
res = c.post('/admin/app/plugin/actions/upload/', {'file': plugin_file}, follow=True)
self.assertRedirects(res, '/admin/login/?next=/admin/app/plugin/actions/upload/')
self.assertFalse(os.path.exists(get_plugins_persistent_path("testabc")))
self.assertEqual(Plugin.objects.filter(pk='testabc').count(), 0)
plugin_file.seek(0)
# Can upload plugin as an admin
c.login(username='testsuperuser', password='test1234')
res = c.post('/admin/app/plugin/actions/upload/', {'file': plugin_file}, follow=True)
self.assertRedirects(res, '/admin/app/plugin/')
messages = list(res.context['messages'])
self.assertTrue('Plugin added successfully' in str(messages[0]))
self.assertTrue(os.path.exists(get_plugins_persistent_path("testabc")))
plugin_file.seek(0)
# Plugin has been added to db
self.assertEqual(Plugin.objects.filter(pk='testabc').count(), 1)
# This is not a persistent plugin
self.assertFalse(get_plugin_by_name('testabc').is_persistent())
# Cannot upload the same plugin again (same name)
res = c.post('/admin/app/plugin/actions/upload/', {'file': plugin_file}, follow=True)
self.assertRedirects(res, '/admin/app/plugin/')
messages = list(res.context['messages'])
self.assertTrue('already exist' in str(messages[0]))
plugin_file.seek(0)
# Can access paths (while being logged in)
res = c.get('/plugins/testabc/hello/')
self.assertEqual(res.status_code, status.HTTP_200_OK)
res = c.get('/api/plugins/testabc/hello/')
self.assertEqual(res.status_code, status.HTTP_200_OK)
# Can access public paths as logged-in, (per plugin directive)
res = c.get('/plugins/testabc/file.txt')
self.assertEqual(res.status_code, status.HTTP_200_OK)
c.logout()
# Can still access the paths as anonymous
res = c.get('/plugins/testabc/hello/')
self.assertEqual(res.status_code, status.HTTP_200_OK)
res = c.get('/api/plugins/testabc/hello/')
self.assertEqual(res.status_code, status.HTTP_200_OK)
# But not the public paths as anonymous (per plugin directive)
res = c.get('/plugins/testabc/file.txt')
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
# Cannot delete plugin as normal user
c.login(username='testuser', password='test1234')
res = c.get('/admin/app/plugin/testabc/delete/', follow=True)
self.assertRedirects(res, '/admin/login/?next=/admin/app/plugin/testabc/delete/')
# Can delete plugin as admin
c.login(username='testsuperuser', password='test1234')
res = c.get('/admin/app/plugin/testabc/delete/', follow=True)
self.assertRedirects(res, '/admin/app/plugin/')
messages = list(res.context['messages'])
# No errors
self.assertEqual(len(messages), 0)
# Directories have been removed
self.assertFalse(os.path.exists(get_plugins_persistent_path("testabc")))
# Cannot access the paths as anonymous
res = c.get('/plugins/testabc/hello/')
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
res = c.get('/api/plugins/testabc/hello/')
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
res = c.get('/plugins/testabc/file.txt')
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
# Try to add malformed plugins files
res = c.post('/admin/app/plugin/actions/upload/', {'file': missing_manifest_plugin_file}, follow=True)
self.assertRedirects(res, '/admin/app/plugin/')
messages = list(res.context['messages'])
self.assertTrue('Cannot load plugin' in str(messages[0]))
self.assertFalse(os.path.exists(get_plugins_persistent_path("test123")))
self.assertEqual(Plugin.objects.filter(pk='test123').count(), 0)
missing_manifest_plugin_file.seek(0)
res = c.post('/admin/app/plugin/actions/upload/', {'file': bad_dir_plugin_file}, follow=True)
self.assertRedirects(res, '/admin/app/plugin/')
messages = list(res.context['messages'])
self.assertTrue('Cannot load plugin' in str(messages[0]))
missing_manifest_plugin_file.seek(0)
plugin_file.close()
missing_manifest_plugin_file.close()
bad_dir_plugin_file.close()

Wyświetl plik

@ -1,7 +1,7 @@
from django.conf.urls import url, include
from .views import app as app_views, public as public_views
from .plugins import get_app_url_patterns
from .plugins.views import app_view_handler
from app.boot import boot
from webodm import settings
@ -35,11 +35,10 @@ urlpatterns = [
url(r'^processingnode/([\d]+)/$', app_views.processing_node, name='processing_node'),
url(r'^api/', include("app.api.urls")),
url(r'^plugins/(?P<plugin_name>[^/.]+)/(.*)$', app_view_handler)
]
handler404 = app_views.handler404
handler500 = app_views.handler500
# TODO: is there a way to place plugins /public directories
# into the static build directories and let nginx serve them?
urlpatterns += get_app_url_patterns()

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "WebODM",
"version": "1.3.4",
"version": "1.3.6",
"description": "User-friendly, extendable application and API for processing aerial imagery.",
"main": "index.js",
"scripts": {

Wyświetl plik

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