Vorspiel

Ich habe in letzter Zeit auf Arbeit mit einigen Rechnern gekämpft, die die AD-Gruppenrichtlinien nicht mehr ausführen wollten, genau genommen gar keine AD-Verbindung mehr hatten. Die Anzahl der Geräte, die sich bei meinem Deployment-Manager gemeldet haben, sank immer weiter.
Mit den PCs schien ansonsten alles in Ordnung: Domänenmitgliedschaft, Internet, Intranet, Serververbindung mit Netzlaufwerken, alles da.
Wie kommt’s?

weiterlesen

Intro

Das Deployment von Adobe Reader DC im Windows Netzwerk via GPO Batch-Startscript ähnelt der Verteilung des alten Adobe Readers sehr – dennoch fasse ich die wichtigsten Steps hier zusammen.

Vorbereitung

Ihr braucht für die Verteilung immer die DC-Basisversion (als .msi) und das aktuellste, gewünschte Update (als .msp).
Außerdem, um die Reader Installation vor dem Deployment anzupassen, den installierten Reader DC Customization Wizard.

Step-by-Step

  1. Lokale AIP-Installation (z.B. C:\adc1516\) der Basisversion mit:
    msiexec /a AcroRdrDC1500720033_de_DE.msi

    Das Bild zeigt den Reader DC Installationsdialog, der durch die AIP gestartet wurde

  2. Aktuellsten Patch in die lokale AIP-Installation Slipstreamen (lokalen .msi Pfad beachten):
    msiexec /a "C:\adc1516\AcroRdrDC1500720033_de_DE.msi" /p AcroRdrDCUpd1501720053.msp

    Das Bild zeigt den CMD Befehl um das aktuelle Reader DC Update in die lokale AIP Installation zu slipstreamen

  3. Im lokalen AIP Ordner C:\adc1516 eine leere setup.ini Datei erzeugen und die AcroRdrDC1500720033_de_DE.msi in AcroRead.msi umbenennen (Letzteres ist nur eine kosmetische Anpassung).
  4. Mit dem Adobe Reader DC Customization Wizard die C:\adc1516\AcroRead.msi öffnen und beliebig anpassen. Änderungen wie vorgegeben als AcroRead.mst speichern.
    Das Bild zeigt den Adobe Reader DC Customization Wizard
  5. (Es müssten jetzt 3 Dateien und 4 Ordner in C:\adc1516\ existieren.) Den lokalen AIP-Ordner auf euer Deployment-Laufwerk verschieben.
    Das Bild zeigt die fertige Reader DC Deployment Installation im Deployment Share
  6. Deployen mit der .msi und .mst:
    start /w msiexec /i "%wd%\deploy\%version%\AcroRead.msi" /qn TRANSFORMS="%wd%\deploy\%version%\AcroRead.mst"

infos, infos, infos, via

Skript und Deploy

Das Deployment habe ich seit Reader 11 etwas verändert. Statt „complete-Ordner“ setze ich nun auf eine Versionsüberprüfung der Programm-exe. Das ist dynamischer und lässt sich differenzierter behandeln. Im Falle von Reader DC ist die Versionierung jedoch etwas seltsam. Beispielsweise lautet das aktuellste Update „AcroRdrDCUpd1501720053.msp“, nach der Installation wird die Dateiversion der .exe jedoch mit 15.17.20050 angegeben. Möglicherweise haben hier die Entwickler geschlampt.

Jedenfalls ist für die Verteilung mit Versionsüberprüfung noch ein Zwischenschritt notwendig:
Schaut im gerade erstellten lokalen AIP-Ordner, bzw. den schon aufs Deployment-Laufwerk kopierten Ordner die Eigenschaften Datei AcroRd32.exe an (liegt unter .\program files\Adobe\Acrobat Reader DC\Reader\) – im Tab Details steht die Versionsnummer, die ihr ins Skript und somit auch als Ordnernamen im Deployment-Laufwerk angeben müsst:
Das Bild zeigt die Dateieigenschaften von AcroRd32.exe und die Verweise auf das Skript und das Deploymentlaufwerk
Außerdem wird die VersionCompare.exe benötigt.

Hier nun das Script:
Code anzeigenDen Code könnt ihr bequem mit den Links/Rechts Pfeiltasten horizontal bewegen.

@echo off & Color 9f & setlocal
set wd=\\server\Deployment\Software\Reader
set log=%wd%\reader-log.txt
set readerEL=999
set retry=0
set forcecleanup=yes
set deactivateToolsSidebar=yes
REM:: ******************
set version=15.17.53
REM:: ******************

REM:: Clientfilter: nur die Computer aus der allowedPCs.txt dürfen installieren
::for /f %%f in (%wd%\allowedPCs.txt) do if "%computername%"=="%%f" goto start
::goto end

REM:: Clientfilter: die Computer aus der deniedPCs.txt dürfen nicht installieren
for /f %%f in (%wd%\deniedPCs.txt) do if "%computername%"=="%%f" goto end

:start
if not exist %wd%\deploy\%version%\complete md %wd%\deploy\%version%\complete
if not exist %wd%\deploy\%version%\complete\%computername% goto beforeinstall

if "%deactivateToolsSidebar%"=="yes" goto restoreToolbar

:beforeinstall
if "%forcecleanup%"=="yes" goto cleanup
goto install

:cleanup
echo %date% %time:~0,8% - %computername% deinstalliert alle Reader Versionen... >> %log%
REM:: uninstall all reader versions
taskkill /im acrord32.exe /t /f
taskkill /im acrord64.exe /t /f
start /w %wd%\cleaner\reader-cleaner-9.exe /silent /product=1
start /w %wd%\cleaner\reader-cleaner-10.exe /silent /product=1
start /w %wd%\cleaner\reader-cleaner-dc.exe /silent /product=1
echo %date% %time:~0,8% - %computername% hat alle Reader Versionen deinstalliert... >> %log%
goto install

:install
echo %date% %time:~0,8% - %computername% startet die Installation... >> %log%
start /w msiexec /i "%wd%\deploy\%version%\AcroRead.msi" /qn TRANSFORMS="%wd%\deploy\%version%\AcroRead.mst"
set readerEL=%errorlevel%

if %readerEL%==1618 goto retry REM:: msiexec process in use, installation already in progress
if %readerEL%==1602 goto retry REM:: user canceled installation
if %readerEL%==1603 goto retry REM:: fatal error, some use it for "already installed" (eg. java)
echo %date% %time:~0,8% - %computername% hat die Installation abgeschlossen, Errorlevel: %readerEL%... >> %log%
if %readerEL%==0 md %wd%\deploy\%version%\complete\%computername%
if "%deactivateToolsSidebar%"=="yes" goto restoreToolbar
goto end

