zoph: Add new app to organize photos

- Identify Freedbombox admin who installed the app to be zoph admin.

- Implement backup and restore.

- Photos directory should remain at /var/lib/zoph, for proper operation of
backups.

- There remains an issue that the App is enabled before it is configured but
will not work correctly until configuration.

- OpenStreetMap mapping is enabled. These should be configurable before
installation.

- Add initial forms.py and views.py to Zoph app, however these are currently
unused as urls.py does not invoke the view. When the view is not invoked the
Zoph App installs, with single signon, currently as the first LDAP user, rather
than the plinth user.

- The first user's preferences are not set, and need to be manually set to avoid
other errors.

* Sunil's changes

- Squash commits and re-split them.

- Drop documentation as it will be auto populated from wiki later.

- Remove outdated validation code from manifest.

- Drop some dead code.

- Don't send MySQL password over command line for improved security. Instead
  rely on Unix authentication of root user similar to backup/restore process.

- Use JSON for exchanging configuration dump to avoid encoding errors.

- Add username validation to avoid a potential SQL injection.

- Update description for neural tone and brevity. Add information about how user
  accounts work in FreedomBox

- Fix functional tests.

- Drop all code related to changing photos path until it is ready.

- Update URL from /zoph to /zoph/ to avoid another redirect.

- Fix disabling the app.

- Use icon that Zoph uses for favicon as logo. Update copyright file.

- Fix spelling unzip.

- Minor refactors. Run yapf and isort.

- Use subprocess.run() instead of os.popen() everywhere for better security with
argument parsing.

- Enable OpenStreetMap by default. User have a choice to disable it before using
the app. Add label to explain privacy concerns.

- Fix dropping database by using --force argument.

- Cleanup enabling the app to not enable the app when updating configuration.

- Use AppView's default template instead of overriding.

- Update functional tests to just check if the app is enabled/disabled as
expected. Checking that Zoph site is available will require reliable handling of
admin user.

Signed-off-by: John Lines <john@paladyn.org>
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
John Lines 2020-12-20 18:05:23 +00:00 committed by Sunil Mohan Adapa
parent 84e671c919
commit f978d2f0d0
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
14 changed files with 1003 additions and 0 deletions

147
actions/zoph Executable file
View File

