featherwiki: Add new app

- Uninstall deletes wikis & extensions

- Use Skylark (v1.8.0)

- Add option to upload existing wiki

- Open wiki links in new tab. Since Feather Wiki modifies browser history, it
takes several clicks to go back and reach the FreedomBox app for Feather Wiki if
the user wants to switch to another wiki file. Opening in a new tab also makes
it easy for the user to move text between wikis (i.e. the Refile use case).

- Improve HTML file path handling. Extract only the HTML file name from the URL.
Return a 404 status if the file cannot be found

- Place featherwiki_nest.cgi file in /usr/lib/cgi-bin. The file is installed as
part of the FreedomBox package, rather than a step in the installation of
Feather Wiki.

[sunil]

- Reorganized description to complete the introduction before talking about
FreedomBox implementation.

- Update description to say that only users of 'wiki' group can access.

- Update description to talk about where the wiki is downloaded from how to
upgrade it.

- Update short description to 'Personal Notebooks'.

- Add UsersAndGroups component and to reuse 'wiki' group properly.

- Reorder component to resemble other apps (could prove useful in future).

- Restrict frontpage shortcut to 'wiki' group users.

- Minor styling updates. Run isort.

- Use pathlib.Path object where possible instead of os.path.

- Perform sanitization in privileged methods instead of callers. This leads
better security if the service is compromised.

- Perform duplicate checking in privileged methods instead of callers.

- Check in privileged action that uploaded file originates from temporary
directory. Otherwise, arbitrary files can moved into DAV directory.

- Switch storage path to /var/lib/ which is an application data folder from
/var/www which is a user data folder.

- Add extra security to the DAV folder by explicitly rejecting .htaccess
directives, forcing mime type and removing all options.

- Update SVG/PNG logo icons to adhere to our guidelines.

- Minor template updates. Add required attributes. Improve i18n. Avoid <p>
  inside <p>.

- Refactor tests for more code reuse and fewer globals.

Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net>
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
Joseph Nuthalapati 2024-05-03 00:54:51 +05:30 committed by Sunil Mohan Adapa
parent 35bfe86bda
commit b7c3a06e85
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
17 changed files with 868 additions and 0 deletions

6
debian/copyright vendored
View File

@ -101,6 +101,12 @@ Copyright: 2012 William Theaker
Comment: https://gitlab.com/fdroid/artwork/blob/master/fdroid-logo-2015/fdroid-logo.svg
License: CC-BY-SA-3.0 or GPL-3+
Files: plinth/modules/featherwiki/static/icons/featherwiki.png
plinth/modules/featherwiki/static/icons/featherwiki.svg
Copyright: 2022 Robbie Antenesse <dev@alamantus.com>
Comment: https://codeberg.org/Alamantus/FeatherWiki/src/branch/main/logo.svg
License: AGPL-3+
Files: plinth/modules/gitweb/static/icons/gitweb.png
plinth/modules/gitweb/static/icons/gitweb.svg
Copyright: 2010 Git Authors

View File

