/src/static/favicon.ico
/src/static/img/apple-touch-icon-*-precomposed.png
/src/systemd/ipfire.org-webapp-*.service
-/src/templates/messages/main.css
+/src/templates/messages/*.css
/ipfire.org.conf.sample
.DS_Store
Makefile
Makefile.in
stamp-*
+*@*.jpg
*.bak
*.py[co]
*.tar.gz
src/backend/base.py \
src/backend/blog.py \
src/backend/bugzilla.py \
+ src/backend/cache.py \
src/backend/campaigns.py \
src/backend/countries.py \
src/backend/database.py \
src/backend/httpclient.py \
src/backend/hwdata.py \
src/backend/iuse.py \
- src/backend/memcached.py \
src/backend/messages.py \
src/backend/mirrors.py \
src/backend/misc.py \
templates_messages_DATA = \
src/templates/messages/base.html \
src/templates/messages/base-promo.html \
+ src/templates/messages/fonts.css \
src/templates/messages/main.css
templates_messagesdir = $(templatesdir)/messages
templates_modules_DATA = \
src/templates/modules/christmas-banner.html \
+ src/templates/modules/ipfire-logo.html \
src/templates/modules/map.html \
src/templates/modules/progress-bar.html
EXTRA_DIST += \
src/sass/listing.sass \
+ src/templates/messages/fonts.sass \
src/templates/messages/main.sass
CLEANFILES += \
+ src/templates/messages/fonts.css \
src/templates/messages/main.css
static_DATA = \
static_imgdir = $(staticdir)/img
+# From https://www.pexels.com/photo/123-let-s-go-imaginary-text-704767/
+
+dist_static_img_auth_DATA = \
+ src/static/img/auth/register.jpg
+
+static_img_auth_DATA = \
+ src/static/img/auth/register@600.jpg
+
+CLEANFILES += \
+ src/static/img/auth/register@600.jpg
+
+static_img_authdir = $(static_imgdir)/auth
+
static_images_tux_DATA = \
src/static/img/tux/ipfire_tux_16x16.png \
src/static/img/tux/ipfire_tux_20x20.png \
-extent $(patsubst src/static/img/apple-touch-icon-%-precomposed.png,%,$@)x$(patsubst src/static/img/apple-touch-icon-%-precomposed.png,%,$@) \
$< $@
+# Resizes images for being used in messages which are 600px wide
+%@600.jpg: %.jpg
+ $(AM_V_GEN)$(MKDIR_P) $(dir $@) && \
+ $(CONVERT) -units PixelsPerInch $< -resize 600x -strip -quality 85 $@
+
# Video Stuff
FFMPEG += \
[ipfire.org],
[https://www.ipfire.org/])
+AC_CONFIG_MACRO_DIR([m4])
AC_CONFIG_AUX_DIR([build-aux])
AC_PREFIX_DEFAULT([/usr])
AC_PROG_SED
# Python
-AM_PATH_PYTHON([3.4])
+AM_PATH_PYTHON([3.11])
+
+AX_PYTHON_MODULE([PIL], [fatal])
+AX_PYTHON_MODULE([feedparser], [fatal])
+AX_PYTHON_MODULE([html2text], [fatal])
+AX_PYTHON_MODULE([iso3166], [fatal])
+AX_PYTHON_MODULE([jsonschema], [fatal])
+AX_PYTHON_MODULE([kerberos], [fatal])
+AX_PYTHON_MODULE([ldap], [fatal])
+AX_PYTHON_MODULE([panoramisk], [fatal])
+AX_PYTHON_MODULE([phonenumbers], [fatal])
+AX_PYTHON_MODULE([psycopg], [fatal])
+AX_PYTHON_MODULE([pycares], [fatal])
+AX_PYTHON_MODULE([pynliner], [fatal])
+AX_PYTHON_MODULE([redis.asyncio], [fatal])
+AX_PYTHON_MODULE([tornado], [fatal])
+AX_PYTHON_MODULE([zxcvbn], [fatal])
# sass
AC_CHECK_PROG(SASSC, [sassc], [sassc])
--- /dev/null
+intltool.m4
+libtool.m4
+ltoptions.m4
+ltsugar.m4
+ltversion.m4
+lt~obsolete.m4
--- /dev/null
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_python_module.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_PYTHON_MODULE(modname[, fatal, python])
+#
+# DESCRIPTION
+#
+# Checks for Python module.
+#
+# If fatal is non-empty then absence of a module will trigger an error.
+# The third parameter can either be "python" for Python 2 or "python3" for
+# Python 3; defaults to Python 3.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Andrew Collier
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 9
+
+AU_ALIAS([AC_PYTHON_MODULE], [AX_PYTHON_MODULE])
+AC_DEFUN([AX_PYTHON_MODULE],[
+ if test -z $PYTHON;
+ then
+ if test -z "$3";
+ then
+ PYTHON="python3"
+ else
+ PYTHON="$3"
+ fi
+ fi
+ PYTHON_NAME=`basename $PYTHON`
+ AC_MSG_CHECKING($PYTHON_NAME module: $1)
+ $PYTHON -c "import $1" 2>/dev/null
+ if test $? -eq 0;
+ then
+ AC_MSG_RESULT(yes)
+ eval AS_TR_CPP(HAVE_PYMOD_$1)=yes
+ else
+ AC_MSG_RESULT(no)
+ eval AS_TR_CPP(HAVE_PYMOD_$1)=no
+ #
+ if test -n "$2"
+ then
+ AC_MSG_ERROR(failed to find required module $1)
+ exit 1
+ fi
+ fi
+])
--- /dev/null
+START TRANSACTION;
+
+-- CREATE INDEX fireinfo_search ON fireinfo USING gin (blob) WHERE expired_at IS NULL;
+-- CREATE UNIQUE INDEX fireinfo_current ON fireinfo USING btree (profile_id) WHERE expired_at IS NULL
+
+-- CREATE INDEX fireinfo_releases_current ON fireinfo USING hash((blob->'system'->'release')) WHERE expired_at IS NULL;
+-- CREATE INDEX fireinfo_releases ON fireinfo USING hash((blob->'system'->'release'));
+
+-- CREATE INDEX fireinfo_arches_current ON fireinfo USING hash((blob->'cpu'->'arch')) WHERE blob->'cpu'->'arch' IS NOT NULL AND expired_at IS NULL;
+-- CREATE INDEX fireinfo_arches ON fireinfo USING hash((blob->'cpu'->'arch')) WHERE blob->'cpu'->'arch' IS NOT NULL;
+
+-- CREATE INDEX fireinfo_cpu_vendors ON fireinfo USING hash((blob->'cpu'->'vendor')) WHERE blob->'cpu'->'vendor' IS NOT NULL;
+
+-- CREATE INDEX fireinfo_hypervisor_vendors_current ON fireinfo USING hash((blob->'hypervisor'->'vendor')) WHERE expired_at IS NULL AND CAST((blob->'system'->'virtual') AS boolean) IS TRUE;
+
+-- XXX virtual index
+
+TRUNCATE TABLE fireinfo;
+
+--EXPLAIN
+
+INSERT INTO fireinfo
+
+SELECT
+ p.public_id AS profile_id,
+ p.time_created AS created_at,
+ (
+ CASE
+ WHEN p.time_valid <= CURRENT_TIMESTAMP THEN p.time_valid
+ ELSE NULL
+ END
+ ) AS expired_at,
+ 0 AS version,
+ (
+ -- Empty the profile if we don't have any data
+ CASE WHEN profile_arches.arch_id IS NULL THEN NULL
+
+ -- Otherwise do some hard work...
+ ELSE
+ -- CPU
+ jsonb_build_object('cpu',
+ jsonb_build_object(
+ 'arch', arches.name,
+ 'bogomips', profile_processors.bogomips,
+ 'speed', profile_processors.clock_speed,
+
+ 'vendor', processors.vendor,
+ 'model', processors.model,
+ 'model_string', processors.model_string,
+ 'stepping', processors.stepping,
+ 'flags', processors.flags,
+ 'family', processors.family,
+ 'count', processors.core_count
+ )
+ )
+
+ -- System
+ || jsonb_build_object('system',
+ jsonb_build_object(
+ 'kernel', kernels.name,
+ 'language', profile_languages.language,
+ 'memory', profile_memory.amount,
+ 'release', releases.name,
+ 'root_size', profile_storage.amount,
+ 'vendor', systems.vendor,
+ 'model', systems.model,
+ 'virtual', CASE WHEN hypervisors.id IS NULL THEN FALSE ELSE TRUE END
+ )
+ )
+
+ -- Hypervisor
+ || CASE
+ WHEN hypervisors.id IS NULL THEN jsonb_build_object()
+ ELSE
+ jsonb_build_object(
+ 'hypervisor',
+ json_build_object('vendor', hypervisors.name)
+ )
+ END
+
+ -- Devices
+ || jsonb_build_object('devices', devices.devices)
+
+ -- Networks
+ || jsonb_build_object('networks',
+ jsonb_build_object(
+ 'green', profile_networks.has_green,
+ 'blue', profile_networks.has_blue,
+ 'orange', profile_networks.has_orange,
+ 'red', profile_networks.has_red
+ )
+ )
+ END
+ ) AS blob,
+ p.time_updated AS last_updated_at,
+ p.private_id AS private_id,
+ locations.location AS country_code
+
+FROM fireinfo_profiles p
+
+LEFT JOIN
+ fireinfo_profiles_locations locations ON p.id = locations.profile_id
+
+LEFT JOIN
+ fireinfo_profiles_arches profile_arches ON p.id = profile_arches.profile_id
+
+LEFT JOIN
+ fireinfo_arches arches ON profile_arches.arch_id = arches.id
+
+LEFT JOIN
+ (
+ SELECT
+ profile_devices.profile_id AS profile_id,
+ jsonb_agg(
+ jsonb_build_object(
+ 'deviceclass', devices.deviceclass,
+ 'subsystem', devices.subsystem,
+ 'vendor', devices.vendor,
+ 'model', devices.model,
+ 'sub_vendor', devices.sub_vendor,
+ 'sub_model', devices.sub_model,
+ 'driver', devices.driver
+ )
+ ) AS devices
+ FROM
+ fireinfo_profiles_devices profile_devices
+ LEFT JOIN
+ fireinfo_devices devices ON profile_devices.device_id = devices.id
+ GROUP BY
+ profile_devices.profile_id
+ ) devices ON p.id = devices.profile_id
+
+LEFT JOIN
+ fireinfo_profiles_processors profile_processors ON p.id = profile_processors.profile_id
+
+LEFT JOIN
+ fireinfo_processors processors ON profile_processors.processor_id = processors.id
+
+LEFT JOIN
+ fireinfo_profiles_kernels profile_kernels ON p.id = profile_kernels.profile_id
+
+LEFT JOIN
+ fireinfo_kernels kernels ON profile_kernels.kernel_id = kernels.id
+
+LEFT JOIN
+ fireinfo_profiles_languages profile_languages ON p.id = profile_languages.profile_id
+
+LEFT JOIN
+ fireinfo_profiles_memory profile_memory ON p.id = profile_memory.profile_id
+
+LEFT JOIN
+ fireinfo_profiles_releases profile_releases ON p.id = profile_releases.profile_id
+
+LEFT JOIN
+ fireinfo_releases releases ON profile_releases.release_id = releases.id
+
+LEFT JOIN
+ fireinfo_profiles_storage profile_storage ON p.id = profile_storage.profile_id
+
+LEFT JOIN
+ fireinfo_profiles_systems profile_systems ON p.id = profile_systems.profile_id
+
+LEFT JOIN
+ fireinfo_systems systems ON profile_systems.system_id = systems.id
+
+LEFT JOIN
+ fireinfo_profiles_virtual profile_virtual ON p.id = profile_virtual.profile_id
+
+LEFT JOIN
+ fireinfo_hypervisors hypervisors ON profile_virtual.hypervisor_id = hypervisors.id
+
+LEFT JOIN
+ fireinfo_profiles_networks profile_networks ON p.id = profile_networks.profile_id
+
+--WHERE
+-- XXX TO FIND A PROFILE WITH DATA
+-- profile_processors.profile_id IS NOT NULL
+
+-- XXX TO FIND A VIRTUAL PROFILE
+--profile_virtual.hypervisor_id IS NOT NULL
+
+--ORDER BY
+-- time_created DESC
+
+--LIMIT 1
+;
+
+COMMIT;
+++ /dev/null
-asn1crypto==0.24.0
-backports-abc==0.5
-certifi==2019.3.9
-cffi==1.11.5
-chardet==3.0.4
-cryptography==2.3.1
-ecdsa==0.13
-feedparser==5.2.1
-file-magic==0.4.0
-html5lib==1.0.1
-idna==2.7
-iso3166==0.9
-ldap3==2.5.1
-Markdown==3.1.1
-oauthlib==3.0.1
-phonenumbers==8.9.15
-Pillow==5.3.0
-psycopg2-binary==2.7.5
-py-dateutil==2.2
-pyasn1==0.4.4
-pyasn1-modules==0.2.2
-pycares==2.3.0
-pycparser==2.19
-pycrypto==2.6.1
-pycurl==7.43.0
-Pygments==2.4.2
-python-ldap==3.1.0
-python3-memcached==1.51
-requests==2.21.0
-requests-oauthlib==1.2.0
-sgmllib3k==1.0.0
-six==1.11.0
-textile==3.0.3
-tornado==6.0.2
-twython==3.7.0
-urllib3==1.24.3
-webencodings==0.5.1
-zxcvbn==4.4.27
self.search_base = self.settings.get("ldap_search_base")
def __len__(self):
- count = self.memcache.get("accounts:count")
-
- if count is None:
- count = self._count("(objectClass=person)")
-
- self.memcache.set("accounts:count", count, 300)
-
- return count
+ return self._count("(objectClass=person)")
def __iter__(self):
accounts = self._search("(objectClass=person)")
# Avatar
- def has_avatar(self):
- has_avatar = self.memcache.get("accounts:%s:has-avatar" % self.uid)
- if has_avatar is None:
- has_avatar = True if self.get_avatar() else False
+ @property
+ def avatar_hash(self):
+ payload = (
+ self.uid,
+ "%s" % self.modified_at,
+ )
+
+ # String the payload together
+ payload = "-".join(payload)
- # Cache avatar status for up to 24 hours
- self.memcache.set("accounts:%s:has-avatar" % self.uid, has_avatar, 3600 * 24)
+ # Run MD5() over the payload
+ h = hashlib.new("md5", payload.encode())
- return has_avatar
+ return h.hexdigest()[:7]
def avatar_url(self, size=None, absolute=False):
+ # This cannot be async because we are calling it from the template engine
url = "/users/%s.jpg?h=%s" % (self.uid, self.avatar_hash)
# Return an absolute URL
return url
- def get_avatar(self, size=None):
+ async def get_avatar(self, size=None):
photo = self._get_bytes("jpegPhoto")
# Exit if no avatar is available
return photo
# Try to retrieve something from the cache
- avatar = self.memcache.get("accounts:%s:avatar:%s" % (self.dn, size))
+ avatar = await self.backend.cache.get("accounts:%s:avatar:%s" % (self.dn, size))
if avatar:
return avatar
avatar = util.generate_thumbnail(photo, size, square=True)
# Save to cache for 15m
- self.memcache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
+ await self.backend.cache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
return avatar
- @property
- def avatar_hash(self):
- hash = self.memcache.get("accounts:%s:avatar-hash" % self.dn)
- if not hash:
- h = hashlib.new("md5")
- h.update(self.get_avatar() or b"")
- hash = h.hexdigest()[:7]
-
- self.memcache.set("accounts:%s:avatar-hash" % self.dn, hash, 86400)
-
- return hash
-
def upload_avatar(self, avatar):
self._set("jpegPhoto", avatar)
- # Delete cached avatar status
- self.memcache.delete("accounts:%s:has-avatar" % self.dn)
-
- # Delete avatar hash
- self.memcache.delete("accounts:%s:avatar-hash" % self.dn)
-
# Consent to promotional emails
def get_consents_to_promotional_emails(self):
from . import asterisk
from . import blog
from . import bugzilla
+from . import cache
from . import campaigns
from . import database
from . import fireinfo
from . import httpclient
from . import iuse
-from . import memcached
from . import messages
from . import mirrors
from . import netboot
# Create HTTPClient
self.http_client = httpclient.HTTPClient(self)
- # Initialize settings first.
+ # Initialize the cache
+ self.cache = cache.Cache(self)
+
+ # Initialize settings first
self.settings = settings.Settings(self)
- self.memcache = memcached.Memcached(self)
# Initialize backend modules.
self.accounts = accounts.Accounts(self)
- self.asterisk = asterisk.Asterisk(self)
self.bugzilla = bugzilla.Bugzilla(self)
self.fireinfo = fireinfo.Fireinfo(self)
self.iuse = iuse.IUse(self)
if r:
raise SystemExit(r)
+ @lazy_property
+ def asterisk(self):
+ return asterisk.Asterisk(self)
+
@lazy_property
def campaigns(self):
return campaigns.Campaigns(self)
--- /dev/null
+#!/usr/bin/python3
+
+import asyncio
+import logging
+import redis.asyncio
+
+from .decorators import *
+
+# Setup logging
+log = logging.getLogger()
+
+class Cache(object):
+ def __init__(self, backend):
+ self.backend = backend
+
+ # Stores connections assigned to tasks
+ self.__connections = {}
+
+ # Create a connection pool
+ self.pool = redis.asyncio.connection.ConnectionPool.from_url(
+ "redis://localhost:6379/0",
+ )
+
+ async def connection(self, *args, **kwargs):
+ """
+ Returns a connection from the pool
+ """
+ # Fetch the current task
+ task = asyncio.current_task()
+
+ assert task, "Could not determine task"
+
+ # Try returning the same connection to the same task
+ try:
+ return self.__connections[task]
+ except KeyError:
+ pass
+
+ # Fetch a new connection from the pool
+ conn = await redis.asyncio.Redis(
+ connection_pool=self.pool,
+ single_connection_client=True,
+ )
+
+ # Store the connection
+ self.__connections[task] = conn
+
+ log.debug("Assigning cache connection %s to %s" % (conn, task))
+
+ # When the task finishes, release the connection
+ task.add_done_callback(self.__release_connection)
+
+ return conn
+
+ def __release_connection(self, task):
+ loop = asyncio.get_running_loop()
+
+ # Retrieve the connection
+ try:
+ conn = self.__connections[task]
+ except KeyError:
+ return
+
+ log.debug("Releasing cache connection %s of %s" % (conn, task))
+
+ # Delete it
+ del self.__connections[task]
+
+ # Return the connection back into the pool
+ asyncio.run_coroutine_threadsafe(conn.close(), loop)
+
+ async def _run(self, command, *args, **kwargs):
+ # Fetch our connection
+ conn = await self.connection()
+
+ # Get the function
+ func = getattr(conn, command)
+
+ # Call the function
+ return await func(*args, **kwargs)
+
+ async def get(self, *args, **kwargs):
+ """
+ Fetches the value of a cached key
+ """
+ return await self._run("get", *args, **kwargs)
+
+ async def set(self, *args, **kwargs):
+ """
+ Puts something into the cache
+ """
+ return await self._run("set", *args, **kwargs)
+
+ async def delete(self, *args, **kwargs):
+ """
+ Deletes the key from the cache
+ """
+ return await self._run("delete", *args, **kwargs)
+
+ async def transaction(self, *args, **kwargs):
+ """
+ Returns a new transaction
+ """
+ conn = await self.connection()
+
+ return await conn.transaction(*args, **kwargs)
+
+ async def pipeline(self, *args, **kwargs):
+ """
+ Returns a new pipeline
+ """
+ conn = await self.connection()
+
+ return conn.pipeline(*args, **kwargs)
from . import misc
# Setup logging
-log = logging.getLogger("pbs.database")
+log = logging.getLogger()
class Connection(object):
"""
configure=self.__configure,
# Set limits for min/max connections in the pool
- min_size=4,
- max_size=128,
+ min_size=8,
+ max_size=512,
# Give clients up to one minute to retrieve a connection
timeout=60,
- # Close connections after they have been idle for one minute
- max_idle=60,
+ # Close connections after they have been idle for a few seconds
+ max_idle=5,
)
def __configure(self, conn):
import datetime
import iso3166
+import json
+import jsonschema
import logging
import re
-from . import database
from . import hwdata
from . import util
from .misc import Object
+from .decorators import *
N_ = lambda x: x
(r"Feroceon .*", r"ARM Feroceon"),
)
-IGNORED_DEVICES = ["usb",]
-
-class ProfileDict(object):
- def __init__(self, data):
- self._data = data
+PROFILE_SCHEMA = {
+ "$schema" : "https://json-schema.org/draft/2020-12/schema",
+ "$id" : "https://fireinfo.ipfire.org/profile.schema.json",
+ "title" : "Fireinfo Profile",
+ "description" : "Fireinfo Profile",
+ "type" : "object",
+ # Properties
+ "properties" : {
+ # Processor
+ "cpu" : {
+ "type" : "object",
+ "properties" : {
+ "arch" : {
+ "type" : "string",
+ "pattern" : r"^[a-z0-9\_]{,8}$",
+ },
+ "count" : {
+ "type" : "integer",
+ },
+ "family" : {
+ "type" : "integer",
+ },
+ "flags" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string",
+ "pattern" : r"^.{,24}$",
+ },
+ },
+ "model" : {
+ "type" : "integer",
+ },
+ "model_string" : {
+ "type" : "string",
+ "pattern" : r"^.{,80}$",
+ },
+ "speed" : {
+ "type" : "number",
+ },
+ "stepping" : {
+ "type" : "integer",
+ },
+ "vendor" : {
+ "type" : "string",
+ "pattern" : r"^.{,80}$",
+ },
+ },
+ "additionalProperties" : False,
+ "required" : [
+ "arch",
+ "count",
+ "family",
+ "flags",
+ "model",
+ "model_string",
+ "speed",
+ "stepping",
+ "vendor",
+ ],
+ },
-class ProfileNetwork(ProfileDict):
- def __eq__(self, other):
- if other is None:
- return False
+ # Devices
+ "devices" : {
+ "type" : "array",
+ "items" : {
+ "type" : "object",
+ "properties" : {
+ "deviceclass" : {
+ "type" : ["string", "null"],
+ "pattern" : r"^.{,20}$",
+ },
+ "driver" : {
+ "type" : ["string", "null"],
+ "pattern" : r"^.{,24}$",
+ },
+ "model" : {
+ "type" : "string",
+ "pattern" : r"^[a-z0-9]{4}$",
+ },
+ "sub_model" : {
+ "type" : ["string", "null"],
+ "pattern" : r"^[a-z0-9]{4}$",
+ },
+ "sub_vendor" : {
+ "type" : ["string", "null"],
+ "pattern" : r"^[a-z0-9]{4}$",
+ },
+ "subsystem" : {
+ "type" : "string",
+ "pattern" : r"^[a-z]{3}$",
+ },
+ "vendor" : {
+ "type" : "string",
+ "pattern" : r"^[a-z0-9]{4}$",
+ },
+ },
+ "additionalProperties" : False,
+ "required" : [
+ "deviceclass",
+ "driver",
+ "model",
+ "subsystem",
+ "vendor",
+ ],
+ },
+ },
- if not self.has_red == other.has_red:
- return False
+ # Network
+ "network" : {
+ "type" : "object",
+ "properties" : {
+ "blue" : {
+ "type" : "boolean",
+ },
+ "green" : {
+ "type" : "boolean",
+ },
+ "orange" : {
+ "type" : "boolean",
+ },
+ "red" : {
+ "type" : "boolean",
+ },
+ },
+ "additionalProperties" : False,
+ },
- if not self.has_green == other.has_green:
- return False
+ # System
+ "system" : {
+ "type" : "object",
+ "properties" : {
+ "kernel_release" : {
+ "type" : "string",
+ "pattern" : r"^.{,40}$",
+ },
+ "language" : {
+ "type" : "string",
+ "pattern" : r"^[a-z]{2}$",
+ },
+ "memory" : {
+ "type" : "integer",
+ },
+ "model" : {
+ "type" : "string",
+ "pattern" : r"^.{,80}$",
+ },
+ "release" : {
+ "type" : "string",
+ "pattern" : r"^.{,80}$",
+ },
+ "root_size" : {
+ "type" : "number",
+ },
+ "vendor" : {
+ "type" : "string",
+ "pattern" : r"^.{,80}$",
+ },
+ "virtual" : {
+ "type" : "boolean"
+ },
+ },
+ "additionalProperties" : False,
+ "required" : [
+ "kernel_release",
+ "language",
+ "memory",
+ "model",
+ "release",
+ "root_size",
+ "vendor",
+ "virtual",
+ ],
+ },
- if not self.has_orange == other.has_orange:
- return False
+ # Hypervisor
+ "hypervisor" : {
+ "type" : "object",
+ "properties" : {
+ "vendor" : {
+ "type" : "string",
+ "pattern" : r"^.{,40}$",
+ },
+ },
+ "additionalProperties" : False,
+ "required" : [
+ "vendor",
+ ],
+ },
- if not self.has_blue == other.has_blue:
- return False
+ # Error - BogoMIPS
+ "bogomips" : {
+ "type" : "number",
+ },
+ },
+ "additionalProperties" : False,
+ "required" : [
+ "cpu",
+ "devices",
+ "network",
+ "system",
+ ],
+}
- return True
+class Network(Object):
+ def init(self, blob):
+ self.blob = blob
def __iter__(self):
ret = []
return iter(ret)
def has_zone(self, name):
- return self._data.get("has_%s" % name)
+ return self.blob.get(name, False)
@property
def has_red(self):
- return self._data.get("has_red", False)
+ return self.has_zone("red")
@property
def has_green(self):
- return self._data.get("has_green", False)
+ return self.has_zone("green")
@property
def has_orange(self):
- return self._data.get("has_orange", False)
+ return self.has_zone("orange")
@property
def has_blue(self):
- return self._data.get("has_blue", False)
+ return self.has_zone("blue")
class Processor(Object):
- def __init__(self, backend, id, data=None, clock_speed=None, bogomips=None):
- Object.__init__(self, backend)
-
- self.id = id
- self.__data = data
- self.__clock_speed = clock_speed
- self.__bogomips = bogomips
+ def init(self, blob):
+ self.blob = blob
def __str__(self):
s = []
return " ".join(s)
- @property
- def data(self):
- if self.__data is None:
- self.__data = self.db.get("SELECT * FROM fireinfo_processors \
- WHERE id = %s", self.id)
-
- return self.__data
-
@property
def vendor(self):
+ vendor = self.blob.get("vendor")
+
try:
- return CPU_VENDORS[self.data.vendor]
+ return CPU_VENDORS[vendor]
except KeyError:
- return self.data.vendor
+ return vendor
@property
def family(self):
- return self.data.family
+ return self.blob.get("family")
@property
def model(self):
- return self.data.model
+ return self.blob.get("model")
@property
def stepping(self):
- return self.data.stepping
+ return self.blob.get("stepping")
@property
def model_string(self):
- if self.data.model_string:
- s = self.data.model_string.split()
-
- return " ".join((e for e in s if e))
+ return self.blob.get("model_string")
@property
def flags(self):
- return self.data.flags
+ return self.blob.get("flags")
def has_flag(self, flag):
return flag in self.flags
@property
def core_count(self):
- return self.data.core_count
+ return self.blob.get("count", 1)
@property
def count(self):
}
}
- def __init__(self, backend, id, data=None):
- Object.__init__(self, backend)
-
- self.id = id
- self.__data = data
+ def init(self, blob):
+ self.blob = blob
def __repr__(self):
return "<%s vendor=%s model=%s>" % (self.__class__.__name__,
def __eq__(self, other):
if isinstance(other, self.__class__):
- return self.id == other.id
+ return self.blob == other.blob
+
+ return NotImplemented
def __lt__(self, other):
if isinstance(other, self.__class__):
self.model_string < other.model_string or \
self.model < other.model
- @property
- def data(self):
- if self.__data is None:
- assert self.id
-
- self.__data = self.db.get("SELECT * FROM fireinfo_devices \
- WHERE id = %s", self.id)
-
- return self.__data
+ return NotImplemented
def is_showable(self):
- if self.driver in IGNORED_DEVICES:
- return False
-
- if self.driver in ("pcieport", "hub"):
+ if self.driver in ("usb", "pcieport", "hub"):
return False
return True
@property
def subsystem(self):
- return self.data.subsystem
+ return self.blob.get("subsystem")
@property
def model(self):
- return self.data.model
+ return self.blob.get("model")
- @property
+ @lazy_property
def model_string(self):
- return self.fireinfo.get_model_string(self.subsystem,
- self.vendor, self.model)
+ return self.fireinfo.get_model_string(self.subsystem, self.vendor, self.model)
@property
def vendor(self):
- return self.data.vendor
+ return self.blob.get("vendor")
- @property
+ @lazy_property
def vendor_string(self):
return self.fireinfo.get_vendor_string(self.subsystem, self.vendor)
@property
def driver(self):
- return self.data.driver
+ return self.blob.get("driver")
- @property
+ @lazy_property
def cls(self):
- classid = self.data.deviceclass
+ classid = self.blob.get("deviceclass")
if self.subsystem == "pci":
classid = classid[:-4]
except KeyError:
return "N/A"
- @property
- def percentage(self):
- return self.data.get("percentage", None)
-
-
-class Profile(Object):
- def __init__(self, backend, id, data=None):
- Object.__init__(self, backend)
-
- self.id = id
- self.__data = data
- def __repr__(self):
- return "<%s %s>" % (self.__class__.__name__, self.public_id)
-
- def __cmp__(self, other):
- return cmp(self.id, other.id)
-
- def is_showable(self):
- if self.arch_id:
- return True
-
- return False
+class System(Object):
+ def init(self, blob):
+ self.blob = blob
@property
- def data(self):
- if self.__data is None:
- self.__data = self.db.get("SELECT * FROM fireinfo_profiles \
- WHERE id = %s", self.id)
-
- return self.__data
+ def arch(self):
+ return self.blob.get("arch")
@property
- def public_id(self):
- return self.data.public_id
+ def language(self):
+ return self.blob.get("language")
@property
- def private_id(self):
- raise NotImplementedError
+ def vendor(self):
+ return self.blob.get("vendor")
@property
- def time_created(self):
- return self.data.time_created
+ def model(self):
+ return self.blob.get("model")
@property
- def time_updated(self):
- return self.data.time_updated
-
- def updated(self, profile_parser=None, country_code=None, when=None):
- valid = self.settings.get_int("fireinfo_profile_days_valid", 14)
-
- self.db.execute("UPDATE fireinfo_profiles \
- SET \
- time_updated = then_or_now(%s), \
- time_valid = then_or_now(%s) + INTERVAL '%s days', \
- updates = updates + 1 \
- WHERE id = %s", when, when, valid, self.id)
-
- if profile_parser:
- self.set_processor_speeds(
- profile_parser.processor_clock_speed,
- profile_parser.processor_bogomips,
- )
-
- if country_code:
- self.set_country_code(country_code)
-
- self.log_profile_update()
-
- def log_profile_update(self):
- # Log that an update was performed for this profile id
- self.db.execute("INSERT INTO fireinfo_profiles_log(public_id) \
- VALUES(%s)", self.public_id)
-
- def expired(self, when=None):
- self.db.execute("UPDATE fireinfo_profiles \
- SET time_valid = then_or_now(%s) WHERE id = %s", when, self.id)
+ def release(self):
+ return self.blob.get("release")
- def parse(self, parser):
- # Processor
- self.processor = parser.processor
- self.set_processor_speeds(parser.processor_clock_speed, parser.processor_bogomips)
+ @property
+ def storage(self):
+ return self.blob.get("storage_size", 0)
- # All devices
- self.devices = parser.devices
+ def is_virtual(self):
+ return self.blob.get("virtual", False)
- # System
- self.system_id = parser.system_id
- # Memory
- self.memory = parser.memory
+class Hypervisor(Object):
+ def init(self, blob):
+ self.blob = blob
- # Storage
- self.storage = parser.storage
+ def __str__(self):
+ return self.vendor
- # Kernel
- self.kernel_id = parser.kernel_id
+ @property
+ def vendor(self):
+ return self.blob.get("vendor")
- # Arch
- self.arch_id = parser.arch_id
- # Release
- self.release_id = parser.release_id
+class Profile(Object):
+ def init(self, profile_id, private_id, created_at, expired_at, version, blob,
+ last_updated_at, country_code, **kwargs):
+ self.profile_id = profile_id
+ self.private_id = private_id
+ self.created_at = created_at
+ self.expired_at = expired_at
+ self.version = version
+ self.blob = blob
+ self.last_updated_at = last_updated_at
+ self.country_code = country_code
- # Language
- self.language = parser.language
+ def __repr__(self):
+ return "<%s %s>" % (self.__class__.__name__, self.profile_id)
- # Virtual
- if parser.virtual:
- self.hypervisor_id = parser.hypervisor_id
+ def is_showable(self):
+ return True if self.blob else False
- # Network
- self.network = parser.network
+ @property
+ def public_id(self):
+ """
+ An alias for the profile ID
+ """
+ return self.profile_id
# Location
@property
def location(self):
- if not hasattr(self, "_location"):
- res = self.db.get("SELECT location FROM fireinfo_profiles_locations \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._location = res.location
- else:
- self._location = None
-
- return self._location
-
- def set_country_code(self, country_code):
- if self.location == country_code:
- return
-
- self.db.execute("DELETE FROM fireinfo_profiles_locations \
- WHERE profile_id = %s", self.id)
- self.db.execute("INSERT INTO fireinfo_profiles_locations(profile_id, location) \
- VALUES(%s, %s)", self.id, country_code)
-
- self._location = country_code
+ return self.country_code
@property
def location_string(self):
# Devices
- @property
- def device_ids(self):
- if not hasattr(self, "_device_ids"):
- res = self.db.query("SELECT device_id FROM fireinfo_profiles_devices \
- WHERE profile_id = %s", self.id)
-
- self._device_ids = sorted([r.device_id for r in res])
-
- return self._device_ids
-
- def get_devices(self):
- if not hasattr(self, "_devices"):
- res = self.db.query("SELECT * FROM fireinfo_devices \
- LEFT JOIN fireinfo_profiles_devices ON \
- fireinfo_devices.id = fireinfo_profiles_devices.device_id \
- WHERE fireinfo_profiles_devices.profile_id = %s", self.id)
-
- self._devices = []
- for row in res:
- device = Device(self.backend, row.id, row)
- self._devices.append(device)
-
- return self._devices
-
- def set_devices(self, devices):
- device_ids = [d.id for d in devices]
-
- self.db.execute("DELETE FROM fireinfo_profiles_devices WHERE profile_id = %s", self.id)
- self.db.executemany("INSERT INTO fireinfo_profiles_devices(profile_id, device_id) \
- VALUES(%s, %s)", ((self.id, d) for d in device_ids))
-
- self._devices = devices
- self._device_ids = device_ids
-
- devices = property(get_devices, set_devices)
-
- def count_device(self, subsystem, vendor, model):
- counter = 0
-
- for dev in self.devices:
- if dev.subsystem == subsystem and dev.vendor == vendor and dev.model == model:
- counter += 1
-
- return counter
+ @lazy_property
+ def devices(self):
+ return [Device(self.backend, blob) for blob in self.blob.get("devices", [])]
# System
- def get_system_id(self):
- if not hasattr(self, "_system_id"):
- res = self.db.get("SELECT system_id AS id FROM fireinfo_profiles_systems \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._system_id = res.id
- else:
- self._system_id = None
-
- return self._system_id
-
- def set_system_id(self, system_id):
- self.db.execute("DELETE FROM fireinfo_profiles_systems WHERE profile_id = %s", self.id)
-
- if system_id:
- self.db.execute("INSERT INTO fireinfo_profiles_systems(profile_id, system_id) \
- VALUES(%s, %s)", self.id, system_id)
-
- self._system_id = None
- if hasattr(self, "_system"):
- del self._system
-
- system_id = property(get_system_id, set_system_id)
-
- @property
+ @lazy_property
def system(self):
- if not hasattr(self, "_system"):
- res = self.db.get("SELECT fireinfo_systems.vendor AS vendor, fireinfo_systems.model AS model \
- FROM fireinfo_profiles_systems \
- LEFT JOIN fireinfo_systems ON fireinfo_profiles_systems.system_id = fireinfo_systems.id \
- WHERE fireinfo_profiles_systems.profile_id = %s", self.id)
-
- if res:
- self._system = (res.vendor, res.model)
- else:
- self._system = (None, None)
-
- return self._system
-
- @property
- def system_vendor(self):
- try:
- v, m = self.system
- return v
- except TypeError:
- pass
-
- @property
- def system_model(self):
- try:
- v, m = self.system
- return m
- except TypeError:
- pass
-
- @property
- def appliance_id(self):
- if not hasattr(self, "_appliance_id"):
- appliances = (
- ("fountainnetworks-duo-box", self._appliance_test_fountainnetworks_duo_box),
- ("fountainnetworks-prime", self._appliance_test_fountainnetworks_prime),
- ("lightningwirelabs-eco-plus", self._appliance_test_lightningwirelabs_eco_plus),
- ("lightningwirelabs-eco", self._appliance_test_lightningwirelabs_eco),
- )
+ return System(self.backend, self.blob.get("system", {}))
- self._appliance_id = None
- for name, test_func in appliances:
- if not test_func():
- continue
-
- self._appliance_id = name
- break
-
- return self._appliance_id
+ # Processor
@property
- def appliance(self):
- if self.appliance_id == "fountainnetworks-duo-box":
- return "Fountain Networks - IPFire Duo Box"
-
- elif self.appliance_id == "fountainnetworks-prime":
- return "Fountain Networks - IPFire Prime Box"
-
- elif self.appliance_id == "lightningwirelabs-eco-plus":
- return "Lightning Wire Labs - IPFire Eco Plus Appliance"
-
- elif self.appliance_id == "lightningwirelabs-eco":
- return "Lightning Wire Labs - IPFire Eco Appliance"
-
- def _appliance_test_fountainnetworks_duo_box(self):
- if not self.processor.vendor == "Intel":
- return False
-
- if not self.processor.model_string == "Intel(R) Celeron(R) 2957U @ 1.40GHz":
- return False
-
- if not self.count_device("pci", "10ec", "8168") == 2:
- return False
-
- # WiFi module
- #if self.count_device("usb", "148f", "5572") < 1:
- # return False
-
- return True
-
- def _appliance_test_fountainnetworks_prime(self):
- if not self.system in (("SECO", None), ("SECO", "0949")):
- return False
-
- # Must have a wireless device
- if self.count_device("usb", "148f", "5572") < 1:
- return False
-
- return True
-
- def _appliance_test_lightningwirelabs_eco(self):
- if not self.system == ("MSI", "MS-9877"):
- return False
-
- # Must have four Intel network adapters
- network_adapters_count = self.count_device("pci", "8086", "10d3")
- if not network_adapters_count == 4:
- return False
-
- return True
-
- def _appliance_test_lightningwirelabs_eco_plus(self):
- if not self.system_vendor == "ASUS":
- return False
-
- if not self.system_model.startswith("P9A-I/2550"):
- return False
-
- # Must have four Intel network adapters
- network_adapters_count = self.count_device("pci", "8086", "1f41")
- if not network_adapters_count == 4:
- return False
-
- return True
-
- # Processors
-
- @property
- def processor_id(self):
- if hasattr(self, "_processor"):
- return self._processor.id
-
- if not hasattr(self, "_processor_id"):
- res = self.db.get("SELECT processor_id FROM fireinfo_profiles_processors \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._processor_id = res.processor_id
- else:
- self._processor_id = None
-
- return self._processor_id
-
- def get_processor(self):
- if not self.processor_id:
- return
-
- if not hasattr(self, "_processor"):
- res = self.db.get("SELECT * FROM fireinfo_profiles_processors \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._processor = self.fireinfo.get_processor_by_id(res.processor_id,
- clock_speed=res.clock_speed, bogomips=res.bogomips)
- else:
- self._processor = None
-
- return self._processor
-
- def set_processor(self, processor):
- self.db.execute("DELETE FROM fireinfo_profiles_processors \
- WHERE profile_id = %s", self.id)
-
- if processor:
- self.db.execute("INSERT INTO fireinfo_profiles_processors(profile_id, processor_id) \
- VALUES(%s, %s)", self.id, processor.id)
-
- self._processor = processor
-
- processor = property(get_processor, set_processor)
-
- def set_processor_speeds(self, clock_speed, bogomips):
- self.db.execute("UPDATE fireinfo_profiles_processors \
- SET clock_speed = %s, bogomips = %s WHERE profile_id = %s",
- clock_speed, bogomips, self.id)
-
- # Compat
- @property
- def cpu(self):
- return self.processor
+ def processor(self):
+ return Processor(self.backend, self.blob.get("cpu", {}))
# Memory
- def get_memory(self):
- if not hasattr(self, "_memory"):
- res = self.db.get("SELECT amount FROM fireinfo_profiles_memory \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._memory = res.amount * 1024
- else:
- self._memory = None
-
- return self._memory
-
- def set_memory(self, amount):
- if self.memory == amount:
- return
-
- amount /= 1024
-
- self.db.execute("DELETE FROM fireinfo_profiles_memory WHERE profile_id = %s", self.id)
- if amount:
- self.db.execute("INSERT INTO fireinfo_profiles_memory(profile_id, amount) \
- VALUES(%s, %s)", self.id, amount)
-
- self._memory = amount * 1024
-
- memory = property(get_memory, set_memory)
+ @property
+ def memory(self):
+ return self.blob.get("memory")
@property
def friendly_memory(self):
return util.format_size(self.memory or 0)
- # Storage
-
- def get_storage(self):
- if not hasattr(self, "_storage"):
- res = self.db.get("SELECT amount FROM fireinfo_profiles_storage \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._storage = res.amount * 1024
- else:
- self._storage = None
-
- return self._storage
-
- def set_storage(self, amount):
- if self.storage == amount:
- return
-
- amount /= 1024
-
- self.db.execute("DELETE FROM fireinfo_profiles_storage WHERE profile_id = %s", self.id)
- if amount:
- self.db.execute("INSERT INTO fireinfo_profiles_storage(profile_id, amount) \
- VALUES(%s, %s)", self.id, amount)
-
- self._storage = amount * 1024
-
- storage = property(get_storage, set_storage)
-
- @property
- def friendly_storage(self):
- return util.format_size(self.storage)
-
- # Kernel
-
- def get_kernel_id(self):
- if not hasattr(self, "_kernel_id"):
- res = self.db.get("SELECT fireinfo_profiles_kernels.kernel_id AS id FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_kernels ON fireinfo_profiles.id = fireinfo_profiles_kernels.profile_id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._kernel_id = res.id
- else:
- self._kernel_id = None
-
- return self._kernel_id
-
- def set_kernel_id(self, kernel_id):
- if self.kernel_id == kernel_id:
- return
-
- self.db.execute("DELETE FROM fireinfo_profiles_kernels WHERE profile_id = %s", self.id)
- if kernel_id:
- self.db.execute("INSERT INTO fireinfo_profiles_kernels(profile_id, kernel_id) \
- VALUES(%s, %s)", self.id, kernel_id)
-
- self._kernel_id = kernel_id
- if hasattr(self, "_kernel"):
- del self._kernel
-
- kernel_id = property(get_kernel_id, set_kernel_id)
-
- @property
- def kernel(self):
- if not hasattr(self, "_kernel"):
- res = self.db.get("SELECT fireinfo_kernels.name AS name FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_kernels ON fireinfo_profiles.id = fireinfo_profiles_kernels.profile_id \
- LEFT JOIN fireinfo_kernels ON fireinfo_kernels.id = fireinfo_profiles_kernels.kernel_id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._kernel = res.name
- else:
- self._kernel = None
-
- return self._kernel
-
- # Arch
-
- def get_arch_id(self):
- if not hasattr(self, "_arch_id"):
- res = self.db.get("SELECT fireinfo_profiles_arches.arch_id AS id FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_arches ON fireinfo_profiles.id = fireinfo_profiles_arches.profile_id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._arch_id = res.id
- else:
- self._arch_id = None
-
- return self._arch_id
-
- def set_arch_id(self, arch_id):
- if self.arch_id == arch_id:
- return
-
- self.db.execute("DELETE FROM fireinfo_profiles_arches WHERE profile_id = %s", self.id)
- if arch_id:
- self.db.execute("INSERT INTO fireinfo_profiles_arches(profile_id, arch_id) \
- VALUES(%s, %s)", self.id, arch_id)
-
- self._arch_id = None
- if hasattr(self, "_arch"):
- del self._arch
-
- arch_id = property(get_arch_id, set_arch_id)
-
- @property
- def arch(self):
- if not hasattr(self, "_arch"):
- res = self.db.get("SELECT fireinfo_arches.name AS name FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_arches ON fireinfo_profiles.id = fireinfo_profiles_arches.profile_id \
- LEFT JOIN fireinfo_arches ON fireinfo_arches.id = fireinfo_profiles_arches.arch_id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._arch = res.name
- else:
- self._arch = None
-
- return self._arch
-
- # Release
-
- def get_release_id(self):
- if not hasattr(self, "_release_id"):
- res = self.db.get("SELECT fireinfo_profiles_releases.release_id AS id FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_releases ON fireinfo_profiles.id = fireinfo_profiles_releases.profile_id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._release_id = res.id
- else:
- self._release_id = None
-
- return self._release_id
-
- def set_release_id(self, release_id):
- if self.release_id == release_id:
- return
-
- self.db.execute("DELETE FROM fireinfo_profiles_releases WHERE profile_id = %s", self.id)
- if release_id:
- self.db.execute("INSERT INTO fireinfo_profiles_releases(profile_id, release_id) \
- VALUES(%s, %s)", self.id, release_id)
-
- self._release_id = release_id
- if hasattr(self, "_release"):
- del self._release
-
- release_id = property(get_release_id, set_release_id)
-
- @property
- def release(self):
- if not hasattr(self, "_release"):
- res = self.db.get("SELECT fireinfo_releases.name AS name FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_releases ON fireinfo_profiles.id = fireinfo_profiles_releases.profile_id \
- LEFT JOIN fireinfo_releases ON fireinfo_profiles_releases.release_id = fireinfo_releases.id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._release = self._format_release(res.name)
- else:
- self._release = None
-
- return self._release
-
- @staticmethod
- def _format_release(r):
- if not r:
- return r
-
- # Remove the development header
- r = r.replace("Development Build: ", "")
-
- pairs = (
- ("-beta", " - Beta "),
- ("-rc", " - Release Candidate "),
- ("core", "Core Update "),
- ("beta", "Beta "),
- )
-
- for k, v in pairs:
- r = r.replace(k, v)
-
- return r
-
- @property
- def release_short(self):
- pairs = (
- (r"Release Candidate (\d+)", r"RC\1"),
- )
-
- s = self.release
- for pattern, repl in pairs:
- if re.search(pattern, s) is None:
- continue
-
- s = re.sub(pattern, repl, s)
-
- return s
-
# Virtual
- @property
- def virtual(self):
- if not hasattr(self, "_virtual"):
- res = self.db.get("SELECT 1 FROM fireinfo_profiles_virtual \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._virtual = True
- else:
- self._virtual = False
-
- return self._virtual
-
- def get_hypervisor_id(self):
- if not hasattr(self, "_hypervisor_id"):
- res = self.db.get("SELECT fireinfo_profiles_virtual.hypervisor_id AS id FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_virtual ON fireinfo_profiles.id = fireinfo_profiles_virtual.profile_id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._hypervisor_id = res.id
- else:
- self._hypervisor_id = None
-
- return self._hypervisor_id
-
- def set_hypervisor_id(self, hypervisor_id):
- self.db.execute("DELETE FROM fireinfo_profiles_virtual WHERE profile_id = %s", self.id)
- self.db.execute("INSERT INTO fireinfo_profiles_virtual(profile_id, hypervisor_id) \
- VALUES(%s, %s)", self.id, hypervisor_id)
-
- self._hypervisor_id = hypervisor_id
-
- hypervisor_id = property(get_hypervisor_id, set_hypervisor_id)
+ def is_virtual(self):
+ return self.system.is_virtual()
@property
def hypervisor(self):
- if not hasattr(self, "_hypervisor"):
- res = self.db.get("SELECT fireinfo_hypervisors.name AS hypervisor FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_virtual ON fireinfo_profiles.id = fireinfo_profiles_virtual.profile_id \
- LEFT JOIN fireinfo_hypervisors ON fireinfo_profiles_virtual.hypervisor_id = fireinfo_hypervisors.id \
- WHERE fireinfo_profiles.id = %s", self.id)
-
- if res:
- self._hypervisor = res.hypervisor
- else:
- self._hypervisor = None
-
- return self._hypervisor
-
- # Language
-
- def get_language(self):
- if not hasattr(self, "_language"):
- res = self.db.get("SELECT language FROM fireinfo_profiles_languages \
- WHERE profile_id = %s", self.id)
-
- if res:
- self._language = res.language
- else:
- self._language = None
-
- return self._language
-
- def set_language(self, language):
- self.db.execute("DELETE FROM fireinfo_profiles_languages WHERE profile_id = %s", self.id)
-
- if language:
- self.db.execute("INSERT INTO fireinfo_profiles_languages(profile_id, language) \
- VALUES(%s, %s)", self.id, language)
-
- self._language = language
-
- language = property(get_language, set_language)
+ return Hypervisor(self.backend, self.blob.get("hypervisor"))
# Network
- def get_network(self):
- if not hasattr(self, "_network"):
- res = self.db.get("SELECT * FROM fireinfo_profiles_networks \
- WHERE profile_id = %s", self.id)
-
- if not res:
- res = {}
-
- self._network = ProfileNetwork(res)
-
- return self._network
-
- def set_network(self, network):
- self.db.execute("DELETE FROM fireinfo_profiles_networks WHERE profile_id = %s", self.id)
-
- if network:
- self.db.execute("INSERT INTO fireinfo_profiles_networks(profile_id, \
- has_red, has_green, has_orange, has_blue) VALUES(%s, %s, %s, %s, %s)",
- self.id, network.has_red, network.has_green, network.has_orange, network.has_blue)
-
- self._network = network
-
- network = property(get_network, set_network)
-
-
-class ProfileData(Object):
- def __init__(self, backend, id, data=None, profile=None):
- Object.__init__(self, backend)
-
- self.id = id
- self._data = data
- self._profile = profile
-
- @property
- def data(self):
- if self._data is None:
- self._data = self.db.get("SELECT * FROM fireinfo_profile_data \
- WHERE id = %s", self.id)
-
- return self._data
-
- @property
- def profile(self):
- if not self._profile:
- self._profile = self.fireinfo.get_profile_by_id(self.profile_id)
-
- return self._profile
-
- @property
- def profile_id(self):
- return self.data.profile_id
-
-
-class ProfileParserError(Exception):
- pass
-
-
-class ProfileParser(Object):
- __device_args = (
- "subsystem",
- "vendor",
- "model",
- "sub_vendor",
- "sub_model",
- "driver",
- "deviceclass",
- )
-
- __processor_args = (
- "vendor",
- "model_string",
- "family",
- "model",
- "stepping",
- "core_count",
- "flags",
- )
-
- def __init__(self, backend, public_id, blob=None):
- Object.__init__(self, backend)
-
- self.public_id = public_id
- self.private_id = None
- self.devices = []
- self.processor = None
- self.processor_clock_speed = None
- self.processor_bogomips = None
- self.system_id = None
- self.memory = None
- self.storage = None
- self.kernel = None
- self.kernel_id = None
- self.arch = None
- self.arch_id = None
- self.release = None
- self.release_id = None
- self.language = None
- self.virtual = None
- self.hypervisor_id = None
- self.network = None
-
- self.__parse_blob(blob)
-
- def equals(self, other):
- if not self.processor_id == other.processor_id:
- return False
-
- if not self.device_ids == other.device_ids:
- return False
-
- if not self.system_id == other.system_id:
- return False
-
- if not self.memory == other.memory:
- return False
-
- if not self.storage == other.storage:
- return False
-
- if not self.kernel_id == other.kernel_id:
- return False
-
- if not self.arch_id == other.arch_id:
- return False
-
- if not self.release_id == other.release_id:
- return False
-
- if not self.language == other.language:
- return False
-
- if not self.virtual == other.virtual:
- return False
-
- if other.virtual:
- if not self.hypervisor_id == other.hypervisor_id:
- return False
-
- if not self.network == other.network:
- return False
-
- return True
-
- def __parse_blob(self, blob):
- _profile = blob.get("profile", {})
- self.private_id = blob.get("private_id")
-
- # Do not try to parse an empty profile
- if not _profile:
- return
-
- # Processor
- _processor = _profile.get("cpu", {})
- self.__parse_processor(_processor)
-
- # Find devices
- _devices = _profile.get("devices", [])
- self.__parse_devices(_devices)
-
- # System
- _system = _profile.get("system")
- if _system:
- self.__parse_system(_system)
-
- # Memory (convert to bytes)
- memory = _system.get("memory", None)
- if memory:
- self.memory = memory * 1024
-
- # Storage size (convert to bytes)
- storage = _system.get("root_size", None)
- if storage:
- self.storage = storage * 1024
-
- # Kernel
- kernel = _system.get("kernel_release", None)
- if kernel:
- self.__parse_kernel(kernel)
-
- # Release
- release = _system.get("release", None)
- if release:
- self.__parse_release(release)
-
- # Language
- language = _system.get("language", None)
- if language:
- self.__parse_language(language)
-
- # Virtual
- self.virtual = _system.get("virtual", False)
- if self.virtual:
- hypervisor = _profile.get("hypervisor")
- self.__parse_hypervisor(hypervisor)
-
- # Network
- _network = _profile.get("network")
- if _network:
- self.__parse_network(_network)
-
- @property
- def device_ids(self):
- return sorted([d.id for d in self.devices])
-
- def __parse_devices(self, _devices):
- self.devices = []
-
- for _device in _devices:
- args = {}
- for arg in self.__device_args:
- args[arg] = _device.get(arg, None)
-
- # Skip if the subsystem is not set
- if not args.get("subsystem", None):
- continue
-
- # Find the device or create a new one.
- device = self.fireinfo.get_device(**args)
- if not device:
- device = self.fireinfo.create_device(**args)
-
- self.devices.append(device)
-
- def __parse_system(self, system):
- vendor = system.get("vendor", None)
- if not vendor:
- vendor = None
-
- model = system.get("model", None)
- if not model:
- model = None
-
- self.system_id = self.fireinfo.get_system(vendor, model)
- if not self.system_id:
- self.system_id = self.fireinfo.create_system(vendor, model)
-
- @property
- def processor_id(self):
- if not self.processor:
- return
-
- return self.processor.id
-
- def __parse_processor(self, _processor):
- args = {}
- for arg in self.__processor_args:
- if arg == "core_count":
- _arg = "count"
- else:
- _arg = arg
-
- args[arg] = _processor.get(_arg, None)
-
- self.processor = self.fireinfo.get_processor(**args)
- if not self.processor:
- self.processor = self.fireinfo.create_processor(**args)
-
- self.processor_clock_speed = _processor.get("speed", None)
- self.processor_bogomips = _processor.get("bogomips", None)
-
- arch = _processor.get("arch", None)
- if arch:
- self.__parse_arch(arch)
-
- def __parse_kernel(self, kernel):
- self.kernel_id = self.fireinfo.get_kernel(kernel)
- if not self.kernel_id:
- self.kernel_id = self.fireinfo.create_kernel(kernel)
- assert self.kernel_id
-
- self.kernel = kernel
-
- def __parse_arch(self, arch):
- self.arch_id = self.fireinfo.get_arch(arch)
- if not self.arch_id:
- self.arch_id = self.fireinfo.create_arch(arch)
-
- self.arch = arch
-
- def __parse_release(self, release):
- # Remove the arch bit
- if release:
- r = [e for e in release.split() if e]
- for s in ("(x86_64)", "(aarch64)", "(i586)", "(armv6l)", "(armv5tel)", "(riscv64)"):
- try:
- r.remove(s)
- break
- except ValueError:
- pass
-
- release = " ".join(r)
-
- self.release_id = self.fireinfo.get_release(release)
- if not self.release_id:
- self.release_id = self.fireinfo.create_release(release)
- assert self.release_id
-
- self.release = release
-
- def __parse_language(self, language):
- self.language = language
- self.language, delim, rest = self.language.partition(".")
- self.language, delim, rest = self.language.partition("_")
-
- def __parse_hypervisor(self, hypervisor):
- vendor = hypervisor.get("vendor", "other")
-
- if vendor in ("other", "unknown"):
- self.hypervisor_id = None
- return
-
- self.hypervisor_id = self.fireinfo.get_hypervisor(vendor)
- if not self.hypervisor_id:
- self.hypervisor_id = self.fireinfo.create_hypervisor(vendor)
-
- def __parse_network(self, network):
- self.network = ProfileNetwork({
- "has_red" : network.get("red", False),
- "has_green" : network.get("green", False),
- "has_orange" : network.get("orange", False),
- "has_blue" : network.get("blue", False),
- })
+ @lazy_property
+ def network(self):
+ return Network(self.backend, self.blob.get("network", {}))
class Fireinfo(Object):
- def get_profile_count(self, when=None):
- res = self.db.get("SELECT COUNT(*) AS count FROM fireinfo_profiles \
- WHERE then_or_now(%s) BETWEEN time_created AND time_valid", when)
-
- if res:
- return res.count
-
- def get_total_updates_count(self, when=None):
- res = self.db.get("SELECT COUNT(*) + SUM(updates) AS count \
- FROM fireinfo_profiles WHERE time_created <= then_or_now(%s)", when)
-
- if res:
- return res.count
-
- # Parser
-
- def parse_profile(self, public_id, blob):
- return ProfileParser(self.backend, public_id, blob)
-
- # Profiles
-
- def profile_exists(self, public_id):
- res = self.db.get("SELECT id FROM fireinfo_profiles \
- WHERE public_id = %s LIMIT 1", public_id)
-
- if res:
- return True
-
- return False
-
- def profile_rate_limit_active(self, public_id, when=None):
- res = self.db.get("SELECT COUNT(*) AS count FROM fireinfo_profiles_log \
- WHERE public_id = %s AND ts >= then_or_now(%s) - INTERVAL '60 minutes'",
- public_id, when)
-
- if res and res.count >= 10:
- return True
-
- return False
-
- def is_private_id_change_permitted(self, public_id, private_id, when=None):
- # Check if a profile exists with a different private id that is still valid
- res = self.db.get("SELECT 1 FROM fireinfo_profiles \
- WHERE public_id = %s AND NOT private_id = %s \
- AND time_valid >= then_or_now(%s) LIMIT 1", public_id, private_id, when)
-
- if res:
- return False
-
- return True
-
- def get_profile(self, public_id, private_id=None, when=None):
- res = self.db.get("SELECT * FROM fireinfo_profiles \
- WHERE public_id = %s AND \
- (CASE WHEN %s IS NULL THEN TRUE ELSE private_id = %s END) AND \
- then_or_now(%s) BETWEEN time_created AND time_valid \
- ORDER BY time_updated DESC LIMIT 1",
- public_id, private_id, private_id, when)
-
- if res:
- return Profile(self.backend, res.id, res)
+ async def expire(self):
+ """
+ Called to expire any profiles that have not been updated in a fortnight
+ """
+ self.db.execute("UPDATE fireinfo SET expired_at = CURRENT_TIMESTAMP \
+ WHERE last_updated_at <= CURRENT_TIMESTAMP - %s", datetime.timedelta(days=14))
- def get_profile_with_data(self, public_id, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT * FROM profiles JOIN fireinfo_profiles ON profiles.id = fireinfo_profiles.id \
- WHERE public_id = %s ORDER BY time_updated DESC LIMIT 1", when, public_id)
+ def _get_profile(self, query, *args, **kwargs):
+ res = self.db.get(query, *args, **kwargs)
if res:
- return Profile(self.backend, res.id, res)
+ return Profile(self.backend, **res)
- def get_profiles(self, public_id):
- res = self.db.query("SELECT * FROM fireinfo_profiles \
- WHERE public_id = %s ORDER BY time_created DESC", public_id)
-
- profiles = []
- for row in res:
- profile = Profile(self.backend, row.id, row)
- profiles.append(profile)
-
- return profiles
-
- def create_profile(self, public_id, private_id, when=None):
- valid = self.settings.get_int("fireinfo_profile_days_valid", 14)
-
- res = self.db.get("INSERT INTO fireinfo_profiles(public_id, private_id, \
- time_created, time_updated, time_valid) VALUES(%s, %s, then_or_now(%s), \
- then_or_now(%s), then_or_now(%s) + INTERVAL '%s days') RETURNING id",
- public_id, private_id, when, when, when, valid)
-
- if res:
- p = Profile(self.backend, res.id)
- p.log_profile_update()
-
- return p
-
- # Devices
-
- def create_device(self, subsystem, vendor, model, sub_vendor=None, sub_model=None,
- driver=None, deviceclass=None):
- res = self.db.get("INSERT INTO fireinfo_devices(subsystem, vendor, model, \
- sub_vendor, sub_model, driver, deviceclass) VALUES(%s, %s, %s, %s, %s, %s, %s) \
- RETURNING id", subsystem, vendor, model, sub_vendor, sub_model, driver, deviceclass)
-
- if res:
- return Device(self.backend, res.id)
-
- def get_device(self, subsystem, vendor, model, sub_vendor=None, sub_model=None,
- driver=None, deviceclass=None):
- res = self.db.get("SELECT * FROM fireinfo_devices \
- WHERE subsystem = %s AND vendor = %s AND model = %s \
- AND sub_vendor IS NOT DISTINCT FROM %s \
- AND sub_model IS NOT DISTINCT FROM %s \
- AND driver IS NOT DISTINCT FROM %s \
- AND deviceclass IS NOT DISTINCT FROM %s \
- LIMIT 1", subsystem, vendor, model, sub_vendor,
- sub_model, driver, deviceclass)
-
- if res:
- return Device(self.backend, res.id, res)
-
- # System
-
- def create_system(self, vendor, model):
- res = self.db.get("INSERT INTO fireinfo_systems(vendor, model) \
- VALUES(%s, %s) RETURNING id", vendor, model)
-
- if res:
- return res.id
-
- def get_system(self, vendor, model):
- res = self.db.get("SELECT id FROM fireinfo_systems WHERE vendor IS NOT DISTINCT FROM %s \
- AND model IS NOT DISTINCT FROM %s LIMIT 1", vendor, model)
-
- if res:
- return res.id
-
- # Processors
-
- def create_processor(self, vendor, model_string, family, model, stepping, core_count, flags=None):
- res = self.db.get("INSERT INTO fireinfo_processors(vendor, model_string, \
- family, model, stepping, core_count, flags) VALUES(%s, %s, %s, %s, %s, %s, %s) \
- RETURNING id", vendor or None, model_string or None, family, model, stepping, core_count, flags)
-
- if res:
- return Processor(self.backend, res.id)
-
- def get_processor_by_id(self, processor_id, **kwargs):
- res = self.db.get("SELECT * FROM fireinfo_processors \
- WHERE id = %s", processor_id)
-
- if res:
- return Processor(self.backend, res.id, data=res, **kwargs)
-
- def get_processor(self, vendor, model_string, family, model, stepping, core_count, flags=None):
- if flags is None:
- flags = []
-
- res = self.db.get("SELECT * FROM fireinfo_processors \
- WHERE vendor IS NOT DISTINCT FROM %s AND model_string IS NOT DISTINCT FROM %s \
- AND family IS NOT DISTINCT FROM %s AND model IS NOT DISTINCT FROM %s \
- AND stepping IS NOT DISTINCT FROM %s AND core_count = %s \
- AND flags <@ %s AND flags @> %s", vendor or None, model_string or None,
- family, model, stepping, core_count, flags, flags)
-
- if res:
- return Processor(self.backend, res.id, res)
-
- # Kernel
-
- def create_kernel(self, kernel):
- res = self.db.get("INSERT INTO fireinfo_kernels(name) VALUES(%s) \
- RETURNING id", kernel)
-
- if res:
- return res.id
-
- def get_kernel(self, kernel):
- res = self.db.get("SELECT id FROM fireinfo_kernels WHERE name = %s", kernel)
-
- if res:
- return res.id
-
- # Arch
+ def get_profile_count(self, when=None):
+ if when:
+ res = self.db.get("""
+ SELECT
+ COUNT(*) AS count
+ FROM
+ fireinfo
+ WHERE
+ created_at <= %s
+ AND
+ (
+ expired_at IS NULL
+ OR
+ expired_at > %s
+ )
+ """)
+ else:
+ res = self.db.get("""
+ SELECT
+ COUNT(*) AS count
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ """,
+ )
- def create_arch(self, arch):
- res = self.db.get("INSERT INTO fireinfo_arches(name) VALUES(%s) \
- RETURNING id", arch)
+ return res.count if res else 0
- if res:
- return res.id
+ def get_profile_histogram(self):
+ today = datetime.date.today()
- def get_arch(self, arch):
- res = self.db.get("SELECT id FROM fireinfo_arches WHERE name = %s", arch)
+ t1 = datetime.date(year=today.year - 10, month=today.month, day=1)
+ t2 = datetime.date(year=today.year, month=today.month, day=1)
- if res:
- return res.id
+ res = self.db.query("""
+ SELECT
+ date,
+ COUNT(*) AS count
+ FROM
+ generate_series(%s, %s, INTERVAL '1 month') date
+ JOIN
+ fireinfo ON date >= created_at
+ AND (expired_at IS NULL OR expired_at > date)
+ GROUP BY
+ date
+ """, t1, t2)
- # Release
+ return { row.date : row.count for row in res }
- def create_release(self, release):
- res = self.db.get("INSERT INTO fireinfo_releases(name) VALUES(%s) \
- RETURNING id", release)
+ # Profiles
- if res:
- return res.id
+ def get_profile(self, profile_id, when=None):
+ if when:
+ return self._get_profile("""
+ SELECT
+ *
+ FROM
+ fireinfo
+ WHERE
+ profile_id = %s
+ AND
+ %s BETWEEN created_at AND expired_at
+ """, profile_id,
+ )
- def get_release(self, release):
- res = self.db.get("SELECT id FROM fireinfo_releases WHERE name = %s", release)
+ return self._get_profile("""
+ SELECT
+ *
+ FROM
+ fireinfo
+ WHERE
+ profile_id = %s
+ AND
+ expired_at IS NULL
+ """, profile_id,
+ )
- if res:
- return res.id
+ # Handle profile
- def get_release_penetration(self, release, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS penetration FROM profiles \
- LEFT JOIN fireinfo_profiles_releases ON profiles.id = fireinfo_profiles_releases.profile_id \
- WHERE fireinfo_profiles_releases.release_id = %s", when, release.fireinfo_id)
+ def handle_profile(self, profile_id, blob, country_code=None, when=None):
+ private_id = blob.get("private_id", None)
+ assert private_id
- if res:
- return res.penetration
+ now = datetime.datetime.utcnow()
- def get_random_country_penetration(self):
- res = self.db.get("SELECT * FROM fireinfo_country_percentages \
- ORDER BY RANDOM() LIMIT 1")
+ # Fetch the profile version
+ version = blob.get("profile_version")
- if res:
- return database.Row({
- "country" : iso3166.countries.get(res.location),
- "percentage" : res.count,
- })
+ # Extract the profile
+ profile = blob.get("profile")
- # Hypervisor
+ # Validate the profile
+ self._validate(profile_id, version, profile)
- def create_hypervisor(self, hypervisor):
- res = self.db.get("INSERT INTO fireinfo_hypervisors(name) VALUES(%s) \
- RETURNING id", hypervisor)
+ # Pre-process the profile
+ profile = self._preprocess(profile)
- if res:
- return res.id
+ # Fetch the previous profile
+ prev = self.get_profile(profile_id)
- def get_hypervisor(self, hypervisor):
- res = self.db.get("SELECT id FROM fireinfo_hypervisors WHERE name = %s",
- hypervisor)
+ if prev:
+ # Check if the private ID matches
+ if not prev.private_id == private_id:
+ logging.error("Private ID for profile %s does not match" % profile_id)
+ return False
- if res:
- return res.id
+ # Check when the last update was
+ elif now - prev.last_updated_at < datetime.timedelta(hours=6):
+ logging.warning("Profile %s has been updated too soon" % profile_id)
+ return False
- # Handle profile
+ # Check if the profile has changed
+ elif prev.version == version and prev.blob == blob:
+ logging.debug("Profile %s has not changed" % profile_id)
+
+ # Update the timestamp
+ self.db.execute("UPDATE fireinfo SET last_updated_at = CURRENT_TIMESTAMP \
+ WHERE profile_id = %s AND expired_at IS NULL", profile_id)
+
+ return True
+
+ # Delete the previous profile
+ self.db.execute("UPDATE fireinfo SET expired_at = CURRENT_TIMESTAMP \
+ WHERE profile_id = %s AND expired_at IS NULL", profile_id)
+
+ # Store the new profile
+ self.db.execute("""
+ INSERT INTO
+ fireinfo
+ (
+ profile_id,
+ private_id,
+ version,
+ blob,
+ country_code
+ )
+ VALUES
+ (
+ %s,
+ %s,
+ %s,
+ %s,
+ %s
+ )
+ """, profile_id, private_id, version, json.dumps(profile), country_code,
+ )
- def handle_profile(self, *args, **kwargs):
- self.db.execute("START TRANSACTION")
+ def _validate(self, profile_id, version, blob):
+ """
+ Validate the profile
+ """
+ if not version == 0:
+ raise ValueError("Unsupported profile version")
- # Wrap all the handling of the profile in a huge transaction.
+ # Validate the blob
try:
- self._handle_profile(*args, **kwargs)
-
- except:
- self.db.execute("ROLLBACK")
- raise
-
- else:
- self.db.execute("COMMIT")
-
- def _handle_profile(self, public_id, profile_blob, country_code=None, when=None):
- private_id = profile_blob.get("private_id", None)
- assert private_id
-
- # Check if the profile already exists in the database.
- profile = self.fireinfo.get_profile(public_id, private_id=private_id, when=when)
+ return jsonschema.validate(blob, schema=PROFILE_SCHEMA)
- # Check if the update can actually be updated
- if profile and self.fireinfo.profile_rate_limit_active(public_id, when=when):
- logging.warning("There were too many updates for this profile in the last hour: %s" % public_id)
- return
-
- elif not self.is_private_id_change_permitted(public_id, private_id, when=when):
- logging.warning("Changing private id is not permitted for profile: %s" % public_id)
- return
-
- # Parse the profile
- profile_parser = self.parse_profile(public_id, profile_blob)
+ # Raise a ValueError instead which is easier to handle later on
+ except jsonschema.exceptions.ValidationError as e:
+ raise ValueError("%s" % e) from e
- # If a profile exists, check if it matches and if so, just update the
- # timestamp.
- if profile:
- # Check if the profile has changed. If so, update the data.
- if profile_parser.equals(profile):
- profile.updated(profile_parser, country_code=country_code, when=when)
- return
+ def _preprocess(self, blob):
+ """
+ Modifies the profile before storing it
+ """
+ # Remove the architecture from the release string
+ blob["system"]["release"]= self._filter_release(blob["system"]["release"])
- # If it does not match, we assume that it is expired and
- # create a new profile.
- profile.expired(when=when)
+ return blob
- # Replace the old profile with a new one
- profile = self.fireinfo.create_profile(public_id, private_id, when=when)
- profile.parse(profile_parser)
+ def _filter_release(self, release):
+ """
+ Removes the arch part
+ """
+ r = [e for e in release.split() if e]
- if country_code:
- profile.set_country_code(country_code)
+ for s in ("(x86_64)", "(aarch64)", "(i586)", "(armv6l)", "(armv5tel)", "(riscv64)"):
+ try:
+ r.remove(s)
+ break
+ except ValueError:
+ pass
- return profile
+ return " ".join(r)
# Data outputs
def get_random_profile(self, when=None):
- # Check if the architecture exists so that we pick a profile with some data
- res = self.db.get("SELECT public_id FROM fireinfo_profiles \
- LEFT JOIN fireinfo_profiles_arches ON fireinfo_profiles.id = fireinfo_profiles_arches.profile_id \
- WHERE fireinfo_profiles_arches.profile_id IS NOT NULL \
- AND then_or_now(%s) BETWEEN time_created AND time_valid ORDER BY RANDOM() LIMIT 1", when)
+ if when:
+ return self._get_profile("""
+ SELECT
+ *
+ FROM
+ fireinfo
+ WHERE
+ created_at <= %s
+ AND
+ (
+ expired_at IS NULL
+ OR
+ expired_at > %s
+ )
+ ORDER BY
+ RANDOM()
+ LIMIT
+ 1
+ """, when, when,
+ )
- if res:
- return res.public_id
+ return self._get_profile("""
+ SELECT
+ *
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ ORDER BY
+ RANDOM()
+ LIMIT
+ 1
+ """)
def get_active_profiles(self, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_at(%s) AS id) \
- SELECT COUNT(*) AS with_data, (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
- LEFT JOIN fireinfo_profiles_releases ON profiles.id = fireinfo_profiles_releases.profile_id \
- WHERE fireinfo_profiles_releases.profile_id IS NOT NULL", when)
+ if when:
+ raise NotImplementedError
- if res:
- return res.with_data, res.count
-
- def get_archive_size(self, when=None):
- res = self.db.get("SELECT COUNT(*) AS count FROM fireinfo_profiles \
- WHERE time_created <= then_or_now(%s)", when)
+ else:
+ res = self.db.get("""
+ SELECT
+ COUNT(*) AS total_profiles,
+ COUNT(*) FILTER (WHERE blob IS NOT NULL) AS active_profiles
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ """)
if res:
- return res.count
-
- def get_geo_location_map(self, when=None, minimum_percentage=0):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_at(%s) AS id) \
- SELECT location, COUNT(location)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
- LEFT JOIN fireinfo_profiles_locations ON profiles.id = fireinfo_profiles_locations.profile_id \
- WHERE fireinfo_profiles_locations.location IS NOT NULL GROUP BY location \
- HAVING COUNT(location)::float / (SELECT COUNT(*) FROM profiles) >= %s ORDER BY count DESC",
- when, minimum_percentage)
-
- return list(((r.location, r.count) for r in res))
+ return res.active_profiles, res.total_profiles
+
+ def get_geo_location_map(self, when=None):
+ if when:
+ res = self.db.query("""
+ SELECT
+ country_code,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ created_at <= %s
+ AND
+ (
+ expired_at IS NULL
+ OR
+ expired_at > %s
+ )
+ AND
+ country_code IS NOT NULL
+ GROUP BY
+ country_code
+ """, when, when)
+ else:
+ res = self.db.query("""
+ SELECT
+ country_code,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ country_code IS NOT NULL
+ GROUP BY
+ country_code
+ """)
+
+ return { row.country_code : row.p for row in res }
@property
def cpu_vendors(self):
- res = self.db.query("SELECT DISTINCT vendor FROM fireinfo_processors ORDER BY vendor")
+ res = self.db.query("""
+ SELECT DISTINCT
+ blob->'cpu'->'vendor' AS vendor
+ FROM
+ fireinfo
+ WHERE
+ blob->'cpu'->'vendor' IS NOT NULL
+ """,
+ )
- return (CPU_VENDORS.get(r.vendor, r.vendor) for r in res)
+ return sorted((CPU_VENDORS.get(row.vendor, row.vendor) for row in res))
def get_cpu_vendors_map(self, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT COALESCE(vendor, %s) AS vendor, COUNT(vendor)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
- LEFT JOIN fireinfo_profiles_processors ON profiles.id = fireinfo_profiles_processors.profile_id \
- LEFT JOIN fireinfo_processors ON fireinfo_profiles_processors.processor_id = fireinfo_processors.id \
- WHERE NOT fireinfo_profiles_processors.processor_id IS NULL GROUP BY vendor ORDER BY count DESC", when, "Unknown")
-
- return ((CPU_VENDORS.get(r.vendor, r.vendor), r.count) for r in res)
-
- def get_cpu_clock_speeds(self, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT AVG(fireinfo_profiles_processors.clock_speed) AS avg, \
- STDDEV(fireinfo_profiles_processors.clock_speed) AS stddev, \
- MIN(fireinfo_profiles_processors.clock_speed) AS min, \
- MAX(fireinfo_profiles_processors.clock_speed) AS max FROM profiles \
- LEFT JOIN fireinfo_profiles_processors ON profiles.id = fireinfo_profiles_processors.profile_id \
- WHERE NOT fireinfo_profiles_processors.processor_id IS NULL \
- AND fireinfo_profiles_processors.clock_speed > 0 \
- AND fireinfo_profiles_processors.clock_speed < fireinfo_profiles_processors.bogomips \
- AND fireinfo_profiles_processors.bogomips <= %s", when, 10000)
+ if when:
+ raise NotImplementedError
- if res:
- return (res.avg or 0, res.stddev or 0, res.min or 0, res.max or 0)
-
- def get_cpus_with_platform_and_flag(self, platform, flag, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id), \
- processors AS (SELECT fireinfo_processors.id AS id, fireinfo_processors.flags AS flags FROM profiles \
- LEFT JOIN fireinfo_profiles_processors ON profiles.id = fireinfo_profiles_processors.profile_id \
- LEFT JOIN fireinfo_processors ON fireinfo_profiles_processors.processor_id = fireinfo_processors.id \
- LEFT JOIN fireinfo_profiles_arches ON profiles.id = fireinfo_profiles_arches.profile_id \
- LEFT JOIN fireinfo_arches ON fireinfo_profiles_arches.arch_id = fireinfo_arches.id \
- WHERE NOT fireinfo_profiles_processors.processor_id IS NULL \
- AND fireinfo_arches.platform = %s AND NOT 'hypervisor' = ANY(fireinfo_processors.flags)) \
- SELECT (COUNT(*)::float / (SELECT NULLIF(COUNT(*), 0) FROM processors)) AS count FROM processors \
- WHERE %s = ANY(processors.flags)", when, platform, flag)
-
- return res.count or 0
-
- def get_common_cpu_flags_by_platform(self, platform, when=None):
- if platform == "arm":
- flags = (
- "lpae", "neon", "thumb", "thumbee", "vfpv3", "vfpv4",
- )
- elif platform == "x86":
- flags = (
- "aes", "avx", "avx2", "lm", "mmx", "mmxext", "nx", "pae",
- "pni", "popcnt", "sse", "sse2", "rdrand", "ssse3", "sse4a",
- "sse4_1", "sse4_2", "pclmulqdq", "rdseed",
- )
else:
- return
+ res = self.db.query("""
+ SELECT
+ blob->'cpu'->'vendor' AS vendor,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ AND
+ blob->'cpu'->'vendor' IS NOT NULL
+ GROUP BY
+ blob->'cpu'->'vendor'
+ """)
+
+ return { CPU_VENDORS.get(row.vendor, row.vendor) : row.p for row in res }
+
+ def get_cpu_flags_map(self, when=None):
+ if when:
+ raise NotImplementedError
- ret = []
- for flag in flags:
- ret.append((flag, self.get_cpus_with_platform_and_flag(platform, flag, when=when)))
+ else:
+ res = self.db.query("""
+ WITH arch_flags AS (
+ SELECT
+ ROW_NUMBER() OVER (PARTITION BY blob->'cpu'->'arch') AS id,
+ blob->'cpu'->'arch' AS arch,
+ blob->'cpu'->'flags' AS flags
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob->'cpu'->'arch' IS NOT NULL
+ AND
+ blob->'cpu'->'flags' IS NOT NULL
+
+ -- Filter out virtual systems
+ AND
+ CAST((blob->'system'->'virtual') AS boolean) IS FALSE
+ )
+
+ SELECT
+ arch,
+ flag,
+ fireinfo_percentage(
+ COUNT(*),
+ (
+ SELECT
+ MAX(id)
+ FROM
+ arch_flags __arch_flags
+ WHERE
+ arch_flags.arch = __arch_flags.arch
+ )
+ ) AS p
+ FROM
+ arch_flags, jsonb_array_elements(arch_flags.flags) AS flag
+ GROUP BY
+ arch, flag
+ """)
+
+ result = {}
- # Add virtual CPU flag "virt" for virtualization support
- if platform == "x86":
- ret.append(("virt",
- self.get_cpus_with_platform_and_flag(platform, "vmx", when=when) + \
- self.get_cpus_with_platform_and_flag(platform, "svm", when=when)))
+ for row in res:
+ try:
+ result[row.arch][row.flag] = row.p
+ except KeyError:
+ result[row.arch] = { row.flag : row.p }
- return sorted(ret, key=lambda x: x[1], reverse=True)
+ return result
def get_average_memory_amount(self, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT AVG(fireinfo_profiles_memory.amount) AS avg FROM profiles \
- LEFT JOIN fireinfo_profiles_memory ON profiles.id = fireinfo_profiles_memory.profile_id", when)
-
- if res:
- return res.avg or 0
+ if when:
+ res = self.db.get("""
+ SELECT
+ AVG(
+ CAST(blob->'system'->'memory' AS numeric)
+ ) AS memory
+ FROM
+ fireinfo
+ WHERE
+ created_at <= %s
+ AND
+ (
+ expired_at IS NULL
+ OR
+ expired_at > %s
+ )
+ """, when)
+ else:
+ res = self.db.get("""
+ SELECT
+ AVG(
+ CAST(blob->'system'->'memory' AS numeric)
+ ) AS memory
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ """,)
+
+ return res.memory if res else 0
def get_arch_map(self, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT fireinfo_arches.name AS arch, COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS count \
- FROM profiles \
- LEFT JOIN fireinfo_profiles_arches ON profiles.id = fireinfo_profiles_arches.profile_id \
- LEFT JOIN fireinfo_arches ON fireinfo_profiles_arches.arch_id = fireinfo_arches.id \
- WHERE NOT fireinfo_profiles_arches.profile_id IS NULL \
- GROUP BY fireinfo_arches.id ORDER BY count DESC", when)
+ if when:
+ raise NotImplementedError
- return ((r.arch, r.count) for r in res)
+ else:
+ res = self.db.query("""
+ SELECT
+ blob->'cpu'->'arch' AS arch,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob->'cpu'->'arch' IS NOT NULL
+ GROUP BY
+ blob->'cpu'->'arch'
+ """)
+
+ return { row.arch : row.p for row in res }
# Virtual
def get_hypervisor_map(self, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id), \
- virtual_profiles AS (SELECT profiles.id AS profile_id, fireinfo_profiles_virtual.hypervisor_id FROM profiles \
- LEFT JOIN fireinfo_profiles_virtual ON profiles.id = fireinfo_profiles_virtual.profile_id \
- WHERE fireinfo_profiles_virtual.profile_id IS NOT NULL) \
- SELECT COALESCE(fireinfo_hypervisors.name, %s) AS name, \
- COUNT(*)::float / (SELECT COUNT(*) FROM virtual_profiles) AS count FROM virtual_profiles \
- LEFT JOIN fireinfo_hypervisors ON virtual_profiles.hypervisor_id = fireinfo_hypervisors.id \
- GROUP BY fireinfo_hypervisors.name ORDER BY count DESC", when, "unknown")
-
- return ((r.name, r.count) for r in res)
+ if when:
+ raise NotImplementedError
+ else:
+ res = self.db.query("""
+ SELECT
+ blob->'hypervisor'->'vendor' AS vendor,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ CAST((blob->'system'->'virtual') AS boolean) IS TRUE
+ AND
+ blob->'hypervisor'->'vendor' IS NOT NULL
+ GROUP BY
+ blob->'hypervisor'->'vendor'
+ """)
+
+ return { row.vendor : row.p for row in res }
def get_virtual_ratio(self, when=None):
- res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
- LEFT JOIN fireinfo_profiles_virtual ON profiles.id = fireinfo_profiles_virtual.profile_id \
- WHERE fireinfo_profiles_virtual.profile_id IS NOT NULL", when)
+ if when:
+ raise NotImplementedError
- if res:
- return res.count
+ else:
+ res = self.db.get("""
+ SELECT
+ fireinfo_percentage(
+ COUNT(*) FILTER (
+ WHERE CAST((blob->'system'->'virtual') AS boolean) IS TRUE
+ ),
+ COUNT(*)
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ """)
+
+ return res.p if res else 0
# Releases
def get_releases_map(self, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT fireinfo_releases.name, COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
- LEFT JOIN fireinfo_profiles_releases ON profiles.id = fireinfo_profiles_releases.profile_id \
- LEFT JOIN fireinfo_releases ON fireinfo_profiles_releases.release_id = fireinfo_releases.id \
- GROUP BY fireinfo_releases.name ORDER BY count DESC", when)
+ if when:
+ raise NotImplementedError
- return ((r.name, r.count) for r in res)
+ else:
+ res = self.db.query("""
+ SELECT
+ blob->'system'->'release' AS release,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ AND
+ blob->'system'->'release' IS NOT NULL
+ GROUP BY
+ blob->'system'->'release'
+ """)
+
+ return { row.release : row.p for row in res }
+
+ # Kernels
def get_kernels_map(self, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT fireinfo_kernels.name, COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
- LEFT JOIN fireinfo_profiles_kernels ON profiles.id = fireinfo_profiles_kernels.profile_id \
- LEFT JOIN fireinfo_kernels ON fireinfo_profiles_kernels.kernel_id = fireinfo_kernels.id \
- GROUP BY fireinfo_kernels.name ORDER BY count DESC", when)
-
- return ((r.name, r.count) for r in res)
-
- def _process_devices(self, devices):
- result = []
-
- for dev in devices:
- dev = Device(self.backend, dev.get("id", None), dev)
- result.append(dev)
-
- return result
-
- def get_driver_map(self, driver, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id), \
- devices AS (SELECT * FROM profiles \
- LEFT JOIN fireinfo_profiles_devices ON profiles.id = fireinfo_profiles_devices.profile_id \
- LEFT JOIN fireinfo_devices ON fireinfo_profiles_devices.device_id = fireinfo_devices.id \
- WHERE driver = %s) \
- SELECT subsystem, model, vendor, driver, deviceclass, \
- COUNT(*)::float / (SELECT COUNT(*) FROM devices) AS percentage FROM devices \
- GROUP BY subsystem, model, vendor, driver, deviceclass \
- ORDER BY percentage DESC", when, driver)
+ if when:
+ raise NotImplementedError
- return self._process_devices(res)
+ else:
+ res = self.db.query("""
+ SELECT
+ blob->'system'->'kernel' AS kernel,
+ fireinfo_percentage(
+ COUNT(*), SUM(COUNT(*)) OVER ()
+ ) AS p
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ AND
+ blob->'system'->'kernel' IS NOT NULL
+ GROUP BY
+ blob->'system'->'kernel'
+ """)
+
+ return { row.kernel : row.p for row in res }
subsystem2class = {
"pci" : hwdata.PCI(),
return cls.get_device(vendor_id, model_id) or ""
def get_vendor_list(self, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
- SELECT DISTINCT fireinfo_devices.subsystem AS subsystem, fireinfo_devices.vendor AS vendor FROM profiles \
- LEFT JOIN fireinfo_profiles_devices ON profiles.id = fireinfo_profiles_devices.profile_id \
- LEFT JOIN fireinfo_devices ON fireinfo_profiles_devices.device_id = fireinfo_devices.id \
- WHERE NOT fireinfo_devices.driver = ANY(%s)", when, IGNORED_DEVICES)
+ if when:
+ raise NotImplementedError
+
+ else:
+ res = self.db.query("""
+ WITH devices AS (
+ SELECT
+ jsonb_array_elements(blob->'devices') AS device
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ AND
+ blob->'devices' IS NOT NULL
+ AND
+ jsonb_typeof(blob->'devices') = 'array'
+ )
+
+ SELECT
+ devices.device->'subsystem' AS subsystem,
+ devices.device->'vendor' AS vendor
+ FROM
+ devices
+ WHERE
+ devices.device->'subsystem' IS NOT NULL
+ AND
+ devices.device->'vendor' IS NOT NULL
+ AND
+ NOT devices.device->>'driver' = 'usb'
+ GROUP BY
+ subsystem, vendor
+ """)
vendors = {}
+
for row in res:
- vendor = self.get_vendor_string(row.subsystem, row.vendor)
+ vendor = self.get_vendor_string(row.subsystem, row.vendor) or row.vendor
# Drop if vendor could not be determined
if vendor is None:
except KeyError:
vendors[vendor] = [(row.subsystem, row.vendor)]
- vendors = list(vendors.items())
- return sorted(vendors)
+ return vendors
+
+ def _get_devices(self, query, *args, **kwargs):
+ res = self.db.query(query, *args, **kwargs)
+
+ return [Device(self.backend, blob) for blob in res]
def get_devices_by_vendor(self, subsystem, vendor, when=None):
- res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id), \
- devices AS (SELECT * FROM profiles \
- LEFT JOIN fireinfo_profiles_devices ON profiles.id = fireinfo_profiles_devices.profile_id \
- LEFT JOIN fireinfo_devices ON fireinfo_profiles_devices.device_id = fireinfo_devices.id \
- WHERE NOT fireinfo_devices.driver = ANY(%s)), \
- vendor_devices AS (SELECT * FROM devices WHERE devices.subsystem = %s AND devices.vendor = %s) \
- SELECT subsystem, model, vendor, driver, deviceclass FROM vendor_devices \
- GROUP BY subsystem, model, vendor, driver, deviceclass", when, IGNORED_DEVICES, subsystem, vendor)
-
- return self._process_devices(res)
+ if when:
+ raise NotImplementedError
+
+ else:
+ return self._get_devices("""
+ WITH devices AS (
+ SELECT
+ jsonb_array_elements(blob->'devices') AS device
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ AND
+ blob->'devices' IS NOT NULL
+ AND
+ jsonb_typeof(blob->'devices') = 'array'
+ )
+
+ SELECT
+ device.deviceclass,
+ device.subsystem,
+ device.vendor,
+ device.model,
+ device.driver
+ FROM
+ devices,
+ jsonb_to_record(devices.device) AS device(
+ deviceclass text,
+ subsystem text,
+ vendor text,
+ sub_vendor text,
+ model text,
+ sub_model text,
+ driver text
+ )
+ WHERE
+ devices.device->>'subsystem' = %s
+ AND
+ devices.device->>'vendor' = %s
+ AND
+ NOT devices.device->>'driver' = 'usb'
+ GROUP BY
+ device.deviceclass,
+ device.subsystem,
+ device.vendor,
+ device.model,
+ device.driver
+ """, subsystem, vendor,
+ )
+
+ def get_devices_by_driver(self, driver, when=None):
+ if when:
+ raise NotImplementedError
+
+ else:
+ return self._get_devices("""
+ WITH devices AS (
+ SELECT
+ jsonb_array_elements(blob->'devices') AS device
+ FROM
+ fireinfo
+ WHERE
+ expired_at IS NULL
+ AND
+ blob IS NOT NULL
+ AND
+ blob->'devices' IS NOT NULL
+ AND
+ jsonb_typeof(blob->'devices') = 'array'
+ )
+
+ SELECT
+ device.deviceclass,
+ device.subsystem,
+ device.vendor,
+ device.model,
+ device.driver
+ FROM
+ devices,
+ jsonb_to_record(devices.device) AS device(
+ deviceclass text,
+ subsystem text,
+ vendor text,
+ sub_vendor text,
+ model text,
+ sub_model text,
+ driver text
+ )
+ WHERE
+ devices.device->>'driver' = '%s'
+ GROUP BY
+ device.deviceclass,
+ device.subsystem,
+ device.vendor,
+ device.model,
+ device.driver
+ """, driver,
+ )
+++ /dev/null
-#!/usr/bin/python
-
-import logging
-import memcache
-
-from .misc import Object
-
-class Memcached(Object):
- def init(self):
- self._connection = memcache.Client(["localhost"], debug=1)
-
- def get(self, key, *args, **kwargs):
- key = self._sanitize_key(key)
-
- logging.debug("Retrieving %s from cache..." % key)
-
- ret = self._connection.get(key, *args, **kwargs)
-
- if ret is None:
- logging.debug("Found nothing for %s" % key)
- else:
- logging.debug("Found object for %s" % key)
-
- return ret
-
- def get_multi(self, keys, *args, **kwargs):
- keys = (self._sanitize_key(key) for key in keys)
-
- logging.debug("Retrieving keys from cache: %s" % keys)
-
- ret = self._connection.get_multi(keys, *args, **kwargs)
-
- if ret is None:
- logging.debug("Found nothing for %s" % keys)
- else:
- logging.debug("Found objects for %s" % keys)
-
- return ret
-
- def add(self, key, data, *args, **kwargs):
- key = self._sanitize_key(key)
-
- if data is None:
- logging.debug("Putting nothing into cache for %s" % key)
- else:
- logging.debug("Putting object into cache for %s" % key)
-
- return self._connection.add(key, data, *args, **kwargs)
-
- def set(self, key, data, *args, **kwargs):
- key = self._sanitize_key(key)
-
- if data is None:
- logging.debug("Putting nothing into cache for %s" % key)
- else:
- logging.debug("Putting object into cache for %s" % key)
-
- return self._connection.set(key, data, *args, **kwargs)
-
- def delete(self, key, *args, **kwargs):
- key = self._sanitize_key(key)
-
- return self._connection.delete(key, *args, **kwargs)
-
- def incr(self, key):
- key = self._sanitize_key(key)
-
- logging.debug("Incrementing key %s" % key)
-
- return self._connection.incr(key)
-
- @staticmethod
- def _sanitize_key(key):
- # Memcache does not seem to like any spaces
- return key.replace(" ", "-")
#!/usr/bin/python3
+import base64
import email
import email.mime.multipart
import email.mime.text
import email.utils
import logging
+import mimetypes
+import os.path
+import pynliner
import random
import smtplib
import socket
from . import util
from .decorators import *
+# Encode emails in UTF-8 by default
+email.charset.add_charset("utf-8", email.charset.SHORTEST, email.charset.QP, "utf-8")
+
class Messages(misc.Object):
@lazy_property
def queue(self):
templates_dir = self.backend.config.get("global", "templates_dir")
assert templates_dir
- return tornado.template.Loader(templates_dir, autoescape=None)
+ # Setup namespace
+ namespace = {
+ "embed_image" : self.embed_image,
+ }
+
+ return tornado.template.Loader(templates_dir, namespace=namespace, autoescape=None)
def make_recipient(self, recipient):
# Use the contact instead of the account
except KeyError:
message.add_header(header, value)
+ # Inline any CSS
+ if extension == "html":
+ message_part = self._inline_css(message_part)
+
# Create a MIMEText object out of it
message_part = email.mime.text.MIMEText(
message_part.get_payload(), mimetype)
if self.backend.debug:
self.template_loader.reset()
+ def _inline_css(self, part):
+ """
+ Inlines any CSS into style attributes
+ """
+ # Fetch the payload
+ payload = part.get_payload()
+
+ # Setup Pynliner
+ p = pynliner.Pynliner().from_string(payload)
+
+ # Run the inlining
+ payload = p.run()
+
+ # Set the payload again
+ part.set_payload(payload)
+
+ return part
+
+ def embed_image(self, path):
+ static_dir = self.backend.config.get("global", "static_dir")
+ assert static_dir
+
+ # Make the path absolute
+ path = os.path.join(static_dir, path)
+
+ # Fetch the mimetype
+ mimetype, encoding = mimetypes.guess_type(path)
+
+ # Read the file
+ with open(path, "rb") as f:
+ data = f.read()
+
+ # Convert data into base64
+ data = base64.b64encode(data)
+
+ # Return everything
+ return "data:%s;base64,%s" % (mimetype, data.decode())
+
async def send_cli(self, template, recipient):
"""
Send a test message from the CLI
def iuse(self):
return self.backend.iuse
- @property
- def memcache(self):
- return self.backend.memcache
-
@property
def settings(self):
return self.backend.settings
class RateLimiterRequest(misc.Object):
- prefix = "ratelimit"
-
def init(self, request, handler, minutes, limit):
self.request = request
self.handler = handler
self.minutes = minutes
self.limit = limit
+ # What is the current time?
self.now = datetime.datetime.utcnow()
- # Fetch the current counter value from the cache
- self.counter = self.get_counter()
-
- # Increment the rate-limiting counter
- self.increment_counter()
+ # When to expire?
+ self.expires_at = self.now + datetime.timedelta(minutes=self.minutes + 1)
- # Write the header if we are not limited
- if not self.is_ratelimited():
- self.write_headers()
+ self.prefix = "-".join((
+ self.__class__.__name__,
+ self.request.host,
+ self.request.path,
+ self.request.method,
+ self.request.remote_ip,
+ ))
- def is_ratelimited(self):
+ async def is_ratelimited(self):
"""
Returns True if the request is prohibited by the rate limiter
"""
+ counter = await self.get_counter()
+
# The client is rate-limited when more requests have been
# received than allowed.
- return self.counter >= self.limit
+ if counter >= self.limit:
+ return True
+
+ # Increment the counter
+ await self.increment_counter()
+
+ # If not ratelimited, write some headers
+ self.write_headers(counter=counter)
+
+ @property
+ def key(self):
+ return "%s-%s" % (self.prefix, self.now.strftime("%Y-%m-%d-%H:%M"))
+
+ @property
+ def keys_to_check(self):
+ for minute in range(self.minutes + 1):
+ when = self.now - datetime.timedelta(minutes=minute)
+
+ yield "%s-%s" % (self.prefix, when.strftime("%Y-%m-%d-%H:%M"))
- def get_counter(self):
+ async def get_counter(self):
"""
Returns the number of requests that have been done in
recent time.
"""
- keys = self.get_keys_to_check()
+ async with await self.backend.cache.pipeline() as p:
+ for key in self.keys_to_check:
+ await p.get(key)
- res = self.memcache.get_multi(keys)
- if res:
- return sum((int(e) for e in res.values()))
+ # Run the pipeline
+ res = await p.execute()
- return 0
+ # Return the sum
+ return sum((int(e) for e in res if e))
- def write_headers(self):
+ def write_headers(self, counter):
# Send the limit to the user
self.handler.set_header("X-Rate-Limit-Limit", self.limit)
# Send the user how many requests are left for this time window
- self.handler.set_header("X-Rate-Limit-Remaining",
- self.limit - self.counter)
+ self.handler.set_header("X-Rate-Limit-Remaining", self.limit - counter)
- expires = self.now + datetime.timedelta(seconds=self.expires_after)
- self.handler.set_header("X-Rate-Limit-Reset", expires.strftime("%s"))
+ # Send when the limit resets
+ self.handler.set_header("X-Rate-Limit-Reset", self.expires_at.strftime("%s"))
- def get_key(self):
- key_prefix = self.get_key_prefix()
+ async def increment_counter(self):
+ async with await self.backend.cache.pipeline() as p:
+ # Increment the key
+ await p.incr(self.key)
- return "%s-%s" % (key_prefix, self.now.strftime("%Y-%m-%d-%H:%M"))
+ # Set expiry
+ await p.expireat(self.key, self.expires_at)
- def get_keys_to_check(self):
- key_prefix = self.get_key_prefix()
-
- keys = []
- for minute in range(self.minutes + 1):
- when = self.now - datetime.timedelta(minutes=minute)
-
- key = "%s-%s" % (key_prefix, when.strftime("%Y-%m-%d-%H:%M"))
- keys.append(key)
-
- return keys
-
- def get_key_prefix(self):
- return "-".join((self.prefix, self.request.host, self.request.path,
- self.request.method, self.request.remote_ip,))
-
- def increment_counter(self):
- key = self.get_key()
-
- # Add the key or increment if it already exists
- if not self.memcache.add(key, "1", self.expires_after):
- self.memcache.incr(key)
-
- @property
- def expires_after(self):
- """
- Returns the number of seconds after which the counter has reset.
- """
- return (self.minutes + 1) * 60
+ # Run the pipeline
+ await p.execute()
if self.__data.blog_id:
return self.backend.blog.get_by_id(self.__data.blog_id)
- @property
- def fireinfo_id(self):
- name = self.sname.replace("ipfire-", "IPFire ").replace("-", " - ")
-
- res = self.db.get("SELECT id FROM fireinfo_releases \
- WHERE name = %s", name)
-
- if res:
- return res.id
-
@property
def stable(self):
return self.__data.stable
# Fireinfo Stuff
- @property
- def penetration(self):
+ def get_usage(self, when=None):
+ name = self.sname.replace("ipfire-", "IPFire ").replace("-", " - ")
+
# Get penetration from fireinfo
- return self.backend.fireinfo.get_release_penetration(self)
+ releases = self.backend.fireinfo.get_releases_map(when=when)
+
+ return releases.get(name, 0)
class Releases(Object):
return page and not page.was_deleted()
def get_page_title(self, page, default=None):
- # Try to retrieve title from cache
- title = self.memcache.get("wiki:title:%s" % page)
- if title:
- return title
-
- # If the title has not been in the cache, we will
- # have to look it up
doc = self.get_page(page)
if doc:
title = doc.title
else:
title = os.path.basename(page)
- # Save in cache for forever
- self.memcache.set("wiki:title:%s" % page, title)
-
return title
def get_page(self, page, revision=None):
page = self._get_page("INSERT INTO wiki(page, author_uid, markdown, changes, address) \
VALUES(%s, %s, %s, %s, %s) RETURNING *", page, author.uid, content or None, changes, address)
- # Update cache
- self.memcache.set("wiki:title:%s" % page.page, page.title)
-
# Send email to all watchers
page._send_watcher_emails(excludes=[author])
if res:
return bytes(res.data)
- def get_thumbnail(self, size):
+ async def get_thumbnail(self, size):
assert self.is_bitmap_image()
- cache_key = "-".join((self.path, util.normalize(self.filename), self.created_at.isoformat(), "%spx" % size))
+ cache_key = "-".join((
+ self.path,
+ util.normalize(self.filename),
+ self.created_at.isoformat(),
+ "%spx" % size,
+ ))
# Try to fetch the data from the cache
- thumbnail = self.memcache.get(cache_key)
+ thumbnail = await self.backend.cache.get(cache_key)
if thumbnail:
return thumbnail
thumbnail = util.generate_thumbnail(self.blob, size)
# Put it into the cache for forever
- self.memcache.set(cache_key, thumbnail)
+ await self.backend.cache.set(cache_key, thumbnail)
return thumbnail
logging.debug("Sending request to %s:" % request.url)
for header in sorted(request.headers):
logging.debug(" %s: %s" % (header, request.headers[header]))
+ if request.body:
+ logging.debug("%s" % json.dumps(kwargs, indent=4, sort_keys=True))
# Send the request
response = await self.backend.http_client.fetch(request)
# Fetch the whole body
body = response.body
+ if body:
+ # Decode the JSON response
+ body = json.loads(body)
+
+ # Log what we have received in a human-readable way
+ logging.debug("%s" % json.dumps(body, indent=4, sort_keys=True))
# Fetch the signature
signature = response.headers.get("Hash")
if not signature:
raise RuntimeError("Could not find signature on response")
- expected_signature = self._sign_response(body)
+ expected_signature = self._sign_response(response.body)
if not hmac.compare_digest(expected_signature, signature):
raise RuntimeError("Invalid signature: %s" % signature)
- # Decode the JSON response
- return json.loads(body)
+ # Return the body
+ return body
+$baseurl: !default
+
/* latin-ext */
@font-face
font-family: "Prompt"
font-style: normal
font-weight: 400
- src: local("Prompt Regular"), local("Prompt-Regular"), url(/static/fonts/Prompt-Regular.ttf) format("truetype")
+ src: local("Prompt Regular"), local("Prompt-Regular"), url(#{$baseurl}/static/fonts/Prompt-Regular.ttf) format("truetype")
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF
/* latin */
font-family: "Prompt"
font-style: normal
font-weight: 400
- src: local("Prompt Regular"), local("Prompt-Regular"), url(/static/fonts/Prompt-Regular.ttf) format("truetype")
+ src: local("Prompt Regular"), local("Prompt-Regular"), url(#{$baseurl}/static/fonts/Prompt-Regular.ttf) format("truetype")
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD
/* latin-ext */
font-family: 'Prompt'
font-style: normal
font-weight: 500
- src: local('Prompt Medium'), local('Prompt-Medium'), url(/static/fonts/Prompt-Medium.ttf) format("truetype")
+ src: local('Prompt Medium'), local('Prompt-Medium'), url(#{$baseurl}/static/fonts/Prompt-Medium.ttf) format("truetype")
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF
/* latin */
font-family: 'Prompt'
font-style: normal
font-weight: 500
- src: local('Prompt Medium'), local('Prompt-Medium'), url(/static/fonts/Prompt-Medium.ttf) format("truetype")
+ src: local('Prompt Medium'), local('Prompt-Medium'), url(#{$baseurl}/static/fonts/Prompt-Medium.ttf) format("truetype")
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD
/* latin-ext */
font-family: "Prompt"
font-style: normal
font-weight: 700
- src: local("Prompt Bold"), local("Prompt-Bold"), url(/static/fonts/Prompt-Bold.ttf) format("truetype")
+ src: local("Prompt Bold"), local("Prompt-Bold"), url(#{$baseurl}/static/fonts/Prompt-Bold.ttf) format("truetype")
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF
/* latin */
font-family: "Prompt"
font-style: normal
font-weight: 700
- src: local("Prompt Bold"), local("Prompt-Bold"), url(/static/fonts/Prompt-Bold.ttf) format("truetype")
+ src: local("Prompt Bold"), local("Prompt-Bold"), url(#{$baseurl}/static/fonts/Prompt-Bold.ttf) format("truetype")
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD
-// To be re-done https://bugzilla.ipfire.org/show_bug.cgi?id=13051
+@import "main.sass"
+
+// Make the body stretch over the entire screen
+body
+ @extend .container
+
+ // Add some space around the content
+ padding: 3rem 1rem;
+
+h1
+ @extend .title, .is-3
+
+// Make all tables .table by default
+table
+ @extend .table, .is-fullwidth, .is-bordered, .is-striped, .is-hoverable
+
+// Fix to show the bottom line of the table
+table
+ tr
+ &:last-child
+ td,
+ th
+ border-bottom-width: 1px !important
// Global Settings
$family-sans-serif: Prompt, sans-serif
+$size-1: 3rem
+$size-2: 2.5rem
+$size-3: 2rem
+$size-4: 1.5rem
+$size-5: 1.25rem
+$size-6: 1rem
+$size-7: 0.75rem
+
+// Make titles slightly larger
+$title-size: $size-2
+
// Colour Palette
$primary: #ff2e52
$primary-invert: #ffffff
$danger: #ac001a
$warning: #f3ff50
+// Pride Colours
+$pride-red: #e40303
+$pride-orange: #ff8c00
+$pride-yellow: #ffed00
+$pride-green: #008026
+$pride-blue: #24408e
+$pride-purple: #732982
+
// Custom Colours
-$lwl: #6534C8
+$lwl: #6534C8
-$custom-colors: ("secondary" : ($secondary, $secondary-invert), "lwl" : ($lwl, $white))
+$custom-colors: ("secondary" : ($secondary, $secondary-invert), "lwl" : ($lwl, $white), "pride-red" : ($pride-red, $white), "pride-orange" : ($pride-orange, $black), "pride-yellow" : ($pride-yellow, $black), "pride-green" : ($pride-green, $white), "pride-blue" : ($pride-blue, $white), "pride-purple" : ($pride-purple, $white))
// Use the primary colour for links
$link: $primary
$section-padding: 3rem 1.5rem
$section-padding-desktop: 3rem 0.5rem
+// Footer
+$footer-padding: 3rem 1.5rem 3rem
+
// Import Bulma
@import "../third-party/bulma/sass/utilities/_all.sass"
@import "../third-party/bulma/sass/base/_all.sass"
#!@PYTHON@
+import asyncio
import sys
-import tornado.ioloop
import tornado.options
import ipfire
-class TaskRunner(object):
- def __init__(self, *args, **kwargs):
- self.backend = ipfire.Backend(*args, **kwargs)
-
- # Create an IOLoop
- self.ioloop = tornado.ioloop.IOLoop.current()
-
- def run_task(self, name, *args, **kwargs):
- """
- This method runs the task with the given name and
- arguments asynchronically and exits the program in
- case on a non-zero exit code
- """
- async def task():
- await self.backend.run_task(name, *args, **kwargs)
-
- return self.ioloop.run_sync(task)
-
-
-def main():
- z = TaskRunner("@configsdir@/@PACKAGE_NAME@.conf")
+async def main():
+ backend = ipfire.Backend("@configsdir@/@PACKAGE_NAME@.conf")
if len(sys.argv) < 2:
sys.stderr.write("Argument needed\n")
args = tornado.options.parse_command_line()
# Run the task
- z.run_task(*args)
+ await backend.run_task(*args)
-main()
+asyncio.run(main())
{% block title %}{{ _("Log In") }}{% end block %}
{% block container %}
- <section class="section">
- <div class="container">
- <div class="columns is-centered">
- <div class="column is-one-third">
- <h1 class="title is-1">
- IPFire<span class="has-text-primary">_</span>
- </h1>
- <h4 class="subtitle is-4">{{Â _("Log In") }}</h4>
-
- <div class="block">
- <form action="" method="POST">
- {% raw xsrf_form_html() %}
-
- {% if next %}<input type="hidden" name="next" value="{{ next }}">{% end %}
-
- <div class="field">
- <div class="control">
- <input class="input is-medium {% if incorrect %}is-danger{% end %}"
- type="text" name="username" {% if username %}value="{{Â username }}"{% end %}
- placeholder="{{ _("Username") }}" required autofocus>
+ <section class="hero is-primary is-fullheight-with-navbar">
+ <div class="hero-body">
+ <div class="container">
+ <div class="columns is-centered">
+ <div class="column is-one-third">
+ <h1 class="title">{{Â _("Log In") }}</h1>
+
+ <div class="block">
+ <form action="" method="POST">
+ {% raw xsrf_form_html() %}
+
+ {% if next %}<input type="hidden" name="next" value="{{ next }}">{% end %}
+
+ <div class="field">
+ <div class="control">
+ <input class="input is-medium {% if incorrect %}is-danger{% end %}"
+ type="text" name="username" {% if username %}value="{{Â username }}"{% end %}
+ placeholder="{{ _("Username") }}" required autofocus>
+ </div>
</div>
- </div>
- <div class="field">
- <div class="control">
- <input class="input is-medium {% if incorrect %}is-danger{% end %}"
- type="password" name="password" placeholder="{{ _("Password") }}" required>
+ <div class="field">
+ <div class="control">
+ <input class="input is-medium {% if incorrect %}is-danger{% end %}"
+ type="password" name="password" placeholder="{{ _("Password") }}" required>
+ </div>
</div>
- </div>
- <div class="field">
- <div class="control">
- <button class="button is-primary is-medium is-fullwidth">
- {{ _("Log In") }}
- </button>
+ <div class="field">
+ <div class="control">
+ <button class="button is-medium is-fullwidth">
+ {{ _("Log In") }}
+ </button>
+ </div>
</div>
- </div>
-
- <div class="field has-text-centered">
- <a class="text-muted" href="/password-reset{% if incorrect %}?username={{ username }}{% end %}">
- {{ _("Did you forget your password?") }}
- </a>
- </div>
- </form>
+
+ <div class="field has-text-centered">
+ <a class="text-muted" href="/password-reset{% if incorrect %}?username={{ username }}{% end %}">
+ {{ _("Did you forget your password?") }}
+ </a>
+ </div>
+ </form>
+ </div>
</div>
</div>
</div>
{% extends "../../messages/base-promo.html" %}
{% block content %}
- <p>
- <strong>{{Â _("Hey again, %s,") % account.first_name }}</strong>
- </p>
-
- <p>
- {{ _("IPFire runs on supporters' donations, people like you!") }}
- </p>
-
- <p>
- {{ _("Why do we need you donations?") }}
- </p>
-
- <ul>
- <li>{{ _("Your money ensures the longevity and long-term success of this project.") }}</li>
- <li>{{ _("It helps us fund developers and extend our skills") }}</li>
- <li>{{ _("It will aid us to promote IPFire to more people around the world") }}</li>
- <li>{{ _("This funds conferences, where we focus on future projects") }}</li>
- <li>{{Â _("It pays for our hosting") }}</li>
- </ul>
-
- <p>
- {{ _("All this, as you would understand, requires money. Every single donation counts.") }}
- </p>
-
- <p>
- {{Â _("If you want to see IPFire thrive, we need your support.") }}
- </p>
-
- <p>
- {{ _("The best way to do this is by setting up a monthly donation which you can do here:") }}
- </p>
-
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate?frequency=monthly&amount=10" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
-
- <p>
- {{Â _("We also have other ways to donate. Please go to https://www.ipfire.org/donate for details.") }}
- </p>
-
- <p>
- {{ _("Thank you so much for your support,") }}
- <br>{{ _("-Michael") }}
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{Â _("Hey again, %s,") % account.first_name }}</h1>
+
+ <p>
+ {{ _("IPFire runs on supporters' donations, people like you!") }}
+ </p>
+
+ <p>
+ {{ _("Why do we need you donations?") }}
+ </p>
+
+ <ul>
+ <li>{{ _("Your money ensures the longevity and long-term success of this project.") }}</li>
+ <li>{{ _("It helps us fund developers and extend our skills") }}</li>
+ <li>{{ _("It will aid us to promote IPFire to more people around the world") }}</li>
+ <li>{{ _("This funds conferences, where we focus on future projects") }}</li>
+ <li>{{Â _("It pays for our hosting") }}</li>
+ </ul>
+
+ <p>
+ {{ _("All this, as you would understand, requires money. Every single donation counts.") }}
+ </p>
+
+ <p>
+ {{Â _("If you want to see IPFire thrive, we need your support.") }}
+ </p>
+
+ <p>
+ {{ _("The best way to do this is by setting up a monthly donation which you can do here:") }}
+ </p>
+
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate?frequency=monthly&amount=10">
+ {{ _("Donate Now") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+
+ <p>
+ {{Â _("We also have other ways to donate. Please go to https://www.ipfire.org/donate for details.") }}
+ </p>
+
+ <p>
+ {{ _("Thank you so much for your support,") }}
+ <br>{{ _("-Michael") }}
+ </p>
+ </td>
+ </tr>
{% end block %}
{% extends "../../messages/base.html" %}
{% block content %}
- <p>
- <strong>{{Â _("Hello %s!") % account.first_name }}</strong>
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{Â _("Hello %s!") % account.first_name }}</h1>
- <p>
- {{Â _("You, or somebody else on your behalf, has requested to change your password.") }} {{ _("If this was not you, please notify a team member.") }}
- </p>
+ <p>
+ {{Â _("You, or somebody else on your behalf, has requested to change your password.") }}
+ {{ _("If this was not you, please notify a team member.") }}
+ </p>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://people.ipfire.org/password-reset/{{Â account.uid }}/{{Â reset_code }}" target="_blank">{{ _("Reset Password") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://people.ipfire.org/password-reset/{{Â account.uid }}/{{Â reset_code }}">
+ {{ _("Reset Password") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
{% end block %}
{% extends "../../messages/base.html" %}
{% block content %}
- <p>
- <strong>{{Â _("Hello once again, %s,") % account.first_name }}</strong>
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{Â _("Hello once again, %s,") % account.first_name }}</h1>
- <p>
- {{ _("we hope you are enjoying using IPFire.") }}
- </p>
+ <p>
+ {{ _("we hope you are enjoying using IPFire.") }}
+ </p>
- <p>
- {{Â _("Did you know that you can get help from our community at https://community.ipfire.org?") }}
- {{Â _("People like me often post on here, providing help and support.") }}
- </p>
+ <p>
+ {{Â _("Did you know that you can get help from our community at https://community.ipfire.org?") }}
+ {{Â _("People like me often post on here, providing help and support.") }}
+ </p>
- <p>
- {{Â _("But we also rely on you donations. Please consider helping us by setting up a small monthly donation:") }}
- </p>
+ <p>
+ {{Â _("But we also rely on you donations. Please consider helping us by setting up a small monthly donation:") }}
+ </p>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate?frequency=monthly" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate?frequency=monthly">
+ {{ _("Donate Now") }}
+ </a>
+ </td>
+ </tr>
+ </table>
- <p>
- {{Â _("Thank you, we really appreciate your support,") }}
- <br>{{ _("-Arne")}}
- </p>
+ <p>
+ {{Â _("Thank you, we really appreciate your support,") }}
+ <br>{{ _("-Arne")}}
+ </p>
+ </td>
+ </tr>
{% end block %}
{% extends "../../messages/base.html" %}
{% block content %}
- <p>
- <strong>{{Â _("Hello %s!") % account.first_name }}</strong>
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{Â _("Hello %s!") % account.first_name }}</h1>
- <p>
- {{ _("I would like to introduce myself: I'm Michael and I am one of the people behind the project. We are a team of passionate people who try to make the Internet a better place. On behalf of everyone, I would like to say: Welcome to the IPFire Project!") }}
- </p>
+ <p>
+ {{ _("I would like to introduce myself: I'm Michael and I am one of the people behind the project. We are a team of passionate people who try to make the Internet a better place. On behalf of everyone, I would like to say: Welcome to the IPFire Project!") }}
+ </p>
- <p>
- {{ _("We want you to feel a part of our team. Can I ask you to set up your profile? We would love to know a little bit more about you.") }}
- {{ _("To do this, please log on to your profile and click the edit button.") }}
- </p>
+ <p>
+ {{ _("We want you to feel a part of our team. Can I ask you to set up your profile? We would love to know a little bit more about you.") }}
+ {{ _("To do this, please log on to your profile and click the edit button.") }}
+ </p>
- <p>
- {{Â _("I would also like to invite you to join our community at https://community.ipfire.org, if you haven't already done so.") }}
- </p>
+ <p>
+ {{Â _("I would also like to invite you to join our community at https://community.ipfire.org, if you haven't already done so.") }}
+ </p>
- <p>
- {{ _("Finally, this organisation relies on the generous donations of people like you. If you can, please consider supporting this project and the team behind it, so that we can continue our long-term vision, fund developers and promote our project.") }}
- </p>
+ <p>
+ {{ _("Finally, this organisation relies on the generous donations of people like you. If you can, please consider supporting this project and the team behind it, so that we can continue our long-term vision, fund developers and promote our project.") }}
+ </p>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate">
+ {{ _("Donate Now") }}
+ </a>
+ </td>
+ </tr>
+ </table>
- <p>
- {{ _("Thank you,") }}
- <br>{{ _("-Michael") }}
- </p>
+ <p>
+ {{ _("Thank you,") }}
+ <br>{{ _("-Michael") }}
+ </p>
+ </td>
+ </tr>
{% end block %}
{% extends "../../messages/base.html" %}
+{% block hero %}
+ <tr class="hero">
+ <td>
+ <img class="g-img" src="{{ embed_image("img/auth/register@600.jpg") }}" alt="IPFire">
+ </td>
+ </tr>
+{% end block %}
+
{% block content %}
- <p>
- <strong>{{Â _("Hello %s!") % first_name }}</strong>
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{Â _("Hello %s!") % first_name }}</h1>
- <p>
- {{Â _("Thank you for registering a new account with us.") }}
- </p>
+ <p>
+ {{Â _("Thank you for registering a new account with us.") }}
+ </p>
- <p>
- {{Â _("This account will allow you to take part in our project. Either by joining conversations, writing documentation, or becoming a developer.") }} {{ _("There are many things you can do with your account.") }}
- </p>
+ <p>
+ {{Â _("This account will allow you to take part in our project. Either by joining conversations, writing documentation, or becoming a developer.") }}
+ {{ _("There are many things you can do with your account.") }}
+ </p>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/activate/{{Â uid }}/{{Â activation_code }}" target="_blank">{{ _("Activate Account") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/activate/{{Â uid }}/{{Â activation_code }}">
+ {{ _("Activate Account") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
{% end block %}
<div class="container">
<div class="columns is-centered">
<div class="column is-one-third">
- <h1 class="title is-1">
+ <h1 class="title">
IPFire<span class="has-text-primary">_</span>
</h1>
- <h4 class="subtitle is-4">{{ _("Reset Your Password") }}</h4>
+ <h4 class="subtitle">{{ _("Reset Your Password") }}</h4>
<div class="block">
<form action="" method="POST">
<div class="container">
<div class="columns is-centered">
<div class="column is-one-third">
- <h1 class="title is-1">
+ <h1 class="title">
IPFire<span class="has-text-primary">_</span>
</h1>
- <h4 class="subtitle is-4">{{ _("Reset Your Password") }}</h4>
+ <h4 class="subtitle">{{ _("Reset Your Password") }}</h4>
<p class="is-size-5">{{ _("You will shortly receive an email with instructions on how to reset your password.") }}</p>
</div>
<div class="container">
<div class="columns is-centered">
<div class="column is-one-third">
- <h1 class="title is-1">
+ <h1 class="title">
IPFire<span class="has-text-primary">_</span>
</h1>
- <h4 class="subtitle is-4">{{ _("Reset Your Password") }}</h4>
+ <h4 class="subtitle">{{ _("Reset Your Password") }}</h4>
<div class="block">
<form action="" method="POST">
<div class="container">
<div class="columns is-centered">
<div class="column is-one-third">
- <h1 class="title is-1">
+ <h1 class="title">
IPFire<span class="has-text-primary">_</span>
</h1>
- <h4 class="subtitle is-4">
+ <h4 class="subtitle">
{{ _("Your account has been created.") }}
{{ _("Please check your email for next steps.") }}
</h4>
<div class="container">
<div class="columns is-centered">
<div class="column is-one-third">
- <h1 class="title is-1">
+ <h1 class="title">
IPFire<span class="has-text-primary">_</span>
</h1>
- <h4 class="subtitle is-4">{{ _("Register A New Account") }}</h4>
+ <h4 class="subtitle">{{ _("Register A New Account") }}</h4>
<div class="block">
<p class="is-size-5">
<a class="navbar-item is-size-4" href="/">
<strong>
{% if request.path.startswith("/projects/location") %}
- IPFire<span class="has-text-primary">_</span>Location
+ {% module IPFireLogo("Location") %}
{% elif hostname.startswith("fireinfo.") %}
- IPFire<span class="has-text-primary">_</span>Fireinfo
+ {% module IPFireLogo("Fireinfo") %}
{% elif hostname.startswith("nopaste.") %}
- IPFire<span class="has-text-primary">_</span>Nopaste
+ {% module IPFireLogo("nopaste") %}
{% else %}
- IPFire<span class="has-text-primary">_</span>
+ {% module IPFireLogo() %}
{% end %}
</strong>
</a>
<div class="control has-icons-left">
<input class="input" type="text"
name="q" {% if "q" in locals() and q %}value="{{Â q }}"{% end %}
- placeholder="{{Â _("Search Docs...") }}">
+ placeholder="{{Â _("Search Documentation...") }}">
<span class="icon is-small is-left">
<i class="fas fa-search"></i>
</span>
<div class="navbar-item">
<a class="button is-primary has-text-weight-bold is-uppercase"
- href="https://www.ipfire.org/donate">
+ href="/donate">
{{ _("Donate") }}
</a>
</div>
+
+ {% if current_user %}
+ <div class="navbar-item has-dropdown is-hoverable">
+ <a class="navbar-link is-arrowless" href="/users/{{ current_user.uid }}">
+ <figure class="image">
+ <img class="is-rounded" style="width: auto" src="{{ current_user.avatar_url(128) }}">
+ </figure>
+ </a>
+
+ <div class="navbar-dropdown">
+ <a class="navbar-item" href="/users/{{ current_user.uid }}/passwd">
+ {{ _("Change Password") }}
+ </a>
+
+ <hr class="navbar-divider">
+
+ <a class="navbar-item" href="/logout">
+ {{Â _("Logout")}}
+ </a>
+ </div>
+ </div>
+ {% end %}
</div>
- {% elif hostname == "fireinfo.ipfire.org" %}
+ {% elif hostname.startswith("fireinfo.") %}
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="fas fa-bars"></span>
</li>
</ul>
</div>
- {% elif hostname == "location.ipfire.org" %}
- <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
- aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
- <span class="fas fa-bars"></span>
- </button>
-
- <div class="collapse navbar-collapse" id="navbar">
- <ul class="navbar-nav ml-auto">
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/how-to-use" %}is-active{% end %}" href="/how-to-use">
- {{ _("How To Use") }}
- </a>
- </li>
-
- <li class="nav-item">
- <a class="nav-link {% if request.path == "/download" %}is-active{% end %}" href="/download">
- {{ _("Download") }}
- </a>
- </li>
- </ul>
-
- <a class="btn btn-primary ml-lg-2" href="https://www.ipfire.org/donate">
- {{ _("Donate") }}
- </a>
- </div>
- {% elif hostname == "nopaste.ipfire.org" %}
+ {% elif hostname.startswith("nopaste.") %}
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="fas fa-bars"></span>
{% block footer %}
<footer class="footer is-flex-shrink-0">
<div class="container">
- <div class="columns">
- <div class="column is-two-fifths">
- {# Show some profile information for users who are logged in #}
- {% if current_user %}
- <h4 class="title is-4 mb-0">{{Â _("Hello, %s!") % current_user }}</h4>
-
- <div class="level is-mobile">
- <div class="level-left">
- <a class="level-item" href="/users/{{Â current_user.uid }}">
- {{Â _("My Profile") }}
- </a>
+ {# Encourage people to join #}
+ {% if not current_user %}
+ <div class="columns">
+ <div class="column is-one-fifth">
+ <a class="button is-primary is-medium is-fullwidth" href="/register">
+ {{ _("Join Now") }}
+ </a>
+ </div>
- <a class="level-item" href="/logout">
- {{Â _("Logout") }}
- </a>
- </div>
+ <div class="column is-one-quarter">
+ <div class="block">
+ <p class="title is-5">
+ {{ _("Join our community and sign up to our newsletter") }}
+ </p>
</div>
-
- {# Otherwise encourage people to join #}
- {% else %}
- <p class="is-size-4">
- <span class="has-text-weight-bold">IPFire</span><span class="has-text-primary has-text-weight-bold">_</span>People
- </p>
-
- <p>
- Join the community and sign up for our newsletter
- </p>
-
- <a class="button is-primary is-outlined is-medium
- has-text-black has-text-weight-bold" href="/register">
- JOIN NOW
- </a>
- <a class="button is-primary is-outlined is-medium
- has-text-black has-text-weight-bold" href="/login">
- LOG IN
- </a>
- {% end %}
+ </div>
</div>
- <div class="column">
- <ul>
- <li>
- <a href="/about">
- {{ _("About") }}
- </a>
- </li>
- <li>
- <a href="/docs">
- {{ _("Documentation") }}
- </a>
- </li>
- <li>
- <a href="/help">
- {{ _("Help") }}
- </a>
- </li>
- <li>
- <a href="/docs/devel">
- {{ _("Development") }}
- </a>
- </li>
- </ul>
+ <div class="columns">
+ <div class="column is-one-fifth">
+ <a class="button is-primary is-medium is-outlined is-fullwidth" href="/login">
+ {{ _("Log In") }}
+ </a>
+ </div>
</div>
+ {% end %}
- <div class="column">
- <ul>
- <li>
- <a href="/download">
- {{ _("Download") }}
- </a>
- </li>
- <li>
- <a href="https://community.ipfire.org/">
- {{ _("Community") }}
- </a>
- </li>
- <li>
- <a href="/sitemap">
- {{ _("Sitemap") }}
- </a>
- </li>
- <li>
- <a href="/legal">
- {{ _("Legal") }}
- </a>
- </li>
- </ul>
- </div>
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ {{Â year }} © IPFire.org
+ </div>
- {% if current_user and current_user.is_staff() %}
- <div class="column">
- <ul>
- <li>
- <a href="/voip">
- {{ _("VoIP") }}
- </a>
- </li>
- </ul>
- </div>
- {% end %}
+ <div class="level-item">
+ <a href="/legal">{{Â _("Legal") }}</a>
+ </div>
- <div class="column is-one-fifth">
- <div class="block">
- <a class="button is-primary is-fullwidth is-medium has-text-weight-bold is-uppercase"
- href="https://www.ipfire.org/donate">
- {{ _("Donate") }}
- </a>
+ <div class="level-item">
+ <a href="/sitemap">{{Â _("Sitemap") }}</a>
+ </div>
</div>
- <div class="block">
- <div class="level is-mobile">
- <div class="level-item">
- <a href="https://social.ipfire.org/@news" title="{{Â _("Mastodon") }}">
- <i class="fa-brands fa-mastodon"></i>
- </a>
- </div>
- <div class="level-item">
- <a href="https://twitter.com/ipfire" title="{{Â _("Twitter") }}">
- <i class="fa-brands fa-twitter"></i>
- </a>
- </div>
- <div class="level-item">
- <a href="https://linkedin.com/company/ipfire" title="{{Â _("LinkedIn") }}">
- <i class="fa-brands fa-linkedin-in"></i>
- </a>
- </div>
+ <div class="level-right">
+ <div class="level-item">
+ <a href="https://social.ipfire.org/@news" title="{{Â _("Mastodon") }}">
+ <i class="fa-brands fa-mastodon px-2"></i>
+ </a>
+ </div>
+ <div class="level-item">
+ <a href="https://twitter.com/ipfire" title="{{Â _("Twitter") }}">
+ <i class="fa-brands fa-twitter px-2"></i>
+ </a>
+ </div>
+ <div class="level-item">
+ <a href="https://linkedin.com/company/ipfire" title="{{Â _("LinkedIn") }}">
+ <i class="fa-brands fa-linkedin-in px-2"></i>
+ </a>
</div>
</div>
</div>
- </div>
</div>
</footer>
{% end block %}
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">{{ _("Home") }}</a>
</ul>
</nav>
- <h1 class="title is-1">{{ _("My Drafts") }}</h1>
+ <h1 class="title">{{ _("My Drafts") }}</h1>
</div>
</div>
</section>
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">{{ _("Home") }}</a>
</ul>
</nav>
- <h1 class="title is-1">{{ _("IPFire Blog") }}</h1>
+ <h1 class="title">{{ _("IPFire Blog") }}</h1>
{% if q %}
- <h6 class="subtitle is-5">
+ <h6 class="subtitle">
{{ _("Search Results for '%s'") % q }}
</h6>
{% end %}
</div>
</div>
</div>
-
- {% module BlogHistoryNavigation() %}
</div>
</div>
+
+ {# Show links to older years... #}
+ {% module BlogHistoryNavigation() %}
</div>
</section>
{% end block %}
{% extends "../../messages/base-promo.html" %}
-{% block content %}
- <p>
- {{ _("there is a new post from %s on the IPFire Blog:") % post.author }}
- </p>
+{% block title %}{{ post.title }}{% end block %}
+
+{# Preview text shown in the inbox preview... #}
+{% block preview %}{{Â post.excerpt }}{% end block %}
- <p>
- <strong>{{Â post.title }}</strong>
- </p>
+{% block content %}
+ <tr class="section">
+ <td>
+ <h1>
+ {{ post.title }}
+ </h1>
- {% if post.excerpt %}
- <blockquote>{{Â post.excerpt }}</blockquote>
- {% end %}
+ <p>
+ {{Â post.excerpt }}
+ </p>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/blog/{{ post.slug }}" target="_blank">{{ _("Click Here To Read More") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/blog/{{ post.slug }}">
+ {{Â _("Read The Full Post On Our Blog") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
{% end block %}
-<h5 class="title is-5">{{ _("Years") }}</h6>
+<div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ {{ _("Archive") }}
+ </div>
-<ul>
- {% for year in years %}
- <li>
- <a class="nav-link" href="/blog/years/{{ year }}">{{ year }}</a>
- </li>
- {% end %}
-</ul>
+ {% for year in years %}
+ <div class="level-item">
+ <a href="/blog/years/{{ year }}">{{ year }}</a>
+ </div>
+ {% end %}
+ </div>
+</div>
</a>
</h5>
- <h6 class="subtitle is-6">
+ <h6 class="subtitle">
{% if post.published_at %}
{{ locale.format_date(post.published_at, shorter=True, relative=relative) }}
{% elif post.created_at %}
<section class="hero {% if "lightningwirelabs.com" in post.tags %}is-lwl{% elif post.is_published() %}is-primary{% else %}is-light{% end %}">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">{{ _("Home") }}</a>
</ul>
</nav>
- <h1 class="title is-1">{{ post.title }}</h1>
+ <h1 class="title">{{ post.title }}</h1>
- <h6 class="subtitle is-6">
+ <h6 class="subtitle">
{{ _("by") }}
{% if isinstance(post.author, accounts.Account) %}
</div>
</section>
+ {# Encourage people to sign up & subscribe... #}
+ {% if not current_user or not current_user.consents_to_promotional_emails %}
+ <section class="has-background-light">
+ <div class="container">
+ <p class="has-text-centered px-2 py-1">
+ {{ _("Do you like what you are reading?") }}
+ {{ _("Subscribe to our newsletter and don't miss out on the latest...") }}
+
+
+
+ {% if not current_user %}
+ <a class="has-text-weight-bold" href="/register">
+ {{ _("Join Now") }}
+ </a>
+ {% else %}
+ <a class="has-text-weight-bold" href="/subscribe">
+ {{ _("Subscribe Now") }}
+ </a>
+ {% end %}
+ </p>
+ </div>
+ </section>
+ {% end %}
+
<section class="section">
<div class="container">
<div class="columns is-justify-content-space-between">
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">{{ _("Home") }}</a>
</ul>
</nav>
- <h1 class="title is-1">{{ _("Write a New Post") }}</h1>
+ <h1 class="title">{{ _("Write a New Post") }}</h1>
</div>
</div>
</section>
<section class="hero has-background-primary-light">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">Home</a>
</li>
</ul>
</nav>
- <h1 class="title is-1">
+ <h1 class="title">
{{ _("Posts in %s") % year }}
</h1>
</div>
<div class="column is-8">
{% module BlogList(posts) %}
</div>
- <div class="column">
- {% module BlogHistoryNavigation() %}
- </div>
</div>
+
+ {% module BlogHistoryNavigation() %}
</div>
</section>
{% end block %}
<section class="hero is-light">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">
<li>
<a href="/docs">
- {{Â _("Docs") }}
+ {{Â _("Documentation") }}
</a>
</li>
</ul>
</nav>
- <h1 class="title is-1">{{ page_title or _("IPFire Documentation") }}</h1>
+ <h1 class="title">{{ page_title or _("IPFire Documentation") }}</h1>
</div>
</div>
</section>
{% block content %}
<div class="container">
<section class="section">
- <h1 class="title is-1">{{ _("Recent Changes") }}</h1>
+ <h1 class="title">{{ _("Recent Changes") }}</h1>
{% module DocsList(recent_changes, show_changes=True) %}
<section class="hero is-light">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">{{ _("Home") }}</a>
</li>
<li class="is-active">
- <a href="#" aria-current="page">{{ _("Docs") }}</a>
+ <a href="#" aria-current="page">{{ _("Documentation") }}</a>
</li>
</ul>
</nav>
- <h1 class="title is-1">{{ _("IPFire Docs") }}</h1>
+ <h1 class="title">{{ _("IPFire Documentation") }}</h1>
{% if q %}
- <h6 class="subtitle is-5s">
+ <h6 class="subtitle">
{{ _("Search Results for '%s'") % q }}
</h6>
{% end %}
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">Home</a>
</li>
</ul>
</nav>
- <h1 class="title is-1">
+ <h1 class="title">
Donate
</h1>
<p class="subtitle">{{ _("Please support our project with your donation today") }}</p>
<div class="control">
<input type="text" class="input" name="first_name"
placeholder="{{ _("First Name" )}}" required
- {% if first_name %}value="{{ first_name }}"{% end %}>
+ {% if current_user %}value="{{ current_user.first_name }}"{% end %}>
</div>
</div>
<div class="control">
<input type="text" class="input" name="last_name"
placeholder="{{ _("Last Name" )}}" required
- {% if last_name %}value="{{ last_name }}"{% end %}>
+ {% if current_user %}value="{{ current_user.last_name }}"{% end %}>
</div>
</div>
</div>
<div class="block">
<div class="control">
<input type="email" class="input" name="email"
- placeholder="{{ _("Email Address") }}" required>
+ placeholder="{{ _("Email Address") }}" required
+ {% if current_user %}value="{{ current_user.email }}"{% end %}>
</div>
</div>
+ {% set lines = current_user.street.splitlines() if current_user else [] %}
+
<div class="block">
<div class="control">
<input type="text" class="input" name="street1"
- placeholder="{{ _("Address Line 1") }}" required>
+ placeholder="{{ _("Address Line 1") }}" required
+ {% if lines %}value="{{ lines[0] }}"{% end %}>
</div>
</div>
<div class="block">
<div class="control">
<input type="text" class="input" name="street2"
- placeholder="{{ _("Address Line 2") }}">
+ placeholder="{{ _("Address Line 2") }}"
+ {% if lines and len(lines) > 1 %}value="{{ lines[1] }}"{% end %}>
</div>
</div>
<div class="column">
<div class="control">
<input type="text" class="input" name="city"
- placeholder="{{ _("City") }}" required>
+ placeholder="{{ _("City") }}" required
+ {% if current_user %}value="{{ current_user.city }}"{% end %}>
</div>
</div>
<div class="column">
<div class="control">
<input type="text" class="input" name="post_code"
- placeholder="{{ _("Post Code") }}" required>
+ placeholder="{{ _("Post Code") }}" required
+ {% if current_user %}value="{{ current_user.postal_code }}"{% end %}>
</div>
</div>
</div>
{% extends "../../messages/base-promo.html" %}
{% block content %}
- <p>
- <strong>{{ _("Dear %s,") % account.first_name }}</strong>
- </p>
-
- <p>
- As we approach the end of the year, now is a good time to look back and reflect on
- the progress of our project to celebrate our achievements and decide where we need
- to redouble our efforts in the coming months.
- </p>
-
- <p>
- We’ve launched nine packed Core Updates to a userbase of millions of people in
- almost 200 countries.
- We’ve developed and improved on our firewall engine adding support for blocking
- any connections from hostile networks, added Two-Factor Authentication for OpenVPN,
- and massively improved our Intrusion Prevention System and Quality of Service.
- </p>
-
- <p>
- As we move into the New Year, in addition to our standard Core Updates,
- we’re looking to bring our software to even more users
- and we strive to continue being more agile than the big boys in the industry.
- </p>
-
- <p>
- 2022 has been a challenging year and with an Open Source project,
- it is increasingly difficult to meet the cost of running it.
- Can you help us meet these costs?
- </p>
-
- <p>
- If you’re able to help, please click the link below -
- your donation ensures the longevity of our project, and gives us the ability
- to keep our product free and open to all!
- </p>
-
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
-
- <p>
- {{ _("All the best,") }}
- <br>{{ _("-Your IPFire Team") }}
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{ _("Hello %s,") % account.first_name }}</h1
+
+ <p>
+ Just like the last couple of years, 2023 has not been without it’s challenges. Here
+ at IPFire, we’re looking back at the year and highlighting the positivity we’ve had
+ from our friends and colleagues across the globe and the progress we've made on
+ our project.
+ </p>
+
+ <p>
+ We want to spread positivity and joy so why not get in touch on
+ Mastodon (https://social.ipfire.org/@news), Twitter (https://twitter.com/ipfire) or
+ LinkedIn (https://www.linkedin.com/company/ipfire) and share your highlights of the year
+ or why you like working with IPFire?
+ We’ll be sharing and retweeting some of our favourites in the weeks to come.
+ </p>
+
+ <p>
+ If you really liked working with IPFire this year, why not consider donating to our project?
+ </p>
+
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate">
+ {{ _("Donate") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+
+ <p>
+ {{ _("All the best,") }}
+ <br>{{ _("-Your IPFire Team") }}
+ </p>
+ </td>
+ </tr>
{% end block %}
From: IPFire Project <no-reply@ipfire.org>
To: {{ account.email_to }}
-Subject: {{Â _("Another year over. Let's celebrate 2022!") }}
+Subject: {{Â _("IPFire Highlights of 2023") }}
Precedence: bulk
X-Auto-Response-Suppress: OOF
-{{ _("Dear %s,") % account.first_name }}
+{{ _("Hello %s,") % account.first_name }}
-As we approach the end of the year, now is a good time to look back and reflect on
-the progress of our project to celebrate our achievements and decide where we need
-to redouble our efforts in the coming months.
+Just like the last couple of years, 2023 has not been without it’s challenges. Here
+at IPFire, we’re looking back at the year and highlighting the positivity we’ve had
+from our friends and colleagues across the globe and the progress we've made on
+our project.
-We’ve launched nine packed Core Updates to a userbase of millions of people in
-almost 200 countries.
-We’ve developed and improved on our firewall engine adding support for blocking
-any connections from hostile networks, added Two-Factor Authentication for OpenVPN,
-and massively improved our Intrusion Prevention System and Quality of Service.
+We want to spread positivity and joy so why not get in touch on
+Mastodon (https://social.ipfire.org/@news), Twitter (https://twitter.com/ipfire) or
+LinkedIn (https://www.linkedin.com/company/ipfire) and share your highlights of the year
+or why you like working with IPFire?
+We’ll be sharing and retweeting some of our favourites in the weeks to come.
-As we move into the New Year, in addition to our standard Core Updates,
-we’re looking to bring our software to even more users
-and we strive to continue being more agile than the big boys in the industry.
-
-2022 has been a challenging year and with an Open Source project,
-it is increasingly difficult to meet the cost of running it.
-Can you help us meet these costs?
-
-If you’re able to help, please click the link below -
-your donation ensures the longevity of our project, and gives us the ability
-to keep our product free and open to all!
+If you really liked working with IPFire this year, why not consider donating to our project?
https://www.ipfire.org/donate
{% extends "../../messages/base-promo.html" %}
{% block content %}
- <p>
- <strong>{{ _("Dear %s,") % account.first_name }}</strong>
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{ _("Hi %s,") % account.first_name }}</h1
- <p>
- When we launched our little project all those years ago,
- never did we think we would reach so far around the world.
- We sometimes wonder if Father Christmas is using us at the North Pole
- just like his penguin friends at the South Pole!
- </p>
+ <p>
+ For so many people around the world, the end of the calendar year is party season and
+ at IPFire we’re celebrating as much as anyone else. Why are we celebrating?
+ We’re celebrating what’s to come next year, of course!
+ </p>
- <p>
- Our Christmas list this year isn’t very long -
- we’re asking for donations to the project to ensure that our IPFire users,
- just like you, can continue making the most of our free-to-use project.
- </p>
+ <p>
+ Thanks to some super-hardwork by our team in 2023, over the next 12 months, you’ll see
+ the following:
+ </p>
- <p>
- We know some people can’t donate and that’s cool but if you can,
- even just the cost of a beer, it would help us to secure the project
- for the year to come.
- </p>
+ <ul>
+ <li>Relaunch of the IPFire website, helping us promote our mission and improve our outreach.</li>
+ <li>Improve usability by making documentation and user guide more accessible.</li>
+ <li>Helping you, the users, to contribute your guidance to the IPFire Wiki and make
+ suggestions for improvements to our code.</li>
+ </ul>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <p>
+ Of course, we’d not be able to do any of this without feedback from our huge user base
+ and our fantastic development team. If you’d like to help our development team towards
+ their goals for next year, you can donate here:
+ </p>
- <p>
- {{ _("Thank you for supporting the project,") }}
- <br>{{ _("-Your IPFire Team") }}
- </p>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate">
+ {{ _("Donate") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+
+ <p>
+ {{ _("Party, party, party!") }}
+ <br>{{ _("-Your IPFire Team") }}
+ </p>
+ </td>
+ </tr>
{% end block %}
From: IPFire Project <no-reply@ipfire.org>
To: {{ account.email_to }}
-Subject: {{Â _("Did you know IPFire is being used on all seven continents?") }}
+Subject: {{Â _("December is Party Season!") }}
Precedence: bulk
X-Auto-Response-Suppress: OOF
-{{ _("Dear %s,") % account.first_name }}
+{{ _("Hi %s,") % account.first_name }}
-When we launched our little project all those years ago,
-never did we think we would reach so far around the world.
-We sometimes wonder if Father Christmas is using us at the North Pole
-just like his penguin friends at the South Pole!
+For so many people around the world, the end of the calendar year is party season and
+at IPFire we’re celebrating as much as anyone else. Why are we celebrating?
+We’re celebrating what’s to come next year, of course!
-Our Christmas list this year isn’t very long -
-we’re asking for donations to the project to ensure that our IPFire users,
-just like you, can continue making the most of our free-to-use project.
+Thanks to some super-hardwork by our team in 2023, over the next 12 months, you’ll see
+the following:
-We know some people can’t donate and that’s cool but if you can,
-even just the cost of a beer, it would help us to secure the project
-for the year to come.
+ * Relaunch of the IPFire website, helping us promote our mission and improve our outreach.
+ * Improve usability by making documentation and user guide more accessible.
+ * Helping you, the users, to contribute your guidance to the IPFire Wiki and make
+ suggestions for improvements to our code.
+
+Of course, we’d not be able to do any of this without feedback from our huge user base
+and our fantastic development team. If you’d like to help our development team towards
+their goals for next year, you can donate here:
https://www.ipfire.org/donate
-{{ _("Thank you for supporting the project,") }}
+Party, party, party!
-{{ _("Your IPFire Team") }}
--
{% extends "../../messages/base-promo.html" %}
{% block content %}
- <p>
- <strong>{{ _("Dear %s,") % account.first_name }}</strong>
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{ _("Dear %s,") % account.first_name }}</h1>
- <p>
- With the holiday season upon us, we hope you’re enjoying the festive treats
- and magical feeling that comes with this time of year.
- </p>
+ <p>
+ The year has drawn to an end and we just wanted to take a moment to say thank you.
+ Your support for the IPFire project is hugely appreciated and we’re grateful that you’ve
+ taken the time to be part of our ever-growing community.
+ </p>
- <p>
- While you’re enjoying the festivities, it might be time to spare a thought for
- your IPFire appliance - whether in a cupboard or under a desk or sitting cool
- as a cucumber in your comms room, she’s been working away all year to look after you!
- </p>
+ <p>
+ We’re celebrating our best year yet including the recent success of our developer summit
+ where we made huge progress on the build system for IPFire 3. This isn’t all that happened
+ in 2023 though! We had may contributors who helped us to release 10 updates to
+ millions of users across every continent.
+ Yes, even our penguin friends in the antarctic are using IPFire!
+ </p>
- <p>
- We would love if you could help us with a donation towards the IPFire project
- to keep our infrastructure running so your hardworking appliance can continue
- to keep you safe!
- </p>
+ <p>
+ We couldn’t have done this without hundreds of donations just like yours. If you’re feeling
+ the love and you can spare a little money, help us start 2024 with a bang!
+ </p>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate?frequency=monthly&amount=10" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate">
+ {{ _("Donate") }}
+ </a>
+ </td>
+ </tr>
+ </table>
- <p>
- Always remember to show appreciation to your IPFire appliance.
- Remember to keep her updated with the latest firmware and please don’t share your
- festive treats with her - most firewalls don’t enjoy pastry crumbs!
- </p>
+ <p>
+ Finally, whatever you’re celebrating at the end of this year, we wish you all the best
+ for the festive season.
+ </p>
- <p>
- {{ _("All the best,") }}
- <br>{{ _("-Your IPFire Team") }}
- </p>
+ <p>
+ {{ _("Thank you,") }}
+ <br>{{ _("-Your IPFire Team") }}
+ </p>
+ </td>
+ </tr>
{% end block %}
From: IPFire Project <no-reply@ipfire.org>
To: {{ account.email_to }}
-Subject: {{Â _("Time to get festive...") }}
+Subject: {{Â _("Seasons Greetings!") }}
Precedence: bulk
X-Auto-Response-Suppress: OOF
-{{ _("Dear %s,") % account.first_name }}
+{{ _("Hi %s,") % account.first_name }}
-With the holiday season upon us, we hope you’re enjoying the festive treats
-and magical feeling that comes with this time of year.
+The year has drawn to an end and we just wanted to take a moment to say thank you.
+Your support for the IPFire project is hugely appreciated and we’re grateful that you’ve
+taken the time to be part of our ever-growing community.
-While you’re enjoying the festivities, it might be time to spare a thought for
-your IPFire appliance - whether in a cupboard or under a desk or sitting cool
-as a cucumber in your comms room, she’s been working away all year to look after you!
+We’re celebrating our best year yet including the recent success of our developer summit
+where we made huge progress on the build system for IPFire 3. This isn’t all that happened
+in 2023 though! We had may contributors who helped us to release 10 updates to
+millions of users across every continent.
+Yes, even our penguin friends in the antarctic are using IPFire!
-We would love if you could help us with a donation towards the IPFire project
-to keep our infrastructure running so your hardworking appliance can continue
-to keep you safe!
+We couldn’t have done this without hundreds of donations just like yours. If you’re feeling
+the love and you can spare a little money, help us start 2024 with a bang!
- https://www.ipfire.org/donate
+ https://www.ipfire.org/donate
-Always remember to show appreciation to your IPFire appliance.
-Remember to keep her updated with the latest firmware and please don’t share your
-festive treats with her - most firewalls don’t enjoy pastry crumbs!
+Finally, whatever you’re celebrating at the end of this year, we wish you all the best
+for the festive season.
-{{ _("All the best,") }}
+{{ _("Thank you,") }}
-{{ _("Your IPFire Team") }}
--
{% extends "../../messages/base-promo.html" %}
{% block content %}
- <p>
- <strong>{{ _("Dear Friends of IPFire,") }}</strong>
- </p>
-
- <p>
- I’m writing to you, about to sign off for Christmas, filled with pride for our
- great project and gratitude for our wonderful supporters.
- </p>
-
- <p>
- This has been a challenging but enjoyable year and I’d like to thank you for
- the support you’ve shown, whether through your contributions to the project,
- your feedback or your donations, our project wouldn’t have such an impact
- without people like you!
- </p>
-
- <p>
- As ever, thank you to my fellow IPFire team members and contributors for your
- efforts to make our project the best it can be
- Everyone has worked super-hard this year so whatever you’re celebrating this
- December, I wish the very best for you and your families over the festive
- break and may you have a happy and prosperous New Year.
- </p>
-
- <p>
- If you haven’t yet donated to the project, it isn’t too late -
- you can donate via the link below.
- </p>
-
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
- <tbody>
- <tr>
- <td align="left">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tbody>
- <tr>
- <td>
- <a href="https://www.ipfire.org/donate" target="_blank">{{ _("Donate Now") }}</a>
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
-
- <p>
- Thank you again for your support - I wish you all a happy and relaxing end to
- the year and I’ll look forward to hearing from you all in 2023.
- </p>
-
- <p>
- {{ _("Best wishes and peace,") }}
- <br>{{ _("-Michael") }}
- </p>
+ <tr class="section">
+ <td>
+ <h1>{{ _("Hi %s,") % account.first_name }}</h1>
+
+ <p>
+ Did you see the highlights of 2023 we shared on our socials? If you didn’t, check our our
+ Mastodon (https://social.ipfire.org/@news), Twitter (https://twitter.com/ipfire) and our
+ LinkedIn (https://www.linkedin.com/company/ipfire) to see some of our favourite users
+ stories sent in by our friends and colleagues using IPFire.
+ </p>
+
+ <p>
+ I wanted to take the opportunity to thank you for all of your support in 2023 and thank you
+ in advance for your continued support as we head into 2024. As you know, we’re the only
+ remaining truly Open Source Linux-based firewall and it is important to us not just to
+ continue to develop this great project, but also to remain Open Source and available to all.
+ Our mission is to continue to provide the same level of service with continued enhancements
+ and better usability and all of this requires the continued to support of our fantastic users.
+ </p>
+
+ <p>
+ If you’d like to help us kick off 2024 the right way, please click the link where
+ you can donate to our project. Every donation is appreciated as it contributes to the
+ continued success of our project and helps us keep our software open to all.
+ </p>
+
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="button">
+ <td>
+ <a class="primary" href="https://www.ipfire.org/donate">
+ {{ _("Donate") }}
+ </a>
+ </td>
+ </tr>
+ </table>
+
+ <p>
+ {{ _("Happy New Year!")}}
+ <br>{{ _("-Michael") }}
+ </p>
+ </td>
+ </tr>
{% end block %}
From: Michael Tremer <michael.tremer@ipfire.org>
To: {{ account.email_to }}
-Subject: {{Â _("Merry Christmas!") }}
+Subject: {{Â _("Wishing You a Happy and Prosperous New Year") }}
Precedence: bulk
X-Auto-Response-Suppress: OOF
-Dear Friends of IPFire,
+{{ _("Hi %s,") % account.first_name }}
-I’m writing to you, about to sign off for Christmas, filled with pride for our
-great project and gratitude for our wonderful supporters.
+Did you see the highlights of 2023 we shared on our socials? If you didn’t, check our our
+Mastodon (https://social.ipfire.org/@news), Twitter (https://twitter.com/ipfire) and our
+LinkedIn (https://www.linkedin.com/company/ipfire) to see some of our favourite users
+stories sent in by our friends and colleagues using IPFire.
-This has been a challenging but enjoyable year and I’d like to thank you for
-the support you’ve shown, whether through your contributions to the project,
-your feedback or your donations, our project wouldn’t have such an impact
-without people like you!
+I wanted to take the opportunity to thank you for all of your support in 2023 and thank you
+in advance for your continued support as we head into 2024. As you know, we’re the only
+remaining truly Open Source Linux-based firewall and it is important to us not just to
+continue to develop this great project, but also to remain Open Source and available to all.
+Our mission is to continue to provide the same level of service with continued enhancements
+and better usability and all of this requires the continued to support of our fantastic users.
-As ever, thank you to my fellow IPFire team members and contributors for your
-efforts to make our project the best it can be
-Everyone has worked super-hard this year so whatever you’re celebrating this
-December, I wish the very best for you and your families over the festive
-break and may you have a happy and prosperous New Year.
-
-If you haven’t yet donated to the project, it isn’t too late -
-you can donate via the link below.
+If you’d like to help us kick off 2024 the right way, please click the link where
+you can donate to our project. Every donation is appreciated as it contributes to the
+continued success of our project and helps us keep our software open to all.
https://www.ipfire.org/donate
-Thank you again for your support - I wish you all a happy and relaxing end to
-the year and I’ll look forward to hearing from you all in 2023.
-
-{{ _("Best wishes and peace,") }}
+{{ _("Happy New Year!")}}
{{ _("-Michael") }}
--
<section class="hero is-primary is-fullheight-with-navbar">
<div class="hero-body">
<div class="container">
- <h1 class="title is-1">
+ <h1 class="title">
{{ _("Thank You") }}
</h1>
<p class="subtitle">
<section class="hero is-light">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">Home</a>
</li>
</ul>
</nav>
- <h1 class="title is-1">
+ <h1 class="title">
{{ _("Mirrors") }}
</h1>
<p class="subtitle">
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">Home</a>
</ul>
</nav>
- <h1 class="title is-1">
+ <h1 class="title">
{{Â _("Download %s") % release }}
</h1>
- <h6 class="subtitle is-6">
+ <h6 class="subtitle">
{{ _("Released %s") % locale.format_date(release.published, relative=True, shorter=True) }}
{% if release.blog %}
<section class="hero is-medium">
<div class="hero-body">
<div class="container">
- <h1 class="title is-1">
+ <h1 class="title">
Thank You For Downloading IPFire<span class="has-text-primary">_</span>
</h1>
- <h4 class="subtitle is-4">{{ _("Your download will begin in a few seconds. If not, click the link below.") }}</h4>
+ <h4 class="subtitle">{{ _("Your download will begin in a few seconds. If not, click the link below.") }}</h4>
<div class="block">
<p class="download-path"></p>
</li>
</ul>
</nav>
+ </div>
+ </div>
+ </section>
- <section class="section">
- <div class="columns is-vcentered">
- <div class="column">
- <div class="has-text-centered">
- <h1 class="title is-1">{{ "{:,d}".format(total) }}</h1>
- <h4 class="title is-4">{{ _("Total amount of profiles") }}</h4>
- </div>
- </div>
+ <section class="section">
+ <div class="container">
+ <div class="columns">
+ <div class="column">
+ <div class="has-text-centered">
+ <h1 class="title">{{ "{:,d}".format(total) }}</h1>
+ <h4 class="title is-4">{{ _("Total Profiles") }}</h4>
+ </div>
+ </div>
- <div class="column">
- <div class="has-text-centered">
- <h1 class="title is-1">{{ "%.2f%%" % (with_data * 100 / total) }}</h1>
- <h4 class="title is-4">{{ _("Reporting back to us") }}</h4>
- </div>
- </div>
+ <div class="column">
+ <div class="has-text-centered">
+ <h1 class="title">{{ "%.2f%%" % (with_data * 100 / total) }}</h1>
+ <h4 class="title is-4">{{ _("Reporting back to us") }}</h4>
</div>
- </section>
+ </div>
</div>
</div>
</section>
+
+ {% if histogram %}
+ <section class="section">
+ <div class="container">
+ <table class="table">
+ <tr>
+ <th>{{ _("Date") }}</th>
+ <th>{{ _("Total Profiles") }}</th>
+ </tr>
+
+ {% for date in sorted(histogram, reverse=True) %}
+ <tr>
+ <th scope="row">{{ format_date(date) }}</th>
+ <td>
+ {{ histogram[date] }}
+ </td>
+ </tr>
+ {% end %}
+ </table>
+ </div>
+ </section>
+ {% end %}
{% end block %}
</li>
</ul>
</nav>
+ </div>
+ </div>
+ </section>
+ <section class="section">
+ <div class="container">
+ <h1 class="title">{{ driver }}</h1>
<h2 class="title is-2">{{ _("All known devices run by %s") % driver }}</h2>
</div>
</div>
<section class="section">
<div class="container">
- {% module FireinfoDeviceTable(driver_map) %}
+ {% module FireinfoDeviceTable(devices) %}
</div>
</section>
{% end block %}
<section class="hero is-medium is-primary">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="https://ipfire.org/">
</ul>
</nav>
- <h1 class="title is-1">{{ _("Fireinfo") }}</h1>
+ <h1 class="title">{{ _("Fireinfo") }}</h1>
<a class="button is-dark is-medium" href="/profile/random">
{{ _("Show a Random Profile") }}
<div class="container">
<div class="columns is-vcentered">
{% if latest_release %}
+ {% set usage = latest_release.get_usage(when=when) %}
+
<div class="column is-half is-centered">
<div>
- <h1 class="title is-1 has-text-primary">
- {{ "%.2f%%" % (latest_release.penetration * 100) }}
+ <h1 class="title has-text-primary">
+ {{ "%.2f%%" % (usage * 100) }}
</h1>
<h5 class="title is-5">
<div class="container">
<h4 class="title is-4">{{ _("Locations") }}</h4>
- {% for country_code, percentage in locations %}
+ {% for cc in sorted(locations, key=lambda cc: locations[cc], reverse=True) %}
<div class="columns is-mobile">
- {% if percentage >= 0.01 %}
+ {% if locations[cc] >= 0.01 %}
<div class="column is-one-fifth">
- <span class="flag-icon flag-icon-{{ country_code.lower() }}"></span>
- <span class="">{{ format_country_name(country_code) }}</span>
+ <span class="flag-icon flag-icon-{{ cc.lower() }}"></span>
+ <span class="">{{ format_country_name(cc) }}</span>
</div>
<div class="column is-7">
- {% module ProgressBar(percentage, "success") %}
+ {% module ProgressBar(locations[cc], "success") %}
</div>
{% end %}
</div>
{% end %}
- <p>
- <span class="has-text-weight-bold">IPFire<span class="has-text-primary">_</span></span>
- {{_("is also running in these countries: %s") % locale.list([(format_country_name(c) or c) for c, p in locations if p < 0.01]) }}
- </p>
+ {% set other_countries = [cc for cc in locations if locations[cc] < 0.01] %}
+
+ {% if other_countries %}
+ <p>
+ <span class="has-text-weight-bold">IPFire<span class="has-text-primary">_</span></span>
+ {{_("is also running in these countries: %s") % locale.list([(format_country_name(cc) or cc) for cc in other_countries]) }}
+ </p>
+ {% end %}
</div>
</section>
<div class="block">
<h4 class="title is-4">{{ _("CPU Vendors") }}</h4>
- {% for name, percentage in cpu_vendors %}
+ {% for vendor in sorted(cpu_vendors, key=lambda v: cpu_vendors[v], reverse=True) %}
<div class="columns">
- <div class="column is-1">{{ name }}</div>
+ <div class="column is-1">{{ vendor }}</div>
<div class="column">
- {% module ProgressBar(percentage, "success") %}
+ {% module ProgressBar(cpu_vendors[vendor], "success") %}
</div>
</div>
{% end %}
<div class="block">
<h4 class="title is-4">{{ _("Architectures") }}</h4>
- {% for name, percentage in arches %}
+ {% for arch in sorted(arches, key=lambda a: arches[a], reverse=True) %}
<div class="columns">
- <div class="column is-1">{{ name }}</div>
+ <div class="column is-1">{{ arch }}</div>
<div class="column">
- {% module ProgressBar(percentage, "success") %}
+ {% module ProgressBar(arches[arch], "success") %}
</div>
</div>
{% end %}
</div>
</div>
<div class="column is-half has-text-centered">
- <h1 class="title is-1">{{ format_size(memory_avg * 1024, "MB") }}</h1>
+ <h1 class="title">{{ format_size(memory_avg * 1024, "MB") }}</h1>
<span class="tag">
{{ _("Average Amount of Memory") }}
<div class="container">
<div class="columns is-vcentered">
<div class="column is-half has-text-centered">
- <h1 class="title is-1 has-text-primary">
+ <h1 class="title has-text-primary">
{{ "%.2f%%" % (virtual_ratio * 100) }}
</h1>
<div class="column is-half">
<h4 class="title is-4">{{ _("Top Hypervisors") }}</h4>
- {% for name, percentage in hypervisors %}
+ {% for vendor in sorted(hypervisors, key=lambda v: hypervisors[v], reverse=True) %}
<div class="columns">
- {% if percentage >= 0.01 %}
+ {% if hypervisors[vendor] >= 0.01 %}
<div class="column is-1">
- {% if name == "unknown" %}
+ {% if vendor == "unknown" %}
<span class="text-muted">{{ _("Unknown") }}</span>
- {% elif name == "VMWare" %}
+ {% elif vendor == "VMWare" %}
VMware
{% else %}
- {{ name }}
+ {{ vendor }}
{% end %}
</div>
<div class="column is-8">
- {% module ProgressBar(percentage, "success") %}
+ {% module ProgressBar(hypervisors[vendor], "success") %}
</div>
{% end %}
</div>
{% block title %}{{ _("Processors") }}{% end block %}
{% block container %}
+ {% set map = backend.fireinfo.get_cpu_flags_map(when=when) %}
+
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="https://ipfire.org/">
</ul>
</nav>
- <h1 class="title is-1">{{ _("Processors") }}</h1>
+ <h1 class="title">{{ _("Processors") }}</h1>
</div>
</div>
</section>
<section class="section">
<div class="container">
- {% for platform in flags %}
- <h2 class="title is-2">{{ platform }}</h2>
+ {% for arch in sorted(map) %}
+ <h2 class="title is-2">{{ arch }}</h2>
{% for flag, percentage in flags[platform] %}
<div class="columns">
</div>
<div class="column">
- {% if percentage >= 0.95 %}
- {% module ProgressBar(percentage, "success") %}
- {% elif percentage >= 0.5 %}
- {% module ProgressBar(percentage, "warning") %}
- {% elif percentage >= 0.1 %}
- {% module ProgressBar(percentage, "info") %}
+ {% if p >= 0.95 %}
+ {% module ProgressBar(p, "success") %}
+ {% elif p >= 0.5 %}
+ {% module ProgressBar(p, "warning") %}
+ {% elif p >= 0.1 %}
+ {% module ProgressBar(p, "info") %}
{% else %}
- {% module ProgressBar(percentage, "danger") %}
+ {% module ProgressBar(p, "danger") %}
{% end %}
</div>
</div>
<div class="container">
<div class="columns">
<div class="column is-8">
- <h1 class="title is-1">{{ _("Profile") }}</h1>
+ <h1 class="title">{{ _("Profile") }}</h1>
<h5 class="title is-5">{{ profile.public_id }}</h5>
</div>
</div>
</section>
<div class="container">
- <div class="columns">
- <div class="column is-10">
- {% if profile.appliance_id %}
- <div class="columns">
- <div class="column is-7">
- <small>{{ _("This is a") }}</small>
- <h5 class="title is-5 is-lwl">{{ profile.appliance }}</h5>
- </div>
-
- <div class="column is-5">
- <a class="button is-lwl" href="https://www.lightningwirelabs.com">
- {{ _("Go to Lightning Wire Labs") }} <span class="fas fa-external-link-alt ml-2"></span>
- </a>
- </div>
- </div>
- {% end %}
- </div>
- </div>
-
-
<div class="card mb-5">
<div class="card-body">
<div class="row">
<div class="col-12 col-sm-8 mb-4">
<h5 class="card-title mb-0">
- {{ _("Running %s") % profile.release }}
+ {{ _("Running %s") % profile.system.release }}
</h5>
<small class="text-muted">
- {{ _("Last update %s") % locale.format_date(profile.time_updated) }}
+ {{ _("Last update %s") % locale.format_date(profile.last_updated_at) }}
</small>
</div>
</div>
<dl class="row mb-0">
- {% if profile.virtual %}
+ {% if profile.is_virtual() %}
<dt class="col-sm-3">{{ _("Hypervisor") }}</dt>
<dd class="col-sm-9">
{% if profile.hypervisor == "VMWare" %}
{{ profile.hypervisor }}
{% end %}
</dd>
- {% elif not profile.appliance_id and profile.system %}
+ {% elif profile.system %}
<dt class="col-sm-3">{{ _("System") }}</dt>
<dd class="col-sm-9">
- {% if profile.system_vendor %}
- {{ profile.system_vendor }}
+ {% if profile.system.vendor %}
+ {{ profile.system.vendor }}
{% end %}
- {% if profile.system_vendor and profile.system_model %}
+ {% if profile.system.vendor and profile.system.model %}
‐
{% end %}
- {% if profile.system_model %}
- {{ profile.system_model }}
+ {% if profile.system.model %}
+ {{ profile.system.model }}
{% end %}
</dd>
{% end %}
</dd>
{% end %}
- {% if profile.storage %}
+ {% if profile.system.storage %}
<dt class="col-md-3">{{ _("Storage") }}</dt>
<dd class="col-md-9">
- {{ format_size(profile.storage) }}
+ {{ format_size(profile.system.storage) }}
</dd>
{% end %}
</dd>
{% end %}
- {% if profile.language %}
+ {% if profile.system.language %}
<dt class="col-md-3">{{ _("Language") }}</dt>
<dd class="col-md-9">
- {{ format_language_name(profile.language) }}
+ {{ format_language_name(profile.system.language) }}
</dd>
{% end %}
</dl>
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="https://ipfire.org/">
</ul>
</nav>
- <h1 class="title is-1">{{ _("Releases") }}</h1>
+ <h1 class="title">{{ _("Releases") }}</h1>
</div>
</div>
</section>
<section class="section">
<div class="container">
- {% for name, percentage in releases %}
+ {% for name in sorted(releases, key=lambda n: releases[n], reverse=True) %}
<div class="columns">
<div class="column is-4">{{ name.replace("core", "Core Update ") }}</div>
<div class="column">
- {% module ProgressBar(percentage, "primary") %}
+ {% module ProgressBar(releases[name], "primary") %}
</div>
</div>
{% end %}
<h2 class="title is-2">{{ _("Kernels") }}</h2>
- {% for name, percentage in kernels %}
+ {% for name in sorted(kernels, key=lambda n: kernels[n], reverse=True) %}
<div class="columns">
<div class="column is-4">{{ name }}</div>
<div class="column">
- {% module ProgressBar(percentage, "info") %}
+ {% module ProgressBar(kernels[name], "info") %}
</div>
</div>
{% end %}
{% block title %}{{ _("Vendors") }}{% end block %}
{% block container %}
-<section class="hero is-primary">
- <div class="hero-body">
- <div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
- <ul>
- <li>
- <a href="https://ipfire.org/">
- Home
- </a>
- </li>
- <li>
- <a href="/">
- {{ _("Fireinfo") }}
- </a>
- </li>
- <li class="is-active">
- <a href="#">
- {{ _("Vendors") }}
- </a>
- </li>
- </ul>
- </nav>
+ <section class="hero is-primary">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="https://ipfire.org/">
+ Home
+ </a>
+ </li>
+ <li>
+ <a href="/">
+ {{ _("Fireinfo") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#">
+ {{ _("Vendors") }}
+ </a>
+ </li>
+ </ul>
+ </nav>
- <h1 class="title is-1">{{ _("Vendors") }}</h1>
+ <h1 class="title is-1">{{ _("Vendors") }}</h1>
+ </div>
</div>
- </div>
-</section>
+ </section>
-<section class="section">
- <div class="container">
- {% for vendor, subsystems in vendors %}
- <div class="columns">
- {% if vendor %}
- <div class="column is-4">{{ vendor }}</div>
- <div class="column is-3">
- {% for subsystem, vendor_id in sorted(subsystems) %}
- <a href="/vendors/{{ subsystem }}/{{ vendor_id }}">{{ subsystem.upper() }}</a>
- {% end %}
- </div>
- {% end %}
- </div>
- {% end %}
- </div>
-</section>
+ <section class="section">
+ <div class="container">
+ {% for vendor, subsystems in vendors %}
+ <div class="columns">
+ {% if vendor %}
+ <div class="column is-4">{{ vendor }}</div>
+ <div class="column is-3">
+ {% for subsystem, vendor_id in sorted(subsystems) %}
+ <a href="/vendors/{{ subsystem }}/{{ vendor_id }}">{{ subsystem.upper() }}</a>
+ {% end %}
+ </div>
+ {% end %}
+ </div>
+ {% end %}
+ </div>
+ </section>
{% end block %}
<div class="hero-body">
<div class="container">
- <h1 class="title is-1">
+ <h1 class="title">
{{ _("More Than A Firewall") }}
</h1>
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">
</ul>
</nav>
- <h1 class="title is-1">{{ _("Download IPFire Location") }}</h1>
- <h6 class="subtitle is-6">
+ <h1 class="title">{{ _("Download IPFire Location") }}</h1>
+ <h6 class="subtitle">
Learn how to download and install <code>libloc</code>
</h6>
</div>
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">
</ul>
</nav>
- <h1 class="title is-1">{{ _("IPFire Location") }}</h1>
- <h6 class="subtitle is-6">
+ <h1 class="title">{{ _("IPFire Location") }}</h1>
+ <h6 class="subtitle">
<code>libloc</code> is versatile, fast and easy to use
in any application.
</h6>
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">
</ul>
</nav>
- <h1 class="title is-1">{{ _("IPFire Location") }}</h1>
- <h6 class="subtitle is-6">
+ <h1 class="title">{{ _("IPFire Location") }}</h1>
+ <h6 class="subtitle">
{{Â _("A powerful, free IP address location database") }}
</h6>
</div>
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">
</ul>
</nav>
- <h1 class="title is-1">{{ _("Lookup %s") % address }}</h1>
+ <h1 class="title">{{ _("Lookup %s") % address }}</h1>
</div>
</div>
</section>
{% extends "base.html" %}
{% block footer %}
- {{Â _("Don't like these emails?") }}
- <a href="https://people.ipfire.org/unsubscribe">{{Â _("Unsubscribe") }}</a>.
+ <unsubscribe>
+ <a href="https://www.ipfire.org/unsubscribe">{{Â _("Unsubscribe") }}</a>
+ </unsubscribe>
{% end block %}
<!DOCTYPE html>
-<html>
- <head>
- <meta name="viewport" content="width=device-width">
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
- <title>{% block title %}{% end block %}</title>
- <style media="all" type="text/css">
- {% include "main.css" %}
+<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
+<head>
+ {# Based on https://www.cerberusemail.com #}
+ <meta charset="utf-8">
+
+ {# Enable "responsiveness" #}
+ <meta name="viewport" content="width=device-width">
+
+ {# Use the latest (edge) version of IE rendering engine #}
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+
+ {# Disable auto-scale in iOS 10 Mail entirely #}
+ <meta name="x-apple-disable-message-reformatting">
+
+ {# Tell iOS not to automatically link certain text strings #}
+ <meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
+
+ {# Declare supported color schemes #}
+ <meta name="color-scheme" content="light dark">
+ <meta name="supported-color-schemes" content="light dark">
+
+ {# Title #}
+ <title>{% block title %}{% end block %}</title>
+
+ {# Make background images in 72ppi Outlook render at correct size #}
+ <!--[if gte mso 9]>
+ <xml>
+ <o:OfficeDocumentSettings>
+ <o:PixelsPerInch>96</o:PixelsPerInch>
+ </o:OfficeDocumentSettings>
+ </xml>
+ <![endif]-->
+
+ {# Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font #}
+ <!--[if mso]>
+ <style>
+ * {
+ font-family: sans-serif !important;
+ }
+ </style>
+ <![endif]-->
+
+ {# All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks.
+ More on that here: https://web.archive.org/web/20190717120616/http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ #}
+ <!--[if !mso]>
+ <style>
+ {% include "fonts.css" %}
</style>
- </head>
-
- <body>
- <span class="preheader">{% block preheader %}{% end preheader %}</span>
- <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
- <tr>
- <td class="container">
- <div class="content">
- {% block container %}
- <table role="presentation" class="main">
- <tr>
- <td class="logo">
- <img src="https://www.ipfire.org/static/img/ipfire-tux.png" alt="{{Â _("IPFire Logo") }}">
- </td>
- </tr>
- <tr>
- <td class="wrapper">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tr>
- <td>
- {% block content %}{% end block %}
- </td>
- </tr>
- </table>
- </td>
- </tr>
- </table>
- {% end block %}
-
- <div class="footer">
- <table role="presentation" border="0" cellpadding="0" cellspacing="0">
- <tr>
- <td class="content-block">
- <span class="apple-link">{{ _("The IPFire Project" )}}</span>
-
- <br>
-
- {% block footer %}{% end block %}
- </td>
- </tr>
- </table>
- </div>
- </div>
+ <![endif]-->
+
+ {# Import the main CSS #}
+ <style>
+ {# Tell the email client that both light and dark styles are provided #}
+ :root {
+ color-scheme: light dark
+ supported-color-schemes: light dark
+ }
+
+ {% include "main.css" %}
+ </style>
+</head>
+
+<body class="bg">
+ <center role="article" aria-roledescription="email" lang="en" class="bg">
+ <!--[if mso | IE]>
+ <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" class="bg">
+ <tr>
+ <td>
+ <![endif]-->
+ {# Visually Hidden Pre-header Text #}
+ <div class="pre-header" aria-hidden="true">
+ {% block preview %}{% end block %}
+ </div>
+
+ {# Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview #}
+ <div class="whitespace">
+ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌
+ </div>
+
+ <div class="container">
+ <!--[if mso]>
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600">
+ <tr>
+ <td>
+ <![endif]-->
+
+ {# Body #}
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ {% block body %}
+ {# Header #}
+ <tr class="header">
+ <td>
+ <h1>
+ IPFire<span class="has-text-primary">_</span>
+ </h1>
+ </td>
+ </tr>
+
+ {# Hero #}
+ {% block hero %}{% end block %}
+
+ {# Content #}
+ <tr class="content">
+ <td>
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
+ {% block content %}{% end block %}
+ </tabke>
+ </td>
+ </tr>
+ {% end block %}
+ </table>
+
+ {# Footer #}
+ <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+ <tr class="footer">
+ <td>
+ {{ _("The IPFire Project") }},
+ {{ _("c/o") }} Lightning Wire Labs GmbH,
+ <span class="unstyle-auto-detected-links">Gerhardstraße 8, 45711 Datteln, Germany</span>
+
+ <br><br>
+
+ {% block footer %}{% end block %}
+ </td>
+ </tr>
+ </table>
+
+ <!--[if mso]>
</td>
- </tr>
+ </tr>
+ </table>
+ <![endif]-->
+ </div>
+
+ <!--[if mso | IE]>
+ </td>
+ </tr>
</table>
- </body>
+ <![endif]-->
+ </center>
+</body>
</html>
--- /dev/null
+$baseurl: "https://michael.dev.ipfire.org"
+
+// Use our main font by default
+*
+ font-family: Prompt, sans-serif
+
+@import "../../sass/_fonts.sass"
-// Needs to be re-done https://bugzilla.ipfire.org/show_bug.cgi?id=13050
+
+// Fonts
+$font-family: Prompt, sans-serif
+
+$font-weight-normal: 500
+$font-weight-bold: 700
+
+// Container
+$container-width: 600px
+
+// A unit to use for padding
+$pad: 20px
+
+// Borders
+$border-radius: 4px
+
+// Colours
+$black: hsl(0, 0%, 4%)
+$white: hsl(0, 0%, 100%)
+$grey: hsl(0, 0%, 97%)
+$light: hsl(0, 0%, 80%)
+
+$primary: #ff2e52
+$primary-inverted: $white
+
+// Background Colours
+$bg-light: $white
+$bg-dark: $grey
+
+// Text Colour
+$text: $black
+$link: $primary
+
+// Font sizes
+$font-size-small: 12px
+$font-size-normal: 16px
+$font-size-large: 20px
+
+$line-height-small: 16px
+$line-height-normal: 22px
+$line-height-large: 28px
+
+// Headings
+$title-1: 30px
+$line-height-title-1: 40px
+
+// Remove spaces around the email design added by some email clients
+html, body
+ margin: 0 auto !important
+ padding: 0 !important
+ height: 100% !important
+ width: 100% !important
+
+// Stop email clients resizing small text
+*
+ -ms-text-size-adjust: 100%
+ -webkit-text-size-adjust: 100%
+
+// Centers email on Android 4.4
+div[style*="margin: 16px 0"]
+ margin: 0 !important
+
+// forces Samsung Android mail clients to use the entire viewport
+#MessageViewBody, #MessageWebViewDiv
+ width: 100% !important
+
+// Stop Outlook from adding extra spacing to tables
+table, td
+ mso-table-lspace: 0pt !important
+ mso-table-rspace: 0pt !important
+
+// Fix a webkit padding issue
+table
+ border-spacing: 0 !important
+ border-collapse: collapse !important
+ table-layout: fixed !important
+ margin: 0 auto !important
+
+// Use a better rendering method when resizing images in IE
+img
+ -ms-interpolation-mode: bicubic
+
+// Prevent Windows 10 Mail from underlining links despite inline CSS
+a
+ text-decoration: none
+
+// A work-around for email clients meddling in triggered links
+a[x-apple-data-detectors], .unstyle-auto-detected-links a, .aBn
+ border-bottom: 0 !important
+ cursor: default !important
+ color: inherit !important
+ text-decoration: none !important
+ font-size: inherit !important
+ font-family: inherit !important
+ font-weight: inherit !important
+ line-height: inherit !important
+
+// Prevent Gmail from displaying a download button on large, non-linked images
+.a6S
+ display: none !important
+ opacity: 0.01 !important
+
+// Prevent Gmail from changing the text color in conversation threads.
+.im
+ color: inherit !important
+
+// If the above doesn't work, add a .g-img class to any image in question.
+img.g-img + div
+ display: none !important
+
+// Set font
+*
+ font-family: $font-family
+ font-weight: $font-weight-normal
+
+body
+ mso-line-height-rule: exactly
+
+// Links
+a
+ color: $link
+
+ &:hover
+ text-decoration: underline
+
+// Center all content
+center
+ width: 100%
+
+// Visually Hidden Pre-header Text
+.pre-header
+ max-height: 0
+ overflow: hidden
+ mso-hide: all
+
+// Some whitespace
+.whitespace
+ display: none
+ font-size: 1px
+ line-height: 1px
+ max-height: 0px
+ max-width: 0px
+ opacity: 0
+ overflow: hidden
+ mso-hide: all
+
+// The main container
+.container
+ max-width: $container-width
+ margin: 0 auto
+
+ // Improve readability on small screens
+ @media screen and (max-width: 600px)
+ p
+ font-size: 17px !important;
+
+// Make tables fill the entire viewport horizontally
+table
+ width: 100%
+ margin: auto
+
+ // The header box
+ tr.header
+ td
+ padding: $pad 0
+ text-align: center
+
+ h1
+ margin: 0 0 10px 0
+ font-size: 50px
+ line-height: 60px
+ font-weight: $font-weight-bold
+
+ span
+ color: $primary
+ font-weight: i$font-weight-bold
+
+ // The hero unit
+ tr.hero
+ td
+ img
+ display: block
+ border: 0
+ width: 100%
+ max-width: $container-width
+ height: auto
+ background: $grey
+ margin: auto
+
+ // Content (i.e. the big box)
+ tr.content
+ td
+ background-color: $bg-dark
+ color: $text
+
+ @media (prefers-color-scheme: dark)
+ background-color: $bg-light
+
+ table
+ // One block in the box
+ tr.section
+ td
+ padding: $pad
+ font-size: $font-size-normal
+ line-height: $line-height-normal
+
+ // Headings
+ h1
+ margin: 0 0 10px 0
+ font-size: $title-1
+ line-height: $line-height-title-1
+
+ // Text
+ p
+ padding: 8px 0
+ margin: 0
+
+ &:last-child
+ padding: 0
+
+ // Links
+ a
+ color: $link
+
+ &:hover
+ text-decoration: underline
+
+ // Buttons
+ tr.button
+ td
+ a
+ display: block
+ border: 1px solid $primary
+ border-radius: $border-radius
+ text-align: center
+ font-size: $font-size-large
+ font-weight: $font-weight-bold
+ line-height: $line-height-large
+ text-decoration: none
+ padding: 16px 20px
+ color: $white
+
+ &.primary
+ background-color: $primary
+ color: $primary-inverted
+
+ &:hover
+ background-color: $primary-inverted
+ color: $primary
+
+ // Change the padding on the last element
+ //tr:last-child
+ // td
+ // padding: 0 $pad
+
+ // Footer
+ tr.footer
+ td
+ padding: $pad
+ font-size: $font-size-small
+ line-height: $line-height-small
+ color: $light
+ text-align: center
+
+ // Make links grey, too
+ a
+ color: inherit
--- /dev/null
+{% set pride_colors = ("pride-red", "pride-orange", "pride-yellow", "pride-green", "pride-blue", "pride-purple") %}
+
+{# Christmas #}
+{% if now.month == 12 %}
+ IPFire<span class="has-text-primary">_</span>{{ suffix or "" }} 🎄
+
+{# Halloween #}
+{% elif now.month == 10 and now.day >= 28 %}
+ IPFire<span class="has-text-primary">_</span>{{ suffix or "" }} 🎃
+
+{# Pride Month #}
+{% elif now.month == 7 %}
+ {% for color, i in zip(pride_colors, "IPFire") %}<span class="has-text-{{ color }}">{{ i }}</span>{% end %}_{{ suffix or "" }}
+
+{# Other times of the year #}
+{% else %}
+ IPFire<span class="has-text-primary">_</span>{{ suffix or "" }}
+{% end %}
<section class="hero has-background-primary-light">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">Home</a>
</li>
</ul>
</nav>
- <h1 class="title is-1">
+ <h1 class="title">
{% if mode == "paste" %}
{{ _("New Paste") }}
{% elif mode == "upload" %}
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">Home</a>
</li>
</ul>
</nav>
- <h1 class="title is-1">
+ <h1 class="title">
About IPFire<span class="has-text-primary">_</span>
</h1>
- <p class="subtitle">The Open Source Firewall</p>
+ <h6 class="subtitle">The Open Source Firewall</h6>
</div>
</div>
</section>
<section class="hero has-background-primary-light">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">Home</a>
</ul>
</nav>
- <h1 class="title is-1">
+ <h1 class="title">
{{ _("Need Help?") }}
</h1>
<section class="hero has-background-primary-light">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">Home</a>
</li>
</ul>
</nav>
- <h1 class="title is-1">{{ _("Legal") }}</h1>
+ <h1 class="title">{{ _("Legal") }}</h1>
</div>
</div>
</section>
</p>
</div>
</div>
+
+ {% if current_user and current_user.is_staff() %}
+ <div class="column is-one-third has-text-centered">
+ <div>
+ <p class="heading">Telephony</p>
+ <p class="title">
+ <a href="/voip">
+ {{Â _("VoIP") }}
+ </a>
+ </p>
+ </div>
+ </div>
+ {% end %}
</div>
</div>
</section>
<div class="container">
<div class="columns is-centered">
<div class="column is-one-third">
- <h1 class="title is-1">
+ <h1 class="title">
{{Â _("Delete User") }}
</h1>
- <h4 class="subtitle is-4">{{Â account }}</h4>
+ <h4 class="subtitle">{{Â account }}</h4>
<div class="block has-text-danger">
<form action="" method="POST">
<section class="hero is-dark">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">
</ul>
</nav>
- <h1 class="title is-1">{{ _("Edit Profile") }}</h1>
- <h6 class="subtitle is-6">{{ account }} | {{ account.uid }}</h6>
+ <h1 class="title">{{ _("Edit Profile") }}</h1>
+ <h6 class="subtitle">{{ account }} | {{ account.uid }}</h6>
</div>
</div>
</section>
<section class="hero is-dark">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">
</ul>
</nav>
- <h1 class="title is-1">{{ _("Groups") }}</h1>
+ <h1 class="title">{{ _("Groups") }}</h1>
</div>
</div>
</section>
<section class="hero is-dark">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">
</ul>
</nav>
- <h1 class="title is-1">{{ group }}</h1>
- <h6 class="subtitle is-6">{{Â group.gid }}</h6>
+ <h1 class="title">{{ group }}</h1>
+ <h6 class="subtitle">{{Â group.gid }}</h6>
</div>
</div>
</section>
<section class="hero is-dark">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">
</ul>
</nav>
- <h1 class="title is-1">{{ _("Users") }}</h1>
+ <h1 class="title">{{ _("Users") }}</h1>
</div>
</div>
</section>
<div class="container">
<div class="columns is-vcentered">
<div class="column">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">
</ul>
</nav>
- <h1 class="title is-1">{{ account }}</h1>
- <h6 class="subtitle is-6">{{Â account.uid }}</h6>
+ <h1 class="title">{{ account }}</h1>
+ <h6 class="subtitle">{{Â account.uid }}</h6>
{# Description #}
{% if account.description %}
{% end %}
{% if account.can_be_managed_by(current_user) %}
- <a class="button is-light" href="/users/{{ account.uid }}/passwd">
- {{ _("Change Password") }}
- </a>
-
<a class="button is-warning" href="/users/{{ account.uid }}/edit">
{{ _("Edit") }}
</a>
<a href="/users/{{ account.uid }}/edit#description">{{Â _("Edit Profile") }}</a>
</div>
-
- {# Suggest uploading an avatar if this user does not have one #}
- {% elif not current_user.has_avatar() %}
- <div class="notification is-info">
- <strong>{{Â _("Upload An Avatar!") }}</strong>
-
- {{Â _("A picture says more than a thousand words") }}
-
- <a href="/users/{{ account.uid }}/edit#avatar">{{Â _("Upload Avatar") }}</a>
- </div>
{% end %}
{% end %}
</div>
<section class="hero is-dark">
<div class="hero-body">
<div class="container">
- <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li>
<a href="/">
</ul>
</nav>
- <h1 class="title is-1">{{ _("VoIP") }}</h1>
+ <h1 class="title">{{ _("VoIP") }}</h1>
</div>
</div>
</section>
# Misc
"ChristmasBanner" : ui_modules.ChristmasBannerModule,
+ "IPFireLogo" : ui_modules.IPFireLogoModule,
"Markdown" : ui_modules.MarkdownModule,
"Map" : ui_modules.MapModule,
"ProgressBar" : ui_modules.ProgressBarModule,
(r"/blog/([0-9a-z\-\._]+)/delete", blog.DeleteHandler),
(r"/blog/([0-9a-z\-\._]+)/edit", blog.EditHandler),
(r"/blog/([0-9a-z\-\._]+)/publish", blog.PublishHandler),
+ (r"/blog/([0-9a-z\-\._]+)/debug/email", blog.DebugEmailHandler),
# Docs
(r"/docs/recent\-changes", docs.RecentChangesHandler),
(r"/docs/watchlist", docs.WatchlistHandler),
(r"/docs/_restore", docs.RestoreHandler),
(r"/docs/_upload", docs.UploadHandler),
- (r"/docs/([A-Za-z0-9\-_\/]+)?/_edit", docs.EditHandler),
- (r"/docs/([A-Za-z0-9\-_\/]+)?/_render", docs.RenderHandler),
- (r"/docs/([A-Za-z0-9\-_\/]+)?/_(watch|unwatch)", docs.WatchHandler),
+ (r"/docs(?:/([A-Za-z0-9\-_\/]+))?/_edit", docs.EditHandler),
+ (r"/docs(?:/([A-Za-z0-9\-_\/]+))?/_render", docs.RenderHandler),
+ (r"/docs(?:/([A-Za-z0-9\-_\/]+))?/_(watch|unwatch)", docs.WatchHandler),
(r"/docs/((?:[A-Za-z0-9\-_\/]+)?(?:.*)\.(?:\w+))/_delete", docs.DeleteFileHandler),
(r"/docs((?:[A-Za-z0-9\-_\/]+)?(?:.*)\.(?:\w+))$", docs.FileHandler),
- (r"/docs([A-Za-z0-9\-_\/]+)?/_files", docs.FilesHandler),
- (r"/docs([A-Za-z0-9\-_\/]+)?", docs.PageHandler),
+ (r"/docs(?:/([A-Za-z0-9\-_\/]+))?/_files", docs.FilesHandler),
+ (r"/docs(?:/([A-Za-z0-9\-_\/]+))?", docs.PageHandler),
# Downloads
(r"/downloads", downloads.IndexHandler),
#!/usr/bin/python
+import asyncio
import datetime
import dateutil.parser
import functools
from .. import util
class ratelimit(object):
- def __init__(self, minutes=15, requests=180):
- self.minutes = minutes
+ """
+ A decorator class which limits how often a function can be called
+ """
+ def __init__(self, *, minutes, requests):
+ self.minutes = minutes
self.requests = requests
def __call__(self, method):
@functools.wraps(method)
- def wrapper(handler, *args, **kwargs):
+ async def wrapper(handler, *args, **kwargs):
# Pass the request to the rate limiter and get a request object
req = handler.backend.ratelimiter.handle_request(handler.request,
handler, minutes=self.minutes, limit=self.requests)
# If the rate limit has been reached, we won't allow
# processing the request and therefore send HTTP error code 429.
- if req.is_ratelimited():
+ if await req.is_ratelimited():
raise tornado.web.HTTPError(429, "Rate limit exceeded")
- return method(handler, *args, **kwargs)
+ # Call the wrapped method
+ result = method(handler, *args, **kwargs)
+
+ # Await it if it is a coroutine
+ if asyncio.iscoroutine(result):
+ return await result
+
+ # Return the result
+ return result
return wrapper
def iuse(self):
return self.backend.iuse
- @property
- def memcached(self):
- return self.backend.memcache
-
@property
def mirrors(self):
return self.backend.mirrors
self.redirect("/drafts")
+class DebugEmailHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self, slug):
+ if not self.current_user.is_staff():
+ raise tornado.web.HTTPError(403)
+
+ # Fetch the post
+ post = self.backend.blog.get_by_slug(slug)
+ if not post:
+ raise tornado.web.HTTPError(404, "Could not find post %s" % slug)
+
+ self.render("blog/messages/announcement.html", account=self.current_user, post=post)
+
+
class HistoryNavigationModule(ui_modules.UIModule):
def render(self):
return self.render_string("blog/modules/history-navigation.html",
def action(self):
return self.get_argument("action", None)
- def get(self, path):
+ async def get(self, path):
# Check permissions
if not self.backend.wiki.check_acl(path, self.current_user):
raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
# Check if image should be resized
if size and file.is_bitmap_image():
- blob = file.get_thumbnail(size)
+ blob = await file.get_thumbnail(size)
else:
blob = file.blob
from . import base
+SKUS = {
+ "monthly" : "IPFIRE-DONATION-MONTHLY",
+ "quarterly" : "IPFIRE-DONATION-QUARTERLY",
+ "yearly" : "IPFIRE-DONATION-YEARLY",
+}
+DEFAULT_SKU = "IPFIRE-DONATION"
+
class DonateHandler(base.BaseHandler):
def get(self):
- country = self.current_country_code
+ if self.current_user:
+ country = self.current_user.country_code
+ else:
+ country = self.current_country_code
# Get defaults
- first_name = self.get_argument("first_name", None)
- last_name = self.get_argument("last_name", None)
amount = self.get_argument_float("amount", None)
currency = self.get_argument("currency", None)
frequency = self.get_argument("frequency", None)
frequency = "one-time"
self.render("donate/donate.html", countries=iso3166.countries,
- country=country, first_name=first_name, last_name=last_name,
- amount=amount, currency=currency, frequency=frequency)
+ country=country, amount=amount, currency=currency, frequency=frequency)
@base.ratelimit(minutes=15, requests=5)
async def post(self):
+ type = self.get_argument("type")
+ if not type in ("individual", "organization"):
+ raise tornado.web.HTTPError(400, "type is of an invalid value: %s" % type)
+
amount = self.get_argument("amount")
currency = self.get_argument("currency", "EUR")
frequency = self.get_argument("frequency")
- # Collect donor information
- donor = {
+ organization = None
+ locale = self.get_browser_locale()
+
+ # Get organization information
+ if type == "organization":
+ organization = {
+ "name" : self.get_argument("organization"),
+ "vat_number" : self.get_argument("vat_number", None),
+ }
+
+ # Collect person information
+ person = {
"email" : self.get_argument("email"),
"title" : self.get_argument("title"),
"first_name" : self.get_argument("first_name"),
"last_name" : self.get_argument("last_name"),
+ "locale" : locale.code,
}
# Collect address information
# Send everything to Zeiterfassung
try:
- # Search for person or create a new one
- response = await self.backend.zeiterfassung.send_request(
- "/api/v1/persons/search", **donor
- )
+ # Create a new organization
+ if organization:
+ organization = await self._create_organization(organization, address)
- if not response:
- response = await self.backend.zeiterfassung.send_request(
- "/api/v1/persons/create", **donor, **address
- )
-
- person = response.get("number")
+ # Create a person
+ person = await self._create_person(person, address, organization)
- donation = {
- "person" : person,
+ # Create a new order
+ order = await self._create_order(person=person, currency=currency)
- # $$$
- "amount" : amount,
- "currency" : currency,
+ # Add donation to the order
+ await self._create_donation(order, frequency, amount, currency,
+ vat_included=(type == "individual"))
- # Is this a recurring donation?
- "recurring" : frequency == "monthly",
+ # Submit the order
+ needs_payment = await self._submit_order(order)
- # Add URLs to redirect the user back
- "success_url" : "https://%s/donate/thank-you" % self.request.host,
- "error_url" : "https://%s/donate/error" % self.request.host,
- "back_url" : "https://%s/donate?amount=%s¤cy=%s&frequency=%s" %
- (self.request.host, amount, currency, frequency),
- }
-
- # Create donation
- response = await self.backend.zeiterfassung.send_request(
- "/api/v1/donations/create/ipfire-project", **donation, **address)
+ # Pay the order
+ if needs_payment:
+ redirect_url = await self._pay_order(order)
+ else:
+ redirect_url = "https://%s/donate/thank-you" % self.request.host
# Redirect the user to the payment page
- redirect_url = response.get("redirect_url")
if not redirect_url:
raise tornado.web.HTTPError(500, "Did not receive a redirect URL")
except Exception:
raise
+ async def _create_organization(self, organization, address):
+ # Check if we have an existing organization
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/organizations/search", **organization,
+ )
+
+ # Update details if we found a match
+ if response:
+ number = response.get("number")
+
+ # Update name
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/organizations/%s/name" % number, **organization
+ )
+
+ # Update VAT number
+ vat_number = organization.get("vat_number", None)
+ if vat_number:
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/organizations/%s/vat-number" % number, vat_number=vat_number,
+ )
+
+ # Update address
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/organizations/%s/address" % number, **address,
+ )
+
+ return number
+
+ # Otherwise we will create a new one
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/organizations/create", **organization, **address,
+ )
+
+ # Return the organization's number
+ return response.get("number")
+
+ async def _create_person(self, person, address, organization=None):
+ """
+ Searches for a matching person or creates a new one
+ """
+ # Check if we have an existing person
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/persons/search", **person
+ )
+
+ # Update details if we found a match
+ if response:
+ number = response.get("number")
+
+ # Update name
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/persons/%s/name" % number, **person,
+ )
+
+ # Update address
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/persons/%s/address" % number, **address,
+ )
+
+ return number
+
+ # If not, we will create a new one
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/persons/create", organization=organization, **person, **address
+ )
+
+ # Return the persons's number
+ return response.get("number")
+
+ async def _create_order(self, person, currency=None):
+ """
+ Creates a new order and returns its ID
+ """
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/orders/create", person=person, currency=currency,
+ )
+
+ # Return the order number
+ return response.get("number")
+
+ async def _create_donation(self, order, frequency, amount, currency,
+ vat_included=False):
+ """
+ Creates a new donation
+ """
+ # Select the correct product
+ try:
+ sku = SKUS[frequency]
+ except KeyError:
+ sku = DEFAULT_SKU
+
+ # Add it to the order
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/orders/%s/products/add" % order, sku=sku, quantity=1,
+ )
+
+ # Set the price
+ await self.backend.zeiterfassung.send_request(
+ "/api/v1/orders/%s/products/%s/price" % (order, sku),
+ price=amount, currency=currency, vat_included=vat_included,
+ )
+
+ async def _submit_order(self, order):
+ """
+ Submits the order
+ """
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/orders/%s/submit" % order,
+ )
+
+ # Return whether this needs payment
+ return not response.get("is_authorized")
+
+ async def _pay_order(self, order):
+ """
+ Pay the order
+ """
+ # Add URLs to redirect the user back
+ urls = {
+ "success_url" : "https://%s/donate/thank-you" % self.request.host,
+ "error_url" : "https://%s/donate/error" % self.request.host,
+ "back_url" : "https://%s/donate" % self.request.host,
+ }
+
+ # Send request
+ response = await self.backend.zeiterfassung.send_request(
+ "/api/v1/orders/%s/pay" % order, **urls,
+ )
+
+ # Return redirect URL
+ return response.get("redirect_url", None)
+
class ThankYouHandler(base.BaseHandler):
def get(self):
#!/usr/bin/python
-import datetime
-import logging
-import re
import json
import tornado.web
-from .. import fireinfo
-
from . import base
from . import ui_modules
return self.get_argument_date("when", None)
-MIN_PROFILE_VERSION = 0
-MAX_PROFILE_VERSION = 0
-
-class Profile(dict):
- def __getattr__(self, key):
- try:
- return self[key]
- except KeyError:
- raise AttributeError(key)
-
- def __setattr__(self, key, val):
- self[key] = val
-
-
class ProfileSendHandler(BaseHandler):
def check_xsrf_cookie(self):
# This cookie is not required here.
pass
- def prepare(self):
- # Create an empty profile.
- self.profile = Profile()
-
- def __check_attributes(self, profile):
- """
- Check for attributes that must be provided,
- """
- attributes = (
- "private_id",
- "profile_version",
- "public_id",
- )
- for attr in attributes:
- if attr not in profile:
- raise tornado.web.HTTPError(400, "Profile lacks '%s' attribute: %s" % (attr, profile))
-
- def __check_valid_ids(self, profile):
- """
- Check if IDs contain valid data.
- """
- for id in ("public_id", "private_id"):
- if re.match(r"^([a-f0-9]{40})$", "%s" % profile[id]) is None:
- raise tornado.web.HTTPError(400, "ID '%s' has wrong format: %s" % (id, profile))
-
- def __check_equal_ids(self, profile):
- """
- Check if public_id and private_id are equal.
- """
- if profile.public_id == profile.private_id:
- raise tornado.web.HTTPError(400, "Public and private IDs are equal: %s" % profile)
-
- def __check_matching_ids(self, profile):
- """
- Check if a profile with the given public_id is already in the
- database. If so we need to check if the private_id matches.
- """
- p = self.profiles.find_one({ "public_id" : profile["public_id"]})
- if not p:
- return
-
- p = Profile(p)
- if p.private_id != profile.private_id:
- raise tornado.web.HTTPError(400, "Mismatch of private_id: %s" % profile)
-
- def __check_profile_version(self, profile):
- """
- Check if this version of the server software does support the
- received profile.
- """
- version = profile.profile_version
-
- if version < MIN_PROFILE_VERSION or version > MAX_PROFILE_VERSION:
- raise tornado.web.HTTPError(400,
- "Profile version is not supported: %s" % version)
-
- def check_profile_blob(self, profile):
- """
- This method checks if the blob is sane.
- """
- checks = (
- self.__check_attributes,
- self.__check_valid_ids,
- self.__check_equal_ids,
- self.__check_profile_version,
- # These checks require at least one database query and should be done
- # at last.
- self.__check_matching_ids,
- )
-
- for check in checks:
- check(profile)
-
- # If we got here, everything is okay and we can go on...
-
def get_profile_blob(self):
profile = self.get_argument("profile", None)
if not profile:
raise tornado.web.HTTPError(400, "No profile received")
- # Try to decode the profile.
+ # Try to decode the profile
try:
return json.loads(profile)
except json.decoder.JSONDecodeError as e:
raise tornado.web.HTTPError(400, "Profile could not be decoded: %s" % e)
- # The GET method is only allowed in debugging mode.
- def get(self, public_id):
- if not self.application.settings["debug"]:
- raise tornado.web.HTTPError(405)
-
- return self.post(public_id)
-
def post(self, public_id):
profile_blob = self.get_profile_blob()
- #self.check_profile_blob(profile_blob)
# Handle the profile.
with self.db.transaction():
self.fireinfo.handle_profile(public_id, profile_blob,
country_code=self.current_country_code)
- except fireinfo.ProfileParserError as e:
- raise tornado.web.HTTPError(400, "Could not parse profile: %s" % e)
+ except ValueError as e:
+ raise tornado.web.HTTPError(400, "Could not process profile: %s" % e)
self.finish("Your profile was successfully saved to the database.")
class IndexHandler(BaseHandler):
def get(self):
data = {
+ "when" : self.when,
+
# Release
"latest_release" : self.backend.releases.get_latest(),
# Hardware
"arches" : self.fireinfo.get_arch_map(when=self.when),
"cpu_vendors" : self.fireinfo.get_cpu_vendors_map(when=self.when),
- "memory_avg" : self.backend.fireinfo.get_average_memory_amount(when=self.when),
+
+ # Memory
+ "memory_avg" : self.backend.fireinfo.get_average_memory_amount(when=self.when),
# Virtualization
"hypervisors" : self.fireinfo.get_hypervisor_map(when=self.when),
class DriverDetail(BaseHandler):
def get(self, driver):
- self.render("fireinfo/driver.html", driver=driver,
- driver_map=self.fireinfo.get_driver_map(driver, when=self.when))
+ devices = self.fireinfo.get_devices_by_driver(driver, when=self.when)
+
+ self.render("fireinfo/driver.html", driver=driver, devices=devices)
class ProfileHandler(BaseHandler):
class RandomProfileHandler(BaseHandler):
def get(self):
- profile_id = self.fireinfo.get_random_profile(when=self.when)
- if profile_id is None:
- raise tornado.web.HTTPError(404)
+ profile = self.fireinfo.get_random_profile(when=self.when)
+ if not profile:
+ raise tornado.web.HTTPError(404, "Could not find a random profile")
- self.redirect("/profile/%s" % profile_id)
+ self.redirect("/profile/%s" % profile.profile_id)
class ReleasesHandler(BaseHandler):
class ProcessorsHandler(BaseHandler):
def get(self):
- flags = {}
-
- for platform in ("arm", "x86"):
- flags[platform] = \
- self.fireinfo.get_common_cpu_flags_by_platform(platform, when=self.when)
-
- return self.render("fireinfo/processors.html", flags=flags)
+ return self.render("fireinfo/processors.html", when=self.when)
class VendorsHandler(BaseHandler):
def get(self):
with_data, total = self.backend.fireinfo.get_active_profiles()
- self.render("fireinfo/admin.html", with_data=with_data, total=total)
+ histogram = self.backend.fireinfo.get_profile_histogram()
+
+ self.render("fireinfo/admin.html", with_data=with_data, total=total,
+ histogram=histogram)
def get(self, profile_id, image_id):
when = self.get_argument_date("when", None)
- profile = self.fireinfo.get_profile_with_data(profile_id, when=when)
+ profile = self.fireinfo.get_profile(profile_id, when=when)
if not profile:
raise tornado.web.HTTPError(404, "Profile '%s' was not found." % profile_id)
return self.handler.backend
+class IPFireLogoModule(UIModule):
+ def render(self, suffix=None):
+ return self.render_string("modules/ipfire-logo.html", suffix=suffix)
+
+
class ChristmasBannerModule(UIModule):
def render(self):
return self.render_string("modules/christmas-banner.html")
class AvatarHandler(base.BaseHandler):
- def get(self, uid):
+ async def get(self, uid):
# Get the desired size of the avatar file
size = self.get_argument("size", None)
self.set_expires(31536000)
# Resize avatar
- avatar = account.get_avatar(size)
+ avatar = await account.get_avatar(size)
# If there is no avatar, we serve a default image
if not avatar:
logging.debug("No avatar uploaded for %s" % account)
# Generate a random avatar with only one letter
- avatar = self._get_avatar(account, size=size)
+ avatar = await self._get_avatar(account, size=size)
# Guess content type
type = imghdr.what(None, avatar)
# Deliver payload
self.finish(avatar)
- def _get_avatar(self, account, size=None, **args):
+ async def _get_avatar(self, account, size=None, **args):
letter = ("%s" % account)[0].upper()
if size is None:
key = "avatar:letter:%s:%s" % (letter, size)
# Fetch avatar from the cache
- avatar = self.memcached.get(key)
+ avatar = await self.backend.cache.get(key)
if not avatar:
avatar = self._make_avatar(letter, size=size, **args)
# Cache for forever
- self.memcached.set(key, avatar)
+ await self.backend.cache.set(key, avatar)
return avatar
self.render("users/edit.html", account=account, countries=countries.get_all())
@tornado.web.authenticated
- def post(self, uid):
+ async def post(self, uid):
account = self.backend.accounts.get_by_uid(uid)
if not account:
raise tornado.web.HTTPError(404, "Could not find account %s" % uid)