Lade...
 

Statisches MorphIT

Statisches MorphIT

Im normalen MorphIT-Modus wird jedem verbundenen Browser genau eine ClassiX-Instanz zugewiesen bis entweder Browser oder ClassiX geschlossen werden. Ist kein ClassiX verfügbar, dann wird der Client in eine Warteschlange eingereiht und der Nutzer muss warten, bis eine Instanz nachgestartet oder wieder frei wird.

Der MorphIT-Server bietet einen statischen Modus an, in dem vorher exportierte statische Views mit eingeschränkter Interaktivität auch ohne verbundenes ClassiX verwendet werden können um so den Server bei einfachen Seitenaufrufen zu entlasten und den Zugriff auf statische Inhalte nicht durch die Anzahl der ClassiX-Prozesse zu beschränken.

Funktionsweise

Bevor man den statischen Modus einrichtet, muss man sich über die Funktionsweise im Klaren sein, da die technische Umsetzung und Sicherheitsaspekte einige Implikationen für die Anwendung haben.

 

Statische Views

Der MorphIT-Client erkennt beim Laden, dass der statische Modus aktiv ist. In dem Fall wird keine Websocket-Verbindung zum MorphIT-Server aufgebaut (dies unterbindet der Server im statischen Modus ohnehin), sondern eine HTTP GET-Anfrage mit dem Pfad /view/0 ausgelöst. Der Server antwortet mit der initialen View, welche vom Client so behandelt wird, als würde sie von einer ClassiX-Instanz kommen und stellt die Oberfläche dar.

Diese statischen Views enthalten zusätzlich zu den regulären views, values, options noch eine Event-Map, die definiert, welche Aktion hinter jedem auslösbaren Event steckt. Im Normalfall führt ein SELECT-Event dazu, dass eine neue View geöffnet wird. Jeder View ist eine ID zugeordnet und jede View kann über /view/ID angefragt werden. Für die Navigation zwischen Views ist keine laufende ClassiX-Instanz notwendig, hierfür reicht ein laufender MorphIT-Server, der die statisch exportierten Views ausliefert.

Webservice Events

195482

Mit rein statischen Views ähnelt der statische Modus primitiven HTML-Seiten, die miteinander verlinkt sind. Da spätestens für den Login und damit den Wechsel in den dynamischen Modus (gekoppelt an eine ClassiX-Instanz) eine Interaktivität mit ClassiX notwendig ist, die sich nicht statisch exportieren lässt, wird jegliche interaktivität auf den statischen Seiten auf HTTP POST Webservice-Anfragen zurückgeführt. Die Webservice-Anfragen werden von speziellen ClassiX-Instanzen (Anzahl konfigurierbar) bedient, die vom MorphIT-Server speziell dafür reserviert werden und nicht mit regulären MorphIT-Verbindungen gekoppelt werden. Über die Anzahl der zu verwendenden Webservice-Instanzen lässt sich die Anwendung auf einfache Weise skalieren.

Webservice-Anfragen schickt der MorphIT-Server über den verbundenen Websocket and die ClassiX-Instanz und dort werden sie über das reguläre Webservice-Interface behandelt und die Antwort über den Websocket zum Server und anschließend als HTTP Antwort an den Client geschickt.

Wurden die statischen Views mehrsprachig exportiert, dann schickt der Client bei jeder Webservice-Anfrage zusätzlich die aktuell aktivierte Sprache (x-morphit-language) und den UTC-Offset (x-morphit-utc-offset) im HTTP-Header mit. Diese Header sind dann auch in ClassiX verfügbar, damit der Webservice in der Sprache des Clients antworten und Daten korrekt umrechnen kann.

 

WebWidgets

Der für WebWidgets verwendete Service webwidgetCommunication erkennt, dass der Client im statischen Modus läuft und schickt automatisch alle WebWidget-Nachrichten als Webservice Anfragen an den Server. Um ein WebWidget auch im statischen Modus zu unterstützen, muss ClassiX auf die entsprechenden Webservice-Anfragen genauso reagieren, wie das WebWidget.

Um zu entscheiden, welche Daten im WebWidget dargestellt werden sollen, kann beim Export im WebWidget der Export-Slot auf wwPrefix=pre gesetzt werden. Dadurch würde allen WebWidget Anfragen dieses Widgets der Präfix PRE_  (Beispiel: PRE_GET_ROOT_POST) vorangestellt werden. Durch unterschiedliche Präfixe können so mehrere WebWidgets vom gleichen Typ unterstützt werden, die auf unterschiedlichen Daten arbeiten.

Widgets

Reguläre Widgets, die keine View öffnen (Beispiel: Login-Button), werden ebenfalls durch Webservice-Anfragen umgesetzt. Damit dies automatisiert funktionieren kann, müssen die Widgets über den Export-Slot entsprechend markiert werden und ClassiX muss auf die Anfrage korrekt reagieren. Mehr dazu hier.

Direktlinks

Im dynamischen Modus wird bei einem Direktlink zunächst eine Verbindung zu einem ClassiX aufgebaut und anschließend die _AUTO-Message in diesem ClassiX ausgeführt. Da im statischen Modus nur authentifizierte Benutzer eine ClassiX-Instanz erhalten dürfen, wird vorher eine spezielle Webservice-Anfrage gestellt VALIDATE_AUTO_POST, in der die Webservice-Instanz vorher überprüfen kann, ob das übergebene Token/URL gültig sind und dem Client ein ClassiX zugewiesen werden soll (Antworttyp: "dynamic"), oder nicht (Antworttyp: "error", sonstiges). 

Ist die Anfrage gültig, dann sollte VALIDATE_AUTO_POST das gleiche JSON zurückgeben, welches im Body der Anfrage übergeben wurde und zusätzlich im JSON das Feld type auf "dynamic" setzen. Der JSON-Body des Anfrageobjekts hat folgendes Format:

Feld Typ Beschreibung
auto string Der Pfad der Auto-Message / Name der Message. (Beispiel: login für LOGIN_AUTO)
path string Der Pfad auf welchem der Direktlink ausgelöst wurde. Ist meist "/" und kann vernachlässigt werden.
params Objekt Eventuelle Parameter des Direktlinks (Login-Token, ...) werden als Felder in diesem Objekt übergeben.

 

Ablaufdiagramm - Direktlink
Ablaufdiagramm - Direktlink

 

Antworttypen

Für Webservice-Anfragen durch reguläre Widgets und Direktlinks sind spezielle Antworten definiert, die vom Client interpretiert werden und so mehr interaktion ermöglichen.

 
Error

Fehler werden vom MorphIT-Client in einer Fehlerdialogbox dargestellt, die mit OK geschlossen werden kann und die aktuelle View wird nicht verändert. Nach Wegklicken des Fehlers kann normal weitergearbeitet werden.

Feld Typ Beschreibung
type string Der String "error"
errorId string Optional: Falls für den Fehler eine Übersetzung in die aktuelle Sprache in den Übersetzungsdateien von MorphIT hinterlegt ist, kann hier der Übersetzungsschlüssel angegeben werden und der Client wird die übersetzte Fehlermeldung darstellen. (Bsp: "server.no_ws_available")
error, error_message string Englischsprachiger Fehlertext, der angezeigt wird, falls der Fehler keine errorId definiert.

 

Attention

Diese Antwort wird vom Client ähnlich behandelt, wie error, die dargestellte Dialogbox lässt sich jedoch besser gestalten. Aufgrund der zustandslosen Kommunikation kann das ClassiX nicht darauf reagieren, welcher Button gedrückt wurde.

Feld Typ Beschreibung
type string

Der String "attention"

parameters Objekt Darstellungsparameter
parameters.text string Der anzuzeigende Text
parameters.level string Optional: Das Notification-Level des Dialogs. Erlaubte Werte: error, info, warning, exception, question. Default: warning
parameters.timeout integer Optional: Falls sich der Dialog nach einer bestimmten Wartezeit selbstständig schließen soll, dann kann hier die Wartezeit in Sekunden angegeben werden.
parameters.buttons Array Optional: Hier kann eine Liste von Buttons definiert werden, die im Dialog angezeigt werden soll. Die Buttons werden als Übersetzungs-Ids relativ zu dialog.button angegeben. (Beispiel: ["ok", "cancel"]). Wird ein leeres Array angegeben, dann wird im Dialog kein Button angezeigt, der Dialog lässt sich dann nicht vom Nutzer schließen.

 

Login / Dynamic

Antwortet ClassiX mit einer Antwort vom Typ login oder dynamic, dann reserviert der MorphIT-Server automatisch einen einmaligen Websocket-Endpunkt für den Client und setzt die Endpunkt-ID in die Antwort hinein, bevor sie zum Client weitergeleitet wird. Der Client verbindet sich dann automatisch mit diesem Endpunkt, der genau eine Verbindung zulässt und anschließend ungültig wird. Der Server akzeptiert die Websocket-Verbindung und verbindet den Client mit einer freien ClassiX-Instanz, oder reiht ihn in die Warteschlange ein. Der Client schaltet dann auf den dynamischen Modus um.

