mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-02-04 08:13:38 +00:00
freedombox Debian release 25.1
-----BEGIN PGP SIGNATURE----- iQJKBAABCgA0FiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAmeGbKUWHGp2YWxsZXJv eUBtYWlsYm94Lm9yZwAKCRB3wMdee2UICFoIEADSD7hiSjyG6O3Z3enPfoO8h4Y8 /8rHbe1Is2f+cbSgYaG1gAUYSKvwOYuAniEoHQTxZ9y6ybSX6QtlRezKI8LRKLww oszHI+F8G+/jwkLL0r6RiAYzxAOLdL/mPLubR70g/ykKJRc9sxZbtWUWddD+0Lqy Udl2jv5gcJNDEsVWbWfUalmxcsV+2h5UGAvh+A+6AVe3vCwwO7uijCKOx50YuQbS ODnm2btr40Z4g0zA3nzn3EHq83MJPjjIuxB8UliakmMuGNdctGheQVBXpRY9jfT9 bsJ475BqbvHZ+SnXPUyt/NjARJyAkmP1XL3W1XZCDtoaGh/7Qmc7Pg4XAVe+7wQ0 Z2ESDwANSDbXOAdqAINXIvrHTiRD6hiOxZTuMZT4qNfprJymgC77voLmI3y2vZXq 8Hbpes33z4RVv2lcpvmXoHi9H1/ceM5ag2o/QZVz0EPldRQVMM51hoQc/84dzZVd a5+4lndHU0JbRN5ENY8nNSiagSZdbDnydXo4n8MyvU7azvkSS5puh4+rI4weLHk1 9alCrbTTQ/9ZmQkOdzG8J5rt3YKrQlEJbdddHYCub95ohtkh0B+Xkd18fdneSAF7 MBakd50+uyFkPuuJzERwKIWqqtc/pa4ADr+QnZroPoX5f4Bqms8dDC0liKL1rqr2 xyaUm93Q5NMv52PzIg== =f4J/ -----END PGP SIGNATURE----- gpgsig -----BEGIN PGP SIGNATURE----- iQJKBAABCgA0FiEEfWrbdQ+RCFWJSEvmd8DHXntlCAgFAmeI/cMWHGp2YWxsZXJv eUBtYWlsYm94Lm9yZwAKCRB3wMdee2UICJAsD/9bvBiBPPVEJwPyevv7w3slkSzw 4Ijs84UlcbJ3/4/n1wASLnf8NhqE5wutdvYWIWhGw9qH2mPDhw/zRYBM3sWr1auI slVkGn9KsC/ptWDMKExNFZIwei3sz+HKWpy2BpVyo6LE5KpdwUm1QShRSNf0RLbf WxlU76kekbyXhEiiUz0EJ30ppEum2M2Oxpgfo5JpXOS/qr2icCdpYgtDC57CXAU+ U2Rmw4jejbge/bHUMAzIf6A8OaxG1PCZ6uzDxMdhRlgLrgyVSFczdcnPhRNEiv3c hLYjz/sALreaO6kT0B6Gggxa97DUKova9vWlaG1ygz+zbFVHsP0lJmAdog1XHyCN OjkYKKVmatZq+aIgw2mYHuHpIypkVPHdoKL5hwsGpbMyrTC3keAxjUxSB8qcZsem dnRpesmY+NRpUKeVfKjkOhO5UU59u0aoUqM7P9fiwG53lwpVi/En4gjTBeUEdTUY yR9OKygfkmKyvimUkR3AIcysdlGbY8NTiI1z8BIm1/qjghTYesZhyxBEmQDitip0 zi/CRzJkqxHmDul7BqvkyWMZIZ89qvTkEgBSnQqew5FuVJIuitzPWusLa3WwsZeA YpVxsN+Vo/fqo9m7VCSwV1fmkr6kfamFQAfd1Mj0OldTSROI2vwnCcBgckmGBNzN VpKuNDfRl+afaOIXQQ== =pmJP -----END PGP SIGNATURE----- Merge tag 'v25.1' into debian/bookworm-backports freedombox Debian release 25.1 Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
commit
9ac4384135
48
debian/changelog
vendored
48
debian/changelog
vendored
@ -1,3 +1,51 @@
|
||||
freedombox (25.1) unstable; urgency=medium
|
||||
|
||||
[ gfbdrgng ]
|
||||
* Translated using Weblate (Russian)
|
||||
* Translated using Weblate (Russian)
|
||||
* Translated using Weblate (Russian)
|
||||
|
||||
[ Sunil Mohan Adapa ]
|
||||
* ui: Update section header style to increase size, remove underline
|
||||
* ui: Fix missing variables in Bootstrap 5.2/Debian stable
|
||||
* web_framework: Disable caching templates files in development mode
|
||||
* ui: Drop remnants of already removed background images
|
||||
* ui: js: Load all JS files in deferred mode to speed up page load
|
||||
* ui: Don't place JS file at the bottom of the page
|
||||
* miniflux: Ignore an type check error with pexpect library
|
||||
* app: Allow apps to instantiate without Django initialization
|
||||
* app: Add tags to menu and frontpage components
|
||||
* doc: dev: Remove short description and add tags to all components
|
||||
* app: Stop showing short description on installation page
|
||||
* apps: Only show app tags all the tags in apps page search box
|
||||
* views: Use tags from menu or shortcut instead of the app
|
||||
* privacy: Introduce utility to lookup external IP address
|
||||
* privacy: Add option in UI to set lookup URL for public IPs
|
||||
* privacy: Show notification for privacy settings again
|
||||
* dynamicdns: Use the public IP lookup URL from privacy app
|
||||
* email: Create DKIM keys for all known domains
|
||||
* email: Show DNS entries for all domains instead of just primary
|
||||
* backups: Handle error when there is not enough space on disk
|
||||
* backups: Add warning that services may become unavailable
|
||||
* backups: Properly cleanup after downloading an archive
|
||||
* backups: Make all generated archive names consistent
|
||||
* email: Fix regression error when installing/operation app
|
||||
|
||||
[ Veiko Aasa ]
|
||||
* deluge: tests: functional: Fix deluge client logged in detection
|
||||
|
||||
[ Dietmar ]
|
||||
* Translated using Weblate (German)
|
||||
|
||||
[ Benedek Nagy ]
|
||||
* email: Show reverse DNS entries to be configured
|
||||
|
||||
[ James Valleroy ]
|
||||
* locale: Update translation strings
|
||||
* doc: Fetch latest manual
|
||||
|
||||
-- James Valleroy <jvalleroy@mailbox.org> Mon, 13 Jan 2025 21:13:33 -0500
|
||||
|
||||
freedombox (24.26.1~bpo12+1) bookworm-backports; urgency=medium
|
||||
|
||||
* Rebuild for bookworm-backports.
|
||||
|
||||
@ -30,23 +30,21 @@ function normally.
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
info = app_module.Info(app_id=self.app_id, version=1,
|
||||
name=_('Transmission'),
|
||||
icon_filename='transmission',
|
||||
short_description=_('BitTorrent Web Client'),
|
||||
description=description,
|
||||
manual_page='Transmission',
|
||||
clients=manifest.clients,
|
||||
donation_url='https://transmissionbt.com/donate/')
|
||||
info = app_module.Info(
|
||||
app_id=self.app_id, version=1, name=_('Transmission'),
|
||||
icon_filename='transmission', description=_description,
|
||||
manual_page='Transmission', clients=manifest.clients,
|
||||
donation_url='https://transmissionbt.com/donate/',
|
||||
tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
The first argument is app_id that is same as the ID for the app. The version is
|
||||
the version number for this app that must be incremented whenever setup() method
|
||||
needs to be called again. name, icon_filename, short_description, description,
|
||||
manual_page and clients provide information that is shown on the app's main
|
||||
page. The donation_url encourages our users to contribute to upstream projects
|
||||
in order ensure their long term sustainability. More information about the
|
||||
parameters is available in :class:`~plinth.app.Info` class documentation.
|
||||
needs to be called again. name, icon_filename, description, manual_page,
|
||||
clients, and tags provide information that is shown on the app's main page. The
|
||||
donation_url encourages our users to contribute to upstream projects in order
|
||||
ensure their long term sustainability. More information about the parameters is
|
||||
available in :class:`~plinth.app.Info` class documentation.
|
||||
|
||||
The description of app should provide basic information on what the app is about
|
||||
and how to use it. It is impractical, however, to explain everything about the
|
||||
@ -322,22 +320,24 @@ when they visit FreedomBox. To provide this shortcut, a
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
shortcut = frontpage.Shortcut(
|
||||
'shortcut-transmission', name, short_description=short_description,
|
||||
icon='transmission', url='/transmission', clients=clients,
|
||||
login_required=True, allowed_groups=[group[0]])
|
||||
shortcut = frontpage.Shortcut('shortcut-transmission', info.name,
|
||||
icon=info.icon_filename,
|
||||
url='/transmission',
|
||||
clients=info.clients, tags=info.tags,
|
||||
login_required=True,
|
||||
allowed_groups=list(groups))
|
||||
self.add(shortcut)
|
||||
|
||||
The first parameter, as usual, is a unique ID. The next three parameters are
|
||||
basic information about the app similar to the menu item. The URL parameter
|
||||
specifies the URL that the user should be directed to when the shortcut is
|
||||
clicked. This is the web interface provided by our app. The next parameter
|
||||
provides a list of clients. This is useful for the FreedomBox mobile app when
|
||||
the information is used to suggest installing mobile apps. This is described in
|
||||
a later section of this tutorial. The next parameter specifies whether anonymous
|
||||
users who are not logged into FreedomBox should be shown this shortcut. The
|
||||
final parameter further restricts to which group of users this shortcut must be
|
||||
shown.
|
||||
The first parameter, as usual, is a unique ID. The next two parameters are basic
|
||||
information about the app similar to the menu item. The URL parameter specifies
|
||||
the URL that the user should be directed to when the shortcut is clicked. This
|
||||
is the web interface provided by our app. The next parameter provides a list of
|
||||
clients. This is useful for the FreedomBox mobile app when the information is
|
||||
used to suggest installing mobile apps. This is described in a later section of
|
||||
this tutorial. The next parameter specifies the list of tags to show on the
|
||||
shortcut. The next parameter specifies whether anonymous users who are not
|
||||
logged into FreedomBox should be shown this shortcut. The final parameter
|
||||
further restricts to which group of users this shortcut must be shown.
|
||||
|
||||
Adding backup/restore functionality
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@ -89,22 +89,23 @@ the Django's localization methods to make that happen.
|
||||
|
||||
info = app_module.Info(...
|
||||
name=_('Transmission'),
|
||||
description=[_('Transmission is a...'),
|
||||
_('BitTorrent is a peer-to-peer...')],
|
||||
...
|
||||
short_description=_('BitTorrent Web Client'),
|
||||
tags=[_('File sharing'), _('BitTorrent'), ...])
|
||||
...)
|
||||
|
||||
Notice that the app's name, description, etc. are wrapped in the ``_()`` method
|
||||
call. This needs to be done for the rest of our app. We use the
|
||||
Notice that the app's name, description, tags, etc. are wrapped in the ``_()``
|
||||
method calls. This needs to be done for the rest of our app. We use the
|
||||
:obj:`~django.utils.translation.gettext_lazy` in some cases and we use the
|
||||
regular :obj:`~django.utils.translation.gettext` in other cases. This is
|
||||
because in the second case the :obj:`~django.utils.translation.gettext` lookup
|
||||
is made once and reused for every user looking at the interface. These users may
|
||||
each have a different language set for their interface. Lookup made for one
|
||||
language for a user should not be used for other users. The ``_lazy`` methods
|
||||
provided by Django makes sure that the return value is an object that will
|
||||
actually be converted to string at the final moment when the string is being
|
||||
displayed. In the first case, the lookup is made and string is returned
|
||||
immediately.
|
||||
regular :obj:`~django.utils.translation.gettext` in other cases. This is because
|
||||
in the second case the :obj:`~django.utils.translation.gettext` lookup is made
|
||||
once and reused for every user looking at the interface. These users may each
|
||||
have a different language set for their interface. Lookup made for one language
|
||||
for a user should not be used for other users. The ``_lazy`` methods provided by
|
||||
Django makes sure that the return value is an object that will actually be
|
||||
converted to string at the final moment when the string is being displayed. In
|
||||
the first case, the lookup is made and string is returned immediately.
|
||||
|
||||
All of this is the usual way internationalization is done in Django. See
|
||||
:doc:`Internationalization and localization <django:topics/i18n/index>`
|
||||
|
||||
@ -45,7 +45,7 @@ a link in FreedomBox web interface. Let us add a link in the apps list. In
|
||||
...
|
||||
|
||||
menu_item = menu.Menu('menu-transmission', 'Transmission',
|
||||
'BitTorrrent Web Client', 'transmission',
|
||||
'transmission', info.tags,
|
||||
'transmission:index', parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
@ -61,12 +61,12 @@ menu item we want to present.
|
||||
* The second parameter is the display name to use for our menu item which
|
||||
happens to be the name of the app as well.
|
||||
|
||||
* The third parameter is a short description for the menu item.
|
||||
|
||||
* The fourth parameter is the name of the icon to use when showing the menu
|
||||
* The third parameter is the name of the icon to use when showing the menu
|
||||
item. An SVG file and a PNG should be created in the
|
||||
``plinth/modules/transmission/static/icons/`` directory.
|
||||
|
||||
* The fourth parameter is the list of tags to show on the menu item.
|
||||
|
||||
* The fifth parameter is the URL that the user should be directed to when the
|
||||
menu item is clicked. This is a Django URL name and we have already created a
|
||||
URL with this name. Note that when including our app's URLs, FreedomBox will
|
||||
|
||||
@ -24,16 +24,6 @@ Although untested, the following similar hardware is also likely to work well wi
|
||||
* [[http://www.pcengines.ch/apu3b2.htm|apu3b2]]
|
||||
* [[http://www.pcengines.ch/apu3b4.htm|apu3b4]]
|
||||
|
||||
* Using i386 image:
|
||||
* [[http://www.pcengines.ch/alix1d.htm|alix1d]]
|
||||
* [[http://www.pcengines.ch/alix1e.htm|alix1e]]
|
||||
* [[http://www.pcengines.ch/alix2d2.htm|alix2d2]]
|
||||
* [[http://www.pcengines.ch/alix2d3.htm|alix2d3]]
|
||||
* [[http://www.pcengines.ch/alix2d13.htm|alix2d13]]
|
||||
* [[http://www.pcengines.ch/alix3d2.htm|alix3d2]]
|
||||
* [[http://www.pcengines.ch/alix3d3.htm|alix3d3]]
|
||||
* [[http://www.pcengines.ch/alix6f2.htm|alix6f2]]
|
||||
|
||||
=== Download ===
|
||||
|
||||
!FreedomBox disk [[FreedomBox/Download|images]] for this hardware are available. Follow the instructions on the [[FreedomBox/Download|download]] page to create a !FreedomBox SD card, USB disk, SSD or hard drive and boot into !FreedomBox. Pick the image meant for all amd64 machines.
|
||||
|
||||
80
doc/manual/en/Customization.raw.wiki
Normal file
80
doc/manual/en/Customization.raw.wiki
Normal file
@ -0,0 +1,80 @@
|
||||
== FreedomBox Customization ==
|
||||
|
||||
<<TableOfContents()>>
|
||||
|
||||
## BEGIN_INCLUDE
|
||||
Though !FreedomBox's philosophy is to have the user make as few decisions as possible about the !FreedomBox itself, a few options for customization have been provided to facilitate some advanced use cases.
|
||||
|
||||
|
||||
=== Change Default App ===
|
||||
|
||||
''Available since version:'' 0.36.0 <<BR>>
|
||||
''Skill level:'' Basic
|
||||
|
||||
''Use Case'': A !FreedomBox that primarily runs only one public-facing application whose web application is set as the landing page when someone visits the domain name of the !FreedomBox over the internet. <<BR>>
|
||||
e.g. A university using !MediaWiki running on !FreedomBox as a course wiki wants its students typing in the domain name into their browser to directly go to the wiki bypassing the !FreedomBox home page.
|
||||
|
||||
''Configuration:'' Change the [[FreedomBox/Manual/Configure#Default_App|Default App]] in the configure page to whichever app you want to be served as default.
|
||||
|
||||
=== Custom Shortcuts ===
|
||||
|
||||
''Available since version:'' 0.40.0 <<BR>>
|
||||
''Skill level:'' Advanced
|
||||
|
||||
''Use Case:'' The administrator of a community deployment of !FreedomBox manually installs a few additional unsupported applications on the !FreedomBox and wants users to be able to transparently access them through the web and mobile applications of !FreedomBox.
|
||||
|
||||
''Note:'' This feature is meant to be used with applications that are end-user facing, i.e have a web or mobile client.
|
||||
|
||||
'''Configuration:'''
|
||||
|
||||
Create a file called `custom-shortcuts.json` in Plinth's configuration directory `/etc/plinth` and add additional shortcuts in JSON format. The file should have follow the same JSON schema as the Plinth API. You can refer to the JSON schema by visiting https://<my-freedombox-url>/plinth/api/1/shortcuts.
|
||||
|
||||
An example file adding one additional shortcut for [[https://nextcloud.com|NextCloud]].
|
||||
|
||||
{{{#!highlight json
|
||||
{
|
||||
"shortcuts": [{
|
||||
"name": "NextCloud",
|
||||
"description": ["Nextcloud is a suite of client-server software for creating and using file hosting services."],
|
||||
"icon_url": "/plinth/custom/static/icons/nextcloud.png",
|
||||
"clients": [{
|
||||
"name": "nextcloud",
|
||||
"platforms": [{
|
||||
"type": "web",
|
||||
"url": "/nextcloud"
|
||||
}]
|
||||
}],
|
||||
"tags" : ["Groupware", "File sync"]
|
||||
}]
|
||||
}
|
||||
}}}
|
||||
|
||||
The corresponding icons for the shortcuts listed in the above file should be placed in the directory `/var/www/plinth/custom/static/icons/`. The file names of the icons should match with those provided in `/etc/plinth/custom-shortcuts.json`.
|
||||
|
||||
After adding an entry for !NextCloud in custom-shortcuts.json and an icon, restart Plinth by executing the command {{{ systemctl restart plinth }}} on the !FreedomBox. You can also restart the !FreedomBox from the web interface.
|
||||
|
||||
After restart the Plinth home page will display an additional shortcut for !NextCloud as shown below: <<BR>>
|
||||
{{attachment:nextcloud-frontpage-shortcut.png|NextCloud custom shortcut on the Plinth home page}}
|
||||
|
||||
The same shortcut will also be displayed in any Android apps connected to the !FreedomBox. <<BR>>
|
||||
|
||||
{{attachment:android-app-custom-shortcut.jpg|NextCloud custom shortcut in the Android app}}
|
||||
|
||||
=== Custom Styling ===
|
||||
|
||||
''Available since version:'' 24.25 <<BR>>
|
||||
''Skill level:'' Advanced
|
||||
|
||||
''Use Case:'' The administrator of a community or home deployment of !FreedomBox wants to customize the web page styling of !FreedomBox.
|
||||
|
||||
''Configuration'': Create a file in the path `/var/www/plinth/custom/static/css/user.css` and write [[https://developer.mozilla.org/en-US/docs/Web/CSS|CSS]] styling rules. This
|
||||
file has the highest priority as per the [[https://developer.mozilla.org/en-US/docs/Web/CSS/Cascade|cascading rules]]. Use the web browser's developer console to understand which rules to override and how much [[https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity|specificity]] is needed.
|
||||
|
||||
{{attachment:customization_styling.png|Home page with customized styling}}
|
||||
|
||||
## END_INCLUDE
|
||||
|
||||
<<Include(FreedomBox/Portal)>>
|
||||
|
||||
----
|
||||
CategoryFreedomBox
|
||||
@ -160,7 +160,6 @@ All !FreedomBox disk images for different hardware is built by the project using
|
||||
|
||||
|| '''Image''' || '''Includes main?''' || '''Includes non-free-firmware?''' || '''Non-free firmware included''' ||
|
||||
|| 32-bit ARM (armhf) || (./) || || ||
|
||||
|| 32-bit x86 (i386) || (./) || (./) || DebianPkg:amd64-microcode, DebianPkg:intel-microcode (see [[Microcode]]) ||
|
||||
|| 64-bit ARM (arm64) || (./) || || ||
|
||||
|| 64-bit x86 (amd64) || (./) || (./) || DebianPkg:amd64-microcode, DebianPkg:intel-microcode (see [[Microcode]]) ||
|
||||
|| A20 OLinuXino Lime || (./) || || ||
|
||||
@ -178,7 +177,6 @@ All !FreedomBox disk images for different hardware is built by the project using
|
||||
|| Pine A64+ || (./) || || ||
|
||||
|| Pioneer Edition !FreedomBox || (./) || || ||
|
||||
|| QEMU/KVM amd64 || (./) || || ||
|
||||
|| QEMU/KVM i386 || (./) || || ||
|
||||
|| Raspberry Pi 2 || (./) || (./) || DebianPkg:raspi-firmware ||
|
||||
|| Raspberry Pi 3 Model B || (./) || (./) || DebianPkg:raspi-firmware, DebianPkg:firmware-brcm80211 ||
|
||||
|| Raspberry Pi 3 Model B+ || (./) || (./) || DebianPkg:raspi-firmware, DebianPkg:firmware-brcm80211 ||
|
||||
@ -186,7 +184,6 @@ All !FreedomBox disk images for different hardware is built by the project using
|
||||
|| Rock64 || (./) || || ||
|
||||
|| !RockPro64 || (./) || || ||
|
||||
|| !VirtualBox for amd64 || (./) || || ||
|
||||
|| !VirtualBox for i386 || (./) || || ||
|
||||
|
||||
## END_INCLUDE
|
||||
|
||||
|
||||
@ -8,6 +8,41 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
|
||||
|
||||
The following are the release notes for each !FreedomBox version.
|
||||
|
||||
== FreedomBox 25.1 (2025-01-13) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
* email: Show DNS entries for all domains instead of just primary
|
||||
* privacy: Add option in UI to set lookup URL for public IPs
|
||||
|
||||
=== Other Changes ===
|
||||
|
||||
* app: Add tags to menu and frontpage components
|
||||
* app: Allow apps to instantiate without Django initialization
|
||||
* app: Stop showing short description on installation page
|
||||
* apps: Only show app tags all the tags in apps page search box
|
||||
* backups: Add warning that services may become unavailable
|
||||
* backups: Handle error when there is not enough space on disk
|
||||
* backups: Make all generated archive names consistent
|
||||
* backups: Properly cleanup after downloading an archive
|
||||
* deluge: tests: functional: Fix deluge client logged in detection
|
||||
* doc: dev: Remove short description and add tags to all components
|
||||
* dynamicdns: Use the public IP lookup URL from privacy app
|
||||
* email: Create DKIM keys for all known domains
|
||||
* email: Fix regression error when installing/operation app
|
||||
* email: Show reverse DNS entries to be configured
|
||||
* locale: Translated using Weblate (German, Russian)
|
||||
* miniflux: Ignore an type check error with pexpect library
|
||||
* privacy: Introduce utility to lookup external IP address
|
||||
* privacy: Show notification for privacy settings again
|
||||
* ui: Don't place JS file at the bottom of the page
|
||||
* ui: Drop remnants of already removed background images
|
||||
* ui: Fix missing variables in Bootstrap 5.2/Debian stable
|
||||
* ui: Update section header style to increase size, remove underline
|
||||
* ui: js: Load all JS files in deferred mode to speed up page load
|
||||
* views: Use tags from menu or shortcut instead of the app
|
||||
* web_framework: Disable caching templates files in development mode
|
||||
|
||||
== FreedomBox 24.26.1 (2025-01-05) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
@ -175,7 +175,7 @@ If you want to mount images locally, use the following to copy built images off
|
||||
|
||||
{{{
|
||||
$ mkdir /tmp/vbox-img1 /tmp/vbox-root1
|
||||
$ vdfuse -f freedombox-unstable_2013.0519_virtualbox-i386-hdd.vdi /tmp/vbox-img1/
|
||||
$ vdfuse -f freedombox-unstable_2013.0519_virtualbox-amd64-hdd.vdi /tmp/vbox-img1/
|
||||
$ sudo mount -o loop /tmp/vbox-img1/Partition1 /tmp/vbox-root1
|
||||
$ cp /tmp/vbox-root1/home/fbx/freedom-maker/build/freedom*vdi ~/
|
||||
$ sudo umount /tmp/vbox-root1
|
||||
|
||||
@ -107,6 +107,10 @@
|
||||
|
||||
<<Include(FreedomBox/ReleaseNotes, , from="## BEGIN_INCLUDE", to="## END_INCLUDE")>>
|
||||
|
||||
= Customizing =
|
||||
|
||||
<<Include(FreedomBox/Customization, , from="## BEGIN_INCLUDE", to="## END_INCLUDE")>>
|
||||
|
||||
= Contributing =
|
||||
|
||||
<<Include(FreedomBox/Contribute, , from="## BEGIN_INCLUDE", to="## END_INCLUDE")>>
|
||||
|
||||
BIN
doc/manual/en/images/android-app-custom-shortcut.jpg
Normal file
BIN
doc/manual/en/images/android-app-custom-shortcut.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
doc/manual/en/images/customization_styling.png
Normal file
BIN
doc/manual/en/images/customization_styling.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
BIN
doc/manual/en/images/nextcloud-frontpage-shortcut.png
Normal file
BIN
doc/manual/en/images/nextcloud-frontpage-shortcut.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@ -24,16 +24,6 @@ Although untested, the following similar hardware is also likely to work well wi
|
||||
* [[http://www.pcengines.ch/apu3b2.htm|apu3b2]]
|
||||
* [[http://www.pcengines.ch/apu3b4.htm|apu3b4]]
|
||||
|
||||
* Using i386 image:
|
||||
* [[http://www.pcengines.ch/alix1d.htm|alix1d]]
|
||||
* [[http://www.pcengines.ch/alix1e.htm|alix1e]]
|
||||
* [[http://www.pcengines.ch/alix2d2.htm|alix2d2]]
|
||||
* [[http://www.pcengines.ch/alix2d3.htm|alix2d3]]
|
||||
* [[http://www.pcengines.ch/alix2d13.htm|alix2d13]]
|
||||
* [[http://www.pcengines.ch/alix3d2.htm|alix3d2]]
|
||||
* [[http://www.pcengines.ch/alix3d3.htm|alix3d3]]
|
||||
* [[http://www.pcengines.ch/alix6f2.htm|alix6f2]]
|
||||
|
||||
=== Download ===
|
||||
|
||||
!FreedomBox disk [[FreedomBox/Download|images]] for this hardware are available. Follow the instructions on the [[FreedomBox/Download|download]] page to create a !FreedomBox SD card, USB disk, SSD or hard drive and boot into !FreedomBox. Pick the image meant for all amd64 machines.
|
||||
|
||||
@ -166,7 +166,6 @@ Todas las imágenes de disco de !FreedomBox para hardware diferente las compila
|
||||
|
||||
|| '''Imagen''' || '''¿Incluye ''main''?''' || '''¿Incluye ''non-free-firmware''?''' || '''Firmware privativo incluído''' ||
|
||||
|| 32-bit ARM (armhf) || (./) || || ||
|
||||
|| 32-bit x86 (i386) || (./) || (./) || DebianPkg:amd64-microcode, DebianPkg:intel-microcode (Ver [[Microcode|Microcódigo]]) ||
|
||||
|| 64-bit ARM (arm64) || (./) || || ||
|
||||
|| 64-bit x86 (amd64) || (./) || (./) || DebianPkg:amd64-microcode, DebianPkg:intel-microcode (Ver [[Microcode|Microcódigo]]) ||
|
||||
|| A20 OLinuXino Lime || (./) || || ||
|
||||
@ -184,7 +183,6 @@ Todas las imágenes de disco de !FreedomBox para hardware diferente las compila
|
||||
|| Pine A64+ || (./) || || ||
|
||||
|| Pioneer Edition !FreedomBox || (./) || || ||
|
||||
|| QEMU/KVM amd64 || (./) || || ||
|
||||
|| QEMU/KVM i386 || (./) || || ||
|
||||
|| Raspberry Pi 2 || (./) || (./) || DebianPkg:raspi-firmware ||
|
||||
|| Raspberry Pi 3 Model B || (./) || (./) || DebianPkg:raspi-firmware , DebianPkg:firmware-brcm80211 ||
|
||||
|| Raspberry Pi 3 Model B+ || (./) || (./) || DebianPkg:raspi-firmware , DebianPkg:firmware-brcm80211 ||
|
||||
@ -192,7 +190,6 @@ Todas las imágenes de disco de !FreedomBox para hardware diferente las compila
|
||||
|| Rock64 || (./) || || ||
|
||||
|| !RockPro64 || (./) || || ||
|
||||
|| !VirtualBox for amd64 || (./) || || ||
|
||||
|| !VirtualBox for i386 || (./) || || ||
|
||||
|
||||
|
||||
## END_INCLUDE
|
||||
|
||||
@ -107,6 +107,10 @@
|
||||
|
||||
<<Include(FreedomBox/ReleaseNotes, , from="## BEGIN_INCLUDE", to="## END_INCLUDE")>>
|
||||
|
||||
= Customizing =
|
||||
|
||||
<<Include(FreedomBox/Customization, , from="## BEGIN_INCLUDE", to="## END_INCLUDE")>>
|
||||
|
||||
= Contributing =
|
||||
|
||||
<<Include(FreedomBox/Contribute, , from="## BEGIN_INCLUDE", to="## END_INCLUDE")>>
|
||||
|
||||
@ -8,6 +8,41 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f
|
||||
|
||||
The following are the release notes for each !FreedomBox version.
|
||||
|
||||
== FreedomBox 25.1 (2025-01-13) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
* email: Show DNS entries for all domains instead of just primary
|
||||
* privacy: Add option in UI to set lookup URL for public IPs
|
||||
|
||||
=== Other Changes ===
|
||||
|
||||
* app: Add tags to menu and frontpage components
|
||||
* app: Allow apps to instantiate without Django initialization
|
||||
* app: Stop showing short description on installation page
|
||||
* apps: Only show app tags all the tags in apps page search box
|
||||
* backups: Add warning that services may become unavailable
|
||||
* backups: Handle error when there is not enough space on disk
|
||||
* backups: Make all generated archive names consistent
|
||||
* backups: Properly cleanup after downloading an archive
|
||||
* deluge: tests: functional: Fix deluge client logged in detection
|
||||
* doc: dev: Remove short description and add tags to all components
|
||||
* dynamicdns: Use the public IP lookup URL from privacy app
|
||||
* email: Create DKIM keys for all known domains
|
||||
* email: Fix regression error when installing/operation app
|
||||
* email: Show reverse DNS entries to be configured
|
||||
* locale: Translated using Weblate (German, Russian)
|
||||
* miniflux: Ignore an type check error with pexpect library
|
||||
* privacy: Introduce utility to lookup external IP address
|
||||
* privacy: Show notification for privacy settings again
|
||||
* ui: Don't place JS file at the bottom of the page
|
||||
* ui: Drop remnants of already removed background images
|
||||
* ui: Fix missing variables in Bootstrap 5.2/Debian stable
|
||||
* ui: Update section header style to increase size, remove underline
|
||||
* ui: js: Load all JS files in deferred mode to speed up page load
|
||||
* views: Use tags from menu or shortcut instead of the app
|
||||
* web_framework: Disable caching templates files in development mode
|
||||
|
||||
== FreedomBox 24.26.1 (2025-01-05) ==
|
||||
|
||||
=== Highlights ===
|
||||
|
||||
@ -175,7 +175,7 @@ If you want to mount images locally, use the following to copy built images off
|
||||
|
||||
{{{
|
||||
$ mkdir /tmp/vbox-img1 /tmp/vbox-root1
|
||||
$ vdfuse -f freedombox-unstable_2013.0519_virtualbox-i386-hdd.vdi /tmp/vbox-img1/
|
||||
$ vdfuse -f freedombox-unstable_2013.0519_virtualbox-amd64-hdd.vdi /tmp/vbox-img1/
|
||||
$ sudo mount -o loop /tmp/vbox-img1/Partition1 /tmp/vbox-root1
|
||||
$ cp /tmp/vbox-root1/home/fbx/freedom-maker/build/freedom*vdi ~/
|
||||
$ sudo umount /tmp/vbox-root1
|
||||
|
||||
@ -3,4 +3,4 @@
|
||||
Package init file.
|
||||
"""
|
||||
|
||||
__version__ = '24.26.1'
|
||||
__version__ = '25.1'
|
||||
|
||||
@ -431,10 +431,14 @@ class LeaderComponent(Component):
|
||||
class Info(FollowerComponent):
|
||||
"""Component to capture basic information about an app."""
|
||||
|
||||
def __init__(self, app_id, version, is_essential=False, depends=None,
|
||||
name=None, icon=None, icon_filename=None,
|
||||
short_description=None, description=None, manual_page=None,
|
||||
clients=None, donation_url=None, tags=None):
|
||||
def __init__(self, app_id: str, version: int, is_essential: bool = False,
|
||||
depends: list[str] | None = None, name: str | None = None,
|
||||
icon: str | None = None, icon_filename: str | None = None,
|
||||
description: list[str] | None = None,
|
||||
manual_page: str | None = None,
|
||||
clients: list[dict] | None = None,
|
||||
donation_url: str | None = None,
|
||||
tags: list[str] | None = None):
|
||||
"""Store the basic properties of an app as a component.
|
||||
|
||||
Each app must contain at least one component of this type to provide
|
||||
@ -480,12 +484,6 @@ class Info(FollowerComponent):
|
||||
used in the primary app page and on the app listing page. Each app
|
||||
typically has either an 'icon' or 'icon_filename' property set.
|
||||
|
||||
'short_description' is the user visible generic name of the app. For
|
||||
example, for the 'Tor' app the short description is 'Anonymity
|
||||
Network'. It is shown along with the name of the app in the list of
|
||||
apps and when viewing the app's main page. It should be a lazily
|
||||
translated Django string.
|
||||
|
||||
'description' is the user visible full description of the app. It is
|
||||
shown along in the app page along with other app details. It should be
|
||||
a list of lazily translated Django strings. Each string is rendered as
|
||||
@ -516,7 +514,6 @@ class Info(FollowerComponent):
|
||||
self.name = name
|
||||
self.icon = icon
|
||||
self.icon_filename = icon_filename
|
||||
self.short_description = short_description
|
||||
self.description = description
|
||||
self.manual_page = manual_page
|
||||
self.clients = clients
|
||||
@ -532,20 +529,19 @@ class Info(FollowerComponent):
|
||||
These can only be retrieved after Django has been configured.
|
||||
"""
|
||||
# Store untranslated original strings instead of proxy objects
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.functional import Promise
|
||||
from django.utils.translation import override
|
||||
with override(language=None):
|
||||
return [str(tag) for tag in self._tags]
|
||||
|
||||
@classmethod
|
||||
def list_tags(self) -> list[str]:
|
||||
"""Return a list of untranslated tags."""
|
||||
tags: set[str] = set()
|
||||
from django.utils.translation import override
|
||||
with override(language=None):
|
||||
for app in App.list():
|
||||
tags.update((str(tag) for tag in app.info.tags))
|
||||
|
||||
return list(tags)
|
||||
try:
|
||||
with override(language=None):
|
||||
return [str(tag) for tag in self._tags]
|
||||
except ImproperlyConfigured:
|
||||
# Hack to allow apps to be instantiated without Django
|
||||
# initialization as required by privileged process.
|
||||
return [
|
||||
tag._proxy____args[0] if isinstance(tag, Promise) else tag
|
||||
for tag in self._tags
|
||||
]
|
||||
|
||||
|
||||
class EnableState(LeaderComponent):
|
||||
|
||||
@ -17,10 +17,14 @@ class Shortcut(app.FollowerComponent):
|
||||
|
||||
_all_shortcuts: ClassVar[dict[str, 'Shortcut']] = {}
|
||||
|
||||
def __init__(self, component_id, name, short_description=None, icon=None,
|
||||
url=None, description=None, manual_page=None,
|
||||
configure_url=None, clients=None, login_required=False,
|
||||
allowed_groups=None):
|
||||
def __init__(self, component_id: str, name: str | None,
|
||||
icon: str | None = None, url: str | None = None,
|
||||
description: list[str] | None = None,
|
||||
manual_page: str | None = None,
|
||||
configure_url: str | None = None,
|
||||
clients: list[dict] | None = None,
|
||||
tags: list[str] | None = None, login_required: bool = False,
|
||||
allowed_groups: list[str] | None = None):
|
||||
"""Initialize the frontpage shortcut component for an app.
|
||||
|
||||
When a user visits this web interface, they are first shown the
|
||||
@ -34,8 +38,6 @@ class Shortcut(app.FollowerComponent):
|
||||
|
||||
'name' is the mandatory title for the shortcut.
|
||||
|
||||
'short_description' is an optional secondary title for the shortcut.
|
||||
|
||||
'icon' is used to find a suitable image to represent the shortcut.
|
||||
|
||||
'url' is link to which the user is redirected when the shortcut is
|
||||
@ -60,6 +62,9 @@ class Shortcut(app.FollowerComponent):
|
||||
service offered by the shortcut. This should be a valid client
|
||||
information structure as validated by clients.py:validate().
|
||||
|
||||
'tags' is a list of tags that describe the app. Tags help users to find
|
||||
similar apps or alternatives and discover use cases.
|
||||
|
||||
If 'login_required' is true, only logged-in users will be shown this
|
||||
shortcut. Anonymous users visiting the frontpage won't be shown this
|
||||
shortcut.
|
||||
@ -77,13 +82,13 @@ class Shortcut(app.FollowerComponent):
|
||||
url = '?selected={id}'.format(id=component_id)
|
||||
|
||||
self.name = name
|
||||
self.short_description = short_description
|
||||
self.url = url
|
||||
self.icon = icon
|
||||
self.description = description
|
||||
self.manual_page = manual_page
|
||||
self.configure_url = configure_url
|
||||
self.clients = clients
|
||||
self.tags = tags
|
||||
self.login_required = login_required
|
||||
self.allowed_groups = set(allowed_groups) if allowed_groups else None
|
||||
|
||||
@ -140,9 +145,10 @@ def add_custom_shortcuts():
|
||||
|
||||
shortcut_id = shortcut.get('id', shortcut['name'])
|
||||
component_id = 'shortcut-custom-' + shortcut_id
|
||||
tags = shortcut.get('tags', [])
|
||||
component = Shortcut(component_id, shortcut['name'],
|
||||
shortcut['short_description'],
|
||||
icon=shortcut['icon_url'], url=web_app_url)
|
||||
icon=shortcut['icon_url'], tags=tags,
|
||||
url=web_app_url)
|
||||
component.set_enabled(True)
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -13,15 +13,16 @@ class Menu(app.FollowerComponent):
|
||||
|
||||
_all_menus: ClassVar[set['Menu']] = set()
|
||||
|
||||
def __init__(self, component_id, name=None, short_description=None,
|
||||
icon=None, url_name=None, url_args=None, url_kwargs=None,
|
||||
parent_url_name=None, order=50, advanced=False):
|
||||
def __init__(self, component_id: str, name: str | None = None,
|
||||
icon: str | None = None, tags: list[str] | None = None,
|
||||
url_name: str | None = None, url_args: list | None = None,
|
||||
url_kwargs: dict | None = None,
|
||||
parent_url_name: str | None = None, order: int = 50,
|
||||
advanced: bool = False):
|
||||
"""Initialize a new menu item with basic properties.
|
||||
|
||||
name is the label of the menu item.
|
||||
|
||||
short_description is an optional description shown on the menu item.
|
||||
|
||||
icon is the icon to be displayed for the menu item. Icon can be the
|
||||
name of a glyphicon from the Fork Awesome font's icon set:
|
||||
https://forkawesome.github.io/Fork-Awesome/icons/. In this case, the
|
||||
@ -33,6 +34,9 @@ class Menu(app.FollowerComponent):
|
||||
icons files plinth/modules/myapp/static/icons/myicon.svg and
|
||||
plinth/modules/myapp/static/icons/myicon.png are used in the interface.
|
||||
|
||||
tags is a list of tags that describe the app. Tags help users to find
|
||||
similar apps or alternatives and discover use cases.
|
||||
|
||||
url_name is the name of url location that will be activated when the
|
||||
menu item is selected. This is not optional. url_args and url_kwargs
|
||||
are sent to reverse() when resolving url from url_name.
|
||||
@ -56,8 +60,8 @@ class Menu(app.FollowerComponent):
|
||||
url = reverse_lazy(url_name, args=url_args, kwargs=url_kwargs)
|
||||
|
||||
self.name = name
|
||||
self.short_description = short_description
|
||||
self.icon = icon
|
||||
self.tags = tags
|
||||
self.url = url
|
||||
self.order = order
|
||||
self.advanced = advanced
|
||||
|
||||
@ -48,10 +48,10 @@ def _get_shortcut_data(shortcut):
|
||||
"""Return detailed information about a shortcut."""
|
||||
shortcut_data = {
|
||||
'name': shortcut.name,
|
||||
'short_description': shortcut.short_description,
|
||||
'description': shortcut.description,
|
||||
'icon_url': _get_icon_url(shortcut.app_id, shortcut.icon),
|
||||
'clients': copy.deepcopy(shortcut.clients),
|
||||
'tags': copy.deepcopy(shortcut.tags),
|
||||
}
|
||||
# XXX: Fix the hardcoding
|
||||
if shortcut.name.startswith('shortcut-ikiwiki-'):
|
||||
|
||||
@ -50,7 +50,7 @@ class AvahiApp(app_module.App):
|
||||
tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-avahi', info.name, None, info.icon,
|
||||
menu_item = menu.Menu('menu-avahi', info.name, info.icon, info.tags,
|
||||
'avahi:index',
|
||||
parent_url_name='system:visibility', order=50)
|
||||
self.add(menu_item)
|
||||
|
||||
@ -47,7 +47,7 @@ class BackupsApp(app_module.App):
|
||||
tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-backups', info.name, None, info.icon,
|
||||
menu_item = menu.Menu('menu-backups', info.name, info.icon, info.tags,
|
||||
'backups:index', parent_url_name='system:data',
|
||||
order=20)
|
||||
self.add(menu_item)
|
||||
|
||||
@ -33,3 +33,7 @@ class BorgArchiveDoesNotExist(BorgError):
|
||||
|
||||
class BorgBusy(BorgError):
|
||||
"""Borg could not acquire lock being busy with another operation."""
|
||||
|
||||
|
||||
class BorgNoSpace(BorgError):
|
||||
"""There is not enough space left on the device to perform operation."""
|
||||
|
||||
@ -77,7 +77,9 @@ class ScheduleForm(forms.Form):
|
||||
|
||||
run_at_hour = forms.IntegerField(
|
||||
label=_('Hour of the day to trigger backup operation'), required=True,
|
||||
min_value=0, max_value=23, help_text=_('In 24 hour format.'))
|
||||
min_value=0, max_value=23, help_text=_(
|
||||
'In 24 hour format. Services may become temporarily unavailable '
|
||||
'while running backup operation at this time of the day.'))
|
||||
|
||||
selected_apps = forms.MultipleChoiceField(
|
||||
label=_('Included apps'), help_text=_('Apps to include in the backup'),
|
||||
|
||||
@ -87,6 +87,11 @@ KNOWN_ERRORS = [
|
||||
'message': _('Backup system is busy with another operation.'),
|
||||
'raise_as': errors.BorgBusy,
|
||||
},
|
||||
{
|
||||
'errors': ['No space left on device'],
|
||||
'message': _('Not enough space left on the disk or remote location.'),
|
||||
'raise_as': errors.BorgNoSpace,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -196,6 +196,9 @@ class BaseBorgRepository(abc.ABC):
|
||||
"""Override to call read() instead of readline()."""
|
||||
chunk = self.read(io.DEFAULT_BUFFER_SIZE)
|
||||
if not chunk:
|
||||
if getattr(self, 'cleanup_func'):
|
||||
self.cleanup_func()
|
||||
|
||||
raise StopIteration
|
||||
|
||||
return chunk
|
||||
@ -204,12 +207,27 @@ class BaseBorgRepository(abc.ABC):
|
||||
self._get_archive_path(archive_name),
|
||||
self._get_encryption_passpharse(), _raw_output=True)
|
||||
|
||||
os.close(read_fd) # Don't use the pipe for communication, just stdout
|
||||
# Write the method request with args to the process
|
||||
proc.stdin.write(input_)
|
||||
proc.stdin.close()
|
||||
proc.stderr.close() # writing to stderr in child will cause SIGPIPE
|
||||
|
||||
return BufferedReader(proc.stdout)
|
||||
def _cleanup_func():
|
||||
"""After the process has been read from, cleanup the process."""
|
||||
try:
|
||||
if proc.stdout:
|
||||
proc.stdout.close()
|
||||
|
||||
if proc.stderr:
|
||||
proc.stderr.close()
|
||||
|
||||
proc.wait(30)
|
||||
os.close(read_fd)
|
||||
except Exception:
|
||||
logger.exception('Closing process failed after download')
|
||||
|
||||
reader = BufferedReader(proc.stdout)
|
||||
reader.cleanup_func = _cleanup_func
|
||||
return reader
|
||||
|
||||
def _get_archive_path(self, archive_name):
|
||||
"""Return full borg path for an archive."""
|
||||
@ -223,6 +241,11 @@ class BaseBorgRepository(abc.ABC):
|
||||
|
||||
return None
|
||||
|
||||
def generate_archive_name(self):
|
||||
"""Return a name to create a backup archive with using time."""
|
||||
return datetime.datetime.now().astimezone().replace(
|
||||
microsecond=0).isoformat()
|
||||
|
||||
def get_archive_apps(self, archive_name):
|
||||
"""Get list of apps included in an archive."""
|
||||
archive_path = self._get_archive_path(archive_name)
|
||||
|
||||
@ -274,12 +274,14 @@ class Schedule:
|
||||
logger.info('Running backup for repository %s, periods %s',
|
||||
self.repository_uuid, periods)
|
||||
|
||||
repository = self._get_repository()
|
||||
|
||||
from . import api
|
||||
periods = list(periods)
|
||||
periods.sort()
|
||||
name = 'scheduled: {periods}: {datetime}'.format(
|
||||
name = 'scheduled: {periods}: {name}'.format(
|
||||
periods=', '.join(periods),
|
||||
datetime=datetime.now().strftime('%Y-%m-%d:%H:%M'))
|
||||
name=repository.generate_archive_name())
|
||||
comment = self._serialize_comment({
|
||||
'type': 'scheduled',
|
||||
'periods': periods
|
||||
@ -290,7 +292,6 @@ class Schedule:
|
||||
if component.app_id not in self.unselected_apps
|
||||
]
|
||||
|
||||
repository = self._get_repository()
|
||||
repository.create_archive(name, app_ids, archive_comment=comment)
|
||||
|
||||
def _run_cleanup(self, repository):
|
||||
|
||||
@ -9,7 +9,7 @@ from unittest.mock import MagicMock, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import plinth.modules.backups.repository # noqa, pylint: disable=unused-import
|
||||
import plinth.modules.backups.repository as repository_module
|
||||
from plinth.app import App
|
||||
|
||||
from ..components import BackupRestore
|
||||
@ -431,15 +431,19 @@ def test_run_schedule(get_instance, get_setup_state, schedule_params,
|
||||
repository.list_archives.side_effect = \
|
||||
lambda: _get_archives_from_test_data(archives_data)
|
||||
get_instance.return_value = repository
|
||||
repository.generate_archive_name = lambda: \
|
||||
repository_module.BaseBorgRepository.generate_archive_name(None)
|
||||
|
||||
with patch('plinth.modules.backups.schedule.datetime') as mock_datetime, \
|
||||
patch('plinth.app.App.list') as app_list:
|
||||
patch('plinth.modules.backups.repository.datetime') \
|
||||
as repo_datetime, patch('plinth.app.App.list') as app_list:
|
||||
app_list.return_value = [
|
||||
_get_test_app('test-app1'),
|
||||
_get_test_app('test-app2'),
|
||||
_get_test_app('test-app3')
|
||||
]
|
||||
|
||||
repo_datetime.datetime.now.return_value = test_now
|
||||
mock_datetime.now.return_value = test_now
|
||||
mock_datetime.strptime = datetime.strptime
|
||||
mock_datetime.min = datetime.min
|
||||
@ -458,7 +462,8 @@ def test_run_schedule(get_instance, get_setup_state, schedule_params,
|
||||
run_periods.sort()
|
||||
name = 'scheduled: {periods}: {datetime}'.format(
|
||||
periods=', '.join(run_periods),
|
||||
datetime=mock_datetime.now().strftime('%Y-%m-%d:%H:%M'))
|
||||
datetime=repo_datetime.datetime.now().astimezone().replace(
|
||||
microsecond=0).isoformat())
|
||||
app_ids = ['test-app1', 'test-app3']
|
||||
archive_comment = json.dumps({
|
||||
'type': 'scheduled',
|
||||
|
||||
@ -7,7 +7,6 @@ import contextlib
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from urllib.parse import unquote
|
||||
|
||||
from django.contrib import messages
|
||||
@ -141,8 +140,7 @@ class CreateArchiveView(FormView):
|
||||
|
||||
name = form.cleaned_data['name']
|
||||
if not name:
|
||||
name = datetime.now().astimezone().replace(
|
||||
microsecond=0).isoformat()
|
||||
name = repository.generate_archive_name()
|
||||
|
||||
selected_apps = form.cleaned_data['selected_apps']
|
||||
with handle_common_errors(self.request):
|
||||
|
||||
@ -60,15 +60,14 @@ class BepastyApp(app_module.App):
|
||||
clients=manifest.clients, tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-bepasty', info.name,
|
||||
info.short_description, info.icon_filename,
|
||||
'bepasty:index', parent_url_name='apps')
|
||||
menu_item = menu.Menu('menu-bepasty', info.name, info.icon_filename,
|
||||
info.tags, 'bepasty:index',
|
||||
parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
shortcut = frontpage.Shortcut('shortcut-bepasty', info.name,
|
||||
info.short_description,
|
||||
info.icon_filename, '/bepasty',
|
||||
clients=manifest.clients)
|
||||
clients=manifest.clients, tags=info.tags)
|
||||
self.add(shortcut)
|
||||
|
||||
packages = Packages('packages-bepasty', ['bepasty'])
|
||||
|
||||
@ -42,8 +42,8 @@ class BindApp(app_module.App):
|
||||
tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-bind', info.name, info.short_description,
|
||||
info.icon, 'bind:index',
|
||||
menu_item = menu.Menu('menu-bind', info.name, info.icon, info.tags,
|
||||
'bind:index',
|
||||
parent_url_name='system:visibility', order=30)
|
||||
self.add(menu_item)
|
||||
|
||||
|
||||
@ -59,15 +59,14 @@ class CalibreApp(app_module.App):
|
||||
donation_url='https://calibre-ebook.com/donate')
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-calibre', info.name,
|
||||
info.short_description, info.icon_filename,
|
||||
'calibre:index', parent_url_name='apps')
|
||||
menu_item = menu.Menu('menu-calibre', info.name, info.icon_filename,
|
||||
info.tags, 'calibre:index',
|
||||
parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
shortcut = frontpage.Shortcut('shortcut-calibre', info.name,
|
||||
short_description=info.short_description,
|
||||
icon=info.icon_filename, url='/calibre',
|
||||
clients=info.clients,
|
||||
clients=info.clients, tags=info.tags,
|
||||
login_required=True,
|
||||
allowed_groups=list(groups))
|
||||
self.add(shortcut)
|
||||
|
||||
@ -56,18 +56,16 @@ class CockpitApp(app_module.App):
|
||||
clients=manifest.clients, tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-cockpit', info.name,
|
||||
info.short_description, info.icon,
|
||||
menu_item = menu.Menu('menu-cockpit', info.name, info.icon, info.tags,
|
||||
'cockpit:index',
|
||||
parent_url_name='system:administration',
|
||||
order=20)
|
||||
self.add(menu_item)
|
||||
|
||||
shortcut = frontpage.Shortcut('shortcut-cockpit', info.name,
|
||||
short_description=info.short_description,
|
||||
icon=info.icon_filename,
|
||||
url='/_cockpit/', clients=info.clients,
|
||||
login_required=True,
|
||||
tags=info.tags, login_required=True,
|
||||
allowed_groups=['admin'])
|
||||
self.add(shortcut)
|
||||
|
||||
|
||||
@ -42,9 +42,9 @@ class ConfigApp(app_module.App):
|
||||
manual_page='Configure', tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-config', _('Configure'), None, info.icon,
|
||||
'config:index', parent_url_name='system:system',
|
||||
order=30)
|
||||
menu_item = menu.Menu('menu-config', _('Configure'), info.icon,
|
||||
info.tags, 'config:index',
|
||||
parent_url_name='system:system', order=30)
|
||||
self.add(menu_item)
|
||||
|
||||
packages = Packages('packages-config', ['zram-tools'])
|
||||
|
||||
@ -54,8 +54,8 @@ class CoturnApp(app_module.App):
|
||||
tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-coturn', info.name, info.short_description,
|
||||
info.icon_filename, 'coturn:index',
|
||||
menu_item = menu.Menu('menu-coturn', info.name, info.icon_filename,
|
||||
info.tags, 'coturn:index',
|
||||
parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ class DateTimeApp(app_module.App):
|
||||
manual_page='DateTime', tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-datetime', info.name, None, info.icon,
|
||||
menu_item = menu.Menu('menu-datetime', info.name, info.icon, info.tags,
|
||||
'datetime:index',
|
||||
parent_url_name='system:system', order=40)
|
||||
self.add(menu_item)
|
||||
|
||||
@ -64,15 +64,14 @@ class DelugeApp(app_module.App):
|
||||
tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-deluge', info.name, info.short_description,
|
||||
info.icon_filename, 'deluge:index',
|
||||
menu_item = menu.Menu('menu-deluge', info.name, info.icon_filename,
|
||||
info.tags, 'deluge:index',
|
||||
parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
shortcut = frontpage.Shortcut('shortcut-deluge', info.name,
|
||||
short_description=info.short_description,
|
||||
url='/deluge', icon=info.icon_filename,
|
||||
clients=info.clients,
|
||||
clients=info.clients, tags=info.tags,
|
||||
login_required=True,
|
||||
allowed_groups=list(groups))
|
||||
self.add(shortcut)
|
||||
|
||||
@ -83,7 +83,6 @@ def _ensure_logged_in(browser):
|
||||
|
||||
def logged_in():
|
||||
active_window_title = _get_active_window_title(browser)
|
||||
|
||||
# Change Default Password window appears once.
|
||||
if active_window_title == 'Change Default Password':
|
||||
_click_active_window_button(browser, 'No')
|
||||
@ -92,7 +91,8 @@ def _ensure_logged_in(browser):
|
||||
browser.find_by_id('_password').first.fill('deluge')
|
||||
_click_active_window_button(browser, 'Login')
|
||||
|
||||
return browser.is_element_not_present_by_css('#add .x-item-disabled')
|
||||
return browser.is_element_present_by_css(
|
||||
'.x-deluge-statusbar.x-connected')
|
||||
|
||||
functional.eventually(logged_in)
|
||||
|
||||
|
||||
@ -55,8 +55,8 @@ class DiagnosticsApp(app_module.App):
|
||||
manual_page='Diagnostics', tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-diagnostics', info.name, None, info.icon,
|
||||
'diagnostics:index',
|
||||
menu_item = menu.Menu('menu-diagnostics', info.name, info.icon,
|
||||
info.tags, 'diagnostics:index',
|
||||
parent_url_name='system:administration',
|
||||
order=30)
|
||||
self.add(menu_item)
|
||||
|
||||
@ -15,6 +15,7 @@ from plinth import app as app_module
|
||||
from plinth import cfg, glib, kvstore, menu
|
||||
from plinth.modules.backups.components import BackupRestore
|
||||
from plinth.modules.names.components import DomainType
|
||||
from plinth.modules.privacy import lookup_public_address
|
||||
from plinth.modules.users.components import UsersAndGroups
|
||||
from plinth.signals import domain_added, domain_removed
|
||||
from plinth.utils import format_lazy
|
||||
@ -42,6 +43,8 @@ _description = [
|
||||
'target=\'_blank\'>ddns.freedombox.org</a> or you may find free update '
|
||||
'URL based services at <a href=\'http://freedns.afraid.org/\' '
|
||||
'target=\'_blank\'>freedns.afraid.org</a>.'),
|
||||
_('This service uses an external service to lookup public IP address. '
|
||||
'This can be configured in the privacy app.'),
|
||||
]
|
||||
|
||||
|
||||
@ -63,8 +66,8 @@ class DynamicDNSApp(app_module.App):
|
||||
manual_page='DynamicDNS', tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-dynamicdns', info.name, None, info.icon,
|
||||
'dynamicdns:index',
|
||||
menu_item = menu.Menu('menu-dynamicdns', info.name, info.icon,
|
||||
info.tags, 'dynamicdns:index',
|
||||
parent_url_name='system:visibility', order=20)
|
||||
self.add(menu_item)
|
||||
|
||||
@ -112,21 +115,12 @@ class DynamicDNSApp(app_module.App):
|
||||
privileged.clean()
|
||||
|
||||
|
||||
def _query_external_address(domain):
|
||||
def _lookup_public_address(domain):
|
||||
"""Return the IP address by querying an external server."""
|
||||
if not domain['ip_lookup_url']:
|
||||
return None
|
||||
|
||||
ip_option = '-6' if domain['use_ipv6'] else '-4'
|
||||
try:
|
||||
ip_address = subprocess.check_output([
|
||||
'wget', ip_option, '-o', '/dev/null', '-t', '3', '-T', '3', '-O',
|
||||
'-', domain['ip_lookup_url']
|
||||
])
|
||||
return ip_address.decode().strip().lower()
|
||||
except subprocess.CalledProcessError as exception:
|
||||
logger.warning('Unable to lookup external IP with URL %s: %s',
|
||||
domain['ip_lookup_url'], exception)
|
||||
ip_type = 'ipv6' if domain['use_ipv6'] else 'ipv4'
|
||||
return lookup_public_address(ip_type)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@ -186,7 +180,7 @@ def _update_dns_for_domain(domain):
|
||||
|
||||
try:
|
||||
dns_address = _query_dns_address(domain)
|
||||
external_address = _query_external_address(domain)
|
||||
external_address = _lookup_public_address(domain)
|
||||
if dns_address == external_address and dns_address is not None:
|
||||
logger.info('Dynamic domain %s is up-to-date: %s',
|
||||
domain['domain'], dns_address)
|
||||
|
||||
@ -39,14 +39,6 @@ class ConfigureForm(forms.Form):
|
||||
help_password = \
|
||||
gettext_lazy('Leave this field empty if you want to keep your '
|
||||
'current password.')
|
||||
help_ip_lookup_url = format_lazy(
|
||||
gettext_lazy('Optional Value. If your {box_name} is not connected '
|
||||
'directly to the Internet (i.e. connected to a NAT '
|
||||
'router) this URL is used to determine the real '
|
||||
'IP address. The URL should simply return the IP where '
|
||||
'the client comes from (example: '
|
||||
'https://ddns.freedombox.org/ip/).'),
|
||||
box_name=gettext_lazy(cfg.box_name))
|
||||
help_username = \
|
||||
gettext_lazy('The username that was used when the account was '
|
||||
'created.')
|
||||
@ -95,11 +87,6 @@ class ConfigureForm(forms.Form):
|
||||
show_password = forms.BooleanField(label=gettext_lazy('Show password'),
|
||||
required=False)
|
||||
|
||||
ip_lookup_url = forms.CharField(
|
||||
label=gettext_lazy('URL to look up public IP'), required=False,
|
||||
help_text=help_ip_lookup_url,
|
||||
validators=[validators.URLValidator(schemes=['http', 'https'])])
|
||||
|
||||
use_ipv6 = forms.BooleanField(
|
||||
label=gettext_lazy('Use IPv6 instead of IPv4'), required=False)
|
||||
|
||||
@ -129,8 +116,7 @@ class ConfigureForm(forms.Form):
|
||||
if not update_url:
|
||||
self.add_error('update_url', message)
|
||||
|
||||
param_map = (('username', '<User>'), ('password', '<Pass>'),
|
||||
('ip_lookup_url', '<Ip>'))
|
||||
param_map = (('username', '<User>'), ('password', '<Pass>'))
|
||||
for field_name, param in param_map:
|
||||
if (update_url and param in update_url
|
||||
and not cleaned_data.get(field_name)):
|
||||
|
||||
@ -53,7 +53,6 @@ def export_config() -> dict[str, bool | dict[str, dict[str, str | None]]]:
|
||||
'server': input_config.get('server'),
|
||||
'username': input_config.get('user', '').split(':')[0] or None,
|
||||
'password': input_config.get('user', '').split(':')[-1] or None,
|
||||
'ip_lookup_url': helper.get('IPURL'),
|
||||
'update_url': _clean(helper.get('POSTURL')) or None,
|
||||
'use_http_basic_auth': _clean(helper.get('POSTAUTH')),
|
||||
'disable_ssl_cert_check': _clean(helper.get('POSTSSLIGNORE')),
|
||||
|
||||
@ -7,6 +7,11 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block page_js %}
|
||||
<script type="text/javascript" src="{% static 'dynamicdns/dynamicdns.js' %}"
|
||||
defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_content %}
|
||||
<h3>{% trans "Status" %}</h3>
|
||||
|
||||
@ -52,7 +57,3 @@
|
||||
{% trans "No status available." %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_js %}
|
||||
<script type="text/javascript" src="{% static 'dynamicdns/dynamicdns.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@ -19,7 +19,6 @@ _configs = {
|
||||
'domain': 'freedombox.example.com',
|
||||
'username': 'tester',
|
||||
'password': 'testingtesting',
|
||||
'ip_lookup_url': 'https://ddns.freedombox.org/ip/',
|
||||
},
|
||||
'gnudip2': {
|
||||
'service_type': 'gnudip',
|
||||
@ -27,7 +26,6 @@ _configs = {
|
||||
'domain': 'freedombox2.example.com',
|
||||
'username': 'tester2',
|
||||
'password': 'testingtesting2',
|
||||
'ip_lookup_url': 'https://ddns2.freedombox.org/ip/',
|
||||
},
|
||||
'noip.com': {
|
||||
'service_type': 'noip.com',
|
||||
@ -37,7 +35,6 @@ _configs = {
|
||||
'domain': 'freedombox3.example.com',
|
||||
'username': 'tester3',
|
||||
'password': 'testingtesting3',
|
||||
'ip_lookup_url': 'https://ddns3.freedombox.org/ip/',
|
||||
'use_ipv6': True,
|
||||
},
|
||||
'freedns.afraid.org': {
|
||||
@ -48,7 +45,6 @@ _configs = {
|
||||
'domain': 'freedombox5.example.com',
|
||||
'username': '',
|
||||
'password': '',
|
||||
'ip_lookup_url': '',
|
||||
'use_ipv6': False,
|
||||
},
|
||||
'other': {
|
||||
@ -59,7 +55,6 @@ _configs = {
|
||||
'domain': 'freedombox6.example.com',
|
||||
'username': 'tester6',
|
||||
'password': 'testingtesting6',
|
||||
'ip_lookup_url': 'https://ddns6.freedombox.org/ip/',
|
||||
'use_ipv6': False,
|
||||
},
|
||||
}
|
||||
|
||||
@ -64,17 +64,16 @@ class EjabberdApp(app_module.App):
|
||||
clients=manifest.clients, tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-ejabberd', info.name,
|
||||
info.short_description, info.icon_filename,
|
||||
'ejabberd:index', parent_url_name='apps')
|
||||
menu_item = menu.Menu('menu-ejabberd', info.name, info.icon_filename,
|
||||
info.tags, 'ejabberd:index',
|
||||
parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
shortcut = frontpage.Shortcut(
|
||||
'shortcut-ejabberd', info.name,
|
||||
short_description=info.short_description, icon=info.icon_filename,
|
||||
'shortcut-ejabberd', info.name, icon=info.icon_filename,
|
||||
description=info.description, manual_page=info.manual_page,
|
||||
configure_url=reverse_lazy('ejabberd:index'), clients=info.clients,
|
||||
login_required=True)
|
||||
tags=info.tags, login_required=True)
|
||||
self.add(shortcut)
|
||||
|
||||
packages = Packages('packages-ejabberd', ['ejabberd'])
|
||||
|
||||
@ -7,6 +7,11 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block page_js %}
|
||||
<script type="text/javascript" src="{% static 'ejabberd/ejabberd.js' %}"
|
||||
defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block status %}
|
||||
{{ block.super }}
|
||||
|
||||
@ -30,8 +35,3 @@
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block page_js %}
|
||||
<script type="text/javascript"
|
||||
src="{% static 'ejabberd/ejabberd.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@ -18,7 +18,7 @@ from plinth.modules.letsencrypt.components import LetsEncrypt
|
||||
from plinth.package import Packages
|
||||
from plinth.privileged import service as service_privileged
|
||||
from plinth.signals import domain_added, domain_removed
|
||||
from plinth.utils import format_lazy
|
||||
from plinth.utils import format_lazy, gettext_noop
|
||||
|
||||
from . import aliases, manifest, privileged
|
||||
|
||||
@ -66,24 +66,23 @@ class EmailApp(plinth.app.App):
|
||||
donation_url='https://rspamd.com/support.html')
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-email', info.name, info.short_description,
|
||||
info.icon_filename, 'email:index',
|
||||
parent_url_name='apps')
|
||||
menu_item = menu.Menu('menu-email', info.name, info.icon_filename,
|
||||
info.tags, 'email:index', parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
shortcut = frontpage.Shortcut(
|
||||
'shortcut-email', info.name,
|
||||
short_description=info.short_description, icon=info.icon_filename,
|
||||
'shortcut-email', info.name, icon=info.icon_filename,
|
||||
description=info.description, manual_page=info.manual_page,
|
||||
configure_url=reverse_lazy('email:index'), clients=info.clients,
|
||||
login_required=True)
|
||||
tags=info.tags, login_required=True)
|
||||
self.add(shortcut)
|
||||
|
||||
shortcut = frontpage.Shortcut(
|
||||
'shortcut-email-aliases', _('My Email Aliases'),
|
||||
short_description=_('Manage Aliases for Mailbox'),
|
||||
icon=info.icon_filename, url=reverse_lazy('email:aliases'),
|
||||
login_required=True)
|
||||
tags = [gettext_noop('More emails'), gettext_noop('Same mailbox')]
|
||||
shortcut = frontpage.Shortcut('shortcut-email-aliases',
|
||||
_('My Email Aliases'),
|
||||
icon=info.icon_filename, tags=tags,
|
||||
url=reverse_lazy('email:aliases'),
|
||||
login_required=True)
|
||||
self.add(shortcut)
|
||||
|
||||
# Other likely install conflicts have been discarded:
|
||||
|
||||
@ -7,10 +7,17 @@ See: https://dmarcguide.globalcyberalliance.org/
|
||||
See: https://support.google.com/a/answer/2466580
|
||||
See: https://datatracker.ietf.org/doc/html/rfc6186
|
||||
See: https://rspamd.com/doc/modules/dkim_signing.html
|
||||
See: https://en.wikipedia.org/wiki/Reverse_DNS_lookup
|
||||
"""
|
||||
|
||||
import ipaddress
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
|
||||
from plinth.modules.privacy import lookup_public_address
|
||||
|
||||
from . import privileged
|
||||
|
||||
|
||||
@dataclass
|
||||
class Entry: # pylint: disable=too-many-instance-attributes
|
||||
@ -39,11 +46,8 @@ class Entry: # pylint: disable=too-many-instance-attributes
|
||||
return ' '.join(pieces)
|
||||
|
||||
|
||||
def get_entries():
|
||||
"""Return the list of DNS entries to make."""
|
||||
from . import privileged
|
||||
|
||||
domain = privileged.domain.get_domains()['primary_domain']
|
||||
def get_entries(domain: str) -> list[Entry]:
|
||||
"""Return the list of DNS entries to be set in DNS server for domain."""
|
||||
mx_spam_entries = [
|
||||
Entry(type_='MX', value=f'{domain}.'),
|
||||
Entry(type_='TXT', value='v=spf1 mx a ~all'),
|
||||
@ -70,3 +74,20 @@ def get_entries():
|
||||
port=995, value=f'{domain}.'),
|
||||
]
|
||||
return mx_spam_entries + dkim_entries + autoconfig_entries
|
||||
|
||||
|
||||
def get_reverse_entries(domain: str) -> list[Entry]:
|
||||
"""Return the list of reverse DNS entries to make."""
|
||||
entries = []
|
||||
for ip_type in typing.get_args(typing.Literal['ipv4', 'ipv6']):
|
||||
try:
|
||||
ip_address = lookup_public_address(ip_type)
|
||||
reverse_pointer = ipaddress.ip_address(ip_address).reverse_pointer
|
||||
except Exception as exception:
|
||||
reverse_pointer = \
|
||||
f'Error querying external {ip_type} address: {exception}'
|
||||
|
||||
entry = Entry(domain=reverse_pointer, type_='PTR', value=f'{domain}.')
|
||||
entries.append(entry)
|
||||
|
||||
return entries
|
||||
|
||||
@ -38,7 +38,8 @@ def set_all_domains(primary_domain=None):
|
||||
|
||||
# Update configuration and don't restart daemons
|
||||
set_domains(primary_domain, list(all_domains))
|
||||
dkim.setup_dkim(primary_domain)
|
||||
for domain in all_domains:
|
||||
dkim.setup_dkim(domain)
|
||||
|
||||
# Copy certificates (self-signed if needed) and restart daemons
|
||||
app = App.get('email')
|
||||
|
||||
95
plinth/modules/email/templates/email-dns.html
Normal file
95
plinth/modules/email/templates/email-dns.html
Normal file
@ -0,0 +1,95 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h3>{% trans "DNS Records for domain:" %} {{ domain }}</h3>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
The following DNS records must be added manually on this domain for the
|
||||
mail server to work properly for this domain.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Domain" %}</th>
|
||||
<th>{% trans "TTL" %}</th>
|
||||
<th>{% trans "Class" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Priority" %}</th>
|
||||
<th>{% trans "Weight" %}</th>
|
||||
<th>{% trans "Port" %}</th>
|
||||
<th>{% trans "Host/Target/Value" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for dns_entry in dns_entries %}
|
||||
<tr>
|
||||
<td>{{ dns_entry.domain|default_if_none:"" }}</td>
|
||||
<td>{{ dns_entry.ttl }}</td>
|
||||
<td>{{ dns_entry.class_ }}</td>
|
||||
<td>{{ dns_entry.type_ }}</td>
|
||||
<td>{{ dns_entry.priority }}</td>
|
||||
<td>{{ dns_entry.weight|default_if_none:"" }}</td>
|
||||
<td>{{ dns_entry.port|default_if_none:"" }}</td>
|
||||
<td class="text-break">{{ dns_entry.get_split_value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if domain == primary_domain %}
|
||||
<h3>{% trans "Reverse DNS Records for IP Addresses" %}</h3>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
If your {{ box_name }} runs on a cloud service infrastructure, you
|
||||
should configure <a href="https://en.wikipedia.org/wiki/Reverse_DNS_lookup">
|
||||
Reverse DNS lookup</a>. This isn't mandatory, however, it greatly improves
|
||||
email deliverability. Reverse DNS isn't configured where your regular DNS
|
||||
is. You should look for it in the settings of your VPS/ISP. Some providers
|
||||
preconfigure the IP address part for you and you only have to set the
|
||||
domain part. Only one of your domains can have Revese DNS lookup
|
||||
configured unless you have multiple public IP addresses.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
An external service is used to lookup public IP address to show in the
|
||||
following section. This can be configured in the privacy app.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Host" %}</th>
|
||||
<th>{% trans "TTL" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Host/Target/Value" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for dns_entry in reverse_dns_entries %}
|
||||
<tr>
|
||||
<td>{{ dns_entry.domain|default_if_none:"" }}</td>
|
||||
<td>{{ dns_entry.ttl }}</td>
|
||||
<td>{{ dns_entry.type_ }}</td>
|
||||
<td class="text-break">{{ dns_entry.get_split_value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@ -17,45 +17,28 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_content %}
|
||||
{{ block.super }}
|
||||
|
||||
<h3>{% trans "DNS Records" %}</h3>
|
||||
<h3>{% trans "Domains" %}</h3>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
The following DNS records must be added manually on your primary domain
|
||||
for the mail server to work properly.
|
||||
The following domains are configured. View details to see the list of DNS
|
||||
entries to be made for the domain.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Domain" %}</th>
|
||||
<th>{% trans "TTL" %}</th>
|
||||
<th>{% trans "Class" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Priority" %}</th>
|
||||
<th>{% trans "Weight" %}</th>
|
||||
<th>{% trans "Port" %}</th>
|
||||
<th>{% trans "Host/Target/Value" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for dns_entry in dns_entries %}
|
||||
<tr>
|
||||
<td>{{ dns_entry.domain|default_if_none:"" }}</td>
|
||||
<td>{{ dns_entry.ttl }}</td>
|
||||
<td>{{ dns_entry.class_ }}</td>
|
||||
<td>{{ dns_entry.type_ }}</td>
|
||||
<td>{{ dns_entry.priority }}</td>
|
||||
<td>{{ dns_entry.weight|default_if_none:"" }}</td>
|
||||
<td>{{ dns_entry.port|default_if_none:"" }}</td>
|
||||
<td class="text-break">{{ dns_entry.get_split_value }}</td>
|
||||
</tr>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="list-group">
|
||||
{% for domain in all_domains %}
|
||||
<div class="list-group-item">
|
||||
<a href="{% url 'email:dns' domain %}"
|
||||
title="{% blocktrans %}View domain: {{ domain }}{% endblocktrans %}">
|
||||
{{ domain }}</a>
|
||||
{% if domain == primary_domain %}<div class="app-icon fa fa-tag"></div>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
URLs for the email module.
|
||||
"""
|
||||
|
||||
from django.urls import path
|
||||
from django.urls import path, re_path
|
||||
from stronghold.decorators import public
|
||||
|
||||
from plinth.utils import non_admin_view
|
||||
@ -12,6 +12,8 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('apps/email/', views.EmailAppView.as_view(), name='index'),
|
||||
re_path('apps/email/dns/(?P<domain>[^/]+)/$', views.DnsView.as_view(),
|
||||
name='dns'),
|
||||
path('apps/email/aliases/', non_admin_view(views.AliasView.as_view()),
|
||||
name='aliases'),
|
||||
path('apps/email/config.xml', public(views.XmlView.as_view())),
|
||||
|
||||
@ -25,7 +25,7 @@ class EmailAppView(AppView):
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add additional context data for rendering the template."""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['dns_entries'] = dns.get_entries()
|
||||
context.update(privileged.domain.get_domains())
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
@ -50,6 +50,21 @@ class EmailAppView(AppView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class DnsView(TemplateView):
|
||||
"""Show the DNS records to configure on a given domain."""
|
||||
template_name = 'email-dns.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add additional context data for rendering the template."""
|
||||
domain = self.kwargs['domain']
|
||||
context = super().get_context_data(**kwargs)
|
||||
primary_domain = privileged.domain.get_domains()['primary_domain']
|
||||
context['primary_domain'] = primary_domain
|
||||
context['dns_entries'] = dns.get_entries(domain)
|
||||
context['reverse_dns_entries'] = dns.get_reverse_entries(domain)
|
||||
return context
|
||||
|
||||
|
||||
class AliasView(FormView):
|
||||
"""View to create, list, enable, disable and delete aliases.
|
||||
|
||||
|
||||
@ -64,7 +64,7 @@ class FeatherWikiApp(app_module.App):
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-featherwiki', info.name,
|
||||
info.short_description, info.icon_filename,
|
||||
info.icon_filename, info.tags,
|
||||
'featherwiki:index', parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
@ -72,11 +72,10 @@ class FeatherWikiApp(app_module.App):
|
||||
# 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,
|
||||
'shortcut-featherwiki', info.name, icon=info.icon_filename,
|
||||
description=info.description, manual_page=info.manual_page,
|
||||
url='/featherwiki/', clients=info.clients, login_required=True,
|
||||
allowed_groups=list(groups))
|
||||
url='/featherwiki/', clients=info.clients, tags=info.tags,
|
||||
login_required=True, allowed_groups=list(groups))
|
||||
self.add(shortcut)
|
||||
|
||||
dropin_configs = DropinConfigs('dropin-configs-featherwiki', [
|
||||
|
||||
@ -63,7 +63,7 @@ class FirewallApp(app_module.App):
|
||||
manual_page='Firewall', tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-firewall', info.name, None, info.icon,
|
||||
menu_item = menu.Menu('menu-firewall', info.name, info.icon, info.tags,
|
||||
'firewall:index',
|
||||
parent_url_name='system:security', order=30)
|
||||
self.add(menu_item)
|
||||
|
||||
@ -50,15 +50,14 @@ class GitwebApp(app_module.App):
|
||||
clients=manifest.clients, tags=manifest.tags)
|
||||
self.add(info)
|
||||
|
||||
menu_item = menu.Menu('menu-gitweb', info.name, info.short_description,
|
||||
info.icon_filename, 'gitweb:index',
|
||||
menu_item = menu.Menu('menu-gitweb', info.name, info.icon_filename,
|
||||
info.tags, 'gitweb:index',
|
||||
parent_url_name='apps')
|
||||
self.add(menu_item)
|
||||
|
||||
shortcut = frontpage.Shortcut('shortcut-gitweb', info.name,
|
||||
short_description=info.short_description,
|
||||
icon=info.icon_filename, url='/gitweb/',
|
||||
clients=info.clients,
|
||||
clients=info.clients, tags=info.tags,
|
||||
login_required=True,
|
||||
allowed_groups=list(groups))
|
||||
self.add(shortcut)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user