diff --git a/.gitignore b/.gitignore index 20c930c..1bfde3c 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,9 @@ docs/_build/ # PyBuilder target/ + +# npm +node_modules/ + +# diff +*.diff diff --git a/MANIFEST.in b/MANIFEST.in index f313fe9..856c390 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,13 +1,11 @@ include README.rst -include certidude/templates/*.sh -include certidude/templates/*.html -include certidude/templates/*.svg include certidude/templates/*.ovpn -include certidude/templates/*.cnf include certidude/templates/*.conf include certidude/templates/*.ini +include certidude/templates/mail/*.md include certidude/static/js/*.js include certidude/static/css/*.css include certidude/static/fonts/*.woff2 include certidude/static/img/*.svg include certidude/static/*.html +include certidude/sql/*/*.sql diff --git a/README.rst b/README.rst index 9f347e8..435971f 100644 --- a/README.rst +++ b/README.rst @@ -67,8 +67,11 @@ To install Certidude: .. code:: bash - apt-get install -y python3 python3-pip python3-dev cython3 build-essential libffi-dev libssl-dev libkrb5-dev - pip3 install --allow-external mysql-connector-python mysql-connector-python + apt-get install -y python python-pip python-dev cython \ + python-pysqlite2 python-mysql.connector python-ldap \ + build-essential libffi-dev libssl-dev libkrb5-dev \ + ldap-utils krb5-user default-mta \ + libsasl2-modules-gssapi-mit pip3 install certidude Make sure you're running PyOpenSSL 0.15+ and netifaces 0.10.4+ from PyPI, @@ -79,6 +82,7 @@ Create a system user for ``certidude``: .. code:: bash adduser --system --no-create-home --group certidude + mkdir /etc/certidude Setting up CA @@ -90,7 +94,7 @@ You can check it with: .. code:: bash - hostname -f + hostname -f The command should return ca.example.co @@ -144,7 +148,7 @@ Install ``nginx`` and ``uwsgi``: .. code:: bash - apt-get install nginx uwsgi uwsgi-plugin-python3 + apt-get install nginx uwsgi uwsgi-plugin-python For easy setup following is reccommended: @@ -162,7 +166,7 @@ Otherwise manually configure ``uwsgi`` application in ``/etc/uwsgi/apps-availabl vaccum = true uid = certidude gid = certidude - plugins = python34 + plugins = python chdir = /tmp module = certidude.wsgi callable = app @@ -192,7 +196,7 @@ configure the site in /etc/nginx/sites-available/certidude: server_name localhost; listen 80 default_server; listen [::]:80 default_server ipv6only=on; - root /usr/local/lib/python3.4/dist-packages/certidude/static; + root /usr/local/lib/python2.7/dist-packages/certidude/static; location /api/ { include uwsgi_params; @@ -201,19 +205,20 @@ configure the site in /etc/nginx/sites-available/certidude: # Add following three if you wish to enable push server on this machine location /pub { - allow 127.0.0.1; # Allow publishing only from CA machine - push_stream_publisher admin; - push_stream_channels_path $arg_id; + allow 127.0.0.1; + nchan_publisher http; + nchan_store_messages off; + nchan_channel_id $arg_id; } location ~ "^/lp/(.*)" { - push_stream_channels_path $1; - push_stream_subscriber long-polling; + nchan_subscriber longpoll; + nchan_channel_id $1; } location ~ "^/ev/(.*)" { - push_stream_channels_path $1; - push_stream_subscriber eventsource; + nchan_subscriber eventsource; + nchan_channel_id $1; } } @@ -254,6 +259,8 @@ Also adjust ``/etc/nginx/nginx.conf``: In your CA ssl.cnf make sure Certidude is aware of your nginx setup: +.. code:: + push_server = http://push.example.com/ Restart the services: @@ -283,7 +290,7 @@ Make sure Certidude machine's fully qualified hostname is correct in ``/etc/host 127.0.0.1 localhost 127.0.1.1 ca.example.lan ca -Set up Samba client configuration in ``/etc/samba/smb.conf``: +Reset Samba client configuration in ``/etc/samba/smb.conf``: .. code:: ini @@ -294,11 +301,36 @@ Set up Samba client configuration in ``/etc/samba/smb.conf``: realm = EXAMPLE.LAN kerberos method = system keytab +Reset Kerberos configuration in ``/etc/krb5.conf``: + +.. code:: ini + + [libdefaults] + default_realm = EXAMPLE.LAN + dns_lookup_realm = true + dns_lookup_kdc = true + forwardable = true + proxiable = true + +Initialize Kerberos credentials: + +.. code:: bash + + kinit administrator + +Join the machine to domain: + +.. code:: bash + + net ads join -k + Set up Kerberos keytab for the web service: .. code:: bash - KRB5_KTNAME=FILE:/etc/certidude/server.keytab net ads keytab add HTTP -U Administrator + KRB5_KTNAME=FILE:/etc/certidude/server.keytab net ads keytab add HTTP -k + chown root:certidude /etc/certidude/server.keytab + chmod 640 /etc/certidude/server.keytab Setting up authorization @@ -379,22 +411,29 @@ Clone the repository: git clone https://github.com/laurivosandi/certidude cd certidude +Install dependencies as shown above and additionally: + +.. code:: bash + + pip install -r requirements.txt + To generate templates: .. code:: bash apt-get install npm nodejs - npm install nunjucks - nunjucks-precompile --include "\\.html$" --include "\\.svg" certidude/static/ > certidude/static/js/templates.js + sudo ln -s nodejs /usr/bin/node # Fix 'env node' on Ubuntu 14.04 + npm install -g nunjucks + nunjucks-precompile --include "\\.html$" --include "\\.svg$" certidude/static/ > certidude/static/js/templates.js To run from source tree: .. code:: bash - PYTHONPATH=. KRB5_KTNAME=/etc/certidude/server.keytab LANG=C.UTF-8 python3 misc/certidude + PYTHONPATH=. KRB5_KTNAME=/etc/certidude/server.keytab LANG=C.UTF-8 python misc/certidude To install the package from the source: .. code:: bash - python3 setup.py install --single-version-externally-managed --root / + python setup.py install --single-version-externally-managed --root / diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 982d67f..547ab52 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -1,13 +1,19 @@ +# encoding: utf-8 + import falcon import mimetypes +import logging import os import click +from datetime import datetime from time import sleep -from certidude import authority +from certidude import authority, mailer from certidude.auth import login_required, authorize_admin -from certidude.decorators import serialize, event_source +from certidude.decorators import serialize, event_source, csrf_protection from certidude.wrappers import Request, Certificate -from certidude import config +from certidude import constants, config + +logger = logging.getLogger("api") class CertificateStatusResource(object): """ @@ -24,7 +30,9 @@ class CertificateStatusResource(object): class CertificateAuthorityResource(object): def on_get(self, req, resp): + logger.info("Served CA certificate to %s", req.context.get("remote_addr")) resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") + resp.append_header("Content-Type", "application/x-x509-ca-cert") resp.append_header("Content-Disposition", "attachment; filename=ca.crt") @@ -34,16 +42,54 @@ class SessionResource(object): @authorize_admin @event_source def on_get(self, req, resp): + if config.ACCOUNTS_BACKEND == "ldap": + import ldap + ft = config.LDAP_MEMBERS_FILTER % (config.ADMINS_GROUP, "*") + r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, + ldap.SCOPE_SUBTREE, ft.encode("utf-8"), ["cn", "member"]) + + for dn,entry in r: + cn, = entry.get("cn") + break + else: + raise ValueError("Failed to look up group %s in LDAP" % repr(group_name)) + + admins = dict([(j, j.split(",")[0].split("=")[1]) for j in entry.get("member")]) + elif config.ACCOUNTS_BACKEND == "posix": + import grp + _, _, gid, members = grp.getgrnam(config.ADMINS_GROUP) + admins = dict([(j, j) for j in members]) + else: + raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND) + return dict( - username=req.context.get("user"), - event_channel = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN, + user = dict( + name=req.context.get("user").name, + gn=req.context.get("user").given_name, + sn=req.context.get("user").surname, + mail=req.context.get("user").mail + ), + request_submission_allowed = sum( # Dirty hack! + [req.context.get("remote_addr") in j + for j in config.REQUEST_SUBNETS]), + user_subnets = config.USER_SUBNETS, autosign_subnets = config.AUTOSIGN_SUBNETS, request_subnets = config.REQUEST_SUBNETS, admin_subnets=config.ADMIN_SUBNETS, - admin_users=config.ADMIN_USERS, - requests=authority.list_requests(), - signed=authority.list_signed(), - revoked=authority.list_revoked()) + admin_users = admins, + #admin_users=config.ADMIN_USERS, + authority = dict( + outbox = config.OUTBOX, + certificate = authority.certificate, + events = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN, + requests=authority.list_requests(), + signed=authority.list_signed(), + revoked=authority.list_revoked(), + ) if config.ADMINS_GROUP in req.context.get("groups") else None, + features=dict( + tagging=config.TAGGING_BACKEND, + leases=False, #config.LEASES_BACKEND, + logging=config.LOGGING_BACKEND)) class StaticResource(object): @@ -58,7 +104,7 @@ class StaticResource(object): if os.path.isdir(path): path = os.path.join(path, "index.html") - print("Serving:", path) + click.echo("Serving: %s" % path) if os.path.exists(path): content_type, content_encoding = mimetypes.guess_type(path) @@ -72,7 +118,33 @@ class StaticResource(object): resp.body = "File '%s' not found" % req.path +class BundleResource(object): + @login_required + def on_get(self, req, resp): + common_name = req.context["user"].mail + logger.info("Signing bundle %s for %s", common_name, req.context.get("user")) + resp.set_header("Content-Type", "application/x-pkcs12") + resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name) + resp.body, cert = authority.generate_pkcs12_bundle(common_name, + owner=req.context.get("user")) + + +import ipaddress + +class NormalizeMiddleware(object): + @csrf_protection + def process_request(self, req, resp, *args): + assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed" + req.context["remote_addr"] = ipaddress.ip_address(req.env["REMOTE_ADDR"].decode("utf-8")) + + def process_response(self, req, resp, resource): + # wtf falcon?! + if isinstance(resp.location, unicode): + resp.location = resp.location.encode("ascii") + def certidude_app(): + from certidude import config + from .revoked import RevocationListResource from .signed import SignedCertificateListResource, SignedCertificateDetailResource from .request import RequestListResource, RequestDetailResource @@ -82,60 +154,56 @@ def certidude_app(): from .tag import TagResource, TagDetailResource from .cfg import ConfigResource, ScriptResource - app = falcon.API() + app = falcon.API(middleware=NormalizeMiddleware()) # Certificate authority API calls app.add_route("/api/ocsp/", CertificateStatusResource()) + app.add_route("/api/bundle/", BundleResource()) app.add_route("/api/certificate/", CertificateAuthorityResource()) app.add_route("/api/revoked/", RevocationListResource()) app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource()) app.add_route("/api/signed/", SignedCertificateListResource()) app.add_route("/api/request/{cn}/", RequestDetailResource()) app.add_route("/api/request/", RequestListResource()) - app.add_route("/api/log/", LogResource()) - app.add_route("/api/tag/", TagResource()) - app.add_route("/api/tag/{identifier}/", TagDetailResource()) - app.add_route("/api/config/", ConfigResource()) - app.add_route("/api/script/", ScriptResource()) app.add_route("/api/", SessionResource()) # Gateway API calls, should this be moved to separate project? app.add_route("/api/lease/", LeaseResource()) app.add_route("/api/whois/", WhoisResource()) - """ - Set up logging - """ + log_handlers = [] + if config.LOGGING_BACKEND == "sql": + from certidude.mysqllog import LogHandler + uri = config.cp.get("logging", "database") + log_handlers.append(LogHandler(uri)) + app.add_route("/api/log/", LogResource(uri)) + elif config.LOGGING_BACKEND == "syslog": + from logging.handlers import SyslogHandler + log_handlers.append(SysLogHandler()) + # Browsing syslog via HTTP is obviously not possible out of the box + elif config.LOGGING_BACKEND: + raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND) - from certidude import config - from certidude.mysqllog import MySQLLogHandler - from datetime import datetime - import logging - import socket - import json + if config.TAGGING_BACKEND == "sql": + uri = config.cp.get("tagging", "database") + app.add_route("/api/tag/", TagResource(uri)) + app.add_route("/api/tag/{identifier}/", TagDetailResource(uri)) + app.add_route("/api/config/", ConfigResource(uri)) + app.add_route("/api/script/", ScriptResource(uri)) + elif config.TAGGING_BACKEND: + raise ValueError("Invalid tagging.backend = %s" % config.TAGGING_BACKEND) - - class PushLogHandler(logging.Handler): - def emit(self, record): - from certidude.push import publish - publish("log-entry", dict( - created = datetime.fromtimestamp(record.created), - message = record.msg % record.args, - severity = record.levelname.lower())) - - if config.DATABASE_POOL: - sql_handler = MySQLLogHandler(config.DATABASE_POOL) - push_handler = PushLogHandler() + if config.PUSH_PUBLISH: + from certidude.push import PushLogHandler + log_handlers.append(PushLogHandler()) for facility in "api", "cli": logger = logging.getLogger(facility) logger.setLevel(logging.DEBUG) - if config.DATABASE_POOL: - logger.addHandler(sql_handler) - logger.addHandler(push_handler) + for handler in log_handlers: + logger.addHandler(handler) - - logging.getLogger("cli").debug("Started Certidude at %s", config.FQDN) + logging.getLogger("cli").debug("Started Certidude at %s", constants.FQDN) import atexit diff --git a/certidude/api/cfg.py b/certidude/api/cfg.py index 45f2515..5b25ad1 100644 --- a/certidude/api/cfg.py +++ b/certidude/api/cfg.py @@ -6,6 +6,7 @@ from random import choice from certidude import config from certidude.auth import login_required, authorize_admin from certidude.decorators import serialize +from certidude.relational import RelationalMixin from jinja2 import Environment, FileSystemLoader logger = logging.getLogger("api") @@ -39,43 +40,42 @@ where device.cn = %s """ -SQL_SELECT_INHERITANCE = """ + +SQL_SELECT_RULES = """ select - tag_inheritance.`id` as `id`, - tag.id as `tag_id`, - tag.`key` as `match_key`, - tag.`value` as `match_value`, - tag_inheritance.`key` as `key`, - tag_inheritance.`value` as `value` -from tag_inheritance -join tag on tag.id = tag_inheritance.tag_id + tag.cn as `cn`, + tag.key as `tag_key`, + tag.value as `tag_value`, + tag_properties.property_key as `property_key`, + tag_properties.property_value as `property_value` +from + tag_properties +join + tag +on + tag.key = tag_properties.tag_key and + tag.value = tag_properties.tag_value """ -class ConfigResource(object): + +class ConfigResource(RelationalMixin): @serialize @login_required @authorize_admin def on_get(self, req, resp): - conn = config.DATABASE_POOL.get_connection() - cursor = conn.cursor(dictionary=True) - cursor.execute(SQL_SELECT_INHERITANCE) - def g(): - for row in cursor: - yield row - cursor.close() - conn.close() - return g() + return self.iterfetch(SQL_SELECT_RULES) -class ScriptResource(object): + +class ScriptResource(RelationalMixin): def on_get(self, req, resp): from certidude.api.whois import address_to_identity node = address_to_identity( - config.DATABASE_POOL.get_connection(), - ipaddress.ip_address(req.env["REMOTE_ADDR"]) + self.connect(), + req.context.get("remote_addr") ) if not node: - resp.body = "Could not map IP address: %s" % req.env["REMOTE_ADDR"] + resp.body = "Could not map IP address: %s" % req.context.get("remote_addr") resp.status = falcon.HTTP_404 return @@ -84,7 +84,7 @@ class ScriptResource(object): key, common_name = identity.split("=") assert "=" not in common_name - conn = config.DATABASE_POOL.get_connection() + conn = self.connect() cursor = conn.cursor() resp.set_header("Content-Type", "text/x-shellscript") diff --git a/certidude/api/log.py b/certidude/api/log.py index f6d04b3..7faaec3 100644 --- a/certidude/api/log.py +++ b/certidude/api/log.py @@ -2,38 +2,14 @@ from certidude import config from certidude.auth import login_required, authorize_admin from certidude.decorators import serialize +from certidude.relational import RelationalMixin + +class LogResource(RelationalMixin): + SQL_CREATE_TABLES = "log_tables.sql" -class LogResource(object): @serialize @login_required @authorize_admin def on_get(self, req, resp): - """ - Translate currently online client's IP-address to distinguished name - """ - - SQL_LOG_ENTRIES = """ - SELECT - * - FROM - log - ORDER BY created DESC - """ - conn = config.DATABASE_POOL.get_connection() - cursor = conn.cursor(dictionary=True) - cursor.execute(SQL_LOG_ENTRIES) - - def g(): - for row in cursor: - yield row - cursor.close() - conn.close() - return tuple(g()) - -# for acquired, released, identity in cursor: -# return { -# "acquired": datetime.utcfromtimestamp(acquired), -# "identity": parse_dn(bytes(identity)) -# } -# return None - + # TODO: Add last id parameter + return self.iterfetch("select * from log order by created desc") diff --git a/certidude/api/request.py b/certidude/api/request.py index 0b500d8..6462b49 100644 --- a/certidude/api/request.py +++ b/certidude/api/request.py @@ -5,45 +5,42 @@ import logging import ipaddress import os from certidude import config, authority, helpers, push, errors -from certidude.auth import login_required, authorize_admin +from certidude.auth import login_required, login_optional, authorize_admin from certidude.decorators import serialize from certidude.wrappers import Request, Certificate +from certidude.firewall import whitelist_subnets, whitelist_content_types logger = logging.getLogger("api") class RequestListResource(object): @serialize + @login_required @authorize_admin def on_get(self, req, resp): - return helpers.list_requests() + return authority.list_requests() + @login_optional + @whitelist_subnets(config.REQUEST_SUBNETS) + @whitelist_content_types("application/pkcs10") def on_post(self, req, resp): """ Submit certificate signing request (CSR) in PEM format """ - # Parse remote IPv4/IPv6 address - remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"].decode("utf-8")) - # Check for CSR submission whitelist - if config.REQUEST_SUBNETS: - for subnet in config.REQUEST_SUBNETS: - if subnet.overlaps(remote_addr): - break - else: - logger.warning("Attempted to submit signing request from non-whitelisted address %s", remote_addr) - raise falcon.HTTPForbidden("Forbidden", "IP address %s not whitelisted" % remote_addr) - - if req.get_header("Content-Type") != "application/pkcs10": - raise falcon.HTTPUnsupportedMediaType( - "This API call accepts only application/pkcs10 content type") - - body = req.stream.read(req.content_length).decode("ascii") + body = req.stream.read(req.content_length) csr = Request(body) + if not csr.common_name: + logger.warning("Rejected signing request without common name from %s", + req.context.get("remote_addr")) + raise falcon.HTTPBadRequest( + "Bad request", + "No common name specified!") + # Check if this request has been already signed and return corresponding certificte if it has been signed try: cert = authority.get_signed(csr.common_name) - except FileNotFoundError: + except EnvironmentError: pass else: if cert.pubkey == csr.pubkey: @@ -56,12 +53,12 @@ class RequestListResource(object): # Process automatic signing if the IP address is whitelisted and autosigning was requested if req.get_param_as_bool("autosign"): for subnet in config.AUTOSIGN_SUBNETS: - if subnet.overlaps(remote_addr): + if subnet.overlaps(req.context.get("remote_addr")): try: resp.set_header("Content-Type", "application/x-x509-user-cert") resp.body = authority.sign(csr).dump() return - except FileExistsError: # Certificate already exists, try to save the request + except EnvironmentError: # Certificate already exists, try to save the request pass break @@ -73,7 +70,8 @@ class RequestListResource(object): pass except errors.DuplicateCommonNameError: # TODO: Certificate renewal - logger.warning("Rejected signing request with overlapping common name from %s", req.env["REMOTE_ADDR"]) + logger.warning("Rejected signing request with overlapping common name from %s", + req.context.get("remote_addr")) raise falcon.HTTPConflict( "CSR with such CN already exists", "Will not overwrite existing certificate signing request, explicitly delete CSR and try again") @@ -86,12 +84,12 @@ class RequestListResource(object): url = config.PUSH_LONG_POLL % csr.fingerprint() click.echo("Redirecting to: %s" % url) resp.status = falcon.HTTP_SEE_OTHER - resp.set_header("Location", url) - logger.warning("Redirecting signing request from %s to %s", req.env["REMOTE_ADDR"], url) + resp.set_header("Location", url.encode("ascii")) + logger.debug("Redirecting signing request from %s to %s", req.context.get("remote_addr"), url) else: # Request was accepted, but not processed resp.status = falcon.HTTP_202 - logger.info("Signing request from %s stored", req.env["REMOTE_ADDR"]) + logger.info("Signing request from %s stored", req.context.get("remote_addr")) class RequestDetailResource(object): @@ -101,11 +99,8 @@ class RequestDetailResource(object): Fetch certificate signing request as PEM """ csr = authority.get_request(cn) -# if not os.path.exists(path): -# raise falcon.HTTPNotFound() - - resp.set_header("Content-Type", "application/pkcs10") - resp.set_header("Content-Disposition", "attachment; filename=%s.csr" % csr.common_name) + logger.debug("Signing request %s was downloaded by %s", + csr.common_name, req.context.get("remote_addr")) return csr @login_required @@ -120,14 +115,17 @@ class RequestDetailResource(object): resp.body = "Certificate successfully signed" resp.status = falcon.HTTP_201 resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn) - logger.info("Signing request %s signed by %s from %s", csr.common_name, req.context["user"], req.env["REMOTE_ADDR"]) + logger.info("Signing request %s signed by %s from %s", csr.common_name, + req.context.get("user"), req.context.get("remote_addr")) @login_required @authorize_admin def on_delete(self, req, resp, cn): try: authority.delete_request(cn) - except FileNotFoundError: + # Logging implemented in the function above + except EnvironmentError as e: resp.body = "No certificate CN=%s found" % cn - logger.warning("User %s attempted to delete non-existant signing request %s from %s", req.context["user"], cn, req.env["REMOTE_ADDR"]) + logger.warning("User %s failed to delete signing request %s from %s, reason: %s", + req.context["user"], cn, req.context.get("remote_addr"), e) raise falcon.HTTPNotFound() diff --git a/certidude/api/revoked.py b/certidude/api/revoked.py index 0f85d94..175f12d 100644 --- a/certidude/api/revoked.py +++ b/certidude/api/revoked.py @@ -1,9 +1,12 @@ +import logging from certidude.authority import export_crl +logger = logging.getLogger("api") + class RevocationListResource(object): def on_get(self, req, resp): + logger.debug("Revocation list requested by %s", req.context.get("remote_addr")) resp.set_header("Content-Type", "application/x-pkcs7-crl") resp.append_header("Content-Disposition", "attachment; filename=ca.crl") resp.body = export_crl() - diff --git a/certidude/api/signed.py b/certidude/api/signed.py index c743cbd..36adcb4 100644 --- a/certidude/api/signed.py +++ b/certidude/api/signed.py @@ -9,40 +9,35 @@ logger = logging.getLogger("api") class SignedCertificateListResource(object): @serialize + @login_required @authorize_admin def on_get(self, req, resp): - for j in authority.list_signed(): - yield omit( - key_type=j.key_type, - key_length=j.key_length, - identity=j.identity, - cn=j.common_name, - c=j.country_code, - st=j.state_or_county, - l=j.city, - o=j.organization, - ou=j.organizational_unit, - fingerprint=j.fingerprint()) + return {"signed":authority.list_signed()} class SignedCertificateDetailResource(object): @serialize def on_get(self, req, resp, cn): # Compensate for NTP lag - from time import sleep - sleep(5) +# from time import sleep +# sleep(5) try: - logger.info("Served certificate %s to %s", cn, req.env["REMOTE_ADDR"]) - resp.set_header("Content-Disposition", "attachment; filename=%s.crt" % cn) - return authority.get_signed(cn) - except FileNotFoundError: - logger.warning("Failed to serve non-existant certificate %s to %s", cn, req.env["REMOTE_ADDR"]) + cert = authority.get_signed(cn) + except EnvironmentError: + logger.warning("Failed to serve non-existant certificate %s to %s", + cn, req.context.get("remote_addr")) resp.body = "No certificate CN=%s found" % cn raise falcon.HTTPNotFound() + else: + logger.debug("Served certificate %s to %s", + cn, req.context.get("remote_addr")) + return cert + @login_required @authorize_admin def on_delete(self, req, resp, cn): - logger.info("Revoked certificate %s by %s from %s", cn, req.context["user"], req.env["REMOTE_ADDR"]) + logger.info("Revoked certificate %s by %s from %s", + cn, req.context.get("user"), req.context.get("remote_addr")) authority.revoke_certificate(cn) diff --git a/certidude/api/tag.py b/certidude/api/tag.py index 229b1a6..8009370 100644 --- a/certidude/api/tag.py +++ b/certidude/api/tag.py @@ -1,117 +1,63 @@ import falcon import logging -from certidude import config +from certidude.relational import RelationalMixin from certidude.auth import login_required, authorize_admin from certidude.decorators import serialize logger = logging.getLogger("api") -SQL_TAG_LIST = """ -select - device_tag.id as `id`, - tag.key as `key`, - tag.value as `value`, - device.cn as `cn` -from - device_tag -join - tag -on - device_tag.tag_id = tag.id -join - device -on - device_tag.device_id = device.id -""" +class TagResource(RelationalMixin): + SQL_CREATE_TABLES = "tag_tables.sql" -SQL_TAG_DETAIL = SQL_TAG_LIST + " where device_tag.id = %s" - -class TagResource(object): @serialize @login_required @authorize_admin def on_get(self, req, resp): - conn = config.DATABASE_POOL.get_connection() - cursor = conn.cursor(dictionary=True) - cursor.execute(SQL_TAG_LIST) + return self.iterfetch("select * from tag") - def g(): - for row in cursor: - yield row - cursor.close() - conn.close() - return tuple(g()) @serialize @login_required @authorize_admin def on_post(self, req, resp): from certidude import push - conn = config.DATABASE_POOL.get_connection() - cursor = conn.cursor() - - args = req.get_param("cn"), - cursor.execute( - "insert ignore device (`cn`) values (%s) on duplicate key update used = NOW();", args) - device_id = cursor.lastrowid - - args = req.get_param("key"), req.get_param("value") - cursor.execute( - "insert into tag (`key`, `value`) values (%s, %s) on duplicate key update used = NOW();", args) - tag_id = cursor.lastrowid - - args = device_id, tag_id - cursor.execute( - "insert into device_tag (`device_id`, `tag_id`) values (%s, %s);", args) - - push.publish("tag-added", str(cursor.lastrowid)) - args = req.get_param("cn"), req.get_param("key"), req.get_param("value") + rowid = self.sql_execute("tag_insert.sql", *args) + push.publish("tag-added", str(rowid)) logger.debug("Tag cn=%s, key=%s, value=%s added" % args) - conn.commit() - cursor.close() - conn.close() -class TagDetailResource(object): +class TagDetailResource(RelationalMixin): + SQL_CREATE_TABLES = "tag_tables.sql" + @serialize @login_required @authorize_admin def on_get(self, req, resp, identifier): - conn = config.DATABASE_POOL.get_connection() - cursor = conn.cursor(dictionary=True) - cursor.execute(SQL_TAG_DETAIL, (identifier,)) + conn = self.sql_connect() + cursor = conn.cursor() + if self.uri.scheme == "mysql": + cursor.execute("select `cn`, `key`, `value` from tag where id = %s", (identifier,)) + else: + cursor.execute("select `cn`, `key`, `value` from tag where id = ?", (identifier,)) + cols = [j[0] for j in cursor.description] for row in cursor: cursor.close() conn.close() - return row + return dict(zip(cols, row)) cursor.close() conn.close() raise falcon.HTTPNotFound() + @serialize @login_required @authorize_admin def on_put(self, req, resp, identifier): from certidude import push - conn = config.DATABASE_POOL.get_connection() - cursor = conn.cursor() - - # Create tag if necessary - args = req.get_param("key"), req.get_param("value") - cursor.execute( - "insert into tag (`key`, `value`) values (%s, %s) on duplicate key update used = NOW();", args) - tag_id = cursor.lastrowid - - # Attach tag to device - cursor.execute("update device_tag set tag_id = %s where `id` = %s limit 1", - (tag_id, identifier)) - conn.commit() - - cursor.close() - conn.close() - + args = req.get_param("value"), identifier + self.sql_execute("tag_update.sql", *args) logger.debug("Tag %s updated, value set to %s", identifier, req.get_param("value")) push.publish("tag-updated", identifier) @@ -122,13 +68,6 @@ class TagDetailResource(object): @authorize_admin def on_delete(self, req, resp, identifier): from certidude import push - conn = config.DATABASE_POOL.get_connection() - cursor = conn.cursor() - cursor.execute("delete from device_tag where id = %s", (identifier,)) - conn.commit() - cursor.close() - conn.close() + self.sql_execute("tag_delete.sql", identifier) push.publish("tag-removed", identifier) logger.debug("Tag %s removed" % identifier) - - diff --git a/certidude/api/whois.py b/certidude/api/whois.py index 1b4c0db..44ce255 100644 --- a/certidude/api/whois.py +++ b/certidude/api/whois.py @@ -46,7 +46,7 @@ class WhoisResource(object): identity = address_to_identity( conn, - ipaddress.ip_address(req.get_param("address") or req.env["REMOTE_ADDR"]) + req.context.get("remote_addr") ) conn.close() @@ -55,4 +55,4 @@ class WhoisResource(object): return dict(address=identity[0], acquired=identity[1], identity=identity[2]) else: resp.status = falcon.HTTP_403 - resp.body = "Failed to look up node %s" % req.env["REMOTE_ADDR"] + resp.body = "Failed to look up node %s" % req.context.get("remote_addr") diff --git a/certidude/auth.py b/certidude/auth.py index 066064f..ea18622 100644 --- a/certidude/auth.py +++ b/certidude/auth.py @@ -1,144 +1,324 @@ import click import falcon -import ipaddress import kerberos import logging import os import re import socket -from certidude import config +from certidude.firewall import whitelist_subnets +from certidude import config, constants logger = logging.getLogger("api") -# Vanilla Kerberos provides only username. -# AD also embeds PAC (Privilege Attribute Certificate), which -# is supposed to be sent via HTTP headers and it contains -# the groups user is part of. -# Even then we would have to manually look up the e-mail -# address eg via LDAP, hence to keep things simple -# we simply use Kerberos to authenticate. - FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] -if config.AUTHENTICATION_BACKEND == "kerberos": - if not os.getenv("KRB5_KTNAME"): +if config.AUTHENTICATION_BACKENDS == {"kerberos"}: + ktname = os.getenv("KRB5_KTNAME") + + if not ktname: click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True) exit(250) + if not os.path.exists(ktname): + click.echo("Kerberos keytab %s does not exist" % ktname, err=True) + exit(248) try: principal = kerberos.getServerPrincipalDetails("HTTP", FQDN) except kerberos.KrbError as exc: - click.echo("Failed to initialize Kerberos, reason: %s" % exc, err=True) + click.echo("Failed to initialize Kerberos, service principal is HTTP/%s, reason: %s" % (FQDN, exc), err=True) exit(249) else: click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN) -else: - NotImplemented -def login_required(func): - def pam_authenticate(resource, req, resp, *args, **kwargs): - """ - Authenticate against PAM with WWW Basic Auth credentials - """ - authorization = req.get_header("Authorization") - if not authorization: - resp.append_header("WWW-Authenticate", "Basic") - raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate") - if not authorization.startswith("Basic "): - raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % authorization) +class User(object): + def __init__(self, name): + if "@" in name: + self.mail = name + self.name, self.domain = name.split("@") + else: + self.mail = None + self.name, self.domain = name, None + self.given_name, self.surname = None, None - from base64 import b64decode - basic, token = authorization.split(" ", 1) - user, passwd = b64decode(token).split(":", 1) + def __repr__(self): + if self.given_name and self.surname: + return u"%s %s <%s>" % (self.given_name, self.surname, self.mail) + else: + return self.mail - import simplepam - if not simplepam.authenticate(user, passwd, "sshd"): - raise falcon.HTTPForbidden("Forbidden", "Invalid password") - req.context["user"] = user +def member_of(group_name): + """ + Check if requesting user is member of an UNIX group + """ + + def wrapper(func): + def posix_check_group_membership(resource, req, resp, *args, **kwargs): + import grp + _, _, gid, members = grp.getgrnam(group_name) + if req.context.get("user").name not in members: + logger.info("User '%s' not member of group '%s'", req.context.get("user").name, group_name) + raise falcon.HTTPForbidden("Forbidden", "User not member of designated group") + req.context.get("groups").add(group_name) + return func(resource, req, resp, *args, **kwargs) + + def ldap_check_group_membership(resource, req, resp, *args, **kwargs): + import ldap + + ft = config.LDAP_MEMBERS_FILTER % (group_name, req.context.get("user").dn) + r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE, + ft.encode("utf-8"), + ["member"]) + + for dn,entry in r: + if not dn: continue + logger.debug("User %s is member of group %s" % ( + req.context.get("user"), repr(group_name))) + req.context.get("groups").add(group_name) + break + else: + raise ValueError("Failed to look up group '%s' with '%s' listed as member in LDAP" % (group_name, req.context.get("user").name)) + + return func(resource, req, resp, *args, **kwargs) + + if config.AUTHORIZATION_BACKEND == "ldap": + return ldap_check_group_membership + elif config.AUTHORIZATION_BACKEND == "posix": + return posix_check_group_membership + else: + raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND) + return wrapper + + +def account_info(func): + # TODO: Use Privilege Account Certificate for Kerberos + + def posix_account_info(resource, req, resp, *args, **kwargs): + import pwd + _, _, _, _, gecos, _, _ = pwd.getpwnam(req.context["user"].name) + gecos = gecos.decode("utf-8").split(",") + full_name = gecos[0] + if full_name and " " in full_name: + req.context["user"].given_name, req.context["user"].surname = full_name.split(" ", 1) + req.context["user"].mail = req.context["user"].name + "@" + constants.DOMAIN return func(resource, req, resp, *args, **kwargs) + def ldap_account_info(resource, req, resp, *args, **kwargs): + import ldap + import ldap.sasl - def kerberos_authenticate(resource, req, resp, *args, **kwargs): - authorization = req.get_header("Authorization") + if "ldap_conn" not in req.context: + for server in config.LDAP_SERVERS: + conn = ldap.initialize(server) + conn.set_option(ldap.OPT_REFERRALS, 0) + if os.path.exists("/etc/krb5.keytab"): + ticket_cache = os.getenv("KRB5CCNAME") + if not ticket_cache: + raise ValueError("Ticket cache not initialized, unable to authenticate with computer account against LDAP server!") + click.echo("Connecing to %s using Kerberos ticket cache from %s" % (server, ticket_cache)) + conn.sasl_interactive_bind_s('', ldap.sasl.gssapi()) + else: + raise NotImplementedError("LDAP simple bind not supported, use Kerberos") + req.context["ldap_conn"] = conn + break + else: + raise ValueError("No LDAP servers!") - if not authorization: - resp.append_header("WWW-Authenticate", "Negotiate") - logger.debug("No Kerberos ticket offered while attempting to access %s from %s", req.env["PATH_INFO"], req.env["REMOTE_ADDR"]) - raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered, are you sure you've logged in with domain user account?") + ft = config.LDAP_USER_FILTER % req.context.get("user").name + r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE, + ft, + ["cn", "givenname", "sn", "mail", "userPrincipalName"]) - token = ''.join(authorization.split()[1:]) + for dn, entry in r: + if not dn: continue + if entry.get("givenname") and entry.get("sn"): + given_name, = entry.get("givenName") + surname, = entry.get("sn") + req.context["user"].given_name = given_name.decode("utf-8") + req.context["user"].surname = surname.decode("utf-8") + else: + cn, = entry.get("cn") + if " " in cn: + req.context["user"].given_name, req.context["user"].surname = cn.decode("utf-8").split(" ", 1) - try: - result, context = kerberos.authGSSServerInit("HTTP@" + FQDN) - except kerberos.GSSError as ex: - # TODO: logger.error - raise falcon.HTTPForbidden("Forbidden", "Authentication System Failure: %s(%s)" % (ex.args[0][0], ex.args[1][0],)) - - try: - result = kerberos.authGSSServerStep(context, token) - except kerberos.GSSError as ex: - s = str(dir(ex)) - kerberos.authGSSServerClean(context) - # TODO: logger.error - raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s (%s)" % (ex.args[0][0], ex.args[1][0])) - except kerberos.KrbError as ex: - kerberos.authGSSServerClean(context) - # TODO: logger.error - raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s" % (ex.args[0],)) - - user = kerberos.authGSSServerUserName(context) - req.context["user"], req.context["user_realm"] = user.split("@") - - try: - # BUGBUG: https://github.com/02strich/pykerberos/issues/6 - #kerberos.authGSSServerClean(context) - pass - except kerberos.GSSError as ex: - # TODO: logger.error - raise error.LoginFailed('Authentication System Failure %s(%s)' % (ex.args[0][0], ex.args[1][0],)) - - if result == kerberos.AUTH_GSS_COMPLETE: - logger.debug("Succesfully authenticated user %s for %s from %s", req.context["user"], req.env["PATH_INFO"], req.env["REMOTE_ADDR"]) - return func(resource, req, resp, *args, **kwargs) - elif result == kerberos.AUTH_GSS_CONTINUE: - # TODO: logger.error - raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI") + req.context["user"].dn = dn.decode("utf-8") + req.context["user"].mail, = entry.get("mail") or entry.get("userPrincipalName") or (None,) + retval = func(resource, req, resp, *args, **kwargs) + req.context.get("ldap_conn").unbind_s() + return retval else: - # TODO: logger.error - raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI") + raise ValueError("Failed to look up %s in LDAP" % req.context.get("user")) - if config.AUTHENTICATION_BACKEND == "kerberos": - return kerberos_authenticate - elif config.AUTHENTICATION_BACKEND == "pam": - return pam_authenticate + if config.ACCOUNTS_BACKEND == "ldap": + return ldap_account_info + elif config.ACCOUNTS_BACKEND == "posix": + return posix_account_info else: - NotImplemented + raise NotImplementedError("Accounts backend %s not supported" % config.ACCOUNTS_BACKEND) + + +def authenticate(optional=False): + def wrapper(func): + def kerberos_authenticate(resource, req, resp, *args, **kwargs): + if optional and not req.get_param_as_bool("authenticate"): + return func(resource, req, resp, *args, **kwargs) + + if not req.auth: + resp.append_header("WWW-Authenticate", "Negotiate") + logger.debug("No Kerberos ticket offered while attempting to access %s from %s", + req.env["PATH_INFO"], req.context.get("remote_addr")) + raise falcon.HTTPUnauthorized("Unauthorized", + "No Kerberos ticket offered, are you sure you've logged in with domain user account?") + + token = ''.join(req.auth.split()[1:]) + + try: + result, context = kerberos.authGSSServerInit("HTTP@" + FQDN) + except kerberos.GSSError as ex: + # TODO: logger.error + raise falcon.HTTPForbidden("Forbidden", + "Authentication System Failure: %s(%s)" % (ex.args[0][0], ex.args[1][0],)) + + try: + result = kerberos.authGSSServerStep(context, token) + except kerberos.GSSError as ex: + kerberos.authGSSServerClean(context) + # TODO: logger.error + raise falcon.HTTPForbidden("Forbidden", + "Bad credentials: %s (%d)" % (ex.args[0][0], ex.args[0][1])) + except kerberos.KrbError as ex: + kerberos.authGSSServerClean(context) + # TODO: logger.error + raise falcon.HTTPForbidden("Forbidden", + "Bad credentials: %s" % (ex.args[0],)) + + user = kerberos.authGSSServerUserName(context) + req.context["user"] = User(user) + req.context["groups"] = set() + + try: + kerberos.authGSSServerClean(context) + except kerberos.GSSError as ex: + # TODO: logger.error + raise falcon.HTTPUnauthorized("Authentication System Failure %s (%s)" % (ex.args[0][0], ex.args[1][0])) + + if result == kerberos.AUTH_GSS_COMPLETE: + logger.debug("Succesfully authenticated user %s for %s from %s", + req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"]) + return account_info(func)(resource, req, resp, *args, **kwargs) + elif result == kerberos.AUTH_GSS_CONTINUE: + # TODO: logger.error + raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI") + else: + # TODO: logger.error + raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI") + + + def ldap_authenticate(resource, req, resp, *args, **kwargs): + """ + Authenticate against LDAP with WWW Basic Auth credentials + """ + + if optional and not req.get_param_as_bool("authenticate"): + return func(resource, req, resp, *args, **kwargs) + + import ldap + + if not req.auth: + resp.append_header("WWW-Authenticate", "Basic") + raise falcon.HTTPUnauthorized("Forbidden", + "Please authenticate with %s domain account or supply UPN" % constants.DOMAIN) + + if not req.auth.startswith("Basic "): + raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth) + + from base64 import b64decode + basic, token = req.auth.split(" ", 1) + user, passwd = b64decode(token).split(":", 1) + + if "ldap_conn" not in req.context: + for server in config.LDAP_SERVERS: + click.echo("Connecting to %s as %s" % (server, user)) + conn = ldap.initialize(server) + conn.set_option(ldap.OPT_REFERRALS, 0) + try: + conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, constants.DOMAIN), passwd) + except ldap.LDAPError, e: + resp.append_header("WWW-Authenticate", "Basic") + logger.debug("Failed to authenticate with user '%s'", user) + raise falcon.HTTPUnauthorized("Forbidden", + "Please authenticate with %s domain account or supply UPN" % constants.DOMAIN) + + req.context["ldap_conn"] = conn + break + else: + raise ValueError("No LDAP servers!") + + req.context["user"] = User(user) + req.context["groups"] = set() + return account_info(func)(resource, req, resp, *args, **kwargs) + + + def pam_authenticate(resource, req, resp, *args, **kwargs): + """ + Authenticate against PAM with WWW Basic Auth credentials + """ + + if optional and not req.get_param_as_bool("authenticate"): + return func(resource, req, resp, *args, **kwargs) + + if not req.auth: + resp.append_header("WWW-Authenticate", "Basic") + raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate") + + if not req.auth.startswith("Basic "): + raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth) + + from base64 import b64decode + basic, token = req.auth.split(" ", 1) + user, passwd = b64decode(token).split(":", 1) + + import simplepam + if not simplepam.authenticate(user, passwd, "sshd"): + raise falcon.HTTPUnauthorized("Forbidden", "Invalid password") + + req.context["user"] = User(user) + req.context["groups"] = set() + return account_info(func)(resource, req, resp, *args, **kwargs) + + if config.AUTHENTICATION_BACKENDS == {"kerberos"}: + return kerberos_authenticate + elif config.AUTHENTICATION_BACKENDS == {"pam"}: + return pam_authenticate + elif config.AUTHENTICATION_BACKENDS == {"ldap"}: + return ldap_authenticate + else: + raise NotImplementedError("Authentication backend %s not supported" % config.AUTHENTICATION_BACKENDS) + return wrapper + + +def login_required(func): + return authenticate()(func) + + +def login_optional(func): + return authenticate(optional=True)(func) def authorize_admin(func): - def wrapped(self, req, resp, *args, **kwargs): - from certidude import config - # Parse remote IPv4/IPv6 address - remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"].decode("utf-8")) - - # Check for administration subnet whitelist - print("Comparing:", config.ADMIN_SUBNETS, "To:", remote_addr) - for subnet in config.ADMIN_SUBNETS: - if subnet.overlaps(remote_addr): - break - else: - logger.info("Rejected access to administrative call %s by %s from %s, source address not whitelisted", req.env["PATH_INFO"], req.context["user"], remote_addr) - raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % remote_addr) + def whitelist_authorize(resource, req, resp, *args, **kwargs): # Check for username whitelist - if req.context.get("user") not in config.ADMIN_USERS: - logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted", req.env["PATH_INFO"], req.context["user"], remote_addr) + if not req.context.get("user") or req.context.get("user") not in config.ADMIN_WHITELIST: + logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted", + req.env["PATH_INFO"], req.context.get("user"), req.context.get("remote_addr")) raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % req.context.get("user")) + return func(resource, req, resp, *args, **kwargs) - # Retain username, TODO: Better abstraction with username, e-mail, sn, gn? + if config.AUTHORIZATION_BACKEND == "whitelist": + return whitelist_authorize + else: + return member_of(config.ADMINS_GROUP)(func) - return func(self, req, resp, *args, **kwargs) - return wrapped diff --git a/certidude/authority.py b/certidude/authority.py index 3d0f381..815e8ea 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -5,47 +5,64 @@ import re import socket import requests from OpenSSL import crypto -from certidude import config, push +from certidude import config, push, mailer from certidude.wrappers import Certificate, Request from certidude.signer import raw_sign from certidude import errors -RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$" +RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$" # https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/ # https://jamielinux.com/docs/openssl-certificate-authority/ # http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py + +# Cache CA certificate +certificate = Certificate(open(config.AUTHORITY_CERTIFICATE_PATH)) + def publish_certificate(func): # TODO: Implement e-mail and nginx notifications using hooks def wrapped(csr, *args, **kwargs): cert = func(csr, *args, **kwargs) assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert)) + if cert.email_address: + mailer.send( + "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address), + "certificate-signed.md", + attachments=(cert,), + certificate=cert) + if config.PUSH_PUBLISH: url = config.PUSH_PUBLISH % csr.fingerprint() click.echo("Publishing certificate at %s ..." % url) requests.post(url, data=cert.dump(), headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"}) + + # For deleting request in the web view, use pubkey modulo push.publish("request-signed", csr.common_name) return cert return wrapped + def get_request(common_name): if not re.match(RE_HOSTNAME, common_name): - raise ValueError("Invalid common name") + raise ValueError("Invalid common name %s" % repr(common_name)) return Request(open(os.path.join(config.REQUESTS_DIR, common_name + ".pem"))) + def get_signed(common_name): if not re.match(RE_HOSTNAME, common_name): - raise ValueError("Invalid common name") + raise ValueError("Invalid common name %s" % repr(common_name)) return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem"))) + def get_revoked(common_name): if not re.match(RE_HOSTNAME, common_name): - raise ValueError("Invalid common name") + raise ValueError("Invalid common name %s" % repr(common_name)) return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem"))) + def store_request(buf, overwrite=False): """ Store CSR for later processing @@ -92,7 +109,7 @@ def revoke_certificate(common_name): cert = get_signed(common_name) revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number) os.rename(cert.path, revoked_filename) - push.publish("certificate-revoked", cert.fingerprint()) + push.publish("certificate-revoked", cert.common_name) def list_requests(directory=config.REQUESTS_DIR): @@ -136,39 +153,50 @@ def delete_request(common_name): raise ValueError("Invalid common name") path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") - request_sha1sum = Request(open(path)).fingerprint() + request = Request(open(path)) os.unlink(path) # Publish event at CA channel - push.publish("request-deleted", request_sha1sum) + push.publish("request-deleted", request.common_name) # Write empty certificate to long-polling URL - requests.delete(config.PUSH_PUBLISH % request_sha1sum, + requests.delete(config.PUSH_PUBLISH % request.common_name, headers={"User-Agent": "Certidude API"}) -def generate_p12_bundle(common_name): + +def generate_pkcs12_bundle(common_name, key_size=4096, owner=None): + """ + Generate private key, sign certificate and return PKCS#12 bundle + """ # Construct private key - click.echo("Generating 4096-bit RSA key...") + click.echo("Generating %d-bit RSA key..." % key_size) key = crypto.PKey() - key.generate_key(crypto.TYPE_RSA, 512) + key.generate_key(crypto.TYPE_RSA, key_size) # Construct CSR csr = crypto.X509Req() csr.set_version(2) # Corresponds to X.509v3 csr.set_pubkey(key) csr.get_subject().CN = common_name - buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr).decode("utf-8") + if owner: + if owner.given_name: + csr.get_subject().GN = owner.given_name + if owner.surname: + csr.get_subject().SN = owner.surname + csr.add_extensions([ + crypto.X509Extension("subjectAltName", True, "email:%s" % owner.mail)]) + + buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr) # Sign CSR cert = sign(Request(buf), overwrite=True) # Generate P12 - ca_certs = crypto.load_certificate(crypto.FILETYPE_PEM, open(config.AUTHORITY_CERTIFICATE_PATH).read()), p12 = crypto.PKCS12() p12.set_privatekey( key ) p12.set_certificate( cert._obj ) - p12.set_ca_certificates( ca_certs ) - return p12.export() + p12.set_ca_certificates([certificate._obj]) + return p12.export(), cert @publish_certificate @@ -187,7 +215,7 @@ def sign(req, overwrite=False, delete=True): elif req.pubkey == old_cert.pubkey: return old_cert else: - raise FileExistsError("Will not overwrite existing certificate") + raise EnvironmentError("Will not overwrite existing certificate") # Sign via signer process cert_buf = signer_exec("sign-request", req.dump()) @@ -216,9 +244,9 @@ def sign2(request, overwrite=False, delete=True, lifetime=None): path = os.path.join(config.SIGNED_DIR, request.common_name + ".pem") if os.path.exists(path): if overwrite: - revoke(request.common_name) + revoke_certificate(request.common_name) else: - raise FileExistsError("File %s already exists!" % path) + raise EnvironmentError("File %s already exists!" % path) buf = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) with open(path + ".part", "wb") as fh: diff --git a/certidude/cli.py b/certidude/cli.py index 6a161e5..b698fdd 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -3,7 +3,6 @@ import asyncore import click -import configparser import hashlib import logging import os @@ -14,11 +13,11 @@ import signal import socket import subprocess import sys -from certidude.signer import SignServer -from certidude.common import expand_paths +from configparser import ConfigParser +from certidude import constants +from certidude.common import expand_paths, ip_address, ip_network from datetime import datetime from humanize import naturaltime -from ipaddress import ip_network, ip_address from jinja2 import Environment, PackageLoader from time import sleep from setproctitle import setproctitle @@ -66,7 +65,7 @@ if os.getuid() >= 1000: def certidude_request_spawn(fork): from certidude.helpers import certidude_request_certificate - clients = configparser.ConfigParser() + clients = ConfigParser() clients.readfp(open("/etc/certidude/client.conf")) services = ConfigParser() @@ -92,7 +91,7 @@ def certidude_request_spawn(fork): os.kill(pid, signal.SIGTERM) click.echo("Terminated process %d" % pid) os.unlink(pid_path) - except (ValueError, ProcessLookupError, FileNotFoundError): + except EnvironmentError: pass if fork: @@ -137,7 +136,7 @@ def certidude_request_spawn(fork): # Set up IPsec via NetworkManager if services.get(endpoint, "service") == "network-manager/strongswan": - config = configparser.ConfigParser() + config = ConfigParser() config.add_section("connection") config.add_section("vpn") config.add_section("ipv4") @@ -218,6 +217,7 @@ def certidude_signer_spawn(kill, no_interaction): """ Spawn privilege isolated signer process """ + from certidude.signer import SignServer from certidude import config _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") @@ -254,7 +254,7 @@ def certidude_signer_spawn(kill, no_interaction): pid = int(fh.readline()) os.kill(pid, 0) click.echo("Found process with PID %d" % pid) - except (ValueError, ProcessLookupError, FileNotFoundError): + except EnvironmentError: pid = 0 if pid > 0: @@ -265,7 +265,7 @@ def certidude_signer_spawn(kill, no_interaction): sleep(1) os.kill(pid, signal.SIGKILL) sleep(1) - except ProcessLookupError: + except EnvironmentError: pass child_pid = os.fork() @@ -280,15 +280,7 @@ def certidude_signer_spawn(kill, no_interaction): logging.basicConfig( filename="/var/log/signer.log", level=logging.INFO) - server = SignServer( - config.SIGNER_SOCKET_PATH, - config.AUTHORITY_PRIVATE_KEY_PATH, - config.AUTHORITY_CERTIFICATE_PATH, - config.CERTIFICATE_LIFETIME, - config.CERTIFICATE_BASIC_CONSTRAINTS, - config.CERTIFICATE_KEY_USAGE_FLAGS, - config.CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS, - config.REVOCATION_LIST_LIFETIME) + server = SignServer() asyncore.loop() @@ -363,8 +355,8 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co common_name, org_unit, email_address, - key_usage="nonRepudiation,digitalSignature,keyEncipherment", - extended_key_usage="serverAuth,ikeIntermediate", + key_usage="digitalSignature,keyEncipherment", + extended_key_usage="serverAuth", wait=True) if not os.path.exists(dhparam_path): @@ -375,7 +367,7 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co return retval # TODO: Add dhparam - config.write(env.get_template("openvpn-site-to-client.ovpn").render(locals())) + config.write(env.get_template("openvpn-site-to-client.ovpn").render(vars())) click.echo("Generated %s" % config.name) click.echo() @@ -385,6 +377,74 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co click.echo() +@click.command("nginx", help="Set up nginx as HTTPS server") +@click.argument("url") +@click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN) +@click.option("--org-unit", "-ou", help="Organizational unit") +@click.option("--tls-config", + default="/etc/nginx/conf.d/tls.conf", + type=click.File(mode="w", atomic=True, lazy=True), + help="TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default") +@click.option("--site-config", "-o", + default="/etc/nginx/sites-available/%s.conf" % HOSTNAME, + type=click.File(mode="w", atomic=True, lazy=True), + help="Site configuration file of nginx, /etc/nginx/sites-available/%s.conf by default" % HOSTNAME) +@click.option("--directory", "-d", default="/etc/nginx/ssl", help="Directory for keys, /etc/nginx/ssl by default") +@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME) +@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME) +@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME) +@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default") +@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to -d by default") +@click.option("--verify-client", "-vc", type=click.Choice(['optional', 'on', 'off'])) +@expand_paths() +def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, verify_client): + # TODO: Intelligent way of getting last IP address in the subnet + from certidude.helpers import certidude_request_certificate + + if not os.path.exists(certificate_path): + click.echo("As HTTPS server certificate needs specific key usage extensions please") + click.echo("use following command to sign on Certidude server instead of web interface:") + click.echo() + click.echo(" certidude sign %s" % common_name) + click.echo() + retval = certidude_request_certificate(url, key_path, request_path, + certificate_path, authority_path, common_name, org_unit, + key_usage="digitalSignature,keyEncipherment", + extended_key_usage="serverAuth", + dns = constants.FQDN, wait=True, bundle=True) + + if not os.path.exists(dhparam_path): + cmd = "openssl", "dhparam", "-out", dhparam_path, "2048" + subprocess.check_call(cmd) + + if retval: + return retval + + context = globals() # Grab constants.BLAH + context.update(locals()) + + if os.path.exists(site_config.name): + click.echo("Configuration file %s already exists, not overwriting" % site_config.name) + else: + site_config.write(env.get_template("nginx-https-site.conf").render(context)) + click.echo("Generated %s" % site_config.name) + + if os.path.exists(tls_config.name): + click.echo("Configuration file %s already exists, not overwriting" % tls_config.name) + else: + tls_config.write(env.get_template("nginx-tls.conf").render(context)) + click.echo("Generated %s" % tls_config.name) + + click.echo() + click.echo("Inspect configuration files, enable it and start nginx service:") + click.echo() + click.echo(" ln -s %s /etc/nginx/sites-enabled/%s" % ( + os.path.relpath(site_config.name, "/etc/nginx/sites-enabled"), + os.path.basename(site_config.name))) + click.secho(" service nginx restart", bold=True) + click.echo() + + @click.command("client", help="Set up OpenVPN client") @click.argument("url") @click.argument("remote") @@ -419,7 +479,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_ return retval # TODO: Add dhparam - config.write(env.get_template("openvpn-client-to-site.ovpn").render(locals())) + config.write(env.get_template("openvpn-client-to-site.ovpn").render(vars())) click.echo("Generated %s" % config.name) click.echo() @@ -435,8 +495,8 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_ @click.option("--org-unit", "-ou", help="Organizational unit") @click.option("--fqdn", "-f", default=FQDN, help="Fully qualified hostname associated with the certificate") @click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, %s by default" % EMAIL) -@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default") -@click.option("--local", "-l", default=None, type=ip_address, help="IP address associated with the certificate, none by default") +@click.option("--subnet", "-sn", default=u"192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default") +@click.option("--local", "-l", type=ip_address, help="IP address associated with the certificate, none by default") @click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed") @click.option("--config", "-o", default="/etc/ipsec.conf", @@ -473,7 +533,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email common_name, org_unit, email_address, - key_usage="nonRepudiation,digitalSignature,keyEncipherment", + key_usage="digitalSignature,keyEncipherment", extended_key_usage="serverAuth,1.3.6.1.5.5.8.2.2", ip_address=local, dns=fqdn, @@ -482,7 +542,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email if retval: return retval - config.write(env.get_template("strongswan-site-to-client.conf").render(locals())) + config.write(env.get_template("strongswan-site-to-client.conf").render(vars())) secrets.write(": RSA %s\n" % key_path) click.echo("Generated %s and %s" % (config.name, secrets.name)) @@ -539,7 +599,7 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo return retval # TODO: Add dhparam - config.write(env.get_template("strongswan-client-to-site.conf").render(locals())) + config.write(env.get_template("strongswan-client-to-site.conf").render(vars())) secrets.write(": RSA %s\n" % key_path) click.echo("Generated %s and %s" % (config.name, secrets.name)) @@ -584,7 +644,7 @@ def certidude_setup_strongswan_networkmanager(url, email_address, common_name, o csum = csummer.hexdigest() uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32] - config = configparser.ConfigParser() + config = ConfigParser() config.add_section("connection") config.add_section("vpn") config.add_section("ipv4") @@ -620,11 +680,12 @@ def certidude_setup_strongswan_networkmanager(url, email_address, common_name, o subprocess.call(("nmcli", "c", "up", "uuid", uuid)) -@click.command("production", help="Set up nginx and uwsgi") +@click.command("production", help="Set up nginx, uwsgi and cron") @click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default") @click.option("--hostname", default=HOSTNAME, help="nginx hostname, '%s' by default" % HOSTNAME) @click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Static files") @click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Specify Kerberos keytab") +@click.option("--push-server", default=None, help="Push server URL") @click.option("--nginx-config", "-n", default="/etc/nginx/nginx.conf", type=click.File(mode="w", atomic=True, lazy=True), @@ -642,19 +703,36 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw subprocess.check_call(cmd) if subprocess.call("net ads testjoin", shell=True): - click.echo("Domain membership check failed, 'net ads testjoin' returned non-zero value", stderr=True) + click.echo("Domain membership check failed, 'net ads testjoin' returned non-zero value", err=True) exit(255) if not os.path.exists(kerberos_keytab): subprocess.call("KRB5_KTNAME=FILE:" + kerberos_keytab + " net ads keytab add HTTP -P") - click.echo("Created Kerberos keytab in '%s'" % kerberos_keytab) + click.echo("Created service principal in Kerberos keytab '%s'" % kerberos_keytab) + + if os.path.exists("/etc/krb5.keytab") and os.path.exists("/etc/samba/smb.conf"): + # Fetch Kerberos ticket for system account + cp = ConfigParser() + cp.read("/etc/samba/smb.conf") + domain = cp.get("global", "realm").lower() + base = ",".join(["dc=" + j for j in domain.split(".")]) + with open("/etc/cron.hourly/certidude", "w") as fh: + fh.write("#!/bin/bash\n") + fh.write("KRB5CCNAME=/run/certidude/krb5cc-new kinit -k %s$\n" % cp.get("global", "netbios name")) + fh.write("chown certidude /run/certidude/krb5cc-new\n") + fh.write("mv /run/certidude/krb5cc-new /run/certidude/krb5cc\n") + os.chmod("/etc/cron.hourly/certidude", 0o755) + click.echo("Created /etc/cron.hourly/certidude for automatic Kerberos TGT renewal") + else: + click.echo("Warning: cronjob for Kerberos ticket renewal not created, LDAP with GSSAPI will not be available!") + if not static_path.endswith("/"): static_path += "/" - nginx_config.write(env.get_template("nginx.conf").render(locals())) + nginx_config.write(env.get_template("nginx.conf").render(vars())) click.echo("Generated: %s" % nginx_config.name) - uwsgi_config.write(env.get_template("uwsgi.ini").render(locals())) + uwsgi_config.write(env.get_template("uwsgi.ini").render(vars())) click.echo("Generated: %s" % uwsgi_config.name) if os.path.exists("/etc/uwsgi/apps-enabled/certidude.ini"): @@ -663,7 +741,7 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw click.echo("Symlinked %s -> /etc/uwsgi/apps-enabled/certidude.ini" % uwsgi_config.name) if not push_server: - click.echo("Remember to install nginx with wandenberg/nginx-push-stream-module!") + click.echo("Remember to install nchan instead of regular nginx!") @click.command("authority", help="Set up Certificate Authority in a directory") @@ -735,6 +813,9 @@ def certidude_setup_authority(parent, country, state, locality, organization, or ca.gmtime_adj_notAfter(authority_lifetime * 24 * 60 * 60) ca.set_issuer(ca.get_subject()) ca.set_pubkey(key) + + # add_extensions shall be called only once and + # there has to be only one subjectAltName! ca.add_extensions([ crypto.X509Extension( b"basicConstraints", @@ -746,7 +827,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or b"keyCertSign, cRLSign"), crypto.X509Extension( b"extendedKeyUsage", - True, + False, b"serverAuth,1.3.6.1.5.5.8.2.2"), crypto.X509Extension( b"subjectKeyIdentifier", @@ -756,21 +837,11 @@ def certidude_setup_authority(parent, country, state, locality, organization, or crypto.X509Extension( b"crlDistributionPoints", False, - crl_distribution_points.encode("ascii")) - ]) - - subject_alt_name = "email:%s" % email_address - ca.add_extensions([ + crl_distribution_points.encode("ascii")), crypto.X509Extension( b"subjectAltName", False, - subject_alt_name.encode("ascii")) - ]) - ca.add_extensions([ - crypto.X509Extension( - b"subjectAltName", - True, - ("DNS:%s" % common_name).encode("ascii")) + "DNS: %s, email: %s" % (common_name.encode("ascii"), email_address.encode("ascii"))) ]) if ocsp_responder_url: @@ -819,7 +890,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or # Set permission bits to 640 os.umask(0o137) with open(certidude_conf, "w") as fh: - fh.write(env.get_template("certidude.conf").render(locals())) + fh.write(env.get_template("certidude.conf").render(vars())) with open(ca_crt, "wb") as fh: fh.write(crypto.dump_certificate(crypto.FILETYPE_PEM, ca)) @@ -988,12 +1059,23 @@ def certidude_sign(common_name, overwrite, lifetime): click.echo("Added extension %s: %s" % (key, value)) click.echo() + @click.command("serve", help="Run built-in HTTP server") @click.option("-u", "--user", default="certidude", help="Run as user") @click.option("-p", "--port", default=80, help="Listen port") @click.option("-l", "--listen", default="0.0.0.0", help="Listen address") @click.option("-s", "--enable-signature", default=False, is_flag=True, help="Allow signing operations with private key of CA") def certidude_serve(user, port, listen, enable_signature): + from certidude import config + + click.echo("Users subnets: %s" % + ", ".join([str(j) for j in config.USER_SUBNETS])) + click.echo("Administrative subnets: %s" % + ", ".join([str(j) for j in config.ADMIN_SUBNETS])) + click.echo("Auto-sign enabled for following subnets: %s" % + ", ".join([str(j) for j in config.AUTOSIGN_SUBNETS])) + click.echo("Request submissions allowed from following subnets: %s" % + ", ".join([str(j) for j in config.REQUEST_SUBNETS])) logging.basicConfig( filename='/var/log/certidude.log', @@ -1004,13 +1086,15 @@ def certidude_serve(user, port, listen, enable_signature): from wsgiref.simple_server import make_server, WSGIServer from socketserver import ThreadingMixIn from certidude.api import certidude_app, StaticResource - from certidude import config class ThreadingWSGIServer(ThreadingMixIn, WSGIServer): pass click.echo("Listening on %s:%d" % (listen, port)) + + # TODO: Bind before dropping privileges, + # but create app (sqlite log files!) after dropping privileges app = certidude_app() app.add_sink(StaticResource(os.path.join(os.path.dirname(__file__), "static"))) @@ -1023,25 +1107,25 @@ def certidude_serve(user, port, listen, enable_signature): from jinja2.debug import make_traceback as _make_traceback "".encode("charmap") - if config.AUTHENTICATION_BACKEND == "pam": + restricted_groups = [] + + if config.AUTHENTICATION_BACKENDS == {"pam"}: # PAM needs access to /etc/shadow import grp name, passwd, gid, mem = grp.getgrnam("shadow") click.echo("Adding current user to shadow group due to PAM authentication backend") - os.setgroups([gid]) - else: - os.setgroups([]) + restricted_groups.append(gid) _, _, uid, gid, gecos, root, shell = pwd.getpwnam(user) - if uid == 0: - click.echo("Please specify unprivileged user") - exit(254) + restricted_groups.append(gid) + + os.setgroups(restricted_groups) os.setgid(gid) os.setuid(uid) click.echo("Switched to user %s (uid=%d, gid=%d); member of groups %s" % - (user, uid, gid, ", ".join([str(j) for j in os.getgroups()]))) + (user, os.getuid(), os.getgid(), ", ".join([str(j) for j in os.getgroups()]))) os.umask(0o007) elif os.getuid() == 0: @@ -1076,6 +1160,7 @@ certidude_setup.add_command(certidude_setup_openvpn) certidude_setup.add_command(certidude_setup_strongswan) certidude_setup.add_command(certidude_setup_client) certidude_setup.add_command(certidude_setup_production) +certidude_setup.add_command(certidude_setup_nginx) certidude_request.add_command(certidude_request_spawn) certidude_signer.add_command(certidude_signer_spawn) entry_point.add_command(certidude_setup) diff --git a/certidude/common.py b/certidude/common.py index 37973b2..a069cd6 100644 --- a/certidude/common.py +++ b/certidude/common.py @@ -1,6 +1,13 @@ import os import click +import ipaddress + +def ip_network(j): + return ipaddress.ip_network(unicode(j)) + +def ip_address(j): + return ipaddress.ip_address(unicode(j)) def expand_paths(): """ diff --git a/certidude/config.py b/certidude/config.py index ad5db4a..7571a48 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -4,50 +4,52 @@ import codecs import configparser import ipaddress import os -import socket import string from random import choice from urllib.parse import urlparse -FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] - cp = configparser.ConfigParser() cp.readfp(codecs.open("/etc/certidude/server.conf", "r", "utf8")) -AUTHENTICATION_BACKEND = cp.get("authentication", "backend") # kerberos, pam -AUTHORIZATION_BACKEND = cp.get("authorization", "backend") # whitelist, ldap, pam +AUTHENTICATION_BACKENDS = set([j for j in + cp.get("authentication", "backends").split(" ") if j]) # kerberos, pam, ldap +AUTHORIZATION_BACKEND = cp.get("authorization", "backend") # whitelist, ldap, posix +ACCOUNTS_BACKEND = cp.get("accounts", "backend") # posix, ldap -ADMIN_USERS = set([j for j in cp.get("authorization", "admin_users").split(" ") if j]) -ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "admin_subnets").split(" ") if j]) -AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "autosign_subnets").split(" ") if j]) -REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "request_subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS) +USER_SUBNETS = set([ipaddress.ip_network(j) for j in + cp.get("authorization", "user subnets").split(" ") if j]) +ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in + cp.get("authorization", "admin subnets").split(" ") if j]).union(USER_SUBNETS) +AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in + cp.get("authorization", "autosign subnets").split(" ") if j]) +REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in + cp.get("authorization", "request subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS) SIGNER_SOCKET_PATH = "/run/certidude/signer.sock" SIGNER_PID_PATH = "/run/certidude/signer.pid" AUTHORITY_DIR = "/var/lib/certidude" -AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private_key_path") -AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate_path") -REQUESTS_DIR = cp.get("authority", "requests_dir") -SIGNED_DIR = cp.get("authority", "signed_dir") -REVOKED_DIR = cp.get("authority", "revoked_dir") - -#LOG_DATA = cp.get("logging", "database") +AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path") +AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path") +REQUESTS_DIR = cp.get("authority", "requests dir") +SIGNED_DIR = cp.get("authority", "signed dir") +REVOKED_DIR = cp.get("authority", "revoked dir") +OUTBOX = cp.get("authority", "outbox") CERTIFICATE_BASIC_CONSTRAINTS = "CA:FALSE" -CERTIFICATE_KEY_USAGE_FLAGS = "nonRepudiation,digitalSignature,keyEncipherment" +CERTIFICATE_KEY_USAGE_FLAGS = "digitalSignature,keyEncipherment" CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS = "clientAuth" -CERTIFICATE_LIFETIME = int(cp.get("signature", "certificate_lifetime")) +CERTIFICATE_LIFETIME = int(cp.get("signature", "certificate lifetime")) -REVOCATION_LIST_LIFETIME = int(cp.get("signature", "revocation_list_lifetime")) +REVOCATION_LIST_LIFETIME = int(cp.get("signature", "revocation list lifetime")) PUSH_TOKEN = "".join([choice(string.ascii_letters + string.digits) for j in range(0,32)]) PUSH_TOKEN = "ca" try: - PUSH_EVENT_SOURCE = cp.get("push", "event_source") - PUSH_LONG_POLL = cp.get("push", "long_poll") + PUSH_EVENT_SOURCE = cp.get("push", "event source") + PUSH_LONG_POLL = cp.get("push", "long poll") PUSH_PUBLISH = cp.get("push", "publish") except configparser.NoOptionError: PUSH_SERVER = cp.get("push", "server") or "http://localhost" @@ -55,18 +57,41 @@ except configparser.NoOptionError: PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s" PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s" -o = urlparse(cp.get("authority", "database") if cp.has_option("authority", "database") else "") -if not o.scheme: - DATABASE_POOL = None -elif o.scheme == "mysql": - import mysql.connector - DATABASE_POOL = mysql.connector.pooling.MySQLConnectionPool( - pool_size = 32, - user=o.username, - password=o.password, - host=o.hostname, - database=o.path[1:]) +TAGGING_BACKEND = cp.get("tagging", "backend") +LOGGING_BACKEND = cp.get("logging", "backend") +LEASES_BACKEND = cp.get("leases", "backend") + + +if "whitelist" == AUTHORIZATION_BACKEND: + USERS_WHITELIST = set([j for j in cp.get("authorization", "users whitelist").split(" ") if j]) + ADMINS_WHITELIST = set([j for j in cp.get("authorization", "admins whitelist").split(" ") if j]) +elif "posix" == AUTHORIZATION_BACKEND: + USERS_GROUP = cp.get("authorization", "posix user group") + ADMINS_GROUP = cp.get("authorization", "posix admin group") +elif "ldap" == AUTHORIZATION_BACKEND: + USERS_GROUP = cp.get("authorization", "ldap user group") + ADMINS_GROUP = cp.get("authorization", "ldap admin group") else: - raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database is supported" % o.scheme) + raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND) + +LDAP_USER_FILTER = cp.get("authorization", "ldap user filter") +LDAP_GROUP_FILTER = cp.get("authorization", "ldap group filter") +LDAP_MEMBERS_FILTER = cp.get("authorization", "ldap members filter") +LDAP_MEMBER_OF_FILTER = cp.get("authorization", "ldap member of filter") + +for line in open("/etc/ldap/ldap.conf"): + line = line.strip().lower() + if "#" in line: + line, _ = line.split("#", 1) + if not " " in line: + continue + key, value = line.split(" ", 1) + if key == "uri": + LDAP_SERVERS = set([j for j in value.split(" ") if j]) + click.echo("LDAP servers: %s" % " ".join(LDAP_SERVERS)) + elif key == "base": + LDAP_BASE = value +else: + click.echo("No LDAP servers specified in /etc/ldap/ldap.conf") diff --git a/certidude/constants.py b/certidude/constants.py new file mode 100644 index 0000000..d04b661 --- /dev/null +++ b/certidude/constants.py @@ -0,0 +1,12 @@ + +import socket + +FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] + +if "." in FQDN: + HOSTNAME, DOMAIN = FQDN.split(".", 1) +else: + HOSTNAME, DOMAIN = FQDN, "local" + click.echo("Unable to determine domain of this computer, falling back to local") + +EXTENSION_WHITELIST = set(["subjectAltName"]) diff --git a/certidude/decorators.py b/certidude/decorators.py index 2771959..3aaac48 100644 --- a/certidude/decorators.py +++ b/certidude/decorators.py @@ -1,13 +1,39 @@ -# encoding: utf-8 - import falcon import ipaddress import json +import logging import re import types from datetime import date, time, datetime from OpenSSL import crypto from certidude.wrappers import Request, Certificate +from urllib.parse import urlparse + +logger = logging.getLogger("api") + +def csrf_protection(func): + """ + Protect resource from common CSRF attacks by checking user agent and referrer + """ + def wrapped(self, req, resp, *args, **kwargs): + # Assume curl and python-requests are used intentionally + if req.user_agent.startswith("curl/") or req.user_agent.startswith("python-requests/"): + return func(self, req, resp, *args, **kwargs) + + # For everything else assert referrer + referrer = req.headers.get("REFERER") + if referrer: + scheme, netloc, path, params, query, fragment = urlparse(referrer) + if netloc == req.host: + return func(self, req, resp, *args, **kwargs) + + # Kaboom! + logger.warning("Prevented clickbait from '%s' with user agent '%s'", + referrer or "-", req.user_agent) + raise falcon.HTTPUnauthorized("Forbidden", + "No suitable UA or referrer provided, cross-site scripting disabled") + return wrapped + def event_source(func): def wrapped(self, req, resp, *args, **kwargs): @@ -15,7 +41,6 @@ def event_source(func): resp.status = falcon.HTTP_SEE_OTHER resp.location = req.context.get("ca").push_server + "/ev/" + req.context.get("ca").uuid resp.body = "Redirecting to:" + resp.location - print("Delegating EventSource handling to:", resp.location) return func(self, req, resp, *args, **kwargs) return wrapped @@ -24,9 +49,10 @@ class MyEncoder(json.JSONEncoder): "organizational_unit", "given_name", "surname", "fqdn", "email_address", \ "key_type", "key_length", "md5sum", "sha1sum", "sha256sum", "key_usage" - CERTIFICATE_ATTRIBUTES = "revokable", "identity", "changed", "common_name", \ + CERTIFICATE_ATTRIBUTES = "revokable", "identity", "common_name", \ "organizational_unit", "given_name", "surname", "fqdn", "email_address", \ - "key_type", "key_length", "sha256sum", "serial_number", "key_usage" + "key_type", "key_length", "sha256sum", "serial_number", "key_usage", \ + "signed", "expires" def default(self, obj): if isinstance(obj, crypto.X509Name): @@ -60,18 +86,25 @@ def serialize(func): Falcon response serialization """ def wrapped(instance, req, resp, **kwargs): - assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed" - resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate"); - resp.set_header("Pragma", "no-cache"); - resp.set_header("Expires", "0"); + resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate") + resp.set_header("Pragma", "no-cache") + resp.set_header("Expires", "0") r = func(instance, req, resp, **kwargs) if resp.body is None: - if req.get_header("Accept").split(",")[0] == "application/json": + if req.accept.startswith("application/json"): resp.set_header("Content-Type", "application/json") resp.set_header("Content-Disposition", "inline") resp.body = json.dumps(r, cls=MyEncoder) + + elif hasattr(r, "content_type") and req.client_accepts(r.content_type): + resp.set_header("Content-Type", r.content_type) + resp.set_header("Content-Disposition", + ("attachment; filename=%s" % r.suggested_filename).encode("ascii")) + resp.body = r.dump() else: - resp.body = repr(r) + logger.debug("Client did not accept application/json or %s, client expected %s" % (r.content_type, req.accept)) + raise falcon.HTTPUnsupportedMediaType( + "Client did not accept application/json or %s" % r.content_type) return r return wrapped diff --git a/certidude/firewall.py b/certidude/firewall.py new file mode 100644 index 0000000..8354482 --- /dev/null +++ b/certidude/firewall.py @@ -0,0 +1,38 @@ + +import falcon +import logging + +logger = logging.getLogger("api") + +def whitelist_subnets(subnets): + """ + Validate source IP address of API call against subnet list + """ + def wrapper(func): + def wrapped(self, req, resp, *args, **kwargs): + # Check for administration subnet whitelist + for subnet in subnets: + if req.context.get("remote_addr") in subnet: + break + else: + logger.info("Rejected access to administrative call %s by %s from %s, source address not whitelisted", + req.env["PATH_INFO"], + req.context.get("user", "unauthenticated user"), + req.context.get("remote_addr")) + raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % remote_addr) + + return func(self, req, resp, *args, **kwargs) + return wrapped + return wrapper + +def whitelist_content_types(*content_types): + def wrapper(func): + def wrapped(self, req, resp, *args, **kwargs): + for content_type in content_types: + if req.get_header("Content-Type") == content_type: + return func(self, req, resp, *args, **kwargs) + raise falcon.HTTPUnsupportedMediaType( + "This API call accepts only %s content type" % ", ".join(content_types)) + return wrapped + return wrapper + diff --git a/certidude/helpers.py b/certidude/helpers.py index af2a782..35ba5fc 100644 --- a/certidude/helpers.py +++ b/certidude/helpers.py @@ -6,7 +6,7 @@ from certidude import errors from certidude.wrappers import Certificate, Request from OpenSSL import crypto -def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None): +def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None, bundle=False): """ Exchange CSR for certificate using Certidude HTTP API server """ @@ -41,7 +41,8 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, click.echo("Attempting to fetch CA certificate from %s" % authority_url) try: - r = requests.get(authority_url) + r = requests.get(authority_url, + headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"}) cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text) except crypto.Error: raise ValueError("Failed to parse PEM: %s" % r.text) @@ -53,7 +54,7 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, try: request = Request(open(request_path)) click.echo("Found signing request: %s" % request_path) - except FileNotFoundError: + except EnvironmentError: # Construct private key click.echo("Generating 4096-bit RSA key...") @@ -69,10 +70,11 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, csr = crypto.X509Req() csr.set_version(2) # Corresponds to X.509v3 csr.set_pubkey(key) + csr.get_subject().CN = common_name + request = Request(csr) # Set subject attributes - request.common_name = common_name if given_name: request.given_name = given_name if surname: @@ -83,20 +85,20 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, # Collect subject alternative names subject_alt_name = set() if email_address: - subject_alt_name.add("email:" + email_address) + subject_alt_name.add("email:%s" % email_address) if ip_address: - subject_alt_name.add("IP:" + ip_address) + subject_alt_name.add("IP:%s" % ip_address) if dns: - subject_alt_name.add("DNS:" + dns) + subject_alt_name.add("DNS:%s" % dns) # Set extensions extensions = [] if key_usage: extensions.append(("keyUsage", key_usage, True)) if extended_key_usage: - extensions.append(("extendedKeyUsage", extended_key_usage, True)) + extensions.append(("extendedKeyUsage", extended_key_usage, False)) if subject_alt_name: - extensions.append(("subjectAltName", ", ".join(subject_alt_name), True)) + extensions.append(("subjectAltName", ", ".join(subject_alt_name), False)) request.set_extensions(extensions) # Dump CSR @@ -113,7 +115,7 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, click.echo("Submitting to %s, waiting for response..." % request_url) submission = requests.post(request_url, data=open(request_path), - headers={"User-Agent": "Certidude", "Content-Type": "application/pkcs10", "Accept": "application/x-x509-user-cert"}) + headers={"Content-Type": "application/pkcs10", "Accept": "application/x-x509-user-cert,application/x-pem-file"}) if submission.status_code == requests.codes.ok: pass @@ -131,12 +133,18 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, try: cert = crypto.load_certificate(crypto.FILETYPE_PEM, submission.text) except crypto.Error: - raise ValueError("Failed to parse PEM: %s" % buf) + raise ValueError("Failed to parse PEM: %s" % submission.text) os.umask(0o022) with open(certificate_path + ".part", "w") as fh: + # Dump certificate fh.write(submission.text) + # Bundle CA certificate, necessary for nginx + if bundle: + with open(authority_path) as ch: + fh.write(ch.read()) + click.echo("Writing certificate to: %s" % certificate_path) os.rename(certificate_path + ".part", certificate_path) diff --git a/certidude/mailer.py b/certidude/mailer.py index df21f71..6fd1b6e 100644 --- a/certidude/mailer.py +++ b/certidude/mailer.py @@ -1,104 +1,90 @@ import os import smtplib -from time import sleep +from markdown import markdown from jinja2 import Environment, PackageLoader from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText +from email.mime.base import MIMEBase from urllib.parse import urlparse -class Mailer(object): - def __init__(self, url): - scheme, netloc, path, params, query, fragment = urlparse(url) - scheme = scheme.lower() +env = Environment(loader=PackageLoader("certidude", "templates/mail")) - if path: - raise ValueError("Path for URL not supported") - if params: - raise ValueError("Parameters for URL not supported") - if query: - raise ValueError("Query for URL not supported") - if fragment: - raise ValueError("Fragment for URL not supported") +def send(recipients, template, attachments=(), **context): + from certidude import authority, config + if not config.OUTBOX: + # Mailbox disabled, don't send e-mail + return + + if not recipients: + raise ValueError("No e-mail recipients specified!") + + scheme, netloc, path, params, query, fragment = urlparse(config.OUTBOX) + scheme = scheme.lower() + + if path: + raise ValueError("Path for URL not supported") + if params: + raise ValueError("Parameters for URL not supported") + if query: + raise ValueError("Query for URL not supported") + if fragment: + raise ValueError("Fragment for URL not supported") - self.username = None - self.password = "" + username = None + password = "" - if scheme == "smtp": - self.secure = False - self.port = 25 - elif scheme == "smtps": - self.secure = True - self.port = 465 + if scheme == "smtp": + secure = False + port = 25 + elif scheme == "smtps": + secure = True + port = 465 + else: + raise ValueError("Unknown scheme '%s', currently SMTP and SMTPS are only supported" % scheme) + + if "@" in netloc: + credentials, netloc = netloc.split("@") + + if ":" in credentials: + username, password = credentials.split(":") else: - raise ValueError("Unknown scheme '%s', currently SMTP and SMTPS are only supported" % scheme) + username = credentials - if "@" in netloc: - credentials, netloc = netloc.split("@") - - if ":" in credentials: - self.username, self.password = credentials.split(":") - else: - self.username = credentials - - if ":" in netloc: - self.server, port_str = netloc.split(":") - self.port = int(port_str) - else: - self.server = netloc - - self.env = Environment(loader=PackageLoader("certidude", "email_templates")) - self.conn = None - - def reconnect(self): - # Gmail employs some sort of IPS - # https://accounts.google.com/DisplayUnlockCaptcha - print("Connecting to:", self.server, self.port) - self.conn = smtplib.SMTP(self.server, self.port) - if self.secure: - self.conn.starttls() - if self.username and self.password: - self.conn.login(self.username, self.password) - - def enqueue(self, sender, recipients, subject, template, **context): - self.send(sender, recipients, subject, template, **context) + if ":" in netloc: + server, port_str = netloc.split(":") + port = int(port_str) + else: + server = netloc - def send(self, sender, recipients, subject, template, **context): + subject, text = env.get_template(template).render(context).split("\n\n", 1) + html = markdown(text) - recipients = [j for j in recipients if j] + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = authority.certificate.email_address + msg["To"] = recipients - if not recipients: - print("No recipients to send e-mail to!") - return - print("Sending e-mail to:", recipients, "body follows:") + part1 = MIMEText(text, "plain") + part2 = MIMEText(html, "html") - msg = MIMEMultipart("alternative") - msg["Subject"] = subject - msg["From"] = sender - msg["To"] = ", ".join(recipients) + msg.attach(part1) + msg.attach(part2) - text = self.env.get_template(template + ".txt").render(context) - html = self.env.get_template(template + ".html").render(context) + for attachment in attachments: + part = MIMEBase(*attachment.content_type.split("/")) + part.add_header('Content-Disposition', 'attachment', filename=attachment.suggested_filename) + part.set_payload(attachment.dump()) + msg.attach(part) - print(text) + # Gmail employs some sort of IPS + # https://accounts.google.com/DisplayUnlockCaptcha + conn = smtplib.SMTP(server, port) + if secure: + conn.starttls() + if username and password: + conn.login(username, password) - part1 = MIMEText(text, "plain") - part2 = MIMEText(html, "html") - - msg.attach(part1) - msg.attach(part2) - - backoff = 1 - while True: - try: - if not self.conn: - self.reconnect() - self.conn.sendmail(sender, recipients, msg.as_string()) - return - except smtplib.SMTPServerDisconnected: - print("Connection to %s unexpectedly closed, probably TCP timeout, backing off for %d second" % (self.server, backoff)) - self.reconnect() - backoff = backoff * 2 - sleep(backoff) + conn.sendmail(authority.certificate.email_address, recipients, msg.as_string()) diff --git a/certidude/mysqllog.py b/certidude/mysqllog.py index d75717b..e8d3083 100644 --- a/certidude/mysqllog.py +++ b/certidude/mysqllog.py @@ -1,34 +1,17 @@ import logging import time +from certidude.api.tag import RelationalMixin -class MySQLLogHandler(logging.Handler): +class LogHandler(logging.Handler, RelationalMixin): + SQL_CREATE_TABLES = "log_tables.sql" - SQL_CREATE_TABLE = """CREATE TABLE IF NOT EXISTS log( - created datetime, facility varchar(30), level int, - severity varchar(10), message text, module varchar(20), - func varchar(20), lineno int, exception text, process int, - thread text, thread_name text)""" - - SQL_INSERT_ENTRY = """insert into log( created, facility, level, severity, - message, module, func, lineno, exception, process, thread, - thread_name) values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s); - """ - - def __init__(self, pool): + def __init__(self, uri): logging.Handler.__init__(self) - self.pool = pool - conn = self.pool.get_connection() - cur = conn.cursor() - cur.execute(self.SQL_CREATE_TABLE) - conn.commit() - cur.close() - conn.close() - + RelationalMixin.__init__(self, uri) + def emit(self, record): - conn = self.pool.get_connection() - cur = conn.cursor() - cur.execute(self.SQL_INSERT_ENTRY, ( + self.sql_execute("log_insert_entry.sql", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created)), record.name, record.levelno, @@ -39,7 +22,4 @@ class MySQLLogHandler(logging.Handler): logging._defaultFormatter.formatException(record.exc_info) if record.exc_info else "", record.process, record.thread, - record.threadName)) - conn.commit() - cur.close() - conn.close() + record.threadName) diff --git a/certidude/push.py b/certidude/push.py index a06d4bf..e5f0b2b 100644 --- a/certidude/push.py +++ b/certidude/push.py @@ -1,7 +1,9 @@ import click import json +import logging import requests +from datetime import datetime from certidude import config @@ -9,13 +11,29 @@ def publish(event_type, event_data): """ Publish event on push server """ - if not isinstance(event_data, str): + if not isinstance(event_data, basestring): from certidude.decorators import MyEncoder event_data = json.dumps(event_data, cls=MyEncoder) - notification = requests.post( - config.PUSH_PUBLISH % config.PUSH_TOKEN, - data=event_data, - headers={"X-EventSource-Event": event_type, "User-Agent": "Certidude API"}) + url = config.PUSH_PUBLISH % config.PUSH_TOKEN + click.echo("Publishing %s event %s on %s" % (event_type, event_data, url)) + try: + notification = requests.post( + url, + data=event_data, + headers={"X-EventSource-Event": event_type, "User-Agent": "Certidude API"}) + except requests.exceptions.ConnectionError: + click.echo("Failed to submit event to push server: %s" % repr(event_data)) + +class PushLogHandler(logging.Handler): + """ + To be used with Python log handling framework for publishing log entries + """ + def emit(self, record): + from certidude.push import publish + publish("log-entry", dict( + created = datetime.fromtimestamp(record.created), + message = record.msg % record.args, + severity = record.levelname.lower())) diff --git a/certidude/relational.py b/certidude/relational.py new file mode 100644 index 0000000..18b2b68 --- /dev/null +++ b/certidude/relational.py @@ -0,0 +1,103 @@ + + +import click +import re +import os +from urllib.parse import urlparse + +SCRIPTS = {} + +class RelationalMixin(object): + """ + Thin wrapper around SQLite and MySQL database connectors + """ + + SQL_CREATE_TABLES = "" + + def __init__(self, uri): + self.uri = urlparse(uri) + if self.SQL_CREATE_TABLES and self.SQL_CREATE_TABLES not in SCRIPTS: + conn = self.sql_connect() + cur = conn.cursor() + with open(self.sql_resolve_script(self.SQL_CREATE_TABLES)) as fh: + click.echo("Executing: %s" % fh.name) + if self.uri.scheme == "sqlite": + cur.executescript(fh.read()) + else: + cur.execute(fh.read()) + conn.commit() + cur.close() + conn.close() + + + def sql_connect(self): + if self.uri.scheme == "mysql": + import mysql.connector + return mysql.connector.connect( + user=self.uri.username, + password=self.uri.password, + host=self.uri.hostname, + database=self.uri.path[1:]) + elif self.uri.scheme == "sqlite": + if self.uri.netloc: + raise ValueError("Malformed database URI %s" % self.uri) + import sqlite3 + return sqlite3.connect(self.uri.path) + else: + raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database or sqlite:///path/to/database.sqlite is supported" % o.scheme) + + + def sql_resolve_script(self, filename): + return os.path.realpath(os.path.join(os.path.dirname(__file__), + "sql", self.uri.scheme, filename)) + + + def sql_load(self, filename): + if filename in SCRIPTS: + return SCRIPTS[filename] + + fh = open(self.sql_resolve_script(filename)) + click.echo("Caching SQL script: %s" % fh.name) + buf = re.sub("\s*\n\s*", " ", fh.read()) + SCRIPTS[filename] = buf + fh.close() + return buf + + + def sql_execute(self, script, *args): + conn = self.sql_connect() + cursor = conn.cursor() + click.echo("Executing %s with %s" % (script, args)) + cursor.execute(self.sql_load(script), args) + rowid = cursor.lastrowid + conn.commit() + cursor.close() + conn.close() + return rowid + + + def iterfetch(self, query, *args): + conn = self.sql_connect() + cursor = conn.cursor() + cursor.execute(query, args) + cols = [j[0] for j in cursor.description] + def g(): + for row in cursor: + yield dict(zip(cols, row)) + cursor.close() + conn.close() + return tuple(g()) + + + def sql_fetchone(self, query, *args): + conn = self.sql_connect() + cursor = conn.cursor() + cursor.execute(query, args) + cols = [j[0] for j in cursor.description] + + for row in cursor: + r = dict(zip(cols, row)) + cursor.close() + conn.close() + return r + return None diff --git a/certidude/signer.py b/certidude/signer.py index 55f0d9d..d569232 100644 --- a/certidude/signer.py +++ b/certidude/signer.py @@ -6,6 +6,7 @@ import socket import os import asyncore import asynchat +from certidude import constants, config from datetime import datetime from OpenSSL import crypto @@ -26,86 +27,83 @@ certificate authoirty (basicConstraints=CA:TRUE) or TLS server certificates (extendedKeyUsage=serverAuth). """ -EXTENSION_WHITELIST = set(["subjectAltName"]) - def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usage=None, extended_key_usage=None): - """ - Sign certificate signing request directly with private key assuming it's readable by the process - """ + """ + Sign certificate signing request directly with private key assuming it's readable by the process + """ - # Initialize X.509 certificate object - cert = crypto.X509() - cert.set_version(2) # This corresponds to X.509v3 + # Initialize X.509 certificate object + cert = crypto.X509() + cert.set_version(2) # This corresponds to X.509v3 - # Set public key - cert.set_pubkey(request.get_pubkey()) + # Set public key + cert.set_pubkey(request.get_pubkey()) - # Set issuer - cert.set_issuer(ca_cert.get_subject()) + # Set issuer + cert.set_issuer(ca_cert.get_subject()) - # TODO: Assert openssl.cnf policy for subject attributes -# if request.get_subject().O != ca_cert.get_subject().O: -# raise ValueError("Orgnization name mismatch!") -# if request.get_subject().C != ca_cert.get_subject().C: -# raise ValueError("Country mismatch!") + # Copy attributes from CA + if ca_cert.get_subject().C: + cert.get_subject().C = ca_cert.get_subject().C + if ca_cert.get_subject().ST: + cert.get_subject().ST = ca_cert.get_subject().ST + if ca_cert.get_subject().L: + cert.get_subject().L = ca_cert.get_subject().L + if ca_cert.get_subject().O: + cert.get_subject().O = ca_cert.get_subject().O - # Copy attributes from CA - if ca_cert.get_subject().C: - cert.get_subject().C = ca_cert.get_subject().C - if ca_cert.get_subject().ST: - cert.get_subject().ST = ca_cert.get_subject().ST - if ca_cert.get_subject().L: - cert.get_subject().L = ca_cert.get_subject().L - if ca_cert.get_subject().O: - cert.get_subject().O = ca_cert.get_subject().O + # Copy attributes from request + cert.get_subject().CN = request.get_subject().CN - # Copy attributes from request - cert.get_subject().CN = request.get_subject().CN - req_subject = request.get_subject() - if hasattr(req_subject, "OU") and req_subject.OU: - cert.get_subject().OU = req_subject.OU + if request.get_subject().SN: + cert.get_subject().SN = request.get_subject().SN + if request.get_subject().GN: + cert.get_subject().GN = request.get_subject().GN - # Copy e-mail, key usage, extended key from request - for extension in request.get_extensions(): - cert.add_extensions([extension]) + if request.get_subject().OU: + cert.get_subject().OU = req_subject.OU - # TODO: Set keyUsage and extendedKeyUsage defaults if none has been provided in the request + # Copy e-mail, key usage, extended key from request + for extension in request.get_extensions(): + cert.add_extensions([extension]) - # Override basic constraints if nececssary - if basic_constraints: + # TODO: Set keyUsage and extendedKeyUsage defaults if none has been provided in the request + + # Override basic constraints if nececssary + if basic_constraints: + cert.add_extensions([ + crypto.X509Extension( + b"basicConstraints", + True, + basic_constraints.encode("ascii"))]) + + if key_usage: + try: cert.add_extensions([ crypto.X509Extension( - b"basicConstraints", + b"keyUsage", True, - basic_constraints.encode("ascii"))]) + key_usage.encode("ascii"))]) + except crypto.Error: + raise ValueError("Invalid value '%s' for keyUsage attribute" % key_usage) - if key_usage: - try: - cert.add_extensions([ - crypto.X509Extension( - b"keyUsage", - True, - key_usage.encode("ascii"))]) - except crypto.Error: - raise ValueError("Invalid value '%s' for keyUsage attribute" % key_usage) + if extended_key_usage: + cert.add_extensions([ + crypto.X509Extension( + b"extendedKeyUsage", + True, + extended_key_usage.encode("ascii"))]) - if extended_key_usage: - cert.add_extensions([ - crypto.X509Extension( - b"extendedKeyUsage", - True, - extended_key_usage.encode("ascii"))]) + # Set certificate lifetime + cert.gmtime_adj_notBefore(-3600) + cert.gmtime_adj_notAfter(lifetime * 24 * 60 * 60) - # Set certificate lifetime - cert.gmtime_adj_notBefore(-3600) - cert.gmtime_adj_notAfter(lifetime * 24 * 60 * 60) - - # Generate serial from 0x10000000000000000000 to 0xffffffffffffffffffff - cert.set_serial_number(random.randint( - 0x1000000000000000000000000000000000000000, - 0xffffffffffffffffffffffffffffffffffffffff)) - cert.sign(private_key, 'sha1') - return cert + # Generate serial from 0x10000000000000000000 to 0xffffffffffffffffffff + cert.set_serial_number(random.randint( + 0x1000000000000000000000000000000000000000, + 0xffffffffffffffffffffffffffffffffffffffff)) + cert.sign(private_key, 'sha256') + return cert class SignHandler(asynchat.async_chat): @@ -128,7 +126,7 @@ class SignHandler(asynchat.async_chat): serial_number, timestamp = line.split(":") # TODO: Assert serial against regex revocation = crypto.Revoked() - revocation.set_rev_date(datetime.fromtimestamp(int(timestamp)).strftime("%Y%m%d%H%M%SZ").encode("ascii")) + revocation.set_rev_date(datetime.utcfromtimestamp(int(timestamp)).strftime("%Y%m%d%H%M%SZ").encode("ascii")) revocation.set_reason(b"keyCompromise") revocation.set_serial(serial_number.encode("ascii")) crl.add_revoked(revocation) @@ -137,7 +135,7 @@ class SignHandler(asynchat.async_chat): self.server.certificate, self.server.private_key, crypto.FILETYPE_PEM, - self.server.revocation_list_lifetime)) + config.REVOCATION_LIST_LIFETIME)) elif cmd == "ocsp-request": NotImplemented # TODO: Implement OCSP @@ -147,7 +145,7 @@ class SignHandler(asynchat.async_chat): for e in request.get_extensions(): key = e.get_short_name().decode("ascii") - if key not in EXTENSION_WHITELIST: + if key not in constants.EXTENSION_WHITELIST: raise ValueError("Certificte Signing Request contains extension '%s' which is not whitelisted" % key) # TODO: Potential exploits during PEM parsing? @@ -155,10 +153,10 @@ class SignHandler(asynchat.async_chat): self.server.private_key, self.server.certificate, request, - basic_constraints=self.server.basic_constraints, - key_usage=self.server.key_usage, - extended_key_usage=self.server.extended_key_usage, - lifetime=self.server.lifetime) + basic_constraints=config.CERTIFICATE_BASIC_CONSTRAINTS, + key_usage=config.CERTIFICATE_KEY_USAGE_FLAGS, + extended_key_usage=config.CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS, + lifetime=config.CERTIFICATE_LIFETIME) self.send(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) else: raise NotImplementedError("Unknown command: %s" % cmd) @@ -175,26 +173,23 @@ class SignHandler(asynchat.async_chat): class SignServer(asyncore.dispatcher): - def __init__(self, socket_path, private_key, certificate, lifetime, basic_constraints, key_usage, extended_key_usage, revocation_list_lifetime): + def __init__(self): asyncore.dispatcher.__init__(self) # Bind to sockets - if os.path.exists(socket_path): - os.unlink(socket_path) + if os.path.exists(config.SIGNER_SOCKET_PATH): + os.unlink(config.SIGNER_SOCKET_PATH) os.umask(0o007) self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) - self.bind(socket_path) + self.bind(config.SIGNER_SOCKET_PATH) self.listen(5) - # Load CA private key and certificate - self.private_key = crypto.load_privatekey(crypto.FILETYPE_PEM, open(private_key).read()) - self.certificate = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate).read()) - self.lifetime = lifetime - self.revocation_list_lifetime = revocation_list_lifetime - self.basic_constraints = basic_constraints - self.key_usage = key_usage - self.extended_key_usage = extended_key_usage + # Load CA private key and certificate + self.private_key = crypto.load_privatekey(crypto.FILETYPE_PEM, + open(config.AUTHORITY_PRIVATE_KEY_PATH).read()) + self.certificate = crypto.load_certificate(crypto.FILETYPE_PEM, + open(config.AUTHORITY_CERTIFICATE_PATH).read()) # Perhaps perform chroot as well, currently results in # (:[('random number generator', 'SSLEAY_RAND_BYTES', 'PRNG not seeded') diff --git a/certidude/sql/mysql/log_insert_entry.sql b/certidude/sql/mysql/log_insert_entry.sql new file mode 100644 index 0000000..92f1ae9 --- /dev/null +++ b/certidude/sql/mysql/log_insert_entry.sql @@ -0,0 +1,27 @@ +insert into log ( + created, + facility, + level, + severity, + message, + module, + func, + lineno, + exception, + process, + thread, + thread_name +) values ( + %s, + %s, + %s, + %s, + %s, + %s, + %s, + %s, + %s, + %s, + %s, + %s +); diff --git a/certidude/sql/mysql/log_tables.sql b/certidude/sql/mysql/log_tables.sql new file mode 100644 index 0000000..f5dba49 --- /dev/null +++ b/certidude/sql/mysql/log_tables.sql @@ -0,0 +1,14 @@ +create table if not exists log ( + created datetime, + facility varchar(30), + level int, + severity varchar(10), + message text, + module varchar(20), + func varchar(20), + lineno int, + exception text, + process int, + thread text, + thread_name text +) diff --git a/certidude/sql/mysql/tag_insert.sql b/certidude/sql/mysql/tag_insert.sql new file mode 100644 index 0000000..3f69567 --- /dev/null +++ b/certidude/sql/mysql/tag_insert.sql @@ -0,0 +1,9 @@ +insert into tag ( + `cn`, + `key`, + `value` +) values ( + %s, + %s, + %s +) diff --git a/certidude/sql/mysql/tag_tables.sql b/certidude/sql/mysql/tag_tables.sql new file mode 100644 index 0000000..e69de29 diff --git a/certidude/sql/sqlite/log_insert_entry.sql b/certidude/sql/sqlite/log_insert_entry.sql new file mode 100644 index 0000000..5cec55a --- /dev/null +++ b/certidude/sql/sqlite/log_insert_entry.sql @@ -0,0 +1,27 @@ +insert into log ( + created, + facility, + level, + severity, + message, + module, + func, + lineno, + exception, + process, + thread, + thread_name +) values ( + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ? +); diff --git a/certidude/sql/sqlite/log_tables.sql b/certidude/sql/sqlite/log_tables.sql new file mode 100644 index 0000000..f5dba49 --- /dev/null +++ b/certidude/sql/sqlite/log_tables.sql @@ -0,0 +1,14 @@ +create table if not exists log ( + created datetime, + facility varchar(30), + level int, + severity varchar(10), + message text, + module varchar(20), + func varchar(20), + lineno int, + exception text, + process int, + thread text, + thread_name text +) diff --git a/certidude/sql/sqlite/tag_delete.sql b/certidude/sql/sqlite/tag_delete.sql new file mode 100644 index 0000000..b18e077 --- /dev/null +++ b/certidude/sql/sqlite/tag_delete.sql @@ -0,0 +1,3 @@ +delete from tag +where id = ? +limit 1 diff --git a/certidude/sql/sqlite/tag_insert.sql b/certidude/sql/sqlite/tag_insert.sql new file mode 100644 index 0000000..4459216 --- /dev/null +++ b/certidude/sql/sqlite/tag_insert.sql @@ -0,0 +1,9 @@ +insert into tag ( + `cn`, + `key`, + `value` +) values ( + ?, + ?, + ? +); diff --git a/certidude/sql/sqlite/tag_list.sql b/certidude/sql/sqlite/tag_list.sql new file mode 100644 index 0000000..c2ae228 --- /dev/null +++ b/certidude/sql/sqlite/tag_list.sql @@ -0,0 +1,16 @@ +select + device_tag.id as `id`, + tag.key as `key`, + tag.value as `value`, + device.cn as `cn` +from + device_tag +join + tag +on + device_tag.tag_id = tag.id +join + device +on + device_tag.device_id = device.id + diff --git a/certidude/sql/sqlite/tag_tables.sql b/certidude/sql/sqlite/tag_tables.sql new file mode 100644 index 0000000..98f72ed --- /dev/null +++ b/certidude/sql/sqlite/tag_tables.sql @@ -0,0 +1,36 @@ +create table if not exists `tag` ( + `id` integer primary key, + `cn` varchar(255) not null, + `key` varchar(255) not null, + `value` varchar(255) not null +); + +create table if not exists `tag_properties` ( + `id` integer primary key, + `tag_key` varchar(255) not null, + `tag_value` varchar(255) not null, + `property_key` varchar(255) not null, + `property_value` varchar(255) not null +); + +/* + +create table if not exists `device_tag` ( + `id` int(11) not null, + `device_id` varchar(45) not null, + `tag_id` varchar(45) not null, + `attached` timestamp null default current_timestamp, + primary key (`id`) +); + +create table if not exists `device` ( + `id` int(11) not null, + `created` timestamp not null default current_timestamp, + `cn` varchar(255) not null, + `product_model` varchar(50) not null, + `product_serial` varchar(50) default null, + `hardware_address` varchar(17) unique not null, + primary key (`id`) +); + +*/ diff --git a/certidude/sql/sqlite/tag_update.sql b/certidude/sql/sqlite/tag_update.sql new file mode 100644 index 0000000..84b9660 --- /dev/null +++ b/certidude/sql/sqlite/tag_update.sql @@ -0,0 +1,4 @@ +update `tag` +set `value` = ? +where `id` = ? +limit 1 diff --git a/certidude/static/css/style.css b/certidude/static/css/style.css index 7851d6a..4c9517a 100644 --- a/certidude/static/css/style.css +++ b/certidude/static/css/style.css @@ -29,12 +29,6 @@ img { max-height: 100%; } -ul { - list-style: none; - margin: 1em 0; - padding: 0; -} - #pending_requests .notify { display: none; } @@ -142,7 +136,17 @@ pre { margin: 0 auto; } -#container li { +#signed ul, +#requests ul, +#log ul { + list-style: none; + margin: 1em 0; + padding: 0; +} + +#signed li, +#requests li, +#log li { margin: 4px 0; padding: 4px 0; clear: both; @@ -164,7 +168,8 @@ pre { .icon{ background-size: 24px; - padding-left: 36px; + background-position: 6px 2px; + padding-left: 32px; background-repeat: no-repeat; display: block; vertical-align: text-bottom; @@ -172,7 +177,7 @@ pre { } #log_entries li span.icon { - background-size: 32px; + background-size: 24px; padding-left: 42px; padding-top: 2px; padding-bottom: 2px; @@ -180,7 +185,8 @@ pre { .tags .tag { display: inline; - background-size: 32px; + background-size: 24px; + background-position: 0 4px; padding-top: 4px; padding-bottom: 4px; padding-right: 1em; @@ -199,25 +205,25 @@ select { } -.icon.tag { background-image: url("../img/iconmonstr-tag-2-icon.svg"); } +.icon.tag { background-image: url("../img/iconmonstr-tag-3.svg"); } -.icon.critical { background-image: url("../img/iconmonstr-error-4-icon.svg"); } -.icon.error { background-image: url("../img/iconmonstr-error-4-icon.svg"); } -.icon.warning { background-image: url("../img/iconmonstr-warning-6-icon.svg"); } -.icon.info { background-image: url("../img/iconmonstr-info-6-icon.svg"); } +.icon.critical { background-image: url("../img/iconmonstr-error-4.svg"); } +.icon.error { background-image: url("../img/iconmonstr-error-4.svg"); } +.icon.warning { background-image: url("../img/iconmonstr-warning-8.svg"); } +.icon.info { background-image: url("../img/iconmonstr-info-8.svg"); } -.icon.revoke { background-image: url("../img/iconmonstr-x-mark-5-icon.svg"); } -.icon.download { background-image: url("../img/iconmonstr-download-12-icon.svg"); } -.icon.sign { background-image: url("../img/iconmonstr-pen-10-icon.svg"); } -.icon.search { background-image: url("../img/iconmonstr-magnifier-4-icon.svg"); } +.icon.revoke { background-image: url("../img/iconmonstr-x-mark-8.svg"); } +.icon.download { background-image: url("../img/iconmonstr-download-12.svg"); } +.icon.sign { background-image: url("../img/iconmonstr-pen-14.svg"); } +.icon.search { background-image: url("../img/iconmonstr-magnifier-4.svg"); } -.icon.phone { background-image: url("../img/iconmonstr-mobile-phone-6-icon.svg"); } -.icon.location { background-image: url("../img/iconmonstr-compass-7-icon.svg"); } -.icon.room { background-image: url("../img/iconmonstr-home-4-icon.svg"); } -.icon.serial { background-image: url("../img/iconmonstr-barcode-4-icon.svg"); } +.icon.phone { background-image: url("../img/iconmonstr-mobile-phone-7.svg"); } +.icon.location { background-image: url("../img/iconmonstr-compass-7.svg"); } +.icon.room { background-image: url("../img/iconmonstr-home-7.svg"); } +.icon.serial { background-image: url("../img/iconmonstr-barcode-4.svg"); } -.icon.wireless { background-image: url("../img/iconmonstr-wireless-6-icon.svg"); } -.icon.password { background-image: url("../img/iconmonstr-lock-3-icon.svg"); } +.icon.wireless { background-image: url("../img/iconmonstr-wireless-6.svg"); } +.icon.password { background-image: url("../img/iconmonstr-lock-3.svg"); } /* Make sure this is the last one */ .icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");} diff --git a/certidude/static/img/iconmonstr-barcode-4-icon.svg b/certidude/static/img/iconmonstr-barcode-4-icon.svg deleted file mode 100644 index 773e5aa..0000000 --- a/certidude/static/img/iconmonstr-barcode-4-icon.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/certidude/static/img/iconmonstr-barcode-4.svg b/certidude/static/img/iconmonstr-barcode-4.svg new file mode 100644 index 0000000..af386ca --- /dev/null +++ b/certidude/static/img/iconmonstr-barcode-4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-calendar-6.svg b/certidude/static/img/iconmonstr-calendar-6.svg new file mode 100644 index 0000000..9399caf --- /dev/null +++ b/certidude/static/img/iconmonstr-calendar-6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-certificate-15-icon.svg b/certidude/static/img/iconmonstr-certificate-15-icon.svg deleted file mode 100644 index 8b27cec..0000000 --- a/certidude/static/img/iconmonstr-certificate-15-icon.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - diff --git a/certidude/static/img/iconmonstr-certificate-15.svg b/certidude/static/img/iconmonstr-certificate-15.svg new file mode 100644 index 0000000..ef8a8bd --- /dev/null +++ b/certidude/static/img/iconmonstr-certificate-15.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-compass-7-icon.svg b/certidude/static/img/iconmonstr-compass-7-icon.svg deleted file mode 100644 index 880bcc9..0000000 --- a/certidude/static/img/iconmonstr-compass-7-icon.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - diff --git a/certidude/static/img/iconmonstr-compass-7.svg b/certidude/static/img/iconmonstr-compass-7.svg new file mode 100644 index 0000000..7b2310b --- /dev/null +++ b/certidude/static/img/iconmonstr-compass-7.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-download-12-icon.svg b/certidude/static/img/iconmonstr-download-12-icon.svg deleted file mode 100644 index feed0ed..0000000 --- a/certidude/static/img/iconmonstr-download-12-icon.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - diff --git a/certidude/static/img/iconmonstr-download-12.svg b/certidude/static/img/iconmonstr-download-12.svg new file mode 100644 index 0000000..e891a49 --- /dev/null +++ b/certidude/static/img/iconmonstr-download-12.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-email-2-icon.svg b/certidude/static/img/iconmonstr-email-2-icon.svg deleted file mode 100644 index 258086e..0000000 --- a/certidude/static/img/iconmonstr-email-2-icon.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - diff --git a/certidude/static/img/iconmonstr-email-2.svg b/certidude/static/img/iconmonstr-email-2.svg new file mode 100644 index 0000000..fd22489 --- /dev/null +++ b/certidude/static/img/iconmonstr-email-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-error-4-icon.svg b/certidude/static/img/iconmonstr-error-4-icon.svg deleted file mode 100644 index 65ff8ac..0000000 --- a/certidude/static/img/iconmonstr-error-4-icon.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - diff --git a/certidude/static/img/iconmonstr-error-4.svg b/certidude/static/img/iconmonstr-error-4.svg new file mode 100644 index 0000000..b8cc627 --- /dev/null +++ b/certidude/static/img/iconmonstr-error-4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-flag-3-icon.svg b/certidude/static/img/iconmonstr-flag-3-icon.svg deleted file mode 100644 index 8e12498..0000000 --- a/certidude/static/img/iconmonstr-flag-3-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - diff --git a/certidude/static/img/iconmonstr-flag-3.svg b/certidude/static/img/iconmonstr-flag-3.svg new file mode 100644 index 0000000..50e9dd0 --- /dev/null +++ b/certidude/static/img/iconmonstr-flag-3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-home-4-icon.svg b/certidude/static/img/iconmonstr-home-4-icon.svg deleted file mode 100644 index 9d1a19b..0000000 --- a/certidude/static/img/iconmonstr-home-4-icon.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - diff --git a/certidude/static/img/iconmonstr-home-7.svg b/certidude/static/img/iconmonstr-home-7.svg new file mode 100644 index 0000000..0988235 --- /dev/null +++ b/certidude/static/img/iconmonstr-home-7.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-info-6-icon.svg b/certidude/static/img/iconmonstr-info-6-icon.svg deleted file mode 100644 index f433119..0000000 --- a/certidude/static/img/iconmonstr-info-6-icon.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/certidude/static/img/iconmonstr-info-8.svg b/certidude/static/img/iconmonstr-info-8.svg new file mode 100644 index 0000000..c08c296 --- /dev/null +++ b/certidude/static/img/iconmonstr-info-8.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-key-2-icon.svg b/certidude/static/img/iconmonstr-key-2-icon.svg deleted file mode 100644 index 1301a5c..0000000 --- a/certidude/static/img/iconmonstr-key-2-icon.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/certidude/static/img/iconmonstr-key-3.svg b/certidude/static/img/iconmonstr-key-3.svg new file mode 100644 index 0000000..b1d4e78 --- /dev/null +++ b/certidude/static/img/iconmonstr-key-3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-lock-3-icon.svg b/certidude/static/img/iconmonstr-lock-3-icon.svg deleted file mode 100644 index fb75911..0000000 --- a/certidude/static/img/iconmonstr-lock-3-icon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - diff --git a/certidude/static/img/iconmonstr-magnifier-4-icon.svg b/certidude/static/img/iconmonstr-magnifier-4-icon.svg deleted file mode 100644 index 5dcbb1a..0000000 --- a/certidude/static/img/iconmonstr-magnifier-4-icon.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - diff --git a/certidude/static/img/iconmonstr-magnifier-4.svg b/certidude/static/img/iconmonstr-magnifier-4.svg new file mode 100644 index 0000000..6674706 --- /dev/null +++ b/certidude/static/img/iconmonstr-magnifier-4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-mobile-phone-6-icon.svg b/certidude/static/img/iconmonstr-mobile-phone-6-icon.svg deleted file mode 100644 index e9af4f5..0000000 --- a/certidude/static/img/iconmonstr-mobile-phone-6-icon.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - diff --git a/certidude/static/img/iconmonstr-mobile-phone-7.svg b/certidude/static/img/iconmonstr-mobile-phone-7.svg new file mode 100644 index 0000000..22155df --- /dev/null +++ b/certidude/static/img/iconmonstr-mobile-phone-7.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-pen-10-icon.svg b/certidude/static/img/iconmonstr-pen-10-icon.svg deleted file mode 100644 index 6bf848a..0000000 --- a/certidude/static/img/iconmonstr-pen-10-icon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - diff --git a/certidude/static/img/iconmonstr-pen-14.svg b/certidude/static/img/iconmonstr-pen-14.svg new file mode 100644 index 0000000..a0fc0b0 --- /dev/null +++ b/certidude/static/img/iconmonstr-pen-14.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-tag-2-icon.svg b/certidude/static/img/iconmonstr-tag-2-icon.svg deleted file mode 100644 index ddbe528..0000000 --- a/certidude/static/img/iconmonstr-tag-2-icon.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - diff --git a/certidude/static/img/iconmonstr-tag-3.svg b/certidude/static/img/iconmonstr-tag-3.svg new file mode 100644 index 0000000..94d80ed --- /dev/null +++ b/certidude/static/img/iconmonstr-tag-3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-time-13-icon.svg b/certidude/static/img/iconmonstr-time-13-icon.svg deleted file mode 100644 index 189521b..0000000 --- a/certidude/static/img/iconmonstr-time-13-icon.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - diff --git a/certidude/static/img/iconmonstr-user-5.svg b/certidude/static/img/iconmonstr-user-5.svg new file mode 100644 index 0000000..57a1ab2 --- /dev/null +++ b/certidude/static/img/iconmonstr-user-5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-warning-6-icon.svg b/certidude/static/img/iconmonstr-warning-6-icon.svg deleted file mode 100644 index b6f93a4..0000000 --- a/certidude/static/img/iconmonstr-warning-6-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/certidude/static/img/iconmonstr-warning-8.svg b/certidude/static/img/iconmonstr-warning-8.svg new file mode 100644 index 0000000..4883cf3 --- /dev/null +++ b/certidude/static/img/iconmonstr-warning-8.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-wireless-6-icon.svg b/certidude/static/img/iconmonstr-wireless-6-icon.svg deleted file mode 100644 index 467c13a..0000000 --- a/certidude/static/img/iconmonstr-wireless-6-icon.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - diff --git a/certidude/static/img/iconmonstr-x-mark-5-icon.svg b/certidude/static/img/iconmonstr-x-mark-5-icon.svg deleted file mode 100644 index c16d84e..0000000 --- a/certidude/static/img/iconmonstr-x-mark-5-icon.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - diff --git a/certidude/static/img/iconmonstr-x-mark-8.svg b/certidude/static/img/iconmonstr-x-mark-8.svg new file mode 100644 index 0000000..8a69429 --- /dev/null +++ b/certidude/static/img/iconmonstr-x-mark-8.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/index.html b/certidude/static/index.html index f677127..28ea348 100644 --- a/certidude/static/index.html +++ b/certidude/static/index.html @@ -14,11 +14,12 @@
diff --git a/certidude/static/js/certidude.js b/certidude/static/js/certidude.js index 2d8e2f3..799bd1c 100644 --- a/certidude/static/js/certidude.js +++ b/certidude/static/js/certidude.js @@ -67,7 +67,6 @@ function onRequestSubmitted(e) { url: "/api/request/" + e.data + "/", dataType: "json", success: function(request, status, xhr) { - console.info(request); $("#pending_requests").prepend( nunjucks.render('views/request.html', { request: request })); } @@ -75,12 +74,12 @@ function onRequestSubmitted(e) { } function onRequestDeleted(e) { - console.log("Removing deleted request #" + e.data); - $("#request_" + e.data).remove(); + console.log("Removing deleted request", e.data); + $("#request-" + e.data.replace("@", "--").replace(".", "-")).remove(); } function onClientUp(e) { - console.log("Adding security association:" + e.data); + console.log("Adding security association:", e.data); var lease = JSON.parse(e.data); var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status"); $status.html(nunjucks.render('views/status.html', { @@ -93,7 +92,7 @@ function onClientUp(e) { } function onClientDown(e) { - console.log("Removing security association:" + e.data); + console.log("Removing security association:", e.data); var lease = JSON.parse(e.data); var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status"); $status.html(nunjucks.render('views/status.html', { @@ -107,7 +106,9 @@ function onClientDown(e) { function onRequestSigned(e) { console.log("Request signed:", e.data); - $("#request_" + e.data).slideUp("normal", function() { $(this).remove(); }); + + $("#request-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); }); + $("#certificate-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); }); $.ajax({ method: "GET", @@ -121,13 +122,14 @@ function onRequestSigned(e) { }); } + function onCertificateRevoked(e) { - console.log("Removing revoked certificate #" + e.data); - $("#certificate_" + e.data).slideUp("normal", function() { $(this).remove(); }); + console.log("Removing revoked certificate", e.data); + $("#certificate-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); }); } function onTagAdded(e) { - console.log("Tag added #" + e.data); + console.log("Tag added", e.data); $.ajax({ method: "GET", url: "/api/tag/" + e.data + "/", @@ -143,12 +145,12 @@ function onTagAdded(e) { } function onTagRemoved(e) { - console.log("Tag removed #" + e.data); + console.log("Tag removed", e.data); $("#tag_" + e.data).remove(); } function onTagUpdated(e) { - console.log("Tag updated #" + e.data); + console.log("Tag updated", e.data); $.ajax({ method: "GET", url: "/api/tag/" + e.data + "/", @@ -175,32 +177,49 @@ $(document).ready(function() { $("#container").html(nunjucks.render('views/error.html', { message: msg })); }, success: function(session, status, xhr) { - console.info("Opening EventSource from:", session.event_channel); - - var source = new EventSource(session.event_channel); - - source.onmessage = function(event) { - console.log("Received server-sent event:", event); - } - - source.addEventListener("log-entry", onLogEntry); - source.addEventListener("up-client", onClientUp); - source.addEventListener("down-client", onClientDown); - source.addEventListener("request-deleted", onRequestDeleted); - source.addEventListener("request-submitted", onRequestSubmitted); - source.addEventListener("request-signed", onRequestSigned); - source.addEventListener("certificate-revoked", onCertificateRevoked); - source.addEventListener("tag-added", onTagAdded); - source.addEventListener("tag-removed", onTagRemoved); - source.addEventListener("tag-updated", onTagUpdated); + $("#login").hide(); /** * Render authority views **/ $("#container").html(nunjucks.render('views/authority.html', { session: session, window: window })); - console.info("Swtiching to requests section"); - $("section").hide(); - $("section#requests").show(); + + if (session.authority) { + $("#log input").each(function(i, e) { + console.info("e.checked:", e.checked , "and", e.id, "@localstorage is", localStorage[e.id], "setting to:", localStorage[e.id] || e.checked, "bool:", localStorage[e.id] || e.checked == "true"); + e.checked = localStorage[e.id] ? localStorage[e.id] == "true" : e.checked; + }); + + $("#log input").change(function() { + localStorage[this.id] = this.checked; + }); + + console.info("Opening EventSource from:", session.authority.events); + + var source = new EventSource(session.authority.events); + + source.onmessage = function(event) { + console.log("Received server-sent event:", event); + } + + source.addEventListener("log-entry", onLogEntry); + source.addEventListener("up-client", onClientUp); + source.addEventListener("down-client", onClientDown); + source.addEventListener("request-deleted", onRequestDeleted); + source.addEventListener("request-submitted", onRequestSubmitted); + source.addEventListener("request-signed", onRequestSigned); + source.addEventListener("certificate-revoked", onCertificateRevoked); + source.addEventListener("tag-added", onTagAdded); + source.addEventListener("tag-removed", onTagRemoved); + source.addEventListener("tag-updated", onTagUpdated); + + console.info("Swtiching to requests section"); + $("section").hide(); + $("section#requests").show(); + $("#section-revoked").show(); + $("#section-signed").show(); + $("#section-requests").show(); + } $("nav#menu li").click(function(e) { $("section").hide(); @@ -231,88 +250,97 @@ $(document).ready(function() { }); - - - $.ajax({ - method: "GET", - url: "/api/config/", - dataType: "json", - success: function(configuration, status, xhr) { - console.info("Appending " + configuration.length + " configuration items"); - $("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration})); - /** - * Fetch tags for certificates - */ - $.ajax({ - method: "GET", - url: "/api/tag/", - dataType: "json", - success:function(tags, status, xhr) { - console.info("Got", tags.length, "tags"); - for (var j = 0; j < tags.length; j++) { - // TODO: Deduplicate - $tag = $("" + tags[j].value + ""); - console.info("Inserting tag", tags[j], $tag); - $tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend(" "); - $tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend($tag); - $tag.click(onTagClicked); - $("#tags_autocomplete").prepend(""); + console.log("Features enabled:", session.features); + if (session.features.tagging) { + console.info("Tagging enabled"); + $("#section-config").show(); + $.ajax({ + method: "GET", + url: "/api/config/", + dataType: "json", + success: function(configuration, status, xhr) { + console.info("Appending", configuration.length, "configuration items"); + $("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration})); + /** + * Fetch tags for certificates + */ + $.ajax({ + method: "GET", + url: "/api/tag/", + dataType: "json", + success:function(tags, status, xhr) { + console.info("Got", tags.length, "tags"); + $("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration})); + for (var j = 0; j < tags.length; j++) { + // TODO: Deduplicate + $tag = $("" + tags[j].value + ""); + console.info("Inserting tag", tags[j], $tag); + $tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend(" "); + $tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend($tag); + $tag.click(onTagClicked); + $("#tags_autocomplete").prepend(""); + } } - } - }); - } - }); + }); + } + }); + } /** * Fetch leases associated with certificates */ - $.ajax({ - method: "GET", - url: "/api/lease/", - dataType: "json", - success: function(leases, status, xhr) { - console.info("Got leases:", leases); - for (var j = 0; j < leases.length; j++) { - var $status = $("#signed_certificates [data-dn='" + leases[j].identity + "'] .status"); - if (!$status.length) { - console.info("Detected rogue client:", leases[j]); - continue; + if (session.features.leases) { + $.ajax({ + method: "GET", + url: "/api/lease/", + dataType: "json", + success: function(leases, status, xhr) { + console.info("Got leases:", leases); + for (var j = 0; j < leases.length; j++) { + var $status = $("#signed_certificates [data-dn='" + leases[j].identity + "'] .status"); + if (!$status.length) { + console.info("Detected rogue client:", leases[j]); + continue; + } + $status.html(nunjucks.render('views/status.html', { + lease: { + address: leases[j].address, + age: (new Date() - new Date(leases[j].released)) / 1000, + identity: leases[j].identity, + acquired: new Date(leases[j].acquired).toLocaleString(), + released: leases[j].released ? new Date(leases[j].released).toLocaleString() : null + }})); } - $status.html(nunjucks.render('views/status.html', { - lease: { - address: leases[j].address, - age: (new Date() - new Date(leases[j].released)) / 1000, - identity: leases[j].identity, - acquired: new Date(leases[j].acquired).toLocaleString(), - released: leases[j].released ? new Date(leases[j].released).toLocaleString() : null - }})); - } - } - }); - return; + } + }); + } + /** * Fetch log entries */ - $.ajax({ - method: "GET", - url: "/api/log/", - dataType: "json", - success:function(entries, status, xhr) { - console.info("Got", entries.length, "log entries"); - for (var j = 0; j < entries.length; j++) { - if ($("#log_level_" + entries[j].severity).prop("checked")) { - $("#log_entries").append(nunjucks.render("views/logentry.html", { - entry: { - created: new Date(entries[j].created).toLocaleString("et-EE"), - message: entries[j].message, - severity: entries[j].severity - } - })); + if (session.features.logging) { + $("#section-log").show(); + $.ajax({ + method: "GET", + url: "/api/log/", + dataType: "json", + success:function(entries, status, xhr) { + console.info("Got", entries.length, "log entries"); + for (var j = 0; j < entries.length; j++) { + if ($("#log_level_" + entries[j].severity).prop("checked")) { + $("#log_entries").append(nunjucks.render("views/logentry.html", { + entry: { + created: new Date(entries[j].created).toLocaleString("et-EE"), + message: entries[j].message, + severity: entries[j].severity + } + })); + } } } - } - }); + }); + } } }); }); diff --git a/certidude/static/js/nunjucks-slim.js b/certidude/static/js/nunjucks-slim.js deleted file mode 100644 index 0b0914b..0000000 --- a/certidude/static/js/nunjucks-slim.js +++ /dev/null @@ -1,2633 +0,0 @@ -/*! Browser bundle of nunjucks 2.3.0 (slim, only works with precompiled templates) */ -var nunjucks = -/******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; - -/******/ // The require function -/******/ function __webpack_require__(moduleId) { - -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) -/******/ return installedModules[moduleId].exports; - -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ exports: {}, -/******/ id: moduleId, -/******/ loaded: false -/******/ }; - -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); - -/******/ // Flag the module as loaded -/******/ module.loaded = true; - -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } - - -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; - -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; - -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; - -/******/ // Load entry module and return exports -/******/ return __webpack_require__(0); -/******/ }) -/************************************************************************/ -/******/ ([ -/* 0 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var lib = __webpack_require__(1); - var env = __webpack_require__(2); - var Loader = __webpack_require__(11); - var loaders = __webpack_require__(3); - var precompile = __webpack_require__(3); - - module.exports = {}; - module.exports.Environment = env.Environment; - module.exports.Template = env.Template; - - module.exports.Loader = Loader; - module.exports.FileSystemLoader = loaders.FileSystemLoader; - module.exports.PrecompiledLoader = loaders.PrecompiledLoader; - module.exports.WebLoader = loaders.WebLoader; - - module.exports.compiler = __webpack_require__(3); - module.exports.parser = __webpack_require__(3); - module.exports.lexer = __webpack_require__(3); - module.exports.runtime = __webpack_require__(8); - module.exports.lib = lib; - module.exports.nodes = __webpack_require__(3); - - module.exports.installJinjaCompat = __webpack_require__(12); - - // A single instance of an environment, since this is so commonly used - - var e; - module.exports.configure = function(templatesPath, opts) { - opts = opts || {}; - if(lib.isObject(templatesPath)) { - opts = templatesPath; - templatesPath = null; - } - - var TemplateLoader; - if(loaders.FileSystemLoader) { - TemplateLoader = new loaders.FileSystemLoader(templatesPath, { - watch: opts.watch, - noCache: opts.noCache - }); - } - else if(loaders.WebLoader) { - TemplateLoader = new loaders.WebLoader(templatesPath, { - useCache: opts.web && opts.web.useCache, - async: opts.web && opts.web.async - }); - } - - e = new env.Environment(TemplateLoader, opts); - - if(opts && opts.express) { - e.express(opts.express); - } - - return e; - }; - - module.exports.compile = function(src, env, path, eagerCompile) { - if(!e) { - module.exports.configure(); - } - return new module.exports.Template(src, env, path, eagerCompile); - }; - - module.exports.render = function(name, ctx, cb) { - if(!e) { - module.exports.configure(); - } - - return e.render(name, ctx, cb); - }; - - module.exports.renderString = function(src, ctx, cb) { - if(!e) { - module.exports.configure(); - } - - return e.renderString(src, ctx, cb); - }; - - if(precompile) { - module.exports.precompile = precompile.precompile; - module.exports.precompileString = precompile.precompileString; - } - - -/***/ }, -/* 1 */ -/***/ function(module, exports) { - - 'use strict'; - - var ArrayProto = Array.prototype; - var ObjProto = Object.prototype; - - var escapeMap = { - '&': '&', - '"': '"', - '\'': ''', - '<': '<', - '>': '>' - }; - - var escapeRegex = /[&"'<>]/g; - - var lookupEscape = function(ch) { - return escapeMap[ch]; - }; - - var exports = module.exports = {}; - - exports.prettifyError = function(path, withInternals, err) { - // jshint -W022 - // http://jslinterrors.com/do-not-assign-to-the-exception-parameter - if (!err.Update) { - // not one of ours, cast it - err = new exports.TemplateError(err); - } - err.Update(path); - - // Unless they marked the dev flag, show them a trace from here - if (!withInternals) { - var old = err; - err = new Error(old.message); - err.name = old.name; - } - - return err; - }; - - exports.TemplateError = function(message, lineno, colno) { - var err = this; - - if (message instanceof Error) { // for casting regular js errors - err = message; - message = message.name + ': ' + message.message; - - try { - if(err.name = '') {} - } - catch(e) { - // If we can't set the name of the error object in this - // environment, don't use it - err = this; - } - } else { - if(Error.captureStackTrace) { - Error.captureStackTrace(err); - } - } - - err.name = 'Template render error'; - err.message = message; - err.lineno = lineno; - err.colno = colno; - err.firstUpdate = true; - - err.Update = function(path) { - var message = '(' + (path || 'unknown path') + ')'; - - // only show lineno + colno next to path of template - // where error occurred - if (this.firstUpdate) { - if(this.lineno && this.colno) { - message += ' [Line ' + this.lineno + ', Column ' + this.colno + ']'; - } - else if(this.lineno) { - message += ' [Line ' + this.lineno + ']'; - } - } - - message += '\n '; - if (this.firstUpdate) { - message += ' '; - } - - this.message = message + (this.message || ''); - this.firstUpdate = false; - return this; - }; - - return err; - }; - - exports.TemplateError.prototype = Error.prototype; - - exports.escape = function(val) { - return val.replace(escapeRegex, lookupEscape); - }; - - exports.isFunction = function(obj) { - return ObjProto.toString.call(obj) === '[object Function]'; - }; - - exports.isArray = Array.isArray || function(obj) { - return ObjProto.toString.call(obj) === '[object Array]'; - }; - - exports.isString = function(obj) { - return ObjProto.toString.call(obj) === '[object String]'; - }; - - exports.isObject = function(obj) { - return ObjProto.toString.call(obj) === '[object Object]'; - }; - - exports.groupBy = function(obj, val) { - var result = {}; - var iterator = exports.isFunction(val) ? val : function(obj) { return obj[val]; }; - for(var i=0; i>> 0; // Hack to convert object.length to a UInt32 - - fromIndex = +fromIndex || 0; - - if(Math.abs(fromIndex) === Infinity) { - fromIndex = 0; - } - - if(fromIndex < 0) { - fromIndex += length; - if (fromIndex < 0) { - fromIndex = 0; - } - } - - for(;fromIndex < length; fromIndex++) { - if (arr[fromIndex] === searchElement) { - return fromIndex; - } - } - - return -1; - }; - - if(!Array.prototype.map) { - Array.prototype.map = function() { - throw new Error('map is unimplemented for this js engine'); - }; - } - - exports.keys = function(obj) { - if(Object.prototype.keys) { - return obj.keys(); - } - else { - var keys = []; - for(var k in obj) { - if(obj.hasOwnProperty(k)) { - keys.push(k); - } - } - return keys; - } - }; - - -/***/ }, -/* 2 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var path = __webpack_require__(3); - var asap = __webpack_require__(4); - var lib = __webpack_require__(1); - var Obj = __webpack_require__(6); - var compiler = __webpack_require__(3); - var builtin_filters = __webpack_require__(7); - var builtin_loaders = __webpack_require__(3); - var runtime = __webpack_require__(8); - var globals = __webpack_require__(9); - var Frame = runtime.Frame; - var Template; - - // Unconditionally load in this loader, even if no other ones are - // included (possible in the slim browser build) - builtin_loaders.PrecompiledLoader = __webpack_require__(10); - - // If the user is using the async API, *always* call it - // asynchronously even if the template was synchronous. - function callbackAsap(cb, err, res) { - asap(function() { cb(err, res); }); - } - - var Environment = Obj.extend({ - init: function(loaders, opts) { - // The dev flag determines the trace that'll be shown on errors. - // If set to true, returns the full trace from the error point, - // otherwise will return trace starting from Template.render - // (the full trace from within nunjucks may confuse developers using - // the library) - // defaults to false - opts = this.opts = opts || {}; - this.opts.dev = !!opts.dev; - - // The autoescape flag sets global autoescaping. If true, - // every string variable will be escaped by default. - // If false, strings can be manually escaped using the `escape` filter. - // defaults to true - this.opts.autoescape = opts.autoescape != null ? opts.autoescape : true; - - // If true, this will make the system throw errors if trying - // to output a null or undefined value - this.opts.throwOnUndefined = !!opts.throwOnUndefined; - this.opts.trimBlocks = !!opts.trimBlocks; - this.opts.lstripBlocks = !!opts.lstripBlocks; - - this.loaders = []; - - if(!loaders) { - // The filesystem loader is only available server-side - if(builtin_loaders.FileSystemLoader) { - this.loaders = [new builtin_loaders.FileSystemLoader('views')]; - } - else if(builtin_loaders.WebLoader) { - this.loaders = [new builtin_loaders.WebLoader('/views')]; - } - } - else { - this.loaders = lib.isArray(loaders) ? loaders : [loaders]; - } - - // It's easy to use precompiled templates: just include them - // before you configure nunjucks and this will automatically - // pick it up and use it - if((true) && window.nunjucksPrecompiled) { - this.loaders.unshift( - new builtin_loaders.PrecompiledLoader(window.nunjucksPrecompiled) - ); - } - - this.initCache(); - - this.globals = globals(); - this.filters = {}; - this.asyncFilters = []; - this.extensions = {}; - this.extensionsList = []; - - for(var name in builtin_filters) { - this.addFilter(name, builtin_filters[name]); - } - }, - - initCache: function() { - // Caching and cache busting - lib.each(this.loaders, function(loader) { - loader.cache = {}; - - if(typeof loader.on === 'function') { - loader.on('update', function(template) { - loader.cache[template] = null; - }); - } - }); - }, - - addExtension: function(name, extension) { - extension._name = name; - this.extensions[name] = extension; - this.extensionsList.push(extension); - return this; - }, - - removeExtension: function(name) { - var extension = this.getExtension(name); - if (!extension) return; - - this.extensionsList = lib.without(this.extensionsList, extension); - delete this.extensions[name]; - }, - - getExtension: function(name) { - return this.extensions[name]; - }, - - hasExtension: function(name) { - return !!this.extensions[name]; - }, - - addGlobal: function(name, value) { - this.globals[name] = value; - return this; - }, - - getGlobal: function(name) { - if(!this.globals[name]) { - throw new Error('global not found: ' + name); - } - return this.globals[name]; - }, - - addFilter: function(name, func, async) { - var wrapped = func; - - if(async) { - this.asyncFilters.push(name); - } - this.filters[name] = wrapped; - return this; - }, - - getFilter: function(name) { - if(!this.filters[name]) { - throw new Error('filter not found: ' + name); - } - return this.filters[name]; - }, - - resolveTemplate: function(loader, parentName, filename) { - var isRelative = (loader.isRelative && parentName)? loader.isRelative(filename) : false; - return (isRelative && loader.resolve)? loader.resolve(parentName, filename) : filename; - }, - - getTemplate: function(name, eagerCompile, parentName, ignoreMissing, cb) { - var that = this; - var tmpl = null; - if(name && name.raw) { - // this fixes autoescape for templates referenced in symbols - name = name.raw; - } - - if(lib.isFunction(parentName)) { - cb = parentName; - parentName = null; - eagerCompile = eagerCompile || false; - } - - if(lib.isFunction(eagerCompile)) { - cb = eagerCompile; - eagerCompile = false; - } - - if (name instanceof Template) { - tmpl = name; - } - else if(typeof name !== 'string') { - throw new Error('template names must be a string: ' + name); - } - else { - for (var i = 0; i < this.loaders.length; i++) { - var _name = this.resolveTemplate(this.loaders[i], parentName, name); - tmpl = this.loaders[i].cache[_name]; - if (tmpl) break; - } - } - - if(tmpl) { - if(eagerCompile) { - tmpl.compile(); - } - - if(cb) { - cb(null, tmpl); - } - else { - return tmpl; - } - } else { - var syncResult; - var _this = this; - - var createTemplate = function(err, info) { - if(!info && !err) { - if(!ignoreMissing) { - err = new Error('template not found: ' + name); - } - } - - if (err) { - if(cb) { - cb(err); - } - else { - throw err; - } - } - else { - var tmpl; - if(info) { - tmpl = new Template(info.src, _this, - info.path, eagerCompile); - - if(!info.noCache) { - info.loader.cache[name] = tmpl; - } - } - else { - tmpl = new Template('', _this, - '', eagerCompile); - } - - if(cb) { - cb(null, tmpl); - } - else { - syncResult = tmpl; - } - } - }; - - lib.asyncIter(this.loaders, function(loader, i, next, done) { - function handle(err, src) { - if(err) { - done(err); - } - else if(src) { - src.loader = loader; - done(null, src); - } - else { - next(); - } - } - - // Resolve name relative to parentName - name = that.resolveTemplate(loader, parentName, name); - - if(loader.async) { - loader.getSource(name, handle); - } - else { - handle(null, loader.getSource(name)); - } - }, createTemplate); - - return syncResult; - } - }, - - express: function(app) { - var env = this; - - function NunjucksView(name, opts) { - this.name = name; - this.path = name; - this.defaultEngine = opts.defaultEngine; - this.ext = path.extname(name); - if (!this.ext && !this.defaultEngine) throw new Error('No default engine was specified and no extension was provided.'); - if (!this.ext) this.name += (this.ext = ('.' !== this.defaultEngine[0] ? '.' : '') + this.defaultEngine); - } - - NunjucksView.prototype.render = function(opts, cb) { - env.render(this.name, opts, cb); - }; - - app.set('view', NunjucksView); - return this; - }, - - render: function(name, ctx, cb) { - if(lib.isFunction(ctx)) { - cb = ctx; - ctx = null; - } - - // We support a synchronous API to make it easier to migrate - // existing code to async. This works because if you don't do - // anything async work, the whole thing is actually run - // synchronously. - var syncResult = null; - - this.getTemplate(name, function(err, tmpl) { - if(err && cb) { - callbackAsap(cb, err); - } - else if(err) { - throw err; - } - else { - syncResult = tmpl.render(ctx, cb); - } - }); - - return syncResult; - }, - - renderString: function(src, ctx, opts, cb) { - if(lib.isFunction(opts)) { - cb = opts; - opts = {}; - } - opts = opts || {}; - - var tmpl = new Template(src, this, opts.path); - return tmpl.render(ctx, cb); - } - }); - - var Context = Obj.extend({ - init: function(ctx, blocks, env) { - // Has to be tied to an environment so we can tap into its globals. - this.env = env || new Environment(); - - // Make a duplicate of ctx - this.ctx = {}; - for(var k in ctx) { - if(ctx.hasOwnProperty(k)) { - this.ctx[k] = ctx[k]; - } - } - - this.blocks = {}; - this.exported = []; - - for(var name in blocks) { - this.addBlock(name, blocks[name]); - } - }, - - lookup: function(name) { - // This is one of the most called functions, so optimize for - // the typical case where the name isn't in the globals - if(name in this.env.globals && !(name in this.ctx)) { - return this.env.globals[name]; - } - else { - return this.ctx[name]; - } - }, - - setVariable: function(name, val) { - this.ctx[name] = val; - }, - - getVariables: function() { - return this.ctx; - }, - - addBlock: function(name, block) { - this.blocks[name] = this.blocks[name] || []; - this.blocks[name].push(block); - return this; - }, - - getBlock: function(name) { - if(!this.blocks[name]) { - throw new Error('unknown block "' + name + '"'); - } - - return this.blocks[name][0]; - }, - - getSuper: function(env, name, block, frame, runtime, cb) { - var idx = lib.indexOf(this.blocks[name] || [], block); - var blk = this.blocks[name][idx + 1]; - var context = this; - - if(idx === -1 || !blk) { - throw new Error('no super block available for "' + name + '"'); - } - - blk(env, context, frame, runtime, cb); - }, - - addExport: function(name) { - this.exported.push(name); - }, - - getExported: function() { - var exported = {}; - for(var i=0; i capacity) { - // Manually shift all values starting at the index back to the - // beginning of the queue. - for (var scan = 0, newLength = queue.length - index; scan < newLength; scan++) { - queue[scan] = queue[scan + index]; - } - queue.length -= index; - index = 0; - } - } - queue.length = 0; - index = 0; - flushing = false; - } - - // `requestFlush` is implemented using a strategy based on data collected from - // every available SauceLabs Selenium web driver worker at time of writing. - // https://docs.google.com/spreadsheets/d/1mG-5UYGup5qxGdEMWkhP6BWCz053NUb2E1QoUTU16uA/edit#gid=783724593 - - // Safari 6 and 6.1 for desktop, iPad, and iPhone are the only browsers that - // have WebKitMutationObserver but not un-prefixed MutationObserver. - // Must use `global` instead of `window` to work in both frames and web - // workers. `global` is a provision of Browserify, Mr, Mrs, or Mop. - var BrowserMutationObserver = global.MutationObserver || global.WebKitMutationObserver; - - // MutationObservers are desirable because they have high priority and work - // reliably everywhere they are implemented. - // They are implemented in all modern browsers. - // - // - Android 4-4.3 - // - Chrome 26-34 - // - Firefox 14-29 - // - Internet Explorer 11 - // - iPad Safari 6-7.1 - // - iPhone Safari 7-7.1 - // - Safari 6-7 - if (typeof BrowserMutationObserver === "function") { - requestFlush = makeRequestCallFromMutationObserver(flush); - - // MessageChannels are desirable because they give direct access to the HTML - // task queue, are implemented in Internet Explorer 10, Safari 5.0-1, and Opera - // 11-12, and in web workers in many engines. - // Although message channels yield to any queued rendering and IO tasks, they - // would be better than imposing the 4ms delay of timers. - // However, they do not work reliably in Internet Explorer or Safari. - - // Internet Explorer 10 is the only browser that has setImmediate but does - // not have MutationObservers. - // Although setImmediate yields to the browser's renderer, it would be - // preferrable to falling back to setTimeout since it does not have - // the minimum 4ms penalty. - // Unfortunately there appears to be a bug in Internet Explorer 10 Mobile (and - // Desktop to a lesser extent) that renders both setImmediate and - // MessageChannel useless for the purposes of ASAP. - // https://github.com/kriskowal/q/issues/396 - - // Timers are implemented universally. - // We fall back to timers in workers in most engines, and in foreground - // contexts in the following browsers. - // However, note that even this simple case requires nuances to operate in a - // broad spectrum of browsers. - // - // - Firefox 3-13 - // - Internet Explorer 6-9 - // - iPad Safari 4.3 - // - Lynx 2.8.7 - } else { - requestFlush = makeRequestCallFromTimer(flush); - } - - // `requestFlush` requests that the high priority event queue be flushed as - // soon as possible. - // This is useful to prevent an error thrown in a task from stalling the event - // queue if the exception handled by Node.js’s - // `process.on("uncaughtException")` or by a domain. - rawAsap.requestFlush = requestFlush; - - // To request a high priority event, we induce a mutation observer by toggling - // the text of a text node between "1" and "-1". - function makeRequestCallFromMutationObserver(callback) { - var toggle = 1; - var observer = new BrowserMutationObserver(callback); - var node = document.createTextNode(""); - observer.observe(node, {characterData: true}); - return function requestCall() { - toggle = -toggle; - node.data = toggle; - }; - } - - // The message channel technique was discovered by Malte Ubl and was the - // original foundation for this library. - // http://www.nonblocking.io/2011/06/windownexttick.html - - // Safari 6.0.5 (at least) intermittently fails to create message ports on a - // page's first load. Thankfully, this version of Safari supports - // MutationObservers, so we don't need to fall back in that case. - - // function makeRequestCallFromMessageChannel(callback) { - // var channel = new MessageChannel(); - // channel.port1.onmessage = callback; - // return function requestCall() { - // channel.port2.postMessage(0); - // }; - // } - - // For reasons explained above, we are also unable to use `setImmediate` - // under any circumstances. - // Even if we were, there is another bug in Internet Explorer 10. - // It is not sufficient to assign `setImmediate` to `requestFlush` because - // `setImmediate` must be called *by name* and therefore must be wrapped in a - // closure. - // Never forget. - - // function makeRequestCallFromSetImmediate(callback) { - // return function requestCall() { - // setImmediate(callback); - // }; - // } - - // Safari 6.0 has a problem where timers will get lost while the user is - // scrolling. This problem does not impact ASAP because Safari 6.0 supports - // mutation observers, so that implementation is used instead. - // However, if we ever elect to use timers in Safari, the prevalent work-around - // is to add a scroll event listener that calls for a flush. - - // `setTimeout` does not call the passed callback if the delay is less than - // approximately 7 in web workers in Firefox 8 through 18, and sometimes not - // even then. - - function makeRequestCallFromTimer(callback) { - return function requestCall() { - // We dispatch a timeout with a specified delay of 0 for engines that - // can reliably accommodate that request. This will usually be snapped - // to a 4 milisecond delay, but once we're flushing, there's no delay - // between events. - var timeoutHandle = setTimeout(handleTimer, 0); - // However, since this timer gets frequently dropped in Firefox - // workers, we enlist an interval handle that will try to fire - // an event 20 times per second until it succeeds. - var intervalHandle = setInterval(handleTimer, 50); - - function handleTimer() { - // Whichever timer succeeds will cancel both timers and - // execute the callback. - clearTimeout(timeoutHandle); - clearInterval(intervalHandle); - callback(); - } - }; - } - - // This is for `asap.js` only. - // Its name will be periodically randomized to break any code that depends on - // its existence. - rawAsap.makeRequestCallFromTimer = makeRequestCallFromTimer; - - // ASAP was originally a nextTick shim included in Q. This was factored out - // into this ASAP package. It was later adapted to RSVP which made further - // amendments. These decisions, particularly to marginalize MessageChannel and - // to capture the MutationObserver implementation in a closure, were integrated - // back into ASAP proper. - // https://github.com/tildeio/rsvp.js/blob/cddf7232546a9cf858524b75cde6f9edf72620a7/lib/rsvp/asap.js - - /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) - -/***/ }, -/* 6 */ -/***/ function(module, exports) { - - 'use strict'; - - // A simple class system, more documentation to come - - function extend(cls, name, props) { - // This does that same thing as Object.create, but with support for IE8 - var F = function() {}; - F.prototype = cls.prototype; - var prototype = new F(); - - // jshint undef: false - var fnTest = /xyz/.test(function(){ xyz; }) ? /\bparent\b/ : /.*/; - props = props || {}; - - for(var k in props) { - var src = props[k]; - var parent = prototype[k]; - - if(typeof parent === 'function' && - typeof src === 'function' && - fnTest.test(src)) { - /*jshint -W083 */ - prototype[k] = (function (src, parent) { - return function() { - // Save the current parent method - var tmp = this.parent; - - // Set parent to the previous method, call, and restore - this.parent = parent; - var res = src.apply(this, arguments); - this.parent = tmp; - - return res; - }; - })(src, parent); - } - else { - prototype[k] = src; - } - } - - prototype.typename = name; - - var new_cls = function() { - if(prototype.init) { - prototype.init.apply(this, arguments); - } - }; - - new_cls.prototype = prototype; - new_cls.prototype.constructor = new_cls; - - new_cls.extend = function(name, props) { - if(typeof name === 'object') { - props = name; - name = 'anonymous'; - } - return extend(new_cls, name, props); - }; - - return new_cls; - } - - module.exports = extend(Object, 'Object', {}); - - -/***/ }, -/* 7 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var lib = __webpack_require__(1); - var r = __webpack_require__(8); - - function normalize(value, defaultValue) { - if(value === null || value === undefined || value === false) { - return defaultValue; - } - return value; - } - - var filters = { - abs: function(n) { - return Math.abs(n); - }, - - batch: function(arr, linecount, fill_with) { - var i; - var res = []; - var tmp = []; - - for(i = 0; i < arr.length; i++) { - if(i % linecount === 0 && tmp.length) { - res.push(tmp); - tmp = []; - } - - tmp.push(arr[i]); - } - - if(tmp.length) { - if(fill_with) { - for(i = tmp.length; i < linecount; i++) { - tmp.push(fill_with); - } - } - - res.push(tmp); - } - - return res; - }, - - capitalize: function(str) { - str = normalize(str, ''); - var ret = str.toLowerCase(); - return r.copySafeness(str, ret.charAt(0).toUpperCase() + ret.slice(1)); - }, - - center: function(str, width) { - str = normalize(str, ''); - width = width || 80; - - if(str.length >= width) { - return str; - } - - var spaces = width - str.length; - var pre = lib.repeat(' ', spaces/2 - spaces % 2); - var post = lib.repeat(' ', spaces/2); - return r.copySafeness(str, pre + str + post); - }, - - 'default': function(val, def, bool) { - if(bool) { - return val ? val : def; - } - else { - return (val !== undefined) ? val : def; - } - }, - - dictsort: function(val, case_sensitive, by) { - if (!lib.isObject(val)) { - throw new lib.TemplateError('dictsort filter: val must be an object'); - } - - var array = []; - for (var k in val) { - // deliberately include properties from the object's prototype - array.push([k,val[k]]); - } - - var si; - if (by === undefined || by === 'key') { - si = 0; - } else if (by === 'value') { - si = 1; - } else { - throw new lib.TemplateError( - 'dictsort filter: You can only sort by either key or value'); - } - - array.sort(function(t1, t2) { - var a = t1[si]; - var b = t2[si]; - - if (!case_sensitive) { - if (lib.isString(a)) { - a = a.toUpperCase(); - } - if (lib.isString(b)) { - b = b.toUpperCase(); - } - } - - return a > b ? 1 : (a === b ? 0 : -1); - }); - - return array; - }, - - dump: function(obj) { - return JSON.stringify(obj); - }, - - escape: function(str) { - if(typeof str === 'string' || - str instanceof r.SafeString) { - return lib.escape(str); - } - return str; - }, - - safe: function(str) { - return r.markSafe(str); - }, - - first: function(arr) { - return arr[0]; - }, - - groupby: function(arr, attr) { - return lib.groupBy(arr, attr); - }, - - indent: function(str, width, indentfirst) { - str = normalize(str, ''); - - if (str === '') return ''; - - width = width || 4; - var res = ''; - var lines = str.split('\n'); - var sp = lib.repeat(' ', width); - - for(var i=0; i .a.b.c. - res = new_ + str.split('').join(new_) + new_; - return r.copySafeness(str, res); - } - - var nextIndex = str.indexOf(old); - // if # of replacements to perform is 0, or the string to does - // not contain the old value, return the string - if(maxCount === 0 || nextIndex === -1){ - return str; - } - - var pos = 0; - var count = 0; // # of replacements made - - while(nextIndex > -1 && (maxCount === -1 || count < maxCount)){ - // Grab the next chunk of src string and add it with the - // replacement, to the result - res += str.substring(pos, nextIndex) + new_; - // Increment our pointer in the src string - pos = nextIndex + old.length; - count++; - // See if there are any more replacements to be made - nextIndex = str.indexOf(old, pos); - } - - // We've either reached the end, or done the max # of - // replacements, tack on any remaining string - if(pos < str.length) { - res += str.substring(pos); - } - - return r.copySafeness(originalStr, res); - }, - - reverse: function(val) { - var arr; - if(lib.isString(val)) { - arr = filters.list(val); - } - else { - // Copy it - arr = lib.map(val, function(v) { return v; }); - } - - arr.reverse(); - - if(lib.isString(val)) { - return r.copySafeness(val, arr.join('')); - } - return arr; - }, - - round: function(val, precision, method) { - precision = precision || 0; - var factor = Math.pow(10, precision); - var rounder; - - if(method === 'ceil') { - rounder = Math.ceil; - } - else if(method === 'floor') { - rounder = Math.floor; - } - else { - rounder = Math.round; - } - - return rounder(val * factor) / factor; - }, - - slice: function(arr, slices, fillWith) { - var sliceLength = Math.floor(arr.length / slices); - var extra = arr.length % slices; - var offset = 0; - var res = []; - - for(var i=0; i= extra) { - slice.push(fillWith); - } - res.push(slice); - } - - return res; - }, - - sort: r.makeMacro(['value', 'reverse', 'case_sensitive', 'attribute'], [], function(arr, reverse, caseSens, attr) { - // Copy it - arr = lib.map(arr, function(v) { return v; }); - - arr.sort(function(a, b) { - var x, y; - - if(attr) { - x = a[attr]; - y = b[attr]; - } - else { - x = a; - y = b; - } - - if(!caseSens && lib.isString(x) && lib.isString(y)) { - x = x.toLowerCase(); - y = y.toLowerCase(); - } - - if(x < y) { - return reverse ? 1 : -1; - } - else if(x > y) { - return reverse ? -1: 1; - } - else { - return 0; - } - }); - - return arr; - }), - - string: function(obj) { - return r.copySafeness(obj, obj); - }, - - striptags: function(input, preserve_linebreaks) { - input = normalize(input, ''); - preserve_linebreaks = preserve_linebreaks || false; - var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>|/gi; - var trimmedInput = filters.trim(input.replace(tags, '')); - var res = ''; - if (preserve_linebreaks) { - res = trimmedInput - .replace(/^ +| +$/gm, '') // remove leading and trailing spaces - .replace(/ +/g, ' ') // squash adjacent spaces - .replace(/(\r\n)/g, '\n') // normalize linebreaks (CRLF -> LF) - .replace(/\n\n\n+/g, '\n\n'); // squash abnormal adjacent linebreaks - } else { - res = trimmedInput.replace(/\s+/gi, ' '); - } - return r.copySafeness(input, res); - }, - - title: function(str) { - str = normalize(str, ''); - var words = str.split(' '); - for(var i = 0; i < words.length; i++) { - words[i] = filters.capitalize(words[i]); - } - return r.copySafeness(str, words.join(' ')); - }, - - trim: function(str) { - return r.copySafeness(str, str.replace(/^\s*|\s*$/g, '')); - }, - - truncate: function(input, length, killwords, end) { - var orig = input; - input = normalize(input, ''); - length = length || 255; - - if (input.length <= length) - return input; - - if (killwords) { - input = input.substring(0, length); - } else { - var idx = input.lastIndexOf(' ', length); - if(idx === -1) { - idx = length; - } - - input = input.substring(0, idx); - } - - input += (end !== undefined && end !== null) ? end : '...'; - return r.copySafeness(orig, input); - }, - - upper: function(str) { - str = normalize(str, ''); - return str.toUpperCase(); - }, - - urlencode: function(obj) { - var enc = encodeURIComponent; - if (lib.isString(obj)) { - return enc(obj); - } else { - var parts; - if (lib.isArray(obj)) { - parts = obj.map(function(item) { - return enc(item[0]) + '=' + enc(item[1]); - }); - } else { - parts = []; - for (var k in obj) { - if (obj.hasOwnProperty(k)) { - parts.push(enc(k) + '=' + enc(obj[k])); - } - } - } - return parts.join('&'); - } - }, - - urlize: function(str, length, nofollow) { - if (isNaN(length)) length = Infinity; - - var noFollowAttr = (nofollow === true ? ' rel="nofollow"' : ''); - - // For the jinja regexp, see - // https://github.com/mitsuhiko/jinja2/blob/f15b814dcba6aa12bc74d1f7d0c881d55f7126be/jinja2/utils.py#L20-L23 - var puncRE = /^(?:\(|<|<)?(.*?)(?:\.|,|\)|\n|>)?$/; - // from http://blog.gerv.net/2011/05/html5_email_address_regexp/ - var emailRE = /^[\w.!#$%&'*+\-\/=?\^`{|}~]+@[a-z\d\-]+(\.[a-z\d\-]+)+$/i; - var httpHttpsRE = /^https?:\/\/.*$/; - var wwwRE = /^www\./; - var tldRE = /\.(?:org|net|com)(?:\:|\/|$)/; - - var words = str.split(/\s+/).filter(function(word) { - // If the word has no length, bail. This can happen for str with - // trailing whitespace. - return word && word.length; - }).map(function(word) { - var matches = word.match(puncRE); - var possibleUrl = matches && matches[1] || word; - - // url that starts with http or https - if (httpHttpsRE.test(possibleUrl)) - return '' + possibleUrl.substr(0, length) + ''; - - // url that starts with www. - if (wwwRE.test(possibleUrl)) - return '' + possibleUrl.substr(0, length) + ''; - - // an email address of the form username@domain.tld - if (emailRE.test(possibleUrl)) - return '' + possibleUrl + ''; - - // url that ends in .com, .org or .net that is not an email address - if (tldRE.test(possibleUrl)) - return '' + possibleUrl.substr(0, length) + ''; - - return word; - - }); - - return words.join(' '); - }, - - wordcount: function(str) { - str = normalize(str, ''); - var words = (str) ? str.match(/\w+/g) : null; - return (words) ? words.length : null; - }, - - 'float': function(val, def) { - var res = parseFloat(val); - return isNaN(res) ? def : res; - }, - - 'int': function(val, def) { - var res = parseInt(val, 10); - return isNaN(res) ? def : res; - } - }; - - // Aliases - filters.d = filters['default']; - filters.e = filters.escape; - - module.exports = filters; - - -/***/ }, -/* 8 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var lib = __webpack_require__(1); - var Obj = __webpack_require__(6); - - // Frames keep track of scoping both at compile-time and run-time so - // we know how to access variables. Block tags can introduce special - // variables, for example. - var Frame = Obj.extend({ - init: function(parent) { - this.variables = {}; - this.parent = parent; - this.topLevel = false; - }, - - set: function(name, val, resolveUp) { - // Allow variables with dots by automatically creating the - // nested structure - var parts = name.split('.'); - var obj = this.variables; - var frame = this; - - if(resolveUp) { - if((frame = this.resolve(parts[0]))) { - frame.set(name, val); - return; - } - frame = this; - } - - for(var i=0; i argNames.length) { - args = Array.prototype.slice.call(arguments, 0, argNames.length); - - // Positional arguments that should be passed in as - // keyword arguments (essentially default values) - var vals = Array.prototype.slice.call(arguments, args.length, argCount); - for(i = 0; i < vals.length; i++) { - if(i < kwargNames.length) { - kwargs[kwargNames[i]] = vals[i]; - } - } - - args.push(kwargs); - } - else if(argCount < argNames.length) { - args = Array.prototype.slice.call(arguments, 0, argCount); - - for(i = argCount; i < argNames.length; i++) { - var arg = argNames[i]; - - // Keyword arguments that should be passed as - // positional arguments, i.e. the caller explicitly - // used the name of a positional arg - args.push(kwargs[arg]); - delete kwargs[arg]; - } - - args.push(kwargs); - } - else { - args = arguments; - } - - return func.apply(this, args); - }; - } - - function makeKeywordArgs(obj) { - obj.__keywords = true; - return obj; - } - - function getKeywordArgs(args) { - var len = args.length; - if(len) { - var lastArg = args[len - 1]; - if(lastArg && lastArg.hasOwnProperty('__keywords')) { - return lastArg; - } - } - return {}; - } - - function numArgs(args) { - var len = args.length; - if(len === 0) { - return 0; - } - - var lastArg = args[len - 1]; - if(lastArg && lastArg.hasOwnProperty('__keywords')) { - return len - 1; - } - else { - return len; - } - } - - // A SafeString object indicates that the string should not be - // autoescaped. This happens magically because autoescaping only - // occurs on primitive string objects. - function SafeString(val) { - if(typeof val !== 'string') { - return val; - } - - this.val = val; - this.length = val.length; - } - - SafeString.prototype = Object.create(String.prototype, { - length: { writable: true, configurable: true, value: 0 } - }); - SafeString.prototype.valueOf = function() { - return this.val; - }; - SafeString.prototype.toString = function() { - return this.val; - }; - - function copySafeness(dest, target) { - if(dest instanceof SafeString) { - return new SafeString(target); - } - return target.toString(); - } - - function markSafe(val) { - var type = typeof val; - - if(type === 'string') { - return new SafeString(val); - } - else if(type !== 'function') { - return val; - } - else { - return function() { - var ret = val.apply(this, arguments); - - if(typeof ret === 'string') { - return new SafeString(ret); - } - - return ret; - }; - } - } - - function suppressValue(val, autoescape) { - val = (val !== undefined && val !== null) ? val : ''; - - if(autoescape && typeof val === 'string') { - val = lib.escape(val); - } - - return val; - } - - function ensureDefined(val, lineno, colno) { - if(val === null || val === undefined) { - throw new lib.TemplateError( - 'attempted to output null or undefined value', - lineno + 1, - colno + 1 - ); - } - return val; - } - - function memberLookup(obj, val) { - obj = obj || {}; - - if(typeof obj[val] === 'function') { - return function() { - return obj[val].apply(obj, arguments); - }; - } - - return obj[val]; - } - - function callWrap(obj, name, context, args) { - if(!obj) { - throw new Error('Unable to call `' + name + '`, which is undefined or falsey'); - } - else if(typeof obj !== 'function') { - throw new Error('Unable to call `' + name + '`, which is not a function'); - } - - // jshint validthis: true - return obj.apply(context, args); - } - - function contextOrFrameLookup(context, frame, name) { - var val = frame.lookup(name); - return (val !== undefined && val !== null) ? - val : - context.lookup(name); - } - - function handleError(error, lineno, colno) { - if(error.lineno) { - return error; - } - else { - return new lib.TemplateError(error, lineno, colno); - } - } - - function asyncEach(arr, dimen, iter, cb) { - if(lib.isArray(arr)) { - var len = arr.length; - - lib.asyncIter(arr, function(item, i, next) { - switch(dimen) { - case 1: iter(item, i, len, next); break; - case 2: iter(item[0], item[1], i, len, next); break; - case 3: iter(item[0], item[1], item[2], i, len, next); break; - default: - item.push(i, next); - iter.apply(this, item); - } - }, cb); - } - else { - lib.asyncFor(arr, function(key, val, i, len, next) { - iter(key, val, i, len, next); - }, cb); - } - } - - function asyncAll(arr, dimen, func, cb) { - var finished = 0; - var len, i; - var outputArr; - - function done(i, output) { - finished++; - outputArr[i] = output; - - if(finished === len) { - cb(null, outputArr.join('')); - } - } - - if(lib.isArray(arr)) { - len = arr.length; - outputArr = new Array(len); - - if(len === 0) { - cb(null, ''); - } - else { - for(i = 0; i < arr.length; i++) { - var item = arr[i]; - - switch(dimen) { - case 1: func(item, i, len, done); break; - case 2: func(item[0], item[1], i, len, done); break; - case 3: func(item[0], item[1], item[2], i, len, done); break; - default: - item.push(i, done); - // jshint validthis: true - func.apply(this, item); - } - } - } - } - else { - var keys = lib.keys(arr); - len = keys.length; - outputArr = new Array(len); - - if(len === 0) { - cb(null, ''); - } - else { - for(i = 0; i < keys.length; i++) { - var k = keys[i]; - func(k, arr[k], i, len, done); - } - } - } - } - - module.exports = { - Frame: Frame, - makeMacro: makeMacro, - makeKeywordArgs: makeKeywordArgs, - numArgs: numArgs, - suppressValue: suppressValue, - ensureDefined: ensureDefined, - memberLookup: memberLookup, - contextOrFrameLookup: contextOrFrameLookup, - callWrap: callWrap, - handleError: handleError, - isArray: lib.isArray, - keys: lib.keys, - SafeString: SafeString, - copySafeness: copySafeness, - markSafe: markSafe, - asyncEach: asyncEach, - asyncAll: asyncAll - }; - - -/***/ }, -/* 9 */ -/***/ function(module, exports) { - - 'use strict'; - - function cycler(items) { - var index = -1; - - return { - current: null, - reset: function() { - index = -1; - this.current = null; - }, - - next: function() { - index++; - if(index >= items.length) { - index = 0; - } - - this.current = items[index]; - return this.current; - }, - }; - - } - - function joiner(sep) { - sep = sep || ','; - var first = true; - - return function() { - var val = first ? '' : sep; - first = false; - return val; - }; - } - - // Making this a function instead so it returns a new object - // each time it's called. That way, if something like an environment - // uses it, they will each have their own copy. - function globals() { - return { - range: function(start, stop, step) { - if(!stop) { - stop = start; - start = 0; - step = 1; - } - else if(!step) { - step = 1; - } - - var arr = []; - var i; - if (step > 0) { - for (i=start; istop; i+=step) { - arr.push(i); - } - } - return arr; - }, - - // lipsum: function(n, html, min, max) { - // }, - - cycler: function() { - return cycler(Array.prototype.slice.call(arguments)); - }, - - joiner: function(sep) { - return joiner(sep); - } - }; - } - - module.exports = globals; - - -/***/ }, -/* 10 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var Loader = __webpack_require__(11); - - var PrecompiledLoader = Loader.extend({ - init: function(compiledTemplates) { - this.precompiled = compiledTemplates || {}; - }, - - getSource: function(name) { - if (this.precompiled[name]) { - return { - src: { type: 'code', - obj: this.precompiled[name] }, - path: name - }; - } - return null; - } - }); - - module.exports = PrecompiledLoader; - - -/***/ }, -/* 11 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var path = __webpack_require__(3); - var Obj = __webpack_require__(6); - var lib = __webpack_require__(1); - - var Loader = Obj.extend({ - on: function(name, func) { - this.listeners = this.listeners || {}; - this.listeners[name] = this.listeners[name] || []; - this.listeners[name].push(func); - }, - - emit: function(name /*, arg1, arg2, ...*/) { - var args = Array.prototype.slice.call(arguments, 1); - - if(this.listeners && this.listeners[name]) { - lib.each(this.listeners[name], function(listener) { - listener.apply(null, args); - }); - } - }, - - resolve: function(from, to) { - return path.resolve(path.dirname(from), to); - }, - - isRelative: function(filename) { - return (filename.indexOf('./') === 0 || filename.indexOf('../') === 0); - } - }); - - module.exports = Loader; - - -/***/ }, -/* 12 */ -/***/ function(module, exports) { - - function installCompat() { - 'use strict'; - - // This must be called like `nunjucks.installCompat` so that `this` - // references the nunjucks instance - var runtime = this.runtime; // jshint ignore:line - var lib = this.lib; // jshint ignore:line - - var orig_contextOrFrameLookup = runtime.contextOrFrameLookup; - runtime.contextOrFrameLookup = function(context, frame, key) { - var val = orig_contextOrFrameLookup.apply(this, arguments); - if (val === undefined) { - switch (key) { - case 'True': - return true; - case 'False': - return false; - case 'None': - return null; - } - } - - return val; - }; - - var orig_memberLookup = runtime.memberLookup; - var ARRAY_MEMBERS = { - pop: function(index) { - if (index === undefined) { - return this.pop(); - } - if (index >= this.length || index < 0) { - throw new Error('KeyError'); - } - return this.splice(index, 1); - }, - remove: function(element) { - for (var i = 0; i < this.length; i++) { - if (this[i] === element) { - return this.splice(i, 1); - } - } - throw new Error('ValueError'); - }, - count: function(element) { - var count = 0; - for (var i = 0; i < this.length; i++) { - if (this[i] === element) { - count++; - } - } - return count; - }, - index: function(element) { - var i; - if ((i = this.indexOf(element)) === -1) { - throw new Error('ValueError'); - } - return i; - }, - find: function(element) { - return this.indexOf(element); - }, - insert: function(index, elem) { - return this.splice(index, 0, elem); - } - }; - var OBJECT_MEMBERS = { - items: function() { - var ret = []; - for(var k in this) { - ret.push([k, this[k]]); - } - return ret; - }, - values: function() { - var ret = []; - for(var k in this) { - ret.push(this[k]); - } - return ret; - }, - keys: function() { - var ret = []; - for(var k in this) { - ret.push(k); - } - return ret; - }, - get: function(key, def) { - var output = this[key]; - if (output === undefined) { - output = def; - } - return output; - }, - has_key: function(key) { - return this.hasOwnProperty(key); - }, - pop: function(key, def) { - var output = this[key]; - if (output === undefined && def !== undefined) { - output = def; - } else if (output === undefined) { - throw new Error('KeyError'); - } else { - delete this[key]; - } - return output; - }, - popitem: function() { - for (var k in this) { - // Return the first object pair. - var val = this[k]; - delete this[k]; - return [k, val]; - } - throw new Error('KeyError'); - }, - setdefault: function(key, def) { - if (key in this) { - return this[key]; - } - if (def === undefined) { - def = null; - } - return this[key] = def; - }, - update: function(kwargs) { - for (var k in kwargs) { - this[k] = kwargs[k]; - } - return null; // Always returns None - } - }; - OBJECT_MEMBERS.iteritems = OBJECT_MEMBERS.items; - OBJECT_MEMBERS.itervalues = OBJECT_MEMBERS.values; - OBJECT_MEMBERS.iterkeys = OBJECT_MEMBERS.keys; - runtime.memberLookup = function(obj, val, autoescape) { // jshint ignore:line - obj = obj || {}; - - // If the object is an object, return any of the methods that Python would - // otherwise provide. - if (lib.isArray(obj) && ARRAY_MEMBERS.hasOwnProperty(val)) { - return function() {return ARRAY_MEMBERS[val].apply(obj, arguments);}; - } - - if (lib.isObject(obj) && OBJECT_MEMBERS.hasOwnProperty(val)) { - return function() {return OBJECT_MEMBERS[val].apply(obj, arguments);}; - } - - return orig_memberLookup.apply(this, arguments); - }; - } - - module.exports = installCompat; - - -/***/ } -/******/ ]); \ No newline at end of file diff --git a/certidude/static/js/nunjucks-slim.min.js b/certidude/static/js/nunjucks-slim.min.js index 08bde32..cb70b7e 100644 --- a/certidude/static/js/nunjucks-slim.min.js +++ b/certidude/static/js/nunjucks-slim.min.js @@ -1,2 +1,2 @@ -/*! Browser bundle of nunjucks 2.3.0 (slim, only works with precompiled templates) */ -var nunjucks=function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={exports:{},id:n,loaded:!1};return t[n].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}([function(t,e,r){"use strict";var n=r(1),i=r(2),o=r(11),s=r(3),u=r(3);t.exports={},t.exports.Environment=i.Environment,t.exports.Template=i.Template,t.exports.Loader=o,t.exports.FileSystemLoader=s.FileSystemLoader,t.exports.PrecompiledLoader=s.PrecompiledLoader,t.exports.WebLoader=s.WebLoader,t.exports.compiler=r(3),t.exports.parser=r(3),t.exports.lexer=r(3),t.exports.runtime=r(8),t.exports.lib=n,t.exports.nodes=r(3),t.exports.installJinjaCompat=r(12);var a;t.exports.configure=function(t,e){e=e||{},n.isObject(t)&&(e=t,t=null);var r;return s.FileSystemLoader?r=new s.FileSystemLoader(t,{watch:e.watch,noCache:e.noCache}):s.WebLoader&&(r=new s.WebLoader(t,{useCache:e.web&&e.web.useCache,async:e.web&&e.web.async})),a=new i.Environment(r,e),e&&e.express&&a.express(e.express),a},t.exports.compile=function(e,r,n,i){return a||t.exports.configure(),new t.exports.Template(e,r,n,i)},t.exports.render=function(e,r,n){return a||t.exports.configure(),a.render(e,r,n)},t.exports.renderString=function(e,r,n){return a||t.exports.configure(),a.renderString(e,r,n)},u&&(t.exports.precompile=u.precompile,t.exports.precompileString=u.precompileString)},function(t,e){"use strict";var r=Array.prototype,n=Object.prototype,i={"&":"&",'"':""","'":"'","<":"<",">":">"},o=/[&"'<>]/g,s=function(t){return i[t]},e=t.exports={};e.prettifyError=function(t,r,n){if(n.Update||(n=new e.TemplateError(n)),n.Update(t),!r){var i=n;n=new Error(i.message),n.name=i.name}return n},e.TemplateError=function(t,e,r){var n=this;if(t instanceof Error){n=t,t=t.name+": "+t.message;try{n.name=""}catch(i){n=this}}else Error.captureStackTrace&&Error.captureStackTrace(n);return n.name="Template render error",n.message=t,n.lineno=e,n.colno=r,n.firstUpdate=!0,n.Update=function(t){var e="("+(t||"unknown path")+")";return this.firstUpdate&&(this.lineno&&this.colno?e+=" [Line "+this.lineno+", Column "+this.colno+"]":this.lineno&&(e+=" [Line "+this.lineno+"]")),e+="\n ",this.firstUpdate&&(e+=" "),this.message=e+(this.message||""),this.firstUpdate=!1,this},n},e.TemplateError.prototype=Error.prototype,e.escape=function(t){return t.replace(o,s)},e.isFunction=function(t){return"[object Function]"===n.toString.call(t)},e.isArray=Array.isArray||function(t){return"[object Array]"===n.toString.call(t)},e.isString=function(t){return"[object String]"===n.toString.call(t)},e.isObject=function(t){return"[object Object]"===n.toString.call(t)},e.groupBy=function(t,r){for(var n={},i=e.isFunction(r)?r:function(t){return t[r]},o=0;on;n++)r+=t;return r},e.each=function(t,e,n){if(null!=t)if(r.each&&t.each===r.each)t.forEach(e,n);else if(t.length===+t.length)for(var i=0,o=t.length;o>i;i++)e.call(n,t[i],i,t)},e.map=function(t,e){var n=[];if(null==t)return n;if(r.map&&t.map===r.map)return t.map(e);for(var i=0;iu?r(e,t[e],u,s,i):n()}var o=e.keys(t),s=o.length,u=-1;i()},e.indexOf=Array.prototype.indexOf?function(t,e,r){return Array.prototype.indexOf.call(t,e,r)}:function(t,e,r){var n=this.length>>>0;for(r=+r||0,Math.abs(r)===1/0&&(r=0),0>r&&(r+=n,0>r&&(r=0));n>r;r++)if(t[r]===e)return r;return-1},Array.prototype.map||(Array.prototype.map=function(){throw new Error("map is unimplemented for this js engine")}),e.keys=function(t){if(Object.prototype.keys)return t.keys();var e=[];for(var r in t)t.hasOwnProperty(r)&&e.push(r);return e}},function(t,e,r){"use strict";function n(t,e,r){s(function(){t(e,r)})}var i,o=r(3),s=r(4),u=r(1),a=r(6),c=r(3),l=r(7),f=r(3),p=r(8),h=r(9),v=p.Frame;f.PrecompiledLoader=r(10);var d=a.extend({init:function(t,e){e=this.opts=e||{},this.opts.dev=!!e.dev,this.opts.autoescape=null!=e.autoescape?e.autoescape:!0,this.opts.throwOnUndefined=!!e.throwOnUndefined,this.opts.trimBlocks=!!e.trimBlocks,this.opts.lstripBlocks=!!e.lstripBlocks,this.loaders=[],t?this.loaders=u.isArray(t)?t:[t]:f.FileSystemLoader?this.loaders=[new f.FileSystemLoader("views")]:f.WebLoader&&(this.loaders=[new f.WebLoader("/views")]),window.nunjucksPrecompiled&&this.loaders.unshift(new f.PrecompiledLoader(window.nunjucksPrecompiled)),this.initCache(),this.globals=h(),this.filters={},this.asyncFilters=[],this.extensions={},this.extensionsList=[];for(var r in l)this.addFilter(r,l[r])},initCache:function(){u.each(this.loaders,function(t){t.cache={},"function"==typeof t.on&&t.on("update",function(e){t.cache[e]=null})})},addExtension:function(t,e){return e._name=t,this.extensions[t]=e,this.extensionsList.push(e),this},removeExtension:function(t){var e=this.getExtension(t);e&&(this.extensionsList=u.without(this.extensionsList,e),delete this.extensions[t])},getExtension:function(t){return this.extensions[t]},hasExtension:function(t){return!!this.extensions[t]},addGlobal:function(t,e){return this.globals[t]=e,this},getGlobal:function(t){if(!this.globals[t])throw new Error("global not found: "+t);return this.globals[t]},addFilter:function(t,e,r){var n=e;return r&&this.asyncFilters.push(t),this.filters[t]=n,this},getFilter:function(t){if(!this.filters[t])throw new Error("filter not found: "+t);return this.filters[t]},resolveTemplate:function(t,e,r){var n=t.isRelative&&e?t.isRelative(r):!1;return n&&t.resolve?t.resolve(e,r):r},getTemplate:function(t,e,r,n,o){var s=this,a=null;if(t&&t.raw&&(t=t.raw),u.isFunction(r)&&(o=r,r=null,e=e||!1),u.isFunction(e)&&(o=e,e=!1),t instanceof i)a=t;else{if("string"!=typeof t)throw new Error("template names must be a string: "+t);for(var c=0;cl){for(var e=0,r=u.length-c;r>e;e++)u[e]=u[e+c];u.length-=c,c=0}}u.length=0,c=0,a=!1}function i(t){var e=1,r=new f(t),n=document.createTextNode("");return r.observe(n,{characterData:!0}),function(){e=-e,n.data=e}}function o(t){return function(){function e(){clearTimeout(r),clearInterval(n),t()}var r=setTimeout(e,0),n=setInterval(e,50)}}t.exports=r;var s,u=[],a=!1,c=0,l=1024,f=e.MutationObserver||e.WebKitMutationObserver;s="function"==typeof f?i(n):o(n),r.requestFlush=s,r.makeRequestCallFromTimer=o}).call(e,function(){return this}())},function(t,e){"use strict";function r(t,e,n){var i=function(){};i.prototype=t.prototype;var o=new i,s=/xyz/.test(function(){xyz})?/\bparent\b/:/.*/;n=n||{};for(var u in n){var a=n[u],c=o[u];"function"==typeof c&&"function"==typeof a&&s.test(a)?o[u]=function(t,e){return function(){var r=this.parent;this.parent=e;var n=t.apply(this,arguments);return this.parent=r,n}}(a,c):o[u]=a}o.typename=e;var l=function(){o.init&&o.init.apply(this,arguments)};return l.prototype=o,l.prototype.constructor=l,l.extend=function(t,e){return"object"==typeof t&&(e=t,t="anonymous"),r(l,t,e)},l}t.exports=r(Object,"Object",{})},function(t,e,r){"use strict";function n(t,e){return null===t||void 0===t||t===!1?e:t}var i=r(1),o=r(8),s={abs:function(t){return Math.abs(t)},batch:function(t,e,r){var n,i=[],o=[];for(n=0;nn;n++)o.push(r);i.push(o)}return i},capitalize:function(t){t=n(t,"");var e=t.toLowerCase();return o.copySafeness(t,e.charAt(0).toUpperCase()+e.slice(1))},center:function(t,e){if(t=n(t,""),e=e||80,t.length>=e)return t;var r=e-t.length,s=i.repeat(" ",r/2-r%2),u=i.repeat(" ",r/2);return o.copySafeness(t,s+t+u)},"default":function(t,e,r){return r?t?t:e:void 0!==t?t:e},dictsort:function(t,e,r){if(!i.isObject(t))throw new i.TemplateError("dictsort filter: val must be an object");var n=[];for(var o in t)n.push([o,t[o]]);var s;if(void 0===r||"key"===r)s=0;else{if("value"!==r)throw new i.TemplateError("dictsort filter: You can only sort by either key or value");s=1}return n.sort(function(t,r){var n=t[s],o=r[s];return e||(i.isString(n)&&(n=n.toUpperCase()),i.isString(o)&&(o=o.toUpperCase())),n>o?1:n===o?0:-1}),n},dump:function(t){return JSON.stringify(t)},escape:function(t){return"string"==typeof t||t instanceof o.SafeString?i.escape(t):t},safe:function(t){return o.markSafe(t)},first:function(t){return t[0]},groupby:function(t,e){return i.groupBy(t,e)},indent:function(t,e,r){if(t=n(t,""),""===t)return"";e=e||4;for(var s="",u=t.split("\n"),a=i.repeat(" ",e),c=0;c-1&&(-1===n||n>c);)s+=t.substring(a,u)+r,a=u+e.length,c++,u=t.indexOf(e,a);return au;u++){var a=o+u*n;i>u&&o++;var c=o+(u+1)*n,l=t.slice(a,c);r&&u>=i&&l.push(r),s.push(l)}return s},sort:o.makeMacro(["value","reverse","case_sensitive","attribute"],[],function(t,e,r,n){return t=i.map(t,function(t){return t}),t.sort(function(t,o){var s,u;return n?(s=t[n],u=o[n]):(s=t,u=o),!r&&i.isString(s)&&i.isString(u)&&(s=s.toLowerCase(),u=u.toLowerCase()),u>s?e?1:-1:s>u?e?-1:1:0}),t}),string:function(t){return o.copySafeness(t,t)},striptags:function(t,e){t=n(t,""),e=e||!1;var r=/<\/?([a-z][a-z0-9]*)\b[^>]*>|/gi,i=s.trim(t.replace(r,"")),u="";return u=e?i.replace(/^ +| +$/gm,"").replace(/ +/g," ").replace(/(\r\n)/g,"\n").replace(/\n\n\n+/g,"\n\n"):i.replace(/\s+/gi," "),o.copySafeness(t,u)},title:function(t){t=n(t,"");for(var e=t.split(" "),r=0;r"+c.substr(0,e)+"":u.test(c)?'"+c.substr(0,e)+"":o.test(c)?''+c+"":a.test(c)?'"+c.substr(0,e)+"":t});return c.join(" ")},wordcount:function(t){t=n(t,"");var e=t?t.match(/\w+/g):null;return e?e.length:null},"float":function(t,e){var r=parseFloat(t);return isNaN(r)?e:r},"int":function(t,e){var r=parseInt(t,10);return isNaN(r)?e:r}};s.d=s["default"],s.e=s.escape,t.exports=s},function(t,e,r){"use strict";function n(t,e,r){return function(){var n,i,u=s(arguments),a=o(arguments);if(u>t.length){n=Array.prototype.slice.call(arguments,0,t.length);var c=Array.prototype.slice.call(arguments,n.length,u);for(i=0;i=t.length&&(e=0),this.current=t[e],this.current}}}function n(t){t=t||",";var e=!0;return function(){var r=e?"":t;return e=!1,r}}function i(){return{range:function(t,e,r){e?r||(r=1):(e=t,t=0,r=1);var n,i=[];if(r>0)for(n=t;e>n;n+=r)i.push(n);else for(n=t;n>e;n+=r)i.push(n);return i},cycler:function(){return r(Array.prototype.slice.call(arguments))},joiner:function(t){return n(t)}}}t.exports=i},function(t,e,r){"use strict";var n=r(11),i=n.extend({init:function(t){this.precompiled=t||{}},getSource:function(t){return this.precompiled[t]?{src:{type:"code",obj:this.precompiled[t]},path:t}:null}});t.exports=i},function(t,e,r){"use strict";var n=r(3),i=r(6),o=r(1),s=i.extend({on:function(t,e){this.listeners=this.listeners||{},this.listeners[t]=this.listeners[t]||[],this.listeners[t].push(e)},emit:function(t){var e=Array.prototype.slice.call(arguments,1);this.listeners&&this.listeners[t]&&o.each(this.listeners[t],function(t){t.apply(null,e)})},resolve:function(t,e){return n.resolve(n.dirname(t),e)},isRelative:function(t){return 0===t.indexOf("./")||0===t.indexOf("../")}});t.exports=s},function(t,e){function r(){"use strict";var t=this.runtime,e=this.lib,r=t.contextOrFrameLookup;t.contextOrFrameLookup=function(t,e,n){var i=r.apply(this,arguments);if(void 0===i)switch(n){case"True":return!0;case"False":return!1;case"None":return null}return i};var n=t.memberLookup,i={pop:function(t){if(void 0===t)return this.pop();if(t>=this.length||0>t)throw new Error("KeyError");return this.splice(t,1)},remove:function(t){for(var e=0;e":">"},o=/[&"'<>]/g,s=function(t){return i[t]},e=t.exports={};e.prettifyError=function(t,r,n){if(n.Update||(n=new e.TemplateError(n)),n.Update(t),!r){var i=n;n=new Error(i.message),n.name=i.name}return n},e.TemplateError=function(t,e,r){var n=this;if(t instanceof Error){n=t,t=t.name+": "+t.message;try{n.name=""}catch(i){n=this}}else Error.captureStackTrace&&Error.captureStackTrace(n);return n.name="Template render error",n.message=t,n.lineno=e,n.colno=r,n.firstUpdate=!0,n.Update=function(t){var e="("+(t||"unknown path")+")";return this.firstUpdate&&(this.lineno&&this.colno?e+=" [Line "+this.lineno+", Column "+this.colno+"]":this.lineno&&(e+=" [Line "+this.lineno+"]")),e+="\n ",this.firstUpdate&&(e+=" "),this.message=e+(this.message||""),this.firstUpdate=!1,this},n},e.TemplateError.prototype=Error.prototype,e.escape=function(t){return t.replace(o,s)},e.isFunction=function(t){return"[object Function]"===n.toString.call(t)},e.isArray=Array.isArray||function(t){return"[object Array]"===n.toString.call(t)},e.isString=function(t){return"[object String]"===n.toString.call(t)},e.isObject=function(t){return"[object Object]"===n.toString.call(t)},e.groupBy=function(t,r){for(var n={},i=e.isFunction(r)?r:function(t){return t[r]},o=0;on;n++)r+=t;return r},e.each=function(t,e,n){if(null!=t)if(r.each&&t.each===r.each)t.forEach(e,n);else if(t.length===+t.length)for(var i=0,o=t.length;o>i;i++)e.call(n,t[i],i,t)},e.map=function(t,e){var n=[];if(null==t)return n;if(r.map&&t.map===r.map)return t.map(e);for(var i=0;iu?r(e,t[e],u,s,i):n()}var o=e.keys(t),s=o.length,u=-1;i()},e.indexOf=Array.prototype.indexOf?function(t,e,r){return Array.prototype.indexOf.call(t,e,r)}:function(t,e,r){var n=this.length>>>0;for(r=+r||0,Math.abs(r)===1/0&&(r=0),0>r&&(r+=n,0>r&&(r=0));n>r;r++)if(t[r]===e)return r;return-1},Array.prototype.map||(Array.prototype.map=function(){throw new Error("map is unimplemented for this js engine")}),e.keys=function(t){if(Object.prototype.keys)return t.keys();var e=[];for(var r in t)t.hasOwnProperty(r)&&e.push(r);return e},e.inOperator=function(t,r){if(e.isArray(r))return-1!==e.indexOf(r,t);if(e.isObject(r))return t in r;throw new Error('Cannot use "in" operator to search for "'+t+'" in unexpected types.')}},function(t,e,r){"use strict";function n(t,e,r){s(function(){t(e,r)})}var i,o=r(3),s=r(4),u=r(1),a=r(6),c=r(3),l=r(7),f=r(3),p=r(8),h=r(9),v=p.Frame;f.PrecompiledLoader=r(10);var d=a.extend({init:function(t,e){e=this.opts=e||{},this.opts.dev=!!e.dev,this.opts.autoescape=null!=e.autoescape?e.autoescape:!0,this.opts.throwOnUndefined=!!e.throwOnUndefined,this.opts.trimBlocks=!!e.trimBlocks,this.opts.lstripBlocks=!!e.lstripBlocks,this.loaders=[],t?this.loaders=u.isArray(t)?t:[t]:f.FileSystemLoader?this.loaders=[new f.FileSystemLoader("views")]:f.WebLoader&&(this.loaders=[new f.WebLoader("/views")]),window.nunjucksPrecompiled&&this.loaders.unshift(new f.PrecompiledLoader(window.nunjucksPrecompiled)),this.initCache(),this.globals=h(),this.filters={},this.asyncFilters=[],this.extensions={},this.extensionsList=[];for(var r in l)this.addFilter(r,l[r])},initCache:function(){u.each(this.loaders,function(t){t.cache={},"function"==typeof t.on&&t.on("update",function(e){t.cache[e]=null})})},addExtension:function(t,e){return e._name=t,this.extensions[t]=e,this.extensionsList.push(e),this},removeExtension:function(t){var e=this.getExtension(t);e&&(this.extensionsList=u.without(this.extensionsList,e),delete this.extensions[t])},getExtension:function(t){return this.extensions[t]},hasExtension:function(t){return!!this.extensions[t]},addGlobal:function(t,e){return this.globals[t]=e,this},getGlobal:function(t){if("undefined"==typeof this.globals[t])throw new Error("global not found: "+t);return this.globals[t]},addFilter:function(t,e,r){var n=e;return r&&this.asyncFilters.push(t),this.filters[t]=n,this},getFilter:function(t){if(!this.filters[t])throw new Error("filter not found: "+t);return this.filters[t]},resolveTemplate:function(t,e,r){var n=t.isRelative&&e?t.isRelative(r):!1;return n&&t.resolve?t.resolve(e,r):r},getTemplate:function(t,e,r,n,o){var s=this,a=null;if(t&&t.raw&&(t=t.raw),u.isFunction(r)&&(o=r,r=null,e=e||!1),u.isFunction(e)&&(o=e,e=!1),t instanceof i)a=t;else{if("string"!=typeof t)throw new Error("template names must be a string: "+t);for(var c=0;cl){for(var e=0,r=u.length-c;r>e;e++)u[e]=u[e+c];u.length-=c,c=0}}u.length=0,c=0,a=!1}function i(t){var e=1,r=new f(t),n=document.createTextNode("");return r.observe(n,{characterData:!0}),function(){e=-e,n.data=e}}function o(t){return function(){function e(){clearTimeout(r),clearInterval(n),t()}var r=setTimeout(e,0),n=setInterval(e,50)}}t.exports=r;var s,u=[],a=!1,c=0,l=1024,f=e.MutationObserver||e.WebKitMutationObserver;s="function"==typeof f?i(n):o(n),r.requestFlush=s,r.makeRequestCallFromTimer=o}).call(e,function(){return this}())},function(t,e){"use strict";function r(t,e,n){var i=function(){};i.prototype=t.prototype;var o=new i,s=/xyz/.test(function(){xyz})?/\bparent\b/:/.*/;n=n||{};for(var u in n){var a=n[u],c=o[u];"function"==typeof c&&"function"==typeof a&&s.test(a)?o[u]=function(t,e){return function(){var r=this.parent;this.parent=e;var n=t.apply(this,arguments);return this.parent=r,n}}(a,c):o[u]=a}o.typename=e;var l=function(){o.init&&o.init.apply(this,arguments)};return l.prototype=o,l.prototype.constructor=l,l.extend=function(t,e){return"object"==typeof t&&(e=t,t="anonymous"),r(l,t,e)},l}t.exports=r(Object,"Object",{})},function(t,e,r){"use strict";function n(t,e){return null===t||void 0===t||t===!1?e:t}var i=r(1),o=r(8),s={abs:function(t){return Math.abs(t)},batch:function(t,e,r){var n,i=[],o=[];for(n=0;nn;n++)o.push(r);i.push(o)}return i},capitalize:function(t){t=n(t,"");var e=t.toLowerCase();return o.copySafeness(t,e.charAt(0).toUpperCase()+e.slice(1))},center:function(t,e){if(t=n(t,""),e=e||80,t.length>=e)return t;var r=e-t.length,s=i.repeat(" ",r/2-r%2),u=i.repeat(" ",r/2);return o.copySafeness(t,s+t+u)},"default":function(t,e,r){return r?t?t:e:void 0!==t?t:e},dictsort:function(t,e,r){if(!i.isObject(t))throw new i.TemplateError("dictsort filter: val must be an object");var n=[];for(var o in t)n.push([o,t[o]]);var s;if(void 0===r||"key"===r)s=0;else{if("value"!==r)throw new i.TemplateError("dictsort filter: You can only sort by either key or value");s=1}return n.sort(function(t,r){var n=t[s],o=r[s];return e||(i.isString(n)&&(n=n.toUpperCase()),i.isString(o)&&(o=o.toUpperCase())),n>o?1:n===o?0:-1}),n},dump:function(t){return JSON.stringify(t)},escape:function(t){return"string"==typeof t||t instanceof o.SafeString?i.escape(t):t},safe:function(t){return o.markSafe(t)},first:function(t){return t[0]},groupby:function(t,e){return i.groupBy(t,e)},indent:function(t,e,r){if(t=n(t,""),""===t)return"";e=e||4;for(var s="",u=t.split("\n"),a=i.repeat(" ",e),c=0;c-1&&(-1===n||n>c);)s+=t.substring(a,u)+r,a=u+e.length,c++,u=t.indexOf(e,a);return au;u++){var a=o+u*n;i>u&&o++;var c=o+(u+1)*n,l=t.slice(a,c);r&&u>=i&&l.push(r),s.push(l)}return s},sum:function(t,e,r){var n=0;"number"==typeof r&&(n+=r),e&&(t=i.map(t,function(t){return t[e]}));for(var o=0;os?e?1:-1:s>u?e?-1:1:0}),t}),string:function(t){return o.copySafeness(t,t)},striptags:function(t,e){t=n(t,""),e=e||!1;var r=/<\/?([a-z][a-z0-9]*)\b[^>]*>|/gi,i=s.trim(t.replace(r,"")),u="";return u=e?i.replace(/^ +| +$/gm,"").replace(/ +/g," ").replace(/(\r\n)/g,"\n").replace(/\n\n\n+/g,"\n\n"):i.replace(/\s+/gi," "),o.copySafeness(t,u)},title:function(t){t=n(t,"");for(var e=t.split(" "),r=0;r"+c.substr(0,e)+"":u.test(c)?'"+c.substr(0,e)+"":o.test(c)?''+c+"":a.test(c)?'"+c.substr(0,e)+"":t});return c.join("")},wordcount:function(t){t=n(t,"");var e=t?t.match(/\w+/g):null;return e?e.length:null},"float":function(t,e){var r=parseFloat(t);return isNaN(r)?e:r},"int":function(t,e){var r=parseInt(t,10);return isNaN(r)?e:r}};s.d=s["default"],s.e=s.escape,t.exports=s},function(t,e,r){"use strict";function n(t,e,r){return function(){var n,i,u=s(arguments),a=o(arguments);if(u>t.length){n=Array.prototype.slice.call(arguments,0,t.length);var c=Array.prototype.slice.call(arguments,n.length,u);for(i=0;i=t.length&&(e=0),this.current=t[e],this.current}}}function n(t){t=t||",";var e=!0;return function(){var r=e?"":t;return e=!1,r}}function i(){return{range:function(t,e,r){"undefined"==typeof e?(e=t,t=0,r=1):r||(r=1);var n,i=[];if(r>0)for(n=t;e>n;n+=r)i.push(n);else for(n=t;n>e;n+=r)i.push(n);return i},cycler:function(){return r(Array.prototype.slice.call(arguments))},joiner:function(t){return n(t)}}}t.exports=i},function(t,e,r){"use strict";var n=r(11),i=n.extend({init:function(t){this.precompiled=t||{}},getSource:function(t){return this.precompiled[t]?{src:{type:"code",obj:this.precompiled[t]},path:t}:null}});t.exports=i},function(t,e,r){"use strict";var n=r(3),i=r(6),o=r(1),s=i.extend({on:function(t,e){this.listeners=this.listeners||{},this.listeners[t]=this.listeners[t]||[],this.listeners[t].push(e)},emit:function(t){var e=Array.prototype.slice.call(arguments,1);this.listeners&&this.listeners[t]&&o.each(this.listeners[t],function(t){t.apply(null,e)})},resolve:function(t,e){return n.resolve(n.dirname(t),e)},isRelative:function(t){return 0===t.indexOf("./")||0===t.indexOf("../")}});t.exports=s},function(t,e){function r(){"use strict";var t=this.runtime,e=this.lib,r=t.contextOrFrameLookup;t.contextOrFrameLookup=function(t,e,n){var i=r.apply(this,arguments);if(void 0===i)switch(n){case"True":return!0;case"False":return!1;case"None":return null}return i};var n=t.memberLookup,i={pop:function(t){if(void 0===t)return this.pop();if(t>=this.length||0>t)throw new Error("KeyError");return this.splice(t,1)},remove:function(t){for(var e=0;e': '>' - }; - - var escapeRegex = /[&"'<>]/g; - - var lookupEscape = function(ch) { - return escapeMap[ch]; - }; - - var exports = module.exports = {}; - - exports.prettifyError = function(path, withInternals, err) { - // jshint -W022 - // http://jslinterrors.com/do-not-assign-to-the-exception-parameter - if (!err.Update) { - // not one of ours, cast it - err = new exports.TemplateError(err); - } - err.Update(path); - - // Unless they marked the dev flag, show them a trace from here - if (!withInternals) { - var old = err; - err = new Error(old.message); - err.name = old.name; - } - - return err; - }; - - exports.TemplateError = function(message, lineno, colno) { - var err = this; - - if (message instanceof Error) { // for casting regular js errors - err = message; - message = message.name + ': ' + message.message; - - try { - if(err.name = '') {} - } - catch(e) { - // If we can't set the name of the error object in this - // environment, don't use it - err = this; - } - } else { - if(Error.captureStackTrace) { - Error.captureStackTrace(err); - } - } - - err.name = 'Template render error'; - err.message = message; - err.lineno = lineno; - err.colno = colno; - err.firstUpdate = true; - - err.Update = function(path) { - var message = '(' + (path || 'unknown path') + ')'; - - // only show lineno + colno next to path of template - // where error occurred - if (this.firstUpdate) { - if(this.lineno && this.colno) { - message += ' [Line ' + this.lineno + ', Column ' + this.colno + ']'; - } - else if(this.lineno) { - message += ' [Line ' + this.lineno + ']'; - } - } - - message += '\n '; - if (this.firstUpdate) { - message += ' '; - } - - this.message = message + (this.message || ''); - this.firstUpdate = false; - return this; - }; - - return err; - }; - - exports.TemplateError.prototype = Error.prototype; - - exports.escape = function(val) { - return val.replace(escapeRegex, lookupEscape); - }; - - exports.isFunction = function(obj) { - return ObjProto.toString.call(obj) === '[object Function]'; - }; - - exports.isArray = Array.isArray || function(obj) { - return ObjProto.toString.call(obj) === '[object Array]'; - }; - - exports.isString = function(obj) { - return ObjProto.toString.call(obj) === '[object String]'; - }; - - exports.isObject = function(obj) { - return ObjProto.toString.call(obj) === '[object Object]'; - }; - - exports.groupBy = function(obj, val) { - var result = {}; - var iterator = exports.isFunction(val) ? val : function(obj) { return obj[val]; }; - for(var i=0; i>> 0; // Hack to convert object.length to a UInt32 - - fromIndex = +fromIndex || 0; - - if(Math.abs(fromIndex) === Infinity) { - fromIndex = 0; - } - - if(fromIndex < 0) { - fromIndex += length; - if (fromIndex < 0) { - fromIndex = 0; - } - } - - for(;fromIndex < length; fromIndex++) { - if (arr[fromIndex] === searchElement) { - return fromIndex; - } - } - - return -1; - }; - - if(!Array.prototype.map) { - Array.prototype.map = function() { - throw new Error('map is unimplemented for this js engine'); - }; - } - - exports.keys = function(obj) { - if(Object.prototype.keys) { - return obj.keys(); - } - else { - var keys = []; - for(var k in obj) { - if(obj.hasOwnProperty(k)) { - keys.push(k); - } - } - return keys; - } - }; - - -/***/ }, -/* 2 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var path = __webpack_require__(3); - var asap = __webpack_require__(4); - var lib = __webpack_require__(1); - var Obj = __webpack_require__(6); - var compiler = __webpack_require__(7); - var builtin_filters = __webpack_require__(13); - var builtin_loaders = __webpack_require__(14); - var runtime = __webpack_require__(12); - var globals = __webpack_require__(17); - var Frame = runtime.Frame; - var Template; - - // Unconditionally load in this loader, even if no other ones are - // included (possible in the slim browser build) - builtin_loaders.PrecompiledLoader = __webpack_require__(16); - - // If the user is using the async API, *always* call it - // asynchronously even if the template was synchronous. - function callbackAsap(cb, err, res) { - asap(function() { cb(err, res); }); - } - - var Environment = Obj.extend({ - init: function(loaders, opts) { - // The dev flag determines the trace that'll be shown on errors. - // If set to true, returns the full trace from the error point, - // otherwise will return trace starting from Template.render - // (the full trace from within nunjucks may confuse developers using - // the library) - // defaults to false - opts = this.opts = opts || {}; - this.opts.dev = !!opts.dev; - - // The autoescape flag sets global autoescaping. If true, - // every string variable will be escaped by default. - // If false, strings can be manually escaped using the `escape` filter. - // defaults to true - this.opts.autoescape = opts.autoescape != null ? opts.autoescape : true; - - // If true, this will make the system throw errors if trying - // to output a null or undefined value - this.opts.throwOnUndefined = !!opts.throwOnUndefined; - this.opts.trimBlocks = !!opts.trimBlocks; - this.opts.lstripBlocks = !!opts.lstripBlocks; - - this.loaders = []; - - if(!loaders) { - // The filesystem loader is only available server-side - if(builtin_loaders.FileSystemLoader) { - this.loaders = [new builtin_loaders.FileSystemLoader('views')]; - } - else if(builtin_loaders.WebLoader) { - this.loaders = [new builtin_loaders.WebLoader('/views')]; - } - } - else { - this.loaders = lib.isArray(loaders) ? loaders : [loaders]; - } - - // It's easy to use precompiled templates: just include them - // before you configure nunjucks and this will automatically - // pick it up and use it - if((true) && window.nunjucksPrecompiled) { - this.loaders.unshift( - new builtin_loaders.PrecompiledLoader(window.nunjucksPrecompiled) - ); - } - - this.initCache(); - - this.globals = globals(); - this.filters = {}; - this.asyncFilters = []; - this.extensions = {}; - this.extensionsList = []; - - for(var name in builtin_filters) { - this.addFilter(name, builtin_filters[name]); - } - }, - - initCache: function() { - // Caching and cache busting - lib.each(this.loaders, function(loader) { - loader.cache = {}; - - if(typeof loader.on === 'function') { - loader.on('update', function(template) { - loader.cache[template] = null; - }); - } - }); - }, - - addExtension: function(name, extension) { - extension._name = name; - this.extensions[name] = extension; - this.extensionsList.push(extension); - return this; - }, - - removeExtension: function(name) { - var extension = this.getExtension(name); - if (!extension) return; - - this.extensionsList = lib.without(this.extensionsList, extension); - delete this.extensions[name]; - }, - - getExtension: function(name) { - return this.extensions[name]; - }, - - hasExtension: function(name) { - return !!this.extensions[name]; - }, - - addGlobal: function(name, value) { - this.globals[name] = value; - return this; - }, - - getGlobal: function(name) { - if(!this.globals[name]) { - throw new Error('global not found: ' + name); - } - return this.globals[name]; - }, - - addFilter: function(name, func, async) { - var wrapped = func; - - if(async) { - this.asyncFilters.push(name); - } - this.filters[name] = wrapped; - return this; - }, - - getFilter: function(name) { - if(!this.filters[name]) { - throw new Error('filter not found: ' + name); - } - return this.filters[name]; - }, - - resolveTemplate: function(loader, parentName, filename) { - var isRelative = (loader.isRelative && parentName)? loader.isRelative(filename) : false; - return (isRelative && loader.resolve)? loader.resolve(parentName, filename) : filename; - }, - - getTemplate: function(name, eagerCompile, parentName, ignoreMissing, cb) { - var that = this; - var tmpl = null; - if(name && name.raw) { - // this fixes autoescape for templates referenced in symbols - name = name.raw; - } - - if(lib.isFunction(parentName)) { - cb = parentName; - parentName = null; - eagerCompile = eagerCompile || false; - } - - if(lib.isFunction(eagerCompile)) { - cb = eagerCompile; - eagerCompile = false; - } - - if (name instanceof Template) { - tmpl = name; - } - else if(typeof name !== 'string') { - throw new Error('template names must be a string: ' + name); - } - else { - for (var i = 0; i < this.loaders.length; i++) { - var _name = this.resolveTemplate(this.loaders[i], parentName, name); - tmpl = this.loaders[i].cache[_name]; - if (tmpl) break; - } - } - - if(tmpl) { - if(eagerCompile) { - tmpl.compile(); - } - - if(cb) { - cb(null, tmpl); - } - else { - return tmpl; - } - } else { - var syncResult; - var _this = this; - - var createTemplate = function(err, info) { - if(!info && !err) { - if(!ignoreMissing) { - err = new Error('template not found: ' + name); - } - } - - if (err) { - if(cb) { - cb(err); - } - else { - throw err; - } - } - else { - var tmpl; - if(info) { - tmpl = new Template(info.src, _this, - info.path, eagerCompile); - - if(!info.noCache) { - info.loader.cache[name] = tmpl; - } - } - else { - tmpl = new Template('', _this, - '', eagerCompile); - } - - if(cb) { - cb(null, tmpl); - } - else { - syncResult = tmpl; - } - } - }; - - lib.asyncIter(this.loaders, function(loader, i, next, done) { - function handle(err, src) { - if(err) { - done(err); - } - else if(src) { - src.loader = loader; - done(null, src); - } - else { - next(); - } - } - - // Resolve name relative to parentName - name = that.resolveTemplate(loader, parentName, name); - - if(loader.async) { - loader.getSource(name, handle); - } - else { - handle(null, loader.getSource(name)); - } - }, createTemplate); - - return syncResult; - } - }, - - express: function(app) { - var env = this; - - function NunjucksView(name, opts) { - this.name = name; - this.path = name; - this.defaultEngine = opts.defaultEngine; - this.ext = path.extname(name); - if (!this.ext && !this.defaultEngine) throw new Error('No default engine was specified and no extension was provided.'); - if (!this.ext) this.name += (this.ext = ('.' !== this.defaultEngine[0] ? '.' : '') + this.defaultEngine); - } - - NunjucksView.prototype.render = function(opts, cb) { - env.render(this.name, opts, cb); - }; - - app.set('view', NunjucksView); - return this; - }, - - render: function(name, ctx, cb) { - if(lib.isFunction(ctx)) { - cb = ctx; - ctx = null; - } - - // We support a synchronous API to make it easier to migrate - // existing code to async. This works because if you don't do - // anything async work, the whole thing is actually run - // synchronously. - var syncResult = null; - - this.getTemplate(name, function(err, tmpl) { - if(err && cb) { - callbackAsap(cb, err); - } - else if(err) { - throw err; - } - else { - syncResult = tmpl.render(ctx, cb); - } - }); - - return syncResult; - }, - - renderString: function(src, ctx, opts, cb) { - if(lib.isFunction(opts)) { - cb = opts; - opts = {}; - } - opts = opts || {}; - - var tmpl = new Template(src, this, opts.path); - return tmpl.render(ctx, cb); - } - }); - - var Context = Obj.extend({ - init: function(ctx, blocks, env) { - // Has to be tied to an environment so we can tap into its globals. - this.env = env || new Environment(); - - // Make a duplicate of ctx - this.ctx = {}; - for(var k in ctx) { - if(ctx.hasOwnProperty(k)) { - this.ctx[k] = ctx[k]; - } - } - - this.blocks = {}; - this.exported = []; - - for(var name in blocks) { - this.addBlock(name, blocks[name]); - } - }, - - lookup: function(name) { - // This is one of the most called functions, so optimize for - // the typical case where the name isn't in the globals - if(name in this.env.globals && !(name in this.ctx)) { - return this.env.globals[name]; - } - else { - return this.ctx[name]; - } - }, - - setVariable: function(name, val) { - this.ctx[name] = val; - }, - - getVariables: function() { - return this.ctx; - }, - - addBlock: function(name, block) { - this.blocks[name] = this.blocks[name] || []; - this.blocks[name].push(block); - return this; - }, - - getBlock: function(name) { - if(!this.blocks[name]) { - throw new Error('unknown block "' + name + '"'); - } - - return this.blocks[name][0]; - }, - - getSuper: function(env, name, block, frame, runtime, cb) { - var idx = lib.indexOf(this.blocks[name] || [], block); - var blk = this.blocks[name][idx + 1]; - var context = this; - - if(idx === -1 || !blk) { - throw new Error('no super block available for "' + name + '"'); - } - - blk(env, context, frame, runtime, cb); - }, - - addExport: function(name) { - this.exported.push(name); - }, - - getExported: function() { - var exported = {}; - for(var i=0; i capacity) { - // Manually shift all values starting at the index back to the - // beginning of the queue. - for (var scan = 0, newLength = queue.length - index; scan < newLength; scan++) { - queue[scan] = queue[scan + index]; - } - queue.length -= index; - index = 0; - } - } - queue.length = 0; - index = 0; - flushing = false; - } - - // `requestFlush` is implemented using a strategy based on data collected from - // every available SauceLabs Selenium web driver worker at time of writing. - // https://docs.google.com/spreadsheets/d/1mG-5UYGup5qxGdEMWkhP6BWCz053NUb2E1QoUTU16uA/edit#gid=783724593 - - // Safari 6 and 6.1 for desktop, iPad, and iPhone are the only browsers that - // have WebKitMutationObserver but not un-prefixed MutationObserver. - // Must use `global` instead of `window` to work in both frames and web - // workers. `global` is a provision of Browserify, Mr, Mrs, or Mop. - var BrowserMutationObserver = global.MutationObserver || global.WebKitMutationObserver; - - // MutationObservers are desirable because they have high priority and work - // reliably everywhere they are implemented. - // They are implemented in all modern browsers. - // - // - Android 4-4.3 - // - Chrome 26-34 - // - Firefox 14-29 - // - Internet Explorer 11 - // - iPad Safari 6-7.1 - // - iPhone Safari 7-7.1 - // - Safari 6-7 - if (typeof BrowserMutationObserver === "function") { - requestFlush = makeRequestCallFromMutationObserver(flush); - - // MessageChannels are desirable because they give direct access to the HTML - // task queue, are implemented in Internet Explorer 10, Safari 5.0-1, and Opera - // 11-12, and in web workers in many engines. - // Although message channels yield to any queued rendering and IO tasks, they - // would be better than imposing the 4ms delay of timers. - // However, they do not work reliably in Internet Explorer or Safari. - - // Internet Explorer 10 is the only browser that has setImmediate but does - // not have MutationObservers. - // Although setImmediate yields to the browser's renderer, it would be - // preferrable to falling back to setTimeout since it does not have - // the minimum 4ms penalty. - // Unfortunately there appears to be a bug in Internet Explorer 10 Mobile (and - // Desktop to a lesser extent) that renders both setImmediate and - // MessageChannel useless for the purposes of ASAP. - // https://github.com/kriskowal/q/issues/396 - - // Timers are implemented universally. - // We fall back to timers in workers in most engines, and in foreground - // contexts in the following browsers. - // However, note that even this simple case requires nuances to operate in a - // broad spectrum of browsers. - // - // - Firefox 3-13 - // - Internet Explorer 6-9 - // - iPad Safari 4.3 - // - Lynx 2.8.7 - } else { - requestFlush = makeRequestCallFromTimer(flush); - } - - // `requestFlush` requests that the high priority event queue be flushed as - // soon as possible. - // This is useful to prevent an error thrown in a task from stalling the event - // queue if the exception handled by Node.js’s - // `process.on("uncaughtException")` or by a domain. - rawAsap.requestFlush = requestFlush; - - // To request a high priority event, we induce a mutation observer by toggling - // the text of a text node between "1" and "-1". - function makeRequestCallFromMutationObserver(callback) { - var toggle = 1; - var observer = new BrowserMutationObserver(callback); - var node = document.createTextNode(""); - observer.observe(node, {characterData: true}); - return function requestCall() { - toggle = -toggle; - node.data = toggle; - }; - } - - // The message channel technique was discovered by Malte Ubl and was the - // original foundation for this library. - // http://www.nonblocking.io/2011/06/windownexttick.html - - // Safari 6.0.5 (at least) intermittently fails to create message ports on a - // page's first load. Thankfully, this version of Safari supports - // MutationObservers, so we don't need to fall back in that case. - - // function makeRequestCallFromMessageChannel(callback) { - // var channel = new MessageChannel(); - // channel.port1.onmessage = callback; - // return function requestCall() { - // channel.port2.postMessage(0); - // }; - // } - - // For reasons explained above, we are also unable to use `setImmediate` - // under any circumstances. - // Even if we were, there is another bug in Internet Explorer 10. - // It is not sufficient to assign `setImmediate` to `requestFlush` because - // `setImmediate` must be called *by name* and therefore must be wrapped in a - // closure. - // Never forget. - - // function makeRequestCallFromSetImmediate(callback) { - // return function requestCall() { - // setImmediate(callback); - // }; - // } - - // Safari 6.0 has a problem where timers will get lost while the user is - // scrolling. This problem does not impact ASAP because Safari 6.0 supports - // mutation observers, so that implementation is used instead. - // However, if we ever elect to use timers in Safari, the prevalent work-around - // is to add a scroll event listener that calls for a flush. - - // `setTimeout` does not call the passed callback if the delay is less than - // approximately 7 in web workers in Firefox 8 through 18, and sometimes not - // even then. - - function makeRequestCallFromTimer(callback) { - return function requestCall() { - // We dispatch a timeout with a specified delay of 0 for engines that - // can reliably accommodate that request. This will usually be snapped - // to a 4 milisecond delay, but once we're flushing, there's no delay - // between events. - var timeoutHandle = setTimeout(handleTimer, 0); - // However, since this timer gets frequently dropped in Firefox - // workers, we enlist an interval handle that will try to fire - // an event 20 times per second until it succeeds. - var intervalHandle = setInterval(handleTimer, 50); - - function handleTimer() { - // Whichever timer succeeds will cancel both timers and - // execute the callback. - clearTimeout(timeoutHandle); - clearInterval(intervalHandle); - callback(); - } - }; - } - - // This is for `asap.js` only. - // Its name will be periodically randomized to break any code that depends on - // its existence. - rawAsap.makeRequestCallFromTimer = makeRequestCallFromTimer; - - // ASAP was originally a nextTick shim included in Q. This was factored out - // into this ASAP package. It was later adapted to RSVP which made further - // amendments. These decisions, particularly to marginalize MessageChannel and - // to capture the MutationObserver implementation in a closure, were integrated - // back into ASAP proper. - // https://github.com/tildeio/rsvp.js/blob/cddf7232546a9cf858524b75cde6f9edf72620a7/lib/rsvp/asap.js - - /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) - -/***/ }, -/* 6 */ -/***/ function(module, exports) { - - 'use strict'; - - // A simple class system, more documentation to come - - function extend(cls, name, props) { - // This does that same thing as Object.create, but with support for IE8 - var F = function() {}; - F.prototype = cls.prototype; - var prototype = new F(); - - // jshint undef: false - var fnTest = /xyz/.test(function(){ xyz; }) ? /\bparent\b/ : /.*/; - props = props || {}; - - for(var k in props) { - var src = props[k]; - var parent = prototype[k]; - - if(typeof parent === 'function' && - typeof src === 'function' && - fnTest.test(src)) { - /*jshint -W083 */ - prototype[k] = (function (src, parent) { - return function() { - // Save the current parent method - var tmp = this.parent; - - // Set parent to the previous method, call, and restore - this.parent = parent; - var res = src.apply(this, arguments); - this.parent = tmp; - - return res; - }; - })(src, parent); - } - else { - prototype[k] = src; - } - } - - prototype.typename = name; - - var new_cls = function() { - if(prototype.init) { - prototype.init.apply(this, arguments); - } - }; - - new_cls.prototype = prototype; - new_cls.prototype.constructor = new_cls; - - new_cls.extend = function(name, props) { - if(typeof name === 'object') { - props = name; - name = 'anonymous'; - } - return extend(new_cls, name, props); - }; - - return new_cls; - } - - module.exports = extend(Object, 'Object', {}); - - -/***/ }, -/* 7 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var lib = __webpack_require__(1); - var parser = __webpack_require__(8); - var transformer = __webpack_require__(11); - var nodes = __webpack_require__(10); - // jshint -W079 - var Object = __webpack_require__(6); - var Frame = __webpack_require__(12).Frame; - - // These are all the same for now, but shouldn't be passed straight - // through - var compareOps = { - '==': '==', - '!=': '!=', - '<': '<', - '>': '>', - '<=': '<=', - '>=': '>=' - }; - - // A common pattern is to emit binary operators - function binOpEmitter(str) { - return function(node, frame) { - this.compile(node.left, frame); - this.emit(str); - this.compile(node.right, frame); - }; - } - - var Compiler = Object.extend({ - init: function(templateName, throwOnUndefined) { - this.templateName = templateName; - this.codebuf = []; - this.lastId = 0; - this.buffer = null; - this.bufferStack = []; - this.scopeClosers = ''; - this.inBlock = false; - this.throwOnUndefined = throwOnUndefined; - }, - - fail: function (msg, lineno, colno) { - if (lineno !== undefined) lineno += 1; - if (colno !== undefined) colno += 1; - - throw new lib.TemplateError(msg, lineno, colno); - }, - - pushBufferId: function(id) { - this.bufferStack.push(this.buffer); - this.buffer = id; - this.emit('var ' + this.buffer + ' = "";'); - }, - - popBufferId: function() { - this.buffer = this.bufferStack.pop(); - }, - - emit: function(code) { - this.codebuf.push(code); - }, - - emitLine: function(code) { - this.emit(code + '\n'); - }, - - emitLines: function() { - lib.each(lib.toArray(arguments), function(line) { - this.emitLine(line); - }, this); - }, - - emitFuncBegin: function(name) { - this.buffer = 'output'; - this.scopeClosers = ''; - this.emitLine('function ' + name + '(env, context, frame, runtime, cb) {'); - this.emitLine('var lineno = null;'); - this.emitLine('var colno = null;'); - this.emitLine('var ' + this.buffer + ' = "";'); - this.emitLine('try {'); - }, - - emitFuncEnd: function(noReturn) { - if(!noReturn) { - this.emitLine('cb(null, ' + this.buffer +');'); - } - - this.closeScopeLevels(); - this.emitLine('} catch (e) {'); - this.emitLine(' cb(runtime.handleError(e, lineno, colno));'); - this.emitLine('}'); - this.emitLine('}'); - this.buffer = null; - }, - - addScopeLevel: function() { - this.scopeClosers += '})'; - }, - - closeScopeLevels: function() { - this.emitLine(this.scopeClosers + ';'); - this.scopeClosers = ''; - }, - - withScopedSyntax: function(func) { - var scopeClosers = this.scopeClosers; - this.scopeClosers = ''; - - func.call(this); - - this.closeScopeLevels(); - this.scopeClosers = scopeClosers; - }, - - makeCallback: function(res) { - var err = this.tmpid(); - - return 'function(' + err + (res ? ',' + res : '') + ') {\n' + - 'if(' + err + ') { cb(' + err + '); return; }'; - }, - - tmpid: function() { - this.lastId++; - return 't_' + this.lastId; - }, - - _templateName: function() { - return this.templateName == null? 'undefined' : JSON.stringify(this.templateName); - }, - - _compileChildren: function(node, frame) { - var children = node.children; - for(var i=0, l=children.length; i 0) { - this.emit(','); - } - - this.compile(node.children[i], frame); - } - - if(endChar) { - this.emit(endChar); - } - }, - - _compileExpression: function(node, frame) { - // TODO: I'm not really sure if this type check is worth it or - // not. - this.assertType( - node, - nodes.Literal, - nodes.Symbol, - nodes.Group, - nodes.Array, - nodes.Dict, - nodes.FunCall, - nodes.Caller, - nodes.Filter, - nodes.LookupVal, - nodes.Compare, - nodes.InlineIf, - nodes.In, - nodes.And, - nodes.Or, - nodes.Not, - nodes.Add, - nodes.Concat, - nodes.Sub, - nodes.Mul, - nodes.Div, - nodes.FloorDiv, - nodes.Mod, - nodes.Pow, - nodes.Neg, - nodes.Pos, - nodes.Compare, - nodes.NodeList - ); - this.compile(node, frame); - }, - - assertType: function(node /*, types */) { - var types = lib.toArray(arguments).slice(1); - var success = false; - - for(var i=0; i 0) { - this.emit(','); - } - - if(arg) { - var id = this.tmpid(); - - this.emitLine('function(cb) {'); - this.emitLine('if(!cb) { cb = function(err) { if(err) { throw err; }}}'); - this.pushBufferId(id); - - this.withScopedSyntax(function() { - this.compile(arg, frame); - this.emitLine('cb(null, ' + id + ');'); - }); - - this.popBufferId(); - this.emitLine('return ' + id + ';'); - this.emitLine('}'); - } - else { - this.emit('null'); - } - }, this); - } - - if(async) { - var res = this.tmpid(); - this.emitLine(', ' + this.makeCallback(res)); - this.emitLine(this.buffer + ' += runtime.suppressValue(' + res + ', ' + autoescape + ' && env.opts.autoescape);'); - this.addScopeLevel(); - } - else { - this.emit(')'); - this.emit(', ' + autoescape + ' && env.opts.autoescape);\n'); - } - }, - - compileCallExtensionAsync: function(node, frame) { - this.compileCallExtension(node, frame, true); - }, - - compileNodeList: function(node, frame) { - this._compileChildren(node, frame); - }, - - compileLiteral: function(node) { - if(typeof node.value === 'string') { - var val = node.value.replace(/\\/g, '\\\\'); - val = val.replace(/"/g, '\\"'); - val = val.replace(/\n/g, '\\n'); - val = val.replace(/\r/g, '\\r'); - val = val.replace(/\t/g, '\\t'); - this.emit('"' + val + '"'); - } - else if (node.value === null) { - this.emit('null'); - } - else { - this.emit(node.value.toString()); - } - }, - - compileSymbol: function(node, frame) { - var name = node.value; - var v; - - if((v = frame.lookup(name))) { - this.emit(v); - } - else { - this.emit('runtime.contextOrFrameLookup(' + - 'context, frame, "' + name + '")'); - } - }, - - compileGroup: function(node, frame) { - this._compileAggregate(node, frame, '(', ')'); - }, - - compileArray: function(node, frame) { - this._compileAggregate(node, frame, '[', ']'); - }, - - compileDict: function(node, frame) { - this._compileAggregate(node, frame, '{', '}'); - }, - - compilePair: function(node, frame) { - var key = node.key; - var val = node.value; - - if(key instanceof nodes.Symbol) { - key = new nodes.Literal(key.lineno, key.colno, key.value); - } - else if(!(key instanceof nodes.Literal && - typeof key.value === 'string')) { - this.fail('compilePair: Dict keys must be strings or names', - key.lineno, - key.colno); - } - - this.compile(key, frame); - this.emit(': '); - this._compileExpression(val, frame); - }, - - compileInlineIf: function(node, frame) { - this.emit('('); - this.compile(node.cond, frame); - this.emit('?'); - this.compile(node.body, frame); - this.emit(':'); - if(node.else_ !== null) - this.compile(node.else_, frame); - else - this.emit('""'); - this.emit(')'); - }, - - compileIn: function(node, frame) { - this.emit('('); - this.compile(node.right, frame); - this.emit('.indexOf('); - this.compile(node.left, frame); - this.emit(') !== -1)'); - }, - - compileOr: binOpEmitter(' || '), - compileAnd: binOpEmitter(' && '), - compileAdd: binOpEmitter(' + '), - // ensure concatenation instead of addition - // by adding empty string in between - compileConcat: binOpEmitter(' + "" + '), - compileSub: binOpEmitter(' - '), - compileMul: binOpEmitter(' * '), - compileDiv: binOpEmitter(' / '), - compileMod: binOpEmitter(' % '), - - compileNot: function(node, frame) { - this.emit('!'); - this.compile(node.target, frame); - }, - - compileFloorDiv: function(node, frame) { - this.emit('Math.floor('); - this.compile(node.left, frame); - this.emit(' / '); - this.compile(node.right, frame); - this.emit(')'); - }, - - compilePow: function(node, frame) { - this.emit('Math.pow('); - this.compile(node.left, frame); - this.emit(', '); - this.compile(node.right, frame); - this.emit(')'); - }, - - compileNeg: function(node, frame) { - this.emit('-'); - this.compile(node.target, frame); - }, - - compilePos: function(node, frame) { - this.emit('+'); - this.compile(node.target, frame); - }, - - compileCompare: function(node, frame) { - this.compile(node.expr, frame); - - for(var i=0; i 0 && !this.skip(lexer.TOKEN_COMMA)) { - this.fail('parseFrom: expected comma', - fromTok.lineno, - fromTok.colno); - } - - var name = this.parsePrimary(); - if(name.value.charAt(0) === '_') { - this.fail('parseFrom: names starting with an underscore ' + - 'cannot be imported', - name.lineno, - name.colno); - } - - if(this.skipSymbol('as')) { - var alias = this.parsePrimary(); - names.addChild(new nodes.Pair(name.lineno, - name.colno, - name, - alias)); - } - else { - names.addChild(name); - } - - withContext = this.parseWithContext(); - } - - return new nodes.FromImport(fromTok.lineno, - fromTok.colno, - template, - names, - withContext); - }, - - parseBlock: function() { - var tag = this.peekToken(); - if(!this.skipSymbol('block')) { - this.fail('parseBlock: expected block', tag.lineno, tag.colno); - } - - var node = new nodes.Block(tag.lineno, tag.colno); - - node.name = this.parsePrimary(); - if(!(node.name instanceof nodes.Symbol)) { - this.fail('parseBlock: variable name expected', - tag.lineno, - tag.colno); - } - - this.advanceAfterBlockEnd(tag.value); - - node.body = this.parseUntilBlocks('endblock'); - - if(!this.peekToken()) { - this.fail('parseBlock: expected endblock, got end of file'); - } - - this.advanceAfterBlockEnd(); - - return node; - }, - - parseExtends: function() { - var tagName = 'extends'; - var tag = this.peekToken(); - if(!this.skipSymbol(tagName)) { - this.fail('parseTemplateRef: expected '+ tagName); - } - - var node = new nodes.Extends(tag.lineno, tag.colno); - node.template = this.parseExpression(); - - this.advanceAfterBlockEnd(tag.value); - return node; - }, - - parseInclude: function() { - var tagName = 'include'; - var tag = this.peekToken(); - if(!this.skipSymbol(tagName)) { - this.fail('parseInclude: expected '+ tagName); - } - - var node = new nodes.Include(tag.lineno, tag.colno); - node.template = this.parseExpression(); - - if(this.skipSymbol('ignore') && this.skipSymbol('missing')) { - node.ignoreMissing = true; - } - - this.advanceAfterBlockEnd(tag.value); - return node; - }, - - parseIf: function() { - var tag = this.peekToken(); - var node; - - if(this.skipSymbol('if') || this.skipSymbol('elif')) { - node = new nodes.If(tag.lineno, tag.colno); - } - else if(this.skipSymbol('ifAsync')) { - node = new nodes.IfAsync(tag.lineno, tag.colno); - } - else { - this.fail('parseIf: expected if or elif', - tag.lineno, - tag.colno); - } - - node.cond = this.parseExpression(); - this.advanceAfterBlockEnd(tag.value); - - node.body = this.parseUntilBlocks('elif', 'else', 'endif'); - var tok = this.peekToken(); - - switch(tok && tok.value) { - case 'elif': - node.else_ = this.parseIf(); - break; - case 'else': - this.advanceAfterBlockEnd(); - node.else_ = this.parseUntilBlocks('endif'); - this.advanceAfterBlockEnd(); - break; - case 'endif': - node.else_ = null; - this.advanceAfterBlockEnd(); - break; - default: - this.fail('parseIf: expected elif, else, or endif, ' + - 'got end of file'); - } - - return node; - }, - - parseSet: function() { - var tag = this.peekToken(); - if(!this.skipSymbol('set')) { - this.fail('parseSet: expected set', tag.lineno, tag.colno); - } - - var node = new nodes.Set(tag.lineno, tag.colno, []); - - var target; - while((target = this.parsePrimary())) { - node.targets.push(target); - - if(!this.skip(lexer.TOKEN_COMMA)) { - break; - } - } - - if(!this.skipValue(lexer.TOKEN_OPERATOR, '=')) { - this.fail('parseSet: expected = in set tag', - tag.lineno, - tag.colno); - } - - node.value = this.parseExpression(); - this.advanceAfterBlockEnd(tag.value); - - return node; - }, - - parseStatement: function () { - var tok = this.peekToken(); - var node; - - if(tok.type !== lexer.TOKEN_SYMBOL) { - this.fail('tag name expected', tok.lineno, tok.colno); - } - - if(this.breakOnBlocks && - lib.indexOf(this.breakOnBlocks, tok.value) !== -1) { - return null; - } - - switch(tok.value) { - case 'raw': return this.parseRaw(); - case 'if': - case 'ifAsync': - return this.parseIf(); - case 'for': - case 'asyncEach': - case 'asyncAll': - return this.parseFor(); - case 'block': return this.parseBlock(); - case 'extends': return this.parseExtends(); - case 'include': return this.parseInclude(); - case 'set': return this.parseSet(); - case 'macro': return this.parseMacro(); - case 'call': return this.parseCall(); - case 'import': return this.parseImport(); - case 'from': return this.parseFrom(); - case 'filter': return this.parseFilterStatement(); - default: - if (this.extensions.length) { - for (var i = 0; i < this.extensions.length; i++) { - var ext = this.extensions[i]; - if (lib.indexOf(ext.tags || [], tok.value) !== -1) { - return ext.parse(this, nodes, lexer); - } - } - } - this.fail('unknown block tag: ' + tok.value, tok.lineno, tok.colno); - } - - return node; - }, - - parseRaw: function() { - // Look for upcoming raw blocks (ignore all other kinds of blocks) - var rawBlockRegex = /([\s\S]*?){%\s*(raw|endraw)\s*(?=%})%}/; - var rawLevel = 1; - var str = ''; - var matches = null; - - // Skip opening raw token - // Keep this token to track line and column numbers - var begun = this.advanceAfterBlockEnd(); - - // Exit when there's nothing to match - // or when we've found the matching "endraw" block - while((matches = this.tokens._extractRegex(rawBlockRegex)) && rawLevel > 0) { - var all = matches[0]; - var pre = matches[1]; - var blockName = matches[2]; - - // Adjust rawlevel - if(blockName === 'raw') { - rawLevel += 1; - } else if(blockName === 'endraw') { - rawLevel -= 1; - } - - // Add to str - if(rawLevel === 0) { - // We want to exclude the last "endraw" - str += pre; - // Move tokenizer to beginning of endraw block - this.tokens.backN(all.length - pre.length); - } else { - str += all; - } - } - - return new nodes.Output( - begun.lineno, - begun.colno, - [new nodes.TemplateData(begun.lineno, begun.colno, str)] - ); - }, - - parsePostfix: function(node) { - var lookup, tok = this.peekToken(); - - while(tok) { - if(tok.type === lexer.TOKEN_LEFT_PAREN) { - // Function call - node = new nodes.FunCall(tok.lineno, - tok.colno, - node, - this.parseSignature()); - } - else if(tok.type === lexer.TOKEN_LEFT_BRACKET) { - // Reference - lookup = this.parseAggregate(); - if(lookup.children.length > 1) { - this.fail('invalid index'); - } - - node = new nodes.LookupVal(tok.lineno, - tok.colno, - node, - lookup.children[0]); - } - else if(tok.type === lexer.TOKEN_OPERATOR && tok.value === '.') { - // Reference - this.nextToken(); - var val = this.nextToken(); - - if(val.type !== lexer.TOKEN_SYMBOL) { - this.fail('expected name as lookup value, got ' + val.value, - val.lineno, - val.colno); - } - - // Make a literal string because it's not a variable - // reference - lookup = new nodes.Literal(val.lineno, - val.colno, - val.value); - - node = new nodes.LookupVal(tok.lineno, - tok.colno, - node, - lookup); - } - else { - break; - } - - tok = this.peekToken(); - } - - return node; - }, - - parseExpression: function() { - var node = this.parseInlineIf(); - return node; - }, - - parseInlineIf: function() { - var node = this.parseOr(); - if(this.skipSymbol('if')) { - var cond_node = this.parseOr(); - var body_node = node; - node = new nodes.InlineIf(node.lineno, node.colno); - node.body = body_node; - node.cond = cond_node; - if(this.skipSymbol('else')) { - node.else_ = this.parseOr(); - } else { - node.else_ = null; - } - } - - return node; - }, - - parseOr: function() { - var node = this.parseAnd(); - while(this.skipSymbol('or')) { - var node2 = this.parseAnd(); - node = new nodes.Or(node.lineno, - node.colno, - node, - node2); - } - return node; - }, - - parseAnd: function() { - var node = this.parseNot(); - while(this.skipSymbol('and')) { - var node2 = this.parseNot(); - node = new nodes.And(node.lineno, - node.colno, - node, - node2); - } - return node; - }, - - parseNot: function() { - var tok = this.peekToken(); - if(this.skipSymbol('not')) { - return new nodes.Not(tok.lineno, - tok.colno, - this.parseNot()); - } - return this.parseIn(); - }, - - parseIn: function() { - var node = this.parseCompare(); - while(1) { - // check if the next token is 'not' - var tok = this.nextToken(); - if (!tok) { break; } - var invert = tok.type === lexer.TOKEN_SYMBOL && tok.value === 'not'; - // if it wasn't 'not', put it back - if (!invert) { this.pushToken(tok); } - if (this.skipSymbol('in')) { - var node2 = this.parseCompare(); - node = new nodes.In(node.lineno, - node.colno, - node, - node2); - if (invert) { - node = new nodes.Not(node.lineno, - node.colno, - node); - } - } - else { - // if we'd found a 'not' but this wasn't an 'in', put back the 'not' - if (invert) { this.pushToken(tok); } - break; - } - } - return node; - }, - - parseCompare: function() { - var compareOps = ['==', '!=', '<', '>', '<=', '>=']; - var expr = this.parseConcat(); - var ops = []; - - while(1) { - var tok = this.nextToken(); - - if(!tok) { - break; - } - else if(lib.indexOf(compareOps, tok.value) !== -1) { - ops.push(new nodes.CompareOperand(tok.lineno, - tok.colno, - this.parseConcat(), - tok.value)); - } - else { - this.pushToken(tok); - break; - } - } - - if(ops.length) { - return new nodes.Compare(ops[0].lineno, - ops[0].colno, - expr, - ops); - } - else { - return expr; - } - }, - - // finds the '~' for string concatenation - parseConcat: function(){ - var node = this.parseAdd(); - while(this.skipValue(lexer.TOKEN_TILDE, '~')) { - var node2 = this.parseAdd(); - node = new nodes.Concat(node.lineno, - node.colno, - node, - node2); - } - return node; - }, - - parseAdd: function() { - var node = this.parseSub(); - while(this.skipValue(lexer.TOKEN_OPERATOR, '+')) { - var node2 = this.parseSub(); - node = new nodes.Add(node.lineno, - node.colno, - node, - node2); - } - return node; - }, - - parseSub: function() { - var node = this.parseMul(); - while(this.skipValue(lexer.TOKEN_OPERATOR, '-')) { - var node2 = this.parseMul(); - node = new nodes.Sub(node.lineno, - node.colno, - node, - node2); - } - return node; - }, - - parseMul: function() { - var node = this.parseDiv(); - while(this.skipValue(lexer.TOKEN_OPERATOR, '*')) { - var node2 = this.parseDiv(); - node = new nodes.Mul(node.lineno, - node.colno, - node, - node2); - } - return node; - }, - - parseDiv: function() { - var node = this.parseFloorDiv(); - while(this.skipValue(lexer.TOKEN_OPERATOR, '/')) { - var node2 = this.parseFloorDiv(); - node = new nodes.Div(node.lineno, - node.colno, - node, - node2); - } - return node; - }, - - parseFloorDiv: function() { - var node = this.parseMod(); - while(this.skipValue(lexer.TOKEN_OPERATOR, '//')) { - var node2 = this.parseMod(); - node = new nodes.FloorDiv(node.lineno, - node.colno, - node, - node2); - } - return node; - }, - - parseMod: function() { - var node = this.parsePow(); - while(this.skipValue(lexer.TOKEN_OPERATOR, '%')) { - var node2 = this.parsePow(); - node = new nodes.Mod(node.lineno, - node.colno, - node, - node2); - } - return node; - }, - - parsePow: function() { - var node = this.parseUnary(); - while(this.skipValue(lexer.TOKEN_OPERATOR, '**')) { - var node2 = this.parseUnary(); - node = new nodes.Pow(node.lineno, - node.colno, - node, - node2); - } - return node; - }, - - parseUnary: function(noFilters) { - var tok = this.peekToken(); - var node; - - if(this.skipValue(lexer.TOKEN_OPERATOR, '-')) { - node = new nodes.Neg(tok.lineno, - tok.colno, - this.parseUnary(true)); - } - else if(this.skipValue(lexer.TOKEN_OPERATOR, '+')) { - node = new nodes.Pos(tok.lineno, - tok.colno, - this.parseUnary(true)); - } - else { - node = this.parsePrimary(); - } - - if(!noFilters) { - node = this.parseFilter(node); - } - - return node; - }, - - parsePrimary: function (noPostfix) { - var tok = this.nextToken(); - var val; - var node = null; - - if(!tok) { - this.fail('expected expression, got end of file'); - } - else if(tok.type === lexer.TOKEN_STRING) { - val = tok.value; - } - else if(tok.type === lexer.TOKEN_INT) { - val = parseInt(tok.value, 10); - } - else if(tok.type === lexer.TOKEN_FLOAT) { - val = parseFloat(tok.value); - } - else if(tok.type === lexer.TOKEN_BOOLEAN) { - if(tok.value === 'true') { - val = true; - } - else if(tok.value === 'false') { - val = false; - } - else { - this.fail('invalid boolean: ' + tok.value, - tok.lineno, - tok.colno); - } - } - else if(tok.type === lexer.TOKEN_NONE) { - val = null; - } - else if (tok.type === lexer.TOKEN_REGEX) { - val = new RegExp(tok.value.body, tok.value.flags); - } - - if(val !== undefined) { - node = new nodes.Literal(tok.lineno, tok.colno, val); - } - else if(tok.type === lexer.TOKEN_SYMBOL) { - node = new nodes.Symbol(tok.lineno, tok.colno, tok.value); - - if(!noPostfix) { - node = this.parsePostfix(node); - } - } - else { - // See if it's an aggregate type, we need to push the - // current delimiter token back on - this.pushToken(tok); - node = this.parseAggregate(); - } - - if(node) { - return node; - } - else { - this.fail('unexpected token: ' + tok.value, - tok.lineno, - tok.colno); - } - }, - - parseFilterName: function() { - var tok = this.expect(lexer.TOKEN_SYMBOL); - var name = tok.value; - - while(this.skipValue(lexer.TOKEN_OPERATOR, '.')) { - name += '.' + this.expect(lexer.TOKEN_SYMBOL).value; - } - - return new nodes.Symbol(tok.lineno, tok.colno, name); - }, - - parseFilterArgs: function(node) { - if(this.peekToken().type === lexer.TOKEN_LEFT_PAREN) { - // Get a FunCall node and add the parameters to the - // filter - var call = this.parsePostfix(node); - return call.args.children; - } - return []; - }, - - parseFilter: function(node) { - while(this.skip(lexer.TOKEN_PIPE)) { - var name = this.parseFilterName(); - - node = new nodes.Filter( - name.lineno, - name.colno, - name, - new nodes.NodeList( - name.lineno, - name.colno, - [node].concat(this.parseFilterArgs(node)) - ) - ); - } - - return node; - }, - - parseFilterStatement: function() { - var filterTok = this.peekToken(); - if(!this.skipSymbol('filter')) { - this.fail('parseFilterStatement: expected filter'); - } - - var name = this.parseFilterName(); - var args = this.parseFilterArgs(name); - - this.advanceAfterBlockEnd(filterTok.value); - var body = this.parseUntilBlocks('endfilter'); - this.advanceAfterBlockEnd(); - - var node = new nodes.Filter( - name.lineno, - name.colno, - name, - new nodes.NodeList( - name.lineno, - name.colno, - // Body is a NodeList with an Output node as a child, - // need to strip those - body.children[0].children.concat(args) - ) - ); - - return new nodes.Output( - name.lineno, - name.colno, - [node] - ); - }, - - parseAggregate: function() { - var tok = this.nextToken(); - var node; - - switch(tok.type) { - case lexer.TOKEN_LEFT_PAREN: - node = new nodes.Group(tok.lineno, tok.colno); break; - case lexer.TOKEN_LEFT_BRACKET: - node = new nodes.Array(tok.lineno, tok.colno); break; - case lexer.TOKEN_LEFT_CURLY: - node = new nodes.Dict(tok.lineno, tok.colno); break; - default: - return null; - } - - while(1) { - var type = this.peekToken().type; - if(type === lexer.TOKEN_RIGHT_PAREN || - type === lexer.TOKEN_RIGHT_BRACKET || - type === lexer.TOKEN_RIGHT_CURLY) { - this.nextToken(); - break; - } - - if(node.children.length > 0) { - if(!this.skip(lexer.TOKEN_COMMA)) { - this.fail('parseAggregate: expected comma after expression', - tok.lineno, - tok.colno); - } - } - - if(node instanceof nodes.Dict) { - // TODO: check for errors - var key = this.parsePrimary(); - - // We expect a key/value pair for dicts, separated by a - // colon - if(!this.skip(lexer.TOKEN_COLON)) { - this.fail('parseAggregate: expected colon after dict key', - tok.lineno, - tok.colno); - } - - // TODO: check for errors - var value = this.parseExpression(); - node.addChild(new nodes.Pair(key.lineno, - key.colno, - key, - value)); - } - else { - // TODO: check for errors - var expr = this.parseExpression(); - node.addChild(expr); - } - } - - return node; - }, - - parseSignature: function(tolerant, noParens) { - var tok = this.peekToken(); - if(!noParens && tok.type !== lexer.TOKEN_LEFT_PAREN) { - if(tolerant) { - return null; - } - else { - this.fail('expected arguments', tok.lineno, tok.colno); - } - } - - if(tok.type === lexer.TOKEN_LEFT_PAREN) { - tok = this.nextToken(); - } - - var args = new nodes.NodeList(tok.lineno, tok.colno); - var kwargs = new nodes.KeywordArgs(tok.lineno, tok.colno); - var checkComma = false; - - while(1) { - tok = this.peekToken(); - if(!noParens && tok.type === lexer.TOKEN_RIGHT_PAREN) { - this.nextToken(); - break; - } - else if(noParens && tok.type === lexer.TOKEN_BLOCK_END) { - break; - } - - if(checkComma && !this.skip(lexer.TOKEN_COMMA)) { - this.fail('parseSignature: expected comma after expression', - tok.lineno, - tok.colno); - } - else { - var arg = this.parseExpression(); - - if(this.skipValue(lexer.TOKEN_OPERATOR, '=')) { - kwargs.addChild( - new nodes.Pair(arg.lineno, - arg.colno, - arg, - this.parseExpression()) - ); - } - else { - args.addChild(arg); - } - } - - checkComma = true; - } - - if(kwargs.children.length) { - args.addChild(kwargs); - } - - return args; - }, - - parseUntilBlocks: function(/* blockNames */) { - var prev = this.breakOnBlocks; - this.breakOnBlocks = lib.toArray(arguments); - - var ret = this.parse(); - - this.breakOnBlocks = prev; - return ret; - }, - - parseNodes: function () { - var tok; - var buf = []; - - while((tok = this.nextToken())) { - if(tok.type === lexer.TOKEN_DATA) { - var data = tok.value; - var nextToken = this.peekToken(); - var nextVal = nextToken && nextToken.value; - - // If the last token has "-" we need to trim the - // leading whitespace of the data. This is marked with - // the `dropLeadingWhitespace` variable. - if(this.dropLeadingWhitespace) { - // TODO: this could be optimized (don't use regex) - data = data.replace(/^\s*/, ''); - this.dropLeadingWhitespace = false; - } - - // Same for the succeding block start token - if(nextToken && - nextToken.type === lexer.TOKEN_BLOCK_START && - nextVal.charAt(nextVal.length - 1) === '-') { - // TODO: this could be optimized (don't use regex) - data = data.replace(/\s*$/, ''); - } - - buf.push(new nodes.Output(tok.lineno, - tok.colno, - [new nodes.TemplateData(tok.lineno, - tok.colno, - data)])); - } - else if(tok.type === lexer.TOKEN_BLOCK_START) { - var n = this.parseStatement(); - if(!n) { - break; - } - buf.push(n); - } - else if(tok.type === lexer.TOKEN_VARIABLE_START) { - var e = this.parseExpression(); - this.advanceAfterVariableEnd(); - buf.push(new nodes.Output(tok.lineno, tok.colno, [e])); - } - else if(tok.type !== lexer.TOKEN_COMMENT) { - // Ignore comments, otherwise this should be an error - this.fail('Unexpected token at top-level: ' + - tok.type, tok.lineno, tok.colno); - } - } - - return buf; - }, - - parse: function() { - return new nodes.NodeList(0, 0, this.parseNodes()); - }, - - parseAsRoot: function() { - return new nodes.Root(0, 0, this.parseNodes()); - } - }); - - // var util = require('util'); - - // var l = lexer.lex('{%- if x -%}\n hello {% endif %}'); - // var t; - // while((t = l.nextToken())) { - // console.log(util.inspect(t)); - // } - - // var p = new Parser(lexer.lex('hello {% filter title %}' + - // 'Hello madam how are you' + - // '{% endfilter %}')); - // var n = p.parseAsRoot(); - // nodes.printNodes(n); - - module.exports = { - parse: function(src, extensions, opts) { - var p = new Parser(lexer.lex(src, opts)); - if (extensions !== undefined) { - p.extensions = extensions; - } - return p.parseAsRoot(); - } - }; - - -/***/ }, -/* 9 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var lib = __webpack_require__(1); - - var whitespaceChars = ' \n\t\r\u00A0'; - var delimChars = '()[]{}%*-+~/#,:|.<>=!'; - var intChars = '0123456789'; - - var BLOCK_START = '{%'; - var BLOCK_END = '%}'; - var VARIABLE_START = '{{'; - var VARIABLE_END = '}}'; - var COMMENT_START = '{#'; - var COMMENT_END = '#}'; - - var TOKEN_STRING = 'string'; - var TOKEN_WHITESPACE = 'whitespace'; - var TOKEN_DATA = 'data'; - var TOKEN_BLOCK_START = 'block-start'; - var TOKEN_BLOCK_END = 'block-end'; - var TOKEN_VARIABLE_START = 'variable-start'; - var TOKEN_VARIABLE_END = 'variable-end'; - var TOKEN_COMMENT = 'comment'; - var TOKEN_LEFT_PAREN = 'left-paren'; - var TOKEN_RIGHT_PAREN = 'right-paren'; - var TOKEN_LEFT_BRACKET = 'left-bracket'; - var TOKEN_RIGHT_BRACKET = 'right-bracket'; - var TOKEN_LEFT_CURLY = 'left-curly'; - var TOKEN_RIGHT_CURLY = 'right-curly'; - var TOKEN_OPERATOR = 'operator'; - var TOKEN_COMMA = 'comma'; - var TOKEN_COLON = 'colon'; - var TOKEN_TILDE = 'tilde'; - var TOKEN_PIPE = 'pipe'; - var TOKEN_INT = 'int'; - var TOKEN_FLOAT = 'float'; - var TOKEN_BOOLEAN = 'boolean'; - var TOKEN_NONE = 'none'; - var TOKEN_SYMBOL = 'symbol'; - var TOKEN_SPECIAL = 'special'; - var TOKEN_REGEX = 'regex'; - - function token(type, value, lineno, colno) { - return { - type: type, - value: value, - lineno: lineno, - colno: colno - }; - } - - function Tokenizer(str, opts) { - this.str = str; - this.index = 0; - this.len = str.length; - this.lineno = 0; - this.colno = 0; - - this.in_code = false; - - opts = opts || {}; - - var tags = opts.tags || {}; - this.tags = { - BLOCK_START: tags.blockStart || BLOCK_START, - BLOCK_END: tags.blockEnd || BLOCK_END, - VARIABLE_START: tags.variableStart || VARIABLE_START, - VARIABLE_END: tags.variableEnd || VARIABLE_END, - COMMENT_START: tags.commentStart || COMMENT_START, - COMMENT_END: tags.commentEnd || COMMENT_END - }; - - this.trimBlocks = !!opts.trimBlocks; - this.lstripBlocks = !!opts.lstripBlocks; - } - - Tokenizer.prototype.nextToken = function() { - var lineno = this.lineno; - var colno = this.colno; - var tok; - - if(this.in_code) { - // Otherwise, if we are in a block parse it as code - var cur = this.current(); - - if(this.is_finished()) { - // We have nothing else to parse - return null; - } - else if(cur === '"' || cur === '\'') { - // We've hit a string - return token(TOKEN_STRING, this.parseString(cur), lineno, colno); - } - else if((tok = this._extract(whitespaceChars))) { - // We hit some whitespace - return token(TOKEN_WHITESPACE, tok, lineno, colno); - } - else if((tok = this._extractString(this.tags.BLOCK_END)) || - (tok = this._extractString('-' + this.tags.BLOCK_END))) { - // Special check for the block end tag - // - // It is a requirement that start and end tags are composed of - // delimiter characters (%{}[] etc), and our code always - // breaks on delimiters so we can assume the token parsing - // doesn't consume these elsewhere - this.in_code = false; - if(this.trimBlocks) { - cur = this.current(); - if(cur === '\n') { - // Skip newline - this.forward(); - }else if(cur === '\r'){ - // Skip CRLF newline - this.forward(); - cur = this.current(); - if(cur === '\n'){ - this.forward(); - }else{ - // Was not a CRLF, so go back - this.back(); - } - } - } - return token(TOKEN_BLOCK_END, tok, lineno, colno); - } - else if((tok = this._extractString(this.tags.VARIABLE_END))) { - // Special check for variable end tag (see above) - this.in_code = false; - return token(TOKEN_VARIABLE_END, tok, lineno, colno); - } - else if (cur === 'r' && this.str.charAt(this.index + 1) === '/') { - // Skip past 'r/'. - this.forwardN(2); - - // Extract until the end of the regex -- / ends it, \/ does not. - var regexBody = ''; - while (!this.is_finished()) { - if (this.current() === '/' && this.previous() !== '\\') { - this.forward(); - break; - } else { - regexBody += this.current(); - this.forward(); - } - } - - // Check for flags. - // The possible flags are according to https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/RegExp) - var POSSIBLE_FLAGS = ['g', 'i', 'm', 'y']; - var regexFlags = ''; - while (!this.is_finished()) { - var isCurrentAFlag = POSSIBLE_FLAGS.indexOf(this.current()) !== -1; - if (isCurrentAFlag) { - regexFlags += this.current(); - this.forward(); - } else { - break; - } - } - - return token(TOKEN_REGEX, {body: regexBody, flags: regexFlags}, lineno, colno); - } - else if(delimChars.indexOf(cur) !== -1) { - // We've hit a delimiter (a special char like a bracket) - this.forward(); - var complexOps = ['==', '!=', '<=', '>=', '//', '**']; - var curComplex = cur + this.current(); - var type; - - if(lib.indexOf(complexOps, curComplex) !== -1) { - this.forward(); - cur = curComplex; - } - - switch(cur) { - case '(': type = TOKEN_LEFT_PAREN; break; - case ')': type = TOKEN_RIGHT_PAREN; break; - case '[': type = TOKEN_LEFT_BRACKET; break; - case ']': type = TOKEN_RIGHT_BRACKET; break; - case '{': type = TOKEN_LEFT_CURLY; break; - case '}': type = TOKEN_RIGHT_CURLY; break; - case ',': type = TOKEN_COMMA; break; - case ':': type = TOKEN_COLON; break; - case '~': type = TOKEN_TILDE; break; - case '|': type = TOKEN_PIPE; break; - default: type = TOKEN_OPERATOR; - } - - return token(type, cur, lineno, colno); - } - else { - // We are not at whitespace or a delimiter, so extract the - // text and parse it - tok = this._extractUntil(whitespaceChars + delimChars); - - if(tok.match(/^[-+]?[0-9]+$/)) { - if(this.current() === '.') { - this.forward(); - var dec = this._extract(intChars); - return token(TOKEN_FLOAT, tok + '.' + dec, lineno, colno); - } - else { - return token(TOKEN_INT, tok, lineno, colno); - } - } - else if(tok.match(/^(true|false)$/)) { - return token(TOKEN_BOOLEAN, tok, lineno, colno); - } - else if(tok === 'none') { - return token(TOKEN_NONE, tok, lineno, colno); - } - else if(tok) { - return token(TOKEN_SYMBOL, tok, lineno, colno); - } - else { - throw new Error('Unexpected value while parsing: ' + tok); - } - } - } - else { - // Parse out the template text, breaking on tag - // delimiters because we need to look for block/variable start - // tags (don't use the full delimChars for optimization) - var beginChars = (this.tags.BLOCK_START.charAt(0) + - this.tags.VARIABLE_START.charAt(0) + - this.tags.COMMENT_START.charAt(0) + - this.tags.COMMENT_END.charAt(0)); - - if(this.is_finished()) { - return null; - } - else if((tok = this._extractString(this.tags.BLOCK_START + '-')) || - (tok = this._extractString(this.tags.BLOCK_START))) { - this.in_code = true; - return token(TOKEN_BLOCK_START, tok, lineno, colno); - } - else if((tok = this._extractString(this.tags.VARIABLE_START))) { - this.in_code = true; - return token(TOKEN_VARIABLE_START, tok, lineno, colno); - } - else { - tok = ''; - var data; - var in_comment = false; - - if(this._matches(this.tags.COMMENT_START)) { - in_comment = true; - tok = this._extractString(this.tags.COMMENT_START); - } - - // Continually consume text, breaking on the tag delimiter - // characters and checking to see if it's a start tag. - // - // We could hit the end of the template in the middle of - // our looping, so check for the null return value from - // _extractUntil - while((data = this._extractUntil(beginChars)) !== null) { - tok += data; - - if((this._matches(this.tags.BLOCK_START) || - this._matches(this.tags.VARIABLE_START) || - this._matches(this.tags.COMMENT_START)) && - !in_comment) { - if(this.lstripBlocks && - this._matches(this.tags.BLOCK_START) && - this.colno > 0 && - this.colno <= tok.length) { - var lastLine = tok.slice(-this.colno); - if(/^\s+$/.test(lastLine)) { - // Remove block leading whitespace from beginning of the string - tok = tok.slice(0, -this.colno); - if(!tok.length) { - // All data removed, collapse to avoid unnecessary nodes - // by returning next token (block start) - return this.nextToken(); - } - } - } - // If it is a start tag, stop looping - break; - } - else if(this._matches(this.tags.COMMENT_END)) { - if(!in_comment) { - throw new Error('unexpected end of comment'); - } - tok += this._extractString(this.tags.COMMENT_END); - break; - } - else { - // It does not match any tag, so add the character and - // carry on - tok += this.current(); - this.forward(); - } - } - - if(data === null && in_comment) { - throw new Error('expected end of comment, got end of file'); - } - - return token(in_comment ? TOKEN_COMMENT : TOKEN_DATA, - tok, - lineno, - colno); - } - } - - throw new Error('Could not parse text'); - }; - - Tokenizer.prototype.parseString = function(delimiter) { - this.forward(); - - var str = ''; - - while(!this.is_finished() && this.current() !== delimiter) { - var cur = this.current(); - - if(cur === '\\') { - this.forward(); - switch(this.current()) { - case 'n': str += '\n'; break; - case 't': str += '\t'; break; - case 'r': str += '\r'; break; - default: - str += this.current(); - } - this.forward(); - } - else { - str += cur; - this.forward(); - } - } - - this.forward(); - return str; - }; - - Tokenizer.prototype._matches = function(str) { - if(this.index + str.length > this.len) { - return null; - } - - var m = this.str.slice(this.index, this.index + str.length); - return m === str; - }; - - Tokenizer.prototype._extractString = function(str) { - if(this._matches(str)) { - this.index += str.length; - return str; - } - return null; - }; - - Tokenizer.prototype._extractUntil = function(charString) { - // Extract all non-matching chars, with the default matching set - // to everything - return this._extractMatching(true, charString || ''); - }; - - Tokenizer.prototype._extract = function(charString) { - // Extract all matching chars (no default, so charString must be - // explicit) - return this._extractMatching(false, charString); - }; - - Tokenizer.prototype._extractMatching = function (breakOnMatch, charString) { - // Pull out characters until a breaking char is hit. - // If breakOnMatch is false, a non-matching char stops it. - // If breakOnMatch is true, a matching char stops it. - - if(this.is_finished()) { - return null; - } - - var first = charString.indexOf(this.current()); - - // Only proceed if the first character doesn't meet our condition - if((breakOnMatch && first === -1) || - (!breakOnMatch && first !== -1)) { - var t = this.current(); - this.forward(); - - // And pull out all the chars one at a time until we hit a - // breaking char - var idx = charString.indexOf(this.current()); - - while(((breakOnMatch && idx === -1) || - (!breakOnMatch && idx !== -1)) && !this.is_finished()) { - t += this.current(); - this.forward(); - - idx = charString.indexOf(this.current()); - } - - return t; - } - - return ''; - }; - - Tokenizer.prototype._extractRegex = function(regex) { - var matches = this.currentStr().match(regex); - if(!matches) { - return null; - } - - // Move forward whatever was matched - this.forwardN(matches[0].length); - - return matches; - }; - - Tokenizer.prototype.is_finished = function() { - return this.index >= this.len; - }; - - Tokenizer.prototype.forwardN = function(n) { - for(var i=0; i 0) || !inline) { - for(var j=0; j argNames.length) { - args = Array.prototype.slice.call(arguments, 0, argNames.length); - - // Positional arguments that should be passed in as - // keyword arguments (essentially default values) - var vals = Array.prototype.slice.call(arguments, args.length, argCount); - for(i = 0; i < vals.length; i++) { - if(i < kwargNames.length) { - kwargs[kwargNames[i]] = vals[i]; - } - } - - args.push(kwargs); - } - else if(argCount < argNames.length) { - args = Array.prototype.slice.call(arguments, 0, argCount); - - for(i = argCount; i < argNames.length; i++) { - var arg = argNames[i]; - - // Keyword arguments that should be passed as - // positional arguments, i.e. the caller explicitly - // used the name of a positional arg - args.push(kwargs[arg]); - delete kwargs[arg]; - } - - args.push(kwargs); - } - else { - args = arguments; - } - - return func.apply(this, args); - }; - } - - function makeKeywordArgs(obj) { - obj.__keywords = true; - return obj; - } - - function getKeywordArgs(args) { - var len = args.length; - if(len) { - var lastArg = args[len - 1]; - if(lastArg && lastArg.hasOwnProperty('__keywords')) { - return lastArg; - } - } - return {}; - } - - function numArgs(args) { - var len = args.length; - if(len === 0) { - return 0; - } - - var lastArg = args[len - 1]; - if(lastArg && lastArg.hasOwnProperty('__keywords')) { - return len - 1; - } - else { - return len; - } - } - - // A SafeString object indicates that the string should not be - // autoescaped. This happens magically because autoescaping only - // occurs on primitive string objects. - function SafeString(val) { - if(typeof val !== 'string') { - return val; - } - - this.val = val; - this.length = val.length; - } - - SafeString.prototype = Object.create(String.prototype, { - length: { writable: true, configurable: true, value: 0 } - }); - SafeString.prototype.valueOf = function() { - return this.val; - }; - SafeString.prototype.toString = function() { - return this.val; - }; - - function copySafeness(dest, target) { - if(dest instanceof SafeString) { - return new SafeString(target); - } - return target.toString(); - } - - function markSafe(val) { - var type = typeof val; - - if(type === 'string') { - return new SafeString(val); - } - else if(type !== 'function') { - return val; - } - else { - return function() { - var ret = val.apply(this, arguments); - - if(typeof ret === 'string') { - return new SafeString(ret); - } - - return ret; - }; - } - } - - function suppressValue(val, autoescape) { - val = (val !== undefined && val !== null) ? val : ''; - - if(autoescape && typeof val === 'string') { - val = lib.escape(val); - } - - return val; - } - - function ensureDefined(val, lineno, colno) { - if(val === null || val === undefined) { - throw new lib.TemplateError( - 'attempted to output null or undefined value', - lineno + 1, - colno + 1 - ); - } - return val; - } - - function memberLookup(obj, val) { - obj = obj || {}; - - if(typeof obj[val] === 'function') { - return function() { - return obj[val].apply(obj, arguments); - }; - } - - return obj[val]; - } - - function callWrap(obj, name, context, args) { - if(!obj) { - throw new Error('Unable to call `' + name + '`, which is undefined or falsey'); - } - else if(typeof obj !== 'function') { - throw new Error('Unable to call `' + name + '`, which is not a function'); - } - - // jshint validthis: true - return obj.apply(context, args); - } - - function contextOrFrameLookup(context, frame, name) { - var val = frame.lookup(name); - return (val !== undefined && val !== null) ? - val : - context.lookup(name); - } - - function handleError(error, lineno, colno) { - if(error.lineno) { - return error; - } - else { - return new lib.TemplateError(error, lineno, colno); - } - } - - function asyncEach(arr, dimen, iter, cb) { - if(lib.isArray(arr)) { - var len = arr.length; - - lib.asyncIter(arr, function(item, i, next) { - switch(dimen) { - case 1: iter(item, i, len, next); break; - case 2: iter(item[0], item[1], i, len, next); break; - case 3: iter(item[0], item[1], item[2], i, len, next); break; - default: - item.push(i, next); - iter.apply(this, item); - } - }, cb); - } - else { - lib.asyncFor(arr, function(key, val, i, len, next) { - iter(key, val, i, len, next); - }, cb); - } - } - - function asyncAll(arr, dimen, func, cb) { - var finished = 0; - var len, i; - var outputArr; - - function done(i, output) { - finished++; - outputArr[i] = output; - - if(finished === len) { - cb(null, outputArr.join('')); - } - } - - if(lib.isArray(arr)) { - len = arr.length; - outputArr = new Array(len); - - if(len === 0) { - cb(null, ''); - } - else { - for(i = 0; i < arr.length; i++) { - var item = arr[i]; - - switch(dimen) { - case 1: func(item, i, len, done); break; - case 2: func(item[0], item[1], i, len, done); break; - case 3: func(item[0], item[1], item[2], i, len, done); break; - default: - item.push(i, done); - // jshint validthis: true - func.apply(this, item); - } - } - } - } - else { - var keys = lib.keys(arr); - len = keys.length; - outputArr = new Array(len); - - if(len === 0) { - cb(null, ''); - } - else { - for(i = 0; i < keys.length; i++) { - var k = keys[i]; - func(k, arr[k], i, len, done); - } - } - } - } - - module.exports = { - Frame: Frame, - makeMacro: makeMacro, - makeKeywordArgs: makeKeywordArgs, - numArgs: numArgs, - suppressValue: suppressValue, - ensureDefined: ensureDefined, - memberLookup: memberLookup, - contextOrFrameLookup: contextOrFrameLookup, - callWrap: callWrap, - handleError: handleError, - isArray: lib.isArray, - keys: lib.keys, - SafeString: SafeString, - copySafeness: copySafeness, - markSafe: markSafe, - asyncEach: asyncEach, - asyncAll: asyncAll - }; - - -/***/ }, -/* 13 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var lib = __webpack_require__(1); - var r = __webpack_require__(12); - - function normalize(value, defaultValue) { - if(value === null || value === undefined || value === false) { - return defaultValue; - } - return value; - } - - var filters = { - abs: function(n) { - return Math.abs(n); - }, - - batch: function(arr, linecount, fill_with) { - var i; - var res = []; - var tmp = []; - - for(i = 0; i < arr.length; i++) { - if(i % linecount === 0 && tmp.length) { - res.push(tmp); - tmp = []; - } - - tmp.push(arr[i]); - } - - if(tmp.length) { - if(fill_with) { - for(i = tmp.length; i < linecount; i++) { - tmp.push(fill_with); - } - } - - res.push(tmp); - } - - return res; - }, - - capitalize: function(str) { - str = normalize(str, ''); - var ret = str.toLowerCase(); - return r.copySafeness(str, ret.charAt(0).toUpperCase() + ret.slice(1)); - }, - - center: function(str, width) { - str = normalize(str, ''); - width = width || 80; - - if(str.length >= width) { - return str; - } - - var spaces = width - str.length; - var pre = lib.repeat(' ', spaces/2 - spaces % 2); - var post = lib.repeat(' ', spaces/2); - return r.copySafeness(str, pre + str + post); - }, - - 'default': function(val, def, bool) { - if(bool) { - return val ? val : def; - } - else { - return (val !== undefined) ? val : def; - } - }, - - dictsort: function(val, case_sensitive, by) { - if (!lib.isObject(val)) { - throw new lib.TemplateError('dictsort filter: val must be an object'); - } - - var array = []; - for (var k in val) { - // deliberately include properties from the object's prototype - array.push([k,val[k]]); - } - - var si; - if (by === undefined || by === 'key') { - si = 0; - } else if (by === 'value') { - si = 1; - } else { - throw new lib.TemplateError( - 'dictsort filter: You can only sort by either key or value'); - } - - array.sort(function(t1, t2) { - var a = t1[si]; - var b = t2[si]; - - if (!case_sensitive) { - if (lib.isString(a)) { - a = a.toUpperCase(); - } - if (lib.isString(b)) { - b = b.toUpperCase(); - } - } - - return a > b ? 1 : (a === b ? 0 : -1); - }); - - return array; - }, - - dump: function(obj) { - return JSON.stringify(obj); - }, - - escape: function(str) { - if(typeof str === 'string' || - str instanceof r.SafeString) { - return lib.escape(str); - } - return str; - }, - - safe: function(str) { - return r.markSafe(str); - }, - - first: function(arr) { - return arr[0]; - }, - - groupby: function(arr, attr) { - return lib.groupBy(arr, attr); - }, - - indent: function(str, width, indentfirst) { - str = normalize(str, ''); - - if (str === '') return ''; - - width = width || 4; - var res = ''; - var lines = str.split('\n'); - var sp = lib.repeat(' ', width); - - for(var i=0; i .a.b.c. - res = new_ + str.split('').join(new_) + new_; - return r.copySafeness(str, res); - } - - var nextIndex = str.indexOf(old); - // if # of replacements to perform is 0, or the string to does - // not contain the old value, return the string - if(maxCount === 0 || nextIndex === -1){ - return str; - } - - var pos = 0; - var count = 0; // # of replacements made - - while(nextIndex > -1 && (maxCount === -1 || count < maxCount)){ - // Grab the next chunk of src string and add it with the - // replacement, to the result - res += str.substring(pos, nextIndex) + new_; - // Increment our pointer in the src string - pos = nextIndex + old.length; - count++; - // See if there are any more replacements to be made - nextIndex = str.indexOf(old, pos); - } - - // We've either reached the end, or done the max # of - // replacements, tack on any remaining string - if(pos < str.length) { - res += str.substring(pos); - } - - return r.copySafeness(originalStr, res); - }, - - reverse: function(val) { - var arr; - if(lib.isString(val)) { - arr = filters.list(val); - } - else { - // Copy it - arr = lib.map(val, function(v) { return v; }); - } - - arr.reverse(); - - if(lib.isString(val)) { - return r.copySafeness(val, arr.join('')); - } - return arr; - }, - - round: function(val, precision, method) { - precision = precision || 0; - var factor = Math.pow(10, precision); - var rounder; - - if(method === 'ceil') { - rounder = Math.ceil; - } - else if(method === 'floor') { - rounder = Math.floor; - } - else { - rounder = Math.round; - } - - return rounder(val * factor) / factor; - }, - - slice: function(arr, slices, fillWith) { - var sliceLength = Math.floor(arr.length / slices); - var extra = arr.length % slices; - var offset = 0; - var res = []; - - for(var i=0; i= extra) { - slice.push(fillWith); - } - res.push(slice); - } - - return res; - }, - - sort: r.makeMacro(['value', 'reverse', 'case_sensitive', 'attribute'], [], function(arr, reverse, caseSens, attr) { - // Copy it - arr = lib.map(arr, function(v) { return v; }); - - arr.sort(function(a, b) { - var x, y; - - if(attr) { - x = a[attr]; - y = b[attr]; - } - else { - x = a; - y = b; - } - - if(!caseSens && lib.isString(x) && lib.isString(y)) { - x = x.toLowerCase(); - y = y.toLowerCase(); - } - - if(x < y) { - return reverse ? 1 : -1; - } - else if(x > y) { - return reverse ? -1: 1; - } - else { - return 0; - } - }); - - return arr; - }), - - string: function(obj) { - return r.copySafeness(obj, obj); - }, - - striptags: function(input, preserve_linebreaks) { - input = normalize(input, ''); - preserve_linebreaks = preserve_linebreaks || false; - var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>|/gi; - var trimmedInput = filters.trim(input.replace(tags, '')); - var res = ''; - if (preserve_linebreaks) { - res = trimmedInput - .replace(/^ +| +$/gm, '') // remove leading and trailing spaces - .replace(/ +/g, ' ') // squash adjacent spaces - .replace(/(\r\n)/g, '\n') // normalize linebreaks (CRLF -> LF) - .replace(/\n\n\n+/g, '\n\n'); // squash abnormal adjacent linebreaks - } else { - res = trimmedInput.replace(/\s+/gi, ' '); - } - return r.copySafeness(input, res); - }, - - title: function(str) { - str = normalize(str, ''); - var words = str.split(' '); - for(var i = 0; i < words.length; i++) { - words[i] = filters.capitalize(words[i]); - } - return r.copySafeness(str, words.join(' ')); - }, - - trim: function(str) { - return r.copySafeness(str, str.replace(/^\s*|\s*$/g, '')); - }, - - truncate: function(input, length, killwords, end) { - var orig = input; - input = normalize(input, ''); - length = length || 255; - - if (input.length <= length) - return input; - - if (killwords) { - input = input.substring(0, length); - } else { - var idx = input.lastIndexOf(' ', length); - if(idx === -1) { - idx = length; - } - - input = input.substring(0, idx); - } - - input += (end !== undefined && end !== null) ? end : '...'; - return r.copySafeness(orig, input); - }, - - upper: function(str) { - str = normalize(str, ''); - return str.toUpperCase(); - }, - - urlencode: function(obj) { - var enc = encodeURIComponent; - if (lib.isString(obj)) { - return enc(obj); - } else { - var parts; - if (lib.isArray(obj)) { - parts = obj.map(function(item) { - return enc(item[0]) + '=' + enc(item[1]); - }); - } else { - parts = []; - for (var k in obj) { - if (obj.hasOwnProperty(k)) { - parts.push(enc(k) + '=' + enc(obj[k])); - } - } - } - return parts.join('&'); - } - }, - - urlize: function(str, length, nofollow) { - if (isNaN(length)) length = Infinity; - - var noFollowAttr = (nofollow === true ? ' rel="nofollow"' : ''); - - // For the jinja regexp, see - // https://github.com/mitsuhiko/jinja2/blob/f15b814dcba6aa12bc74d1f7d0c881d55f7126be/jinja2/utils.py#L20-L23 - var puncRE = /^(?:\(|<|<)?(.*?)(?:\.|,|\)|\n|>)?$/; - // from http://blog.gerv.net/2011/05/html5_email_address_regexp/ - var emailRE = /^[\w.!#$%&'*+\-\/=?\^`{|}~]+@[a-z\d\-]+(\.[a-z\d\-]+)+$/i; - var httpHttpsRE = /^https?:\/\/.*$/; - var wwwRE = /^www\./; - var tldRE = /\.(?:org|net|com)(?:\:|\/|$)/; - - var words = str.split(/\s+/).filter(function(word) { - // If the word has no length, bail. This can happen for str with - // trailing whitespace. - return word && word.length; - }).map(function(word) { - var matches = word.match(puncRE); - var possibleUrl = matches && matches[1] || word; - - // url that starts with http or https - if (httpHttpsRE.test(possibleUrl)) - return '' + possibleUrl.substr(0, length) + ''; - - // url that starts with www. - if (wwwRE.test(possibleUrl)) - return '' + possibleUrl.substr(0, length) + ''; - - // an email address of the form username@domain.tld - if (emailRE.test(possibleUrl)) - return '' + possibleUrl + ''; - - // url that ends in .com, .org or .net that is not an email address - if (tldRE.test(possibleUrl)) - return '' + possibleUrl.substr(0, length) + ''; - - return word; - - }); - - return words.join(' '); - }, - - wordcount: function(str) { - str = normalize(str, ''); - var words = (str) ? str.match(/\w+/g) : null; - return (words) ? words.length : null; - }, - - 'float': function(val, def) { - var res = parseFloat(val); - return isNaN(res) ? def : res; - }, - - 'int': function(val, def) { - var res = parseInt(val, 10); - return isNaN(res) ? def : res; - } - }; - - // Aliases - filters.d = filters['default']; - filters.e = filters.escape; - - module.exports = filters; - - -/***/ }, -/* 14 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var Loader = __webpack_require__(15); - var PrecompiledLoader = __webpack_require__(16); - - var WebLoader = Loader.extend({ - init: function(baseURL, opts) { - this.baseURL = baseURL || '.'; - opts = opts || {}; - - // By default, the cache is turned off because there's no way - // to "watch" templates over HTTP, so they are re-downloaded - // and compiled each time. (Remember, PRECOMPILE YOUR - // TEMPLATES in production!) - this.useCache = !!opts.useCache; - - // We default `async` to false so that the simple synchronous - // API can be used when you aren't doing anything async in - // your templates (which is most of the time). This performs a - // sync ajax request, but that's ok because it should *only* - // happen in development. PRECOMPILE YOUR TEMPLATES. - this.async = !!opts.async; - }, - - resolve: function(from, to) { // jshint ignore:line - throw new Error('relative templates not support in the browser yet'); - }, - - getSource: function(name, cb) { - var useCache = this.useCache; - var result; - this.fetch(this.baseURL + '/' + name, function(err, src) { - if(err) { - if(cb) { - cb(err.content); - } else { - if (err.status === 404) { - result = null; - } else { - throw err.content; - } - } - } - else { - result = { src: src, - path: name, - noCache: !useCache }; - if(cb) { - cb(null, result); - } - } - }); - - // if this WebLoader isn't running asynchronously, the - // fetch above would actually run sync and we'll have a - // result here - return result; - }, - - fetch: function(url, cb) { - // Only in the browser please - var ajax; - var loading = true; - - if(window.XMLHttpRequest) { // Mozilla, Safari, ... - ajax = new XMLHttpRequest(); - } - else if(window.ActiveXObject) { // IE 8 and older - /* global ActiveXObject */ - ajax = new ActiveXObject('Microsoft.XMLHTTP'); - } - - ajax.onreadystatechange = function() { - if(ajax.readyState === 4 && loading) { - loading = false; - if(ajax.status === 0 || ajax.status === 200) { - cb(null, ajax.responseText); - } - else { - cb({ status: ajax.status, content: ajax.responseText }); - } - } - }; - - url += (url.indexOf('?') === -1 ? '?' : '&') + 's=' + - (new Date().getTime()); - - ajax.open('GET', url, this.async); - ajax.send(); - } - }); - - module.exports = { - WebLoader: WebLoader, - PrecompiledLoader: PrecompiledLoader - }; - - -/***/ }, -/* 15 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var path = __webpack_require__(3); - var Obj = __webpack_require__(6); - var lib = __webpack_require__(1); - - var Loader = Obj.extend({ - on: function(name, func) { - this.listeners = this.listeners || {}; - this.listeners[name] = this.listeners[name] || []; - this.listeners[name].push(func); - }, - - emit: function(name /*, arg1, arg2, ...*/) { - var args = Array.prototype.slice.call(arguments, 1); - - if(this.listeners && this.listeners[name]) { - lib.each(this.listeners[name], function(listener) { - listener.apply(null, args); - }); - } - }, - - resolve: function(from, to) { - return path.resolve(path.dirname(from), to); - }, - - isRelative: function(filename) { - return (filename.indexOf('./') === 0 || filename.indexOf('../') === 0); - } - }); - - module.exports = Loader; - - -/***/ }, -/* 16 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var Loader = __webpack_require__(15); - - var PrecompiledLoader = Loader.extend({ - init: function(compiledTemplates) { - this.precompiled = compiledTemplates || {}; - }, - - getSource: function(name) { - if (this.precompiled[name]) { - return { - src: { type: 'code', - obj: this.precompiled[name] }, - path: name - }; - } - return null; - } - }); - - module.exports = PrecompiledLoader; - - -/***/ }, -/* 17 */ -/***/ function(module, exports) { - - 'use strict'; - - function cycler(items) { - var index = -1; - - return { - current: null, - reset: function() { - index = -1; - this.current = null; - }, - - next: function() { - index++; - if(index >= items.length) { - index = 0; - } - - this.current = items[index]; - return this.current; - }, - }; - - } - - function joiner(sep) { - sep = sep || ','; - var first = true; - - return function() { - var val = first ? '' : sep; - first = false; - return val; - }; - } - - // Making this a function instead so it returns a new object - // each time it's called. That way, if something like an environment - // uses it, they will each have their own copy. - function globals() { - return { - range: function(start, stop, step) { - if(!stop) { - stop = start; - start = 0; - step = 1; - } - else if(!step) { - step = 1; - } - - var arr = []; - var i; - if (step > 0) { - for (i=start; istop; i+=step) { - arr.push(i); - } - } - return arr; - }, - - // lipsum: function(n, html, min, max) { - // }, - - cycler: function() { - return cycler(Array.prototype.slice.call(arguments)); - }, - - joiner: function(sep) { - return joiner(sep); - } - }; - } - - module.exports = globals; - - -/***/ }, -/* 18 */ -/***/ function(module, exports) { - - function installCompat() { - 'use strict'; - - // This must be called like `nunjucks.installCompat` so that `this` - // references the nunjucks instance - var runtime = this.runtime; // jshint ignore:line - var lib = this.lib; // jshint ignore:line - - var orig_contextOrFrameLookup = runtime.contextOrFrameLookup; - runtime.contextOrFrameLookup = function(context, frame, key) { - var val = orig_contextOrFrameLookup.apply(this, arguments); - if (val === undefined) { - switch (key) { - case 'True': - return true; - case 'False': - return false; - case 'None': - return null; - } - } - - return val; - }; - - var orig_memberLookup = runtime.memberLookup; - var ARRAY_MEMBERS = { - pop: function(index) { - if (index === undefined) { - return this.pop(); - } - if (index >= this.length || index < 0) { - throw new Error('KeyError'); - } - return this.splice(index, 1); - }, - remove: function(element) { - for (var i = 0; i < this.length; i++) { - if (this[i] === element) { - return this.splice(i, 1); - } - } - throw new Error('ValueError'); - }, - count: function(element) { - var count = 0; - for (var i = 0; i < this.length; i++) { - if (this[i] === element) { - count++; - } - } - return count; - }, - index: function(element) { - var i; - if ((i = this.indexOf(element)) === -1) { - throw new Error('ValueError'); - } - return i; - }, - find: function(element) { - return this.indexOf(element); - }, - insert: function(index, elem) { - return this.splice(index, 0, elem); - } - }; - var OBJECT_MEMBERS = { - items: function() { - var ret = []; - for(var k in this) { - ret.push([k, this[k]]); - } - return ret; - }, - values: function() { - var ret = []; - for(var k in this) { - ret.push(this[k]); - } - return ret; - }, - keys: function() { - var ret = []; - for(var k in this) { - ret.push(k); - } - return ret; - }, - get: function(key, def) { - var output = this[key]; - if (output === undefined) { - output = def; - } - return output; - }, - has_key: function(key) { - return this.hasOwnProperty(key); - }, - pop: function(key, def) { - var output = this[key]; - if (output === undefined && def !== undefined) { - output = def; - } else if (output === undefined) { - throw new Error('KeyError'); - } else { - delete this[key]; - } - return output; - }, - popitem: function() { - for (var k in this) { - // Return the first object pair. - var val = this[k]; - delete this[k]; - return [k, val]; - } - throw new Error('KeyError'); - }, - setdefault: function(key, def) { - if (key in this) { - return this[key]; - } - if (def === undefined) { - def = null; - } - return this[key] = def; - }, - update: function(kwargs) { - for (var k in kwargs) { - this[k] = kwargs[k]; - } - return null; // Always returns None - } - }; - OBJECT_MEMBERS.iteritems = OBJECT_MEMBERS.items; - OBJECT_MEMBERS.itervalues = OBJECT_MEMBERS.values; - OBJECT_MEMBERS.iterkeys = OBJECT_MEMBERS.keys; - runtime.memberLookup = function(obj, val, autoescape) { // jshint ignore:line - obj = obj || {}; - - // If the object is an object, return any of the methods that Python would - // otherwise provide. - if (lib.isArray(obj) && ARRAY_MEMBERS.hasOwnProperty(val)) { - return function() {return ARRAY_MEMBERS[val].apply(obj, arguments);}; - } - - if (lib.isObject(obj) && OBJECT_MEMBERS.hasOwnProperty(val)) { - return function() {return OBJECT_MEMBERS[val].apply(obj, arguments);}; - } - - return orig_memberLookup.apply(this, arguments); - }; - } - - module.exports = installCompat; - - -/***/ } -/******/ ]); \ No newline at end of file diff --git a/certidude/static/js/nunjucks.min.js b/certidude/static/js/nunjucks.min.js deleted file mode 100644 index 12870b4..0000000 --- a/certidude/static/js/nunjucks.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! Browser bundle of nunjucks 2.3.0 */ -var nunjucks=function(e){function t(i){if(n[i])return n[i].exports;var r=n[i]={exports:{},id:i,loaded:!1};return e[i].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";var i=n(1),r=n(2),s=n(15),o=n(14),a=n(3);e.exports={},e.exports.Environment=r.Environment,e.exports.Template=r.Template,e.exports.Loader=s,e.exports.FileSystemLoader=o.FileSystemLoader,e.exports.PrecompiledLoader=o.PrecompiledLoader,e.exports.WebLoader=o.WebLoader,e.exports.compiler=n(7),e.exports.parser=n(8),e.exports.lexer=n(9),e.exports.runtime=n(12),e.exports.lib=i,e.exports.nodes=n(10),e.exports.installJinjaCompat=n(18);var l;e.exports.configure=function(e,t){t=t||{},i.isObject(e)&&(t=e,e=null);var n;return o.FileSystemLoader?n=new o.FileSystemLoader(e,{watch:t.watch,noCache:t.noCache}):o.WebLoader&&(n=new o.WebLoader(e,{useCache:t.web&&t.web.useCache,async:t.web&&t.web.async})),l=new r.Environment(n,t),t&&t.express&&l.express(t.express),l},e.exports.compile=function(t,n,i,r){return l||e.exports.configure(),new e.exports.Template(t,n,i,r)},e.exports.render=function(t,n,i){return l||e.exports.configure(),l.render(t,n,i)},e.exports.renderString=function(t,n,i){return l||e.exports.configure(),l.renderString(t,n,i)},a&&(e.exports.precompile=a.precompile,e.exports.precompileString=a.precompileString)},function(e,t){"use strict";var n=Array.prototype,i=Object.prototype,r={"&":"&",'"':""","'":"'","<":"<",">":">"},s=/[&"'<>]/g,o=function(e){return r[e]},t=e.exports={};t.prettifyError=function(e,n,i){if(i.Update||(i=new t.TemplateError(i)),i.Update(e),!n){var r=i;i=new Error(r.message),i.name=r.name}return i},t.TemplateError=function(e,t,n){var i=this;if(e instanceof Error){i=e,e=e.name+": "+e.message;try{i.name=""}catch(r){i=this}}else Error.captureStackTrace&&Error.captureStackTrace(i);return i.name="Template render error",i.message=e,i.lineno=t,i.colno=n,i.firstUpdate=!0,i.Update=function(e){var t="("+(e||"unknown path")+")";return this.firstUpdate&&(this.lineno&&this.colno?t+=" [Line "+this.lineno+", Column "+this.colno+"]":this.lineno&&(t+=" [Line "+this.lineno+"]")),t+="\n ",this.firstUpdate&&(t+=" "),this.message=t+(this.message||""),this.firstUpdate=!1,this},i},t.TemplateError.prototype=Error.prototype,t.escape=function(e){return e.replace(s,o)},t.isFunction=function(e){return"[object Function]"===i.toString.call(e)},t.isArray=Array.isArray||function(e){return"[object Array]"===i.toString.call(e)},t.isString=function(e){return"[object String]"===i.toString.call(e)},t.isObject=function(e){return"[object Object]"===i.toString.call(e)},t.groupBy=function(e,n){for(var i={},r=t.isFunction(n)?n:function(e){return e[n]},s=0;si;i++)n+=e;return n},t.each=function(e,t,i){if(null!=e)if(n.each&&e.each===n.each)e.forEach(t,i);else if(e.length===+e.length)for(var r=0,s=e.length;s>r;r++)t.call(i,e[r],r,e)},t.map=function(e,t){var i=[];if(null==e)return i;if(n.map&&e.map===n.map)return e.map(t);for(var r=0;ra?n(t,e[t],a,o,r):i()}var s=t.keys(e),o=s.length,a=-1;r()},t.indexOf=Array.prototype.indexOf?function(e,t,n){return Array.prototype.indexOf.call(e,t,n)}:function(e,t,n){var i=this.length>>>0;for(n=+n||0,Math.abs(n)===1/0&&(n=0),0>n&&(n+=i,0>n&&(n=0));i>n;n++)if(e[n]===t)return n;return-1},Array.prototype.map||(Array.prototype.map=function(){throw new Error("map is unimplemented for this js engine")}),t.keys=function(e){if(Object.prototype.keys)return e.keys();var t=[];for(var n in e)e.hasOwnProperty(n)&&t.push(n);return t}},function(e,t,n){"use strict";function i(e,t,n){o(function(){e(t,n)})}var r,s=n(3),o=n(4),a=n(1),l=n(6),c=n(7),h=n(13),u=n(14),p=n(12),f=n(17),m=p.Frame;u.PrecompiledLoader=n(16);var d=l.extend({init:function(e,t){t=this.opts=t||{},this.opts.dev=!!t.dev,this.opts.autoescape=null!=t.autoescape?t.autoescape:!0,this.opts.throwOnUndefined=!!t.throwOnUndefined,this.opts.trimBlocks=!!t.trimBlocks,this.opts.lstripBlocks=!!t.lstripBlocks,this.loaders=[],e?this.loaders=a.isArray(e)?e:[e]:u.FileSystemLoader?this.loaders=[new u.FileSystemLoader("views")]:u.WebLoader&&(this.loaders=[new u.WebLoader("/views")]),window.nunjucksPrecompiled&&this.loaders.unshift(new u.PrecompiledLoader(window.nunjucksPrecompiled)),this.initCache(),this.globals=f(),this.filters={},this.asyncFilters=[],this.extensions={},this.extensionsList=[];for(var n in h)this.addFilter(n,h[n])},initCache:function(){a.each(this.loaders,function(e){e.cache={},"function"==typeof e.on&&e.on("update",function(t){e.cache[t]=null})})},addExtension:function(e,t){return t._name=e,this.extensions[e]=t,this.extensionsList.push(t),this},removeExtension:function(e){var t=this.getExtension(e);t&&(this.extensionsList=a.without(this.extensionsList,t),delete this.extensions[e])},getExtension:function(e){return this.extensions[e]},hasExtension:function(e){return!!this.extensions[e]},addGlobal:function(e,t){return this.globals[e]=t,this},getGlobal:function(e){if(!this.globals[e])throw new Error("global not found: "+e);return this.globals[e]},addFilter:function(e,t,n){var i=t;return n&&this.asyncFilters.push(e),this.filters[e]=i,this},getFilter:function(e){if(!this.filters[e])throw new Error("filter not found: "+e);return this.filters[e]},resolveTemplate:function(e,t,n){var i=e.isRelative&&t?e.isRelative(n):!1;return i&&e.resolve?e.resolve(t,n):n},getTemplate:function(e,t,n,i,s){var o=this,l=null;if(e&&e.raw&&(e=e.raw),a.isFunction(n)&&(s=n,n=null,t=t||!1),a.isFunction(t)&&(s=t,t=!1),e instanceof r)l=e;else{if("string"!=typeof e)throw new Error("template names must be a string: "+e);for(var c=0;ch){for(var t=0,n=a.length-c;n>t;t++)a[t]=a[t+c];a.length-=c,c=0}}a.length=0,c=0,l=!1}function r(e){var t=1,n=new u(e),i=document.createTextNode("");return n.observe(i,{characterData:!0}),function(){t=-t,i.data=t}}function s(e){return function(){function t(){clearTimeout(n),clearInterval(i),e()}var n=setTimeout(t,0),i=setInterval(t,50)}}e.exports=n;var o,a=[],l=!1,c=0,h=1024,u=t.MutationObserver||t.WebKitMutationObserver;o="function"==typeof u?r(i):s(i),n.requestFlush=o,n.makeRequestCallFromTimer=s}).call(t,function(){return this}())},function(e,t){"use strict";function n(e,t,i){var r=function(){};r.prototype=e.prototype;var s=new r,o=/xyz/.test(function(){xyz})?/\bparent\b/:/.*/;i=i||{};for(var a in i){var l=i[a],c=s[a];"function"==typeof c&&"function"==typeof l&&o.test(l)?s[a]=function(e,t){return function(){var n=this.parent;this.parent=t;var i=e.apply(this,arguments);return this.parent=n,i}}(l,c):s[a]=l}s.typename=t;var h=function(){s.init&&s.init.apply(this,arguments)};return h.prototype=s,h.prototype.constructor=h,h.extend=function(e,t){return"object"==typeof e&&(t=e,e="anonymous"),n(h,e,t)},h}e.exports=n(Object,"Object",{})},function(e,t,n){"use strict";function i(e){return function(t,n){this.compile(t.left,n),this.emit(e),this.compile(t.right,n)}}var r=n(1),s=n(8),o=n(11),a=n(10),l=n(6),c=n(12).Frame,h={"==":"==","!=":"!=","<":"<",">":">","<=":"<=",">=":">="},u=l.extend({init:function(e,t){this.templateName=e,this.codebuf=[],this.lastId=0,this.buffer=null,this.bufferStack=[],this.scopeClosers="",this.inBlock=!1,this.throwOnUndefined=t},fail:function(e,t,n){throw void 0!==t&&(t+=1),void 0!==n&&(n+=1),new r.TemplateError(e,t,n)},pushBufferId:function(e){this.bufferStack.push(this.buffer),this.buffer=e,this.emit("var "+this.buffer+' = "";')},popBufferId:function(){this.buffer=this.bufferStack.pop()},emit:function(e){this.codebuf.push(e)},emitLine:function(e){this.emit(e+"\n")},emitLines:function(){r.each(r.toArray(arguments),function(e){this.emitLine(e)},this)},emitFuncBegin:function(e){this.buffer="output",this.scopeClosers="",this.emitLine("function "+e+"(env, context, frame, runtime, cb) {"),this.emitLine("var lineno = null;"),this.emitLine("var colno = null;"),this.emitLine("var "+this.buffer+' = "";'),this.emitLine("try {")},emitFuncEnd:function(e){e||this.emitLine("cb(null, "+this.buffer+");"),this.closeScopeLevels(),this.emitLine("} catch (e) {"),this.emitLine(" cb(runtime.handleError(e, lineno, colno));"),this.emitLine("}"),this.emitLine("}"),this.buffer=null},addScopeLevel:function(){this.scopeClosers+="})"},closeScopeLevels:function(){this.emitLine(this.scopeClosers+";"),this.scopeClosers=""},withScopedSyntax:function(e){var t=this.scopeClosers;this.scopeClosers="",e.call(this),this.closeScopeLevels(),this.scopeClosers=t},makeCallback:function(e){var t=this.tmpid();return"function("+t+(e?","+e:"")+") {\nif("+t+") { cb("+t+"); return; }"},tmpid:function(){return this.lastId++,"t_"+this.lastId},_templateName:function(){return null==this.templateName?"undefined":JSON.stringify(this.templateName)},_compileChildren:function(e,t){for(var n=e.children,i=0,r=n.length;r>i;i++)this.compile(n[i],t)},_compileAggregate:function(e,t,n,i){n&&this.emit(n);for(var r=0;r0&&this.emit(","),this.compile(e.children[r],t);i&&this.emit(i)},_compileExpression:function(e,t){this.assertType(e,a.Literal,a.Symbol,a.Group,a.Array,a.Dict,a.FunCall,a.Caller,a.Filter,a.LookupVal,a.Compare,a.InlineIf,a.In,a.And,a.Or,a.Not,a.Add,a.Concat,a.Sub,a.Mul,a.Div,a.FloorDiv,a.Mod,a.Pow,a.Neg,a.Pos,a.Compare,a.NodeList),this.compile(e,t)},assertType:function(e){for(var t=r.toArray(arguments).slice(1),n=!1,i=0;i0&&this.emit(","),e){var i=this.tmpid();this.emitLine("function(cb) {"),this.emitLine("if(!cb) { cb = function(err) { if(err) { throw err; }}}"),this.pushBufferId(i),this.withScopedSyntax(function(){this.compile(e,t),this.emitLine("cb(null, "+i+");")}),this.popBufferId(),this.emitLine("return "+i+";"),this.emitLine("}")}else this.emit("null")},this),n){var l=this.tmpid();this.emitLine(", "+this.makeCallback(l)),this.emitLine(this.buffer+" += runtime.suppressValue("+l+", "+o+" && env.opts.autoescape);"),this.addScopeLevel()}else this.emit(")"),this.emit(", "+o+" && env.opts.autoescape);\n")},compileCallExtensionAsync:function(e,t){this.compileCallExtension(e,t,!0)},compileNodeList:function(e,t){this._compileChildren(e,t)},compileLiteral:function(e){if("string"==typeof e.value){var t=e.value.replace(/\\/g,"\\\\");t=t.replace(/"/g,'\\"'),t=t.replace(/\n/g,"\\n"),t=t.replace(/\r/g,"\\r"),t=t.replace(/\t/g,"\\t"),this.emit('"'+t+'"')}else null===e.value?this.emit("null"):this.emit(e.value.toString())},compileSymbol:function(e,t){var n,i=e.value;(n=t.lookup(i))?this.emit(n):this.emit('runtime.contextOrFrameLookup(context, frame, "'+i+'")')},compileGroup:function(e,t){this._compileAggregate(e,t,"(",")")},compileArray:function(e,t){this._compileAggregate(e,t,"[","]")},compileDict:function(e,t){this._compileAggregate(e,t,"{","}")},compilePair:function(e,t){var n=e.key,i=e.value;n instanceof a.Symbol?n=new a.Literal(n.lineno,n.colno,n.value):n instanceof a.Literal&&"string"==typeof n.value||this.fail("compilePair: Dict keys must be strings or names",n.lineno,n.colno),this.compile(n,t),this.emit(": "),this._compileExpression(i,t)},compileInlineIf:function(e,t){this.emit("("),this.compile(e.cond,t),this.emit("?"),this.compile(e.body,t),this.emit(":"),null!==e.else_?this.compile(e.else_,t):this.emit('""'),this.emit(")")},compileIn:function(e,t){this.emit("("),this.compile(e.right,t),this.emit(".indexOf("),this.compile(e.left,t),this.emit(") !== -1)")},compileOr:i(" || "),compileAnd:i(" && "),compileAdd:i(" + "),compileConcat:i(' + "" + '),compileSub:i(" - "),compileMul:i(" * "),compileDiv:i(" / "),compileMod:i(" % "),compileNot:function(e,t){this.emit("!"),this.compile(e.target,t)},compileFloorDiv:function(e,t){this.emit("Math.floor("),this.compile(e.left,t),this.emit(" / "),this.compile(e.right,t),this.emit(")")},compilePow:function(e,t){this.emit("Math.pow("),this.compile(e.left,t),this.emit(", "),this.compile(e.right,t),this.emit(")")},compileNeg:function(e,t){this.emit("-"),this.compile(e.target,t)},compilePos:function(e,t){this.emit("+"),this.compile(e.target,t)},compileCompare:function(e,t){this.compile(e.expr,t);for(var n=0;n0&&!this.skip(i.TOKEN_COMMA)&&this.fail("parseFrom: expected comma",e.lineno,e.colno);var a=this.parsePrimary();if("_"===a.value.charAt(0)&&this.fail("parseFrom: names starting with an underscore cannot be imported",a.lineno,a.colno),this.skipSymbol("as")){var l=this.parsePrimary();s.addChild(new r.Pair(a.lineno,a.colno,a,l))}else s.addChild(a);n=this.parseWithContext()}return new r.FromImport(e.lineno,e.colno,t,s,n)},parseBlock:function(){var e=this.peekToken();this.skipSymbol("block")||this.fail("parseBlock: expected block",e.lineno,e.colno);var t=new r.Block(e.lineno,e.colno);return t.name=this.parsePrimary(),t.name instanceof r.Symbol||this.fail("parseBlock: variable name expected",e.lineno,e.colno),this.advanceAfterBlockEnd(e.value),t.body=this.parseUntilBlocks("endblock"),this.peekToken()||this.fail("parseBlock: expected endblock, got end of file"),this.advanceAfterBlockEnd(),t},parseExtends:function(){var e="extends",t=this.peekToken();this.skipSymbol(e)||this.fail("parseTemplateRef: expected "+e);var n=new r.Extends(t.lineno,t.colno);return n.template=this.parseExpression(),this.advanceAfterBlockEnd(t.value),n},parseInclude:function(){var e="include",t=this.peekToken();this.skipSymbol(e)||this.fail("parseInclude: expected "+e);var n=new r.Include(t.lineno,t.colno);return n.template=this.parseExpression(),this.skipSymbol("ignore")&&this.skipSymbol("missing")&&(n.ignoreMissing=!0),this.advanceAfterBlockEnd(t.value),n},parseIf:function(){var e,t=this.peekToken();this.skipSymbol("if")||this.skipSymbol("elif")?e=new r.If(t.lineno,t.colno):this.skipSymbol("ifAsync")?e=new r.IfAsync(t.lineno,t.colno):this.fail("parseIf: expected if or elif",t.lineno,t.colno),e.cond=this.parseExpression(),this.advanceAfterBlockEnd(t.value),e.body=this.parseUntilBlocks("elif","else","endif");var n=this.peekToken();switch(n&&n.value){case"elif":e.else_=this.parseIf();break;case"else":this.advanceAfterBlockEnd(),e.else_=this.parseUntilBlocks("endif"),this.advanceAfterBlockEnd();break;case"endif":e.else_=null,this.advanceAfterBlockEnd();break;default:this.fail("parseIf: expected elif, else, or endif, got end of file")}return e},parseSet:function(){var e=this.peekToken();this.skipSymbol("set")||this.fail("parseSet: expected set",e.lineno,e.colno);for(var t,n=new r.Set(e.lineno,e.colno,[]);(t=this.parsePrimary())&&(n.targets.push(t),this.skip(i.TOKEN_COMMA)););return this.skipValue(i.TOKEN_OPERATOR,"=")||this.fail("parseSet: expected = in set tag",e.lineno,e.colno),n.value=this.parseExpression(),this.advanceAfterBlockEnd(e.value),n},parseStatement:function(){var e,t=this.peekToken();if(t.type!==i.TOKEN_SYMBOL&&this.fail("tag name expected",t.lineno,t.colno),this.breakOnBlocks&&-1!==o.indexOf(this.breakOnBlocks,t.value))return null;switch(t.value){case"raw":return this.parseRaw();case"if":case"ifAsync":return this.parseIf();case"for":case"asyncEach":case"asyncAll":return this.parseFor();case"block":return this.parseBlock();case"extends":return this.parseExtends();case"include":return this.parseInclude();case"set":return this.parseSet();case"macro":return this.parseMacro();case"call":return this.parseCall();case"import":return this.parseImport();case"from":return this.parseFrom();case"filter":return this.parseFilterStatement();default:if(this.extensions.length)for(var n=0;n0;){var o=i[0],a=i[1],l=i[2];"raw"===l?t+=1:"endraw"===l&&(t-=1),0===t?(n+=a,this.tokens.backN(o.length-a.length)):n+=o}return new r.Output(s.lineno,s.colno,[new r.TemplateData(s.lineno,s.colno,n)])},parsePostfix:function(e){for(var t,n=this.peekToken();n;){if(n.type===i.TOKEN_LEFT_PAREN)e=new r.FunCall(n.lineno,n.colno,e,this.parseSignature());else if(n.type===i.TOKEN_LEFT_BRACKET)t=this.parseAggregate(),t.children.length>1&&this.fail("invalid index"),e=new r.LookupVal(n.lineno,n.colno,e,t.children[0]);else{if(n.type!==i.TOKEN_OPERATOR||"."!==n.value)break;this.nextToken();var s=this.nextToken();s.type!==i.TOKEN_SYMBOL&&this.fail("expected name as lookup value, got "+s.value,s.lineno,s.colno),t=new r.Literal(s.lineno,s.colno,s.value),e=new r.LookupVal(n.lineno,n.colno,e,t)}n=this.peekToken()}return e},parseExpression:function(){var e=this.parseInlineIf();return e},parseInlineIf:function(){var e=this.parseOr();if(this.skipSymbol("if")){var t=this.parseOr(),n=e;e=new r.InlineIf(e.lineno,e.colno),e.body=n,e.cond=t,this.skipSymbol("else")?e.else_=this.parseOr():e.else_=null}return e},parseOr:function(){for(var e=this.parseAnd();this.skipSymbol("or");){var t=this.parseAnd();e=new r.Or(e.lineno,e.colno,e,t)}return e},parseAnd:function(){for(var e=this.parseNot();this.skipSymbol("and");){var t=this.parseNot();e=new r.And(e.lineno,e.colno,e,t)}return e},parseNot:function(){var e=this.peekToken();return this.skipSymbol("not")?new r.Not(e.lineno,e.colno,this.parseNot()):this.parseIn()},parseIn:function(){for(var e=this.parseCompare();;){var t=this.nextToken();if(!t)break;var n=t.type===i.TOKEN_SYMBOL&&"not"===t.value;if(n||this.pushToken(t),!this.skipSymbol("in")){n&&this.pushToken(t);break}var s=this.parseCompare();e=new r.In(e.lineno,e.colno,e,s),n&&(e=new r.Not(e.lineno,e.colno,e))}return e},parseCompare:function(){for(var e=["==","!=","<",">","<=",">="],t=this.parseConcat(),n=[];;){var i=this.nextToken();if(!i)break;if(-1===o.indexOf(e,i.value)){this.pushToken(i);break}n.push(new r.CompareOperand(i.lineno,i.colno,this.parseConcat(),i.value))}return n.length?new r.Compare(n[0].lineno,n[0].colno,t,n):t},parseConcat:function(){for(var e=this.parseAdd();this.skipValue(i.TOKEN_TILDE,"~");){var t=this.parseAdd();e=new r.Concat(e.lineno,e.colno,e,t)}return e},parseAdd:function(){for(var e=this.parseSub();this.skipValue(i.TOKEN_OPERATOR,"+");){var t=this.parseSub();e=new r.Add(e.lineno,e.colno,e,t)}return e},parseSub:function(){for(var e=this.parseMul();this.skipValue(i.TOKEN_OPERATOR,"-");){var t=this.parseMul();e=new r.Sub(e.lineno,e.colno,e,t)}return e},parseMul:function(){for(var e=this.parseDiv();this.skipValue(i.TOKEN_OPERATOR,"*");){var t=this.parseDiv();e=new r.Mul(e.lineno,e.colno,e,t)}return e},parseDiv:function(){for(var e=this.parseFloorDiv();this.skipValue(i.TOKEN_OPERATOR,"/");){var t=this.parseFloorDiv();e=new r.Div(e.lineno,e.colno,e,t)}return e},parseFloorDiv:function(){for(var e=this.parseMod();this.skipValue(i.TOKEN_OPERATOR,"//");){var t=this.parseMod();e=new r.FloorDiv(e.lineno,e.colno,e,t)}return e},parseMod:function(){for(var e=this.parsePow();this.skipValue(i.TOKEN_OPERATOR,"%");){var t=this.parsePow();e=new r.Mod(e.lineno,e.colno,e,t)}return e},parsePow:function(){for(var e=this.parseUnary();this.skipValue(i.TOKEN_OPERATOR,"**");){var t=this.parseUnary();e=new r.Pow(e.lineno,e.colno,e,t)}return e},parseUnary:function(e){var t,n=this.peekToken();return t=this.skipValue(i.TOKEN_OPERATOR,"-")?new r.Neg(n.lineno,n.colno,this.parseUnary(!0)):this.skipValue(i.TOKEN_OPERATOR,"+")?new r.Pos(n.lineno,n.colno,this.parseUnary(!0)):this.parsePrimary(),e||(t=this.parseFilter(t)),t},parsePrimary:function(e){var t,n=this.nextToken(),s=null;return n?n.type===i.TOKEN_STRING?t=n.value:n.type===i.TOKEN_INT?t=parseInt(n.value,10):n.type===i.TOKEN_FLOAT?t=parseFloat(n.value):n.type===i.TOKEN_BOOLEAN?"true"===n.value?t=!0:"false"===n.value?t=!1:this.fail("invalid boolean: "+n.value,n.lineno,n.colno):n.type===i.TOKEN_NONE?t=null:n.type===i.TOKEN_REGEX&&(t=new RegExp(n.value.body,n.value.flags)):this.fail("expected expression, got end of file"),void 0!==t?s=new r.Literal(n.lineno,n.colno,t):n.type===i.TOKEN_SYMBOL?(s=new r.Symbol(n.lineno,n.colno,n.value),e||(s=this.parsePostfix(s))):(this.pushToken(n),s=this.parseAggregate()),s?s:void this.fail("unexpected token: "+n.value,n.lineno,n.colno)},parseFilterName:function(){for(var e=this.expect(i.TOKEN_SYMBOL),t=e.value;this.skipValue(i.TOKEN_OPERATOR,".");)t+="."+this.expect(i.TOKEN_SYMBOL).value;return new r.Symbol(e.lineno,e.colno,t)},parseFilterArgs:function(e){if(this.peekToken().type===i.TOKEN_LEFT_PAREN){var t=this.parsePostfix(e);return t.args.children}return[]},parseFilter:function(e){for(;this.skip(i.TOKEN_PIPE);){var t=this.parseFilterName();e=new r.Filter(t.lineno,t.colno,t,new r.NodeList(t.lineno,t.colno,[e].concat(this.parseFilterArgs(e))))}return e},parseFilterStatement:function(){var e=this.peekToken();this.skipSymbol("filter")||this.fail("parseFilterStatement: expected filter");var t=this.parseFilterName(),n=this.parseFilterArgs(t);this.advanceAfterBlockEnd(e.value);var i=this.parseUntilBlocks("endfilter");this.advanceAfterBlockEnd();var s=new r.Filter(t.lineno,t.colno,t,new r.NodeList(t.lineno,t.colno,i.children[0].children.concat(n)));return new r.Output(t.lineno,t.colno,[s])},parseAggregate:function(){var e,t=this.nextToken();switch(t.type){case i.TOKEN_LEFT_PAREN:e=new r.Group(t.lineno,t.colno);break;case i.TOKEN_LEFT_BRACKET:e=new r.Array(t.lineno,t.colno);break;case i.TOKEN_LEFT_CURLY:e=new r.Dict(t.lineno,t.colno);break;default:return null}for(;;){var n=this.peekToken().type;if(n===i.TOKEN_RIGHT_PAREN||n===i.TOKEN_RIGHT_BRACKET||n===i.TOKEN_RIGHT_CURLY){this.nextToken();break}if(e.children.length>0&&(this.skip(i.TOKEN_COMMA)||this.fail("parseAggregate: expected comma after expression",t.lineno,t.colno)),e instanceof r.Dict){var s=this.parsePrimary();this.skip(i.TOKEN_COLON)||this.fail("parseAggregate: expected colon after dict key",t.lineno,t.colno);var o=this.parseExpression();e.addChild(new r.Pair(s.lineno,s.colno,s,o))}else{var a=this.parseExpression();e.addChild(a)}}return e},parseSignature:function(e,t){var n=this.peekToken();if(!t&&n.type!==i.TOKEN_LEFT_PAREN){if(e)return null;this.fail("expected arguments",n.lineno,n.colno)}n.type===i.TOKEN_LEFT_PAREN&&(n=this.nextToken());for(var s=new r.NodeList(n.lineno,n.colno),o=new r.KeywordArgs(n.lineno,n.colno),a=!1;;){if(n=this.peekToken(),!t&&n.type===i.TOKEN_RIGHT_PAREN){this.nextToken();break}if(t&&n.type===i.TOKEN_BLOCK_END)break;if(a&&!this.skip(i.TOKEN_COMMA))this.fail("parseSignature: expected comma after expression",n.lineno,n.colno);else{var l=this.parseExpression();this.skipValue(i.TOKEN_OPERATOR,"=")?o.addChild(new r.Pair(l.lineno,l.colno,l,this.parseExpression())):s.addChild(l)}a=!0}return o.children.length&&s.addChild(o),s},parseUntilBlocks:function(){var e=this.breakOnBlocks;this.breakOnBlocks=o.toArray(arguments);var t=this.parse();return this.breakOnBlocks=e,t},parseNodes:function(){for(var e,t=[];e=this.nextToken();)if(e.type===i.TOKEN_DATA){var n=e.value,s=this.peekToken(),o=s&&s.value;this.dropLeadingWhitespace&&(n=n.replace(/^\s*/,""),this.dropLeadingWhitespace=!1),s&&s.type===i.TOKEN_BLOCK_START&&"-"===o.charAt(o.length-1)&&(n=n.replace(/\s*$/,"")),t.push(new r.Output(e.lineno,e.colno,[new r.TemplateData(e.lineno,e.colno,n)]))}else if(e.type===i.TOKEN_BLOCK_START){var a=this.parseStatement();if(!a)break;t.push(a)}else if(e.type===i.TOKEN_VARIABLE_START){var l=this.parseExpression();this.advanceAfterVariableEnd(),t.push(new r.Output(e.lineno,e.colno,[l]))}else e.type!==i.TOKEN_COMMENT&&this.fail("Unexpected token at top-level: "+e.type,e.lineno,e.colno);return t},parse:function(){return new r.NodeList(0,0,this.parseNodes())},parseAsRoot:function(){return new r.Root(0,0,this.parseNodes())}});e.exports={parse:function(e,t,n){var r=new a(i.lex(e,n));return void 0!==t&&(r.extensions=t),r.parseAsRoot()}}},function(e,t,n){"use strict";function i(e,t,n,i){return{type:e,value:t,lineno:n,colno:i}}function r(e,t){this.str=e,this.index=0,this.len=e.length,this.lineno=0,this.colno=0,this.in_code=!1,t=t||{};var n=t.tags||{};this.tags={BLOCK_START:n.blockStart||c,BLOCK_END:n.blockEnd||h,VARIABLE_START:n.variableStart||u,VARIABLE_END:n.variableEnd||p,COMMENT_START:n.commentStart||f,COMMENT_END:n.commentEnd||m},this.trimBlocks=!!t.trimBlocks,this.lstripBlocks=!!t.lstripBlocks}var s=n(1),o=" \n \r ",a="()[]{}%*-+~/#,:|.<>=!",l="0123456789",c="{%",h="%}",u="{{",p="}}",f="{#",m="#}",d="string",v="whitespace",g="data",y="block-start",k="block-end",x="variable-start",b="variable-end",E="comment",w="left-paren",T="right-paren",L="left-bracket",_="right-bracket",O="left-curly",A="right-curly",S="operator",N="comma",C="colon",B="tilde",F="pipe",K="int",R="float",I="boolean",M="none",P="symbol",D="special",V="regex";r.prototype.nextToken=function(){var e,t=this.lineno,n=this.colno;if(this.in_code){var r=this.current();if(this.is_finished())return null;if('"'===r||"'"===r)return i(d,this.parseString(r),t,n);if(e=this._extract(o))return i(v,e,t,n);if((e=this._extractString(this.tags.BLOCK_END))||(e=this._extractString("-"+this.tags.BLOCK_END)))return this.in_code=!1,this.trimBlocks&&(r=this.current(),"\n"===r?this.forward():"\r"===r&&(this.forward(),r=this.current(),"\n"===r?this.forward():this.back())),i(k,e,t,n);if(e=this._extractString(this.tags.VARIABLE_END))return this.in_code=!1,i(b,e,t,n);if("r"===r&&"/"===this.str.charAt(this.index+1)){this.forwardN(2);for(var c="";!this.is_finished();){if("/"===this.current()&&"\\"!==this.previous()){this.forward();break}c+=this.current(),this.forward()}for(var h=["g","i","m","y"],u="";!this.is_finished();){var p=-1!==h.indexOf(this.current());if(!p)break;u+=this.current(),this.forward()}return i(V,{body:c,flags:u},t,n)}if(-1!==a.indexOf(r)){this.forward();var f,m=["==","!=","<=",">=","//","**"],D=r+this.current();switch(-1!==s.indexOf(m,D)&&(this.forward(),r=D),r){case"(":f=w;break;case")":f=T;break;case"[":f=L;break;case"]":f=_;break;case"{":f=O;break;case"}":f=A;break;case",":f=N;break;case":":f=C;break;case"~":f=B;break;case"|":f=F;break;default:f=S}return i(f,r,t,n)}if(e=this._extractUntil(o+a),e.match(/^[-+]?[0-9]+$/)){if("."===this.current()){this.forward();var j=this._extract(l);return i(R,e+"."+j,t,n)}return i(K,e,t,n)}if(e.match(/^(true|false)$/))return i(I,e,t,n);if("none"===e)return i(M,e,t,n);if(e)return i(P,e,t,n);throw new Error("Unexpected value while parsing: "+e)}var U=this.tags.BLOCK_START.charAt(0)+this.tags.VARIABLE_START.charAt(0)+this.tags.COMMENT_START.charAt(0)+this.tags.COMMENT_END.charAt(0);if(this.is_finished())return null;if((e=this._extractString(this.tags.BLOCK_START+"-"))||(e=this._extractString(this.tags.BLOCK_START)))return this.in_code=!0,i(y,e,t,n);if(e=this._extractString(this.tags.VARIABLE_START))return this.in_code=!0,i(x,e,t,n);e="";var W,G=!1;for(this._matches(this.tags.COMMENT_START)&&(G=!0,e=this._extractString(this.tags.COMMENT_START));null!==(W=this._extractUntil(U));){if(e+=W,(this._matches(this.tags.BLOCK_START)||this._matches(this.tags.VARIABLE_START)||this._matches(this.tags.COMMENT_START))&&!G){if(this.lstripBlocks&&this._matches(this.tags.BLOCK_START)&&this.colno>0&&this.colno<=e.length){var Y=e.slice(-this.colno);if(/^\s+$/.test(Y)&&(e=e.slice(0,-this.colno),!e.length))return this.nextToken()}break}if(this._matches(this.tags.COMMENT_END)){if(!G)throw new Error("unexpected end of comment");e+=this._extractString(this.tags.COMMENT_END);break}e+=this.current(),this.forward()}if(null===W&&G)throw new Error("expected end of comment, got end of file");return i(G?E:g,e,t,n)},r.prototype.parseString=function(e){this.forward();for(var t="";!this.is_finished()&&this.current()!==e;){var n=this.current();if("\\"===n){switch(this.forward(),this.current()){case"n":t+="\n";break;case"t":t+=" ";break;case"r":t+="\r";break;default:t+=this.current()}this.forward()}else t+=n,this.forward()}return this.forward(),t},r.prototype._matches=function(e){if(this.index+e.length>this.len)return null;var t=this.str.slice(this.index,this.index+e.length);return t===e},r.prototype._extractString=function(e){return this._matches(e)?(this.index+=e.length,e):null},r.prototype._extractUntil=function(e){return this._extractMatching(!0,e||"")},r.prototype._extract=function(e){return this._extractMatching(!1,e)},r.prototype._extractMatching=function(e,t){if(this.is_finished())return null;var n=t.indexOf(this.current());if(e&&-1===n||!e&&-1!==n){var i=this.current();this.forward();for(var r=t.indexOf(this.current());(e&&-1===r||!e&&-1!==r)&&!this.is_finished();)i+=this.current(),this.forward(),r=t.indexOf(this.current());return i}return""},r.prototype._extractRegex=function(e){var t=this.currentStr().match(e);return t?(this.forwardN(t[0].length),t):null},r.prototype.is_finished=function(){return this.index>=this.len},r.prototype.forwardN=function(e){for(var t=0;e>t;t++)this.forward()},r.prototype.forward=function(){this.index++,"\n"===this.previous()?(this.lineno++,this.colno=0):this.colno++},r.prototype.backN=function(e){for(var t=0;e>t;t++)this.back()},r.prototype.back=function(){if(this.index--,"\n"===this.current()){this.lineno--;var e=this.src.lastIndexOf("\n",this.index-1);-1===e?this.colno=this.index:this.colno=this.index-e}else this.colno--},r.prototype.current=function(){return this.is_finished()?"":this.str.charAt(this.index)},r.prototype.currentStr=function(){return this.is_finished()?"":this.str.substr(this.index)},r.prototype.previous=function(){return this.str.charAt(this.index-1)},e.exports={lex:function(e,t){return new r(e,t)},TOKEN_STRING:d,TOKEN_WHITESPACE:v,TOKEN_DATA:g,TOKEN_BLOCK_START:y,TOKEN_BLOCK_END:k,TOKEN_VARIABLE_START:x,TOKEN_VARIABLE_END:b,TOKEN_COMMENT:E,TOKEN_LEFT_PAREN:w,TOKEN_RIGHT_PAREN:T,TOKEN_LEFT_BRACKET:L,TOKEN_RIGHT_BRACKET:_,TOKEN_LEFT_CURLY:O,TOKEN_RIGHT_CURLY:A,TOKEN_OPERATOR:S,TOKEN_COMMA:N,TOKEN_COLON:C,TOKEN_TILDE:B,TOKEN_PIPE:F,TOKEN_INT:K,TOKEN_FLOAT:R,TOKEN_BOOLEAN:I,TOKEN_NONE:M,TOKEN_SYMBOL:P,TOKEN_SPECIAL:D,TOKEN_REGEX:V}},function(e,t,n){(function(t){"use strict";function i(e,t,n){e instanceof t&&n.push(e),e instanceof a&&e.findAll(t,n)}function r(e,n){function i(e,n,i){for(var r=e.split("\n"),s=0;s0||!i))for(var o=0;n>o;o++)t.stdout.write(" ");s===r.length-1?t.stdout.write(r[s]):t.stdout.write(r[s]+"\n")}}if(n=n||0,i(e.typename+": ",n),e instanceof c)i("\n"),s.each(e.children,function(e){r(e,n+2)});else if(e instanceof re)i(e.extName+"."+e.prop),i("\n"),e.args&&r(e.args,n+2),e.contentArgs&&s.each(e.contentArgs,function(e){r(e,n+2)});else{var o=null,l=null;if(e.iterFields(function(e,t){e instanceof a?(o=o||{},o[t]=e):(l=l||{},l[t]=e)}),l?i(JSON.stringify(l,null,2)+"\n",null,!0):i("\n"),o)for(var h in o)r(o[h],n+2)}}var s=n(1),o=n(6),a=o.extend("Node",{init:function(e,t){this.lineno=e,this.colno=t;for(var n=this.fields,i=0,r=n.length;r>i;i++){var s=n[i],o=arguments[i+2];void 0===o&&(o=null),this[s]=o}},findAll:function(e,t){t=t||[];var n,r;if(this instanceof c){var s=this.children;for(n=0,r=s.length;r>n;n++)i(s[n],e,t)}else{var o=this.fields;for(n=0,r=o.length;r>n;n++)i(this[o[n]],e,t)}return t},iterFields:function(e){s.each(this.fields,function(t){e(this[t],t)},this)}}),l=a.extend("Value",{fields:["value"]}),c=a.extend("NodeList",{fields:["children"],init:function(e,t,n){this.parent(e,t,n||[])},addChild:function(e){this.children.push(e)}}),h=c.extend("Root"),u=l.extend("Literal"),p=l.extend("Symbol"),f=c.extend("Group"),m=c.extend("Array"),d=a.extend("Pair",{fields:["key","value"]}),v=c.extend("Dict"),g=a.extend("LookupVal",{fields:["target","val"]}),y=a.extend("If",{fields:["cond","body","else_"]}),k=y.extend("IfAsync"),x=a.extend("InlineIf",{fields:["cond","body","else_"]}),b=a.extend("For",{fields:["arr","name","body","else_"]}),E=b.extend("AsyncEach"),w=b.extend("AsyncAll"),T=a.extend("Macro",{fields:["name","args","body"]}),L=T.extend("Caller"),_=a.extend("Import",{fields:["template","target","withContext"]}),O=a.extend("FromImport",{fields:["template","names","withContext"],init:function(e,t,n,i,r){this.parent(e,t,n,i||new c,r)}}),A=a.extend("FunCall",{fields:["name","args"]}),S=A.extend("Filter"),N=S.extend("FilterAsync",{fields:["name","args","symbol"]}),C=v.extend("KeywordArgs"),B=a.extend("Block",{fields:["name","body"]}),F=a.extend("Super",{fields:["blockName","symbol"]}),K=a.extend("TemplateRef",{fields:["template"]}),R=K.extend("Extends"),I=a.extend("Include",{fields:["template","ignoreMissing"]}),M=a.extend("Set",{fields:["targets","value"]}),P=c.extend("Output"),D=u.extend("TemplateData"),V=a.extend("UnaryOp",{fields:["target"]}),j=a.extend("BinOp",{fields:["left","right"]}),U=j.extend("In"),W=j.extend("Or"),G=j.extend("And"),Y=V.extend("Not"),H=j.extend("Add"),$=j.extend("Concat"),z=j.extend("Sub"),X=j.extend("Mul"),q=j.extend("Div"),J=j.extend("FloorDiv"),Q=j.extend("Mod"),Z=j.extend("Pow"),ee=V.extend("Neg"),te=V.extend("Pos"),ne=a.extend("Compare",{fields:["expr","ops"]}),ie=a.extend("CompareOperand",{fields:["expr","type"]}),re=a.extend("CallExtension",{fields:["extName","prop","args","contentArgs"],init:function(e,t,n,i){this.extName=e._name||e,this.prop=t,this.args=n||new c,this.contentArgs=i||[],this.autoescape=e.autoescape}}),se=re.extend("CallExtensionAsync");e.exports={Node:a,Root:h,NodeList:c,Value:l,Literal:u,Symbol:p,Group:f,Array:m,Pair:d,Dict:v,Output:P,TemplateData:D,If:y,IfAsync:k,InlineIf:x,For:b,AsyncEach:E,AsyncAll:w,Macro:T,Caller:L,Import:_,FromImport:O,FunCall:A,Filter:S,FilterAsync:N,KeywordArgs:C,Block:B,Super:F,Extends:R,Include:I,Set:M,LookupVal:g,BinOp:j,In:U,Or:W,And:G,Not:Y,Add:H,Concat:$,Sub:z,Mul:X,Div:q,FloorDiv:J,Mod:Q,Pow:Z,Neg:ee,Pos:te,Compare:ne,CompareOperand:ie,CallExtension:re,CallExtensionAsync:se,printNodes:r}}).call(t,n(3))},function(e,t,n){"use strict";function i(){return"hole_"+d++}function r(e,t){for(var n=null,i=0;ie.length){i=Array.prototype.slice.call(arguments,0,e.length);var c=Array.prototype.slice.call(arguments,i.length,a);for(r=0;ri;i++)s.push(n);r.push(s)}return r},capitalize:function(e){e=i(e,"");var t=e.toLowerCase();return s.copySafeness(e,t.charAt(0).toUpperCase()+t.slice(1))},center:function(e,t){if(e=i(e,""),t=t||80,e.length>=t)return e;var n=t-e.length,o=r.repeat(" ",n/2-n%2),a=r.repeat(" ",n/2);return s.copySafeness(e,o+e+a)},"default":function(e,t,n){return n?e?e:t:void 0!==e?e:t},dictsort:function(e,t,n){if(!r.isObject(e))throw new r.TemplateError("dictsort filter: val must be an object");var i=[];for(var s in e)i.push([s,e[s]]);var o;if(void 0===n||"key"===n)o=0;else{if("value"!==n)throw new r.TemplateError("dictsort filter: You can only sort by either key or value");o=1}return i.sort(function(e,n){var i=e[o],s=n[o];return t||(r.isString(i)&&(i=i.toUpperCase()),r.isString(s)&&(s=s.toUpperCase())),i>s?1:i===s?0:-1}),i},dump:function(e){return JSON.stringify(e)},escape:function(e){return"string"==typeof e||e instanceof s.SafeString?r.escape(e):e},safe:function(e){return s.markSafe(e)},first:function(e){return e[0]},groupby:function(e,t){return r.groupBy(e,t)},indent:function(e,t,n){if(e=i(e,""),""===e)return"";t=t||4;for(var o="",a=e.split("\n"),l=r.repeat(" ",t),c=0;c-1&&(-1===i||i>c);)o+=e.substring(l,a)+n, -l=a+t.length,c++,a=e.indexOf(t,l);return la;a++){var l=s+a*i;r>a&&s++;var c=s+(a+1)*i,h=e.slice(l,c);n&&a>=r&&h.push(n),o.push(h)}return o},sort:s.makeMacro(["value","reverse","case_sensitive","attribute"],[],function(e,t,n,i){return e=r.map(e,function(e){return e}),e.sort(function(e,s){var o,a;return i?(o=e[i],a=s[i]):(o=e,a=s),!n&&r.isString(o)&&r.isString(a)&&(o=o.toLowerCase(),a=a.toLowerCase()),a>o?t?1:-1:o>a?t?-1:1:0}),e}),string:function(e){return s.copySafeness(e,e)},striptags:function(e,t){e=i(e,""),t=t||!1;var n=/<\/?([a-z][a-z0-9]*)\b[^>]*>|/gi,r=o.trim(e.replace(n,"")),a="";return a=t?r.replace(/^ +| +$/gm,"").replace(/ +/g," ").replace(/(\r\n)/g,"\n").replace(/\n\n\n+/g,"\n\n"):r.replace(/\s+/gi," "),s.copySafeness(e,a)},title:function(e){e=i(e,"");for(var t=e.split(" "),n=0;n"+c.substr(0,t)+"":a.test(c)?'"+c.substr(0,t)+"":s.test(c)?''+c+"":l.test(c)?'"+c.substr(0,t)+"":e});return c.join(" ")},wordcount:function(e){e=i(e,"");var t=e?e.match(/\w+/g):null;return t?t.length:null},"float":function(e,t){var n=parseFloat(e);return isNaN(n)?t:n},"int":function(e,t){var n=parseInt(e,10);return isNaN(n)?t:n}};o.d=o["default"],o.e=o.escape,e.exports=o},function(e,t,n){"use strict";var i=n(15),r=n(16),s=i.extend({init:function(e,t){this.baseURL=e||".",t=t||{},this.useCache=!!t.useCache,this.async=!!t.async},resolve:function(e,t){throw new Error("relative templates not support in the browser yet")},getSource:function(e,t){var n,i=this.useCache;return this.fetch(this.baseURL+"/"+e,function(r,s){if(r)if(t)t(r.content);else{if(404!==r.status)throw r.content;n=null}else n={src:s,path:e,noCache:!i},t&&t(null,n)}),n},fetch:function(e,t){var n,i=!0;window.XMLHttpRequest?n=new XMLHttpRequest:window.ActiveXObject&&(n=new ActiveXObject("Microsoft.XMLHTTP")),n.onreadystatechange=function(){4===n.readyState&&i&&(i=!1,0===n.status||200===n.status?t(null,n.responseText):t({status:n.status,content:n.responseText}))},e+=(-1===e.indexOf("?")?"?":"&")+"s="+(new Date).getTime(),n.open("GET",e,this.async),n.send()}});e.exports={WebLoader:s,PrecompiledLoader:r}},function(e,t,n){"use strict";var i=n(3),r=n(6),s=n(1),o=r.extend({on:function(e,t){this.listeners=this.listeners||{},this.listeners[e]=this.listeners[e]||[],this.listeners[e].push(t)},emit:function(e){var t=Array.prototype.slice.call(arguments,1);this.listeners&&this.listeners[e]&&s.each(this.listeners[e],function(e){e.apply(null,t)})},resolve:function(e,t){return i.resolve(i.dirname(e),t)},isRelative:function(e){return 0===e.indexOf("./")||0===e.indexOf("../")}});e.exports=o},function(e,t,n){"use strict";var i=n(15),r=i.extend({init:function(e){this.precompiled=e||{}},getSource:function(e){return this.precompiled[e]?{src:{type:"code",obj:this.precompiled[e]},path:e}:null}});e.exports=r},function(e,t){"use strict";function n(e){var t=-1;return{current:null,reset:function(){t=-1,this.current=null},next:function(){return t++,t>=e.length&&(t=0),this.current=e[t],this.current}}}function i(e){e=e||",";var t=!0;return function(){var n=t?"":e;return t=!1,n}}function r(){return{range:function(e,t,n){t?n||(n=1):(t=e,e=0,n=1);var i,r=[];if(n>0)for(i=e;t>i;i+=n)r.push(i);else for(i=e;i>t;i+=n)r.push(i);return r},cycler:function(){return n(Array.prototype.slice.call(arguments))},joiner:function(e){return i(e)}}}e.exports=r},function(e,t){function n(){"use strict";var e=this.runtime,t=this.lib,n=e.contextOrFrameLookup;e.contextOrFrameLookup=function(e,t,i){var r=n.apply(this,arguments);if(void 0===r)switch(i){case"True":return!0;case"False":return!1;case"None":return null}return r};var i=e.memberLookup,r={pop:function(e){if(void 0===e)return this.pop();if(e>=this.length||0>e)throw new Error("KeyError");return this.splice(e,1)},remove:function(e){for(var t=0;t\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -22,14 +22,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-certificate-15-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-calendar-6.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -46,14 +46,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-compass-7-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-certificate-15.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -70,14 +70,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-download-12-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-compass-7.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -94,14 +94,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-email-2-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-download-12.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -118,14 +118,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-error-4-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-email-2.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\n\n\n\n\n\n\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -142,14 +142,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-flag-3-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-error-4.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -166,14 +166,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-home-4-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-flag-3.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -190,14 +190,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-info-6-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-home-7.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\n\n\n\n\n\n\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -214,14 +214,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-key-2-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-info-8.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -238,14 +238,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-lock-3-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-key-3.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -262,14 +262,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-magnifier-4-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-magnifier-4.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -286,14 +286,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-mobile-phone-6-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-mobile-phone-7.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -310,14 +310,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-pen-10-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-pen-14.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -334,14 +334,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-tag-2-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-tag-3.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -358,14 +358,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-time-13-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-user-5.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -382,14 +382,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-warning-6-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-warning-8.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\n\n\n\n\n\n\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -406,38 +406,14 @@ root: root })(); })(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-wireless-6-icon.svg"] = (function() { +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-x-mark-8.svg"] = (function() { function root(env, context, frame, runtime, cb) { var lineno = null; var colno = null; var output = ""; try { var parentTemplate = null; -output += "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"; -if(parentTemplate) { -parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); -} else { -cb(null, output); -} -; -} catch (e) { - cb(runtime.handleError(e, lineno, colno)); -} -} -return { -root: root -}; - -})(); -})(); -(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-x-mark-5-icon.svg"] = (function() { -function root(env, context, frame, runtime, cb) { -var lineno = null; -var colno = null; -var output = ""; -try { -var parentTemplate = null; -output += "\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; +output += ""; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -461,7 +437,7 @@ var colno = null; var output = ""; try { var parentTemplate = null; -output += "\n\n\n \n \n Certidude server\n \n \n \n \n \n \n\n\n \n
\n Loading certificate authority...\n
\n\n\n\n\n\n\n"; +output += "\n\n\n \n \n Certidude server\n \n \n \n \n \n \n\n\n \n
\n Loading certificate authority...\n
\n\n\n\n\n\n\n"; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { @@ -485,12 +461,38 @@ var colno = null; var output = ""; try { var parentTemplate = null; -output += "\n
\n

Hi "; -output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"username"), env.opts.autoescape); -output += ",

\n\n

Request submission is allowed from: "; -if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets")) { +output += "\n

\n

"; +output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"gn"), env.opts.autoescape); +output += " "; +output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"sn"), env.opts.autoescape); +output += " ("; +output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"name"), env.opts.autoescape); +output += ") settings

\n\n

Mails will be sent to: "; +output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"mail"), env.opts.autoescape); +output += "

\n\n

You can click here to generate bundle\nfor current user account.

\n\n"; +if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) { +output += "\n\n

Authority certificate

\n\n

Several things such as CRL location and e-mails are hardcoded into\nthe certificate and\nas such require complete reset of X509 infrastructure if some of them needs to be changed:

\n\n

Mails will appear from: "; +output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"certificate")),"email_address"), env.opts.autoescape); +output += "

\n\n\n

Authority settings

\n\n

These can be reconfigured via /etc/certidude/server.conf on the server.

\n\n

Outgoing mail server:\n"; +if(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"outbox")) { +output += "\n "; +output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"outbox"), env.opts.autoescape); +output += "\n"; +; +} +else { +output += "\n E-mail disabled\n"; +; +} +output += "

\n\n

Authenticated users allowed from:\n\n"; +if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets"))) { +output += "\n anywhere\n

\n"; +; +} +else { +output += "\n

\n
    \n "; frame = frame.push(); -var t_3 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"); +var t_3 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets"); if(t_3) {var t_2 = t_3.length; for(var t_1=0; t_1 < t_3.length; t_1++) { var t_4 = t_3[t_1]; @@ -502,26 +504,29 @@ frame.set("loop.revindex0", t_2 - t_1 - 1); frame.set("loop.first", t_1 === 0); frame.set("loop.last", t_1 === t_2 - 1); frame.set("loop.length", t_2); +output += "\n
  • "; output += runtime.suppressValue(t_4, env.opts.autoescape); -output += " "; +output += "
  • \n "; ; } } frame = frame.pop(); +output += "\n
\n"; +; +} +output += "\n\n\n

Request submission is allowed from:\n\n"; +if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"))) { +output += "\n anywhere\n

\n"; ; } else { -output += "anywhere"; -; -} -output += "

\n

Autosign is allowed from: "; -if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets")) { +output += "\n

\n
    \n "; frame = frame.push(); -var t_7 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets"); +var t_7 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"); if(t_7) {var t_6 = t_7.length; for(var t_5=0; t_5 < t_7.length; t_5++) { var t_8 = t_7[t_5]; -frame.set("i", t_8); +frame.set("subnet", t_8); frame.set("loop.index", t_5 + 1); frame.set("loop.index0", t_5); frame.set("loop.revindex", t_6 - t_5); @@ -529,26 +534,29 @@ frame.set("loop.revindex0", t_6 - t_5 - 1); frame.set("loop.first", t_5 === 0); frame.set("loop.last", t_5 === t_6 - 1); frame.set("loop.length", t_6); +output += "\n
  • "; output += runtime.suppressValue(t_8, env.opts.autoescape); -output += " "; +output += "
  • \n "; ; } } frame = frame.pop(); +output += "\n
\n"; +; +} +output += "\n\n

Autosign is allowed from:\n"; +if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets"))) { +output += "\n anywhere\n

\n"; ; } else { -output += "nowhere"; -; -} -output += "

\n

Authority administration is allowed from: "; -if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets")) { +output += "\n

\n
    \n "; frame = frame.push(); -var t_11 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"); +var t_11 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets"); if(t_11) {var t_10 = t_11.length; for(var t_9=0; t_9 < t_11.length; t_9++) { var t_12 = t_11[t_9]; -frame.set("i", t_12); +frame.set("subnet", t_12); frame.set("loop.index", t_9 + 1); frame.set("loop.index0", t_9); frame.set("loop.revindex", t_10 - t_9); @@ -556,25 +564,29 @@ frame.set("loop.revindex0", t_10 - t_9 - 1); frame.set("loop.first", t_9 === 0); frame.set("loop.last", t_9 === t_10 - 1); frame.set("loop.length", t_10); +output += "\n
  • "; output += runtime.suppressValue(t_12, env.opts.autoescape); -output += " "; +output += "
  • \n "; ; } } frame = frame.pop(); +output += "\n
\n"; +; +} +output += "\n\n

Authority administration is allowed from:\n"; +if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"))) { +output += "\n anywhere\n

\n"; ; } else { -output += "anywhere"; -; -} -output += "\n

Authority administration allowed for: "; +output += "\n

    \n "; frame = frame.push(); -var t_15 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users"); +var t_15 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"); if(t_15) {var t_14 = t_15.length; for(var t_13=0; t_13 < t_15.length; t_13++) { var t_16 = t_15[t_13]; -frame.set("i", t_16); +frame.set("subnet", t_16); frame.set("loop.index", t_13 + 1); frame.set("loop.index0", t_13); frame.set("loop.revindex", t_14 - t_13); @@ -582,71 +594,132 @@ frame.set("loop.revindex0", t_14 - t_13 - 1); frame.set("loop.first", t_13 === 0); frame.set("loop.last", t_13 === t_14 - 1); frame.set("loop.length", t_14); +output += "\n
  • "; output += runtime.suppressValue(t_16, env.opts.autoescape); -output += " "; +output += "
  • \n "; ; } } frame = frame.pop(); -output += "

    \n
\n"; -var t_17; -t_17 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity"); -frame.set("s", t_17, true); -if(frame.topLevel) { -context.setVariable("s", t_17); +output += "\n \n"; +; } -if(frame.topLevel) { -context.addExport("s", t_17); -} -output += "\n\n\n
\n

Pending requests

\n\n\n
    \n "; +output += "\n\n

    Authority administration allowed for:

    \n\n
      \n"; frame = frame.push(); -var t_20 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"requests"); -runtime.asyncEach(t_20, 1, function(request, t_18, t_19,next) { -frame.set("request", request); -frame.set("loop.index", t_18 + 1); -frame.set("loop.index0", t_18); -frame.set("loop.revindex", t_19 - t_18); -frame.set("loop.revindex0", t_19 - t_18 - 1); -frame.set("loop.first", t_18 === 0); -frame.set("loop.last", t_18 === t_19 - 1); -frame.set("loop.length", t_19); -output += "\n "; -env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_23,t_21) { -if(t_23) { cb(t_23); return; } -t_21.render(context.getVariables(), frame, function(t_24,t_22) { -if(t_24) { cb(t_24); return; } -output += t_22 -output += "\n\t "; -next(t_18); -})}); -}, function(t_26,t_25) { -if(t_26) { cb(t_26); return; } +var t_19 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users"); +if(t_19) {var t_17; +if(runtime.isArray(t_19)) { +var t_18 = t_19.length; +for(t_17=0; t_17 < t_19.length; t_17++) { +var t_20 = t_19[t_17][0] +frame.set("handle", t_19[t_17][0]); +var t_21 = t_19[t_17][1] +frame.set("full_name", t_19[t_17][1]); +frame.set("loop.index", t_17 + 1); +frame.set("loop.index0", t_17); +frame.set("loop.revindex", t_18 - t_17); +frame.set("loop.revindex0", t_18 - t_17 - 1); +frame.set("loop.first", t_17 === 0); +frame.set("loop.last", t_17 === t_18 - 1); +frame.set("loop.length", t_18); +output += "\n
    • "; +output += runtime.suppressValue(t_21, env.opts.autoescape); +output += "
    • \n"; +; +} +} else { +t_17 = -1; +var t_18 = runtime.keys(t_19).length; +for(var t_22 in t_19) { +t_17++; +var t_23 = t_19[t_22]; +frame.set("handle", t_22); +frame.set("full_name", t_23); +frame.set("loop.index", t_17 + 1); +frame.set("loop.index0", t_17); +frame.set("loop.revindex", t_18 - t_17); +frame.set("loop.revindex0", t_18 - t_17 - 1); +frame.set("loop.first", t_17 === 0); +frame.set("loop.last", t_17 === t_18 - 1); +frame.set("loop.length", t_18); +output += "\n
    • "; +output += runtime.suppressValue(t_23, env.opts.autoescape); +output += "
    • \n"; +; +} +} +} frame = frame.pop(); -output += "\n
    • \n

      No certificate signing requests to sign! You can submit a certificate signing request by:

      \n
      certidude setup client ";
      +output += "\n
    \n
\n\n"; +; +} +else { +output += "\n

Here you can renew your certificates

\n\n"; +; +} +output += "\n\n"; +var t_24; +t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity"); +frame.set("s", t_24, true); +if(frame.topLevel) { +context.setVariable("s", t_24); +} +if(frame.topLevel) { +context.addExport("s", t_24); +} +output += "\n\n\n"; +if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) { +output += "\n
\n

Pending requests

\n\n

Submit a certificate signing request with Certidude:

\n
certidude setup client ";
 output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"common_name"), env.opts.autoescape);
-output += "
\n \n \n
\n\n\n
\n

Signed certificates

\n \n
    \n "; +output += "\n\n
      \n "; frame = frame.push(); -var t_29 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"signed"))); -runtime.asyncEach(t_29, 1, function(certificate, t_27, t_28,next) { -frame.set("certificate", certificate); -frame.set("loop.index", t_27 + 1); -frame.set("loop.index0", t_27); -frame.set("loop.revindex", t_28 - t_27); -frame.set("loop.revindex0", t_28 - t_27 - 1); -frame.set("loop.first", t_27 === 0); -frame.set("loop.last", t_27 === t_28 - 1); -frame.set("loop.length", t_28); -output += "\n "; -env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_32,t_30) { +var t_27 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests"); +if(t_27) {var t_26 = t_27.length; +for(var t_25=0; t_25 < t_27.length; t_25++) { +var t_28 = t_27[t_25]; +frame.set("request", t_28); +frame.set("loop.index", t_25 + 1); +frame.set("loop.index0", t_25); +frame.set("loop.revindex", t_26 - t_25); +frame.set("loop.revindex0", t_26 - t_25 - 1); +frame.set("loop.first", t_25 === 0); +frame.set("loop.last", t_25 === t_26 - 1); +frame.set("loop.length", t_26); +output += "\n "; +env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_31,t_29) { +if(t_31) { cb(t_31); return; } +t_29.render(context.getVariables(), frame, function(t_32,t_30) { if(t_32) { cb(t_32); return; } -t_30.render(context.getVariables(), frame, function(t_33,t_31) { -if(t_33) { cb(t_33); return; } -output += t_31 +output += t_30 output += "\n\t "; -next(t_27); })}); -}, function(t_35,t_34) { -if(t_35) { cb(t_35); return; } +} +} +frame = frame.pop(); +output += "\n
    • \n

      No certificate signing requests to sign!

      \n
    • \n
    \n
\n\n
\n

Signed certificates

\n \n
    \n "; +frame = frame.push(); +var t_35 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"signed"))); +if(t_35) {var t_34 = t_35.length; +for(var t_33=0; t_33 < t_35.length; t_33++) { +var t_36 = t_35[t_33]; +frame.set("certificate", t_36); +frame.set("loop.index", t_33 + 1); +frame.set("loop.index0", t_33); +frame.set("loop.revindex", t_34 - t_33); +frame.set("loop.revindex0", t_34 - t_33 - 1); +frame.set("loop.first", t_33 === 0); +frame.set("loop.last", t_33 === t_34 - 1); +frame.set("loop.length", t_34); +output += "\n "; +env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_39,t_37) { +if(t_39) { cb(t_39); return; } +t_37.render(context.getVariables(), frame, function(t_40,t_38) { +if(t_40) { cb(t_40); return; } +output += t_38 +output += "\n\t "; +})}); +} +} frame = frame.pop(); output += "\n
\n
\n\n
\n

Log

\n

\n \n \n \n \n \n

\n
    \n
\n
\n\n
\n

Revoked certificates

\n

To fetch certificate revocation list:

\n
curl ";
 output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "window")),"location")),"href"), env.opts.autoescape);
@@ -656,41 +729,44 @@ output += "/certificate/ > session.pem\n    openssl ocsp -issuer session.pem -CA
 output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"url"), env.opts.autoescape);
 output += "/ocsp/ -serial 0x\n    
\n -->\n
    \n "; frame = frame.push(); -var t_38 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"revoked"); -if(t_38) {var t_37 = t_38.length; -for(var t_36=0; t_36 < t_38.length; t_36++) { -var t_39 = t_38[t_36]; -frame.set("j", t_39); -frame.set("loop.index", t_36 + 1); -frame.set("loop.index0", t_36); -frame.set("loop.revindex", t_37 - t_36); -frame.set("loop.revindex0", t_37 - t_36 - 1); -frame.set("loop.first", t_36 === 0); -frame.set("loop.last", t_36 === t_37 - 1); -frame.set("loop.length", t_37); +var t_43 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked"); +if(t_43) {var t_42 = t_43.length; +for(var t_41=0; t_41 < t_43.length; t_41++) { +var t_44 = t_43[t_41]; +frame.set("j", t_44); +frame.set("loop.index", t_41 + 1); +frame.set("loop.index0", t_41); +frame.set("loop.revindex", t_42 - t_41); +frame.set("loop.revindex0", t_42 - t_41 - 1); +frame.set("loop.first", t_41 === 0); +frame.set("loop.last", t_41 === t_42 - 1); +frame.set("loop.length", t_42); output += "\n
  • \n "; -output += runtime.suppressValue(runtime.memberLookup((t_39),"changed"), env.opts.autoescape); +output += runtime.suppressValue(runtime.memberLookup((t_44),"changed"), env.opts.autoescape); output += "\n "; -output += runtime.suppressValue(runtime.memberLookup((t_39),"serial_number"), env.opts.autoescape); +output += runtime.suppressValue(runtime.memberLookup((t_44),"serial_number"), env.opts.autoescape); output += " "; -output += runtime.suppressValue(runtime.memberLookup((t_39),"identity"), env.opts.autoescape); +output += runtime.suppressValue(runtime.memberLookup((t_44),"identity"), env.opts.autoescape); output += "\n
  • \n "; ; } } -if (!t_37) { +if (!t_42) { output += "\n
  • Great job! No certificate signing requests to sign.
  • \n\t "; } frame = frame.pop(); -output += "\n
\n
\n\n
\n
\n"; +output += "\n \n
\n\n
\n
\n\n"; +; +} +output += "\n"; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { cb(null, output); } -})}); +; } catch (e) { cb(runtime.handleError(e, lineno, colno)); } @@ -894,8 +970,8 @@ var colno = null; var output = ""; try { var parentTemplate = null; -output += "
  • \n\nFetch\n"; @@ -912,7 +988,7 @@ output += "\n\n\n\n
    \n"; -env.getTemplate("img/iconmonstr-certificate-15-icon.svg", false, "views/request.html", null, function(t_3,t_1) { +env.getTemplate("img/iconmonstr-certificate-15.svg", false, "views/request.html", null, function(t_3,t_1) { if(t_3) { cb(t_3); return; } t_1.render(context.getVariables(), frame, function(t_4,t_2) { if(t_4) { cb(t_4); return; } @@ -920,9 +996,9 @@ output += t_2 output += "\n"; output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"identity"), env.opts.autoescape); output += "\n
    \n\n"; -(function(cb) {if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address")) { +if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address")) { output += "\n
    "; -env.getTemplate("img/iconmonstr-email-2-icon.svg", false, "views/request.html", null, function(t_7,t_5) { +env.getTemplate("img/iconmonstr-email-2.svg", false, "views/request.html", null, function(t_7,t_5) { if(t_7) { cb(t_7); return; } t_5.render(context.getVariables(), frame, function(t_8,t_6) { if(t_8) { cb(t_8); return; } @@ -930,12 +1006,10 @@ output += t_6 output += " "; output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address"), env.opts.autoescape); output += "
    \n"; -cb()})}); +})}); } -else { -cb()} -})(function() {output += "\n\n
    \n"; -env.getTemplate("img/iconmonstr-key-2-icon.svg", false, "views/request.html", null, function(t_11,t_9) { +output += "\n\n
    \n"; +env.getTemplate("img/iconmonstr-key-3.svg", false, "views/request.html", null, function(t_11,t_9) { if(t_11) { cb(t_11); return; } t_9.render(context.getVariables(), frame, function(t_12,t_10) { if(t_12) { cb(t_12); return; } @@ -957,9 +1031,9 @@ if(frame.topLevel) { context.addExport("key_usage", t_13); } output += "\n"; -(function(cb) {if(runtime.contextOrFrameLookup(context, frame, "key_usage")) { +if(runtime.contextOrFrameLookup(context, frame, "key_usage")) { output += "\n
    \n"; -env.getTemplate("img/iconmonstr-flag-3-icon.svg", false, "views/request.html", null, function(t_16,t_14) { +env.getTemplate("img/iconmonstr-flag-3.svg", false, "views/request.html", null, function(t_16,t_14) { if(t_16) { cb(t_16); return; } t_14.render(context.getVariables(), frame, function(t_17,t_15) { if(t_17) { cb(t_17); return; } @@ -967,17 +1041,15 @@ output += t_15 output += "\n"; output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"key_usage"), env.opts.autoescape); output += "\n
    \n"; -cb()})}); +})}); } -else { -cb()} -})(function() {output += "\n\n
  • \n\n"; +output += "\n\n\n\n"; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { cb(null, output); } -})})})})})}); +})})})}); } catch (e) { cb(runtime.handleError(e, lineno, colno)); } @@ -995,8 +1067,8 @@ var colno = null; var output = ""; try { var parentTemplate = null; -output += "
  • Fetch\n \n\n
    \n "; -env.getTemplate("img/iconmonstr-certificate-15-icon.svg", false, "views/signed.html", null, function(t_3,t_1) { +env.getTemplate("img/iconmonstr-certificate-15.svg", false, "views/signed.html", null, function(t_3,t_1) { if(t_3) { cb(t_3); return; } t_1.render(context.getVariables(), frame, function(t_4,t_2) { if(t_4) { cb(t_4); return; } output += t_2 output += "\n "; -output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"identity"), env.opts.autoescape); +output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape); output += "\n
    \n\n "; -(function(cb) {if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address")) { +if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address")) { output += "\n
    "; -env.getTemplate("img/iconmonstr-email-2-icon.svg", false, "views/signed.html", null, function(t_7,t_5) { +env.getTemplate("img/iconmonstr-email-2.svg", false, "views/signed.html", null, function(t_7,t_5) { if(t_7) { cb(t_7); return; } t_5.render(context.getVariables(), frame, function(t_8,t_6) { if(t_8) { cb(t_8); return; } @@ -1024,32 +1096,53 @@ output += t_6 output += " "; output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address"), env.opts.autoescape); output += "
    \n "; -cb()})}); +})}); } -else { -cb()} -})(function() {output += "\n\n "; -output += "\n\n
    \n \n
    \n\n
    \n "; -env.getTemplate("views/status.html", false, "views/signed.html", null, function(t_15,t_13) { +output += " "; +output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"given_name"), env.opts.autoescape); +output += " "; +output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"surname"), env.opts.autoescape); +output += "
    \n "; +})}); +} +output += "\n\n
    \n "; +env.getTemplate("img/iconmonstr-calendar-6.svg", false, "views/signed.html", null, function(t_15,t_13) { if(t_15) { cb(t_15); return; } t_13.render(context.getVariables(), frame, function(t_16,t_14) { if(t_16) { cb(t_16); return; } output += t_14 -output += "\n
    \n
  • \n"; +output += "\n -\n \n
    \n\n "; +output += "\n\n
    \n \n
    \n
    \n\n"; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { cb(null, output); } -})})})})})})}); +})})})})})}); } catch (e) { cb(runtime.handleError(e, lineno, colno)); } @@ -1135,6 +1228,44 @@ return { root: root }; +})(); +})(); +(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["views/tags.html"] = (function() { +function root(env, context, frame, runtime, cb) { +var lineno = null; +var colno = null; +var output = ""; +try { +var parentTemplate = null; +output += ""; +output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"value"), env.opts.autoescape); +output += "\n"; +if(parentTemplate) { +parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); +} else { +cb(null, output); +} +; +} catch (e) { + cb(runtime.handleError(e, lineno, colno)); +} +} +return { +root: root +}; + })(); })(); (function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["views/tagtypes.html"] = (function() { @@ -1144,7 +1275,7 @@ var colno = null; var output = ""; try { var parentTemplate = null; -output += "\n\n\n\n\n\n\n\n\n\n\n"; +output += "\n\n\n\n\n\n\n\n\n\n"; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { diff --git a/certidude/static/robots.txt b/certidude/static/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/certidude/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/certidude/static/views/authority.html b/certidude/static/views/authority.html index 1c94cd2..8f50479 100644 --- a/certidude/static/views/authority.html +++ b/certidude/static/views/authority.html @@ -1,36 +1,127 @@
    -

    Hi {{session.username}},

    +

    {{ session.user.gn }} {{ session.user.sn }} ({{session.user.name }}) settings

    -

    Request submission is allowed from: {% if session.request_subnets %}{% for i in session.request_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}

    -

    Autosign is allowed from: {% if session.autosign_subnets %}{% for i in session.autosign_subnets %}{{ i }} {% endfor %}{% else %}nowhere{% endif %}

    -

    Authority administration is allowed from: {% if session.admin_subnets %}{% for i in session.admin_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %} -

    Authority administration allowed for: {% for i in session.admin_users %}{{ i }} {% endfor %}

    +

    Mails will be sent to: {{ session.user.mail }}

    + +

    You can click here to generate bundle +for current user account.

    + +{% if session.authority %} + +

    Authority certificate

    + +

    Several things such as CRL location and e-mails are hardcoded into +the certificate and +as such require complete reset of X509 infrastructure if some of them needs to be changed:

    + +

    Mails will appear from: {{ session.authority.certificate.email_address }}

    + + +

    Authority settings

    + +

    These can be reconfigured via /etc/certidude/server.conf on the server.

    + +

    Outgoing mail server: +{% if session.authority.outbox %} + {{ session.authority.outbox }} +{% else %} + E-mail disabled +{% endif %}

    + +

    Authenticated users allowed from: + +{% if "0.0.0.0/0" in session.user_subnets %} + anywhere +

    +{% else %} +

    + +{% endif %} + + +

    Request submission is allowed from: + +{% if "0.0.0.0/0" in session.request_subnets %} + anywhere +

    +{% else %} +

    + +{% endif %} + +

    Autosign is allowed from: +{% if "0.0.0.0/0" in session.autosign_subnets %} + anywhere +

    +{% else %} +

    + +{% endif %} + +

    Authority administration is allowed from: +{% if "0.0.0.0/0" in session.admin_subnets %} + anywhere +

    +{% else %} + +{% endif %} + +

    Authority administration allowed for:

    + +
    + +{% else %} +

    Here you can renew your certificates

    + +{% endif %} + {% set s = session.certificate.identity %} +{% if session.authority %}

    Pending requests

    +

    Submit a certificate signing request with Certidude:

    +
    certidude setup client {{session.common_name}}
    -

    Signed certificates

    @@ -62,7 +153,7 @@ -->