Feld Typ Beschreibung
type string Der String "login"Ab MorphIT 3.16.3 alternativ auch "dynamic"
endpoint string

Dieser String wird vom MorphIT-Server automatisch in die Antwort gesetzt und wird vom Client genutzt, um eine Verbindung zum ClassiX aufzubauen. Dadurch, dass nur ein von ClassiX autorisierter Client diese Websocket-ID kennt, wird verhindert, dass Angreifer durch aufbauen von Socket-Verbindungen auf unbestimmte Zeit Server-Ressourcen beanspruchen und so authentifizierte Nutzer stören können. Die ID ist ein zufällig generierter SHA-256 Hash.

Für den Fall, dass eine dedizierte ClassiX-Instanz (und keine vorgestartete) zugewiesen werden soll, dann muss hier der von RequestMorphITBinding zurückgegebene CX_MORPHIT_NONCE oder der von request_binding zurückgegebene endpoint reingesetzt werden. Dieser Wert wird vom MorphIT-Server dann nicht überschrieben.

params Objekt

Optional: Falls der MorphIT-Client auf seiner verbundenen Instanz direkt eine AUTO-Message auslösen soll, dann kann diese mit ihren Parametern hier definiert werden. Zusätzlich zu dem Pflichtfeld auto können beliebige Parameter über das params-Objekt an den Event-Handler der _AUTO-Message übergeben werden.

Die vom Client folgende _AUTO-Message wird nicht vorher durch eine VALIDATE_AUTO_POST-Message validiert.

auto string

Optional: Falls der MorphIT-Client unmittelbar nach dem Verbindungsaufbau zur neu zugewiesenen ClassiX-Instanz eine AUTO-Message auslösen soll, dann kann diese hier angegeben werden (ohne den _AUTO-Suffix). Der hier angegebene Name wird in Großbuchstaben umgewandelt und um den Suffix _AUTO ergänzt, um den Message-Namen zu erhalten.


Wichtig: Da diese Nachricht nach der Zuweisung einer ClassiX-Instanz ausgelöst wird, wird für diese AUTO-message auch kein VALIDATE_AUTO_POST-Webservice ausgelöst, um die Message zu validieren.

params.(paramName) string Optional: beliebige Parameterwerte, die im Body der Client in der _AUTO-Anfrage übergeben soll.
Falls der Antworttyp "login" lautet, dann sendet der Client automatisch die gleichen Anfrageparameter mit, die bei diesem Webservice-Aufruf übergeben wurden (meist die Login-Daten). Diese müssen in dem Fall nicht explizit in die Antwort gesetzt werden.
url
4.1.4
string Optional: Falls gesetzt, dann wird der Client dazu aufgefordert, auf die angegebene URL zu wechseln, ehe die Websocket-Verbindung aufgebaut wird. Dieser Wechsel löst keine Navigation aus, hat aber den Effekt, dass alle Ressourcen relativ zu dieser neuen URL angefragt werden. Über diese URL kann kein Port oder Domain-Wechsel vollzogen werden.

Um einen vollständigen Login zu ermöglichen, gibt es einen kleinen Unterschied zwischen login und dynamic. Antwortet der Webservice mit login, dann muss der MorphIT-Client die Anfrage-Parameter der dazugehörigen Webservice-Anfrage (meist die Login-Daten) in den auto-Parametern erneut senden. Auf diese Weise kann die zugewiesene ClassiX-Instanz einen vollständigen Login durchführen.

 

Die Ablaufdiagramme von statischem Login und statischem Direktlink unterscheiden sich kaum.

Ablaufdiagramm - Statischer Login
Ablaufdiagramm - Statischer Login

 

Redirect

ClassiX kann mit dieser Response den Client dazu bringen, eine bestimmte statische View zu laden (kann auch ein View-Alias sein). So kann der Client als Antwort auf eine Webservice-Anfrage auf eine bestimmte View umgeleitet werden, anstatt dem Nutzer eine Rückmeldung über attention/error zu geben.

Feld Typ Beschreibung
type string Der String "redirect"
view string Die zu ladende View (Bsp: "0"). Kann auch ein View-Alias wie "login" sein.

 

UpdateView

4.1.0

Dieser Antwort-Typ ermöglicht es auch im statischen Modus eine interaktive Anwendung zu schreiben. 
Alle Felder dieser Nachricht sind optional und können beliebig kombiniert werden. Zu verändernde Widgets werden in dieser Nachricht immer über ihre Parameternamen referenziert, nicht über die Widget-ID. Damit diese Nachricht korrekt verarbeitet werden kann, ist es wichtig, dass gleiche Parameternamen innerhalb einer View nicht mehrfach vergeben sind.

Feld Typ Beschreibung
type string Der String "update_view"
redirect string

Die zu ladende View. Dies kann entweder ein View-Index (0-...) sein oder der Name einer Alias-View. Ist dieses Feld gesetzt, dann wird zuerst die angegebene View geladen und die restlichen Anpassungen der View beziehen sich dann auf die neu geladene View.
Ist redirect nicht gesetzt, dann wird die aktuelle View des Clients angepasst.

Falls redirect gesetzt ist, dann werden alle vom Nutzer eingegebenen Daten durch die Daten der neuen View ersetzt. Dieser Umstand kann ausgenutzt werden, um die vom Nutzer eingegebenen Daten explizit zu löschen, indem die gleiche View per redirect erneut geladen wird.

validation Objekt Dieses Objekt setzt den Validation-State der Widgets. Die Parameter-Namen der Widgets sind die Schlüssel und die Werte sind JSON-Objekte der Form {"level", "text"}. Gültige Level sind: "info", "warning", "error".
values Objekt Dieses Objekt kann die Werte einzelner Widgets überschreiben. Auch hier sind die Parameternamen der Widgets die Schlüssel und die Werte sind die zu setzenden Werte der Widgets. Das Format der Werte ist abhängig vom Widget-Typ und hat das gleiche Format, wie sie im Body der Webservice-Anfrage an ClassiX übergeben wurden.
views Objekt

Dieses Objekt kann die Darstellung einzelner Widgets überschreiben. Auch hier sind die Parameternamen der Widgets die Schlüssel und die Werte sind Objekte, deren Eigenschaften die Views der Widgets überschreiben. Die gesamte View kann hier nicht beschrieben werden. 
Dies sind die wichtigsten Schlüsel:

Feld Typ Beschreibung
type string Der Widget-Typ
left, top, width, height string Die Positionierung des Widgets. Angaben müssen das "px"-Suffix haben (Bsp: "123px")
title string Der anzuzeigende Text für Gruppen, Prompts, Items, Fenster, Checkboxen, ...
tooltip string Der anzuzeigende Tooltip
locked bool Entspricht dem IV-Flag LOCKED
hidden bool Entspricht dem IV-Flag HIDDEN
readonly bool Entspricht dem IV-Flag VIEW_ONLY
focus string Falls dieses Feld gesetzt ist, dann wird das Widget mit dem angegebenen Parameter-Namen fokussiert. Das funktioniert jedoch nur dann, wenn es keinen Validation-State in der zu ladenden View gibt, da ansonsten die Validation-Infobox fokussiert wird.
dialog Objekt Ein Objekt mit den Eigenschaften level ("info", "warning", "error") und text. Falls dieses Objekt gesetzt ist, dann wird auf der aktualisierten View ein Dialog mit dem entsprechenden Text und Level dargestellt. Der Nutzer kann diesen Dialog nur per OK bestätigen.
customization
4.1.4
Objekt

In diesem Objekt können Anpassungen definiert werden, die für die gesamte Dauer der statischen MorphIT-Session erhalten bleiben und auf alle künftigen Views angewandt werden. Dieses Objekt hat eine ähnliche Struktur, wie die update_view-Nachricht selbst und erlaubt folgende Felder.

  • views - Die vom Nutzer nicht veränderbaren Widget-Eigenschaften
  • values - Die Widget-Werte
  • options - Globale eigenschaften, wie der Projektname und Text im Header

Die zu setzenden Werte können in der Antwort auch mehrsprachig hinterlegt (Vorraussetzung: mehrsprachiger statischer Export) werden. Hierzu wird anstatt eines einfachen Strings ein Objekt mit folgender Struktur verwendet:

{"t":{"de":"deutscher Text", "en":"english text", ...}}

Beispiel:

{
  "type":"update_view",
  "redirect": "login_direct",
  "customization": {
    "views":{
      "mainWindow": {
        "title": {"t":{"de":"Willkommen!", "en":"Welcome!"}}
      }
    }
  }
}

 