:restoreToolbar
if exist "c:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\AcroApp\" set instpath=c:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\AcroApp
move "%instpath%\ENU\disabled\AppCenter_R.aapp" "%instpath%\ENU"
move "%instpath%\ENU\disabled\Home.aapp" "%instpath%\ENU"
move "%instpath%\ENU\disabled\Viewer.aapp" "%instpath%\ENU"
move "%instpath%\DEU\disabled\AppCenter_R.aapp" "%instpath%\DEU"
move "%instpath%\DEU\disabled\Home.aapp" "%instpath%\DEU"
move "%instpath%\DEU\disabled\Viewer.aapp" "%instpath%\DEU"
rd "%instpath%\ENU\disabled"
rd "%instpath%\DEU\disabled"
echo %date% %time:~0,8% - %computername% hat die Installation optimiert >> %log%
goto end

:retry
if %retry%==1 goto retryfailed
echo %date% %time:~0,8% - %computername% hat die Installation abgeschlossen, Errorlevel: %readerEL%, retry in 5min... >> %log%
set retry=1
ping localhost -n 300 > nul
goto cleanup

:retryfailed
echo %date% %time:~0,8% - %computername% hat die Installation abgeschlossen, Errorlevel: %readerEL%, retry failed, end >> %log%
goto end

:end
endlocal
exit

Alternatives Deployment

Auf der Seite 404techsupport.com habe ich einen noch einfacheren Deployment-Guide gesehen. Dort wird nur anhand des aktuellsten Installers gearbeitet – leider hatte ich noch nicht die Zeit dieses Deployment zu testen. Das hole ich sicher noch nach und Update dann hier.
Einzige Einschränkung: Das Deployment funktioniert nur über .exe, ist also nicht für das normale GPO Softwaredeployment via .msi geeignet.
Hier die Zusammenfassung, die der Autor mir per Mail zukommen ließ:

I download the latest executable from Adobe’s FTP site for Adobe Reader DC and deploy that with a script.
ftp://ftp.adobe.com/pub/adobe/reader/win/AcrobatDC/1501620039/

I created a transform using the Adobe Customization Wizard and the .msi version of the installer.
My script then installs the executable and uses the transform as a parameter.

\\server.fqdn\share\AdobeReader\LatestReaderDC.exe /sPB /rs /msi TRANSFORMS=\\server.fqdn\share\AdobeReader\ReaderDC.mst

(The script runs at startup assigned by Group Policy. It checks a network location for a „receipt“ for that computer to see if it has run before, and if not, executes the above command. I empty the „receipts“ folder after I download the new installer to the share so that the script executes on their next startup.)

If that works for your deployment options, it’s pretty straight-forward and avoids the annoying .msp patching (security patch or quarterly patch?). If you need an .msi to deploy, you are probably stuck with your steps of creating the admin install.

Kurz notiert:
Dank PowerShell lassen sich bei Microsoft Exchange – in meinem Fall Exchange Online, also ein hosted Exchange – Änderungen gleich für mehrere oder sogar alle Nutzer ausführen. Vor allem bei Änderungen, die normalerweise nicht über die Adminoberfläche administrierbar sind, sondern über den Nutzer direkt eingestellt werden müssen, lohnt sich das enorm.

Verbindung zu Exchange Online in PowerShell herstellen

In PowerShell folgende Befehle nacheinander eingeben:

$UserCredential = Get-Credential

An dieser Stelle dann die Mail-Credentials eines Exchange Admins eingeben.

$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $UserCredential -Authentication Basic -AllowRedirection

Speichert die Verbindung zu Exchange Online mit den Credentials in ein Objekt

Import-PSSession $Session

Lädt das Session Objekt

Anschließend könnt ihr auf dem Exchange Befehle ausführen, beispielsweise

Get-Mailbox

:
microsoft-exchange-online-changes-to-multiple-or-all-users-get-mailbox

In den folgenden Beispielen soll die Abwesenheitsmeldung bzw. Automatische Antwort eingestellt werden. Diese Einstellung eines Nutzers lässt sich mit folgenden Befehl abrufen:

Get-MailboxAutoReplyConfiguration -Identity hs@barketing.de

Änderungen für einzelne Benutzer

Aktivieren ohne zeitliche Einschränkung:

Set-MailboxAutoReplyConfiguration -Identity hs@barketing.de -AutoReplyState Enabled -ExternalAudience All -ExternalMessage "Guten Tag<br>Bla bla bla, Urlaub bla.<br>Mit freundlichen Grüßen"

Aktivieren mit Start- und Endzeitpunkt:

Set-MailboxAutoReplyConfiguration -Identity hs@barketing -AutoReplyState Scheduled -StartTime "11/11/2013 00:00" -EndTime "11/13/2013 18:00" -ExternalAudience All -InternalMessage "Guten Tag<br>Bla bla bla, Urlaub bla.<br>Mit freundlichen Grüßen"

(via)

microsoft-exchange-online-changes-to-multiple-or-all-users-set-mailboxautoreply

Änderungen für mehrere/alle Benutzer

Anhand des Pipe-Operators

|

können wieder Ausgaben eines Befehls an den nächsten Befehl zur Weiterverarbeitung übergeben werden.
Alle Benutzer:

Get-Mailbox | Set-MailboxAutoReplyConfiguration -AutoReplyState Enabled -ExternalAudience All -ExternalMessage "Guten Tag<br>Bla bla bla, Urlaub bla.<br>Mit freundlichen Grüßen"

Über

Get-Mailbox

werden alle Mailkonten geladen und an den

Set

-Befehl übergeben, der dadurch keinen

Identity

-Parameter mehr braucht.
Hinweis: Die Massenverarbeitung dauert natürlich entsprechend lange – für die 30 Nutzer bei uns hat der Befehl 4 Minuten gebraucht. Also nicht ungeduldig werden.

Mit Benutzervorauswahl:

Get-User | where {$_.Department -eq "Sales"} | Get-Mailbox | Set-MailboxAutoReplyConfiguration -AutoReplyState Enabled -ExternalAudience All -ExternalMessage [...]

Somit werden Nutzer erst durch den

where

-Befehl gefiltert, deren Postfächer geladen und weitergegeben. (via)

Typisches Problem – es funktioniert nicht

Wichtig:
Damit AutoReply-Regeln tatsächlich auch funktionieren, müssen in den Mailkonten auch wirklich E-Mails eingehen.
Bei Konten, die ihre E-Mails nur via SMTP weiterleiten und keine lokale Kopie der Mails in ihrem Postfach empfangen, funktioniert das Auto-Reply deswegen nicht.
Neben den AutoReply-Einstellungen muss demnach auch die Einstellung, dass beim Weiterleiten der Mails eine lokale Kopie behalten werden soll, gesetzt werden.
microsoft-exchange-online-changes-to-multiple-or-all-users-check-forwarding-settings