@ -0,0 +1,120 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app for Feather Wiki.
This is a FreedomBox-native implementation of a Feather Wiki Nest.
This app doesn't install any Debian packages.
"""
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from plinth import app as app_module
from plinth import cfg, frontpage, menu
from plinth.config import DropinConfigs
from plinth.modules.apache.components import Webserver
from plinth.modules.backups.components import BackupRestore
from plinth.modules.firewall.components import Firewall
from plinth.modules.users.components import UsersAndGroups
from plinth.utils import format_lazy
from . import manifest, privileged
_description = [
format_lazy(
_('Feather Wiki is a tool to create simple self-contained wikis, each '
'stored in a single HTML file on your {box_name}. You can use it as '
'a personal wiki, as a web notebook, or for project documentation.'),
box_name=_(cfg.box_name)),
_('Each wiki is a small file. Create as many wikis as you like, such as '
'one wiki per topic. Customize each wiki to your liking with extensions '
'and other customization options.'),
_('Feather Wiki is downloaded from upstream website and not from Debian. '
'Wikis need to be upgraded to newer version manually.'),
format_lazy(
_('Wikis are not public by default, but they can be downloaded for '
'sharing or publishing. They can be edited by <a href="{users_url}">'
'any user</a> on {box_name} belonging to the wiki group. '
'Simultaneous editing is not supported.'), box_name=_(cfg.box_name),
users_url=reverse_lazy('users:index'))
]
class FeatherWikiApp(app_module.App):
"""FreedomBox app for Feather Wiki."""
app_id = 'featherwiki'
_version = 1
def __init__(self):
"""Create components for the app."""
super().__init__()
groups = {'wiki': _('View and edit wiki applications')}
info = app_module.Info(self.app_id, self._version,
name=_('Feather Wiki'),
icon_filename='featherwiki',
short_description=_('Personal Notebooks'),
description=_description,
manual_page='FeatherWiki',
clients=manifest.clients)
self.add(info)
menu_item = menu.Menu('menu-featherwiki', info.name,
info.short_description, info.icon_filename,
'featherwiki:index', parent_url_name='apps')
self.add(menu_item)
# The shortcut is a simple directory listing provided by Apache server.
# Expecting a large number of wiki files, so creating a shortcut for
# each file (like in ikiwiki's case) will crowd the front page.
shortcut = frontpage.Shortcut(
'shortcut-featherwiki', info.name,
short_description=info.short_description, icon=info.icon_filename,
description=info.description, manual_page=info.manual_page,
url='/featherwiki/', clients=info.clients, login_required=True,
allowed_groups=list(groups))
self.add(shortcut)
dropin_configs = DropinConfigs('dropin-configs-featherwiki', [
'/etc/apache2/conf-available/featherwiki-freedombox.conf',
])
self.add(dropin_configs)
firewall = Firewall('firewall-featherwiki', info.name,
ports=['http', 'https'], is_external=True)
self.add(firewall)
webserver = Webserver('webserver-featherwiki',
'featherwiki-freedombox')
self.add(webserver)
users_and_groups = UsersAndGroups('users-and-groups-featherwiki',
groups=groups)
self.add(users_and_groups)
backup_restore = BackupRestore('backup-restore-featherwiki',
**manifest.backup)
self.add(backup_restore)
def setup(self, old_version=None):
"""Install and configure the app."""
super().setup(old_version)
privileged.setup()
if not old_version:
self.enable()
def uninstall(self):
"""Purge directory with all the wikis."""
super().uninstall()
privileged.uninstall()
def get_wiki_list():
"""List all the Feather Wiki files."""
return sorted([
path.name for path in privileged.wiki_dir.iterdir()
if path.suffix == '.html'
])

View File

@ -0,0 +1,25 @@
##
## On all sites, provide Feather Wiki files on a path: /featherwiki
##
Alias /featherwiki /var/lib/featherwiki
<Location /featherwiki>
Include includes/freedombox-single-sign-on.conf
<IfModule mod_auth_pubtkt.c>
TKTAuthToken "admin" "wiki"
</IfModule>
</Location>
<Directory /var/lib/featherwiki>
Dav On
# Don't accept overrides in .htaccess
AllowOverride None
# Disable following symlinks, show an index page
Options Indexes
# Accept and serve only HTML files
ForceType text/html
</Directory>

View File

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

View File

@ -0,0 +1,37 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Django forms for configuring Feather Wiki."""
from django import forms
from django.core import validators
from django.utils.translation import gettext_lazy as _
class CreateWikiForm(forms.Form):
"""Form to create a new wiki file."""
name = forms.CharField(
label=_('Name of the wiki file, with file extension ".html"'),
strip=True, help_text=_(
'Wiki title and description can be set from within the wiki. '
'This file name is independent of the wiki title.'))
class RenameWikiForm(forms.Form):
"""Form to rename a wiki file."""
new_name = forms.CharField(
label=_('New name for the wiki file, with file extension ".html"'),
strip=True, help_text=_(
'Renaming the file has no effect on the title of the wiki.'))
class UploadWikiForm(forms.Form):
"""Form to upload a wiki file."""
file = forms.FileField(
label=_('A Feather Wiki file with .html file extension'),
required=True, validators=[
validators.FileExtensionValidator(
['html'], _('Feather Wiki files must be in HTML format'))
], help_text=_(
'Upload an existing Feather Wiki file from this computer.'))

View File

