diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc9271..eaa254a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ ## CHANGELOG +### [25.1.0] - Dec 29, 2024 +- Added support for nested templates within `template.j2` using Jinja2 include syntax +- Added support for encrypted DataTemplates using Vaulty (ChaCha20-Poly1305 encryption) +- Fixed a cosmetic issue where the button bar would visibily change size when loading a DataTemplate +- With password protected DataTemplates the prompt will now specify whether it needs the "Open" or "Modify" password +- Removed the `dt_hash` field within saved DataTemplates as it wasn't being used for anything +- You can no longer add the same DataSet using differences between uppercase and lowercase characters +- The DataSet dropdown is now sorted alphabetically with "Default" always on top +- Added the ability to remove protection from a DataTemplate after it has been added +- We no longer update the modify time of local repository files on access +- Don't output timestamp when running via systemd +- Updated Pandoc to 3.6.1 in Dockerfile + ### [24.12.1] - Dec 3, 2024 - Fixed an issue where rows with an incorrect number of fields in `data.csv` weren't being coloured red @@ -351,6 +364,8 @@ ### 21.11.0 - Nov 29, 2021 - Initial release + +[25.1.0]: https://github.com/cmason3/jinjafx_server/compare/24.12.1...25.1.0 [24.12.1]: https://github.com/cmason3/jinjafx_server/compare/24.12.0...24.12.1 [24.12.0]: https://github.com/cmason3/jinjafx_server/compare/24.10.1...24.12.0 [24.10.1]: https://github.com/cmason3/jinjafx_server/compare/24.10.0...24.10.1 diff --git a/README.md b/README.md index 8cdab9e..4e689ab 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@  

JinjaFx Server - Jinja2 Templating Tool

