diff --git a/contrib/testing/python-runtime-check.sh b/contrib/testing/python-runtime-check.sh
new file mode 100755
index 0000000..9c9fd15
--- /dev/null
+++ b/contrib/testing/python-runtime-check.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+# /****************************************************************************
+# *
+# * Copyright (c) 2001 Novell, Inc. All Rights Reserved.
+# *
+# * This program is free software; you can redistribute it and/or
+# * modify it under the terms of version 2 of the GNU General Public License
+# * as published by the Free Software Foundation.
+# *
+# * 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 General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, contact Novell, Inc.
+# *
+# * To contact Novell about this file by physical or electronic mail, you
+# * may find current contact information at www.novell.com.
+# *
+# ****************************************************************************/
+
+set -euo pipefail
+
+temporary=$(mktemp -d "${TMPDIR:-/tmp}/bongo-python-runtime.XXXXXX")
+trap 'rm -rf -- "$temporary"' EXIT
+export PYTHONPYCACHEPREFIX="${temporary}/pycache"
+
+python3 -m compileall -q \
+ "$(python3 -c 'import sysconfig; print(sysconfig.get_path("purelib"))')/bongo" \
+ "$(python3 -c 'import sysconfig; print(sysconfig.get_path("purelib"))')/bongo_web"
+
+python3 - <<'PY'
+import io
+
+from bongo import table
+from bongo.Console import wrap
+from bongo.external.simpletal import simpleTAL, simpleTALES
+from bongo.external.simpletal.simpleElementTree import parseFile
+from bongo.storetool import BackupCommands, CalendarCommands, MailCommands
+
+assert isinstance(wrap("Grüße 世界", width=8), str)
+assert "Müller" in table.format_table(["Name"], [["Müller"]])
+
+context = simpleTALES.Context()
+context.addGlobal("name", "A&B")
+template = simpleTAL.compileHTMLTemplate(
+ '
placeholder
')
+output = io.StringIO()
+template.expand(context, output)
+assert output.getvalue() == "A&B
"
+
+root = parseFile(io.StringIO("value"))
+assert root.find("child").text == "value"
+PY
+
+bongo-admin --help >"${temporary}/bongo-admin.help"
+bongo-storetool --help >"${temporary}/bongo-storetool.help"
+bongo-queuetool --help >"${temporary}/bongo-queuetool.help"
+bongo-web --help >"${temporary}/bongo-web.help"
+/usr/libexec/bongo/bongo-sa-update --help \
+ >"${temporary}/bongo-sa-update.help"
+
+set +e
+/usr/libexec/bongo/bongo-acme-dns-provider \
+ --config "${temporary}/missing.json" --provider audit --type nsupdate \
+ --operation present --record _acme-challenge.example.test \
+ --value audit --ttl 60 >"${temporary}/bongo-acme.json"
+status=$?
+set -e
+test "${status}" -eq 1
+grep -q '"ok":false' "${temporary}/bongo-acme.json"
+
+echo "PYTHON-3 PASS compileall=installed imports=storetool,template,xml cli=6"
diff --git a/src/apps/admin/bongo/admintool/AgentCommands.py b/src/apps/admin/bongo/admintool/AgentCommands.py
index d87301a..b8d85cf 100644
--- a/src/apps/admin/bongo/admintool/AgentCommands.py
+++ b/src/apps/admin/bongo/admintool/AgentCommands.py
@@ -63,8 +63,7 @@ class AgentInfo(Command):
attrs = Util.GetAgentAttributes(mdb, dn)
keys = list(attrs.keys())
- keys.sort(lambda x,y: cmp(Util.PropertiesPrintable.get(x),
- Util.PropertiesPrintable.get(y)))
+ keys.sort(key=lambda key: Util.PropertiesPrintable.get(key, key))
for k in keys:
print(" |->(%s)" % Util.PropertiesPrintable[k])
diff --git a/src/apps/admin/bongo/admintool/DemoCommands.py b/src/apps/admin/bongo/admintool/DemoCommands.py
index a9eac0c..06be230 100644
--- a/src/apps/admin/bongo/admintool/DemoCommands.py
+++ b/src/apps/admin/bongo/admintool/DemoCommands.py
@@ -27,6 +27,11 @@ from bongo.admin import Util
from . import MdbUtil
+
+def _smtp_eols(value):
+ return value.replace("\r\n", "\n").replace("\r", "\n").replace(
+ "\n", "\r\n")
+
if os.path.isfile("demo/contacts/list.vcf") :
demopath = "demo"
else :
@@ -84,7 +89,7 @@ class DemoCommand(Command):
data = data.replace("@ADDRESS@", address)
data = data.replace("@NAME@", name)
- uid = store.Write("/mail/INBOX", DocTypes.Mail, email.Utils.fix_eols(data))
+ uid = store.Write("/mail/INBOX", DocTypes.Mail, _smtp_eols(data))
if filename in demo["starMail"] :
conversation = store.PropGet(uid, "nmap.mail.conversation")
print("starring conversation %s" % (conversation))
diff --git a/src/apps/admin/bongo/admintool/ServerCommands.py b/src/apps/admin/bongo/admintool/ServerCommands.py
index 1308976..db76289 100644
--- a/src/apps/admin/bongo/admintool/ServerCommands.py
+++ b/src/apps/admin/bongo/admintool/ServerCommands.py
@@ -38,8 +38,7 @@ class ServerInfo(Command):
attrs = Util.GetServerAttributes(mdb, server)
keys = list(attrs.keys())
- keys.sort(lambda x,y: cmp(Util.PropertiesPrintable.get(x),
- Util.PropertiesPrintable.get(y)))
+ keys.sort(key=lambda key: Util.PropertiesPrintable.get(key, key))
for key in keys:
print(" |->(%s)" % Util.PropertiesPrintable[key])
diff --git a/src/apps/admin/bongo/admintool/UserCommands.py b/src/apps/admin/bongo/admintool/UserCommands.py
index 5365d06..77282c7 100644
--- a/src/apps/admin/bongo/admintool/UserCommands.py
+++ b/src/apps/admin/bongo/admintool/UserCommands.py
@@ -111,8 +111,7 @@ class UserInfo(Command):
attrs = Util.GetUserAttributes(mdb, context, username)
keys = list(attrs.keys())
- keys.sort(lambda x,y: cmp(Util.PropertiesPrintable.get(x),
- Util.PropertiesPrintable.get(y)))
+ keys.sort(key=lambda key: Util.PropertiesPrintable.get(key, key))
for k in keys:
print(" |->(%s)" % Util.PropertiesPrintable[k])
diff --git a/src/apps/storetool/CMakeLists.txt b/src/apps/storetool/CMakeLists.txt
index 99b621b..9a6db54 100644
--- a/src/apps/storetool/CMakeLists.txt
+++ b/src/apps/storetool/CMakeLists.txt
@@ -13,7 +13,7 @@ ${SBIN_INSTALL_DIR})
if(BUILD_TESTING)
add_test(NAME storetool-document-type
COMMAND ${CMAKE_COMMAND} -E env
- "PYTHONPATH=${CMAKE_SOURCE_DIR}/src/libs/python:${CMAKE_CURRENT_SOURCE_DIR}"
+ "PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}:${CMAKE_SOURCE_DIR}/src/libs/python"
"PYTHONPYCACHEPREFIX=${CMAKE_CURRENT_BINARY_DIR}/pycache"
${Python3_EXECUTABLE} -m unittest discover
-s ${CMAKE_CURRENT_SOURCE_DIR}/tests -v)
diff --git a/src/apps/storetool/bongo/__init__.py b/src/apps/storetool/bongo/__init__.py
index f907027..b36383a 100644
--- a/src/apps/storetool/bongo/__init__.py
+++ b/src/apps/storetool/bongo/__init__.py
@@ -1,7 +1,3 @@
-# THIS FILE SHOULD NOT BE INSTALLED!
+from pkgutil import extend_path
-# It exists only to merge the src/[...]/storetool/bongo and src/libs/python/bongo
-# directories in the Bongo source tree together. In an installed
-# system, they're going to be in the same place.
-
-__path__.append(__path__[0] + "/../../../libs/python/bongo")
+__path__ = extend_path(__path__, __name__)
diff --git a/src/apps/storetool/bongo/storetool/BackupCommands.py b/src/apps/storetool/bongo/storetool/BackupCommands.py
index 8bf38ff..cf65043 100644
--- a/src/apps/storetool/bongo/storetool/BackupCommands.py
+++ b/src/apps/storetool/bongo/storetool/BackupCommands.py
@@ -1,309 +1,281 @@
-import logging
-import string
-import sys
+# /****************************************************************************
+# *
+# * Copyright (c) 2001 Novell, Inc. All Rights Reserved.
+# *
+# * This program is free software; you can redistribute it and/or
+# * modify it under the terms of version 2 of the GNU General Public License
+# * as published by the Free Software Foundation.
+# *
+# * 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 General Public License for more details.
+# *
+# * You should have received a copy of the GNU General Public License
+# * along with this program; if not, contact Novell, Inc.
+# *
+# * To contact Novell about this file by physical or electronic mail, you
+# * may find current contact information at www.novell.com.
+# *
+# ****************************************************************************/
-from gettext import gettext as _
+"""Portable PAX backups for a Bongo user Store."""
+
+from __future__ import annotations
+
+import io
+import posixpath
import tarfile
from bongo.cmdparse import Command
-from bongo.BongoError import BongoError
from bongo.store.StoreClient import DocTypes, StoreClient
-# import/export store contents as tar files
-# ideally we want to farm the tar format stuff itself out into a separate lib.
-# Sadly, the TarFile implementation that comes with Python is mostly useless
-class PAXHeader:
- def __init__(self, filename):
- self.keywords = {}
- self.filename = filename
+_METADATA_PREFIX = "BONGO."
+_GENERATED_PROPERTIES = {
+ "nmap.collection",
+ "nmap.flags",
+ "nmap.guid",
+ "nmap.index",
+ "nmap.type",
+}
- def SetKey(self, key, value):
- self.keywords[key] = value
- def Keys(self):
- return self.keywords
+def _text(value):
+ if isinstance(value, bytes):
+ return value.decode("utf-8", errors="replace")
+ return str(value)
- def ToString(self):
- header = tarfile.TarInfo(self.filename + ".metadata")
- header.type = "x"
- content = ""
- for key in list(self.keywords.keys()):
- if self.keywords[key] != None:
- content += "%d %s=%s\n" % (len(key) + len(self.keywords[key]) + 5, key, self.keywords[key])
- header.size = len(content)
+def _path(collection, filename):
+ filename = _text(filename)
+ if filename.startswith("/"):
+ return filename
+ if collection == "/":
+ return "/" + filename
+ return collection.rstrip("/") + "/" + filename
- result = header.tobuf()
- result += content
- remainder = len(content) % 512
- if remainder != 0:
- result += "\0" * (512 - remainder)
- return result
+def _metadata(item, properties):
+ result = {
+ "BONGO.uid": _text(item.uid),
+ "BONGO.type": str(int(item.type)),
+ "BONGO.flags": str(int(item.flags)),
+ }
+ imapuid = getattr(item, "imapuid", None)
+ if imapuid not in (None, "", "-", 0, "0"):
+ result["BONGO.imapuid"] = _text(imapuid)
+ for key, value in properties.items():
+ if value is not None:
+ result[_METADATA_PREFIX + _text(key)] = _text(value)
+ return result
+
+
+def _properties(metadata):
+ result = {}
+ for key, value in metadata.items():
+ if not key.startswith(_METADATA_PREFIX):
+ continue
+ name = key[len(_METADATA_PREFIX):]
+ if name in {"uid", "type", "flags", "imapuid"}:
+ continue
+ if name in _GENERATED_PROPERTIES:
+ continue
+ result[name] = value
+ return result
+
+
+def _safe_member(member):
+ name = member.name
+ if (not name.startswith("/") or "\0" in name or
+ posixpath.normpath(name) != name or
+ member.issym() or member.islnk() or
+ not (member.isdir() or member.isfile())):
+ raise ValueError("unsafe or unsupported backup member %r" % name)
+ metadata = member.pax_headers
+ for required in ("BONGO.uid", "BONGO.type", "BONGO.flags"):
+ if required not in metadata:
+ raise ValueError("%s has no %s metadata" % (name, required))
+ int(metadata["BONGO.type"])
+ int(metadata["BONGO.flags"])
+
+
+def validate_archive(filename):
+ """Return validated members without extracting anything."""
+ with tarfile.open(filename, "r:*") as archive:
+ members = archive.getmembers()
+ if not members:
+ raise ValueError("backup archive is empty")
+ names = set()
+ for member in members:
+ _safe_member(member)
+ if member.name in names:
+ raise ValueError("duplicate backup member %s" % member.name)
+ names.add(member.name)
+ return members
- def FromString(self, content):
- for line in string.split(content, "\n"):
- space = string.find(line, " ")
- equals = string.find(line, "=")
- key = line[space+1:equals]
- value = line[equals+1:]
- self.keywords[key] = value
class StoreBackupCommand(Command):
- log = logging.getLogger("Bongo.StoreTool")
- user = "admin"
- store = "_system"
-
def __init__(self):
- Command.__init__(self, "store-backup", aliases=["sb"],
- summary="Create a backup of a store",
- usage="%prog %cmd [