Einen Überblick über die Weiterleitungseinstellungen aller Nutzer bekommt ihr mit diesem Befehl:

Get-Mailbox | FL DeliverToMailboxAndForward, ForwardingAddress, ForwardingSmtpAddress

Mit diesem schnellen Überblick könnt ihr euch entweder selbst die Nutzer raussuchen, die eine Weiterleitung eingerichtet haben jedoch keine lokalen Kopien in ihr Postfach kriegen (und somit auch keine Automatische Antwort abschicken).
Oder ihr nutzt einfach folgenden Befehl. Dieser aktiviert diese Einstellung der lokalen Kopie für alle Benutzer, die eine Weiterleitung (intern sowie extern) eingerichtet haben:

Get-Mailbox | Where {$_.ForwardingSmtpAddress -ne $Null -OR $_.ForwardingAddress -ne $Null} | Set-Mailbox -DeliverToMailboxAndForward $True

(via)

Fehler

Folgender Fehler kann bei WordPress auftreten, vermutlich nur bei bestimmten Hostern, nachdem es installiert oder hosterübergreifend umgezogen wurde:
Das Bild zeigt den WordPress Dialog, bei dem FTP Verbindungsinformationen benötigt werden sowie den Fehler, dass diese nicht korrekt seien - egal, welche Daten man eingibt. Das liegt an fehlerhaften Besitzrechten nach der WordPress Installation auf einigen Hostern wie beispielsweise auch All-Inkl und Hosteurope.
Der Fehler erscheint vermutlich bei der Installation oder Aktualisierung von Plugins oder Themes.

Erklärung

Das Bild zeigt den WordPress Dialog, bei dem FTP Verbindungsinformationen benötigt werden. Das Phänomen tritt auf, wenn ihr WordPress von Hand auf euren FTP hochgeladen und danach die Installation ausgeführt habt. Dadurch ist das WordPress Core mit den Besitzerrechten eures FTP Nutzers, mit dem ihr die WordPress Dateien hochgeladen habt, versehen. Alle Tätigkeiten aus dem WordPress System heraus, also beispielweise das Installieren von Plugins über die WordPress „Plugins“ Oberfläche, werden jedoch server-intern von einem anderen Nutzer ausgeführt. Dieser Nutzer heißt je nach System oftmals PHP-User, www-User, www-data oder ähnlich. Nun fehlen dem Nutzer aber die Zugriffsrechte auf die entsprechenden Ordner.

Lösungen

Es gibt mehrere mögliche Lösungen:

1)
Gebt bei der WordPress Nachfrage die FTP-Nutzerdaten ein, mit denen ihr WordPress hochgeladen habt. Den Nutzer könnt ihr, falls ihr es nicht mehr genau wisst, vielleicht mit dem WebFTP-Tool eures Hosters auslesen:
Das Bild zeigt die Oberfläche eines WebFTP Programms mit markierten Besitzerinformationen eines Ordners
Im besten Falle führt WordPress danach alle Plugin Aktionen ohne Klagen aus und ihr hört nie wieder von dem Problem. Aber dann wärt ihr wahrscheinlich auch gar nicht hier gelandet.

2)
Ändert die Besitzrechte von wp-content. Nutzt dafür vom Hoster zur Verfügung gestellte Möglichkeiten der Besitzrechteveränderung (Adminbackend -> Tools).
Zuerst wird der Ordner wp-content auf den PHP-User gestellt, die benötigte WordPress Funktion getestet (z.B. Plugin-Update, sollte jetzt gehen) und dann stellt ihr den Besitzer wieder zurück auf den FTP-User. Bei mir hat das die Probleme dauerhaft behoben und die Besitzrechte sind einheitlich weiterhin auf dem FTP-Nutzer.

3)
Mein zweiter Tipp ist eigentlich der langfristig sicherste: installiert WordPress nicht von Hand sondern über die 1-Click-Install-Methode eures Hosters. Praktisch jeder Webhoster bietet die Installation von bekannter Software auf seinem System an, WordPress ebenfalls. Dabei richtet der Hoster ein fertig installiertes und eingerichtetes System für euch ein, meistens mit nur wenigen Klicks. Dieses System hat definitiv keine Rechteprobleme, ist nun aber frisch installiert. Je nachdem, wie jung oder alt euer Blog ist, kostet es nun weniger oder mehr Aufwand, die Inhalte in das neu installierte System zu transferieren. Aber diese Lösung ist am sichersten und wird keine zukünftigen Probleme mehr machen, ich empfehle das also.
Export/Import: Der Export besteht im Grundlegenden aus 2 Typen: Daten (Beiträge, Kommentare usw), Plugins und Themes.
Daten: Benutzt entweder die WordPress eigene Export-Funktion unter „Werkzeuge -> Daten exportieren“ oder exportiert eure komplette WordPress Datenbank (SQL Export via phpMyAdmin). Diesen Export dann im neuen WordPress bzw. dessen Datenbank.
Plugins/Themes: Entweder ihr installiert im neuen WordPress die Komponenten von Hand (je nachdem wieviele es sind), oder ihr zieht euch direkt vom FTP die Ordner „plugins“ und „themes“ und überschreibt einfach diese Ordner in eurem neuen WordPress System.
Anschließend müsste der neue WordPress Blog die Plugins, Themes (die beide ggf. noch aktiviert werden müssen) und Daten enthalten.
Ich werde aber demnächst nochmal einen ausführlichen Beitrag über gute WordPress Backup Methoden schreiben und eine komplette Backup Lösung konzipieren.

4)
Tragt in eure .htaccess Datei in der Root eures WordPress Blogs (also direkt im obersten Ordner, beispielsweise „/blog/“) folgende Zeile ein:

AddHandler php5-cgi .php

Hintergrund: Link

Neu in meiner Software-Deployment Auflistung ist ab jetzt KeePass 2, eine ziemliche gute und erweiterbare Passwortmanagement-Freeware.
Zu KeePass selbst werde ich in Zukunft noch einige Artikel schreiben, von der Installation bis zur Integration in Browser und Smartphone. Jetzt erstmal schnell das Deployment im Windows Active Directory via Batch Script, im üblichen Stil.

Vorbereitung

Das Bild zeigt die vielen Deploymentparameter und -optionen des KeePass 2 InstallersDer .exe Installer von der Homepage (Pro Version) liefert beim Aufruf mit dem Parameter „/?“ eine lange Liste von Optionen zur Anpassung der Installation. Diese findet ihr hier auch nochmal zur besseren Lesbarkeit in HTML Form.
Praktisch: Ihr könnt mit allen Parametern die Installation anpassen und bei der ersten Installation mit dem Parameter /SAVEINF=filename das Set an Optionen in eine Konfigurationsdatei speichern. Für die nächste Installation reicht als Parameter dann nur noch /LOADINF. So lassen sich beispielsweise unterschiedliche Deploymentprofile erstellen und nutzen.