@ -0,0 +1,147 @@
#!/usr/bin/python3
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Configuration helper for Zoph server.
"""
import argparse
import configparser
import json
import os
import re
import subprocess
from plinth import action_utils
APACHE_CONF = '/etc/apache2/conf-available/zoph.conf'
DB_BACKUP_FILE = '/var/lib/plinth/backups-data/zoph-database.sql'
def parse_arguments():
"""Return parsed command line arguments as dictionary."""
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
subparsers.add_parser('pre-install',
help='Perform Zoph pre-install configuration')
subparser = subparsers.add_parser('setup',
help='Perform Zoph configuration setup')
subparsers.add_parser('get-configuration',
help='Return the current configuration')
subparser = subparsers.add_parser('set-configuration',
help='Configure zoph')
subparser.add_argument('--admin-user', help='Name of the admin user')
subparser.add_argument('--enable-osm', help='Enable OpenSteetMap maps')
subparsers.add_parser('is-configured', help='return true if configured')
subparsers.add_parser('dump-database', help='Dump database to file')
subparsers.add_parser('restore-database',
help='Restore database from file')
subparsers.required = True
return parser.parse_args()
def subcommand_pre_install(_):
"""Preseed debconf values before packages are installed."""
action_utils.debconf_set_selections([
'zoph zoph/dbconfig-install boolean true',
'zoph zoph/mysql/admin-user string root'
])
def subcommand_get_configuration(_):
"""Return the current configuration."""
configuration = {}
process = subprocess.run(['zoph', '--dump-config'], stdout=subprocess.PIPE,
check=True)
for line in process.stdout.decode().splitlines():
name, value = line.partition(':')[::2]
configuration[name.strip()] = value[1:]
print(json.dumps(configuration))
def _zoph_configure(key, value):
"""Set a configure value in Zoph."""
subprocess.run(['zoph', '--config', key, value], check=True)
def subcommand_setup(_):
"""Setup Zoph configuration."""
_zoph_configure('import.enable', 'true')
_zoph_configure('import.upload', 'true')
_zoph_configure('import.rotate', 'true')
_zoph_configure('path.unzip', 'unzip')
_zoph_configure('path.untar', 'tar xvf')
_zoph_configure('path.ungz', 'gunzip')
# Maps using OpenStreetMap is enabled by default.
_zoph_configure('maps.provider', 'osm')
def _get_db_name():
"""Return the name of the database configured by dbconfig."""
config = configparser.ConfigParser()
config.read_file(open('/etc/zoph.ini', 'r'))
return config['zoph']['db_name'].strip('"')
def subcommand_set_configuration(arguments):
"""Setup Zoph Apache configuration."""
_zoph_configure('interface.user.remote', 'true')
# Note that using OpenSteetmap as a mapping provider is a very nice
# feature, but some people may regard its use as a privacy issue
if arguments.enable_osm:
value = 'osm' if arguments.enable_osm == 'True' else ''
_zoph_configure('maps.provider', value)
if arguments.admin_user:
# Edit the database to rename the admin user to FreedomBox admin user.
admin_user = arguments.admin_user
if not re.match(r'^[\w.@][\w.@-]+\Z', admin_user, flags=re.ASCII):
# Check to avoid SQL injection
raise ValueError('Invalid username')
query = f"UPDATE zoph_users SET user_name='{admin_user}' \
WHERE user_name='admin';"
subprocess.run(['mysql', _get_db_name()], input=query.encode(),
check=True)
def subcommand_is_configured(_):
"""Print whether zoph app is configured."""
subprocess.run(['zoph', '--get-config', 'interface.user.remote'],
check=True)
def subcommand_dump_database(_):
"""Dump database to file."""
db_name = _get_db_name()
os.makedirs(os.path.dirname(DB_BACKUP_FILE), exist_ok=True)
with open(DB_BACKUP_FILE, 'w') as db_backup_file:
subprocess.run(['mysqldump', db_name], stdout=db_backup_file,
check=True)
def subcommand_restore_database(_):
"""Restore database from file."""
db_name = _get_db_name()
subprocess.run(['mysqladmin', '--force', 'drop', db_name])
subprocess.run(['mysqladmin', 'create', db_name], check=True)
with open(DB_BACKUP_FILE, 'r') as db_restore_file:
subprocess.run(['mysql', db_name], stdin=db_restore_file, check=True)
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()
subcommand = arguments.subcommand.replace('-', '_')
subcommand_method = globals()['subcommand_' + subcommand]
subcommand_method(arguments)
if __name__ == '__main__':
main()

2
debian/copyright vendored
View File

@ -69,6 +69,8 @@ Files: static/themes/default/icons/diaspora.png
static/themes/default/icons/privoxy.png
static/themes/default/icons/privoxy.svg
static/themes/default/icons/radicale.svg
static/themes/default/icons/zoph.png
static/themes/default/icons/zoph.svg
static/themes/default/img/network-connection.svg
static/themes/default/img/network-connection-vertical.svg
static/themes/default/img/network-ethernet.svg

View File

@ -0,0 +1,130 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app to configure Zoph web application
"""
import json
import logging
from django.utils.translation import ugettext_lazy as _
from plinth import actions
from plinth import app as app_module
from plinth import cfg, frontpage, menu
from plinth.modules.apache.components import Webserver
from plinth.modules.backups.components import BackupRestore
from plinth.modules.firewall.components import Firewall
from plinth.utils import format_lazy
from . import manifest
logger = logging.getLogger(__name__)
version = 1
managed_packages = ['zoph']
_description = [
format_lazy(
_('Zoph manages your photo collection. Photos are stored on your '
'{box_name}, under your control. Instead of focusing on galleries '
'for public display, Zoph focuses on managing them for your own '
'use, organizing them by who took them, where they were taken, '
'and who is in them. Photos can be linked to multiple hierarchical '
'albums and categories. It is easy to find all photos containing a '
'person, or photos taken on a date, or photos taken at a location '
'using search, map and calendar views. Individual photos can be '
'shared with others by sending a direct link.'),
box_name=_(cfg.box_name)),
format_lazy(
_('The {box_name} user who setup Zoph will also become the '
'administrator in Zoph. For additional users, accounts must be '
'created both in {box_name} and in Zoph with the same user name.'),
box_name=_(cfg.box_name))
]
app = None
class ZophApp(app_module.App):
"""FreedomBox app for Zoph."""
app_id = 'zoph'
def __init__(self):
"""Create components for the app."""
super().__init__()
info = app_module.Info(app_id=self.app_id, version=version,
name=_('Zoph'), icon_filename='zoph',
short_description=_('Photo Organizer'),
description=_description, manual_page='Zoph',
clients=manifest.clients)
self.add(info)
menu_item = menu.Menu('menu-zoph', info.name, info.short_description,
info.icon_filename, 'zoph:index',
parent_url_name='apps')
self.add(menu_item)
shortcut = frontpage.Shortcut('shortcut-zoph', info.name,
short_description=info.short_description,
icon=info.icon_filename, url='/zoph/',
clients=info.clients,
login_required=True)
self.add(shortcut)
firewall = Firewall('firewall-zoph', info.name,
ports=['http', 'https'], is_external=True)
self.add(firewall)
webserver = Webserver('webserver-zoph', 'zoph',
urls=['https://{host}/zoph/'])
self.add(webserver)
backup_restore = ZophBackupRestore('backup-restore-zoph',
**manifest.backup)
self.add(backup_restore)
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.call('pre', actions.superuser_run, 'zoph', ['pre-install'])
helper.install(managed_packages)
helper.call('post', actions.superuser_run, 'zoph', ['setup'])
helper.call('post', app.enable)
def set_configuration(admin_user=None, enable_osm=None):
"""Configure Zoph."""
args = []
if admin_user:
args += ['--admin-user', admin_user]
if enable_osm is not None:
args += ['--enable-osm', str(enable_osm)]
actions.superuser_run('zoph', ['set-configuration'] + args)
def is_configured():
"""Return whether the Zoph app is configured."""
output = actions.superuser_run('zoph', ['is-configured'])
return output.strip() == 'true'
def get_configuration():
"""Return full configuration of Zoph."""
configuration = actions.superuser_run('zoph', ['get-configuration'])
return json.loads(configuration)
class ZophBackupRestore(BackupRestore):
"""Component to backup/restore Zoph database"""
def backup_pre(self, packet):
"""Save database contents."""
actions.superuser_run('zoph', ['dump-database'])
def restore_post(self, packet):
"""Restore database contents."""
actions.superuser_run('zoph', ['restore-database'])