-JinjaFx Server is a lightweight web server that provides a Web UI to JinjaFx. It is a separate Python module which imports the "jinjafx" module to generate outputs from a web interface - it does require the "requests" module which isn't in the base install. Usage instructions are provided below, although it is considered an additional component and not part of the base JinjaFx tool, although it is probably a much easier way to use it. +JinjaFx Server is a lightweight web server that provides a Web UI to JinjaFx. It is a separate Python module which imports the "jinjafx" module to generate outputs from a web interface. Usage instructions are provided below, although it is considered an additional component and not part of the base JinjaFx tool, although it is probably a much easier way to use it. There is an AWS hosted version available at https://jinjafx.io, which is free to use and will always be running the latest development version. ### Installation @@ -45,7 +45,31 @@ Once JinjaFx Server has been started with the `-s` argument then point your web For health checking purposes, if you specify the URL `/ping` then you should get an "OK" response if the JinaFx Server is up and working (these requests are omitted from the logs). -The preferred method of running the JinjaFx Server is with HAProxy in front of it as it supports TLS termination and HTTP/2 (and more recently HTTP/3 using QUIC) or using a container orchestration tool like Kubernetes - please see the [/kubernetes](/kubernetes) directory for more information about running JinjaFx using Kubernetes. +The preferred method of running the JinjaFx Server is with HAProxy in front of it as it supports TLS termination and HTTP/2 (and more recently HTTP/3 using QUIC) or using a container orchestration tool like Kubernetes - please see the [/kubernetes](/kubernetes) directory for more information about running JinjaFx as a container. + +If you don't want to go down the container route then you can also install it as a service using systemd - the following commands will install a Python Virtual Environment in `/opt/jinjafx` and start it via systemd: + +``` +sudo python3 -m venv /opt/jinjafx +sudo /opt/jinjafx/bin/python3 -m pip install jinjafx_server lxml + +sudo tee /etc/systemd/system/jinjafx.service >/dev/null <<-EOF +[Unit] +Description=JinjaFx Server + +[Service] +Environment="VIRTUAL_ENV=/opt/jinjafx" +ExecStart=/opt/jinjafx/bin/python3 -u -m jinjafx_server -s -l 127.0.0.1 -p 8080 +SyslogIdentifier=jinjafx_server +TimeoutStartSec=60 +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + +sudo systemctl enable --now jinjafx +``` The "-r", "-s3" or "-github" arguments (mutually exclusive) allow you to specify a repository ("-r" is a local directory, "-s3" is an AWS S3 URL and "-github" is a GitHub repository) that will be used to store DataTemplates on the server via the "Get Link" and "Update Link" buttons. The generated link is guaranteed to be unique and a different link will be created every time - version 1.3.0 changed the behaviour, where previously the same link was always generated for the same DataTemplate, but this made it difficult to update DataTemplates without the link changing as it was basically a cryptographic hash of your DataTemplate. If you use an AWS S3 bucket then you will also need to provide some credentials via the two environment variables which has read and write permissions to the S3 URL. diff --git a/jinjafx_server.py b/jinjafx_server.py index b1608a3..35cb5e3 100755 --- a/jinjafx_server.py +++ b/jinjafx_server.py @@ -28,7 +28,7 @@ import re, argparse, hashlib, traceback, glob, hmac, uuid, struct, binascii, gzip, requests, ctypes, subprocess import cmarkgfm, emoji -__version__ = '24.12.1' +__version__ = '25.1.0' llock = threading.RLock() rlock = threading.RLock() @@ -329,11 +329,24 @@ def do_GET(self, head=False, cache=True, versioned=False): r = [ 'application/json', 200, json.dumps({ 'dt': self.e(rr).decode('utf-8') }).encode('utf-8'), sys._getframe().f_lineno ] - os.utime(fpath, None) - else: r = [ 'text/plain', 404, '404 Not Found\r\n'.encode('utf-8'), sys._getframe().f_lineno ] + if r[1] == 200: + if dt.lstrip().startswith('$VAULTY;'): + if 'X-Dt-Password' in self.headers: + try: + dt = jinjafx.Vaulty().decrypt(dt, self.headers['X-Dt-Password']) + r = [ 'application/json', 200, json.dumps({ 'dt': self.e(dt.encode('utf-8')).decode('utf-8') }).encode('utf-8'), sys._getframe().f_lineno ] + + except Exception: + cheaders['X-Dt-Authentication'] = 'Open' + r = [ 'text/plain', 401, '401 Unauthorized\r\n'.encode('utf-8'), sys._getframe().f_lineno ] + + else: + cheaders['X-Dt-Authentication'] = 'Open' + r = [ 'text/plain', 401, '401 Unauthorized\r\n'.encode('utf-8'), sys._getframe().f_lineno ] + if r[1] == 200: mo = re.search(r'dt_password: "(\S+)"', dt) if mo != None: @@ -344,12 +357,15 @@ def do_GET(self, head=False, cache=True, versioned=False): if mm != None: t = binascii.unhexlify(mm.group(1).encode('utf-8')) if t != self.derive_key(self.headers['X-Dt-Password'], t[2:int(t[1]) + 2], t[0]): + cheaders['X-Dt-Authentication'] = 'Modify' r = [ 'text/plain', 401, '401 Unauthorized\r\n'.encode('utf-8'), sys._getframe().f_lineno ] else: + cheaders['X-Dt-Authentication'] = 'Open' r = [ 'text/plain', 401, '401 Unauthorized\r\n'.encode('utf-8'), sys._getframe().f_lineno ] else: + cheaders['X-Dt-Authentication'] = 'Open' r = [ 'text/plain', 401, '401 Unauthorized\r\n'.encode('utf-8'), sys._getframe().f_lineno ] else: @@ -458,6 +474,8 @@ def do_POST(self): self.elapsed = None self.error = None + cheaders = {} + uc = self.path.split('?', 1) params = { x[0]: x[1] for x in [x.split('=') for x in uc[1].split('&') ] } if len(uc) > 1 else { } fpath = uc[0] @@ -483,9 +501,17 @@ def do_POST(self): gvars = {} dt = json.loads(postdata.decode('utf-8')) - template = self.d(dt['template']) if 'template' in dt and len(dt['template'].strip()) > 0 else b'' data = self.d(dt['data']) if 'data' in dt and len(dt['data'].strip()) > 0 else b'' + if isinstance(dt['template'], dict): + for t in dt['template']: + dt['template'][t] = self.d(dt['template'][t]).decode('utf-8') if len(dt['template'][t].strip()) > 0 else '' + + else: + dt['template'] = self.d(dt['template']).decode('utf-8') if len(dt['template'].strip()) > 0 else '' + + template = dt['template'] + if 'vars' in dt and len(dt['vars'].strip()) > 0: gyaml = self.d(dt['vars']).decode('utf-8') @@ -493,10 +519,10 @@ def do_POST(self): vpw = self.d(dt['vpw']).decode('utf-8') if gyaml.lstrip().startswith('$ANSIBLE_VAULT;'): - gyaml = jinjafx.Vault().decrypt(gyaml.encode('utf-8'), vpw).decode('utf-8') + gyaml = jinjafx.AnsibleVault().decrypt(gyaml.encode('utf-8'), vpw).decode('utf-8') def yaml_vault_tag(loader, node): - return jinjafx.Vault().decrypt(node.value.encode('utf-8'), vpw).decode('utf-8') + return jinjafx.AnsibleVault().decrypt(node.value.encode('utf-8'), vpw).decode('utf-8') yaml.add_constructor('!vault', yaml_vault_tag, yaml.SafeLoader) @@ -511,7 +537,7 @@ def yaml_vault_tag(loader, node): ocount = 0 ret = [0, None] - t = StoppableJinjaFx(jinjafx.JinjaFx().jinjafx, template.decode('utf-8'), data.decode('utf-8'), gvars, ret) + t = StoppableJinjaFx(jinjafx.JinjaFx().jinjafx, template, data.decode('utf-8'), gvars, ret) if timelimit > 0: while t.is_alive() and ((time.time() * 1000) - st) <= (timelimit * 1000): @@ -627,15 +653,24 @@ def html_escape(text): dt_password = '' dt_opassword = '' dt_mpassword = '' + dt_epassword = '' dt_revision = 1 + dt_protected = 0 + dt_encrypted = 0 if hasattr(self, 'headers'): + if 'X-Dt-Protected' in self.headers: + dt_protected = int(self.headers['X-Dt-Protected']) if 'X-Dt-Password' in self.headers: dt_password = self.headers['X-Dt-Password'] if 'X-Dt-Open-Password' in self.headers: dt_opassword = self.headers['X-Dt-Open-Password'] + if 'X-Dt-Encrypted' in self.headers: + dt_encrypted = int(self.headers['X-Dt-Encrypted']) if 'X-Dt-Modify-Password' in self.headers: dt_mpassword = self.headers['X-Dt-Modify-Password'] + if 'X-Dt-Encrypt-Password' in self.headers: + dt_epassword = self.headers['X-Dt-Encrypt-Password'] if 'X-Dt-Revision' in self.headers: dt_revision = int(self.headers['X-Dt-Revision']) @@ -666,7 +701,7 @@ def html_escape(text): dt_yml += ' "' + ds + '":\n' if vdt['data'] == '': - dt_yml += ' data: ""\n\n' + dt_yml += ' data: ""\n' else: dt_yml += ' data: |2\n' dt_yml += re.sub('^', ' ' * 8, vdt['data'].rstrip(), flags=re.MULTILINE) + '\n\n' @@ -682,7 +717,7 @@ def html_escape(text): vdt['vars'] = self.d(dt['vars']).decode('utf-8') if 'vars' in dt and len(dt['vars'].strip()) > 0 else '' if vdt['data'] == '': - dt_yml += ' data: ""\n\n' + dt_yml += ' data: ""\n' else: dt_yml += ' data: |2\n' dt_yml += re.sub('^', ' ' * 4, vdt['data'].rstrip(), flags=re.MULTILINE) + '\n\n' @@ -693,19 +728,35 @@ def html_escape(text): dt_yml += ' vars: |2\n' dt_yml += re.sub('^', ' ' * 4, vdt['vars'].rstrip(), flags=re.MULTILINE) + '\n\n' - vdt['template'] = self.d(dt['template']).decode('utf-8') if 'template' in dt and len(dt['template'].strip()) > 0 else '' + if isinstance(dt['template'], dict): + dt_yml += ' template:\n' - if vdt['template'] == '': - dt_yml += ' template: ""\n' + for t in dt['template']: + te = self.d(dt['template'][t]).decode('utf-8') if len(dt['template'][t].strip()) > 0 else '' + + if te == '': + dt_yml += ' "' + t + '": ""\n' + else: + dt_yml += ' "' + t + '": |2\n' + dt_yml += re.sub('^', ' ' * 6, te, flags=re.MULTILINE) + '\n\n' + else: - dt_yml += ' template: |2\n' - dt_yml += re.sub('^', ' ' * 4, vdt['template'], flags=re.MULTILINE) + '\n' + te = self.d(dt['template']).decode('utf-8') if len(dt['template'].strip()) > 0 else '' - dt_yml += '\nrevision: ' + str(dt_revision) + '\n' - dt_yml += 'dataset: "' + dt['dataset'] + '"\n' + if te == '': + dt_yml += ' template: ""\n' + else: + dt_yml += ' template: |2\n' + dt_yml += re.sub('^', ' ' * 4, te, flags=re.MULTILINE) + '\n\n' + + if not dt_yml.endswith('\n\n'): + dt_yml += '\n' - dt_hash = hashlib.sha256(dt_yml.encode('utf-8')).hexdigest() - dt_yml += 'dt_hash: "' + dt_hash + '"\n' + dt_yml += 'revision: ' + str(dt_revision) + '\n' + dt_yml += 'dataset: "' + dt['dataset'] + '"\n' + + if dt_encrypted: + dt_yml += 'encrypted: 1\n' if 'id' in params: if re.search(r'^[A-Za-z0-9_-]{1,24}$', params['id']): @@ -728,54 +779,87 @@ def update_dt(rdt, dt_yml, r): rpassword = mm.group(1) if mm != None else mo.group(1) t = binascii.unhexlify(rpassword.encode('utf-8')) if t != self.derive_key(dt_password, t[2:int(t[1]) + 2], t[0]): + cheaders['X-Dt-Authentication'] = 'Modify' if (mm != None) else 'Open' r = [ 'text/plain', 401, '401 Unauthorized\r\n', sys._getframe().f_lineno ] else: + cheaders['X-Dt-Authentication'] = 'Modify' if (mm != None) else 'Open' r = [ 'text/plain', 401, '401 Unauthorized\r\n', sys._getframe().f_lineno ] if r[1] != 401: - if dt_opassword != '' or dt_mpassword != '': - if dt_opassword != '': - dt_yml += 'dt_password: "' + binascii.hexlify(self.derive_key(dt_opassword)).decode('utf-8') + '"\n' + if dt_protected: + if dt_opassword != '' or dt_mpassword != '': + if dt_opassword != '': + dt_yml += 'dt_password: "' + binascii.hexlify(self.derive_key(dt_opassword)).decode('utf-8') + '"\n' - if dt_mpassword != '': - dt_yml += 'dt_mpassword: "' + binascii.hexlify(self.derive_key(dt_mpassword)).decode('utf-8') + '"\n' + if dt_mpassword != '': + dt_yml += 'dt_mpassword: "' + binascii.hexlify(self.derive_key(dt_mpassword)).decode('utf-8') + '"\n' - else: - if mo != None: - dt_yml += 'dt_password: "' + mo.group(1) + '"\n' + else: + if mo != None: + dt_yml += 'dt_password: "' + mo.group(1) + '"\n' - if mm != None: - dt_yml += 'dt_mpassword: "' + mm.group(1) + '"\n' + if mm != None: + dt_yml += 'dt_mpassword: "' + mm.group(1) + '"\n' return dt_yml, r def add_client_fields(dt_yml, remote_addr): dt_yml += 'remote_addr: "' + remote_addr + '"\n' dt_yml += 'updated: "' + str(int(time.time())) + '"\n' - return dt_yml if aws_s3_url: rr = aws_s3_get(aws_s3_url, dt_filename) if rr.status_code == 200: - m = re.search(r'revision: (\d+)', rr.text) - if m != None: - if dt_revision <= int(m.group(1)): - r = [ 'text/plain', 409, '409 Conflict\r\n', sys._getframe().f_lineno ] + if rr.text.lstrip().startswith('$VAULTY;'): + if dt_epassword != '': + try: + content = jinjafx.Vaulty().decrypt(rr.text, dt_epassword) + + except Exception: + r = [ 'text/plain', 403, '403 Forbidden\r\n', sys._getframe().f_lineno ] - if r[1] != 409: - dt_yml, r = update_dt(rr.text, dt_yml, r) + else: + r = [ 'text/plain', 403, '403 Forbidden\r\n', sys._getframe().f_lineno ] + + else: + content = rr.text + + if r[1] != 403: + m = re.search(r'revision: (\d+)', rr.text) + if m != None: + if dt_revision <= int(m.group(1)): + r = [ 'text/plain', 409, '409 Conflict\r\n', sys._getframe().f_lineno ] + + if r[1] != 409: + dt_yml, r = update_dt(rr.text, dt_yml, r) if r[1] == 500 or r[1] == 200: dt_yml = add_client_fields(dt_yml, remote_addr) - rr = aws_s3_put(aws_s3_url, dt_filename, dt_yml, 'application/yaml') - if rr.status_code == 200: - r = [ 'text/plain', 200, dt_link + '\r\n', sys._getframe().f_lineno ] + if dt_encrypted: + if dt_opassword != '': + dt_yml = jinjafx.Vaulty().encrypt(dt_yml, dt_opassword) + '\n' + + elif dt_epassword != '': + dt_yml = jinjafx.Vaulty().encrypt(dt_yml, dt_epassword) + '\n' - elif rr.status_code == 403: - r = [ 'text/plain', 403, '403 Forbidden\r\n', sys._getframe().f_lineno ] + else: + r = [ 'text/plain', 400, '400 Bad Request\r\n', sys._getframe().f_lineno ] + + if r[1] != 400: + if dt_encrypted: + rr = aws_s3_put(aws_s3_url, dt_filename, dt_yml, 'application/vaulty') + + else: + rr = aws_s3_put(aws_s3_url, dt_filename, dt_yml, 'application/yaml') + + if rr.status_code == 200: + r = [ 'text/plain', 200, dt_link + '\r\n', sys._getframe().f_lineno ] + + elif rr.status_code == 403: + r = [ 'text/plain', 403, '403 Forbidden\r\n', sys._getframe().f_lineno ] elif github_url: sha = None @@ -789,44 +873,92 @@ def add_client_fields(dt_yml, remote_addr): if jobj.get('encoding') and jobj.get('encoding') == 'base64': content = base64.b64decode(content).decode('utf-8') - m = re.search(r'revision: (\d+)', content) - if m != None: - if dt_revision <= int(m.group(1)): - r = [ 'text/plain', 409, '409 Conflict\r\n', sys._getframe().f_lineno ] + if content.lstrip().startswith('$VAULTY;'): + if dt_epassword != '': + try: + content = jinjafx.Vaulty().decrypt(content, dt_epassword) + + except Exception: + r = [ 'text/plain', 403, '403 Forbidden\r\n', sys._getframe().f_lineno ] + + else: + r = [ 'text/plain', 403, '403 Forbidden\r\n', sys._getframe().f_lineno ] + + if r[1] != 403: + m = re.search(r'revision: (\d+)', content) + if m != None: + if dt_revision <= int(m.group(1)): + r = [ 'text/plain', 409, '409 Conflict\r\n', sys._getframe().f_lineno ] - if r[1] != 409: - dt_yml, r = update_dt(content, dt_yml, r) + if r[1] != 409: + dt_yml, r = update_dt(content, dt_yml, r) if r[1] == 500 or r[1] == 200: dt_yml = add_client_fields(dt_yml, remote_addr) - rr = github_put(github_url, dt_filename, dt_yml, sha) - if str(rr.status_code).startswith('2'): - r = [ 'text/plain', 200, dt_link + '\r\n', sys._getframe().f_lineno ] + if dt_encrypted: + if dt_opassword != '': + dt_yml = jinjafx.Vaulty().encrypt(dt_yml, dt_opassword) + '\n' - elif rr.status_code == 401: - r = [ 'text/plain', 403, '403 Forbidden\r\n', sys._getframe().f_lineno ] + elif dt_epassword != '': + dt_yml = jinjafx.Vaulty().encrypt(dt_yml, dt_epassword) + '\n' - else: - print(rr.text) + else: + r = [ 'text/plain', 400, '400 Bad Request\r\n', sys._getframe().f_lineno ] + + if r[1] != 400: + rr = github_put(github_url, dt_filename, dt_yml, sha) + + if str(rr.status_code).startswith('2'): + r = [ 'text/plain', 200, dt_link + '\r\n', sys._getframe().f_lineno ] + + elif rr.status_code == 401: + r = [ 'text/plain', 403, '403 Forbidden\r\n', sys._getframe().f_lineno ] + + else: + print(rr.text) else: dt_filename = os.path.normpath(repository + '/' + dt_filename) if os.path.isfile(dt_filename): with open(dt_filename, 'rb') as f: - rr = f.read() + rr = f.read().decode('utf-8') - m = re.search(r'revision: (\d+)', rr.decode('utf-8')) - if m != None: - if dt_revision <= int(m.group(1)): - r = [ 'text/plain', 409, '409 Conflict\r\n', sys._getframe().f_lineno ] + if rr.lstrip().startswith('$VAULTY'): + if dt_epassword != '': + try: + rr = jinjafx.Vaulty().decrypt(rr, dt_epassword) - if r[1] != 409: - dt_yml, r = update_dt(rr.decode('utf-8'), dt_yml, r) + except Exception: + r = [ 'text/plain', 403, '403 Forbidden\r\n', sys._getframe().f_lineno ] + + else: + r = [ 'text/plain', 403, '403 Forbidden\r\n', sys._getframe().f_lineno ] + + if r[1] != 403: + m = re.search(r'revision: (\d+)', rr) + if m != None: + if dt_revision <= int(m.group(1)): + r = [ 'text/plain', 409, '409 Conflict\r\n', sys._getframe().f_lineno ] + + if r[1] != 409: + dt_yml, r = update_dt(rr, dt_yml, r) if r[1] == 500 or r[1] == 200: dt_yml = add_client_fields(dt_yml, remote_addr) + + if dt_encrypted: + if dt_opassword != '': + dt_yml = jinjafx.Vaulty().encrypt(dt_yml, dt_opassword) + '\n' + + elif dt_epassword != '': + dt_yml = jinjafx.Vaulty().encrypt(dt_yml, dt_epassword) + '\n' + + else: + r = [ 'text/plain', 400, '400 Bad Request\r\n', sys._getframe().f_lineno ] + + if r[1] == 500 or r[1] == 200: with open(dt_filename, 'w') as f: f.write(dt_yml) @@ -869,6 +1001,10 @@ def add_client_fields(dt_yml, remote_addr): self.send_header('Content-Type', r[0]) self.send_header('Content-Length', str(len(r[2]))) self.send_header('X-Content-Type-Options', 'nosniff') + + for k in cheaders: + self.send_header(k, cheaders[k]) + self.end_headers() self.wfile.write(r[2]) @@ -927,8 +1063,9 @@ def main(rflag=[0]): global pandoc try: - print('JinjaFx Server v' + __version__ + ' - Jinja2 Templating Tool') - print('Copyright (c) 2020-2025 Chris Mason \n') + if not os.getenv('JOURNAL_STREAM'): + print('JinjaFx Server v' + __version__ + ' - Jinja2 Templating Tool') + print('Copyright (c) 2020-2025 Chris Mason \n') update_versioned_links(base + '/www') @@ -1007,7 +1144,7 @@ def signal_handler(*args): soft, hard = resource.getrlimit(resource.RLIMIT_AS) resource.setrlimit(resource.RLIMIT_AS, (args.ml * 1024 * 1024, hard)) - log('Starting JinjaFx Server (PID is ' + str(os.getpid()) + ') on http://' + args.l + ':' + str(args.p) + '...') + log(f'Starting JinjaFx Server (PID is {os.getpid()}) on http://{args.l}:{args.p}...') s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -1024,7 +1161,7 @@ def signal_handler(*args): while rflag[0] < 2: time.sleep(0.1) - log('Terminating JinjaFx Server...') + log(f'Terminating JinjaFx Server...') except Exception as e: @@ -1035,7 +1172,6 @@ def signal_handler(*args): finally: if rflag[0] > 0: -# s.shutdown(1) s.close() @@ -1044,7 +1180,12 @@ def log(t, ae=''): with llock: timestamp = datetime.datetime.now().strftime('%b %d %H:%M:%S.%f')[:19] - print('[' + timestamp + '] ' + t + ae) + + if os.getenv('JOURNAL_STREAM'): + print(re.sub(r'\033\[(?:1;[0-9][0-9]|0)m', '', t + ae)) + + else: + print('[' + timestamp + '] ' + t + ae) logring.append('[' + timestamp + '] ' + t + ae) logring = logring[-1024:] diff --git a/kubernetes/Dockerfile b/kubernetes/Dockerfile index 5af7146..cb7dc27 100644 --- a/kubernetes/Dockerfile +++ b/kubernetes/Dockerfile @@ -7,8 +7,8 @@ RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends wget git build-essential; \ -wget -P /tmp https://github.com/jgm/pandoc/releases/download/3.5/pandoc-3.5-1-amd64.deb; \ -dpkg -i /tmp/pandoc-3.5-1-amd64.deb; \ +wget -P /tmp https://github.com/jgm/pandoc/releases/download/3.6.1/pandoc-3.6.1-1-amd64.deb; \ +dpkg -i /tmp/pandoc-3.6.1-1-amd64.deb; \ python3 -m venv /opt/jinjafx; \ /opt/jinjafx/bin/python3 -m pip install --upgrade git+https://github.com/cmason3/jinjafx_server.git@${BRANCH} lxml; \ diff --git a/kubernetes/README.md b/kubernetes/README.md index fd8fa28..b7456cc 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -1,6 +1,6 @@ ## JinjaFx Server as a Container in Kubernetes -JinjaFx Server will always be available in Docker Hub at [https://hub.docker.com/repository/docker/cmason3/jinjafx_server](https://hub.docker.com/repository/docker/cmason3/jinjafx_server) - the `latest` tag will always refer to the latest released version, although it is recommended to use explicit version tags. +JinjaFx Server will always be available in Docker Hub at https://hub.docker.com/repository/docker/cmason3/jinjafx_server - the `latest` tag will always refer to the latest released version, although it is recommended to use explicit version tags. The following steps will run JinjaFx Server in a container using Kubernetes Ingress - Ingress is basically the same concept as Virtual Hosting (the default Ingress uses nginx), which works with HTTP and relies on the "Host" header to direct the request to the correct container. In a Virtual Hosting scenario you would typically point different DNS A records towards the same IP, but in our example we are using a Wildcard DNS entry for our whole Kubernetes cluster, e.g: diff --git a/setup.py b/setup.py index e1c1b10..523b109 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ packages=["jinjafx_server"], include_package_data=True, package_data={'': ['www/*', 'pandoc/reference.docx']}, - install_requires=["jinjafx>=1.22.1", "requests", "cmarkgfm>=0.5.0", "emoji"], + install_requires=["jinjafx>=1.23.1", "requests", "cmarkgfm>=0.5.0", "emoji"], entry_points={ "console_scripts": [ "jinjafx_server=jinjafx_server:main", diff --git a/www/dt.html b/www/dt.html index dfdf901..0c931b7 100644 --- a/www/dt.html +++ b/www/dt.html @@ -6,9 +6,9 @@ JinjaFx DataTemplate - + - +
diff --git a/www/index.html b/www/index.html index 09f723f..fe4cd1b 100644 --- a/www/index.html +++ b/www/index.html @@ -12,8 +12,8 @@ - - + + @@ -32,7 +32,7 @@ - +
@@ -54,7 +54,7 @@
-
+
+ +
+ + +
+

