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:
James Valleroy 2025-01-16 07:38:25 -05:00
commit 9ac4384135
174 changed files with 14023 additions and 10608 deletions

48
debian/changelog vendored
View File

@ -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.

View File

@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -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>`

View File

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

View File

@ -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.

View 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

View File

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

View File

@ -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 ===

View File

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

View File

@ -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")>>

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -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.

View File

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

View File

@ -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")>>

View File

@ -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 ===

View File

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

View File

@ -3,4 +3,4 @@
Package init file.
"""
__version__ = '24.26.1'
__version__ = '25.1'

View File

@ -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):

View File

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

View File

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

View File

@ -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-'):

View File

@ -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)

View File

@ -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)

View File

@ -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."""

View File

@ -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'),

View File

@ -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,
},
]

View File

@ -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)

View File

@ -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):

View File

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

View File

@ -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):

View File

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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)):

View File

@ -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')),

View File

@ -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 %}

View File

@ -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,
},
}

View File

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

View File

@ -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 %}

View File

@ -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:

View File

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

View File

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

View 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 %}

View File

@ -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 %}

View File

@ -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())),

View File

@ -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.

View File

@ -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', [

View File

@ -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)

View File

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