diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fbedac6 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e95d352 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +vendor/ +*.zip +/node_modules/ +.claude/ +*.log diff --git a/README.de.md b/README.de.md index 58304ff..6f80b49 100644 --- a/README.de.md +++ b/README.de.md @@ -3,7 +3,7 @@    - + 🇬🇧 [English version → README.md](README.md) diff --git a/README.md b/README.md index be1d0b1..8e56a3d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@    - + 🇩🇪 [Deutsche Version → README.de.md](README.de.md) diff --git a/breznflow/assets/brezn.css b/breznflow/assets/brezn.css deleted file mode 100644 index 06de5f1..0000000 --- a/breznflow/assets/brezn.css +++ /dev/null @@ -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; -} diff --git a/breznflow/breznflow.php b/breznflow/breznflow.php index 3bc3a92..b35b56c 100644 --- a/breznflow/breznflow.php +++ b/breznflow/breznflow.php @@ -8,12 +8,12 @@ * Plugin Name: BreznFlow * Plugin URI: https://breznflow.com/ * 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 PHP: 8.0 - * Author: mifupa - * Author URI: https://mifupa.com/ - * License: GPL-2.0-or-later + * Author: NoSchmarrn.dev + * Author URI: https://noschmarrn.dev/ + * License: GPLv2 or later * License URI: https://www.gnu.org/licenses/gpl-2.0.html * Text Domain: breznflow * Domain Path: /languages @@ -23,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } -define( 'BREZNFLOW_VERSION', '1.0.1' ); +define( 'BREZNFLOW_VERSION', '1.0.2' ); define( 'BREZNFLOW_FILE', __FILE__ ); define( 'BREZNFLOW_DIR', plugin_dir_path( __FILE__ ) ); define( 'BREZNFLOW_URL', plugin_dir_url( __FILE__ ) ); diff --git a/breznflow/includes/Admin/AdminMenu.php b/breznflow/includes/Admin/AdminMenu.php index 75edeb6..7d78f5f 100644 --- a/breznflow/includes/Admin/AdminMenu.php +++ b/breznflow/includes/Admin/AdminMenu.php @@ -170,7 +170,18 @@ class AdminMenu { * @return 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 ) { case 2: require BREZNFLOW_DIR . 'includes/Admin/views/wizard-step-2.php'; diff --git a/breznflow/includes/Admin/ThemesPage.php b/breznflow/includes/Admin/ThemesPage.php index 7dde88a..2c65810 100644 --- a/breznflow/includes/Admin/ThemesPage.php +++ b/breznflow/includes/Admin/ThemesPage.php @@ -52,11 +52,16 @@ class ThemesPage { 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. - $filename = isset( $file['name'] ) ? sanitize_file_name( $file['name'] ) : ''; - if ( ! str_ends_with( $filename, '.breznflow.json' ) && ! str_ends_with( $filename, '.json' ) ) { + if ( ! str_ends_with( $file_name, '.breznflow.json' ) && ! str_ends_with( $file_name, '.json' ) ) { wp_safe_redirect( add_query_arg( array( @@ -69,7 +74,7 @@ class ThemesPage { exit; } - if ( ! isset( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) { + if ( '' === $file_tmp || ! is_uploaded_file( $file_tmp ) ) { wp_safe_redirect( add_query_arg( array( @@ -83,7 +88,7 @@ class ThemesPage { } // 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 ) { wp_safe_redirect( add_query_arg( diff --git a/breznflow/includes/Admin/WizardPage.php b/breznflow/includes/Admin/WizardPage.php index de60ab5..833d5e4 100644 --- a/breznflow/includes/Admin/WizardPage.php +++ b/breznflow/includes/Admin/WizardPage.php @@ -83,8 +83,12 @@ class WizardPage { wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'breznflow' ) ) ); } - // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- verified above; sanitize_textarea_field() is not used because strip_tags() corrupts JSON (strips HTML inside string values); sanitization happens after json_decode() in WorkflowSanitizer - $raw = isset( $_POST['json'] ) ? trim( wp_unslash( (string) $_POST['json'] ) ) : ''; + // 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'] ) ? wp_unslash( $_POST['json'] ) : ''; + if ( ! is_string( $raw ) ) { + $raw = ''; + } + $raw = trim( $raw ); if ( '' === $raw ) { wp_send_json_error( array( 'message' => __( 'No JSON provided.', 'breznflow' ) ) ); @@ -191,8 +195,12 @@ class WizardPage { wp_die( esc_html__( 'Insufficient permissions.', 'breznflow' ) ); } - // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- verified above; sanitize_textarea_field() is not used because strip_tags() corrupts JSON (strips HTML inside string values); sanitization happens after json_decode() in WorkflowSanitizer - $raw = isset( $_POST['breznflow_json'] ) ? trim( wp_unslash( (string) $_POST['breznflow_json'] ) ) : ''; + // 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'] ) ? wp_unslash( $_POST['breznflow_json'] ) : ''; + if ( ! is_string( $raw ) ) { + $raw = ''; + } + $raw = trim( $raw ); if ( '' === $raw ) { wp_safe_redirect( @@ -296,9 +304,10 @@ class WizardPage { wp_safe_redirect( add_query_arg( array( - 'page' => 'breznflow-add', - 'step' => '2', - 'post_id' => $post_id, + 'page' => 'breznflow-add', + 'step' => '2', + 'post_id' => $post_id, + '_wpnonce' => wp_create_nonce( 'breznflow_wizard_step' ), ), admin_url( 'admin.php' ) ) @@ -368,9 +377,10 @@ class WizardPage { wp_safe_redirect( add_query_arg( array( - 'page' => 'breznflow-add', - 'step' => '3', - 'post_id' => $post_id, + 'page' => 'breznflow-add', + 'step' => '3', + 'post_id' => $post_id, + '_wpnonce' => wp_create_nonce( 'breznflow_wizard_step' ), ), admin_url( 'admin.php' ) ) diff --git a/breznflow/includes/Admin/WorkflowListTable.php b/breznflow/includes/Admin/WorkflowListTable.php index 1c07e6d..2325e1d 100644 --- a/breznflow/includes/Admin/WorkflowListTable.php +++ b/breznflow/includes/Admin/WorkflowListTable.php @@ -162,9 +162,10 @@ class WorkflowListTable extends \WP_List_Table { protected function column_title( $item ): string { $edit_url = add_query_arg( array( - 'page' => 'breznflow-add', - 'step' => '2', - 'post_id' => $item->ID, + 'page' => 'breznflow-add', + 'step' => '2', + 'post_id' => $item->ID, + '_wpnonce' => wp_create_nonce( 'breznflow_wizard_step' ), ), admin_url( 'admin.php' ) ); diff --git a/breznflow/includes/Admin/views/wizard-step-2.php b/breznflow/includes/Admin/views/wizard-step-2.php index 35eec3c..951e121 100644 --- a/breznflow/includes/Admin/views/wizard-step-2.php +++ b/breznflow/includes/Admin/views/wizard-step-2.php @@ -11,7 +11,7 @@ if ( ! defined( 'ABSPATH' ) ) { } // 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; $workflow = $post_id > 0 ? get_post( $post_id ) : null; diff --git a/breznflow/includes/Admin/views/wizard-step-3.php b/breznflow/includes/Admin/views/wizard-step-3.php index 57a91d5..d6594d6 100644 --- a/breznflow/includes/Admin/views/wizard-step-3.php +++ b/breznflow/includes/Admin/views/wizard-step-3.php @@ -11,7 +11,7 @@ if ( ! defined( 'ABSPATH' ) ) { } // 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; $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(); 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( 'breznflow-renderer', @@ -160,9 +160,10 @@ $preview_theme = in_array( $saved_theme, $allowed_themes, true ) ? $saved_them echo esc_url( add_query_arg( array( - 'page' => 'breznflow-add', - 'step' => '2', - 'post_id' => $post_id, + 'page' => 'breznflow-add', + 'step' => '2', + 'post_id' => $post_id, + '_wpnonce' => wp_create_nonce( 'breznflow_wizard_step' ), ), admin_url( 'admin.php' ) ) diff --git a/breznflow/includes/DownloadHandler.php b/breznflow/includes/DownloadHandler.php index 53b9be3..977905b 100644 --- a/breznflow/includes/DownloadHandler.php +++ b/breznflow/includes/DownloadHandler.php @@ -36,11 +36,13 @@ class DownloadHandler { * @return 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; } - $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 ) { status_header( 400 ); diff --git a/breznflow/includes/EmbedHandler.php b/breznflow/includes/EmbedHandler.php index 6bfb8c9..b953c85 100644 --- a/breznflow/includes/EmbedHandler.php +++ b/breznflow/includes/EmbedHandler.php @@ -36,7 +36,7 @@ class EmbedHandler { * @return void */ public function handle_embed(): void { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only embed endpoint, no state change; only serves published data. + // 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'] ) ) { return; } @@ -80,11 +80,11 @@ class EmbedHandler { } $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'] ) ) : ''; $theme = in_array( $url_theme, $allowed_themes, true ) ? $url_theme : ( $settings['default_theme'] ?? 'dark' ); $theme = in_array( $theme, $allowed_themes, true ) ? $theme : 'dark'; - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only embed page, no state change. + // 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; $body_bgs = array( @@ -96,19 +96,20 @@ class EmbedHandler { ); $body_bg = $body_bgs[ $theme ] ?? '#1a1a2e'; + // Hide admin bar on standalone embed page. + show_admin_bar( false ); + // Set headers. header( 'Content-Type: text/html; charset=utf-8' ); header( 'X-Robots-Tag: noindex, nofollow' ); header( 'X-Content-Type-Options: nosniff' ); header_remove( 'X-Frame-Options' ); - $article_url = esc_url( get_permalink( $post_id ) ); + $article_url = get_permalink( $post_id ); $anchor_id = 'breznflow-' . $post_id; - $blog_name = esc_html( get_bloginfo( 'name' ) ); - $blog_url = esc_url( home_url( '/' ) ); - $title = esc_html( $post->post_title ); - $css_url = esc_url( BREZNFLOW_URL . 'assets/renderer.css' ) . '?v=' . BREZNFLOW_VERSION; - $js_url = esc_url( BREZNFLOW_URL . 'assets/renderer.js' ) . '?v=' . BREZNFLOW_VERSION; + $blog_name = get_bloginfo( 'name' ); + $blog_url = home_url( '/' ); + $title = $post->post_title; $inline_data = array( array( @@ -131,55 +132,60 @@ class EmbedHandler { ), ); - $icons_json = wp_json_encode( Features\NodeTypeRegistry::get_registry() ); - $data_json = wp_json_encode( $inline_data ); - $i18n_json = wp_json_encode( Shortcode::get_js_i18n() ); + // Enqueue renderer assets via WordPress enqueue system. + wp_enqueue_style( 'breznflow-renderer', BREZNFLOW_URL . 'assets/renderer.css', array(), BREZNFLOW_VERSION ); + wp_enqueue_script( 'breznflow-renderer', BREZNFLOW_URL . 'assets/renderer.js', array(), BREZNFLOW_VERSION, true ); + + foreach ( \BreznFlow\Features\ThemeRegistry::BUILTIN as $bf_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 ?>
-