mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-29 15:06:26 +02:00
Finished Iframe Implementation
This commit is contained in:
@@ -6,7 +6,7 @@ setup_admin_email: "{{ users.administrator.email }}"
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
portfolio_iframe: true
|
||||
central_database: true
|
||||
credentials:
|
||||
domains:
|
||||
|
@@ -5,7 +5,7 @@ credentials:
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
portfolio_iframe: true
|
||||
central_database: true
|
||||
docker:
|
||||
services:
|
||||
|
@@ -6,8 +6,8 @@ accounts:
|
||||
icon:
|
||||
class: fa-solid fa-users
|
||||
children:
|
||||
- name: Publishing Channels
|
||||
description: Platforms where I share content.
|
||||
- name: Follow Us
|
||||
description: Follow us to stay up to recieve the newest CyMaIS updates
|
||||
icon:
|
||||
class: fas fa-newspaper
|
||||
{% if ["mastodon", "bluesky"] | any_in(group_names) %}
|
||||
@@ -32,7 +32,7 @@ accounts:
|
||||
icon:
|
||||
class: fa-brands fa-bluesky
|
||||
alternatives:
|
||||
- link: accounts.publishingchannels.microblogs.mastodon
|
||||
- link: accounts.followus.microblogs.mastodon
|
||||
identifier: "{{service_provider.contact.bluesky}}"
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -102,7 +102,6 @@ company:
|
||||
navigation:
|
||||
header:
|
||||
children:
|
||||
- link: accounts.publishingchannels
|
||||
- name: Contact
|
||||
description: Get in touch with {{ 'us' if service_provider.type == 'legal' else 'me' }}
|
||||
icon:
|
||||
@@ -146,4 +145,10 @@ navigation:
|
||||
class: fa-solid fa-expand-arrows-alt
|
||||
onclick: "toggleFullscreen()"
|
||||
|
||||
- name: Open in new tab
|
||||
description: Open the currently embedded iframe URL in a fresh browser tab
|
||||
icon:
|
||||
class: fa-solid fa-up-right-from-square
|
||||
onclick: openIframeInNewTab()
|
||||
|
||||
{% include 'footer_menu.yaml.j2' %}
|
30
roles/docker-portfolio/templates/javascript.js.j2
Normal file
30
roles/docker-portfolio/templates/javascript.js.j2
Normal file
@@ -0,0 +1,30 @@
|
||||
window.addEventListener("message", function(event) {
|
||||
const allowedSuffix = ".{{ primary_domain }}";
|
||||
const origin = event.origin;
|
||||
|
||||
// 1. Only allow messages from *.{{ primary_domain }}
|
||||
if (!origin.endsWith(allowedSuffix)) return;
|
||||
|
||||
const data = event.data;
|
||||
|
||||
// 2. Only process valid iframeLocationChange messages
|
||||
if (data && data.type === "iframeLocationChange" && typeof data.href === "string") {
|
||||
try {
|
||||
const hrefUrl = new URL(data.href);
|
||||
|
||||
// 3. Only allow redirects to *.{{ primary_domain }}
|
||||
if (!hrefUrl.hostname.endsWith(allowedSuffix)) return;
|
||||
|
||||
// 4. Update the ?iframe= parameter in the browser URL
|
||||
const newUrl = new URL(window.location);
|
||||
newUrl.searchParams.set("iframe", hrefUrl.href);
|
||||
window.history.replaceState({}, "", newUrl);
|
||||
} catch (e) {
|
||||
// Invalid or malformed URL – ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
{% if enable_debug | bool %}
|
||||
console.log("[iframe-sync] Listener for iframe messages is active.");
|
||||
{% endif %}
|
@@ -3,7 +3,7 @@ features:
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
simpleicons: true # Activate Brand Icons for your groups
|
||||
nasa_api_key: false # Set api key to use the Nasa Picture of the Day as Background
|
||||
javascript: true # Necessary for URL sync
|
||||
csp:
|
||||
whitelist:
|
||||
script-src-elem:
|
||||
|
@@ -6,4 +6,14 @@
|
||||
- name: "Activate Global Matomo Tracking for {{domain}}"
|
||||
include_role:
|
||||
name: nginx-modifier-matomo
|
||||
when: applications | is_feature_enabled('matomo',application_id)
|
||||
when: applications | is_feature_enabled('matomo',application_id)
|
||||
|
||||
- name: "Activate Portfolio iFrame Notifier for {{ domain }}"
|
||||
include_role:
|
||||
name: nginx-modifier-iframe
|
||||
when: applications | is_feature_enabled('portfolio_iframe', application_id)
|
||||
|
||||
- name: "Activate Javascript for {{ domain }}"
|
||||
include_role:
|
||||
name: nginx-modifier-javascript
|
||||
when: applications | is_feature_enabled('javascript', application_id)
|
@@ -2,20 +2,32 @@
|
||||
sub_filter_once off;
|
||||
sub_filter_types text/html;
|
||||
|
||||
{% set features_css_final = applications.get(application_id).get('features').get('css') | bool %}
|
||||
{% set features_matomo_final = applications.get(application_id).get('features').get('matomo') | bool %}
|
||||
{% set modifier_css_enabled = applications | is_feature_enabled('css',application_id) %}
|
||||
{% set modifier_matomo_enabled = applications | is_feature_enabled('matomo',application_id) %}
|
||||
{% set modifier_iframe_enabled = applications | is_feature_enabled('portfolio_iframe',application_id) %}
|
||||
{% set modifier_javascript_enabled = applications | is_feature_enabled('javascript',application_id) %}
|
||||
|
||||
|
||||
{% if features_matomo_final | bool %}
|
||||
{# Include Global Matomo Tracking #}
|
||||
{% include 'roles/nginx-modifier-matomo/templates/matomo-tracking.conf.j2' %}
|
||||
{% if modifier_iframe_enabled or modifier_css_enabled or modifier_matomo_enabled or modifier_javascript_enabled %}
|
||||
sub_filter '</head>' '
|
||||
{%- if modifier_css_enabled -%}
|
||||
{%- include "roles/nginx-modifier-css/templates/head_sub.j2" -%}
|
||||
{%- endif -%}
|
||||
{%- if modifier_matomo_enabled -%}
|
||||
{%- include "roles/nginx-modifier-matomo/templates/head_sub.j2" -%}
|
||||
{%- endif -%}
|
||||
{%- if modifier_iframe_enabled -%}
|
||||
{%- include "roles/nginx-modifier-iframe/templates/head_sub.j2" -%}
|
||||
{%- endif -%}
|
||||
{%- if modifier_javascript_enabled -%}
|
||||
{%- include "roles/nginx-modifier-javascript/templates/head_sub.j2" -%}
|
||||
{%- endif -%}
|
||||
</head>';
|
||||
{% endif %}
|
||||
|
||||
{% if features_css_final | bool or features_matomo_final | bool %}
|
||||
sub_filter '</head>' '{% if features_matomo_final | bool %}{% include 'roles/nginx-modifier-matomo/templates/script.j2' %}{% endif %}{% if features_css_final | bool %}{% include 'roles/nginx-modifier-css/templates/link.j2' %}{% endif %}</head>';
|
||||
{% if modifier_css_enabled | bool %}
|
||||
{% include 'roles/nginx-modifier-css/templates/location.conf.j2' %}
|
||||
{% endif %}
|
||||
|
||||
{% if features_css_final | bool %}
|
||||
{# Include Global CSS Location #}
|
||||
{% include 'roles/nginx-modifier-css/templates/location.conf.j2' %}
|
||||
{% endif %}
|
||||
{% if modifier_matomo_enabled %}
|
||||
{% include 'roles/nginx-modifier-matomo/templates/matomo-tracking.conf.j2' %}
|
||||
{% endif %}
|
24
roles/nginx-modifier-iframe/README.md
Normal file
24
roles/nginx-modifier-iframe/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
# 🌐 iFrame Notifier for Nginx
|
||||
|
||||
This Ansible role injects a small JavaScript snippet into your HTML responses that enables parent pages to get notified whenever the iframe’s location changes and forces external links to open in a new tab.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Location Change Notification**
|
||||
Uses `postMessage` to inform the parent window of any URL changes inside the iframe (including pushState/popState events) for seamless SPA support.
|
||||
|
||||
- **External Link Handling**
|
||||
Automatically sets `target="_blank"` and `rel="noopener"` on links pointing outside your primary domain to improve security and user experience.
|
||||
|
||||
- **Easy CSP Integration**
|
||||
Calculates a CSP hash for the injected script so you can safely allow it via your Content Security Policy.
|
||||
|
||||
---
|
||||
|
||||
## Author
|
||||
|
||||
Developed by **Kevin Veen-Birkenbach**
|
||||
[https://www.veen.world](https://www.veen.world) 🎉
|
28
roles/nginx-modifier-iframe/meta/main.yml
Normal file
28
roles/nginx-modifier-iframe/meta/main.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
---
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Injects a JS snippet into HTML to notify parent windows of iframe location changes and force external links to new tabs."
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
license: "CyMaIS NonCommercial License (CNCL)"
|
||||
repository: https://s.veen.world/cymais
|
||||
issue_tracker_url: https://s.veen.world/cymaisissues
|
||||
documentation: https://s.veen.world/cymais
|
||||
license_url: "https://s.veen.world/cncl"
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
- name: Archlinux
|
||||
versions:
|
||||
- rolling
|
||||
galaxy_tags:
|
||||
- nginx
|
||||
- iframe
|
||||
- javascript
|
||||
- csp
|
||||
- security
|
||||
- postMessage
|
||||
dependencies:
|
||||
- nginx
|
12
roles/nginx-modifier-iframe/tasks/main.yml
Normal file
12
roles/nginx-modifier-iframe/tasks/main.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
- name: Load iFrame handler JS template
|
||||
set_fact:
|
||||
iframe_code: "{{ lookup('template','iframe-handler.js.j2') }}"
|
||||
|
||||
- name: Collapse iFrame code into one-liner
|
||||
set_fact:
|
||||
iframe_code_one_liner: "{{ iframe_code | to_one_liner }}"
|
||||
|
||||
- name: Append iFrame CSP hash
|
||||
set_fact:
|
||||
applications: "{{ applications | append_csp_hash(application_id, iframe_code_one_liner) }}"
|
||||
changed_when: false
|
1
roles/nginx-modifier-iframe/templates/head_sub.j2
Normal file
1
roles/nginx-modifier-iframe/templates/head_sub.j2
Normal file
@@ -0,0 +1 @@
|
||||
<script>{{ iframe_code_one_liner | replace("'", "\\'") }}</script>
|
46
roles/nginx-modifier-iframe/templates/iframe-handler.js.j2
Normal file
46
roles/nginx-modifier-iframe/templates/iframe-handler.js.j2
Normal file
@@ -0,0 +1,46 @@
|
||||
(function() {
|
||||
var primary = "{{ primary_domain }}";
|
||||
var allowedOrigin = "https://{{ domains | get_domain('portfolio') }}";
|
||||
|
||||
function notifyParent() {
|
||||
try {
|
||||
window.parent.postMessage({
|
||||
type: "iframeLocationChange",
|
||||
href: window.location.href
|
||||
}, allowedOrigin);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function forceExternalLinks() {
|
||||
Array.prototype.forEach.call(document.querySelectorAll("a[href]"), function(a) {
|
||||
try {
|
||||
var url = new URL(a.href, location);
|
||||
if (!url.hostname.endsWith(primary)) {
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener";
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("load", function() {
|
||||
notifyParent();
|
||||
forceExternalLinks();
|
||||
});
|
||||
window.addEventListener("popstate", function() {
|
||||
notifyParent();
|
||||
forceExternalLinks();
|
||||
});
|
||||
|
||||
// SPA support
|
||||
var _pushState = history.pushState;
|
||||
history.pushState = function() {
|
||||
_pushState.apply(history, arguments);
|
||||
notifyParent();
|
||||
forceExternalLinks();
|
||||
};
|
||||
})();
|
||||
|
||||
{% if enable_debug | bool %}
|
||||
console.log("[iframe-sync] Sender for iframe messages is active.");
|
||||
{% endif %}
|
28
roles/nginx-modifier-javascript/README.md
Normal file
28
roles/nginx-modifier-javascript/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 🌐 Global JavaScript Injector for Nginx
|
||||
|
||||
## Description
|
||||
|
||||
This Ansible role injects a custom JavaScript snippet into all HTML responses served by Nginx. It leverages Nginx’s `sub_filter` to seamlessly insert your application-specific script just before the closing `</head>` tag, ensuring that your code runs on every page load—perfect for global feature flags, analytics, or UI enhancements.
|
||||
|
||||
## Features
|
||||
|
||||
- **One-line Script Injection**
|
||||
Collapses your JavaScript into a single line and injects it via `sub_filter` for minimal footprint and maximal compatibility.
|
||||
|
||||
- **Easy CSP Integration**
|
||||
Automatically computes and appends a CSP hash entry for your script, so you can lock down Content Security Policy without lifting a finger.
|
||||
|
||||
- **Conditional Activation**
|
||||
Activates only when you enable the `javascript` feature for a given application, keeping your server blocks clean and performant.
|
||||
|
||||
- **Debug Mode**
|
||||
Supports an `enable_debug` flag that appends optional `console.log` statements for easier troubleshooting in staging or development.
|
||||
|
||||
## Author
|
||||
|
||||
Developed by **Kevin Veen-Birkenbach**
|
||||
Consulting & Coaching Solutions — [veen.world](https://www.veen.world)
|
||||
|
||||
---
|
||||
|
||||
Happy automating! 🎉
|
28
roles/nginx-modifier-javascript/meta/main.yml
Normal file
28
roles/nginx-modifier-javascript/meta/main.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Injects a custom JavaScript snippet into Nginx-served HTML responses via sub_filter."
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
license: "CyMaIS NonCommercial License (CNCL)"
|
||||
license_url: "https://s.veen.world/cncl"
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
- name: Archlinux
|
||||
versions:
|
||||
- rolling
|
||||
galaxy_tags:
|
||||
- nginx
|
||||
- javascript
|
||||
- csp
|
||||
- sub_filter
|
||||
- injection
|
||||
- global
|
||||
repository: "https://s.veen.world/cymais"
|
||||
documentation: "https://s.veen.world/cymais"
|
||||
issue_tracker_url: "https://s.veen.world/cymaisissues"
|
||||
|
||||
dependencies:
|
||||
- nginx
|
12
roles/nginx-modifier-javascript/tasks/main.yml
Normal file
12
roles/nginx-modifier-javascript/tasks/main.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
- name: "Load JavaScript code for '{{ application_id }}'"
|
||||
set_fact:
|
||||
javascript_code: "{{ lookup('template', modifier_javascript_template_file) }}"
|
||||
|
||||
- name: "Collapse Javascript code into one-liner for '{{application_id}}'"
|
||||
set_fact:
|
||||
javascript_code_one_liner: "{{ javascript_code | to_one_liner }}"
|
||||
|
||||
- name: "Append Javascript CSP hash for '{{application_id}}'"
|
||||
set_fact:
|
||||
applications: "{{ applications | append_csp_hash(application_id, javascript_code_one_liner) }}"
|
||||
changed_when: false
|
1
roles/nginx-modifier-javascript/templates/head_sub.j2
Normal file
1
roles/nginx-modifier-javascript/templates/head_sub.j2
Normal file
@@ -0,0 +1 @@
|
||||
<script>{{ javascript_code_one_liner | replace("'", "\\'") }}</script>
|
1
roles/nginx-modifier-javascript/vars/main.yml
Normal file
1
roles/nginx-modifier-javascript/vars/main.yml
Normal file
@@ -0,0 +1 @@
|
||||
modifier_javascript_template_file: "{{ playbook_dir }}/roles/docker-{{ application_id }}/templates/javascript.js.j2"
|
@@ -45,45 +45,16 @@
|
||||
when: "matomo_site_id is not defined or matomo_site_id is none"
|
||||
changed_when: false
|
||||
|
||||
- name: Set the Matomo tracking code from a template file
|
||||
- name: Load Matomo tracking JS template
|
||||
set_fact:
|
||||
matomo_tracking_code: "{{ lookup('template', 'matomo-tracking.js.j2') }}"
|
||||
matomo_tracking_code: "{{ lookup('template','matomo-tracking.js.j2') }}"
|
||||
|
||||
- name: Set the tracking code as a one-liner
|
||||
- name: Collapse Matomo code into one-liner
|
||||
set_fact:
|
||||
matomo_tracking_code_one_liner: "{{ matomo_tracking_code | regex_replace('\\n', '') | regex_replace('\\s+', ' ') }}"
|
||||
matomo_tracking_code_one_liner: "{{ matomo_tracking_code | to_one_liner }}"
|
||||
|
||||
- name: Ensure csp.hashes exists for this app
|
||||
- name: Append Matomo CSP hash
|
||||
set_fact:
|
||||
applications: >-
|
||||
{{
|
||||
applications
|
||||
| combine({
|
||||
(application_id): {
|
||||
'csp': {
|
||||
'hashes': {}
|
||||
}
|
||||
}
|
||||
}, recursive=True)
|
||||
}}
|
||||
applications: "{{ applications | append_csp_hash(application_id, matomo_tracking_code_one_liner) }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Append Matomo one-liner to script-src inline hashes
|
||||
set_fact:
|
||||
applications: >-
|
||||
{{
|
||||
applications
|
||||
| combine({
|
||||
(application_id): {
|
||||
'csp': {
|
||||
'hashes': {
|
||||
'script-src-elem': (
|
||||
applications[application_id]['csp']['hashes'].get('script-src', [])
|
||||
+ [ matomo_tracking_code_one_liner ]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, recursive=True)
|
||||
}}
|
||||
changed_when: false
|
||||
|
@@ -13,3 +13,7 @@ _paq.push(["enableLinkTracking"]);
|
||||
var d=document, g=d.createElement("script"), s=d.getElementsByTagName("script")[0];
|
||||
g.async=true; g.src=u+"matomo.js"; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
|
||||
{% if enable_debug | bool %}
|
||||
console.log("Matomo is loaded.");
|
||||
{% endif %}
|
Reference in New Issue
Block a user