Beispiel:
Das folgende JSON setzt den Validation-State für zwei Widgets, füllt das E-Mail-Widget mit einem Dummy-Text aus und unchecked die Checkbox und unlocked einen Button.

{ "type":"update_view", "validation": { "email":{ "level": "error", "text": "Email must be filled out!" }, "comment": { "level": "info", "text": "A comment may be helpful" } }, "values": { "email":"example@host.tld", "confirmCheckbox": false }, "views": { "resetButton": { "locked": false } } }

 

Beispiel:
Das folgende JSON leitet den Client auf die Login-Seite um, fokussiert das Widget mit dem Parameternamen "userID" und zeigt einen Dialog an, der den Nutzer darüber informiert, dass die Registrierung erfolgreich war.

{ "type":"update_view", "redirect":"login", "focus":"userID", "dialog":{ "level":"info", "text":"Registration successful" } }

 

Statischer Export

Bevor der statische Modus verwendet werden kann, müssen zuerst die statischen Views erzeugt werden. Hierfür bietet MorphIT einen automatisierten Crawler an, der alle erreichbaren Views automatisiert durchwandert, miteinander verknüpft und anschließend herunterlädt. Der Crawler ist über das DesignMode-Menü (nur aktiv, wenn design_mode_enabled=true in der Konfiguration gesetzt ist) erreichbar (Button: "Dump views"). Während des Exports muss der MorphIT-Server noch im nicht statischen Modus laufen (static.enabled=false), ansonsten wird der Button auch nicht angezeigt.

Hierdurch öffnet sich zunächst das Export-Overlay, welches verhindert, dass mit der Oberfläche während des Exports interagiert werden kann. Hier befinden sich zusätzlich Einstellungen für den Export, ein Button zum Starten des Exports und ein Log, welches bei Export-Fehlern eine Diagnose ermöglicht.

Vorbereitung

Vor dem Export muss man sich klar machen, wie der statische Export funktioniert. Ausgehend von der start-View werden so lange unbekannte Events ausgelöst, bis der Export weiß, welches Event zu welcher View führt. Events von HIDDEN, LOCKED oder NON_SELECTABLE Widgets werden ignoriert. Löst ClassiX während des Exports einen Fehler oder Attention aus, bricht der Export ab. Der automatische Export füllt keine Felder aus, sondern löst nur SELECT aus. Um die statischen Views mit Interaktivität zu exportieren, müssen einige Widgets durch das Setzen von speziellen Slot-Werten für den statischen Export mit zusätzlichen Informationen versehen werden. 

Da der Crawler von jeder View aus, jedes Event einmal auslöst, steigt die Dauer des Exports mit steigender Anzahl an Events/Views sehr schnell. Im Export-Overlay gibt es Einstellungen, die den Export besser visualisieren, dafür aber bei längeren Läufen stark ausbremsen. Die Voreinstellungen garantieren den schnellesten Export.

Widget-Slots

Soll das SELECT-Event auf dem Widget zu einer Webservice-Anfrage führen, dann muss der Slot-Schlüssel action auf dem auslösenden Widget definiert werden und parameter auf den zu übertragenden Parametern. Im folgenden ein Beispiel für eine Login-Maske:

String(userStr, 0, 0, 100) [ INITIALIZE: "parameter=username" Widget Put(morphIt.externalID) ] String(passStr, PASSWORD, 0, 10, 100) [ INITIALIZE: "parameter=password" Widget Put(morphIt.externalID) ] Button(login, 0, 20, 100, 8) [ INITIALIZE: "action=login_static" Widget Put(morphIt.externalID) SELECT: DoRegularLogin ]

Beim Export wird der Crawler das SELECT-Event auf dem Button nicht auslösen, sondern in der View vermerken, dass bei einem SELECT ein Webservice ausgelöst werden soll. Die parameter-Felder werden als JSON-Objekt im HTTP-Body übertragen. Query-Parameter, die im Client gesetzt sind, werden als Query-Parameter im HTTP-Request-Objekt ebenfalls übertragen und sind im Webservice verfügbar.
Läuft der MorphIT-Server unter HTTPS (empfohlen!), dann werden auch die Webservice-Anfragen über HTTPS gestellt und deren Inhalt ist somit verschlüsselt und kann nicht von dritten mitgelesen werden. Dieser Webservice muss in ClassiX anschließend ausprogrammiert werden:

Msg(LOGIN_STATIC_POST) // will be send to webservice instance first LOGIN_STATIC_POST: { LocalVar(reqjs, name, pw) Call(GetBody) jsonTools::LoadFromString -> reqjs //parse body reqjs Copy(username) -> name reqjs Copy(password) -> pw // result if login doesn't match "{\"type\":\"error\", \"error\":\"Wrong username/password passed!\"}" jsonTools::LoadFromString -> result // very simplified login check name "admin" = pw "pass" = & if { // login successful CreateTransObject(CX_JSON_OBJECT) -> result "login" result Put(type) //response type = "login" "login_static" result Put(auto) //client should trigger LOGIN_STATIC_AUTO } // return response result ReturnStack } Msg(LOGIN_STATIC_AUTO) // will be send to assigned instance LOGIN_STATIC_AUTO: { LocalVar(loginjs, name, pw) jsonTools::LoadFromString -> loginjs // Because we have answered with type="login" we get the same login parameters again // If we had answered with type="dynamic" then we wouldn't have access to the same parameters loginjs Copy(username) -> name loginjs Copy(password) -> pw // recheck login to be safe name "admin" = pw "pass" = & if { // actually login user with that user/password ... } else { // We can open an attention here, because this is the connected ClassiX instance "Invalid login data!" Attention(,ERROR) TerminateApp } }
Vorsicht: Es ist wichtig, sich klar zu machen, dass in dem aufgezeigten Beispiel die Nachricht LOGIN_STATIC_POST von der WebService-Instanz verarbeitet wird. Die Nachricht LOGIN_STATIC_AUTO wird von der später zugewiesenen ClassiX-Instanz (nicht die gleiche!) abgearbeitet. In LOGIN_STATIC_POST eine Modulvariable zu setzen, um den Login-Status zu halten, ist deshalb ein Fehler und wird nicht funktionieren.
Falls Informationen, wie ein Login-Token zwischen der Webservice-Instanz und der späteren ClassiX-Instanz ausgetauscht werden müssen, dann müssen diese gemeinsamen Daten in die Datenbank geschrieben werden.

Das hier aufgeführte Login-Beispiel dient nur der Veranschaulichung. In der konkreten Umsetzung sollte darauf geachtet werden, dass das Login-Token nur einmalig  verwendet werden kann und nach einer endlichen Zeit abläuft, damit gültige (nicht verwendete) Tokens nicht durch einen Angreifer durch ausprobieren erraten werden können. Diese Zeit sollte nicht zu kurz gewählt werden, falls mehr Benutzer das System benutzen, als ClassiX-Prozesse gestartet werden können. Der Client würde nach dem Login in eine Warteschlange gehängt werden und würde das Login-Token erst benutzen, wenn eine ClassiX-Instanz frei wird. Läuft das Token in der Zwischenzeit ab, dann muss der Nutzer sich erneut einloggen. Um das zu verhindern, sollte ein Timeout von etwa 5-10 Minuten gewählt werden.

Der Ablauf eines statischen Logins (mit anderen Messages) ist im folgenden Ablaufdiagramm bildlich dargestellt.

Ablaufdiagramm - Statischer Login
Ablaufdiagramm - Statischer Login

 

Widgets, deren SELECT-Event nicht nur zu einer neuen View führt und sich nicht auf Webservices abbilden lässt, können in der exportierten View wie folgt versteckt werden:

[ INITIALIZE: "hidden=true" Widget Put(morphIt.externalID) ]

 

Slot-Werte

Für den Export können Widgets über den Slot morphIt.externalID mit zusätzlicher Semantik versehen werden. Der Slot ist ein String mit der Syntax: schlüssel=wert;schlüssel=wert;schlüssel=wert 
Bei Schlüsseln wird nicht zwischen Groß-/Kleinschreibung unterschieden.

Es sind folgende Schlüssel definiert:

Schlüssel Wert Beschreibung
action Pfad Der Pfad der Webservice-Anfrage, die beim Auslösen des SELECT-Events auf dem Widget gesendet werden soll.
action@ID Pfad Der Pfad der Webservice-Anfrage, die beim Auslösen des SELECT-Events auf den Widgetteil mit der id ID gesendet werden soll. Diese Syntax ist analog zu SELECT@ID aus dem Link-Widget, da sie sich auf genau die Unterlinks des Link-Widgets bezieht.
captcha
4.1.1
Pfad Hierüber wird ein Widget für den statischen Export als CAPTCHA markiert und der angegebene Pfad(e) (als Komma-separierte Liste) bestimmt, welche Webservices durch das CAPTCHA geschützt werden. Widgets die einen dieser Pfade in action definieren, werden durch das CAPTCHA automatisch gelocked und unlocked, sobald das CAPTCHA gelöst wurde.
columnAsId
4.5.0
Spaltenindex ( ≥0 ) Wenn ObjectComboboxen als parameter für Webservice Events (action) definiert wurden, dann kann man hiermit angeben, dass die Combobox den Text der i-ten Spalte als ihren Wert schicken soll und nicht die Id des Eintrags.
eventSource
4.5.1
Name

