BreznFlow 1.0.0 — WordPress.org submission

Initial public release of BreznFlow, an n8n workflow renderer for WordPress.
Fully PHPCS-compliant (WordPress Coding Standards), security-hardened,
and ready for WordPress.org plugin review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael 2026-03-30 11:27:36 +00:00
commit fd83e4810b
43 changed files with 9823 additions and 0 deletions

168
assets/admin.css Normal file
View file

@ -0,0 +1,168 @@
/* BreznFlow — Admin Styles */
.breznflow-wizard,
.breznflow-list-page,
.breznflow-settings-page {
max-width: 900px;
}
/* Wizard Steps */
.breznflow-wizard-steps {
display: flex;
gap: 0;
margin: 16px 0 24px;
border-bottom: 2px solid #ddd;
}
.breznflow-step {
padding: 8px 20px;
font-size: 14px;
color: #666;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
}
.breznflow-step.active {
color: #2271b1;
border-bottom-color: #2271b1;
font-weight: 600;
}
.breznflow-step.done {
color: #00a32a;
}
/* Cards */
.breznflow-card {
background: #fff;
border: 1px solid #ddd;
border-radius: 6px;
padding: 20px 24px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,.04);
}
.breznflow-card h2 {
margin-top: 0;
font-size: 16px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-bottom: 16px;
}
.breznflow-card-security {
border-color: #f0c000;
background: #fffbee;
}
/* URL Import */
.breznflow-url-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.breznflow-url-row input {
flex: 1;
}
/* File upload */
.breznflow-file-upload {
margin-bottom: 16px;
}
.breznflow-file-upload label,
.breznflow-step1-import-url label,
.breznflow-json-field label {
display: block;
font-weight: 600;
margin-bottom: 6px;
}
/* JSON textarea */
.breznflow-json-field textarea {
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
resize: vertical;
}
/* Validation result */
#breznflow-validation-result {
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 12px;
font-size: 13px;
}
#breznflow-validation-result.success {
background: #edfaef;
border: 1px solid #00a32a;
color: #00a32a;
}
#breznflow-validation-result.error {
background: #fdecea;
border: 1px solid #d63638;
color: #d63638;
}
/* Shortcode preview */
.breznflow-shortcode-preview {
display: flex;
align-items: center;
gap: 12px;
background: #f6f7f7;
padding: 10px 14px;
border-radius: 4px;
font-size: 14px;
}
.breznflow-shortcode-preview code {
flex: 1;
font-size: 14px;
}
/* Mask log */
.breznflow-mask-log {
margin: 10px 0 0;
padding-left: 20px;
font-size: 13px;
line-height: 2;
}
.breznflow-mask-log em {
color: #666;
}
/* Step 2 meta info */
.breznflow-step2-meta {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 16px;
font-size: 13px;
color: #555;
}
/* List table */
.breznflow-badge-ai {
background: #7c3aed;
color: #fff;
padding: 1px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
}
/* Preview container in step 3 */
.breznflow-preview-container {
border: 1px solid #ddd;
border-radius: 6px;
overflow: hidden;
min-height: 300px;
}
/* Multiselect */
.breznflow-multiselect {
min-height: 80px;
}

199
assets/admin.js Normal file
View file

@ -0,0 +1,199 @@
/* BreznFlow — Admin JS (vanilla ES2020, no dependencies) */
/* global breznflowAdmin */
(function () {
'use strict';
// ── AJAX helpers ──────────────────────────────────────────────────────────
function post(action, data, callback) {
const params = new URLSearchParams();
params.append('action', action);
params.append('nonce', breznflowAdmin.nonce);
for (const [k, v] of Object.entries(data)) {
params.append(k, v);
}
fetch(breznflowAdmin.ajaxUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
})
.then(function(r) { return r.json(); })
.then(callback)
.catch(function(err) {
callback({ success: false, data: { message: String(err) } });
});
}
// ── Step 1: Validation ────────────────────────────────────────────────────
const jsonTextarea = document.getElementById('breznflow-json');
const validateBtn = document.getElementById('breznflow-validate-btn');
const submitBtn = document.getElementById('breznflow-step1-submit');
const resultDiv = document.getElementById('breznflow-validation-result');
function showResult(success, message) {
if (!resultDiv) return;
resultDiv.removeAttribute('hidden');
resultDiv.className = success ? 'success' : 'error';
resultDiv.textContent = message;
}
if (validateBtn && jsonTextarea) {
validateBtn.addEventListener('click', function () {
const json = jsonTextarea.value.trim();
if (!json) {
showResult(false, breznflowAdmin.i18n.pasteFirst || 'Please paste a workflow JSON first.');
return;
}
validateBtn.disabled = true;
validateBtn.textContent = breznflowAdmin.i18n.validating || 'Validating...';
post('breznflow_validate_json', { json: json }, function(resp) {
validateBtn.disabled = false;
validateBtn.textContent = breznflowAdmin.i18n.validateJson || 'Validate JSON';
if (resp.success) {
const msg = (breznflowAdmin.i18n.valid || 'Valid n8n workflow') +
': ' + resp.data.name + ' \u2014 ' + resp.data.nodes + ' ' + (breznflowAdmin.i18n.nodes || 'nodes');
showResult(true, msg);
if (submitBtn) submitBtn.disabled = false;
} else {
showResult(false, (breznflowAdmin.i18n.invalid || 'Invalid') + ': ' + (resp.data && resp.data.message ? resp.data.message : 'Unknown error'));
if (submitBtn) submitBtn.disabled = true;
}
});
});
}
// ── Step 1: File upload → textarea ────────────────────────────────────────
const fileInput = document.getElementById('breznflow-file');
if (fileInput && jsonTextarea) {
fileInput.addEventListener('change', function () {
const file = this.files && this.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
jsonTextarea.value = e.target.result;
if (resultDiv) {
resultDiv.setAttribute('hidden', '');
resultDiv.className = '';
resultDiv.textContent = '';
}
if (submitBtn) submitBtn.disabled = true;
};
reader.readAsText(file);
});
}
// ── Step 1: URL fetch ─────────────────────────────────────────────────────
const urlInput = document.getElementById('breznflow-url');
const fetchBtn = document.getElementById('breznflow-fetch-url');
if (fetchBtn && urlInput && jsonTextarea) {
fetchBtn.addEventListener('click', function () {
const url = urlInput.value.trim();
if (!url) return;
fetchBtn.disabled = true;
fetchBtn.textContent = breznflowAdmin.i18n.fetching || 'Fetching...';
post('breznflow_fetch_url', { url: url }, function(resp) {
fetchBtn.disabled = false;
fetchBtn.textContent = breznflowAdmin.i18n.fetch || 'Fetch';
if (resp.success && resp.data && resp.data.json) {
jsonTextarea.value = resp.data.json;
if (submitBtn) submitBtn.disabled = true;
if (resultDiv) {
resultDiv.setAttribute('hidden', '');
resultDiv.textContent = '';
}
} else {
showResult(false, resp.data && resp.data.message ? resp.data.message : (breznflowAdmin.i18n.fetchFailed || 'Fetch failed'));
}
});
});
}
// ── Copy Shortcode buttons ────────────────────────────────────────────────
function attachCopyButtons() {
document.querySelectorAll('.breznflow-copy-sc').forEach(function(btn) {
btn.addEventListener('click', function () {
const sc = btn.getAttribute('data-sc') || '';
const label = btn.textContent;
if (!sc) return;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(sc).then(function() {
btn.textContent = breznflowAdmin.i18n.copied || 'Copied!';
setTimeout(function() { btn.textContent = label; }, 2000);
}).catch(function() {
fallbackCopy(sc, btn, label);
});
} else {
fallbackCopy(sc, btn, label);
}
});
});
}
function fallbackCopy(text, btn, label) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
try {
document.execCommand('copy');
btn.textContent = breznflowAdmin.i18n.copied || 'Copied!';
setTimeout(function() { btn.textContent = label; }, 2000);
} catch (e) {
// silently ignore
}
document.body.removeChild(ta);
}
attachCopyButtons();
// ── Step 2: Live shortcode preview ────────────────────────────────────────
const scLive = document.getElementById('breznflow-shortcode-live');
if (scLive) {
const postIdInput = document.querySelector('input[name="breznflow_post_id"]');
const postId = postIdInput ? postIdInput.value : '';
const modeRadios = document.querySelectorAll('input[name="default_mode"]');
const showTitleCb = document.querySelector('input[name="show_title"]');
const showInfoCb = document.querySelector('input[name="show_infobox"]');
const showDlCb = document.querySelector('input[name="show_download"]');
const zoomInput = document.querySelector('input[name="default_zoom"]');
function updatePreview() {
let sc = '[breznflow id="' + postId + '"';
const modeVal = document.querySelector('input[name="default_mode"]:checked');
if (modeVal && modeVal.value !== 'visual') sc += ' mode="' + modeVal.value + '"';
if (zoomInput && zoomInput.value !== '100') sc += ' zoom="' + zoomInput.value + '"';
if (showTitleCb && !showTitleCb.checked) sc += ' show_title="0"';
if (showInfoCb && !showInfoCb.checked) sc += ' show_infobox="0"';
if (showDlCb && showDlCb.checked) sc += ' show_download="1"';
sc += ']';
scLive.textContent = sc;
// Update copy button data attribute
const copyBtn = scLive.nextElementSibling;
if (copyBtn && copyBtn.classList.contains('breznflow-copy-sc')) {
copyBtn.setAttribute('data-sc', sc);
}
}
modeRadios.forEach(function(r) { r.addEventListener('change', updatePreview); });
if (showTitleCb) showTitleCb.addEventListener('change', updatePreview);
if (showInfoCb) showInfoCb.addEventListener('change', updatePreview);
if (showDlCb) showDlCb.addEventListener('change', updatePreview);
if (zoomInput) zoomInput.addEventListener('input', updatePreview);
}
}());

52
assets/brezn.css Normal file
View file

@ -0,0 +1,52 @@
/*
Theme Name: Brezn
Theme ID: brezn
Description: Biergarten bei Nacht dark amber canvas, Bavarian gold nodes, royal blue connections, state-seal red logic accents.
Author: BreznFlow
*/
.breznflow-wrap[data-theme="brezn"],
.breznflow-modal-overlay[data-theme="brezn"],
.breznflow-fs-portal[data-theme="brezn"] {
--breznflow-canvas-bg: #0d0800;
--breznflow-node-bg: #1e1300;
--breznflow-node-text: #f5c800;
--breznflow-node-sub: #8a6c00;
--breznflow-node-border: #3a2a00;
--breznflow-connection: #0066b3;
--breznflow-connection-hover: #3399ff;
--breznflow-toolbar-bg: #080500;
--breznflow-toolbar-text: #f5c800;
--breznflow-toolbar-border: #2a1f00;
--breznflow-panel-bg: #080500;
--breznflow-panel-text: #e8d070;
--breznflow-panel-border: #2a1f00;
--breznflow-btn-bg: #0066b3;
--breznflow-btn-text: #ffffff;
--breznflow-btn-border: #0077cc;
--breznflow-btn-hover-bg: #0077cc;
--breznflow-action-bar-bg: #080500;
--breznflow-action-bar-border: #2a1f00;
--breznflow-modal-overlay-bg: rgba(5, 3, 0, 0.88);
--breznflow-modal-bg: #100c00;
--breznflow-modal-border: #3a2a00;
--breznflow-modal-title: #f5c800;
--breznflow-modal-text: #e8d070;
--breznflow-modal-sub: #8a6c00;
--breznflow-modal-close: #8a6c00;
--breznflow-modal-secondary-bg: #0d0800;
--breznflow-modal-secondary-border: #3a2a00;
--breznflow-modal-code-bg: #050300;
--breznflow-tooltip-bg: rgba(5, 3, 0, 0.95);
--breznflow-tooltip-text: #f5c800;
--breznflow-fullscreen-overlay-bg: rgba(0, 0, 0, 0.92);
--breznflow-minimap-bg: rgba(13, 8, 0, 0.9);
--breznflow-minimap-border: #3a2a00;
--breznflow-color-trigger: #22c55e;
--breznflow-color-http: #0066b3;
--breznflow-color-code: #f5c800;
--breznflow-color-logic: #cc2200;
--breznflow-color-database: #b88a00;
--breznflow-color-ai: #ff8c00;
--breznflow-color-fallback: #5b9bc4;
}

613
assets/renderer.css Normal file
View file

