Auch dieser Artikel reiht sich in die Liste der Software-Batch-AD-Deployment-Guides ein.
Im Falle von HipChat wird der Artikel recht kurz, denn hier passiert nichts ungewöhnliches.

Vorbereitung

Das Bild zeigt das HipChat Deployment Verzeichnis mit seinen üblichen DateienDer aktuellste HipChat-Installer (für Windows) ist als .exe immer unter dieser URL verfügbar.
Anschließend wird wieder ein übliches Deployment-Verzeichnis auf einem für die PCs verfügbaren Netzlaufwerk erstellt: Installer, Installer-Batch, allowedPCs.txt und deniedPCs.txt (mehr Informationen zum Clientfilter hier). Im Standardfall (so auch in diesem Deployment-Script) wird die deniedPCs.txt benutzt, um einzelne Clients von der Verteilung auszuschließen. Die Textdatei muss dann Computernamen enthalten, einen pro Zeile.
Der HipChat-Installer muss folgendermaßen umbenannt werden: „HipChat_[Version].exe“ und der Versionsstring muss in das Deployment-Script in Zeile 11.

Deployment-Script

19.09.2016: Version 4.27.1.1658 getestet und verteilt.

Hinweis: Wer nicht nur ein Update sondern eine komplette Reinstallation von Hipchat im Netzwerk ausrollen will, kann mein angepasstes Script – hier als Download – nutzen. Die benötigte VersionCompare.exe erhaltet ihr hier.

Hier das Script für ein normales Update:

@echo off && color 9f && setlocal
set wd=\\lea\Deployment\Software\Hipchat
set log=%wd%\hipchat.log
set tools=\\lea\Deployment\Sonstiges\tools
set hipEL=999
set instversion=0.0
set versionEL=9
set exepath=none
set retry=0
REM:: ######## EDIT THIS ####
set newversion=4.1658
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 check
::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)\Atlassian\HipChat4\HipChat.exe" set exepath="C:\Program Files (x86)\Atlassian\HipChat4\HipChat.exe"
::if exist "c:\Program Files (x86)\Skype\Phone\skype.exe" set exepath="c:\Program Files (x86)\Skype\Phone\skype.exe"
if %exepath%==none echo %date% %time:~0,8% - %computername% findet das .exe Verzeichnis nicht && goto taskkill
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 taskkill
if "%versionEL%"=="0" echo %date% %time:~0,8% - %computername% hat bereits %instversion% installiert >> %log% & goto end
if "%versionEL%"=="1" echo %date% %time:~0,8% - %computername% hat bereits %instversion% (neuer) installiert >> %log% & goto end
goto end

:taskkill
TASKKILL /f /im hipchat.exe
goto install

:install
echo %date% %time:~0,8% - %computername% installiert... >> %log%
::msiexec.exe /i %wd%\deploy\%newversion%\SkypeSetup.msi /qn /norestart FEATURE_IEPLUGIN=0 FEATURE_FFPLUGIN=0
start /w %wd%\HipChat_%newversion%.exe /verysilent /norestart /restartapplications /lang=german /lang=1031 /64 /64bit /x64
set hipEL=%errorlevel%
if %hipEL%==1618 goto retry REM:: msiexec process in use, installation already in progress (eg. windows updates running)
if %hipEL%==1602 goto retry REM:: user canceled installation (eg. taskkill)
if %hipEL%==1603 goto retry REM:: fatal error, some use it for "already installed" (eg. java)
if %hipEL%==1638 goto uninstall REM:: another product is already installed, denies an update
if %hipEL%==1625 goto uninstall REM:: skype installer sometimes threw this one, don't know why
set hipEL=%errorlevel%
echo %date% %time:~0,8% - %computername% hat Version %newversion% mit EL %hipEL% abgeschlossen >> %log%
md %wd%\done\%computername%
goto end

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

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

:uninstall
if %retry%==1 goto retryfailed
TASKKILL /f /im hipchat.exe
echo %date% %time:~0,8% - %computername% deinstalliert Version %instversion%... >> %log%
start /w "" "C:\Program Files (x86)\Atlassian\HipChat4\unins000.exe" /s /silent /qn
del /q /s "C:\Program Files (x86)\Atlassian"
echo %date% %time:~0,8% - %computername% - %instversion% deinstalliert, retry... >> %log%
set retry=1
goto taskkill

