Compare commits

..

7 commits
v1.0.1 ... main

Author SHA1 Message Date
Michael
5c4d5f6686 release: v1.0.4
Security

- Add looks_like_secret() entropy heuristic: vendor regex (AIza, sk-,
  ghp_, gho_, Slack xox, Bearer) + length/char-class fallback +
  path/whitespace denylist. Defensible hybrid: zero false-positives
  on known token formats, catches custom tokens without tripping on
  URLs or slugs.
- Gate generic 'key'-named fields and ?key= URL params with the
  entropy heuristic. Closes the n8n queryParameters Google-API-key
  bypass without false-positives on benign values.
- Entropy fallback in mask_name_value_pair for custom-header value
  patterns (X-App-Token etc.) whose names we cannot enumerate.
- Redact credentials[].name per node (id retained), clear
  meta.instanceId so exports no longer correlate to the source n8n
  instance.
- Opt-in tag clearing at publish time: wizard step 3 checkbox with
  the current tag list inline, only shown when tags exist.
- Wizard step 3 now renders a collapsible Reason / Key / Note table
  so publishers can verify exactly what was masked before publishing.

Mobile

- touch-action: none on .breznflow-svg to stop the
  browser-vs-plugin gesture tug-of-war.
- Rewrote pointer handling as a Map-based multi-pointer state
  machine with { passive: false } listeners: single-finger pan is
  now smooth on iOS and Android, pinch-to-zoom anchored at the
  finger midpoint, double-tap toggles 100/200 % zoom.
- Minimap ported to pointer events + setPointerCapture — tap and
  drag navigation work on touch.

Docs

- Expand Sensitive Data Masking section of both READMEs to describe
  the 1.0.4 passes and the opt-in tag removal.
- Version badge 1.0.3 -> 1.0.4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 18:58:51 +00:00
Michael
1b3de37a54 docs: bump version badge to 1.0.3 in READMEs
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 13:34:44 +00:00
Michael
f6cfcb1005 release: v1.0.3
Fix double rendering when "Easy Table of Contents" (or any plugin that
re-runs the_content filters) is active.

- Shortcode re-entry guard via md5 fingerprint of post_id + resolved
  render settings — silently skips duplicate passes while preserving
  legitimate multi-embed with different attributes
- Wrapper DOM id is now unique per instance (breznflow-wrap-<POST>-<N>),
  enabling multiple embeds of the same workflow in one post
- Share-anchor span id="breznflow-<POST>" emitted only on the first
  instance per post to keep the DOM valid and existing deep-links working
- View counter increments moved after the dedupe check so re-entrant
  scans do not overcount views
- JS renderer tracks mounted containers in a WeakSet — defensive guard
  that catches any duplicates server-side dedupe might miss
- readme: add Learn more section with website, FAQ, and demo links

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 13:32:33 +00:00
Michael
066414724b release: v1.0.2
- Fix WordPress.org plugin review issues (nonce verification, input sanitization, output escaping)
- Embed page uses wp_enqueue_style/wp_enqueue_script with wp_head/wp_footer
- Update plugin author to NoSchmarrn.dev
- Shorten readme.txt short description to ≤150 chars
- Add GitHub Actions release workflow
- Add .gitignore
2026-04-14 11:21:48 +00:00
Michael
fb206850d5 Fix plugin headers: separate Plugin URI and Author URI
- Plugin URI: https://breznflow.com/
- Author URI: https://mifupa.com/
- Author: mifupa
- License: GPL-2.0-or-later (SPDX)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 15:41:55 +00:00
Michael
cc6bfb7c83 Update contributor to mifupadev, built for mifupa.com
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 15:30:51 +00:00
Michael
5ef9e65617 Restructure repo: move plugin into breznflow/ subfolder, add README/LICENSE
- Move all plugin files into breznflow/ subdirectory (matches BreznGEO structure)
- Add README.md (English) and README.de.md (German) with full documentation
- Add GPL-2.0 LICENSE file
- Rewrite readme.txt: expanded description, FAQs, external services, changelog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 15:26:24 +00:00
49 changed files with 1966 additions and 283 deletions

32
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Release
on:
push:
tags:
- 'v*.*.*'
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get version from tag
id: version
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- name: Create ZIP
run: zip -r breznflow.zip breznflow/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: "BreznFlow v${{ steps.version.outputs.version }}"
files: breznflow.zip
generate_release_notes: true

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
vendor/
*.zip
/node_modules/
.claude/
*.log

339
LICENSE Normal file
View file

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

366
README.de.md Normal file
View file

