$content])), (string)($vars['title'] ?? '')); return; } stream_template('layout_' . $layout, $vars + ['content' => $content]); } /** The file of a template, from this plugin or an add-on (template_dirs()). */ function template_file(string $tpl): string { if (!preg_match('#^[a-z0-9_/]+$#', $tpl)) throw new RuntimeException(esc_html('Bad template name: ' . $tpl)); foreach (template_dirs() as $dir) if (is_file($dir . $tpl . '.php')) return $dir . $tpl . '.php'; throw new RuntimeException(esc_html('Template not found: ' . $tpl)); } /** Print a template in place (like get_template_part()). The template escapes what it prints. */ function include_template(string $__tpl, array $__vars = []): void { $__file = template_file($__tpl); // The template's variables, by name. Only plain variable names; names starting with "__" and $this are never set. foreach ($__vars as $__name => $__value) { if (!is_string($__name) || $__name === 'this' || str_starts_with($__name, '__') || !preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $__name)) continue; ${$__name} = $__value; } unset($__name, $__value, $__vars); include $__file; } /** A template's markup as a string, for emails and for blocks a page places later (printed with wp_kses()). */ function render_partial(string $tpl, array $vars = []): string { ob_start(); include_template($tpl, $vars); return (string)ob_get_clean(); } /** Print a whole page or document from a template, with internal paths mapped to WordPress URLs. */ function stream_template(string $tpl, array $vars = []): void { ob_start(fn(string $buffer): string => gatecraft_map_html($buffer)); include_template($tpl, $vars); ob_end_flush(); } /** Rewrite internal paths in our HTML ("/e/x", "/admin/...", "/uploads/...") to real WordPress URLs. */ function gatecraft_map_html(string $html): string { $html = i18n_html($html); $html = preg_replace_callback('/\b(href|action|formaction|src|data-bg|data-endpoint|data-quote-url)="(\/(?!\/)[^"]*)"/', function ($m) { return $m[1] . '="' . esc_attr(map_url(html_entity_decode($m[2], ENT_QUOTES, 'UTF-8'))) . '"'; }, $html); // GET forms (filters, search): the browser replaces the action's query string with the form fields, // which would drop page=... in wp-admin. Move those parameters into hidden inputs. $html = preg_replace_callback('/
'; } /** A row of filter chips: [value => [label, count]]. The chosen one is marked; links keep the other filters. */ function filter_chips(string $param, string $current, array $chips, array $query, string $label = 'Filter'): string { $h = ''; } function paginate(int $total, int $perPage, int $page, array $query): string { $pages = (int)ceil($total / $perPage); if ($pages <= 1) return ''; $h = ''; } /** * The tags and attributes the plugin's own HTML helpers produce (form fields, icons, badges, charts, QR codes). * Helper output is passed through wp_kses() with this list before it is printed. */ function allowed_html(): array { static $a = null; if ($a !== null) return $a; $global = ['class', 'id', 'title', 'role', 'hidden', 'tabindex', 'lang', 'dir', 'translate', 'data-*', 'aria-label', 'aria-hidden', 'aria-current', 'aria-expanded', 'aria-pressed', 'aria-describedby', 'aria-live', 'aria-controls', 'aria-labelledby', 'aria-invalid', 'aria-required', 'aria-selected', 'aria-haspopup', 'aria-disabled', 'aria-modal', 'aria-atomic', 'aria-busy', 'aria-valuemin', 'aria-valuemax', 'aria-valuenow']; $tags = [ 'a' => ['href', 'target', 'rel', 'download', 'hreflang'], 'abbr' => [], 'article' => [], 'aside' => [], 'b' => [], 'br' => [], 'code' => [], 'em' => [], 'i' => [], 'button' => ['type', 'name', 'value', 'disabled', 'formaction', 'formnovalidate', 'form', 'autofocus'], 'details' => ['open'], 'summary' => [], 'div' => [], 'span' => [], 'p' => [], 'pre' => [], 'section' => [], 'header' => [], 'footer' => [], 'nav' => [], 'main' => [], 'strong' => [], 'small' => [], 'mark' => [], 'kbd' => [], 'sup' => [], 'sub' => [], 'hr' => [], 'time' => ['datetime'], 'h1' => [], 'h2' => [], 'h3' => [], 'h4' => [], 'h5' => [], 'h6' => [], 'ul' => [], 'ol' => ['start', 'reversed'], 'li' => ['value'], 'dl' => [], 'dt' => [], 'dd' => [], 'blockquote' => ['cite'], 'figure' => [], 'figcaption' => [], 'noscript' => [], 'table' => [], 'caption' => [], 'colgroup' => ['span'], 'col' => ['span'], 'thead' => [], 'tbody' => [], 'tfoot' => [], 'tr' => [], 'th' => ['scope', 'colspan', 'rowspan'], 'td' => ['colspan', 'rowspan'], 'img' => ['src', 'srcset', 'sizes', 'alt', 'width', 'height', 'loading', 'decoding'], 'iframe' => ['src', 'name', 'width', 'height', 'allow', 'allowfullscreen', 'frameborder', 'loading', 'referrerpolicy'], 'video' => ['src', 'poster', 'width', 'height', 'controls', 'muted', 'playsinline', 'autoplay', 'loop', 'preload'], 'audio' => ['src', 'controls', 'preload', 'loop'], 'source' => ['src', 'srcset', 'type', 'media', 'sizes'], 'track' => ['src', 'kind', 'srclang', 'label', 'default'], 'form' => ['method', 'action', 'enctype', 'novalidate', 'autocomplete', 'target'], 'fieldset' => ['disabled'], 'legend' => [], 'label' => ['for'], 'input' => ['type', 'name', 'value', 'checked', 'disabled', 'readonly', 'required', 'placeholder', 'maxlength', 'minlength', 'min', 'max', 'step', 'pattern', 'autocomplete', 'inputmode', 'accept', 'capture', 'autofocus', 'autocapitalize', 'spellcheck', 'size', 'multiple', 'list', 'form', 'formnovalidate'], 'select' => ['name', 'required', 'disabled', 'multiple', 'autocomplete', 'size'], 'option' => ['value', 'selected', 'disabled', 'label'], 'optgroup' => ['label', 'disabled'], 'textarea' => ['name', 'rows', 'cols', 'required', 'placeholder', 'maxlength', 'disabled', 'readonly', 'spellcheck', 'autocomplete'], 'datalist' => [], 'output' => ['for', 'name'], 'template' => [], 'progress' => ['value', 'max'], 'meter' => ['value', 'min', 'max', 'low', 'high', 'optimum'], 'svg' => ['xmlns', 'viewbox', 'width', 'height', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'focusable', 'preserveaspectratio', 'shape-rendering'], 'g' => ['fill', 'stroke', 'transform', 'opacity'], 'title' => [], 'defs' => [], 'path' => ['d', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-dashoffset', 'fill-rule', 'clip-rule', 'transform', 'opacity'], 'rect' => ['x', 'y', 'width', 'height', 'rx', 'ry', 'fill', 'stroke', 'stroke-width', 'transform', 'opacity'], 'circle' => ['cx', 'cy', 'r', 'fill', 'fill-opacity', 'opacity', 'stroke', 'stroke-width', 'stroke-dasharray', 'stroke-dashoffset', 'clip-path', 'transform'], 'clippath' => [], 'line' => ['x1', 'y1', 'x2', 'y2', 'stroke', 'stroke-width', 'stroke-linecap'], 'polyline' => ['points', 'fill', 'stroke', 'stroke-width'], 'polygon' => ['points', 'fill', 'stroke'], 'text' => ['x', 'y', 'text-anchor', 'dominant-baseline', 'font-size', 'fill'], ]; $a = []; foreach ($tags as $t => $attrs) $a[$t] = array_fill_keys(array_merge($global, $attrs), true); // Rich text written in the editor (event descriptions, announcements) keeps what rich_clean() allowed when it was saved. foreach (rich_allowed_html() as $t => $attrs) $a[$t] = ($a[$t] ?? []) + (is_array($attrs) ? $attrs : []); return $a; } /** How the list of events is laid out on the home page and in the events shortcode: value => label. */ function event_list_views(): array { return ['small' => 'Small', 'medium' => 'Medium', 'wide' => 'Wide', 'full' => 'Full width', 'list' => 'List']; } function event_list_view(?string $v = null): string { $v ??= setting('event_list_view'); return isset(event_list_views()[$v]) ? $v : 'medium'; } /** Colour picker with a hex box and a few presets. Empty means "use the default". Used for ticket category colours. */ function color_field(string $id, ?string $value, string $label = 'Colour', string $help = 'Pick a colour or type a hex code. Leave empty to use the event colour.'): string { $v = $value && preg_match('/^#[0-9a-f]{6}$/i', $value) ? strtolower($value) : ''; $presets = ['#b8860b' => 'Gold', '#8b939c' => 'Silver', '#a0522d' => 'Bronze', '#dc2626' => 'Red', '#ea580c' => 'Orange', '#16a34a' => 'Green', '#0d9488' => 'Teal', '#2563eb' => 'Blue', '#7c3aed' => 'Violet', '#db2777' => 'Pink', '#0f172a' => 'Black']; $h = '