View File

@ -0,0 +1 @@
plinth.modules.zoph

View File

@ -0,0 +1,16 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app for configuring Zoph.
"""
from django import forms
from django.utils.translation import ugettext_lazy as _
class ZophForm(forms.Form):
"""Zoph application configuration form."""
enable_osm = forms.BooleanField(
label=_('Enable OpenStreetMap for maps'), required=False,
help_text=_('When enabled, requests will be made to OpenStreetMap '
'servers from user\'s browser. This impacts privacy.'))

View File

@ -0,0 +1,21 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
from django.utils.translation import ugettext_lazy as _
clients = [{
'name': _('Zoph'),
'platforms': [{
'type': 'web',
'url': '/zoph/',
}]
}]
backup = {
'data': {
'files': ['/var/lib/plinth/backups-data/zoph-database.sql'],
'directories': ['/var/lib/zoph/']
},
'secrets': {
'files': ['/etc/zoph.ini'],
}
}

View File

@ -0,0 +1,31 @@
{% extends "app.html" %}
{% comment %}
# SPDX-License-Identifier: AGPL-3.0-or-later
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% block pagetitle %}
<h2>{{ name }}</h2>
{% endblock %}
{% block configuration %}
<h3>{% trans "Setup" %}</h3>
<p>
{% blocktrans trimmed with username=request.user.username %}
User account <strong>{{ username }}</strong> will become the administrator
account for Zoph.
{% endblocktrans %}
</p>
<form class="form form-configuration" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-primary" name="zoph_setup"
value="{% trans "Setup" %}"/>
</form>
{% endblock %}

View File

View File

@ -0,0 +1,19 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Functional, browser based tests for zoph app.
"""
from pytest_bdd import given, parsers, scenarios
from plinth.tests import functional
scenarios('zoph.feature')
@given(parsers.parse('the zoph application is setup'))
def _zoph_is_setup(session_browser):
"""Click setup button on the setup page."""
functional.nav_to_module(session_browser, 'zoph')
button = session_browser.find_by_css('input[name="zoph_setup"]')
if button:
functional.submit(session_browser, element=button)