:end
endlocal
exit

Und das war’s auch schon. Bei einem Update muss nur die neue .exe-Datei heruntergeladen und die Version in Zeile 11 angepasst werden.
Das Script kommt als Computer-Startscript in das GPO und schon startet die Verteilung:
Das Bild zeigt die Logausgaben des HipChat-Deployments
Das Bild zeigt das Deployment der 1648er Version anhand des allnew-Update-Scripts (siehe Hinweis und Download oben). Dabei wird an jedem PC, unabhängig der installierten Version (deswegen wird überall „Version 0.0“ deinstalliert), HipChat komplett deinstalliert und neu installiert. Beim Umstieg auf 1648 würde ich das empfehlen, weitere Update werden ich auch wieder mit dem normalen Script erledigen.

Worum geht’s?

swords-and-souls-iconIch stelle kurz und knackig meinen in AutoIt 3 programmierten aktiven Bot für das Browserspiel Swords and Souls vor. Dieser ist hauptsächlich dafür da, Arena-Stages zu farmen. Außerdem nutzt er aktiv (und clever) alle Skills, leert regelmäßig die Bank, tauscht Kleeblätter ein, kauft EXP und speichert zwischendurch. Er könnte also ohne Probleme über Stunden durchlaufen und den Charakter damit verbessern.

Funktionen

Steuerung durch Shortcuts:

  • Shift+Alt+A – In der Arena kämpfen
  • Shift+Alt+M – Bank leeren und Kleeblätter eintauschen
  • Shift+Alt+O – Zurück zur Übersicht gehen, um andere Funktionen zu nutzen
  • Shift+Alt+H – Diese Hilfe noch einmal zeigen
  • Shift+Alt+X / ESC – Programm beenden

Farmingpausen mit:

  • Speichern
  • Gold von der Bank einsammeln
  • Kleeblätter eintauschen
  • Erfahrung für Gold kaufen (erst nach dem Besiegen des Endgegners möglich)

Cleveres Skillsystem:

  • Priorität 1: Heilen (Skill 5): nur wenn HP unter 40%
  • Priorität 2: Schildschlag (Skill 2): in Bosskämpfen immer benutzen, bei normalen Gegnern nur wenn dessen HP > 25%
  • Priorität 3: Schutzschild (Skill 4): immer wenn möglich
  • Priorität 4: Gift (Skill 3): in Bosskämpfen immer benutzen, bei normalen Gegnern nur wenn dessen HP > 50%
  • Priorität 5: Hack’n’Slay (Skill 6): in Bosskämpfen immer benutzen, bei normalen Gegnern nur wenn dessen HP > 50%
  • Priorität 6: Doppelangriff (Skill 1): in Bosskämpfen immer benutzen, bei normalen Gegnern nur wenn dessen HP > 25%

Screenshots

Die Einrichtung muss direkt nach dem Start geschehen und erfolgt durch einen Klick auf den obersten linkesten Pixel des Spielfensters:
swords-and-souls-bot-start-prepare-topleft-corner-pixel
Das Browserfenster muss so groß sein, dass das Spiel komplett angezeigt wird. Am Anfang werden in einem Hilfedialog alle Funktionen gelistet:
swords-and-souls-bot-start-help
In der Arena wird zuerst das Level für das Farming gewählt, anschließend noch ob das Pausenfeature genutzt werden soll und schon legt der Bot los:
swords-and-souls-bot-arena-start
swords-and-souls-bot-arena-fight

Video

Download und Code

Hier gibts den Bot als Download für 32/64bit Windows und, falls jemand den Bot weiterentwickeln oder anpassen möchte, den kompletten Code 1:1.

Download section
swords-and-souls-bot.exe (32bit)
swords-and-souls-bot_x64.exe (64bit)

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

#Region ;**** Directives created by AutoIt3Wrapper_GUI ****
#AutoIt3Wrapper_Icon=swords-and-souls-bot.ico
#AutoIt3Wrapper_Compile_Both=y
#AutoIt3Wrapper_Res_Description=AutoIt Bot for the browsergame "Swords and Souls", developed by Hannes Schurig in 2016
#AutoIt3Wrapper_Res_Fileversion=1.0
#AutoIt3Wrapper_Res_LegalCopyright=Hannes Schurig
#AutoIt3Wrapper_Res_Language=1031
#AutoIt3Wrapper_Add_Constants=n
#EndRegion ;**** Directives created by AutoIt3Wrapper_GUI ****

