* * * Both values are validated before use, the attributes are escaped, and the * script URL is built from the fixed Zyff origin — never taken from an API * response or a post — so nothing stored in this site can load a script from * anywhere else. * * Rendering needs no connection and makes no request from this server. The * visitor's browser loads the runtime, which fetches the widget and draws it. * * @package ZyffWidgets */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Block render, shortcode and site-wide placement. */ class Zyff_Widgets_Embed { /** Option: widgets shown on every page, as a list of [ 'id' => …, 'type' => … ]. */ const SITEWIDE_OPTION = 'zyff_widgets_sitewide'; /** Hook everything up. */ public static function init() { add_shortcode( 'zyff', array( __CLASS__, 'shortcode' ) ); add_action( 'wp_footer', array( __CLASS__, 'sitewide' ), 5 ); } /** * The markup for one widget, with its runtime enqueued. * * Returns '' for anything malformed: an empty space on the page is the * worst acceptable outcome, never an error box on a visitor's screen. * * @param string $public_id The widget's public id. * @param string $type The widget type (Zyff's platform id). * @return string */ public static function markup( $public_id, $type ) { $public_id = (string) $public_id; $type = (string) $type; if ( ! preg_match( Zyff_Widgets_API::ID_PATTERN, $public_id ) || ! preg_match( Zyff_Widgets_API::TYPE_PATTERN, $type ) ) { return ''; } // One handle per type, so two FAQ widgets on a page load faq.js once. // A null version keeps the URL exactly as published: it is Zyff's // "forever" URL, and Zyff updates what it serves. wp_enqueue_script( 'zyff-runtime-' . $type, Zyff_Widgets_API::origin() . '/w/' . $type . '.js', array(), null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- see above. array( 'strategy' => 'defer', 'in_footer' => true, ) ); return sprintf( '
', esc_attr( 'zyff-' . $type ), esc_attr( $type ), esc_attr( $public_id ) ); } /** * [zyff id="wgt_8f3a2c5d91" type="faq"] * * @param array|string $atts Shortcode attributes. * @return string */ public static function shortcode( $atts ) { $atts = shortcode_atts( array( 'id' => '', 'type' => '', ), $atts, 'zyff' ); return self::markup( $atts['id'], $atts['type'] ); } /** * Widgets chosen as "Show on every page", printed in the footer. * * The natural home for a popup, an announcement bar, a chat button or a * back-to-top button: they position themselves, so where the container * sits in the page does not matter. */ public static function sitewide() { if ( is_admin() || is_feed() || is_embed() ) { return; } foreach ( self::sitewide_widgets() as $w ) { echo self::markup( $w['id'], $w['type'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- markup() escapes every attribute. } } /** * The stored site-wide list, re-validated on read. * * @return array */ public static function sitewide_widgets() { $stored = get_option( self::SITEWIDE_OPTION, array() ); $out = array(); if ( ! is_array( $stored ) ) { return $out; } foreach ( $stored as $w ) { if ( is_array( $w ) && isset( $w['id'], $w['type'] ) && preg_match( Zyff_Widgets_API::ID_PATTERN, $w['id'] ) && preg_match( Zyff_Widgets_API::TYPE_PATTERN, $w['type'] ) ) { $out[] = array( 'id' => $w['id'], 'type' => $w['type'], ); } } return $out; } }