diff --git a/actions/i2p b/actions/i2p
index 5ac069fd1..5e34ab98d 100755
--- a/actions/i2p
+++ b/actions/i2p
@@ -23,7 +23,7 @@ import argparse
import os
from plinth import action_utils, cfg
-from plinth.modules.i2p.helpers import TunnelEditor
+from plinth.modules.i2p.helpers import RouterEditor, TunnelEditor
cfg.read()
module_config_path = os.path.join(cfg.config_dir, 'modules-enabled')
@@ -87,36 +87,13 @@ def subcommand_add_favorite(arguments):
:param arguments:
:type arguments:
"""
- router_config_path = os.path.join(I2P_CONF_DIR, 'router.config')
- # Read config
- with open(router_config_path) as config_file:
- config_lines = config_file.readlines()
-
- found_favorites = False
url = arguments.url
- new_favorite = '{name},{description},{url},{icon},'.format(
- name=arguments.name, description='', url=arguments.url,
- icon='/themes/console/images/eepsite.png')
- for i in range(len(config_lines)):
- line = config_lines[i]
- # Find favorites line
- if line.startswith('routerconsole.favorites'):
- found_favorites = True
- if url in line:
- print('URL already in favorites')
- exit(0)
-
- # Append favorite
- config_lines[i] = line.strip() + new_favorite + '\n'
- break
-
- if not found_favorites:
- config_lines.append('routerconsole.favorites=' + new_favorite + '\n')
-
- # Update config
- with open(router_config_path, mode='w') as config_file:
- config_file.writelines(config_lines)
+ editor = RouterEditor()
+ editor.read_conf().add_favorite(
+ arguments.name,
+ url
+ ).write_conf()
print('Added {} to favorites'.format(url))
diff --git a/plinth/modules/i2p/__init__.py b/plinth/modules/i2p/__init__.py
index f2ec2bb34..0ab07b132 100644
--- a/plinth/modules/i2p/__init__.py
+++ b/plinth/modules/i2p/__init__.py
@@ -59,6 +59,10 @@ proxies_service = None
manual_page = 'I2P'
+# TODO: Add all default favorites
+# the router.config favorites is empty until a change is made
+# in the front-end, but we currently do not have a method of
+# doing that, so we need to add the favorites ourselves
additional_favorites = [
('Searx instance', 'http://ransack.i2p'),
('Torrent tracker', 'http://tracker2.postman.i2p'),
diff --git a/plinth/modules/i2p/helpers.py b/plinth/modules/i2p/helpers.py
index ac052e2cb..0897efa19 100644
--- a/plinth/modules/i2p/helpers.py
+++ b/plinth/modules/i2p/helpers.py
@@ -17,6 +17,7 @@
Various helpers for the I2P app.
"""
+from collections import OrderedDict
import os
import re
@@ -25,6 +26,7 @@ import augeas
I2P_CONF_DIR = '/var/lib/i2p/i2p-config'
FILE_TUNNEL_CONF = os.path.join(I2P_CONF_DIR, 'i2ptunnel.config')
TUNNEL_IDX_REGEX = re.compile(r'tunnel.(\d+).name$')
+I2P_ROUTER_CONF = os.path.join(I2P_CONF_DIR, 'router.config')
class TunnelEditor():
@@ -133,3 +135,93 @@ class TunnelEditor():
def __setitem__(self, tunnel_prop, value):
self.aug.set(self.calc_prop_path(tunnel_prop), value)
+
+
+class RouterEditor(object):
+ """
+
+ :type aug: augeas.Augeas
+ """
+
+ FAVORITE_PROP = 'routerconsole.favorites'
+ FAVORITE_TUPLE_SIZE = 4
+
+ def __init__(self, filename=None):
+ self.conf_filename = filename or I2P_ROUTER_CONF
+ self.aug = None
+
+ def read_conf(self):
+ self.aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
+ augeas.Augeas.NO_MODL_AUTOLOAD)
+ self.aug.set('/augeas/load/Properties/lens', 'Properties.lns')
+ self.aug.set('/augeas/load/Properties/incl[last() + 1]', self.conf_filename)
+ self.aug.load()
+ return self
+
+ def write_conf(self):
+ self.aug.save()
+ return self
+
+ @property
+ def favorite_property(self):
+ return '/files{filename}/{prop}'.format(filename=self.conf_filename, prop=self.FAVORITE_PROP)
+
+ def add_favorite(self, name, url, description='', icon='/themes/console/images/eepsite.png'):
+ """
+ Adds a favorite to the router.config
+
+ Favorites are in a single string and separated by ','.
+ none of the incoming params can therefore use commas.
+ I2P replaces the commas by dots.
+
+ That's ok for the name and description,
+ but not for the url and icon
+
+ :type name: basestring
+ :type url: basestring
+ :type description: basestring
+ :type icon: basestring
+ """
+ if ',' in url:
+ raise ValueError('URL cannot contain commas')
+ if ',' in icon:
+ raise ValueError('Icon cannot contain commas')
+
+ name = name.replace(',', '.')
+ description = description.replace(',', '.')
+
+ prop = self.favorite_property
+ favorites = self.aug.get(prop) or ''
+ new_favorite = '{name},{description},{url},{icon},'.format(
+ name=name, description=description, url=url,
+ icon=icon
+ )
+ self.aug.set(prop, favorites + new_favorite)
+ return self
+
+ def get_favorites(self):
+ favs_string = self.aug.get(self.favorite_property) or ''
+ favs_split = favs_string.split(',')
+
+ # There's a trailing comma --> 1 extra
+ favs_len = len(favs_split)
+ if favs_len > 0:
+ favs_split = favs_split[:-1]
+ favs_len = len(favs_split)
+
+ if favs_len % self.FAVORITE_TUPLE_SIZE:
+ raise SyntaxError("Invalid number of fields in favorite line")
+
+ favs = OrderedDict()
+ i = 0
+ while i < favs_len:
+ next_idx = i + self.FAVORITE_TUPLE_SIZE
+ t = favs_split[i:next_idx]
+ name, description, url, icon = t
+ favs[url] = {
+ "name": name,
+ "description": description,
+ "icon": icon
+ }
+ i = next_idx
+ return favs
diff --git a/plinth/modules/i2p/tests/__init__.py b/plinth/modules/i2p/tests/__init__.py
index e69de29bb..52393bed3 100644
--- a/plinth/modules/i2p/tests/__init__.py
+++ b/plinth/modules/i2p/tests/__init__.py
@@ -0,0 +1,18 @@
+# This file is part of FreedomBox.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+from pathlib import Path
+
+DATA_DIR = Path(__file__).parent / 'data'
diff --git a/plinth/modules/i2p/tests/data/router.config b/plinth/modules/i2p/tests/data/router.config
new file mode 100644
index 000000000..8661f7f28
--- /dev/null
+++ b/plinth/modules/i2p/tests/data/router.config
@@ -0,0 +1,23 @@
+# NOTE: This I2P config file must use UTF-8 encoding
+i2np.lastIPChange=1555091394049
+i2np.ntcp2.iv=i3yLM2tPW6QH5h4YYZ8EWQ==
+i2np.ntcp2.sp=j1K18jMDa5SPH23R2cHMJ-dUyPdo~uooZp6Uz06qP0k=
+i2np.udp.internalPort=18778
+i2np.udp.port=18778
+jbigi.lastProcessor=Haswell Celeron/Pentium w/ AVX model 60/64
+router.blocklistVersion=1523108115000
+router.firstInstalled=1555091372597
+router.firstVersion=0.9.38
+router.inboundPool.randomKey=c1BGKzCBpxYfsH0AiEMIS39zYWzyBuWO9lYTqeA91dk=
+router.outboundPool.randomKey=Of0jkHuGeUAHr~NoIAQbY930fbYacb4NyX3CjbVKKFI=
+router.passwordManager.migrated=true
+router.previousVersion=0.9.38
+router.startup.jetty9.migrated=true
+router.updateDisabled=true
+router.updateLastInstalled=1555091372597
+routerconsole.country=
+routerconsole.favorites=anoncoin.i2p,The Anoncoin project,http://anoncoin.i2p/,/themes/console/images/anoncoin_32.png,Dev Builds,Development builds of I2P,http://bobthebuilder.i2p/,/themes/console/images/script_gear.png,Dev Forum,Development forum,http://zzz.i2p/,/themes/console/images/group_gear.png,echelon.i2p,I2P Applications,http://echelon.i2p/,/themes/console/images/box_open.png,exchanged.i2p,Anonymous cryptocurrency exchange,http://exchanged.i2p/,/themes/console/images/exchanged.png,I2P Bug Reports,Bug tracker,http://trac.i2p2.i2p/report/1,/themes/console/images/bug.png,I2P FAQ,Frequently Asked Questions,http://i2p-projekt.i2p/faq,/themes/console/images/question.png,I2P Forum,Community forum,http://i2pforum.i2p/,/themes/console/images/group.png,I2P Plugins,Add-on directory,http://i2pwiki.i2p/index.php?title=Plugins,/themes/console/images/info/plugin_link.png,I2P Technical Docs,Technical documentation,http://i2p-projekt.i2p/how,/themes/console/images/education.png,I2P Wiki,Anonymous wiki - share the knowledge,http://i2pwiki.i2p/,/themes/console/images/i2pwiki_logo.png,Planet I2P,I2P News,http://planet.i2p/,/themes/console/images/world.png,PrivateBin,Encrypted I2P Pastebin,http://paste.crypthost.i2p/,/themes/console/images/paste_plain.png,Project Website,I2P home page,http://i2p-projekt.i2p/,/themes/console/images/info_rhombus.png,stats.i2p,I2P Network Statistics,http://stats.i2p/cgi-bin/dashboard.cgi,/themes/console/images/chart_line.png,The Tin Hat,Privacy guides and tutorials,http://secure.thetinhat.i2p/,/themes/console/images/thetinhat.png,Trac Wiki,,http://trac.i2p2.i2p/,/themes/console/images/billiard_marker.png,
+routerconsole.lang=en
+routerconsole.newsLastChecked=1555093599462
+routerconsole.newsLastUpdated=1553195540000
+routerconsole.welcomeWizardComplete=true
diff --git a/plinth/modules/i2p/tests/test_router_editor.py b/plinth/modules/i2p/tests/test_router_editor.py
new file mode 100644
index 000000000..10feb1c02
--- /dev/null
+++ b/plinth/modules/i2p/tests/test_router_editor.py
@@ -0,0 +1,86 @@
+# This file is part of FreedomBox.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+import unittest
+
+from plinth.modules.i2p.helpers import RouterEditor
+from plinth.modules.i2p.tests import DATA_DIR
+
+ROUTER_CONF_PATH = str(DATA_DIR / 'router.config')
+
+
+class RouterEditingTests(unittest.TestCase):
+
+ def setUp(self):
+ self.editor = RouterEditor(ROUTER_CONF_PATH)
+
+ def test_count_favs(self):
+ self.editor.read_conf()
+ favs = self.editor.get_favorites()
+ self.assertEqual(len(favs.keys()), 17)
+
+ def test_add_normal_favorite(self):
+ self.editor.read_conf()
+ name = 'Somewhere'
+ url = 'http://somewhere-again.i2p'
+ description = "Just somewhere else"
+ self.editor.add_favorite(
+ name, url, description
+ )
+
+ favs = self.editor.get_favorites()
+ self.assertIn(url, favs)
+ favorite = favs[url]
+ self.assertEqual(favorite['name'], name)
+ self.assertEqual(favorite['description'], description)
+
+ self.assertEqual(len(favs), 18)
+
+ def test_add_favorite_with_comma(self):
+ self.editor.read_conf()
+ name = 'Name,with,comma'
+ expected_name = name.replace(',', '.')
+ url = 'http://url-without-comma.i2p'
+ description = "Another,comma,to,play,with"
+ expected_description = description.replace(',', '.')
+
+ self.editor.add_favorite(
+ name, url, description
+ )
+
+ favs = self.editor.get_favorites()
+ self.assertIn(url, favs)
+ favorite = favs[url]
+ self.assertEqual(favorite['name'], expected_name)
+ self.assertEqual(favorite['description'], expected_description)
+
+ self.assertEqual(len(favs), 18)
+
+ def test_add_fav_to_empty_config(self):
+ self.editor.conf_filename = '/tmp/inexistent.conf'
+ self.editor.read_conf()
+ self.assertEqual(len(self.editor.get_favorites()), 0)
+
+ name = 'Test Favorite'
+ url = 'http://test-fav.i2p'
+ self.editor.add_favorite(
+ name, url
+ )
+ self.assertEqual(len(self.editor.get_favorites()), 1)
+
+
+if __name__ == '__main__':
+ unittest.main()