#include <Misc.au3>
#include <Array.au3>
#include <MsgBoxConstants.au3>

AutoItSetOption("SendKeyDelay", 40)
AutoItSetOption("SendKeyDownDelay", 40)
AutoItSetOption("MouseClickDelay", 40)
AutoItSetOption("WinTitleMatchMode", 2)
AutoItSetOption("MouseCoordMode", 1)

$noCoords = True
Local $ol[2]
$i = 0 ; fight counter
$survival = False
Local $levelcoords[2] = [0,0]
$clickCriticals = True
$pauseFarming = True
$pauseFarmingAfterXMatches = 2

HotKeySet("{Esc}", "exitnow") ; ESC
HotKeySet("+!x", "exitnow") ; Shift+Alt+X
HotKeySet("+!a", "arenaFight") ; Shift+Alt+A
HotKeySet("+!m", "museum") ; Shift+Alt+M
HotKeySet("+!o", "overview") ; Shift+Alt+O
HotKeySet("+!h", "help") ; Shift+Alt+H

Func exitnow()
	Exit
EndFunc

; prepare
WinActivate("Swords")
Sleep(300)
MsgBox($MB_TOPMOST + $MB_SETFOREGROUND + $MB_DEFBUTTON1 + $MB_ICONINFORMATION + $MB_OK,"Ecke oben links anklicken","Klicke in den obersten linkesten Pixel des Spiels.")
While $noCoords
		If _IsPressed("01") Then
		$ol = MouseGetPos()
		ConsoleWrite("####### Koords: " & $ol[0] & " " & $ol[1] & @CRLF)
		$noCoords = False
		EndIf
WEnd

If checkSingleCoordWithColor($ol[0]+578, $ol[1]+400, 0xB65D52) Then
		ConsoleWrite("####### Location: Map Overview" & @CRLF)
		MsgBox($MB_TOPMOST + $MB_SETFOREGROUND + $MB_DEFBUTTON1 + $MB_ICONINFORMATION + $MB_OK, "Deine Position", "Du befindest dich in der Übersichtskarte.")
EndIf

If checkSingleCoordWithColor($ol[0]+394, $ol[1]+460, 0x745818) Then
		ConsoleWrite("####### Location: Museum" & @CRLF)
		MsgBox($MB_TOPMOST + $MB_SETFOREGROUND + $MB_DEFBUTTON1 + $MB_ICONINFORMATION + $MB_OK, "Deine Position", "Du befindest dich im Museum.")
EndIf

If checkSingleCoordWithColor($ol[0]+66, $ol[1]+64, 0xC8B05E) Then
		ConsoleWrite("####### Location: Arena" & @CRLF)
		MsgBox($MB_TOPMOST + $MB_SETFOREGROUND + $MB_DEFBUTTON1 + $MB_ICONINFORMATION + $MB_OK, "Deine Position", "Du befindest dich in der Arena.")
EndIf

help()

Func help()
	MsgBox($MB_TOPMOST + $MB_SETFOREGROUND + $MB_DEFBUTTON1 + $MB_ICONINFORMATION + $MB_OK, "Hilfe", "Du kannst folgende Shortcuts nutzen:" & @CRLF & _
	"Shift+Alt+A - In der Arena kämpfen" & @CRLF & _
	"Shift+Alt+M - Bank leeren und Kleeblätter eintauschen" & @CRLF & _
	"Shift+Alt+O - Zurück zur Übersicht gehen, um andere Funktionen zu nutzen" & @CRLF & _
	"Shift+Alt+H - Diese Hilfe noch einmal zeigen" & @CRLF & _
	"Shift+Alt+X / ESC - Programm beenden" & @CRLF & _
	"Dies ist ein Maus/Tastatur-Bot. Der PC ist, während der Bot läuft, nicht direkt benutzbar.")
EndFunc

Func overview()
	; force-go to overview map
	While Not checkSingleCoordWithColor($ol[0]+578, $ol[1]+400, 0xB65D52)
		MouseClick("left", $ol[0]+25, $ol[1]+20,1)
		Sleep(800)
	WEnd
EndFunc