Wird auf einem Widget mit action ein SELECT-Event ausgelöst, dann kann über eventSource das auslösende Widget als Parameter in der Webservice-Anfrage mit übertragen werden. Der bei eventSource angegebene Name ist der Parametername und kann innerhalb eines Fensters mehrfach verwendet werden, der übertragen wird und der Parameterwert ist der Feld aus id oder parameter des Widgets.

Werden wie im Beispiel unten zwei Buttons mit den folgenden Slot-Werten angelegt, dann würden beide Buttons den gleichen Webservice auslösen und je nachdem, auf welchen Button geklickt wurde, wird entweder trigger=btn1 oder trigger=btn2 als Parameter an den Webservice übergeben.
  Button("action=select_picture;id=btn1;eventSource=trigger")
  Button("action=select_picture;id=btn2;eventSource=trigger")

hidden true/false

true - Das Widget wird in der exportierten statischen View nicht angezeigt und exportiert keine Events.
false - Ein normaler Weise nicht sichtbares Widget wird im statischen Export als sichtbares Widget exportiert und Events können darauf ausgeführt werden. Solche Widgets dürfen kein SELECT-Event definieren, welches eine neue View liefert. Eine Webservice-Action ist jedoch erlaubt.

id
4.1.3
Name Ähnlich wie bei parameter kann man sich in einer update_view-Response auf ein Widget mit diesem Namen beziehen. Im Gegensatz zu parameter werden die Werte dieser Widges nicht bei action-Anfragen übermittelt.
indexAsId
4.5.0
true Wenn ObjectComboboxen als parameter für Webservice Events (action) definiert wurden, dann kann man hiermit angeben, dass die Combobox den Index (≥0) des ausgewählten Eintrags (als String) als ihren Wert schicken soll und nicht die Id des Eintrags.
locked
4.1.1
true/false true - Das Widget wird in der exportierten statischen View gelocked und exportiert keine Events.
false - Ein normaler Weise gelocktes Widget wird im statischen Export als reguläres Widget exportiert und Events können darauf ausgeführt werden. Solche Widgets dürfen kein SELECT-Event definieren, welches eine neue View liefert. Eine Webservice-Action ist jedoch erlaubt.
parameter Name Wird in dem Fenster dieses Widgets ein SELECT auf einem Widget mit einer definierten action ausgelöst, dann wird der Wert dieses Widgets im JSON-Body der Webservice-Anfrage unter dem Schlüssel Name mitgeschickt. Widgets, die parameter nicht setzen, werden bei action-Anfragen nicht automatisch mit übertragen. In der update_view-Response kann man sich auf die Widgets der View über diesen Namen beziehen.
wwPrefix Präfix Wird nur von WebWidgets ausgewertet und enthält einen Nachrichtenpräfix, der dem Pfad jeder Webservice-Anfrage durch das WebWidget (mit Unterstrich getrennt) vorangestellt wird.
lang
4.1.0
Sprach-Kürzel Mit diesem Wert sollten die Menüitems markiert werden, mit denen die Sprache im System umgestellt werden kann. Findet der statische Export mindestens 2 solcher Buttons, dann werden die Views sprachunabhängig exportiert und der statische Export generiert zusätzlich eine languages.json und für jede Sprache eine xx_lang.json, die zusammen mit den Views im gleichen Verzeichnis abgelegt werden. Mit hilfe dieser Übersetzungsdateien können die Views dann in jede Sprache übersetzt werden.
Es sollten die gängigen Sprachkürzel de, en, fr, es, ... verwendet werden.

 

WebWidgets

Damit WebWidgets im statischen Modus ebenfalls funktionieren, müssen zusätzlich zu den bisher üblichen _SOCKET-Messages der WebWidgets gleichnamige _POST-Messages (im Modul) behandelt werden. Das WebWidget sollte für die Abarbeitung der Nachrichten nicht geöffnet werden, um möglichst keinen Zustand in die  Webservice-Instanz zu bringen und die Instanz kann sich nicht darauf verlassen, dass die WebWidgets durch ein vorheriges Event mit Daten befüllt wurden (jede Anfrage muss in sich abgeschlossen sein).

Falls in einer statischen Anwendung mehrere WebWidgets gleichen Typs (mit unterschiedlichen Daten/Einstellungen) verwendet werden sollen, kann über den Schlüssel wwPrefix ein Präfix definiert werden, der jeder Nachricht dieses WebWidgets vorangestellt wird, sodass anhand der Nachricht erkennbar ist, welches WebWidget Daten anfragt. Beispiel (Organigramm):

Web(...) [ INITIALIZE: "wwPrefix=abc" Widget Put(morphIt.externalID) ] Web(...) [ INITIALIZE: "wwPrefix=test" Widget Put(morphIt.externalID) ] Msg(ABC_GET_ROOT, TEST_GET_ROOT) ABC_GET_ROOT: //... TEST_GET_ROOT: //...

 

CAPTCHAs

4.1.1

MorphIT bietet die Möglichkeit an, im statischen Modus auf einfache Weise reCAPTCHA einzubinden, um den Zugriff auf bestimmte Webservices durch Bots und damit mögliche DOS-Attacken zu unterbinden. Der Nutzer muss zunächst durch das CAPTCHA als echter Benutzer identifiziert werden, ehe die Webservice-Anfrage an ClassiX durchgereicht wird. Da die CAPTCHAS dazu dienen, den Zugriff auf ClassiX-Ressourcen gegen unberechtigten Zugriff zu schützen werden sie nur im statischen Modus implementiert, da bei einer bestehenden MorphIT-Verbindung davon ausgegangen wird, dass der Nutzer bereits berechtigt ist.

Voraussetzungen:

  • Google-Account
  • MorphIT-Server und MorphIT-Clients haben eine Internetverbindung

Um ein CAPTCHA in MorphIT einzubinden, muss zunächst ein Google-Account angelegt und ein reCAPTCHA (Version 2) für den Server registriert werden. Als Hostnamen sollte man dabei alle Hostnamen/Domains eintragen über die der MorphIT-Server für Clients erreichbar sein soll. Für eine rein lokale Verwendung muss localhost eingetragen werden. Mit der Registrierung des reCAPTCHAs erhält man einen Site-Key und einen Secret-Key. Diese Schlüssel müssen in der Server-Konfiguration unter static.captcha.secret_key und static.captcha.site_key eingetragen werden. Damit ist der Server vorbereitet.

Vor dem statischen Export muss nun für das CAPTCHA ein Platzhalter-Widget (z.B. ein Button) definiert werden, welches im exportierten Zustand durch das CAPTCHA ersetzt wird. Das Widget kann im normalen Modus HIDDEN sein und über den Export-Slot hidden=false nur für den statischen Export sichtbar gemacht werden. Die Größe des Widgets wird ignoriert, da der statische Export hierfür die optimale Größe (300 x 75 Pixel) einsetzt. 

Beispielcode:

Button(captchaBtn, HIDDEN, 100, 11, 200, 7, "CAPTCHA") [ INITIALIZE: "captcha=CREATE_USER_CLOUD_ACCOUNT_STATIC;hidden=false" Widget Put(morphIt.externalID) ] Button(OKBtn, LOCKED, 200, 11, 200, 7, T("&Speichern", "&Save")) [ INITIALIZE: "call_to_action" Widget Put(morphIt.drawingID) "action=CREATE_USER_CLOUD_ACCOUNT_STATIC;locked=false" Widget Put(morphIt.externalID) SELECT: SaveObject Drop ] Attach(OKBtn, RIGHT, 15) Attach(captchaBtn, OKBtn, RIGHT, OPPOSITE, 100)

 

Erklärung:

  1. Der OKBtn löst im statischen Modus den Webservice CREATE_USER_CLOUD_ACCOUNT_STATIC aus, deshalb muss auch in dem Captcha-Widget captcha=CREATE_USER_CLOUD_ACCOUNT_STATIC gesetzt werden. Dadurch wird das CAPTCHA mit dem Button verknüpft.
  2. Der OKBtn ist normaler Weise gelocked und wird erst durch die Eingabevalidierung geunlocked (ALTERED). Da der statische Modus kein ALTERED-Event unterstützt, muss der Button immer geunlocked sein, die geschieht über locked=false.
  3. Damit das Captcha-Widget im regulären Betrieb nicht stört, ist es HIDDEN und wird für den statischen Export über hidden=false sichtbar gemacht.

 