@ -0,0 +1,16 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Application manifest for Feather Wiki."""
from django.utils.translation import gettext_lazy as _
from .privileged import wiki_dir
clients = [{
'name': _('Feather Wiki'),
'platforms': [{
'type': 'web',
'url': '/featherwiki/'
}]
}]
backup = {'data': {'directories': [str(wiki_dir)]}}

View File

@ -0,0 +1,93 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Configure Feather Wiki."""
import pathlib
import re
import shutil
import tempfile
import urllib.request
from plinth.actions import privileged
# Needs to be changed on a new release
EMPTY_WIKI_FILE = 'https://feather.wiki/builds/v1.8.x/FeatherWiki_Skylark.html'
wiki_dir = pathlib.Path('/var/lib/featherwiki')
def _set_ownership(path: pathlib.Path):
"""Makes www-data:www-data the owner of the give path."""
shutil.chown(path, user='www-data', group='www-data')
@privileged
def setup():
"""Setup wiki dir and CGI script."""
wiki_dir.mkdir(parents=True, exist_ok=True)
_set_ownership(wiki_dir)
def _normalize_wiki_file_name(name):
"""Return a normalized file name from a wiki name."""
file_name = name.replace(' ', '_')
invalid_characters = r'[\/\\\:\*\?\"\'\<\>\|]'
file_name = re.sub(invalid_characters, '', file_name)
if not file_name.endswith('.html'):
return file_name + '.html'
return file_name
@privileged
def create_wiki(file_name: str):
"""Initialize wiki with the latest version of Feather Wiki."""
file_name = _normalize_wiki_file_name(file_name)
response = urllib.request.urlopen(EMPTY_WIKI_FILE)
file_path = wiki_dir / file_name
if file_path.exists():
raise ValueError('Wiki exists')
file_path.write_bytes(response.read())
_set_ownership(file_path)
@privileged
def add_wiki_file(upload_file_path: str):
"""Add an uploaded wiki file."""
upload_file_path = pathlib.Path(upload_file_path)
temp_dir = tempfile.gettempdir()
if not upload_file_path.is_relative_to(temp_dir):
raise Exception('Uploaded file is not in expected temp directory.')
file_name = _normalize_wiki_file_name(upload_file_path.name)
file_path = wiki_dir / file_name
if file_path.exists():
raise ValueError('Wiki exists')
shutil.move(upload_file_path, file_path)
_set_ownership(file_path)
@privileged
def rename_wiki(old_name: str, new_name: str):
"""Rename wiki file."""
old_name = _normalize_wiki_file_name(old_name)
new_name = _normalize_wiki_file_name(new_name)
file_path = wiki_dir / new_name
if file_path.exists():
raise ValueError('Wiki exists')
(wiki_dir / old_name).rename(file_path)
@privileged
def delete_wiki(file_name: str):
"""Delete one wiki file by name."""
file_name = _normalize_wiki_file_name(file_name)
(wiki_dir / file_name).unlink(missing_ok=True)
@privileged
def uninstall():
"""Delete all the wiki content."""
shutil.rmtree(wiki_dir)

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg2"
sodipodi:docname="featherwiki.svg"
viewBox="0 0 511.99998 511.99999"
version="1.1"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
width="512"
height="512"
inkscape:export-filename="featherwiki.png"
inkscape:export-xdpi="48"
inkscape:export-ydpi="48"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs11" />
<sodipodi:namedview
id="base"
bordercolor="#666666"
inkscape:pageshadow="2"
inkscape:window-y="23"
pagecolor="#ffffff"
inkscape:window-height="1429"
inkscape:window-maximized="0"
inkscape:zoom="1.4283557"
inkscape:window-x="26"
showgrid="false"
borderopacity="1.0"
inkscape:current-layer="layer1"
inkscape:cx="175.37648"
inkscape:cy="344.45202"
inkscape:window-width="1789"
inkscape:pageopacity="0.0"
inkscape:document-units="px"
inkscape:document-rotation="0"
inkscape:snap-global="false"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<g
id="layer1"
inkscape:label="Vrstva 1"
inkscape:groupmode="layer">
<g
id="g907"
transform="matrix(0.51715286,0,0,0.51715286,-2.4633634,-2.5670809)">
<g
id="g850"
transform="matrix(-2.53053,-0.03341354,-0.03341354,-2.53053,1038.0811,2209.6311)">
<path
id="path844"
sodipodi:nodetypes="cccccccsccccccccccsscscsscscaaacsscccccccccccccccccccccc"
style="color:#000000;opacity:1;fill:#19457c;fill-opacity:1;fill-rule:evenodd"
inkscape:connector-curvature="0"
d="m 205.22,478.53 c -23.15688,16.34454 -41.83323,53.12276 -43.6252,79.5 l 18.34,27.97 -18.6,-19.53 c -0.0333,8.4167 0.51776,17.394 1.125,27.094 l 12,14.844 -11.531,-6.375 c 0.34608,5.9871 0.66846,12.155 0.8125,18.688 0.53129,24.095 3.2621,61.668 7.8125,98.219 l 15.16,31.78 -14.03,-23.22 c 1.2784,9.6063 2.6675,19.06 4.1875,28.094 l 14.156,17 -13.5,-13.312 c 1.3937,8.0034 2.8954,15.596 4.4688,22.656 l 14,9.1562 -18.741,-5.1097 c 2.2337,8.2287 16.005,10.247 20.225,13.24 0,0 -18.352,-1.4767 -16.481,0.0653 5.136,4.2312 12.223,5.0179 14.951,6.2104 5.2176,2.2806 3.3255,2.0318 3.3255,2.0318 -1.7183,-0.31873 -8.3403,4.3107 -12.856,4.275 -3.2433,-0.0256 2.6433,0.16019 6.3662,-0.25953 14.31,-4.1628 7.819,-1.0595 8.9684,-1.9324 1.8048,-1.3706 -10.876,5.7242 -13.106,5.8769 -3.1577,0.21605 3.0489,1.4336 5.3547,0.35605 6.3347,-3.0329 12.819,-5.5291 19.655,-1.5848 4.907,2.8316 9.6512,-0.21607 7.8019,-1.3297 -1.7484,-1.8359 0.41271,0.30444 0.2971,0.43753 -1.4859,1.7106 -6.0522,-0.7293 -6.1023,-2.9946 -0.0714,-3.2319 9.9407,-1.7546 8.5089,-4.6529 -0.29476,-0.59665 4.1858,1.4365 -1.8251,0.80916 -2.1331,-0.31119 -2.9558,-0.90497 -4.4048,1.2467 0,0 1.8069,-3.3305 0.92239,-3.0232 -11.553,4.0142 6.8807,-4.0658 9.8015,-11.357 l -13.494,2.5441 10.051,-11.724 c 0.76355,-2.9062 1.512,-5.9353 2.25,-9.0312 l -13.54,7.78 14.06,-10.06 c 0.92178,-3.9543 1.8354,-8.0372 2.7188,-12.25 l -18.188,12.875 18.906,-16.406 c 1.3869,-6.8048 2.7087,-13.9 3.9688,-21.125 l -19.875,22 20.96,-28.38 c 7.2214,-43.29 11.924,-90.896 11.906,-120.09 l -13,8.4375 c 12.83559,-7.69668 13.00208,-13.67041 12.59405,-26.343 l -13.281,12.094 13.188,-15.031 c -0.5639,-15.697 -1.5394,-30.203 -3.5,-42.625 l -18.11,15.38 14.19,-32.69 c -6.41339,-14.04841 -22.03415,-48.83827 -35.28095,-50.219 z" />
<path
id="path846"
style="fill:#f0e2e2;fill-rule:evenodd;stroke:#000000;stroke-width:0.30448;stroke-linecap:round"
inkscape:connector-curvature="0"
d="m 205.03,478.51 c 0.98759,-6.7e-4 1.7961,80.659 1.8047,180.04 0.009,99.385 -0.7859,180.05 -1.7735,180.05 -0.98759,6.7e-4 -1.7961,-80.659 -1.8047,-180.04 -0.009,-99.385 0.78591,-180.05 1.7735,-180.05 z" />
<path
id="path848"
sodipodi:nodetypes="cccc"
style="fill:#000000;fill-rule:evenodd"
inkscape:connector-curvature="0"
d="m 202.28,842.66 c 2.174,-4.381 4.3103,-2.3783 5.4014,0.47766 l -2.514,-28.929 z" />
</g>
<g
id="g842"
transform="matrix(-2.7355499,-0.95049478,-1.0223921,2.7094995,1991.4013,-1095.9208)">
<path
id="path836"
sodipodi:nodetypes="cccccccccccccccccscccccscacssccccccccccccccccccccccc"
style="color:#000000;opacity:1;fill:#1f5598;fill-rule:evenodd"
inkscape:connector-curvature="0"
d="m 205.22,478.53 c -23.15688,16.34454 -41.83323,53.12276 -43.6252,79.5 l 18.34,27.97 -18.6,-19.53 c -0.0333,8.4167 0.51776,17.394 1.125,27.094 l 12,14.844 -11.531,-6.375 c 1.61117,39.57325 4.38737,82.31761 8.625,116.907 l 15.16,31.78 -14.03,-23.22 c 1.2784,9.6063 2.6675,19.06 4.1875,28.094 l 14.156,17 -13.5,-13.312 c 1.3937,8.0034 2.8954,15.596 4.4688,22.656 l 14,9.1562 -18.741,-5.1097 c 2.2337,8.2287 16.005,10.247 20.225,13.24 0,0 -18.352,-1.4767 -16.481,0.0653 5.136,4.2312 12.223,5.0179 14.951,6.2104 0.58151,3.17031 -8.46009,4.5849 -9.5305,6.3068 -3.2433,-0.0256 2.6433,0.16019 6.3662,-0.25953 0,0 4.65117,2.59746 -4.1376,3.9445 -3.1577,0.21605 3.0489,1.4336 5.3547,0.35605 6.3347,-3.0329 12.819,-5.5291 19.655,-1.5848 4.907,2.8316 9.6512,-0.21607 7.8019,-1.3297 -1.7484,-1.8359 0.41271,0.30444 0.2971,0.43753 -1.4859,1.7106 -6.0522,-0.7293 -6.1023,-2.9946 0,0 1.71899,-1.59312 2.279,-2.59704 0.51529,-0.92375 1.8069,-3.3305 0.92239,-3.0232 -11.553,4.0142 6.8807,-4.0658 9.8015,-11.357 l -13.494,2.5441 10.051,-11.724 c 0.76355,-2.9062 1.512,-5.9353 2.25,-9.0312 l -13.54,7.78 14.06,-10.06 c 0.92178,-3.9543 1.8354,-8.0372 2.7188,-12.25 l -18.188,12.875 18.906,-16.406 c 1.3869,-6.8048 2.7087,-13.9 3.9688,-21.125 l -19.875,22 20.96,-28.38 c 7.2214,-43.29 11.924,-90.896 11.906,-120.09 l -13,8.4375 12.969,-10.656 c -0.0506,-5.26738 -0.22316,-10.99106 -0.37495,-15.687 l -13.281,12.094 13.188,-15.031 c -0.5639,-15.697 -1.5394,-30.203 -3.5,-42.625 l -18.11,15.38 14.19,-32.69 c -6.41339,-14.04841 -22.03415,-48.83827 -35.28095,-50.219 z" />
<path
id="path838"
style="fill:#f0e2e2;fill-rule:evenodd;stroke:#000000;stroke-width:0.30448;stroke-linecap:round"
inkscape:connector-curvature="0"
d="m 205.03,478.51 c 0.98759,-6.7e-4 1.7961,80.659 1.8047,180.04 0.009,99.385 -0.7859,180.05 -1.7735,180.05 -0.98759,6.7e-4 -1.7961,-80.659 -1.8047,-180.04 -0.009,-99.385 0.78591,-180.05 1.7735,-180.05 z" />
<path
id="path840"
sodipodi:nodetypes="cccc"
style="fill:#000000;fill-rule:evenodd"
inkscape:connector-curvature="0"
d="m 202.28,842.66 c 2.174,-4.381 4.3103,-2.3783 5.4014,0.47766 l -2.514,-28.929 z" />
</g>
<g
id="g858"
transform="matrix(2.7355499,-0.95049478,1.0223921,2.7094995,-991.40128,-1095.9208)">
<path
id="path852"
sodipodi:nodetypes="ssccccccccccccccccsscscssccssccccccccccccccccccccccs"
style="color:#000000;opacity:1;fill:#1f5598;fill-opacity:0.993151;fill-rule:evenodd"
inkscape:connector-curvature="0"
d="m 205.22,478.53 c -9.3228,0.20557 -32.409,40.138 -38.719,55.875 -2.9559,7.373 -4.3693,15.194 -4.9062,23.625 l 18.34,27.97 -18.6,-19.53 c -0.0333,8.4167 0.51776,17.394 1.125,27.094 l 12,14.844 -11.531,-6.375 c 1.61117,39.57325 4.38737,82.31761 8.625,116.907 l 15.16,31.78 -14.03,-23.22 c 1.2784,9.6063 2.6675,19.06 4.1875,28.094 l 14.156,17 -13.5,-13.312 c 1.3937,8.0034 2.8954,15.596 4.4688,22.656 l 14,9.1562 -18.741,-5.1097 c 2.2337,8.2287 16.005,10.247 20.225,13.24 0,0 -18.352,-1.4767 -16.481,0.0653 5.136,4.2312 12.223,5.0179 14.951,6.2104 5.2176,2.2806 3.3255,2.0318 3.3255,2.0318 -1.7183,-0.31873 -8.3403,4.3107 -12.856,4.275 -3.2433,-0.0256 2.6433,0.16019 6.3662,-0.25953 14.31,-4.1628 7.819,-1.0595 8.9684,-1.9324 1.8048,-1.3706 -10.876,5.7242 -13.106,5.8769 -3.1577,0.21605 3.0489,1.4336 5.3547,0.35605 6.3347,-3.0329 12.819,-5.5291 19.655,-1.5848 0.96206,-2.55384 0.78043,-5.49588 4.2757,-6.48381 2.47806,-0.70042 4.60156,-3.1899 3.71705,-2.8826 -11.553,4.0142 4.08604,-4.2064 7.00684,-11.4976 l -13.494,2.5441 10.051,-11.724 c 0.76355,-2.9062 1.512,-5.9353 2.25,-9.0312 l -13.54,7.78 14.06,-10.06 c 0.92178,-3.9543 1.8354,-8.0372 2.7188,-12.25 l -18.188,12.875 18.906,-16.406 c 1.3869,-6.8048 2.7087,-13.9 3.9688,-21.125 l -19.875,22 20.96,-28.38 c 7.2214,-43.29 11.924,-90.896 11.906,-120.09 l -13,8.4375 12.969,-10.656 c -0.0506,-5.26738 -0.22316,-10.99106 -0.37495,-15.687 l -13.281,12.094 13.188,-15.031 c -0.5639,-15.697 -1.5394,-30.203 -3.5,-42.625 l -18.11,15.38 14.19,-32.69 c -6.41339,-14.04841 -22.03415,-48.83827 -35.28095,-50.219 z" />
<path
id="path854"
style="fill:#f0e2e2;fill-rule:evenodd;stroke:#000000;stroke-width:0.30448;stroke-linecap:round"
inkscape:connector-curvature="0"
d="m 205.03,478.51 c 0.98759,-6.7e-4 1.7961,80.659 1.8047,180.04 0.009,99.385 -0.7859,180.05 -1.7735,180.05 -0.98759,6.7e-4 -1.7961,-80.659 -1.8047,-180.04 -0.009,-99.385 0.78591,-180.05 1.7735,-180.05 z" />
<path
id="path856"
sodipodi:nodetypes="cccc"
style="fill:#000000;fill-rule:evenodd"
inkscape:connector-curvature="0"
d="m 202.28,842.66 c 2.174,-4.381 4.3103,-2.3783 5.4014,0.47766 l -2.514,-28.929 z" />
</g>
</g>
</g>
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work>
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" />
<dc:publisher>
<cc:Agent
rdf:about="http://openclipart.org/">
<dc:title>Openclipart</dc:title>
</cc:Agent>
</dc:publisher>
<dc:date>2012-09-02T06:27:26</dc:date>
<dc:description>a blue feather</dc:description>
<dc:source>https://openclipart.org/detail/172047/feather-by-t-i-n-a-172047</dc:source>
<dc:creator>
<cc:Agent>
<dc:title>T-i-n-a</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>bird</rdf:li>
<rdf:li>blue</rdf:li>
<rdf:li>feather</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/publicdomain/zero/1.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,60 @@
{% extends "app.html" %}
{% comment %}
# SPDX-License-Identifier: AGPL-3.0-or-later
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% block configuration %}
{{ block.super }}
<h3>{% trans "Manage Wikis" %}</h3>
<div class="btn-toolbar">
<a href="{% url 'featherwiki:create' %}" class="btn btn-default"
role="button" title="{% trans 'Create Wiki' %}">
<span class="fa fa-plus" aria-hidden="true"></span>
{% trans 'Create Wiki' %}
</a>
<a href="{% url 'featherwiki:upload' %}" class="btn btn-default"
role="button" title="{% trans 'Upload Wiki' %}">
<span class="fa fa-upload" aria-hidden="true"></span>
{% trans 'Upload Wiki' %}
</a>
</div>
<div class="row">
<div class="col-md-6">
{% if not wikis %}
<p>{% trans 'No wikis available.' %}</p>
{% else %}
<div id="featherwiki-wiki-list" class="list-group list-group-two-column">
{% for wiki in wikis %}
<div class="list-group-item">
<a class="wiki-label" href="/featherwiki/{{ wiki }}" target="_blank"
title="{% blocktrans %}Go to wiki {{ wiki }}{% endblocktrans %}">
{{ wiki }}
</a>
<a href="{% url 'featherwiki:rename' wiki %}"
class="wiki-edit btn btn-default btn-sm secondary"
role="button"
title="{% blocktrans %}Rename wiki {{ wiki }}{% endblocktrans %}">
<span class="fa fa-pencil-square-o" aria-hidden="true"></span>
</a>
<a href="{% url 'featherwiki:delete' wiki %}"
class="wiki-delete btn btn-default btn-sm secondary"
role="button"
title="{% blocktrans %}Delete wiki {{ wiki }}{% endblocktrans %}">
<span class="fa fa-trash-o" aria-hidden="true"></span>
</a>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% comment %}
# SPDX-License-Identifier: AGPL-3.0-or-later
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% block content %}
<h3>
{% blocktrans trimmed %}
Delete wiki <em>{{ name }}</em>
{% endblocktrans %}
</h3>
<p>
{% blocktrans trimmed %}
<strong>Hint</strong>: You can download a copy of this wiki from within
Feather Wiki before deleting it.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
Delete this wiki file permanently?</p>
{% endblocktrans %}
</p>
<form class="form form-delete" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-md btn-danger"
value="{% blocktrans %}Delete{% endblocktrans %}"/>
</form>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% comment %}
# SPDX-License-Identifier: AGPL-3.0-or-later
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% block content %}
<h3>{{ title }}</h3>
<form class="form form-featherwiki" method="post"
enctype="multipart/form-data">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary"
value="{% trans "Upload" %}"/>
</form>
{% endblock %}

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<title>Dummy Feather Wiki File</title>
</head>
<body>
<p> This is a not a real Feather Wiki file. </p>
</body>
</html>