Func arenaFight()
	; check for overview map
	If checkSingleCoordWithColor($ol[0]+578, $ol[1]+400, 0xB65D52) Then
		; go to arena
		MouseClick("left",$ol[0]+235, $ol[1]+165,1)
	; check for arena
	ElseIf checkSingleCoordWithColor($ol[0]+66, $ol[1]+64, 0xC8B05E) Then
		; continue
	Else
		; force-go to overview map
		While Not checkSingleCoordWithColor($ol[0]+578, $ol[1]+400, 0xB65D52)
			MouseClick("left", $ol[0]+25, $ol[1]+20,1)
			Sleep(800)
		WEnd
		; go to arena
		MouseClick("left",$ol[0]+235, $ol[1]+165,1)
	EndIf

	; level input
	$level = InputBox("Welches Level?", "Bitte gib das Level ein, dass gefarmt werden soll." & @CRLF & _
	"Möglich Eingaben sind '1' - '30', 'survival' und 'final'", "", " M", 280, 150)

	If $level > 0 And $level <= 10 Then
		$levelcoords[0] = $ol[0] + 119 + (($level - 1)*62)
		$levelcoords[1] = $ol[1] + 292
	ElseIf $level > 10 And $level <= 20 Then
		$levelcoords[0] = $ol[0] + 97 + (($level - 11)*66)
		$levelcoords[1] = $ol[1] + 334
	ElseIf $level > 20 And $level <= 30 Then
		$levelcoords[0] = $ol[0] + 75 + (($level - 21)*71)
		$levelcoords[1] = $ol[1] + 392
	ElseIf $level = "final" Then
		$levelcoords[0] = $ol[0] + 489
		$levelcoords[1] = $ol[1] + 482
	ElseIf $level = "survival" Then
		$levelcoords[0] = $ol[0] + 339
		$levelcoords[1] = $ol[1] + 456
		$survival = True
	Else
		MsgBox($MB_TOPMOST + $MB_SETFOREGROUND + $MB_DEFBUTTON1 + $MB_ICONERROR + $MB_OK, "Falsche Eingabe", "Die Eingabe konnte nicht verwertet werden." & @CRLF & _
		"Der Bot setzt sich zurück, anschließend kannst Du erneut den Arena-Modus starten.")
		; force-go to overview map
		While Not checkSingleCoordWithColor($ol[0]+578, $ol[1]+400, 0xB65D52)
			MouseClick("left", $ol[0]+25, $ol[1]+20,1)
			Sleep(800)
		WEnd
		Sleep(1000)
		Return
	EndIf

	If $levelcoords[0] == 0 Then
		MsgBox($MB_TOPMOST + $MB_SETFOREGROUND + $MB_DEFBUTTON1 + $MB_ICONERROR + $MB_OK, "Unerwarteter Fehler", "Der Bot setzt sich zurück, anschließend kannst Du erneut den Arena-Modus starten.")
		; force-go to overview map
		While Not checkSingleCoordWithColor($ol[0]+578, $ol[1]+400, 0xB65D52)
			MouseClick("left", $ol[0]+25, $ol[1]+20,1)
			Sleep(800)
		WEnd
		Sleep(1000)
		Return
	EndIf

	; pause farming regulary after some matches - save, grab income, turn in cloverleafs frequently, get exp for gold?
	$save = MsgBox($MB_TOPMOST + $MB_SETFOREGROUND + $MB_DEFBUTTON1 + $MB_ICONQUESTION + $MB_YESNO, "Farming mit Pausen?", "Soll der Bot während des Farmens weitere hilfreiche Aufgaben übernehmen? Folgende Aufgaben würde der Bot alle " & $pauseFarmingAfterXMatches & " Matches ebenfalls übernehmen:" & @CRLF & _
	"- Speichern" & @CRLF & _
	"- Gold von der Bank einsammeln" & @CRLF & _
	"- Kleeblätter eintauschen" & @CRLF & _
	"- Erfahrung für Gold kaufen (erst nach dem Besiegen des Endgegners möglich)", "", " M")
	If $save = $IDYES Then
		$pauseFarming = True
	Else
		$pauseFarming = False
	EndIf

	; arena:
	while 1
		WinActivate("Swords")
		Sleep(1200)