Plugins

Eine der Stärken von KeePass ist die starke Erweiterbarkeit mit Hilfe der vielen Plugins und Tools. Die Installation und Verteilung ist bei fast allen relativ einfach: es reicht schon die entsprechende Plugin-Datei (z.B. .plgx) in das Programmverzeichnis zu legen, ggf. auch in einen seperaten „plugins“ Unterordner für eine verbesserte Übersicht. Beim nächsten Start von KeePass wird das Plugin automatisch erkannt und geladen.
Aus diesem Grund kopiert das Deployment-Script benötigte Plugins einfach nur in den Installationsordner – fertig!

Deployment

Aktuell: Version 2.34 via .exe
Das im Skript verwendete Programm VersionCompare ist eine Eigenprogrammierung und hier als Download verfügbar.

Code anzeigenDen Code könnt ihr bequem mit den Links/Rechts Pfeiltasten horizontal bewegen.

@echo off && color 9f && setlocal

set wd=\\lea\Deployment\Software\KeePass
set log=%wd%\keepass.log
set tools=\\lea\Deployment\Sonstiges\tools
set instversion=0.0
set versionEL=9
set regEL=999
set exepath=none
set retry=0

REM:: ######## EDIT THIS ####
set newversion=2.34
REM:: #######################

REM:: Clientfilter: nur die Computer aus der allowedPCs.txt dürfen installieren
REM::for /f %%f in (%wd%\allowedPCs.txt) do if "%computername%"=="%%f" goto check
REM::goto end

REM:: Clientfilter: die Computer aus der deniedPCs.txt dürfen nicht installieren
for /f %%f in (%wd%\deniedPCs.txt) do if "%computername%"=="%%f" goto end

:check
if exist "c:\Program Files (x86)\KeePass2\KeePass.exe" set exepath="c:\Program Files (x86)\KeePass2\KeePass.exe"
if exist "c:\Program Files\KeePass2\KeePass.exe" set exepath="c:\Program Files\KeePass2\KeePass.exe"
if %exepath%==none goto install
goto checkversion

:checkversion
for /f "tokens=1-3" %%i in ('%tools%\sigcheck %exepath%') do ( if "%%i %%j"=="File version:" set instversion=%%k )
%tools%\VersionCompare.exe %instversion% %newversion%
set versionEL=%errorlevel%
if "%versionEL%"=="-1" goto install
if "%versionEL%"=="0" echo %date% %time:~0,8% - %computername% hat bereits %instversion% installiert >> %log% & goto plugins
if "%versionEL%"=="1" echo %date% %time:~0,8% - %computername% hat bereits %instversion% (neuer) installiert >> %log% & goto plugins

:install
echo %date% %time:~0,8% - %computername% installiert... >> %log%
TASKKILL /f /im KeePass.exe
start /w %wd%\KeePass-%newversion%.exe /VERYSILENT /SP- /SUPPRESSMSGBOXES /LOG="logs/%computername%.log" /NORESTART /CLOSEAPPLICATIONS /SAVEINF="standardsetup.inf" /DIR="%ProgramFiles%\KeePass2" /GROUP="KeePass 2"
set kpel=%errorlevel%
if %kpel%==1618 goto retry REM:: msiexec process in use, installation already in progress (eg. windows updates running)
if %kpel%==1602 goto retry REM:: user canceled installation (eg. taskkill)
if %kpel%==1603 goto retry REM:: fatal error, some use it for "already installed" (eg. java)
if %kpel%==1638 goto retry REM:: another product is already installed, denies an update
echo %date% %time:~0,8% - %computername% hat Version %newversion% mit EL %kpel% abgeschlossen >> %log%
goto plugins

:plugins
REM:: copy plugins
if exist "c:\Program Files (x86)\KeePass2\KeePass.exe" xcopy /Y /S /I "%wd%\plugins" "c:\Program Files (x86)\KeePass2\plugins"
if exist "c:\Program Files\KeePass2\KeePass.exe" xcopy /Y /S /I "%wd%\plugins" "c:\Program Files\KeePass2\plugins"
goto end

:retry
if %retry%==1 goto retryfailed
echo %date% %time:~0,8% - %computername% hatte den Fehler %kpel%, retry in 500Sek... >> %log%
set retry=1
REM:: 5 Minuten warten
ping localhost -n 500 > nul
goto install

:retryfailed
echo _!_ %date% %time:~0,8% - %computername% hat die Installation abgebrochen, RETRY FAILED! >> %log%
goto end

:end
endlocal
exit

Dieser Beitrag ist eine Ergänzung bzw. Erweiterung des vorherigen Posts „FTP-Backup-Lösung mit PHP“. Die große Neuerung der Version 1.2 ist die Funktion MySQL Datenbanken sichern zu können. Auch hier wird ein Backup erstellt, überschüssige Backups (wenn mehr vorhanden sind als der gewünschte Maximalwert) werden gelöscht und neue Backups ggf. zu einem externen Server übertragen. Mit Version 1.2.1 gibt es zusätzlich die Möglichkeit, ALLE Ordner der Root-Ebene, mit Ausnahmen, zu sichern und Version 1.2.2 ermöglicht detailliertere Ausnahmen.

Features

Diese Lösung (v1.2.2) bietet nun folgenden Funktionsumfang:

  • beliebig viele Ordner des All-Inkl Accounts in einzelne .tar.gz Archive sichern
  • oder: alle Ordner der Root-Ebene, mit möglichen Ausnahmen, sichern
  • Detailliertere Ausnahmen mit Datei- und Ordnermasken wie z.B. „*.tar.gz“
  • Einschränkung der Anzahl aufgehobener Backups – älteste Backups werden automatisch gelöscht
  • detaillierte Ausgabe inklusive benötigter Zeit
  • E-Mail Benachrichtigung
  • Farbliche Hervorhebung
  • Verbesserungen des Backup Prozesses, zusätzliche Überprüfungen und Debug Infos bei Fehlern
  • Verbinden eines externen FTP Server und Kopieren aller neuen Backups
  • Angabe eines beliebigen FTP Ports
  • Verbindung über FTPs (SSL FTP) Port 21 wird verwendet, unsicheres FTP nur noch als Fallback
  • detailliertere Informationen über die Backups in der Benachrichtigungsmail
  • Backup von beliebig vielen MySQL Datenbanken von localhost, Aufräumen der Backups und Export an externen Server
  • E-Mail Anpassungen über Parameter möglich – Betreff, Anmerkungen, Details
  • ausführliche Ausgabe aller Backups im Skript und per Mail

