Jörg Reinholz: Das Skript zum Sonntag - PHP-Skripte automatisch reparieren

Moin!

Immer wieder bekomme ich Anfragen, dass bestimmte PHP-Skripte nur eine "leere Seite" liefern. Grund ist meist, dass der Verwender diese in einem Windows-Editor (deshalb auch unter "Barrierefreiheit" "getackert") geöffnet hatte (der dann prompt eine BOM hineinschrieb) oder Leerzeichen bzw. Leerzeilen "hineingepfuscht" hat, was dann bei einem Aufruf von header() nicht mit einer Fehlermeldung quittiert wird ...

Dieses Skript repariert Einiges davon:

<?php
## repariert PHP Skripte:
## entfernt BOMs
## entfernt Leerzeilen am Beginn
## entfernt Leerzeichen vor <? oder <?php 
## ersetzt <? durch <?php

# Konfiguration:

$config['DIR'] = '/tmp';   # In welchem Verzeichnis sollen die Dateien repariert werden?
#$config['DIR'] = '.';     # aktuelles Verzeichnis
#$config['DIR'] = __DIR__; # Verzeichnis dieses Skriptes
#$config['DIR'] = $SERVER['DOCUMENT_ROOT'];

$config['IGNOREUPPERLOWER'] = false; # Für Linux-Benutzer...
#$config['IGNOREUPPERLOWER'] = true; # Für Windows-Benutzer...

$config['FIND'] = '*.inc.php'; # * beliebiges Zeichen beliebig oft
                           # ? beliebiges Zeichen genau einmal

$arExkludeList=array(
    # Nach letztem Eintrag kein Komma!
    'diese_nicht/*', # Unterverzeichnis ausgehend von $config['DIR'], kein führender "/"
    '*.ex'    # Dateien mit Endung "ex"
);

$habeBackup = false; # Setzen Sie dieses aus true, nachdem ein Backup gemacht wurde.

## Ab hier ändern nur Programmierer, die wissen, was sie tun:

define ( 'DIR', rtrim($config['DIR']), '/' );
define ( 'FIND', $config['FIND'] );
define ( 'IGNOREUPPERLOWER', $config['IGNOREUPPERLOWER']);

if ( isset($SERVER['DOCUMENT_ROOT']) ) {
    define ('NL', "<br>\n");
} else {
    define ('NL', "\n");
}

$fileList=getFileList(DIR);
$arExkludeListRegex=array();

foreach ($arExkludeList as $s) {
   if ($s) {
        $arExkludeListRegex[] = patternToRegex(DIR . '/' . $s, true, IGNOREUPPERLOWER);
   }
}
unset($arExkludeList);

foreach ($fileList as $file) {
    $isExcluded=false;
    foreach ($arExkludeListRegex as $regex) {
        if ( ! $isExcluded && preg_match($regex, $file)  ) {
           $isExcluded=true;
           break;
        }
    }

    if ($isExcluded) {
        echo "Info: '$file' wurde ausgeschlossen.", NL;
    } elseif (! is_readable($file)) {
        echo "Warnung: Keine Leserechte an '$file'", NL;
    } elseif (! is_writable($file)) {
        echo "Warnung: Keine Schreibrechte an '$file'", NL;
    } elseif ($habeBackup) {
        echo "Info: Verarbeite '$file'";
        repair($file);
    } else {
        echo "Info: würde '$file' verarbeiten, wenn das Anlegen eines Backups bestätigt wäre.", NL;
    }
}

echo "### Fertig! ###", NL;

#### Von diesem Skript verwendete Funktionen ####
# sollten unverändert bleiben.

