XLS Klasse generiert Microsoft Excel Dateien (iframe AG, S-Node)

24. März 2010

Auch wenn sie jetzt vielleicht einige Fragen, ob hier jemand wirklich richtige Excel Dateien schreibt, muss ich Sie enttäuschen. Hier handelt es sich um eine Klasse, die anstelle von csv (comma seperated values) ein tsv (tab seperated values) generiert. Das Encoding der Werte wird in LATIN1 zurückgegeben.

Doch wesshalb das ganze?

Des öfteren musste man normale csv im Excel importieren, was öfters zu unerwarteten Ergebnissen geführt hat.

<?php

/**
 * xls.class.php
 *
 * Distributed under the GNU Lesser General Public License (LGPL v3)
 * (http://www.gnu.org/licenses/lgpl.html)
 * This program is distributed in the hope that it will be useful -
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * Author: iframe AG, Dominik Zogg 
 *
 */

class xls {

    /* private string filename */
    var $_filename = "";

    /* private array data */
    var $_data = array();

    /* private string renderedContent */
    var $_renderedContent = "";

    /* __construct */
    function xls($filename) {
        $this->_filename = substr($filename, 0 ,
                                  strpos($filename, "."));
    }

    /* public function addData() */
    function addData($data) {
        if(is_array($data)) {
            $this->_data = $data;
            $this->renderContent();
        }
    }

    /* private function renderContent() */
    function renderContent() {
        $this->_renderedContent = "";
        $badsigns = array("\t", "\r", "\n");
        $goodsigns = array(" ", "", "");
        foreach($this->_data as $row) {
            $rowstring = "";
            foreach($row as $field) {
                $rowstring .=
                str_replace($badsigns,
                            $goodsigns,
                            utf8_decode(stripslashes($field))) .
                            "\t";
            }
            $this->_renderedContent .=
            substr($rowstring, 0, -1) . "\r\n";
        }
    }

    /* public function sendData() */
    function sendData() {
        header("Content-type: application/vnd.ms-excel");
        header('Content-Disposition: attachment;
               filename="' .
               $this->_filename . '_' . date("d-m-Y") . '.xls"');
        echo $this->_renderedContent;
    }

}

?>

Download xls.class.php

ICS Klasse, generiert Kalender
(iframe AG, S-Node)

22. März 2010

Mein Ziel war es eine Klasse für das S-Node CMS der Firma iframe AG zu schreiben, welche aus einem zweidimensionalen Array (zb. ein Datenbankresultset):

$events[0]['id'];
$events[0]['creation_date'];
$events[0]['from_date'];
$events[0]['end_date'];
$events[0]['title'];
$events[0]['text'];
$events[0]['location'];

einen Kalender generiert im ics oder auch ical Format. Die Klasse sollte schlicht sein, das implementieren was auch wirklich gebraucht wird.
Die Rückgabe ergibt ein Download eines ics Files.

Der Komplette Code:

<?php

/**
 * ics.class.php
 *
 * Distributed under the GNU Lesser General Public License
 * (LGPL v3) (http://www.gnu.org/licenses/lgpl.html)
 * This program is distributed in the hope that it will be useful
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * Author: iframe AG, Dominik Zogg 
 *
 */

class ics {

    var $filename = "";
    var $events = array();

    var $linebreak = "\r\n";

    var $cal_header = "";
    var $cal_footer = "";

    var $calendar = "";

    /**
     * @param array $events: (
     *  $events[0]['id'];
     *  $events[0]['creation_date'];
     *  $events[0]['from_date'];
     *  $events[0]['end_date'];
     *  $events[0]['title'];
     *  $events[0]['text'];
     *  $events[0]['location'];
     * )
     * @param string $filename
     *
     */
    function ics ($events,
                  $filename = "calendar",
                  $linebreak = "\r\n") {
        if(is_array($events)) {
            if($linebreak == "\r\n" ||
               $linebreak == "\n" ||
               $linebreak == "\r") {
                $this->linebreak = $linebreak;
            }
            $this->filename = $filename;
            $this->generate_header();
            $this->generate_footer();
            $this->prepare_calendar($events);
            $this->build_calendar();
        }
    }

    /**
     * @param array $events
     *
     */
    function prepare_calendar ($events) {
        if(is_array($events)) {
            foreach($events as $event) {
                if($returnvalue = $this->generate_event($event)) {
                    array_push($this->events, $returnvalue);
                }
            }
        }
    }

