Site Builder
Editing:
modules-faq.php
writable 0666
<?php /************************************************************************** * FAQ DISPLAY MODULE – gold‑card accordion © BestDealOn 2025 * ---------------------------------------------------------------------- * • Requires helpers.php (get_faq()) to be loaded first. * • Lets authors use a SAFE subset of HTML inside answers / questions: * <b> <strong> <i> <em> <u> <br> <ul> <ol> <li> * <a href="…"> (other attributes are stripped automatically) **************************************************************************/ if (!function_exists('get_faq')) return; // defensive fallback $cats = array_filter(get_faq(), fn($c)=>!empty($c['faqs'])); if (!$cats) return; /* ------------------------------------------------------------------ */ /* 1. HTML sanitizer – super‑light allow‑list */ /* ------------------------------------------------------------------ */ function safe_html(string $html): string { static $allowedTags = ['b','strong','i','em','u','br','ul','ol','li','a']; static $allowedAttr = ['href']; if (trim($html)==='') return ''; // load as HTML‑fragment $doc = new DOMDocument('1.0','utf-8'); $doc->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'. '<div>'.$html.'</div>', LIBXML_HTML_NOIMPLIED|LIBXML_HTML_NODEFDTD); $wrapper = $doc->getElementsByTagName('div')->item(0); $walker = new RecursiveIteratorIterator( new RecursiveDOMIterator($wrapper), RecursiveIteratorIterator::SELF_FIRST ); /** @var DOMNode $node */ foreach($walker as $node){ if ($node instanceof DOMElement){ /* strip non‑allowed tags */ if (!in_array(strtolower($node->tagName), $allowedTags, true)){ $node->parentNode->replaceChild($doc->createTextNode($node->textContent), $node); continue; } /* strip disallowed attributes */ for($i=$node->attributes->length-1;$i>=0;$i--){ $attr=$node->attributes->item($i); if (!in_array(strtolower($attr->name), $allowedAttr, true)){ $node->removeAttributeNode($attr); } } /* normalise <a> … */ if ($node->tagName==='a' && $node->hasAttribute('href')){ $href = $node->getAttribute('href'); if (!preg_match('#^https?://#i', $href)){ // turn relative / javascript: etc. into plain text $txt = $doc->createTextNode($node->textContent.' (link removed)'); $node->parentNode->replaceChild($txt, $node); continue; } $node->setAttribute('target','_blank'); $node->setAttribute('rel','noopener noreferrer'); } } } /* return innerHTML of wrapper */ $out=''; foreach($wrapper->childNodes as $c) $out.=$doc->saveHTML($c); return $out; } /** * Tiny Recursive DOM iterator helper * (avoids pulling in the whole DOM‑Traversal extension) */ class RecursiveDOMIterator implements RecursiveIterator { private $position=0; private $nodeList; public function __construct(DOMNode $domNode){ $this->nodeList = $domNode->childNodes; } public function rewind(){ $this->position=0; } public function valid(){ return $this->position < $this->nodeList->length; } public function key(){ return $this->position; } public function current(){ return $this->nodeList->item($this->position); } public function next(){ $this->position++; } public function hasChildren(){ return $this->current()->hasChildNodes(); } public function getChildren(){ return new self($this->current()); } } /* ------------------------------------------------------------------ */ /* 2. Card markup */ /* ------------------------------------------------------------------ */ ?> <section class="faq-shell"> <div class="faq-card"> <h2>Frequently Asked Questions</h2> <?php foreach($cats as $ci=>$cat): ?> <h3 class="faq-cat"><?= safe_html($cat['name'] ?: 'FAQ') ?></h3> <?php foreach (($cat['faqs'] ?? []) as $qi=>$qa): if (!($qa['active']??true)) continue; $id="faq{$ci}_{$qi}"; ?> <details class="faq-item" id="<?= $id ?>"> <summary> <span class="q"><?= safe_html($qa['q'] ?? 'Untitled question') ?></span> <svg class="chev" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"> <path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> </summary> <div class="a"><?= safe_html($qa['a'] ?? '') ?></div> </details> <?php endforeach; ?> <?php endforeach; ?> </div> </section> <script type="application/ld+json"><?= json_encode([ '@context'=>'https://schema.org', '@type' =>'FAQPage', 'mainEntity'=>array_merge(...array_map(function($c){ return array_values(array_filter(array_map(function($qa){ if(!($qa['active']??true)) return null; return [ '@type'=>'Question', 'name'=>strip_tags($qa['q']), 'acceptedAnswer'=>['@type'=>'Answer','text'=>strip_tags($qa['a'])] ]; },$c['faqs']??[]))); },$cats)) ],JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT) ?></script> <style> /* ===== identical visual stylesheet (colour‑contrast safe) ===== */ .faq-shell{ margin:2.6rem auto;max-width:clamp(480px,70vw,720px);width:100%; font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif } .faq-card{background:#fff;padding:1.8rem 1.6rem;border:3.5px solid #ffb63b; border-radius:27px;box-shadow:0 8px 26px rgb(0 0 0/6%)} .faq-card h2{margin:0 0 1.2rem;font-size:1.35rem;font-weight:700;color:#0F3760;text-align:center} .faq-cat{margin:1.6rem 0 .7rem;font-size:1.08rem;font-weight:700;color:#B15A00} .faq-item{border:1px solid #FFE3A1;border-radius:10px;margin:.55rem 0;background:#FFFEF9; transition:border-color .2s,box-shadow .2s} .faq-item summary{display:flex;justify-content:space-between;align-items:center; list-style:none;cursor:pointer;padding:.9rem 1.1rem;font-weight:600;outline:none} .faq-item summary::-webkit-details-marker{display:none} .faq-item summary:focus-visible{outline:3px solid #0066FF;outline-offset:2px} .faq-item .q{color:#934400;flex:1} .faq-item .chev{flex:none;margin-left:.6rem;color:#934400;transition:transform .25s} .faq-item[open]{border-color:#FFB63B;box-shadow:0 3px 12px rgb(196 155 99/28%)} .faq-item[open] .chev{transform:rotate(180deg)} .faq-item .a{padding:0 1.1rem 1rem;line-height:1.6;color:#333;border-top:1px solid #FFE3A1} @media(prefers-reduced-motion:reduce){.faq-item .chev{transition:none}} @media(prefers-color-scheme:dark){ .faq-card{background:#1E1E1E;border-color:#FFB63B;box-shadow:0 6px 20px rgb(0 0 0/50%)} .faq-card h2{color:#E6EDF3} .faq-cat{color:#FFB63B} .faq-item{background:#282828;border-color:#605336} .faq-item summary:focus-visible{outline-color:#4C8DFF} .faq-item .q{color:#FFC26B}.faq-item .chev{color:#FFC26B} .faq-item .a{color:#D0D4DA;border-top-color:#605336} } @media(max-width:500px){.faq-item summary{padding:.8rem .9rem}.faq-item .a{padding:.4rem .9rem 1rem}} </style>
Save changes
Create folder
writable 0777
Create
Cancel