<?php /** * Plugin Name: QR7 Helper * Description: Redirects old .html/.shtml URLs to WordPress posts * Version: 3.3 */ // One-time opcode cache flush (remove after confirmed) if (get_option('sr_opcache_v3') !== 'done') { if (function_exists('opcache_reset')) { @opcache_reset(); } if (function_exists('apc_clear_cache')) { @apc_clear_cache(); } update_option('sr_opcache_v3', 'done'); } add_action('template_redirect', 'qr7_redirect_old_urls', 1); function qr7_redirect_old_urls() { if (!is_404()) return; $uri = $_SERVER['REQUEST_URI']; $uri = strtok($uri, '?'); $uri = urldecode($uri); $uri = preg_replace('#^/+#', '/', $uri); $is_old = preg_match('/\.(s?html?|pdf|png|jpg|jpeg|gif)/i', $uri) || $uri === '/index.shtml' || $uri === '/Aboutus.html' || preg_match('#^/[A-Z][a-zA-Z]+[0-9]+[a-zA-Z]?$#', $uri); if (!$is_old) return; $filename = basename($uri); $fl = strtolower($filename); $special = array('index.shtml'=>'/','index.html'=>'/','aboutus.html'=>'/about-2/','incaseofanynewsform.html'=>'/','readercomments152.html'=>'/'); if (isset($special[$fl])) { wp_redirect(home_url($special[$fl]), 301); exit; } if (preg_match('/\.(pdf|png|jpg|jpeg|gif)$/i', $filename)) { wp_redirect(home_url('/'), 301); exit; } $base = preg_replace('/\.s?html.*/i', '', $filename); if ($base === $filename) { $base = $filename; } $slug = strtolower(preg_replace('/([a-zA-Z])([0-9])/', '$1-$2', $base)); $slug = preg_replace('/[^a-z0-9-]/', '', $slug); $slug = preg_replace('/-+/', '-', trim($slug, '-')); global $wpdb; $post_id = $wpdb->get_var($wpdb->prepare("SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_status = 'publish' AND post_type = 'post' LIMIT 1", $slug)); if ($post_id) { wp_redirect(get_permalink($post_id), 301); exit; } $bn = preg_replace('/[^a-z0-9]/', '', strtolower($base)); $like = '%' . $wpdb->esc_like($bn) . '%'; $post_id = $wpdb->get_var($wpdb->prepare("SELECT ID FROM {$wpdb->posts} WHERE REPLACE(REPLACE(post_name,'-',''),'_','') LIKE %s AND post_status='publish' AND post_type='post' ORDER BY ID ASC LIMIT 1", $like)); if ($post_id) { wp_redirect(get_permalink($post_id), 301); exit; } $alt = preg_replace('/-0+/', '-', preg_replace('/^0+/', '', $slug)); if ($alt !== $slug) { $post_id = $wpdb->get_var($wpdb->prepare("SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_status='publish' AND post_type='post' LIMIT 1", $alt)); if ($post_id) { wp_redirect(get_permalink($post_id), 301); exit; } } wp_redirect(home_url('/'), 301); exit; } // === OPSEC HARDENING === // Block REST API user enumeration (keep - prevents /wp-json/wp/v2/users from exposing usernames) add_filter('rest_endpoints', function($endpoints) { if (isset($endpoints['/wp/v2/users'])) { unset($endpoints['/wp/v2/users']); } if (isset($endpoints['/wp/v2/users/(?P<id>[\d]+)'])) { unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']); } return $endpoints; }); // Author archives are now enabled (authors have bios + assigned articles) // Removed author blocking - was preventing author pages from loading // Remove WP version meta tag remove_action('wp_head', 'wp_generator'); // Remove REST API link from head remove_action('wp_head', 'rest_output_link_wp_head', 10); remove_action('wp_head', 'wp_oembed_add_discovery_links', 10); // Remove REST API link from HTTP headers remove_action('template_redirect', 'rest_output_link_header', 11, 0); // === SITEMAP CACHE FIX === // Override 30-day cache on sitemaps via Rank Math filter add_filter('rank_math/sitemap/http_headers', function($headers) { $headers['Cache-Control'] = 'no-cache, must-revalidate, max-age=0'; $headers['Pragma'] = 'no-cache'; unset($headers['Expires']); // Also try PHP-level override after Apache header_remove('Expires'); header('Cache-Control: no-cache, must-revalidate, max-age=0', true); header('Pragma: no-cache', true); return $headers; }); // Also use wp_headers filter for WP core sitemaps add_filter('wp_headers', function($headers) { if (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], 'sitemap') !== false) { $headers['Cache-Control'] = 'no-cache, must-revalidate, max-age=0'; $headers['Pragma'] = 'no-cache'; header_remove('Expires'); return $headers; } return $headers; }); // === GOOGLE NEWS SITEMAP === // Direct output on init - bypasses WP rewrite system entirely add_action('init', function() { $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); if ($uri !== '/news-sitemap.xml') return; header('Content-Type: application/xml; charset=UTF-8'); header('Cache-Control: no-cache, must-revalidate, max-age=0'); header('X-Robots-Tag: noindex'); $posts = get_posts(array( 'numberposts' => 1000, 'post_type' => 'post', 'post_status' => 'publish', 'date_query' => array(array('after' => '48 hours ago')), 'orderby' => 'date', 'order' => 'DESC', )); echo '<?xml version="1.0" encoding="UTF-8"?>' . " "; echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"' . " "; echo ' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">' . " "; foreach ($posts as $post) { $url = get_permalink($post); $date = get_the_date('Y-m-d\TH:i:sP', $post); $title = htmlspecialchars($post->post_title, ENT_XML1, 'UTF-8'); echo " <url> "; echo " <loc>{$url}</loc> "; echo " <news:news> "; echo " <news:publication> "; echo " <news:name>Scottish Review</news:name> "; echo " <news:language>en</news:language> "; echo " </news:publication> "; echo " <news:publication_date>{$date}</news:publication_date> "; echo " <news:title>{$title}</news:title> "; echo " </news:news> "; echo " </url> "; } echo '</urlset>'; die(); }, 0); // === ENHANCED SCHEMA === // Add NewsMediaOrganization + publishingPrinciples to homepage add_action('wp_head', function() { if (!is_front_page()) return; $schema = array( '@context' => 'https://schema.org', '@type' => 'NewsMediaOrganization', '@id' => home_url('/#newsorg'), 'name' => 'Scottish Review', 'url' => home_url('/'), 'logo' => array( '@type' => 'ImageObject', 'url' => home_url('/wp-content/uploads/2026/02/sr-favicon-512.png'), ), 'foundingDate' => '1995-01-01', 'description' => "Scotland's independent commentary and analysis since 1995. News, politics, culture, sport, and entertainment.", 'publishingPrinciples' => home_url('/editorial-standards/'), 'ethicsPolicy' => home_url('/editorial-standards/'), 'correctionsPolicy' => home_url('/editorial-standards/#corrections'), 'actionableFeedbackPolicy' => home_url('/contact/'), 'masthead' => home_url('/about/'), 'ownershipFundingInfo' => home_url('/about/'), 'sameAs' => array(), ); echo '<script type="application/ld+json">' . json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . '</script>' . "\n"; }, 1); // Add missing OG tags add_action('wp_head', function() { if (!is_front_page()) return; echo '<meta property="og:site_name" content="Scottish Review" />' . "\n"; echo '<meta name="twitter:title" content="Scottish Review" />' . "\n"; echo '<meta name="twitter:description" content="Scotland&#039;s independent commentary and analysis since 1995." />' . "\n"; }, 2); // === CACHE BUST THEME CSS === add_filter('style_loader_src', function($src) { if (strpos($src, 'scottishreview-theme/style.css') !== false) { return add_query_arg('v', '20260223b', $src); } return $src; }); // === ROBOTS.TXT: ADD NEWS SITEMAP === add_filter('robots_txt', function($output) { $output .= "Sitemap: https://scottishreview.net/news-sitemap.xml "; return $output; }, 100); // === TEXT-TO-SPEECH: REGISTER META === add_action('init', function() { register_meta('post', 'sr_audio_url', array('show_in_rest' => true, 'single' => true, 'type' => 'string')); register_meta('post', 'sr_audio_media_id', array('show_in_rest' => true, 'single' => true, 'type' => 'string')); }); // === TEXT-TO-SPEECH: LISTEN BUTTON === // 1. Inline CSS via wp_head add_action('wp_head', function() { if (!is_singular('post')) return; ?> <style> .sr-tts-bar{display:flex;align-items:center;gap:10px;padding:12px 16px;margin:0 0 24px;background:#f8f9fa;border:1px solid #e2e8f0;border-radius:8px;cursor:pointer;transition:background .2s,border-color .2s;font-family:Verdana,Arial,sans-serif;user-select:none;-webkit-user-select:none} .sr-tts-bar:hover{background:#f0f4f8;border-color:#cbd5e1} .sr-tts-bar svg{flex-shrink:0;color:#1a5276} .sr-tts-label{font-size:13px;color:#555;white-space:nowrap} .sr-tts-progress{display:none;flex:1;height:3px;background:#e2e8f0;border-radius:2px;overflow:hidden;min-width:40px} .sr-tts-progress-fill{height:100%;background:#1a5276;border-radius:2px;width:0;transition:width .3s} .sr-tts-time{font-size:11px;color:#94a3b8;font-variant-numeric:tabular-nums;white-space:nowrap} .sr-tts-speeds{display:flex;gap:4px} .sr-tts-speed{font-size:11px;padding:3px 8px;border:1px solid #cbd5e1;border-radius:12px;background:none;cursor:pointer;color:#666;font-family:Verdana,Arial,sans-serif;transition:all .15s} .sr-tts-speed:hover{border-color:#1a5276;color:#1a5276} .sr-tts-speed.active{background:#1a5276;color:#fff;border-color:#1a5276} .sr-tts-stop{display:none;width:28px;height:28px;border:none;background:none;cursor:pointer;color:#94a3b8;padding:4px;border-radius:50%;transition:background .2s,color .2s;flex-shrink:0} .sr-tts-stop:hover{background:rgba(0,0,0,.08);color:#333} .sr-tts-bar.playing{background:#1a5276;border-color:#1a5276} .sr-tts-bar.playing svg{color:#fff} .sr-tts-bar.playing .sr-tts-label{color:#fff} .sr-tts-bar.playing .sr-tts-time{color:rgba(255,255,255,.7)} .sr-tts-bar.playing .sr-tts-progress{display:block;background:rgba(255,255,255,.2)} .sr-tts-bar.playing .sr-tts-progress-fill{background:#fff} .sr-tts-bar.playing .sr-tts-stop{display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.6)} .sr-tts-bar.playing .sr-tts-stop:hover{background:rgba(255,255,255,.15);color:#fff} .sr-tts-bar.playing .sr-tts-speed{border-color:rgba(255,255,255,.3);color:rgba(255,255,255,.7)} .sr-tts-bar.playing .sr-tts-speed:hover{border-color:#fff;color:#fff} .sr-tts-bar.playing .sr-tts-speed.active{background:rgba(255,255,255,.2);color:#fff;border-color:rgba(255,255,255,.5)} .sr-tts-bar.paused{background:#f0f4f8;border-color:#1a5276} .sr-tts-bar.paused .sr-tts-label{color:#1a5276} .sr-tts-bar.paused .sr-tts-stop{display:flex;align-items:center;justify-content:center} .sr-tts-bar.paused .sr-tts-progress{display:block} @media(max-width:480px){.sr-tts-bar{padding:10px 12px;gap:8px}.sr-tts-label{font-size:12px}.sr-tts-speed{padding:3px 6px;font-size:10px}} </style> <?php }, 99); // 2. Prepend button HTML to article content add_filter('the_content', function($content) { if (!is_singular('post') || is_admin()) return $content; $audio_url = get_post_meta(get_the_ID(), 'sr_audio_url', true); $data_attr = $audio_url ? ' data-audio="' . esc_attr($audio_url) . '"' : ''; $btn = '<div class="sr-tts-bar" id="sr-tts-bar" role="button" tabindex="0" aria-label="Listen to this article"' . $data_attr . '>' . '<svg class="sr-tts-icon-play" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5,3 19,12 5,21"></polygon></svg>' . '<svg class="sr-tts-icon-pause" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>' . '<span class="sr-tts-label">Listen to this article</span>' . '<div class="sr-tts-progress"><div class="sr-tts-progress-fill"></div></div>' . '<span class="sr-tts-time"></span>' . '<div class="sr-tts-speeds">' . '<button class="sr-tts-speed" data-rate="0.8" type="button">0.8x</button>' . '<button class="sr-tts-speed active" data-rate="1" type="button">1x</button>' . '<button class="sr-tts-speed" data-rate="1.2" type="button">1.2x</button>' . '</div>' . '<button class="sr-tts-stop" aria-label="Stop" type="button">' . '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>' . '</button>' . '</div>'; return $btn . $content; }, 20); // 3. Inline JavaScript via wp_footer add_action('wp_footer', function() { if (!is_singular('post')) return; ?> <script> (function(){ var bar = document.getElementById('sr-tts-bar'); if (!bar) return; var audioUrl = bar.getAttribute('data-audio'); var useMP3 = !!audioUrl; var hasSynth = ('speechSynthesis' in window); // Hide button if no audio source available if (!useMP3 && !hasSynth) { bar.style.display = 'none'; return; } var iconPlay = bar.querySelector('.sr-tts-icon-play'); var iconPause = bar.querySelector('.sr-tts-icon-pause'); var label = bar.querySelector('.sr-tts-label'); var progressFill = bar.querySelector('.sr-tts-progress-fill'); var timeEl = bar.querySelector('.sr-tts-time'); var stopBtn = bar.querySelector('.sr-tts-stop'); var speedBtns = bar.querySelectorAll('.sr-tts-speed'); var state = 'idle'; var rate = 1.0; var audio = null; // === MP3 MODE (pre-generated, natural voice) === if (useMP3) { audio = new Audio(audioUrl); audio.preload = 'metadata'; function fmtTime(s) { var m = Math.floor(s / 60), sec = Math.floor(s % 60); return m + ':' + (sec < 10 ? '0' : '') + sec; } audio.addEventListener('loadedmetadata', function() { timeEl.textContent = fmtTime(audio.duration); }); audio.addEventListener('timeupdate', function() { if (audio.duration) { var pct = (audio.currentTime / audio.duration) * 100; progressFill.style.width = pct + '%'; timeEl.textContent = fmtTime(audio.currentTime) + ' / ' + fmtTime(audio.duration); } }); audio.addEventListener('ended', function() { state = 'idle'; updateUI(); }); function doPlay() { audio.playbackRate = rate; audio.play(); state = 'playing'; updateUI(); } function doPause() { audio.pause(); state = 'paused'; updateUI(); } function doResume() { audio.playbackRate = rate; audio.play(); state = 'playing'; updateUI(); } function doStop() { audio.pause(); audio.currentTime = 0; state = 'idle'; updateUI(); } // Speed change: apply immediately function applySpeed(newRate) { rate = newRate; audio.playbackRate = rate; if (state === 'idle' && audio.duration) { timeEl.textContent = fmtTime(audio.duration / rate); } } // === SPEECH SYNTHESIS FALLBACK === } else { var synth = window.speechSynthesis; var chunks = [], currentChunk = 0, totalChars = 0, spokenChars = 0; var voice = null, keepAlive = null; function getText() { var el = document.querySelector('.single-post-content'); if (!el) return ''; var clone = el.cloneNode(true); var rm = clone.querySelectorAll('.sr-tts-bar, .sr-toc, script, style, table, iframe'); for (var i = 0; i < rm.length; i++) rm[i].parentNode.removeChild(rm[i]); return (clone.textContent || clone.innerText || '').replace(/\s+/g, ' ').trim(); } function chunkText(text) { var max = 3000, parts = []; while (text.length > 0) { if (text.length <= max) { parts.push(text); break; } var slice = text.substring(0, max); var bp = Math.max(slice.lastIndexOf('. '), slice.lastIndexOf('? '), slice.lastIndexOf('! ')); if (bp < max * 0.3) bp = slice.lastIndexOf(' '); if (bp <= 0) bp = max; parts.push(text.substring(0, bp + 1).trim()); text = text.substring(bp + 1).trim(); } return parts; } function pickVoice() { var voices = synth.getVoices(), uk = null, en = null; for (var i = 0; i < voices.length; i++) { if (voices[i].lang === 'en-GB') { if (!uk || voices[i].name.indexOf('Google') !== -1) uk = voices[i]; } if (!en && voices[i].lang.indexOf('en') === 0) en = voices[i]; } return uk || en || voices[0] || null; } function startKeepAlive() { stopKeepAlive(); keepAlive = setInterval(function() { if (synth.speaking && !synth.paused) { synth.pause(); synth.resume(); } }, 12000); } function stopKeepAlive() { if (keepAlive) { clearInterval(keepAlive); keepAlive = null; } } function speakChunk(idx) { if (idx >= chunks.length) { doStop(); return; } currentChunk = idx; var utt = new SpeechSynthesisUtterance(chunks[idx]); if (voice) utt.voice = voice; utt.rate = rate; utt.lang = 'en-GB'; utt.onend = function() { spokenChars += chunks[idx].length; progressFill.style.width = Math.round(spokenChars/totalChars*100)+'%'; speakChunk(idx+1); }; utt.onerror = function(e) { if (e.error !== 'canceled' && e.error !== 'interrupted') { spokenChars += chunks[idx].length; speakChunk(idx+1); } }; synth.speak(utt); startKeepAlive(); } function doPlay() { synth.cancel(); var text = getText(); if (!text) return; chunks = chunkText(text); totalChars = text.length; spokenChars = 0; state = 'playing'; updateUI(); speakChunk(0); } function doPause() { synth.pause(); state = 'paused'; stopKeepAlive(); updateUI(); } function doResume() { synth.resume(); state = 'playing'; startKeepAlive(); updateUI(); } function doStop() { synth.cancel(); stopKeepAlive(); state = 'idle'; spokenChars = 0; updateUI(); } function applySpeed(newRate) { rate = newRate; if (state === 'playing') { synth.cancel(); speakChunk(currentChunk); } } // Init voices function initVoices() { voice = pickVoice(); var t = getText(); totalChars = t.length; timeEl.textContent = Math.ceil(totalChars/5/150) + ' min'; } if (synth.getVoices().length > 0) initVoices(); else { synth.addEventListener('voiceschanged', initVoices); setTimeout(function() { if (!voice) initVoices(); }, 1000); } window.addEventListener('beforeunload', function() { synth.cancel(); }); } // === SHARED UI === function updateUI() { bar.className = 'sr-tts-bar' + (state !== 'idle' ? ' ' + state : ''); iconPlay.style.display = state === 'playing' ? 'none' : ''; iconPause.style.display = state === 'playing' ? '' : 'none'; if (state === 'idle') { label.textContent = 'Listen to this article'; progressFill.style.width = '0%'; } else if (state === 'playing') { label.textContent = 'Listening...'; } else if (state === 'paused') { label.textContent = 'Paused'; } } bar.addEventListener('click', function(e) { if (e.target.closest('.sr-tts-stop') || e.target.closest('.sr-tts-speed')) return; if (state === 'idle') doPlay(); else if (state === 'playing') doPause(); else if (state === 'paused') doResume(); }); stopBtn.addEventListener('click', function(e) { e.stopPropagation(); doStop(); }); for (var i = 0; i < speedBtns.length; i++) { speedBtns[i].addEventListener('click', function(e) { e.stopPropagation(); var nr = parseFloat(this.getAttribute('data-rate')); for (var j = 0; j < speedBtns.length; j++) speedBtns[j].classList.remove('active'); this.classList.add('active'); applySpeed(nr); }); } })(); </script> <?php }, 100); // === RANK MATH SITEMAP FIX (added by audit) === add_action('init', function() { if (isset($_GET['rm_fix_sitemap']) && current_user_can('manage_options')) { $modules = get_option('rank_math_modules', array()); echo '
Current modules: ' . print_r($modules, true) . '
'; if (!in_array('sitemap', $modules)) { $modules[] = 'sitemap'; update_option('rank_math_modules', $modules); echo '

Sitemap module ENABLED

'; flush_rewrite_rules(); echo '

Rewrite rules flushed

'; } else { echo '

Sitemap module already active

'; // Try flushing rewrite rules anyway flush_rewrite_rules(); echo '

Rewrite rules flushed

'; } $modules_after = get_option('rank_math_modules', array()); echo '
Modules after: ' . print_r($modules_after, true) . '
'; die(); } }, 1); // === END SITEMAP FIX === Page Not Found | Scottish Review

Page not found

Sorry, the page you're looking for doesn't exist. It may have been moved or removed.

Back to homepage