View File

@ -0,0 +1,27 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
@apps @zoph
Feature: Zoph Organises PHotos
Run photo organiser
Background:
Given I'm a logged in user
Given the zoph application is installed
Given the zoph application is setup
Scenario: Enable zoph application
Given the zoph application is disabled
When I enable the zoph application
Then the zoph application is enabled
@backups
Scenario: Backup and restore zoph
Given the zoph application is enabled
When I create a backup of the zoph app data with name test_zoph
And I restore the zoph app data backup with name test_zoph
Then the zoph application is enabled
Scenario: Disable zoph application
Given the zoph application is enabled
When I disable the zoph application
Then the zoph application is disabled

View File

@ -0,0 +1,13 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
URLs for the Zoph module.
"""
from django.conf.urls import url
from .views import SetupView, ZophAppView
urlpatterns = [
url(r'^apps/zoph/setup/$', SetupView.as_view(), name='setup'),
url(r'^apps/zoph/$', ZophAppView.as_view(app_id='zoph'), name='index')
]

View File

@ -0,0 +1,74 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app for configuring Zoph photo organiser.
"""
import logging
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
from plinth import views
from plinth.errors import ActionError
from plinth.modules import zoph
from .forms import ZophForm
logger = logging.getLogger(__name__)
class SetupView(TemplateView):
"""Show zoph setup page."""
template_name = 'zoph-pre-setup.html'
success_url = reverse_lazy('zoph:index')
def get_context_data(self, *args, **kwargs):
"""Provide context data to the template."""
context = super().get_context_data(**kwargs)
context['title'] = zoph.app.info.name
context['app_info'] = zoph.app.info
return context
def post(self, _request, *args, **kwargs):
"""Handle form submission."""
admin_user = self.request.user.get_username()
zoph.set_configuration(admin_user=admin_user)
return HttpResponseRedirect(reverse_lazy('zoph:index'))
class ZophAppView(views.AppView):
"""App configuration page."""
form_class = ZophForm
app_id = 'zoph'
def dispatch(self, request, *args, **kwargs):
"""Redirect to setup page if setup is not done yet."""
if not zoph.is_configured():
return redirect('zoph:setup')
return super().dispatch(request, *args, **kwargs)
def get_initial(self):
"""Get the current settings from Zoph."""
status = super().get_initial()
config = zoph.get_configuration()
status['enable_osm'] = (config['maps.provider'] == 'osm')
return status
def form_valid(self, form):
"""Apply the changes submitted in the form."""
old_status = form.initial
new_status = form.cleaned_data
if old_status['enable_osm'] != new_status['enable_osm']:
try:
zoph.set_configuration(enable_osm=new_status['enable_osm'])
messages.success(self.request, _('Configuration updated.'))
except ActionError:
messages.error(self.request,
_('An error occurred during configuration.'))
return super().form_valid(form)

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,522 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg7631"
height="512"
width="512"
version="1.1"
sodipodi:docname="Camera-photo.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2153"
inkscape:window-height="1248"
id="namedview78"
showgrid="false"
inkscape:zoom="0.86915209"
inkscape:cx="622.88765"
inkscape:cy="-14.518099"
inkscape:window-x="437"
inkscape:window-y="374"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs3">
<linearGradient
id="linearGradient2656">
<stop
id="stop2658"
offset="0" />
<stop
id="stop2660"
stop-opacity="0"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient2290">
<stop
id="stop2292"
stop-color="#555753"
offset="0" />
<stop
id="stop2294"
stop-color="#313330"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3301">
<stop
id="stop3303"
stop-color="#fff"
offset="0" />
<stop
id="stop3305"
stop-color="#cbcbcb"
offset="1" />
</linearGradient>
<radialGradient
id="radialGradient3214"
xlink:href="#linearGradient2656"
gradientUnits="userSpaceOnUse"
cy="36.812"
cx="25.375"
gradientTransform="matrix(1,0,0,0.45357,0,20.115)"
r="17.5" />
<linearGradient
id="linearGradient3222"
y2="30.566"
gradientUnits="userSpaceOnUse"
x2="38.416"
y1="39.051998"
x1="22.549999">
<stop
id="stop2617"
stop-color="#363d40"
offset="0" />
<stop
id="stop2623"
stop-color="#818f95"
offset=".5" />
<stop
id="stop2619"
stop-color="#31383b"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3224"
y2="30.917"
gradientUnits="userSpaceOnUse"
x2="33.903999"
y1="35.303001"
x1="23.660999">
<stop
id="stop2675"
stop-color="#8b8b8b"
offset="0" />
<stop
id="stop2681"
stop-color="#d3d9da"
offset=".5" />
<stop
id="stop2677"
stop-color="#828282"
offset="1" />
</linearGradient>
<radialGradient
id="radialGradient3230"
gradientUnits="userSpaceOnUse"
cy="23.672001"
cx="16.875"
gradientTransform="matrix(1,0,0,1.0135,0,-0.3049)"
r="4.625">
<stop
id="stop2593"
stop-color="#7a7a7a"
offset="0" />
<stop
id="stop2595"
offset="1" />
</radialGradient>
<linearGradient
id="linearGradient3232"
y2="27.073"
gradientUnits="userSpaceOnUse"
x2="18.422001"
y1="18.849001"
x1="12.836">
<stop
id="stop2633"
stop-color="#010101"
offset="0" />
<stop
id="stop2635"
stop-color="#959595"
offset="1" />
</linearGradient>
<radialGradient
id="radialGradient3234"
gradientUnits="userSpaceOnUse"
cy="22.688"
cx="14.75"
gradientTransform="matrix(1,0,0,1.0357,0,-0.81027)"
r="1.75">
<stop
id="stop2820"
stop-color="#fff"
offset="0" />
<stop
id="stop2822"
stop-color="#fff"
stop-opacity="0"
offset="1" />
</radialGradient>
<linearGradient
id="linearGradient3479"
y2="30.191"
xlink:href="#linearGradient3301"
gradientUnits="userSpaceOnUse"
x2="6.5595999"
gradientTransform="matrix(0.65524,0,0,1.8985,15.689,-36.745)"
y1="29.374001"
x1="6.5595999" />
<linearGradient
id="linearGradient3484"
y2="30.191"
xlink:href="#linearGradient3301"
gradientUnits="userSpaceOnUse"
x2="6.5595999"
gradientTransform="matrix(1.4778,0,0,1.9822,19.315,-52.801)"
y1="29.374001"
x1="6.5595999" />
<linearGradient
id="linearGradient3486"
y2="24.701"
gradientUnits="userSpaceOnUse"
x2="7.9151001"
gradientTransform="matrix(1.3184,0,0,0.77319,19.455,-15.414)"
y1="30.816999"
x1="7.9151001">
<stop
id="stop2232"
stop-color="#eee"
offset="0" />
<stop
id="stop2234"
stop-color="#a2a2a2"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3489"
y2="12.847"
gradientUnits="userSpaceOnUse"
x2="29.395"
gradientTransform="translate(0.74746,-1.1933)"
y1="9.4122"
x1="29.395">
<stop
id="stop2270"
stop-color="#555753"
offset="0" />
<stop
id="stop2272"
stop-color="#141514"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3497"
y2="57.125"
gradientUnits="userSpaceOnUse"
x2="35.888"
gradientTransform="translate(0.74746,-1.1933)"
y1="16.125"
x1="16.667">
<stop
id="stop2692"
stop-color="#fff"
offset="0" />
<stop
id="stop2694"
stop-color="#fff"
stop-opacity="0"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3503"
y2="29.719"
xlink:href="#linearGradient2290"
gradientUnits="userSpaceOnUse"
x2="14.844"
gradientTransform="matrix(1.025,0,0,1,0.67951,-1.1933)"
y1="29.719"
x1="7.8705001" />
<linearGradient
id="linearGradient3510"
y2="26.093"
gradientUnits="userSpaceOnUse"
x2="30.129999"
gradientTransform="translate(0.74746,-1.1933)"
y1="26.093"
x1="16.353001">
<stop
id="stop2306"
stop-color="#353633"
offset="0" />
<stop
id="stop2308"
stop-color="#676964"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3513"
y2="29.719"
xlink:href="#linearGradient2290"
gradientUnits="userSpaceOnUse"
x2="14.844"
gradientTransform="matrix(1.0358,0,0,1,0.58499,-0.99129)"
y1="29.719"
x1="7.8705001" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2656"
id="radialGradient885"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,0.45357,0,20.115)"
cx="25.375"
cy="36.812"
r="17.5" />
</defs>
<metadata
id="metadata4">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>Jakub Steiner</dc:title>
</cc:Agent>
</dc:creator>
<dc:source>http://jimmac.musichall.cz</dc:source>
<dc:title>Photo Camera</dc:title>
<dc:subject>
<rdf:Bag>
<rdf:li>camera</rdf:li>
<rdf:li>photo</rdf:li>
<rdf:li>SLR</rdf:li>
</rdf:Bag>
</dc:subject>
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/2.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/2.0/">
<cc:permits
rdf:resource="http://web.resource.org/cc/Reproduction" />
<cc:permits
rdf:resource="http://web.resource.org/cc/Distribution" />
<cc:requires
rdf:resource="http://web.resource.org/cc/Notice" />
<cc:requires
rdf:resource="http://web.resource.org/cc/Attribution" />
<cc:permits
rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
<cc:requires
rdf:resource="http://web.resource.org/cc/ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(0,464)">
<g
id="g916"
transform="matrix(11.768627,0,0,11.768624,-26.453521,-490.44427)">
<path
style="display:block;opacity:0.39873005;fill:url(#radialGradient885)"
inkscape:connector-curvature="0"
display="block"
transform="matrix(0.69319,0,0,0.81102,14.658,8.7634)"
d="m 42.875,36.812 a 17.5,7.9375 0 1 1 -35,0 17.5,7.9375 0 1 1 35,0 z"
id="path2664" />
<path
style="display:block;opacity:0.58228;fill:url(#radialGradient3214)"
inkscape:connector-curvature="0"
display="block"
transform="matrix(1.1575,0,0,0.81102,-5.1236,5.7634)"
d="m 42.875,36.812 a 17.5,7.9375 0 1 1 -35,0 17.5,7.9375 0 1 1 35,0 z"
id="path2654" />
<path
style="display:block;fill:url(#linearGradient3513);stroke:#282828;stroke-width:2"
inkscape:connector-curvature="0"
display="block"
d="m 9.6802,17.727 c -3.4712,0 -6.2792,1.12 -6.2792,2.5 0,0.157 0.157,0.289 0.2265,0.438 -0.0102,0.049 -0.0971,0.041 -0.0971,0.094 0,0 -0.1294,16.312 -0.1294,16.468 0,1.38 2.808,2.5 6.2792,2.5 3.4718,0 6.2798,-1.12 6.2798,-2.5 L 15.83,20.759 c 0,-0.053 -0.087,-0.045 -0.097,-0.094 0.07,-0.149 0.227,-0.281 0.227,-0.438 0,-1.38 -2.808,-2.5 -6.2798,-2.5 z"
id="path2608" />
<path
style="display:block;fill:url(#linearGradient3510);stroke:#282828"
inkscape:connector-curvature="0"
display="block"
d="m 2.8903,37.455 h 40.143 c 1.111,0.101 2.22,-1.15 2.22,-1.858 V 17.95 c -0.101,-1.112 -3.969,-2.424 -4.879,-2.525 h -2.432 l -2.923,-3.085 h -10.557 l -3.436,3.031 c 0,0 -6.727,0.212 -11.941,1.167 -5.2141,0.956 -6.3375,2.588 -6.3375,3.8 l 0.1428,17.117 z"
id="path8417" />
<path
style="display:block;fill:#3f413e"
inkscape:connector-curvature="0"
display="block"
d="m 34.638,12.714 1.364,3.788 1.869,-0.454 z"
id="path2300" />
<path
style="display:block;fill:#585b57;stroke:#a7a7a7;stroke-width:0.7956"
inkscape:connector-curvature="0"
display="block"
transform="matrix(1.2569,0,0,1.2569,-10.939,-11.218)"
d="m 43.134,33.908 a 1.97,1.97 0 1 1 -3.94,0 1.97,1.97 0 1 1 3.94,0 z"
id="path2302" />
<path
style="display:block;fill:url(#linearGradient3222)"
inkscape:connector-curvature="0"
display="block"
transform="matrix(1.0829,0,0,1.0829,-2.573,-5.579)"
d="m 40.714,33.571 a 10.143,10.143 0 1 1 -20.285,0 10.143,10.143 0 1 1 20.285,0 z"
id="path2280" />
<path
style="display:block;fill:url(#linearGradient3224)"
inkscape:connector-curvature="0"
display="block"
transform="matrix(1.0829,0,0,1.0829,-2.4076,-3.6893)"
d="m 40.714,33.571 a 10.143,10.143 0 1 1 -20.285,0 10.143,10.143 0 1 1 20.285,0 z"
id="path2278" />
<path
style="display:block;fill:url(#linearGradient3503)"
inkscape:connector-curvature="0"
display="block"
d="m 9.6802,17.525 c -3.4352,0 -6.214,1.12 -6.214,2.5 0,0.157 0.1554,0.289 0.2242,0.438 -0.0101,0.049 -0.0961,0.041 -0.0961,0.094 0,0 -0.1281,16.312 -0.1281,16.468 0,1.38 2.7788,2.5 6.214,2.5 3.4348,0 6.2138,-1.12 6.2138,-2.5 L 15.766,20.557 c 0,-0.053 -0.086,-0.045 -0.096,-0.094 0.069,-0.149 0.224,-0.281 0.224,-0.438 0,-1.38 -2.779,-2.5 -6.2138,-2.5 z"
id="path7949" />
<path
style="display:block;fill:#7b7e79"
inkscape:connector-curvature="0"
display="block"
d="m 24.991,12.967 -0.757,3.636 -0.354,1.212 -2.778,-2.071 z"
id="path2298" />
<path
style="display:block;fill:#a7aba7"
inkscape:connector-curvature="0"
display="block"
d="m 19.778,26.802 4.91,-8.583 h 11.213 l 4.838,7.116 -4.737,-8.732 H 24.183 Z"
id="path2276" />
<path
style="display:block;fill:#888a85"
inkscape:connector-curvature="0"
display="block"
d="m 21.06,15.682 c 0,0 -12.146,0.638 -16.156,2.75 -0.8523,0.448 -1.4883,0.84 -1.4883,1.442 0,1.38 2.8241,3.026 6.1755,3.026 3.3518,0 5.9998,-1.495 5.9998,-2.875 0,-0.314 -0.18,-0.629 -0.437,-0.906 l 8.625,-1.812 z"
id="path7953" />
<path
style="display:block;opacity:0.41772;fill:none;stroke:url(#linearGradient3497)"
inkscape:connector-curvature="0"
display="block"
d="m 25.301,13.289 -3.918,2.793 c 0,0 -6.444,0.435 -11.249,1.315 -4.8042,0.88 -6.2199,1.834 -6.3449,3.013 l 0.2261,16.591 c 0.296,1.161 3.2301,2.074 6.2288,2.074 2.998,0 5.501,-1.6 5.797,-2.761 l 26.488,0.125 c 1.461,0.03 1.732,-0.701 1.732,-1.353 V 18.409 c 0,-0.712 -2.842,-1.864 -3.679,-1.957 l -2.934,0.062 -3.24,-3.225 z"
id="path2683" />
<path
style="display:block;fill:#2e3436"
inkscape:connector-curvature="0"
display="block"
transform="matrix(1.0829,0,0,1.0829,-2.3034,-2.7622)"
d="m 40.714,33.571 a 10.143,10.143 0 1 1 -20.285,0 10.143,10.143 0 1 1 20.285,0 z"
id="path8415" />
<path
style="fill:url(#radialGradient3230);stroke:url(#linearGradient3232);stroke-width:0.59631002"
inkscape:connector-curvature="0"
transform="matrix(1.677,0,0,1.677,4.2359,-4.2438)"
d="m 19.875,22.562 a 4.125,4.1875 0 1 1 -8.25,0 4.125,4.1875 0 1 1 8.25,0 z"
id="path2589" />
<path
style="opacity:0.60759002;fill:#ffffff;fill-opacity:0.52866002"
inkscape:connector-curvature="0"
transform="matrix(1.677,0,0,1.677,6.2474,-4.3885)"
d="m 16.5,22.688 a 1.75,1.8125 0 1 1 -3.5,0 1.75,1.8125 0 1 1 3.5,0 z"
id="path2599" />
<path
style="opacity:0.37342003;fill:#ffffff"
inkscape:connector-curvature="0"
transform="matrix(1.6732,0,0,1.6732,4.0814,-6.1939)"
d="m 16.5,22.688 a 1.75,1.8125 0 1 1 -3.5,0 1.75,1.8125 0 1 1 3.5,0 z"
id="path2601" />
<path
style="fill:url(#radialGradient3234)"
inkscape:connector-curvature="0"
transform="matrix(0.41273,0,0,0.41273,28.199,27.789)"
d="m 16.5,22.688 a 1.75,1.8125 0 1 1 -3.5,0 1.75,1.8125 0 1 1 3.5,0 z"
id="path8405" />
<path
style="display:block;fill:url(#linearGradient3489)"
inkscape:connector-curvature="0"
display="block"
d="M 22.971,14.381 25.9,12.259 26.809,9.8351 h 6.364 l 1.314,2.9299 2.828,2.424 -2.525,-10.91 -9.9,3e-4 -1.919,10.102 z"
id="path1507" />
<rect
style="fill:url(#linearGradient3484);stroke:url(#linearGradient3486)"
x="25.226"
y="3.4433999"
width="9.3284998"
ry="1.5035"
rx="1.5035"
height="4.5837998"
id="rect3297" />
<path
style="display:block;fill:#2f302e"
inkscape:connector-curvature="0"
display="block"
transform="matrix(0.35944,0,0,0.55434,8.446,-1.1203)"
d="m 14.857,38.214 a 6.0715,2.5000412 0 1 1 -12.143,0 6.0715,2.5000412 0 1 1 12.143,0 z"
id="path2288" />
<path
style="display:block;fill:#c4c6c3"
inkscape:connector-curvature="0"
display="block"
d="m 3.4509,20.057 c -0.1356,1.786 2.4438,3.88 6.1954,3.882 3.8137,0.001 5.8497,-2.184 6.0517,-4.103 l 4.545,-1.718 -5.151,1.01 C 15.193,21.048 13.39,22.959 9.6513,22.9 5.565,22.834 4.0759,21.245 3.4509,20.057 Z"
id="path2312" />
<rect
style="fill:url(#linearGradient3479);stroke:#464646"
x="18.309999"
y="17.124001"
width="4.1362"
ry="0.69542003"
rx="0.69542003"
height="4.3902001"
id="rect2282" />
<path
style="opacity:0.43038001;fill:#ffffff"
inkscape:connector-curvature="0"
transform="matrix(0.38984,0,0,0.38984,5.1999,11.193)"
d="m 16.5,22.688 a 1.75,1.8125 0 1 1 -3.5,0 1.75,1.8125 0 1 1 3.5,0 z"
id="path2612" />
<path
style="fill:#ffffff"
inkscape:connector-curvature="0"
transform="matrix(0.45611,0,0,0.45611,21.05,20.2)"
d="m 16.5,22.688 a 1.75,1.8125 0 1 1 -3.5,0 1.75,1.8125 0 1 1 3.5,0 z"
id="path2629" />
<path
style="display:block;fill:#c5c5c5;fill-opacity:0.52866002"
inkscape:connector-curvature="0"
display="block"
transform="matrix(1.0385,0,0,1.6364,-0.44004,-13.904)"
d="m 42.25,20.562 a 1.625,0.6875 0 1 1 -3.25,0 1.625,0.6875 0 1 1 3.25,0 z"
id="path2641" />
<path
style="display:block;fill:#565656"
inkscape:connector-curvature="0"
display="block"
transform="matrix(1.0385,0,0,1.6364,-0.37754,-13.529)"
d="m 42.25,20.562 a 1.625,0.6875 0 1 1 -3.25,0 1.625,0.6875 0 1 1 3.25,0 z"
id="path2639" />
<path
style="display:block;fill:none;stroke:#777777"
inkscape:connector-curvature="0"
display="block"
d="m 24.019,27.229 c 2.603,-3.368 8.755,-4.193 12.275,-1.041"
id="path2650" />
<path
style="opacity:0.67089;fill:#ffffff"
inkscape:connector-curvature="0"
d="m 30.967,30.625 c -1.62,0 -2.91,1.368 -2.91,3.045 0,0.384 0.247,0.644 0.372,0.982 0.127,0.017 0.208,0.135 0.339,0.135 1.616,0 2.91,-1.338 2.91,-3.012 0,-0.393 -0.274,-0.67 -0.406,-1.015 -0.115,-0.014 -0.186,-0.135 -0.305,-0.135 z"
id="path2668" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB