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:
commit
fd83e4810b
43 changed files with 9823 additions and 0 deletions
168
assets/admin.css
Normal file
168
assets/admin.css
Normal 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
199
assets/admin.js
Normal 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
52
assets/brezn.css
Normal 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
613
assets/renderer.css
Normal 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
1544
assets/renderer.js
Normal file
File diff suppressed because it is too large
Load diff
52
assets/themes/brezn.css
Normal file
52
assets/themes/brezn.css
Normal 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
53
assets/themes/dark.css
Normal 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
52
assets/themes/light.css
Normal 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
52
assets/themes/minimal.css
Normal 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
52
assets/themes/tech.css
Normal 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
52
breznflow.php
Normal 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();
|
||||||
|
}
|
||||||
|
);
|
||||||
207
includes/Admin/AdminMenu.php
Normal file
207
includes/Admin/AdminMenu.php
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
138
includes/Admin/SettingsPage.php
Normal file
138
includes/Admin/SettingsPage.php
Normal 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',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
188
includes/Admin/ThemesPage.php
Normal file
188
includes/Admin/ThemesPage.php
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
420
includes/Admin/WizardPage.php
Normal file
420
includes/Admin/WizardPage.php
Normal 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.16–31.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
204
includes/Admin/WorkflowListTable.php
Normal file
204
includes/Admin/WorkflowListTable.php
Normal 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 ) . '" />';
|
||||||
|
}
|
||||||
|
}
|
||||||
65
includes/Admin/views/list.php
Normal file
65
includes/Admin/views/list.php
Normal 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>
|
||||||
198
includes/Admin/views/settings.php
Normal file
198
includes/Admin/views/settings.php
Normal 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>
|
||||||
148
includes/Admin/views/themes.php
Normal file
148
includes/Admin/views/themes.php
Normal 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>
|
||||||
75
includes/Admin/views/wizard-step-1.php
Normal file
75
includes/Admin/views/wizard-step-1.php
Normal 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>
|
||||||
212
includes/Admin/views/wizard-step-2.php
Normal file
212
includes/Admin/views/wizard-step-2.php
Normal 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>
|
||||||
179
includes/Admin/views/wizard-step-3.php
Normal file
179
includes/Admin/views/wizard-step-3.php
Normal 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
113
includes/Core.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
includes/DownloadHandler.php
Normal file
84
includes/DownloadHandler.php
Normal 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
185
includes/EmbedHandler.php
Normal 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>•</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
includes/Features/InfoBoxBuilder.php
Normal file
87
includes/Features/InfoBoxBuilder.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
166
includes/Features/NodeCategorizer.php
Normal file
166
includes/Features/NodeCategorizer.php
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
813
includes/Features/NodeTypeRegistry.php
Normal file
813
includes/Features/NodeTypeRegistry.php
Normal 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 ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
106
includes/Features/RelatedWorkflows.php
Normal file
106
includes/Features/RelatedWorkflows.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
265
includes/Features/ThemeImporter.php
Normal file
265
includes/Features/ThemeImporter.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
includes/Features/ThemeRegistry.php
Normal file
106
includes/Features/ThemeRegistry.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
includes/Features/ViewCounter.php
Normal file
55
includes/Features/ViewCounter.php
Normal 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
102
includes/PostType.php
Normal 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,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
174
includes/Security/MaskingRules.php
Normal file
174
includes/Security/MaskingRules.php
Normal 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 8–512 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
160
includes/Security/WorkflowSanitizer.php
Normal file
160
includes/Security/WorkflowSanitizer.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
196
includes/Security/WorkflowValidator.php
Normal file
196
includes/Security/WorkflowValidator.php
Normal 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
317
includes/Shortcode.php
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
languages/breznflow-de_DE.mo
Normal file
BIN
languages/breznflow-de_DE.mo
Normal file
Binary file not shown.
892
languages/breznflow-de_DE.po
Normal file
892
languages/breznflow-de_DE.po
Normal 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
900
languages/breznflow.pot
Normal 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
BIN
languages/messages.mo
Normal file
Binary file not shown.
123
readme.txt
Normal file
123
readme.txt
Normal 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
56
uninstall.php
Normal 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_%'
|
||||||
|
)
|
||||||
|
);
|
||||||
Loading…
Reference in a new issue