mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-06-10 11:00:22 +00:00
wordpress: New app to manage a WordPress site/blog
- Use php-fpm instead of using mod-php.
- Create database and setup permissions manually. Tables and initial data are
created during the initial setup process done by WordPress. Database upgrades
are handled by WordPress. Minor versions are upgraded automatically and major
version need user intervention.
- Backup/restore functionality including database.
- Install recommended extensions for performance.
- Setup and run cron jobs to ensure that scheduled publications are
completed (among other things). Service has systemd security features. Timer is
set to run every 10 minutes.
- Functional tests for adding/removing posts and backup/restore.
- Increase file upload size limit to 128MiB.
- A private mode (default) for keeping the setup process secure. Should be
disabled after first setup is completed. This uses a new approach using
file-based flag for different Apache configurations.
TODO:
- Find a nice way to allow WordPress to upload plugins/themes. Currently this
operation files and users are expected to manually scp the files to
/var/lib/wordpress/wp-content/{plugins,themes} directory.
Tests:
- Functional tests.
- Schedule publishing of a post. Notice that post got published.
- Test uploading a file larger than 2MiB.
- Test enabling permalinks. This leads to nicer looking URLs.
- Test adding images to posts/pages.
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
5340cf3119
commit
efa615201b
189
actions/wordpress
Executable file
189
actions/wordpress
Executable file
@ -0,0 +1,189 @@
|
||||
#!/usr/bin/python3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Configuration helper for WordPress.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import pathlib
|
||||
import random
|
||||
import shutil
|
||||
import string
|
||||
import subprocess
|
||||
|
||||
import augeas
|
||||
from plinth import action_utils
|
||||
from plinth.modules.wordpress import PUBLIC_ACCESS_FILE
|
||||
|
||||
_config_file_path = pathlib.Path('/etc/wordpress/config-default.php')
|
||||
_db_file_path = pathlib.Path('/etc/wordpress/database.php')
|
||||
_db_backup_file = pathlib.Path(
|
||||
'/var/lib/plinth/backups-data/wordpress-database.sql')
|
||||
DB_HOST = 'localhost'
|
||||
DB_NAME = 'wordpress_fbx'
|
||||
DB_USER = 'wordpress_fbx'
|
||||
|
||||
|
||||
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('setup',
|
||||
help='Create initial configuration and database')
|
||||
subparsers.add_parser('dump-database', help='Dump database to file')
|
||||
subparsers.add_parser('restore-database',
|
||||
help='Restore database from file')
|
||||
subparser = subparsers.add_parser('set-public',
|
||||
help='Allow/disallow public access')
|
||||
subparser.add_argument('--enable', choices=('True', 'False'),
|
||||
help='Whether to enable or disable public acceess')
|
||||
|
||||
subparsers.required = True
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def subcommand_setup(_):
|
||||
"""Create initial configuration and database for WordPress."""
|
||||
if _db_file_path.exists() or _config_file_path.exists():
|
||||
return
|
||||
|
||||
db_password = _generate_secret_key(16)
|
||||
|
||||
_create_config_file(DB_HOST, DB_NAME, DB_USER, db_password)
|
||||
_create_database(DB_NAME)
|
||||
_set_privileges(DB_HOST, DB_NAME, DB_USER, db_password)
|
||||
|
||||
|
||||
def _create_config_file(db_host, db_name, db_user, db_password):
|
||||
"""Create a PHP configuration file included by WordPress."""
|
||||
secret_keys = [_generate_secret_key() for _ in range(8)]
|
||||
|
||||
config_contents = f'''<?php
|
||||
# Created by FreedomBox
|
||||
include_once('{_db_file_path}');
|
||||
define('DB_NAME', $dbname);
|
||||
define('DB_USER', $dbuser);
|
||||
define('DB_PASSWORD', $dbpass);
|
||||
define('DB_HOST', $dbserver);
|
||||
|
||||
define('AUTH_KEY', '{secret_keys[0]}');
|
||||
define('SECURE_AUTH_KEY', '{secret_keys[1]}');
|
||||
define('LOGGED_IN_KEY', '{secret_keys[2]}');
|
||||
define('NONCE_KEY', '{secret_keys[3]}');
|
||||
define('AUTH_SALT', '{secret_keys[4]}');
|
||||
define('SECURE_AUTH_SALT', '{secret_keys[5]}');
|
||||
define('LOGGED_IN_SALT', '{secret_keys[6]}');
|
||||
define('NONCE_SALT', '{secret_keys[7]}');
|
||||
|
||||
define('WP_CONTENT_DIR', '/var/lib/wordpress/wp-content');
|
||||
|
||||
define('DISABLE_WP_CRON', true);
|
||||
'''
|
||||
_config_file_path.write_text(config_contents)
|
||||
|
||||
db_contents = f'''<?php
|
||||
# Created by FreedomBox
|
||||
$dbuser='{db_user}';
|
||||
$dbpass='{db_password}';
|
||||
$dbname='{db_name}';
|
||||
$dbserver='{db_host}';
|
||||
'''
|
||||
old_umask = os.umask(0o037)
|
||||
try:
|
||||
_db_file_path.write_text(db_contents)
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
shutil.chown(_db_file_path, group='www-data')
|
||||
|
||||
|
||||
def _create_database(db_name):
|
||||
"""Create an empty MySQL database for WordPress."""
|
||||
# Wordpress' install.php creates the tables.
|
||||
# SQL injection is avoided due to known input.
|
||||
query = f'''CREATE DATABASE {db_name};'''
|
||||
subprocess.run(['mysql', '--user', 'root'], input=query.encode(),
|
||||
check=True)
|
||||
|
||||
|
||||
def _set_privileges(db_host, db_name, db_user, db_password):
|
||||
"""Create user, set password and provide permissions on the database."""
|
||||
# SQL injection is avoided due to known input.
|
||||
query = f'''GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,DROP,ALTER
|
||||
ON {db_name}.*
|
||||
TO {db_user}@{db_host}
|
||||
IDENTIFIED BY '{db_password}';
|
||||
FLUSH PRIVILEGES;
|
||||
'''
|
||||
subprocess.run(['mysql', '--user', 'root'], input=query.encode(),
|
||||
check=True)
|
||||
|
||||
|
||||
def _generate_secret_key(length=64, chars=None):
|
||||
"""Generate a new random secret key for use with WordPress."""
|
||||
chars = chars or (string.ascii_letters + string.digits +
|
||||
'!@#$%^&*()-_ []{}<>~`+=,.:/?|')
|
||||
rand = random.SystemRandom()
|
||||
return ''.join(rand.choice(chars) for _ in range(length))
|
||||
|
||||
|
||||
def subcommand_set_public(arguments):
|
||||
"""Allow/disallow public access."""
|
||||
public_access_file = pathlib.Path(PUBLIC_ACCESS_FILE)
|
||||
if arguments.enable == 'True':
|
||||
public_access_file.touch()
|
||||
else:
|
||||
public_access_file.unlink(missing_ok=True)
|
||||
|
||||
action_utils.service_reload('apache2')
|
||||
|
||||
|
||||
def subcommand_dump_database(_):
|
||||
"""Dump database to file."""
|
||||
_db_backup_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with _db_backup_file.open('w') as file_handle:
|
||||
subprocess.run([
|
||||
'mysqldump', '--add-drop-database', '--add-drop-table',
|
||||
'--add-drop-trigger', '--user', 'root', '--databases', DB_NAME
|
||||
], stdout=file_handle, check=True)
|
||||
|
||||
|
||||
def subcommand_restore_database(_):
|
||||
"""Restore database from file."""
|
||||
with _db_backup_file.open('r') as file_handle:
|
||||
subprocess.run(['mysql', '--user', 'root'], stdin=file_handle,
|
||||
check=True)
|
||||
|
||||
_set_privileges(DB_HOST, DB_NAME, DB_USER, _read_db_password())
|
||||
|
||||
|
||||
def _read_db_password():
|
||||
"""Return the password stored in the DB configuration file."""
|
||||
aug = _load_augeas()
|
||||
return aug.get('./$dbpass').strip('\'"')
|
||||
|
||||
|
||||
def _load_augeas():
|
||||
"""Initialize augeas."""
|
||||
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
|
||||
augeas.Augeas.NO_MODL_AUTOLOAD)
|
||||
aug.transform('Phpvars', str(_db_file_path))
|
||||
aug.set('/augeas/context', '/files' + str(_db_file_path))
|
||||
aug.load()
|
||||
|
||||
return aug
|
||||
|
||||
|
||||
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()
|
||||
6
debian/copyright
vendored
6
debian/copyright
vendored
@ -278,6 +278,12 @@ Copyright: 2005 Andrew Dolgov
|
||||
Comment: https://git.tt-rss.org/fox/tt-rss/src/master/images/favicon-72px.png
|
||||
License: GPL-3+
|
||||
|
||||
Files: static/themes/default/icons/wordpress.png
|
||||
static/themes/default/icons/wordpress.svg
|
||||
Copyright: 2011-2021 WordPress Contributors
|
||||
Comment: https://github.com/WordPress/wordpress-develop/blob/master/src/wp-admin/images/wordpress-logo.svg
|
||||
License: GPL-2+
|
||||
|
||||
Files: static/themes/default/icons/windows.png
|
||||
static/themes/default/icons/windows.svg
|
||||
Copyright: 2007 ruli (https://thenounproject.com/2007ruli/)
|
||||
|
||||
119
plinth/modules/wordpress/__init__.py
Normal file
119
plinth/modules/wordpress/__init__.py
Normal file
@ -0,0 +1,119 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
FreedomBox app to configure WordPress.
|
||||
"""
|
||||
|
||||
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.daemon import Daemon
|
||||
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
|
||||
|
||||
PUBLIC_ACCESS_FILE = '/etc/wordpress/is_public'
|
||||
|
||||
version = 1
|
||||
|
||||
managed_services = ['wordpress-freedombox.timer']
|
||||
|
||||
# Add php to avoid wordpress package bringing in lib-apache2-mod-php.
|
||||
# WordPress only supports MySQL/MariaDB as database server.
|
||||
managed_packages = [
|
||||
'wordpress',
|
||||
'php', # Avoid WordPress package bringing in libapache2-mod-php
|
||||
'php-imagick', # Optional, for performance
|
||||
'php-ssh2', # Optional, to upload plugins/themes using SSH connection
|
||||
'php-zip', # Optional, for performance
|
||||
'default-mysql-server', # WordPress only supports MySQL/MariaDB as DB
|
||||
]
|
||||
|
||||
_description = [
|
||||
_('WordPress is a popular way to create and manage websites and blogs. '
|
||||
'Content can be managed using a visual interface. Layout and '
|
||||
'functionality of the web pages can be customized. Appearance can be '
|
||||
'chosen using themes. Administration interface and produced web pages '
|
||||
'are suitable for mobile devices.'),
|
||||
format_lazy(
|
||||
_('You need to run WordPress setup by visiting the app before making '
|
||||
'the site publicly available below. Setup must be run when '
|
||||
'accessing {box_name} with the correct domain name. Enable '
|
||||
'permalinks in administrator interface for better URLs to your '
|
||||
'pages and posts.'), box_name=_(cfg.box_name)),
|
||||
_('WordPress has its own user accounts. First administrator account is '
|
||||
'created during setup. Bookmark the <a '
|
||||
'href="/wordpress/wp-admin/">admin page</a> to reach administration '
|
||||
'interface in the future.'),
|
||||
_('After a major version upgrade, you need to manually run database '
|
||||
'upgrade from administrator interface. Additional plugins or themes may '
|
||||
'be installed and upgraded at your own risk.'),
|
||||
]
|
||||
|
||||
app = None
|
||||
|
||||
|
||||
class WordPressApp(app_module.App):
|
||||
"""FreedomBox app for WordPress."""
|
||||
|
||||
app_id = 'wordpress'
|
||||
|
||||
def __init__(self):
|
||||
"""Create components for the app."""
|
||||
super().__init__()
|
||||
|
||||
info = app_module.Info(
|
||||
app_id=self.app_id, version=version, name=_('WordPress'),
|
||||
icon_filename='wordpress', short_description=_('Website and Blog'),
|
||||
description=_description, manual_page='WordPress',
|
||||
clients=manifest.clients,
|
||||
donation_url='https://wordpressfoundation.org/donate/')
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-wordpress', info.name,
|
||||
info.short_description, info.icon_filename,
|
||||
'wordpress:index', parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
shortcut = frontpage.Shortcut('shortcut-wordpress', info.name,
|
||||
short_description=info.short_description,
|
||||
icon=info.icon_filename,
|
||||
url='/wordpress/', clients=info.clients)
|
||||
self.add(shortcut)
|
||||
|
||||
firewall = Firewall('firewall-wordpress', info.name,
|
||||
ports=['http', 'https'], is_external=True)
|
||||
self.add(firewall)
|
||||
|
||||
webserver = Webserver('webserver-wordpress', 'wordpress-freedombox',
|
||||
urls=['https://{host}/wordpress/'])
|
||||
self.add(webserver)
|
||||
|
||||
daemon = Daemon('daemon-wordpress', managed_services[0])
|
||||
self.add(daemon)
|
||||
|
||||
backup_restore = WordPressBackupRestore('backup-restore-wordpress',
|
||||
**manifest.backup)
|
||||
self.add(backup_restore)
|
||||
|
||||
|
||||
class WordPressBackupRestore(BackupRestore):
|
||||
"""Component to backup/restore WordPress."""
|
||||
|
||||
def backup_pre(self, packet):
|
||||
"""Save database contents."""
|
||||
actions.superuser_run('wordpress', ['dump-database'])
|
||||
|
||||
def restore_post(self, packet):
|
||||
"""Restore database contents."""
|
||||
actions.superuser_run('wordpress', ['restore-database'])
|
||||
|
||||
|
||||
def setup(helper, old_version=None):
|
||||
"""Install and configure the module."""
|
||||
helper.install(managed_packages)
|
||||
helper.call('post', actions.superuser_run, 'wordpress', ['setup'])
|
||||
helper.call('post', app.enable)
|
||||
@ -0,0 +1,56 @@
|
||||
##
|
||||
## On all sites, provide WordPress on a default path: /wordpress
|
||||
##
|
||||
## Requires the following Apache modules to be enabled:
|
||||
## mod_alias
|
||||
## mod_rewrite
|
||||
## mod_proxy_fcgi
|
||||
## mod_auth_pubtkt
|
||||
##
|
||||
|
||||
# Match longer aliases first to meet expectations
|
||||
Alias /wordpress/wp-content /var/lib/wordpress/wp-content
|
||||
Alias /wordpress /usr/share/wordpress
|
||||
|
||||
<Directory /usr/share/wordpress>
|
||||
Options FollowSymLinks
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
RewriteBase /wordpress/
|
||||
RewriteRule ^index\.php$ - [L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /wordpress/index.php [L]
|
||||
</IfModule>
|
||||
|
||||
# Allow access only if site is marked as public or if user is an admin
|
||||
<IfFile !/etc/wordpress/is_public>
|
||||
Include includes/freedombox-single-sign-on.conf
|
||||
<IfModule mod_auth_pubtkt.c>
|
||||
TKTAuthToken "admin"
|
||||
</IfModule>
|
||||
</IfFile>
|
||||
|
||||
# Increase maximum upload file size
|
||||
<IfModule proxy_fcgi_module>
|
||||
ProxyFCGISetEnvIf true PHP_VALUE "post_max_size=128M \n upload_max_filesize = 128M"
|
||||
</IfModule>
|
||||
</Directory>
|
||||
|
||||
<Directory /var/lib/wordpress/wp-content>
|
||||
Options FollowSymLinks
|
||||
|
||||
# Allow access only if site is marked as public or if user is an admin
|
||||
<IfFile !/etc/wordpress/is_public>
|
||||
Include includes/freedombox-single-sign-on.conf
|
||||
<IfModule mod_auth_pubtkt.c>
|
||||
TKTAuthToken "admin"
|
||||
</IfModule>
|
||||
</IfFile>
|
||||
|
||||
<IfFile /etc/wordpress/is_public>
|
||||
Require all granted
|
||||
</IfFile>
|
||||
</Directory>
|
||||
@ -0,0 +1 @@
|
||||
plinth.modules.wordpress
|
||||
@ -0,0 +1,37 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
[Unit]
|
||||
Description=WordPress Scheduled Events Trigger (Cron)
|
||||
Documentation=https://rtcamp.com/tutorials/wordpress/wp-cron-crontab/
|
||||
|
||||
[Service]
|
||||
CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER CAP_NET_ADMIN CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE CAP_KILL CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_LINUX_IMMUTABLE CAP_IPC_LOCK CAP_SYS_CHROOT CAP_BLOCK_SUSPEND CAP_LEASE CAP_SYS_PACCT CAP_SYS_TTY_CONFIG CAP_SYS_BOOT CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYS_NICE CAP_SYS_RESOURCE
|
||||
DevicePolicy=closed
|
||||
ExecStart=php --file /usr/share/wordpress/wp-cron.php
|
||||
Group=www-data
|
||||
LockPersonality=yes
|
||||
NoNewPrivileges=yes
|
||||
PrivateDevices=yes
|
||||
PrivateMounts=yes
|
||||
PrivateTmp=yes
|
||||
PrivateUsers=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectClock=yes
|
||||
ProtectHome=yes
|
||||
ProtectHostname=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectSystem=strict
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
StateDirectory=wordpress/wp-content
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@resources
|
||||
SystemCallFilter=~@privileged
|
||||
SystemCallErrorNumber=EPERM
|
||||
Type=simple
|
||||
User=www-data
|
||||
@ -0,0 +1,11 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
[Unit]
|
||||
Description=WordPress Scheduled Events Trigger (Cron) Timer
|
||||
Documentation=https://rtcamp.com/tutorials/wordpress/wp-cron-crontab/
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*:0/10
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
17
plinth/modules/wordpress/forms.py
Normal file
17
plinth/modules/wordpress/forms.py
Normal file
@ -0,0 +1,17 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
FreedomBox app for configuring WordPress.
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class WordPressForm(forms.Form):
|
||||
"""WordPress configuration form"""
|
||||
|
||||
is_public = forms.BooleanField(
|
||||
label=_('Public access'), required=False, help_text=_(
|
||||
'Allow all visitors. Disabling allows only administrators to view '
|
||||
'the WordPress site or blog. Enable only after performing initial '
|
||||
'WordPress setup.'))
|
||||
21
plinth/modules/wordpress/manifest.py
Normal file
21
plinth/modules/wordpress/manifest.py
Normal file
@ -0,0 +1,21 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
clients = [{
|
||||
'name': _('WordPress'),
|
||||
'platforms': [{
|
||||
'type': 'web',
|
||||
'url': '/wordpress/'
|
||||
}]
|
||||
}]
|
||||
|
||||
backup = {
|
||||
'data': {
|
||||
'files': ['/var/lib/plinth/backups-data/wordpress-database.sql'],
|
||||
'directories': ['/var/lib/wordpress/']
|
||||
},
|
||||
'secrets': {
|
||||
'directories': ['/etc/wordpress/']
|
||||
},
|
||||
}
|
||||
0
plinth/modules/wordpress/tests/__init__.py
Normal file
0
plinth/modules/wordpress/tests/__init__.py
Normal file
167
plinth/modules/wordpress/tests/test_functional.py
Normal file
167
plinth/modules/wordpress/tests/test_functional.py
Normal file
@ -0,0 +1,167 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Functional, browser based tests for WordPress.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from plinth.tests import functional
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', autouse=True)
|
||||
def fixture_background(session_browser):
|
||||
"""Login and install the app."""
|
||||
functional.login(session_browser)
|
||||
functional.install(session_browser, 'wordpress')
|
||||
yield
|
||||
functional.app_disable(session_browser, 'wordpress')
|
||||
|
||||
|
||||
def test_enable_disable(session_browser):
|
||||
"""Test enabling the app."""
|
||||
functional.app_disable(session_browser, 'wordpress')
|
||||
|
||||
functional.app_enable(session_browser, 'wordpress')
|
||||
assert functional.service_is_running(session_browser, 'wordpress')
|
||||
assert functional.is_available(session_browser, 'wordpress')
|
||||
|
||||
functional.app_disable(session_browser, 'wordpress')
|
||||
assert functional.service_is_not_running(session_browser, 'wordpress')
|
||||
assert not functional.is_available(session_browser, 'wordpress')
|
||||
|
||||
|
||||
def test_post(session_browser):
|
||||
"""Test writing a blog post."""
|
||||
functional.app_enable(session_browser, 'wordpress')
|
||||
_write_post(session_browser, 'FunctionalTest')
|
||||
assert _get_post(session_browser, 'FunctionalTest')
|
||||
_delete_post(session_browser, 'FunctionalTest')
|
||||
assert not _get_post(session_browser, 'FunctionalTest')
|
||||
|
||||
|
||||
def test_public_mode(session_browser):
|
||||
"""Test that site is available without login in public mode."""
|
||||
functional.app_enable(session_browser, 'wordpress')
|
||||
_enable_public_mode(session_browser, True)
|
||||
|
||||
def no_login_prompt():
|
||||
_load_site(session_browser)
|
||||
return not functional.is_login_prompt(session_browser)
|
||||
|
||||
try:
|
||||
functional.logout(session_browser)
|
||||
functional.eventually(no_login_prompt)
|
||||
finally:
|
||||
functional.login(session_browser)
|
||||
|
||||
|
||||
def test_private_mode(session_browser):
|
||||
"""Test that site is not available without login in public mode."""
|
||||
functional.app_enable(session_browser, 'wordpress')
|
||||
_enable_public_mode(session_browser, False)
|
||||
|
||||
def login_prompt():
|
||||
_load_site(session_browser)
|
||||
return functional.is_login_prompt(session_browser)
|
||||
|
||||
try:
|
||||
functional.logout(session_browser)
|
||||
functional.eventually(login_prompt)
|
||||
finally:
|
||||
functional.login(session_browser)
|
||||
|
||||
|
||||
@pytest.mark.backups
|
||||
def test_backup(session_browser):
|
||||
"""Test backing up and restoring."""
|
||||
functional.app_enable(session_browser, 'wordpress')
|
||||
_write_post(session_browser, 'FunctionalTest')
|
||||
functional.backup_create(session_browser, 'wordpress', 'test_wordpress')
|
||||
_delete_post(session_browser, 'FunctionalTest')
|
||||
functional.backup_restore(session_browser, 'wordpress', 'test_wordpress')
|
||||
assert _get_post(session_browser, 'FunctionalTest')
|
||||
|
||||
|
||||
def _load_site(browser):
|
||||
"""Visit WordPress site and wait until becomes available."""
|
||||
functional.visit(browser, '/wordpress/wp-admin/')
|
||||
|
||||
def loaded():
|
||||
browser.reload()
|
||||
title_node = browser.find_by_css('title')
|
||||
return (not title_node or '404' not in title_node[0].text)
|
||||
|
||||
functional.eventually(loaded)
|
||||
|
||||
|
||||
def _visit_site(browser):
|
||||
"""Visit WordPress and run the first setup wizard if needed."""
|
||||
_load_site(browser)
|
||||
if '/install.php' in browser.url:
|
||||
browser.fill('weblog_title', 'Test Blog')
|
||||
browser.fill('user_name', functional.config['DEFAULT']['username'])
|
||||
# browser.fill() once does not work for some reason for password field
|
||||
browser.fill('admin_password',
|
||||
functional.config['DEFAULT']['password'])
|
||||
browser.fill('admin_password',
|
||||
functional.config['DEFAULT']['password'])
|
||||
browser.check('pw_weak')
|
||||
browser.fill('admin_email', 'admin@example.org')
|
||||
functional.submit(browser)
|
||||
|
||||
if not browser.find_by_css('.install-success'):
|
||||
raise Exception('WordPress installation failed')
|
||||
|
||||
functional.visit(browser, '/wordpress/wp-admin/')
|
||||
|
||||
if not browser.find_by_id('wpadminbar'):
|
||||
functional.visit(browser, '/wordpress/wp-login.php')
|
||||
browser.fill('log', functional.config['DEFAULT']['username'])
|
||||
browser.fill('pwd', functional.config['DEFAULT']['password'])
|
||||
functional.submit(browser)
|
||||
|
||||
|
||||
def _write_post(browser, title):
|
||||
"""Create a blog post in WordPress site."""
|
||||
post = _get_post(browser, title)
|
||||
if post:
|
||||
_delete_post(browser, title)
|
||||
|
||||
functional.visit(browser, '/wordpress/wp-admin/post-new.php')
|
||||
if browser.find_by_css('.edit-post-welcome-guide'):
|
||||
browser.find_by_css('.components-modal__header button')[0].click()
|
||||
|
||||
browser.find_by_id('post-title-0').fill(title)
|
||||
browser.find_by_css('.editor-post-publish-button__button')[0].click()
|
||||
functional.eventually(browser.find_by_css, ['.editor-post-publish-button'])
|
||||
browser.find_by_css('.editor-post-publish-button')[0].click()
|
||||
|
||||
|
||||
def _delete_post(browser, title):
|
||||
"""Delete a blog post in WordPress site."""
|
||||
post = _get_post(browser, title)
|
||||
if not post:
|
||||
raise Exception('Post not found')
|
||||
|
||||
delete_element = post.find_by_css('.submitdelete')[0]
|
||||
browser.visit(delete_element['href'])
|
||||
|
||||
|
||||
def _get_post(browser, title):
|
||||
"""Return whether a blog post with a given title is available."""
|
||||
_visit_site(browser)
|
||||
functional.visit(browser, '/wordpress/wp-admin/edit.php')
|
||||
xpath = '//tr[contains(@class, "type-post")][.//a[contains(@class, ' \
|
||||
f'"row-title") and contains(text(), "{title}")]]'
|
||||
post = browser.find_by_xpath(xpath)
|
||||
return post[0] if post else None
|
||||
|
||||
|
||||
def _enable_public_mode(browser, should_enable):
|
||||
"""Enable/disable the public mode."""
|
||||
checkbox = browser.find_by_id('id_is_public')
|
||||
if should_enable:
|
||||
checkbox.check()
|
||||
else:
|
||||
checkbox.uncheck()
|
||||
|
||||
functional.submit(browser, form_class='form-configuration')
|
||||
13
plinth/modules/wordpress/urls.py
Normal file
13
plinth/modules/wordpress/urls.py
Normal file
@ -0,0 +1,13 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
URLs for the WordPress module.
|
||||
"""
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .views import WordPressAppView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^apps/wordpress/$', WordPressAppView.as_view(app_id='wordpress'),
|
||||
name='index'),
|
||||
]
|
||||
38
plinth/modules/wordpress/views.py
Normal file
38
plinth/modules/wordpress/views.py
Normal file
@ -0,0 +1,38 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
FreedomBox app for configuring WordPress.
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import ugettext as _
|
||||
from plinth import actions, views
|
||||
|
||||
from . import PUBLIC_ACCESS_FILE
|
||||
from .forms import WordPressForm
|
||||
|
||||
|
||||
class WordPressAppView(views.AppView):
|
||||
"""Serve configuration page."""
|
||||
form_class = WordPressForm
|
||||
app_id = 'wordpress'
|
||||
|
||||
def get_initial(self):
|
||||
"""Get the current WordPress settings."""
|
||||
status = super().get_initial()
|
||||
status['is_public'] = pathlib.Path(PUBLIC_ACCESS_FILE).exists()
|
||||
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['is_public'] != new_status['is_public']:
|
||||
actions.superuser_run(
|
||||
'wordpress',
|
||||
['set-public', '--enable',
|
||||
str(new_status['is_public'])])
|
||||
messages.success(self.request, _('Configuration updated'))
|
||||
|
||||
return super().form_valid(form)
|
||||
BIN
static/themes/default/icons/wordpress.png
Normal file
BIN
static/themes/default/icons/wordpress.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
54
static/themes/default/icons/wordpress.svg
Normal file
54
static/themes/default/icons/wordpress.svg
Normal file
@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512 512"
|
||||
enable-background="new 0 0 64 64"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="wordpress.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
|
||||
inkscape:export-filename="/home/bunny/work/freedombox/freedombox/static/themes/default/icons/wordpress.png"
|
||||
inkscape:export-xdpi="48"
|
||||
inkscape:export-ydpi="48"><metadata
|
||||
id="metadata15"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs13" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1848"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview11"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.77929688"
|
||||
inkscape:cx="458.42259"
|
||||
inkscape:cy="386.63154"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1"
|
||||
inkscape:document-rotation="0" /><style
|
||||
id="style2">.style0{fill: #0073aa;}</style><g
|
||||
id="g8"
|
||||
transform="scale(8)"><g
|
||||
id="g6"><path
|
||||
d="m 4.548,31.999 c 0,10.9 6.3,20.3 15.5,24.706 L 6.925,20.827 C 5.402,24.2 4.5,28 4.5,31.999 Z m 45.983,-1.385 c 0,-3.394 -1.219,-5.742 -2.264,-7.57 -1.391,-2.263 -2.695,-4.177 -2.695,-6.439 0,-2.523 1.912,-4.872 4.609,-4.872 0.121,0 0.2,0 0.4,0.022 C 45.653,7.3 39.1,4.5 32,4.548 c -9.591,0 -18.027,4.921 -22.936,12.4 0.645,0 1.3,0 1.8,0.033 2.871,0 7.316,-0.349 7.316,-0.349 1.479,-0.086 1.7,2.1 0.2,2.3 0,0 -1.487,0.174 -3.142,0.261 l 9.997,29.735 6.008,-18.017 -4.276,-11.718 c -1.479,-0.087 -2.879,-0.261 -2.879,-0.261 -1.48,-0.087 -1.306,-2.349 0.174,-2.262 0,0 4.5,0.3 7.2,0.349 2.87,0 7.317,-0.349 7.317,-0.349 1.479,-0.086 1.7,2.1 0.2,2.262 0,0 -1.489,0.174 -3.142,0.261 l 9.92,29.508 2.739,-9.148 C 49.628,35.7 50.5,33 50.5,30.614 Z M 32.481,34.4 24.244,58.334 c 2.46,0.7 5.1,1.1 7.8,1.1 3.197,0 6.262,-0.552 9.116,-1.556 -0.072,-0.118 -0.141,-0.243 -0.196,-0.379 z M 56.088,18.8 c 0.119,0.9 0.2,1.8 0.2,2.823 0,2.785 -0.521,5.916 -2.088,9.832 l -8.385,24.242 c 8.161,-4.758 13.65,-13.6 13.65,-23.728 C 59.451,27.2 58.2,22.7 56.1,18.83 Z M 32,0 C 14.355,0 0,14.355 0,32 0,49.6 14.4,64 32,64 49.6,64 64,49.645 64,31.999 64,14.4 49.6,0 32,0 Z m 0,62.533 C 15.165,62.533 1.467,48.835 1.467,31.999 1.467,15.2 15.2,1.5 32,1.5 48.8,1.5 62.534,15.2 62.5,32.032 62.533,48.8 48.8,62.5 32,62.533 Z"
|
||||
class="style0"
|
||||
id="path4" /></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
Loading…
x
Reference in New Issue
Block a user