; ------------- MANAGING MATCHES --------------------------------------------------

		; lost/survival done, retry
		If checkSingleCoordWithColor($ol[0] + 351, $ol[1] + 326, 0x5A3D1D) Then
			MouseClick("left",$ol[0] + 351, $ol[1] + 326, 1)
			$i += 1
		EndIf

		; survival start
		If $survival And checkSingleCoordWithColor($ol[0]+292, $ol[1]+175, 0x938D6F) Then
			; click survival
			MouseClick("left", $levelcoords[0], $levelcoords[1], 1)
			Sleep(500)
			; click "start"
			MouseClick("left", $levelcoords[0], $levelcoords[1], 1)
			Sleep(1500)
		EndIf

		; survival ended, "ok"
		If $survival And checkSingleCoordWithColor($ol[0] + 426, $ol[1] + 448, 0x5D3F1D) Then
			MouseClick("left", $ol[0] + 426, $ol[1] + 448, 1)
			Sleep(1000)
		EndIf

		; final ended, scarecrow talk
		If checkSingleCoordWithColor($ol[0] + 360, $ol[1] + 511, 0xE6DFC7) Then
			MouseClick("left", $ol[0] + 360, $ol[1] + 511, 1)
			Sleep(1000)
		EndIf

		; normal match start
		If checkSingleCoordWithColor($ol[0]+292, $ol[1]+175, 0x938D6F) Then
			MouseClick("left", $levelcoords[0], $levelcoords[1], 1)
		EndIf

		; normal match finished, continue
		If checkSingleCoordWithColor($ol[0]+378, $ol[1]+454, 0xFFFFFF) Then
			MouseClick("left", $ol[0]+378, $ol[1]+454, 1)
			$i += 1
		EndIf

; ------------- FIGHTING --------------------------------------------------

		; Skill 5 - Healing
		; check if available
		If checkSingleCoordWithColor($ol[0]+490, $ol[1]+560, 0xFFE7C1) Then
			; just use if u're below 50% health
			If checkSingleCoordWithColor($ol[0]+284, $ol[1]+453, 0x730520) Then
				; enough health
			Else
				Send("5")
			EndIf
		EndIf

		; Skill 2
		; check if available
		If checkSingleCoordWithColor($ol[0]+306, $ol[1]+565, 0xFFE7C1) Then
			; if in boss battle - use it anytime
			If checkCoordRangeWithColor($ol[0]+404, $ol[1]+451, $ol[0]+442, $ol[1]+469, 0xFFD23E) Then
				Send("2")
			Else
				; if not: only if enemy health 25% or more
				If checkSingleCoordWithColor($ol[0]+485, $ol[1]+453, 0x730520) Then
				Send("2")
				EndIf
			EndIf
		EndIf

		; Skill 4
		; if available
		If checkSingleCoordWithColor($ol[0]+430, $ol[1]+560, 0xFFE7C1) Then
			Send("4")
		EndIf

		; Skill 3 - Poison
		If checkSingleCoordWithColor($ol[0]+369, $ol[1]+552, 0xFFE7C1) Then
			; if in boss battle - use it anytime
			If checkCoordRangeWithColor($ol[0]+404, $ol[1]+451, $ol[0]+442, $ol[1]+469, 0xFFD23E) Then
				Send("3")
			Else
				; if not: only if enemy health 50% or more
				If checkSingleCoordWithColor($ol[0]+515, $ol[1]+453, 0x730520) Then
				Send("3")
				EndIf
			EndIf
		EndIf

		; Skill 6 - Slashing
		If checkSingleCoordWithColor($ol[0]+548, $ol[1]+561, 0xFFE7C1) Then
			; if in boss battle - use it anytime
			If checkCoordRangeWithColor($ol[0]+404, $ol[1]+451, $ol[0]+442, $ol[1]+469, 0xFFD23E) Then
				Send("6")
			Else
				; if not: only if enemy health 50% or more
				If checkSingleCoordWithColor($ol[0]+515, $ol[1]+453, 0x730520) Then
				Send("6")
				EndIf
			EndIf
		EndIf

		; Skill 1
		; if available
		If checkSingleCoordWithColor($ol[0]+249, $ol[1]+559, 0xFFE7C1) Then
			; if in boss battle - use it anytime
			If checkCoordRangeWithColor($ol[0]+404, $ol[1]+451, $ol[0]+442, $ol[1]+469, 0xFFD23E) Then
				Send("1")
			Else
				; if not: only if enemy health 25% or more
				If checkSingleCoordWithColor($ol[0]+485, $ol[1]+453, 0x730520) Then
				Send("1")
				EndIf
			EndIf
		EndIf