Enthält die Oberfläche beim statischen Export mindestens ein CAPTCHA, dann wird zusätzlich eine captcha_webservices.json zusammen mit den restlichen Views beim Abschluss des Exports runtergeladen. Diese Datei sollte zusammen mit den Views in das gleiche Verzeichnis exportiert werden und wird vom MorphIT-Server verwendet, um festzulegen, welche Webservice-Anfragen durch ein CAPTCHA verifiziert sein müssen, und welche nicht. Fehlt diese Datei, dann werden keine CAPTCHAs vom Server verifiziert und alle Anfragen können auch ohne das Lösen eines Captchas theoretisch ausgeführt werden.

reCAPTCHA liefert bei erfolgreicher Bestätigung einen Token, welcher im x-morphit-captcha HTTP-Header in der Webservice-Anfrage an den MorphIT-Server übermittelt wird und von diesem einmalig verifiziert werden kann. Webservice-Anfragen für Webservices, die in captcha_webservices.json deklariert sind und mit einem ungültigen Token oder fehlendem Token gesendet wurden, werden vom MorphIT-Server mit einer Fehlermeldung abgelehnt und ClassiX bekommt von diesen Anfragen nichts mit.

Ergebnis:

Captcha

 

Direktlinks

Falls Direktlinks im statischen Modus unterstützt werden sollen, muss wie hier bereits beschrieben die Message VALIDATE_AUTO_POST definiert werden.

Alias Views

Soll der Client über eine bestimmte URL in eine bestimmte Maske im statischen Modus (vor dem login) springen können (Beispiel: Login-Maske direkt öffnen), dann müssen hierfür nach dem statischen Export für die zu erreichende View eine Umleitung von einem frei wählbaren Identifier auf die zu öffnende View eingerichtet werden. 

Diese Umleitung lässt sich am einfachsten über den Design-Mode generieren. Wird der Server im statischen Modus gestartet (static.enabled=true), dann erscheint im DesignMode-Menü der Button "Alias view". Dieser Button fragt zunächst nach einem Alias-Namen für die aktuelle View und generiert anschließend eine Umleitungsdatei, die der Browser automatisch runterlädt. Diese Datei muss in das gleiche Verzeichnis gelegt werden, wie die automatisch exportierten Views des statischen Exports, damit der Alias funktioniert.

Navigiert man beispielsweise zur Login-Maske und setzt denn den Aliasnamen auf login, dann lädt der Browser eine login_view.json runter, die eine Umleitung auf die eigentliche View enthält. Kopiert man diese Datei in das Verzeichnis aus welchem der Server die Views lädt, kann im Client über die URL-Query ?view=login direkt in die Login-Maske gesprungen werden.

Ist die ID der View bekannt, dann kann auch über ?view=9 in die View 9_view.json gesprungen werden. Da die IDs von der Reihenfolge der Events im Export-Prozess abhängen, ist es empfehlenswert und einprägsamer hierfür Aliasnamen zu vergeben. Dass sich die IDs ändern können heißt auch, dass die Umleitungs-Dateien potenziell nach jedem Export neu erzeugt werden müssen (oder zumindest kontrolliert werden müsen).

 

Scriptgesteuerter Export

4.1.3

Der Scriptgesteuerte MorphIT-Export ist eine Möglichkeit, um den statischen Export weiter zu automatisieren und auch kompliziertere Anwendungen zu exportieren. Hierbei wird vor dem Export ein Export-Script in das Export-Overlay gedroppt (eine JavaScript-Datei) und dieses Script kann den Export in gewissem Umfang steuern.

Das Script kann auf Events während des Exports reagieren und den Export wie folgt beeinflussen:

  • Views auswerten & Export durch Fehlermeldung abbrechen
  • Events auf der aktuellen View auslösen und den Export an einer anderen View fortsetzen
  • Einfache Dialoge darstellen
  • Den aktuellen Exportzustand untersuchen und Alias-Views definieren

 

Das nachfolgende Beispielscript ist ein komplexes Beispiel dessen, was aktuell mit dem Export-Script möglich ist. Das Script exportiert zwei Anwendungen hintereinander und definiert Alias-Views, sodass über die Alias-Views zwischen den einzelnen Anwendungen später navigiert werden kann.

Dieses Script:

  1. Prüft anhand der MorphIT-Properties vor dem Export (onExportStart), ob die richtige Anwendung gestartet ist
  2. Definiert nach dem Export (onExplorationEnd) 2 Alias-Views, indem es prüft, welche Views durch Anklicken bestimmter Widgets erreicht wurden
  3. Stellt einen Dialog dar, der dem Nutzer Anweisungen gibt
  4. Schließt die aktuell verbundene ClassiX-Anwendung und bestätigt die Bestätigungsabfrage (onAttention)
  5. Prüft, anhand der MorphIT-Properties, ob die neue Instanz die erwartete ist.
  6. Definiert einen Alias für diese neue View und setzt den Export von dort aus fort.

Code: (export-script.js)

/** This script is the export script for the cloud application. * It will: * - define the alias views 'login', 'register' and 'login_direct' * - guide the user to correctly export the regular portal login views and the product login view */ events.onExportStart = function(firstView) { var properties = firstView.view.options.properties; if (properties.project !== 'classix.cloud' || properties.project_type !== undefined) { throw new Error("The selected export script is incompatible with the currently connected ClassiX instance! This export script has been written for classix.cloud portal application!"); } }; // Return true or a promise, which resolves to true to rerun the exploration. // The passed view will be an ActiveView on which an event can be triggered. All other views are accessible through exportContext.view(id) events.onExplorationEnd = function(lastView) { // Define login alias var initialView = exportContext.view(0); var loginItemId = initialView.findWidget(function(widget) { return _.get(widget, 'slots.number') === 'UserIcon'; }); var loginViewId = initialView.findEvent(function(event) { return event.id === loginItemId; }).action.view; exportContext.defineAlias(loginViewId, 'login'); // Define register alias var registerViewId = exportContext.view(loginViewId).findEvent(function(event) { return event.event === 'SELECT@new'; // This is how we detect the register link }).action.view; exportContext.defineAlias(registerViewId, 'register'); ////////////// // Now prepare changing the application to export the product login view ////////////// // Register an attention handler here because the ClassiX instance will want to know whether we really want to shut it down // and we don't want to error there and simply confirm this dialog. var oldAttentionHandler = events.onAttention; events.onAttention = function(attention) { attention.clickButton(attention.buttons[0]); events.onAttention = oldAttentionHandler; }; // Show a dialog with further instructions for the user before closing the application showDialog('Information', "The export of the cloud portal views has been completed and ClassiX closed.\n" + "Please start the cloud product login application to continue exporting the product login views."); var controlWin = lastView.findWidget(function(widget) { return widget.control_window === true; }); return controlWin.close().then(function(currentView) { // The promise gets completed as soon as the client reconnects to the next ClassiX instance. // The view hasn't yet been processed by the static export and has thus no id. // We set the project_type property to 'client_login' to mark the expected application as such if (currentView.view.options.properties.project_type !== 'client_login') { throw new Error("Export script expected a different application to connect to. Client login application hasn't been recognized."); } hideDialog(); // The next time exploration has completed, we are done events.onExplorationEnd = function() { return false; }; // Define an alias for this view exportContext.defineAlias(exportContext.nextViewId(), 'login_direct'); // continue exploration from this new view return true; }); };

 

Script-Events:

onExportStart(activeView:ActiveView) : { bool | Promise }

Diese Funktion wird vor dem Export mit der aktuell geladenen View aufgerufen. Die Funktion kann Events auslösen und damit die initiale View ändern. Wenn die Funktion fertig ist, und der Export gestartet werden soll, dann sollte sie true liefern, um dem Export mitzuteilen, dass sich die aktive View geändert hat und die View neu geladen werden muss (per load_view-Nachricht). Gibt sie false zurück, dann geht der Export davon aus, dass die View nicht verändert wurde.
Falls die Funktion asynchron arbeitet (Views lädt), dann kann sie auch ein Promise auf true oder false zurückgeben. Der Export wartet dann darauf, dass die asynchrone Operation abgeschlossen ist, ehe der Export gestartet wird.

Der Export kann gänzlich unterbunden werden, indem ein Fehler in dieser Funktion geworfen wird. Der Fehler wird im Status-Fenster des Exports angezeigt und der Export wird nicht gestartet.

 
onAfterNormalization(normalizedView:View, originialView) : View
4.5.0