View File

@ -0,0 +1,105 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Functional, browser based tests for Feather Wiki app."""
import pathlib
import pytest
from plinth.tests import functional
pytestmark = [pytest.mark.apps, pytest.mark.featherwiki]
course_1 = 'Computer Organization and Architecture'
file_name_1 = 'Computer_Organization_and_Architecture.html'
class TestFeatherWikiApp(functional.BaseAppTests):
app_name = 'featherwiki'
has_service = False
has_web = True
def _create_wiki_file(self, session_browser):
"""Add a wiki to using the 'Create' functionality."""
functional.nav_to_module(session_browser, 'featherwiki')
wiki_link_1 = f'/featherwiki/{file_name_1}'
if self._get_links_in_app_page(session_browser, wiki_link_1):
return
session_browser.links.find_by_href(
'/plinth/apps/featherwiki/create/').first.click()
session_browser.find_by_id('id_featherwiki-name').fill(course_1)
functional.submit(session_browser, form_class='form-featherwiki')
def _get_links_in_app_page(self, session_browser, link):
"""Return the links matching a href in the app page."""
functional.nav_to_module(session_browser, 'featherwiki')
return session_browser.links.find_by_href(link)
def _get_links_in_apache_listing(self, session_browser, link):
"""Return the links matching a href in the index page."""
default_url = functional.config['DEFAULT']['url']
session_browser.visit(f'{default_url}/featherwiki')
return session_browser.links.find_by_href(link)
def _assert_wiki_present(self, session_browser, file_name, present=True):
"""Assert that a wiki is present."""
wiki_link = f'/featherwiki/{file_name}'
assert bool(self._get_links_in_app_page(session_browser,
wiki_link)) == present
assert bool(
self._get_links_in_apache_listing(session_browser,
file_name)) == present
def _assert_wiki_works(self, session_browser, file_name):
"""Assert that wiki loads and run as expected."""
wiki_link = f'/featherwiki/{file_name}'
default_url = functional.config['DEFAULT']['url']
session_browser.visit(f'{default_url}{wiki_link}')
links = session_browser.links.find_by_href('https://feather.wiki')
assert len(links) == 1
def test_wiki_file_access(self, session_browser):
"""Test creating a new wiki file."""
self._create_wiki_file(session_browser)
self._assert_wiki_present(session_browser, file_name_1)
self._assert_wiki_works(session_browser, file_name_1)
def test_rename_wiki_file(self, session_browser):
"""Test changing the name of a wiki file."""
self._create_wiki_file(session_browser)
new_course = 'A Midsummer Night\'s Dream'
new_file_name = 'A_Midsummer_Nights_Dream.html'
self._get_links_in_app_page(
session_browser, '/plinth/apps/featherwiki/' + file_name_1 +
'/rename/').first.click()
session_browser.find_by_id('id_featherwiki-new_name').fill(new_course)
functional.submit(session_browser, form_class='form-featherwiki')
self._assert_wiki_present(session_browser, new_file_name)
self._assert_wiki_works(session_browser, new_file_name)
def test_upload_wiki_file(self, session_browser):
"""Test uploading an existing wiki file."""
_test_data_dir = pathlib.Path(__file__).parent / 'data'
test_wiki_file = str(_test_data_dir / 'dummy_wiki.html')
session_browser.links.find_by_href(
'/plinth/apps/featherwiki/upload/').first.click()
session_browser.attach_file('featherwiki-file', test_wiki_file)
functional.submit(session_browser, form_class='form-featherwiki')
self._assert_wiki_present(session_browser, 'dummy_wiki.html')
def test_delete_wiki_file(self, session_browser):
"""Test deleting an existing wiki file"""
self._create_wiki_file(session_browser)
self._get_links_in_app_page(
session_browser, '/plinth/apps/featherwiki/' + file_name_1 +
'/delete/').first.click()
functional.submit(session_browser, form_class='form-delete')
self._assert_wiki_present(session_browser, file_name_1, present=False)

View File

@ -0,0 +1,20 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""URLs for the Feather Wiki app."""
from django.urls import re_path
from .views import (CreateWikiView, FeatherWikiAppView, RenameWikiView,
UploadWikiView, delete)
urlpatterns = [
re_path(r'^apps/featherwiki/$', FeatherWikiAppView.as_view(),
name='index'),
re_path(r'^apps/featherwiki/create/$', CreateWikiView.as_view(),
name='create'),
re_path(r'^apps/featherwiki/upload/$', UploadWikiView.as_view(),
name='upload'),
re_path(r'^apps/featherwiki/(?P<old_name>.+\.html)/rename/$',
RenameWikiView.as_view(), name='rename'),
re_path(r'^apps/featherwiki/(?P<name>.+\.html)/delete/$', delete,
name='delete'),
]