; ------------- REST --------------------------------------------------

		; save every 5th fight
		If $pauseFarming And $i >= $pauseFarmingAfterXMatches Then
			Sleep(1000)
			; force-go to overview map
			While Not checkSingleCoordWithColor($ol[0]+578, $ol[1]+400, 0xB65D52)
				MouseClick("left", $ol[0]+25, $ol[1]+20,1)
				Sleep(800)
			WEnd

			museum()
			expForGold()

			; go to arena
			MouseClick("left",$ol[0]+235, $ol[1]+165,1)
			$i = 0
			Sleep(1000)
		EndIf

	WEnd
EndFunc

Func museum()
	; force-go to overview map
	While Not checkSingleCoordWithColor($ol[0]+578, $ol[1]+400, 0xB65D52)
		MouseClick("left", $ol[0]+25, $ol[1]+20,1)
		Sleep(800)
	WEnd

	; go to museum
	MouseClick("left",$ol[0]+329, $ol[1]+459, 1)
	Sleep(1000)

	; get cash
	MouseClick("left",$ol[0]+479, $ol[1]+431, 1)
	Sleep(1000)

	; auto-invest clovers if u're in the room and have some
	While checkSingleCoordWithColor($ol[0]+651, $ol[1]+311, 0xE8E5C2)
		MouseClick("left",$ol[0]+651, $ol[1]+311,1)
		MouseMove($ol[0]+551, $ol[1]+211, 1)
		Sleep(2000)
	WEnd
	Sleep(1000)

	; get cash again
	MouseClick("left",$ol[0]+479, $ol[1]+431, 1)
	Sleep(1000)

	; force-go to overview map
	While Not checkSingleCoordWithColor($ol[0]+578, $ol[1]+400, 0xB65D52)
		MouseClick("left", $ol[0]+25, $ol[1]+20,1)
		Sleep(800)
	WEnd
	Sleep(1000)
EndFunc

Func expForGold()
	; force-go to overview map
	While Not checkSingleCoordWithColor($ol[0]+578, $ol[1]+400, 0xB65D52)
		MouseClick("left", $ol[0]+25, $ol[1]+20,1)
		Sleep(800)
	WEnd

	; go to shop
	MouseClick("left",$ol[0]+528, $ol[1]+213, 1)
	Sleep(1000)

	For $x = 1 To 30 Step +1
		MouseClick("left",$ol[0]+396, $ol[1]+548,1)
		Sleep(300)
	Next

	; force-go to overview map
	While Not checkSingleCoordWithColor($ol[0]+578, $ol[1]+400, 0xB65D52)
		MouseClick("left", $ol[0]+25, $ol[1]+20,1)
		Sleep(800)
	WEnd
	Sleep(1000)
EndFunc

Func training()

	; this feature is not ready yet
	Return

	; check for overview map
	If Not checkSingleCoordWithColor($ol[0]+578, $ol[1]+400, 0xB65D52) Then
		MsgBox($MB_TOPMOST + $MB_SETFOREGROUND + $MB_DEFBUTTON1 + $MB_ICONINFORMATION + $MB_OK, "Übersichtskarte", "Bitte gehe in die Übersichtskarte, um die Botfunktionen zu starten.")
		Return
	EndIf

	;training crit:
	While 1
		If checkSingleCoordWithColor(685, 382, 0xffffff) Then
			MouseClick("left")
			Sleep(700)
			MouseClick("left")
			$i +=1
			ContinueLoop
		EndIf
		If checkSingleCoordWithColor(789, 381, 0xffffff) Then
			MouseClick("left")
			Sleep(700)
			MouseClick("left")
			$i +=1
			ContinueLoop
		EndIf
		If $i>50 Then
			; quit crit training
			MouseClick("left", $ol[0]+25, $ol[1]+20, 1)
			Sleep(5000)
			; quit training room & go to map
			MouseClick("left", 298, 123, 1)
			Sleep(3000)
			; go to training room
			MouseClick("left", 673, 415, 1)
			Sleep(3000)
			$i = 0
			; start crit training
			MouseClick("left", 872, 147, 1)
			Sleep(3000)
		EndIf
	WEnd

	; training attack:
	While 1
		If Not checkSingleCoordWithColorV(326, 455, 0xCC9362, 3) Then
			Send("{LEFT}")
			ContinueLoop
		EndIf
		If Not checkSingleCoordWithColorV(560, 466, 0xAD794C, 4) Then
			Send("{RIGHT}")
			ContinueLoop
		EndIf
		If Not checkSingleCoordWithColorV(563, 541, 0x926743, 3) Then
			Send("{Down}")
			ContinueLoop
		EndIf
		If Not checkSingleCoordWithColorV(511, 373, 0xCF996B, 3) Then
			Send("{Up}")
			ContinueLoop
		EndIf
		If Not checkSingleCoordWithColorV(322, 424, 0xD4A57D, 3) Then
			Send("{LEFT}")
			ContinueLoop
		EndIf
	WEnd