Zwischen den Zeilen 37 und 82 findet ihr alle Variablen, die ihr anpassen müsst/könnt.

Zur Datenbanksicherung ist zu sagen, dass diese auf den Hoster All-Inkl optimiert ist. Sie sichert nur Datenbanken von localhost und benötigt den PHP Befehl „exec()“ sowie die Komponenten „mysqldump“ und „gzip“, die auf All-Inkl Servern erlaubt bzw. installiert sind. Auf anderen Hostern müssen daher ggf. diese Möglichkeiten geschaffen oder die MySQL Sicherung (Zeile 190-191) verändert werden.

Screenshot

Das Bild zeigt die Ausgaben des Backup Skripts und die versendete E-Mail Benachrichtigung

Code

Schaut für Code-Alternativen oder ein weniger komplexes System auch auf die Version 1.1 und 1.0

Update 08.2017: Version 1.2.2 nur noch als Download
Code/Download der backup.phpx

Sicherheit: Absicherung mit .htpasswd

Das Verzeichnis, in dem die backup.php und die Backups liegen, sollte natürlich mit einer .htpasswd abgesichert werden. Mit einer eingerichteten .htpasswd Datei ist zuerst ein Login nötig, eh man auf bestimmte Bereiche des Webspaces zugreifen darf:
Das Bild zeigt eine Login Datenabfrage beim Aufruf der Backup URL
Die Datei .htpasswd enthält hierbei die Login Daten und in der .htaccess des Backup Unterordners wird festgelegt, dass eine .htpasswd diesen Ordner schützt. Die .htpasswd generiert ihr euch am besten mit diesem Generator und baut sie dann folgendermaßen in die .htaccess dieses Ordners ein:

AuthType Basic
AuthName "Backups"
AuthUserFile /www/htdocs/all-inkl-account/backup/.htpasswd
Require valid-user

Sicherheit: Absicherung mit .htaccess

Da wir schonmal bei .htaccess sind, erhöhen wir die Sicherheit mit ein paar weiteren grundlegenden Zeilen:

#block access to certain file types
<FilesMatch ".(htaccess|htpasswd|ini|phps|log|sh|tar.gz)$">
 Order Allow,Deny
 Deny from all
</FilesMatch>

# disable directory browsing
Options All -Indexes