@ -0,0 +1,366 @@
# BreznFlow
![PHP 8.0+](https://img.shields.io/badge/PHP-8.0%2B-blue)
![WordPress 6.0+](https://img.shields.io/badge/WordPress-6.0%2B-21759b)
![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0--or--later-green)
![Version](https://img.shields.io/badge/Version-1.0.4-orange)
🇬🇧 [English version → README.md](README.md)
---
BreznFlow ist ein WordPress-Plugin, das n8n-Automations-Workflows als interaktive SVG-Diagramme rendert — direkt in Beiträgen und Seiten. Workflow-JSON einfügen, und das Plugin erzeugt ein zoombares, klickbares Diagramm mit Node-Detail-Panel, automatischer Maskierung sensibler Daten und farbcodierten Node-Icons.
Keine externen Abhängigkeiten. Kein CDN. Kein Tracking. Vanilla JavaScript. Ein Shortcode: `[breznflow id="X"]`.
---
## Warum dieses Plugin existiert
n8n-Workflows sind mächtig, aber visuell teilen ist überraschend schwierig. Screenshots sind statisch und veralten schnell. Den n8n-Editor einzubetten ist unpraktisch. JSON in einen Blogpost kopieren ist unlesbar.
BreznFlow löst das, indem es den rohen JSON-Export in ein interaktives Diagramm verwandelt — die gleichen Nodes, die gleichen Verbindungen, aber als sauberes SVG innerhalb von WordPress gerendert. Leser können zoomen, pannen und jeden Node anklicken, um seine Parameter zu sehen.
Entwickelt in Passau, Bayern — für [mifupa.com](https://mifupa.com), einem persönlichen Blog, auf dem regelmäßig n8n-Automationen vorgestellt werden und eine bessere Darstellung brauchten.
---
## Inhaltsverzeichnis
- [Warum dieses Plugin existiert](#warum-dieses-plugin-existiert)
- [Verzeichnisstruktur](#verzeichnisstruktur)
- [Features](#features)
- [Datenspeicherung](#datenspeicherung)
- [Sicherheit](#sicherheit)
- [Shortcode](#shortcode)
- [Installation](#installation)
- [Technischer Stack](#technischer-stack)
- [Lizenz](#lizenz)
---
## Verzeichnisstruktur
```
breznflow/
├── breznflow.php # Plugin-Header, Konstanten (BREZNFLOW_VERSION, BREZNFLOW_DIR, BREZNFLOW_URL)
├── uninstall.php # Aufräumen bei Plugin-Löschung
├── readme.txt # WordPress.org Plugin-Readme
├── assets/
│ ├── admin.css # Admin-Stylesheet (Wizard, Settings, Listtable)
│ ├── admin.js # Admin-JavaScript (Wizard-Schritte, JSON-Validierung)
│ ├── renderer.css # Frontend SVG-Renderer Styles
│ ├── renderer.js # Frontend-Renderer (SVG-Engine, Pan/Zoom, Detail-Panel)
│ ├── brezn.css # Legacy Brezn-Theme (Kompatibilität)
│ └── themes/
│ ├── dark.css # Dark-Theme (Standard)
│ ├── light.css # Light-Theme
│ ├── minimal.css # Minimal-Theme
│ ├── tech.css # Tech-Theme
│ └── brezn.css # Brezn-Theme
├── includes/
│ ├── Core.php # Singleton-Bootstrap, lädt alle Abhängigkeiten
│ ├── PostType.php # CPT breznflow_workflow + Taxonomie breznflow_category
│ ├── Shortcode.php # [breznflow] Shortcode mit 13 Attributen
│ ├── DownloadHandler.php # JSON-Download-Endpunkt (?breznflow_download={id})
│ ├── EmbedHandler.php # Standalone Embed-Seite (?breznflow_embed={id})
│ ├── Admin/
│ │ ├── AdminMenu.php # Menüstruktur + Dashboard-Render
│ │ ├── SettingsPage.php # Plugin-Einstellungen (16 Optionen, validiert)
│ │ ├── ThemesPage.php # Theme-Verwaltung (Import/Löschen eigener Themes)
│ │ ├── WizardPage.php # 3-Schritt Import-Wizard (Einfügen/Upload → Konfigurieren → Vorschau)
│ │ ├── WorkflowListTable.php # WP_List_Table für Workflow-Verwaltung
│ │ └── views/ # PHP-Templates für alle Admin-Seiten
│ ├── Features/
│ │ ├── NodeTypeRegistry.php # 86 Node-Typen mit Markenfarben und Icons
│ │ ├── NodeCategorizer.php # Kategorisiert Nodes (Trigger, Action, Logic, AI, etc.)
│ │ ├── InfoBoxBuilder.php # „3× HTTP Request, 2× Code" Node-Zusammenfassung
│ │ ├── ViewCounter.php # View-Zähler pro Workflow
│ │ ├── RelatedWorkflows.php # Ähnliche Workflows nach gemeinsamen Node-Typen
│ │ ├── ThemeRegistry.php # 5 Built-in-Themes + Custom-Theme-Support
│ │ └── ThemeImporter.php # Import/Export von .breznflow.json Theme-Dateien
│ └── Security/
│ ├── MaskingRules.php # Secret-Erkennung (URL-Parameter, Header, Entropie)
│ ├── WorkflowValidator.php # JSON-Schema-Validierung für n8n-Exporte
│ └── WorkflowSanitizer.php # Zwei-Pass-Sanitierung: Strings + Secret-Maskierung
└── languages/
├── breznflow.pot # Übersetzungsvorlage
├── breznflow-de_DE.po # Deutsche Übersetzung
└── breznflow-de_DE.mo # Kompilierte deutsche Übersetzung
```
---
## Features
### Interaktiver SVG-Renderer
Das Herzstück von BreznFlow. Jeder n8n-Node wird zu einem klickbaren SVG-Element mit farbcodierten Icons, Verbindungslinien und Labels. Der Renderer unterstützt:
- **Pan & Zoom** — Mausrad zoomt zur Cursorposition, Klick-Drag zum Verschieben
- **Node-Klick** — öffnet das Detail-Panel unterhalb des Diagramms mit allen Node-Parametern
- **Auto-Fit** — Workflows über einem konfigurierbaren Node-Schwellenwert (Standard: 30) zoomen automatisch zum Trigger-Node beim Laden
- **Minimap** — optionales Minimap-Overlay zur Navigation in großen Workflows
- **Vollbild** — Portal-basierter Vollbildmodus
Alles wird clientseitig in Vanilla JavaScript gerendert — kein Canvas, kein WebGL, keine externen Bibliotheken.
---
### 3-Schritt Import-Wizard
1. **Einfügen oder hochladen** — JSON direkt einfügen, `.json`-Datei hochladen oder von URL abrufen
2. **Konfigurieren** — Darstellungsmodus, Theme, Zoom, Titel, Kategorien einstellen
3. **Vorschau** — Live-SVG-Vorschau mit Maskierungs-Zusammenfassung vor dem Veröffentlichen
Der Wizard validiert JSON gegen das n8n-Schema, sanitiert alle Strings und maskiert erkannte Geheimnisse. Das Maskierungs-Protokoll zeigt genau, was entfernt wurde und warum.
**Import von URL:** Ruft Workflow-JSON von beliebigen öffentlichen URLs per `wp_remote_get()` ab. Anfragen an private/interne Netzwerkadressen (localhost, LAN-Bereiche, Cloud-Metadaten-Endpunkte) werden blockiert.
---
### Node Type Registry
86 vordefinierte Node-Typen mit markengetreuen Farben und 2-Buchstaben-Icons:
| Kategorie | Beispiele |
|---|---|
| Trigger | Schedule, Webhook, Manual, Form |
| Kernlogik | HTTP Request, Code, IF, Switch, Merge, Filter |
| Datentransformation | HTML, XML, Markdown, Crypto |
| Datenbanken | MySQL, PostgreSQL, Redis, MongoDB, SQLite, Supabase |
| Kommunikation | Slack, Telegram, Discord, Gmail, WhatsApp |
| Google | Sheets, Drive, Calendar, Docs, YouTube |
| Dev-Tools | GitHub, GitLab, Jira, Confluence, Linear, Notion |
| KI | OpenAI, Claude, Gemini, Ollama, Hugging Face, Mistral, LangChain |
| Speicher | FTP, SSH, Airtable, Baserow |
| CRM/Marketing | HubSpot, Salesforce, Mailchimp, Brevo |
Unbekannte Node-Typen bekommen einen deterministischen Fallback: 2-Buchstaben-Initialen und eine Farbe aus einem djb2-Hash — der gleiche unbekannte Typ sieht immer gleich aus.
---
### Theme-System
5 Built-in-Themes: **Dark** (Standard), **Light**, **Minimal**, **Tech**, **Brezn**.
Eigene Themes können als `.breznflow.json`-Dateien mit 41 CSS-Farbtokens importiert werden. Custom-Themes werden in `wp_options` gespeichert und als Inline-CSS-Variablen gerendert.
Themes sind global, pro Workflow oder pro Shortcode wählbar via `theme="dark"`.
---
### Action Bar
Unterhalb des Diagramms (nicht im Compact-Modus) bietet die Action Bar:
| Aktion | Steuerung | Funktion |
|---|---|---|
| **Share** | Globale Einstellung | Zeigt Artikel-Link + Anker-Link für Hash-Navigation |
| **Embed** | Global + pro Post | Zeigt iframe-Embed-Code für Standalone-Einbettung |
| **Get JSON** | Globale Einstellung | Zeigt formatiertes JSON mit Größe in KB |
| **Download** | Global + pro Post | Lädt sanitiertes JSON als Datei herunter |
Jede Aktion ist global in den Einstellungen steuerbar und per Shortcode überschreibbar.
---
### Maskierung sensibler Daten
BreznFlow speichert nie das rohe Workflow-JSON. Vor dem Speichern läuft eine Drei-Pass-Sanitierung:
**Pass 1 — String-Sanitierung:** Alle Strings durchlaufen `sanitize_text_field()`. Ausnahme: `jsCode`-Felder bleiben erhalten, werden aber mit `esc_html()` ausgegeben (nie ausgeführt).
**Pass 2 — Secret-Erkennung:**
- **URL-Parameter:** `api_key`, `token`, `secret`, `password`, `access_token`, `auth`, `client_secret` in Query-Strings → `[REDACTED]`
- **Generische `?key=`-URL-Parameter** *(1.0.4)*: werden nur redactet, wenn der gefangene Wert `looks_like_secret()` matcht — schließt die Google-API-Key-Lücke ohne False-Positives auf harmlosen Werten.
- **Header-Werte:** Authorization, Bearer, X-API-Key und ähnliche Header-Namen in `{name, value}`-Paaren → Wert maskiert
- **Wert-Entropie-Fallback** *(1.0.4)*: `{name, value}`-Paare, deren Name die Allowlist nicht matcht, werden dennoch maskiert, wenn der Wert selbst secret-förmig aussieht — deckt Custom-Header (`X-App-Token`) und n8ns generisches `queryParameters.key`-Pattern ab.
- **Bekannte Vendor-Token** *(1.0.4)*: `looks_like_secret()` matcht `AIza…`, `sk-…`, `ghp_…`, `gho_…`, Slack `xox…`, `Bearer …` (JWT) — plus Längen- und Zeichenklassen-Entropie-Fallback sowie Pfad-/Whitespace-Denylist gegen False-Positives.
- **Hochentropie-Bedingungen:** Werte in IF/Switch-Conditions, die UUID-Muster, Groß-/Kleinschreibung+Ziffern oder lange Strings ohne Leerzeichen matchen → per Entropie-Heuristik maskiert
- **Credential-Anzeigenamen** *(1.0.4)*: `credentials[].name` pro Node wird durch `[REDACTED]` ersetzt (die `id` bleibt — sie referenziert die n8n-DB und ist ohne den Server wertlos).
**Pass 3 — Identifizierende Metadaten** *(1.0.4)*: `meta.instanceId` wird geleert, damit Workflow-Exporte nicht mehr der ausgebenden n8n-Instanz zugeordnet werden können.
**Optional — Tag-Entfernung** *(1.0.4)*: Wizard Schritt 3 bietet eine Opt-in-Checkbox zum Entfernen von Workflow-Tags. Tags sind oft harmlos (`production`, `v2`), manchmal aber identifizierend — der Publisher entscheidet pro Workflow.
Ein **Maskierungs-Protokoll** zeichnet jedes maskierte Element mit Grund, Key und Hinweis auf. Schritt 3 zeigt es als ausklappbare Grund / Key / Hinweis-Tabelle, damit der Publisher vor Publish genau prüfen kann was veröffentlicht wird.
---
### Darstellungsmodi
| Modus | Was angezeigt wird |
|---|---|
| `visual` | Vollständiges Diagramm mit Toolbar, Detail-Panel, Action Bar |
| `info` | Nur Node-Zähler (InfoBox) — kein Diagramm |
| `compact` | Diagramm ohne Toolbar und Action Bar |
Konfigurierbar global, pro Workflow oder pro Shortcode.
---
### Embed-Handler
Liefert eine Standalone-HTML-Seite unter `?breznflow_embed={id}` für iframe-Einbettung. Die Seite enthält nur den SVG-Renderer — kein WordPress-Theme, keine Admin-Bar.
**Dual-Gate-Sicherheit:** Sowohl die globale `allow_embed`-Einstellung als auch das Per-Post-Meta `_breznflow_show_embed` müssen aktiviert sein.
URL-Parameter: `?theme={id}` und `?minimap=0|1`.
HTTP-Header enthalten `X-Robots-Tag: noindex, nofollow` und entfernen `X-Frame-Options` für Embedding.
---
### Weitere Features
- **View-Zähler** — zählt, wie oft jeder Workflow angezeigt wird
- **Ähnliche Workflows** — zeigt verwandte Workflows nach gemeinsamen Node-Typen
- **InfoBox** — kompakte Zusammenfassung wie „3× HTTP Request, 2× Code, 1× OpenAI"
- **KI-Erkennung** — erkennt und markiert automatisch Workflows mit KI-Nodes
- **Schema.org HowTo** — optionale JSON-LD-Strukturdatenausgabe
- **Anker-Navigation**`<span id="breznflow-{id}">` für Hash-basiertes Deep-Linking mit 60px Scroll-Offset
---
## Datenspeicherung
### WordPress Options (wp_options)
| Option-Key | Inhalt |
|---|---|
| `breznflow_settings` | Alle Plugin-Einstellungen (16 Keys, serialisiertes Array) |
| `breznflow_custom_themes` | Custom-Theme-Definitionen (serialisiertes Array) |
### Post Meta (wp_postmeta)
| Meta-Key | Inhalt |
|---|---|
| `_breznflow_raw_json` | Sanitiertes Workflow-JSON (nie das Roh-Original) |
| `_breznflow_original_name` | Originaler Workflow-Name aus n8n |
| `_breznflow_node_count` | Gesamtzahl der Nodes |
| `_breznflow_node_summary` | Kategorisierte Node-Anzahl (JSON) |
| `_breznflow_has_ai_nodes` | Ob der Workflow KI-Nodes enthält |
| `_breznflow_ai_node_types` | Liste der KI-Node-Typen (JSON) |
| `_breznflow_mask_log` | Maskierte Werte beim Import (JSON) |
| `_breznflow_default_zoom` | Zoom-Level pro Workflow (10200) |
| `_breznflow_show_title` | Titel anzeigen |
| `_breznflow_show_infobox` | InfoBox anzeigen |
| `_breznflow_show_download` | Download erlauben |
| `_breznflow_show_embed` | Einbettung erlauben |
| `_breznflow_show_minimap` | Minimap anzeigen |
| `_breznflow_default_mode` | Darstellungsmodus |
| `_breznflow_default_theme` | Theme-ID |
### Uninstall-Cleanup
`uninstall.php` löscht bei Plugin-Deinstallation:
- Alle `breznflow_workflow`-Posts und deren Meta
- Option `breznflow_settings`
- Alle `breznflow_category`-Taxonomie-Terms
- Alle Transients mit Präfix `breznflow_`
---
## Sicherheit
### Eingabevalidierung
- Workflow-JSON wird vor dem Import gegen das n8n-Schema validiert
- Alle `$_POST` / `$_GET`-Werte über `wp_unslash()` + spezifische Sanitizer verarbeitet
- Alle Ausgaben escaped mit `esc_html`, `esc_attr`, `esc_url`
- SQL-Queries ausschließlich via `$wpdb->prepare()`
### CSRF-Schutz
Jede Admin-Aktion validiert eine WordPress-Nonce. Capability-Checks erfordern `manage_options` für Einstellungen und `edit_posts` für Workflow-Verwaltung.
### Download- & Embed-Sicherheit
Beide Endpunkte validieren:
1. Post existiert, ist Typ `breznflow_workflow` und Status `publish`
2. Die entsprechende Per-Post-Meta-Berechtigung ist aktiviert
3. Die globale Einstellung ist aktiviert
4. Das gespeicherte JSON ist valide
Response-Header enthalten `X-Content-Type-Options: nosniff`. Downloads nutzen `Cache-Control: no-store`.
### Import von URL
Das „Von URL abrufen"-Feature im Wizard blockiert Anfragen an private/interne Netzwerkadressen (localhost, `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, Cloud-Metadaten-Endpunkte) zur Verhinderung von SSRF-Angriffen.
---
## Shortcode
```
[breznflow id="42"]
[breznflow id="42" mode="compact" theme="light" zoom="80"]
[breznflow id="42" show_share="0" show_embed="1" show_minimap="0"]
```
| Attribut | Standard | Beschreibung |
|---|---|---|
| `id` | — | Workflow-Post-ID (erforderlich) |
| `mode` | `visual` | `visual`, `info` oder `compact` |
| `theme` | `dark` | Theme-ID |
| `zoom` | `100` | Initiales Zoom-Level (10200) |
| `show_title` | `true` | Workflow-Titel anzeigen |
| `show_infobox` | `true` | Node-Zusammenfassung anzeigen |
| `show_minimap` | `true` | Minimap-Overlay anzeigen |
| `show_download` | `false` | Download-Button anzeigen |
| `show_share` | `true` | Share-Aktion anzeigen |
| `show_embed` | `false` | Embed-Aktion anzeigen |
| `show_get_json` | `false` | „Get JSON"-Aktion anzeigen |
| `max_code_lines` | `50` | Max. Zeilen in Code-Node-Anzeige |
**Auflösungs-Hierarchie:** Shortcode-Attribut → Post-Meta → Plugin-Einstellungen.
---
## Installation
**Via GitHub Release (empfohlen):**
1. `breznflow.zip` vom [neuesten Release](https://github.com/noschmarrn/breznflow/releases/latest) herunterladen
2. In WordPress unter *Plugins → Installieren → Plugin hochladen* einspielen
**Manuell (clone):**
```bash
cd /path/to/wordpress/wp-content/plugins/
git clone https://github.com/noschmarrn/breznflow.git
wp plugin activate breznflow
```
**Nach der Aktivierung:**
1. *BreznFlow → Workflow hinzufügen*
2. n8n Workflow-JSON einfügen (oder `.json`-Datei hochladen)
3. Darstellungseinstellungen konfigurieren und Vorschau prüfen
4. Veröffentlichen — `[breznflow id="X"]` in beliebigen Beitrag oder Seite einfügen
Kein Build-Step. Alle Assets sind direkte JS/CSS-Dateien.
---
## Technischer Stack
| Komponente | Technologie |
|---|---|
| Backend | PHP 8.0+, WordPress Plugin API |
| Namespace | `BreznFlow\` |
| Architektur | Singleton-Core, Feature-Klassen mit `register()` |
| Rendering | Vanilla JavaScript SVG-Generierung (kein Canvas, keine Bibliotheken) |
| Datenbank | WordPress Options API + Post Meta |
| Caching | WordPress Transients (Related Workflows) |
| Frontend | Vanilla JS, kein Build-Step, kein externes CDN |
| I18n | `.pot`-File, Text-Domain `breznflow` |
| Coding Standard | WordPress PHPCS |
| Lizenz | GPL-2.0-or-later |
---
## Lizenz
GPL-2.0-or-later — [https://www.gnu.org/licenses/gpl-2.0.html](https://www.gnu.org/licenses/gpl-2.0.html)
Copyright (c) 20252026 [mifupa.com](https://mifupa.com)

366
README.md Normal file
View file

@ -0,0 +1,366 @@
# BreznFlow
![PHP 8.0+](https://img.shields.io/badge/PHP-8.0%2B-blue)
![WordPress 6.0+](https://img.shields.io/badge/WordPress-6.0%2B-21759b)
![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0--or--later-green)
![Version](https://img.shields.io/badge/Version-1.0.4-orange)
🇩🇪 [Deutsche Version → README.de.md](README.de.md)
---
BreznFlow is a WordPress plugin that renders n8n automation workflows as interactive SVG diagrams — directly in posts and pages. Paste your workflow JSON, and the plugin turns it into a zoomable, clickable diagram with node detail panels, sensitive data masking, and brand-colored node icons.
No external dependencies. No CDN. No tracking. Vanilla JavaScript. One shortcode: `[breznflow id="X"]`.
---
## Why This Plugin Exists
n8n workflows are powerful, but sharing them visually is surprisingly hard. Screenshots are static and get outdated. Embedding the n8n editor is impractical. Copy-pasting JSON into a blog post is unreadable.
BreznFlow solves this by turning the raw JSON export into an interactive diagram — the same nodes, the same connections, but rendered as a clean SVG inside WordPress. Readers can zoom, pan, and click any node to inspect its parameters.
Built in Passau, Bavaria — for [mifupa.com](https://mifupa.com), a personal blog where n8n automations are documented regularly and needed a better way to be presented.
---
## Table of Contents
- [Why This Plugin Exists](#why-this-plugin-exists)
- [Directory Structure](#directory-structure)
- [Features](#features)
- [Data Storage](#data-storage)
- [Security](#security)
- [Shortcode](#shortcode)
- [Installation](#installation)
- [Tech Stack](#tech-stack)
- [License](#license)
---
## Directory Structure
```
breznflow/
├── breznflow.php # Plugin header, constants (BREZNFLOW_VERSION, BREZNFLOW_DIR, BREZNFLOW_URL)
├── uninstall.php # Cleanup on plugin deletion
├── readme.txt # WordPress.org plugin readme
├── assets/
│ ├── admin.css # Admin stylesheet (wizard, settings, list table)
│ ├── admin.js # Admin JavaScript (wizard steps, JSON validation)
│ ├── renderer.css # Frontend SVG renderer styles
│ ├── renderer.js # Frontend renderer (SVG engine, pan/zoom, detail panel)
│ ├── brezn.css # Legacy Brezn theme (compat)
│ └── themes/
│ ├── dark.css # Dark theme (default)
│ ├── light.css # Light theme
│ ├── minimal.css # Minimal theme
│ ├── tech.css # Tech theme
│ └── brezn.css # Brezn theme
├── includes/
│ ├── Core.php # Singleton bootstrap, loads all dependencies
│ ├── PostType.php # CPT breznflow_workflow + taxonomy breznflow_category
│ ├── Shortcode.php # [breznflow] shortcode with 13 attributes
│ ├── DownloadHandler.php # JSON download endpoint (?breznflow_download={id})
│ ├── EmbedHandler.php # Standalone embed page (?breznflow_embed={id})
│ ├── Admin/
│ │ ├── AdminMenu.php # Menu structure + dashboard render
│ │ ├── SettingsPage.php # Plugin settings (16 options, validated)
│ │ ├── ThemesPage.php # Theme management (import/delete custom themes)
│ │ ├── WizardPage.php # 3-step import wizard (paste/upload → configure → preview)
│ │ ├── WorkflowListTable.php # WP_List_Table for workflow management
│ │ └── views/ # PHP templates for all admin pages
│ ├── Features/
│ │ ├── NodeTypeRegistry.php # 86 node types with brand colors and icons
│ │ ├── NodeCategorizer.php # Categorizes nodes (trigger, action, logic, AI, etc.)
│ │ ├── InfoBoxBuilder.php # "3× HTTP Request, 2× Code" node summary
│ │ ├── ViewCounter.php # Per-workflow view counting
│ │ ├── RelatedWorkflows.php # Related workflows by shared node types
│ │ ├── ThemeRegistry.php # 5 built-in themes + custom theme support
│ │ └── ThemeImporter.php # Import/export .breznflow.json theme files
│ └── Security/
│ ├── MaskingRules.php # Secret detection patterns (URL params, headers, entropy)
│ ├── WorkflowValidator.php # JSON schema validation for n8n exports
│ └── WorkflowSanitizer.php # Two-pass sanitization: strings + secret masking
└── languages/
├── breznflow.pot # Translation template
├── breznflow-de_DE.po # German translation
└── breznflow-de_DE.mo # Compiled German translation
```
---
## Features
### Interactive SVG Renderer
The core of BreznFlow. Every n8n node becomes a clickable SVG element with brand-colored icons, connection lines, and labels. The renderer supports:
- **Pan & zoom** — mouse wheel zooms to cursor position, click-drag to pan
- **Node click** — opens the detail panel below the diagram with all node parameters
- **Auto-fit** — workflows exceeding a configurable node threshold (default: 30) automatically zoom to the trigger node on load
- **Minimap** — optional minimap overlay for navigation in large workflows
- **Fullscreen** — portal-based fullscreen mode
All rendering happens client-side in vanilla JavaScript — no canvas, no WebGL, no external libraries.
---
### 3-Step Import Wizard
1. **Paste or upload** — paste JSON directly, upload a `.json` file, or fetch from URL
2. **Configure** — set display mode, theme, zoom level, title, categories
3. **Preview** — live SVG preview with security masking summary before publishing
The wizard validates JSON against the n8n schema, sanitizes all strings, and masks detected secrets. The masking log shows exactly what was redacted and why.
**Import from URL:** Fetches workflow JSON from any public URL using `wp_remote_get()`. Requests to private/internal network addresses (localhost, LAN ranges, cloud metadata endpoints) are blocked.
---
### Node Type Registry
86 predefined node types with brand-accurate colors and 2-letter icons:
| Category | Examples |
|---|---|
| Triggers | Schedule, Webhook, Manual, Form |
| Core Logic | HTTP Request, Code, IF, Switch, Merge, Filter |
| Data Transformation | HTML, XML, Markdown, Crypto |
| Databases | MySQL, PostgreSQL, Redis, MongoDB, SQLite, Supabase |
| Communication | Slack, Telegram, Discord, Gmail, WhatsApp |
| Google | Sheets, Drive, Calendar, Docs, YouTube |
| Dev Tools | GitHub, GitLab, Jira, Confluence, Linear, Notion |
| AI | OpenAI, Claude, Gemini, Ollama, Hugging Face, Mistral, LangChain |
| Storage | FTP, SSH, Airtable, Baserow |
| CRM/Marketing | HubSpot, Salesforce, Mailchimp, Brevo |
Unknown node types get a deterministic fallback: 2-letter initials, and a color derived from a djb2 hash — so the same unknown type always looks the same.
---
### Theme System
5 built-in themes: **Dark** (default), **Light**, **Minimal**, **Tech**, **Brezn**.
Custom themes can be imported as `.breznflow.json` files containing 41 CSS color tokens. Custom themes are stored in `wp_options` and rendered as inline CSS variables.
Themes are selectable globally, per-workflow, or per-shortcode via `theme="dark"`.
---
### Action Bar
Below the diagram (non-compact mode), the action bar provides:
| Action | Control | What it does |
|---|---|---|
| **Share** | Global setting | Shows article link + anchor link for hash navigation |
| **Embed** | Global + per-post | Shows iframe embed code for standalone embedding |
| **Get JSON** | Global setting | Displays formatted JSON with size in KB |
| **Download** | Global + per-post | Downloads sanitized JSON file |
Each action can be toggled globally in settings and overridden per shortcode.
---
### Sensitive Data Masking
BreznFlow never stores raw workflow JSON. Before saving, a three-pass sanitization runs:
**Pass 1 — String sanitization:** All string values pass through `sanitize_text_field()`. Exception: `jsCode` fields are preserved as-is but displayed with `esc_html()` (never executed).
**Pass 2 — Secret detection:**
- **URL parameters:** `api_key`, `token`, `secret`, `password`, `access_token`, `auth`, `client_secret` in query strings → `[REDACTED]`
- **Generic `?key=` URL params** *(1.0.4)*: redacted only when the captured value matches `looks_like_secret()` — closes the Google API key bypass without false-positives on harmless values.
- **Header values:** Authorization, Bearer, X-API-Key and similar header names in `{name, value}` pairs → value masked
- **Value-entropy fallback** *(1.0.4)*: `{name, value}` pairs whose name does not match the allowlist are still masked when the value itself looks secret-shaped — covers custom headers (`X-App-Token`) and n8n's `queryParameters` generic-`key` pattern.
- **Known vendor tokens** *(1.0.4)*: `looks_like_secret()` matches `AIza…`, `sk-…`, `ghp_…`, `gho_…`, Slack `xox…`, `Bearer …` (JWT) — with a length+char-class entropy fallback and a path/whitespace denylist for false-positive control.
- **High-entropy conditions:** Values in IF/Switch conditions that match UUID patterns, mixed-case+digits, or long strings without spaces → masked via entropy heuristic
- **Credential display names** *(1.0.4)*: `credentials[].name` is replaced with `[REDACTED]` per node (the credential `id` is retained — it references the n8n DB and is useless without the server).
**Pass 3 — Identifying metadata** *(1.0.4)*: `meta.instanceId` is cleared so workflow exports cannot be correlated to the originating n8n instance.
**Optional — Tag removal** *(1.0.4)*: Wizard step 3 offers an opt-in checkbox to strip workflow tags. Tags are often innocuous (`production`, `v2`) but sometimes identifying — the publisher decides per workflow.
A **mask log** records every masked item with reason, key, and note. Step 3 shows it as a collapsible Reason / Key / Note table so the publisher can review exactly what will be published.
---
### Display Modes
| Mode | What's shown |
|---|---|
| `visual` | Full diagram with toolbar, detail panel, action bar |
| `info` | Node counts only (InfoBox) — no diagram |
| `compact` | Diagram without toolbar or action bar |
Configurable globally, per-workflow, or per-shortcode.
---
### Embed Handler
Serves a standalone HTML page at `?breznflow_embed={id}` for iframe embedding. The page contains only the SVG renderer — no WordPress theme, no admin bar.
**Dual-gate security:** Both the global `allow_embed` setting and the per-post `_breznflow_show_embed` meta must be enabled.
URL parameters: `?theme={id}` and `?minimap=0|1`.
HTTP headers include `X-Robots-Tag: noindex, nofollow` and remove `X-Frame-Options` to allow embedding.
---
### Additional Features
- **View Counter** — tracks how many times each workflow is displayed
- **Related Workflows** — shows similar workflows by shared node types
- **InfoBox** — compact summary like "3× HTTP Request, 2× Code, 1× OpenAI"
- **AI Detection** — automatically detects and badges workflows containing AI nodes
- **Schema.org HowTo** — optional JSON-LD structured data output
- **Anchor Navigation**`<span id="breznflow-{id}">` for hash-based deep linking with 60px scroll offset
---
## Data Storage
### WordPress Options (wp_options)
| Option Key | Content |
|---|---|
| `breznflow_settings` | All plugin settings (16 keys, serialized array) |
| `breznflow_custom_themes` | Custom theme definitions (serialized array) |
### Post Meta (wp_postmeta)
| Meta Key | Content |
|---|---|
| `_breznflow_raw_json` | Sanitized workflow JSON (never the raw original) |
| `_breznflow_original_name` | Original workflow name from n8n |
| `_breznflow_node_count` | Total number of nodes |
| `_breznflow_node_summary` | Categorized node counts (JSON) |
| `_breznflow_has_ai_nodes` | Whether workflow contains AI nodes |
| `_breznflow_ai_node_types` | List of AI node types present (JSON) |
| `_breznflow_mask_log` | Masked values during import (JSON) |
| `_breznflow_default_zoom` | Per-workflow zoom level (10200) |
| `_breznflow_show_title` | Show title |
| `_breznflow_show_infobox` | Show info box |
| `_breznflow_show_download` | Allow download |
| `_breznflow_show_embed` | Allow embedding |
| `_breznflow_show_minimap` | Show minimap |
| `_breznflow_default_mode` | Display mode |
| `_breznflow_default_theme` | Theme ID |
### Uninstall Cleanup
`uninstall.php` removes on plugin deletion:
- All `breznflow_workflow` posts and their meta
- Option `breznflow_settings`
- All `breznflow_category` taxonomy terms
- All transients with prefix `breznflow_`
---
## Security
### Input Validation
- Workflow JSON is validated against the n8n schema before import
- All `$_POST` / `$_GET` values processed via `wp_unslash()` + specific sanitizers
- All output escaped with `esc_html`, `esc_attr`, `esc_url`
- SQL queries exclusively via `$wpdb->prepare()`
### CSRF Protection
Every admin action validates a WordPress nonce. Capability checks require `manage_options` for settings and `edit_posts` for workflow management.
### Download & Embed Security
Both endpoints validate:
1. Post exists, is type `breznflow_workflow`, and has status `publish`
2. The corresponding per-post meta permission is enabled
3. The global setting is enabled
4. The stored JSON is valid
Response headers include `X-Content-Type-Options: nosniff`. Downloads use `Cache-Control: no-store`.
### Import from URL
The "Fetch from URL" feature in the wizard blocks requests to private/internal network addresses (localhost, `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, cloud metadata endpoints) to prevent SSRF attacks.
---
## Shortcode
```
[breznflow id="42"]
[breznflow id="42" mode="compact" theme="light" zoom="80"]
[breznflow id="42" show_share="0" show_embed="1" show_minimap="0"]
```
| Attribute | Default | Description |
|---|---|---|
| `id` | — | Workflow post ID (required) |
| `mode` | `visual` | `visual`, `info`, or `compact` |
| `theme` | `dark` | Theme ID |
| `zoom` | `100` | Initial zoom level (10200) |
| `show_title` | `true` | Show workflow title |
| `show_infobox` | `true` | Show node summary box |
| `show_minimap` | `true` | Show minimap overlay |
| `show_download` | `false` | Show download button |
| `show_share` | `true` | Show share action |
| `show_embed` | `false` | Show embed action |
| `show_get_json` | `false` | Show "Get JSON" action |
| `max_code_lines` | `50` | Max lines in code node display |
**Resolution hierarchy:** Shortcode attribute → Post meta → Plugin settings.
---
## Installation
**Via GitHub Release (recommended):**
1. Download `breznflow.zip` from the [latest release](https://github.com/noschmarrn/breznflow/releases/latest)
2. In WordPress go to *Plugins → Add New → Upload Plugin*
**Manual (clone):**
```bash
cd /path/to/wordpress/wp-content/plugins/
git clone https://github.com/noschmarrn/breznflow.git
wp plugin activate breznflow
```
**After activation:**
1. Go to *BreznFlow → Add Workflow*
2. Paste your n8n workflow JSON (or upload a `.json` file)
3. Configure display settings and preview
4. Publish — use `[breznflow id="X"]` in any post or page
The plugin has no build step. All assets are direct JS/CSS files.
---
## Tech Stack
| Component | Technology |
|---|---|
| Backend | PHP 8.0+, WordPress Plugin API |
| Namespace | `BreznFlow\` |
| Architecture | Singleton core, feature classes with `register()` |
| Rendering | Vanilla JavaScript SVG generation (no canvas, no libraries) |
| Database | WordPress Options API + Post Meta |
| Caching | WordPress transients (related workflows) |
| Frontend | Vanilla JS, no build step, no external CDN |
| i18n | `.pot` file, text domain `breznflow` |
| Coding Standard | WordPress PHPCS |
| License | GPL-2.0-or-later |
---
## License
GPL-2.0-or-later — [https://www.gnu.org/licenses/gpl-2.0.html](https://www.gnu.org/licenses/gpl-2.0.html)
Copyright (c) 20252026 [mifupa.com](https://mifupa.com)

View file

@ -1,52 +0,0 @@
/*
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;
}

View file

@ -84,6 +84,11 @@
display: block; display: block;
width: 100%; width: 100%;
overflow: visible; overflow: visible;
/* Claim all touch gestures on the diagram: single-finger pan, two-finger
pinch, double-tap. Trade-off: starting a touch on the SVG means page
scroll is blocked until the finger lifts. Page scroll around the
diagram (container margins) keeps default behavior. */
touch-action: none;
} }
/* Canvas (zoom/pan transform applied here) */ /* Canvas (zoom/pan transform applied here) */
@ -407,7 +412,7 @@
z-index: 5; z-index: 5;
} }
.breznflow-minimap svg { display: block; } .breznflow-minimap svg { display: block; touch-action: none; }
/* Infobox node badges — interactive highlight */ /* Infobox node badges — interactive highlight */
.breznflow-infobox-node { .breznflow-infobox-node {

View file

@ -368,9 +368,16 @@
onNavigate(pos.x / fitScale, pos.y / fitScale); onNavigate(pos.x / fitScale, pos.y / fitScale);
} }
svg.addEventListener('mousedown', function(e) { dragging = true; navigate(e); e.stopPropagation(); }); svg.addEventListener('pointerdown', function(e) {
svg.addEventListener('mousemove', function(e) { if (dragging) navigate(e); }); dragging = true;
document.addEventListener('mouseup', function() { dragging = false; }); navigate(e);
e.stopPropagation();
if (e.cancelable) e.preventDefault();
try { svg.setPointerCapture(e.pointerId); } catch (_) { /* iOS quirk */ }
}, { passive: false });
svg.addEventListener('pointermove', function(e) { if (dragging) navigate(e); }, { passive: false });
svg.addEventListener('pointerup', function() { dragging = false; });
svg.addEventListener('pointercancel', function() { dragging = false; });
return { return {
el: wrap, el: wrap,
@ -1095,25 +1102,11 @@
self._applyTransform(); self._applyTransform();
}, { passive: false }); }, { passive: false });
svg.addEventListener('pointerdown', function(e) { self._initPointerState();
const t = e.target; svg.addEventListener('pointerdown', function(e) { self._onPointerDown(e); }, { passive: false });
const isBg = t === svg || svg.addEventListener('pointermove', function(e) { self._onPointerMove(e); }, { passive: false });
(t.getAttribute && (t.getAttribute('class') === 'breznflow-canvas' || t.getAttribute('class') === 'breznflow-connection-path')); svg.addEventListener('pointerup', function(e) { self._onPointerUp(e); }, { passive: false });
if (!isBg) return; svg.addEventListener('pointercancel', function(e) { self._onPointerUp(e); }, { passive: false });
self.isPanning = true;
self.panStart = { x: e.clientX - self.tx, y: e.clientY - self.ty };
svg.setPointerCapture(e.pointerId);
});
svg.addEventListener('pointermove', function(e) {
if (!self.isPanning) return;
self.tx = e.clientX - self.panStart.x;
self.ty = e.clientY - self.panStart.y;
self._applyTransform();
});
svg.addEventListener('pointerup', function() { self.isPanning = false; });
svg.addEventListener('pointercancel', function() { self.isPanning = false; });
svg.addEventListener('click', function(e) { svg.addEventListener('click', function(e) {
const t = e.target; const t = e.target;
@ -1132,6 +1125,156 @@
}); });
}; };
BreznFlowRenderer.prototype._initPointerState = function() {
this._pointers = new Map();
this._lastTap = { t: 0, x: 0, y: 0 };
this._pinchStart = null;
};
BreznFlowRenderer.prototype._isBgTarget = function(t) {
if (!t) return false;
if (t === this._svg) return true;
if (!t.getAttribute) return false;
const cls = t.getAttribute('class');
return cls === 'breznflow-canvas' || cls === 'breznflow-connection-path';
};
BreznFlowRenderer.prototype._onPointerDown = function(e) {
const isBg = this._isBgTarget(e.target);
const rect = this._svg.getBoundingClientRect();
const px = e.clientX - rect.left;
const py = e.clientY - rect.top;
// Double-tap on empty canvas toggles zoom. Must run before pointer is
// added to the map so pointers.size===0 check is meaningful.
if (isBg && this._pointers.size === 0) {
const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
const dt = now - this._lastTap.t;
const dx = px - this._lastTap.x;
const dy = py - this._lastTap.y;
if (dt < 300 && (dx * dx + dy * dy) < 900) {
this._lastTap = { t: 0, x: 0, y: 0 };
if (e.cancelable) e.preventDefault();
this._handleDoubleTap(px, py);
return;
}
this._lastTap = { t: now, x: px, y: py };
}
this._pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
// Non-bg targets (nodes, badges) fall through to default click handling.
if (!isBg) return;
if (e.cancelable) e.preventDefault();
try { this._svg.setPointerCapture(e.pointerId); } catch (_) { /* iOS Safari quirk */ }
if (this._pointers.size === 1) {
this.isPanning = true;
this.panStart = { x: e.clientX - this.tx, y: e.clientY - this.ty };
} else if (this._pointers.size === 2) {
this.isPanning = false;
this._pinchStart = this._computePinchState();
}
};
BreznFlowRenderer.prototype._onPointerMove = function(e) {
if (!this._pointers.has(e.pointerId)) return;
this._pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (this._pinchStart && this._pointers.size === 2) {
this._applyPinch();
} else if (this.isPanning && this._pointers.size === 1) {
this.tx = e.clientX - this.panStart.x;
this.ty = e.clientY - this.panStart.y;
this._applyTransform();
}
};
BreznFlowRenderer.prototype._onPointerUp = function(e) {
if (!this._pointers.delete(e.pointerId)) return;
if (this._pinchStart && this._pointers.size < 2) {
this._pinchStart = null;
// Transitioning 2→1 fingers: re-seat pan-start on the remaining
// pointer so the surviving finger doesn't cause a jump.
if (this._pointers.size === 1) {
const remaining = this._pointers.values().next().value;
this.isPanning = true;
this.panStart = { x: remaining.x - this.tx, y: remaining.y - this.ty };
}
}
if (this._pointers.size === 0) {
this.isPanning = false;
}
};
BreznFlowRenderer.prototype._computePinchState = function() {
const pts = Array.from(this._pointers.values());
const p1 = pts[0], p2 = pts[1];
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
return {
d: Math.max(1, Math.sqrt(dx * dx + dy * dy)),
cx: (p1.x + p2.x) / 2,
cy: (p1.y + p2.y) / 2,
tx: this.tx,
ty: this.ty,
userZoom: this.userZoom
};
};
BreznFlowRenderer.prototype._applyPinch = function() {
if (!this._pinchStart) return;
const pts = Array.from(this._pointers.values());
if (pts.length !== 2) return;
const p1 = pts[0], p2 = pts[1];
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const d = Math.max(1, Math.sqrt(dx * dx + dy * dy));
const cx = (p1.x + p2.x) / 2;
const cy = (p1.y + p2.y) / 2;
const ratio = d / this._pinchStart.d;
const newZoom = Math.max(MIN_SCALE * 100, Math.min(MAX_SCALE * 100, this._pinchStart.userZoom * ratio));
const baseScale = this.layout && this.layout.scale ? this.layout.scale : 1;
const oldS = baseScale * (this._pinchStart.userZoom / 100);
const newS = baseScale * (newZoom / 100);
if (oldS === 0) return;
// Two moves compounded: (1) zoom anchored at the start-center in SVG
// coords, (2) pan by how far the current center drifted from the
// start-center. Order matters — zoom first, then translate.
const rect = this._svg.getBoundingClientRect();
const sx = this._pinchStart.cx - rect.left;
const sy = this._pinchStart.cy - rect.top;
const dxCenter = cx - this._pinchStart.cx;
const dyCenter = cy - this._pinchStart.cy;
this.tx = sx - (sx - this._pinchStart.tx) * (newS / oldS) + dxCenter;
this.ty = sy - (sy - this._pinchStart.ty) * (newS / oldS) + dyCenter;
this.userZoom = newZoom;
if (this.zoomLabel) this.zoomLabel.textContent = Math.round(newZoom) + '%';
this._applyTransform();
};
BreznFlowRenderer.prototype._handleDoubleTap = function(px, py) {
// Toggle 100↔200 %, zoom-anchored at tap point (identical math to
// wheel-zoom above — keeps the world point under the cursor fixed).
const newZoom = this.userZoom > 150 ? 100 : 200;
const baseScale = this.layout && this.layout.scale ? this.layout.scale : 1;
const oldS = baseScale * (this.userZoom / 100);
const newS = baseScale * (newZoom / 100);
if (oldS === 0) return;
this.tx = px - (px - this.tx) * (newS / oldS);
this.ty = py - (py - this.ty) * (newS / oldS);
this.userZoom = newZoom;
if (this.zoomLabel) this.zoomLabel.textContent = Math.round(newZoom) + '%';
this._applyTransform();
};
BreznFlowRenderer.prototype._toggleFullscreen = function(btn) { BreznFlowRenderer.prototype._toggleFullscreen = function(btn) {
if (this._fsActive) { if (this._fsActive) {
this._exitFullscreen(btn); this._exitFullscreen(btn);
@ -1520,9 +1663,18 @@
function init() { function init() {
if (typeof breznflowData === 'undefined' || !Array.isArray(breznflowData)) return; if (typeof breznflowData === 'undefined' || !Array.isArray(breznflowData)) return;
// Defensive guard: if any filter re-ran the shortcode, breznflowData may
// contain duplicate entries pointing at the same container. Mounting twice
// would stack two full UIs via appendChild. Track mounted containers and
// skip repeats — independent of server-side dedupe.
const mounted = new WeakSet();
for (const data of breznflowData) { for (const data of breznflowData) {
const container = document.getElementById('breznflow-wrap-' + data.id); const domId = data.dom_id || ('breznflow-wrap-' + data.id);
const container = document.getElementById(domId);
if (!container) continue; if (!container) continue;
if (mounted.has(container)) continue;
mounted.add(container);
try { try {
const renderer = new BreznFlowRenderer(data); const renderer = new BreznFlowRenderer(data);

View file

@ -6,9 +6,9 @@
* @since 1.0.0 * @since 1.0.0
* *
* Plugin Name: BreznFlow * Plugin Name: BreznFlow
* Plugin URI: https://noschmarrn.dev/ * Plugin URI: https://breznflow.com/
* Description: Display n8n automation workflows with an interactive SVG diagram, node detail panel, and sensitive data masking. * Description: Display n8n automation workflows with an interactive SVG diagram, node detail panel, and sensitive data masking.
* Version: 1.0.1 * Version: 1.0.4
* Requires at least: 6.0 * Requires at least: 6.0
* Requires PHP: 8.0 * Requires PHP: 8.0
* Author: NoSchmarrn.dev * Author: NoSchmarrn.dev
@ -23,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
} }
define( 'BREZNFLOW_VERSION', '1.0.1' ); define( 'BREZNFLOW_VERSION', '1.0.4' );
define( 'BREZNFLOW_FILE', __FILE__ ); define( 'BREZNFLOW_FILE', __FILE__ );
define( 'BREZNFLOW_DIR', plugin_dir_path( __FILE__ ) ); define( 'BREZNFLOW_DIR', plugin_dir_path( __FILE__ ) );
define( 'BREZNFLOW_URL', plugin_dir_url( __FILE__ ) ); define( 'BREZNFLOW_URL', plugin_dir_url( __FILE__ ) );

View file

@ -170,7 +170,18 @@ class AdminMenu {
* @return void * @return void
*/ */
public function render_wizard(): void { public function render_wizard(): void {
$step = isset( $_GET['step'] ) ? (int) $_GET['step'] : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $step = isset( $_GET['step'] ) ? (int) $_GET['step'] : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- step is cast to int and only selects a template; nonce verified below for steps with sensitive params.
// Steps 2 and 3 receive post_id via GET — verify nonce to prevent CSRF.
if ( $step >= 2 ) {
if (
! isset( $_GET['_wpnonce'] )
|| ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'breznflow_wizard_step' )
) {
wp_die( esc_html__( 'Security check failed. Please try again.', 'breznflow' ), 403 );
}
}
switch ( $step ) { switch ( $step ) {
case 2: case 2:
require BREZNFLOW_DIR . 'includes/Admin/views/wizard-step-2.php'; require BREZNFLOW_DIR . 'includes/Admin/views/wizard-step-2.php';

View file

@ -52,11 +52,16 @@ class ThemesPage {
wp_die( esc_html__( 'Insufficient permissions.', 'breznflow' ) ); wp_die( esc_html__( 'Insufficient permissions.', 'breznflow' ) );
} }
$file = $_FILES['breznflow_theme_file']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput // Access individual $_FILES keys with explicit sanitization.
$file_name = isset( $_FILES['breznflow_theme_file']['name'] )
? sanitize_file_name( wp_unslash( $_FILES['breznflow_theme_file']['name'] ) )
: '';
$file_tmp = isset( $_FILES['breznflow_theme_file']['tmp_name'] )
? sanitize_text_field( $_FILES['breznflow_theme_file']['tmp_name'] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- tmp_name is a server-generated path, not user input.
: '';
// Verify file extension. // Verify file extension.
$filename = isset( $file['name'] ) ? sanitize_file_name( $file['name'] ) : ''; if ( ! str_ends_with( $file_name, '.breznflow.json' ) && ! str_ends_with( $file_name, '.json' ) ) {
if ( ! str_ends_with( $filename, '.breznflow.json' ) && ! str_ends_with( $filename, '.json' ) ) {
wp_safe_redirect( wp_safe_redirect(
add_query_arg( add_query_arg(
array( array(
@ -69,7 +74,7 @@ class ThemesPage {
exit; exit;
} }
if ( ! isset( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) { if ( '' === $file_tmp || ! is_uploaded_file( $file_tmp ) ) {
wp_safe_redirect( wp_safe_redirect(
add_query_arg( add_query_arg(
array( array(
@ -83,7 +88,7 @@ class ThemesPage {
} }
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$raw = file_get_contents( $file['tmp_name'] ); $raw = file_get_contents( $file_tmp );
if ( false === $raw ) { if ( false === $raw ) {
wp_safe_redirect( wp_safe_redirect(
add_query_arg( add_query_arg(

View file

@ -83,8 +83,12 @@ class WizardPage {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'breznflow' ) ) ); 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 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitize_textarea_field() cannot be used: its internal strip_tags() corrupts JSON containing HTML-like string values. Input is validated structurally via json_decode() in WorkflowValidator and sanitized field-by-field in WorkflowSanitizer.
$raw = isset( $_POST['json'] ) ? trim( wp_unslash( (string) $_POST['json'] ) ) : ''; $raw = isset( $_POST['json'] ) ? wp_unslash( $_POST['json'] ) : '';
if ( ! is_string( $raw ) ) {
$raw = '';
}
$raw = trim( $raw );
if ( '' === $raw ) { if ( '' === $raw ) {
wp_send_json_error( array( 'message' => __( 'No JSON provided.', 'breznflow' ) ) ); wp_send_json_error( array( 'message' => __( 'No JSON provided.', 'breznflow' ) ) );
@ -191,8 +195,12 @@ class WizardPage {
wp_die( esc_html__( 'Insufficient permissions.', 'breznflow' ) ); 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 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitize_textarea_field() cannot be used: its internal strip_tags() corrupts JSON containing HTML-like string values. Input is validated structurally via json_decode() in WorkflowValidator and sanitized field-by-field in WorkflowSanitizer.
$raw = isset( $_POST['breznflow_json'] ) ? trim( wp_unslash( (string) $_POST['breznflow_json'] ) ) : ''; $raw = isset( $_POST['breznflow_json'] ) ? wp_unslash( $_POST['breznflow_json'] ) : '';
if ( ! is_string( $raw ) ) {
$raw = '';
}
$raw = trim( $raw );
if ( '' === $raw ) { if ( '' === $raw ) {
wp_safe_redirect( wp_safe_redirect(
@ -296,9 +304,10 @@ class WizardPage {
wp_safe_redirect( wp_safe_redirect(
add_query_arg( add_query_arg(
array( array(
'page' => 'breznflow-add', 'page' => 'breznflow-add',
'step' => '2', 'step' => '2',
'post_id' => $post_id, 'post_id' => $post_id,
'_wpnonce' => wp_create_nonce( 'breznflow_wizard_step' ),
), ),
admin_url( 'admin.php' ) admin_url( 'admin.php' )
) )
@ -368,9 +377,10 @@ class WizardPage {
wp_safe_redirect( wp_safe_redirect(
add_query_arg( add_query_arg(
array( array(
'page' => 'breznflow-add', 'page' => 'breznflow-add',
'step' => '3', 'step' => '3',
'post_id' => $post_id, 'post_id' => $post_id,
'_wpnonce' => wp_create_nonce( 'breznflow_wizard_step' ),
), ),
admin_url( 'admin.php' ) admin_url( 'admin.php' )
) )
@ -394,6 +404,24 @@ class WizardPage {
wp_die( esc_html__( 'Invalid workflow ID.', 'breznflow' ) ); wp_die( esc_html__( 'Invalid workflow ID.', 'breznflow' ) );
} }
// Opt-in tag removal. Runs against the already-sanitized stored JSON,
// so there is no risk of undoing the pass-1/2/3 masking from step 1.
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above
if ( ! empty( $_POST['breznflow_strip_tags'] ) ) {
$raw_json = get_post_meta( $post_id, '_breznflow_raw_json', true );
$decoded = $raw_json ? json_decode( $raw_json, true ) : null;
if ( is_array( $decoded ) ) {
$mask_log_raw = get_post_meta( $post_id, '_breznflow_mask_log', true );
$mask_log = $mask_log_raw ? json_decode( $mask_log_raw, true ) : array();
if ( ! is_array( $mask_log ) ) {
$mask_log = array();
}
$stripped = WorkflowSanitizer::strip_workflow_tags( $decoded, $mask_log );
update_post_meta( $post_id, '_breznflow_raw_json', wp_slash( wp_json_encode( $stripped ) ) );
update_post_meta( $post_id, '_breznflow_mask_log', wp_slash( wp_json_encode( $mask_log ) ) );
}
}
wp_update_post( wp_update_post(
array( array(
'ID' => $post_id, 'ID' => $post_id,

View file

@ -162,9 +162,10 @@ class WorkflowListTable extends \WP_List_Table {
protected function column_title( $item ): string { protected function column_title( $item ): string {
$edit_url = add_query_arg( $edit_url = add_query_arg(
array( array(
'page' => 'breznflow-add', 'page' => 'breznflow-add',
'step' => '2', 'step' => '2',
'post_id' => $item->ID, 'post_id' => $item->ID,
'_wpnonce' => wp_create_nonce( 'breznflow_wizard_step' ),
), ),
admin_url( 'admin.php' ) admin_url( 'admin.php' )
); );

View file

@ -11,7 +11,7 @@ if ( ! defined( 'ABSPATH' ) ) {
} }
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- template file, not global scope // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- template file, not global scope
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.WP.GlobalVariablesOverride.Prohibited // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.WP.GlobalVariablesOverride.Prohibited -- nonce verified in AdminMenu::render_wizard() before this template loads.
$post_id = isset( $_GET['post_id'] ) ? (int) $_GET['post_id'] : 0; $post_id = isset( $_GET['post_id'] ) ? (int) $_GET['post_id'] : 0;
$workflow = $post_id > 0 ? get_post( $post_id ) : null; $workflow = $post_id > 0 ? get_post( $post_id ) : null;

View file

@ -11,7 +11,7 @@ if ( ! defined( 'ABSPATH' ) ) {
} }
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- template file, not global scope // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- template file, not global scope
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.WP.GlobalVariablesOverride.Prohibited // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.WP.GlobalVariablesOverride.Prohibited -- nonce verified in AdminMenu::render_wizard() before this template loads.
$post_id = isset( $_GET['post_id'] ) ? (int) $_GET['post_id'] : 0; $post_id = isset( $_GET['post_id'] ) ? (int) $_GET['post_id'] : 0;
$workflow = $post_id > 0 ? get_post( $post_id ) : null; $workflow = $post_id > 0 ? get_post( $post_id ) : null;
@ -30,15 +30,25 @@ $meta_zoom = (int) get_post_meta( $post_id, '_breznflow_default_zoom', true )
$zoom = $meta_zoom ? $meta_zoom : 100; $zoom = $meta_zoom ? $meta_zoom : 100;
$show_infobox = (int) get_post_meta( $post_id, '_breznflow_show_infobox', true ); $show_infobox = (int) get_post_meta( $post_id, '_breznflow_show_infobox', true );
// Check for code nodes with jsCode. // Check for code nodes with jsCode and collect workflow tag names.
$has_code_nodes = false; $has_code_nodes = false;
$workflow_tags = array();
if ( $raw_json ) { if ( $raw_json ) {
$data = json_decode( $raw_json, true ); $data = json_decode( $raw_json, true );
if ( is_array( $data ) && ! empty( $data['nodes'] ) ) { if ( is_array( $data ) ) {
foreach ( $data['nodes'] as $node ) { if ( ! empty( $data['nodes'] ) ) {
if ( isset( $node['parameters']['jsCode'] ) ) { foreach ( $data['nodes'] as $node ) {
$has_code_nodes = true; if ( isset( $node['parameters']['jsCode'] ) ) {
break; $has_code_nodes = true;
break;
}
}
}
if ( ! empty( $data['tags'] ) && is_array( $data['tags'] ) ) {
foreach ( $data['tags'] as $tag ) {
if ( is_array( $tag ) && isset( $tag['name'] ) && '' !== $tag['name'] ) {
$workflow_tags[] = (string) $tag['name'];
}
} }
} }
} }
@ -102,7 +112,7 @@ $preview_theme = in_array( $saved_theme, $allowed_themes, true ) ? $saved_them
} }
$bf_custom_css = \BreznFlow\Features\ThemeRegistry::get_custom_theme_css(); $bf_custom_css = \BreznFlow\Features\ThemeRegistry::get_custom_theme_css();
if ( $bf_custom_css ) { if ( $bf_custom_css ) {
wp_add_inline_style( 'breznflow-renderer', $bf_custom_css ); wp_add_inline_style( 'breznflow-renderer', wp_strip_all_tags( $bf_custom_css ) );
} }
wp_localize_script( wp_localize_script(
'breznflow-renderer', 'breznflow-renderer',
@ -142,6 +152,29 @@ $preview_theme = in_array( $saved_theme, $allowed_themes, true ) ? $saved_them
); );
?> ?>
</p> </p>
<details class="breznflow-mask-details">
<summary><?php esc_html_e( 'Show what was masked', 'breznflow' ); ?></summary>
<div class="breznflow-mask-table-wrap" style="max-height:360px;overflow:auto;margin-top:8px;">
<table class="widefat striped">
<thead>
<tr>
<th scope="col"><?php esc_html_e( 'Reason', 'breznflow' ); ?></th>
<th scope="col"><?php esc_html_e( 'Key', 'breznflow' ); ?></th>
<th scope="col"><?php esc_html_e( 'Note', 'breznflow' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $mask_log as $entry ) : ?>
<tr>
<td><code><?php echo esc_html( (string) ( $entry['reason'] ?? '' ) ); ?></code></td>
<td><code><?php echo esc_html( (string) ( $entry['key'] ?? '' ) ); ?></code></td>
<td><?php echo esc_html( (string) ( $entry['note'] ?? '' ) ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</details>
</div> </div>
<?php endif; ?> <?php endif; ?>
@ -154,15 +187,36 @@ $preview_theme = in_array( $saved_theme, $allowed_themes, true ) ? $saved_them
<input type="hidden" name="action" value="breznflow_publish_workflow" /> <input type="hidden" name="action" value="breznflow_publish_workflow" />
<input type="hidden" name="breznflow_post_id" value="<?php echo esc_attr( (string) $post_id ); ?>" /> <input type="hidden" name="breznflow_post_id" value="<?php echo esc_attr( (string) $post_id ); ?>" />
<?php wp_nonce_field( 'breznflow_publish', 'breznflow_nonce' ); ?> <?php wp_nonce_field( 'breznflow_publish', 'breznflow_nonce' ); ?>
<?php if ( ! empty( $workflow_tags ) ) : ?>
<p>
<label>
<input type="checkbox" name="breznflow_strip_tags" value="1" />
<strong><?php esc_html_e( 'Remove workflow tags before publishing', 'breznflow' ); ?></strong>
</label>
<br />
<span class="description">
<?php
printf(
/* translators: %s: comma-separated list of tag names */
esc_html__( 'Current tags: %s. Tags are organisational labels in n8n — harmless by default, but sometimes identifying (e.g. an internal project name).', 'breznflow' ),
esc_html( implode( ', ', $workflow_tags ) )
);
?>
</span>
</p>
<?php endif; ?>
<p> <p>
<a href=" <a href="
<?php <?php
echo esc_url( echo esc_url(
add_query_arg( add_query_arg(
array( array(
'page' => 'breznflow-add', 'page' => 'breznflow-add',
'step' => '2', 'step' => '2',
'post_id' => $post_id, 'post_id' => $post_id,
'_wpnonce' => wp_create_nonce( 'breznflow_wizard_step' ),
), ),
admin_url( 'admin.php' ) admin_url( 'admin.php' )
) )

View file

@ -36,11 +36,13 @@ class DownloadHandler {
* @return void * @return void
*/ */
public function handle_download(): void { public function handle_download(): void {
if ( ! isset( $_GET['breznflow_download'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only download endpoint; serves only published data gated by global allow_download setting + per-post _breznflow_show_download meta. No state change.
if ( ! isset( $_GET['breznflow_download'] ) ) {
return; return;
} }
$post_id = (int) $_GET['breznflow_download']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only endpoint (see above).
$post_id = (int) $_GET['breznflow_download'];
if ( $post_id <= 0 ) { if ( $post_id <= 0 ) {
status_header( 400 ); status_header( 400 );

View file

@ -36,7 +36,7 @@ class EmbedHandler {
* @return void * @return void
*/ */
public function handle_embed(): void { public function handle_embed(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only embed endpoint, no state change; only serves published data. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only embed endpoint; serves only published data gated by global + per-post settings. No state change.
if ( ! isset( $_GET['breznflow_embed'] ) ) { if ( ! isset( $_GET['breznflow_embed'] ) ) {
return; return;
} }
@ -80,11 +80,11 @@ class EmbedHandler {
} }
$allowed_themes = \BreznFlow\Features\ThemeRegistry::get_theme_ids(); $allowed_themes = \BreznFlow\Features\ThemeRegistry::get_theme_ids();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only embed endpoint; theme is validated against whitelist.
$url_theme = isset( $_GET['theme'] ) ? sanitize_text_field( wp_unslash( $_GET['theme'] ) ) : ''; $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( $url_theme, $allowed_themes, true ) ? $url_theme : ( $settings['default_theme'] ?? 'dark' );
$theme = in_array( $theme, $allowed_themes, true ) ? $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. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only embed endpoint; boolean flag, sanitized.
$show_minimap_embed = isset( $_GET['minimap'] ) ? ( '0' !== sanitize_text_field( wp_unslash( $_GET['minimap'] ) ) ) : true; $show_minimap_embed = isset( $_GET['minimap'] ) ? ( '0' !== sanitize_text_field( wp_unslash( $_GET['minimap'] ) ) ) : true;
$body_bgs = array( $body_bgs = array(
@ -96,19 +96,20 @@ class EmbedHandler {
); );
$body_bg = $body_bgs[ $theme ] ?? '#1a1a2e'; $body_bg = $body_bgs[ $theme ] ?? '#1a1a2e';
// Hide admin bar on standalone embed page.
show_admin_bar( false );
// Set headers. // Set headers.
header( 'Content-Type: text/html; charset=utf-8' ); header( 'Content-Type: text/html; charset=utf-8' );
header( 'X-Robots-Tag: noindex, nofollow' ); header( 'X-Robots-Tag: noindex, nofollow' );
header( 'X-Content-Type-Options: nosniff' ); header( 'X-Content-Type-Options: nosniff' );
header_remove( 'X-Frame-Options' ); header_remove( 'X-Frame-Options' );
$article_url = esc_url( get_permalink( $post_id ) ); $article_url = get_permalink( $post_id );
$anchor_id = 'breznflow-' . $post_id; $anchor_id = 'breznflow-' . $post_id;
$blog_name = esc_html( get_bloginfo( 'name' ) ); $blog_name = get_bloginfo( 'name' );
$blog_url = esc_url( home_url( '/' ) ); $blog_url = home_url( '/' );
$title = esc_html( $post->post_title ); $title = $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( $inline_data = array(
array( array(
@ -131,55 +132,60 @@ class EmbedHandler {
), ),
); );
$icons_json = wp_json_encode( Features\NodeTypeRegistry::get_registry() ); // Enqueue renderer assets via WordPress enqueue system.
$data_json = wp_json_encode( $inline_data ); wp_enqueue_style( 'breznflow-renderer', BREZNFLOW_URL . 'assets/renderer.css', array(), BREZNFLOW_VERSION );
$i18n_json = wp_json_encode( Shortcode::get_js_i18n() ); wp_enqueue_script( 'breznflow-renderer', BREZNFLOW_URL . 'assets/renderer.js', array(), BREZNFLOW_VERSION, true );
foreach ( \BreznFlow\Features\ThemeRegistry::BUILTIN as $bf_embed_id => $bf_embed_name ) {
wp_enqueue_style(
'breznflow-theme-' . $bf_embed_id,
\BreznFlow\Features\ThemeRegistry::get_builtin_url( $bf_embed_id ),
array( 'breznflow-renderer' ),
BREZNFLOW_VERSION
);
}
$embed_custom_css = \BreznFlow\Features\ThemeRegistry::get_custom_theme_css();
if ( $embed_custom_css ) {
wp_add_inline_style( 'breznflow-renderer', wp_strip_all_tags( $embed_custom_css ) );
}
$embed_layout_css = '*, *::before, *::after { box-sizing: border-box; }'
. 'html, body { margin: 0; padding: 0; height: 100%; background: ' . 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; }';
wp_add_inline_style( 'breznflow-renderer', $embed_layout_css );
wp_localize_script( 'breznflow-renderer', 'breznflowData', $inline_data );
wp_localize_script( 'breznflow-renderer', 'breznflowIcons', Features\NodeTypeRegistry::get_registry() );
wp_localize_script( 'breznflow-renderer', 'breznflowI18n', Shortcode::get_js_i18n() );
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- intentional standalone HTML output; all dynamic values escaped above
?><!DOCTYPE html> ?><!DOCTYPE html>
<html lang="<?php echo esc_attr( get_bloginfo( 'language' ) ); ?>" data-theme="<?php echo esc_attr( $theme ); ?>"> <html lang="<?php echo esc_attr( get_bloginfo( 'language' ) ); ?>" data-theme="<?php echo esc_attr( $theme ); ?>">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<title><?php echo $title; ?></title> <title><?php echo esc_html( $title ); ?></title>
<link rel="stylesheet" href="<?php echo $css_url; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet -- standalone embed page, no wp_head(). ?>"> <?php 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> </head>
<body> <body>
<div id="breznflow-embed-viewer"> <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 id="breznflow-wrap-<?php echo (int) $post_id; ?>" class="breznflow-embed" data-id="<?php echo (int) $post_id; ?>"></div>
</div> </div>
<footer id="breznflow-embed-footer"> <footer id="breznflow-embed-footer">
<a href="<?php echo $article_url; ?>#<?php echo esc_attr( $anchor_id ); ?>"><?php echo $title; ?></a> <a href="<?php echo esc_url( $article_url ); ?>#<?php echo esc_attr( $anchor_id ); ?>"><?php echo esc_html( $title ); ?></a>
<span>&bull;</span> <span>&bull;</span>
<span><?php esc_html_e( 'Source:', 'breznflow' ); ?> <a href="<?php echo $blog_url; ?>"><?php echo $blog_name; ?></a></span> <span><?php esc_html_e( 'Source:', 'breznflow' ); ?> <a href="<?php echo esc_url( $blog_url ); ?>"><?php echo esc_html( $blog_name ); ?></a></span>
</footer> </footer>
<script> <?php wp_footer(); ?>
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> </body>
</html> </html>
<?php <?php
// phpcs:enable
exit; exit;
} }
} }

View file

@ -88,15 +88,15 @@ class ThemeRegistry {
$css = ''; $css = '';
foreach ( $themes as $theme ) { foreach ( $themes as $theme ) {
$id = $theme['id']; $id = sanitize_key( $theme['id'] );
$sel = '.breznflow-wrap[data-theme="' . $id . '"],' $sel = '.breznflow-wrap[data-theme="' . $id . '"],'
. '.breznflow-modal-overlay[data-theme="' . $id . '"],' . '.breznflow-modal-overlay[data-theme="' . $id . '"],'
. '.breznflow-fs-portal[data-theme="' . $id . '"]'; . '.breznflow-fs-portal[data-theme="' . $id . '"]';
$css .= $sel . '{'; $css .= $sel . '{';
foreach ( $theme['tokens'] as $token => $value ) { foreach ( $theme['tokens'] as $token => $value ) {
$var = '--breznflow-' . str_replace( '_', '-', $token ); $var = '--breznflow-' . str_replace( '_', '-', sanitize_key( $token ) );
$css .= $var . ':' . $value . ';'; $css .= $var . ':' . esc_attr( $value ) . ';';
} }
$css .= '}'; $css .= '}';
} }

View file

@ -21,6 +21,20 @@ class MaskingRules {
/** URL query param pattern for sensitive keys. */ /** 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'; const URL_PARAM_PATTERN = '/([?&](?:api[_-]?key|apikey|token|secret|password|access_token|auth|private_key|client_secret)=)[^&\s#]+/i';
/**
* Known vendor secret formats.
*
* @since 1.0.4
*/
const HIGH_ENTROPY_PATTERNS = array(
'/^AIza[0-9A-Za-z_-]{35}$/', // Google API key.
'/^sk-[A-Za-z0-9_-]{20,}$/', // OpenAI / Anthropic family.
'/^ghp_[A-Za-z0-9]{36}$/', // GitHub personal access token.
'/^gho_[A-Za-z0-9]{36}$/', // GitHub OAuth token.
'/^xox[baprs]-[A-Za-z0-9-]{10,}$/', // Slack token.
'/^Bearer\s+[A-Za-z0-9._~+\/=-]{20,}$/', // Bearer auth header.
);
/** Safe-list values that should never be masked (condition rightValue). */ /** Safe-list values that should never be masked (condition rightValue). */
const SAFE_CONDITION_VALUES = array( const SAFE_CONDITION_VALUES = array(
'true', 'true',
@ -81,6 +95,25 @@ class MaskingRules {
}, },
$value $value
); );
$value = null !== $masked ? $masked : $value;
// Conditional pass for generic `?key=…` / `&key=…` — only redact when
// the captured value itself looks like a secret.
$masked = preg_replace_callback(
'/([?&]key=)([^&\s#]+)/i',
function ( $matches ) use ( &$log ) {
if ( ! self::looks_like_secret( $matches[2] ) ) {
return $matches[0];
}
$log[] = array(
'reason' => 'url_param_generic_key',
'key' => $matches[0],
'note' => 'Generic key query-param holds secret-shaped value.',
);
return $matches[1] . '[REDACTED]';
},
$value
);
return null !== $masked ? $masked : $value; return null !== $masked ? $masked : $value;
} }
@ -118,9 +151,74 @@ class MaskingRules {
); );
return '[REDACTED]'; return '[REDACTED]';
} }
// Generic `key` field names (common in n8n queryParameters) gated by
// entropy check to avoid false positives on harmless values like
// `{name:"key", value:"weather_berlin"}`.
$generic_key_names = array( 'key', 'x-key', 'access-key', 'x_key' );
if ( in_array( strtolower( $field_key ), $generic_key_names, true )
&& '' !== $value
&& '[REDACTED]' !== $value
&& self::looks_like_secret( $value ) ) {
$log[] = array(
'reason' => 'generic_key_with_entropy',
'key' => $field_key,
'note' => 'Generic key-named field holds secret-shaped value.',
);
return '[REDACTED]';
}
return $value; return $value;
} }
/**
* Heuristically detects whether a string value looks like a secret.
*
* Defense-in-depth: complements the name-based filter in
* mask_sensitive_field() by catching generic fields (e.g. n8n's
* queryParameters `{name:"key"}`) and custom headers whose names we
* cannot enumerate.
*
* Matches:
* - Known vendor tokens: `AIzaSy…`, `sk-proj-…`, `ghp_…`, `Bearer eyJ…`
* - Opaque high-entropy strings: ≥30 chars, mixed classes,
* no whitespace, no path separators.
*
* Skips (false-positive control):
* - URLs / filesystem paths (contain `/`)
* - Strings with whitespace (not vendor-matched)
* - Simple slugs like `weather_berlin`, `my-plugin`
*
* @since 1.0.4
* @param string $value Candidate value.
* @return bool True if the value has secret-like characteristics.
*/
public static function looks_like_secret( string $value ): bool {
$len = strlen( $value );
if ( $len < 20 ) {
return false;
}
foreach ( self::HIGH_ENTROPY_PATTERNS as $pattern ) {
if ( 1 === preg_match( $pattern, $value ) ) {
return true;
}
}
if ( $len < 30 ) {
return false;
}
if ( 1 === preg_match( '/[\s\/]/', $value ) ) {
return false;
}
$classes = (int) (bool) preg_match( '/[a-z]/', $value );
$classes += (int) (bool) preg_match( '/[A-Z]/', $value );
$classes += (int) (bool) preg_match( '/[0-9]/', $value );
return $classes >= 2;
}
/** /**
* Applies condition rightValue heuristic masking. * Applies condition rightValue heuristic masking.
* Used specifically for condition node parameter values. * Used specifically for condition node parameter values.

View file

@ -39,16 +39,100 @@ class WorkflowSanitizer {
if ( isset( $sanitized['nodes'] ) && is_array( $sanitized['nodes'] ) ) { if ( isset( $sanitized['nodes'] ) && is_array( $sanitized['nodes'] ) ) {
foreach ( $sanitized['nodes'] as &$node ) { foreach ( $sanitized['nodes'] as &$node ) {
$node = $this->mask_node( $node ); $node = $this->mask_node( $node );
$node = $this->mask_node_credentials( $node );
} }
unset( $node ); unset( $node );
} }
// Pass 3: Strip identifying metadata that ties a workflow to a
// specific n8n instance. The value is not a secret per se, but it
// correlates workflows across leaks.
if ( isset( $sanitized['meta'] ) && is_array( $sanitized['meta'] )
&& array_key_exists( 'instanceId', $sanitized['meta'] ) ) {
$this->mask_log[] = array(
'reason' => 'meta_instance_id',
'key' => 'meta.instanceId',
'note' => 'n8n instance identifier cleared.',
);
$sanitized['meta']['instanceId'] = null;
}
return array( return array(
'data' => $sanitized, 'data' => $sanitized,
'mask_log' => $this->mask_log, 'mask_log' => $this->mask_log,
); );
} }
/**
* Removes workflow tags (opt-in from wizard step 3).
*
* Tags in n8n are organisational labels (e.g. "production", "Donau") that
* can be either innocuous or identifying. Unlike credentials, there is no
* reliable heuristic to separate the two, so this is user-controlled and
* runs only when the publisher explicitly opts in.
*
* Static and stateless so callers (WizardPage::handle_publish) can invoke
* it without constructing a new Sanitizer instance.
*
* @since 1.0.4
* @param array $data Workflow data.
* @param array $mask_log Passed by reference removal entries appended.
* @return array Data with tags cleared.
*/
public static function strip_workflow_tags( array $data, array &$mask_log ): array {
if ( empty( $data['tags'] ) || ! is_array( $data['tags'] ) ) {
return $data;
}
foreach ( $data['tags'] as $tag ) {
$name = is_array( $tag ) && isset( $tag['name'] ) ? (string) $tag['name'] : '';
$mask_log[] = array(
'reason' => 'tags_cleared',
'key' => 'tags[].name',
'note' => '' !== $name
? 'Tag "' . esc_html( $name ) . '" removed.'
: 'Tag removed.',
);
}
$data['tags'] = array();
return $data;
}
/**
* Redacts credential display names on a node.
*
* n8n stores node credentials as an associative map keyed by credential
* type, each with `{ id, name }`. The `id` refers to the n8n DB and is
* useless without that server we keep it so round-tripping stays intact.
* The `name` is user-chosen (e.g. "Donau", "Privater OpenAI") and leaks
* organisational context. We replace it with `[REDACTED]`.
*
* @since 1.0.4
* @param array $node Single workflow node array.
* @return array Node with credential names redacted.
*/
private function mask_node_credentials( array $node ): array {
if ( ! isset( $node['credentials'] ) || ! is_array( $node['credentials'] ) ) {
return $node;
}
foreach ( $node['credentials'] as $cred_type => &$cred ) {
if ( is_array( $cred ) && isset( $cred['name'] ) && is_string( $cred['name'] )
&& '' !== $cred['name'] && '[REDACTED]' !== $cred['name'] ) {
$this->mask_log[] = array(
'reason' => 'credential_name',
'key' => (string) $cred_type . '.name',
'note' => 'Credential display name redacted (id retained).',
);
$cred['name'] = '[REDACTED]';
}
}
unset( $cred );
return $node;
}
/** /**
* Recursively sanitizes all string values in the data. * Recursively sanitizes all string values in the data.
* jsCode is preserved as-is (displayed with esc_html(), never executed). * jsCode is preserved as-is (displayed with esc_html(), never executed).
@ -156,5 +240,16 @@ class WorkflowSanitizer {
return; return;
} }
} }
// Entropy fallback: catches custom-header names we cannot enumerate
// (e.g. X-App-Token) when the value itself looks secret-shaped.
if ( MaskingRules::looks_like_secret( (string) $item['value'] ) ) {
$this->mask_log[] = array(
'reason' => 'value_entropy',
'key' => 'value',
'note' => 'Parameter "' . esc_html( $item['name'] ) . '" has secret-shaped value.',
);
$item['value'] = '[REDACTED]';
}
} }
} }

View file

@ -39,6 +39,31 @@ class Shortcode {
*/ */
private static bool $assets_enqueued = false; private static bool $assets_enqueued = false;
/**
* Monotonic counter for unique wrapper DOM IDs within a request.
*
* @var int
*/
private static int $instance_counter = 0;
/**
* Fingerprints of already-rendered shortcode invocations (post_id + resolved settings).
* Used to silently skip re-entrant passes triggered by plugins like Easy Table of Contents,
* which run the_content filters a second time to scan for headings.
*
* @var array<string, true>
*/
private static array $fingerprints = array();
/**
* Post IDs that have already emitted the share-anchor span in this request.
* The anchor id="breznflow-<POSTID>" must be unique in the DOM, so only the
* first instance per post emits it; later instances get a wrapper only.
*
* @var array<int, true>
*/
private static array $anchored_posts = array();
/** /**
* Registers the shortcode and footer hook. * Registers the shortcode and footer hook.
* *
@ -154,7 +179,27 @@ class Shortcode {
$max_code_lines = '' !== $atts['max_code_lines'] ? max( 1, (int) $atts['max_code_lines'] ) : (int) $settings['max_code_lines']; $max_code_lines = '' !== $atts['max_code_lines'] ? max( 1, (int) $atts['max_code_lines'] ) : (int) $settings['max_code_lines'];
// Increment view count. // Re-entry guard: plugins like Easy Table of Contents run the_content filters
// twice to scan for headings, which re-executes this shortcode. The returned
// HTML is often deduplicated by the outer filter, but the static $render_queue
// below would still accumulate a duplicate entry and the JS renderer would
// mount the workflow twice onto the same container. Hashing post_id + fully
// resolved render settings lets us silently skip those re-entrant passes
// while still allowing legitimate multi-embed (same post, different atts).
$fingerprint = md5(
(string) $post_id . '|' .
$mode . '|' . (string) $zoom . '|' . (string) $show_title . '|' .
(string) $show_infobox . '|' . (string) $allow_download . '|' .
(string) $show_minimap . '|' . (string) $max_code_lines . '|' .
$theme . '|' . (string) $allow_share . '|' . (string) $allow_embed . '|' .
(string) $allow_get_json
);
if ( isset( self::$fingerprints[ $fingerprint ] ) ) {
return '';
}
self::$fingerprints[ $fingerprint ] = true;
// Increment view count (after dedupe check, so re-entrant scans don't overcount).
ViewCounter::increment( $post_id ); ViewCounter::increment( $post_id );
// Enqueue assets once. // Enqueue assets once.
@ -163,9 +208,15 @@ class Shortcode {
self::$assets_enqueued = true; self::$assets_enqueued = true;
} }
// Unique per-instance DOM id so multiple embeds of the same workflow coexist.
++self::$instance_counter;
$instance_id = $post_id . '-' . self::$instance_counter;
$dom_id = 'breznflow-wrap-' . $instance_id;
// Queue workflow data for JS output. // Queue workflow data for JS output.
self::$render_queue[] = array( self::$render_queue[] = array(
'id' => $post_id, 'id' => $post_id,
'dom_id' => $dom_id,
'workflow' => $workflow, 'workflow' => $workflow,
'mode' => $mode, 'mode' => $mode,
'zoom' => $zoom ? $zoom : 100, 'zoom' => $zoom ? $zoom : 100,
@ -209,10 +260,18 @@ class Shortcode {
); );
$html .= InfoBoxBuilder::build( $categorized ); $html .= InfoBoxBuilder::build( $categorized );
} else { } else {
// Anchor span is only emitted for the first instance per post so the
// id="breznflow-<POSTID>" target stays unique; legacy deep-links keep working.
$anchor_html = '';
if ( ! isset( self::$anchored_posts[ $post_id ] ) ) {
$anchor_html = '<span id="breznflow-' . esc_attr( (string) $post_id ) . '" '
. 'aria-hidden="true" style="position:absolute;top:-60px;left:0"></span>';
self::$anchored_posts[ $post_id ] = true;
}
$html .= '<div style="position:relative">' $html .= '<div style="position:relative">'
. '<span id="breznflow-' . esc_attr( (string) $post_id ) . '" ' . $anchor_html
. 'aria-hidden="true" style="position:absolute;top:-60px;left:0"></span>' . '<div id="' . esc_attr( $dom_id ) . '" '
. '<div id="breznflow-wrap-' . esc_attr( (string) $post_id ) . '" '
. 'class="breznflow-embed" data-id="' . esc_attr( (string) $post_id ) . '">' . 'class="breznflow-embed" data-id="' . esc_attr( (string) $post_id ) . '">'
. '</div>' . '</div>'
. '</div>'; . '</div>';
@ -311,7 +370,7 @@ class Shortcode {
// Output custom themes as inline CSS. // Output custom themes as inline CSS.
$custom_css = ThemeRegistry::get_custom_theme_css(); $custom_css = ThemeRegistry::get_custom_theme_css();
if ( $custom_css ) { if ( $custom_css ) {
wp_add_inline_style( 'breznflow-renderer', $custom_css ); wp_add_inline_style( 'breznflow-renderer', wp_strip_all_tags( $custom_css ) );
} }
} }
} }

234
breznflow/readme.txt Normal file
View file

@ -0,0 +1,234 @@
=== BreznFlow ===
Contributors: mifupadev
Tags: n8n, workflow, automation, diagram, svg
Requires at least: 6.0
Tested up to: 6.9
Stable tag: 1.0.4
Requires PHP: 8.0
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Display n8n workflows as interactive SVG diagrams with zoom, pan, node details, and automatic secret masking.
== Description ==
BreznFlow turns n8n workflow JSON exports into interactive, zoomable SVG diagrams inside WordPress. Paste your workflow, and the plugin renders every node with brand-accurate colors, connection lines, and clickable detail panels — directly in your posts and pages.
The plugin was built for mifupa.com, a personal blog where n8n automations are documented regularly. Screenshots get outdated. Embedding the n8n editor is impractical. BreznFlow solves this: one shortcode, one interactive diagram, zero external dependencies.
= Learn more =
* Website: <a href="https://breznflow.com/">breznflow.com</a>
* FAQ: <a href="https://breznflow.com/faq.html">breznflow.com/faq</a>
* Live demo: <a href="https://breznflow.com/demo.html">breznflow.com/demo</a>
= At a glance =
* Renders n8n workflow JSON as interactive SVG diagrams with zoom, pan, and click
* 86 node types with brand-accurate colors and icons (OpenAI, Slack, GitHub, Telegram, and more)
* Click any node to inspect its parameters in a detail panel below the diagram
* Automatically masks API keys, tokens, and secrets before storage — the raw JSON is never saved
* 3-step import wizard: paste JSON, configure display, preview and publish
* 5 built-in themes (Dark, Light, Minimal, Tech, Brezn) plus custom theme import
* Shortcode `[breznflow id="X"]` with 13 attributes for per-instance configuration
* Action bar with share, embed, get JSON, and download actions
* Standalone embed page for iframe integration
* Related workflows by shared node types
* View counter and AI detection badges
* Zero dependencies — vanilla JavaScript, no external CDN, no tracking
= What makes BreznFlow different =
* **No external services.** Everything runs locally inside WordPress. No CDN, no SaaS, no API calls during page loads.
* **Security first.** Sensitive data (API keys, tokens, secrets) is automatically detected and replaced with `[REDACTED]` before storage. The raw workflow JSON is never saved.
* **Real interactivity.** Not a static image — visitors can zoom, pan, and click nodes to see their configuration.
* **Vanilla JavaScript.** No React, no jQuery, no build step. The renderer is a single JS file that generates SVG elements directly.
* **Built for real sites.** Running on the developer's own production sites since version 1.0.
= Display modes =
* **Visual** — full diagram with toolbar, detail panel, and action bar
* **Info** — node counts only (InfoBox summary like "3× HTTP Request, 2× Code") — no diagram
* **Compact** — diagram without toolbar or action bar
= Theme system =
5 built-in themes: Dark (default), Light, Minimal, Tech, and Brezn. Custom themes can be imported as `.breznflow.json` files containing 41 CSS color tokens. Themes are selectable globally, per workflow, or per shortcode.
= Node type registry =
86 predefined node types across 10 categories: Triggers, Core Logic, Data Transformation, Databases, Communication, Google, Dev Tools, AI, Storage, and CRM/Marketing. Unknown node types get a deterministic fallback with auto-generated colors — the same unknown type always looks the same.
= Sensitive data masking =
Before saving, BreznFlow runs a two-pass sanitization:
1. All strings pass through WordPress sanitization
2. Secret detection scans for API keys in URL parameters, sensitive header values (Authorization, Bearer, X-API-Key), and high-entropy condition values
A masking log records every redaction with reason and context — visible in the wizard preview.
= Action bar =
Below the diagram, the action bar provides four configurable actions:
* **Share** — article link and anchor link for hash navigation
* **Embed** — iframe embed code for standalone embedding
* **Get JSON** — formatted JSON display with file size
* **Download** — sanitized JSON file download
Each action can be enabled/disabled globally, per workflow, or per shortcode. Embed and Download use dual-gate security (global setting AND per-post permission).
== Installation ==
1. Download the plugin zip and go to **Plugins → Add New → Upload Plugin** in your WordPress admin.
2. Upload the zip file and click **Install Now**, then **Activate**.
3. Go to **BreznFlow → Add Workflow**.
4. Paste your n8n workflow JSON, upload a `.json` file, or fetch from a URL.
5. Configure display settings (mode, theme, zoom) and preview the diagram.
6. Publish — use `[breznflow id="X"]` in any post or page.
The plugin has no build step. All assets are direct JS/CSS files without transpiling or bundling.
== Frequently Asked Questions ==
= Where do I get a workflow JSON? =
In n8n, open your workflow and use the menu: **Workflow → Export → Download JSON**. Then paste the JSON into BreznFlow's import wizard or upload the file directly.
= Are my API keys safe? =
BreznFlow automatically detects and replaces common secret patterns with `[REDACTED]` before storing. This includes API keys in URL parameters, Authorization headers, and high-entropy condition values. The masking log in the wizard shows exactly what was masked and why. The raw workflow JSON is never saved — only the sanitized version.
Note: JavaScript code in Code nodes (`jsCode`) is displayed as plain text and never executed in the browser, but is NOT automatically scanned for secrets. Review Code node contents manually before publishing.
= Can I embed multiple workflows on one page? =
Yes. Use `[breznflow id="1"]`, `[breznflow id="2"]`, etc. The JavaScript and CSS are loaded only once regardless of how many shortcodes are on the page.
= What n8n version is supported? =
The plugin supports the standard n8n workflow JSON export format. It has been tested with workflows from n8n 1.x. The node type registry covers 86 predefined types — unknown types are handled gracefully with auto-generated colors and initials.
= Can visitors download the workflow JSON? =
Only if you enable it. Downloads require both the global `allow_download` setting and the per-workflow download permission to be enabled. Only the sanitized (masked) JSON is available — never the original.
= How does the embed feature work? =
When enabled, BreznFlow serves a standalone HTML page at `?breznflow_embed={id}` — no WordPress theme, no admin bar, just the SVG renderer. You can embed this via iframe. Both the global `allow_embed` setting and the per-workflow `_breznflow_show_embed` permission must be enabled (dual-gate security). The embed page includes `X-Robots-Tag: noindex, nofollow` headers.
= Can I customize the appearance? =
Yes, at three levels: (1) Choose from 5 built-in themes or import custom themes with 41 CSS color tokens. (2) Set display mode, zoom level, and feature toggles per workflow. (3) Override any setting per shortcode with 13 available attributes.
= What are the shortcode attributes? =
`[breznflow id="42" mode="compact" theme="light" zoom="80" show_title="1" show_infobox="1" show_minimap="0" show_download="0" show_share="1" show_embed="0" show_get_json="0" max_code_lines="50"]`
Only `id` is required. All other attributes fall back to the workflow's saved settings, then to the global plugin settings.
= Does it work with page builders? =
Yes. BreznFlow uses a standard WordPress shortcode (`[breznflow]`), which works in Gutenberg, the Classic Editor, Elementor, Divi, WPBakery, and any other builder that processes shortcodes.
= Will it slow down my site? =
No. The renderer JS and CSS are only loaded on pages that contain a `[breznflow]` shortcode. No external HTTP requests are made during page loads. All rendering happens client-side.
== External Services ==
This plugin optionally connects to external services only when you explicitly 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 makes 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, no cookies
* **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 and internal network addresses are blocked: localhost, `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, and cloud metadata endpoints (e.g. `169.254.169.254`).
== Screenshots ==
1. 3-step import wizard — Step 1: paste, upload, or fetch your n8n workflow JSON
2. Step 2: configure display settings, theme, and categories
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.4 =
* Security: Detect generic `key` fields (n8n Google API pattern `queryParameters.parameters[].name = "key"`) and redact them when the value looks secret-shaped. Closes a gap where API keys bypassed the name-based filter.
* Security: Defense-in-depth entropy heuristic (`looks_like_secret()`) with vendor regex for AIza / sk- / ghp_ / Bearer plus a length+char-class fallback — catches custom tokens the name allowlist can't enumerate.
* Security: Redact credential display names and `meta.instanceId` so workflow exports can no longer be correlated to the originating n8n instance or team.
* Security: Optional tag removal at publish time (opt-in checkbox in wizard step 3). Workflow tags are often harmless but occasionally identifying — publisher decides per workflow.
* Security: Wizard step 3 now shows a collapsible Reason / Key / Note table listing exactly what was masked, so publishers can verify before clicking Publish.
* Mobile: Rewrote the SVG touch handling. Single-finger pan is now smooth; pinch-to-zoom and double-tap-to-zoom work on iOS and Android. `touch-action: none` on the diagram SVG ends the browser-vs-plugin gesture tug-of-war that caused the "finger loses tracking" stutter.
* Mobile: Minimap now responds to touch — tap or drag to navigate.
* Note: Starting a touch on the diagram SVG blocks page scroll until the finger lifts. This is intentional so gestures are unambiguous; scroll around the diagram still works.
= 1.0.3 =
* Fixed double rendering when "Easy Table of Contents" (or any plugin that re-runs `the_content` filters) is active. The shortcode now silently deduplicates re-entrant invocations via a fingerprint of post id + resolved render settings.
* Wrapper `id` is now unique per instance (`breznflow-wrap-<POSTID>-<COUNTER>`), enabling multiple embeds of the same workflow with different attributes in one post.
* Anchor span `id="breznflow-<POSTID>"` is emitted only for the first instance per post to keep the DOM valid and preserve existing share links.
* Renderer now guards against mounting twice onto the same container.
= 1.0.2 =
* Fixed WordPress.org plugin review issues.
* Embed page now uses wp_enqueue_style/wp_enqueue_script with wp_head/wp_footer instead of direct HTML tags.
* Added nonce verification to wizard step navigation (steps 2 and 3).
* Improved input sanitization for $_FILES handling in theme import.
* Improved JSON input handling with explicit type validation.
* Added wp_strip_all_tags() escaping for inline CSS in wp_add_inline_style() calls.
* Added late escaping (sanitize_key, esc_attr) in custom theme CSS output.
* Improved phpcs:ignore documentation for public read-only endpoints.
= 1.0.1 =
* Fixed WordPress Plugin Check warnings for WordPress.org compliance.
* Removed deprecated `load_plugin_textdomain()` call — translations are now loaded automatically by WordPress (since WP 4.6).
* Prefixed all global template variables in themes.php with `breznflow_` for WPCS naming conventions compliance.
= 1.0.0 =
* Interactive SVG renderer with zoom, pan, and node detail panel.
* 3-step import wizard with JSON validation, URL fetch, and sensitive data masking.
* 86 node type registry with brand colors and icons.
* Shortcode `[breznflow]` with 13 attributes for mode, zoom, theme, and display toggles.
* Auto-fit zoom for large workflows (configurable threshold, default: 30 nodes).
* Minimap toggle per workflow and via shortcode attribute.
* 5 built-in themes (Dark, Light, Minimal, Tech, Brezn) plus custom theme import via `.breznflow.json` files.
* Action bar with share, embed, get JSON, and download buttons.
* Embed handler for standalone iframe embedding with dual-gate security.
* Download handler for sanitized JSON export with dual-gate security.
* Two-pass sensitive data masking: URL parameters, header values, and entropy-based condition detection.
* View counter and related workflows by shared node types.
* AI detection badges for workflows containing AI nodes.
* InfoBox node summary (e.g. "3× HTTP Request, 2× Code").
* Schema.org HowTo structured data support.
* Anchor navigation with `#breznflow-{id}` hash links and 60px scroll offset.
* Custom post type `breznflow_workflow` with hierarchical `breznflow_category` taxonomy.
* Complete German translation.
* Zero dependencies — vanilla JavaScript, no external CDN, no tracking.
== Upgrade Notice ==
= 1.0.4 =
Closes a secret-masking gap for n8n's generic `key` query-parameter pattern (Google API keys), redacts credential names and instance IDs, and rewrites mobile touch handling so pan / pinch / double-tap work on iOS and Android.
= 1.0.3 =
Fixes duplicate workflow rendering when "Easy Table of Contents" is active, and enables reliable multi-embed of the same workflow in one post.
= 1.0.2 =
Fixes WordPress.org plugin review issues: proper asset enqueueing, nonce verification, input sanitization, and output escaping.
= 1.0.1 =
Fixes WordPress Plugin Check warnings. No functionality changes.
= 1.0.0 =
Initial release.

View file

@ -1,123 +0,0 @@
=== BreznFlow ===
Contributors: noschmarrn
Tags: n8n, workflow, automation, diagram, svg
Requires at least: 6.0
Tested up to: 6.9
Stable tag: 1.0.1
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.