breznflow/includes/EmbedHandler.php
Michael fd83e4810b BreznFlow 1.0.0 — WordPress.org submission
Initial public release of BreznFlow, an n8n workflow renderer for WordPress.
Fully PHPCS-compliant (WordPress Coding Standards), security-hardened,
and ready for WordPress.org plugin review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 11:27:36 +00:00

185 lines
7.1 KiB
PHP

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