function patternToRegex($string, $allRow=false, $ignoreUpperLower=false) {
    if ($ignoreUpperLower) {
        $ignoreUpperLower='i';
    } else {
        $ignoreUpperLower='';
    }

    $string=str_replace('/', '\\/', $string);

    $search  = array( '.' , '*' , '?');
    $replacment = array( '\.', '.*', '.');

    if ($allRow) {
        return '/^' . str_replace($search, $replacment,  $string) . '$/' . $ignoreUpperLower;
    } else {
        return '/' . str_replace($search, $replacment,  $string) . '/' . $ignoreUpperLower;
    }
}

function getFileList($dir) {
    $list=array();
    $regex = patternToRegex(FIND, false, IGNOREUPPERLOWER);
    if ( is_dir($dir) && is_readable($dir) ) {
        $d = dir($dir);
        if ($d) {
            while (false !== ($entry = $d->read())) {
                if ( '.' != $entry && '..' != $entry ) {
                    $entry = $dir .'/'. $entry;
                    if ( is_dir($entry) ) {
                        $arT = getFileList($entry);
                        foreach ($arT as $s) {
                            $list[]=$s;
                        }
                    } elseif ( preg_match($regex, $entry) ) {
                        $list[] = $entry;
    }   }   }   }   }
    return $list;
}


#### Arbeitende Funktionen ####
# können für andere Verwendungen ersetzt werden

function repair($file) {
    $newSkript = '';
    $isFirstLine = true;

    $ar=file($file);

    foreach ($ar as $row) {
        #$row = trim($row);
        $row = rmBOM($row);                      #löscht alle bekannten BOM
        if ( $isFirstLine ) {
            $row = rmOnlySpaces($row);           #löscht alle Spaces in Zeilen, die nur solche
                                                 #enthalten 
        }
        if ( $row ) {
            $row = rmSpacesBevorPhpTag($row);    #löscht alle spaces vor <?php
                                                 #ersetzt <? und <?PHP durch <?php
            $newSkript .= $row;
            $isFirstLine = false;
        }
    }
    file_put_contents($file, $newSkript);
    echo " ... erledigt!", NL;
}


function rmBOM($str, $replacment = '') {
 # source: https://en.wikipedia.org/wiki/Byte_order_mark

    $boms=array(
        urldecode('%EF%BB%BF'),       #UTF-8
        urldecode('%FE%FF'),          #UTF-16 (BE)
        urldecode('%FF%FE'),          #UTF-16 (LE)
        urldecode('%00%00%FE%FF'),    #UTF-32 (BE)
        urldecode('%FF%FE%00%00'),    #UTF-32 (LE)
        urldecode('%2B%2F%76%38'),    #UTF-7
        urldecode('%2B%2F%76%39'),    #UTF-7
        urldecode('%2B%2F%76%2B'),    #UTF-7rmOnlySpaces
        urldecode('%2B%2F%76%2F'),    #UTF-7
        urldecode('%2B%2F%76%38%2D'), #UTF-7
        urldecode('%F7%64%4C'),       #UTF-1rmOnlySpaces
        urldecode('%DD%73%66%73'),    #UTF-EeBCDIC
        urldecode('%0E%FE%FF'),       #SCSU
        urldecode('%FB%EE%28'),       #BOCU-1
        urldecode('%84%31%95%33')     #GB-18030
    );
    return str_replace($boms, $replacment, $str);
}

function rmSpacesBevorPhpTag($str, $replacment = '') {
    $replacment .= '<?php';
    return preg_replace('/^\s*<\?([pP][hH][pP]){0,1}/', $replacment, $str);
}

function rmOnlySpaces($str, $replacment = 'repair($file)') {
    return preg_replace('/^\s+$/', $replacment, $str);
}