Die Views werden vom Export-Prozess normalisiert, damit gleiche Views trotz unterschiedlicher Widget-Ids als gleich erkannt werden können. Diese Event-Funktion wird nach jedem solchen Normalisierungsschritt aufgerufen. Die Funktion muss die neue View zurückgeben, die als Ergebnis der Normalisierung verwendet werden soll. Hierüber kann der Normalisierungsvorgang durch das Export-Script bei Bedarf angepasst werden. normalizedView ist das Ergebnis der Normalisierung von originalView ({views,values,windows,options}) und sollte von der Funktion zurückgegeben werden insofern keine gänzlich neue View erzeugt wird. Bei mehrsprachigen Anwendungen wird diese Funktion für jede View einmal mit der regulären View aufgerufen und einmal mit einer automatisch generierten mehrsprachigen View.

 

onExplorationEnd(activeView:ActiveView) : { bool | Promise }

Diese Funktion wird vom Export aufgerufen, sobald nachdem alle Events in allen Views ausprobiert wurden und die Ergebnisse der Events in den Views hinterlegt wurden. Innerhalb dieser Funktion kann das Script wieder Events auslösen, die Views wechseln, Aliase definieren und Views bearbeiten. Sobald dies abgeschlossen ist, sollte die Funktion ein bool (synchron) oder Promise auf bool (asynchron) zurückgeben. true heißt dabei, dass der Export von der jetzt aktiven View fortgesetzt werden soll, false heißt, dass der Export abgeschlossen ist und die Views heruntergeladen werden sollen. Wird in dieser Funktion ein Fehler geworfen, dann wird der Export ohne Download der Views abgebrochen und die Fehlermeldung im Status-Fenster geloggt.

 

onAttention(attention:ScriptAttention)

Diese Funktion wird aufgerufen, sobald ClassiX ein Attention oder eine DialogBox öffnet. Indem diese Funktion definiert wird, lässt sich das Standardverhalten überschreiben, welches den Dialog als Fehler interpretiert, ihn schließt und den Export abbricht. Über das übergebene Objekt lässt sich die Attention wieder schließen.

 

Script-Klassen:

ActiveView

Repräsentiert die aktuell geladene View. Dieses Objekt enthält folgende Felder:

  • view - Das eigentliche View-Objekt {views,values,windows,options} der aktuellen View
  • id - Die Id dieser View
  • idMap - Ein Objekt, das eine Map darstellt von {exportId -> widgetId}, wobei die Export-Id die Id ist, die im Export-Slot unter id oder parameter angegeben wird.

Die Klasse definiert folgende Methoden:

  • defineAlias(name:string) - Für die aktive View wird im aktuellen ExportContext ein Alias mit dem gegebenen Namen hinterlegt, der im Anschluss an den Export zusammen mit den Views heruntergeladen wird.
  • widget(exportId:string) - Falls die idMap einen Eintrag für die gegebene exportId enthält, dann wird das ScriptWidget-Objekt für dieses Widget zurückgegeben.
  • findWidget(predicate:(widget,id)=>bool) - Falls die übergebene Funktion für ein Widget true liefert, dann wird dieses Widget als ScriptWidget zurückgegeben. Der widget-Parameter von predicate sind die Objekte aus view.views.
 
ScriptWidget

Repräsentiert ein Widget in der aktuell aktiven View. Diese Klasse ist nur dazu da, um Events auf Widgets einfacher auszulösen und das funktioniert nur dann, wenn die View, von der dieses ScriptWidget stammt, noch die aktive View ist.
Felder:

  • id - Die Id des Widgets

Methoden:

  • select() - Löst das SELECT-Event auf dem Widget aus und liefert ein Promise auf die nachfolgende ActiveView zurück.
  • close() - Löst das CLOSE-Event auf dem Widget aus und liefert ein Promise auf die nachfolgende ActiveView zurück.
 
ScriptAttention

Repräsentiert eine von ClassiX geöffnete Attention oder DialogBox
Felder: 

  • id - Die Id der Websocket-Nachricht
  • text - Der anzuzeigende Text
  • level - Das Level der Attention (info,warning,error,exception,question)
  • timeout - Der Timeout des Dialogs in Sekunden (falls vorhanden)
  • buttons - Ein Array mit Button-Ids der Buttons, die gedrückt werden können

Methoden:

  • clickButton(buttonId) - Die Attention wird durch einen Klick auf den Button mit der gegebenen Id geschlossen. Diese Methode kehrt sofort zurück und nicht erst nachdem der Dialog vollständig geschlossen ist.

View

Repräsentiert eine normalisierte View, die vom Export-Prozess gefunden wurde. Diese Klasse hat folgende Felder:

  • view - Das eigentliche View-Objekt {views,values,windows,options}
  • map - Ein Array mit Widget-Ids der View, das verwendet wird, um zwischen den original Ids und den normalisierten Ids zu übersetzten. Der Index im Array entspricht der normalisierten Id und der Wert der original Id.
  • eventMap - Ein Objekt der Form {eventName -> {id -> {event}}} welches alle in der View gefundenen Events enthält
  • languageEvents - Ein Array mit je einem Event-Objekt für jede aus der View heraus erreichbare Sprache. In einsprachigen Anwendungen ist dies ein leeres Array.
  • localized - Falls es sich um eine mehrsprachige Anwendung handelt, dann wird dieses Feld in einer automatisch generierten mehrsprachigen View gesetzt und enthält die ursprüngliche einsprachige View.

Methoden:

  • findWidget(predicate:(widget,id)=>bool) - Genau wie in ActiveView kann hiermit nach einem bestimmten Widget in der View gesucht werden, nur dass hier als Ergebnis die Widget-Id zurückgegeben wird.
  • findEvent(predicate:(event)=>bool) - Hiermit können die Event-Objekte der View ({id,event,action}) nach einem bestimmten Event durchsucht werden. Das Event-Objekt wird als Ergebnis zurückgegeben.

Ablauf

Die Seite von welcher der Export gestartet wird, ist die initiale View (View 0), die später als Startseite vom Server ausgeliefert wird. Es ist also durchaus möglich, dass man vor dem Export zu einer bestimmten Maske/Modul navigiert, welches dann den Einstiegspunkt für den statischen Modus darstellt. Auf diese Weise kann eine statische Login-Seite generiert werden, die nur einen Login-Button enthält, obwohl die Hauptanwendung beim Start deutlich mehr Funktionalität anbietet.

Von der initialen View aus beginnnend, liest er alle auslösbaren Events ein (nur SELECT und CLOSE für sichtbare, nicht gesperrte Widgets) und löst auf der aktuellen View ein noch unbekanntes Event aus und merkt sich die View, die als Ergebnis von ClassiX zurückgegeben wurde. Der Export versucht durch normalisierung gleiche Views zu identifizieren damit der Export nicht endlos läuft. Sollte der Export scheinbar endlos laufen und ständig neue Views entdecken, dann sollten die Views auf ihre Unterschiede geprüft werden. Jeder Unterschied in views, values, options führt dazu, dass die Views als unterschiedlich betrachtet werden. Aufgrunddessen erhöht sich der Export-Aufwand direkt um den Faktor 2, wenn man viel mit FLOAT-Fenstern arbeitet, denn dann gibt es jede view einmal mit offnem und einem mit geschlossenem FLOAT-Fenster. Bei MODAL-Fenstern ist das Problem nicht so dramatisch, weil man von den zusätzlichen Views nur die Events des modalen Fensters auslösen kann.

Der Fortschrittsbalken zeigt an, wie viele der bekannten Events bereits ausgelöst und mit einer Nachfolge-View verknüpft wurden. Sollte ClassiX während des Prozesses ein Attention oder einen Fehler liefern, bleibt der Crawler stehen und stoppt den Export. Anhand des Logs kann nachvollzogen werden, welches Event den Fehler ausgelöst hat und möglicher Weise vom Export ausgeschlossen werden muss.

Sobald er Export abgeschlossen ist, werden alle Views als einzelne .json-Dateien heruntergeladen. Falls nur (k)eine Datei heruntergeladen wird, muss möglicher Weise etwas an den Download-Einstellungen des Browsers angepasst werden. Die heruntergeladenen Dateien müssen dann in das Verzeichnis, welches als static.path (Default: ${projectPath}/data/views) konfiguriert ist, verschoben werden.

4.1.0:
Die Views werden aufgrund des Download-Limits von 10 Dateien als .zip runtergeladen, welches dann noch in das entsprechende Zielverzeichnis entpackt werden muss.

Der Browser, die verwendete ClassiX-Instanz und der Server sollten jetzt beendet werden. Jetzt muss noch die Serverkonfiguration (static.enabled) angepasst werden und der Server neu gestartet werden.

Mehrsprachiger statischer Export

Der statische Export sucht auf jeder View, die er erreicht nach Widgets mit dem lang-Attribut. Werden zwei solche Elemente gefunden, dann wird die View mehrsprachig exportiert, das läuft wie folgt ab:

Bei jeder erreichten View werden alle Sprachen durchgeklickt und die von ClassiX geschickten Views untereinander verglichen. Dabei werden alle Zeichenketten, die sich zwischen den Sprachen unterscheiden, durch eine Übersetzungs-ID ersetzt. Diese ID ist der Index in die Übersetzungstabelle der jeweiligen Sprache. Für jede erreichbare Sprache wird eine solche Tabelle aufgebaut, die alle sprachspezifischen Zeichenketten enthält. Falls Übersetzungstabellen generiert wurden, dann werden diese zusätzlich zu den Views als xx_lang.json runtergeladen zusammen mit einer languages.json, die die Liste aller unterstützten Sprachen enthält.

Serverkonfiguration

/config/custom/config.js

Name Typ Beschreibung
static.enabled bool Wenn true, dann werden reguläre websocket-Verbindungen deaktiviert und der Server liefert statische Views. Außerdem werden dann auch ClassiX-Instanzen als Webservice-Instanzen reserviert. (Default: false)
static.path string Der Pfad unter dem der Server die exportierten Views erwartet. (Default: ${projectPath}/data/views)
static.login_timeout integer Die Dauer in Millisekunden, die ein reservierter Websocket nach einer login-Antwort für den Client reserviert bleibt, ehe er abläuft. Der Socket läuft nur dann ab, wenn der Client die Verbindung nicht aufbaut, was normaler Weise nur dann der Fall ist, wenn der Browser in dem Moment geschlossen wird. (Default: 1 Minute)
Diese Dauer hat ein potenzieller Angreifer Zeit, um eine gültige Websocket-ID zu erraten.
static.webservice.enabled
4.6.1
bool Gibt an, ob der Server die Webservice-Schnittstelle aktivieren und ClassiX-Instanzen als Webservice reservieren soll. (Default: Gleicher Wert, wie static.enabled)
static.webservice.endpoint string Der Endpunkt an dem der Server Webservice-Anfragen entgegennehmen soll. (Default: /morphit/webservice)
static.webservice.instances integer | Array

Die Anzahl an Webservice-Instanzen, die der Server reservieren soll. Hierüber kann der Webservice belibig skaliert werden, falls die Anfragen zu langsam beantwortet werden. (Default: 1)
4.2.0 können die Instanzen über ein Array genauer konfiguriert werden.

static.webservice.methods
4.12.0
Array Gibt an, welche HTTP-Methods (GET, POST, PUT, DELETE, ...) and ClassiX-Webservice-Instanzen weitergereicht werden sollen. (Default: POST)
Diese Option kann für jede Webservice-Konfiguration einzeln überschrieben werden.
static.webservice.
request_timeout
integer Falls eine Webservice-Anfrage nach dieser Zeit (Millisekunden) nicht der Webservice-Instanz beantwortet wurde, wird die ClassiX-Instanz beendet und durch eine andere ersetzt. (Default: 1 Minute)
static.webservice.
request_queue_limit
integer Die maximale Anzahl an unbearbeiteten Webservice-Anfragen, die aufgesammelt wird, ehe die nächsten eingehenden Anfragen mit einer Fehlermeldung abgelehnt werden. (Default: 100)
static.webservice.
request_receive_timeout
4.6.1
integer Die maximale Zeit (in Millisekunden), die ein HTTP-Client Zeit hat, um eine Webservice-Anfrage zum Server zu übertragen, ehe die Verbindung zum Schutz gegen DoS-Angriffe einfach geschlossen wird. (Default: 30 Sekunden)
static.webservice.
request_receive_max_size
4.6.1
integer Die maximale Menge an Daten (in Bytes), die ein HTTP-Client in einer Webservice-Anfrage an den Server übertragen darf, ehe die Verbindung zum Schutz gegen DoS-Angriffe einfach geschlossen wird. (Default: 10 MB)
static.webservice.
request_preload_limit
4.6.1
integer Die maximale Anzahl an Anfragen, die pro Webservice-Konfiguration parallel vorgepuffert werden. Diese Option muss nur dann angepasst werden, wenn es sehr viele sehr langsame HTTP-Clients gibt, die mehrere Sekunden benötigen, um ihre Anfrage zu übertragen und damit schnellere HTTP-Clients behindern, deren Anfrage währenddessen nicht verarbeitet werden kann. (Default: 10)
static.webservice.
registration_webservice
4.2.0
string Der Webservice, der an einer ClassiX-Instanz ausgelöst werden soll, sobald das ClassiX als Webservice-Instanz reserviert wird. Die Webservice-Konfiguration wird in dieser Webservice-Anfrage im Body übertragen. Kann auf false gesetzt werden, um keinen Webservice auszulösen. (Default: "webservice_assigned")

Erweiterte Webservice-Konfiguration

4.2.0

Bei sehr unterschiedlich anspruchsvollen Aufgaben der Webservices macht es mitunter Sinn, die Aufgaben auf unterschiedliche Instanzen zu verteilen. Um beispielsweise sicherzustellen, dass sich Nutzer immer einloggen können (Dauer: < 1 Sekunde) und gleichzeitig über einen Webservice aufwendige Berechnungen angestellt werden können (Dauer: > 1 Minute), reicht es nicht einfach, die Anzahl der Webservice-Instanzen (static.webservice.instances) hochzusetzen. Wenn es genug lang laufende Anfragen gibt, dann sind alle Webservices damit blockiert und kein Nutzer kann sich einloggen. 
Um das Problem zu lösen, kann man nun im Detail konfigurieren, wie viele Instanzen wofür zuständig sein sollen. Dafür wird in static.webservice.instances anstatt einer Zahl ein Array von Objekten gesetzt, welches angibt, wie viele Instanzen für welche Webservice-Konfiguration zuständig sind. Jede Konfiguration hat ihre eigene Warteschlange für ausstehende Webservice-Anfragen.

Die Objekte haben folgendes Format:

Feld Typ Beschreibung
allowCORS
4.10.7
boolean Falls true, dann funktionieren die Webservice-Anfragen dieser Pfadkonfiguration auch über CORS.
(Default: false)
id
4.5.4
string Eine kurze Bezeichnung, die zur Identifizierung der Konfiguration in Logs und in der Admin-Konsole dient.
Falls eine id gesetzt ist, dann wird diese ID bei WebService-ClassiX-Instanzen in der Admin-Konsole mit ausgegeben, sodass man auf einen Blick erkennen kann, welche Instanz welcher Konfiguration zugeordnet ist.

Ist keine id definiert, dann wird keine zusätzliche Information in der Admin-Konsole ausgegeben.
instances integer Die Anzahl der Instanzen, die für diesen Pfad reserviert werden sollen. (Default: 1)
launch
4.6.0
Objekt

Ist dieses Objekt gesetzt, dann wird die Webservice-Konfiguration zu einer aktiv startenden Konfiguration und startet aktiv eigene Instanzen über den Launcher als dedizierte ClassiX-Instanzen mit den angegebenen Parametern, die dann direkt dieser Konfiguration zugewiesen werden.

Auf diese Weise kann eine MorphIT-Installation für unterschiedliche Webservices einfach unterschiedliche CalssiX-Produkte starten, die nur dieses Webservices implementieren. Das hat den Vorteil, dass die Hauptanwendung schlank bleiben kann und nicht die benötigten Webservices implementieren muss und umgekehrt die Webservice-Anwendung schlank bleiben kann, weil sie nie als interaktive Instanz verwendet wird.

Das hier angegeben Objekt entspricht dem Objekt, das auch beim Serverbefehl launch_dedicated_classix übergeben wird und unterstützt die Felder:

  • cmd 
  • host (Default: "localhost")
  • config.cwd (Default: ws.launcher.launch.cwd)
  • config.env (Default: {} = Keine Umgebungsvariablen)

Da diese Instanzen über  den Mechanismus der dedizierten ClassiX-Instanzen gestartet werden, erhalten sie auch die Systemrolle dedicated anstatt prelaunched.

methods
4.12.0
Array Gibt an, welche HTTP-Methods (GET, POST, PUT, DELETE, ...) and die ClassiX-Webservice-Instanzen dieser Konfiguration weitergereicht werden sollen. (Default: static.webservice.methods)
path string | Array

Ein Pfad (beginnend mit '/') oderArray von Webservice-Pfaden für die diese Definition gelten soll. Reguläre Ausdrücke können hier verwendet werden (Ausgenommen sind ^ und $, die werden automatisch ergänzt). Der Ausdruck muss den gesamten Pfad matchen und kann auch optional den Query-String matchen. Kann auf einen Leersting gesetzt werden, um keine Anfragen zu akzeptieren. 

Das Feld ist optional. (Default: ".*")

4.4.3 ist der Pfad case-insensitiv. In allen vorherigen Versionen ist er case-sensitiv.

