PageSourceSearch

https://www.stadtwerk-am-see.de/de/static/js/httpClient/httpClient.js?nc=1790327236423

js stadtwerk-am-see.de collected 2026-09-25 20:48:00 UTC 16,213 bytes, 452 lines download raw bytes

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.