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
This commit is contained in:
Michael 2026-04-14 11:21:48 +00:00
parent fb206850d5
commit 066414724b
17 changed files with 166 additions and 132 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

View file

@ -3,7 +3,7 @@
![PHP 8.0+](https://img.shields.io/badge/PHP-8.0%2B-blue) ![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) ![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) ![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.1-orange) ![Version](https://img.shields.io/badge/Version-1.0.2-orange)
🇬🇧 [English version → README.md](README.md) 🇬🇧 [English version → README.md](README.md)

View file

@ -3,7 +3,7 @@
![PHP 8.0+](https://img.shields.io/badge/PHP-8.0%2B-blue) ![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) ![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) ![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.1-orange) ![Version](https://img.shields.io/badge/Version-1.0.2-orange)
🇩🇪 [Deutsche Version → README.de.md](README.de.md) 🇩🇪 [Deutsche Version → README.de.md](README.de.md)

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

@ -8,12 +8,12 @@
* Plugin Name: BreznFlow * Plugin Name: BreznFlow
* Plugin URI: https://breznflow.com/ * 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.2
* Requires at least: 6.0 * Requires at least: 6.0
* Requires PHP: 8.0 * Requires PHP: 8.0
* Author: mifupa * Author: NoSchmarrn.dev
* Author URI: https://mifupa.com/ * Author URI: https://noschmarrn.dev/
* License: GPL-2.0-or-later * License: GPLv2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html * License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: breznflow * Text Domain: breznflow
* Domain Path: /languages * Domain Path: /languages
@ -23,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
} }
define( 'BREZNFLOW_VERSION', '1.0.1' ); define( 'BREZNFLOW_VERSION', '1.0.2' );
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(
@ -299,6 +307,7 @@ class WizardPage {
'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' )
) )
@ -371,6 +380,7 @@ class WizardPage {
'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' )
) )

View file

@ -165,6 +165,7 @@ class WorkflowListTable extends \WP_List_Table {
'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;
@ -102,7 +102,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',
@ -163,6 +163,7 @@ $preview_theme = in_array( $saved_theme, $allowed_themes, true ) ? $saved_them
'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

@ -311,7 +311,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 ) );
} }
} }
} }

View file

@ -3,12 +3,12 @@ Contributors: mifupadev
Tags: n8n, workflow, automation, diagram, svg Tags: n8n, workflow, automation, diagram, svg
Requires at least: 6.0 Requires at least: 6.0
Tested up to: 6.9 Tested up to: 6.9
Stable tag: 1.0.1 Stable tag: 1.0.2
Requires PHP: 8.0 Requires PHP: 8.0
License: GPL-2.0-or-later License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html License URI: https://www.gnu.org/licenses/gpl-2.0.html
Display n8n automation workflows as interactive SVG diagrams in WordPress posts and pages — with zoom, pan, node detail panels, and automatic sensitive data masking. Display n8n workflows as interactive SVG diagrams with zoom, pan, node details, and automatic secret masking.
== Description == == Description ==
@ -158,6 +158,16 @@ For security, requests to private and internal network addresses are blocked: lo
== Changelog == == Changelog ==
= 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 = = 1.0.1 =
* Fixed WordPress Plugin Check warnings for WordPress.org compliance. * 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). * Removed deprecated `load_plugin_textdomain()` call — translations are now loaded automatically by WordPress (since WP 4.6).
@ -186,6 +196,9 @@ For security, requests to private and internal network addresses are blocked: lo
== Upgrade Notice == == Upgrade Notice ==
= 1.0.2 =
Fixes WordPress.org plugin review issues: proper asset enqueueing, nonce verification, input sanitization, and output escaping.
= 1.0.1 = = 1.0.1 =
Fixes WordPress Plugin Check warnings. No functionality changes. Fixes WordPress Plugin Check warnings. No functionality changes.