@ -0,0 +1,613 @@
/* BreznFlow — Frontend Renderer Styles */
/* Color values live in assets/themes/*.css — this file contains structure only. */
/* Wrap */
.breznflow-embed {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 14px;
line-height: 1.5;
border: 1px solid var(--breznflow-node-border);
border-radius: 8px;
overflow: hidden;
background: var(--breznflow-canvas-bg);
}
.breznflow-wrap {
display: flex;
flex-direction: column;
width: 100%;
}
/* Title */
.breznflow-title {
margin: 0 0 0.5em;
}
/* Toolbar */
.breznflow-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--breznflow-toolbar-bg);
border-bottom: 1px solid var(--breznflow-toolbar-border);
flex-wrap: wrap;
}
.breznflow-toolbar-name {
font-weight: 600;
color: var(--breznflow-toolbar-text);
margin-right: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 40%;
}
.breznflow-btn {
background: var(--breznflow-btn-bg);
color: var(--breznflow-btn-text);
border: 1px solid var(--breznflow-btn-border);
border-radius: 4px;
padding: 4px 10px;
cursor: pointer;
font-size: 13px;
line-height: 1.4;
transition: background 0.15s;
}
.breznflow-btn:hover {
background: var(--breznflow-btn-hover-bg);
}
.breznflow-zoom-label {
color: var(--breznflow-node-sub);
font-size: 12px;
min-width: 36px;
text-align: center;
}
/* Diagram Container */
.breznflow-diagram-container {
position: relative;
overflow: hidden;
min-height: 200px;
cursor: grab;
user-select: none;
}
.breznflow-diagram-container:active {
cursor: grabbing;
}
.breznflow-svg {
display: block;
width: 100%;
overflow: visible;
}
/* Canvas (zoom/pan transform applied here) */
.breznflow-canvas {
transition: none;
}
/* Connections */
.breznflow-connection-path {
fill: none;
stroke: var(--breznflow-connection);
stroke-width: 2;
stroke-linecap: round;
transition: stroke 0.2s;
}
.breznflow-connection-path:hover {
stroke: var(--breznflow-connection-hover);
}
/* Nodes */
.breznflow-node {
cursor: pointer;
}
.breznflow-node:hover .breznflow-node-box {
filter: brightness(1.15);
}
.breznflow-node.selected .breznflow-node-box {
stroke-width: 2;
}
.breznflow-node-box {
fill: var(--breznflow-node-bg);
stroke: var(--breznflow-node-border);
stroke-width: 1;
rx: 6;
ry: 6;
transition: filter 0.15s;
}
.breznflow-node-icon-rect {
rx: 4;
ry: 4;
}
.breznflow-node-icon-text {
dominant-baseline: central;
text-anchor: middle;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
font-weight: 700;
}
.breznflow-node-name {
fill: var(--breznflow-node-text);
dominant-baseline: central;
text-anchor: start;
font-size: 13px;
font-weight: 600;
}
.breznflow-node-type {
fill: var(--breznflow-node-sub);
dominant-baseline: central;
text-anchor: start;
font-size: 10px;
}
/* Detail Panel */
.breznflow-detail-panel {
background: var(--breznflow-panel-bg);
border-top: 1px solid var(--breznflow-panel-border);
color: var(--breznflow-panel-text);
overflow: hidden;
max-height: 0;
transition: max-height 0.3s ease;
}
.breznflow-detail-panel.open {
max-height: 400px;
overflow-y: auto;
}
.breznflow-detail-panel-inner {
padding: 16px;
}
.breznflow-detail-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.breznflow-detail-title {
font-size: 15px;
font-weight: 600;
}
.breznflow-detail-close {
background: none;
border: none;
color: var(--breznflow-node-sub);
cursor: pointer;
font-size: 18px;
padding: 0 4px;
line-height: 1;
}
.breznflow-detail-close:hover {
color: var(--breznflow-panel-text);
}
.breznflow-detail-dl {
display: grid;
grid-template-columns: max-content 1fr;
gap: 4px 16px;
margin: 0;
font-size: 13px;
}
.breznflow-detail-dt {
color: var(--breznflow-node-sub);
font-weight: 500;
}
.breznflow-detail-dd {
margin: 0;
word-break: break-word;
color: var(--breznflow-panel-text);
}
.breznflow-detail-multiline {
margin: 0;
font-family: inherit;
font-size: 0.88em;
white-space: pre-wrap;
word-break: break-word;
color: var(--breznflow-panel-text);
line-height: 1.5;
}
.breznflow-detail-code {
background: var(--breznflow-modal-code-bg);
border: 1px solid var(--breznflow-modal-border);
border-radius: 4px;
padding: 8px;
font-family: monospace;
font-size: 12px;
overflow-y: auto;
white-space: pre;
color: #a8e6cf;
margin: 4px 0;
}
/* InfoBox */
.breznflow-infobox {
background: var(--breznflow-toolbar-bg);
border-top: 1px solid var(--breznflow-node-border);
padding: 12px 16px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.breznflow-infobox-nodes {
display: flex;
flex-wrap: wrap;
gap: 6px;
flex: 1;
}
.breznflow-infobox-node {
background: var(--breznflow-btn-bg);
border-radius: 4px;
padding: 2px 8px;
font-size: 12px;
color: var(--breznflow-btn-text);
white-space: nowrap;
}
.breznflow-infobox-node strong {
color: var(--breznflow-node-text);
}
.breznflow-infobox-more {
font-size: 12px;
color: var(--breznflow-node-sub);
background: none;
border: none;
padding: 2px 4px;
cursor: pointer;
border-radius: 3px;
transition: color 0.15s, background 0.15s;
}
.breznflow-infobox-more:hover {
color: var(--breznflow-panel-text);
background: rgba(255, 255, 255, 0.08);
}
.breznflow-infobox-ai {
display: flex;
align-items: center;
}
.breznflow-ai-badge {
background: linear-gradient(135deg, #7c3aed, #4338ca);
color: #fff;
border-radius: 12px;
padding: 2px 10px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
}
.breznflow-infobox-total {
font-size: 12px;
color: var(--breznflow-node-sub);
white-space: nowrap;
}
/* Admin badge */
.breznflow-badge-ai {
background: #7c3aed;
color: #fff;
padding: 1px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
}
/* Arrowhead */
.breznflow-arrow-head {
fill: var(--breznflow-connection);
}
/* Category node border colors */
[data-category="trigger"] .breznflow-node-box { stroke: var(--breznflow-color-trigger); stroke-width: 2; }
[data-category="http"] .breznflow-node-box { stroke: var(--breznflow-color-http); stroke-width: 2; }
[data-category="code"] .breznflow-node-box { stroke: var(--breznflow-color-code); stroke-width: 2; }
[data-category="logic"] .breznflow-node-box { stroke: var(--breznflow-color-logic); stroke-width: 2; }
[data-category="database"].breznflow-node-box { stroke: var(--breznflow-color-database); stroke-width: 2; }
[data-category="ai"] .breznflow-node-box { stroke: var(--breznflow-color-ai); stroke-width: 2; }
/* Connection dots — match category color */
[data-category="trigger"] circle { stroke: var(--breznflow-color-trigger); }
[data-category="http"] circle { stroke: var(--breznflow-color-http); }
[data-category="code"] circle { stroke: var(--breznflow-color-code); }
[data-category="logic"] circle { stroke: var(--breznflow-color-logic); }
[data-category="database"]circle { stroke: var(--breznflow-color-database); }
[data-category="ai"] circle { stroke: var(--breznflow-color-ai); }
/* Tooltip */
.breznflow-tooltip {
position: absolute;
pointer-events: none;
background: var(--breznflow-tooltip-bg);
color: var(--breznflow-tooltip-text);
font-size: 12px;
padding: 4px 10px;
border-radius: 4px;
white-space: nowrap;
transform: translate(-50%, -100%);
opacity: 0;
transition: opacity 0.12s;
z-index: 10;
}
.breznflow-tooltip.visible { opacity: 1; }
/* Fullscreen portal — appended to <body> to escape any containing block */
.breznflow-fs-portal {
position: fixed;
inset: 0;
top: var(--wp-admin--admin-bar--height, 0px);
z-index: 9999;
display: flex;
align-items: stretch;
padding: 48px;
background: var(--breznflow-fullscreen-overlay-bg);
cursor: default;
}
.breznflow-fs-portal .breznflow-wrap {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.7);
cursor: default;
}
.breznflow-fs-portal .breznflow-diagram-container {
flex: 1;
min-height: 0;
height: auto !important;
cursor: grab;
}
.breznflow-fs-portal .breznflow-diagram-container:active {
cursor: grabbing;
}
/* Minimap */
.breznflow-minimap {
position: absolute;
bottom: 8px;
right: 8px;
width: 160px;
height: 100px;
background: var(--breznflow-minimap-bg);
border: 1px solid var(--breznflow-minimap-border);
border-radius: 4px;
overflow: hidden;
cursor: crosshair;
z-index: 5;
}
.breznflow-minimap svg { display: block; }
/* Infobox node badges — interactive highlight */
.breznflow-infobox-node {
cursor: pointer;
border-left: 3px solid transparent;
padding-left: 6px;
transition: background 0.15s, opacity 0.15s;
}
.breznflow-infobox-node:hover {
background: var(--breznflow-btn-hover-bg);
}
.breznflow-infobox-node.active {
background: var(--breznflow-btn-hover-bg);
outline: 1px solid rgba(255,255,255,0.15);
outline-offset: -1px;
}
/* Category-colored left borders on infobox badges */
.breznflow-infobox-node[data-category="trigger"] { border-left-color: var(--breznflow-color-trigger); }
.breznflow-infobox-node[data-category="http"] { border-left-color: var(--breznflow-color-http); }
.breznflow-infobox-node[data-category="code"] { border-left-color: var(--breznflow-color-code); }
.breznflow-infobox-node[data-category="logic"] { border-left-color: var(--breznflow-color-logic); }
.breznflow-infobox-node[data-category="database"]{ border-left-color: var(--breznflow-color-database); }
.breznflow-infobox-node[data-category="ai"] { border-left-color: var(--breznflow-color-ai); }
/* Dim all nodes when highlight is active */
.breznflow-nodes.breznflow-dim .breznflow-node {
opacity: 0.15;
transition: opacity 0.12s;
}
/* Keep highlighted node fully visible */
.breznflow-nodes.breznflow-dim .breznflow-node.breznflow-highlighted {
opacity: 1;
}
/* Brighten the highlighted node box */
.breznflow-node.breznflow-highlighted .breznflow-node-box {
filter: brightness(1.35);
}
/* Action Bar */
.breznflow-action-bar {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
padding: 8px 12px;
background: var(--breznflow-action-bar-bg);
border-top: 1px solid var(--breznflow-action-bar-border);
}
/* Modal Overlay + Box */
.breznflow-modal-overlay {
position: fixed;
inset: 0;
z-index: 10000;
background: var(--breznflow-modal-overlay-bg);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.breznflow-modal-box {
background: var(--breznflow-modal-bg);
border: 1px solid var(--breznflow-modal-border);
border-radius: 8px;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.7);
min-width: 320px;
max-width: 540px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.breznflow-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px 10px;
border-bottom: 1px solid var(--breznflow-modal-border);
}
.breznflow-modal-title {
font-size: 15px;
font-weight: 600;
color: var(--breznflow-modal-title);
}
.breznflow-modal-close {
background: none;
border: none;
color: var(--breznflow-modal-close);
cursor: pointer;
font-size: 20px;
padding: 0 4px;
line-height: 1;
}
.breznflow-modal-close:hover {
color: var(--breznflow-modal-text);
}
.breznflow-modal-body {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Share Blocks */
.breznflow-share-block {
background: var(--breznflow-modal-secondary-bg);
border: 1px solid var(--breznflow-modal-secondary-border);
border-radius: 6px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.breznflow-share-label {
font-size: 11px;
font-weight: 600;
color: var(--breznflow-modal-sub);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.breznflow-share-preview {
font-size: 12px;
color: var(--breznflow-modal-text);
white-space: pre-wrap;
word-break: break-all;
background: var(--breznflow-modal-code-bg);
border: 1px solid var(--breznflow-modal-border);
border-radius: 4px;
padding: 8px;
font-family: monospace;
line-height: 1.5;
margin: 0;
}
/* Textarea + JSON meta */
.breznflow-modal-textarea {
width: 100%;
min-height: 90px;
background: var(--breznflow-modal-code-bg);
border: 1px solid var(--breznflow-modal-border);
border-radius: 4px;
color: var(--breznflow-modal-text);
font-family: monospace;
font-size: 12px;
padding: 8px;
resize: vertical;
box-sizing: border-box;
}
.breznflow-modal-desc {
font-size: 13px;
color: var(--breznflow-modal-sub);
margin: 0;
}
.breznflow-json-meta {
display: flex;
justify-content: flex-end;
}
.breznflow-json-size {
font-size: 11px;
color: var(--breznflow-modal-sub);
}
.breznflow-copy-btn {
align-self: flex-start;
}
/* Mobile */
@media (max-width: 600px) {
.breznflow-btn-zoom {
display: none;
}
.breznflow-detail-panel.open {
max-height: 300px;
}
.breznflow-modal-box {
min-width: 0;
max-width: 100%;
}
.breznflow-action-bar {
padding: 6px 8px;
gap: 6px;
}
}

1544
assets/renderer.js Normal file

File diff suppressed because it is too large Load diff

52
assets/themes/brezn.css Normal file
View file

@ -0,0 +1,52 @@
/*
Theme Name: Brezn
Theme ID: brezn
Description: Biergarten bei Nacht dark amber canvas, Bavarian gold nodes, royal blue connections, state-seal red logic accents.
Author: BreznFlow
*/
.breznflow-wrap[data-theme="brezn"],
.breznflow-modal-overlay[data-theme="brezn"],
.breznflow-fs-portal[data-theme="brezn"] {
--breznflow-canvas-bg: #0d0800;
--breznflow-node-bg: #1e1300;
--breznflow-node-text: #f5c800;
--breznflow-node-sub: #8a6c00;
--breznflow-node-border: #3a2a00;
--breznflow-connection: #0066b3;
--breznflow-connection-hover: #3399ff;
--breznflow-toolbar-bg: #080500;
--breznflow-toolbar-text: #f5c800;
--breznflow-toolbar-border: #2a1f00;
--breznflow-panel-bg: #080500;
--breznflow-panel-text: #e8d070;
--breznflow-panel-border: #2a1f00;
--breznflow-btn-bg: #0066b3;
--breznflow-btn-text: #ffffff;
--breznflow-btn-border: #0077cc;
--breznflow-btn-hover-bg: #0077cc;
--breznflow-action-bar-bg: #080500;
--breznflow-action-bar-border: #2a1f00;
--breznflow-modal-overlay-bg: rgba(5, 3, 0, 0.88);
--breznflow-modal-bg: #100c00;
--breznflow-modal-border: #3a2a00;
--breznflow-modal-title: #f5c800;
--breznflow-modal-text: #e8d070;
--breznflow-modal-sub: #8a6c00;
--breznflow-modal-close: #8a6c00;
--breznflow-modal-secondary-bg: #0d0800;
--breznflow-modal-secondary-border: #3a2a00;
--breznflow-modal-code-bg: #050300;
--breznflow-tooltip-bg: rgba(5, 3, 0, 0.95);
--breznflow-tooltip-text: #f5c800;
--breznflow-fullscreen-overlay-bg: rgba(0, 0, 0, 0.92);
--breznflow-minimap-bg: rgba(13, 8, 0, 0.9);
--breznflow-minimap-border: #3a2a00;
--breznflow-color-trigger: #22c55e;
--breznflow-color-http: #0066b3;
--breznflow-color-code: #f5c800;
--breznflow-color-logic: #cc2200;
--breznflow-color-database: #b88a00;
--breznflow-color-ai: #ff8c00;
--breznflow-color-fallback: #5b9bc4;
}

53
assets/themes/dark.css Normal file
View file

@ -0,0 +1,53 @@
/*
Theme Name: Dark
Theme ID: dark
Description: Deep navy canvas, dark panels. The default BreznFlow theme.
Author: BreznFlow
*/
:root,
.breznflow-wrap[data-theme="dark"],
.breznflow-modal-overlay[data-theme="dark"],
.breznflow-fs-portal[data-theme="dark"] {
--breznflow-canvas-bg: #1a1a2e;
--breznflow-node-bg: #2D2D2D;
--breznflow-node-text: #f0f0f0;
--breznflow-node-sub: #9ca3af;
--breznflow-node-border: #3f3f3f;
--breznflow-connection: #555;
--breznflow-connection-hover: #888;
--breznflow-toolbar-bg: #1f1f1f;
--breznflow-toolbar-text: #e0e0e0;
--breznflow-toolbar-border: #3f3f3f;
--breznflow-panel-bg: #1f1f1f;
--breznflow-panel-text: #e0e0e0;
--breznflow-panel-border: #333;
--breznflow-btn-bg: #333;
--breznflow-btn-text: #e0e0e0;
--breznflow-btn-border: #555;
--breznflow-btn-hover-bg: #444;
--breznflow-action-bar-bg: #1f1f1f;
--breznflow-action-bar-border: #3f3f3f;
--breznflow-modal-overlay-bg: rgba(0,0,0,0.65);
--breznflow-modal-bg: #1f1f1f;
--breznflow-modal-border: #444;
--breznflow-modal-title: #e0e0e0;
--breznflow-modal-text: #e0e0e0;
--breznflow-modal-sub: #aaa;
--breznflow-modal-close: #888;
--breznflow-modal-secondary-bg: #2a2a2a;
--breznflow-modal-secondary-border: #3a3a3a;
--breznflow-modal-code-bg: #111;
--breznflow-tooltip-bg: rgba(0,0,0,0.85);
--breznflow-tooltip-text: #f0f0f0;
--breznflow-fullscreen-overlay-bg: rgba(0,0,0,0.85);
--breznflow-minimap-bg: rgba(0,0,0,0.72);
--breznflow-minimap-border: #555;
--breznflow-color-trigger: #22c55e;
--breznflow-color-http: #3b82f6;
--breznflow-color-code: #f97316;
--breznflow-color-logic: #a855f7;
--breznflow-color-database: #eab308;
--breznflow-color-ai: #ec4899;
--breznflow-color-fallback: #6366f1;
}

52
assets/themes/light.css Normal file
View file

@ -0,0 +1,52 @@
/*
Theme Name: Light
Theme ID: light
Description: Light blue-grey canvas, white nodes, dark text. For bright blogs.
Author: BreznFlow
*/
.breznflow-wrap[data-theme="light"],
.breznflow-modal-overlay[data-theme="light"],
.breznflow-fs-portal[data-theme="light"] {
--breznflow-canvas-bg: #eef2f7;
--breznflow-node-bg: #ffffff;
--breznflow-node-text: #111827;
--breznflow-node-sub: #6b7280;
--breznflow-node-border: #d1d5db;
--breznflow-connection: #94a3b8;
--breznflow-connection-hover: #475569;
--breznflow-toolbar-bg: #f8fafc;
--breznflow-toolbar-text: #374151;
--breznflow-toolbar-border: #e2e8f0;
--breznflow-panel-bg: #f8fafc;
--breznflow-panel-text: #374151;
--breznflow-panel-border: #e2e8f0;
--breznflow-btn-bg: #e2e8f0;
--breznflow-btn-text: #374151;
--breznflow-btn-border: #cbd5e1;
--breznflow-btn-hover-bg: #d1d9e6;
--breznflow-action-bar-bg: #f1f5f9;
--breznflow-action-bar-border: #e2e8f0;
--breznflow-modal-overlay-bg: rgba(0,0,0,0.4);
--breznflow-modal-bg: #ffffff;
--breznflow-modal-border: #d1d5db;
--breznflow-modal-title: #111827;
--breznflow-modal-text: #374151;
--breznflow-modal-sub: #6b7280;
--breznflow-modal-close: #9ca3af;
--breznflow-modal-secondary-bg: #f1f5f9;
--breznflow-modal-secondary-border: #e2e8f0;
--breznflow-modal-code-bg: #f8fafc;
--breznflow-tooltip-bg: rgba(0,0,0,0.75);
--breznflow-tooltip-text: #ffffff;
--breznflow-fullscreen-overlay-bg: rgba(0,0,0,0.7);
--breznflow-minimap-bg: rgba(255,255,255,0.85);
--breznflow-minimap-border: #d1d5db;
--breznflow-color-trigger: #16a34a;
--breznflow-color-http: #2563eb;
--breznflow-color-code: #ea580c;
--breznflow-color-logic: #9333ea;
--breznflow-color-database: #ca8a04;
--breznflow-color-ai: #db2777;
--breznflow-color-fallback: #4f46e5;
}

52
assets/themes/minimal.css Normal file
View file

@ -0,0 +1,52 @@
/*
Theme Name: Minimal
Theme ID: minimal
Description: Almost white, muted category colors, documentation style.
Author: BreznFlow
*/
.breznflow-wrap[data-theme="minimal"],
.breznflow-modal-overlay[data-theme="minimal"],
.breznflow-fs-portal[data-theme="minimal"] {
--breznflow-canvas-bg: #fafafa;
--breznflow-node-bg: #ffffff;
--breznflow-node-text: #374151;
--breznflow-node-sub: #9ca3af;
--breznflow-node-border: #e5e7eb;
--breznflow-connection: #d1d5db;
--breznflow-connection-hover: #9ca3af;
--breznflow-toolbar-bg: #ffffff;
--breznflow-toolbar-text: #6b7280;
--breznflow-toolbar-border: #f3f4f6;
--breznflow-panel-bg: #ffffff;
--breznflow-panel-text: #374151;
--breznflow-panel-border: #f3f4f6;
--breznflow-btn-bg: #f3f4f6;
--breznflow-btn-text: #6b7280;
--breznflow-btn-border: #e5e7eb;
--breznflow-btn-hover-bg: #e9ecef;
--breznflow-action-bar-bg: #ffffff;
--breznflow-action-bar-border: #f3f4f6;
--breznflow-modal-overlay-bg: rgba(0,0,0,0.2);
--breznflow-modal-bg: #ffffff;
--breznflow-modal-border: #e5e7eb;
--breznflow-modal-title: #111827;
--breznflow-modal-text: #374151;
--breznflow-modal-sub: #9ca3af;
--breznflow-modal-close: #d1d5db;
--breznflow-modal-secondary-bg: #fafafa;
--breznflow-modal-secondary-border: #f3f4f6;
--breznflow-modal-code-bg: #f9fafb;
--breznflow-tooltip-bg: rgba(55,65,81,0.9);
--breznflow-tooltip-text: #ffffff;
--breznflow-fullscreen-overlay-bg: rgba(0,0,0,0.5);
--breznflow-minimap-bg: rgba(255,255,255,0.9);
--breznflow-minimap-border: #e5e7eb;
--breznflow-color-trigger: #86efac;
--breznflow-color-http: #93c5fd;
--breznflow-color-code: #fdba74;
--breznflow-color-logic: #d8b4fe;
--breznflow-color-database: #fde68a;
--breznflow-color-ai: #f9a8d4;
--breznflow-color-fallback: #c4b5fd;
}

52
assets/themes/tech.css Normal file
View file

@ -0,0 +1,52 @@
/*
Theme Name: Tech
Theme ID: tech
Description: GitHub Dark palette. Deep black canvas, electric blue accents. SSH console feel.
Author: BreznFlow
*/
.breznflow-wrap[data-theme="tech"],
.breznflow-modal-overlay[data-theme="tech"],
.breznflow-fs-portal[data-theme="tech"] {
--breznflow-canvas-bg: #0d1117;
--breznflow-node-bg: #161b22;
--breznflow-node-text: #c9d1d9;
--breznflow-node-sub: #8b949e;
--breznflow-node-border: #30363d;
--breznflow-connection: #21262d;
--breznflow-connection-hover: #58a6ff;
--breznflow-toolbar-bg: #010409;
--breznflow-toolbar-text: #58a6ff;
--breznflow-toolbar-border: #21262d;
--breznflow-panel-bg: #0d1117;
--breznflow-panel-text: #c9d1d9;
--breznflow-panel-border: #21262d;
--breznflow-btn-bg: #21262d;
--breznflow-btn-text: #58a6ff;
--breznflow-btn-border: #30363d;
--breznflow-btn-hover-bg: #30363d;
--breznflow-action-bar-bg: #010409;
--breznflow-action-bar-border: #21262d;
--breznflow-modal-overlay-bg: rgba(1,4,9,0.85);
--breznflow-modal-bg: #161b22;
--breznflow-modal-border: #30363d;
--breznflow-modal-title: #58a6ff;
--breznflow-modal-text: #c9d1d9;
--breznflow-modal-sub: #8b949e;
--breznflow-modal-close: #484f58;
--breznflow-modal-secondary-bg: #0d1117;
--breznflow-modal-secondary-border: #21262d;
--breznflow-modal-code-bg: #010409;
--breznflow-tooltip-bg: rgba(1,4,9,0.95);
--breznflow-tooltip-text: #58a6ff;
--breznflow-fullscreen-overlay-bg: rgba(1,4,9,0.92);
--breznflow-minimap-bg: rgba(13,17,23,0.9);
--breznflow-minimap-border: #30363d;
--breznflow-color-trigger: #3fb950;
--breznflow-color-http: #58a6ff;
--breznflow-color-code: #f0883e;
--breznflow-color-logic: #bc8cff;
--breznflow-color-database: #e3b341;
--breznflow-color-ai: #ff7b9c;
--breznflow-color-fallback: #79c0ff;
}

52
breznflow.php Normal file
View file

@ -0,0 +1,52 @@
<?php
/**
* BreznFlow main plugin file.
*
* @package BreznFlow
* @since 1.0.0
*
* Plugin Name: BreznFlow
* Plugin URI: https://noschmarrn.dev/
* Description: Display n8n automation workflows with an interactive SVG diagram, node detail panel, and sensitive data masking.
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 8.0
* Author: NoSchmarrn.dev
* Author URI: https://noschmarrn.dev/
* License: GPLv2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: breznflow
* Domain Path: /languages
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'BREZNFLOW_VERSION', '1.0.0' );
define( 'BREZNFLOW_FILE', __FILE__ );
define( 'BREZNFLOW_DIR', plugin_dir_path( __FILE__ ) );
define( 'BREZNFLOW_URL', plugin_dir_url( __FILE__ ) );
require_once BREZNFLOW_DIR . 'includes/Core.php';
add_action(
'plugins_loaded',
static function (): void {
\BreznFlow\Core::instance()->init();
}
);
register_activation_hook(
BREZNFLOW_FILE,
function () {
flush_rewrite_rules();
}
);
register_deactivation_hook(
BREZNFLOW_FILE,
function () {
flush_rewrite_rules();
}
);

View file

@ -0,0 +1,207 @@
<?php
/**
* Admin menu registration and dashboard rendering.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Registers the BreznFlow admin menu pages and handles delete actions.
*
* @since 1.0.0
*/
class AdminMenu {
/**
* Registers admin menu hooks and loads the themes sub-page.
*
* @since 1.0.0
* @return void
*/
public function register(): void {
add_action( 'admin_menu', array( $this, 'add_menus' ) );
add_action( 'admin_init', array( $this, 'handle_delete_action' ) );
require_once BREZNFLOW_DIR . 'includes/Admin/ThemesPage.php';
( new ThemesPage() )->register();
}
/**
* Adds the top-level menu and submenu pages to wp-admin.
*
* @since 1.0.0
* @return void
*/
public function add_menus(): void {
add_menu_page(
__( 'BreznFlow', 'breznflow' ),
__( 'BreznFlow', 'breznflow' ),
'edit_posts',
'breznflow',
array( $this, 'render_dashboard' ),
'dashicons-networking',
30
);
add_submenu_page(
'breznflow',
__( 'All Workflows', 'breznflow' ),
__( 'All Workflows', 'breznflow' ),
'edit_posts',
'breznflow',
array( $this, 'render_dashboard' )
);
add_submenu_page(
'breznflow',
__( 'Add Workflow', 'breznflow' ),
__( 'Add Workflow', 'breznflow' ),
'edit_posts',
'breznflow-add',
array( $this, 'render_wizard' )
);
add_submenu_page(
'breznflow',
__( 'Themes', 'breznflow' ),
__( 'Themes', 'breznflow' ),
'manage_options',
'breznflow-themes',
array( $this, 'render_themes' )
);
add_submenu_page(
'breznflow',
__( 'Settings', 'breznflow' ),
__( 'Settings', 'breznflow' ),
'manage_options',
'breznflow-settings',
array( $this, 'render_settings' )
);
}
/**
* Handles single and bulk delete actions early (admin_init),
* before any output is sent so wp_safe_redirect() works correctly.
*/
public function handle_delete_action(): void {
// Only act on our page.
if ( ! isset( $_GET['page'] ) || 'breznflow' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
$action = isset( $_GET['action'] ) ? sanitize_key( $_GET['action'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// ── Single-row delete ─────────────────────────────────────────────────
if ( 'delete' === $action && isset( $_GET['post'] ) && ! isset( $_GET['workflow'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$post_id = (int) sanitize_text_field( wp_unslash( $_GET['post'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if (
$post_id > 0
&& check_admin_referer( 'breznflow_delete_' . $post_id )
&& current_user_can( 'delete_post', $post_id )
) {
wp_trash_post( $post_id );
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow',
'deleted' => '1',
),
admin_url( 'admin.php' )
)
);
exit;
}
}
// ── Bulk delete ───────────────────────────────────────────────────────
// WP_List_Table sends action2 when the bottom select is used.
$bulk = $action;
if ( 'delete' !== $bulk && isset( $_GET['action2'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$bulk = sanitize_key( $_GET['action2'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
if ( 'delete' === $bulk && isset( $_GET['workflow'] ) && is_array( $_GET['workflow'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
check_admin_referer( 'bulk-workflows' );
$count = 0;
$raw_workflows = array_map( 'absint', wp_unslash( $_GET['workflow'] ) );
foreach ( $raw_workflows as $pid ) {
if ( $pid > 0 && current_user_can( 'delete_post', $pid ) ) {
wp_trash_post( $pid );
++$count;
}
}
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow',
'deleted' => $count,
),
admin_url( 'admin.php' )
)
);
exit;
}
}
/**
* Renders the workflow list dashboard page.
*
* @since 1.0.0
* @return void
*/
public function render_dashboard(): void {
require_once BREZNFLOW_DIR . 'includes/Admin/WorkflowListTable.php';
$table = new WorkflowListTable();
$table->prepare_items();
require BREZNFLOW_DIR . 'includes/Admin/views/list.php';
}
/**
* Renders the add-workflow wizard page for the current step.
*
* @since 1.0.0
* @return void
*/
public function render_wizard(): void {
$step = isset( $_GET['step'] ) ? (int) $_GET['step'] : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
switch ( $step ) {
case 2:
require BREZNFLOW_DIR . 'includes/Admin/views/wizard-step-2.php';
break;
case 3:
require BREZNFLOW_DIR . 'includes/Admin/views/wizard-step-3.php';
break;
default:
require BREZNFLOW_DIR . 'includes/Admin/views/wizard-step-1.php';
break;
}
}
/**
* Renders the themes management page.
*
* @since 1.0.0
* @return void
*/
public function render_themes(): void {
require_once BREZNFLOW_DIR . 'includes/Admin/ThemesPage.php';
( new ThemesPage() )->render();
}
/**
* Renders the plugin settings page.
*
* @since 1.0.0
* @return void
*/
public function render_settings(): void {
require BREZNFLOW_DIR . 'includes/Admin/views/settings.php';
}
}

View file

@ -0,0 +1,138 @@
<?php
/**
* Plugin settings registration and sanitization.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Registers the breznflow_settings option and provides default values.
*
* @since 1.0.0
*/
class SettingsPage {
/**
* Registers the settings group with WordPress.
*
* @since 1.0.0
* @return void
*/
public function register(): void {
add_action( 'admin_init', array( $this, 'register_settings' ) );
}
/**
* Registers the breznflow_settings_group with a sanitize callback.
*
* @since 1.0.0
* @return void
*/
public function register_settings(): void {
register_setting(
'breznflow_settings_group',
'breznflow_settings',
array(
'sanitize_callback' => array( $this, 'sanitize_settings' ),
)
);
}
/**
* Sanitizes the settings array on save.
*
* @param mixed $input Raw input from the settings form.
* @return array Sanitized settings.
*/
public function sanitize_settings( $input ): array {
if ( ! is_array( $input ) ) {
return self::get_defaults();
}
$defaults = self::get_defaults();
$clean = array();
$clean['default_zoom'] = isset( $input['default_zoom'] )
? max( 10, min( 200, (int) $input['default_zoom'] ) )
: $defaults['default_zoom'];
$clean['autofit_threshold'] = isset( $input['autofit_threshold'] )
? max( 0, min( 500, (int) $input['autofit_threshold'] ) )
: $defaults['autofit_threshold'];
$clean['max_code_lines'] = isset( $input['max_code_lines'] )
? max( 5, min( 500, (int) $input['max_code_lines'] ) )
: $defaults['max_code_lines'];
$clean['allow_download'] = ! empty( $input['allow_download'] );
$clean['download_label'] = isset( $input['download_label'] )
? sanitize_text_field( wp_unslash( $input['download_label'] ) )
: $defaults['download_label'];
$clean['default_mode'] = in_array( $input['default_mode'] ?? '', array( 'visual', 'info', 'compact' ), true )
? $input['default_mode']
: $defaults['default_mode'];
$clean['show_infobox_default'] = ! empty( $input['show_infobox_default'] );
$clean['show_title_default'] = ! empty( $input['show_title_default'] );
$clean['schema_howto'] = ! empty( $input['schema_howto'] );
$clean['related_workflows'] = ! empty( $input['related_workflows'] );
$clean['view_counting'] = ! empty( $input['view_counting'] );
$clean['allow_share'] = ! empty( $input['allow_share'] );
$clean['allow_embed'] = ! empty( $input['allow_embed'] );
$clean['allow_get_json'] = ! empty( $input['allow_get_json'] );
$allowed_themes = \BreznFlow\Features\ThemeRegistry::get_theme_ids();
$clean['default_theme'] = in_array( $input['default_theme'] ?? '', $allowed_themes, true )
? $input['default_theme']
: $defaults['default_theme'];
// Renderer colors — validate hex format.
$color_keys = array( 'trigger', 'action', 'logic', 'transform', 'database', 'ai' );
$clean['renderer_colors'] = array();
foreach ( $color_keys as $key ) {
$raw_color = isset( $input['renderer_colors'][ $key ] )
? sanitize_text_field( wp_unslash( $input['renderer_colors'][ $key ] ) )
: $defaults['renderer_colors'][ $key ];
$clean['renderer_colors'][ $key ] = preg_match( '/^#[0-9a-fA-F]{6}$/', $raw_color )
? $raw_color
: $defaults['renderer_colors'][ $key ];
}
return $clean;
}
/**
* Returns default settings.
*
* @return array
*/
public static function get_defaults(): array {
return array(
'default_zoom' => 100,
'autofit_threshold' => 30,
'max_code_lines' => 50,
'allow_download' => false,
'download_label' => __( 'Download JSON', 'breznflow' ),
'default_mode' => 'visual',
'show_infobox_default' => true,
'show_title_default' => true,
'schema_howto' => false,
'related_workflows' => true,
'view_counting' => true,
'allow_share' => true,
'allow_embed' => false,
'allow_get_json' => false,
'default_theme' => 'dark',
'renderer_colors' => array(
'trigger' => '#ff6b6b',
'action' => '#4ecdc4',
'logic' => '#f39c12',
'transform' => '#26c6da',
'database' => '#4479a1',
'ai' => '#7c3aed',
),
);
}
}

View file

@ -0,0 +1,188 @@
<?php
/**
* Theme management admin page.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use BreznFlow\Features\ThemeImporter;
use BreznFlow\Features\ThemeRegistry;
/**
* Handles theme import, deletion, and the themes admin page rendering.
*
* @since 1.0.0
*/
class ThemesPage {
/**
* Registers admin_init hooks for import and delete handling.
*
* @since 1.0.0
* @return void
*/
public function register(): void {
add_action( 'admin_init', array( $this, 'handle_import' ) );
add_action( 'admin_init', array( $this, 'handle_delete' ) );
}
/**
* Processes the theme import form submission.
* Runs early in admin_init so wp_safe_redirect() works before any output.
*/
public function handle_import(): void {
if ( ! isset( $_GET['page'] ) || 'breznflow-themes' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
if ( empty( $_POST['_wpnonce'] ) || empty( $_FILES['breznflow_theme_file'] ) ) {
return;
}
check_admin_referer( 'breznflow_theme_import' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Insufficient permissions.', 'breznflow' ) );
}
$file = $_FILES['breznflow_theme_file']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
// Verify file extension.
$filename = isset( $file['name'] ) ? sanitize_file_name( $file['name'] ) : '';
if ( ! str_ends_with( $filename, '.breznflow.json' ) && ! str_ends_with( $filename, '.json' ) ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow-themes',
'error' => 'invalid_file',
),
admin_url( 'admin.php' )
)
);
exit;
}
if ( ! isset( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow-themes',
'error' => 'upload_failed',
),
admin_url( 'admin.php' )
)
);
exit;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$raw = file_get_contents( $file['tmp_name'] );
if ( false === $raw ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow-themes',
'error' => 'read_failed',
),
admin_url( 'admin.php' )
)
);
exit;
}
$data = json_decode( $raw, true );
if ( ! is_array( $data ) ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow-themes',
'error' => 'invalid_json',
),
admin_url( 'admin.php' )
)
);
exit;
}
$result = ThemeImporter::import( $data );
if ( is_wp_error( $result ) ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow-themes',
'error' => 'validation',
'message' => rawurlencode( $result->get_error_message() ),
),
admin_url( 'admin.php' )
)
);
exit;
}
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow-themes',
'imported' => '1',
),
admin_url( 'admin.php' )
)
);
exit;
}
/**
* Processes a theme delete action.
*/
public function handle_delete(): void {
if ( ! isset( $_GET['page'] ) || 'breznflow-themes' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
if ( empty( $_GET['action'] ) || 'delete_theme' !== $_GET['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
$id = isset( $_GET['theme_id'] ) ? sanitize_key( $_GET['theme_id'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( '' === $id ) {
return;
}
check_admin_referer( 'breznflow_theme_delete_' . $id );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Insufficient permissions.', 'breznflow' ) );
}
ThemeImporter::delete( $id );
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow-themes',
'deleted' => '1',
),
admin_url( 'admin.php' )
)
);
exit;
}
/**
* Renders the themes management page.
*
* @since 1.0.0
* @return void
*/
public function render(): void {
require BREZNFLOW_DIR . 'includes/Admin/views/themes.php';
}
}

View file