Jörg Reinholz

  1. Das hat mich auch schon Zeit gekostet mit dem BOMs. BOM am Anfang eines Scripts hat doch sicher keine sinnvolle Funktion, wäre das ignorieren dieser Zeichen eigentlich nicht längst ein Punkt für eine neue PHP Version gewesen?

    1. Moin!

      Das hat mich auch schon Zeit gekostet mit dem BOMs. BOM am Anfang eines Scripts hat doch sicher keine sinnvolle Funktion, wäre das ignorieren dieser Zeichen eigentlich nicht längst ein Punkt für eine neue PHP Version gewesen?

      Nein. Geht nicht. Denn PHP weiß nicht warum die BOM da ist. Kann ja auch gewollt sein...

      Jörg Reinholz

      1. Nein. Geht nicht. Denn PHP weiß nicht warum die BOM da ist. Kann ja auch gewollt sein...

        Was könnte die BOM dort gewolltes bewirken?

        1. Hallo

          Nein. Geht nicht. Denn PHP weiß nicht warum die BOM da ist. Kann ja auch gewollt sein... Was könnte die BOM dort gewolltes bewirken?

          Sie soll die Zeichenkodierung angeben. Was sonst? Schließlich ist sie genau dazu da.

          Tschö, Auge

          --
          Wir hören immer wieder, dass Regierungscomputer gehackt wurden. Ich denke, man sollte die Sicherheit seiner Daten nicht Regierungen anvertrauen.
          Jan Koum, Mitgründer von WhatsApp, im Heise.de-Interview
          1. Sie soll die Zeichenkodierung angeben. Was sonst? Schließlich ist sie genau dazu da.

            Jou aber dann könnte PHP doch genau das tun was damit gedacht ist, nämlich sie auszuwerten und nicht als Teil des Scripts anzusehen, statt ein ganzes Script lahmzulegen? Also zu deutsch: warum kann man nicht in Unicode PHP programmieren? Wär doch langsam Zeit dazu.

            Warum ich vorhin schon wieder nicht angemeldet war ist mir langsam ein Rätsel! Mein Browser zuhause soll sich eigentlich merken wer ich bin. Oder wirft die Seite die Anmeldung nach ein paar Tagen wieder weg?

            1. Hallo

              Sie soll die Zeichenkodierung angeben. Was sonst? Schließlich ist sie genau dazu da. Jou aber dann könnte PHP doch genau das tun was damit gedacht ist, nämlich sie auszuwerten und nicht als Teil des Scripts anzusehen, statt ein ganzes Script lahmzulegen?

              Hatten wir schon. PHP kann nur erraten, wozu die Steuerzeichen in der Datei stehen. Und raten ist per definitionem nicht zuverlässig.

              Also zu deutsch: warum kann man nicht in Unicode PHP programmieren? Wär doch langsam Zeit dazu.

              Kann man es denn nicht? Dass die Unicode-Unterstützung immernoch der mb-Erweiterung bedarf, ist bekannt. Wenn die nativen Stringfunktionen UTF8 unterstützen würden, wäre das schöner, aber es ist eben nicht unmöglich.

              Warum ich vorhin schon wieder nicht angemeldet war ist mir langsam ein Rätsel! Mein Browser zuhause soll sich eigentlich merken wer ich bin. Oder wirft die Seite die Anmeldung nach ein paar Tagen wieder weg?

              Um die Ferengi-Erwerbsregeln zu paraphrasieren: Ein Cookie ist ein Cookie ist ein Cookie. Und ein solches hat eine Ablauffrist.

              Wenn ich ein Gerät selten nutze, geht mir auch schon mal die dortige Anmeldung im Forum flöten. Dies aber nicht schon nach ein paar Tagen, sondern nach ein paar Wochen. Ein Blick in die Cookie-Liste sagt mir, dass das Cookie remember_user_token, welches wohl das Relevante ist, bei mir das Ablaufdatum 12.02.2016 hat. Das sind von jetzt an 10 Tage, was meine Beobachtung, wenn ich das richtige Cookie erwischt haben sollte, bestätigt.

              Tschö, Auge

              --
              Wir hören immer wieder, dass Regierungscomputer gehackt wurden. Ich denke, man sollte die Sicherheit seiner Daten nicht Regierungen anvertrauen.
              Jan Koum, Mitgründer von WhatsApp, im Heise.de-Interview
              1. Hallo Auge,

                Wenn ich ein Gerät selten nutze, geht mir auch schon mal die dortige Anmeldung im Forum flöten. Dies aber nicht schon nach ein paar Tagen, sondern nach ein paar Wochen. Ein Blick in die Cookie-Liste sagt mir, dass das Cookie remember_user_token, welches wohl das Relevante ist, bei mir das Ablaufdatum 12.02.2016 hat. Das sind von jetzt an 10 Tage, was meine Beobachtung, wenn ich das richtige Cookie erwischt haben sollte, bestätigt.

                Exakt, „remember me“ hat ein Ablaufdatum vor 10 Tagen.

                LG,
                CK

                1. Hallo

                  Wenn ich ein Gerät selten nutze, geht mir auch schon mal die dortige Anmeldung im Forum flöten. Dies aber nicht schon nach ein paar Tagen, sondern nach ein paar Wochen.

                  zu unkonkret

                  Exakt, „remember me“ hat ein Ablaufdatum vor 10 Tagen.

                  Um korrekt zu sein, die Haltbarkeitsdauer beträgt 1 3/7 (in Worten: einunddreisiebtel) Wochen.

                  Spaß beiseite; gibt es einen konkreten Grund für die Wahl genau dieser Haltbarkeit?

                  Tschö, Auge

                  --
                  Wir hören immer wieder, dass Regierungscomputer gehackt wurden. Ich denke, man sollte die Sicherheit seiner Daten nicht Regierungen anvertrauen.
                  Jan Koum, Mitgründer von WhatsApp, im Heise.de-Interview
                  1. Hallo Auge,

                    Spaß beiseite; gibt es einen konkreten Grund für die Wahl genau dieser Haltbarkeit?

                    Ja: nachts war es dunkler als draussen ;-)

                    Nein, kein spezifischer Grund. Willkürlich festgelegt aus der Überlegung, dass Regulars vermutlich durchaus alle 10 Tage hier herein schauen und Nicht-Regulars sich eh regelmässig neu werden einloggen müssen.

                    LG,
                    CK

        2. Moin!

          Nein. Geht nicht. Denn PHP weiß nicht warum die BOM da ist. Kann ja auch gewollt sein... Was könnte die BOM dort gewolltes bewirken?

          Ich zitiere:

          "Bei unzureichend spezifizierten Protokollen wird empfohlen, das Unicode-Zeichen U+FEFF (BOM, byte order mark), das für ein Leerzeichen mit Breite Null und ohne Zeilenumbruch (zero width no-break space) steht, an den Anfang des Datenstroms zu setzen – wird es als das ungültige Unicode-Zeichen U+FFFE (not a character) interpretiert, heißt das, dass die Byte-Reihenfolge zwischen Sender und Empfänger verschieden ist und die Bytes jedes 16-Bit-Worts beim Empfänger vertauscht werden müssen, um den sich anschließenden Datenstrom korrekt auszuwerten."

          Du darfst nicht immer nur daran denken, dass HTML, XML oder dergleichen gesendet wird. Es kann ja auch PlainText oder gar etwas wie Postscript, LaTeX, DVI ... gesendet werden. Auch der Client ist auch nicht immer ein Browser, sondern womöglich eine Anwendung... welche die Daten weiter verarbeitet und eben bei UTF eine BOM erwartet. Und da PHP selbst schon ein Templatesystem ist wundert es sich eben nicht, wenn die BOM im "Template" (das ist hier das Skript) steht.

          Also liegt es in der Verantwortung des Programmierers dafür zu sorgen, dass im Template nichts steht, was da nicht hinein gehört. Nur macht es eben der Windows-Notepad für viele Anwender recht schwierig - Microsoft wird (recht nassforsch) behaupten, die Mehrzahl der wolle das so auf die BOM zu verzichten, die wird gemäß den Voreinstellungen einfach ohne jede Interaktion eingefügt.

          Jörg Reinholz

  2. So, da haben sich auf Grund einer recht lockeren mittleren Maustaste (die zugleich Scrollrad ist) beim Einfügen ins Forum ein paar Fehler eingeschlichen, die dann auch nicht gleich offensichtlich wurden.

    Verbesserte Version

    <?php
    ## repariert PHP Skripte:
    ## entfernt BOMs
    ## entfernt Leerzeilen am Beginn
    ## entfernt Leerzeichen vor <? oder <?php
    ## ersetzt <? durch <?php
    
    # Konfiguration:
    
    $config['DIR'] = '/tmp';   # In welchem Verzeichnis sollen die Dateien repariert werden?
    #$config['DIR'] = '.';     # aktuelles Verzeichnis
    #$config['DIR'] = __DIR__; # Verzeichnis dieses Skriptes
    #$config['DIR'] = $SERVER['DOCUMENT_ROOT'];
    
    $config['IGNOREUPPERLOWER'] = false; # Für Linux-Benutzer...
    #$config['IGNOREUPPERLOWER'] = true; # Für Windows-Benutzer...
    
    # Diese Dateien werden gesucht und bearbeitet:
    $config['FIND'] = '*.php'; # * beliebiges Zeichen beliebig oft
                               # ? beliebiges Zeichen genau einmal
    
    $arExkludeList=array(
        # Diese Dateien aber nicht bearbeiten:
        # Nach letztem Eintrag kein Komma!
        'test/*', # Unterverzeichnis ausgehend von $config['DIR'], kein führender "/"
        '*.ex'    # Dateien mit Endung ".ex"
    );
    
    $habeBackup = false; # Setzen Sie dieses auf true, nachdem ein Backup gemacht wurde.
    
    ## Ab hier ändern nur Programmierer, die wissen, was sie tun:
    
    define ( 'DIR', rtrim($config['DIR']), '/' );
    define ( 'FIND', $config['FIND'] );
    define ( 'IGNOREUPPERLOWER', $config['IGNOREUPPERLOWER']);
    
    if ( isset($_SERVER['DOCUMENT_ROOT']) ) {
        define ('NL', "<br>\n");
    } else {
        define ('NL', "\n");
    }
    
    $fileList=getFileList(DIR);
    $arExkludeListRegex=array();
    
    foreach ($arExkludeList as $s) {
       if ($s) {
            $arExkludeListRegex[] = patternToRegex(DIR . '/' . $s, true, IGNOREUPPERLOWER);
       }
    }
    unset($arExkludeList);
    
    foreach ($fileList as $file) {
        $isExcluded=false;
        foreach ($arExkludeListRegex as $regex) {
            if ( ! $isExcluded && preg_match($regex, $file)  ) {
               $isExcluded=true;
               break;
            }
        }
    
        if ($isExcluded) {
            echo "Info: '$file' wurde ausgeschlossen.", NL;
        } elseif (! is_readable($file)) {
            echo "Warnung: Keine Leserechte an '$file'", NL;
        } elseif (! is_writable($file)) {
            echo "Warnung: Keine Schreibrechte an '$file'", NL;
        } elseif ($habeBackup) {
            echo "Info: Verarbeite '$file'";
            repair($file);
        } else {
            echo "Info: Würde '$file' verarbeiten, wenn das Anlegen eines Backups bestätigt wäre.", NL;
        }
    }
    
    echo "### Fertig! ###", NL;
    
    #### Von diesem Skript verwendete Funktionen ####
    # sollten unverändert bleiben.
    
    function patternToRegex($string, $allRow=false, $ignoreUpperLower=false) {
        if ($ignoreUpperLower) {
            $ignoreUpperLower='i';
        } else {
            $ignoreUpperLower='';
        }
    
        $string=str_replace('/', '\\/', $string);
    
        $search  = array( '.' , '*' , '?');
        $replacment = array( '\.', '.*', '.');
    
        if ($allRow) {
            return '/^' . str_replace($search, $replacment,  $string) . '$/' . $ignoreUpperLower;
        } else {
            return '/' . str_replace($search, $replacment,  $string) . '/' . $ignoreUpperLower;
        }
    }
    
    function getFileList($dir) {
        $list=array();
        $regex = patternToRegex(FIND, false, IGNOREUPPERLOWER);
        if ( is_dir($dir) && is_readable($dir) ) {
            $d = dir($dir);
            if ($d) {
                while (false !== ($entry = $d->read())) {
                    if ( '.' != $entry && '..' != $entry ) {
                        $entry = $dir .'/'. $entry;
                        if ( is_dir($entry) ) {
                            $arT = getFileList($entry);
                            foreach ($arT as $s) {
                                $list[]=$s;
                            }
                        } elseif ( preg_match($regex, $entry) ) {
                            $list[] = $entry;
        }   }   }   }   }
        return $list;
    }
    
    
    #### Arbeitende Funktionen ####
    # können für andere Verwendungen ersetzt werden
    
    function repair($file) {
        $newSkript = '';
        $isFirstLine = true;
    
        $ar=file($file);
    
        foreach ($ar as $row) {
            #$row = trim($row);
            $row = rmBOM($row);                      #löscht alle bekannten BOM
            if ( $isFirstLine ) {
                $row = rmOnlySpaces($row);           #löscht alle Spaces in Zeilen, die nur solche
                                                     #enthalten
            }
            if ( $row ) {
                $row = rmSpacesBevorPhpTag($row);    #löscht alle spaces vor <?php
                                                     #ersetzt <? und <?PHP durch <?php
                $newSkript .= $row;
                $isFirstLine = false;
            }
        }
        file_put_contents($file, $newSkript);
        echo " ... erledigt!", NL;
    }
    
    
    function rmBOM($str, $replacment = '') {
     # source: https://en.wikipedia.org/wiki/Byte_order_mark
    
        $boms=array(
            urldecode('%EF%BB%BF'),       #UTF-8
            urldecode('%FE%FF'),          #UTF-16 (BE)
            urldecode('%FF%FE'),          #UTF-16 (LE)
            urldecode('%00%00%FE%FF'),    #UTF-32 (BE)
            urldecode('%FF%FE%00%00'),    #UTF-32 (LE)
            urldecode('%2B%2F%76%38'),    #UTF-7
            urldecode('%2B%2F%76%39'),    #UTF-7
            urldecode('%2B%2F%76%2B'),    #UTF-7
            urldecode('%2B%2F%76%2F'),    #UTF-7
            urldecode('%2B%2F%76%38%2D'), #UTF-7
            urldecode('%F7%64%4C'),       #UTF-1
            urldecode('%DD%73%66%73'),    #UTF-EeBCDIC
            urldecode('%0E%FE%FF'),       #SCSU
            urldecode('%FB%EE%28'),       #BOCU-1
            urldecode('%84%31%95%33')     #GB-18030
        );
        return str_replace($boms, $replacment, $str);
    }
    
    function rmSpacesBevorPhpTag($str, $replacment = '') {
        $replacment .= '<?php';
        return preg_replace('/^\s*<\?([pP][hH][pP]){0,1}/', $replacment, $str);
    }
    
    function rmOnlySpaces($str, $replacment = '') {
        return preg_replace('/^\s+$/', $replacment, $str);
    }
    
    1. Hallo Jörg Reinholz,

      Das wär doch sicher auch was für das Wiki, oder was meinst du?

      http://wiki.selfhtml.org/wiki/PHP/Anwendung_und_Praxis/Skripte_reparieren

      Bis demnächst
      Matthias

      --
      Das Geheimnis des Könnens liegt im Wollen. (Giuseppe Mazzini)