1/*************************************************************************** 2// Firma: Hehner Reus Systems GmbH 3// Projekt: com.cerno.basis 4// Datei: httpClient.js 5// Erstellung: 19.02.2025 6// Verwendung: Hilfsfunktionen für asynchrone HTTP-Requests. 7/***************************************************************************/ 8 9/** 10 * Toolklasse mit statischen Funktionen für das Abschicken von asynchronen HTTP-Requests. 11 */ 12class HRHttpClient 13{ 14 /** 15 * Führt einen HTTP-GET-Request per Ajax durch. Es wird die Fetch-API verwendet. 16 * 17 * Für weitere Beschreibungen der Parameter und Rückgaben @see #sendRequest. 18 */ 19 static async doGet(url, config = {}) 20 { 21 config.method = 'GET'; 22 23 return this.#sendRequest(url, config); 24 } 25 26 /** 27 * Führt einen HTTP-GET-Request per Ajax durch und gibt Plain-Text zurück. 28 * 29 * Achtung: Fetch interpretiert Textdaten beim Abruf mit der .text()-Methode standardmäßig in UTF-8. 30 * Daher müssen wir den Umweg über den Array-Buffer gehen. 31 * 32 * @return Der Response-Body als Plain-Text oder null, falls kein Text ermittelt werden konnte. 33 */ 34 static async doGetPlainText(url, config = {}) 35 { 36 const response = await this.doGet(url, config); 37 if (!response) 38 return null; 39 40 // Body als Array-Buffer abrufen, damit wir die Bytes 41 // von Hand in Iso-8859-1 decodieren können. 42 const buffer = await response.arrayBuffer(); 43 const decoder = new TextDecoder('iso-8859-1'); 44 return decoder.decode(buffer); 45 } 46 47 /** 48 * Führt einen HTTP-GET-Request per Ajax durch und gibt JSON zurück, sofern der Response auch ein JSON-Response ist. 49 * 50 * Achtung: Fetch interpretiert Textdaten beim Abruf mit der .text()-Methode standardmäßig in UTF-8. 51 * Da die json-Methode diese unter der Haube nutzt, müssen wir den Umweg über den Array-Buffer gehen. 52 * 53 * @return Das JSON-Objekt im Response-Body oder null, falls kein JSON-Objekt ermittelt werden konnte. 54 */ 55 static async doGetJson(url, config = {}) 56 { 57 const text = await this.doGetPlainText(url); 58 59 try 60 { 61 return JSON.parse(text); 62 } 63 catch (e) 64 { 65 HRLogError(e); 66 return null; 67 } 68 69 return response ? await response.json() : null; 70 } 71 72 /** 73 * Führt einen HTTP-POST-Request per Ajax durch. Es wird die Fetch-API verwendet. 74 * 75 * Für weitere Beschreibungen der Parameter und Rückgaben @see #sendRequest. 76 */ 77 static async doPost(url, config = {}) 78 { 79 config.method = 'POST'; 80 81 return this.#sendRequest(url, config); 82 } 83 84 /** 85 * Führt einen HTTP-GET-Request per Ajax durch und setzt den HTML-Response an der übergebenen Stelle in das HTML-Dokument. 86 * 87 * Für weitere Beschreibungen der Parameter und Rückgaben @see #sendRequest. 88 * @param domDest Element oder CSS-Selektor des Elements, dessen Inhalt mit dem Ergebnis des Ajax-Aufrufs überschrieben werden soll. Default: null.</li> 89 * Spezielle Konfigurationseinstellung für die HTML-Substitution: 90 * <li>execJS: Führt in der Antwort enthaltenes JavaScript aus. Default: true.</li> 91 */ 92 static async replaceHtml(url, domDest, config = {}) 93 { 94 // Der Parameter überschreibt den Konfigurationsschalter hier. 95 if (domDest) 96 config.domDest = domDest; 97 98 try 99 { 100 return this.#sendRequest(url, config); 101 } 102 catch (e) 103 { 104 HRLogError(e); 105 return null; 106 } 107 } 108 109 /** 110 * Versendet ein Formular per Ajax. 111 * 112 * @param form Das zu versendende form-Objekt. 113 * @param config [optional] Weitere Konfigurationen des Requests. 114 * @returns {Promise<Response|null>} 115 */ 116 static async sendForm(form, config = {}) 117 { 118 // Es muss zumindest ein Formular zum Senden übergeben werden. 119 if (!form) 120 { 121 HRLogError(new Error('Es wurde kein zu sendendes Formular übergeben.')); 122 return null; 123 } 124 125 // Das Formular kann auch als CSS-Selektor oder Formular-Index übergeben werden. 126 let formObj = form; 127 if (typeof(form) === 'string' || typeof(form) === 'number') 128 { 129 formObj = document.forms[form]; 130 if (form == null && typeof(form) !== 'number') 131 formObj = document.querySelector(form); 132 } 133 134 // Das Formular... 135 if (!formObj) 136 { 137 HRLogError(new Error(`Das zu versendende Formular zum ${typeof(form) === 'number' ? 'Selektor' : Index} '${form}' konnte nicht gefunden werden.`)); 138 return null; 139 } 140 141 let url = formObj.action;
142 try 143 { 144 config.body = new FormData(form); 145 } 146 catch (e) 147 { 148 // Die Parameter werden stattdessen als URL-Parameter ergänzt. 149 if (url) 150 { 151 url += (url.indexOf('?') >= 0 ? '&' : '?') 152 url += HRQueryStringFromFormdata(form) 153 } 154 } 155 156 return this.#sendRequest(url, config); 157 } 158 159 /** 160 * Versendet ein FormData-Objekt per Ajax. 161 * 162 * @param {FormData} formData Das zu sendende FormData-Objekt. 163 * @param {string} url Die Ziel-URL für den Request. 164 * @param {object} [config] Zusätzliche Konfiguration für den Request. 165 * @returns {Promise<Response|null>} 166 */ 167 static async sendFormData(formData, url, config = {}) 168 { 169 // Prüfung, ob formData wirklich ein FormData-Objekt ist 170 if (!(formData instanceof FormData)) 171 { 172 HRLogError(new Error('Das übergebene Objekt ist kein gültiges FormData-Objekt.')); 173 return null; 174 } 175 176 // Eine URL ist zwingend erforderlich 177 if (!url) 178 { 179 HRLogError(new Error('Es wurde keine URL zum Senden des FormData angegeben.')); 180 return null; 181 } 182 183 // Body setzen, falls noch nicht vorhanden 184 config.body = formData; 185 186 // Standard-Method setzen, falls nicht angegeben 187 if (!config.method) 188 config.method = 'POST'; 189 190 try 191 { 192 return this.#sendRequest(url, config); 193 } 194 catch (e) 195 { 196 HRLogError(e); 197 return null; 198 } 199 } 200 201 /** 202 * Führt einen Ajax-Request aus. 203 * 204 * @param url Die URL, die aufgerufen werden soll. 205 * @param config [optional] Kapselt optionale Konfigurationsparameter. 206 * <ul> 207 * <li>domDest: Element oder CSS-Selektor des Elements, dessen Inhalt mit dem Ergebnis des Ajax-Aufrufs überschrieben werden soll. Default: null.</li> 208 * <li>error: Diese Funktion wird am Ende der Verabeitung des Response aufgerufen, wenn es einen Statuscode größer 300 gab. Der genaue Statuscode wird als Parameter übergeben. Default: null.</li> 209 * <li>method: Die aufzurufende HTTP-Methode ('POST', 'GET'). Default: 'POST'.</li> 210 * <li>keepAlive: Falls true, wird der Request nicht vom Browser abgeräumt, auch wenn die Webpage, die den Request initiiert 211 * hat, selbst schon abgeräumt wurde. Default: false.</li> 212 * <li>timeout [nur ohne keepAlive]: Zeit in Sekunden, nach der der Aufruf aufgegeben und abgebrochen werden soll. Default: 0 (kein Abbruch).</li> 213 * <li>body [nur bei POST-Requests]: Daten, die dem POST-Request als Body mitgeschickt werden sollen. Default: null.</li> 214 * <li>event: Konfigurationsparameter für das Ajax-Event: 215 * <ul> 216 * <li>name: Der Name des Ajax-Events. Default: HREventNames.ajaxload.</li> 217 * <li>bubbles: Default: true.</li> 218 * <li>useStandard: Default: false.</li> 219 * <li>notifySelf: Default: true.</li> 220 * <li>notifyChildren: Default: true.</li> 221 * <li>notifyParents: Default: true.</li> 222 * <li>notifyAll: Default: true.</li> 223 * </ul> 224 * </li> 225 * </ul> 226 * @return Das Response-Objekt (der Fetch API) des Aufrufs. Der Nutzer kann über die Funktionen text() und json() 227 * dadurch dann selbst entscheiden, in welchem Format er den Response verarbeiten will. Falls keine URL 228 * übergeben wurde, wird null zurückgegeben. 229 * 230 * Beispiel-Aufruf: 231 * const response = await HRAjax.sendRequest('https://jsonplaceholder.typicode.com/posts'); 232 * const responseBody = await response.json(); 233 * Anmerkung: Der Aufruf muss aufgrund der Verwendung des await-Schlüsselwortes natürlich selbst wieder in einer async-Funktion stattfinden. 234 */ 235 static async #sendRequest(url, config = {}) 236 { 237 try 238 { 239 if (!url) 240 return null; 241 242 // Konfigurationsobjekt mit Default-Werten mischen. 243 config = this.#mergeSendConfig(config); 244 245 const body = config.body; 246 247 // Ajax nutzt den UTF-8-Zeichensatz. 248 if (body) 249 { 250 if (body instanceof FormData && typeof(body.get) === 'function' && !body.get('__ENCODING__')) 251 body.set('__ENCODING__', 'UTF-8'); 252 } 253 else if (url.indexOf('__ENCODING__') < 0) 254 { 255 // GET-Aufruf: Encoding-Parameter also an URL hängen. 256 const urlObj = new URL(url, window.location.origin); 257 urlObj.searchParams.set('__ENCODING__', 'UTF-8'); 258 url = urlObj.toString(); 259 } 260 261 // Soll der Aufruf invalidiert werden?
262 if (config.invalidate) 263 { 264 // Ermitteln der Invalidierungsurl. Diese wird im HTML-Code der invalidierenden 265 // Anwendung über das data-Attribut 'data-invalidation-action' gesetzt. Wir hangeln 266 // uns also im DOM so lange nach oben bis wir auf ein solches data-Attribut treffen. 267 let sender = config.sender; 268 269 if (sender) 270 { 271 if (typeof(config.sender) === 'string') 272 { 273 sender = document.getElementById(config.sender); 274 if (sender == null && document.querySelector) 275 sender = document.querySelector(config.sender); 276 } 277 278 if (sender) 279 { 280 const invalidatingParent = sender.closest('[data-invalidation-action]'); 281 if (invalidatingParent && invalidatingParent.dataset.invalidationAction.length > 0) 282 { 283 body.set('Call.InvalidationAction', invalidatingParent.dataset.invalidationAction); 284 285 // Es wird nun das HTML des invalidierenden Parents ausgetauscht, nicht mehr das HTML der invalidierten Subanwendung. 286 config.domDest = invalidatingParent; 287 } 288 } 289 } 290 } 291 292 // Den eigentlichen Ajax-Aufruf per Fetch machen. 293 const response = await fetch(url, { 294 method: config.method, 295 body: body, 296 keepalive: config.keepAlive 297 }); 298 299 // Wenn der Request missglückt ist, beenden wir die Verarbeitung direkt. 300 if (!response.ok && typeof(HRLogError) === 'function') 301 { 302 HRLogError(new Error(`HTTP-Fehler mit Status ${response.status}. Aufruf: ${url}.`)); 303 return response; 304 } 305 306 if (config.domDest) 307 { 308 // Body als Array-Buffer abrufen, damit wir die Bytes 309 // von Hand in Iso-8859-1 decodieren können. 310 const buffer = await response.arrayBuffer(); 311 const decoder = new TextDecoder('iso-8859-1'); 312 313 if (config.domDest && typeof(config.domDest) === 'string') 314 { 315 let domDest = document.getElementById(config.domDest); 316 if (domDest == null && document.querySelector) 317 domDest = document.querySelector(config.domDest); 318 config.domDest = domDest; 319 } 320 321 this.injectHtml(config.domDest, decoder.decode(buffer)); 322 323 // Ein eigenes load-Event lostreten. 324 if (HREventManager && typeof(HREventManager.dispatchEvent) === 'function') 325 { 326 const eventConfig = {notifyChildren: true, notifyAll: true, bubbles: true, useStandard: false}; 327 if (config.event) 328 { 329 for (prop in config.event) 330 { 331 eventConfig[prop] = config.event[prop]; 332 } 333 } 334 335 HREventManager.dispatchEvent(config.domDest, HREventNames.ajaxload, eventConfig); 336 } 337 } 338 339 // Den Response für weitere Verarbeitung zurückgeben. 340 return response; 341 } 342 catch (e) 343 { 344 if (typeof(HRLogError) === 'function') 345 HRLogError(e); 346 } 347 } 348 349 /** 350 * Liefert eine vollständige Konfiguration mit Defaultwerten, sofern diese nicht durch das 351 * Konfigurationsobjekt des Aufrufers vorgegeben wurden. 352 * 353 * @param userConfig Konfiguration des Aufrufers. Überschreibt die Default-Konfigurationswerte. 354 * @returns Eine Konfiguration für den Reqest bestehend aus Nutzer-Einstellungen und Default-Werten. 355 */ 356 static #mergeSendConfig(userConfig) 357 { 358 // Mit Hilfe des Spread-Operators setzen wir Default-Werte und 359 // überschreiben sie mit der übergebenen User-Config. 360 return { ... { 361 method: 'POST', 362 body: null, 363 keepAlive: false, 364 execJS: true, 365 domDest: null, 366 timeout: 0, 367 invalidate: false, 368 sender: null, 369 form: null, 370 error: null, 371 }, ...userConfig}; 372 } 373 374 /** 375 * Schreibt HTML-Inhalt in ein angegebenes DOM-Ziel und führt ggf. eingebetteten JavaScript-Code aus. 376 * 377 * @param {HTMLElement|string} domDest - Das Ziel-Element, in das der HTML-Inhalt eingefügt werden soll. 378 * @param {string} content - Der HTML-Inhalt, der in das Ziel-Element eingesetzt wird. 379 */ 380 static injectHtml(domDest, content) 381 { 382 // Beachte: Der Einbau eines Leerstrings ist ggf. auch erwünscht! 383 if (domDest != undefined && domDest != null) 384 { 385 domDest.innerHTML = content; 386 387 // TODO: Evaluation und Ausführung des JS-Codes. Hier wird aktuell 388 // noch die exec-Funktion aus der ajax.js aufgerufen. 389 if (execJS) 390 execJS(domDest); 391 } 392 } 393 394 /** 395 * Führt den Code eines nachgeladenen Skript-Blocks aus. 396 * 397 * @param scriptEl Das nachzuladende Skript-Element. 398 */ 399 static executeScriptElement(scriptEl) 400 { 401 const newScript = document.createElement('script'); 402 403 // Attribute übernehmen. 404 for (const attr of scriptEl.attributes) 405 { 406 newScript.setAttribute(attr.name, attr.value); 407 } 408 409 if (scriptEl.src) 410 { 411 // Externes Script: Browser lädt und führt es selbst aus. 412 newScript.src = scriptEl.src; 413 document.head.appendChild(newScript); 414 } 415 else 416 { 417 // Inline-Script: Inhalt kopieren (Browser führt es beim Einfügen aus). 418 newScript.textContent = scriptEl.textContent || scriptEl.innerHTML; 419 document.body.appendChild(newScript); 420 } 421 } 422 423 /** 424 * DOM-Bereich nach Skripten scannen und diese ausführen. 425 * 426 * @param container DOM-Bereich, in dem Skripte gesucht werden. 427 */ 428 static executeJS(container) 429 { 430 if (!container) 431 return; 432 433 if (typeof container === 'string') 434 container = document.getElementById(container); 435 436 const scripts = container.querySelectorAll('script'); 437 438 // Gefundene Skripte durchlaufen.
439 scripts.forEach((script, idx) => { 440 const type = (script.getAttribute('type') || '').toLowerCase(); 441 442 // JSON oder Module überspringen. 443 if (type === 'application/json' || type === 'module') 444 return; 445 446 if (idx == scripts.length - 1) 447 console.log(script.textContent || script.innerHTML); 448 449 this.executeScriptElement(script); 450 }); 451 } 452}
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.