Outputs


@@ -178,7 +195,7 @@

Outputs


<output "Output 2"> ... </output> -

By default an output is rendered as text, but you can also tell JinjaFx to render it as HTML or Markdown (GFM), e.g:

+

By default an output is rendered as text, but you can also tell JinjaFx to render it as HTML or Markdown, e.g:

<output:html "Output 1">
 ...
 </output>
@@ -186,6 +203,8 @@ 

Outputs


<output:markdown "Output 2"> ... </output>
+

JinjaFx supports nested templates - if you click on the "+" at the top right of the "template.j2" pane then you can create multiple templates and import them into the "Default" template via the following syntax:

+
{% include "<template>" %}

Dynamic CSV


Within the "data.csv" pane you can specify regular CSV where each row will be treated as a data set using the header row for variable names. However, it also supports something which I call Dynamic CSV where you can use regular expression based static character classes or static groups as values within the data rows using (value1|value2|value3) or [a-f]. These will then be expanded into multiple rows, e.g:

DEVICE, TYPE
@@ -321,7 +340,7 @@ 

Protect DataTemplate

+ diff --git a/www/jinjafx.css b/www/jinjafx.css index 7d6745c..da3346e 100644 --- a/www/jinjafx.css +++ b/www/jinjafx.css @@ -51,7 +51,6 @@ a:link { a:hover { text-decoration: underline; } - .btn-primary { --bs-btn-color: #fff; --bs-btn-disabled-color: #fff; diff --git a/www/jinjafx_dt.js b/www/jinjafx_dt.js index f4654e8..e27a860 100644 --- a/www/jinjafx_dt.js +++ b/www/jinjafx_dt.js @@ -33,14 +33,14 @@ dtx += ' datasets:\n'; - Object.keys(dt.datasets).forEach(function(ds) { + Object.keys(dt.datasets).sort(window.opener.default_on_top).forEach(function(ds) { var data = dt.datasets[ds].data.match(/\S/) ? window.opener.d(dt.datasets[ds].data).replace(/\s+$/g, '') : ""; var vars = dt.datasets[ds].vars.match(/\S/) ? window.opener.d(dt.datasets[ds].vars).replace(/\s+$/g, '') : ""; dtx += ' "' + ds + '":\n'; if (data == '') { - dtx += ' data: ""\n\n'; + dtx += ' data: ""\n'; } else { dtx += ' data: |2\n'; @@ -61,7 +61,7 @@ var vars = dt.vars.match(/\S/) ? window.opener.d(dt.vars).replace(/\s+$/g, '') : ""; if (data == '') { - dtx += ' data: ""\n\n'; + dtx += ' data: ""\n'; } else { dtx += ' data: |2\n'; @@ -77,14 +77,31 @@ } } - var template = dt.template.match(/\S/) ? window.opener.d(dt.template).replace(/\s+$/g, '') : ""; + if (typeof dt.template == "object") { + dtx += ' template:\n'; - if (template == '') { - dtx += ' template: ""\n'; + Object.keys(dt.template).sort(window.opener.default_on_top).forEach(function(t) { + var template = dt.template[t].match(/\S/) ? window.opener.d(dt.template[t]).replace(/\s+$/g, '') : ""; + + if (template == '') { + dtx += ' "' + t + '": ""\n'; + } + else { + dtx += ' "' + t + '": |2\n'; + dtx += window.opener.quote(template.replace(/^/gm, ' ')) + '\n\n'; + } + }); } else { - dtx += ' template: |2\n'; - dtx += window.opener.quote(template.replace(/^/gm, ' ')) + '\n'; + var template = dt.template.match(/\S/) ? window.opener.d(dt.template).replace(/\s+$/g, '') : ""; + + if (template == '') { + dtx += ' template: ""\n'; + } + else { + dtx += ' template: |2\n'; + dtx += window.opener.quote(template.replace(/^/gm, ' ')) + '\n\n'; + } } document.getElementById('container').innerHTML = dtx; diff --git a/www/jinjafx_m.css b/www/jinjafx_m.css index 077bc9e..f1f261f 100644 --- a/www/jinjafx_m.css +++ b/www/jinjafx_m.css @@ -71,12 +71,18 @@ textarea { border-radius: 0.5rem; padding: 10px; } +.templates { + position: absolute; + z-index: 2; + right: 8px; + top: 8px; +} .info { position: absolute; z-index: 2; - right: 30px; - top: 30px; - bottom: 30px; + right: 15px; + top: 15px; + bottom: 15px; width: 50%; visibility: hidden; } diff --git a/www/jinjafx_m.js b/www/jinjafx_m.js index ca75860..631c7fb 100644 --- a/www/jinjafx_m.js +++ b/www/jinjafx_m.js @@ -16,6 +16,16 @@ function rot47(data) { }); } +function default_on_top(a, b) { + if (a == 'Default') { + return -1; + } + else if (b == 'Default') { + return 1; + } + return a.localeCompare(b); +} + var _fromCC = String.fromCharCode.bind(String); function _utob(c) { @@ -97,11 +107,18 @@ function getStatusText(code) { var datasets = { 'Default': [CodeMirror.Doc('', 'data'), CodeMirror.Doc('', 'yaml')] }; + var templates = { + 'Default': CodeMirror.Doc('', 'template') + }; var current_ds = 'Default'; + var current_t = 'Default'; var pending_dt = ''; + var dt_protected = false; + var dt_encrypted = false; var dt_password = null; var dt_opassword = null; var dt_mpassword = null; + var dt_epassword = null; var input_form = null; var r_input_form = null; var jinput = null; @@ -128,14 +145,14 @@ function getStatusText(code) { function switch_dataset(ds, sflag, dflag) { if (sflag) { - datasets[current_ds][0] = window.cmData.swapDoc(datasets[ds][0]); - datasets[current_ds][1] = window.cmVars.swapDoc(datasets[ds][1]); + datasets[current_ds][0] = window.cmData.getDoc(); + datasets[current_ds][1] = window.cmVars.getDoc(); } - else { + + if (ds != current_ds) { window.cmData.swapDoc(datasets[ds][0]); window.cmVars.swapDoc(datasets[ds][1]); - } - if (ds != current_ds) { + if (dflag) { window.addEventListener('beforeunload', onBeforeUnload); if (document.getElementById('get_link').value != 'false') { @@ -150,10 +167,37 @@ function getStatusText(code) { fe.focus(); } + function select_template(e) { + switch_template(e.currentTarget.t_name, true, false); + } + + function switch_template(t, sflag, dflag) { + if (sflag) { + templates[current_t] = window.cmTemplate.getDoc(); + } + + if (t != current_t) { + window.cmTemplate.swapDoc(templates[t]); + + if (dflag) { + window.addEventListener('beforeunload', onBeforeUnload); + if (document.getElementById('get_link').value != 'false') { + document.title = 'JinjaFx [unsaved]'; + } + dirty = true; + } + document.getElementById('delete_t').disabled = (t == 'Default'); + document.getElementById('selected_t').innerHTML = t; + current_t = t; + onDataBlur(); + } + fe.focus(); + } + function rebuild_datasets() { document.getElementById('datasets').innerHTML = ''; - Object.keys(datasets).forEach(function(ds) { + Object.keys(datasets).sort(default_on_top).forEach(function(ds) { var a = document.createElement('a'); a.classList.add('dropdown-item', 'text-decoration-none'); a.addEventListener('click', select_dataset, false); @@ -192,7 +236,7 @@ function getStatusText(code) { xsplit = null; if (window.cmgVars.getValue().match(/\S/)) { - var ds = Object.keys(datasets)[0]; + var ds = Object.keys(datasets).sort(default_on_top)[0]; datasets[ds][1].setValue(window.cmgVars.getValue().trimEnd() + "\n\n" + datasets[ds][1].getValue()); } @@ -203,10 +247,41 @@ function getStatusText(code) { document.getElementById('selected_ds').innerHTML = current_ds; } + function rebuild_templates() { + document.getElementById('templates').innerHTML = ''; + + Object.keys(templates).sort(default_on_top).forEach(function(t) { + var a = document.createElement('a'); + a.classList.add('dropdown-item', 'text-decoration-none'); + a.addEventListener('click', select_template, false); + a.href = '#'; + a.t_name = t; + a.innerHTML = t; + document.getElementById('templates').appendChild(a); + }); + + if (Object.keys(templates).length > 1) { + document.getElementById('select_t').disabled = false; + document.getElementById('delete_t').disabled = (current_t == 'Default'); + } + else { + document.getElementById('select_t').disabled = true; + document.getElementById('delete_t').disabled = true; + } + document.getElementById('selected_t').innerHTML = current_t; + } + function delete_dataset(ds) { delete datasets[ds]; rebuild_datasets(); - switch_dataset(Object.keys(datasets)[0], false, true); + switch_dataset(Object.keys(datasets).sort(default_on_top)[0], false, true); + fe.focus(); + } + + function delete_template(t) { + delete templates[t]; + rebuild_templates(); + switch_template(Object.keys(templates).sort(default_on_top)[0], false, true); fe.focus(); } @@ -248,7 +323,18 @@ function getStatusText(code) { function jinjafx_generate() { var vaulted_vars = dt.vars.indexOf('$ANSIBLE_VAULT;') > -1; dt.vars = e(dt.vars); - dt.template = e(window.cmTemplate.getValue().replace(/\t/g, " ")); + + if (Object.keys(templates).length === 1) { + dt.template = e(window.cmTemplate.getValue().replace(/\t/g, " ")); + } + else { + dt.template = {}; + + Object.keys(templates).sort(default_on_top).forEach(function(t) { + dt.template[t] = e(templates[t].getValue().replace(/\t/g, " ")); + }); + } + dt.id = dt_id; dt.dataset = current_ds; @@ -294,19 +380,46 @@ function getStatusText(code) { }).show(); return false; } + else if (method == "delete_template") { + if (window.cmTemplate.getValue().match(/\S/)) { + if (confirm("Are You Sure?") === true) { + delete_template(current_t); + } + } + else { + delete_template(current_t); + } + return false; + } + else if (method == "add_template") { + document.getElementById("t_name").value = ''; + new bootstrap.Modal(document.getElementById('template_input'), { + keyboard: true + }).show(); + return false; + } if (method == "protect") { document.getElementById('password_open2').classList.remove('is-invalid'); document.getElementById('password_open2').classList.remove('is-valid'); document.getElementById('password_modify2').classList.remove('is-invalid'); document.getElementById('password_modify2').classList.remove('is-valid'); + + if (dt_protected) { + document.getElementById('ml-protect-dt-ok').innerText = 'Update'; + } + else { + document.getElementById('ml-protect-dt-ok').innerText = 'OK'; + } + new bootstrap.Modal(document.getElementById('protect_dt'), { keyboard: false }).show(); return false; } - if (window.cmTemplate.getValue().length === 0) { + switch_template(current_t, true, false); + if (templates['Default'].getValue().length === 0) { window.cmTemplate.focus(); set_status("darkred", "ERROR", "No Template"); return false; @@ -542,7 +655,17 @@ function getStatusText(code) { } dt.dataset = current_ds; - dt.template = e(window.cmTemplate.getValue().replace(/\t/g, " ")); + + if (Object.keys(templates).length === 1) { + dt.template = e(window.cmTemplate.getValue().replace(/\t/g, " ")); + } + else { + dt.template = {}; + + Object.keys(templates).sort(default_on_top).forEach(function(t) { + dt.template[t] = e(templates[t].getValue().replace(/\t/g, " ")); + }); + } if ((current_ds === 'Default') && (Object.keys(datasets).length === 1)) { dt.vars = e(window.cmVars.getValue().replace(/\t/g, " ")); @@ -556,7 +679,7 @@ function getStatusText(code) { } switch_dataset(current_ds, true, false); - Object.keys(datasets).forEach(function(ds) { + Object.keys(datasets).sort(default_on_top).forEach(function(ds) { dt.datasets[ds] = {}; dt.datasets[ds].data = e(datasets[ds][0].getValue()); dt.datasets[ds].vars = e(datasets[ds][1].getValue().replace(/\t/g, " ")); @@ -595,6 +718,7 @@ function getStatusText(code) { if (v_dt_id !== null) { xHR.open("POST", "/get_link?id=" + v_dt_id, true); + xHR.setRequestHeader("X-Dt-Protected", dt_protected ? 1 : 0); if (dt_password !== null) { xHR.setRequestHeader("X-Dt-Password", dt_password); } @@ -604,6 +728,10 @@ function getStatusText(code) { if (dt_mpassword != null) { xHR.setRequestHeader("X-Dt-Modify-Password", dt_mpassword); } + if (dt_epassword != null) { + xHR.setRequestHeader("X-Dt-Encrypt-Password", dt_epassword); + } + xHR.setRequestHeader("X-Dt-Encrypted", dt_encrypted ? 1 : 0); xHR.setRequestHeader("X-Dt-Revision", revision + 1); } else { @@ -614,11 +742,20 @@ function getStatusText(code) { if (this.status === 200) { if (v_dt_id !== null) { revision += 1; - if (dt_mpassword != null) { - dt_password = dt_mpassword; + if (dt_protected) { + if (dt_mpassword != null) { + dt_password = dt_mpassword; + } + else if (dt_opassword != null) { + dt_password = dt_opassword; + } + if (dt_opassword != null) { + dt_epassword = dt_opassword; + } } - else if (dt_opassword != null) { - dt_password = dt_opassword; + else { + dt_epassword = null; + dt_password = null; } dt_opassword = null; dt_mpassword = null; @@ -634,6 +771,7 @@ function getStatusText(code) { } else if (this.status == 401) { protect_action = 2; + document.getElementById('lb_protect').innerHTML = 'DataTemplate ' + this.getResponseHeader('X-Dt-Authentication') + ' Passsword'; new bootstrap.Modal(document.getElementById('protect_input'), { keyboard: false }).show(); @@ -704,6 +842,7 @@ function getStatusText(code) { xHR.onload = function() { if (this.status === 401) { protect_action = 1; + document.getElementById('lb_protect').innerHTML = 'DataTemplate ' + this.getResponseHeader('X-Dt-Authentication') + ' Passsword'; new bootstrap.Modal(document.getElementById('protect_input'), { keyboard: false }).show(); @@ -713,6 +852,14 @@ function getStatusText(code) { try { var dt = jsyaml.load(d(JSON.parse(this.responseText)['dt']), jsyaml_schema); + if (dt.hasOwnProperty('encrypted')) { + dt_encrypted = (dt['encrypted'] === 1); + dt_epassword = dt_password; + } + else { + dt_encrypted = false; + } + if (dt.hasOwnProperty('dataset')) { load_datatemplate(dt['dt'], qs, dt['dataset']); } @@ -728,6 +875,10 @@ function getStatusText(code) { document.getElementById('protect').classList.remove('disabled'); if (dt.hasOwnProperty('dt_password') || dt.hasOwnProperty('dt_mpassword')) { document.getElementById('protect_text').innerHTML = 'Update Protection'; + dt_protected = true; + } + else { + dt_protected = false; } if (dt.hasOwnProperty('updated')) { @@ -752,6 +903,7 @@ function getStatusText(code) { reset_location(''); } document.getElementById('lbuttons').classList.remove('d-none'); + document.getElementById('buttons').classList.remove('d-none'); loaded = true; clear_wait(); }; @@ -760,6 +912,7 @@ function getStatusText(code) { set_status("darkred", "ERROR", "XMLHttpRequest.onError()"); reset_location(''); document.getElementById('lbuttons').classList.remove('d-none'); + document.getElementById('buttons').classList.remove('d-none'); loaded = true; clear_wait(); }; @@ -767,6 +920,7 @@ function getStatusText(code) { set_status("darkred", "ERROR", "XMLHttpRequest.onTimeout()"); reset_location(''); document.getElementById('lbuttons').classList.remove('d-none'); + document.getElementById('buttons').classList.remove('d-none'); loaded = true; clear_wait(); }; @@ -779,6 +933,7 @@ function getStatusText(code) { } else { document.getElementById('lbuttons').classList.remove('d-none'); + document.getElementById('buttons').classList.remove('d-none'); loaded = true; } } @@ -786,6 +941,7 @@ function getStatusText(code) { console.log(ex); set_status("darkred", "ERROR", ex); document.getElementById('lbuttons').classList.remove('d-none'); + document.getElementById('buttons').classList.remove('d-none'); loaded = true; onChange(null, true); } } @@ -802,6 +958,8 @@ function getStatusText(code) { document.getElementById('delete_ds').onclick = function() { jinjafx('delete_dataset'); }; document.getElementById('add_ds').onclick = function() { jinjafx('add_dataset'); }; + document.getElementById('delete_t').onclick = function() { jinjafx('delete_template'); }; + document.getElementById('add_t').onclick = function() { jinjafx('add_template'); }; document.getElementById('get').onclick = function() { jinjafx('get_link'); }; document.getElementById('get2').onclick = function() { jinjafx('get_link'); }; document.getElementById('update').onclick = function() { jinjafx('update_link'); }; @@ -874,7 +1032,7 @@ function getStatusText(code) { keyboard: false }).show(); }; - + sobj = document.getElementById("status"); window.onresize = function() { @@ -1295,10 +1453,12 @@ function getStatusText(code) { document.getElementById('ml-protect-dt-ok').onclick = function() { dt_opassword = null; dt_mpassword = null; - + dt_encrypted = false; + if (document.getElementById('password_open1').value.match(/\S/)) { if (document.getElementById('password_open1').value == document.getElementById('password_open2').value) { dt_opassword = document.getElementById('password_open2').value; + dt_encrypted = document.getElementById('encrypt_dt').checked; } else { set_status("darkred", "ERROR", "Password Verification Failed"); @@ -1321,6 +1481,7 @@ function getStatusText(code) { if (dt_opassword === dt_mpassword) { dt_mpassword = null; } + dt_protected = true; document.getElementById('protect_text').innerHTML = 'Update Protection'; window.addEventListener('beforeunload', onBeforeUnload); document.title = 'JinjaFx [unsaved]'; @@ -1328,6 +1489,15 @@ function getStatusText(code) { set_status("green", "OK", "Protection Set - Update Required", 10000); dt_password = null; } + else if (dt_protected) { + document.getElementById('protect_text').innerHTML = 'Protect Link'; + window.addEventListener('beforeunload', onBeforeUnload); + document.title = 'JinjaFx [unsaved]'; + dirty = true; + set_status("green", "OK", "Protection Removed - Update Required", 10000); + dt_protected = false; + dt_password = null; + } else { set_status("darkred", "ERROR", "Invalid Password"); } @@ -1340,6 +1510,8 @@ function getStatusText(code) { document.getElementById("password_modify1").value = ''; document.getElementById("password_modify2").value = ''; document.getElementById("password_modify2").disabled = true; + document.getElementById('encrypt_dt').disabled = true; + document.getElementById('encrypt_dt').checked = false; fe.focus(); }); @@ -1376,6 +1548,7 @@ function getStatusText(code) { } loaded = true; document.getElementById('lbuttons').classList.remove('d-none'); + document.getElementById('buttons').classList.remove('d-none'); set_status("darkred", "ERROR", "Invalid Password"); } } @@ -1383,6 +1556,7 @@ function getStatusText(code) { if (protect_action == 1) { reset_location(''); document.getElementById('lbuttons').classList.remove('d-none'); + document.getElementById('buttons').classList.remove('d-none'); dt_password = null; loaded = true; } @@ -1397,29 +1571,84 @@ function getStatusText(code) { document.getElementById('dataset_input').addEventListener('shown.bs.modal', function (e) { document.getElementById("ds_name").focus(); }); + + document.getElementById('template_input').addEventListener('shown.bs.modal', function (e) { + document.getElementById("t_name").focus(); + }); document.getElementById('ml-dataset-ok').onclick = function() { var new_ds = document.getElementById("ds_name").value; if (new_ds.match(/^[A-Z][A-Z0-9_ -]*$/i)) { - if (!datasets.hasOwnProperty(new_ds)) { + var existing = ''; + for (var p in datasets) { + if (datasets.hasOwnProperty(p)) { + if (new_ds.toLowerCase() === p.toLowerCase()) { + existing = p; + break; + } + } + } + if (existing == '') { datasets[new_ds] = [CodeMirror.Doc('', 'data'), CodeMirror.Doc('', 'yaml')]; rebuild_datasets(); + switch_dataset(new_ds, true, true); + } + else { + switch_dataset(existing, true, true); } - switch_dataset(new_ds, true, true); } else { set_status("darkred", "ERROR", "Invalid Data Set Name"); } }; + document.getElementById('ml-template-ok').onclick = function() { + var new_t = document.getElementById("t_name").value; + + if (new_t.match(/^[A-Z][A-Z0-9_ -]*$/i)) { + var existing = ''; + for (var p in templates) { + if (templates.hasOwnProperty(p)) { + if (new_t.toLowerCase() === p.toLowerCase()) { + existing = p; + break; + } + } + } + if (existing == '') { + templates[new_t] = CodeMirror.Doc('', 'template'); + rebuild_templates(); + switch_template(new_t, true, true); + } + else { + switch_template(existing, true, true); + } + } + else { + set_status("darkred", "ERROR", "Invalid Template Name"); + } + }; + document.getElementById('ds_name').onkeyup = function(e) { if (e.which == 13) { document.getElementById('ml-dataset-ok').click(); } }; + + document.getElementById('t_name').onkeyup = function(e) { + if (e.which == 13) { + document.getElementById('ml-template-ok').click(); + } + }; function check_open() { + if (document.getElementById('password_open1').value.match(/\S/)) { + document.getElementById('encrypt_dt').disabled = false; + } + else { + document.getElementById('encrypt_dt').disabled = true; + } if (document.getElementById('password_open1').value == document.getElementById('password_open2').value) { document.getElementById('password_open2').classList.remove('is-invalid'); document.getElementById('password_open2').classList.add('is-valid'); @@ -1468,6 +1697,7 @@ function getStatusText(code) { if (document.getElementById('password_open2').disabled == true) { document.getElementById('password_open2').disabled = false; document.getElementById('password_open2').classList.add('is-invalid'); + document.getElementById('encrypt_dt').disabled = false; } else { check_open(); @@ -1478,6 +1708,8 @@ function getStatusText(code) { document.getElementById('password_open2').value = ''; document.getElementById('password_open2').classList.remove('is-valid'); document.getElementById('password_open2').classList.remove('is-invalid'); + document.getElementById('encrypt_dt').disabled = true; + document.getElementById('encrypt_dt').checked = false; } }; @@ -1554,6 +1786,7 @@ function getStatusText(code) { else { set_status("darkred", "HTTP ERROR 503", "Service Unavailable"); reset_location(''); + document.getElementById('buttons').classList.remove('d-none'); loaded = true; } } @@ -1578,11 +1811,13 @@ function getStatusText(code) { else { set_status("darkred", "HTTP ERROR 503", "Service Unavailable"); reset_location(''); + document.getElementById('buttons').classList.remove('d-none'); loaded = true; } } else { reset_location(''); + document.getElementById('buttons').classList.remove('d-none'); loaded = true; } } @@ -1590,7 +1825,9 @@ function getStatusText(code) { if (document.getElementById('get_link').value != 'false') { document.getElementById('lbuttons').classList.remove('d-none'); } + document.getElementById('stemplates').style.visibility = 'hidden'; document.getElementById('template_info').style.visibility = 'visible'; + document.getElementById('buttons').classList.remove('d-none'); loaded = true; } } @@ -1607,6 +1844,7 @@ function getStatusText(code) { function remove_info() { document.getElementById('template_info').classList.add('fade-out'); document.getElementById('template_info').style.zIndex = -1000; + document.getElementById('stemplates').style.visibility = 'visible'; } function set_wait() { @@ -1700,6 +1938,7 @@ function getStatusText(code) { dt_password = null; dt_opassword = null; dt_mpassword = null; + dt_epassword = null; input_form = null; document.getElementById('update').classList.add('d-none'); document.getElementById('get').classList.remove('d-none'); @@ -1784,8 +2023,10 @@ function getStatusText(code) { } if (tinfo) { if (editor == window.cmTemplate) { - document.getElementById('template_info').classList.add('fade-out'); - document.getElementById('template_info').style.zIndex = -1000; + remove_info(); + //document.getElementById('template_info').classList.add('fade-out'); + //document.getElementById('template_info').style.zIndex = -1000; + //document.getElementById('stemplates').style.visibility = 'visible'; tinfo = false; } } @@ -1795,6 +2036,7 @@ function getStatusText(code) { function load_datatemplate(_dt, _qs, _ds) { try { current_ds = 'Default'; + current_t = 'Default'; window.cmgVars.setValue(""); @@ -1808,7 +2050,7 @@ function getStatusText(code) { }); if ((_ds == null) || !datasets.hasOwnProperty(_ds)) { - current_ds = Object.keys(datasets)[0]; + current_ds = Object.keys(datasets).sort(default_on_top)[0]; } else { current_ds = _ds; @@ -1830,7 +2072,28 @@ function getStatusText(code) { datasets['Default'][1].setValue(_dt.hasOwnProperty("vars") ? _dt.vars : ""); window.cmVars.swapDoc(datasets['Default'][1]); } - window.cmTemplate.setValue(_dt.hasOwnProperty("template") ? _dt.template : ""); + + if (_dt.hasOwnProperty("template")) { + if (typeof _dt['template'] == "object") { + templates = {}; + + Object.keys(_dt['template']).forEach(function(t) { + templates[t] = CodeMirror.Doc(_dt['template'][t], 'template'); + }); + } + else { + templates = { + 'Default': CodeMirror.Doc(_dt['template'], 'template') + }; + } + } + else { + templates = { + 'Default': CodeMirror.Doc('', 'template') + }; + } + + window.cmTemplate.swapDoc(templates[current_t]); window.cmData.getDoc().clearHistory(); window.cmVars.getDoc().clearHistory(); @@ -1838,6 +2101,8 @@ function getStatusText(code) { window.cmTemplate.getDoc().clearHistory(); rebuild_datasets(); + rebuild_templates(); + remove_info(); loaded = true; } catch (ex) { diff --git a/www/logs.html b/www/logs.html index df43dff..6f6d729 100644 --- a/www/logs.html +++ b/www/logs.html @@ -6,7 +6,7 @@ JinjaFx Logs - + diff --git a/www/output.html b/www/output.html index 75e03f4..b54d6b5 100644 --- a/www/output.html +++ b/www/output.html @@ -6,7 +6,7 @@ Generating... - +