hawkmaster: utf8 und latin gemischt?

Hallo zusammen,

vor ein paar Wochen hatte ich meine Webanwendung und die MySQL DB auf utf8 umgestellt. Folgendes hatte ich geändert:
in der MySQL my.ini Datei:
----------------------------------
[mysqld]
default-character-set = utf8
character-set-server = utf8
collation-server= utf8_general_ci
init_connect = SET NAMES utf8

Die Datenbank "testdbNEU" exportiert und nach UTF8 gewandelt. Wieder importiert.

in der Apache httpd.conf
---------------------------------
AddDefaultCharset utf-8

in der php.ini Datei
---------------------------------
default_charset = "UTF-8"

Zusätzlich in allen PHP Seiten den Content Type auf utf8 gesetzt:
------------------------------------------------------------------
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

soweit so gut. Es waren (sind) noch ein paar Änderungen zusätzlich notwendig wegen den String Funktionen aber sonst läuft alles.

Heute hatte ich nun folgende Situation:
Auf dem MySQL Server läuft noch eine andere alte DB "testdbALT" und auf dem Apache eine weitere alte WebAndwendung die beide noch in Latin1 bzw. iso-8859-1 laufen.
Ich bekam nun Probleme mit dieser Anwendung weil ja generell über den Apache und PHP bzw. der my.ini IMMER alles in UTF übertragen wurde. So konnte ich mich z.b. nicht einloggen (User Login mit Passwörter)

Die Frage ist nun: Wie kann es erreichen (zumindest auf dem Testsytem mit der Entwicklungsumgebung) das man utf8 und ISO gemischt laufen lässt?

Ich habe folgendes versucht:
Alle Änderungen in der my.ini, php.ini und httpd.conf rückgängig gemacht.
Dafür in der neuen UTF8 Anwendung die mit der UTF8 DB arbeitet nach der PDO Verbindung
$DBO->query("SET NAMES 'utf8'");
$DBO->query("SET CHARACTER SET 'utf8'");
$DBO->query("SET collation_connection = utf8_unicode_ci");

und in einer zentralen include Datei;
header("Content-Type: text/html; charset=utf-8");

Bei ersten Versuchen lief nun wieder die alte Anwendung mit ISO bzw. Latin und auch die neue UTF8 Anwendung.

Ich bin mir aber nicht 100% sicher, ob ich irgend etwas übersehen habe, bzw. ob es andere Lösungswege gibt?

Wie gesagt: Dies ist nur meine Entwicklungsumgebung. Später in Produktivumgebung wird alles auf UTF8 laufen.

