Umlautproblemen in Tntnet-Applicationen mit Hilfe von cxxtools aus dem Weg gehen

Standard

In der Vergangenheit hatte ich, dank der strenge Typisierung, mit C++ selten Probleme mit UTF-8 und Umlauten. Ganz anders in Python2.x, Ruby und Bash. Hier habe ich schon Stunden und Tage damit zugebracht, Fälle abzufangen, in denen ich einen unerwarteten String-Codierung bekam. Die – vermeintliche – Zeitersparnis, die ich durch Duck-Typing habe, bezahle ich später durch Laufzeit-Typ-Prüfung doppelt und dreifach wieder zurück. Zum Einen ist der Code mit Duck-Typing ineffizienter und langsamer, zum Anderen schreibe ich dann doch wieder mehr Code, weil ich Duck-Types auf ihren jeweiligen Type testen muss, um ggf nicht ASCII-Code mit Unicode vergleichen bzw. verarbeiten zu müssen. So wird Duck-Typing zu einer „Informatik-Schuldenfalle“, in der man am Ende soviel drauf zahlt, das nie wieder erwirtschaften kann. Zur Ehrenrettung von Skriptsprache: Für Prototyping haben Python, Ruby(-on Rails) und Co ihre Berechtigung, aber für den produktiven Betrieb von Software, die einen längeren Lebenszyklus hat, ist C++ nach wie vor ökonomischer.

Jetzt  zu C++ und meinem Ausgangsproblem. In meiner eccp-View-Datei durchlaufe ich in einer for-Schleife eine Liste mit Objekten die eine Eigenschaft „Name“ haben. Diese Namen sind alphabetisch in der Liste vorsortiert. Ich will sie als Liste auf einer Website ausgeben.

Um die Liste etwas übersichtlicher zu gestalten, habe ich mir überlegt, dass ich für jeden Anfangsbuchstaben einen Abschnitt mit einer Überschrift anlegen möchte, so das alle Namen, die mit „A“ anfangen, die Überschrift „A“ bekommen und die mit „C“ beginnen eine Überschrift „C“ erhalten… Der Algorithmus ist recht überschaubar und leicht verständlich:


01| % string lastLetter = "";
02| % for ( unsigned int i=0; i<s_keywordTitlesCounts.size(); i++) {
03| %     if ( lastLetter != s_keywordTitlesCounts[i].Name.substr (0,1) ) {
04| %         lastLetter = s_keywordTitlesCounts[i].Name.substr (0,1);
05|          <h3 id="letter_" >
08| % } 

In Zeile 01 definiere ich eine Variable, in der ich den letzten Anfangsbuchstaben speichere. In Zeile 02 beginnt der Schleifendurchlauf. In Zeile 03 überprüfe ich, ob der Anfangsbuchstabe schon aufgetaucht ist. Die Methode ".substr(0,1)" gehört zur Klasse std::string und gibt das erste Zeichen des String zurück. Wurde der Anfangsbuchstabe noch nicht verwendet, wird er in Zeile 04 in der Variable "lastLetter" gespeichert. Danach wird in Zeile 05-07 die Überschrift angelegt.

Der Algorithmus funktioniert recht gut, bis ein Name mit einen Umlaut oder Sonderzeichen beginnt. Dann sieht man an der Stelle, an der das
Sonderzeichen stehen sollte ein ? oder ein anderes sonderbares Zeichen. Was ist passiert?

Ein "std::string" ist de facto ein "std::basic_string<char>" und der „char" ist der da runter liegende Basis-Datentyp. Der char-Type hat nur 1 Byte. Der Buchstabe „ä“ braucht jedoch in der utf-8 Form 2 Bytes, um gespeichert werden zu können, nämlich „\xc3\xa4„. Mit der string-Funktion ".substr(0,1)" wird aber nur das 1. Zeichen, bzw. das 1. Byte zurückgegeben, also der char, mit dem Bytewert „\xc3„. Das ist aber nur die Hälfte von dem Umlaut. Die zeichenweise Verarbeitung inklusive substr-Funktion macht bei einem std::string deshalb keinen Sinn, wenn man utf-8 verwendet. Genauso verhält es sich übrigens auch mit stringstreams.

Es gibt eine ganze Reihe von Lösungsansätzen für dieses Problem. Dabei kann auf die Boost-Lib zurückgegriffen werden;  oder C++11-Fatures genutzt werden;  das Qt-Framework hat ebenfalls eine eigene String-Implementiierung namens „QString„, die ich auch ziemlich cool finde und mit der sich das Problem in den Griff kriegen lässt.

Eine weitere Lösung für Unicode-Strings findet sich im cxxtools-Framwork. Dazu gibt es die „cxxtools::String", welche auf std::basic_string<cxxtools::Char> basieren. Und die „cxxtools::Char" können auch Unicode Zeichen repräsentieren und somit auch ein ‚ä‘ beinhalten. Mit der Funktion „cxxtools::utf8codec::encode(cxxtools::String)" bekommt man den Utf-8 Repräsentanten des Strings. Mit „cxxtools::utf8codec::decode(std::string)" wird ein utf-8-kodierten String in „cxxtools::String" konvertiert.

Wenn man Tntnet verwendet, hat man cxxtools automatisch dabei, von daher wäre es naheliegend cxxtools::String zu verwenden. Das sähe in meinem Beispiel so aus:


01| 
02|     #include <cxxtools/utf8codec.h>
03|     #include <cxxtools/string.h> 
04| 
[...]
05| %     cxxtools::String lastLetter;
06| %     cxxtools::String uName = cxxtools::Utf8Codec::decode( s_keywordTitlesCounts[i].Name );
07| %     if ( lastLetter != uName.substr (0,1) ) {
08| %         lastLetter = uName.substr (0,1);
09|          <h3 id="letter_" >
12| % } 

In Zeile 01-04 werden die nötigen Header-Datein eingebunden. In Zeile 05 kommt cxxtools::String zu ersten Mal zum Einsatz. In der nächsten Zeile 06 konvertiere ich den normalen C++-String in einen UTF8-Cxxtools-String. Jetzt bekomme ich auch in Zeile 10 den Wert bzw. den Buchstaben zurück,, den ich erwartet habe. Wird ein cxxtools::String auf einem std::ostream ausgegeben, dann wird er automatisch utf-8-kodiert. Genau das passiert in tntnet in den -Tags (Zeile 10).

Idealerweise verwendet man in der ganzen Applikation cxxtools::Strings. In meiner eigenen Anwendung ist mir jetzt aufgefallen, dass dieser Algorithmus ebenfalls nicht mit Umlauten funktioniert:


01| std::string OString::LowerCase ( std::string keywords ) {
02|     keywords = StrReplace ( "A", "a", keywords);
03|     keywords = StrReplace ( "B", "b", keywords);
04|     keywords = StrReplace ( "C", "c", keywords);
[...]
05|     keywords = StrReplace ( "X", "x", keywords);
06|     keywords = StrReplace ( "Y", "y", keywords);
07|     keywords = StrReplace ( "Z", "z", keywords);
08|     keywords = StrReplace ( "Ä", "ä", keywords);
09|     keywords = StrReplace ( "Ö", "ö", keywords);
10|     keywords = StrReplace ( "Ü", "ü", keywords);
11|     return keywords;
12| }
13| 
14| std::string OString::StrReplace (
15|     const std::string s_string,
16|     const std::string r_string,
17|     const std::string i_string
18| ) {
19|   int pos;
20|   // return string
21|   std::string b_string = i_string;
22|   while (true) {
23|     pos = b_string.find(s_string);
24|     if (pos == -1) {
25|       break;
26|     } else {
27|       b_string.erase(pos, s_string.length());
28|       b_string.insert(pos, r_string);
29|     }
30|   }
31|   return b_string;
32| }

Das Problem lauert hier wieder in Zeile 27-28. Alternativ könnte hier wieder cxxtools::String helfen. Ich habe jedoch gesehen, dass es noch eine undokumentierte Funktion namens CXXTOOLS_API Char cxxtools::toupper( const Char & ch) gibt. Möglicherweise brauche ich meinen eigenen Algorithmus gar nicht. Wenn ich es herausgefunden habe, werde ich es hier berichten.

Siehe auch:


Creative Commons Lizenzvertrag

Tntnet: Formular-Reloads verhindern

Standard

Es gibt mehrere Gründe dafür den Reload eines HTML-Formulars zu verhindern:

  • Man will vermeiden, dass durch einen Page-Reload der User versehentlich Daten mehrmals speichert und unter Umständen damit Duplikate anlegt. Das passiert typischerweise wenn Seiten sehr langsam reagieren und der Benutzer ungeduldig auf die F5-Taste drückt oder den Reload-Button des Browsers anklickt.
  • Ein anderes Szenario wäre, dass der User in verschiedenen Browser-Fenstern das selbe Formular geöffnet hat. Dadurch könnte der User ungewollt seine zuvor im ersten Fenster gespeicherten Daten mit den auf dem zweiten Fenster überschreiben. Oder die Datenänderungen beeinflussen sich sogar wechselseitig, weil die Formulardaten in der Session zwischengespeichert wurden.
  • Möglicherweise ist aber auch die Motivation möglichst wenig Angriffsfläche für Hacker zu bieten. Um ein Angriff in Form von Session Fixation oder Session Hijacking vorzubeugen.

Im Folgenden wird beschrieben wie eine Lösung dieses Problems aussehen könnte. Der Ansatz orientiert sich am MVC-Konzep, wie er hier im Blog schon und in den Artikeln aufgezeigt wurde:

Der View

Ausgangspunkt wird wahrscheinlich immer ein View mit einem Formular sein. Das Formular könnte etwar so aussehen (Auszug aus der ecpp-Datei):

<form method="post" >
    <$$  SessionForm::Manager::getFormToken( request )  $>
    <input
        type="hidden"
        name="arg_delete_account_id"
        value="<$ s_accountList[i_account].getID() $>" >
    <button name="arg_delete_account_button"
            value="pushed"
            type="submit">Löschen
    </button>
</form >

Die Besonderheit ist der Aufruf der statischen Funktion SessionForm::Manager::getFormToken(). Hier mit wird das Formular um ein automatisch generiertes Element erweitert. Request ist eine C++ Scoped-Variable, die zur Laufzeit in der ecpp-Umgebung, also dem View zur Verfügen steht. Diese Variable muss der Funktion getFormToken() mitgegeben werden. Der SessionForm::Manager wird den Token, den er in dem Webformular einbaut als session shared Variable ablegen, um später entscheiden zu können, ob die gesendeten Daten zu einem veralteten Formular gehören. Die Klasse SessionForm::Manager wurde von uns selbst erstellt. Der Aufbau der Klasse wird weiter unten erklärt.

Wird in einer Seite mit mehreren HTML-forms gearbeitet muss immer der selbe Token verwendet werden. Zum Beispiel so:

% std::string formToken = SessionForm::Manager::getFormToken( request );
<form method="post" action="EditAccount">
    <$$ formToken $>
    <i><b>"<$ s_accountList[i_account].getLogin_name() $>"</b></i>
    <input
        type="hidden"
        name="arg_edit_account_id"
        value="<$ s_accountList[i_account].getID() $>" >
    <button name="arg_edit_account_button"
            value="pushed"
            type="submit">Bearbeiten
    </button>
</form >
<form method="post" >
    <$$ formToken $>
    <input
        type="hidden"
        name="arg_delete_account_id"
        value="<$ s_accountList[i_account].getID() $>" >
    <button name="arg_delete_account_button"
            value="pushed"
            type="submit">Löschen
    </button>
</form >

Da mit jedem Aufruf der Funktion getFormToken() ein neuer, allein gültiger, Token generiert wird, werden mit jedem Aufruf alle vorige Token annulliert (ungültig gemacht). Natürlich kann man die Generierung des Token auch in den Controller verlegen und über TNT_REQUEST_SHARED_VAR an den View übertragen. Gerade bei komplexeren Applikationen wird das helfen, die Aufgaben zwischen den Code-Teilen zu strukturieren.

Manager

Die Implementierung der Manager-Klasse, die wir oben in den View-Code schon verwendet haben, könnte so aussehen:


std::string Manager::getFormToken( tnt::HttpRequest& request )
{
    TNT_SESSION_SHARED_VAR( std::string, SESSIONFORM_AVAILABLE_TOKEN, () );
    std::string token = genRandomToken(16);
    token += ":" + OString::IntToStr( request.getSerial() );
    std::string formTokenInput = "";
    SESSIONFORM_AVAILABLE_TOKEN = token;
    return formTokenInput;
}

std::string Manager::genRandomToken ( const int len) {
    std::string randomString = "";
    static const char alphanum[] =
        "0123456789"
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        "abcdefghijklmnopqrstuvwxyz";
    for ( int i = 0; i < len; ++i ) {
        int randNo = srand() % (sizeof(alphanum) - 1) ;
        randomString.push_back ( alphanum[randNo] );
    }
    log_debug( "randomString (Token): " << randomString );
    return randomString;
}

Interessant ist die Funktion request.getSerial(). Diese gibt nämlich eine fortlaufende Nummer zurück. Für jedem request wird eins hochgezählt. Damit wird verhindert, dass der große Zufall mal eintreten könnte, dass wir zweimal die selbe Zufallszahl haben.

Um die Qualität der Zufallszahlen zu erhöhen, sollte der folgende Funktionsaufruf in der main-Funktion (oder in einem anderen Teil des Codes der zu Initialisierung geeignet ist) stehen:

/* initialize random seed: */
srand (time(NULL));

Andernfalls wird man sich darüber wundern, das die „Zufallszahlen“ gar nicht so zufällig sind und damit die die Sicherheitsbemühungen wieder zunichte machen.

Routing

Was jetzt noch fehlt, damit die Komponente alle Formular-Daten überprüft, ist eine Route, die dafür sorgt, dass der Controller, der den Formular-Torken prüft, immer aufgerufen wird. Dazu muss die folgende Route gesetzt werden.

// controller rout for SessionForm token check.
app.mapUrl( "^/(.*)", "SessionForm::Controller" );

Sollen nur bestimmte Formulare-Seiten überwacht werden, muss die Route dementsprechend angepasst werden.

Weiterleitungen

Sollten Formulardaten von veralteten Seiten stammen, wird der Besucher auf die Fehler-Seite /SessionForm/NoAvailabeToken umgeleitet. Möchte man statt der Standard Seite eine eigene Seite gestalten, ist das Routing anzupassen:

// controller rout for SessionForm token check.
app.mapUrl( "^/SessionForm/NoAvailabeToken", "MyTokenErrorView" );

Controller

Der Controller der, die Verarbeitung prüft, könnte so aussehen:

unsigned SessionForm::Controller::operator() (
    tnt::HttpRequest& request,
    tnt::HttpReply& reply,
    tnt::QueryParams& qparam
) {
    TNT_SESSION_SHARED_VAR( std::string, SESSIONFORM_AVAILABLE_TOKEN, () );
    if ( qparam.has("SESSIONFORM_TOKEN") ) {

        std::string SESSIONFORM_TOKEN =
            qparam.arg<std::string>("SESSIONFORM_TOKEN");

        if ( SESSIONFORM_TOKEN != SESSIONFORM_AVAILABLE_TOKEN ) {
            return reply.redirect ( "/SessionForm/NoAvailabeTokenView" );
        }
    };
    return DECLINED;
}

Wer ein vollständiges Beispiel der Implementierung sehen will, kann sich den Code von meinem Projekt Peruschim auf GitHub anschauen.


Creative Commons Lizenzvertrag

Casablanca – ein C++ REST SDK im Beta-Stadium

Standard

Das Casablanca-Projekt ist auf http://casablanca.codeplex.com/ gehostet. Einige Leser werden es schon ahnen: es ist ein Projekt von MicroSoft, denn die Code-Hosting-Seite gehört MicroSoft. Ich würde nicht darüber schreiben, wenn das Projekt nicht unter der freien Apache License stehen würde und der der Code nicht unter Linux verwendbar wäre. Aber so ist es tatsächlich!

Ich wurde durch ein interessantes Video auf das Projekt aufmerksam. Es ist trägt den Titel: A C++ REST SDK: OSS web services on Windows and Linux (Direktlink zu Video ca. 100MB) und ist vom September 2013. Das Video geht ca. eine halbe Stunde. Am Anfang wird erläutert, worin MicroSoft die strategische Bedeutung des Projektes sieht, nämlich bei dem Thema Cloud. Dann wird an Hand einiger Code-Schnipsel die praktische Funktionsweise demonstriert. Gegen Ende wird auch noch mal explizit gefragt, ob der Code tatsächlich unter Linux läuft, was Niklas Gustafsson bestätigt.