EndFunc

Func checkSingleCoordWithColor($x, $y, $color)
		Return IsArray(PixelSearch($x-3, $y-3, $x+3, $y+3, $color, 3))
EndFunc

Func checkSingleCoordWithColorV($x, $y, $color, $variation)
		Return IsArray(PixelSearch($x-3, $y-3, $x+3, $y+3, $color, $variation))
EndFunc

Func checkCoordRangeWithColor($x1, $y1, $x2, $y2, $color)
		Return IsArray(PixelSearch($x1-3, $y1-3, $x2+3, $y2+3, $color, 3))
EndFunc

; keep-alive for shotcuts
while 1
	sleep(100000000)
WEnd

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)

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.

Achtung: Diese Backup Lösung habe ich im nächsten Artikel noch einmal stark erweitert. Ich empfehle diese komplexere Lösung zu benutzen und die darin enthaltenen Sicherheitshinweise zu beachten.

FTP Backups (bisher)

Mein Blog läuft auf dem Hoster ALL-INKL.com, den ich nebenbei gesagt nach mehreren Jahren immernoch empfehlen kann.
Bisher habe ich halbwegs (un)regelmäßig manuell, von Hand, FTP Backups erstellt. Also alle paar Monate habe ich über einen FTP Tool wie FileZilla bestimmte Ordner heruntergeladen, Datei für Datei, anschließend in ein Archiv gepackt und irgendwo verstaut. Dieser Prozess ist natürlich in vielerlei Hinsicht nicht zuverlässig, zeitintensiv und aufwändig.
Auch das Sichern der Ordner automatisiert mit Tools wie SyncBack (mein Artikel dazu) wird immer zeitintensiver, je größer die zu sichernde Datenmenge wird. Außerdem muss immer ein PC, auf dem die Tools laufen, an sein.

FTP Backups via PHP Skript

Sinnvoller ist es, diese Backups auf dem Server des Webhosters erstellen zu lassen. So wird der heimische PC nicht belastet.
All-Inkl bietet dafür ein recht einfaches PHP Skript zum Sichern eines einzelnen FTP Ordners oder des gesamten All-Inkl FTP Accounts.

Ich habe dieses Skript ausgebaut und um verschiedene Funktionen ergänzt:

  • beliebig viele Ordner des All-Inkl Accounts in einzelne .tar.gz Archive sichern
  • Einschränkung der Anzahl aufgehobener Backups – älteste Backups werden automatisch gelöscht
  • detaillierte Ausgabe inklusive benötigter Zeit
  • E-Mail Benachrichtigung

Screenshot

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

Code