    function generate_header () {
        $this->cal_header = "BEGIN:VCALENDAR" .
                            $this->linebreak .
                            "PRODID:-//iframe AG/S-NODE" .
                            $this->linebreak .
                            "VERSION:2.0" .
                            $this->linebreak .
                            "METHOD:PUBLISH";
    }

    function generate_footer () {
        $this->cal_footer = "END:VCALENDAR";
    }

    /**
     * @param array $event
     * @return string or FALSE
     *
     */
    function generate_event ($event) {

        $event_data = array();
        $event_str = "BEGIN:VEVENT" . $this->linebreak;

        // Die UID setzen
        if(isset($event['id'])) {
            $event_data['UID'] =
            md5($event['id'] . "@" . $_SERVER['SERVER_NAME']);
        }

        // Das Erstellungsdatum setzen
        if(isset($event['creation_date'])) {
            $event_data['DTSTAMP'] =
            gmstrftime("%Y%m%dT%H%M00Z", $event['creation_date']);
        }
        // Falls kein Erstellungsdatum angegeben wurde den DSTAMP
        // auf das from_date setzen, falls vorhanden
        elseif(isset($event['from_date'])) {
            $event_data['DTSTAMP'] =
            gmstrftime("%Y%m%dT%H%M00Z", $event['from_date']);
        }

        // Das Startdatum setzen
        if(isset($event['from_date'])) {
            $event_data['DTSTART'] =
            gmstrftime("%Y%m%dT%H%M00Z", $event['from_date']);
        }

        // Das Enddatum setzen
        if(isset($event['end_date'])) {
            $event_data['DTEND'] =
            gmstrftime("%Y%m%dT%H%M00Z", $event['end_date']);
        }

        // Den Titel setzen
        if(isset($event['title'])) {
            $event_data['SUMMARY'] =
            $this->clean_values($event['title']);
        }

        // Die Beschreibung setzen
        if(isset($event['text'])) {
            $event_data['DESCRIPTION'] =
            $this->clean_values($event['text']);
        }

        // Den Ort setzen
        if(isset($event['location'])) {
            $event_data['LOCATION'] =
            $this->clean_values($event['location']);
        }

        /**
         * Falls die mindest Werte gesetzt sind,
         * den Event zurueckgeben im richtigen Format
         */
        if(isset($event_data['UID']) &&
           isset($event_data['DTSTAMP']) &&
           isset($event_data['DTSTART']) &&
           isset($event_data['DTEND']) &&
           isset($event_data['SUMMARY'])) {
            foreach($event_data as $key => $value) {
                $event_str .= $key . ":" .
                                      $value .
                                      $this->linebreak;
            }
            $event_str .= "END:VEVENT" . $this->linebreak;
            return($event_str);
        }
        else {
            return(FALSE);
        }

    }

    /**
     * @param string $input
     * @return string
     *
     */
    function clean_values($input) {
        $badsigns = array("<br />", "<br/>", "<br>", "\r\n",
                           "\r", "\n", "\t", '"');
        $goodsigns = array('\n', '\n', '\n', '',
                            '', ' ', '\"');
        return(trim(str_replace($badsigns,
                                $goodsigns,
                                strip_tags($input, ""))));
    }

    function build_calendar () {
        $this->calendar = $this->cal_header . $this->linebreak;
        foreach($this->events as $event) {
            $this->calendar .= $event;
        }
        $this->calendar .= $this->cal_footer;
    }

    function return_calendar () {
        header("Content-type: text/calendar");
        header('Content-Disposition: attachment;
               filename="' .
               $this->filename . '_' . date("d-m-Y") . '.ics"');
        echo $this->calendar;
        die();
    }

}

?>

Download ics.class.php

MYSQL Encodingtester

19. März 2010

Als ich mal nicht sicher war, was für ein Encoding die Daten aus meiner Datenbank haben, schrieb ich dieses kleine PHP Script:

<?php

//header('content-type: text/html; charset=latin1');
header('content-type: text/html; charset=utf8');

$connection['host'] = "localhost";
$connection['username'] = "";
$connection['password'] = "";
$connection['database'] = "";

// ***************************************

$link = mysql_connect($connection['host'],
                      $connection['username'],
                      $connection['password']);
if (!$link) {
    die(mysql_error());
}

// ***************************************

$db = mysql_select_db($connection['database'], $link);
if (!$db) {
    die (mysql_error());
}

// ***************************************

$result = mysql_query("SELECT * FROM xt_articles", $link);

while ($row = mysql_fetch_assoc($result)) {
    print_r($row);
}

mysql_close($link);

?>

PDO Objekt Beispielcode

19. März 2010