View File

@ -0,0 +1,157 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Django views for Feather Wiki."""
import tempfile
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView
from plinth import app as app_module
from plinth import views
from plinth.modules import featherwiki
from . import privileged
from .forms import CreateWikiForm, RenameWikiForm, UploadWikiForm
DUPLICATE_FILE_ERROR = _('A wiki file with the given name already exists.')
class FeatherWikiAppView(views.AppView):
"""Serve configuration page."""
app_id = 'featherwiki'
template_name = 'featherwiki_configure.html'
def get_context_data(self, *args, **kwargs):
"""Add wikis to the context data."""
context = super().get_context_data(*args, **kwargs)
context['wikis'] = featherwiki.get_wiki_list()
return context
class CreateWikiView(SuccessMessageMixin, FormView):
"""View to create a new repository."""
form_class = CreateWikiForm
prefix = 'featherwiki'
template_name = 'form.html'
success_url = reverse_lazy('featherwiki:index')
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Create Wiki')
return context
def form_valid(self, form):
"""Create the repository on valid form submission."""
try:
privileged.create_wiki(form.cleaned_data['name'])
self.success_message = _('Wiki created.')
except ValueError:
messages.error(self.request, DUPLICATE_FILE_ERROR)
except Exception as error:
messages.error(
self.request, "{0} {1}".format(
_('An error occurred while creating the wiki.'), error))
return super().form_valid(form)
class RenameWikiView(SuccessMessageMixin, FormView):
"""View to edit an existing repository."""
form_class = RenameWikiForm
prefix = 'featherwiki'
template_name = 'form.html'
success_url = reverse_lazy('featherwiki:index')
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Rename Wiki')
return context
def form_valid(self, form):
"""Rename the wiki on valid form submission."""
try:
privileged.rename_wiki(self.kwargs['old_name'],
form.cleaned_data['new_name'])
self.success_message = _('Wiki renamed.')
except ValueError:
messages.error(self.request, DUPLICATE_FILE_ERROR)
except Exception as error:
messages.error(
self.request, "{0} {1}".format(
_('An error occurred while renaming the wiki.'), error))
return super().form_valid(form)
class UploadWikiView(SuccessMessageMixin, FormView):
"""View to upload an existing wiki file."""
form_class = UploadWikiForm
prefix = 'featherwiki'
template_name = 'featherwiki_upload_file.html'
success_url = reverse_lazy('featherwiki:index')
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Upload Wiki File')
return context
def form_valid(self, form):
"""Add the wiki file on valid form submission."""
multipart_file = self.request.FILES['featherwiki-file']
try:
with tempfile.TemporaryDirectory() as temp_dir:
wiki_file_name = temp_dir + '/' + multipart_file.name
with open(wiki_file_name, 'wb+') as wiki_file:
for chunk in multipart_file.chunks():
wiki_file.write(chunk)
privileged.add_wiki_file(wiki_file_name)
self.success_message = _('Wiki file added.')
except ValueError:
messages.error(self.request, DUPLICATE_FILE_ERROR)
except Exception as error:
messages.error(
self.request, "{0} {1}".format(_('Failed to add wiki file.'),
error))
return redirect(reverse_lazy('featherwiki:index'))
return super().form_valid(form)
def delete(request, name):
"""Handle deleting wikis, showing a confirmation dialog first.
On GET, display a confirmation page.
On POST, delete the wiki.
"""
app = app_module.App.get('featherwiki')
if request.method == 'POST':
try:
privileged.delete_wiki(name)
messages.success(request, _('{name} deleted.').format(name=name))
except Exception as error:
messages.error(
request,
_('Could not delete {name}: {error}').format(
name=name, error=error))
return redirect(reverse_lazy('featherwiki:index'))
return TemplateResponse(request, 'featherwiki_delete.html', {
'title': app.info.name,
'name': name
})