@ -0,0 +1,420 @@
<?php
/**
* Three-step workflow import wizard.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use BreznFlow\Security\WorkflowValidator;
use BreznFlow\Security\WorkflowSanitizer;
use BreznFlow\Features\NodeCategorizer;
/**
* Handles the wizard steps, AJAX validation, URL fetching, and publish actions.
*
* @since 1.0.0
*/
class WizardPage {
/**
* Registers all wizard-related hooks.
*
* @since 1.0.0
* @return void
*/
public function register(): void {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'wp_ajax_breznflow_validate_json', array( $this, 'ajax_validate_json' ) );
add_action( 'wp_ajax_breznflow_fetch_url', array( $this, 'ajax_fetch_url' ) );
add_action( 'admin_post_breznflow_save_step1', array( $this, 'handle_step1' ) );
add_action( 'admin_post_breznflow_save_step2', array( $this, 'handle_step2' ) );
add_action( 'admin_post_breznflow_publish_workflow', array( $this, 'handle_publish' ) );
}
/**
* Enqueues admin CSS and JS for wizard pages.
*
* @since 1.0.0
* @param string $hook Current admin page hook suffix.
* @return void
*/
public function enqueue_assets( string $hook ): void {
if ( ! in_array( $hook, array( 'toplevel_page_breznflow', 'breznflow_page_breznflow-add' ), true ) ) {
return;
}
wp_enqueue_style( 'breznflow-admin', BREZNFLOW_URL . 'assets/admin.css', array(), BREZNFLOW_VERSION );
wp_enqueue_script( 'breznflow-admin', BREZNFLOW_URL . 'assets/admin.js', array(), BREZNFLOW_VERSION, true );
wp_localize_script(
'breznflow-admin',
'breznflowAdmin',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'breznflow_wizard' ),
'i18n' => array(
'validating' => __( 'Validating...', 'breznflow' ),
'valid' => __( 'Valid n8n workflow', 'breznflow' ),
'invalid' => __( 'Invalid workflow', 'breznflow' ),
'fetching' => __( 'Fetching URL...', 'breznflow' ),
'copyShortcode' => __( 'Copy', 'breznflow' ),
'copied' => __( 'Copied!', 'breznflow' ),
'validateJson' => __( 'Validate JSON', 'breznflow' ),
'fetch' => __( 'Fetch', 'breznflow' ),
'fetchFailed' => __( 'Fetch failed', 'breznflow' ),
'pasteFirst' => __( 'Please paste a workflow JSON first.', 'breznflow' ),
'nodes' => __( 'nodes', 'breznflow' ),
),
)
);
}
/**
* AJAX: Validate JSON without saving.
*/
public function ajax_validate_json(): void {
check_ajax_referer( 'breznflow_wizard', 'nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'breznflow' ) ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- verified above; sanitize_textarea_field() is not used because strip_tags() corrupts JSON (strips HTML inside string values); sanitization happens after json_decode() in WorkflowSanitizer
$raw = isset( $_POST['json'] ) ? trim( wp_unslash( (string) $_POST['json'] ) ) : '';
if ( '' === $raw ) {
wp_send_json_error( array( 'message' => __( 'No JSON provided.', 'breznflow' ) ) );
}
$result = WorkflowValidator::validate( $raw );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success(
array(
'name' => esc_html( $result['name'] ),
'nodes' => count( $result['nodes'] ),
)
);
}
/**
* AJAX: Fetch JSON from a URL.
*/
public function ajax_fetch_url(): void {
check_ajax_referer( 'breznflow_wizard', 'nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'breznflow' ) ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified via check_ajax_referer() above
$url = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '';
if ( '' === $url ) {
wp_send_json_error( array( 'message' => __( 'No URL provided.', 'breznflow' ) ) );
}
if ( ! $this->is_ssrf_safe_url( $url ) ) {
wp_send_json_error( array( 'message' => __( 'This URL is not allowed.', 'breznflow' ) ) );
}
$response = wp_remote_get(
$url,
array(
'timeout' => 15,
'user-agent' => 'BreznFlow/' . BREZNFLOW_VERSION . '; WordPress/' . get_bloginfo( 'version' ),
)
);
if ( is_wp_error( $response ) ) {
wp_send_json_error( array( 'message' => $response->get_error_message() ) );
}
$body = wp_remote_retrieve_body( $response );
if ( '' === $body ) {
wp_send_json_error( array( 'message' => __( 'Empty response from URL.', 'breznflow' ) ) );
}
wp_send_json_success( array( 'json' => $body ) );
}
/**
* Checks whether a URL is safe to fetch (no SSRF risk).
* Blocks loopback, private RFC-1918, and link-local (cloud metadata) ranges.
*
* @param string $url The URL to check.
* @return bool True if safe to fetch, false if blocked.
*/
private function is_ssrf_safe_url( string $url ): bool {
$host = wp_parse_url( $url, PHP_URL_HOST );
if ( ! $host || '' === $host ) {
return false;
}
// Strip IPv6 brackets: [::1] → ::1.
$host = trim( $host, '[]' );
$ip = gethostbyname( $host );
$blocked = array(
'/^127\./', // 127.x.x.x — Loopback.
'/^10\./', // 10.x.x.x — RFC 1918.
'/^192\.168\./', // 192.168.x.x — RFC 1918.
'/^172\.(1[6-9]|2[0-9]|3[01])\./', // 172.1631.x.x — RFC 1918.
'/^169\.254\./', // 169.254.x.x — Link-local / AWS metadata.
'/^0\./', // 0.x.x.x — Invalid.
'/^::1$/', // IPv6 loopback.
'/^fc[0-9a-f]{2}:/i', // IPv6 ULA fc::/7.
'/^fd[0-9a-f]{2}:/i', // IPv6 ULA fd::/8.
'/^fe80:/i', // IPv6 link-local.
);
foreach ( $blocked as $pattern ) {
if ( preg_match( $pattern, $ip ) ) {
return false;
}
}
return true;
}
/**
* POST handler: Step 1 validate, sanitize, create draft.
*/
public function handle_step1(): void {
check_admin_referer( 'breznflow_step1', 'breznflow_nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_die( esc_html__( 'Insufficient permissions.', 'breznflow' ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- verified above; sanitize_textarea_field() is not used because strip_tags() corrupts JSON (strips HTML inside string values); sanitization happens after json_decode() in WorkflowSanitizer
$raw = isset( $_POST['breznflow_json'] ) ? trim( wp_unslash( (string) $_POST['breznflow_json'] ) ) : '';
if ( '' === $raw ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow-add',
'error' => 'empty',
),
admin_url( 'admin.php' )
)
);
exit;
}
$validated = WorkflowValidator::validate( $raw );
if ( is_wp_error( $validated ) ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow-add',
'error' => 'validation',
'message' => rawurlencode( $validated->get_error_message() ),
),
admin_url( 'admin.php' )
)
);
exit;
}
// Sanitize + mask.
$sanitizer = new WorkflowSanitizer();
$processed = $sanitizer->process( $validated );
$data = $processed['data'];
$mask_log = $processed['mask_log'];
// Fallback for nodes whose name was stripped entirely by sanitization.
if ( isset( $data['nodes'] ) && is_array( $data['nodes'] ) ) {
foreach ( $data['nodes'] as &$node ) {
if ( isset( $node['name'] ) && '' === $node['name'] ) {
$node['name'] = isset( $node['type'] ) ? sanitize_text_field( $node['type'] ) : __( 'Unnamed Node', 'breznflow' );
}
}
unset( $node );
}
// Categorize nodes.
$categorized = NodeCategorizer::categorize( $data['nodes'] );
$node_summary = array();
foreach ( $categorized['counts'] as $label => $count ) {
$node_summary[ $label ] = $count;
}
// Create or update draft post.
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified via check_admin_referer() above
$post_id = isset( $_POST['breznflow_post_id'] ) ? (int) $_POST['breznflow_post_id'] : 0;
$settings = \BreznFlow\Admin\SettingsPage::get_defaults();
$saved = get_option( 'breznflow_settings', array() );
$settings = array_merge( $settings, $saved );
$post_data = array(
'post_type' => 'breznflow_workflow',
'post_status' => 'draft',
'post_title' => sanitize_text_field( $data['name'] ),
);
if ( $post_id > 0 ) {
$post_data['ID'] = $post_id;
$post_id = wp_update_post( $post_data );
} else {
$post_id = wp_insert_post( $post_data );
}
if ( is_wp_error( $post_id ) || 0 === $post_id ) {
wp_die( esc_html__( 'Failed to create workflow post.', 'breznflow' ) );
}
// Store sanitized JSON (never raw).
// wp_slash() counteracts update_metadata()'s internal wp_unslash() which would corrupt JSON backslashes.
update_post_meta( $post_id, '_breznflow_raw_json', wp_slash( wp_json_encode( $data ) ) );
update_post_meta( $post_id, '_breznflow_original_name', sanitize_text_field( $data['name'] ) );
update_post_meta( $post_id, '_breznflow_node_count', (int) $categorized['total'] );
update_post_meta( $post_id, '_breznflow_node_summary', wp_slash( wp_json_encode( $node_summary ) ) );
update_post_meta( $post_id, '_breznflow_has_ai_nodes', (int) $categorized['has_ai'] );
update_post_meta( $post_id, '_breznflow_ai_node_types', wp_slash( wp_json_encode( $categorized['ai_nodes'] ) ) );
update_post_meta( $post_id, '_breznflow_mask_log', wp_slash( wp_json_encode( $mask_log ) ) );
// Defaults.
update_post_meta( $post_id, '_breznflow_default_zoom', (int) $settings['default_zoom'] );
update_post_meta( $post_id, '_breznflow_show_title', (int) $settings['show_title_default'] );
update_post_meta( $post_id, '_breznflow_show_infobox', (int) $settings['show_infobox_default'] );
update_post_meta( $post_id, '_breznflow_show_download', (int) $settings['allow_download'] );
update_post_meta( $post_id, '_breznflow_show_embed', (int) $settings['allow_embed'] );
update_post_meta( $post_id, '_breznflow_show_minimap', 1 );
update_post_meta( $post_id, '_breznflow_default_mode', sanitize_text_field( $settings['default_mode'] ) );
update_post_meta( $post_id, '_breznflow_default_theme', $settings['default_theme'] ?? 'dark' );
// Invalidate related workflow caches.
delete_transient( 'breznflow_related_' . $post_id );
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow-add',
'step' => '2',
'post_id' => $post_id,
),
admin_url( 'admin.php' )
)
);
exit;
}
/**
* POST handler: Step 2 save settings, redirect to step 3.
*/
public function handle_step2(): void {
check_admin_referer( 'breznflow_step2', 'breznflow_nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_die( esc_html__( 'Insufficient permissions.', 'breznflow' ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified via check_admin_referer() above
$post_id = isset( $_POST['breznflow_post_id'] ) ? (int) $_POST['breznflow_post_id'] : 0;
if ( $post_id <= 0 || 'breznflow_workflow' !== get_post_type( $post_id ) ) {
wp_die( esc_html__( 'Invalid workflow ID.', 'breznflow' ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above
$title = isset( $_POST['post_title'] ) ? sanitize_text_field( wp_unslash( $_POST['post_title'] ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above
$mode = isset( $_POST['default_mode'] ) ? sanitize_text_field( wp_unslash( $_POST['default_mode'] ) ) : 'visual';
$mode = in_array( $mode, array( 'visual', 'info', 'compact' ), true ) ? $mode : 'visual';
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above
$zoom = isset( $_POST['default_zoom'] ) ? max( 10, min( 200, (int) $_POST['default_zoom'] ) ) : 100;
if ( $title ) {
wp_update_post(
array(
'ID' => $post_id,
'post_title' => $title,
)
);
}
update_post_meta( $post_id, '_breznflow_default_mode', $mode );
update_post_meta( $post_id, '_breznflow_default_zoom', $zoom );
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above
update_post_meta( $post_id, '_breznflow_show_title', ! empty( $_POST['show_title'] ) ? 1 : 0 );
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above
update_post_meta( $post_id, '_breznflow_show_infobox', ! empty( $_POST['show_infobox'] ) ? 1 : 0 );
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above
update_post_meta( $post_id, '_breznflow_show_download', ! empty( $_POST['show_download'] ) ? 1 : 0 );
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above
update_post_meta( $post_id, '_breznflow_show_embed', ! empty( $_POST['show_embed'] ) ? 1 : 0 );
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above
update_post_meta( $post_id, '_breznflow_show_minimap', ! empty( $_POST['show_minimap'] ) ? 1 : 0 );
$allowed_themes = \BreznFlow\Features\ThemeRegistry::get_theme_ids();
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above
$posted_theme = isset( $_POST['default_theme'] ) ? sanitize_text_field( wp_unslash( $_POST['default_theme'] ) ) : 'dark';
$theme_val = in_array( $posted_theme, $allowed_themes, true ) ? $posted_theme : 'dark';
update_post_meta( $post_id, '_breznflow_default_theme', $theme_val );
// Categories.
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above
if ( isset( $_POST['breznflow_categories'] ) && is_array( $_POST['breznflow_categories'] ) ) {
$cat_ids = array_map( 'intval', wp_unslash( $_POST['breznflow_categories'] ) );
wp_set_post_terms( $post_id, $cat_ids, 'breznflow_category' );
}
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow-add',
'step' => '3',
'post_id' => $post_id,
),
admin_url( 'admin.php' )
)
);
exit;
}
/**
* POST handler: Publish workflow.
*/
public function handle_publish(): void {
check_admin_referer( 'breznflow_publish', 'breznflow_nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_die( esc_html__( 'Insufficient permissions.', 'breznflow' ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above
$post_id = isset( $_POST['breznflow_post_id'] ) ? (int) $_POST['breznflow_post_id'] : 0;
if ( $post_id <= 0 || 'breznflow_workflow' !== get_post_type( $post_id ) ) {
wp_die( esc_html__( 'Invalid workflow ID.', 'breznflow' ) );
}
wp_update_post(
array(
'ID' => $post_id,
'post_status' => 'publish',
)
);
// Invalidate caches.
delete_transient( 'breznflow_related_' . $post_id );
delete_transient( 'breznflow_stats_summary' );
wp_safe_redirect(
add_query_arg(
array(
'page' => 'breznflow',
'published' => 1,
'post_id' => $post_id,
),
admin_url( 'admin.php' )
)
);
exit;
}
}

View file

@ -0,0 +1,204 @@
<?php
/**
* Workflow list table for the admin dashboard.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Admin;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
/**
* Extends WP_List_Table to display workflows in the admin.
*
* @since 1.0.0
*/
class WorkflowListTable extends \WP_List_Table {
/**
* Constructor sets table configuration.
*
* @since 1.0.0
*/
public function __construct() {
parent::__construct(
array(
'singular' => 'workflow',
'plural' => 'workflows',
'ajax' => false,
)
);
}
/**
* Returns the list of table columns.
*
* @return array<string, string>
*/
public function get_columns(): array {
return array(
'cb' => '<input type="checkbox" />',
'title' => __( 'Title', 'breznflow' ),
'nodes' => __( 'Nodes', 'breznflow' ),
'ai' => __( 'AI', 'breznflow' ),
'mode' => __( 'Mode', 'breznflow' ),
'views' => __( 'Views', 'breznflow' ),
'shortcode' => __( 'Shortcode', 'breznflow' ),
'date' => __( 'Date', 'breznflow' ),
);
}
/**
* Returns sortable columns configuration.
*
* @return array<string, array<int, mixed>>
*/
protected function get_sortable_columns(): array {
return array(
'title' => array( 'title', false ),
'nodes' => array( 'nodes', false ),
'views' => array( 'views', false ),
'date' => array( 'date', false ),
);
}
/**
* Returns the bulk action choices.
*
* @return array<string, string>
*/
protected function get_bulk_actions(): array {
return array(
'delete' => __( 'Delete', 'breznflow' ),
);
}
/**
* Queries workflows and sets up pagination.
*
* @since 1.0.0
* @return void
*/
public function prepare_items(): void {
$per_page = 20;
$current_page = $this->get_pagenum();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$orderby = isset( $_GET['orderby'] ) ? sanitize_key( $_GET['orderby'] ) : 'date';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order_raw = isset( $_GET['order'] ) ? strtolower( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ) : '';
$order = 'asc' === $order_raw ? 'ASC' : 'DESC';
$args = array(
'post_type' => 'breznflow_workflow',
'post_status' => array( 'publish', 'draft' ),
'posts_per_page' => $per_page,
'paged' => $current_page,
'orderby' => $orderby,
'order' => $order,
);
$query = new \WP_Query( $args );
$this->set_pagination_args(
array(
'total_items' => $query->found_posts,
'per_page' => $per_page,
)
);
$this->items = $query->posts;
$this->_column_headers = array(
$this->get_columns(),
array(),
$this->get_sortable_columns(),
);
}
/**
* Renders a non-special column value for a row.
*
* @param \WP_Post $item The current post object.
* @param string $column_name The current column name.
* @return string
*/
protected function column_default( $item, $column_name ): string {
switch ( $column_name ) {
case 'nodes':
return (string) (int) get_post_meta( $item->ID, '_breznflow_node_count', true );
case 'ai':
$has_ai = (int) get_post_meta( $item->ID, '_breznflow_has_ai_nodes', true );
return $has_ai ? '<span class="breznflow-badge-ai" title="' . esc_attr__( 'Contains AI nodes', 'breznflow' ) . '">AI</span>' : '—';
case 'mode':
$mode = get_post_meta( $item->ID, '_breznflow_default_mode', true );
return esc_html( $mode ? $mode : 'visual' );
case 'views':
return (string) (int) get_post_meta( $item->ID, '_breznflow_view_count', true );
case 'shortcode':
$sc = '[breznflow id="' . $item->ID . '"]';
return '<code>' . esc_html( $sc ) . '</code> '
. '<button class="button button-small breznflow-copy-sc" data-sc="' . esc_attr( $sc ) . '">'
. esc_html__( 'Copy', 'breznflow' )
. '</button>';
case 'date':
return esc_html( get_the_date( 'Y-m-d', $item ) );
default:
return '';
}
}
/**
* Renders the title column with edit/delete row actions.
*
* @param \WP_Post $item The current post object.
* @return string
*/
protected function column_title( $item ): string {
$edit_url = add_query_arg(
array(
'page' => 'breznflow-add',
'step' => '2',
'post_id' => $item->ID,
),
admin_url( 'admin.php' )
);
$delete_url = wp_nonce_url(
add_query_arg(
array(
'action' => 'delete',
'post' => $item->ID,
'page' => 'breznflow',
),
admin_url( 'admin.php' )
),
'breznflow_delete_' . $item->ID
);
$title = esc_html( $item->post_title );
$status = 'publish' === $item->post_status ? '' : ' <span class="post-state">(' . esc_html__( 'Draft', 'breznflow' ) . ')</span>';
$actions = array(
'edit' => '<a href="' . esc_url( $edit_url ) . '">' . esc_html__( 'Edit', 'breznflow' ) . '</a>',
'delete' => '<a href="' . esc_url( $delete_url ) . '" class="submitdelete">' . esc_html__( 'Delete', 'breznflow' ) . '</a>',
);
return '<strong><a href="' . esc_url( $edit_url ) . '">' . $title . '</a>' . $status . '</strong>'
. $this->row_actions( $actions );
}
/**
* Renders the checkbox column for bulk actions.
*
* @param \WP_Post $item The current post object.
* @return string
*/
protected function column_cb( $item ): string {
return '<input type="checkbox" name="workflow[]" value="' . esc_attr( (string) $item->ID ) . '" />';
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* Template: Workflow list table page.
*
* @package BreznFlow
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- template file, not global scope
/**
* The workflow list table instance passed from AdminMenu.
*
* @var \BreznFlow\Admin\WorkflowListTable $table
*/
?>
<div class="wrap breznflow-list-page">
<h1 class="wp-heading-inline"><?php esc_html_e( 'Workflows', 'breznflow' ); ?></h1>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=breznflow-add' ) ); ?>" class="page-title-action">
<?php esc_html_e( 'Add Workflow', 'breznflow' ); ?>
</a>
<?php if ( isset( $_GET['deleted'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
<div class="notice notice-success is-dismissible">
<p>
<?php
$deleted_count = absint( $_GET['deleted'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
printf(
/* translators: %d: number of deleted workflows */
esc_html( _n( '%d workflow moved to trash.', '%d workflows moved to trash.', $deleted_count, 'breznflow' ) ),
absint( $deleted_count )
);
?>
</p>
</div>
<?php endif; ?>
<?php if ( isset( $_GET['published'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
<div class="notice notice-success is-dismissible">
<p>
<?php
$pid = isset( $_GET['post_id'] ) ? (int) $_GET['post_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $pid > 0 ) {
$sc = '[breznflow id="' . $pid . '"]';
printf(
/* translators: %s: shortcode string */
esc_html__( 'Workflow published! Use shortcode: %s', 'breznflow' ),
'<code>' . esc_html( $sc ) . '</code>'
);
} else {
esc_html_e( 'Workflow published!', 'breznflow' );
}
?>
</p>
</div>
<?php endif; ?>
<form method="get">
<input type="hidden" name="page" value="breznflow" />
<?php $table->display(); ?>
</form>
</div>

View file

@ -0,0 +1,198 @@
<?php
/**
* Template: Plugin settings page.
*
* @package BreznFlow
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- template file, not global scope
$settings = \BreznFlow\Admin\SettingsPage::get_defaults();
$saved = get_option( 'breznflow_settings', array() );
$settings = array_merge( $settings, $saved );
?>
<div class="wrap breznflow-settings-page">
<h1><?php esc_html_e( 'BreznFlow Settings', 'breznflow' ); ?></h1>
<?php settings_errors( 'breznflow_settings' ); ?>
<form method="post" action="options.php">
<?php settings_fields( 'breznflow_settings_group' ); ?>
<div class="breznflow-card">
<h2><?php esc_html_e( 'Display Defaults', 'breznflow' ); ?></h2>
<table class="form-table">
<tr>
<th><label for="bf-default-mode"><?php esc_html_e( 'Default Mode', 'breznflow' ); ?></label></th>
<td>
<select name="breznflow_settings[default_mode]" id="bf-default-mode">
<option value="visual" <?php selected( 'visual', $settings['default_mode'] ); ?>>
<?php esc_html_e( 'Visual', 'breznflow' ); ?>
</option>
<option value="info" <?php selected( 'info', $settings['default_mode'] ); ?>>
<?php esc_html_e( 'Info', 'breznflow' ); ?>
</option>
<option value="compact" <?php selected( 'compact', $settings['default_mode'] ); ?>>
<?php esc_html_e( 'Compact', 'breznflow' ); ?>
</option>
</select>
</td>
</tr>
<tr>
<th><label for="bf-default-zoom"><?php esc_html_e( 'Default Zoom (%)', 'breznflow' ); ?></label></th>
<td>
<input type="number" name="breznflow_settings[default_zoom]" id="bf-default-zoom"
min="10" max="200" value="<?php echo esc_attr( (string) $settings['default_zoom'] ); ?>" class="small-text" />
</td>
</tr>
<tr>
<th><label for="bf-autofit-threshold"><?php esc_html_e( 'Large Workflow Threshold', 'breznflow' ); ?></label></th>
<td>
<input type="number" name="breznflow_settings[autofit_threshold]" id="bf-autofit-threshold"
min="0" max="500" value="<?php echo esc_attr( (string) $settings['autofit_threshold'] ); ?>" class="small-text" />
<p class="description"><?php esc_html_e( 'Workflows with this many nodes or more start zoomed in at the trigger node (0 = disabled).', 'breznflow' ); ?></p>
</td>
</tr>
<tr>
<th><label for="bf-max-code-lines"><?php esc_html_e( 'Max Code Lines', 'breznflow' ); ?></label></th>
<td>
<input type="number" name="breznflow_settings[max_code_lines]" id="bf-max-code-lines"
min="5" max="500" value="<?php echo esc_attr( (string) $settings['max_code_lines'] ); ?>" class="small-text" />
<p class="description"><?php esc_html_e( 'Maximum lines of code shown in node detail panel.', 'breznflow' ); ?></p>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Show by Default', 'breznflow' ); ?></th>
<td>
<label>
<input type="checkbox"
name="breznflow_settings[show_title_default]" value="1"
<?php checked( 1, (int) $settings['show_title_default'] ); ?> />
<?php esc_html_e( 'Show workflow title', 'breznflow' ); ?></label><br />
<label>
<input type="checkbox"
name="breznflow_settings[show_infobox_default]" value="1"
<?php checked( 1, (int) $settings['show_infobox_default'] ); ?> />
<?php esc_html_e( 'Show node info box', 'breznflow' ); ?></label>
</td>
</tr>
</table>
</div>
<div class="breznflow-card">
<h2><?php esc_html_e( 'Workflow Theme', 'breznflow' ); ?></h2>
<table class="form-table">
<tr>
<th><label for="bf-default-theme"><?php esc_html_e( 'Default Theme', 'breznflow' ); ?></label></th>
<td>
<select name="breznflow_settings[default_theme]" id="bf-default-theme">
<?php foreach ( \BreznFlow\Features\ThemeRegistry::discover() as $bf_theme_id => $bf_theme_meta ) : ?>
<option value="<?php echo esc_attr( $bf_theme_id ); ?>"
<?php selected( $bf_theme_id, $settings['default_theme'] ?? 'dark' ); ?>>
<?php echo esc_html( $bf_theme_meta['name'] ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description">
<?php esc_html_e( 'Can be overridden per workflow or via shortcode attr theme="light". Custom themes: import via BreznFlow → Themes.', 'breznflow' ); ?>
</p>
</td>
</tr>
</table>
</div>
<div class="breznflow-card">
<h2><?php esc_html_e( 'Action Bar', 'breznflow' ); ?></h2>
<table class="form-table">
<tr>
<th><?php esc_html_e( 'Share', 'breznflow' ); ?></th>
<td>
<label>
<input type="checkbox"
name="breznflow_settings[allow_share]" value="1"
<?php checked( 1, (int) $settings['allow_share'] ); ?> />
<?php esc_html_e( 'Visitors can share workflow links', 'breznflow' ); ?></label>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Embed', 'breznflow' ); ?></th>
<td>
<label>
<input type="checkbox"
name="breznflow_settings[allow_embed]" value="1"
<?php checked( 1, (int) $settings['allow_embed'] ); ?> />
<?php esc_html_e( 'Allow iframe embedding (creates public embed URL)', 'breznflow' ); ?></label>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Get JSON', 'breznflow' ); ?></th>
<td>
<label>
<input type="checkbox"
name="breznflow_settings[allow_get_json]" value="1"
<?php checked( 1, (int) $settings['allow_get_json'] ); ?> />
<?php esc_html_e( 'Visitors can view/copy workflow JSON', 'breznflow' ); ?></label>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Allow Download', 'breznflow' ); ?></th>
<td>
<label>
<input type="checkbox"
name="breznflow_settings[allow_download]" value="1"
<?php checked( 1, (int) $settings['allow_download'] ); ?> />
<?php esc_html_e( 'Allow visitors to download sanitized workflow JSON', 'breznflow' ); ?></label>
</td>
</tr>
<tr>
<th><label for="bf-download-label"><?php esc_html_e( 'Download Button Label', 'breznflow' ); ?></label></th>
<td>
<input type="text" name="breznflow_settings[download_label]" id="bf-download-label"
value="<?php echo esc_attr( $settings['download_label'] ); ?>" class="regular-text" />
</td>
</tr>
</table>
</div>
<div class="breznflow-card">
<h2><?php esc_html_e( 'Features', 'breznflow' ); ?></h2>
<table class="form-table">
<tr>
<th><?php esc_html_e( 'View Counting', 'breznflow' ); ?></th>
<td>
<label>
<input type="checkbox"
name="breznflow_settings[view_counting]" value="1"
<?php checked( 1, (int) $settings['view_counting'] ); ?> />
<?php esc_html_e( 'Track shortcode render count per workflow', 'breznflow' ); ?></label>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Related Workflows', 'breznflow' ); ?></th>
<td>
<label>
<input type="checkbox"
name="breznflow_settings[related_workflows]" value="1"
<?php checked( 1, (int) $settings['related_workflows'] ); ?> />
<?php esc_html_e( 'Enable related workflows by shared node types', 'breznflow' ); ?></label>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Schema.org HowTo', 'breznflow' ); ?></th>
<td>
<label>
<input type="checkbox"
name="breznflow_settings[schema_howto]" value="1"
<?php checked( 1, (int) $settings['schema_howto'] ); ?> />
<?php esc_html_e( 'Output Schema.org HowTo structured data in page head', 'breznflow' ); ?></label>
</td>
</tr>
</table>
</div>
<?php submit_button(); ?>
</form>
</div>

View file

@ -0,0 +1,148 @@
<?php
/**
* Template: Theme management page.
*
* @package BreznFlow
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use BreznFlow\Features\ThemeImporter;
use BreznFlow\Features\ThemeRegistry;
$custom_themes = ThemeImporter::get_all();
?>
<div class="wrap breznflow-admin-wrap">
<h1><?php esc_html_e( 'BreznFlow — Themes', 'breznflow' ); ?></h1>
<?php if ( isset( $_GET['imported'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
<div class="notice notice-success is-dismissible">
<p><?php esc_html_e( 'Theme imported successfully.', 'breznflow' ); ?></p>
</div>
<?php endif; ?>
<?php if ( isset( $_GET['deleted'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
<div class="notice notice-success is-dismissible">
<p><?php esc_html_e( 'Theme deleted.', 'breznflow' ); ?></p>
</div>
<?php endif; ?>
<?php if ( isset( $_GET['error'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
<?php
$error_code = sanitize_key( $_GET['error'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$messages = array(
'invalid_file' => __( 'Invalid file type. Please upload a .breznflow.json file.', 'breznflow' ),
'upload_failed' => __( 'File upload failed. Please try again.', 'breznflow' ),
'read_failed' => __( 'Could not read the uploaded file.', 'breznflow' ),
'invalid_json' => __( 'File is not valid JSON.', 'breznflow' ),
'validation' => isset( $_GET['message'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
? urldecode( sanitize_text_field( wp_unslash( $_GET['message'] ) ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
: __( 'Theme validation failed.', 'breznflow' ),
);
$msg = $messages[ $error_code ] ?? __( 'An error occurred.', 'breznflow' );
?>
<div class="notice notice-error is-dismissible">
<p><?php echo esc_html( $msg ); ?></p>
</div>
<?php endif; ?>
<!-- Import Section -->
<div class="breznflow-card">
<h2><?php esc_html_e( 'Import Custom Theme', 'breznflow' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Upload a .breznflow.json file containing a valid theme definition. The file must include all 41 color tokens.', 'breznflow' ); ?>
</p>
<form method="post" enctype="multipart/form-data" action="<?php echo esc_url( admin_url( 'admin.php?page=breznflow-themes' ) ); ?>">
<?php wp_nonce_field( 'breznflow_theme_import' ); ?>
<table class="form-table">
<tr>
<th><label for="breznflow_theme_file"><?php esc_html_e( 'Theme File', 'breznflow' ); ?></label></th>
<td>
<input type="file" name="breznflow_theme_file" id="breznflow_theme_file" accept=".json,.breznflow.json">
</td>
</tr>
</table>
<?php submit_button( __( 'Import Theme', 'breznflow' ) ); ?>
</form>
</div>
<!-- Built-in Themes -->
<div class="breznflow-card">
<h2><?php esc_html_e( 'Built-in Themes', 'breznflow' ); ?></h2>
<table class="wp-list-table widefat striped">
<thead>
<tr>
<th><?php esc_html_e( 'Name', 'breznflow' ); ?></th>
<th><?php esc_html_e( 'ID', 'breznflow' ); ?></th>
<th><?php esc_html_e( 'Type', 'breznflow' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( ThemeRegistry::BUILTIN as $bf_theme_id => $bf_theme_name ) : ?>
<tr>
<td><?php echo esc_html( $bf_theme_name ); ?></td>
<td><code><?php echo esc_html( $bf_theme_id ); ?></code></td>
<td><?php esc_html_e( 'Built-in', 'breznflow' ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="description">
<?php esc_html_e( 'Built-in themes are read-only and updated with the plugin.', 'breznflow' ); ?>
</p>
</div>
<!-- Custom Themes -->
<div class="breznflow-card">
<h2><?php esc_html_e( 'Custom Themes', 'breznflow' ); ?></h2>
<?php if ( empty( $custom_themes ) ) : ?>
<p class="description"><?php esc_html_e( 'No custom themes imported yet.', 'breznflow' ); ?></p>
<?php else : ?>
<table class="wp-list-table widefat striped">
<thead>
<tr>
<th><?php esc_html_e( 'Name', 'breznflow' ); ?></th>
<th><?php esc_html_e( 'ID', 'breznflow' ); ?></th>
<th><?php esc_html_e( 'Version', 'breznflow' ); ?></th>
<th><?php esc_html_e( 'Actions', 'breznflow' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $custom_themes as $theme ) : ?>
<tr>
<td><?php echo esc_html( $theme['name'] ); ?></td>
<td><code><?php echo esc_html( $theme['id'] ); ?></code></td>
<td><?php echo esc_html( (string) $theme['version'] ); ?></td>
<td>
<a href="
<?php
echo esc_url(
wp_nonce_url(
add_query_arg(
array(
'page' => 'breznflow-themes',
'action' => 'delete_theme',
'theme_id' => $theme['id'],
),
admin_url( 'admin.php' )
),
'breznflow_theme_delete_' . $theme['id']
)
);
?>
"
onclick="return confirm('<?php echo esc_js( __( 'Delete this theme?', 'breznflow' ) ); ?>');"
class="submitdelete">
<?php esc_html_e( 'Delete', 'breznflow' ); ?>
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>

View file

@ -0,0 +1,75 @@
<?php
/**
* Template: Wizard step 1 JSON import.
*
* @package BreznFlow
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- template file, not global scope
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$view_error = isset( $_GET['error'] ) ? sanitize_key( $_GET['error'] ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$view_message = isset( $_GET['message'] ) ? sanitize_text_field( wp_unslash( $_GET['message'] ) ) : '';
?>
<div class="wrap breznflow-wizard">
<h1 class="wp-heading-inline">
<?php esc_html_e( 'Add Workflow', 'breznflow' ); ?>
</h1>
<div class="breznflow-wizard-steps">
<span class="breznflow-step active"><?php esc_html_e( '1. Import JSON', 'breznflow' ); ?></span>
<span class="breznflow-step"><?php esc_html_e( '2. Configure', 'breznflow' ); ?></span>
<span class="breznflow-step"><?php esc_html_e( '3. Preview & Publish', 'breznflow' ); ?></span>
</div>
<?php if ( 'validation' === $view_error && $view_message ) : ?>
<div class="notice notice-error"><p><?php echo esc_html( urldecode( $view_message ) ); ?></p></div>
<?php elseif ( 'empty' === $view_error ) : ?>
<div class="notice notice-error"><p><?php esc_html_e( 'Please provide a workflow JSON.', 'breznflow' ); ?></p></div>
<?php endif; ?>
<div class="breznflow-card">
<h2><?php esc_html_e( 'Import n8n Workflow', 'breznflow' ); ?></h2>
<div class="breznflow-step1-import-url">
<label for="breznflow-url"><?php esc_html_e( 'Import from URL (optional)', 'breznflow' ); ?></label>
<div class="breznflow-url-row">
<input type="url" id="breznflow-url" class="regular-text" placeholder="https://example.com/workflow.json" />
<button type="button" class="button" id="breznflow-fetch-url"><?php esc_html_e( 'Fetch', 'breznflow' ); ?></button>
</div>
<p class="description"><?php esc_html_e( 'Or paste JSON directly below.', 'breznflow' ); ?></p>
</div>
<div class="breznflow-file-upload">
<label for="breznflow-file"><?php esc_html_e( 'Upload JSON file', 'breznflow' ); ?></label>
<input type="file" id="breznflow-file" accept=".json" class="regular-text" />
</div>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" id="breznflow-step1-form">
<input type="hidden" name="action" value="breznflow_save_step1" />
<?php wp_nonce_field( 'breznflow_step1', 'breznflow_nonce' ); ?>
<div class="breznflow-json-field">
<label for="breznflow-json"><?php esc_html_e( 'n8n Workflow JSON', 'breznflow' ); ?></label>
<textarea name="breznflow_json" id="breznflow-json" rows="16" class="large-text code" required
placeholder='{"name": "My Workflow", "nodes": [...], "connections": {...}}'></textarea>
</div>
<div id="breznflow-validation-result" hidden></div>
<p class="submit">
<button type="button" class="button button-secondary" id="breznflow-validate-btn">
<?php esc_html_e( 'Validate JSON', 'breznflow' ); ?>
</button>
<button type="submit" class="button button-primary" id="breznflow-step1-submit" disabled>
<?php esc_html_e( 'Continue to Settings', 'breznflow' ); ?>
</button>
</p>
</form>
</div>
</div>

View file

@ -0,0 +1,212 @@
<?php
/**
* Template: Wizard step 2 configure workflow display settings.
*
* @package BreznFlow
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- template file, not global scope
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.WP.GlobalVariablesOverride.Prohibited
$post_id = isset( $_GET['post_id'] ) ? (int) $_GET['post_id'] : 0;
$workflow = $post_id > 0 ? get_post( $post_id ) : null;
if ( ! $workflow || 'breznflow_workflow' !== $workflow->post_type ) {
wp_die( esc_html__( 'Invalid workflow.', 'breznflow' ) );
}
$meta_mode = get_post_meta( $post_id, '_breznflow_default_mode', true );
$display_mode = $meta_mode ? $meta_mode : 'visual';
$meta_zoom = (int) get_post_meta( $post_id, '_breznflow_default_zoom', true );
$zoom = $meta_zoom ? $meta_zoom : 100;
$show_title = (int) get_post_meta( $post_id, '_breznflow_show_title', true );
$show_infobox = (int) get_post_meta( $post_id, '_breznflow_show_infobox', true );
$show_download = (int) get_post_meta( $post_id, '_breznflow_show_download', true );
$show_embed = (int) get_post_meta( $post_id, '_breznflow_show_embed', true );
$saved_theme_raw = get_post_meta( $post_id, '_breznflow_default_theme', true );
$saved_theme = $saved_theme_raw ? $saved_theme_raw : 'dark';
$show_minimap = get_post_meta( $post_id, '_breznflow_show_minimap', true );
$show_minimap = '' === $show_minimap ? 1 : (int) $show_minimap;
$mask_log_raw = get_post_meta( $post_id, '_breznflow_mask_log', true );
$mask_log = json_decode( $mask_log_raw ? $mask_log_raw : '[]', true );
$has_ai = (int) get_post_meta( $post_id, '_breznflow_has_ai_nodes', true );
$node_count = (int) get_post_meta( $post_id, '_breznflow_node_count', true );
$categories = get_terms(
array(
'taxonomy' => 'breznflow_category',
'hide_empty' => false,
)
);
$current_cats = wp_get_post_terms( $post_id, 'breznflow_category', array( 'fields' => 'ids' ) );
?>
<div class="wrap breznflow-wizard">
<h1 class="wp-heading-inline">
<?php esc_html_e( 'Configure Workflow', 'breznflow' ); ?>
</h1>
<div class="breznflow-wizard-steps">
<span class="breznflow-step done"><?php esc_html_e( '1. Import JSON', 'breznflow' ); ?></span>
<span class="breznflow-step active"><?php esc_html_e( '2. Configure', 'breznflow' ); ?></span>
<span class="breznflow-step"><?php esc_html_e( '3. Preview & Publish', 'breznflow' ); ?></span>
</div>
<div class="breznflow-step2-meta">
<span>
<?php
/* translators: %d: number of workflow nodes */
printf( esc_html__( '%d nodes', 'breznflow' ), absint( $node_count ) );
?>
</span>
<?php if ( $has_ai ) : ?>
<span class="breznflow-badge-ai"><?php esc_html_e( 'AI-powered', 'breznflow' ); ?></span>
<?php endif; ?>
</div>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="breznflow_save_step2" />
<input type="hidden" name="breznflow_post_id" value="<?php echo esc_attr( (string) $post_id ); ?>" />
<?php wp_nonce_field( 'breznflow_step2', 'breznflow_nonce' ); ?>
<div class="breznflow-card">
<h2><?php esc_html_e( 'Basic Settings', 'breznflow' ); ?></h2>
<table class="form-table">
<tr>
<th><label for="post_title"><?php esc_html_e( 'Workflow Title', 'breznflow' ); ?></label></th>
<td>
<input type="text" name="post_title" id="post_title" class="regular-text"
value="<?php echo esc_attr( $workflow->post_title ); ?>" required />
</td>
</tr>
<?php if ( ! is_wp_error( $categories ) && ! empty( $categories ) ) : ?>
<tr>
<th><label><?php esc_html_e( 'Categories', 'breznflow' ); ?></label></th>
<td>
<select name="breznflow_categories[]" multiple class="breznflow-multiselect">
<?php foreach ( $categories as $bf_cat ) : ?>
<option value="<?php echo esc_attr( (string) $bf_cat->term_id ); ?>"
<?php selected( in_array( $bf_cat->term_id, (array) $current_cats, true ) ); ?>>
<?php echo esc_html( $bf_cat->name ); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<?php endif; ?>
<tr>
<th><label><?php esc_html_e( 'Display Mode', 'breznflow' ); ?></label></th>
<td>
<label><input type="radio" name="default_mode" value="visual" <?php checked( 'visual', $display_mode ); ?> />
<?php esc_html_e( 'Visual (diagram + infobox)', 'breznflow' ); ?></label><br />
<label><input type="radio" name="default_mode" value="info" <?php checked( 'info', $display_mode ); ?> />
<?php esc_html_e( 'Info only (infobox, no diagram)', 'breznflow' ); ?></label><br />
<label><input type="radio" name="default_mode" value="compact" <?php checked( 'compact', $display_mode ); ?> />
<?php esc_html_e( 'Compact (diagram only, no toolbar)', 'breznflow' ); ?></label>
</td>
</tr>
<tr>
<th><label for="default_zoom"><?php esc_html_e( 'Default Zoom', 'breznflow' ); ?></label></th>
<td>
<input type="range" name="default_zoom" id="default_zoom"
min="10" max="200" value="<?php echo esc_attr( (string) $zoom ); ?>"
oninput="document.getElementById('zoom-label').textContent=this.value+'%'" />
<span id="zoom-label"><?php echo esc_html( $zoom . '%' ); ?></span>
</td>
</tr>
<tr>
<th><?php esc_html_e( 'Display Options', 'breznflow' ); ?></th>
<td>
<label><input type="checkbox" name="show_title" <?php checked( 1, $show_title ); ?> />
<?php esc_html_e( 'Show title above diagram', 'breznflow' ); ?></label><br />
<label><input type="checkbox" name="show_infobox" <?php checked( 1, $show_infobox ); ?> />
<?php esc_html_e( 'Show node info box', 'breznflow' ); ?></label><br />
<label><input type="checkbox" name="show_download" <?php checked( 1, $show_download ); ?> />
<?php esc_html_e( 'Allow JSON download', 'breznflow' ); ?></label><br />
<label><input type="checkbox" name="show_embed" <?php checked( 1, $show_embed ); ?> />
<?php esc_html_e( 'Allow iframe embed', 'breznflow' ); ?></label><br />
<label><input type="checkbox" name="show_minimap" <?php checked( 1, $show_minimap ); ?> />
<?php esc_html_e( 'Show minimap button', 'breznflow' ); ?></label>
</td>
</tr>
<tr>
<th><label for="bf-wiz-theme"><?php esc_html_e( 'Viewer Theme', 'breznflow' ); ?></label></th>
<td>
<select name="default_theme" id="bf-wiz-theme">
<?php foreach ( \BreznFlow\Features\ThemeRegistry::discover() as $bf_theme_id => $bf_theme_meta ) : ?>
<option value="<?php echo esc_attr( $bf_theme_id ); ?>" <?php selected( $bf_theme_id, $saved_theme ); ?>>
<?php echo esc_html( $bf_theme_meta['name'] ); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
</table>
</div>
<div class="breznflow-card">
<h2><?php esc_html_e( 'Shortcode Preview', 'breznflow' ); ?></h2>
<div class="breznflow-shortcode-preview">
<code id="breznflow-shortcode-live">[breznflow id="<?php echo esc_html( (string) $post_id ); ?>"]</code>
<button type="button" class="button button-small breznflow-copy-sc"
data-sc='[breznflow id="<?php echo esc_attr( (string) $post_id ); ?>"]'>
<?php esc_html_e( 'Copy', 'breznflow' ); ?>
</button>
</div>
</div>
<?php if ( ! empty( $mask_log ) ) : ?>
<div class="breznflow-card">
<h2><?php esc_html_e( 'Security Masking Log', 'breznflow' ); ?></h2>
<details>
<summary>
<?php
printf(
/* translators: %d: number of masked items */
esc_html( _n( '%d item was sanitized', '%d items were sanitized', count( $mask_log ), 'breznflow' ) ),
(int) count( $mask_log )
);
?>
</summary>
<ul class="breznflow-mask-log">
<?php foreach ( $mask_log as $entry ) : ?>
<li>
<code><?php echo esc_html( $entry['key'] ?? '' ); ?></code>
<?php echo esc_html( $entry['note'] ?? '' ); ?>
<em>(<?php echo esc_html( $entry['reason'] ?? '' ); ?>)</em>
</li>
<?php endforeach; ?>
</ul>
</details>
</div>
<?php endif; ?>
<p class="submit">
<a href="
<?php
echo esc_url(
add_query_arg(
array(
'page' => 'breznflow-add',
'step' => '1',
),
admin_url( 'admin.php' )
)
);
?>
"
class="button button-secondary"><?php esc_html_e( '← Back', 'breznflow' ); ?></a>
<button type="submit" class="button button-primary">
<?php esc_html_e( 'Continue to Preview', 'breznflow' ); ?>
</button>
</p>
</form>
</div>

View file

@ -0,0 +1,179 @@
<?php
/**
* Template: Wizard step 3 preview and publish.
*
* @package BreznFlow
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- template file, not global scope
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.WP.GlobalVariablesOverride.Prohibited
$post_id = isset( $_GET['post_id'] ) ? (int) $_GET['post_id'] : 0;
$workflow = $post_id > 0 ? get_post( $post_id ) : null;
if ( ! $workflow || 'breznflow_workflow' !== $workflow->post_type ) {
wp_die( esc_html__( 'Invalid workflow.', 'breznflow' ) );
}
$raw_json = get_post_meta( $post_id, '_breznflow_raw_json', true );
$mask_log_raw = get_post_meta( $post_id, '_breznflow_mask_log', true );
$mask_log = json_decode( $mask_log_raw ? $mask_log_raw : '[]', true );
$node_count = (int) get_post_meta( $post_id, '_breznflow_node_count', true );
$has_ai = (int) get_post_meta( $post_id, '_breznflow_has_ai_nodes', true );
$meta_mode = get_post_meta( $post_id, '_breznflow_default_mode', true );
$display_mode = $meta_mode ? $meta_mode : 'visual';
$meta_zoom = (int) get_post_meta( $post_id, '_breznflow_default_zoom', true );
$zoom = $meta_zoom ? $meta_zoom : 100;
$show_infobox = (int) get_post_meta( $post_id, '_breznflow_show_infobox', true );
// Check for code nodes with jsCode.
$has_code_nodes = false;
if ( $raw_json ) {
$data = json_decode( $raw_json, true );
if ( is_array( $data ) && ! empty( $data['nodes'] ) ) {
foreach ( $data['nodes'] as $node ) {
if ( isset( $node['parameters']['jsCode'] ) ) {
$has_code_nodes = true;
break;
}
}
}
}
$settings = \BreznFlow\Admin\SettingsPage::get_defaults();
$saved = get_option( 'breznflow_settings', array() );
$settings = array_merge( $settings, $saved );
$icon_registry = \BreznFlow\Features\NodeTypeRegistry::get_registry();
$saved_theme_raw = get_post_meta( $post_id, '_breznflow_default_theme', true );
$saved_theme = $saved_theme_raw ? $saved_theme_raw : ( $settings['default_theme'] ?? 'dark' );
$allowed_themes = \BreznFlow\Features\ThemeRegistry::get_theme_ids();
$preview_theme = in_array( $saved_theme, $allowed_themes, true ) ? $saved_theme : 'dark';
?>
<div class="wrap breznflow-wizard">
<h1 class="wp-heading-inline">
<?php esc_html_e( 'Preview & Publish', 'breznflow' ); ?>
</h1>
<div class="breznflow-wizard-steps">
<span class="breznflow-step done"><?php esc_html_e( '1. Import JSON', 'breznflow' ); ?></span>
<span class="breznflow-step done"><?php esc_html_e( '2. Configure', 'breznflow' ); ?></span>
<span class="breznflow-step active"><?php esc_html_e( '3. Preview & Publish', 'breznflow' ); ?></span>
</div>
<?php if ( $has_code_nodes ) : ?>
<div class="notice notice-warning">
<p><strong><?php esc_html_e( 'Note:', 'breznflow' ); ?></strong>
<?php
esc_html_e( 'This workflow contains Code nodes. JavaScript code is displayed as-is and cannot be automatically scanned for hardcoded secrets. Please review the code in the node detail panel before publishing.', 'breznflow' ); // phpcs:ignore Generic.Files.LineLength.MaxExceeded
?>
</p>
</div>
<?php endif; ?>
<div class="breznflow-card">
<h2><?php esc_html_e( 'Workflow Preview', 'breznflow' ); ?></h2>
<p class="description">
<?php
printf(
/* translators: 1: node count, 2: workflow name */
esc_html__( 'Showing %1$d nodes from "%2$s".', 'breznflow' ),
absint( $node_count ),
esc_html( $workflow->post_title )
);
?>
</p>
<?php if ( $raw_json ) : ?>
<div class="breznflow-preview-container">
<?php
wp_enqueue_style( 'breznflow-renderer', BREZNFLOW_URL . 'assets/renderer.css', array(), BREZNFLOW_VERSION );
wp_enqueue_script( 'breznflow-renderer', BREZNFLOW_URL . 'assets/renderer.js', array(), BREZNFLOW_VERSION, true );
foreach ( \BreznFlow\Features\ThemeRegistry::BUILTIN as $bf_theme_id => $bf_theme_name ) {
wp_enqueue_style(
'breznflow-theme-' . $bf_theme_id,
\BreznFlow\Features\ThemeRegistry::get_builtin_url( $bf_theme_id ),
array( 'breznflow-renderer' ),
BREZNFLOW_VERSION
);
}
$bf_custom_css = \BreznFlow\Features\ThemeRegistry::get_custom_theme_css();
if ( $bf_custom_css ) {
wp_add_inline_style( 'breznflow-renderer', $bf_custom_css );
}
wp_localize_script(
'breznflow-renderer',
'breznflowData',
array(
array(
'id' => $post_id,
'workflow' => json_decode( $raw_json, true ),
'mode' => $display_mode,
'zoom' => $zoom,
'show_infobox' => $show_infobox,
'show_title' => 0,
'max_code_lines' => (int) $settings['max_code_lines'],
'theme' => $preview_theme,
),
)
);
wp_localize_script( 'breznflow-renderer', 'breznflowIcons', $icon_registry );
wp_localize_script( 'breznflow-renderer', 'breznflowI18n', \BreznFlow\Shortcode::get_js_i18n() );
?>
<div id="breznflow-wrap-<?php echo esc_attr( (string) $post_id ); ?>" class="breznflow-embed"
data-id="<?php echo esc_attr( (string) $post_id ); ?>">
</div>
</div>
<?php endif; ?>
</div>
<?php if ( ! empty( $mask_log ) ) : ?>
<div class="breznflow-card breznflow-card-security">
<h2><?php esc_html_e( 'Security Summary', 'breznflow' ); ?></h2>
<p>
<?php
printf(
/* translators: %d: number of masked items */
esc_html( _n( '%d value was masked for security.', '%d values were masked for security.', count( $mask_log ), 'breznflow' ) ),
(int) count( $mask_log )
);
?>
</p>
</div>
<?php endif; ?>
<div class="breznflow-card">
<h2><?php esc_html_e( 'Publish', 'breznflow' ); ?></h2>
<p><?php esc_html_e( 'Use the shortcode below in any post or page to embed this workflow.', 'breznflow' ); ?></p>
<p><code>[breznflow id="<?php echo esc_html( (string) $post_id ); ?>"]</code></p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="breznflow_publish_workflow" />
<input type="hidden" name="breznflow_post_id" value="<?php echo esc_attr( (string) $post_id ); ?>" />
<?php wp_nonce_field( 'breznflow_publish', 'breznflow_nonce' ); ?>
<p>
<a href="
<?php
echo esc_url(
add_query_arg(
array(
'page' => 'breznflow-add',
'step' => '2',
'post_id' => $post_id,
),
admin_url( 'admin.php' )
)
);
?>
"
class="button button-secondary"><?php esc_html_e( '← Edit Settings', 'breznflow' ); ?></a>
<button type="submit" class="button button-primary button-hero">
<?php esc_html_e( 'Publish Workflow', 'breznflow' ); ?>
</button>
</p>
</form>
</div>
</div>

113
includes/Core.php Normal file
View file

@ -0,0 +1,113 @@
<?php
/**
* Plugin core bootstrap class.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Singleton core class that loads dependencies and registers hooks.
*
* @since 1.0.0
*/
class Core {
/**
* Singleton instance.
*
* @var Core|null
*/
private static ?Core $instance = null;
/**
* Returns the singleton instance.
*
* @since 1.0.0
* @return self
*/
public static function instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Initializes the plugin by loading text domain, dependencies, and hooks.
*
* @since 1.0.0
* @return void
*/
public function init(): void {
load_plugin_textdomain( 'breznflow', false, dirname( plugin_basename( BREZNFLOW_FILE ) ) . '/languages' );
$this->load_dependencies();
$this->register_hooks();
}
/**
* Loads all required class files.
*
* @since 1.0.0
* @return void
*/
private function load_dependencies(): void {
// Security.
require_once BREZNFLOW_DIR . 'includes/Security/MaskingRules.php';
require_once BREZNFLOW_DIR . 'includes/Security/WorkflowValidator.php';
require_once BREZNFLOW_DIR . 'includes/Security/WorkflowSanitizer.php';
// Post type.
require_once BREZNFLOW_DIR . 'includes/PostType.php';
// Features.
require_once BREZNFLOW_DIR . 'includes/Features/NodeTypeRegistry.php';
require_once BREZNFLOW_DIR . 'includes/Features/NodeCategorizer.php';
require_once BREZNFLOW_DIR . 'includes/Features/InfoBoxBuilder.php';
require_once BREZNFLOW_DIR . 'includes/Features/ViewCounter.php';
require_once BREZNFLOW_DIR . 'includes/Features/RelatedWorkflows.php';
require_once BREZNFLOW_DIR . 'includes/Features/ThemeRegistry.php';
require_once BREZNFLOW_DIR . 'includes/Features/ThemeImporter.php';
// Shortcode, download & embed.
require_once BREZNFLOW_DIR . 'includes/Shortcode.php';
require_once BREZNFLOW_DIR . 'includes/DownloadHandler.php';
require_once BREZNFLOW_DIR . 'includes/EmbedHandler.php';
// SettingsPage is loaded always because Shortcode::render() calls SettingsPage::get_defaults() on the frontend.
require_once BREZNFLOW_DIR . 'includes/Admin/SettingsPage.php';
// Admin-only classes.
if ( is_admin() ) {
require_once BREZNFLOW_DIR . 'includes/Admin/AdminMenu.php';
require_once BREZNFLOW_DIR . 'includes/Admin/WizardPage.php';
require_once BREZNFLOW_DIR . 'includes/Admin/WorkflowListTable.php';
}
}
/**
* Instantiates components and registers their hooks.
*
* @since 1.0.0
* @return void
*/
private function register_hooks(): void {
( new PostType() )->register();
( new Shortcode() )->register();
( new DownloadHandler() )->register();
( new EmbedHandler() )->register();
( new Features\ViewCounter() )->register();
( new Features\RelatedWorkflows() )->register();
if ( is_admin() ) {
( new Admin\AdminMenu() )->register();
( new Admin\SettingsPage() )->register();
( new Admin\WizardPage() )->register();
}
}
}

View file

@ -0,0 +1,84 @@
<?php
/**
* Handles workflow JSON file downloads.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Serves sanitized workflow JSON as a downloadable file when the
* breznflow_download query parameter is present.
*
* @since 1.0.0
*/
class DownloadHandler {
/**
* Registers the template_redirect hook for download handling.
*
* @since 1.0.0
* @return void
*/
public function register(): void {
add_action( 'template_redirect', array( $this, 'handle_download' ) );
}
/**
* Processes the download request and serves the JSON file.
*
* @since 1.0.0
* @return void
*/
public function handle_download(): void {
if ( ! isset( $_GET['breznflow_download'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
$post_id = (int) $_GET['breznflow_download']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $post_id <= 0 ) {
status_header( 400 );
exit;
}
$post = get_post( $post_id );
if ( ! $post || 'breznflow_workflow' !== $post->post_type || 'publish' !== $post->post_status ) {
status_header( 404 );
exit;
}
// Check if download is allowed.
$meta_download = (bool) get_post_meta( $post_id, '_breznflow_show_download', true );
$global_settings = get_option( 'breznflow_settings', array() );
$global_download = ! empty( $global_settings['allow_download'] );
if ( ! $meta_download || ! $global_download ) {
status_header( 403 );
exit;
}
// Read the already-sanitized JSON (NEVER raw).
$json = get_post_meta( $post_id, '_breznflow_raw_json', true );
if ( ! $json ) {
status_header( 404 );
exit;
}
$filename = 'workflow-' . $post_id . '.json';
header( 'Content-Type: application/json; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . rawurlencode( $filename ) . '"' );
header( 'X-Content-Type-Options: nosniff' );
header( 'Cache-Control: no-store, no-cache, must-revalidate' );
header( 'Content-Length: ' . strlen( $json ) );
echo $json; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON output for download, not HTML
exit;
}
}

185
includes/EmbedHandler.php Normal file
View file

@ -0,0 +1,185 @@
<?php
/**
* Handles standalone embed page rendering for workflows.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Serves a standalone HTML page for embedding a workflow via iframe
* when the breznflow_embed query parameter is present.
*
* @since 1.0.0
*/
class EmbedHandler {
/**
* Registers the template_redirect hook for embed handling.
*
* @since 1.2.0
* @return void
*/
public function register(): void {
add_action( 'template_redirect', array( $this, 'handle_embed' ) );
}
/**
* Processes the embed request and outputs a standalone HTML page.
*
* @since 1.2.0
* @return void
*/
public function handle_embed(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only embed endpoint, no state change; only serves published data.
if ( ! isset( $_GET['breznflow_embed'] ) ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only embed endpoint.
$post_id = (int) $_GET['breznflow_embed'];
if ( $post_id <= 0 ) {
status_header( 400 );
exit;
}
$settings = array_merge( Admin\SettingsPage::get_defaults(), get_option( 'breznflow_settings', array() ) );
if ( empty( $settings['allow_embed'] ) ) {
status_header( 403 );
exit;
}
$post = get_post( $post_id );
if ( ! $post || 'breznflow_workflow' !== $post->post_type || 'publish' !== $post->post_status ) {
status_header( 404 );
exit;
}
$show_embed = (bool) get_post_meta( $post_id, '_breznflow_show_embed', true );
if ( ! $show_embed ) {
status_header( 403 );
exit;
}
$raw_json = get_post_meta( $post_id, '_breznflow_raw_json', true );
if ( ! $raw_json ) {
status_header( 404 );
exit;
}
$workflow = json_decode( $raw_json, true );
if ( ! is_array( $workflow ) ) {
status_header( 500 );
exit;
}
$allowed_themes = \BreznFlow\Features\ThemeRegistry::get_theme_ids();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$url_theme = isset( $_GET['theme'] ) ? sanitize_text_field( wp_unslash( $_GET['theme'] ) ) : '';
$theme = in_array( $url_theme, $allowed_themes, true ) ? $url_theme : ( $settings['default_theme'] ?? 'dark' );
$theme = in_array( $theme, $allowed_themes, true ) ? $theme : 'dark';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only embed page, no state change.
$show_minimap_embed = isset( $_GET['minimap'] ) ? ( '0' !== sanitize_text_field( wp_unslash( $_GET['minimap'] ) ) ) : true;
$body_bgs = array(
'dark' => '#1a1a2e',
'light' => '#eef2f7',
'minimal' => '#fafafa',
'tech' => '#0d1117',
'brezn' => '#001f4d',
);
$body_bg = $body_bgs[ $theme ] ?? '#1a1a2e';
// Set headers.
header( 'Content-Type: text/html; charset=utf-8' );
header( 'X-Robots-Tag: noindex, nofollow' );
header( 'X-Content-Type-Options: nosniff' );
header_remove( 'X-Frame-Options' );
$article_url = esc_url( get_permalink( $post_id ) );
$anchor_id = 'breznflow-' . $post_id;
$blog_name = esc_html( get_bloginfo( 'name' ) );
$blog_url = esc_url( home_url( '/' ) );
$title = esc_html( $post->post_title );
$css_url = esc_url( BREZNFLOW_URL . 'assets/renderer.css' ) . '?v=' . BREZNFLOW_VERSION;
$js_url = esc_url( BREZNFLOW_URL . 'assets/renderer.js' ) . '?v=' . BREZNFLOW_VERSION;
$inline_data = array(
array(
'id' => $post_id,
'workflow' => $workflow,
'mode' => 'visual',
'zoom' => 100,
'autofit_threshold' => (int) ( $settings['autofit_threshold'] ?? 30 ),
'show_title' => false,
'show_infobox' => false,
'show_download' => false,
'show_minimap' => $show_minimap_embed,
'show_share' => false,
'show_embed' => false,
'show_get_json' => false,
'max_code_lines' => (int) ( $settings['max_code_lines'] ?? 50 ),
'download_label' => '',
'download_url' => '',
'theme' => $theme,
),
);
$icons_json = wp_json_encode( Features\NodeTypeRegistry::get_registry() );
$data_json = wp_json_encode( $inline_data );
$i18n_json = wp_json_encode( Shortcode::get_js_i18n() );
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- intentional standalone HTML output; all dynamic values escaped above
?><!DOCTYPE html>
<html lang="<?php echo esc_attr( get_bloginfo( 'language' ) ); ?>" data-theme="<?php echo esc_attr( $theme ); ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title><?php echo $title; ?></title>
<link rel="stylesheet" href="<?php echo $css_url; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet -- standalone embed page, no wp_head(). ?>">
<?php foreach ( \BreznFlow\Features\ThemeRegistry::BUILTIN as $bf_embed_id => $bf_embed_name ) : ?>
<link rel="stylesheet" href="<?php echo esc_url( \BreznFlow\Features\ThemeRegistry::get_builtin_url( $bf_embed_id ) ) . '?v=' . BREZNFLOW_VERSION; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet -- standalone embed page, no wp_head(). ?>">
<?php endforeach; ?>
<?php $embed_custom_css = \BreznFlow\Features\ThemeRegistry::get_custom_theme_css(); if ( $embed_custom_css ) : ?>
<style><?php echo wp_strip_all_tags( $embed_custom_css ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSS from validated color tokens, stripped of HTML tags. ?></style>
<?php endif; ?>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; background: <?php echo esc_attr( $body_bg ); ?>; color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
body { display: flex; flex-direction: column; }
#breznflow-embed-viewer { flex: 1; min-height: 0; }
#breznflow-embed-viewer .breznflow-embed { height: 100%; border-radius: 0; border: none; }
#breznflow-embed-footer { padding: 6px 12px; background: #111; border-top: 1px solid #333; font-size: 11px; color: #888; display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
#breznflow-embed-footer a { color: #aaa; text-decoration: none; }
#breznflow-embed-footer a:hover { color: #e0e0e0; }
</style>
</head>
<body>
<div id="breznflow-embed-viewer">
<div id="breznflow-wrap-<?php echo (int) $post_id; ?>" class="breznflow-embed" data-id="<?php echo (int) $post_id; ?>"></div>
</div>
<footer id="breznflow-embed-footer">
<a href="<?php echo $article_url; ?>#<?php echo esc_attr( $anchor_id ); ?>"><?php echo $title; ?></a>
<span>&bull;</span>
<span><?php esc_html_e( 'Source:', 'breznflow' ); ?> <a href="<?php echo $blog_url; ?>"><?php echo $blog_name; ?></a></span>
</footer>
<script>
var breznflowData = <?php echo $data_json; ?>;
var breznflowIcons = <?php echo $icons_json; ?>;
var breznflowI18n = <?php echo $i18n_json; ?>;
</script>
<script src="<?php echo $js_url; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript -- standalone embed page, no wp_head(). ?>"></script>
</body>
</html>
<?php
// phpcs:enable
exit;
}
}

View file

@ -0,0 +1,87 @@
<?php
/**
* Builds the node summary InfoBox HTML.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Features;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Builds the HTML info box summarizing workflow node statistics.
*
* @since 1.0.0
*/
class InfoBoxBuilder {
/**
* Builds the InfoBox HTML for display in frontend.
*
* @param array $categorized Result from NodeCategorizer::categorize().
* @return string Safe HTML string.
*/
public static function build( array $categorized ): string {
$counts = $categorized['counts'] ?? array();
$has_ai = $categorized['has_ai'] ?? false;
$total = $categorized['total'] ?? 0;
$display = array();
$more = 0;
$i = 0;
foreach ( $counts as $label => $count ) {
if ( $i < 6 ) {
$display[] = array(
'label' => $label,
'count' => $count,
);
} else {
++$more;
}
++$i;
}
$html = '<div class="breznflow-infobox">';
$html .= '<div class="breznflow-infobox-nodes">';
foreach ( $display as $item ) {
$html .= sprintf(
'<span class="breznflow-infobox-node"><strong>%dx</strong> %s</span>',
(int) $item['count'],
esc_html( $item['label'] )
);
}
if ( $more > 0 ) {
$html .= sprintf(
'<span class="breznflow-infobox-more">+ %s %s</span>',
(int) $more,
/* translators: %d: number of additional node types */
esc_html( _n( 'more type', 'more types', $more, 'breznflow' ) )
);
}
$html .= '</div>';
if ( $has_ai ) {
$html .= '<div class="breznflow-infobox-ai">';
$html .= '<span class="breznflow-ai-badge">' . esc_html__( 'AI-powered', 'breznflow' ) . '</span>';
$html .= '</div>';
}
$html .= sprintf(
'<div class="breznflow-infobox-total">%s</div>',
sprintf(
/* translators: %d: total node count */
esc_html( _n( '%d node', '%d nodes', $total, 'breznflow' ) ),
(int) $total
)
);
$html .= '</div>';
return $html;
}
}

View file

@ -0,0 +1,166 @@
<?php
/**
* Categorizes workflow nodes by type and detects AI nodes.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Features;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Categorizes n8n workflow nodes by type (trigger, action, AI, etc.).
*
* @since 1.0.0
*/
class NodeCategorizer {
/** Keywords indicating AI-related nodes. */
const AI_KEYWORDS = array(
'openai',
'anthropic',
'claude',
'gemini',
'googleai',
'huggingface',
'langchain',
'agent',
'vectorstore',
'gpt',
'ollama',
'mistral',
'cohere',
'lmchat',
'chainllm',
'memorybuffer',
);
/**
* Categorizes nodes and computes counts.
*
* @param array $nodes Array of node objects from workflow data.
* @return array{counts: array, by_category: array, ai_nodes: array, has_ai: bool, total: int}
*/
public static function categorize( array $nodes ): array {
$counts = array();
$by_category = array();
$ai_nodes = array();
foreach ( $nodes as $node ) {
$type = isset( $node['type'] ) ? (string) $node['type'] : '';
$slug = NodeTypeRegistry::extract_slug( $type );
$entry = NodeTypeRegistry::lookup( $type );
$label = $entry['label'] ?? $slug;
// Count by display label.
if ( ! isset( $counts[ $label ] ) ) {
$counts[ $label ] = 0;
}
++$counts[ $label ];
// Check for AI nodes.
$slug_lower = strtolower( $slug );
foreach ( self::AI_KEYWORDS as $keyword ) {
if ( str_contains( $slug_lower, $keyword ) ) {
if ( ! in_array( $type, $ai_nodes, true ) ) {
$ai_nodes[] = $type;
}
break;
}
}
// Group by category.
$category = self::get_category( $slug );
if ( ! isset( $by_category[ $category ] ) ) {
$by_category[ $category ] = array();
}
$by_category[ $category ][] = $label;
}
arsort( $counts );
return array(
'counts' => $counts,
'by_category' => $by_category,
'ai_nodes' => $ai_nodes,
'has_ai' => ! empty( $ai_nodes ),
'total' => count( $nodes ),
);
}
/**
* Determines the category for a node slug.
*
* @since 1.0.0
* @param string $slug Node type slug.
* @return string Category name.
*/
private static function get_category( string $slug ): string {
$slug_lower = strtolower( $slug );
// AI check first.
foreach ( self::AI_KEYWORDS as $keyword ) {
if ( str_contains( $slug_lower, $keyword ) ) {
return 'ai';
}
}
$trigger_slugs = array( 'scheduletrigger', 'webhook', 'manualtrigger', 'formtrigger', 'emailreadimap', 'rssfeadread' );
foreach ( $trigger_slugs as $t ) {
if ( str_contains( $slug_lower, 'trigger' ) || str_contains( $slug_lower, 'webhook' ) ) {
return 'trigger';
}
}
$logic_slugs = array(
'if',
'switch',
'filter',
'merge',
'splitinbatches',
'splitout',
'sort',
'limit',
'removeduplicates',
'aggregate',
'comparedatasets',
);
foreach ( $logic_slugs as $l ) {
if ( $slug_lower === $l ) {
return 'logic';
}
}
$code_slugs = array(
'code',
'function',
'executeworkflow',
'set',
'editfields',
'html',
'xml',
'markdown',
'crypto',
'tofile',
'converttofile',
'extractfromfile',
);
foreach ( $code_slugs as $c ) {
if ( $slug_lower === $c ) {
return 'transform';
}
}
$db_slugs = array( 'mysql', 'postgres', 'redis', 'mongodb', 'sqlite', 'microsoftsql', 'supabase' );
foreach ( $db_slugs as $d ) {
if ( $slug_lower === $d ) {
return 'database';
}
}
return 'action';
}
}

View file

@ -0,0 +1,813 @@
<?php
/**
* Registry of known n8n node types with display metadata.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Features;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Registry of known n8n node types with their display metadata.
*
* @since 1.0.0
*/
class NodeTypeRegistry {
/**
* Returns the full node type registry.
* Keys are n8n node slugs (part after last dot in type string).
*
* @return array<string, array{label: string, icon: string, symbol?: string, color: string, bg: string}>
*/
public static function get_registry(): array {
return array(
// Triggers.
'scheduleTrigger' => array(
'label' => 'Schedule Trigger',
'icon' => 'symbol',
'symbol' => '⏰',
'color' => '#31C48D',
'bg' => '#1a3a2e',
),
'webhook' => array(
'label' => 'Webhook',
'icon' => 'symbol',
'symbol' => '⇆',
'color' => '#5277D4',
'bg' => '#1a2040',
),
'manualTrigger' => array(
'label' => 'Manual Trigger',
'icon' => 'symbol',
'symbol' => '▶',
'color' => '#FF6B35',
'bg' => '#3a1a00',
),
'formTrigger' => array(
'label' => 'Form Trigger',
'icon' => 'symbol',
'symbol' => '▤',
'color' => '#9B59B6',
'bg' => '#2a1040',
),
'emailReadImap' => array(
'label' => 'Email (IMAP)',
'icon' => 'symbol',
'symbol' => '✉',
'color' => '#E74C3C',
'bg' => '#3a1010',
),
'respondToWebhook' => array(
'label' => 'Respond to Webhook',
'icon' => 'symbol',
'symbol' => '↩',
'color' => '#5277D4',
'bg' => '#1a2040',
),
// Core logic.
'code' => array(
'label' => 'Code',
'icon' => 'symbol',
'symbol' => '{ }',
'color' => '#FF9500',
'bg' => '#3a2000',
),
'function' => array(
'label' => 'Function',
'icon' => 'symbol',
'symbol' => 'ƒ',
'color' => '#FF9500',
'bg' => '#3a2000',
),
'executeWorkflow' => array(
'label' => 'Execute Workflow',
'icon' => 'symbol',
'symbol' => '↺',
'color' => '#7F8C8D',
'bg' => '#2d2d2d',
),
'httpRequest' => array(
'label' => 'HTTP Request',
'icon' => 'symbol',
'symbol' => '⊕',
'color' => '#5277D4',
'bg' => '#1a2040',
),
'wait' => array(
'label' => 'Wait',
'icon' => 'symbol',
'symbol' => 'II',
'color' => '#9B59B6',
'bg' => '#2a1040',
),
'if' => array(
'label' => 'If',
'icon' => 'symbol',
'symbol' => '?',
'color' => '#F39C12',
'bg' => '#3a2800',
),
'switch' => array(
'label' => 'Switch',
'icon' => 'symbol',
'symbol' => '⋈',
'color' => '#F39C12',
'bg' => '#3a2800',
),
'merge' => array(
'label' => 'Merge',
'icon' => 'symbol',
'symbol' => '⊕',
'color' => '#26C6DA',
'bg' => '#003040',
),
'splitInBatches' => array(
'label' => 'Split in Batches',
'icon' => 'symbol',
'symbol' => '⋮',
'color' => '#95A5A6',
'bg' => '#2d2d2d',
),
'set' => array(
'label' => 'Set',
'icon' => 'symbol',
'symbol' => '=',
'color' => '#26C6DA',
'bg' => '#003040',
),
'editFields' => array(
'label' => 'Edit Fields',
'icon' => 'symbol',
'symbol' => '=',
'color' => '#26C6DA',
'bg' => '#003040',
),
'filter' => array(
'label' => 'Filter',
'icon' => 'symbol',
'symbol' => '▽',
'color' => '#95A5A6',
'bg' => '#2d2d2d',
),
'aggregate' => array(
'label' => 'Aggregate',
'icon' => 'symbol',
'symbol' => '∑',
'color' => '#26C6DA',
'bg' => '#003040',
),
'splitOut' => array(
'label' => 'Split Out',
'icon' => 'symbol',
'symbol' => '↔',
'color' => '#95A5A6',
'bg' => '#2d2d2d',
),
'sort' => array(
'label' => 'Sort',
'icon' => 'symbol',
'symbol' => '↕',
'color' => '#95A5A6',
'bg' => '#2d2d2d',
),
'limit' => array(
'label' => 'Limit',
'icon' => 'symbol',
'symbol' => '⊐',
'color' => '#95A5A6',
'bg' => '#2d2d2d',
),
'removeDuplicates' => array(
'label' => 'Remove Duplicates',
'icon' => 'symbol',
'symbol' => '≠',
'color' => '#95A5A6',
'bg' => '#2d2d2d',
),
'itemLists' => array(
'label' => 'Item Lists',
'icon' => 'symbol',
'symbol' => '≡',
'color' => '#95A5A6',
'bg' => '#2d2d2d',
),
'compareDatasets' => array(
'label' => 'Compare Datasets',
'icon' => 'symbol',
'symbol' => '△',
'color' => '#26C6DA',
'bg' => '#003040',
),
'noOp' => array(
'label' => 'No Operation',
'icon' => 'symbol',
'symbol' => '∅',
'color' => '#BDC3C7',
'bg' => '#2d2d2d',
),
'stickyNote' => array(
'label' => 'Sticky Note',
'icon' => 'symbol',
'symbol' => '📌',
'color' => '#F1C40F',
'bg' => '#3a3000',
),
'stopAndError' => array(
'label' => 'Stop and Error',
'icon' => 'symbol',
'symbol' => '✕',
'color' => '#E74C3C',
'bg' => '#3a1010',
),
// Data transformation.
'toFile' => array(
'label' => 'To File',
'icon' => 'symbol',
'symbol' => '↓',
'color' => '#95A5A6',
'bg' => '#2d2d2d',
),
'convertToFile' => array(
'label' => 'Convert to File',
'icon' => 'symbol',
'symbol' => '↓',
'color' => '#95A5A6',
'bg' => '#2d2d2d',
),
'html' => array(
'label' => 'HTML',
'icon' => 'symbol',
'symbol' => '</>',
'color' => '#E34F26',
'bg' => '#3a1000',
),
'xml' => array(
'label' => 'XML',
'icon' => 'symbol',
'symbol' => '</>',
'color' => '#95A5A6',
'bg' => '#2d2d2d',
),
'markdown' => array(
'label' => 'Markdown',
'icon' => 'symbol',
'symbol' => 'M↓',
'color' => '#083FA1',
'bg' => '#001040',
),
'crypto' => array(
'label' => 'Crypto',
'icon' => 'symbol',
'symbol' => '🔒',
'color' => '#F39C12',
'bg' => '#3a2800',
),
'compression' => array(
'label' => 'Compression',
'icon' => 'symbol',
'symbol' => '⇩',
'color' => '#95A5A6',
'bg' => '#2d2d2d',
),
'extractFromFile' => array(
'label' => 'Extract from File',
'icon' => 'symbol',
'symbol' => '↑',
'color' => '#95A5A6',
'bg' => '#2d2d2d',
),
// Databases.
'mySql' => array(
'label' => 'MySQL',
'icon' => 'initials',
'symbol' => 'My',
'color' => '#4479A1',
'bg' => '#001530',
),
'postgres' => array(
'label' => 'PostgreSQL',
'icon' => 'initials',
'symbol' => 'PG',
'color' => '#336791',
'bg' => '#001030',
),
'redis' => array(
'label' => 'Redis',
'icon' => 'initials',
'symbol' => 'Re',
'color' => '#DC382D',
'bg' => '#3a1010',
),
'mongodb' => array(
'label' => 'MongoDB',
'icon' => 'initials',
'symbol' => 'Mg',
'color' => '#4DB33D',
'bg' => '#103010',
),
'sqlite' => array(
'label' => 'SQLite',
'icon' => 'initials',
'symbol' => 'SL',
'color' => '#003B57',
'bg' => '#001020',
),
'microsoftSql' => array(
'label' => 'MS SQL',
'icon' => 'initials',
'symbol' => 'MS',
'color' => '#CC2927',
'bg' => '#3a1010',
),
'supabase' => array(
'label' => 'Supabase',
'icon' => 'initials',
'symbol' => 'Sb',
'color' => '#3ECF8E',
'bg' => '#103020',
),
// Communication.
'slack' => array(
'label' => 'Slack',
'icon' => 'initials',
'symbol' => 'Sl',
'color' => '#4A154B',
'bg' => '#200020',
),
'telegram' => array(
'label' => 'Telegram',
'icon' => 'symbol',
'symbol' => '✈',
'color' => '#2CA5E0',
'bg' => '#003040',
),
'discord' => array(
'label' => 'Discord',
'icon' => 'initials',
'symbol' => 'Dc',
'color' => '#5865F2',
'bg' => '#101080',
),
'mattermost' => array(
'label' => 'Mattermost',
'icon' => 'initials',
'symbol' => 'Mm',
'color' => '#0058CC',
'bg' => '#001840',
),
'emailSend' => array(
'label' => 'Send Email',
'icon' => 'symbol',
'symbol' => '✉',
'color' => '#E74C3C',
'bg' => '#3a1010',
),
'sendEmail' => array(
'label' => 'Send Email',
'icon' => 'symbol',
'symbol' => '✉',
'color' => '#E74C3C',
'bg' => '#3a1010',
),
'gmail' => array(
'label' => 'Gmail',
'icon' => 'initials',
'symbol' => 'Gm',
'color' => '#EA4335',
'bg' => '#3a1010',
),
'microsoftOutlook' => array(
'label' => 'Outlook',
'icon' => 'initials',
'symbol' => 'Ol',
'color' => '#0078D4',
'bg' => '#001840',
),
'whatsapp' => array(
'label' => 'WhatsApp',
'icon' => 'initials',
'symbol' => 'WA',
'color' => '#25D366',
'bg' => '#103020',
),
'twilio' => array(
'label' => 'Twilio',
'icon' => 'initials',
'symbol' => 'Tw',
'color' => '#F22F46',
'bg' => '#3a1010',
),
// Google.
'googleSheets' => array(
'label' => 'Google Sheets',
'icon' => 'initials',
'symbol' => 'GS',
'color' => '#34A853',
'bg' => '#103020',
),
'googleDrive' => array(
'label' => 'Google Drive',
'icon' => 'initials',
'symbol' => 'GD',
'color' => '#4285F4',
'bg' => '#001840',
),
'googleCalendar' => array(
'label' => 'Google Calendar',
'icon' => 'initials',
'symbol' => 'GC',
'color' => '#4285F4',
'bg' => '#001840',
),
'googleDocs' => array(
'label' => 'Google Docs',
'icon' => 'initials',
'symbol' => 'Gd',
'color' => '#4285F4',
'bg' => '#001840',
),
'youTube' => array(
'label' => 'YouTube',
'icon' => 'initials',
'symbol' => 'YT',
'color' => '#FF0000',
'bg' => '#3a0000',
),
// Dev / Version control.
'github' => array(
'label' => 'GitHub',
'icon' => 'initials',
'symbol' => 'GH',
'color' => '#24292E',
'bg' => '#000000',
),
'gitlab' => array(
'label' => 'GitLab',
'icon' => 'initials',
'symbol' => 'GL',
'color' => '#FC6D26',
'bg' => '#3a1a00',
),
'jira' => array(
'label' => 'Jira',
'icon' => 'initials',
'symbol' => 'Ji',
'color' => '#0052CC',
'bg' => '#001840',
),
'confluence' => array(
'label' => 'Confluence',
'icon' => 'initials',
'symbol' => 'Cf',
'color' => '#0052CC',
'bg' => '#001840',
),
'linear' => array(
'label' => 'Linear',
'icon' => 'initials',
'symbol' => 'Li',
'color' => '#5E6AD2',
'bg' => '#101040',
),
'notion' => array(
'label' => 'Notion',
'icon' => 'initials',
'symbol' => 'No',
'color' => '#FFFFFF',
'bg' => '#1a1a1a',
),
// AI.
'openAi' => array(
'label' => 'OpenAI',
'icon' => 'initials',
'symbol' => 'OA',
'color' => '#00A67E',
'bg' => '#003020',
),
'anthropicClaude' => array(
'label' => 'Anthropic',
'icon' => 'initials',
'symbol' => 'An',
'color' => '#D97757',
'bg' => '#3a1a00',
),
'anthropic' => array(
'label' => 'Anthropic',
'icon' => 'initials',
'symbol' => 'An',
'color' => '#D97757',
'bg' => '#3a1a00',
),
'gemini' => array(
'label' => 'Gemini',
'icon' => 'initials',
'symbol' => 'Gm',
'color' => '#4285F4',
'bg' => '#001840',
),
'googleAi' => array(
'label' => 'Google AI',
'icon' => 'initials',
'symbol' => 'GA',
'color' => '#4285F4',
'bg' => '#001840',
),
'huggingFace' => array(
'label' => 'HuggingFace',
'icon' => 'initials',
'symbol' => 'HF',
'color' => '#FFD21E',
'bg' => '#3a2a00',
),
'mistral' => array(
'label' => 'Mistral',
'icon' => 'initials',
'symbol' => 'Mi',
'color' => '#FF7000',
'bg' => '#3a1a00',
),
'ollama' => array(
'label' => 'Ollama',
'icon' => 'initials',
'symbol' => 'Ol',
'color' => '#7C3AED',
'bg' => '#200040',
),
'lmChatOllama' => array(
'label' => 'Ollama Chat',
'icon' => 'initials',
'symbol' => 'Ol',
'color' => '#7C3AED',
'bg' => '#200040',
),
'cohere' => array(
'label' => 'Cohere',
'icon' => 'initials',
'symbol' => 'Co',
'color' => '#39594D',
'bg' => '#102020',
),
'lmChatOpenAi' => array(
'label' => 'OpenAI Chat',
'icon' => 'initials',
'symbol' => 'OA',
'color' => '#00A67E',
'bg' => '#003020',
),
'lmChatAnthropic' => array(
'label' => 'Anthropic Chat',
'icon' => 'initials',
'symbol' => 'An',
'color' => '#D97757',
'bg' => '#3a1a00',
),
'agent' => array(
'label' => 'AI Agent',
'icon' => 'symbol',
'symbol' => '🤖',
'color' => '#7C3AED',
'bg' => '#200040',
),
'chainLlm' => array(
'label' => 'LLM Chain',
'icon' => 'symbol',
'symbol' => '⛓',
'color' => '#7C3AED',
'bg' => '#200040',
),
'vectorStore' => array(
'label' => 'Vector Store',
'icon' => 'symbol',
'symbol' => '⊞',
'color' => '#7C3AED',
'bg' => '#200040',
),
'memoryBufferWindow' => array(
'label' => 'Memory Buffer',
'icon' => 'symbol',
'symbol' => '◫',
'color' => '#7C3AED',
'bg' => '#200040',
),
// Storage / Files.
'ftp' => array(
'label' => 'FTP',
'icon' => 'initials',
'symbol' => 'FT',
'color' => '#F39C12',
'bg' => '#3a2800',
),
'ssh' => array(
'label' => 'SSH',
'icon' => 'symbol',
'symbol' => '$_',
'color' => '#2ECC71',
'bg' => '#103020',
),
'readWriteFile' => array(
'label' => 'Read/Write Files',
'icon' => 'symbol',
'symbol' => '📁',
'color' => '#F39C12',
'bg' => '#3a2800',
),
'airtable' => array(
'label' => 'Airtable',
'icon' => 'initials',
'symbol' => 'At',
'color' => '#18BFFF',
'bg' => '#003040',
),
'baserow' => array(
'label' => 'Baserow',
'icon' => 'initials',
'symbol' => 'Br',
'color' => '#4A90D9',
'bg' => '#001840',
),
// CRM / Marketing.
'hubspot' => array(
'label' => 'HubSpot',
'icon' => 'initials',
'symbol' => 'Hs',
'color' => '#FF7A59',
'bg' => '#3a1a00',
),
'salesforce' => array(
'label' => 'Salesforce',
'icon' => 'initials',
'symbol' => 'Sf',
'color' => '#00A1E0',
'bg' => '#003040',
),
'mailchimp' => array(
'label' => 'Mailchimp',
'icon' => 'initials',
'symbol' => 'Mc',
'color' => '#FFE01B',
'bg' => '#3a3000',
),
'brevo' => array(
'label' => 'Brevo',
'icon' => 'initials',
'symbol' => 'Bv',
'color' => '#0092FF',
'bg' => '#003040',
),
// WordPress / CMS.
'wordpress' => array(
'label' => 'WordPress',
'icon' => 'initials',
'symbol' => 'WP',
'color' => '#21759B',
'bg' => '#002030',
),
'rssFeedRead' => array(
'label' => 'RSS Feed',
'icon' => 'symbol',
'symbol' => '◉',
'color' => '#F26522',
'bg' => '#3a1000',
),
);
}
/**
* Looks up a node by its full n8n type string.
* Extracts the slug (last segment after dot) for registry lookup.
*
* @param string $type Full n8n type string e.g. "n8n-nodes-base.httpRequest".
* @return array Node type data or fallback entry.
*/
public static function lookup( string $type ): array {
$registry = self::get_registry();
$slug = self::extract_slug( $type );
// Case-sensitive exact match first.
if ( isset( $registry[ $slug ] ) ) {
return $registry[ $slug ];
}
// Case-insensitive fallback.
$slug_lower = strtolower( $slug );
foreach ( $registry as $key => $entry ) {
if ( strtolower( $key ) === $slug_lower ) {
return $entry;
}
}
// Generate fallback entry.
return self::generate_fallback( $type, $slug );
}
/**
* Extracts the node slug from a full n8n type string.
*
* @param string $type e.g. "n8n-nodes-base.httpRequest" "httpRequest".
*/
public static function extract_slug( string $type ): string {
$parts = explode( '.', $type );
return end( $parts );
}
/**
* Generates a deterministic fallback entry for unknown node types.
* Uses djb2 hash for a consistent color.
*
* @since 1.0.0
* @param string $type Full n8n node type string.
* @param string $slug Extracted slug portion.
* @return array Fallback display metadata.
*/
private static function generate_fallback( string $type, string $slug ): array {
$hue = self::djb2_color_hue( $type );
$hex = self::hsl_to_hex( $hue, 60, 55 );
$bg = self::hsl_to_hex( $hue, 40, 15 );
// Derive 2-letter initials from slug.
$initials = self::derive_initials( $slug );
return array(
'label' => $slug,
'icon' => 'initials',
'symbol' => $initials,
'color' => $hex,
'bg' => $bg,
);
}
/**
* Derives 2-letter initials from a camelCase or PascalCase slug.
*
* @since 1.0.0
* @param string $slug Node type slug.
* @return string Two uppercase initials.
*/
private static function derive_initials( string $slug ): string {
// Split on camelCase boundaries.
$parts = preg_split( '/(?=[A-Z])/', $slug, -1, PREG_SPLIT_NO_EMPTY );
if ( ! $parts ) {
return strtoupper( substr( $slug, 0, 2 ) );
}
if ( count( $parts ) >= 2 ) {
return strtoupper( substr( $parts[0], 0, 1 ) . substr( $parts[1], 0, 1 ) );
}
return strtoupper( substr( $slug, 0, 2 ) );
}
/**
* Computes a djb2 hash and maps it to a color hue in the 0-359 range.
*
* @since 1.0.0
* @param string $str Input string to hash.
* @return int Hue value between 0 and 359.
*/
private static function djb2_color_hue( string $str ): int {
$hash = 5381;
$len = strlen( $str );
for ( $i = 0; $i < $len; $i++ ) {
$hash = ( ( $hash << 5 ) + $hash ) + ord( $str[ $i ] );
$hash &= 0xFFFFFFFF;
}
return abs( $hash ) % 360;
}
/**
* Converts HSL to 6-digit hex color string.
*
* @since 1.0.0
* @param int $h Hue (0-359).
* @param int $s Saturation (0-100).
* @param int $l Lightness (0-100).
* @return string Hex color string (e.g. "#ff8800").
*/
private static function hsl_to_hex( int $h, int $s, int $l ): string {
$s /= 100;
$l /= 100;
$a = $s * min( $l, 1 - $l );
$f = static function ( $n ) use ( $h, $l, $a ) {
$k = fmod( $n + $h / 30, 12 );
return $l - $a * max( min( $k - 3, 9 - $k, 1 ), -1 );
};
$r = (int) round( $f( 0 ) * 255 );
$g = (int) round( $f( 8 ) * 255 );
$b = (int) round( $f( 4 ) * 255 );
return sprintf( '#%02x%02x%02x', max( 0, min( 255, $r ) ), max( 0, min( 255, $g ) ), max( 0, min( 255, $b ) ) );
}
}

View file

@ -0,0 +1,106 @@
<?php
/**
* Finds related workflows by shared node types.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Features;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Finds and displays workflows related by shared node types.
*
* @since 1.0.0
*/
class RelatedWorkflows {
/**
* Registers related workflows hooks.
*
* @since 1.0.0
* @return void
*/
public function register(): void {
// Can be extended to add hooks for related workflow display.
}
/**
* Gets related workflows by shared node types.
* Results are cached in a transient for 1 hour.
*
* @param int $post_id Workflow post ID.
* @param int $limit Max number of related workflows to return.
* @return array Array of WP_Post objects.
*/
public static function get( int $post_id, int $limit = 5 ): array {
$settings = get_option( 'breznflow_settings', array() );
if ( isset( $settings['related_workflows'] ) && ! $settings['related_workflows'] ) {
return array();
}
$cache_key = 'breznflow_related_' . $post_id;
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return $cached;
}
$summary = get_post_meta( $post_id, '_breznflow_node_summary', true );
if ( ! $summary ) {
return array();
}
$node_types = array_keys( (array) json_decode( $summary, true ) );
if ( empty( $node_types ) ) {
return array();
}
// Search other workflows with overlapping node types.
$related = get_posts(
array(
'post_type' => 'breznflow_workflow',
'post_status' => 'publish',
'posts_per_page' => $limit,
'post__not_in' => array( $post_id ), // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => '_breznflow_node_summary',
'compare' => 'EXISTS',
),
),
)
);
// Filter by shared node types.
$scored = array();
foreach ( $related as $post ) {
$other_summary = get_post_meta( $post->ID, '_breznflow_node_summary', true );
if ( ! $other_summary ) {
continue;
}
$other_types = array_keys( (array) json_decode( $other_summary, true ) );
$shared = count( array_intersect( $node_types, $other_types ) );
if ( $shared > 0 ) {
$scored[] = array(
'post' => $post,
'shared' => $shared,
);
}
}
usort(
$scored,
static function ( $a, $b ) {
return $b['shared'] - $a['shared'];
}
);
$result = array_column( array_slice( $scored, 0, $limit ), 'post' );
set_transient( $cache_key, $result, HOUR_IN_SECONDS );
return $result;
}
}

View file

@ -0,0 +1,265 @@
<?php
/**
* Validates and stores custom theme definitions.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Features;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles import, validation and storage of custom theme JSON files.
*
* @since 1.0.0
*/
class ThemeImporter {
/**
* All 41 allowed token names. Any key outside this list is rejected during import.
*
* @var string[]
*/
const ALLOWED_TOKENS = array(
'canvas_bg',
'node_bg',
'node_text',
'node_sub',
'node_border',
'connection',
'connection_hover',
'toolbar_bg',
'toolbar_text',
'toolbar_border',
'panel_bg',
'panel_text',
'panel_border',
'btn_bg',
'btn_text',
'btn_border',
'btn_hover_bg',
'action_bar_bg',
'action_bar_border',
'modal_overlay_bg',
'modal_bg',
'modal_border',
'modal_title',
'modal_text',
'modal_sub',
'modal_close',
'modal_secondary_bg',
'modal_secondary_border',
'modal_code_bg',
'tooltip_bg',
'tooltip_text',
'fullscreen_overlay_bg',
'minimap_bg',
'minimap_border',
'color_trigger',
'color_http',
'color_code',
'color_logic',
'color_database',
'color_ai',
'color_fallback',
);
/** IDs reserved for built-in themes — custom themes must not use these. */
const BUILTIN_IDS = array( 'dark', 'light', 'minimal', 'tech', 'brezn' );
/**
* Validates a decoded theme array.
*
* @param array $data Decoded JSON as PHP array.
* @return true|\WP_Error
*/
public static function validate( array $data ) {
// Required top-level fields.
foreach ( array( 'name', 'id', 'version', 'tokens' ) as $field ) {
if ( ! isset( $data[ $field ] ) ) {
return new \WP_Error(
'missing_field',
/* translators: %s: field name */
sprintf( __( 'Missing required field: %s', 'breznflow' ), $field )
);
}
}
// No extra top-level fields allowed.
$extra = array_diff( array_keys( $data ), array( 'name', 'id', 'version', 'tokens' ) );
if ( ! empty( $extra ) ) {
return new \WP_Error(
'extra_fields',
/* translators: %s: comma-separated field names */
sprintf( __( 'Unexpected fields: %s', 'breznflow' ), implode( ', ', $extra ) )
);
}
// id: must sanitize cleanly and not collide with built-ins.
$id = sanitize_key( (string) $data['id'] );
if ( '' === $id || $id !== (string) $data['id'] ) {
return new \WP_Error(
'invalid_id',
__( 'Theme ID must contain only lowercase letters, numbers, and hyphens.', 'breznflow' )
);
}
if ( in_array( $id, self::BUILTIN_IDS, true ) ) {
return new \WP_Error(
'reserved_id',
/* translators: %s: theme ID */
sprintf( __( 'Theme ID "%s" is reserved for built-in themes.', 'breznflow' ), $id )
);
}
// name: non-empty string, max 80 chars.
$name = sanitize_text_field( (string) $data['name'] );
if ( '' === $name ) {
return new \WP_Error( 'invalid_name', __( 'Theme name must not be empty.', 'breznflow' ) );
}
if ( mb_strlen( $name ) > 80 ) {
return new \WP_Error( 'invalid_name', __( 'Theme name must be 80 characters or fewer.', 'breznflow' ) );
}
// version: must be an integer.
if ( ! is_int( $data['version'] ) && ! ctype_digit( (string) $data['version'] ) ) {
return new \WP_Error( 'invalid_version', __( 'Theme version must be an integer.', 'breznflow' ) );
}
// tokens: must be an array.
if ( ! is_array( $data['tokens'] ) ) {
return new \WP_Error( 'invalid_tokens', __( 'Tokens must be an object.', 'breznflow' ) );
}
// Tokens: no extra keys.
$token_keys = array_keys( $data['tokens'] );
$extra_tokens = array_diff( $token_keys, self::ALLOWED_TOKENS );
if ( ! empty( $extra_tokens ) ) {
return new \WP_Error(
'unknown_tokens',
/* translators: %s: comma-separated token names */
sprintf( __( 'Unknown tokens: %s', 'breznflow' ), implode( ', ', $extra_tokens ) )
);
}
// Tokens: all 41 must be present.
$missing = array_diff( self::ALLOWED_TOKENS, $token_keys );
if ( ! empty( $missing ) ) {
return new \WP_Error(
'missing_tokens',
/* translators: %s: comma-separated token names */
sprintf( __( 'Missing tokens: %s', 'breznflow' ), implode( ', ', $missing ) )
);
}
// Each token value must be a safe color expression.
foreach ( $data['tokens'] as $token => $value ) {
if ( ! self::validate_color_value( (string) $value ) ) {
return new \WP_Error(
'invalid_color',
/* translators: 1: token name, 2: value */
sprintf( __( 'Invalid color value for "%1$s": %2$s', 'breznflow' ), $token, $value )
);
}
}
return true;
}
/**
* Validates and stores a theme in wp_options.
* If a theme with the same ID already exists it is replaced.
*
* @param array $data Decoded JSON as PHP array.
* @return true|\WP_Error
*/
public static function import( array $data ) {
$result = self::validate( $data );
if ( is_wp_error( $result ) ) {
return $result;
}
$themes = get_option( 'breznflow_custom_themes', array() );
// Remove existing entry with the same ID.
foreach ( $themes as $i => $t ) {
if ( sanitize_key( $data['id'] ) === $t['id'] ) {
array_splice( $themes, $i, 1 );
break;
}
}
$themes[] = array(
'id' => sanitize_key( $data['id'] ),
'name' => sanitize_text_field( $data['name'] ),
'version' => (int) $data['version'],
'tokens' => $data['tokens'],
);
update_option( 'breznflow_custom_themes', $themes );
return true;
}
/**
* Deletes a custom theme by ID.
*
* @param string $id Theme ID.
* @return bool True if deleted, false if not found.
*/
public static function delete( string $id ): bool {
$id = sanitize_key( $id );
$themes = get_option( 'breznflow_custom_themes', array() );
$new = array_values( array_filter( $themes, fn( $t ) => $t['id'] !== $id ) );
if ( count( $new ) === count( $themes ) ) {
return false;
}
update_option( 'breznflow_custom_themes', $new );
return true;
}
/**
* Returns all stored custom themes.
*
* @return array[]
*/
public static function get_all(): array {
return get_option( 'breznflow_custom_themes', array() );
}
/**
* Validates a single CSS color value.
* Accepts: #rgb, #rrggbb, #rrggbbaa, rgb(...), rgba(...)
* Rejects: url(...), @import, semicolons, curly braces, etc.
*
* @param string $value Raw token value.
* @return bool
*/
private static function validate_color_value( string $value ): bool {
$value = trim( $value );
// Hex: #rgb, #rrggbb, #rrggbbaa.
if ( preg_match( '/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3}([0-9a-fA-F]{2})?)?$/', $value ) ) {
return true;
}
// rgb(r, g, b).
if ( preg_match( '/^rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)$/', $value ) ) {
return true;
}
// rgba(r, g, b, a) — alpha must be 0..1 decimal.
if ( preg_match(
'/^rgba\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*(0(\.\d+)?|1(\.0+)?)\s*\)$/',
$value
) ) {
return true;
}
return false;
}
}

View file

@ -0,0 +1,106 @@
<?php
/**
* Registry of built-in and custom viewer themes.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Features;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Manages built-in and custom visual themes for the workflow renderer.
*
* @since 1.0.0
*/
class ThemeRegistry {
/**
* Built-in theme IDs and display names.
* IDs are used as CSS data-theme attribute values and as stylesheet handles.
*/
const BUILTIN = array(
'dark' => 'Dark',
'light' => 'Light',
'minimal' => 'Minimal',
'tech' => 'Tech',
'brezn' => 'Brezn',
);
/**
* Returns all available themes (built-in + custom) as:
* [ 'id' => ['name' => string, 'custom' => bool], ... ]
*
* @return array<string, array{name: string, custom: bool}>
*/
public static function discover(): array {
$themes = array();
foreach ( self::BUILTIN as $id => $name ) {
$themes[ $id ] = array(
'name' => $name,
'custom' => false,
);
}
$customs = get_option( 'breznflow_custom_themes', array() );
foreach ( $customs as $theme ) {
$themes[ $theme['id'] ] = array(
'name' => $theme['name'],
'custom' => true,
);
}
return $themes;
}
/**
* Returns all theme IDs as a flat array.
*
* @return string[]
*/
public static function get_theme_ids(): array {
return array_keys( self::discover() );
}
/**
* Returns the URL to a built-in theme CSS file.
*
* @param string $id Built-in theme ID.
* @return string
*/
public static function get_builtin_url( string $id ): string {
return BREZNFLOW_URL . 'assets/themes/' . sanitize_key( $id ) . '.css';
}
/**
* Generates a CSS block for all custom themes, suitable for wp_add_inline_style().
* Tokens are mapped: token_name --breznflow-token-name
*
* @return string CSS string (empty if no custom themes stored).
*/
public static function get_custom_theme_css(): string {
$themes = get_option( 'breznflow_custom_themes', array() );
$css = '';
foreach ( $themes as $theme ) {
$id = $theme['id'];
$sel = '.breznflow-wrap[data-theme="' . $id . '"],'
. '.breznflow-modal-overlay[data-theme="' . $id . '"],'
. '.breznflow-fs-portal[data-theme="' . $id . '"]';
$css .= $sel . '{';
foreach ( $theme['tokens'] as $token => $value ) {
$var = '--breznflow-' . str_replace( '_', '-', $token );
$css .= $var . ':' . $value . ';';
}
$css .= '}';
}
return $css;
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* Tracks shortcode render counts per workflow.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Features;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Tracks and retrieves view counts for workflow posts.
*
* @since 1.0.0
*/
class ViewCounter {
/**
* Registers the view counter hooks.
*
* @since 1.0.0
* @return void
*/
public function register(): void {
// View counting is triggered from Shortcode::render() directly.
// This class provides the static increment/get methods.
}
/**
* Increments the view count for a workflow post.
*
* @param int $post_id Workflow post ID.
*/
public static function increment( int $post_id ): void {
$settings = get_option( 'breznflow_settings', array() );
if ( isset( $settings['view_counting'] ) && ! $settings['view_counting'] ) {
return;
}
$current = (int) get_post_meta( $post_id, '_breznflow_view_count', true );
update_post_meta( $post_id, '_breznflow_view_count', $current + 1 );
}
/**
* Gets the view count for a workflow post.
*
* @param int $post_id Workflow post ID.
* @return int View count.
*/
public static function get( int $post_id ): int {
return (int) get_post_meta( $post_id, '_breznflow_view_count', true );
}
}

102
includes/PostType.php Normal file
View file

@ -0,0 +1,102 @@
<?php
/**
* Custom post type and taxonomy registration.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Registers the breznflow_workflow post type and breznflow_category taxonomy.
*
* @since 1.0.0
*/
class PostType {
/**
* Hooks post type and taxonomy registration into WordPress init.
*
* @since 1.0.0
* @return void
*/
public function register(): void {
add_action( 'init', array( $this, 'register_post_type' ) );
add_action( 'init', array( $this, 'register_taxonomy' ) );
}
/**
* Registers the breznflow_workflow custom post type.
*
* @since 1.0.0
* @return void
*/
public function register_post_type(): void {
register_post_type(
'breznflow_workflow',
array(
'labels' => array(
'name' => __( 'Workflows', 'breznflow' ),
'singular_name' => __( 'Workflow', 'breznflow' ),
'add_new' => __( 'Add Workflow', 'breznflow' ),
'add_new_item' => __( 'Add New Workflow', 'breznflow' ),
'edit_item' => __( 'Edit Workflow', 'breznflow' ),
'new_item' => __( 'New Workflow', 'breznflow' ),
'view_item' => __( 'View Workflow', 'breznflow' ),
'search_items' => __( 'Search Workflows', 'breznflow' ),
'not_found' => __( 'No workflows found', 'breznflow' ),
'not_found_in_trash' => __( 'No workflows found in trash', 'breznflow' ),
'menu_name' => __( 'BreznFlow', 'breznflow' ),
),
'public' => false,
'show_ui' => true,
'show_in_menu' => false,
'show_in_rest' => false,
'capability_type' => 'post',
'has_archive' => false,
'rewrite' => false,
'supports' => array( 'title', 'author' ),
'taxonomies' => array( 'breznflow_category' ),
)
);
}
/**
* Registers the breznflow_category taxonomy.
*
* @since 1.0.0
* @return void
*/
public function register_taxonomy(): void {
register_taxonomy(
'breznflow_category',
'breznflow_workflow',
array(
'labels' => array(
'name' => __( 'Workflow Categories', 'breznflow' ),
'singular_name' => __( 'Workflow Category', 'breznflow' ),
'search_items' => __( 'Search Categories', 'breznflow' ),
'all_items' => __( 'All Categories', 'breznflow' ),
'parent_item' => __( 'Parent Category', 'breznflow' ),
'parent_item_colon' => __( 'Parent Category:', 'breznflow' ),
'edit_item' => __( 'Edit Category', 'breznflow' ),
'update_item' => __( 'Update Category', 'breznflow' ),
'add_new_item' => __( 'Add New Category', 'breznflow' ),
'new_item_name' => __( 'New Category Name', 'breznflow' ),
'menu_name' => __( 'Categories', 'breznflow' ),
),
'hierarchical' => true,
'public' => false,
'show_ui' => true,
'show_in_menu' => false,
'show_in_rest' => false,
'show_admin_column' => true,
'rewrite' => false,
)
);
}
}

View file

@ -0,0 +1,174 @@
<?php
/**
* Masking rules for sensitive data in workflow parameters.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Security;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Provides rules for masking sensitive values in workflow data.
*
* @since 1.0.0
*/
class MaskingRules {
/** URL query param pattern for sensitive keys. */
const URL_PARAM_PATTERN = '/([?&](?:api[_-]?key|apikey|token|secret|password|access_token|auth|private_key|client_secret)=)[^&\s#]+/i';
/** Safe-list values that should never be masked (condition rightValue). */
const SAFE_CONDITION_VALUES = array(
'true',
'false',
'null',
'undefined',
'0',
'1',
'yes',
'no',
'success',
'error',
'active',
'inactive',
'enabled',
'disabled',
'pending',
'complete',
'completed',
'failed',
);
/**
* Applies all masking rules to a string value.
*
* @param string $value Raw string to inspect.
* @param string $field_key The parameter key name (for context-aware masking).
* @param array $log Passed by reference masked items appended here.
* @return string Possibly masked value.
*/
public static function apply( string $value, string $field_key, array &$log ): string {
$value = self::mask_url_params( $value, $log );
$value = self::mask_sensitive_field( $value, $field_key, $log );
return $value;
}
/**
* Masks sensitive URL query parameters.
*
* @since 1.0.0
* @param string $value Raw string to inspect.
* @param array $log Passed by reference masked items appended here.
* @return string Possibly masked value.
*/
private static function mask_url_params( string $value, array &$log ): string {
if ( ! str_contains( $value, '=' ) && ! str_contains( $value, '?' ) ) {
return $value;
}
$masked = preg_replace_callback(
self::URL_PARAM_PATTERN,
function ( $matches ) use ( &$log ) {
$log[] = array(
'reason' => 'url_param',
'key' => $matches[0],
'note' => 'Sensitive URL parameter value replaced.',
);
return $matches[1] . '[REDACTED]';
},
$value
);
return null !== $masked ? $masked : $value;
}
/**
* Masks values of fields with inherently sensitive names.
*
* @since 1.0.0
* @param string $value Raw string to inspect.
* @param string $field_key The parameter key name.
* @param array $log Passed by reference masked items appended here.
* @return string Possibly masked value.
*/
private static function mask_sensitive_field( string $value, string $field_key, array &$log ): string {
$sensitive_keys = array(
'api_key',
'apikey',
'token',
'secret',
'password',
'access_token',
'auth',
'private_key',
'client_secret',
'apiKey',
'accessToken',
'clientSecret',
'privateKey',
);
if ( in_array( $field_key, $sensitive_keys, true ) && '' !== $value && '[REDACTED]' !== $value ) {
$log[] = array(
'reason' => 'sensitive_field_name',
'key' => $field_key,
'note' => 'Field name indicates sensitive data.',
);
return '[REDACTED]';
}
return $value;
}
/**
* Applies condition rightValue heuristic masking.
* Used specifically for condition node parameter values.
*
* @since 1.0.0
* @param string $value Raw condition value.
* @param array $log Passed by reference masked items appended here.
* @return string Possibly masked value.
*/
public static function apply_condition_heuristic( string $value, array &$log ): string {
$len = strlen( $value );
// Must be 8512 chars.
if ( $len < 8 || $len > 512 ) {
return $value;
}
// Safe-list check.
if ( in_array( strtolower( $value ), self::SAFE_CONDITION_VALUES, true ) ) {
return $value;
}
// n8n expression check.
if ( str_starts_with( $value, '={{' ) && str_ends_with( $value, '}}' ) ) {
return $value;
}
// ISO date check.
if ( preg_match( '/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/', $value ) ) {
return $value;
}
// Entropy check: UUID-shaped, or mixed case+digits, or long with no spaces.
$is_uuid = (bool) preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value );
$is_complex = (bool) preg_match( '/[A-Z]/', $value )
&& (bool) preg_match( '/[a-z]/', $value )
&& (bool) preg_match( '/[0-9]/', $value );
$is_long_no_space = $len > 20 && ! str_contains( $value, ' ' );
if ( $is_uuid || $is_complex || $is_long_no_space ) {
$log[] = array(
'reason' => 'condition_heuristic',
'key' => 'rightValue',
'note' => 'Value matches entropy heuristic for potential secret.',
);
return '[REDACTED]';
}
return $value;
}
}

View file

@ -0,0 +1,160 @@
<?php
/**
* Sanitizes and masks sensitive data in validated workflow arrays.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Security;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Sanitizes and masks sensitive data in validated n8n workflow arrays.
*/
class WorkflowSanitizer {
/**
* Log of masked items accumulated during processing.
*
* @var array<int, array<string, string>>
*/
private array $mask_log = array();
/**
* Processes (sanitizes + masks) a validated workflow data array.
*
* @param array $data Validated workflow data from WorkflowValidator.
* @return array{ data: array, mask_log: array } Processed data and mask log.
*/
public function process( array $data ): array {
$this->mask_log = array();
// Pass 1: Sanitize all strings.
$sanitized = $this->sanitize_recursive( $data );
// Pass 2: Mask secrets in nodes.
if ( isset( $sanitized['nodes'] ) && is_array( $sanitized['nodes'] ) ) {
foreach ( $sanitized['nodes'] as &$node ) {
$node = $this->mask_node( $node );
}
unset( $node );
}
return array(
'data' => $sanitized,
'mask_log' => $this->mask_log,
);
}
/**
* Recursively sanitizes all string values in the data.
* jsCode is preserved as-is (displayed with esc_html(), never executed).
*
* @since 1.0.0
* @param mixed $value Value to sanitize.
* @param string $parent_key Parent array key for context.
* @return mixed Sanitized value.
*/
private function sanitize_recursive( $value, string $parent_key = '' ): mixed {
if ( is_array( $value ) ) {
$result = array();
foreach ( $value as $key => $item ) {
$result[ $key ] = $this->sanitize_recursive( $item, (string) $key );
}
return $result;
}
if ( is_string( $value ) ) {
// Preserve jsCode as-is; it will be displayed with esc_html().
if ( 'jsCode' === $parent_key ) {
return $value;
}
return sanitize_text_field( $value );
}
if ( is_int( $value ) || is_float( $value ) ) {
return $value;
}
if ( is_bool( $value ) ) {
return $value;
}
return null;
}
/**
* Applies masking rules to a single node's parameters.
*
* @since 1.0.0
* @param array $node Single workflow node array.
* @return array Node with masked parameter values.
*/
private function mask_node( array $node ): array {
if ( ! isset( $node['parameters'] ) || ! is_array( $node['parameters'] ) ) {
return $node;
}
$node['parameters'] = $this->mask_parameters_recursive( $node['parameters'] );
return $node;
}
/**
* Recursively applies masking rules to parameter values.
*
* @since 1.0.0
* @param array $params Parameters array to process.
* @return array Parameters with sensitive values masked.
*/
private function mask_parameters_recursive( array $params ): array {
foreach ( $params as $key => &$value ) {
if ( is_string( $value ) && 'jsCode' !== $key ) {
$value = MaskingRules::apply( $value, (string) $key, $this->mask_log );
// Condition rightValue heuristic.
if ( 'rightValue' === $key && '[REDACTED]' !== $value ) {
$value = MaskingRules::apply_condition_heuristic( $value, $this->mask_log );
}
} elseif ( is_array( $value ) ) {
// {name, value} pair pattern — e.g. HTTP header/body parameters.
// If the 'name' field indicates a sensitive header, mask its 'value'.
if ( isset( $value['name'], $value['value'] ) && is_string( $value['value'] ) ) {
$this->mask_name_value_pair( $value );
}
$value = $this->mask_parameters_recursive( $value );
}
}
unset( $value );
return $params;
}
/**
* Masks the 'value' of a {name, value} pair when the name implies sensitive data.
* Covers HTTP headers like Authorization, API-Key, X-Auth-Token, etc.
*
* @param array $item Passed by reference modifies $item['value'] in place.
*/
private function mask_name_value_pair( array &$item ): void {
if ( '[REDACTED]' === $item['value'] ) {
return;
}
$name_lower = strtolower( (string) $item['name'] );
$sensitive = array( 'authorization', 'token', 'api-key', 'apikey', 'x-api-key', 'x-auth', 'secret', 'password', 'bearer' );
foreach ( $sensitive as $keyword ) {
if ( str_contains( $name_lower, $keyword ) ) {
$this->mask_log[] = array(
'reason' => 'sensitive_header_name',
'key' => 'value',
'note' => 'Parameter name "' . esc_html( $item['name'] ) . '" indicates sensitive data.',
);
$item['value'] = '[REDACTED]';
return;
}
}
}
}

View file

@ -0,0 +1,196 @@
<?php
/**
* Validates raw n8n workflow JSON against expected schema.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Security;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Validates raw n8n workflow JSON against size, structure and format constraints.
*
* @since 1.0.0
*/
class WorkflowValidator {
const MAX_NODES = 500;
const MAX_SIZE = 2_097_152; // 2MB.
const UUID_PATTERN = '/^[0-9a-f-]{36}$/i';
const TYPE_PATTERN = '/^[a-zA-Z0-9@.\/_-]+$/';
/**
* Validates a raw n8n workflow JSON string.
*
* @param string $raw Raw JSON string from user input.
* @return array|\WP_Error Decoded array on success, WP_Error on failure.
*/
public static function validate( string $raw ) {
// Check 5: Size limit (fail-fast before heavy processing).
if ( strlen( $raw ) > self::MAX_SIZE ) {
return new \WP_Error(
'breznflow_too_large',
__( 'Workflow JSON exceeds the 2MB size limit.', 'breznflow' )
);
}
// Check 1: JSON parse.
try {
$data = json_decode( $raw, true, 512, JSON_THROW_ON_ERROR );
} catch ( \JsonException $e ) {
return new \WP_Error(
'breznflow_invalid_json',
sprintf(
/* translators: %s: JSON parse error message */
__( 'Invalid JSON: %s', 'breznflow' ),
$e->getMessage()
)
);
}
// Check 2: Top-level schema.
if ( ! is_array( $data ) ) {
return new \WP_Error( 'breznflow_not_object', __( 'Workflow must be a JSON object.', 'breznflow' ) );
}
if ( empty( $data['name'] ) || ! is_string( $data['name'] ) ) {
return new \WP_Error( 'breznflow_missing_name', __( 'Workflow must have a non-empty "name" field.', 'breznflow' ) );
}
if ( ! isset( $data['nodes'] ) || ! is_array( $data['nodes'] ) || empty( $data['nodes'] ) ) {
return new \WP_Error( 'breznflow_missing_nodes', __( 'Workflow must have a non-empty "nodes" array.', 'breznflow' ) );
}
if ( ! isset( $data['connections'] ) || ! is_array( $data['connections'] ) ) {
return new \WP_Error( 'breznflow_missing_connections', __( 'Workflow must have a "connections" object.', 'breznflow' ) );
}
// Check 3: Node structure + count.
if ( count( $data['nodes'] ) > self::MAX_NODES ) {
return new \WP_Error(
'breznflow_too_many_nodes',
sprintf(
/* translators: %d: maximum node count */
__( 'Workflow has too many nodes (max %d).', 'breznflow' ),
self::MAX_NODES
)
);
}
foreach ( $data['nodes'] as $index => $node ) {
$err = self::validate_node( $node, $index );
if ( is_wp_error( $err ) ) {
return $err;
}
}
// Check 4: Connection integrity.
$node_names = array_column( $data['nodes'], 'name' );
foreach ( array_keys( $data['connections'] ) as $conn_node_name ) {
if ( ! in_array( $conn_node_name, $node_names, true ) ) {
return new \WP_Error(
'breznflow_invalid_connection',
sprintf(
/* translators: %s: node name from connections */
__( 'Connection references unknown node: "%s".', 'breznflow' ),
esc_html( $conn_node_name )
)
);
}
}
return $data;
}
/**
* Validates a single node structure.
*
* @param mixed $node Node data (should be array).
* @param int $index Node index for error messages.
* @return true|\WP_Error
*/
private static function validate_node( $node, int $index ) {
if ( ! is_array( $node ) ) {
return new \WP_Error(
'breznflow_invalid_node',
sprintf(
/* translators: %d: node index */
__( 'Node %d must be an object.', 'breznflow' ),
$index
)
);
}
// id: UUID format.
if ( empty( $node['id'] ) || ! is_string( $node['id'] ) || ! preg_match( self::UUID_PATTERN, $node['id'] ) ) {
return new \WP_Error(
'breznflow_invalid_node_id',
sprintf(
/* translators: %d: node index */
__( 'Node %d has an invalid or missing "id" (must be UUID format).', 'breznflow' ),
$index
)
);
}
// name: non-empty string.
if ( ! isset( $node['name'] ) || ! is_string( $node['name'] ) || '' === $node['name'] ) {
return new \WP_Error(
'breznflow_invalid_node_name',
sprintf(
/* translators: %d: node index */
__( 'Node %d has an invalid or missing "name".', 'breznflow' ),
$index
)
);
}
// type: alphanumeric + allowed chars.
if ( empty( $node['type'] ) || ! is_string( $node['type'] ) || ! preg_match( self::TYPE_PATTERN, $node['type'] ) ) {
return new \WP_Error(
'breznflow_invalid_node_type',
sprintf(
/* translators: %d: node index */
__( 'Node %d has an invalid or missing "type".', 'breznflow' ),
$index
)
);
}
// position: [int, int].
if (
! isset( $node['position'] ) ||
! is_array( $node['position'] ) ||
count( $node['position'] ) < 2 ||
! is_numeric( $node['position'][0] ) ||
! is_numeric( $node['position'][1] )
) {
return new \WP_Error(
'breznflow_invalid_node_position',
sprintf(
/* translators: %d: node index */
__( 'Node %d has an invalid or missing "position" ([x, y] required).', 'breznflow' ),
$index
)
);
}
// typeVersion: numeric.
if ( ! isset( $node['typeVersion'] ) || ! is_numeric( $node['typeVersion'] ) ) {
return new \WP_Error(
'breznflow_invalid_node_version',
sprintf(
/* translators: %d: node index */
__( 'Node %d has an invalid or missing "typeVersion".', 'breznflow' ),
$index
)
);
}
return true;
}
}

317
includes/Shortcode.php Normal file
View file

@ -0,0 +1,317 @@
<?php
/**
* Shortcode handler for rendering workflows on the frontend.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use BreznFlow\Features\NodeTypeRegistry;
use BreznFlow\Features\ThemeRegistry;
use BreznFlow\Features\ViewCounter;
use BreznFlow\Features\InfoBoxBuilder;
use BreznFlow\Features\NodeCategorizer;
use BreznFlow\Admin\SettingsPage;
/**
* Handles the [breznflow] shortcode, asset enqueueing, and JS data output.
*
* @since 1.0.0
*/
class Shortcode {
/**
* Accumulated workflow data for wp_localize_script output.
*
* @var array<int, array<string, mixed>>
*/
private static array $render_queue = array();
/**
* Whether frontend assets have been enqueued for this page load.
*
* @var bool
*/
private static bool $assets_enqueued = false;
/**
* Registers the shortcode and footer hook.
*
* @since 1.0.0
* @return void
*/
public function register(): void {
add_shortcode( 'breznflow', array( $this, 'render' ) );
add_action( 'wp_footer', array( $this, 'output_script_data' ), 1 );
}
/**
* Renders the [breznflow] shortcode.
*
* @param array $atts Shortcode attributes.
* @return string HTML output.
*/
public function render( $atts ): string {
$settings = SettingsPage::get_defaults();
$saved = get_option( 'breznflow_settings', array() );
$settings = array_merge( $settings, $saved );
$atts = shortcode_atts(
array(
'id' => 0,
'mode' => '',
'show_title' => '',
'show_infobox' => '',
'show_download' => '',
'show_minimap' => '',
'zoom' => '',
'max_code_lines' => '',
'preset' => '',
'show_share' => '',
'show_embed' => '',
'show_get_json' => '',
'theme' => '',
),
$atts,
'breznflow'
);
$post_id = (int) $atts['id'];
if ( $post_id <= 0 ) {
return '';
}
$post = get_post( $post_id );
if ( ! $post || 'breznflow_workflow' !== $post->post_type || 'publish' !== $post->post_status ) {
return '';
}
$raw_json = get_post_meta( $post_id, '_breznflow_raw_json', true );
if ( ! $raw_json ) {
return '';
}
$workflow = json_decode( $raw_json, true );
if ( ! is_array( $workflow ) ) {
return '';
}
// Resolve settings from meta, overridden by shortcode attrs.
$att_mode = $atts['mode'];
$meta_mode = get_post_meta( $post_id, '_breznflow_default_mode', true );
$mode = $att_mode ? $att_mode : ( $meta_mode ? $meta_mode : $settings['default_mode'] );
$mode = in_array( $mode, array( 'visual', 'info', 'compact' ), true ) ? $mode : 'visual';
$zoom = '' !== $atts['zoom']
? max( 10, min( 200, (int) $atts['zoom'] ) )
: (int) get_post_meta( $post_id, '_breznflow_default_zoom', true );
$show_title = '' !== $atts['show_title']
? (bool) $atts['show_title']
: (bool) get_post_meta( $post_id, '_breznflow_show_title', true );
$show_infobox = '' !== $atts['show_infobox']
? (bool) $atts['show_infobox']
: (bool) get_post_meta( $post_id, '_breznflow_show_infobox', true );
// Download: shortcode can only disable if meta has it enabled (not enable if meta disables).
$meta_download = (bool) get_post_meta( $post_id, '_breznflow_show_download', true );
$global_download = (bool) $settings['allow_download'];
$allow_download = $meta_download && $global_download;
if ( '' !== $atts['show_download'] && ! (bool) $atts['show_download'] ) {
$allow_download = false;
}
$allow_share = (bool) $settings['allow_share'];
if ( '' !== $atts['show_share'] && ! (bool) $atts['show_share'] ) {
$allow_share = false;
}
// Embed: dual-gate (global + per-post meta).
$meta_embed = (bool) get_post_meta( $post_id, '_breznflow_show_embed', true );
$allow_embed = $meta_embed && (bool) $settings['allow_embed'];
if ( '' !== $atts['show_embed'] && ! (bool) $atts['show_embed'] ) {
$allow_embed = false;
}
$allow_get_json = (bool) $settings['allow_get_json'];
if ( '' !== $atts['show_get_json'] && ! (bool) $atts['show_get_json'] ) {
$allow_get_json = false;
}
$allowed_themes = ThemeRegistry::get_theme_ids();
$att_theme = $atts['theme'];
$meta_theme = get_post_meta( $post_id, '_breznflow_default_theme', true );
$theme = $att_theme ? $att_theme : ( $meta_theme ? $meta_theme : ( $settings['default_theme'] ?? 'dark' ) );
$theme = in_array( $theme, $allowed_themes, true ) ? $theme : 'dark';
$meta_minimap = get_post_meta( $post_id, '_breznflow_show_minimap', true );
$show_minimap = '' !== $atts['show_minimap']
? (bool) $atts['show_minimap']
: ( '' !== $meta_minimap ? (bool) $meta_minimap : true );
$max_code_lines = '' !== $atts['max_code_lines'] ? max( 1, (int) $atts['max_code_lines'] ) : (int) $settings['max_code_lines'];
// Increment view count.
ViewCounter::increment( $post_id );
// Enqueue assets once.
if ( ! self::$assets_enqueued ) {
$this->enqueue_assets( $settings );
self::$assets_enqueued = true;
}
// Queue workflow data for JS output.
self::$render_queue[] = array(
'id' => $post_id,
'workflow' => $workflow,
'mode' => $mode,
'zoom' => $zoom ? $zoom : 100,
'autofit_threshold' => (int) ( $settings['autofit_threshold'] ?? 30 ),
'show_title' => $show_title,
'show_infobox' => $show_infobox,
'show_download' => $allow_download,
'show_minimap' => $show_minimap,
'max_code_lines' => $max_code_lines,
'download_label' => esc_html( $settings['download_label'] ),
'download_url' => $allow_download ? esc_url( add_query_arg( 'breznflow_download', $post_id, home_url( '/' ) ) ) : '',
'show_share' => $allow_share,
'show_embed' => $allow_embed,
'show_get_json' => $allow_get_json,
'permalink' => $allow_share ? esc_url( get_permalink() ) : '',
'anchor_id' => 'breznflow-' . $post_id,
'workflow_title' => esc_html( $post->post_title ),
'node_count' => (int) get_post_meta( $post_id, '_breznflow_node_count', true ),
'is_ai_powered' => (bool) get_post_meta( $post_id, '_breznflow_has_ai_nodes', true ),
'blog_name' => esc_html( get_bloginfo( 'name' ) ),
'blog_url' => esc_url( home_url( '/' ) ),
'embed_url' => $allow_embed ? esc_url( add_query_arg( 'breznflow_embed', $post_id, home_url( '/' ) ) ) : '',
'theme' => $theme,
);
// Build HTML placeholder.
$html = '';
if ( $show_title ) {
$html .= '<h3 class="breznflow-title">' . esc_html( $post->post_title ) . '</h3>';
}
if ( 'info' === $mode ) {
$node_summary = get_post_meta( $post_id, '_breznflow_node_summary', true );
$node_count = (int) get_post_meta( $post_id, '_breznflow_node_count', true );
$has_ai = (int) get_post_meta( $post_id, '_breznflow_has_ai_nodes', true );
$categorized = array(
'counts' => (array) json_decode( $node_summary ? $node_summary : '{}', true ),
'has_ai' => (bool) $has_ai,
'total' => $node_count,
);
$html .= InfoBoxBuilder::build( $categorized );
} else {
$html .= '<div style="position:relative">'
. '<span id="breznflow-' . esc_attr( (string) $post_id ) . '" '
. 'aria-hidden="true" style="position:absolute;top:-60px;left:0"></span>'
. '<div id="breznflow-wrap-' . esc_attr( (string) $post_id ) . '" '
. 'class="breznflow-embed" data-id="' . esc_attr( (string) $post_id ) . '">'
. '</div>'
. '</div>';
}
return $html;
}
/**
* Outputs accumulated workflow data as a single wp_localize_script call.
* Hooked to wp_footer priority 1 (before scripts).
*/
public function output_script_data(): void {
if ( empty( self::$render_queue ) ) {
return;
}
wp_localize_script( 'breznflow-renderer', 'breznflowData', self::$render_queue );
wp_localize_script( 'breznflow-renderer', 'breznflowIcons', NodeTypeRegistry::get_registry() );
wp_localize_script( 'breznflow-renderer', 'breznflowI18n', self::get_js_i18n() );
}
/**
* Returns translatable strings for the frontend renderer JS.
*
* @return array<string, string>
*/
public static function get_js_i18n(): array {
return array(
'share' => __( 'Share', 'breznflow' ),
'embed' => __( 'Embed', 'breznflow' ),
'getJson' => __( 'Get JSON', 'breznflow' ),
'copy' => __( 'Copy', 'breznflow' ),
'copied' => __( 'Copied!', 'breznflow' ),
'error' => __( 'Error', 'breznflow' ),
'close' => __( 'Close', 'breznflow' ),
'zoomIn' => __( 'Zoom in', 'breznflow' ),
'zoomOut' => __( 'Zoom out', 'breznflow' ),
'resetView' => __( 'Reset view', 'breznflow' ),
'fullscreen' => __( 'Fullscreen', 'breznflow' ),
'minimap' => __( 'Minimap', 'breznflow' ),
'highlightInDiagram' => __( 'Highlight in diagram', 'breznflow' ),
'articleLink' => __( 'Article Link', 'breznflow' ),
'anchorLink' => __( 'Workflow Anchor Link', 'breznflow' ),
'embedDesc' => __( 'Embed this workflow on any website:', 'breznflow' ),
'optionalParams' => __( 'Optional URL parameters:', 'breznflow' ),
'code' => __( 'Code', 'breznflow' ),
'credential' => __( 'Credential', 'breznflow' ),
'type' => __( 'Type', 'breznflow' ),
'aiPowered' => __( 'AI-powered', 'breznflow' ),
'more' => __( 'more', 'breznflow' ),
'node' => __( 'node', 'breznflow' ),
'nodes' => __( 'nodes', 'breznflow' ),
'line' => __( 'line', 'breznflow' ),
'lines' => __( 'lines', 'breznflow' ),
);
}
/**
* Outputs Schema.org HowTo structured data if enabled.
*/
public function maybe_output_schema(): void {
if ( ! is_singular() ) {
return;
}
$settings = get_option( 'breznflow_settings', array() );
if ( empty( $settings['schema_howto'] ) ) {
return;
}
// Schema output is handled per-shortcode; here we hook for future expansion.
}
/**
* Enqueues renderer assets.
*
* @since 1.0.0
* @param array $settings Plugin settings array (reserved for future use).
* @return void
*/
private function enqueue_assets( array $settings ): void {
wp_enqueue_style( 'breznflow-renderer', BREZNFLOW_URL . 'assets/renderer.css', array(), BREZNFLOW_VERSION );
wp_enqueue_script( 'breznflow-renderer', BREZNFLOW_URL . 'assets/renderer.js', array(), BREZNFLOW_VERSION, true );
// Enqueue built-in theme CSS files.
foreach ( ThemeRegistry::BUILTIN as $id => $name ) {
wp_enqueue_style(
'breznflow-theme-' . $id,
ThemeRegistry::get_builtin_url( $id ),
array( 'breznflow-renderer' ),
BREZNFLOW_VERSION
);
}
// Output custom themes as inline CSS.
$custom_css = ThemeRegistry::get_custom_theme_css();
if ( $custom_css ) {
wp_add_inline_style( 'breznflow-renderer', $custom_css );
}
}
}

Binary file not shown.

View file

@ -0,0 +1,892 @@
# German translation for BreznFlow
# Copyright (C) 2025 NoSchmarrn.dev
# This file is distributed under the same license as the BreznFlow plugin.
msgid ""
msgstr ""
"Project-Id-Version: BreznFlow 1.3.0\n"
"Report-Msgid-Bugs-To: https://noschmarrn.dev/\n"
"POT-Creation-Date: 2026-03-30T00:00:00+00:00\n"
"PO-Revision-Date: 2026-03-30T00:00:00+00:00\n"
"Last-Translator: NoSchmarrn.dev <info@noschmarrn.dev>\n"
"Language-Team: German <info@noschmarrn.dev>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: BreznFlow 1.3.0\n"
"X-Domain: breznflow\n"
#. Post type labels
#: includes/PostType.php
msgid "Workflows"
msgstr "Workflows"
#: includes/PostType.php
msgid "Workflow"
msgstr "Workflow"
#: includes/PostType.php includes/Admin/AdminMenu.php includes/Admin/views/list.php includes/Admin/views/wizard-step-1.php
msgid "Add Workflow"
msgstr "Workflow hinzufügen"
#: includes/PostType.php
msgid "Add New Workflow"
msgstr "Neuen Workflow hinzufügen"
#: includes/PostType.php
msgid "Edit Workflow"
msgstr "Workflow bearbeiten"
#: includes/PostType.php
msgid "New Workflow"
msgstr "Neuer Workflow"
#: includes/PostType.php
msgid "View Workflow"
msgstr "Workflow ansehen"
#: includes/PostType.php
msgid "Search Workflows"
msgstr "Workflows suchen"
#: includes/PostType.php
msgid "No workflows found"
msgstr "Keine Workflows gefunden"
#: includes/PostType.php
msgid "No workflows found in trash"
msgstr "Keine Workflows im Papierkorb gefunden"
#: includes/PostType.php includes/Admin/AdminMenu.php
msgid "BreznFlow"
msgstr "BreznFlow"
#. Taxonomy labels
#: includes/PostType.php
msgid "Workflow Categories"
msgstr "Workflow-Kategorien"
#: includes/PostType.php
msgid "Workflow Category"
msgstr "Workflow-Kategorie"
#: includes/PostType.php
msgid "Search Categories"
msgstr "Kategorien suchen"
#: includes/PostType.php
msgid "All Categories"
msgstr "Alle Kategorien"
#: includes/PostType.php
msgid "Parent Category"
msgstr "Übergeordnete Kategorie"
#: includes/PostType.php
msgid "Parent Category:"
msgstr "Übergeordnete Kategorie:"
#: includes/PostType.php
msgid "Edit Category"
msgstr "Kategorie bearbeiten"
#: includes/PostType.php
msgid "Update Category"
msgstr "Kategorie aktualisieren"
#: includes/PostType.php
msgid "Add New Category"
msgstr "Neue Kategorie hinzufügen"
#: includes/PostType.php
msgid "New Category Name"
msgstr "Neuer Kategoriename"
#: includes/PostType.php
msgid "Categories"
msgstr "Kategorien"
#. Validator messages
#: includes/Security/WorkflowValidator.php
msgid "Workflow JSON exceeds the 2MB size limit."
msgstr "Workflow-JSON überschreitet das 2-MB-Limit."
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Invalid JSON: %s"
msgstr "Ungültiges JSON: %s"
#: includes/Security/WorkflowValidator.php
msgid "Workflow must be a JSON object."
msgstr "Der Workflow muss ein JSON-Objekt sein."
#: includes/Security/WorkflowValidator.php
msgid "Workflow must have a non-empty \"name\" field."
msgstr "Der Workflow muss ein nicht-leeres \"name\"-Feld haben."
#: includes/Security/WorkflowValidator.php
msgid "Workflow must have a non-empty \"nodes\" array."
msgstr "Der Workflow muss ein nicht-leeres \"nodes\"-Array haben."
#: includes/Security/WorkflowValidator.php
msgid "Workflow must have a \"connections\" object."
msgstr "Der Workflow muss ein \"connections\"-Objekt haben."
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Workflow has too many nodes (max %d)."
msgstr "Der Workflow hat zu viele Nodes (max. %d)."
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Connection references unknown node: \"%s\"."
msgstr "Verbindung referenziert unbekannten Node: \"%s\"."
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Node %d must be an object."
msgstr "Node %d muss ein Objekt sein."
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Node %d has an invalid or missing \"id\" (must be UUID format)."
msgstr "Node %d hat eine ungültige oder fehlende \"id\" (UUID-Format erforderlich)."
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Node %d has an invalid or missing \"name\"."
msgstr "Node %d hat einen ungültigen oder fehlenden \"name\"."
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Node %d has an invalid or missing \"type\"."
msgstr "Node %d hat einen ungültigen oder fehlenden \"type\"."
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Node %d has an invalid or missing \"position\" ([x, y] required)."
msgstr "Node %d hat eine ungültige oder fehlende \"position\" ([x, y] erforderlich)."
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Node %d has an invalid or missing \"typeVersion\"."
msgstr "Node %d hat eine ungültige oder fehlende \"typeVersion\"."
#. InfoBox
#: includes/Features/InfoBoxBuilder.php
#, php-format
msgid "%d node"
msgid_plural "%d nodes"
msgstr[0] "%d Node"
msgstr[1] "%d Nodes"
#: includes/Features/InfoBoxBuilder.php
msgid "more type"
msgid_plural "more types"
msgstr[0] "weiterer Typ"
msgstr[1] "weitere Typen"
#: includes/Features/InfoBoxBuilder.php includes/Shortcode.php includes/Admin/views/wizard-step-2.php
msgid "AI-powered"
msgstr "KI-gestützt"
#. Admin menu
#: includes/Admin/AdminMenu.php
msgid "All Workflows"
msgstr "Alle Workflows"
#: includes/Admin/AdminMenu.php includes/Admin/views/settings.php
msgid "Settings"
msgstr "Einstellungen"
#: includes/Admin/AdminMenu.php
msgid "Themes"
msgstr "Themes"
#. Wizard page
#: includes/Admin/WizardPage.php includes/Admin/ThemesPage.php
msgid "Insufficient permissions."
msgstr "Unzureichende Berechtigungen."
#: includes/Admin/WizardPage.php
msgid "No JSON provided."
msgstr "Kein JSON angegeben."
#: includes/Admin/WizardPage.php
msgid "No URL provided."
msgstr "Keine URL angegeben."
#: includes/Admin/WizardPage.php
msgid "This URL is not allowed."
msgstr "Diese URL ist nicht erlaubt."
#: includes/Admin/WizardPage.php
msgid "Empty response from URL."
msgstr "Leere Antwort von der URL."
#: includes/Admin/WizardPage.php
msgid "Unnamed Node"
msgstr "Unbenannter Node"
#: includes/Admin/WizardPage.php
msgid "Failed to create workflow post."
msgstr "Workflow-Beitrag konnte nicht erstellt werden."
#: includes/Admin/WizardPage.php includes/Admin/views/wizard-step-2.php
msgid "Invalid workflow ID."
msgstr "Ungültige Workflow-ID."
#: includes/Admin/WizardPage.php includes/Admin/views/wizard-step-2.php includes/Admin/views/wizard-step-3.php
msgid "Invalid workflow."
msgstr "Ungültiger Workflow."
#: includes/Admin/WizardPage.php
msgid "Validating..."
msgstr "Wird validiert…"
#: includes/Admin/WizardPage.php
msgid "Valid n8n workflow"
msgstr "Gültiger n8n-Workflow"
#: includes/Admin/WizardPage.php
msgid "Invalid workflow"
msgstr "Ungültiger Workflow"
#: includes/Admin/WizardPage.php
msgid "Fetching URL..."
msgstr "URL wird abgerufen…"
#: includes/Admin/WizardPage.php includes/Shortcode.php includes/Admin/WorkflowListTable.php includes/Admin/views/wizard-step-2.php
msgid "Copy"
msgstr "Kopieren"
#: includes/Admin/WizardPage.php includes/Shortcode.php
msgid "Copied!"
msgstr "Kopiert!"
#: includes/Admin/WizardPage.php
msgid "Validate JSON"
msgstr "JSON validieren"
#: includes/Admin/WizardPage.php includes/Admin/views/wizard-step-1.php
msgid "Fetch"
msgstr "Abrufen"
#: includes/Admin/WizardPage.php
msgid "Fetch failed"
msgstr "Abruf fehlgeschlagen"
#: includes/Admin/WizardPage.php
msgid "Please paste a workflow JSON first."
msgstr "Bitte zuerst ein Workflow-JSON einfügen."
#. Workflow list table
#: includes/Admin/WorkflowListTable.php
msgid "Title"
msgstr "Titel"
#: includes/Admin/WorkflowListTable.php
msgid "Nodes"
msgstr "Nodes"
#: includes/Admin/WorkflowListTable.php
msgid "AI"
msgstr "KI"
#: includes/Admin/WorkflowListTable.php
msgid "Mode"
msgstr "Modus"
#: includes/Admin/WorkflowListTable.php
msgid "Views"
msgstr "Aufrufe"
#: includes/Admin/WorkflowListTable.php
msgid "Shortcode"
msgstr "Shortcode"
#: includes/Admin/WorkflowListTable.php
msgid "Date"
msgstr "Datum"
#: includes/Admin/WorkflowListTable.php includes/Admin/views/themes.php
msgid "Delete"
msgstr "Löschen"
#: includes/Admin/WorkflowListTable.php
msgid "Contains AI nodes"
msgstr "Enthält KI-Nodes"
#: includes/Admin/WorkflowListTable.php
msgid "Draft"
msgstr "Entwurf"
#: includes/Admin/WorkflowListTable.php
msgid "Edit"
msgstr "Bearbeiten"
#. Workflow list page
#: includes/Admin/views/list.php
#, php-format
msgid "%d workflow moved to trash."
msgid_plural "%d workflows moved to trash."
msgstr[0] "%d Workflow in den Papierkorb verschoben."
msgstr[1] "%d Workflows in den Papierkorb verschoben."
#: includes/Admin/views/list.php
#, php-format
msgid "Workflow published! Use shortcode: %s"
msgstr "Workflow veröffentlicht! Shortcode verwenden: %s"
#: includes/Admin/views/list.php
msgid "Workflow published!"
msgstr "Workflow veröffentlicht!"
#. Wizard step 1
#: includes/Admin/views/wizard-step-1.php includes/Admin/views/wizard-step-2.php includes/Admin/views/wizard-step-3.php
msgid "1. Import JSON"
msgstr "1. JSON importieren"
#: includes/Admin/views/wizard-step-1.php includes/Admin/views/wizard-step-2.php includes/Admin/views/wizard-step-3.php
msgid "2. Configure"
msgstr "2. Konfigurieren"
#: includes/Admin/views/wizard-step-1.php includes/Admin/views/wizard-step-2.php includes/Admin/views/wizard-step-3.php
msgid "3. Preview & Publish"
msgstr "3. Vorschau & Veröffentlichen"
#: includes/Admin/views/wizard-step-1.php
msgid "Please provide a workflow JSON."
msgstr "Bitte ein Workflow-JSON angeben."
#: includes/Admin/views/wizard-step-1.php
msgid "Import n8n Workflow"
msgstr "n8n-Workflow importieren"
#: includes/Admin/views/wizard-step-1.php
msgid "Import from URL (optional)"
msgstr "Von URL importieren (optional)"
#: includes/Admin/views/wizard-step-1.php
msgid "Or paste JSON directly below."
msgstr "Oder JSON direkt unten einfügen."
#: includes/Admin/views/wizard-step-1.php
msgid "Upload JSON file"
msgstr "JSON-Datei hochladen"
#: includes/Admin/views/wizard-step-1.php
msgid "n8n Workflow JSON"
msgstr "n8n-Workflow-JSON"
#: includes/Admin/views/wizard-step-1.php
msgid "Continue to Settings"
msgstr "Weiter zu Einstellungen"
#. Wizard step 2
#: includes/Admin/views/wizard-step-2.php
msgid "Configure Workflow"
msgstr "Workflow konfigurieren"
#: includes/Admin/views/wizard-step-2.php
#, php-format
msgid "%d nodes"
msgstr "%d Nodes"
#: includes/Admin/views/wizard-step-2.php
msgid "Basic Settings"
msgstr "Grundeinstellungen"
#: includes/Admin/views/wizard-step-2.php
msgid "Workflow Title"
msgstr "Workflow-Titel"
#: includes/Admin/views/wizard-step-2.php
msgid "Display Mode"
msgstr "Anzeigemodus"
#: includes/Admin/views/wizard-step-2.php
msgid "Visual (diagram + infobox)"
msgstr "Visuell (Diagramm + Infobox)"
#: includes/Admin/views/wizard-step-2.php
msgid "Info only (infobox, no diagram)"
msgstr "Nur Info (Infobox, kein Diagramm)"
#: includes/Admin/views/wizard-step-2.php
msgid "Compact (diagram only, no toolbar)"
msgstr "Kompakt (nur Diagramm, keine Toolbar)"
#: includes/Admin/views/wizard-step-2.php
msgid "Default Zoom"
msgstr "Standard-Zoom"
#: includes/Admin/views/wizard-step-2.php
msgid "Display Options"
msgstr "Anzeigeoptionen"
#: includes/Admin/views/wizard-step-2.php
msgid "Show title above diagram"
msgstr "Titel über dem Diagramm anzeigen"
#: includes/Admin/views/wizard-step-2.php
msgid "Show node info box"
msgstr "Node-Infobox anzeigen"
#: includes/Admin/views/wizard-step-2.php
msgid "Allow JSON download"
msgstr "JSON-Download erlauben"
#: includes/Admin/views/wizard-step-2.php
msgid "Allow iframe embed"
msgstr "iFrame-Einbettung erlauben"
#: includes/Admin/views/wizard-step-2.php
msgid "Show minimap button"
msgstr "Minimap-Button anzeigen"
#: includes/Admin/views/wizard-step-2.php
msgid "Viewer Theme"
msgstr "Viewer-Theme"
#: includes/Admin/views/wizard-step-2.php
msgid "Shortcode Preview"
msgstr "Shortcode-Vorschau"
#: includes/Admin/views/wizard-step-2.php
msgid "Security Masking Log"
msgstr "Sicherheits-Maskierungsprotokoll"
#: includes/Admin/views/wizard-step-2.php
#, php-format
msgid "%d item was sanitized"
msgid_plural "%d items were sanitized"
msgstr[0] "%d Eintrag wurde bereinigt"
msgstr[1] "%d Einträge wurden bereinigt"
#: includes/Admin/views/wizard-step-2.php
msgid "← Back"
msgstr "← Zurück"
#: includes/Admin/views/wizard-step-2.php
msgid "Continue to Preview"
msgstr "Weiter zur Vorschau"
#. Wizard step 3
#: includes/Admin/views/wizard-step-3.php
msgid "Preview & Publish"
msgstr "Vorschau & Veröffentlichen"
#: includes/Admin/views/wizard-step-3.php
msgid "Note:"
msgstr "Hinweis:"
#: includes/Admin/views/wizard-step-3.php
msgid "This workflow contains Code nodes. JavaScript code is displayed as-is and cannot be automatically scanned for hardcoded secrets. Please review the code in the node detail panel before publishing."
msgstr "Dieser Workflow enthält Code-Nodes. JavaScript-Code wird unverändert angezeigt und kann nicht automatisch auf fest codierte Geheimnisse geprüft werden. Bitte überprüfen Sie den Code im Node-Detailpanel vor der Veröffentlichung."
#: includes/Admin/views/wizard-step-3.php
msgid "Workflow Preview"
msgstr "Workflow-Vorschau"
#: includes/Admin/views/wizard-step-3.php
#, php-format
msgid "Showing %1$d nodes from \"%2$s\"."
msgstr "Zeige %1$d Nodes von \"%2$s\"."
#: includes/Admin/views/wizard-step-3.php
msgid "Security Summary"
msgstr "Sicherheitsübersicht"
#: includes/Admin/views/wizard-step-3.php
#, php-format
msgid "%d value was masked for security."
msgid_plural "%d values were masked for security."
msgstr[0] "%d Wert wurde aus Sicherheitsgründen maskiert."
msgstr[1] "%d Werte wurden aus Sicherheitsgründen maskiert."
#: includes/Admin/views/wizard-step-3.php
msgid "Publish"
msgstr "Veröffentlichen"
#: includes/Admin/views/wizard-step-3.php
msgid "Use the shortcode below in any post or page to embed this workflow."
msgstr "Verwenden Sie den folgenden Shortcode in einem Beitrag oder einer Seite, um diesen Workflow einzubetten."
#: includes/Admin/views/wizard-step-3.php
msgid "← Edit Settings"
msgstr "← Einstellungen bearbeiten"
#: includes/Admin/views/wizard-step-3.php
msgid "Publish Workflow"
msgstr "Workflow veröffentlichen"
#. Settings page
#: includes/Admin/views/settings.php
msgid "BreznFlow Settings"
msgstr "BreznFlow-Einstellungen"
#: includes/Admin/views/settings.php
msgid "Display Defaults"
msgstr "Anzeige-Standardwerte"
#: includes/Admin/views/settings.php
msgid "Default Mode"
msgstr "Standardmodus"
#: includes/Admin/views/settings.php
msgid "Visual"
msgstr "Visuell"
#: includes/Admin/views/settings.php
msgid "Info"
msgstr "Info"
#: includes/Admin/views/settings.php
msgid "Compact"
msgstr "Kompakt"
#: includes/Admin/views/settings.php
msgid "Default Zoom (%)"
msgstr "Standard-Zoom (%)"
#: includes/Admin/views/settings.php
msgid "Large Workflow Threshold"
msgstr "Schwellenwert für große Workflows"
#: includes/Admin/views/settings.php
msgid "Workflows with this many nodes or more start zoomed in at the trigger node (0 = disabled)."
msgstr "Workflows mit dieser Anzahl an Nodes oder mehr starten hereingezoomt am Trigger-Node (0 = deaktiviert)."
#: includes/Admin/views/settings.php
msgid "Max Code Lines"
msgstr "Max. Code-Zeilen"
#: includes/Admin/views/settings.php
msgid "Maximum lines of code shown in node detail panel."
msgstr "Maximale Anzahl an Code-Zeilen im Node-Detailpanel."
#: includes/Admin/views/settings.php
msgid "Show by Default"
msgstr "Standardmäßig anzeigen"
#: includes/Admin/views/settings.php
msgid "Show workflow title"
msgstr "Workflow-Titel anzeigen"
#: includes/Admin/views/settings.php
msgid "Workflow Theme"
msgstr "Workflow-Theme"
#: includes/Admin/views/settings.php
msgid "Default Theme"
msgstr "Standard-Theme"
#: includes/Admin/views/settings.php
msgid "Can be overridden per workflow or via shortcode attr theme=\"light\". Custom themes: import via BreznFlow → Themes."
msgstr "Kann pro Workflow oder per Shortcode-Attribut theme=\"light\" überschrieben werden. Eigene Themes: Import über BreznFlow → Themes."
#: includes/Admin/views/settings.php
msgid "Action Bar"
msgstr "Aktionsleiste"
#: includes/Admin/views/settings.php includes/Shortcode.php
msgid "Share"
msgstr "Teilen"
#: includes/Admin/views/settings.php
msgid "Visitors can share workflow links"
msgstr "Besucher können Workflow-Links teilen"
#: includes/Admin/views/settings.php includes/Shortcode.php
msgid "Embed"
msgstr "Einbetten"
#: includes/Admin/views/settings.php
msgid "Allow iframe embedding (creates public embed URL)"
msgstr "iFrame-Einbettung erlauben (erstellt öffentliche Embed-URL)"
#: includes/Admin/views/settings.php includes/Shortcode.php
msgid "Get JSON"
msgstr "JSON anzeigen"
#: includes/Admin/views/settings.php
msgid "Visitors can view/copy workflow JSON"
msgstr "Besucher können Workflow-JSON ansehen/kopieren"
#: includes/Admin/views/settings.php
msgid "Allow Download"
msgstr "Download erlauben"
#: includes/Admin/views/settings.php
msgid "Allow visitors to download sanitized workflow JSON"
msgstr "Besuchern den Download des bereinigten Workflow-JSON erlauben"
#: includes/Admin/views/settings.php
msgid "Download Button Label"
msgstr "Download-Button-Beschriftung"
#: includes/Admin/views/settings.php
msgid "Features"
msgstr "Funktionen"
#: includes/Admin/views/settings.php
msgid "View Counting"
msgstr "Aufrufzählung"
#: includes/Admin/views/settings.php
msgid "Track shortcode render count per workflow"
msgstr "Shortcode-Aufrufe pro Workflow zählen"
#: includes/Admin/views/settings.php
msgid "Related Workflows"
msgstr "Verwandte Workflows"
#: includes/Admin/views/settings.php
msgid "Enable related workflows by shared node types"
msgstr "Verwandte Workflows anhand gemeinsamer Node-Typen aktivieren"
#: includes/Admin/views/settings.php
msgid "Schema.org HowTo"
msgstr "Schema.org HowTo"
#: includes/Admin/views/settings.php
msgid "Output Schema.org HowTo structured data in page head"
msgstr "Schema.org-HowTo-Strukturdaten im Seitenkopf ausgeben"
#. Settings defaults
#: includes/Admin/SettingsPage.php
msgid "Download JSON"
msgstr "JSON herunterladen"
#. Themes page
#: includes/Admin/views/themes.php
msgid "BreznFlow — Themes"
msgstr "BreznFlow — Themes"
#: includes/Admin/views/themes.php
msgid "Theme imported successfully."
msgstr "Theme erfolgreich importiert."
#: includes/Admin/views/themes.php
msgid "Theme deleted."
msgstr "Theme gelöscht."
#: includes/Admin/views/themes.php
msgid "Invalid file type. Please upload a .breznflow.json file."
msgstr "Ungültiger Dateityp. Bitte eine .breznflow.json-Datei hochladen."
#: includes/Admin/views/themes.php
msgid "File upload failed. Please try again."
msgstr "Datei-Upload fehlgeschlagen. Bitte erneut versuchen."
#: includes/Admin/views/themes.php
msgid "Could not read the uploaded file."
msgstr "Die hochgeladene Datei konnte nicht gelesen werden."
#: includes/Admin/views/themes.php
msgid "File is not valid JSON."
msgstr "Die Datei enthält kein gültiges JSON."
#: includes/Admin/views/themes.php
msgid "Theme validation failed."
msgstr "Theme-Validierung fehlgeschlagen."
#: includes/Admin/views/themes.php
msgid "An error occurred."
msgstr "Ein Fehler ist aufgetreten."
#: includes/Admin/views/themes.php
msgid "Import Custom Theme"
msgstr "Eigenes Theme importieren"
#: includes/Admin/views/themes.php
msgid "Upload a .breznflow.json file containing a valid theme definition. The file must include all 41 color tokens."
msgstr "Laden Sie eine .breznflow.json-Datei mit einer gültigen Theme-Definition hoch. Die Datei muss alle 41 Farb-Token enthalten."
#: includes/Admin/views/themes.php
msgid "Theme File"
msgstr "Theme-Datei"
#: includes/Admin/views/themes.php
msgid "Import Theme"
msgstr "Theme importieren"
#: includes/Admin/views/themes.php
msgid "Built-in Themes"
msgstr "Integrierte Themes"
#: includes/Admin/views/themes.php
msgid "Name"
msgstr "Name"
#: includes/Admin/views/themes.php
msgid "ID"
msgstr "ID"
#: includes/Admin/views/themes.php
msgid "Type"
msgstr "Typ"
#: includes/Admin/views/themes.php
msgid "Built-in"
msgstr "Integriert"
#: includes/Admin/views/themes.php
msgid "Built-in themes are read-only and updated with the plugin."
msgstr "Integrierte Themes sind schreibgeschützt und werden mit dem Plugin aktualisiert."
#: includes/Admin/views/themes.php
msgid "Custom Themes"
msgstr "Eigene Themes"
#: includes/Admin/views/themes.php
msgid "No custom themes imported yet."
msgstr "Noch keine eigenen Themes importiert."
#: includes/Admin/views/themes.php
msgid "Version"
msgstr "Version"
#: includes/Admin/views/themes.php
msgid "Actions"
msgstr "Aktionen"
#: includes/Admin/views/themes.php
msgid "Delete this theme?"
msgstr "Dieses Theme löschen?"
#. Theme importer validation
#: includes/Features/ThemeImporter.php
#, php-format
msgid "Missing required field: %s"
msgstr "Erforderliches Feld fehlt: %s"
#: includes/Features/ThemeImporter.php
#, php-format
msgid "Unexpected fields: %s"
msgstr "Unerwartete Felder: %s"
#: includes/Features/ThemeImporter.php
msgid "Theme ID must contain only lowercase letters, numbers, and hyphens."
msgstr "Die Theme-ID darf nur Kleinbuchstaben, Zahlen und Bindestriche enthalten."
#: includes/Features/ThemeImporter.php
#, php-format
msgid "Theme ID \"%s\" is reserved for built-in themes."
msgstr "Die Theme-ID \"%s\" ist für integrierte Themes reserviert."
#: includes/Features/ThemeImporter.php
msgid "Theme name must not be empty."
msgstr "Der Theme-Name darf nicht leer sein."
#: includes/Features/ThemeImporter.php
msgid "Theme name must be 80 characters or fewer."
msgstr "Der Theme-Name darf maximal 80 Zeichen lang sein."
#: includes/Features/ThemeImporter.php
msgid "Theme version must be an integer."
msgstr "Die Theme-Version muss eine Ganzzahl sein."
#: includes/Features/ThemeImporter.php
msgid "Tokens must be an object."
msgstr "Tokens müssen ein Objekt sein."
#: includes/Features/ThemeImporter.php
#, php-format
msgid "Unknown tokens: %s"
msgstr "Unbekannte Tokens: %s"
#: includes/Features/ThemeImporter.php
#, php-format
msgid "Missing tokens: %s"
msgstr "Fehlende Tokens: %s"
#: includes/Features/ThemeImporter.php
#, php-format
msgid "Invalid color value for \"%1$s\": %2$s"
msgstr "Ungültiger Farbwert für \"%1$s\": %2$s"
#. Embed handler
#: includes/EmbedHandler.php
msgid "Source:"
msgstr "Quelle:"
#. Frontend renderer JS strings — Share, Embed, Get JSON are defined above in settings.php context
#: includes/Shortcode.php
msgid "Error"
msgstr "Fehler"
#: includes/Shortcode.php
msgid "Close"
msgstr "Schließen"
#: includes/Shortcode.php
msgid "Zoom in"
msgstr "Vergrößern"
#: includes/Shortcode.php
msgid "Zoom out"
msgstr "Verkleinern"
#: includes/Shortcode.php
msgid "Reset view"
msgstr "Ansicht zurücksetzen"
#: includes/Shortcode.php
msgid "Fullscreen"
msgstr "Vollbild"
#: includes/Shortcode.php
msgid "Minimap"
msgstr "Minimap"
#: includes/Shortcode.php
msgid "Highlight in diagram"
msgstr "Im Diagramm hervorheben"
#: includes/Shortcode.php
msgid "Article Link"
msgstr "Artikel-Link"
#: includes/Shortcode.php
msgid "Workflow Anchor Link"
msgstr "Workflow-Anker-Link"
#: includes/Shortcode.php
msgid "Embed this workflow on any website:"
msgstr "Diesen Workflow auf einer beliebigen Website einbetten:"
#: includes/Shortcode.php
msgid "Optional URL parameters:"
msgstr "Optionale URL-Parameter:"
#: includes/Shortcode.php
msgid "Code"
msgstr "Code"
#: includes/Shortcode.php
msgid "Credential"
msgstr "Zugangsdaten"
#: includes/Shortcode.php
msgid "more"
msgstr "weitere"
#: includes/Shortcode.php
msgid "node"
msgstr "Node"
#: includes/Shortcode.php includes/Admin/WizardPage.php
msgid "nodes"
msgstr "Nodes"
#: includes/Shortcode.php
msgid "line"
msgstr "Zeile"
#: includes/Shortcode.php
msgid "lines"
msgstr "Zeilen"

900
languages/breznflow.pot Normal file
View file

@ -0,0 +1,900 @@
# Copyright (C) 2025 NoSchmarrn.dev
# This file is distributed under the same license as the BreznFlow plugin.
msgid ""
msgstr ""
"Project-Id-Version: BreznFlow 1.3.0\n"
"Report-Msgid-Bugs-To: https://noschmarrn.dev/\n"
"POT-Creation-Date: 2026-03-30T00:00:00+00:00\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"X-Generator: BreznFlow 1.3.0\n"
"X-Domain: breznflow\n"
#. Post type labels
#: includes/PostType.php
msgid "Workflows"
msgstr ""
#: includes/PostType.php
msgid "Workflow"
msgstr ""
#: includes/PostType.php includes/Admin/AdminMenu.php includes/Admin/views/list.php includes/Admin/views/wizard-step-1.php
msgid "Add Workflow"
msgstr ""
#: includes/PostType.php
msgid "Add New Workflow"
msgstr ""
#: includes/PostType.php
msgid "Edit Workflow"
msgstr ""
#: includes/PostType.php
msgid "New Workflow"
msgstr ""
#: includes/PostType.php
msgid "View Workflow"
msgstr ""
#: includes/PostType.php
msgid "Search Workflows"
msgstr ""
#: includes/PostType.php
msgid "No workflows found"
msgstr ""
#: includes/PostType.php
msgid "No workflows found in trash"
msgstr ""
#: includes/PostType.php includes/Admin/AdminMenu.php
msgid "BreznFlow"
msgstr ""
#. Taxonomy labels
#: includes/PostType.php
msgid "Workflow Categories"
msgstr ""
#: includes/PostType.php
msgid "Workflow Category"
msgstr ""
#: includes/PostType.php
msgid "Search Categories"
msgstr ""
#: includes/PostType.php
msgid "All Categories"
msgstr ""
#: includes/PostType.php
msgid "Parent Category"
msgstr ""
#: includes/PostType.php
msgid "Parent Category:"
msgstr ""
#: includes/PostType.php
msgid "Edit Category"
msgstr ""
#: includes/PostType.php
msgid "Update Category"
msgstr ""
#: includes/PostType.php
msgid "Add New Category"
msgstr ""
#: includes/PostType.php
msgid "New Category Name"
msgstr ""
#: includes/PostType.php
msgid "Categories"
msgstr ""
#. Validator messages
#: includes/Security/WorkflowValidator.php
msgid "Workflow JSON exceeds the 2MB size limit."
msgstr ""
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Invalid JSON: %s"
msgstr ""
#: includes/Security/WorkflowValidator.php
msgid "Workflow must be a JSON object."
msgstr ""
#: includes/Security/WorkflowValidator.php
msgid "Workflow must have a non-empty \"name\" field."
msgstr ""
#: includes/Security/WorkflowValidator.php
msgid "Workflow must have a non-empty \"nodes\" array."
msgstr ""
#: includes/Security/WorkflowValidator.php
msgid "Workflow must have a \"connections\" object."
msgstr ""
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Workflow has too many nodes (max %d)."
msgstr ""
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Connection references unknown node: \"%s\"."
msgstr ""
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Node %d must be an object."
msgstr ""
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Node %d has an invalid or missing \"id\" (must be UUID format)."
msgstr ""
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Node %d has an invalid or missing \"name\"."
msgstr ""
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Node %d has an invalid or missing \"type\"."
msgstr ""
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Node %d has an invalid or missing \"position\" ([x, y] required)."
msgstr ""
#: includes/Security/WorkflowValidator.php
#, php-format
msgid "Node %d has an invalid or missing \"typeVersion\"."
msgstr ""
#. InfoBox
#: includes/Features/InfoBoxBuilder.php
#, php-format
msgid "%d node"
msgid_plural "%d nodes"
msgstr[0] ""
msgstr[1] ""
#: includes/Features/InfoBoxBuilder.php
msgid "more type"
msgid_plural "more types"
msgstr[0] ""
msgstr[1] ""
#: includes/Features/InfoBoxBuilder.php includes/Shortcode.php includes/Admin/views/wizard-step-2.php
msgid "AI-powered"
msgstr ""
#. Admin menu
#: includes/Admin/AdminMenu.php
msgid "All Workflows"
msgstr ""
#: includes/Admin/AdminMenu.php includes/Admin/views/settings.php
msgid "Settings"
msgstr ""
#: includes/Admin/AdminMenu.php
msgid "Themes"
msgstr ""
#. Wizard page
#: includes/Admin/WizardPage.php includes/Admin/ThemesPage.php
msgid "Insufficient permissions."
msgstr ""
#: includes/Admin/WizardPage.php
msgid "No JSON provided."
msgstr ""
#: includes/Admin/WizardPage.php
msgid "No URL provided."
msgstr ""
#: includes/Admin/WizardPage.php
msgid "This URL is not allowed."
msgstr ""
#: includes/Admin/WizardPage.php
msgid "Empty response from URL."
msgstr ""
#: includes/Admin/WizardPage.php
msgid "Unnamed Node"
msgstr ""
#: includes/Admin/WizardPage.php
msgid "Failed to create workflow post."
msgstr ""
#: includes/Admin/WizardPage.php includes/Admin/views/wizard-step-2.php
msgid "Invalid workflow ID."
msgstr ""
#: includes/Admin/WizardPage.php includes/Admin/views/wizard-step-2.php includes/Admin/views/wizard-step-3.php
msgid "Invalid workflow."
msgstr ""
#: includes/Admin/WizardPage.php
msgid "Validating..."
msgstr ""
#: includes/Admin/WizardPage.php
msgid "Valid n8n workflow"
msgstr ""
#: includes/Admin/WizardPage.php
msgid "Invalid workflow"
msgstr ""
#: includes/Admin/WizardPage.php
msgid "Fetching URL..."
msgstr ""
#: includes/Admin/WizardPage.php includes/Shortcode.php includes/Admin/WorkflowListTable.php includes/Admin/views/wizard-step-2.php
msgid "Copy"
msgstr ""
#: includes/Admin/WizardPage.php includes/Shortcode.php
msgid "Copied!"
msgstr ""
#: includes/Admin/WizardPage.php
msgid "Validate JSON"
msgstr ""
#: includes/Admin/WizardPage.php includes/Admin/views/wizard-step-1.php
msgid "Fetch"
msgstr ""
#: includes/Admin/WizardPage.php
msgid "Fetch failed"
msgstr ""
#: includes/Admin/WizardPage.php
msgid "Please paste a workflow JSON first."
msgstr ""
#. Workflow list table
#: includes/Admin/WorkflowListTable.php
msgid "Title"
msgstr ""
#: includes/Admin/WorkflowListTable.php
msgid "Nodes"
msgstr ""
#: includes/Admin/WorkflowListTable.php
msgid "AI"
msgstr ""
#: includes/Admin/WorkflowListTable.php
msgid "Mode"
msgstr ""
#: includes/Admin/WorkflowListTable.php
msgid "Views"
msgstr ""
#: includes/Admin/WorkflowListTable.php
msgid "Shortcode"
msgstr ""
#: includes/Admin/WorkflowListTable.php
msgid "Date"
msgstr ""
#: includes/Admin/WorkflowListTable.php includes/Admin/views/themes.php
msgid "Delete"
msgstr ""
#: includes/Admin/WorkflowListTable.php
msgid "Contains AI nodes"
msgstr ""
#: includes/Admin/WorkflowListTable.php
msgid "Draft"
msgstr ""
#: includes/Admin/WorkflowListTable.php
msgid "Edit"
msgstr ""
#. Workflow list page
#: includes/Admin/views/list.php
#, php-format
msgid "%d workflow moved to trash."
msgid_plural "%d workflows moved to trash."
msgstr[0] ""
msgstr[1] ""
#: includes/Admin/views/list.php
#, php-format
msgid "Workflow published! Use shortcode: %s"
msgstr ""
#: includes/Admin/views/list.php
msgid "Workflow published!"
msgstr ""
#. Wizard step 1
#: includes/Admin/views/wizard-step-1.php includes/Admin/views/wizard-step-2.php includes/Admin/views/wizard-step-3.php
msgid "1. Import JSON"
msgstr ""
#: includes/Admin/views/wizard-step-1.php includes/Admin/views/wizard-step-2.php includes/Admin/views/wizard-step-3.php
msgid "2. Configure"
msgstr ""
#: includes/Admin/views/wizard-step-1.php includes/Admin/views/wizard-step-2.php includes/Admin/views/wizard-step-3.php
msgid "3. Preview & Publish"
msgstr ""
#: includes/Admin/views/wizard-step-1.php
msgid "Please provide a workflow JSON."
msgstr ""
#: includes/Admin/views/wizard-step-1.php
msgid "Import n8n Workflow"
msgstr ""
#: includes/Admin/views/wizard-step-1.php
msgid "Import from URL (optional)"
msgstr ""
#: includes/Admin/views/wizard-step-1.php
msgid "Or paste JSON directly below."
msgstr ""
#: includes/Admin/views/wizard-step-1.php
msgid "Upload JSON file"
msgstr ""
#: includes/Admin/views/wizard-step-1.php
msgid "n8n Workflow JSON"
msgstr ""
#: includes/Admin/views/wizard-step-1.php
msgid "Continue to Settings"
msgstr ""
#. Wizard step 2
#: includes/Admin/views/wizard-step-2.php
msgid "Configure Workflow"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
#, php-format
msgid "%d nodes"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Basic Settings"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Workflow Title"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Display Mode"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Visual (diagram + infobox)"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Info only (infobox, no diagram)"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Compact (diagram only, no toolbar)"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Default Zoom"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Display Options"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Show title above diagram"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Show node info box"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Allow JSON download"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Allow iframe embed"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Show minimap button"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Viewer Theme"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Shortcode Preview"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Security Masking Log"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
#, php-format
msgid "%d item was sanitized"
msgid_plural "%d items were sanitized"
msgstr[0] ""
msgstr[1] ""
#: includes/Admin/views/wizard-step-2.php
msgid "← Back"
msgstr ""
#: includes/Admin/views/wizard-step-2.php
msgid "Continue to Preview"
msgstr ""
#. Wizard step 3
#: includes/Admin/views/wizard-step-3.php
msgid "Preview & Publish"
msgstr ""
#: includes/Admin/views/wizard-step-3.php
msgid "Note:"
msgstr ""
#: includes/Admin/views/wizard-step-3.php
msgid "This workflow contains Code nodes. JavaScript code is displayed as-is and cannot be automatically scanned for hardcoded secrets. Please review the code in the node detail panel before publishing."
msgstr ""
#: includes/Admin/views/wizard-step-3.php
msgid "Workflow Preview"
msgstr ""
#: includes/Admin/views/wizard-step-3.php
#, php-format
msgid "Showing %1$d nodes from \"%2$s\"."
msgstr ""
#: includes/Admin/views/wizard-step-3.php
msgid "Security Summary"
msgstr ""
#: includes/Admin/views/wizard-step-3.php
#, php-format
msgid "%d value was masked for security."
msgid_plural "%d values were masked for security."
msgstr[0] ""
msgstr[1] ""
#: includes/Admin/views/wizard-step-3.php
msgid "Publish"
msgstr ""
#: includes/Admin/views/wizard-step-3.php
msgid "Use the shortcode below in any post or page to embed this workflow."
msgstr ""
#: includes/Admin/views/wizard-step-3.php
msgid "← Edit Settings"
msgstr ""
#: includes/Admin/views/wizard-step-3.php
msgid "Publish Workflow"
msgstr ""
#. Settings page
#: includes/Admin/views/settings.php
msgid "BreznFlow Settings"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Display Defaults"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Default Mode"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Visual"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Info"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Compact"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Default Zoom (%)"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Large Workflow Threshold"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Workflows with this many nodes or more start zoomed in at the trigger node (0 = disabled)."
msgstr ""
#: includes/Admin/views/settings.php
msgid "Max Code Lines"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Maximum lines of code shown in node detail panel."
msgstr ""
#: includes/Admin/views/settings.php
msgid "Show by Default"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Show workflow title"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Workflow Theme"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Default Theme"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Can be overridden per workflow or via shortcode attr theme=\"light\". Custom themes: import via BreznFlow → Themes."
msgstr ""
#: includes/Admin/views/settings.php
msgid "Action Bar"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Share"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Visitors can share workflow links"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Embed"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Allow iframe embedding (creates public embed URL)"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Get JSON"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Visitors can view/copy workflow JSON"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Allow Download"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Allow visitors to download sanitized workflow JSON"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Download Button Label"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Features"
msgstr ""
#: includes/Admin/views/settings.php
msgid "View Counting"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Track shortcode render count per workflow"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Related Workflows"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Enable related workflows by shared node types"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Schema.org HowTo"
msgstr ""
#: includes/Admin/views/settings.php
msgid "Output Schema.org HowTo structured data in page head"
msgstr ""
#. Settings defaults
#: includes/Admin/SettingsPage.php
msgid "Download JSON"
msgstr ""
#. Themes page
#: includes/Admin/views/themes.php
msgid "BreznFlow — Themes"
msgstr ""
#: includes/Admin/views/themes.php
msgid "Theme imported successfully."
msgstr ""
#: includes/Admin/views/themes.php
msgid "Theme deleted."
msgstr ""
#: includes/Admin/views/themes.php
msgid "Invalid file type. Please upload a .breznflow.json file."
msgstr ""
#: includes/Admin/views/themes.php
msgid "File upload failed. Please try again."
msgstr ""
#: includes/Admin/views/themes.php
msgid "Could not read the uploaded file."
msgstr ""
#: includes/Admin/views/themes.php
msgid "File is not valid JSON."
msgstr ""
#: includes/Admin/views/themes.php
msgid "Theme validation failed."
msgstr ""
#: includes/Admin/views/themes.php
msgid "An error occurred."
msgstr ""
#: includes/Admin/views/themes.php
msgid "Import Custom Theme"
msgstr ""
#: includes/Admin/views/themes.php
msgid "Upload a .breznflow.json file containing a valid theme definition. The file must include all 41 color tokens."
msgstr ""
#: includes/Admin/views/themes.php
msgid "Theme File"
msgstr ""
#: includes/Admin/views/themes.php
msgid "Import Theme"
msgstr ""
#: includes/Admin/views/themes.php
msgid "Built-in Themes"
msgstr ""
#: includes/Admin/views/themes.php
msgid "Name"
msgstr ""
#: includes/Admin/views/themes.php
msgid "ID"
msgstr ""
#: includes/Admin/views/themes.php
msgid "Type"
msgstr ""
#: includes/Admin/views/themes.php
msgid "Built-in"
msgstr ""
#: includes/Admin/views/themes.php
msgid "Built-in themes are read-only and updated with the plugin."
msgstr ""
#: includes/Admin/views/themes.php
msgid "Custom Themes"
msgstr ""
#: includes/Admin/views/themes.php
msgid "No custom themes imported yet."
msgstr ""
#: includes/Admin/views/themes.php
msgid "Version"
msgstr ""
#: includes/Admin/views/themes.php
msgid "Actions"
msgstr ""
#: includes/Admin/views/themes.php
msgid "Delete this theme?"
msgstr ""
#. Theme importer validation
#: includes/Features/ThemeImporter.php
#, php-format
msgid "Missing required field: %s"
msgstr ""
#: includes/Features/ThemeImporter.php
#, php-format
msgid "Unexpected fields: %s"
msgstr ""
#: includes/Features/ThemeImporter.php
msgid "Theme ID must contain only lowercase letters, numbers, and hyphens."
msgstr ""
#: includes/Features/ThemeImporter.php
#, php-format
msgid "Theme ID \"%s\" is reserved for built-in themes."
msgstr ""
#: includes/Features/ThemeImporter.php
msgid "Theme name must not be empty."
msgstr ""
#: includes/Features/ThemeImporter.php
msgid "Theme name must be 80 characters or fewer."
msgstr ""
#: includes/Features/ThemeImporter.php
msgid "Theme version must be an integer."
msgstr ""
#: includes/Features/ThemeImporter.php
msgid "Tokens must be an object."
msgstr ""
#: includes/Features/ThemeImporter.php
#, php-format
msgid "Unknown tokens: %s"
msgstr ""
#: includes/Features/ThemeImporter.php
#, php-format
msgid "Missing tokens: %s"
msgstr ""
#: includes/Features/ThemeImporter.php
#, php-format
msgid "Invalid color value for \"%1$s\": %2$s"
msgstr ""
#. Embed handler
#: includes/EmbedHandler.php
msgid "Source:"
msgstr ""
#. Frontend renderer JS strings
#: includes/Shortcode.php
msgid "Share"
msgstr ""
#: includes/Shortcode.php
msgid "Embed"
msgstr ""
#: includes/Shortcode.php
msgid "Get JSON"
msgstr ""
#: includes/Shortcode.php
msgid "Error"
msgstr ""
#: includes/Shortcode.php
msgid "Close"
msgstr ""
#: includes/Shortcode.php
msgid "Zoom in"
msgstr ""
#: includes/Shortcode.php
msgid "Zoom out"
msgstr ""
#: includes/Shortcode.php
msgid "Reset view"
msgstr ""
#: includes/Shortcode.php
msgid "Fullscreen"
msgstr ""
#: includes/Shortcode.php
msgid "Minimap"
msgstr ""
#: includes/Shortcode.php
msgid "Highlight in diagram"
msgstr ""
#: includes/Shortcode.php
msgid "Article Link"
msgstr ""
#: includes/Shortcode.php
msgid "Workflow Anchor Link"
msgstr ""
#: includes/Shortcode.php
msgid "Embed this workflow on any website:"
msgstr ""
#: includes/Shortcode.php
msgid "Optional URL parameters:"
msgstr ""
#: includes/Shortcode.php
msgid "Code"
msgstr ""
#: includes/Shortcode.php
msgid "Credential"
msgstr ""
#: includes/Shortcode.php
msgid "more"
msgstr ""
#: includes/Shortcode.php
msgid "node"
msgstr ""
#: includes/Shortcode.php includes/Admin/WizardPage.php
msgid "nodes"
msgstr ""
#: includes/Shortcode.php
msgid "line"
msgstr ""
#: includes/Shortcode.php
msgid "lines"
msgstr ""

BIN
languages/messages.mo Normal file

Binary file not shown.

123
readme.txt Normal file
View file

@ -0,0 +1,123 @@
=== BreznFlow ===
Contributors: noschmarrn
Tags: n8n, workflow, automation, diagram, svg
Requires at least: 6.0
Tested up to: 6.9
Stable tag: 1.0.0
Requires PHP: 8.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Visually display n8n automation workflows in posts and pages with an interactive SVG diagram, node detail panel, and automatic sensitive data masking.
== Description ==
BreznFlow lets you embed beautiful, interactive diagrams of your n8n automation workflows into WordPress posts and pages.
**Key Features:**
* **Interactive SVG Diagram** — zoom, pan, and click nodes to see their configuration
* **Node Detail Panel** — click any node to see its parameters below the diagram
* **Sensitive Data Masking** — API keys, tokens, and secrets in URL parameters are automatically replaced with [REDACTED] before storage
* **Node Type Registry** — 80+ node types with brand colors and icons (OpenAI, Slack, GitHub, and more)
* **InfoBox** — shows "3x HTTP Request, 2x Code" node summary
* **AI Detection** — automatically detects and badges AI-powered workflows
* **Multiple Display Modes** — visual (diagram), info (node counts only), compact (diagram without toolbar)
* **Shortcode System** — `[breznflow id="X"]` with per-shortcode attribute overrides
* **Download Button** — let visitors download the sanitized (masked) JSON
* **3-Step Import Wizard** — paste JSON, configure display, preview and publish
* **Related Workflows** — shows similar workflows by shared node types
* **View Counting** — tracks how many times each workflow has been displayed
* **Zero Dependencies** — vanilla JavaScript, no external CDN, no tracking
**How to use:**
1. Go to BreznFlow → Add Workflow
2. Paste your n8n workflow JSON export (or upload a .json file)
3. Configure display settings
4. Preview the diagram and publish
5. Add `[breznflow id="X"]` to any post or page
**Security:**
BreznFlow never stores your raw workflow JSON. Before saving, it validates the JSON against the n8n schema, sanitizes all strings, and masks detected secrets (API keys in URL parameters, high-entropy condition values). The stored JSON is always the sanitized version.
JavaScript code in Code nodes (`jsCode`) is displayed as plain text and is never executed in the browser.
== Installation ==
1. Upload the `breznflow` folder to `/wp-content/plugins/`
2. Activate the plugin in WordPress admin
3. Go to BreznFlow in your admin menu
4. Add your first workflow via the 3-step wizard
== Frequently Asked Questions ==
= Where do I get a workflow JSON? =
In n8n, open your workflow and use the menu: Workflow → Export → Download JSON.
= Are my API keys safe? =
BreznFlow automatically detects and replaces common secret patterns (API keys in URL parameters, high-entropy condition values) with `[REDACTED]` before storing. The masking log in the wizard shows exactly what was masked and why. JavaScript code in Code nodes is NOT automatically scanned — review it manually before publishing.
= Can I embed multiple workflows on one page? =
Yes. Use `[breznflow id="1"]`, `[breznflow id="2"]`, etc. The JavaScript is loaded only once regardless of how many shortcodes are on the page.
= What n8n version is supported? =
The plugin was developed against n8n workflow JSON exports and supports the standard export format. It has been tested with workflows from n8n 1.x.
= Can visitors download the workflow JSON? =
Yes, if you enable the download option. Only the sanitized (masked) JSON is available for download — never the original.
== External Services ==
This plugin optionally connects to external services if you choose to use the "Import from URL" feature in the workflow import wizard.
= Import from URL =
If you choose to import a workflow by pasting a URL instead of uploading or pasting JSON directly, the plugin will make an HTTP request to that URL using WordPress's built-in `wp_remote_get()` function.
* **When:** Only when you click the "Fetch" button in the Add Workflow wizard
* **What is sent:** Only the URL you provide — no WordPress data, no user data
* **To whom:** Whatever server hosts the URL you provide
* **Privacy policy:** Depends on the server you connect to
No data is transmitted automatically. No data is sent during normal page loads or to visitors browsing your site.
For security, requests to private/internal network addresses (localhost, LAN ranges, cloud metadata endpoints) are blocked.
== Screenshots ==
1. 3-step import wizard — Step 1: paste or upload your n8n JSON
2. Step 2: configure display settings and preview the shortcode
3. Step 3: live SVG preview with security masking summary
4. Frontend diagram with node detail panel open
5. Compact mode showing only the node info box
6. Workflow list in admin with shortcode copy button
== Changelog ==
= 1.0.0 =
* Interactive SVG renderer with zoom, pan, and node detail panel
* 3-step import wizard with JSON validation and sensitive data masking
* 80+ node type registry with brand colors and icons
* Shortcode `[breznflow]` with mode, zoom, and display attributes
* Auto-fit zoom for large workflows (configurable threshold)
* Minimap toggle per workflow and via shortcode attribute
* 5 built-in themes (Dark, Light, Minimal, Tech, Brezn) plus custom theme import
* Action bar with share, embed, get JSON, and download buttons
* Embed handler for standalone iframe embedding
* Download handler for sanitized JSON export
* Sensitive data masking (API keys, tokens, secrets) with mask log
* View counter and related workflows by shared node types
* Schema.org HowTo structured data support
* Zero dependencies — vanilla JavaScript, no external CDN, no tracking
== Upgrade Notice ==
= 1.0.0 =
Initial release.

56
uninstall.php Normal file
View file

@ -0,0 +1,56 @@
<?php
/**
* Uninstall handler for BreznFlow.
*
* Removes all workflow posts, settings, taxonomy terms, and transients
* when the plugin is deleted via the WordPress admin.
*
* @package BreznFlow
* @since 1.0.0
*/
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
// Delete all workflow CPT posts and their meta.
$breznflow_post_ids = get_posts(
array(
'post_type' => 'breznflow_workflow',
'post_status' => 'any',
'posts_per_page' => -1,
'fields' => 'ids',
)
);
foreach ( $breznflow_post_ids as $breznflow_post_id ) {
wp_delete_post( $breznflow_post_id, true );
}
// Delete plugin settings.
delete_option( 'breznflow_settings' );
// Delete taxonomy terms.
$breznflow_terms = get_terms(
array(
'taxonomy' => 'breznflow_category',
'hide_empty' => false,
)
);
if ( ! is_wp_error( $breznflow_terms ) ) {
foreach ( $breznflow_terms as $breznflow_term ) {
wp_delete_term( $breznflow_term->term_id, 'breznflow_category' );
}
}
// Flush transients.
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- uninstall cleanup, no caching needed.
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
'_transient_breznflow_%',
'_transient_timeout_breznflow_%'
)
);