Hier findet ihr eine Reihe von VB-Skripts (Visual Basic Scripts) für Windows 2000 und Active Directory. Sie dienen in erster Linie zu Demonstrationszwecken, sollen aber auch einige typische administrative Aufgaben erleichtern. Es handelt sich um frühe Versuche, also erwartet bitte nichts allzu Tolles! 😉
Alle Dateien laufen unter Windows 2000 und setzen zum Teil eine vorhandene Active-Directory-Domäne voraus. Die Skripts sind Public Domain, d.h. ihr könnt sie gern verwenden und verändern. Sie können als Zip-Datei heruntergeladen werden:
VB-Skripts für Windows und Active Directory (3,9 KiB, 11.439-mal heruntergeladen, letzte Änderung am 23. November 2002)
Achtung: Ich übernehme keine Gewähr für die Funktion oder für eventuelle Fehler! Die Benutzung dieser Skripts geschieht vollständig auf eigene Gefahr!
Folgende Skripts findet ihr hier:
- Ausgeben einer kompletten Ordnerstruktur in eine Textdatei
- Anlegen von Benutzern mit Telefonnummer (Beispiel, Teil 1)
- Ändern der Telefonnummer bei allen Benutzern (Beispiel, Teil 2)
- Ändern des Anmeldenamens
- Anlegen zahlreicher Benutzer zu Testzwecken
- Passwort bei allen Benutzern einer OU ändern
Und auf weiteren Seiten gibt es Beispiele, wie das AD-Schema erweitert werden kann und wie man die AD-Konfiguration bearbeitet.
Ausgeben einer kompletten Ordnerstruktur in eine Textdatei
Dieses Beispiel zeigt den Umgang mit dem Dateisystem (und nebenbei auch den Trick, wie man verschachtelte Strukturen mit rekursiven Aufrufen behandeln kann). Es liest von einem beliebigen Ordner aus alle Unterordner komplett aus und schreibt diese in eine Textdatei. Diese Datei kann man dann z. B. zu Dokumentationszwecken nutzen.
>> Zur Erläuterung
1. drive=InputBox("Bitte Startordner eingeben!","Ordnerstruktur2Text") 2. ordner = "Root" & vbCrLf 3. dateiname = "Ordnerstruktur2Text.txt" 4. ebene = 0 5. ' Dateisystem-Zugriff ermöglichen: 6. set fs = CreateObject("Scripting.FileSystemObject") 7. ' existiert der angegebene Ordner überhaupt? 8. if fs.FolderExists(drive) then 9. ' Ausgabedatei 10. outputname = fs.Buildpath(drive, dateiname) 11. set output = fs.CreateTextFile(outputname) 12. ' Startordner ansprechen 13. set folder = fs.GetFolder(drive) 14. ' alle Unterordner des Ordners ausgeben: 15. listFolder(folder) 16. else 17. msgBox "Den Startordner gibt es gar nicht!",vbCritical,"Ordnerstruktur2Text" 18. wscript.quit 19. end if 20. ' Ergebnis in die Datei schreiben 21. output.write Ordner 22. output.close 23. ' Erfolg melden 24. Nachricht = "Fertig." & vbCrLf & vbCrLf 25. Nachricht = Nachricht & "Datei gespeichert als:" & vbCrLf 26. Nachricht = Nachricht & outputname 27. msgBox Nachricht,,"Ordnerstruktur2Text" 28. sub listFolder(folder) 29. ' Ordnerebene 30. ebene = ebene + 1 31. ' Fehlerbehandlung aus, falls Berechtigungen fehlen 32. on error resume next 33. ' keine Unterordner? Dann zurück 34. if folder.subfolders.count = 0 then 35. ebene = ebene - 1 36. on error goto 0 37. exit sub 38. end if 39. ' Unterordner auflisten und ggf. auch deren Unterordner ... 40. for each subfolder in folder.subfolders 41. ' mit Tabs einrücken 42. tabs = string(ebene, vbTab) 43. ordner = ordner & tabs & subfolder.name & vbCrLf 44. ' Rekursion durch Unterordner 45. listFolder(subfolder) 46. next 47. ' Fehlerbehandlung wieder an ... 48. on error goto 0 49. ' ... und eine Ebene hoch 50. ebene = ebene - 1 51. end sub
Erläuterung: Zeilen 1 bis 4 legen einige Werte fest (der Startordner wird vom Benutzer abgefragt). Der Zugriff auf das Dateisystem geschieht über das „Scripting.FileSystemObject“, über welches auch geprüft wird, ob es den Startordner überhaupt gibt (8). Falls nicht, wird unter „else“ das Skript mit Fehlermeldung beendet (16-18).
Ansonsten wird die Ausgabedatei angelegt (10/11), und es geht los mit dem Aufruf der eigentlichen Funktion (15).
Prozedur „listFolder“ (28-51): Um die ausgegebenen Ordner in der Textdatei passend einzurücken, wird in der Variablen „ebene“ die Schachtelungstiefe gemessen. Für jeden Unterordner wird sie um eins erhöht. Falls keine Zugriffsberechtigung auf einen Ordner vorhanden ist (Windows NT/2000/XP/.NET), würde das Skript abbrechen, daher wird hier die Fehlerbehandlung kurzerhand abgeschaltet (32). Sollte der aktuelle Ordner gar keine Unterordner haben, springt das Skript wieder zurück (34-38). Die Schleife in Zeilen 40 bis 46 erledigt die eigentliche Arbeit: Sie schreibt nicht nur den Namen jedes Unterordners in die Variable „Ordner“ (43), sondern sorgt über die „String“-Funktion für die passende Einrückung (pro Ebene ein Tabulator; 42). Der eigentliche Kniff folgt aber in Zeile 45: Für jeden Unterordner ruft die Funktion sich hier selbst auf und wandert so rekursiv durch alle Ordner, egal, wie tief (oder wie flach) die Verschachtelung ist. Sind alle Ordner einer Ebene durchlaufen, so schließt die Schleife ab und reduziert den Ebenenzähler wieder um eins (50).
Zum Abschluss wird die gefüllte Variable „Ordner“ in die Datei kopiert (21) und der Benutzer über deren Speicherort informiert (24-27).
Anlegen von Benutzern mit Telefonnummer (Beispiel, Teil 1)
In diesem ADSI-Beispiel (Active Directory Service Interface) geht es um folgendes Szenario:
Eine Firma bekommt eine neue Telefonanlage. Dadurch ändert sich bei allen Nutzern die Durchwahl. Dies manuell bei allen Benutzern zu ändern, wäre ein riesiger Aufwand. Per Skript erledigt dies eine kleine Schleife.
Dieses erste Skript dient nun dazu, Benutzerkonten mit einer Beispiel-Telefonnummer anzulegen, damit das eigentliche Beispiel angewandt werden kann:
1. dim rootDSE 2. set rootDSE = GetObject("LDAP://RootDSE") 3. domainname=rootDSE.Get("defaultnamingcontext") 4. set domain = GetObject("LDAP://" & domainname) 5. ' Neuen Benutzernamen erfragen 6. Nutzer = InputBox("Benutzername?") 7. ' Telefonnummer aufbauen: 4711 plus 1 Zufallsziffer 8. Telefon = "4711" 9. Randomize 10. Telefon = Telefon & CInt(rnd(1)*10) 11. ' User anlegen 12. Set usr = domain.Create("user", "CN=" & Nutzer & ",OU=Hardware") 13. usr.Put "samAccountName", Nutzer 14. usr.Put "userAccountControl", 512 15. usr.Put "telephoneNumber", Telefon 16. usr.SetInfo 17. usr.SetPassword "geheim"
Erläuterung: Hier wird ein einzelnes Benutzerkonto angelegt. Dazu wird, nach Verbindung mit der AD-Domäne (Zeilen 1 bis 4), ein Benutzername angefragt. Sodann wird eine (teilweise zufällige) Telefonnummer generiert (8 bis 10). Dann wird der Benutzer angelegt (12 bis 17), und zwar nur mit den hier wichtigen Eigenschaften.
Ändern der Telefonnummer bei allen Benutzern (Beispiel, Teil 2)
Hier der zweite Teil, in dem es ernst wird: Erweitere die Telefonnummer jedes Benutzers um das Präfix „123“.
>> Zur Erläuterung
1. set objRoot = GetObject("LDAP://rootDSE") 2. strDomain = objRoot.Get("DefaultNamingContext") 3. strOU = InputBox("Welche OU?") 4. strObjOU = "LDAP://OU=" & strOU & "," & strDomain 5. set objOU = GetObject(strObjOU) 6. for each varUser in objOU 7. set objUser = GetObject("LDAP://" & varUser.Name & ",OU=" & strOU & "," & strDomain) 8. objUser.GetInfo 9. strTelAlt = objUser.Get("telephoneNumber") 10. strTelNeu = "123" & strTelAlt 11. objUser.Put "telephoneNumber", strTelNeu 12. objUser.SetInfo 13. next
Erläuterung: Nach Verbindung zum AD (1/2) wird die OU abgefragt (3) und ausgelesen (4/5). Sodann geht eine Schleife alle Objekte durch (6-13), liest die Telefonnummer aus (9) und setzt das Präfix davor (10). Danach wird die Nummer geändert (11) und das Objekt geschrieben (12).
Das Skript setzt voraus, dass in der fraglichen OU nur Benutzerkonten liegen (keine Gruppen usw.) und dass jedes Konto eine Telefonnummer hat. Diese Einschränkung wurde aus Gründen der Übersichtlichkeit gemacht und ließe sich recht leicht umgehen (vgl. dazu das Beispiel „PasswordChanger“).
Anmerkung: Dieses Beispiel ist leicht variiert übernommen aus Michela/Palme: Active Directory, Microsoft Press 1999.
Ändern des Anmeldenamens
Eine Variante des Telefon-Beispiels ist dieses: Bei allen Benutzern einer anzugebenden OU soll der Anmeldename nach einem festen Schema geändert werden. Dabei sollen der Windows-2000-basierte Name (User principal Name, UPN) und der NT-basierte Name (SAM-Anmeldename) gleich sein. Eine solche Anforderung trifft man oft in Migrationsprojekten an, wo im Zuge der Migration von Benutzerkonten auch die Anmeldenamen vereinheitlicht werden sollen.
Das Schema, das hier genutzt wird, ist „Vorname.Nachname“. Falls diese Kombination länger ist als 20 Zeichen (was bei NT-Namen unzulässig wäre), wird sie gekürzt auf die ersten 20 Zeichen von „V.Nachname“. Umlaute werden dabei automatisch umgeschrieben.
Diese Lösung prüft allerdings nicht die Eindeutigkeit der Namen. Außerdem bricht das Skript ab, wenn es auf ein Objekt ohne Anmeldenamen stößt (etwa einen Kontakt). Das lässt sich aber leicht durch eine Prüfung verhindern.
>> Zur Erläuterung
1. ''''''''''''''''''''''''''''''''''''''''''''' 2. ' LogonNameÄndern.vbs 3. ' 4. ' Ändert den SAM-Logon-Namen und den UPN 5. ' von Benutzerkonten einer anzugebenden OU 6. ' ins Format "Vorname.Nachname". Umlaute 7. ' werden umgeschrieben. 8. ' 9. ' Von Nils@Kaczenski.de 10. ' 11. ' Keine Gewähr! Nutzung auf eigene Gefahr! 12. ' 13. ''''''''''''''''''''''''''''''''''''''''''''' 14. ' Verbindung zur Domäne 15. set objRoot = GetObject("LDAP://rootDSE") 16. strDomain = objRoot.Get("DefaultNamingContext") 17. ' Benutzer fragen ... 18. strOU = InputBox("Welche OU soll bearbeitet werden?") 19. strUPNDomain = InputBox("Wie soll die UPN-Domäne heißen?",,"@domain.dom") 20. ' Verbindungsstring 21. strObjOU = "LDAP://OU=" & strOU & "," & strDomain 22. ' OU ansprechen 23. set objOU = GetObject(strObjOU) 24. intZahl = 0 25. ' alle Objekte bearbeiten 26. ' muss evtl. auf geeignete Objekttypen beschränkt werden 27. for each varUser in objOU 28. set objUser = GetObject("LDAP://" & varUser.Name & ",OU=" & strOU & "," & strDomain) 29. objUser.GetInfo 30. strNachname = objUser.Get("sn") 31. strVorname = objUser.Get("givenName") 32. strNachname = Umlaute(strNachname) 33. strVorname = Umlaute(strVorname) 34. ' Länge auf 20 Zeichen begrenzen (SAM-Namen) 35. if len (strVorname & "." & strNachname) <= 20 then 36. strSAMNeu = strVorname & "." & strNachname 37. else 38. strSAMNeu = left(strVorname,1) & "." & left(strNachname,18) 39. end if 40. ' UPN zusammenbauen 41. strUPNNeu = strSAMNeu & strUPNDomain 42. ' Neue Namen schreiben 43. objUser.Put "sAMAccountName", strSAMNeu 44. objUser.Put "userPrincipalName", strUPNNeu 45. objUser.SetInfo 46. intZahl = intZahl + 1 47. next 48. ' Erfolgsmeldung ausgeben 49. msgBox intZahl & " Benutzer bearbeitet." 50. function Umlaute(strText) 51. ' Funktion: schreibt Umlaute und Sonderzeichen um 52. ' Eingabeparameter: strText: zu ändernder String 53. ' Kommentar: 54. strText = replace(strText, "Ä", "Ae") 55. strText = replace(strText, "Ö", "Oe") 56. strText = replace(strText, "Ü", "Ue") 57. strText = replace(strText, "ä", "ae") 58. strText = replace(strText, "ö", "oe") 59. strText = replace(strText, "ü", "ue") 60. strText = replace(strText, "ß", "ss") 61. Umlaute = strText 62. end function
Erläuterung: Die Zeilen 14 bis 23 tun dasselbe wie die Zeilen 1 bis 5 im vorigen Beispiel. Die Variable „intZahl“ zählt einfach mit (24, 46 und 49).
In den Zeilen 27 bis 47 wird eine ähnliche Schleife wie im vorigen Beispiel durchlaufen: Für jeden Benutzer werden Vor- und Nachname ausgelesen (30/31). In zwei Variablen wird der neue Name von Umlauten bereinigt (32/33), geprüft und dann zusammengebaut (34-41). Dann werden die beiden Namen in die Benutzereigenschaften geschrieben (43-45).
Die Funktion „Umlaute“ (50-62) bereinigt mit der „replace“-Funktion den übergebenen String um Umlaute.
Anlegen zahlreicher Benutzer zu Testzwecken
Dieses Skript ist in der Test- und Laborpraxis sehr nützlich: Es legt in einer gegebenen OU massenhaft Benutzerkonten an. Hier ohne weitere Attribute, aber das lässt sich ja leicht ergänzen.
>> Zur Erläuterung
1. dim rootDSE 2. set rootDSE = GetObject("LDAP://RootDSE") 3. domainname=rootDSE.Get("defaultnamingcontext") 4. set domain = GetObject("LDAP://" & domainname) 5. Anzahl = inputBox("Wie viele User?") 6. ZielOU = inputBox("In welcher OU sollen die User angelegt werden?") 7. randomize 8. Zufallszahl = Int(100 * rnd + 1) 9. i = 0 10. do until i > Anzahl - 1 11. MUsername = "MassenUser" & Zufallszahl & "-" & i 12. Set usr = domain.Create("user", "CN=" & MUsername & ", OU=" & ZielOU) 13. usr.Put "samAccountName", MUsername 14. usr.Put "userPrincipalName", MUsername & "@domain.dom" 15. usr.Put "userAccountControl", 512 16. usr.SetInfo 17. set usr = nothing 18. i = i+1 19. loop 20. set domain = nothing 21. if i>0 then 22. Erfolg = "Fertig: " & i & " User angelegt (Massenuser" 23. Erfolg = Erfolg & Zufallszahl & "-0 bis " & i-1 & ")." 24. else 25. Erfolg = "Keine User angelegt." 26. end if 27. msgBox(Erfolg)
Erläuterung: Auch hier wieder erst die Verbindung zum AD (1 bis 4). Dann Abfrage der Ziel-OU und der Anzahl gewünschter Benutzer (5/6). Damit man das Skript mehrfach anwenden kann, wird dann eine Zufallszahl zwischen 1 und 100 generiert, die hinterher an die Benutzernamen angehängt wird (7/8). Dann die eigentliche Schleife (9 bis 19): Aufbau des Nutzernamens mit Zufallszahl und fortlaufender Nummer („MassenUser17-1“; Zeile 11), dann Anlegen des Kontos in der Ziel-OU (12) und Angabe der wichtigen Attribute (13-15). Mit „SetInfo“ wird das Objekt dann vom Cache ins AD übertragen (16). Am Ende wird nur noch aufgefäumt (20) und der Benutzer über den Erfolg informiert (21-27).
Passwort bei allen Benutzern einer OU ändern (PasswordChanger)
Hier nun ein (wie ich finde) besonderes Bonbon: Manchmal muss man für eine größere Zahl von Benutzerkonten ein neues Passwort vergeben. Das kann vorkommen, wenn man einen Einbruch in die Domäne vermutet, aber auch, wenn man eine größere Zahl von Konten aus einer Textdatei importiert hat.
Dieses Skript generiert für alle Benutzer einer OU ein neues, zufälliges Passwort. Dabei wird dieses Passwort so aufgebaut, dass es auch strengen Richtlinien genügt („starkes“ Passwort aus vier Zeichensorten: Groß- und Kleinbuchstaben, Ziffern, Sonderzeichen). Damit man nun hinterher auch weiß, wer welches Passwort hat, werden die Passwörter in eine Datei geschrieben, die man dann z. B. mit Excel öffnen kann. Die Datei liegt im Ordner %systemroot%PWC; es empfiehlt sich, diesen Ordner mit Berechtigungen zu schützen.
>> Zur Erläuterung
1. ' Verbindung 2. set objRoot = GetObject("LDAP://rootDSE") 3. strDomain = objRoot.Get("DefaultNamingContext") 4. ' Zugriff aufs Dateisystem 5. set fso = CreateObject("Scripting.FileSystemObject") 6. with fso 7. systemPfad = .GetSpecialFolder(WindowsFolder) 8. dateiPfad = .BuildPath(systemPfad, "PWC") 9. if not .FolderExists(dateiPfad) then .CreateFolder(dateiPfad) 10. dateiName = "PWC" & CStr(Now) & ".csv" 11. dateiName = replace(dateiName, ":", "-") 12. pfadName = .BuildPath(dateiPfad, dateiName) 13. set datei = .CreateTextFile(pfadName) 14. end with 15. datei.WriteLine "DN,password" 16. ' OU-Auswahl 17. strOU = InputBox("Welche OU?") 18. strObjOU = "LDAP://OU=" & strOU & "," & strDomain 19. Laenge = InputBox("Wie lang soll das Passwort sein (min. 4 Zeichen)?") 20. set objOU = GetObject(strObjOU) 21. for each varUser in objOU 22. set objUser = GetObject("LDAP://" & varUser.Name & ",OU=" & strOU & "," & strDomain) 23. objTyp = objUser.GetEx("objectClass") 24. AttrZahl = UBound(objTyp) 25. objUserTest = objTyp(AttrZahl) 26. if objUserTest = "user" then SetzePasswort(Laenge) 27. next 28. MsgBox("Passwortdatei wird gespeichert unter: " & vbCrLf & pfadName) 29. datei.close 30. sub SetzePasswort(PLaenge) 31. objUser.GetInfo 32. objUName = objUser.Get("distinguishedName") 33. NeuPass = Kennwort(PLaenge) 34. objUser.SetPassword NeuPass 35. objUser.SetInfo 36. datei.WriteLine (chr(34) & objUName & chr(34) & "," & chr(34) & NeuPass & chr(34)) 37. end sub 38. function Kennwort(Anzahl) 39. if Anzahl > 128 then Anzahl = 128 40. Wort = "" 41. Wort = Zeichen(48, 57) ' Ziffern 42. Wort = Wort & Zeichen(65, 90) ' Großbuchstaben 43. Wort = Wort & Zeichen(97, 122) ' Kleinbuchstaben 44. Wort = Wort & Zeichen(33, 47) ' Satzzeichen 45. if Anzahl > 4 then 46. for i = 5 to Anzahl 47. Wort = Wort & Zeichen (33, 122) 'sonstige Zeichen 48. next 49. end if 50. Wort = Verschiebe(Wort) ' Zeichenfolge zufällig ändern 51. Kennwort = Wort 52. end function 53. function Zeichen(Anfang, Ende) 54. randomize 55. Zufall = Int((Ende - Anfang +1) * rnd + Anfang) 56. Zeichen = chr(Zufall) 57. end function 58. function Verschiebe(VWort) 59. WLaenge = len(VWort) 60. NeuWort = "" 61. ReDim WFeld (WLaenge) 62. for i = 1 to WLaenge 63. WFeld(i) = mid(VWort, i, 1) 64. next 65. zahl = 0 66. do until len(NeuWort) = WLaenge 67. randomize 68. j = Int((WLaenge) * rnd + 1) 69. if WFeld(j) <> "" then 70. NeuWort = NeuWort & WFeld(j) 71. WFeld(j) = "" 72. end if 73. zahl = zahl + 1 74. if zahl > 1000 then exit do ' Zur Sicherheit 75. loop 76. Verschiebe = NeuWort 77. end function
Erläuterung: Dieses Skript ist etwas komplexer; daher nur kurze Hinweise:
Verbindung zum AD (1/2), dann Anlegen der Passwortdatei (4 bis 15) als CSV-Datei. Dazu wird, falls noch nicht vorhanden, der Ordner %systemroot%\PWC angelegt (7 bis 9). Die Datei selbst wird mit Datum und Uhrzeit benannt, damit mehrere Durchgänge möglich sind (10 bis 13).
Abfrage von OU und Passwortlänge (17 bis 19), dann die wesentliche Schleife, die alle Objekte der Ziel-OU ausliest (21 bis 27). Hierbei werden nur Benutzerkonten angesprochen, was etwas umständliches Auslesen der Objektklasse erfordert, die bei „echten“ Benutzern mehrteilig ist (Computerkonten sind intern auch Benutzer, haben aber kein Passwort!). Das eigentliche Passwort wird dann mit der Prozedur „SetzePasswort“ erzeugt (26).
Prozedur „SetzePasswort“ (30 bis 37): Erwartet die Angabe der Passwortlänge. Liest den Benutzernamen aus (einzig zu dem Zweck, den Namen in die Passwortdatei zu schreiben; Zeile 32 und 36). Das Passwort selbst wiederum wird mit der Funktion „Kennwort“ generiert und dann geschrieben (33/34).
Funktion „Kennwort“ (38 bis 52): Erwartet die Angabe der Passwortlänge. Prüft erst die Maximallänge (39) und generiert dann die ersten vier Zeichen; eines pro Zeichensorte. Daraus erklärt sich die Minimallänge von 4 Zeichen (40 bis 44). Falls mehr als 4 Zeichen gewünscht sind, werden die weiteren Zeichen über die Funktion „Zeichen“ generiert. Dieser Aufbau stellt sicher, dass auf jeden Fall vier Zeichensorten auftauchen, auch wenn die weiteren Zeichen alle derselben Sorte entspringen sollten (45 bis 49). Danach wird mit der Funktion „Verschiebe“ die Reihenfolge der Zeichen noch mal per Zufall verschoben, damit nicht immer dieselbe Folge von Zeichensorten auftaucht (wäre ein Angriffspunkt; Zeile 50).
Funktion „Zeichen“ (53 bis 57): Erwartet die Angabe eines Zeichenbereichs (ASCII-Bereich) über das erste und das letzte ASCII-Zeichen. Dann wird aus dem gegebenen Bereich ein zufälliges Zeichen zurückgegeben.
Funktion „Verschiebe“ (58 bis 77): Erwartet ein Wort als Parameter. Die Funktion verschiebt alle Buchstaben des Wortes an zufällige neue Positionen. Für das Wort wird ein Array generiert, das in jedes Feld genau ein Zeichen des Wortes ablegt (59 bis 64). Dann wird dieses Array durchlaufen: Per Zufall wird ein Feld des Array ausgewählt (67/68). Wenn es nicht leer ist, wird das Zeichen aus diesem Feld an das neue Wort angehängt. Danach wird das Feld gelöscht (69 bis 72). Auf diese Weise ist sichergestellt, dass jedes Zeichen nur einmal im neuen Wort landet. Das wird so lange wiederholt, bis alle Zeichen des alten Wortes im neuen Wort gelandet sind. Da eine solche Schleife immer etwas gefährlich ist, wird sie nach spätestens 1000 Umdrehungen abgebrochen (73/74). Diese Funktion ist vielleicht nicht besonders elegant und mit Sicherheit nicht allzu performant. Sie skizziert aber, wie man in einer einfachen Skriptsprache ein solches Problem lösen kann. (Falls jemand Besseres vorzuschlagen hat, freue ich mich!)
http://faq-o-matic.net/?p=625