1/** 2 * external-embed-guard.js 3 * 4 * Gegenstueck zu global/cms/ExternalEmbedGuard.php. 5 * 6 * Der PHP-Guard prueft beim Ausliefern der Seite, ob der Host einer externen 7 * Einbettung (z.B. ein Vergleichsrechner im iframe) ueberhaupt antwortet. Diese 8 * Datei deckt den zweiten Fall ab: der Host antwortet dem Server, aber der 9 * iframe laedt beim Besucher trotzdem nicht. Ohne Guard rendert der Browser 10 * dann seine eigene Timeout-Fehlerseite mitten im Seitenlayout. 11 * 12 * Ablauf pro Wrapper (div.external-embed[data-embed-guard]): 13 * - Timer starten (data-embed-timeout, Standard 15 Sekunden) 14 * - laedt der iframe rechtzeitig, passiert nichts 15 * - laeuft der Timer ab, wird der Host zuerst gegengeprueft; nur wenn auch das 16 * fehlschlaegt, ersetzt die Hinweis-Box den iframe 17 * 18 * Die Gegenpruefung ist wichtig, weil der Guard sonst bei langsamen Verbindungen 19 * oder bei einem bereits fertig geladenen iframe faelschlich zuschlagen wuerde 20 * (das load-Event ist dann schon vorbei, bevor diese Datei ueberhaupt laeuft). 21 * 22 * Der Original-iframe steht immer in data-embed-html, damit "Erneut versuchen" 23 * ihn neu einsetzen kann. Ein falsch-negatives Pruefergebnis kostet also nie 24 * den Inhalt. 25 * 26 * Jedes Einblenden der Hinweis-Box wird an data-embed-log gemeldet 27 * (process-embed-guard-log.php), sonst bliebe genau dieser Fall unsichtbar: 28 * der Server erreicht den fremden Host ja, nur der Besucher nicht. Gemeldet 29 * wird einmal je Wrapper und Seitenaufruf; die Drosselung der Log-Eintraege 30 * passiert serverseitig. 31 */ 32(function ($) 33{ 34 'use strict'; 35 36 if (typeof $ === 'undefined') 37 { 38 return; 39 } 40 41 var DEFAULT_TIMEOUT = 15000; 42 var PROBE_TIMEOUT = 5000; 43 44 /** 45 * Prueft, ob der fremde Host erreichbar ist. 46 * 47 * Hier ist bewusst fetch statt $.ajax im Einsatz: nur fetch kann mit 48 * mode 'no-cors' eine fremde Domain anfragen, ohne dass die CORS-Regeln 49 * greifen. $.ajax wuerde bei jeder fremden Domain einen Fehler melden - 50 * ununterscheidbar davon, dass der Server wirklich tot ist. Die opaque 51 * Response ist nicht lesbar, aber genau das reicht: es geht nur um die 52 * Frage, ob ueberhaupt eine Verbindung zustande kommt. 53 * 54 * @param {String} url zu pruefende Adresse 55 * @param {Function} callback erhaelt true, wenn der Host antwortet 56 */ 57 function probe(url, callback) 58 { 59 if (!url || typeof window.fetch !== 'function') 60 { 61 // Ohne Pruefmoeglichkeit lieber nichts tun als faelschlich ausblenden 62 callback(true); 63 return; 64 } 65 66 var settled = false; 67 var finish = function (reachable) 68 { 69 if (!settled) 70 { 71 settled = true; 72 callback(reachable); 73 } 74 }; 75 76 var controller = (typeof window.AbortController === 'function') ? new window.AbortController() : null; 77 var options = { mode: 'no-cors', cache: 'no-store', credentials: 'omit' }; 78 if (controller) 79 { 80 options.signal = controller.signal; 81 } 82 83 window.setTimeout(function () 84 { 85 if (controller) 86 { 87 try 88 { 89 controller.abort(); 90 } 91 catch (error) 92 { 93 // Abbruch ist optional - das Timeout entscheidet ohnehin 94 } 95 } 96 finish(false); 97 }, PROBE_TIMEOUT); 98 99 window.fetch(url, options).then(function () 100 { 101 finish(true); 102 }).catch(function () 103 { 104 finish(false); 105 }); 106 } 107 108 /** 109 * Meldet dem Server, dass dem Besucher der Hinweis gezeigt werden musste. 110 * 111 * sendBeacon mit text/plain, damit der Cross-Domain-Aufruf ohne 112 * CORS-Preflight auskommt und den Seitenwechsel ueberlebt. Faellt auf 113 * fetch/no-cors zurueck, wo sendBeacon fehlt. Die Antwort interessiert 114 * nicht - schlaegt die Meldung fehl, sieht der Besucher trotzdem den 115 * Hinweis. 116 * 117 * @param {jQuery} $wrapper 118 * @param {String} reason Kurzkennung des Ausloesers 119 */ 120 function report($wrapper, reason) 121 { 122 var url = $wrapper.attr('data-embed-log'); 123 if (!url || $wrapper.attr('data-embed-reported') === '1') 124 { 125 return; 126 } 127 $wrapper.attr('data-embed-reported', '1'); 128 129 var payload = JSON.stringify({
130 src: $wrapper.attr('data-embed-src') || '', 131 page: window.location.href, 132 reason: reason || '' 133 }); 134 135 try 136 { 137 if (navigator && typeof navigator.sendBeacon === 'function') 138 { 139 navigator.sendBeacon(url, new Blob([payload], { type: 'text/plain;charset=UTF-8' })); 140 return; 141 } 142 143 if (typeof window.fetch === 'function') 144 { 145 window.fetch(url, { 146 method: 'POST', 147 mode: 'no-cors', 148 cache: 'no-store', 149 credentials: 'omit', 150 keepalive: true, 151 headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, 152 body: payload 153 }).catch(function () 154 { 155 // Meldung ist Nebensache, der Besucher merkt davon nichts 156 }); 157 } 158 } 159 catch (error) 160 { 161 // s.o. - ein fehlgeschlagener Log-Versuch darf nichts weiter tun 162 } 163 } 164 165 /** 166 * Setzt die Schaltflaeche waehrend eines Ladeversuchs auf "beschaeftigt". 167 * 168 * @param {jQuery} $wrapper 169 * @param {Boolean} busy 170 */ 171 function setRetryBusy($wrapper, busy) 172 { 173 var $button = $wrapper.find('[data-embed-retry]'); 174 if (!$button.length) 175 { 176 return; 177 } 178 179 if (busy) 180 { 181 $button.prop('disabled', true).text('Wird geladen â¦'); 182 } 183 else 184 { 185 $button.prop('disabled', false).text('Erneut versuchen'); 186 } 187 } 188 189 /** 190 * Blendet die Hinweis-Box ein und entfernt den haengenden iframe. 191 * 192 * @param {jQuery} $wrapper 193 * @param {String} reason Kurzkennung des Ausloesers fuers Log 194 */ 195 function showNotice($wrapper, reason) 196 { 197 // src leeren, damit der Browser den haengenden Request aufgibt 198 $wrapper.find('iframe').attr('src', 'about:blank').remove(); 199 $wrapper.find('[data-embed-notice]').removeAttr('hidden').css('display', 'block'); 200 setRetryBusy($wrapper, false); 201 report($wrapper, reason); 202 } 203 204 /** 205 * Versteckt die Hinweis-Box. 206 * 207 * @param {jQuery} $wrapper 208 */ 209 function hideNotice($wrapper) 210 { 211 $wrapper.find('[data-embed-notice]').attr('hidden', 'hidden').css('display', 'none'); 212 } 213 214 /** 215 * Ueberwacht einen iframe auf Ladeerfolg. 216 * 217 * @param {jQuery} $wrapper 218 * @param {jQuery} $iframe 219 */ 220 function watch($wrapper, $iframe) 221 { 222 var timeout = parseInt($wrapper.attr('data-embed-timeout'), 10); 223 if (!timeout || timeout < 1000) 224 { 225 timeout = DEFAULT_TIMEOUT; 226 } 227 228 var settled = false; 229 230 var timer = window.setTimeout(function () 231 { 232 if (settled) 233 { 234 return; 235 } 236 237 probe($wrapper.attr('data-embed-src'), function (reachable) 238 { 239 if (settled || reachable) 240 { 241 // Erreichbar heisst: der iframe braucht nur laenger 242 setRetryBusy($wrapper, false); 243 return; 244 } 245 settled = true; 246 showNotice($wrapper, 'Timeout nach ' + timeout + ' ms, Host antwortet dem Browser nicht'); 247 }); 248 }, timeout); 249 250 $iframe.on('load', function () 251 { 252 settled = true; 253 window.clearTimeout(timer); 254 setRetryBusy($wrapper, false); 255 }); 256 } 257 258 /** 259 * Setzt den Original-iframe neu ein ("Erneut versuchen"). 260 * 261 * @param {jQuery} $wrapper 262 */ 263 function reload($wrapper) 264 { 265 var html = $wrapper.attr('data-embed-html'); 266 if (!html) 267 { 268 return; 269 } 270 271 setRetryBusy($wrapper, true); 272 $wrapper.find('iframe').remove(); 273 hideNotice($wrapper); 274 $wrapper.prepend(html); 275 276 var $iframe = $wrapper.find('iframe'); 277 if ($iframe.length) 278 { 279 watch($wrapper, $iframe); 280 } 281 else 282 { 283 showNotice($wrapper, 'iframe-Markup fehlt'); 284 } 285 } 286 287 /** 288 * Bereitet einen einzelnen Wrapper vor. 289 * 290 * @param {jQuery} $wrapper 291 */ 292 function initWrapper($wrapper) 293 { 294 if ($wrapper.attr('data-embed-ready') === '1') 295 { 296 return; 297 } 298 $wrapper.attr('data-embed-ready', '1'); 299 300 $wrapper.find('[data-embed-retry]').on('click', function () 301 { 302 reload($wrapper); 303 }); 304 305 var $iframe = $wrapper.find('iframe'); 306 if ($iframe.length) 307 { 308 watch($wrapper, $iframe); 309 } 310 } 311 312 $(function () 313 { 314 $('[data-embed-guard]').each(function () 315 { 316 initWrapper($(this)); 317 }); 318 }); 319 320})(window.jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.