Achtung: Dieses Backup Skript habe ich in Version 1.1 noch einmal stark erweitert. Ich empfehle diese komplexere Lösung zu benutzen. Besser wäre sogar die Version 1.2, mit der sich zusätzlich MySQL Datenbanken mitsichern lassen.

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;

  // ########## EDIT THIS VARIABLES ###################
  $foldertobackup = array("bonnie", "tests", "locationmap", "blog"); // which root folders should get backed up?
  $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 = "admin@yourdomain.com"; // valid mail address to send the mail to
  // ##################################################

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

    // 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;
    if (is_int($backuptime)) {
      echo "Backup fertig: ".$archivname." (Dauer: ".$backuptime." Sekunden)<br>";
    } else {
      echo "Backup fertig: ".$archivname."<br>";
    }

    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 $key => $value) {
      if($i>=$backupfilemaximum) {
        echo $key." wird gelöscht...<br>";
        if (unlink($key)) {
          echo "Datei erfolgreich gelöscht.<br>";
        } else {
          echo "Fehler beim Löschen der Datei.<br>";
        }
      }
      $i++;
    }
    $jobendtime = time() - $jobtime;
    if (is_int($jobendtime)) {
      echo "######################################## (Dauer: ".$jobendtime." Sekunden)<br>";
      $alltime += $jobendtime;
    } else {
      echo "########################################<br>";
    }
  }

  echo "<br><br>Die automatische Sicherung des FTP-PHP-Backup-Skripts '".pathinfo(__FILE__, PATHINFO_BASENAME)."' hat ".count($foldertobackup)." Verzeichnisse in insgesamt ".$alltime." Sekunden gesichert.<br><br>";

  // ######### send mail
  if (!isset($sendmail) || $sendmail== 0 && in_array($sendmail, array("no", "nein"))) {
    echo "Benachrichtigungsmail wurde nicht verschickt.";
  } else {
    if(!preg_match( '/^([a-zA-Z0-9])+([.a-zA-Z0-9_-])*@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-]+)+/' , $sendmailto)) {
      echo "FEHLER: Mail konnte nicht versendet werden, da die Adresse ungültig ist!";
    } else {
      mail(
        $sendmailto,
        "Automatische FTP Sicherung abgeschlossen",
        "Die automatische Sicherung des FTP-PHP-Backup-Skripts ".pathinfo(__FILE__, PATHINFO_BASENAME)." hat ".count($foldertobackup)." Verzeichnisse in insgesamt ".$alltime." Sekunden gesichert.",
        "From: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Reply-To: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Content-Type: text/html\r\n"
      ) or die("FEHLER: Mail konnte wegen eines unbekannten Fehlers nicht versendet werden");
      echo "Benachrichtigungsmail wurde erfolgreich verschickt!";
    }
  }
?>

Wichtige Anmerkungen

Bitte beachtet, dass einige Zeilen angepasst werden müssen und der Code nur bei dem Webhoster All-Inkl getestet wurde. Die Zeile

include "Archive/Tar.php"

, die ein externes Modul einbindet, könnte auf anderen Websern zu Problemen führen. Am besten testet ihr es einfach.
Thema Sicherheit: Wie Kenny in den Kommentaren korrekt angemerkt hat, müssen noch Anpassungen erfolgen, wenn das Skript als tatsächliche Backup-Lösung zum Einsatz kommen soll.

  • Die Sicherungen landen bei diesem Skript direkt in dem Skript-Ordner. Die Sicherung(en) sollte(n) nach der Sicherung an einen anderen Ort, beispielsweise auf einen anderen Server oder ein NAS, kopiert werden. Oder ihr automatisiert den Download der Sicherungen nach dem Backupprozess.
  • Sowohl das Skript als auch die Sicherungen können ohne weitere Maßnahmen direkt angesprochen werden. Ihr solltet natürlich den Backup Ordner beispielsweise durch eine htpasswd absichern und den Zugriff auf die Backups über die htaccess einschränken.

Ich werde dieses Online-Backup Thema in Zukunft vermutlich nochmal aufgreifen, erweitern und optimieren. Ich verlinke das dann hier.

Dieser Beitrag soll nur alle meine Batch-Softwaredeployments in einer Übersicht zusammenfassen:

Firefox

Stand: 19.09.2016 – Version: 48.0.2 getestet und läuft
Beitrag

Flash

Stand: 19.09.2016 – Version: 23.0 getestet und läuft
Beitrag

Java

Stand: 16.08.2016 – Version: 8u101 getestet und läuft
Beitrag

Neu: Reader DC

Stand: 16.08.2016 – Version DCUpd1501720053 getestet und läuft
Deployment-Guide

Reader (klassisch, Reader XI)

Stand: 28.07.2015 – Version: 11.0.12 getestet und läuft (deaktiviert)
Beitrag, Update-Kurzfassund 1 und 2

Skype

Stand: 17.03.2016 – Version: 7.21 getestet und läuft (deaktiviert)
Beitrag, Update-Kurzfassung

KeePass 2

Stand: 11.07.2016 – Version: 2.34 getestet und läuft
Beitrag

HipChat (inkl. Uninstaller)

Stand: 19.09.2016 – Version: 4.27.1.1658 getestet und läuft
Beitrag, Uninstaller