Um mal jemanden die PDO Klasse etwas näher zu bringen und selbst mal mit Prepared Statements zu arbeiten habe ich mal einen kleinen Beispielcode geschrieben:

<?php

/* MYSQL Verbindungsdaten */
$mysql_connection['host'] = "";
$mysql_connection['username'] = "";
$mysql_connection['password'] = "";
$mysql_connection['database'] = "";

/* POSTGRESQL Verbindungsdaten */
$pgsql_connection['host'] = "";
$pgsql_connection['username'] = "";
$pgsql_connection['password'] = "";
$pgsql_connection['database'] = "";

/* MYSQL Verbindung aufbauen */
$mysql = new PDO("mysql:dbname={$mysql_connection['database']};
                 host={$mysql_connection['host']};",
                 $mysql_connection['username'],
                 $mysql_connection['password']);

/* POSTGRESQL Verbindung aufbauen */
$pgsql = new PDO("pgsql:host={$pgsql_connection['host']}
                      dbname={$pgsql_connection['database']}
                      user={$pgsql_connection['username']}
                      password={$pgsql_connection['password']}");

/* Pseudodaten fuer den INSERT */
$ins_data = array (
                        array(
                            ":username" => "hans",
                            ":password" => "asdasda?sadas",
                              ),
                        array(
                            ":username" => "sepp",
                            ":password" => "sadssadad?sadas",
                              ),
                          );

/* Pseudodaten fuer den SELECT */
$sel_data = array (
                        array(
                            ":username" => "hans",
                              ),
                        array(
                            ":username" => "sepp",
                              ),
                          );

/* INSERT Statement (SQL) */
$sql_ins = "
    INSERT INTO test_user (
        username,
        password
    ) VALUES (
        :username,
        :password
    )
";

/* INSERT Statement vorbereiten in der MYSQL Datenbank */
$mysql_prep_ins = $mysql->prepare($sql_ins);

/* INSERT Statement vorbereiten in der POSTGRESQL Datenbank */
$pgsql_prep_ins = $pgsql->prepare($sql_ins);

/* SELECT Statement (SQL) */
$sql_sel = "
    SELECT
        *
    FROM
        test_user
    WHERE
        username = :username
";

/* SELECT Statement vorbereiten in der MYSQL Datenbank */
$mysql_prep_sel = $mysql->prepare($sql_sel);

/* SELECT Statement vorbereiten in der POSTGRESQL Datenbank */
$pgsql_prep_sel = $pgsql->prepare($sql_sel);

/* Die Pseudodaten fuer den INSERT abarbeiten */
foreach($ins_data as $row) {

    /* Die INSERT Abfrage ausfuehren in der MYSQL Datenbank */
    $mysql_prep_ins->execute($row);

    /* Die INSERT Abfrage ausfuehren in der POSTGRESQL Datenbank */
    $pgsql_prep_ins->execute($row);

}

/* Die Pseudodaten fuer den SELECT abarbeiten */
foreach($sel_data as $row) {

    /* Die SELECT Abfrage ausfuehren in der MYSQL Datenbank */
    $mysql_prep_sel->execute($row);

    /* Die Daten der SELECT Abfrage holen (ASSOC) */
    $mysql_result = $mysql_prep_sel->fetchAll(PDO::FETCH_ASSOC);

    /* Die Daten ausgeben */
    print_r($mysql_result);

    /* Die SELECT Abfrage ausfuehren in der POSTGRESQL Datenbank */
    $pgsql_prep_sel->execute($row);

    /* Die Daten der SELECT Abfrage holen (ASSOC) */
    $pgsql_result = $pgsql_prep_sel->fetchAll(PDO::FETCH_ASSOC);

    /* Die Daten ausgeben */
    print_r($pgsql_result);
}

?>

MYSQL Datenbankmigration

06. März 2010

Wer kennt das Problem nicht, man möchte eine MYSQL Datenbank von einem Server zum anderen migrieren?

Doch wie geht man vor?

Die einfachste und wohl auch teuerste Lösung, wäre warscheindlich sich eine Navicat Vollversion zu kaufen, was einem pro Lizenz im professionellen Umfeld gut und gerne 130$ pro Lizenz kostet.

Aber auch mit Navicat seid Ihr vom ISO-8859-1 (besser bekannt als LATIN1) Wechsel auf UTF-8 nicht 100% gefeit. Richtig lustigs wird’s aber, wenn ihr noch einen alten MYSQL 4.0 Server habt, der noch kein UTF-8 versteht, ihr in aber schon damit geflutet habt. Die Wahrscheinlichkeit ist gross, spätestens dann wenn ihr einigermassen aktuelle CMS, Blogsoftware usw. einsetzt.

Was tut Ihr jetzt?

mysqldump?

Der wird logischerweise davon ausgehen, dass Ihr noch LATIN 1 einsetzt, ist ja heute noch so in der Standardkonfiguration (ja das MYSQL Projekt hat die Globalisierung wohl verschlafen). Ihr bekommt mit Glück einigermassen sauberes UTF-8 zurück, welches spätestens nach iconv wieder glänzt. Oder ihr habt ein schier unlösbares encoding Chaos.

Auch Navicat korrigiert nicht alle Encodingprobleme, dh. auch hier bleiben noch Fehler.

Was genau machen wir jetzt?

Da meine PHP Skills normalerweise mit der Herausforderung besser werden, dachte ich an ein Datenbank-Migrations-Script.

Ihr Administratoren da draussen könnt euch aber sicher noch an den Tag erinnern, als Ihr in der Standart Konfiguration bei #bind-address 127.0.0.1 die # entfernt habt.

Unser Problem hat sich soeben vom Encodingproblem zu einem “ich komme gar nicht an meine Daten” Problem gewandelt.

Eine Art Tunnel muss her:

  • ssh2 Klasse von PHP (Extension) nutzen, die Anfrage auf dem Terminal machen, xml ausgeben, xml parsen… (habe ich alles mehr oder weniger zum laufen gebracht, ist aber sau lahm, sehr unzuverlässig, weil die Klasse nicht mit jeder PHP Version sauber kompiliert, es sollte ein UNIX Server sein, da die meisten Windows Server kein SSHD am laufen haben und last but not least: Habt Ihr einen SSH Zugang zum Server? Nicht? Dann geht es euch wie den meisten anderen ;)
  • ssh Tunnel per shell_exec(“ssh -f -L 15000:127.0.0.1:3306 username@password -p 22} sleep 60 >> /tmp/logname.log”) und dann per PDO Objekt weiter, auch hier braucht Ihr einen SSH Zugang zum Server. Den Ihr wahrscheinlich nicht habt.
  • HTTP Tunnel aufbauen, dh. auf dem Server auf dem die Datenbank läuft bzw. den Zugriff auf die Datenbank hat kommt ein kleines php File, welches die Datenbankabfrage als $_POST Request bekommt und als JSON zurück gibt. Die ganze Logik, dh. die Verarbeitung kann sein, wo immer man es wünscht. Der grösste Nachteil ist sicherlich die Sicherheit, weil sowohl die Anfrage als auch die Antwort in Klartext verläuft.