registration_webservice string Der Webservice, der bei dieser Instanz ausgelöst werden soll, sobald sie als Webservice-Instanz dieser Konfiguration zugewiesen wurde. Dieser Pfad kann Query-Parameter enthalten. Default ist der Wert aus: static.webservice.registration_webservice
Das dazugehörige Konfigurationsobjekt wird im Body der Webservice-Anfrage mitgesendet.
Kann auf false gesetzt werden, um zu verhindern, dass ein Webservice ausgelöst wird.

Beispiel:

"instances":[ { "id":"login", "path":"/login" }, { "path":"/long_running_task", "launch": { "cmd":"task_webservice_runner.bat" } }, { "id":"default", "instances":2 }, { "id":"worker", "parameters":["do","important","work!"], "instances":2, "registration_webservice":"start_worker" } ]

Hier werden 6 Webservice-Instanzen definiert:

  • Die erste Instanz ist nur für den Login zuständig und sorgt dafür, dass sich zu jeder Zeit jemand einloggen kann.
  • Die zweite Instanz ist nur für lang laufende Aufgaben zuständig. Dadurch belasten lang laufende Aufgaben den Login-Vorgang nicht.
    Diese Instanz startet über den angegebenen Startbefehl eine dedizierte ClassiX-Instanz, die dieser Konfiguration zugewiesen wird.
  • Zwei Instanzen bearbeiten die restlichen Webservice-Anfragen (Default-Pfad ist ".*").
  • Die letzten zwei Instanzen erhalten keine Webservice-Anfragen von außen und arbeiten als Worker-Prozesse im Hintergrund.

Die Pfade werden von oben nach unten ausgewertet, sodass ein Wildcard-Pfad (".*") dafür sorgt, dass die darauf folgenden Definitionen keine Webeservice-Anfragen bearbeiten, auch wenn sie explizit einen Pfad definieren.

In dem Beispiel wird bei den Worker-Instanzen bei der Registrierung der Webservice /start_worker ausgelöst (Message: START_WORKER_POST) und dabei das folgende JSON im Body übermittelt:

{ "id":"worker", "parameters":["do","important","work!"], "instances":2, "registration_webservice":"start_worker" }

Das Feld parameters wurde hier nur gesetzt, um zu veranschaulichen, dass beliebige Felder in der Webservice-Konfiguration gesetzt werden können und diese dann an die Webservice-Instanz bei der Registrierung übertragen werden. Der Registrierungswebservice kann dazu verwendet werden, um eine entsprechende Initialisierung der Webservice-Prozesse vorzunehmen (Cache vorladen, Timed-Trigger starten, ...).

Sicherheitsaspekte

 

Zugriff auf alle statischen Seiten

Im dynamischen MorphIT-Modus gibt es eine stehende WebSocket-Verbindung die von dritten nicht manipuliert und bei aktiviertem SSL auch nicht mitgelesen werden kann. ClassiX kann somit bei jedem Event entscheiden, welche View der Nutzer sieht. Im statischen Modus hat ClassiX diese Kontrolle nicht, da am Ausliefern der Views kein ClassiX beteiligt ist (Performance) und es keine stehende Verbindung zu den Clients gibt, da HTTP grundsätzlich stateless ist.

Das ist auch so gewollt um möglichst viele Clients mit möglichst geringem Ressourcenaufwand bedienen zu können. In der Konsequenz heißt das auch, dass jede statische View, die exportiert wurde, von jedem beliebigen Client abgerufen werden kann. Cookies werden im statischen Modus nicht eingesetzt.

Da die statischen Views nur vor dem Login eingesetzt werden sollten, sollte es auch keine Rolle spielen, dass ClassiX keine Kontrolle über die Auslieferung und Reihenfolge der Views hat.

 

Webservice-Anfragen

Die Webservice-Anfragen können, wie die Websockets auch durch SSL abgesichert werden und sind damit gegen das Mitlesen durch Dritte geschützt. Parameter werden immer im Body der HTTP-POST-Anfragen übertragen. Die Abarbeitung der Anfragen sollte in ClassiX stateless implementiert sein, damit ungünstige Kombinationen von Anfragen nicht zu unerwarteten Ergebnissen führen.

Der MorphIT-Server hat eine konfigurierbare Request-Queue-Länge (Standard: 100), die dazu führt, dass bei zu vielen Anfragen am Server neue Anfragen mit einer Fehlermeldung abgelehnt werden. Dadurch ist der Server nach einem DOS-Angriff sofort wieder verfügbar, sobald die Anfragen aus der Request-Queue abgearbeitet wurden. Mehrere eingehende Anfragen werden parallel vollständig in den Server geladen, ehe sie einer ClassiX-Instanz zugewiesen werden, damit langsamere HTTP-Clients die schnelleren Clients nicht ausbremsen. Zu große oder zu langsame Anfragen werden vom Server abgebrochen.

Durch den Einsatz von CATPCHAs im statischen Modus, können Spamming und DOS-Angriffe weiter eingeschränkt werden. Der MorphIT-Server weiß, welche Webservices ein CAPTCHA benötigen und diese werden vom Server direkt abgelehnt, wenn das CAPTCHA fehlt oder ungültig ist.

 

Login

Essentiell für den statischen Modus ist die Absicherung des Wechels in den dynamischen Modus (Login). Hierbei muss sichergestellt werden, dass kein unauthentifizierter Benutzer Zugriff auf eine ClassiX-Instanz (Webservice-Instanz ausgeschlossen) erhält. Das wird wie folgt sichergestellt:

Der Server akzeptiert grundsätzlich keine Websocket-Verbindungen von MorphIT-Clients. Erst, wenn eine ClassiX-Instanz auf eine Webservice-Anfrage des Clients mit einer login-Nachricht antwortet, generiert der Server eine zufällige Endpoint-ID, an welcher der Client eine Websocket-Verbindung mit dem Server aufbauen kann. Die ID ist ein zufällig generierter SHA-256-Hash und somit nicht trivial zu erraten. Diese ID wird dem Client nur einmalig in der Webservice-Antwort mitgeteilt und ist nur für genau einen Verbindungsaufbau gültig. Da der Client unmittelbar auf die login-Antwort die Verbindung aufbaut und damit die Endpoint-ID wieder ungültig wird, nützt einem Angreifer das Mitlesen der ID nichts.

Für den Fall, dass der Client die Verbindung nicht aufbaut, weil der Tab in genau dem Moment geschlossen wird, verfällt die ID nach einer einstellbaren Zeit (Default: 1 Minute), damit sich unbenutzte IDs nicht ansammeln und potenziell erraten werden können. Sobald der Client die Vebindung aufgebaut hat, ist seine Kommunikation mit der Instanz durch den SSL(optional, empfohlen)-Websocket abhörsicher und vor Manipulation geschützt.

Aufgrund des zustandlosen HTTP-Protokolls kann nicht ausgeschlossen werden, dass ein bösartiger Client für den Login ein anderes Login-Token verwendet, als der Webservice ihm zurückgegeben hat, weil er woher auch immer "kennt". Der Client könnte sich also mit Nutzer A einloggen, um einen Websocket zu reservieren und anschließend der zugewiesenen ClassiX-Instanz das Login-Token von Nutzer B geben (an das er irgendwie rangekommen ist). Kennt der Angreifer also das Login-Token eines Administrators und die Zugangsdaten eines normalen Nutzers, dann kann er sich als Administrator einloggen. Um ein solches Angriffsszenario zu unterbinden gelten folgende Richtlinien für die Implementierung des Login-Prozesses in ClassiX:

  • ClassiX darf es einem Nutzer nicht ermöglichen, an die Login-Token anderer Nutzer zu kommen.
  • Der Login-Prozess im ClassiX-System muss sicherstellen, dass das herausgegebene Login-Token nur einmalig gültig.
  • Ein Login-Token muss nach einer bestimmten Zeit seine Gültigkeit verlieren, wenn es nicht benutzt wird.

 

Direktlinks

Auch für Direktlinks gilt, dass kein unauthentifizierter Nutzer Zugriff auf eine ClassiX-Instanz erhalten darf. Da die Direktlinks statisch gar nicht abgearbeitet werden können, muss eine ClassiX-Instanz diese bearbeiten. Um sicherzustellen, dass ein bösartiger Client nicht mit einem beliebigen (ungültigen) Direktlink eine ClassiX-Instanz zugewiesen bekommt, muss der Direktlink zuerst von der Webservice-Instanz validiert werden (VALIDATE_AUTO_POST). Ungültige Links werden so abgelehnt und erhalten keine Verbindung. Gültige Links werden über den login-Mechanismus authentifiziert und die Verbindung die gleiche Weise aufgebaut.

 

WebWidgets

WebWidgets arbeiten im statischen Modus auch über HTTP-POST-Anfragen und werden von der WebService-Instanz abgearbeitet. Auch hier wird keine stehende Verbindung zu einer ClassiX-Instanz benötigt.