vielen Dank und viele Grüße
hawk

  1. Wenn die Daten in Deiner Datenbank in Latin-1 gespeichert sind, wirst Du auch mit noch so viel fragen nach UTF-8 die gespeicherten Latin-1-Daten bekommen.

    Die einzige Möglichkeit, immer UTF-8 zu bekommen, besteht darin, jedes einzelne Item in der Datenbank zu konvertieren - und zur Sicherheit auch alle Eingaben darauf zu prüfen, ob sie in UTF-8 reinkommen, da man bei Daten, die von außen kommen, nie zu sehr sicher gehen kann.

    Gruß, LX

    --
    RFC 1925, Satz 8: Es ist komplizierter als man denkt.
    1. Hi!

      Wenn die Daten in Deiner Datenbank in Latin-1 gespeichert sind, wirst Du auch mit noch so viel fragen nach UTF-8 die gespeicherten Latin-1-Daten bekommen.

      Diese Aussage ist nicht richtig. Die Datenbank-Einstellung ist nur ein Defaultwert für neue Tabellen. Deren Wert ist nur ein Defaultwert für neue Felder. Letzlich zählt das was pro Feld eingestellt ist. Wenn der Feldparameter zur Kodierung mit dem tatsächlichen Inhalt übereinstimmt, kommt nun die Verbindung zwischen Client und Server ins Spiel. Die dort ausgehandelte Kodierung ist diejenige, in der MySQL die Daten erwartet und ausliefert. (Konkret sind es drei Konfigurationswerte. Zwei für den Hinweg, einer für das Resultset.) Ist diese zu Verbindungskodierung eine andere als die Feldkodierung, kodiert MySQL die Daten um (was prinzipbedingt nicht für jedes Quelle-Ziel-Kodierungspaar verlustfrei geht).

      Von und zum Client spricht die Black-Box MySQL also mit der Verbindungskodierung. Was innen drin passiert ist für den Client belanglos.

      Die einzige Möglichkeit, immer UTF-8 zu bekommen, besteht darin, jedes einzelne Item in der Datenbank zu konvertieren - und zur Sicherheit auch alle Eingaben darauf zu prüfen, ob sie in UTF-8 reinkommen, da man bei Daten, die von außen kommen, nie zu sehr sicher gehen kann.

      Was auch immer du konvertieren willst ... siehe oben.

      Prüfungen sind zwar nicht verkehrt aber im Kodierungsfall verzichtbar. Wenn nicht vorsätzlich am Client manipuliert wurde, sendet der die Daten in der Kodierung der das Formular enthaltenden Seite. (accept-charset-Attribute im Formular werden nicht immer beachtet.)

      Lo!

      1. Hi!

        Wenn die Daten in Deiner Datenbank in Latin-1 gespeichert sind, wirst Du auch mit noch so viel fragen nach UTF-8 die gespeicherten Latin-1-Daten bekommen.

        Diese Aussage ist nicht richtig. (...)
        Von und zum Client spricht die Black-Box MySQL also mit der Verbindungskodierung. Was innen drin passiert ist für den Client belanglos.

        Könnte es sein, dass Du Dir gerade selbst wiedersprochen hast? Meine Aussage war, dass das Daten- und Datenbankformat voneinander unabhängig ist. Du sagst erst, das sei falsch und dann behauptest Du das gleiche?

        (...) Wenn nicht vorsätzlich am Client manipuliert wurde (...)

        Auch diese Möglichkeit sollte man immer in Betracht ziehen. Kein System ist so bedeutungslos, dass sich nicht irgendwann ein Angreifer findet.

        Gruß, LX

        --
        RFC 1925, Satz 8: Es ist komplizierter als man denkt.
        1. Hi!

          Wenn die Daten in Deiner Datenbank in Latin-1 gespeichert sind, wirst Du auch mit noch so viel fragen nach UTF-8 die gespeicherten Latin-1-Daten bekommen.
          Diese Aussage ist nicht richtig. (...)
          Von und zum Client spricht die Black-Box MySQL also mit der Verbindungskodierung. Was innen drin passiert ist für den Client belanglos.

          Könnte es sein, dass Du Dir gerade selbst wiedersprochen hast? Meine Aussage war, dass das Daten- und Datenbankformat voneinander unabhängig ist. Du sagst erst, das sei falsch und dann behauptest Du das gleiche?

          Glaub ich nicht, aber vielleicht habe ich dich falsch verstanden. Ich ging davon aus, dass du meinst, ein Feld sei auf Latin1 eingestellt und die Daten stehen auch als Latin1 darin. In dem Fall bekommst du sehr wohl die Daten UTF-8-kodiert geliefert, wenn du auf der Verbindung UTF-8 eingestellt hast, und das ohne weitere Handlungen. MySQL kodiert das selbständig um. Deine Aussage las sich für mich, dass generell nur die Kodierung zurückkommt, in der die Daten intern abgelegt sind, also Latin1 in dem Fall. Und das wäre nicht richtig. (Außerdem sprichst du schon wieder vom "Datenbankformat". Die individuelle Kodierungseinstellung jedes einzelnen Feldes ist für die Daten relevant.)

          Lo!

  2. Hi!

    [mysqld]
    default-character-set = utf8
    character-set-server = utf8
    collation-server= utf8_general_ci

    default-character-set kannst du weglassen, das ist nicht mehr aktuell und von character-set-server abgelöst. Lässt du beides drin, ist das doppelt gemoppelt.

    init_connect = SET NAMES utf8

    Das ist überflüssig, weil du sowieso den Server auf UTF-8 stehen hast. Verbindungen werden immer mit der Default-Kodierung des Servers erstellt. (Was dann individuell ausgehandelt wird steht auf einem anderen Blatt.)

    Auf dem MySQL Server läuft noch eine andere alte DB "testdbALT" und auf dem Apache eine weitere alte WebAndwendung die beide noch in Latin1 bzw. iso-8859-1 laufen.

    Soweit kein Problem.

    Ich bekam nun Probleme mit dieser Anwendung weil ja generell über den Apache und PHP bzw. der my.ini IMMER alles in UTF übertragen wurde. So konnte ich mich z.b. nicht einloggen (User Login mit Passwörter)
    Die Frage ist nun: Wie kann es erreichen (zumindest auf dem Testsytem mit der Entwicklungsumgebung) das man utf8 und ISO gemischt laufen lässt?

    Ein Mischbetrieb stellt im Prinzip kein Problem dar. Die Apache-Konfiguration lässt sich verzeichnisfein vornehmen. Wenn AddDefaultCharset nicht sowieso schon auf die mit PHP erzeugten text/html-Ressourcen wirkt, dann wäre ebenfalls eine Konfiguration im Verzeichnis empfehlenswert. Läuft PHP als Modul, kann man mit php_value php.ini-Werte ändern. Für CGI-Betrieb kann eine eigene php.ini pro Verzeichnis erstellt werden (die dann exklusiv gilt).

    Alle Änderungen in der my.ini, php.ini und httpd.conf rückgängig gemacht.
    Dafür in der neuen UTF8 Anwendung die mit der UTF8 DB arbeitet nach der PDO Verbindung
    $DBO->query("SET NAMES 'utf8'");
    $DBO->query("SET CHARACTER SET 'utf8'");
    $DBO->query("SET collation_connection = utf8_unicode_ci");

    Bitte verwende nur SET NAMES. SET CHARACTER SET ist in den meisten Fällen unnötig bis schädlich. Obendrein ist die Verwendung von SET NAMES _und_ SET CHARACTER SET sinnlos, weil beide die gleichen Parameter einstellen, nur eben mit unterschiedlichen Werten.

    Jede Anwendung sollte sich ihre benötigte Kodierung (und auch Kollation) auf der Verbindung selbst einstellen und sich nicht auf einen Default-Wert verlassen. Besonders bei Mischbetrieb ist das empfehlenswert. Wenn nun eine oder mehrere Anwendungen von einer bestimmten Einstellung ausgehen und sich nicht umschreiben lassen, muss man halt diese Einstellung als Default-Wert nehem. Die anderen Anwendungen müssen sich dann selbst ihre individuellen Wünsche aushandeln.

    Ich bin mir aber nicht 100% sicher, ob ich irgend etwas übersehen habe, bzw. ob es andere Lösungswege gibt?

    Du kannst das so machen, wie du es jetzt hast (und meine Hinweise beachten).

    Wie gesagt: Dies ist nur meine Entwicklungsumgebung. Später in Produktivumgebung wird alles auf UTF8 laufen.

    Auch wenn das später vorgesehen ist, würde ich die Einstellungen jeweils individuell vornehmen. Vielleicht kommt ja doch noch was altes unflexibles mit auf die Maschine.

    Lo!

    1. Hallo dedlfix,

      vielen Dank für deine Prüfung und deine Hinweise.

      default-character-set kannst du weglassen, das ist nicht mehr aktuell und von character-set-server abgelöst. Lässt du beides drin, ist das doppelt gemoppelt.

      init_connect = SET NAMES utf8

      Das ist überflüssig, weil du sowieso den Server auf UTF-8 stehen hast.

      Das werde ich mir merken. Danke.

      Ein Mischbetrieb stellt im Prinzip kein Problem dar. Die Apache-Konfiguration lässt sich verzeichnisfein vornehmen. Wenn AddDefaultCharset nicht sowieso schon auf die mit PHP erzeugten text/html-Ressourcen wirkt, dann wäre ebenfalls eine Konfiguration im Verzeichnis empfehlenswert. Läuft PHP als Modul, kann man mit php_value php.ini-Werte ändern.

      Ich habe PHP als Modul laufen.
      Du meinst dies über eine .htaccess Datei realisieren?
      z.b. so:
      php_value default_charset utf-8 (oder mit Hochkomma "utf-8" ?)
      Dann bräuchte man aber die .htaccess Datei auch in jedem Unterverzeichnis der Webanwendung oder?
      Könnte man die Anweisung nicht auch über
      ini_set(default_charset utf-8) in einer zentralen include Datei vornehmen?

      Und: Könnte man sich dann die Zeile mit,
      header("Content-Type: text/html; charset=utf-8");

      sparen?

      Bitte verwende nur SET NAMES. SET CHARACTER SET ist in den meisten Fällen unnötig bis schädlich. Obendrein ist die Verwendung von SET NAMES _und_ SET CHARACTER SET sinnlos, weil beide die gleichen Parameter einstellen, nur eben mit unterschiedlichen Werten.

      OK, das werde ich berücksichtigen. Ich hatte dies mit SET CHARACTER SET in einigen Beispielen gesehen.
      Was wohl noch wirklich notwendig ist (zumindest bei mir) war die Zeile mit
      $DBO->query("SET collation_connection = utf8_unicode_ci");
      Ohne dieser Zeile hatte ich Probleme bei einem Login mit Usernamen mit Umlauten.

      vielen Dank und viele Grüße
      hawk

      1. Hi!

        Ich habe PHP als Modul laufen.
        Du meinst dies über eine .htaccess Datei realisieren?
        Dann bräuchte man aber die .htaccess Datei auch in jedem Unterverzeichnis der Webanwendung oder?

        Nein, eine .htaccess gilt auch für alle Unterverzeichnisse. Alternativ kannst du die Einstellungen für den VHost vornehmen, wenn du jeder Anwendung einen eigenen VHost spendiert hast.

        Und: Könnte man sich dann die Zeile mit,
        header("Content-Type: text/html; charset=utf-8");
        sparen?

        Prüf das mit einem geeignete Werkzeug, beispielsweise der livehttpheaders-Extension für den Firefox.

        Ich hatte dies mit SET CHARACTER SET in einigen Beispielen gesehen.

        Das sieht man gelegentlich und es klingt auf den ersten Blick ja auch zielführend, ist es aber in einigen Konstellationen nicht [*].

        Was wohl noch wirklich notwendig ist (zumindest bei mir) war die Zeile mit
        $DBO->query("SET collation_connection = utf8_unicode_ci");
        Ohne dieser Zeile hatte ich Probleme bei einem Login mit Usernamen mit Umlauten.

        Vermutlich hast du da einen Vergleich gemacht. Der ist natürlich abhängig von den Vergleichsregeln der verwendeten Kollation. Besser wäre es aber, Dinge bei denen es auf Exaktheit ankommt, wie bei Passwörtern, auf die Berücksichtigung sprachlicher Besonderheiten zu verzichten, sprich: utf8_bin für diese Felder zu verwenden. Ein Vergleich

        Bei folgendem Beispiel steht die Verbindungskodierung auf UTF-8 (SET NAMES utf8 oder mysql_set_charset('utf8')) und die 'ä's sind auch UTF-8-kodiert.

        CREATE TABLE `test` (  
          `utf8general` varchar(20),  
          `utf8bin` varchar(50) character set utf8 collate utf8_bin,  
          `bin` varbinary(20),  
          PRIMARY KEY  (`ID`)  
        ) DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;  
          
        INSERT INTO `test` (`utf8general`, `utf8bin`, `bin`) VALUES ('ä', 'ä', 'ä');
        

        Ich habe drei Spalten angelegt, eine mit utf8_general_ci, eine utf8_bin und eine rein binäre. In allen dreien ist ein 'ä' abgelegt.
        Kurze Abschweifung: Der Unterschied zwischen den letzten beiden Spalten ist, dass bin sich an Bytes orientiert und utf8bin zwar UTF8-Zeichen berücksichtigt, aber Vergleiche binär durchführt.

        SELECT LENGTH(bin), CHARACTER_LENGTH(bin), LENGTH(utf8bin), CHARACTER_LENGTH(utf8bin) FROM test``

        Diese Abfrage liefert 2,2,2,1 als Ergebnis. LENGTH liefert generell die Byteanzahl, CHARACTER_LENGTH hingegen die Zeichenanzahl. Bei bin ist beides gleich, aber utf8bin sagt korrekt, dass 'ä' nur 1 Zeichen ist. (Bei utf8general wäre das ebenfalls so.) bin interessiert nicht weiter, weil Text und keine Binärdaten gespeichert werden sollen.

        SELECT * FROM `test` WHERE `utf8general` = 'ä'  
        SELECT * FROM `test` WHERE `utf8general` = 'Ä'  
        SELECT * FROM `test` WHERE `utf8general` = 'a'  
        SELECT * FROM `test` WHERE `utf8general` = 'A'  
          
        SELECT * FROM `test` WHERE `utf8bin` = 'ä'  
        SELECT * FROM `test` WHERE `utf8bin` = 'Ä'   -- leere Ergebnismenge  
        SELECT * FROM `test` WHERE `utf8bin` = 'a'   -- leere Ergebnismenge  
        SELECT * FROM `test` WHERE `utf8bin` = 'A'   -- leere Ergebnismenge  
          
        SELECT * FROM `test` WHERE `utf8general` = 'ä' COLLATE utf8_bin  
        SELECT * FROM `test` WHERE `utf8general` = 'Ä' COLLATE utf8_bin   -- leere Ergebnismenge  
        SELECT * FROM `test` WHERE `utf8general` = 'a' COLLATE utf8_bin   -- leere Ergebnismenge  
        SELECT * FROM `test` WHERE `utf8general` = 'A' COLLATE utf8_bin   -- leere Ergebnismenge
        

        Diese "Messreihe" zeigt, dass entweder eine utf8_bin-Spalte oder eine explizite Angabe der gewünschten Kollation in der Query zu einem exakten Vergleich führt.

        [*] Ich möchte das nicht weiter ausführen. Wer sich dafür interessiert, möge bitte die Unterschiede von SET NAMES und SET CHARACTER SET und die Arbeitsweise MySQLs bei Connection Character Sets and Collations beachten und sich ein Szenario vorstellen, bei dem eine Datenbank-Kollation (aus welchem Grunde auch immer) beispielswiese auf Latin1 steht, für die Felder und die Verbindung aber UTF8 verwendet wird.

        Lo!

        1. Hallo dedlfix,

          nochmals herzlichen Dank für deine Mühe und Testreihe.
          War sehr aufschlussreich.

          Nur noch als Info.

          Ich hatte bereits nach der Umstellung auf UTF8 die Tabelle "webusers" in der die Anwender mit Loginname und Passwort drin stehen entsprechend angepasst.
          So hatte ich die Spalte "loginname" auf varchar(200) und Collation utf8_bin eingestellt. Die Spalte pwd hatte ich auf varbinary(200) gestellt. Die Passwörter speichere ich AES_ENCRYPT(), daher varbinary.
          Bei einem Login mache ich dann:
          $DBO->prepare("SELECT count(*) as usercount FROM webusers WHERE loginname = :usr AND pwd = AES_ENCRYPT(:pwd,@pswd_key)

          Warum der Login zuerst nicht ging lag in der Tat an deinem zu Recht bemängelten
          $DBO->query("SET CHARACTER SET 'utf8'");

          Macht man zusätzlich zu SET Names utf8 noch character SET:
          $DBO->query("SET NAMES 'utf8'");
          $DBO->query("SET CHARACTER SET 'utf8'");

          braucht man unbedingt;
          $DBO->query("SET collation_connection = utf8_unicode_ci");

          Lass ich jedoch beides weg, also nur
          $DBO->query("SET NAMES 'utf8'");

          klappt der Login auch mit Umlaute im Passwort und Usernamen problemlos.

          vielen Dank und viele Grüße
          hawk

          1. Hi!

            Die Spalte pwd hatte ich auf varbinary(200) gestellt. Die Passwörter speichere ich AES_ENCRYPT(), daher varbinary.

            Stimmt, ich erinnere mich. Fürs Protokoll: In dem Fall ist das (var)binary statt ..._bin richtig, weil AES_ENCRYPT() Bytes statt Text liefert.

            Lo!