# prevent basic url hacking stuff
# from: http://www.queness.com/post/5421/17-useful-htaccess-tricks-and-tips
RewriteEngine On
# proc/self/environ? no way!
RewriteCond %{QUERY_STRING} proc/self/environ [OR]
# Block out any script trying to set a mosConfig value through the URL
RewriteCond %{QUERY_STRING} mosConfig_[a-zA-Z_]{1,21}(=|\%3D) [OR]
# Block out any script trying to base64_encode crap to send via URL
RewriteCond %{QUERY_STRING} base64_encode.*(.*) [OR]
# Block out any script that includes a <script> tag in URL
RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR]
# Block out any script trying to set a PHP GLOBALS variable via URL
RewriteCond %{QUERY_STRING} GLOBALS(=|[|\%[0-9A-Z]{0,2}) [OR]
# Block out any script trying to modify a _REQUEST variable via URL
RewriteCond %{QUERY_STRING} _REQUEST(=|[|\%[0-9A-Z]{0,2})
# Send all blocked request to homepage with 403 Forbidden error!
RewriteRule ^(.*)$ /index.htm [F,L]

ErrorDocument 401 /backup/index.htm
ErrorDocument 403 /backup/index.htm
ErrorDocument 404 /backup/index.htm
ErrorDocument 500 /backup/index.htm

Dadurch werden Zugriffe auf bestimmte Dateitypen (auch die Backup Dateien), Verzeichnisse und Zugriffe mit sicherheitskritischen Merkmalen unterbunden. Alle diese nicht validen Zugriffe bekommen die index.htm serviert, welches einfach nur eine leere HTML Datei ist. Somit wird den Abfragenden auch kein detaillierter Grund gegeben, warum der Zugriff fehlschlug.

Automatisierung mit All-Inkl Cronjobs

Zu guter Letzt hilft diese Sicherungslösung natürlich nur, wenn sie automatisiert wird. Auch dies ist stark abhängig von eurem Hoster, System, dem Anwendungsbereich usw.
Im Falle von All-Inkl als Webhoster, könnt ihr die Cronjob Funktionalität im KAS (KAS -> Tools -> Cronjobs) benutzen:
Das Bild zeigt die Cronjob Einrichtungsoberfläche von All-Inkl

Dieser Beitrag ist eine Ergänzung bzw. Erweiterung des vorherigen Posts „FTP Backup Skript in PHP“.

Folgende Änderungen werde ich hier besprechen:

  • Erweiterung des Skripts
  • Sicherheit: Absicherung mit .htpasswd
  • Sicherheit: Absicherung mit .htaccess
  • Automatisierung mit All-Inkl Cronjobs

Erweiterung des Backup Skripts

Das neue Skript bietet nun neue Funktionalitäten:

  • Farbliche Hervorhebung
  • Verbesserungen des Backup Prozesses, zusätzliche Überprüfungen und Debug Infos bei Fehlern
  • Verbinden eines externen FTP Server und Kopieren aller neuen Backups
  • Angabe eines beliebigen Ports
  • Verbindung über FTPs (SSL FTP) Port 21 wird verwendet, unsicheres FTP nur noch als Fallback
  • detailliertere Informationen über die Backups in der Benachrichtigungsmail

Nun werden also von beliebig vielen Ordner des FTP-Root Backups erstellt, gegebenenfalls aufgeräumt wenn mehr Backups existieren als aufgehoben werden sollen und anschließend alle neuen Backups auf einen externen FTP Server kopiert. Für den Upload wird der passive FTP Modus verwendet, da dieser in den seltensten Fällen Probleme macht. Sollte der Wechsel zum passiven FTP fehlschlagen, wird dennoch aktives FTP probiert.
Alle nötigen Informationen werden in den Variablen in Zeile 16 bis 25 angegeben. Format und Hilfe steht jeweils dabei, eigentlich sollte da alles klar sein.

Die Erweiterungen machen aus dem Skript eine beispielhafte Backup-Lösung für FTP Inhalte. Auch hier am Beispiel von All-Inkl als Hoster. Wer die Lösung unabhängig von All-Inkl einsetzen möchte, wird das Archive_Tar PHP Modul und irgendeine Art von Cronjob-Funktionalität brauchen.

Screenshot

php-ftp-backup-neu

Code

Achtung: Update (09.06.2015): Dieses Skript ist Version 1.1, basierend auf dem grundlegenden Backup Skript aus diesem Artikel. In Version 1.2 (diesen Artikel) lassen sich nun auch MySQL Datenbanken mitsichern.

Update (02.06.2015): Anpassung der Variablennamen zur besseren Lesbarkeit, Fehlerkorrekturen, verbesserte FTP-URI-Verarbeitung, Portangabe möglich, Verbindung über FTPs (FTP über SSL über Port 21) möglich (mit Fallback zu unsicherem FTP wenn FTPs nicht funktioniert), erweiterte Informationen über die erfolgten Backups in der Benachrichtigungsmail, kleine Optimierungen
Code anzeigenDen Code könnt ihr bequem mit den Links/Rechts Pfeiltasten horizontal bewegen.

<?
// PHP-Konfiguration optimieren
// if no errors are shown, please check htaccess restrictions by "php_flag display_errors off"
// in this or parent folders
@error_reporting(E_ALL);
@ini_set("max_execution_time", 300);
@ini_set("memory_limit", "256M");
header('Content-Type: text/html; charset=utf-8');
include "Archive/Tar.php";
$pfad = preg_replace('/(\/www\/htdocs\/\w+\/).*/', '$1', realpath(__FILE__));
$alltime = 0;
$newbackups = array();
$backupinfo = array();
echo "<style>.ok{color:#478F47;}.err{color:#DA3833;}.grey{color:grey;}.warn{color:#f0ad4e;}th,td{border-bottom: 1px solid #aaa;}</style>";

// ########## EDIT THIS VARIABLES ###################
$foldertobackup = array("tools", "reports"); // which root folders should get backed up?
$copytoexternalftp = 1; // copy new backup files to external ftp server? should be 1/"yes"/"ja" or 0/"no"/"nein"
// external (ftp) servers to copy new backups to, format:
// in general: ftp://username:password@url:port/path (port is required!)
// ftp://user:pw@ftp.server.com:21/
// ftp://user:pw@serverurl.com:21/optional/path
// secure sftp connection (port 22) in preparation but not ready yet
$externalftpuri = "ftp://admin:password@firma.dns.com:21/Data/FTP-Backups";
$backupfilemaximum = 2; // how many archives should be stored?
$dir = $pfad."backup/"; // in which subfolder is this backup php file? this would be: "root/backup/"
$sendmail = 1; // send notification mail when all backups are done - should be 1/"yes"/"ja" or 0/"no"/"nein"
$sendmailto = "adminmail@firma.de"; // valid mail address to send the mail to
// ##################################################

echo "<span class='ok'>".date("d.m.Y G:i:s")."</span><br>";

foreach ($foldertobackup as $verzeichnis) {
   $jobtime = time();
   echo "<br><br>########################################<br>";
   echo "<strong>Verzeichnis $verzeichnis wird gesichert...</strong><br>";
   flush();

   // check if folder exists
   if(!file_exists($pfad.$verzeichnis)) {
      echo "<span class='err'>Sicherung fehlgeschlagen. Zu sichernder Ordner $pfad$verzeichnis existiert nicht.</span>";
      continue;
   }

   // Name: [verzeichnis]_[Datum]_[Uhrzeit].tar.gz
   $archivname = $verzeichnis.date('_Y-m-d_His').".tar.gz";
   // Name: [All-Inkl-Accountname]_[Datum]_[Uhrzeit].tar.gz
   //$archivname = preg_replace('/.+\/(.+)\/$/', '$1', $pfad).date('_Y-m-d_His').".tar.gz";

   // Auszuschließende Ressourcen
   $ignorieren = array("*.sql.gz", "*.tar.gz", "usage", "logs");

   // ######### create backup
   $archiv = new Archive_Tar($archivname, true);
   $archiv->setIgnoreList($ignorieren);
   $archiv->createModify($pfad.$verzeichnis, "", $pfad);
   $backuptime = time() - $jobtime;
   $archivsize = round(filesize($dir.$archivname)/1000000);
   if (!is_numeric($archivsize)) {
      $archivsize = "filesize error";
   }
   if (is_int($backuptime)) {
      echo "<span class='ok'>Backup fertig: $archivname (Größe: $archivsize MB, Dauer: $backuptime Sekunden)</span><br>";
   } else {
      echo "<span class='ok'>Backup fertig: $archivname</span><br>";
   }

   // check created archive
   if (!is_object($archiv)==1 || !is_numeric($archivsize) || !$archivsize>50) {
      // abort process due to wrong type or too small filesize (which likely is an error)
      echo "<span class='err'>Sicherung fehlgeschlagen. Erstelltes Archiv ist fehlerhaft.</span><br>Mehr Infos <a href='http://hannes-schurig.de/09/05/2015/ftp-backup-skript-in-php/' target='blank'>hier</a><br>";
      // debug
      echo "<p class='grey'>Debug:<br>";
      echo "Pfad: $dir.$archivname<br>";
      echo "Typ: ".gettype($archiv)."<br>";
      echo "Größe: $archivsize MB</p>";
      continue;
   }
   $newbackups[$archivname] = $dir.$archivname;

   echo "Aufräumen der Backups...<br>";
   flush();
   // integer starts at 0 before counting
   $i = 0;
   $backupfiles = array();
   // ######### collect valid backup files
   if ($handle = opendir($dir)) {
      while (($file = readdir($handle)) !== false) {
         if (  is_int(strpos($file, $verzeichnis)) == true &&
                  preg_match('/\.tar.gz$/i', $file) &&
                  !in_array($file, array('.', '..')) &&
                  !is_dir($dir.$file)
         ) {
            $backupfiles[$dir.$file] = filectime($dir.$file);
         }
      }
   }
   echo count($backupfiles)." valide Backups dieses Ordners gefunden, ";
   echo "$backupfilemaximum Backups sollen behalten werden. ";
   $backupcountdif = count($backupfiles)-$backupfilemaximum;
   if ($backupcountdif<=0) {
      echo "Kein Backup wird gelöscht.<br>";
   } else if ($backupcountdif==1) {
      echo "1 Backup wird gelöscht:<br>";
   } else if ($backupcountdif>=2) {
      echo "$backupcountdif Backups werden gelöscht:<br>";
   }
   flush();

   // ######### sort and delete oldest backups
   // sort backup files by date
   arsort($backupfiles);
   // reset counter variable
   $i = 0;
   // delete oldest files
   foreach ($backupfiles as $filepath => $value) {
      if($i>=$backupfilemaximum) {
         echo "$filepath wird gelöscht...<br>";
         if (unlink($filepath)) {
            echo "<span class='ok'>Datei erfolgreich gelöscht.</span><br>";
         } else {
            echo "<span class='err'>Fehler beim Löschen der Datei.</span><br>";
         }
      }
      $i++;
   }
   $jobendtime = time() - $jobtime;
   array_push($backupinfo, array($verzeichnis, $archivname, $archivsize." MB", $jobendtime." Sekunden", date("d.m.Y G:i:s")));
   echo "<span class='ok'>Backup für Verzeichnis $verzeichnis abgeschlossen.</span><br>";
   if (is_int($jobendtime)) {
      echo "######################################## (Dauer: $jobendtime Sekunden)<br>";
      $alltime += $jobendtime;
   } else {
      echo "########################################<br>";
   }
}

if(count($newbackups)>0) {
   echo "<br><span class='ok'>Die automatische Sicherung des Skripts '".pathinfo(__FILE__, PATHINFO_BASENAME)."' hat ".count($newbackups)." Verzeichnisse in insgesamt $alltime Sekunden gesichert.</span><br><br>";
} else {
   echo "<br><span class='err'>Es scheint leider so als wenn keine Backups erfolgreich erstellt wurden.</span><br><br>";
}
flush();

// ######### copy backups to external storages
if (!isset($copytoexternalftp) || $copytoexternalftp== 0 && in_array($copytoexternalftp, array("no", "nein"))) {
   echo "<span class='warn'>Backups werden nicht auf einen externen FTP kopiert. Option ist deaktiviert.</span><br>";
} else {
   $ftptime = time();
   echo "########################################<br>";
   echo "<strong>".count($newbackups)." Backups werden auf externen FTP kopiert...</strong><br>";
   flush();
   $ftpcon = getFtpConnectionByURI($externalftpuri);
   flush();
   if(gettype($ftpcon)=="resource") {
      foreach ($newbackups as $filename => $fullpath) {
         $uploadtime = time();
         echo "Kopiere .$filename. (Größe: $archivsize MB) auf den FTP...<br>";
         flush();
         if (ftp_put($ftpcon, $filename, $filename, FTP_ASCII)) {
            $uploadendtime = time() - $uploadtime;
            if (is_int($uploadendtime)) {
               echo "<span class='ok'>Backup erfolgreich kopiert. (Dauer: $uploadendtime Sekunden)</span><br>";
            } else {
               echo "<span class='ok'>Backup erfolgreich kopiert.</span><br>";
            }
         } else {
            echo "<span class='err'>Fehler beim Kopieren des Backups.</span><br>";
         }
         flush();
      }
   }
   $ftpendtime = time() - $ftptime;
   if (is_int($ftptime)) {
      echo "######################################## (Dauer: $ftpendtime Sekunden)<br>";
   } else {
      echo "########################################<br>";
   }
}
flush();

// ######### send mail
if (!isset($sendmail) || $sendmail== 0 && in_array($sendmail, array("no", "nein"))) {
   echo "<br><span class='warn'>Benachrichtigungsmail wurde nicht verschickt. Option ist deaktiviert.</span><br>";
} else {
   if(!preg_match( '/^([a-zA-Z0-9])+([.a-zA-Z0-9_-])*@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-]+)+/' , $sendmailto)) {
      echo "<br><span class='err'>FEHLER: Mail konnte nicht versendet werden, da die Adresse ungültig ist!</span><br>";
   } else {
      $mailsubject = "Automatische FTP Sicherung abgeschlossen";
      $mailtext = "Die automatische Sicherung des FTP-PHP-Backup-Skripts ".pathinfo(__FILE__, PATHINFO_BASENAME)." hat ".count($foldertobackup)." Verzeichnisse in insgesamt $alltime Sekunden gesichert.<br><br>";
      
      // add backup informations as table to mailtext
      $mailtext .= "<table border=0 style='border-spacing:0;'><thead><tr><th>Verzeichnis</th><th>Dateiname</th><th>Größe</th><th>Dauer</th><th>Timestamp</th></tr></thead><tbody>";
      for($i=0;$i<count($backupinfo);$i++) {
         $mailtext .= "<tr>";
         for($j=0;$j<count($backupinfo[$i]);$j++) {
            $mailtext .= "<td>" . $backupinfo[$i][$j] . "</td>";
         } 
         $mailtext .="</tr>";
      }
      $mailtext .= "</tbody></table>";
      mail(
         $sendmailto,
         $mailsubject,
         $mailtext,
         "From: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Reply-To: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Content-Type: text/html\r\n"
      ) or die("<br><span class='err'>FEHLER: Mail konnte wegen eines unbekannten Fehlers nicht versendet werden.</span><br>");
      echo "<br><span class='ok'>Benachrichtigungsmail wurde erfolgreich verschickt!</span><br>";
   }
}
flush();

// function to get ftp connection object from URI
// basics were from: http://php.net/manual/de/function.ftp-connect.php#89811
function getFtpConnectionByURI($uri)
{
   // Split FTP URI into:
   // $match[0] = ftp://admin:password@barketing.dns.com:21/Data/FTP-Backups
   // $match[1] = ftp
   // $match[2] = admin
   // $match[3] = password
   // $match[4] = barketing.dns.com
   // $match[5] = 21
   // $match[6] = /Data/FTP-Backups
   preg_match("/([a-z]*?):\/\/(.*?):(.*?)@(.*?):(.*?)(\/.*)/i", $uri, $match);
   
   $ftpcon = null;

   // check if port is set and uri is formatted correctly
   if(is_int(intval($match[5]))) {
      $port = intval($match[5]);

      // check if ftp(s) or sftp is chosen
      if($match[1]=="ftp") {
         // set up and ftp(s) connection, login
         echo "Stelle (FTPs über SSL - Port $port) Verbindung zu FTP Server $match[4] her...<br>";
         // try ftps over ssl, usally through  port 21
         $ftpcon = ftp_ssl_connect($match[4], $port, 30);
         if (!gettype($ftpcon)=="resource") {
            echo "<span class='warn'>FTP über SSL - Port $port - Verbindung fehlgeschlagen!</span><br>";
            echo "Stelle (unsicheres FTP - Port $port) Verbindung zu FTP Server $match[4] her...<br>";
            // try normal insecure ftp
            $ftpcon = ftp_connect($match[4], $port, 30);
         }
         if (gettype($ftpcon)=="resource") {
            $login = ftp_login($ftpcon, $match[2], $match[3]);
            $pasv = ftp_pasv($ftpcon, true);
         }
      } else if($match[1]=="sftp") {
         echo "<span class='err'>SFTP Unterstützung noch nicht implementiert.</span><br>";
         // if(!$port==22) {
         //    echo "<span class='warn'>SFTP Übertragung aber Port ist nicht 22. Ist der gewählte Port $port korrekt?</span><br>";
         // }
         // echo "Stelle (sichere sFTP - Port $port) Verbindung zu FTP Server $match[4]$match[5]:$match[6] her...<br>";
         // $ftpcon = ssh2_connect($match[4], $port, 30);
         // ssh2_auth_password($ftpcon, $match[2], $match[3]);
         // $sftp = ssh2_sftp($ftpcon);
      } else {
         echo "<span class='err'>Kein gültiger Verbindungstyp (ftp/sftp) angegeben.</span><br>";
      }
   } else {
      echo "<span class='err'>Der Port ist fehlerhaft angegeben. Bitte URI prüfen.</span><br>";
   }
   
   if ($ftpcon && gettype($ftpcon)=="resource")
   {
      echo "<span class='ok'>Verbindung hergestellt</span>";
      if ($login) {
         echo "<span class='ok'>, Login erfolgreich</span>";
         if ($pasv) {
            echo "<span class='ok'>, passiver Modus aktiviert</span>";
            if(!isset($match[6]) || $match[6] == "") {
               echo ".<br>";
               return $ftpcon;
            } else if (ftp_chdir($ftpcon, $match[6])) {
               echo "<span class='ok'>, Verzeichniswechsel zu $match[6] erfolgreich.</span><br>";
               return $ftpcon;
            } else {
               echo "<span class='err'>, Verzeichniswechsel zu $match[6] fehlerhaft.</span><br>";
               return null;
            }
         } else {
            echo "<span class='err'>, passiver Modus konnte nicht aktiviert werden. Upload wird trotzdem probiert.</span><br>";
            return $ftpcon;
         }
      } else {
         echo "<span class='err'>, Login fehlgeschlagen.</span><br>";
         return null;
      }
   }
   echo "<span class='err'>Fehler beim Verbinden mit dem FTP Server $match[4]$match[5]$match[6].</span><br>";
   echo "<p class='grey'>Debug:<br>";
   echo "URI (komplett): $match[0]<br>";
   echo "Typ: $match[0]<br>";
   echo "URI ohne Typ: $match[4]<br>";
   echo "Username: $match[2]<br>";
   echo "Passwort: $match[3]<br>";
   echo "Port: $match[5]<br>";
   echo "Unterordner: $match[6]<br>";
   echo "</p>";
   // Or retun null
   return null;
}
// from: http://stackoverflow.com/a/10473026/516047
function startsWith($haystack, $needle) {
   // search backwards starting from haystack length characters from the end
   return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== FALSE;
}

?>

Sicherheit: Absicherung mit .htpasswd

Das Verzeichnis, in dem die backup.php und die Backups liegen, sollte natürlich mit einer .htpasswd abgesichert werden. Mit einer eingerichteten .htpasswd Datei ist zuerst ein Login nötig, eh man auf bestimmte Bereiche des Webspaces zugreifen darf:
Das Bild zeigt eine Login Datenabfrage beim Aufruf der Backup URL
Die Datei .htpasswd enthält hierbei die Login Daten und in der .htaccess des Backup Unterordners wird festgelegt, dass eine .htpasswd diesen Ordner schützt. Die .htpasswd generiert ihr euch am besten mit diesem Generator und baut sie dann folgendermaßen in die .htaccess dieses Ordners ein:

AuthType Basic
AuthName &quot;Backups&quot;
AuthUserFile /www/htdocs/all-inkl-account/backup/.htpasswd
Require valid-user

Sicherheit: Absicherung mit .htaccess

Da wir schonmal bei .htaccess sind, erhöhen wir die Sicherheit mit ein paar weiteren grundlegenden Zeilen:

#block access to certain file types
&lt;FilesMatch &quot;.(htaccess|htpasswd|ini|phps|log|sh|tar.gz)$&quot;&gt;
 Order Allow,Deny
 Deny from all
&lt;/FilesMatch&gt;

# disable directory browsing
Options All -Indexes

# prevent basic url hacking stuff
# from: http://www.queness.com/post/5421/17-useful-htaccess-tricks-and-tips
RewriteEngine On
# proc/self/environ? no way!
RewriteCond %{QUERY_STRING} proc/self/environ [OR]
# Block out any script trying to set a mosConfig value through the URL
RewriteCond %{QUERY_STRING} mosConfig_[a-zA-Z_]{1,21}(=|\%3D) [OR]
# Block out any script trying to base64_encode crap to send via URL
RewriteCond %{QUERY_STRING} base64_encode.*(.*) [OR]
# Block out any script that includes a &lt;script&gt; tag in URL
RewriteCond %{QUERY_STRING} (&lt;|%3C).*script.*(&gt;|%3E) [NC,OR]
# Block out any script trying to set a PHP GLOBALS variable via URL
RewriteCond %{QUERY_STRING} GLOBALS(=|[|\%[0-9A-Z]{0,2}) [OR]
# Block out any script trying to modify a _REQUEST variable via URL
RewriteCond %{QUERY_STRING} _REQUEST(=|[|\%[0-9A-Z]{0,2})
# Send all blocked request to homepage with 403 Forbidden error!
RewriteRule ^(.*)$ /index.htm [F,L]

ErrorDocument 401 /backup/index.htm
ErrorDocument 403 /backup/index.htm
ErrorDocument 404 /backup/index.htm
ErrorDocument 500 /backup/index.htm

Dadurch werden Zugriffe auf bestimmte Dateitypen (auch die Backup Dateien), Verzeichnisse und Zugriffe mit sicherheitskritischen Merkmalen unterbunden. Alle diese nicht validen Zugriffe bekommen die index.htm serviert, welches einfach nur eine leere HTML Datei ist. Somit wird den Abfragenden auch kein detaillierter Grund gegeben, warum der Zugriff fehlschlug.

Automatisierung mit All-Inkl Cronjobs

Zu guter Letzt hilft diese Sicherungslösung natürlich nur, wenn sie automatisiert wird. Auch dies ist stark abhängig von eurem Hoster, System, dem Anwendungsbereich usw.
Im Falle von All-Inkl als Webhoster, könnt ihr die Cronjob Funktionalität im KAS (KAS -> Tools -> Cronjobs) benutzen:
Das Bild zeigt die Cronjob Einrichtungsoberfläche von All-Inkl

All-Inkl bietet sogar das Zusenden aller Skriptausgaben. Ihr erhaltet also zusätzlich zu dem kurzen E-Mail-Bericht, der im Skript generiert werden kann, noch eine weitere Mail mit allen Ausgaben. Diese sind dann zwar nicht mehr farbig, aber was solls:
Das Bild zeigt die Cronjob E-Mail mit den Skriptausgaben

Danke an Kenny für das wertvolle Feedback im letzten Artikel, das dazu beigetragen hat, dass ich diese Erweiterung nochmal gepostet habe. Meinst du, dass das jetzt eine Basic Backup Lösung sein könnte? Das einzige, was noch fehlt, ist das Backup Management auf dem externen FTP Server, damit der nicht überläuft. Aber das ist nun wirklich Aufgabe der Admins zu entscheiden und zu verwalten, wie lange Backups aufgehoben werden sollen.