Es gibt sicherlich noch andere Möglichkeiten, aber ich bin bei der letzteren gelandet, da sie zuverlässig und vorallem vielseitig einsetzbar ist.

Ok, da war doch noch ein Encodingproblem oder?

Viele von euch kennen wahrscheinlich die Funktion utf8_encode(). Kennt Ihr den grössen Nachteil dieser Funktion? Sie geht nur, wenn sauberes LATIN1 daher kommt. utf8_encode() auf ein UTF8 String ruiniert euch eure Daten.

Ich habe desshalb in meine Tunnelimplementation noch eine Funktion eingebaut, welche mir ziemlich zuverlässig auch Mischencoding korrigiert:

function encode_to_utf8 ($input) {
    $output = $input;
    if($encoding == get_encoding($input)) {
        $output = iconv($encoding, "UTF-8", $output);
    }
    return($output);
}

function get_encoding ($input) {
    $encondings = array("ASCII", "UTF-8", "ISO-8859-1");
    $input_md5 = md5($input);
    foreach($encondings as $enconding) {
        $sample = iconv($enconding, $enconding, $input);
        if(md5($sample) == $input_md5) {
            return($enconding);
        }
    }
    return(false);
}

function recursive_encode_to_utf8 ($array) {
    if(is_array($array)) {
        foreach($array as $key => $value) {
            if(is_array($value)) {
                $tmparray[$key] = recursive_encode_to_utf8($value);
            }
            else {
                $tmparray[$key] = encode_to_utf8($value);
            }
        }
        return($tmparray);
    }
    else {
        return(encode_to_utf8($array));
    }
}

Downloads in der Reihenfolge der Liste (keines dieser Scripte bittet eine Komplettlösung, sodern sie beruhen auf meinen Erfahrungen. Sie stehen alle unter der LGPL):

mysql_over_ssh2_shell

pdo_over_ssh

mysql_over_http_tunnel