diff --git a/src/server/book.html b/src/server/book.html new file mode 100755 index 0000000..9a6da3a --- /dev/null +++ b/src/server/book.html @@ -0,0 +1,689 @@ + + + + + + + Terminvergabe - Daten eintragen - Service Berlin - Berlin.de + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+
+
+ + + +
+
+
+ + + +
+ + + + + +
+ +
+
+
+
+ + + + + + +
+
+
+
+ +
+
+ +
+
+ + +
+
+ +
+ + + +
+
+
+ +
+
+ + + + + + + + + +
+
+ + + + + +
+
+
+
+
+

Terminvereinbarung

+
+ + + +
+
+
+
1
+
Dienstleistung
+
+
    +
  • Abmeldung einer Wohnung
  • +
+ +
+ +
+
+ +
+ +
+
+
+
2
+
Standort
+
+ Bürgeramt Schöneberg - John-F.-Kennedy-Platz -, 10825 Berlin +
+ Zahlungshinweis:
+ Am Standort kann nur mit girocard (mit PIN) bezahlt werden. + + +
+ +
+
+ +
+ +
+
+
+
3
+
Datum
+
+ Fr. 10. Januar 2020 +
+ +
+
+ +
+ +
+
+
+
4
+
Uhrzeit
+
+ 08:00 +
+ +
+
+ +
+ +
+
+
+
5
+
Kundendaten & Buchung
+
+ +
+
+
+ +
+ + + + +

Bitte geben Sie Ihre Kundendaten an:

+
+
+
+
+ +
+ +
+
+
+ + +
+
+ +
+ +
+
+
+ + + + + + + + + + +
+ +
+ +
+
+ + +
+
+ + +

+ Ihre Daten werden gemäß der Datenschutzerklärung verarbeitet. +

+ + + +
+ +
+ +
+

+ * Pflichtfelder +

+ + Abbrechen +
+ +
+
+
+ + + + + + +
+ + +
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + +
+ + +
+ + + + + +
+
+ + + + + + + diff --git a/src/server/captcha/20191112_220643.png b/src/server/captcha/20191112_220643.png new file mode 100755 index 0000000..307368c Binary files /dev/null and b/src/server/captcha/20191112_220643.png differ diff --git a/src/server/captcha/20191112_220657.png b/src/server/captcha/20191112_220657.png new file mode 100755 index 0000000..e773ae4 Binary files /dev/null and b/src/server/captcha/20191112_220657.png differ diff --git a/src/server/captcha/20191112_220710.png b/src/server/captcha/20191112_220710.png new file mode 100755 index 0000000..caa21a7 Binary files /dev/null and b/src/server/captcha/20191112_220710.png differ diff --git a/src/server/captcha/20191112_220724.png b/src/server/captcha/20191112_220724.png new file mode 100755 index 0000000..296775d Binary files /dev/null and b/src/server/captcha/20191112_220724.png differ diff --git a/src/server/captcha/20191112_222730.png b/src/server/captcha/20191112_222730.png new file mode 100755 index 0000000..04218d3 Binary files /dev/null and b/src/server/captcha/20191112_222730.png differ diff --git a/src/server/captcha/20191112_222743.png b/src/server/captcha/20191112_222743.png new file mode 100755 index 0000000..3fb4e34 Binary files /dev/null and b/src/server/captcha/20191112_222743.png differ diff --git a/src/server/captcha/20191112_222748.png b/src/server/captcha/20191112_222748.png new file mode 100755 index 0000000..1b23600 Binary files /dev/null and b/src/server/captcha/20191112_222748.png differ diff --git a/src/server/captcha/20191112_222759.png b/src/server/captcha/20191112_222759.png new file mode 100755 index 0000000..f75ce77 Binary files /dev/null and b/src/server/captcha/20191112_222759.png differ diff --git a/src/server/captcha/20191112_224130.png b/src/server/captcha/20191112_224130.png new file mode 100755 index 0000000..1bfd507 Binary files /dev/null and b/src/server/captcha/20191112_224130.png differ diff --git a/src/server/captcha/20191112_224542.png b/src/server/captcha/20191112_224542.png new file mode 100755 index 0000000..b2ad7f9 Binary files /dev/null and b/src/server/captcha/20191112_224542.png differ diff --git a/src/server/captcha/20191113_001238.png b/src/server/captcha/20191113_001238.png new file mode 100755 index 0000000..6d3ee52 Binary files /dev/null and b/src/server/captcha/20191113_001238.png differ diff --git a/src/server/captcha/20191113_004229.png b/src/server/captcha/20191113_004229.png new file mode 100755 index 0000000..04fc2f0 Binary files /dev/null and b/src/server/captcha/20191113_004229.png differ diff --git a/src/server/captcha/20191113_004844.png b/src/server/captcha/20191113_004844.png new file mode 100755 index 0000000..2d53fa5 Binary files /dev/null and b/src/server/captcha/20191113_004844.png differ diff --git a/src/server/captcha/20191113_005727.png b/src/server/captcha/20191113_005727.png new file mode 100755 index 0000000..54aebad Binary files /dev/null and b/src/server/captcha/20191113_005727.png differ diff --git a/src/server/captcha/20191113_005740.png b/src/server/captcha/20191113_005740.png new file mode 100755 index 0000000..f977bed Binary files /dev/null and b/src/server/captcha/20191113_005740.png differ diff --git a/src/server/captcha/20191113_131311.png b/src/server/captcha/20191113_131311.png new file mode 100755 index 0000000..1d20a5e Binary files /dev/null and b/src/server/captcha/20191113_131311.png differ diff --git a/src/server/captcha/20191113_131316.png b/src/server/captcha/20191113_131316.png new file mode 100755 index 0000000..fe38072 Binary files /dev/null and b/src/server/captcha/20191113_131316.png differ diff --git a/src/server/captcha/20191113_131328.png b/src/server/captcha/20191113_131328.png new file mode 100755 index 0000000..e1e8bab Binary files /dev/null and b/src/server/captcha/20191113_131328.png differ diff --git a/src/server/captcha/20191113_155430.png b/src/server/captcha/20191113_155430.png new file mode 100755 index 0000000..bfec36b Binary files /dev/null and b/src/server/captcha/20191113_155430.png differ diff --git a/src/server/captcha/20191119_000912.png b/src/server/captcha/20191119_000912.png new file mode 100755 index 0000000..5747a62 Binary files /dev/null and b/src/server/captcha/20191119_000912.png differ diff --git a/src/server/captcha/20191119_001250.png b/src/server/captcha/20191119_001250.png new file mode 100755 index 0000000..22fa282 Binary files /dev/null and b/src/server/captcha/20191119_001250.png differ diff --git a/src/server/captcha/20191119_001553.png b/src/server/captcha/20191119_001553.png new file mode 100755 index 0000000..0178547 Binary files /dev/null and b/src/server/captcha/20191119_001553.png differ diff --git a/src/server/captcha/20191119_002129.png b/src/server/captcha/20191119_002129.png new file mode 100755 index 0000000..1b47044 Binary files /dev/null and b/src/server/captcha/20191119_002129.png differ diff --git a/src/server/captcha/20191119_002142.png b/src/server/captcha/20191119_002142.png new file mode 100755 index 0000000..8cc0494 Binary files /dev/null and b/src/server/captcha/20191119_002142.png differ diff --git a/src/server/captcha/20191119_004255.png b/src/server/captcha/20191119_004255.png new file mode 100755 index 0000000..4b538b3 Binary files /dev/null and b/src/server/captcha/20191119_004255.png differ diff --git a/src/server/captcha/20191119_004308.png b/src/server/captcha/20191119_004308.png new file mode 100755 index 0000000..88b1f13 Binary files /dev/null and b/src/server/captcha/20191119_004308.png differ diff --git a/src/server/captcha/20191119_004321.png b/src/server/captcha/20191119_004321.png new file mode 100755 index 0000000..070f01a Binary files /dev/null and b/src/server/captcha/20191119_004321.png differ diff --git a/src/server/captcha/20191119_004527.png b/src/server/captcha/20191119_004527.png new file mode 100755 index 0000000..8c7b753 Binary files /dev/null and b/src/server/captcha/20191119_004527.png differ diff --git a/src/server/captcha/20191119_004542.png b/src/server/captcha/20191119_004542.png new file mode 100755 index 0000000..4bdcb34 Binary files /dev/null and b/src/server/captcha/20191119_004542.png differ diff --git a/src/server/captcha/20191119_010320.png b/src/server/captcha/20191119_010320.png new file mode 100755 index 0000000..03074f2 Binary files /dev/null and b/src/server/captcha/20191119_010320.png differ diff --git a/src/server/captcha/20191119_012611.png b/src/server/captcha/20191119_012611.png new file mode 100755 index 0000000..b28ac16 Binary files /dev/null and b/src/server/captcha/20191119_012611.png differ diff --git a/src/server/captcha/20191119_012627.png b/src/server/captcha/20191119_012627.png new file mode 100755 index 0000000..61d6c9a Binary files /dev/null and b/src/server/captcha/20191119_012627.png differ diff --git a/src/server/captcha/20191119_140157.png b/src/server/captcha/20191119_140157.png new file mode 100755 index 0000000..eafdd10 Binary files /dev/null and b/src/server/captcha/20191119_140157.png differ diff --git a/src/server/captcha/20191119_140211.png b/src/server/captcha/20191119_140211.png new file mode 100755 index 0000000..eef67d7 Binary files /dev/null and b/src/server/captcha/20191119_140211.png differ diff --git a/src/server/captcha/20191119_144530.png b/src/server/captcha/20191119_144530.png new file mode 100755 index 0000000..bc0287c Binary files /dev/null and b/src/server/captcha/20191119_144530.png differ diff --git a/src/server/captcha/20191119_153237.png b/src/server/captcha/20191119_153237.png new file mode 100755 index 0000000..4a00d78 Binary files /dev/null and b/src/server/captcha/20191119_153237.png differ diff --git a/src/server/captcha/20191119_153640.png b/src/server/captcha/20191119_153640.png new file mode 100755 index 0000000..0bcb16a Binary files /dev/null and b/src/server/captcha/20191119_153640.png differ diff --git a/src/server/captcha/20191119_153657.png b/src/server/captcha/20191119_153657.png new file mode 100755 index 0000000..16796de Binary files /dev/null and b/src/server/captcha/20191119_153657.png differ diff --git a/src/server/captcha/20191119_154544.png b/src/server/captcha/20191119_154544.png new file mode 100755 index 0000000..636807c Binary files /dev/null and b/src/server/captcha/20191119_154544.png differ diff --git a/src/server/captcha/20191119_154750.png b/src/server/captcha/20191119_154750.png new file mode 100755 index 0000000..d263052 Binary files /dev/null and b/src/server/captcha/20191119_154750.png differ diff --git a/src/server/captcha/20191119_154805.png b/src/server/captcha/20191119_154805.png new file mode 100755 index 0000000..bc30d62 Binary files /dev/null and b/src/server/captcha/20191119_154805.png differ diff --git a/src/server/captcha/20191119_154817.png b/src/server/captcha/20191119_154817.png new file mode 100755 index 0000000..e222ad6 Binary files /dev/null and b/src/server/captcha/20191119_154817.png differ diff --git a/src/server/captcha/20191119_154830.png b/src/server/captcha/20191119_154830.png new file mode 100755 index 0000000..5f12819 Binary files /dev/null and b/src/server/captcha/20191119_154830.png differ diff --git a/src/server/captcha/20191119_154843.png b/src/server/captcha/20191119_154843.png new file mode 100755 index 0000000..188499c Binary files /dev/null and b/src/server/captcha/20191119_154843.png differ diff --git a/src/server/captcha/20191119_155132.png b/src/server/captcha/20191119_155132.png new file mode 100755 index 0000000..90da147 Binary files /dev/null and b/src/server/captcha/20191119_155132.png differ diff --git a/src/server/captcha/20191119_155308.png b/src/server/captcha/20191119_155308.png new file mode 100755 index 0000000..bdfdd2c Binary files /dev/null and b/src/server/captcha/20191119_155308.png differ diff --git a/src/server/captcha/20191119_155324.png b/src/server/captcha/20191119_155324.png new file mode 100755 index 0000000..5888faa Binary files /dev/null and b/src/server/captcha/20191119_155324.png differ diff --git a/src/server/captcha/20191119_161605.png b/src/server/captcha/20191119_161605.png new file mode 100755 index 0000000..aa220ae Binary files /dev/null and b/src/server/captcha/20191119_161605.png differ diff --git a/src/server/captcha/20191119_161931.png b/src/server/captcha/20191119_161931.png new file mode 100755 index 0000000..7fe098a Binary files /dev/null and b/src/server/captcha/20191119_161931.png differ diff --git a/src/server/captcha/20191119_162121.png b/src/server/captcha/20191119_162121.png new file mode 100755 index 0000000..6a9a812 Binary files /dev/null and b/src/server/captcha/20191119_162121.png differ diff --git a/src/server/captcha/20191119_162135.png b/src/server/captcha/20191119_162135.png new file mode 100755 index 0000000..164c938 Binary files /dev/null and b/src/server/captcha/20191119_162135.png differ diff --git a/src/server/captcha/20191119_162757.png b/src/server/captcha/20191119_162757.png new file mode 100755 index 0000000..bcb1916 Binary files /dev/null and b/src/server/captcha/20191119_162757.png differ diff --git a/src/server/captcha/20191119_162809.png b/src/server/captcha/20191119_162809.png new file mode 100755 index 0000000..0138231 Binary files /dev/null and b/src/server/captcha/20191119_162809.png differ diff --git a/src/server/captcha/20191119_163620.png b/src/server/captcha/20191119_163620.png new file mode 100755 index 0000000..87dce0f Binary files /dev/null and b/src/server/captcha/20191119_163620.png differ diff --git a/src/server/captcha/20191119_164255.png b/src/server/captcha/20191119_164255.png new file mode 100755 index 0000000..c45b043 Binary files /dev/null and b/src/server/captcha/20191119_164255.png differ diff --git a/src/server/captcha/20191119_164458.png b/src/server/captcha/20191119_164458.png new file mode 100755 index 0000000..0a6fa47 Binary files /dev/null and b/src/server/captcha/20191119_164458.png differ diff --git a/src/server/captcha/20191119_164513.png b/src/server/captcha/20191119_164513.png new file mode 100755 index 0000000..10feff6 Binary files /dev/null and b/src/server/captcha/20191119_164513.png differ diff --git a/src/server/captcha/20191121_105618.png b/src/server/captcha/20191121_105618.png new file mode 100755 index 0000000..ed26640 Binary files /dev/null and b/src/server/captcha/20191121_105618.png differ diff --git a/src/server/captcha/20191121_110012.png b/src/server/captcha/20191121_110012.png new file mode 100755 index 0000000..504a6b6 Binary files /dev/null and b/src/server/captcha/20191121_110012.png differ diff --git a/src/server/captcha/20191121_110204.png b/src/server/captcha/20191121_110204.png new file mode 100755 index 0000000..b39dd58 Binary files /dev/null and b/src/server/captcha/20191121_110204.png differ diff --git a/src/server/captcha/20191121_110515.png b/src/server/captcha/20191121_110515.png new file mode 100755 index 0000000..5546aeb Binary files /dev/null and b/src/server/captcha/20191121_110515.png differ diff --git a/src/server/captcha/20191121_111346.png b/src/server/captcha/20191121_111346.png new file mode 100755 index 0000000..24752f9 Binary files /dev/null and b/src/server/captcha/20191121_111346.png differ diff --git a/src/server/captcha/20191121_111421.png b/src/server/captcha/20191121_111421.png new file mode 100755 index 0000000..694976b Binary files /dev/null and b/src/server/captcha/20191121_111421.png differ diff --git a/src/server/captcha/20191121_111433.png b/src/server/captcha/20191121_111433.png new file mode 100755 index 0000000..3c0738b Binary files /dev/null and b/src/server/captcha/20191121_111433.png differ diff --git a/src/server/captcha/20191121_111447.png b/src/server/captcha/20191121_111447.png new file mode 100755 index 0000000..3be42ce Binary files /dev/null and b/src/server/captcha/20191121_111447.png differ diff --git a/src/server/captcha/20191121_111505.png b/src/server/captcha/20191121_111505.png new file mode 100755 index 0000000..39188cd Binary files /dev/null and b/src/server/captcha/20191121_111505.png differ diff --git a/src/server/captcha/20191121_111519.png b/src/server/captcha/20191121_111519.png new file mode 100755 index 0000000..c47b243 Binary files /dev/null and b/src/server/captcha/20191121_111519.png differ diff --git a/src/server/captcha/20191121_131115.png b/src/server/captcha/20191121_131115.png new file mode 100755 index 0000000..405f546 Binary files /dev/null and b/src/server/captcha/20191121_131115.png differ diff --git a/src/server/captcha/20191121_131122.png b/src/server/captcha/20191121_131122.png new file mode 100755 index 0000000..739ddb0 Binary files /dev/null and b/src/server/captcha/20191121_131122.png differ diff --git a/src/server/captcha/20191121_132346.png b/src/server/captcha/20191121_132346.png new file mode 100755 index 0000000..13ec4b9 Binary files /dev/null and b/src/server/captcha/20191121_132346.png differ diff --git a/src/server/captcha/20191121_132349.png b/src/server/captcha/20191121_132349.png new file mode 100755 index 0000000..89a813c Binary files /dev/null and b/src/server/captcha/20191121_132349.png differ diff --git a/src/server/captcha/20191121_132356.png b/src/server/captcha/20191121_132356.png new file mode 100755 index 0000000..dbf9ef8 Binary files /dev/null and b/src/server/captcha/20191121_132356.png differ diff --git a/src/server/captcha/20191121_132409.png b/src/server/captcha/20191121_132409.png new file mode 100755 index 0000000..ec93ca4 Binary files /dev/null and b/src/server/captcha/20191121_132409.png differ diff --git a/src/server/captcha/20191121_132419.png b/src/server/captcha/20191121_132419.png new file mode 100755 index 0000000..31388a1 Binary files /dev/null and b/src/server/captcha/20191121_132419.png differ diff --git a/src/server/captcha/20191121_134906.png b/src/server/captcha/20191121_134906.png new file mode 100755 index 0000000..98cfb12 Binary files /dev/null and b/src/server/captcha/20191121_134906.png differ diff --git a/src/server/captcha/20191121_135157.png b/src/server/captcha/20191121_135157.png new file mode 100755 index 0000000..8b8d40a Binary files /dev/null and b/src/server/captcha/20191121_135157.png differ diff --git a/src/server/captcha/20191121_135209.png b/src/server/captcha/20191121_135209.png new file mode 100755 index 0000000..12c0b4e Binary files /dev/null and b/src/server/captcha/20191121_135209.png differ diff --git a/src/server/captcha/20191121_135506.png b/src/server/captcha/20191121_135506.png new file mode 100755 index 0000000..d50a0c6 Binary files /dev/null and b/src/server/captcha/20191121_135506.png differ diff --git a/src/server/captcha/20191121_135509.png b/src/server/captcha/20191121_135509.png new file mode 100755 index 0000000..253ee7e Binary files /dev/null and b/src/server/captcha/20191121_135509.png differ diff --git a/src/server/captcha/20191121_135512.png b/src/server/captcha/20191121_135512.png new file mode 100755 index 0000000..38020bc Binary files /dev/null and b/src/server/captcha/20191121_135512.png differ diff --git a/src/server/captcha/20191121_135514.png b/src/server/captcha/20191121_135514.png new file mode 100755 index 0000000..c406732 Binary files /dev/null and b/src/server/captcha/20191121_135514.png differ diff --git a/src/server/captcha/20191121_140201.png b/src/server/captcha/20191121_140201.png new file mode 100755 index 0000000..942c7cd Binary files /dev/null and b/src/server/captcha/20191121_140201.png differ diff --git a/src/server/captcha/20191121_140224.png b/src/server/captcha/20191121_140224.png new file mode 100755 index 0000000..2b06827 Binary files /dev/null and b/src/server/captcha/20191121_140224.png differ diff --git a/src/server/captcha/20191121_140655.png b/src/server/captcha/20191121_140655.png new file mode 100755 index 0000000..e08b2c7 Binary files /dev/null and b/src/server/captcha/20191121_140655.png differ diff --git a/src/server/captcha/20191121_140829.png b/src/server/captcha/20191121_140829.png new file mode 100755 index 0000000..21b8139 Binary files /dev/null and b/src/server/captcha/20191121_140829.png differ diff --git a/src/server/captcha/20191121_140834.png b/src/server/captcha/20191121_140834.png new file mode 100755 index 0000000..aa6dfab Binary files /dev/null and b/src/server/captcha/20191121_140834.png differ diff --git a/src/server/captcha/20191121_140840.png b/src/server/captcha/20191121_140840.png new file mode 100755 index 0000000..70ac6a3 Binary files /dev/null and b/src/server/captcha/20191121_140840.png differ diff --git a/src/server/captcha/20191121_141047.png b/src/server/captcha/20191121_141047.png new file mode 100755 index 0000000..ac9343c Binary files /dev/null and b/src/server/captcha/20191121_141047.png differ diff --git a/src/server/captcha/20191121_141052.png b/src/server/captcha/20191121_141052.png new file mode 100755 index 0000000..3d7e769 Binary files /dev/null and b/src/server/captcha/20191121_141052.png differ diff --git a/src/server/captcha/20191121_141242.png b/src/server/captcha/20191121_141242.png new file mode 100755 index 0000000..7d14fe3 Binary files /dev/null and b/src/server/captcha/20191121_141242.png differ diff --git a/src/server/captcha/20191121_141247.png b/src/server/captcha/20191121_141247.png new file mode 100755 index 0000000..45247ed Binary files /dev/null and b/src/server/captcha/20191121_141247.png differ diff --git a/src/server/captcha/20191121_142850.png b/src/server/captcha/20191121_142850.png new file mode 100755 index 0000000..e9448ef Binary files /dev/null and b/src/server/captcha/20191121_142850.png differ diff --git a/src/server/captcha/20191121_143233.png b/src/server/captcha/20191121_143233.png new file mode 100755 index 0000000..7b46188 Binary files /dev/null and b/src/server/captcha/20191121_143233.png differ diff --git a/src/server/captcha/20191121_143238.png b/src/server/captcha/20191121_143238.png new file mode 100755 index 0000000..fede91e Binary files /dev/null and b/src/server/captcha/20191121_143238.png differ diff --git a/src/server/captcha/20191121_143243.png b/src/server/captcha/20191121_143243.png new file mode 100755 index 0000000..147f4a8 Binary files /dev/null and b/src/server/captcha/20191121_143243.png differ diff --git a/src/server/captcha/20191121_143256.png b/src/server/captcha/20191121_143256.png new file mode 100755 index 0000000..8efb2f0 Binary files /dev/null and b/src/server/captcha/20191121_143256.png differ diff --git a/src/server/captcha/20191121_143655.png b/src/server/captcha/20191121_143655.png new file mode 100755 index 0000000..315a616 Binary files /dev/null and b/src/server/captcha/20191121_143655.png differ diff --git a/src/server/captcha/20191121_143701.png b/src/server/captcha/20191121_143701.png new file mode 100755 index 0000000..3913abc Binary files /dev/null and b/src/server/captcha/20191121_143701.png differ diff --git a/src/server/captcha/20191121_143853.png b/src/server/captcha/20191121_143853.png new file mode 100755 index 0000000..81c6136 Binary files /dev/null and b/src/server/captcha/20191121_143853.png differ diff --git a/src/server/captcha/20191121_143900.png b/src/server/captcha/20191121_143900.png new file mode 100755 index 0000000..f96c868 Binary files /dev/null and b/src/server/captcha/20191121_143900.png differ diff --git a/src/server/captcha/20191121_144122.png b/src/server/captcha/20191121_144122.png new file mode 100755 index 0000000..4649428 Binary files /dev/null and b/src/server/captcha/20191121_144122.png differ diff --git a/src/server/captcha/20191121_144138.png b/src/server/captcha/20191121_144138.png new file mode 100755 index 0000000..16c437f Binary files /dev/null and b/src/server/captcha/20191121_144138.png differ diff --git a/src/server/captcha/20191121_144152.png b/src/server/captcha/20191121_144152.png new file mode 100755 index 0000000..e7a3c87 Binary files /dev/null and b/src/server/captcha/20191121_144152.png differ diff --git a/src/server/captcha/20191121_144207.png b/src/server/captcha/20191121_144207.png new file mode 100755 index 0000000..9f89fd3 Binary files /dev/null and b/src/server/captcha/20191121_144207.png differ diff --git a/src/server/captcha/20191121_144343.png b/src/server/captcha/20191121_144343.png new file mode 100755 index 0000000..3b490d0 Binary files /dev/null and b/src/server/captcha/20191121_144343.png differ diff --git a/src/server/captcha/20191121_150346.png b/src/server/captcha/20191121_150346.png new file mode 100755 index 0000000..68bb409 Binary files /dev/null and b/src/server/captcha/20191121_150346.png differ diff --git a/src/server/captcha/20191121_150359.png b/src/server/captcha/20191121_150359.png new file mode 100755 index 0000000..e4639dd Binary files /dev/null and b/src/server/captcha/20191121_150359.png differ diff --git a/src/server/captcha/20191121_152648.png b/src/server/captcha/20191121_152648.png new file mode 100755 index 0000000..748ad39 Binary files /dev/null and b/src/server/captcha/20191121_152648.png differ diff --git a/src/server/captcha/20191121_162349.png b/src/server/captcha/20191121_162349.png new file mode 100755 index 0000000..0bf7194 Binary files /dev/null and b/src/server/captcha/20191121_162349.png differ diff --git a/src/server/captcha/20191121_162557.png b/src/server/captcha/20191121_162557.png new file mode 100755 index 0000000..34ffba5 Binary files /dev/null and b/src/server/captcha/20191121_162557.png differ diff --git a/src/server/captcha/20191121_162610.png b/src/server/captcha/20191121_162610.png new file mode 100755 index 0000000..702dfce Binary files /dev/null and b/src/server/captcha/20191121_162610.png differ diff --git a/src/server/captcha/20191121_172940.png b/src/server/captcha/20191121_172940.png new file mode 100755 index 0000000..ed01199 Binary files /dev/null and b/src/server/captcha/20191121_172940.png differ diff --git a/src/server/captcha/20191121_173218.png b/src/server/captcha/20191121_173218.png new file mode 100755 index 0000000..6e254e2 Binary files /dev/null and b/src/server/captcha/20191121_173218.png differ diff --git a/src/server/captcha/20191121_173224.png b/src/server/captcha/20191121_173224.png new file mode 100755 index 0000000..2d0f290 Binary files /dev/null and b/src/server/captcha/20191121_173224.png differ diff --git a/src/server/captcha/20191121_173236.png b/src/server/captcha/20191121_173236.png new file mode 100755 index 0000000..4868d56 Binary files /dev/null and b/src/server/captcha/20191121_173236.png differ diff --git a/src/server/captcha/20191121_173250.png b/src/server/captcha/20191121_173250.png new file mode 100755 index 0000000..c99755d Binary files /dev/null and b/src/server/captcha/20191121_173250.png differ diff --git a/src/server/captcha/20191121_173413.png b/src/server/captcha/20191121_173413.png new file mode 100755 index 0000000..3a04c08 Binary files /dev/null and b/src/server/captcha/20191121_173413.png differ diff --git a/src/server/captcha/20191121_173631.png b/src/server/captcha/20191121_173631.png new file mode 100755 index 0000000..5ea9618 Binary files /dev/null and b/src/server/captcha/20191121_173631.png differ diff --git a/src/server/captcha/20191121_173643.png b/src/server/captcha/20191121_173643.png new file mode 100755 index 0000000..be7cd7e Binary files /dev/null and b/src/server/captcha/20191121_173643.png differ diff --git a/src/server/captcha/20191121_173656.png b/src/server/captcha/20191121_173656.png new file mode 100755 index 0000000..213b71e Binary files /dev/null and b/src/server/captcha/20191121_173656.png differ diff --git a/src/server/captcha/20191121_174900.png b/src/server/captcha/20191121_174900.png new file mode 100755 index 0000000..11b7a4a Binary files /dev/null and b/src/server/captcha/20191121_174900.png differ diff --git a/src/server/captcha/20191121_175231.png b/src/server/captcha/20191121_175231.png new file mode 100755 index 0000000..d92a579 Binary files /dev/null and b/src/server/captcha/20191121_175231.png differ diff --git a/src/server/captcha/20191121_200659.png b/src/server/captcha/20191121_200659.png new file mode 100755 index 0000000..e8c1e2e Binary files /dev/null and b/src/server/captcha/20191121_200659.png differ diff --git a/src/server/captcha/20191121_200711.png b/src/server/captcha/20191121_200711.png new file mode 100755 index 0000000..223f3d9 Binary files /dev/null and b/src/server/captcha/20191121_200711.png differ diff --git a/src/server/captcha/20191121_205640.png b/src/server/captcha/20191121_205640.png new file mode 100755 index 0000000..c72b66b Binary files /dev/null and b/src/server/captcha/20191121_205640.png differ diff --git a/src/server/captcha/20191121_232046.png b/src/server/captcha/20191121_232046.png new file mode 100755 index 0000000..d118db0 Binary files /dev/null and b/src/server/captcha/20191121_232046.png differ diff --git a/src/server/captcha/20191121_232102.png b/src/server/captcha/20191121_232102.png new file mode 100755 index 0000000..e50927f Binary files /dev/null and b/src/server/captcha/20191121_232102.png differ diff --git a/src/server/captcha/20191122_002856.png b/src/server/captcha/20191122_002856.png new file mode 100755 index 0000000..96aa60b Binary files /dev/null and b/src/server/captcha/20191122_002856.png differ diff --git a/src/server/captcha/20191122_003817.png b/src/server/captcha/20191122_003817.png new file mode 100755 index 0000000..aaba55a Binary files /dev/null and b/src/server/captcha/20191122_003817.png differ diff --git a/src/server/captcha/20191122_003829.png b/src/server/captcha/20191122_003829.png new file mode 100755 index 0000000..8bbb0f7 Binary files /dev/null and b/src/server/captcha/20191122_003829.png differ diff --git a/src/server/captcha/20191123_133658.png b/src/server/captcha/20191123_133658.png new file mode 100755 index 0000000..f9802f9 Binary files /dev/null and b/src/server/captcha/20191123_133658.png differ diff --git a/src/server/captcha/20191123_135753.png b/src/server/captcha/20191123_135753.png new file mode 100755 index 0000000..33dda9e Binary files /dev/null and b/src/server/captcha/20191123_135753.png differ diff --git a/src/server/captcha/20191123_140038.png b/src/server/captcha/20191123_140038.png new file mode 100755 index 0000000..eeae45c Binary files /dev/null and b/src/server/captcha/20191123_140038.png differ diff --git a/src/server/captcha/20191123_140729.png b/src/server/captcha/20191123_140729.png new file mode 100755 index 0000000..7133277 Binary files /dev/null and b/src/server/captcha/20191123_140729.png differ diff --git a/src/server/captcha/20191123_141046.png b/src/server/captcha/20191123_141046.png new file mode 100755 index 0000000..eaf1cd2 Binary files /dev/null and b/src/server/captcha/20191123_141046.png differ diff --git a/src/server/captcha/20191123_143031.png b/src/server/captcha/20191123_143031.png new file mode 100755 index 0000000..91d315c Binary files /dev/null and b/src/server/captcha/20191123_143031.png differ diff --git a/src/server/captcha/20191123_143109.png b/src/server/captcha/20191123_143109.png new file mode 100755 index 0000000..ba23d3b Binary files /dev/null and b/src/server/captcha/20191123_143109.png differ diff --git a/src/server/captcha/20191123_143147.png b/src/server/captcha/20191123_143147.png new file mode 100755 index 0000000..bb265b0 Binary files /dev/null and b/src/server/captcha/20191123_143147.png differ diff --git a/src/server/captcha/20191123_143321.png b/src/server/captcha/20191123_143321.png new file mode 100755 index 0000000..490251e Binary files /dev/null and b/src/server/captcha/20191123_143321.png differ diff --git a/src/server/captcha/20191123_143328.png b/src/server/captcha/20191123_143328.png new file mode 100755 index 0000000..10bc774 Binary files /dev/null and b/src/server/captcha/20191123_143328.png differ diff --git a/src/server/captcha/20191123_143728.png b/src/server/captcha/20191123_143728.png new file mode 100755 index 0000000..c497a3f Binary files /dev/null and b/src/server/captcha/20191123_143728.png differ diff --git a/src/server/captcha/20191123_143741.png b/src/server/captcha/20191123_143741.png new file mode 100755 index 0000000..25d588a Binary files /dev/null and b/src/server/captcha/20191123_143741.png differ diff --git a/src/server/captcha/20191123_143755.png b/src/server/captcha/20191123_143755.png new file mode 100755 index 0000000..f73349c Binary files /dev/null and b/src/server/captcha/20191123_143755.png differ diff --git a/src/server/captcha/20191123_143808.png b/src/server/captcha/20191123_143808.png new file mode 100755 index 0000000..ae61c07 Binary files /dev/null and b/src/server/captcha/20191123_143808.png differ diff --git a/src/server/captcha/20191123_143822.png b/src/server/captcha/20191123_143822.png new file mode 100755 index 0000000..cbf0650 Binary files /dev/null and b/src/server/captcha/20191123_143822.png differ diff --git a/src/server/captcha/20191123_143833.png b/src/server/captcha/20191123_143833.png new file mode 100755 index 0000000..87df364 Binary files /dev/null and b/src/server/captcha/20191123_143833.png differ diff --git a/src/server/captcha/20191123_143851.png b/src/server/captcha/20191123_143851.png new file mode 100755 index 0000000..7725251 Binary files /dev/null and b/src/server/captcha/20191123_143851.png differ diff --git a/src/server/captcha/20191123_143857.png b/src/server/captcha/20191123_143857.png new file mode 100755 index 0000000..58d2f41 Binary files /dev/null and b/src/server/captcha/20191123_143857.png differ diff --git a/src/server/captcha/20191123_144221.png b/src/server/captcha/20191123_144221.png new file mode 100755 index 0000000..075c757 Binary files /dev/null and b/src/server/captcha/20191123_144221.png differ diff --git a/src/server/captcha/20191123_144514.png b/src/server/captcha/20191123_144514.png new file mode 100755 index 0000000..fe7f8d7 Binary files /dev/null and b/src/server/captcha/20191123_144514.png differ diff --git a/src/server/captcha/20191123_144938.png b/src/server/captcha/20191123_144938.png new file mode 100755 index 0000000..5a0b2b1 Binary files /dev/null and b/src/server/captcha/20191123_144938.png differ diff --git a/src/server/captcha/20191123_144956.png b/src/server/captcha/20191123_144956.png new file mode 100755 index 0000000..00461d8 Binary files /dev/null and b/src/server/captcha/20191123_144956.png differ diff --git a/src/server/captcha/20191123_164553.png b/src/server/captcha/20191123_164553.png new file mode 100755 index 0000000..b7b47e6 Binary files /dev/null and b/src/server/captcha/20191123_164553.png differ diff --git a/src/server/captcha/20191125_205020.png b/src/server/captcha/20191125_205020.png new file mode 100755 index 0000000..028861e Binary files /dev/null and b/src/server/captcha/20191125_205020.png differ diff --git a/src/server/captcha/20191125_221649.png b/src/server/captcha/20191125_221649.png new file mode 100755 index 0000000..015073c Binary files /dev/null and b/src/server/captcha/20191125_221649.png differ diff --git a/src/server/captcha/20191125_232414.png b/src/server/captcha/20191125_232414.png new file mode 100755 index 0000000..10f3de6 Binary files /dev/null and b/src/server/captcha/20191125_232414.png differ diff --git a/src/server/captcha/20191126_015135.png b/src/server/captcha/20191126_015135.png new file mode 100755 index 0000000..53d6f68 Binary files /dev/null and b/src/server/captcha/20191126_015135.png differ diff --git a/src/server/captcha/20191126_015148.png b/src/server/captcha/20191126_015148.png new file mode 100755 index 0000000..2e2bc10 Binary files /dev/null and b/src/server/captcha/20191126_015148.png differ diff --git a/src/server/captcha/20191126_022405.png b/src/server/captcha/20191126_022405.png new file mode 100755 index 0000000..3477d5f Binary files /dev/null and b/src/server/captcha/20191126_022405.png differ diff --git a/src/server/captcha/20191126_022515.png b/src/server/captcha/20191126_022515.png new file mode 100755 index 0000000..135ab60 Binary files /dev/null and b/src/server/captcha/20191126_022515.png differ diff --git a/src/server/captcha/20191126_025850.png b/src/server/captcha/20191126_025850.png new file mode 100755 index 0000000..cd62131 Binary files /dev/null and b/src/server/captcha/20191126_025850.png differ diff --git a/src/server/captcha/20191126_170219.png b/src/server/captcha/20191126_170219.png new file mode 100755 index 0000000..a0cfe84 Binary files /dev/null and b/src/server/captcha/20191126_170219.png differ diff --git a/src/server/captcha/20191126_173630.png b/src/server/captcha/20191126_173630.png new file mode 100755 index 0000000..1e0642a Binary files /dev/null and b/src/server/captcha/20191126_173630.png differ diff --git a/src/server/captcha/20191126_180857.png b/src/server/captcha/20191126_180857.png new file mode 100755 index 0000000..893943b Binary files /dev/null and b/src/server/captcha/20191126_180857.png differ diff --git a/src/server/captcha/20191126_181843.png b/src/server/captcha/20191126_181843.png new file mode 100755 index 0000000..46031d5 Binary files /dev/null and b/src/server/captcha/20191126_181843.png differ diff --git a/src/server/captcha/20191126_181903.png b/src/server/captcha/20191126_181903.png new file mode 100755 index 0000000..8c3c742 Binary files /dev/null and b/src/server/captcha/20191126_181903.png differ diff --git a/src/server/captcha/20191126_181928.png b/src/server/captcha/20191126_181928.png new file mode 100755 index 0000000..34f4d11 Binary files /dev/null and b/src/server/captcha/20191126_181928.png differ diff --git a/src/server/captcha/20191126_185050.png b/src/server/captcha/20191126_185050.png new file mode 100755 index 0000000..7bba8eb Binary files /dev/null and b/src/server/captcha/20191126_185050.png differ diff --git a/src/server/captcha/20191126_191212.png b/src/server/captcha/20191126_191212.png new file mode 100755 index 0000000..d303169 Binary files /dev/null and b/src/server/captcha/20191126_191212.png differ diff --git a/src/server/captcha/20191126_194335.png b/src/server/captcha/20191126_194335.png new file mode 100755 index 0000000..7333e70 Binary files /dev/null and b/src/server/captcha/20191126_194335.png differ diff --git a/src/server/captcha/20191126_201557.png b/src/server/captcha/20191126_201557.png new file mode 100755 index 0000000..10ec038 Binary files /dev/null and b/src/server/captcha/20191126_201557.png differ diff --git a/src/server/captcha/20191126_222206.png b/src/server/captcha/20191126_222206.png new file mode 100755 index 0000000..be90a8a Binary files /dev/null and b/src/server/captcha/20191126_222206.png differ diff --git a/src/server/captcha/20191126_222222.png b/src/server/captcha/20191126_222222.png new file mode 100755 index 0000000..d566bdc Binary files /dev/null and b/src/server/captcha/20191126_222222.png differ diff --git a/src/server/captcha/20191126_222236.png b/src/server/captcha/20191126_222236.png new file mode 100755 index 0000000..56c12ec Binary files /dev/null and b/src/server/captcha/20191126_222236.png differ diff --git a/src/server/captcha/20191126_222248.png b/src/server/captcha/20191126_222248.png new file mode 100755 index 0000000..f15a3e5 Binary files /dev/null and b/src/server/captcha/20191126_222248.png differ diff --git a/src/server/captcha/20191126_222300.png b/src/server/captcha/20191126_222300.png new file mode 100755 index 0000000..c97baab Binary files /dev/null and b/src/server/captcha/20191126_222300.png differ diff --git a/src/server/captcha/20191126_225447.png b/src/server/captcha/20191126_225447.png new file mode 100755 index 0000000..241f352 Binary files /dev/null and b/src/server/captcha/20191126_225447.png differ diff --git a/src/server/captcha/20191127_014354.png b/src/server/captcha/20191127_014354.png new file mode 100755 index 0000000..5b71687 Binary files /dev/null and b/src/server/captcha/20191127_014354.png differ diff --git a/src/server/captcha/20191127_021615.png b/src/server/captcha/20191127_021615.png new file mode 100755 index 0000000..6320480 Binary files /dev/null and b/src/server/captcha/20191127_021615.png differ diff --git a/src/server/captcha/20191127_030455.png b/src/server/captcha/20191127_030455.png new file mode 100755 index 0000000..9c75ee0 Binary files /dev/null and b/src/server/captcha/20191127_030455.png differ diff --git a/src/server/captcha/20191128_111934.png b/src/server/captcha/20191128_111934.png new file mode 100755 index 0000000..8cced4b Binary files /dev/null and b/src/server/captcha/20191128_111934.png differ diff --git a/src/server/captcha/20191128_114013.png b/src/server/captcha/20191128_114013.png new file mode 100755 index 0000000..9b028be Binary files /dev/null and b/src/server/captcha/20191128_114013.png differ diff --git a/src/server/captcha/20191128_114022.png b/src/server/captcha/20191128_114022.png new file mode 100755 index 0000000..7c2e1e3 Binary files /dev/null and b/src/server/captcha/20191128_114022.png differ diff --git a/src/server/captcha/20191128_120801.png b/src/server/captcha/20191128_120801.png new file mode 100755 index 0000000..6efa7db Binary files /dev/null and b/src/server/captcha/20191128_120801.png differ diff --git a/src/server/captcha/20191128_145428.png b/src/server/captcha/20191128_145428.png new file mode 100755 index 0000000..cc6929e Binary files /dev/null and b/src/server/captcha/20191128_145428.png differ diff --git a/src/server/captcha/20191128_145434.png b/src/server/captcha/20191128_145434.png new file mode 100755 index 0000000..b1fca4d Binary files /dev/null and b/src/server/captcha/20191128_145434.png differ diff --git a/src/server/captcha/20191128_145535.png b/src/server/captcha/20191128_145535.png new file mode 100755 index 0000000..10bde70 Binary files /dev/null and b/src/server/captcha/20191128_145535.png differ diff --git a/src/server/captcha/20191128_151522.png b/src/server/captcha/20191128_151522.png new file mode 100755 index 0000000..9789ee0 Binary files /dev/null and b/src/server/captcha/20191128_151522.png differ diff --git a/src/server/captcha/20191128_151749.png b/src/server/captcha/20191128_151749.png new file mode 100755 index 0000000..e02c96e Binary files /dev/null and b/src/server/captcha/20191128_151749.png differ diff --git a/src/server/captcha/20191128_151804.png b/src/server/captcha/20191128_151804.png new file mode 100755 index 0000000..714224a Binary files /dev/null and b/src/server/captcha/20191128_151804.png differ diff --git a/src/server/captcha/20191128_151820.png b/src/server/captcha/20191128_151820.png new file mode 100755 index 0000000..99d6b31 Binary files /dev/null and b/src/server/captcha/20191128_151820.png differ diff --git a/src/server/captcha/20191128_151831.png b/src/server/captcha/20191128_151831.png new file mode 100755 index 0000000..8f3953f Binary files /dev/null and b/src/server/captcha/20191128_151831.png differ diff --git a/src/server/captcha/20191128_152158.png b/src/server/captcha/20191128_152158.png new file mode 100755 index 0000000..7c4a7da Binary files /dev/null and b/src/server/captcha/20191128_152158.png differ diff --git a/src/server/captcha/20191128_152207.png b/src/server/captcha/20191128_152207.png new file mode 100755 index 0000000..002d7f0 Binary files /dev/null and b/src/server/captcha/20191128_152207.png differ diff --git a/src/server/captcha/20191128_152219.png b/src/server/captcha/20191128_152219.png new file mode 100755 index 0000000..64515d2 Binary files /dev/null and b/src/server/captcha/20191128_152219.png differ diff --git a/src/server/captcha/20191128_152230.png b/src/server/captcha/20191128_152230.png new file mode 100755 index 0000000..0c54749 Binary files /dev/null and b/src/server/captcha/20191128_152230.png differ diff --git a/src/server/captcha/20191128_152642.png b/src/server/captcha/20191128_152642.png new file mode 100755 index 0000000..a39b1ad Binary files /dev/null and b/src/server/captcha/20191128_152642.png differ diff --git a/src/server/captcha/20191128_153525.png b/src/server/captcha/20191128_153525.png new file mode 100755 index 0000000..900f6f0 Binary files /dev/null and b/src/server/captcha/20191128_153525.png differ diff --git a/src/server/captcha/20191128_153601.png b/src/server/captcha/20191128_153601.png new file mode 100755 index 0000000..3e3ff47 Binary files /dev/null and b/src/server/captcha/20191128_153601.png differ diff --git a/src/server/captcha/20191128_154841.png b/src/server/captcha/20191128_154841.png new file mode 100755 index 0000000..2ef0de1 Binary files /dev/null and b/src/server/captcha/20191128_154841.png differ diff --git a/src/server/captcha/20191128_154914.png b/src/server/captcha/20191128_154914.png new file mode 100755 index 0000000..217aa5b Binary files /dev/null and b/src/server/captcha/20191128_154914.png differ diff --git a/src/server/captcha/20191128_154946.png b/src/server/captcha/20191128_154946.png new file mode 100755 index 0000000..2c69be5 Binary files /dev/null and b/src/server/captcha/20191128_154946.png differ diff --git a/src/server/captcha/20191128_155131.png b/src/server/captcha/20191128_155131.png new file mode 100755 index 0000000..353679c Binary files /dev/null and b/src/server/captcha/20191128_155131.png differ diff --git a/src/server/captcha/20191128_155146.png b/src/server/captcha/20191128_155146.png new file mode 100755 index 0000000..1e09294 Binary files /dev/null and b/src/server/captcha/20191128_155146.png differ diff --git a/src/server/captcha/20191128_155159.png b/src/server/captcha/20191128_155159.png new file mode 100755 index 0000000..19241a4 Binary files /dev/null and b/src/server/captcha/20191128_155159.png differ diff --git a/src/server/captcha/20191128_155214.png b/src/server/captcha/20191128_155214.png new file mode 100755 index 0000000..b0e07a3 Binary files /dev/null and b/src/server/captcha/20191128_155214.png differ diff --git a/src/server/captcha/20191128_155229.png b/src/server/captcha/20191128_155229.png new file mode 100755 index 0000000..fdc590b Binary files /dev/null and b/src/server/captcha/20191128_155229.png differ diff --git a/src/server/captcha/20191128_155241.png b/src/server/captcha/20191128_155241.png new file mode 100755 index 0000000..0c8bb36 Binary files /dev/null and b/src/server/captcha/20191128_155241.png differ diff --git a/src/server/captcha/20191128_155253.png b/src/server/captcha/20191128_155253.png new file mode 100755 index 0000000..cfb2a3d Binary files /dev/null and b/src/server/captcha/20191128_155253.png differ diff --git a/src/server/captcha/20191128_155310.png b/src/server/captcha/20191128_155310.png new file mode 100755 index 0000000..8f9fb13 Binary files /dev/null and b/src/server/captcha/20191128_155310.png differ diff --git a/src/server/captcha/20191128_155411.png b/src/server/captcha/20191128_155411.png new file mode 100755 index 0000000..f2fdbda Binary files /dev/null and b/src/server/captcha/20191128_155411.png differ diff --git a/src/server/captcha/20191128_155424.png b/src/server/captcha/20191128_155424.png new file mode 100755 index 0000000..28bab77 Binary files /dev/null and b/src/server/captcha/20191128_155424.png differ diff --git a/src/server/captcha/20191128_155436.png b/src/server/captcha/20191128_155436.png new file mode 100755 index 0000000..ad9af96 Binary files /dev/null and b/src/server/captcha/20191128_155436.png differ diff --git a/src/server/captcha/20191128_160502.png b/src/server/captcha/20191128_160502.png new file mode 100755 index 0000000..5369990 Binary files /dev/null and b/src/server/captcha/20191128_160502.png differ diff --git a/src/server/captcha/20191128_161521.png b/src/server/captcha/20191128_161521.png new file mode 100755 index 0000000..a9c2d3e Binary files /dev/null and b/src/server/captcha/20191128_161521.png differ diff --git a/src/server/captcha/20191128_161528.png b/src/server/captcha/20191128_161528.png new file mode 100755 index 0000000..5618621 Binary files /dev/null and b/src/server/captcha/20191128_161528.png differ diff --git a/src/server/captcha/20191128_163610.png b/src/server/captcha/20191128_163610.png new file mode 100755 index 0000000..ca5a23d Binary files /dev/null and b/src/server/captcha/20191128_163610.png differ diff --git a/src/server/captcha/20191128_163617.png b/src/server/captcha/20191128_163617.png new file mode 100755 index 0000000..3413dae Binary files /dev/null and b/src/server/captcha/20191128_163617.png differ diff --git a/src/server/captcha/20191128_163843.png b/src/server/captcha/20191128_163843.png new file mode 100755 index 0000000..1de6b2b Binary files /dev/null and b/src/server/captcha/20191128_163843.png differ diff --git a/src/server/captcha/20191128_163908.png b/src/server/captcha/20191128_163908.png new file mode 100755 index 0000000..716edce Binary files /dev/null and b/src/server/captcha/20191128_163908.png differ diff --git a/src/server/captcha/20191128_163928.png b/src/server/captcha/20191128_163928.png new file mode 100755 index 0000000..1e6944b Binary files /dev/null and b/src/server/captcha/20191128_163928.png differ diff --git a/src/server/captcha/20191128_164032.png b/src/server/captcha/20191128_164032.png new file mode 100755 index 0000000..75d8fc8 Binary files /dev/null and b/src/server/captcha/20191128_164032.png differ diff --git a/src/server/captcha/20191128_164044.png b/src/server/captcha/20191128_164044.png new file mode 100755 index 0000000..3e909ac Binary files /dev/null and b/src/server/captcha/20191128_164044.png differ diff --git a/src/server/captcha/20191128_164447.png b/src/server/captcha/20191128_164447.png new file mode 100755 index 0000000..6977f95 Binary files /dev/null and b/src/server/captcha/20191128_164447.png differ diff --git a/src/server/captcha/20191128_170555.png b/src/server/captcha/20191128_170555.png new file mode 100755 index 0000000..1f0b47b Binary files /dev/null and b/src/server/captcha/20191128_170555.png differ diff --git a/src/server/captcha/20191128_170757.png b/src/server/captcha/20191128_170757.png new file mode 100755 index 0000000..81d395e Binary files /dev/null and b/src/server/captcha/20191128_170757.png differ diff --git a/src/server/captcha/20191128_171240.png b/src/server/captcha/20191128_171240.png new file mode 100755 index 0000000..1fa0f26 Binary files /dev/null and b/src/server/captcha/20191128_171240.png differ diff --git a/src/server/captcha/20191128_171352.png b/src/server/captcha/20191128_171352.png new file mode 100755 index 0000000..72256bf Binary files /dev/null and b/src/server/captcha/20191128_171352.png differ diff --git a/src/server/captcha/20191128_171410.png b/src/server/captcha/20191128_171410.png new file mode 100755 index 0000000..08969e2 Binary files /dev/null and b/src/server/captcha/20191128_171410.png differ diff --git a/src/server/captcha/20191128_172105.png b/src/server/captcha/20191128_172105.png new file mode 100755 index 0000000..96ced47 Binary files /dev/null and b/src/server/captcha/20191128_172105.png differ diff --git a/src/server/captcha/20191128_172456.png b/src/server/captcha/20191128_172456.png new file mode 100755 index 0000000..bd5c1b2 Binary files /dev/null and b/src/server/captcha/20191128_172456.png differ diff --git a/src/server/captcha/20191128_172655.png b/src/server/captcha/20191128_172655.png new file mode 100755 index 0000000..8e26d10 Binary files /dev/null and b/src/server/captcha/20191128_172655.png differ diff --git a/src/server/captcha/20191128_173218.png b/src/server/captcha/20191128_173218.png new file mode 100755 index 0000000..7eb0336 Binary files /dev/null and b/src/server/captcha/20191128_173218.png differ diff --git a/src/server/captcha/20191128_173233.png b/src/server/captcha/20191128_173233.png new file mode 100755 index 0000000..7e2c8b1 Binary files /dev/null and b/src/server/captcha/20191128_173233.png differ diff --git a/src/server/captcha/20191128_173238.png b/src/server/captcha/20191128_173238.png new file mode 100755 index 0000000..24faee4 Binary files /dev/null and b/src/server/captcha/20191128_173238.png differ diff --git a/src/server/captcha/20191128_173251.png b/src/server/captcha/20191128_173251.png new file mode 100755 index 0000000..1111413 Binary files /dev/null and b/src/server/captcha/20191128_173251.png differ diff --git a/src/server/captcha/20191128_173302.png b/src/server/captcha/20191128_173302.png new file mode 100755 index 0000000..5975f59 Binary files /dev/null and b/src/server/captcha/20191128_173302.png differ diff --git a/src/server/captcha/20191129_200534.png b/src/server/captcha/20191129_200534.png new file mode 100755 index 0000000..2ef9e04 Binary files /dev/null and b/src/server/captcha/20191129_200534.png differ diff --git a/src/server/captcha/20191129_205543.png b/src/server/captcha/20191129_205543.png new file mode 100755 index 0000000..fddf034 Binary files /dev/null and b/src/server/captcha/20191129_205543.png differ diff --git a/src/server/captcha/20191129_212947.png b/src/server/captcha/20191129_212947.png new file mode 100755 index 0000000..c95ca98 Binary files /dev/null and b/src/server/captcha/20191129_212947.png differ diff --git a/src/server/captcha/20191129_212956.png b/src/server/captcha/20191129_212956.png new file mode 100755 index 0000000..8dab676 Binary files /dev/null and b/src/server/captcha/20191129_212956.png differ diff --git a/src/server/captcha/20191129_213008.png b/src/server/captcha/20191129_213008.png new file mode 100755 index 0000000..36b55c3 Binary files /dev/null and b/src/server/captcha/20191129_213008.png differ diff --git a/src/server/captcha/20191129_213305.png b/src/server/captcha/20191129_213305.png new file mode 100755 index 0000000..894aeed Binary files /dev/null and b/src/server/captcha/20191129_213305.png differ diff --git a/src/server/captcha/20191129_213506.png b/src/server/captcha/20191129_213506.png new file mode 100755 index 0000000..8d0f5bf Binary files /dev/null and b/src/server/captcha/20191129_213506.png differ diff --git a/src/server/captcha/20191129_213511.png b/src/server/captcha/20191129_213511.png new file mode 100755 index 0000000..97b86fc Binary files /dev/null and b/src/server/captcha/20191129_213511.png differ diff --git a/src/server/captcha/20191129_220533.png b/src/server/captcha/20191129_220533.png new file mode 100755 index 0000000..8ad6cdb Binary files /dev/null and b/src/server/captcha/20191129_220533.png differ diff --git a/src/server/captcha/20191129_222100.png b/src/server/captcha/20191129_222100.png new file mode 100755 index 0000000..9e48397 Binary files /dev/null and b/src/server/captcha/20191129_222100.png differ diff --git a/src/server/captcha/20191130_235340.png b/src/server/captcha/20191130_235340.png new file mode 100755 index 0000000..750f247 Binary files /dev/null and b/src/server/captcha/20191130_235340.png differ diff --git a/src/server/captcha/20191130_235347.png b/src/server/captcha/20191130_235347.png new file mode 100755 index 0000000..6ea511e Binary files /dev/null and b/src/server/captcha/20191130_235347.png differ diff --git a/src/server/captcha/20191130_235514.png b/src/server/captcha/20191130_235514.png new file mode 100755 index 0000000..4e25a20 Binary files /dev/null and b/src/server/captcha/20191130_235514.png differ diff --git a/src/server/captcha/20191130_235539.png b/src/server/captcha/20191130_235539.png new file mode 100755 index 0000000..6f6cdf6 Binary files /dev/null and b/src/server/captcha/20191130_235539.png differ diff --git a/src/server/captcha/20191201_000656.png b/src/server/captcha/20191201_000656.png new file mode 100755 index 0000000..cff0d52 Binary files /dev/null and b/src/server/captcha/20191201_000656.png differ diff --git a/src/server/captcha/20191203_205711.png b/src/server/captcha/20191203_205711.png new file mode 100755 index 0000000..0afc09b Binary files /dev/null and b/src/server/captcha/20191203_205711.png differ diff --git a/src/server/captcha/20191203_210934.png b/src/server/captcha/20191203_210934.png new file mode 100755 index 0000000..e0ec631 Binary files /dev/null and b/src/server/captcha/20191203_210934.png differ diff --git a/src/server/captcha/20191203_212147.png b/src/server/captcha/20191203_212147.png new file mode 100755 index 0000000..68b3431 Binary files /dev/null and b/src/server/captcha/20191203_212147.png differ diff --git a/src/server/captcha/20191203_212202.png b/src/server/captcha/20191203_212202.png new file mode 100755 index 0000000..5bf0259 Binary files /dev/null and b/src/server/captcha/20191203_212202.png differ diff --git a/src/server/captcha/20191203_212215.png b/src/server/captcha/20191203_212215.png new file mode 100755 index 0000000..c29f324 Binary files /dev/null and b/src/server/captcha/20191203_212215.png differ diff --git a/src/server/captcha/20191203_212234.png b/src/server/captcha/20191203_212234.png new file mode 100755 index 0000000..a1d847d Binary files /dev/null and b/src/server/captcha/20191203_212234.png differ diff --git a/src/server/captcha/20191203_212648.png b/src/server/captcha/20191203_212648.png new file mode 100755 index 0000000..3f1f5cf Binary files /dev/null and b/src/server/captcha/20191203_212648.png differ diff --git a/src/server/captcha/20191203_212905.png b/src/server/captcha/20191203_212905.png new file mode 100755 index 0000000..f5783d4 Binary files /dev/null and b/src/server/captcha/20191203_212905.png differ diff --git a/src/server/captcha/20191203_213527.png b/src/server/captcha/20191203_213527.png new file mode 100755 index 0000000..0edd35e Binary files /dev/null and b/src/server/captcha/20191203_213527.png differ diff --git a/src/server/captcha/20191203_213605.png b/src/server/captcha/20191203_213605.png new file mode 100755 index 0000000..460fe43 Binary files /dev/null and b/src/server/captcha/20191203_213605.png differ diff --git a/src/server/captcha/20191203_213638.png b/src/server/captcha/20191203_213638.png new file mode 100755 index 0000000..d5c086a Binary files /dev/null and b/src/server/captcha/20191203_213638.png differ diff --git a/src/server/captcha/20191203_213911.png b/src/server/captcha/20191203_213911.png new file mode 100755 index 0000000..2acf9f5 Binary files /dev/null and b/src/server/captcha/20191203_213911.png differ diff --git a/src/server/captcha/20191203_215517.png b/src/server/captcha/20191203_215517.png new file mode 100755 index 0000000..498d8ea Binary files /dev/null and b/src/server/captcha/20191203_215517.png differ diff --git a/src/server/captcha/20191203_220637.png b/src/server/captcha/20191203_220637.png new file mode 100755 index 0000000..9dfffde Binary files /dev/null and b/src/server/captcha/20191203_220637.png differ diff --git a/src/server/captcha/20191203_223939.png b/src/server/captcha/20191203_223939.png new file mode 100755 index 0000000..bb2d1e4 Binary files /dev/null and b/src/server/captcha/20191203_223939.png differ diff --git a/src/server/captcha/20191203_223954.png b/src/server/captcha/20191203_223954.png new file mode 100755 index 0000000..16f2f9d Binary files /dev/null and b/src/server/captcha/20191203_223954.png differ diff --git a/src/server/captcha/20191203_231124.png b/src/server/captcha/20191203_231124.png new file mode 100755 index 0000000..43b9184 Binary files /dev/null and b/src/server/captcha/20191203_231124.png differ diff --git a/src/server/captcha/20191204_201951.png b/src/server/captcha/20191204_201951.png new file mode 100755 index 0000000..f3b640c Binary files /dev/null and b/src/server/captcha/20191204_201951.png differ diff --git a/src/server/captcha/20191204_202007.png b/src/server/captcha/20191204_202007.png new file mode 100755 index 0000000..b1c5a09 Binary files /dev/null and b/src/server/captcha/20191204_202007.png differ diff --git a/src/server/captcha/20191204_202205.png b/src/server/captcha/20191204_202205.png new file mode 100755 index 0000000..0589ef9 Binary files /dev/null and b/src/server/captcha/20191204_202205.png differ diff --git a/src/server/captcha/20191204_205231.png b/src/server/captcha/20191204_205231.png new file mode 100755 index 0000000..8632d3f Binary files /dev/null and b/src/server/captcha/20191204_205231.png differ diff --git a/src/server/captcha/20191204_210943.png b/src/server/captcha/20191204_210943.png new file mode 100755 index 0000000..c716b39 Binary files /dev/null and b/src/server/captcha/20191204_210943.png differ diff --git a/src/server/captcha/20191204_211335.png b/src/server/captcha/20191204_211335.png new file mode 100755 index 0000000..928840d Binary files /dev/null and b/src/server/captcha/20191204_211335.png differ diff --git a/src/server/captcha/20191204_211416.png b/src/server/captcha/20191204_211416.png new file mode 100755 index 0000000..43dfef1 Binary files /dev/null and b/src/server/captcha/20191204_211416.png differ diff --git a/src/server/captcha/20191204_214450.png b/src/server/captcha/20191204_214450.png new file mode 100755 index 0000000..d02bc45 Binary files /dev/null and b/src/server/captcha/20191204_214450.png differ diff --git a/src/server/captcha/20191204_225617.png b/src/server/captcha/20191204_225617.png new file mode 100755 index 0000000..19474d4 Binary files /dev/null and b/src/server/captcha/20191204_225617.png differ diff --git a/src/server/captcha/20191204_225634.png b/src/server/captcha/20191204_225634.png new file mode 100755 index 0000000..7c27c85 Binary files /dev/null and b/src/server/captcha/20191204_225634.png differ diff --git a/src/server/captcha/20191204_230030.png b/src/server/captcha/20191204_230030.png new file mode 100755 index 0000000..a42e5be Binary files /dev/null and b/src/server/captcha/20191204_230030.png differ diff --git a/src/server/captcha/20191204_235128.png b/src/server/captcha/20191204_235128.png new file mode 100755 index 0000000..1dc68a2 Binary files /dev/null and b/src/server/captcha/20191204_235128.png differ diff --git a/src/server/captcha/20191204_235319.png b/src/server/captcha/20191204_235319.png new file mode 100755 index 0000000..68bfa0a Binary files /dev/null and b/src/server/captcha/20191204_235319.png differ diff --git a/src/server/captcha/20191204_235714.png b/src/server/captcha/20191204_235714.png new file mode 100755 index 0000000..5b3a915 Binary files /dev/null and b/src/server/captcha/20191204_235714.png differ diff --git a/src/server/captcha/20191205_000910.png b/src/server/captcha/20191205_000910.png new file mode 100755 index 0000000..fd4e7fe Binary files /dev/null and b/src/server/captcha/20191205_000910.png differ diff --git a/src/server/captcha/20191205_004046.png b/src/server/captcha/20191205_004046.png new file mode 100755 index 0000000..bf88038 Binary files /dev/null and b/src/server/captcha/20191205_004046.png differ diff --git a/src/server/captcha/20191205_095008.png b/src/server/captcha/20191205_095008.png new file mode 100755 index 0000000..2e1bffd Binary files /dev/null and b/src/server/captcha/20191205_095008.png differ diff --git a/src/server/captcha/20191205_100829.png b/src/server/captcha/20191205_100829.png new file mode 100755 index 0000000..ecb3d2f Binary files /dev/null and b/src/server/captcha/20191205_100829.png differ diff --git a/src/server/captcha/20191205_104525.png b/src/server/captcha/20191205_104525.png new file mode 100755 index 0000000..364a25d Binary files /dev/null and b/src/server/captcha/20191205_104525.png differ diff --git a/src/server/captcha/20191205_104532.png b/src/server/captcha/20191205_104532.png new file mode 100755 index 0000000..1577d2b Binary files /dev/null and b/src/server/captcha/20191205_104532.png differ diff --git a/src/server/captcha/20191205_104729.png b/src/server/captcha/20191205_104729.png new file mode 100755 index 0000000..82b95d6 Binary files /dev/null and b/src/server/captcha/20191205_104729.png differ diff --git a/src/server/captcha/20191205_202557.png b/src/server/captcha/20191205_202557.png new file mode 100755 index 0000000..7b58127 Binary files /dev/null and b/src/server/captcha/20191205_202557.png differ diff --git a/src/server/captcha/20191205_205621.png b/src/server/captcha/20191205_205621.png new file mode 100755 index 0000000..715da55 Binary files /dev/null and b/src/server/captcha/20191205_205621.png differ diff --git a/src/server/captcha/20191205_205843.png b/src/server/captcha/20191205_205843.png new file mode 100755 index 0000000..16a131d Binary files /dev/null and b/src/server/captcha/20191205_205843.png differ diff --git a/src/server/captcha/20191205_205923.png b/src/server/captcha/20191205_205923.png new file mode 100755 index 0000000..9c704de Binary files /dev/null and b/src/server/captcha/20191205_205923.png differ diff --git a/src/server/captcha/20191206_000359.png b/src/server/captcha/20191206_000359.png new file mode 100755 index 0000000..3221b11 Binary files /dev/null and b/src/server/captcha/20191206_000359.png differ diff --git a/src/server/captcha/20191206_003432.png b/src/server/captcha/20191206_003432.png new file mode 100755 index 0000000..bbca63a Binary files /dev/null and b/src/server/captcha/20191206_003432.png differ diff --git a/src/server/captcha/20191206_003735.png b/src/server/captcha/20191206_003735.png new file mode 100755 index 0000000..8fcf07f Binary files /dev/null and b/src/server/captcha/20191206_003735.png differ diff --git a/src/server/captcha/20191206_010924.png b/src/server/captcha/20191206_010924.png new file mode 100755 index 0000000..b16c25e Binary files /dev/null and b/src/server/captcha/20191206_010924.png differ diff --git a/src/server/captcha/20191206_011048.png b/src/server/captcha/20191206_011048.png new file mode 100755 index 0000000..3d8e928 Binary files /dev/null and b/src/server/captcha/20191206_011048.png differ diff --git a/src/server/captcha/20191206_104643.png b/src/server/captcha/20191206_104643.png new file mode 100755 index 0000000..ed657b0 Binary files /dev/null and b/src/server/captcha/20191206_104643.png differ diff --git a/src/server/captcha/20191206_104656.png b/src/server/captcha/20191206_104656.png new file mode 100755 index 0000000..5f418c4 Binary files /dev/null and b/src/server/captcha/20191206_104656.png differ diff --git a/src/server/captcha/20191206_104710.png b/src/server/captcha/20191206_104710.png new file mode 100755 index 0000000..cd6b3b4 Binary files /dev/null and b/src/server/captcha/20191206_104710.png differ diff --git a/src/server/captcha/20191206_104723.png b/src/server/captcha/20191206_104723.png new file mode 100755 index 0000000..297cfd0 Binary files /dev/null and b/src/server/captcha/20191206_104723.png differ diff --git a/src/server/captcha/20191206_104740.png b/src/server/captcha/20191206_104740.png new file mode 100755 index 0000000..6ba9819 Binary files /dev/null and b/src/server/captcha/20191206_104740.png differ diff --git a/src/server/captcha/20191206_113108.png b/src/server/captcha/20191206_113108.png new file mode 100755 index 0000000..4bd9000 Binary files /dev/null and b/src/server/captcha/20191206_113108.png differ diff --git a/src/server/captcha/20191206_113407.png b/src/server/captcha/20191206_113407.png new file mode 100755 index 0000000..c9ace70 Binary files /dev/null and b/src/server/captcha/20191206_113407.png differ diff --git a/src/server/captcha/20191206_113445.png b/src/server/captcha/20191206_113445.png new file mode 100755 index 0000000..52743f8 Binary files /dev/null and b/src/server/captcha/20191206_113445.png differ diff --git a/src/server/captcha/20191206_120948.png b/src/server/captcha/20191206_120948.png new file mode 100755 index 0000000..3a30d95 Binary files /dev/null and b/src/server/captcha/20191206_120948.png differ diff --git a/src/server/captcha/20191206_124157.png b/src/server/captcha/20191206_124157.png new file mode 100755 index 0000000..9aca868 Binary files /dev/null and b/src/server/captcha/20191206_124157.png differ diff --git a/src/server/captcha/20191206_124209.png b/src/server/captcha/20191206_124209.png new file mode 100755 index 0000000..549a9fa Binary files /dev/null and b/src/server/captcha/20191206_124209.png differ diff --git a/src/server/captcha/20191206_124225.png b/src/server/captcha/20191206_124225.png new file mode 100755 index 0000000..7e57884 Binary files /dev/null and b/src/server/captcha/20191206_124225.png differ diff --git a/src/server/captcha/20191206_124238.png b/src/server/captcha/20191206_124238.png new file mode 100755 index 0000000..552029b Binary files /dev/null and b/src/server/captcha/20191206_124238.png differ diff --git a/src/server/captcha/20191206_131402.png b/src/server/captcha/20191206_131402.png new file mode 100755 index 0000000..abdf344 Binary files /dev/null and b/src/server/captcha/20191206_131402.png differ diff --git a/src/server/captcha/20191206_131413.png b/src/server/captcha/20191206_131413.png new file mode 100755 index 0000000..cd9df7b Binary files /dev/null and b/src/server/captcha/20191206_131413.png differ diff --git a/src/server/captcha/20191206_131422.png b/src/server/captcha/20191206_131422.png new file mode 100755 index 0000000..98e4c89 Binary files /dev/null and b/src/server/captcha/20191206_131422.png differ diff --git a/src/server/captcha/20191206_131759.png b/src/server/captcha/20191206_131759.png new file mode 100755 index 0000000..1e3fcaa Binary files /dev/null and b/src/server/captcha/20191206_131759.png differ diff --git a/src/server/captcha/20191206_135356.png b/src/server/captcha/20191206_135356.png new file mode 100755 index 0000000..8ddb1a0 Binary files /dev/null and b/src/server/captcha/20191206_135356.png differ diff --git a/src/server/captcha/20191206_150111.png b/src/server/captcha/20191206_150111.png new file mode 100755 index 0000000..730e0f1 Binary files /dev/null and b/src/server/captcha/20191206_150111.png differ diff --git a/src/server/captcha/20191206_153133.png b/src/server/captcha/20191206_153133.png new file mode 100755 index 0000000..0108d77 Binary files /dev/null and b/src/server/captcha/20191206_153133.png differ diff --git a/src/server/captcha/20191206_153605.png b/src/server/captcha/20191206_153605.png new file mode 100755 index 0000000..a526d28 Binary files /dev/null and b/src/server/captcha/20191206_153605.png differ diff --git a/src/server/captcha/20191206_153622.png b/src/server/captcha/20191206_153622.png new file mode 100755 index 0000000..2eec982 Binary files /dev/null and b/src/server/captcha/20191206_153622.png differ diff --git a/src/server/captcha/20191206_154404.png b/src/server/captcha/20191206_154404.png new file mode 100755 index 0000000..9c28496 Binary files /dev/null and b/src/server/captcha/20191206_154404.png differ diff --git a/src/server/captcha/20191206_154642.png b/src/server/captcha/20191206_154642.png new file mode 100755 index 0000000..20865c2 Binary files /dev/null and b/src/server/captcha/20191206_154642.png differ diff --git a/src/server/captcha/20191206_154659.png b/src/server/captcha/20191206_154659.png new file mode 100755 index 0000000..f180f95 Binary files /dev/null and b/src/server/captcha/20191206_154659.png differ diff --git a/src/server/captcha/20191206_154940.png b/src/server/captcha/20191206_154940.png new file mode 100755 index 0000000..56b5b4d Binary files /dev/null and b/src/server/captcha/20191206_154940.png differ diff --git a/src/server/captcha/20191210_134041.png b/src/server/captcha/20191210_134041.png new file mode 100755 index 0000000..299c592 Binary files /dev/null and b/src/server/captcha/20191210_134041.png differ diff --git a/src/server/captcha/20191210_135242.png b/src/server/captcha/20191210_135242.png new file mode 100755 index 0000000..12d6157 Binary files /dev/null and b/src/server/captcha/20191210_135242.png differ diff --git a/src/server/captcha/20191210_135306.png b/src/server/captcha/20191210_135306.png new file mode 100755 index 0000000..aeda012 Binary files /dev/null and b/src/server/captcha/20191210_135306.png differ diff --git a/src/server/captcha/20191210_135314.png b/src/server/captcha/20191210_135314.png new file mode 100755 index 0000000..2728223 Binary files /dev/null and b/src/server/captcha/20191210_135314.png differ diff --git a/src/server/captcha/20191210_142337.png b/src/server/captcha/20191210_142337.png new file mode 100755 index 0000000..be341e3 Binary files /dev/null and b/src/server/captcha/20191210_142337.png differ diff --git a/src/server/captcha/20191210_150929.png b/src/server/captcha/20191210_150929.png new file mode 100755 index 0000000..085548f Binary files /dev/null and b/src/server/captcha/20191210_150929.png differ diff --git a/src/server/captcha/20191210_150946.png b/src/server/captcha/20191210_150946.png new file mode 100755 index 0000000..735332a Binary files /dev/null and b/src/server/captcha/20191210_150946.png differ diff --git a/src/server/captcha/20191210_184300.png b/src/server/captcha/20191210_184300.png new file mode 100755 index 0000000..305c440 Binary files /dev/null and b/src/server/captcha/20191210_184300.png differ diff --git a/src/server/captcha/20191210_184305.png b/src/server/captcha/20191210_184305.png new file mode 100755 index 0000000..9f31e08 Binary files /dev/null and b/src/server/captcha/20191210_184305.png differ diff --git a/src/server/captcha/20191210_184317.png b/src/server/captcha/20191210_184317.png new file mode 100755 index 0000000..961b683 Binary files /dev/null and b/src/server/captcha/20191210_184317.png differ diff --git a/src/server/captcha/20191210_190530.png b/src/server/captcha/20191210_190530.png new file mode 100755 index 0000000..0001af9 Binary files /dev/null and b/src/server/captcha/20191210_190530.png differ diff --git a/src/server/captcha/20191210_190547.png b/src/server/captcha/20191210_190547.png new file mode 100755 index 0000000..acdeda2 Binary files /dev/null and b/src/server/captcha/20191210_190547.png differ diff --git a/src/server/captcha/20191210_191006.png b/src/server/captcha/20191210_191006.png new file mode 100755 index 0000000..d9e6bbc Binary files /dev/null and b/src/server/captcha/20191210_191006.png differ diff --git a/src/server/captcha/20191210_191021.png b/src/server/captcha/20191210_191021.png new file mode 100755 index 0000000..0d244b1 Binary files /dev/null and b/src/server/captcha/20191210_191021.png differ diff --git a/src/server/captcha/20191210_191027.png b/src/server/captcha/20191210_191027.png new file mode 100755 index 0000000..c06f11b Binary files /dev/null and b/src/server/captcha/20191210_191027.png differ diff --git a/src/server/captcha/20191210_191052.png b/src/server/captcha/20191210_191052.png new file mode 100755 index 0000000..794f485 Binary files /dev/null and b/src/server/captcha/20191210_191052.png differ diff --git a/src/server/captcha/20191210_193731.png b/src/server/captcha/20191210_193731.png new file mode 100755 index 0000000..056a92c Binary files /dev/null and b/src/server/captcha/20191210_193731.png differ diff --git a/src/server/captcha/20191210_193748.png b/src/server/captcha/20191210_193748.png new file mode 100755 index 0000000..3e783bd Binary files /dev/null and b/src/server/captcha/20191210_193748.png differ diff --git a/src/server/captcha/20191210_193800.png b/src/server/captcha/20191210_193800.png new file mode 100755 index 0000000..ee3d4b3 Binary files /dev/null and b/src/server/captcha/20191210_193800.png differ diff --git a/src/server/captcha/20191210_194326.png b/src/server/captcha/20191210_194326.png new file mode 100755 index 0000000..70e15ca Binary files /dev/null and b/src/server/captcha/20191210_194326.png differ diff --git a/src/server/captcha/20191210_194337.png b/src/server/captcha/20191210_194337.png new file mode 100755 index 0000000..df75dfe Binary files /dev/null and b/src/server/captcha/20191210_194337.png differ diff --git a/src/server/captcha/20191210_195321.png b/src/server/captcha/20191210_195321.png new file mode 100755 index 0000000..2d2724d Binary files /dev/null and b/src/server/captcha/20191210_195321.png differ diff --git a/src/server/captcha/20191210_195331.png b/src/server/captcha/20191210_195331.png new file mode 100755 index 0000000..e481465 Binary files /dev/null and b/src/server/captcha/20191210_195331.png differ diff --git a/src/server/captcha/20191210_195345.png b/src/server/captcha/20191210_195345.png new file mode 100755 index 0000000..4e67b77 Binary files /dev/null and b/src/server/captcha/20191210_195345.png differ diff --git a/src/server/captcha/20191210_195402.png b/src/server/captcha/20191210_195402.png new file mode 100755 index 0000000..5f30c2e Binary files /dev/null and b/src/server/captcha/20191210_195402.png differ diff --git a/src/server/captcha/20191210_195418.png b/src/server/captcha/20191210_195418.png new file mode 100755 index 0000000..2aba95d Binary files /dev/null and b/src/server/captcha/20191210_195418.png differ diff --git a/src/server/captcha/20191210_195444.png b/src/server/captcha/20191210_195444.png new file mode 100755 index 0000000..cfd0ae1 Binary files /dev/null and b/src/server/captcha/20191210_195444.png differ diff --git a/src/server/captcha/20191210_195510.png b/src/server/captcha/20191210_195510.png new file mode 100755 index 0000000..91ff404 Binary files /dev/null and b/src/server/captcha/20191210_195510.png differ diff --git a/src/server/captcha/20191210_195531.png b/src/server/captcha/20191210_195531.png new file mode 100755 index 0000000..1cc9096 Binary files /dev/null and b/src/server/captcha/20191210_195531.png differ diff --git a/src/server/captcha/20191210_195546.png b/src/server/captcha/20191210_195546.png new file mode 100755 index 0000000..5c4e983 Binary files /dev/null and b/src/server/captcha/20191210_195546.png differ diff --git a/src/server/captcha/20191210_195741.png b/src/server/captcha/20191210_195741.png new file mode 100755 index 0000000..e47a99b Binary files /dev/null and b/src/server/captcha/20191210_195741.png differ diff --git a/src/server/captcha/20191210_195751.png b/src/server/captcha/20191210_195751.png new file mode 100755 index 0000000..4a2c820 Binary files /dev/null and b/src/server/captcha/20191210_195751.png differ diff --git a/src/server/captcha/20191210_195811.png b/src/server/captcha/20191210_195811.png new file mode 100755 index 0000000..ad7a7a6 Binary files /dev/null and b/src/server/captcha/20191210_195811.png differ diff --git a/src/server/captcha/20191210_195813.png b/src/server/captcha/20191210_195813.png new file mode 100755 index 0000000..718fd6b Binary files /dev/null and b/src/server/captcha/20191210_195813.png differ diff --git a/src/server/captcha/20191210_195830.png b/src/server/captcha/20191210_195830.png new file mode 100755 index 0000000..a64aec9 Binary files /dev/null and b/src/server/captcha/20191210_195830.png differ diff --git a/src/server/captcha/20191210_195846.png b/src/server/captcha/20191210_195846.png new file mode 100755 index 0000000..a8d4130 Binary files /dev/null and b/src/server/captcha/20191210_195846.png differ diff --git a/src/server/captcha/20191210_195934.png b/src/server/captcha/20191210_195934.png new file mode 100755 index 0000000..a1bc210 Binary files /dev/null and b/src/server/captcha/20191210_195934.png differ diff --git a/src/server/captcha/20191210_195937.png b/src/server/captcha/20191210_195937.png new file mode 100755 index 0000000..7858ff1 Binary files /dev/null and b/src/server/captcha/20191210_195937.png differ diff --git a/src/server/captcha/20191210_195950.png b/src/server/captcha/20191210_195950.png new file mode 100755 index 0000000..9a95689 Binary files /dev/null and b/src/server/captcha/20191210_195950.png differ diff --git a/src/server/captcha/20191210_200006.png b/src/server/captcha/20191210_200006.png new file mode 100755 index 0000000..39ec094 Binary files /dev/null and b/src/server/captcha/20191210_200006.png differ diff --git a/src/server/captcha/20191210_200022.png b/src/server/captcha/20191210_200022.png new file mode 100755 index 0000000..eff8db4 Binary files /dev/null and b/src/server/captcha/20191210_200022.png differ diff --git a/src/server/captcha/20191210_200039.png b/src/server/captcha/20191210_200039.png new file mode 100755 index 0000000..fb92d0b Binary files /dev/null and b/src/server/captcha/20191210_200039.png differ diff --git a/src/server/captcha/20191210_200056.png b/src/server/captcha/20191210_200056.png new file mode 100755 index 0000000..d7b426e Binary files /dev/null and b/src/server/captcha/20191210_200056.png differ diff --git a/src/server/captcha/20191210_200112.png b/src/server/captcha/20191210_200112.png new file mode 100755 index 0000000..cb24982 Binary files /dev/null and b/src/server/captcha/20191210_200112.png differ diff --git a/src/server/captcha/20191210_200146.png b/src/server/captcha/20191210_200146.png new file mode 100755 index 0000000..0d60b50 Binary files /dev/null and b/src/server/captcha/20191210_200146.png differ diff --git a/src/server/captcha/20191210_200148.png b/src/server/captcha/20191210_200148.png new file mode 100755 index 0000000..783be63 Binary files /dev/null and b/src/server/captcha/20191210_200148.png differ diff --git a/src/server/captcha/20191210_200220.png b/src/server/captcha/20191210_200220.png new file mode 100755 index 0000000..b527b25 Binary files /dev/null and b/src/server/captcha/20191210_200220.png differ diff --git a/src/server/captcha/20191210_203420.png b/src/server/captcha/20191210_203420.png new file mode 100755 index 0000000..2150afd Binary files /dev/null and b/src/server/captcha/20191210_203420.png differ diff --git a/src/server/captcha/20191210_205231.png b/src/server/captcha/20191210_205231.png new file mode 100755 index 0000000..b29fa70 Binary files /dev/null and b/src/server/captcha/20191210_205231.png differ diff --git a/src/server/captcha/20191210_205240.png b/src/server/captcha/20191210_205240.png new file mode 100755 index 0000000..6c95acc Binary files /dev/null and b/src/server/captcha/20191210_205240.png differ diff --git a/src/server/captcha/20191210_205249.png b/src/server/captcha/20191210_205249.png new file mode 100755 index 0000000..51cefda Binary files /dev/null and b/src/server/captcha/20191210_205249.png differ diff --git a/src/server/captcha/20191210_205255.png b/src/server/captcha/20191210_205255.png new file mode 100755 index 0000000..813138e Binary files /dev/null and b/src/server/captcha/20191210_205255.png differ diff --git a/src/server/captcha/20191210_205316.png b/src/server/captcha/20191210_205316.png new file mode 100755 index 0000000..eab2834 Binary files /dev/null and b/src/server/captcha/20191210_205316.png differ diff --git a/src/server/captcha/20191210_205325.png b/src/server/captcha/20191210_205325.png new file mode 100755 index 0000000..d777845 Binary files /dev/null and b/src/server/captcha/20191210_205325.png differ diff --git a/src/server/captcha/20191210_205341.png b/src/server/captcha/20191210_205341.png new file mode 100755 index 0000000..a3a1cbc Binary files /dev/null and b/src/server/captcha/20191210_205341.png differ diff --git a/src/server/captcha/20191210_205422.png b/src/server/captcha/20191210_205422.png new file mode 100755 index 0000000..2fb074c Binary files /dev/null and b/src/server/captcha/20191210_205422.png differ diff --git a/src/server/captcha/20191210_205425.png b/src/server/captcha/20191210_205425.png new file mode 100755 index 0000000..5aef88f Binary files /dev/null and b/src/server/captcha/20191210_205425.png differ diff --git a/src/server/captcha/20191210_205446.png b/src/server/captcha/20191210_205446.png new file mode 100755 index 0000000..94dc15b Binary files /dev/null and b/src/server/captcha/20191210_205446.png differ diff --git a/src/server/captcha/20191210_205504.png b/src/server/captcha/20191210_205504.png new file mode 100755 index 0000000..7ac4c20 Binary files /dev/null and b/src/server/captcha/20191210_205504.png differ diff --git a/src/server/captcha/20191210_212451.png b/src/server/captcha/20191210_212451.png new file mode 100755 index 0000000..fe938ca Binary files /dev/null and b/src/server/captcha/20191210_212451.png differ diff --git a/src/server/captcha/20191210_215626.png b/src/server/captcha/20191210_215626.png new file mode 100755 index 0000000..e54481b Binary files /dev/null and b/src/server/captcha/20191210_215626.png differ diff --git a/src/server/captcha/20191210_222505.png b/src/server/captcha/20191210_222505.png new file mode 100755 index 0000000..849d72f Binary files /dev/null and b/src/server/captcha/20191210_222505.png differ diff --git a/src/server/captcha/20191210_230451.png b/src/server/captcha/20191210_230451.png new file mode 100755 index 0000000..ffd79a4 Binary files /dev/null and b/src/server/captcha/20191210_230451.png differ diff --git a/src/server/captcha/20191210_230858.png b/src/server/captcha/20191210_230858.png new file mode 100755 index 0000000..581d61a Binary files /dev/null and b/src/server/captcha/20191210_230858.png differ diff --git a/src/server/captcha/20191210_234119.png b/src/server/captcha/20191210_234119.png new file mode 100755 index 0000000..54146a8 Binary files /dev/null and b/src/server/captcha/20191210_234119.png differ diff --git a/src/server/captcha/20191216_165936.png b/src/server/captcha/20191216_165936.png new file mode 100755 index 0000000..0b4b6a9 Binary files /dev/null and b/src/server/captcha/20191216_165936.png differ diff --git a/src/server/captcha/20191216_165949.png b/src/server/captcha/20191216_165949.png new file mode 100755 index 0000000..422a2de Binary files /dev/null and b/src/server/captcha/20191216_165949.png differ diff --git a/src/server/captcha/20191216_170002.png b/src/server/captcha/20191216_170002.png new file mode 100755 index 0000000..e4d2f5b Binary files /dev/null and b/src/server/captcha/20191216_170002.png differ diff --git a/src/server/captcha/20191216_173028.png b/src/server/captcha/20191216_173028.png new file mode 100755 index 0000000..c741e9b Binary files /dev/null and b/src/server/captcha/20191216_173028.png differ diff --git a/src/server/captcha/20191216_190554.png b/src/server/captcha/20191216_190554.png new file mode 100755 index 0000000..33c071d Binary files /dev/null and b/src/server/captcha/20191216_190554.png differ diff --git a/src/server/captcha/20191216_190823.png b/src/server/captcha/20191216_190823.png new file mode 100755 index 0000000..25bbca0 Binary files /dev/null and b/src/server/captcha/20191216_190823.png differ diff --git a/src/server/captcha/20191216_195709.png b/src/server/captcha/20191216_195709.png new file mode 100755 index 0000000..2d1cc64 Binary files /dev/null and b/src/server/captcha/20191216_195709.png differ diff --git a/src/server/captcha/20191216_195929.png b/src/server/captcha/20191216_195929.png new file mode 100755 index 0000000..8edf74e Binary files /dev/null and b/src/server/captcha/20191216_195929.png differ diff --git a/src/server/captcha/20191216_203007.png b/src/server/captcha/20191216_203007.png new file mode 100755 index 0000000..63e7c0c Binary files /dev/null and b/src/server/captcha/20191216_203007.png differ diff --git a/src/server/captcha/20191216_203216.png b/src/server/captcha/20191216_203216.png new file mode 100755 index 0000000..651ba7e Binary files /dev/null and b/src/server/captcha/20191216_203216.png differ diff --git a/src/server/captcha/20191216_203410.png b/src/server/captcha/20191216_203410.png new file mode 100755 index 0000000..87f4f98 Binary files /dev/null and b/src/server/captcha/20191216_203410.png differ diff --git a/src/server/captcha/20191216_210518.png b/src/server/captcha/20191216_210518.png new file mode 100755 index 0000000..8624240 Binary files /dev/null and b/src/server/captcha/20191216_210518.png differ diff --git a/src/server/captcha/20191216_221418.png b/src/server/captcha/20191216_221418.png new file mode 100755 index 0000000..69c76b7 Binary files /dev/null and b/src/server/captcha/20191216_221418.png differ diff --git a/src/server/captcha/20191216_223939.png b/src/server/captcha/20191216_223939.png new file mode 100755 index 0000000..50bbca4 Binary files /dev/null and b/src/server/captcha/20191216_223939.png differ diff --git a/src/server/captcha/20191216_223952.png b/src/server/captcha/20191216_223952.png new file mode 100755 index 0000000..de8452a Binary files /dev/null and b/src/server/captcha/20191216_223952.png differ diff --git a/src/server/captcha/20191216_224203.png b/src/server/captcha/20191216_224203.png new file mode 100755 index 0000000..7ed7ae5 Binary files /dev/null and b/src/server/captcha/20191216_224203.png differ diff --git a/src/server/captcha/20191216_231449.png b/src/server/captcha/20191216_231449.png new file mode 100755 index 0000000..f4bf040 Binary files /dev/null and b/src/server/captcha/20191216_231449.png differ diff --git a/src/server/captcha/20191217_130304.png b/src/server/captcha/20191217_130304.png new file mode 100755 index 0000000..6807076 Binary files /dev/null and b/src/server/captcha/20191217_130304.png differ diff --git a/src/server/captcha/20191217_133413.png b/src/server/captcha/20191217_133413.png new file mode 100755 index 0000000..86672b7 Binary files /dev/null and b/src/server/captcha/20191217_133413.png differ diff --git a/src/server/captcha/20191217_133541.png b/src/server/captcha/20191217_133541.png new file mode 100755 index 0000000..b633648 Binary files /dev/null and b/src/server/captcha/20191217_133541.png differ diff --git a/src/server/captcha/20191217_133829.png b/src/server/captcha/20191217_133829.png new file mode 100755 index 0000000..5325cbe Binary files /dev/null and b/src/server/captcha/20191217_133829.png differ diff --git a/src/server/captcha/20191217_134722.png b/src/server/captcha/20191217_134722.png new file mode 100755 index 0000000..781b518 Binary files /dev/null and b/src/server/captcha/20191217_134722.png differ diff --git a/src/server/captcha/20191217_134738.png b/src/server/captcha/20191217_134738.png new file mode 100755 index 0000000..5a38c62 Binary files /dev/null and b/src/server/captcha/20191217_134738.png differ diff --git a/src/server/captcha/20191217_141831.png b/src/server/captcha/20191217_141831.png new file mode 100755 index 0000000..3514e0f Binary files /dev/null and b/src/server/captcha/20191217_141831.png differ diff --git a/src/server/captcha/20191217_193201.png b/src/server/captcha/20191217_193201.png new file mode 100755 index 0000000..278635a Binary files /dev/null and b/src/server/captcha/20191217_193201.png differ diff --git a/src/server/captcha/20191217_193443.png b/src/server/captcha/20191217_193443.png new file mode 100755 index 0000000..f80ecc8 Binary files /dev/null and b/src/server/captcha/20191217_193443.png differ diff --git a/src/server/captcha/20191217_200518.png b/src/server/captcha/20191217_200518.png new file mode 100755 index 0000000..92fabf4 Binary files /dev/null and b/src/server/captcha/20191217_200518.png differ diff --git a/src/server/captcha/20191217_201455.png b/src/server/captcha/20191217_201455.png new file mode 100755 index 0000000..98122bd Binary files /dev/null and b/src/server/captcha/20191217_201455.png differ diff --git a/src/server/captcha/20191217_201511.png b/src/server/captcha/20191217_201511.png new file mode 100755 index 0000000..db833da Binary files /dev/null and b/src/server/captcha/20191217_201511.png differ diff --git a/src/server/captcha/20191217_201708.png b/src/server/captcha/20191217_201708.png new file mode 100755 index 0000000..6d522a5 Binary files /dev/null and b/src/server/captcha/20191217_201708.png differ diff --git a/src/server/captcha/20191217_203018.png b/src/server/captcha/20191217_203018.png new file mode 100755 index 0000000..7267fae Binary files /dev/null and b/src/server/captcha/20191217_203018.png differ diff --git a/src/server/captcha/20191217_210153.png b/src/server/captcha/20191217_210153.png new file mode 100755 index 0000000..cc55e44 Binary files /dev/null and b/src/server/captcha/20191217_210153.png differ diff --git a/src/server/captcha/20191223_195444.png b/src/server/captcha/20191223_195444.png new file mode 100755 index 0000000..382593c Binary files /dev/null and b/src/server/captcha/20191223_195444.png differ diff --git a/src/server/captcha/20191223_202507.png b/src/server/captcha/20191223_202507.png new file mode 100755 index 0000000..e142916 Binary files /dev/null and b/src/server/captcha/20191223_202507.png differ diff --git a/src/server/captcha/20191223_204320.png b/src/server/captcha/20191223_204320.png new file mode 100755 index 0000000..828fadf Binary files /dev/null and b/src/server/captcha/20191223_204320.png differ diff --git a/src/server/captcha/20191223_212456.png b/src/server/captcha/20191223_212456.png new file mode 100755 index 0000000..90493af Binary files /dev/null and b/src/server/captcha/20191223_212456.png differ diff --git a/src/server/captcha/20191223_212507.png b/src/server/captcha/20191223_212507.png new file mode 100755 index 0000000..a53c6c2 Binary files /dev/null and b/src/server/captcha/20191223_212507.png differ diff --git a/src/server/captcha/20191223_212758.png b/src/server/captcha/20191223_212758.png new file mode 100755 index 0000000..0ec63a0 Binary files /dev/null and b/src/server/captcha/20191223_212758.png differ diff --git a/src/server/captcha/20191223_214626.png b/src/server/captcha/20191223_214626.png new file mode 100755 index 0000000..c15802d Binary files /dev/null and b/src/server/captcha/20191223_214626.png differ diff --git a/src/server/captcha/20191223_221749.png b/src/server/captcha/20191223_221749.png new file mode 100755 index 0000000..a940d8e Binary files /dev/null and b/src/server/captcha/20191223_221749.png differ diff --git a/src/server/captcha/20191223_225029.png b/src/server/captcha/20191223_225029.png new file mode 100755 index 0000000..233eff3 Binary files /dev/null and b/src/server/captcha/20191223_225029.png differ diff --git a/src/server/captcha/20191223_225422.png b/src/server/captcha/20191223_225422.png new file mode 100755 index 0000000..5ac741c Binary files /dev/null and b/src/server/captcha/20191223_225422.png differ diff --git a/src/server/captcha/20191223_233319.png b/src/server/captcha/20191223_233319.png new file mode 100755 index 0000000..ee30c16 Binary files /dev/null and b/src/server/captcha/20191223_233319.png differ diff --git a/src/server/captcha/20191223_233505.png b/src/server/captcha/20191223_233505.png new file mode 100755 index 0000000..48afafb Binary files /dev/null and b/src/server/captcha/20191223_233505.png differ diff --git a/src/server/captcha/20200103_194952.png b/src/server/captcha/20200103_194952.png new file mode 100755 index 0000000..c494a82 Binary files /dev/null and b/src/server/captcha/20200103_194952.png differ diff --git a/src/server/captcha/20200103_195159.png b/src/server/captcha/20200103_195159.png new file mode 100755 index 0000000..3546965 Binary files /dev/null and b/src/server/captcha/20200103_195159.png differ diff --git a/src/server/captcha/20200103_195514.png b/src/server/captcha/20200103_195514.png new file mode 100755 index 0000000..3f7c0dc Binary files /dev/null and b/src/server/captcha/20200103_195514.png differ diff --git a/src/server/captcha/20200103_195919.png b/src/server/captcha/20200103_195919.png new file mode 100755 index 0000000..ec09368 Binary files /dev/null and b/src/server/captcha/20200103_195919.png differ diff --git a/src/server/captcha/20200103_200051.png b/src/server/captcha/20200103_200051.png new file mode 100755 index 0000000..4b63b7e Binary files /dev/null and b/src/server/captcha/20200103_200051.png differ diff --git a/src/server/captcha/20200103_200234.png b/src/server/captcha/20200103_200234.png new file mode 100755 index 0000000..a427b1f Binary files /dev/null and b/src/server/captcha/20200103_200234.png differ diff --git a/src/server/captcha/20200103_201402.png b/src/server/captcha/20200103_201402.png new file mode 100755 index 0000000..85585ee Binary files /dev/null and b/src/server/captcha/20200103_201402.png differ diff --git a/src/server/captcha/20200103_201432.png b/src/server/captcha/20200103_201432.png new file mode 100755 index 0000000..ce9121a Binary files /dev/null and b/src/server/captcha/20200103_201432.png differ diff --git a/src/server/captcha/20200103_201649.png b/src/server/captcha/20200103_201649.png new file mode 100755 index 0000000..a9555c9 Binary files /dev/null and b/src/server/captcha/20200103_201649.png differ diff --git a/src/server/captcha/20200103_201949.png b/src/server/captcha/20200103_201949.png new file mode 100755 index 0000000..dfea737 Binary files /dev/null and b/src/server/captcha/20200103_201949.png differ diff --git a/src/server/captcha/20200103_201953.png b/src/server/captcha/20200103_201953.png new file mode 100755 index 0000000..503f99c Binary files /dev/null and b/src/server/captcha/20200103_201953.png differ diff --git a/src/server/captcha/20200103_202014.png b/src/server/captcha/20200103_202014.png new file mode 100755 index 0000000..96d791c Binary files /dev/null and b/src/server/captcha/20200103_202014.png differ diff --git a/src/server/captcha/20200103_202029.png b/src/server/captcha/20200103_202029.png new file mode 100755 index 0000000..f7e5f61 Binary files /dev/null and b/src/server/captcha/20200103_202029.png differ diff --git a/src/server/captcha/20200103_202045.png b/src/server/captcha/20200103_202045.png new file mode 100755 index 0000000..0adc367 Binary files /dev/null and b/src/server/captcha/20200103_202045.png differ diff --git a/src/server/captcha/20200103_202102.png b/src/server/captcha/20200103_202102.png new file mode 100755 index 0000000..3e97920 Binary files /dev/null and b/src/server/captcha/20200103_202102.png differ diff --git a/src/server/captcha/20200103_202114.png b/src/server/captcha/20200103_202114.png new file mode 100755 index 0000000..7d3004b Binary files /dev/null and b/src/server/captcha/20200103_202114.png differ diff --git a/src/server/captcha/20200103_202129.png b/src/server/captcha/20200103_202129.png new file mode 100755 index 0000000..94ca038 Binary files /dev/null and b/src/server/captcha/20200103_202129.png differ diff --git a/src/server/captcha/20200103_202144.png b/src/server/captcha/20200103_202144.png new file mode 100755 index 0000000..d3dbae0 Binary files /dev/null and b/src/server/captcha/20200103_202144.png differ diff --git a/src/server/captcha/20200103_202146.png b/src/server/captcha/20200103_202146.png new file mode 100755 index 0000000..74edd8b Binary files /dev/null and b/src/server/captcha/20200103_202146.png differ diff --git a/src/server/captcha/20200103_202210.png b/src/server/captcha/20200103_202210.png new file mode 100755 index 0000000..6fcc92f Binary files /dev/null and b/src/server/captcha/20200103_202210.png differ diff --git a/src/server/captcha/20200103_202233.png b/src/server/captcha/20200103_202233.png new file mode 100755 index 0000000..3dffaac Binary files /dev/null and b/src/server/captcha/20200103_202233.png differ diff --git a/src/server/captcha/20200103_202248.png b/src/server/captcha/20200103_202248.png new file mode 100755 index 0000000..c1cc2a0 Binary files /dev/null and b/src/server/captcha/20200103_202248.png differ diff --git a/src/server/captcha/20200103_202303.png b/src/server/captcha/20200103_202303.png new file mode 100755 index 0000000..6afc7ad Binary files /dev/null and b/src/server/captcha/20200103_202303.png differ diff --git a/src/server/captcha/20200103_202316.png b/src/server/captcha/20200103_202316.png new file mode 100755 index 0000000..d1ce86d Binary files /dev/null and b/src/server/captcha/20200103_202316.png differ diff --git a/src/server/captcha/20200103_204251.png b/src/server/captcha/20200103_204251.png new file mode 100755 index 0000000..a426b08 Binary files /dev/null and b/src/server/captcha/20200103_204251.png differ diff --git a/src/server/captcha/20200103_205202.png b/src/server/captcha/20200103_205202.png new file mode 100755 index 0000000..a932378 Binary files /dev/null and b/src/server/captcha/20200103_205202.png differ diff --git a/src/server/captcha/20200103_213137.png b/src/server/captcha/20200103_213137.png new file mode 100755 index 0000000..b79e77d Binary files /dev/null and b/src/server/captcha/20200103_213137.png differ diff --git a/src/server/captcha/20200103_213149.png b/src/server/captcha/20200103_213149.png new file mode 100755 index 0000000..bbfda79 Binary files /dev/null and b/src/server/captcha/20200103_213149.png differ diff --git a/src/server/captcha/20200103_213203.png b/src/server/captcha/20200103_213203.png new file mode 100755 index 0000000..4904c5d Binary files /dev/null and b/src/server/captcha/20200103_213203.png differ diff --git a/src/server/captcha/20200103_213211.png b/src/server/captcha/20200103_213211.png new file mode 100755 index 0000000..dd16dff Binary files /dev/null and b/src/server/captcha/20200103_213211.png differ diff --git a/src/server/captcha/20200103_213426.png b/src/server/captcha/20200103_213426.png new file mode 100755 index 0000000..caa04ae Binary files /dev/null and b/src/server/captcha/20200103_213426.png differ diff --git a/src/server/captcha/20200103_220659.png b/src/server/captcha/20200103_220659.png new file mode 100755 index 0000000..d564aa5 Binary files /dev/null and b/src/server/captcha/20200103_220659.png differ diff --git a/src/server/captcha/20200103_222551.png b/src/server/captcha/20200103_222551.png new file mode 100755 index 0000000..38882ae Binary files /dev/null and b/src/server/captcha/20200103_222551.png differ diff --git a/src/server/captcha/20200103_222604.png b/src/server/captcha/20200103_222604.png new file mode 100755 index 0000000..223c930 Binary files /dev/null and b/src/server/captcha/20200103_222604.png differ diff --git a/src/server/captcha/20200103_225753.png b/src/server/captcha/20200103_225753.png new file mode 100755 index 0000000..7f6dc7e Binary files /dev/null and b/src/server/captcha/20200103_225753.png differ diff --git a/src/server/captcha/20200104_132538.png b/src/server/captcha/20200104_132538.png new file mode 100755 index 0000000..e5a3458 Binary files /dev/null and b/src/server/captcha/20200104_132538.png differ diff --git a/src/server/captcha/20200104_132749.png b/src/server/captcha/20200104_132749.png new file mode 100755 index 0000000..2abd0d0 Binary files /dev/null and b/src/server/captcha/20200104_132749.png differ diff --git a/src/server/captcha/20200104_135842.png b/src/server/captcha/20200104_135842.png new file mode 100755 index 0000000..8230461 Binary files /dev/null and b/src/server/captcha/20200104_135842.png differ diff --git a/src/server/captcha/20200104_143049.png b/src/server/captcha/20200104_143049.png new file mode 100755 index 0000000..7da1b2e Binary files /dev/null and b/src/server/captcha/20200104_143049.png differ diff --git a/src/server/captcha/20200104_143104.png b/src/server/captcha/20200104_143104.png new file mode 100755 index 0000000..8a0c40c Binary files /dev/null and b/src/server/captcha/20200104_143104.png differ diff --git a/src/server/captcha/20200104_143116.png b/src/server/captcha/20200104_143116.png new file mode 100755 index 0000000..f965d05 Binary files /dev/null and b/src/server/captcha/20200104_143116.png differ diff --git a/src/server/captcha/20200104_143127.png b/src/server/captcha/20200104_143127.png new file mode 100755 index 0000000..d0910c0 Binary files /dev/null and b/src/server/captcha/20200104_143127.png differ diff --git a/src/server/captcha/20200104_143140.png b/src/server/captcha/20200104_143140.png new file mode 100755 index 0000000..5f8e635 Binary files /dev/null and b/src/server/captcha/20200104_143140.png differ diff --git a/src/server/captcha/20200104_143510.png b/src/server/captcha/20200104_143510.png new file mode 100755 index 0000000..9c54973 Binary files /dev/null and b/src/server/captcha/20200104_143510.png differ diff --git a/src/server/captcha/20200104_152321.png b/src/server/captcha/20200104_152321.png new file mode 100755 index 0000000..40b42e1 Binary files /dev/null and b/src/server/captcha/20200104_152321.png differ diff --git a/src/server/captcha/20200104_155744.png b/src/server/captcha/20200104_155744.png new file mode 100755 index 0000000..99d62c8 Binary files /dev/null and b/src/server/captcha/20200104_155744.png differ diff --git a/src/server/captcha/20200104_162132.png b/src/server/captcha/20200104_162132.png new file mode 100755 index 0000000..3be82bc Binary files /dev/null and b/src/server/captcha/20200104_162132.png differ diff --git a/src/server/captcha/20200104_162148.png b/src/server/captcha/20200104_162148.png new file mode 100755 index 0000000..2630a62 Binary files /dev/null and b/src/server/captcha/20200104_162148.png differ diff --git a/src/server/captcha/20200104_162402.png b/src/server/captcha/20200104_162402.png new file mode 100755 index 0000000..ee1737f Binary files /dev/null and b/src/server/captcha/20200104_162402.png differ diff --git a/src/server/captcha/20200104_164416.png b/src/server/captcha/20200104_164416.png new file mode 100755 index 0000000..b8c8a6d Binary files /dev/null and b/src/server/captcha/20200104_164416.png differ diff --git a/src/server/captcha/20200104_171722.png b/src/server/captcha/20200104_171722.png new file mode 100755 index 0000000..4b1c6eb Binary files /dev/null and b/src/server/captcha/20200104_171722.png differ diff --git a/src/server/captcha/20200104_171738.png b/src/server/captcha/20200104_171738.png new file mode 100755 index 0000000..f820cda Binary files /dev/null and b/src/server/captcha/20200104_171738.png differ diff --git a/src/server/captcha/20200104_171751.png b/src/server/captcha/20200104_171751.png new file mode 100755 index 0000000..602df85 Binary files /dev/null and b/src/server/captcha/20200104_171751.png differ diff --git a/src/server/captcha/20200104_172040.png b/src/server/captcha/20200104_172040.png new file mode 100755 index 0000000..e86b0a3 Binary files /dev/null and b/src/server/captcha/20200104_172040.png differ diff --git a/src/server/captcha/20200104_181842.png b/src/server/captcha/20200104_181842.png new file mode 100755 index 0000000..c6aa826 Binary files /dev/null and b/src/server/captcha/20200104_181842.png differ diff --git a/src/server/captcha/20200104_181909.png b/src/server/captcha/20200104_181909.png new file mode 100755 index 0000000..524d1c2 Binary files /dev/null and b/src/server/captcha/20200104_181909.png differ diff --git a/src/server/captcha/20200104_181912.png b/src/server/captcha/20200104_181912.png new file mode 100755 index 0000000..055183f Binary files /dev/null and b/src/server/captcha/20200104_181912.png differ diff --git a/src/server/captcha/20200104_181927.png b/src/server/captcha/20200104_181927.png new file mode 100755 index 0000000..860c1af Binary files /dev/null and b/src/server/captcha/20200104_181927.png differ diff --git a/src/server/captcha/20200104_181941.png b/src/server/captcha/20200104_181941.png new file mode 100755 index 0000000..09b14bc Binary files /dev/null and b/src/server/captcha/20200104_181941.png differ diff --git a/src/server/captcha/20200104_181956.png b/src/server/captcha/20200104_181956.png new file mode 100755 index 0000000..78b7e7a Binary files /dev/null and b/src/server/captcha/20200104_181956.png differ diff --git a/src/server/captcha/20200104_182025.png b/src/server/captcha/20200104_182025.png new file mode 100755 index 0000000..db09817 Binary files /dev/null and b/src/server/captcha/20200104_182025.png differ diff --git a/src/server/captcha/20200104_182037.png b/src/server/captcha/20200104_182037.png new file mode 100755 index 0000000..34e8d6d Binary files /dev/null and b/src/server/captcha/20200104_182037.png differ diff --git a/src/server/captcha/20200104_182049.png b/src/server/captcha/20200104_182049.png new file mode 100755 index 0000000..97b8e30 Binary files /dev/null and b/src/server/captcha/20200104_182049.png differ diff --git a/src/server/captcha/20200104_182104.png b/src/server/captcha/20200104_182104.png new file mode 100755 index 0000000..defbe3b Binary files /dev/null and b/src/server/captcha/20200104_182104.png differ diff --git a/src/server/captcha/20200104_182116.png b/src/server/captcha/20200104_182116.png new file mode 100755 index 0000000..55088db Binary files /dev/null and b/src/server/captcha/20200104_182116.png differ diff --git a/src/server/captcha/20200104_182128.png b/src/server/captcha/20200104_182128.png new file mode 100755 index 0000000..f6ea920 Binary files /dev/null and b/src/server/captcha/20200104_182128.png differ diff --git a/src/server/captcha/20200104_182139.png b/src/server/captcha/20200104_182139.png new file mode 100755 index 0000000..3c1ff04 Binary files /dev/null and b/src/server/captcha/20200104_182139.png differ diff --git a/src/server/captcha/20200104_182201.png b/src/server/captcha/20200104_182201.png new file mode 100755 index 0000000..d5a6360 Binary files /dev/null and b/src/server/captcha/20200104_182201.png differ diff --git a/src/server/captcha/20200104_182230.png b/src/server/captcha/20200104_182230.png new file mode 100755 index 0000000..f020041 Binary files /dev/null and b/src/server/captcha/20200104_182230.png differ diff --git a/src/server/captcha/20200104_182245.png b/src/server/captcha/20200104_182245.png new file mode 100755 index 0000000..dcd8608 Binary files /dev/null and b/src/server/captcha/20200104_182245.png differ diff --git a/src/server/captcha/20200104_182255.png b/src/server/captcha/20200104_182255.png new file mode 100755 index 0000000..7e30722 Binary files /dev/null and b/src/server/captcha/20200104_182255.png differ diff --git a/src/server/captcha/20200117_180945.png b/src/server/captcha/20200117_180945.png new file mode 100755 index 0000000..b8a8597 Binary files /dev/null and b/src/server/captcha/20200117_180945.png differ diff --git a/src/server/captcha/20200206_233535.png b/src/server/captcha/20200206_233535.png new file mode 100755 index 0000000..f9b4246 Binary files /dev/null and b/src/server/captcha/20200206_233535.png differ diff --git a/src/server/captcha/20200208_172036.png b/src/server/captcha/20200208_172036.png new file mode 100755 index 0000000..b915dd8 Binary files /dev/null and b/src/server/captcha/20200208_172036.png differ diff --git a/src/server/captcha/20200208_181047.png b/src/server/captcha/20200208_181047.png new file mode 100755 index 0000000..1d33ec2 Binary files /dev/null and b/src/server/captcha/20200208_181047.png differ diff --git a/src/server/captcha/20200208_181251.png b/src/server/captcha/20200208_181251.png new file mode 100755 index 0000000..339bcd8 Binary files /dev/null and b/src/server/captcha/20200208_181251.png differ diff --git a/src/server/captcha/20200208_190952.png b/src/server/captcha/20200208_190952.png new file mode 100755 index 0000000..78d8dd2 Binary files /dev/null and b/src/server/captcha/20200208_190952.png differ diff --git a/src/server/captcha/20200208_191010.png b/src/server/captcha/20200208_191010.png new file mode 100755 index 0000000..ebb7b40 Binary files /dev/null and b/src/server/captcha/20200208_191010.png differ diff --git a/src/server/captcha/20200208_191022.png b/src/server/captcha/20200208_191022.png new file mode 100755 index 0000000..2aaaba9 Binary files /dev/null and b/src/server/captcha/20200208_191022.png differ diff --git a/src/server/captcha/20200208_191035.png b/src/server/captcha/20200208_191035.png new file mode 100755 index 0000000..86e574c Binary files /dev/null and b/src/server/captcha/20200208_191035.png differ diff --git a/src/server/captcha/20200208_191837.png b/src/server/captcha/20200208_191837.png new file mode 100755 index 0000000..30e23a2 Binary files /dev/null and b/src/server/captcha/20200208_191837.png differ diff --git a/src/server/captcha/20200208_194102.png b/src/server/captcha/20200208_194102.png new file mode 100755 index 0000000..34c3b70 Binary files /dev/null and b/src/server/captcha/20200208_194102.png differ diff --git a/src/server/captcha/20200210_112154.png b/src/server/captcha/20200210_112154.png new file mode 100755 index 0000000..a97e45a Binary files /dev/null and b/src/server/captcha/20200210_112154.png differ diff --git a/src/server/captcha/20200210_112407.png b/src/server/captcha/20200210_112407.png new file mode 100755 index 0000000..e315961 Binary files /dev/null and b/src/server/captcha/20200210_112407.png differ diff --git a/src/server/captcha/20200210_113213.png b/src/server/captcha/20200210_113213.png new file mode 100755 index 0000000..176ec5f Binary files /dev/null and b/src/server/captcha/20200210_113213.png differ diff --git a/src/server/captcha/20200210_113228.png b/src/server/captcha/20200210_113228.png new file mode 100755 index 0000000..d664655 Binary files /dev/null and b/src/server/captcha/20200210_113228.png differ diff --git a/src/server/captcha/20200210_113241.png b/src/server/captcha/20200210_113241.png new file mode 100755 index 0000000..dd6e127 Binary files /dev/null and b/src/server/captcha/20200210_113241.png differ diff --git a/src/server/captcha/20200210_113401.png b/src/server/captcha/20200210_113401.png new file mode 100755 index 0000000..7e28e18 Binary files /dev/null and b/src/server/captcha/20200210_113401.png differ diff --git a/src/server/captcha/20200210_113416.png b/src/server/captcha/20200210_113416.png new file mode 100755 index 0000000..bdaf04a Binary files /dev/null and b/src/server/captcha/20200210_113416.png differ diff --git a/src/server/captcha/20200210_113433.png b/src/server/captcha/20200210_113433.png new file mode 100755 index 0000000..a46b900 Binary files /dev/null and b/src/server/captcha/20200210_113433.png differ diff --git a/src/server/captcha/20200210_113445.png b/src/server/captcha/20200210_113445.png new file mode 100755 index 0000000..9bb701d Binary files /dev/null and b/src/server/captcha/20200210_113445.png differ diff --git a/src/server/captcha/20200210_113830.png b/src/server/captcha/20200210_113830.png new file mode 100755 index 0000000..df371cd Binary files /dev/null and b/src/server/captcha/20200210_113830.png differ diff --git a/src/server/captcha/20200210_113854.png b/src/server/captcha/20200210_113854.png new file mode 100755 index 0000000..46861b3 Binary files /dev/null and b/src/server/captcha/20200210_113854.png differ diff --git a/src/server/captcha/20200210_113905.png b/src/server/captcha/20200210_113905.png new file mode 100755 index 0000000..0c4241a Binary files /dev/null and b/src/server/captcha/20200210_113905.png differ diff --git a/src/server/captcha/20200210_113913.png b/src/server/captcha/20200210_113913.png new file mode 100755 index 0000000..3207ec3 Binary files /dev/null and b/src/server/captcha/20200210_113913.png differ diff --git a/src/server/captcha/20200210_114012.png b/src/server/captcha/20200210_114012.png new file mode 100755 index 0000000..1e3e166 Binary files /dev/null and b/src/server/captcha/20200210_114012.png differ diff --git a/src/server/captcha/20200210_114140.png b/src/server/captcha/20200210_114140.png new file mode 100755 index 0000000..c644d7f Binary files /dev/null and b/src/server/captcha/20200210_114140.png differ diff --git a/src/server/captcha/20200210_114218.png b/src/server/captcha/20200210_114218.png new file mode 100755 index 0000000..0b1e494 Binary files /dev/null and b/src/server/captcha/20200210_114218.png differ diff --git a/src/server/captcha/20200210_115837.png b/src/server/captcha/20200210_115837.png new file mode 100755 index 0000000..239d03e Binary files /dev/null and b/src/server/captcha/20200210_115837.png differ diff --git a/src/server/captcha/20200210_115849.png b/src/server/captcha/20200210_115849.png new file mode 100755 index 0000000..e10d024 Binary files /dev/null and b/src/server/captcha/20200210_115849.png differ diff --git a/src/server/captcha/20200210_115901.png b/src/server/captcha/20200210_115901.png new file mode 100755 index 0000000..07246d8 Binary files /dev/null and b/src/server/captcha/20200210_115901.png differ diff --git a/src/server/captcha/20200210_115954.png b/src/server/captcha/20200210_115954.png new file mode 100755 index 0000000..89ad712 Binary files /dev/null and b/src/server/captcha/20200210_115954.png differ diff --git a/src/server/captcha/20200229_230836.png b/src/server/captcha/20200229_230836.png new file mode 100755 index 0000000..28cb9ac Binary files /dev/null and b/src/server/captcha/20200229_230836.png differ diff --git a/src/server/captcha/20200229_232021.png b/src/server/captcha/20200229_232021.png new file mode 100755 index 0000000..3409034 Binary files /dev/null and b/src/server/captcha/20200229_232021.png differ diff --git a/src/server/captcha/20200229_232049.png b/src/server/captcha/20200229_232049.png new file mode 100755 index 0000000..8f19891 Binary files /dev/null and b/src/server/captcha/20200229_232049.png differ diff --git a/src/server/captcha/20200229_232715.png b/src/server/captcha/20200229_232715.png new file mode 100755 index 0000000..fcbedbb Binary files /dev/null and b/src/server/captcha/20200229_232715.png differ diff --git a/src/server/captcha/20200301_163613.png b/src/server/captcha/20200301_163613.png new file mode 100755 index 0000000..6864349 Binary files /dev/null and b/src/server/captcha/20200301_163613.png differ diff --git a/src/server/captcha/20200301_164425.png b/src/server/captcha/20200301_164425.png new file mode 100755 index 0000000..e1d3bcb Binary files /dev/null and b/src/server/captcha/20200301_164425.png differ diff --git a/src/server/captcha/20200818_151243.png b/src/server/captcha/20200818_151243.png new file mode 100755 index 0000000..67ee14d Binary files /dev/null and b/src/server/captcha/20200818_151243.png differ diff --git a/src/server/captcha/20200818_151259.png b/src/server/captcha/20200818_151259.png new file mode 100755 index 0000000..683172e Binary files /dev/null and b/src/server/captcha/20200818_151259.png differ diff --git a/src/server/captcha/20200818_153044.png b/src/server/captcha/20200818_153044.png new file mode 100755 index 0000000..a0f1abc Binary files /dev/null and b/src/server/captcha/20200818_153044.png differ diff --git a/src/server/captcha/20200818_162653.png b/src/server/captcha/20200818_162653.png new file mode 100755 index 0000000..22ddaaf Binary files /dev/null and b/src/server/captcha/20200818_162653.png differ diff --git a/src/server/cookie/120335 b/src/server/cookie/120335 new file mode 100755 index 0000000..4319c42 --- /dev/null +++ b/src/server/cookie/120335 @@ -0,0 +1,3 @@ +#LWP-Cookies-1.0 +Set-Cookie3: BIGipServerpool_zms20.verwalt-berlin.de=3323229194.47873.0000; path="/"; domain=service.berlin.de; path_spec; secure; discard; Httponly; version=0 +Set-Cookie3: Zmsappointment=p4o4e6fc3s1h8n4dffj8sag247; path="/"; domain=service.berlin.de; path_spec; discard; version=0 diff --git a/src/server/dbi_async.pl b/src/server/dbi_async.pl new file mode 100755 index 0000000..b84374a --- /dev/null +++ b/src/server/dbi_async.pl @@ -0,0 +1,46 @@ +use strict; +use warnings; +use Data::Dumper; +use feature qw(say); + +&db_query("SELECT `ID`, `NAME`, `HREF` FROM `venues`", $connection, 'selectall_hashref', 'ID'); + +sub db_query { + # &Delimiter((caller(0))[3]); + my ($query, $connection, $switch, @rest) = @_; + my $response; + + # say 'DATABASE CONNECTION STATE: ' . $connection->ping; + + my $statement; + if ($switch eq 'fetchrow') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchrow(); + } + elsif ($switch eq 'do') { + $response = $connection->do($query) or die $connection->errstr; + $connection->do($query, sub { + my ($rv, $dbh) = @_; + print Dumper $rv; + print Dumper $dbh; + }); + } + elsif ($switch eq 'selectall_hashref') { + # $response = $connection->selectall_hashref($query, $rest[0]) or die $connection->errstr; + $connection->selectall_hashref($query, sub { + ($response) = @_; + }); + } + elsif ($switch eq 'selectall_arrayref') { + # $response = $connection->selectall_arrayref($query) or die $connection->errstr; + $connection->selectall_arrayref($query, sub { + ($response) = @_; + }); + } + $statement->finish if $statement; + + print Dumper $response; + die; + return $response; +} \ No newline at end of file diff --git a/src/server/hashref.pl b/src/server/hashref.pl new file mode 100755 index 0000000..93b0805 --- /dev/null +++ b/src/server/hashref.pl @@ -0,0 +1,23 @@ +use strict; +use warnings; +use Data::Dumper; +use feature qw(say); + +my %hash = ( 'FOOBAR' => { 'NAME' => 'JOHN DOE' }, 'BLUB' => 0 ); +my $hash_ref = \%hash; + +$$hash_ref{'FOOBAR'}{'RESERVED'} = 1; + +print Dumper $hash_ref; + +say scalar(keys(%{$$hash_ref{'FOOBAR'}})); + +# for (1) { +# say 'foo'; +# } + +my $iteration = 0; + +for (my $i=0; $i <= $iteration; $i++) { + say "$i foo"; +} \ No newline at end of file diff --git a/src/server/output/-7 b/src/server/output/-7 new file mode 100755 index 0000000..0b11f0e --- /dev/null +++ b/src/server/output/-7 @@ -0,0 +1,547 @@ +$VAR1 = ' + + + + + Terminvergabe - Verifizierung - Service Berlin - Berlin.de + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+
+
+ + + +
+
+
+ + + +
+ + + + + +
+ +
+
+
+
+ + + + + + +
+
+
+
+ +
+
+ +
+
+ + +
+
+ +
+ + + +
+
+
+ +
+
+ +
+

In den Vormittagsstunden ist der eMail-Versand aus dem Terminbuchungssystem etwas verzögert.

+ + + + + + + + +
+
+ + + + + +
+
+
+
+
+

Bitte verifizieren sie sich

+
+ + +

In Ihrem Interesse möchten wir gerne verhindern, dass speziell entwickelte Programme freie Termine automatisch buchen. Diese Progamme hindern reguläre Besucher daran, einen frei werdenden Termin zu buchen.

+

Diese Seite hier erscheint, da wir nicht sicher sind, ob Sie ein Mensch sind. In der Regel erscheint diese Seite nicht. Ein Browser-Plugin wie ein Ad-Blocker oder ein Privacy-Plugin können aber schon dazu führen, dass wir nicht mehr unterscheiden können, ob wir es mit einem Mensch oder einer Maschine zu tun haben.

+

Bitte zeigen Sie mit der unten gestellten Aufgabe, dass Sie ein Mensch und keine Maschine sind:

+
+
+

Lesen Sie die Buchstaben und Zahlen in diesem Bild

+

+ +

+
+
+
+ +
+ +
+
+
+ + +
+

+ Falls Sie den Text nicht entziffern können, laden Sie diese Seite erneut oder senden Sie das Formular ab. Sie erhalten daraufhin ein anderes Bild. +

+
+
+ +
+
+
+ + + + + + +
+ + +
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + +
+ + +
+ + + + + + +'; diff --git a/src/server/service.berlin.de.1.pl b/src/server/service.berlin.de.1.pl new file mode 100755 index 0000000..d1fc071 --- /dev/null +++ b/src/server/service.berlin.de.1.pl @@ -0,0 +1,1124 @@ +#!usr/bin/perl +use strict; +use warnings 'redefine'; +# use diagnostics; +use Data::Dumper; +use HTML::TreeBuilder::XPath; +use feature qw(say); +use File::Basename; +use HTTP::Cookies; +use MIME::Base64 qw(decode_base64); +use Encode; +use utf8; +use DateTime::Format::Strptime; +use DateTime::Format::MySQL; +use DBI; +use DBD::MariaDB; +use DateTime::Format::DateParse; +use autodie qw(:all); +use Parallel::ForkManager; +use DBIx::Connector; +# use Storable; +# use Data::Printer; +# use DateTime; +# use HTML::Entities; +# binmode STDOUT, ':encoding(utf-8)'; +# binmode STDIN, ':encoding(utf-8)'; +# use open ':std', ':encoding(UTF-8)'; + +my ($zsmappointment, $ts, $mech, $ua, $cookie_jar, $strp, %data, %services, %venues, $connection, $strp_datetime, $pm); + +my %config = ( + 'MYSQL' => { + 'ACCESS' => { + 'database' => 'terminsnipe', + 'hostname' => 'zinomedia.de', + 'user' => 'terminsnipe', + 'pw' => 'dIHB6JLk0CBBx1p7', + 'port' => 3306, + }, + 'OPTIONS' => { + 'PrintError' => 0, + 'RaiseError' => 1, + 'AutoCommit' => 1, + } + }, + 'URL_ROOT' => 'https://service.berlin.de', + 'URL_TERMIN_DAY' => 'https://service.berlin.de/terminvereinbarung/termin/day', + 'URL_TERMIN_TIME' => 'https://service.berlin.de/terminvereinbarung/termin/time', + 'URL_HUMAN' => 'https://service.berlin.de/terminvereinbarung/termin/human', + 'URL_STANDORTE' => 'https://service.berlin.de/standorte', + 'URL_DIENSTLEISTUNGEN' => 'https://service.berlin.de/dienstleistungen', + 'BROWSER' => 'MECH', + 'CAPTCHA_DIR' => './captcha', + 'COOKIE_DIR' => './cookie', + 'COOKIE_PATH' => './cookie/.cookies.txt', + 'FOLLOW_NEXT' => 0, + 'OUTPUT_PATH' => './output/data', + 'FILTER_SERVICES_TO_OUTPUT' => 1, + 'FILTER_SERVICES_PATH' => './output/filter_services', + 'DB_REFRESH_SERVICES' => 0, + 'RUN_SERVICES' => 0, + 'RUN_VENUES' => 0, + 'VENUES_TO_FILE' => 1, + 'VENUES_PATH' => './output/venues', + 'DB_REFRESH_VENUES' => 1, + 'DUMP_SERVICES_TO_FILE' => 0, + 'SERVICES_PATH' => './output/services', + 'OUTPUT_DIR' => './output', + 'DB_CLEAN_DATETIMES' => 1, + 'RATE_LIMIT_SLEEP_MIN' => 5, + 'SLEEP_LOOP_MIN' => 1, + 'SERVICES' => { + 'Melderegisterauskunft sperren' => { 'RUN' => 0, 'ID' => 120678 }, + 'Erweiterung einer Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121629 }, + 'Ersatzführerschein nach Verlust/Diebstahl' => { 'RUN' => 0, 'ID' => 121593 }, + 'Kinderreisepass beantragen / verlängern / aktualisieren' => { 'RUN' => 0, 'ID' => 121469 }, + 'Namensänderung in den Fahrzeugpapieren' => { 'RUN' => 0, 'ID' => 324173 }, + 'Änderung/Wechsel der Hauptwohnung' => { 'RUN' => 0, 'ID' => 120697 }, + 'Umschreibung einer ausländischen Fahrerlaubnis aus einem Nicht-EU/EWR-Land (Drittstaat/Anlage 11)' => { 'RUN' => 0, 'ID' => 327537 }, + 'Historisches Kennzeichen beantragen' => { 'RUN' => 0, 'ID' => 121478 }, + 'Verlängerung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121634 }, + 'Adressänderung in der Zulassungsbescheinigung Teil I (ZBI) bzw. in dem Fahrzeugschein' => { 'RUN' => 0, 'ID' => 120658 }, + 'Blaue Karte EU zu einem neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 326798 }, + 'Neufahrzeug anmelden' => { 'RUN' => 0, 'ID' => 120882 }, + 'Zulassung eines aus dem EU-Ausland eingeführten fabrikneuen Fahrzeuges mit Zulassungsbescheinigung Teil II' => { 'RUN' => 0, 'ID' => 324199 }, + 'Führungszeugnis' => { 'RUN' => 0, 'ID' => 120926 }, + 'Aufenthaltserlaubnis für im Bundesgebiet geborene Kinder - Erteilung' => { 'RUN' => 0, 'ID' => 324269 }, + 'Kraftfahrzeug – technische Änderung melden' => { 'RUN' => 0, 'ID' => 120904 }, + 'Reisepass beantragen' => { 'RUN' => 0, 'ID' => 121151 }, + 'Meldebescheinigung beantragen' => { 'RUN' => 0, 'ID' => 120702 }, + 'Umtausch eines Kartenführerscheins' => { 'RUN' => 0, 'ID' => 121616 }, + 'Umschreibung einer Dienstfahrerlaubnis' => { 'RUN' => 0, 'ID' => 121615 }, + 'Bescheinigung über ein unbefristetes Aufenthaltsrecht' => { 'RUN' => 0, 'ID' => 324921 }, + 'Elektrokennzeichen beantragen' => { 'RUN' => 0, 'ID' => 327084 }, + 'Abmeldung einer Wohnung' => { 'RUN' => 0, 'ID' => 120335 }, + 'Zulassungsbescheinigung Teil II für KFZ wegen Verlust oder Diebstahl ersetzen' => { 'RUN' => 0, 'ID' => 121584 }, + 'Beglaubigung von Unterschriften' => { 'RUN' => 0, 'ID' => 158142 }, + 'Kraftfahrzeugkennzeichen wegen Unleserlichkeit ersetzen' => { 'RUN' => 0, 'ID' => 324907 }, + 'Zulassung von Taxen (Kraftdroschken)' => { 'RUN' => 0, 'ID' => 326033 }, + 'Fahrerlaubnis - Erweiterung auf die Klassen D1, D1E, D und DE' => { 'RUN' => 0, 'ID' => 324450 }, + 'Wiederzulassung eines Kraftfahrzeuges ohne Halterwechsel beantragen' => { 'RUN' => 0, 'ID' => 120905 }, + 'Ersterteilung einer Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121627 }, + 'Beglaubigung von Kopien' => { 'RUN' => 0, 'ID' => 121701 }, + 'Saisonkennzeichen für KFZ beantragen' => { 'RUN' => 0, 'ID' => 121480 }, + 'Anmeldung einer Wohnung' => { 'RUN' => 0, 'ID' => 120686 }, + 'Zulassung eines aus dem EU-Ausland eingeführten fabrikneuen Fahrzeuges ohne Zulassungsbescheinigung Teil II' => { 'RUN' => 0, 'ID' => 324200 }, + 'Kraftfahrzeug ummelden – nach einem Umzug nach Berlin' => { 'RUN' => 0, 'ID' => 120918 }, + 'Umschreibung eines zugelassenen Fahrzeuges mit Berliner Kennzeichen auf einen anderen Halter' => { 'RUN' => 0, 'ID' => 120906 }, + 'Reisepass beantragen (vorläufiger Reisepass)' => { 'RUN' => 0, 'ID' => 121153 }, + 'Kraftfahrzeug außer Betrieb setzen (Kfz abmelden)' => { 'RUN' => 0, 'ID' => 120877 }, + 'Umstellung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 124556 }, + 'Widerspruchsrechte gegen Datenübermittlungen und Melderegisterauskünfte' => { 'RUN' => 0, 'ID' => 319141 }, + 'Gewerbezentralregister - Auskunft beantragen' => { 'RUN' => 0, 'ID' => 327835 }, + 'Wechselkennzeichen beantragen' => { 'RUN' => 0, 'ID' => 324587 }, + 'Personalausweis vorläufig beantragen' => { 'RUN' => 0, 'ID' => 120682 }, + 'Personalausweis beantragen' => { 'RUN' => 0, 'ID' => 120703 }, + 'Kraftfahrzeugkennzeichen ersetzen – nach Diebstahl oder Verlust eines Nummernschildes' => { 'RUN' => 0, 'ID' => 324196 }, + 'Umschreibung einer ausländischen Fahrerlaubnis aus einem EU-/EWR-Staat' => { 'RUN' => 0, 'ID' => 121598 }, + 'Selbstfahrermietfahrzeug – Beginn/Beendigung der Nutzung' => { 'RUN' => 0, 'ID' => 326039 }, + 'Zulassungsbescheinigung Teil I für KFZ wegen Verlust oder Diebstahl ersetzen' => { 'RUN' => 0, 'ID' => 121575 }, + 'Kraftfahrzeugkennzeichen auf Wunsch ändern' => { 'RUN' => 0, 'ID' => 121473 }, + 'Begleitetes Fahren mit 17' => { 'RUN' => 0, 'ID' => 121589 }, + 'Fahrerlaubnis zur Fahrgastbeförderung (P-Schein) - Verlängerung' => { 'RUN' => 0, 'ID' => 324389 }, + 'Zulassung eines außer Betrieb gesetzten Fahrzeuges mit Berliner Kennzeichen auf einen anderen Halter' => { 'RUN' => 0, 'ID' => 324169 }, + 'Befreiung von der Ausweispflicht' => { 'RUN' => 0, 'ID' => 327044 }, + 'Grünes Kennzeichen beantragen' => { 'RUN' => 0, 'ID' => 121482 }, + 'Aufenthaltserlaubnis in einen neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 121874 }, + 'Kraftfahrzeug außer Betrieb setzen, unvollständige Unterlagen (Kfz abmelden)' => { 'RUN' => 0, 'ID' => 325881 }, + 'Führerschein: Ausstellung – Internationaler Führerschein' => { 'RUN' => 0, 'ID' => 121591 }, + 'Neuerteilung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121637 }, + 'Niederlassungserlaubnis oder Erlaubnis zum Daueraufenthalt-EU in einen neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 324280 }, + 'Fahrerlaubnis zur Fahrgastbeförderung (P-Schein) - Erteilung' => { 'RUN' => 0, 'ID' => 121622 }, + } +); + + +# # INIT +# &init(); +# &getRegister(); +# # db_createRequestDienstleister(19); +# die; + +# # MAIN +# &getStandorte() if $config{'RUN_VENUES'}; +# &getDienstleistungen() if $config{'RUN_SERVICES'}; +# # &run(); +# &db_request(); +# &Summary(); + + + +&programLoop(); + +# SUBS + +sub getRegister { + &Delimiter((caller(0))[3]); + my ($dataset_ref, $user_id) = @_; + + my $href = $$dataset_ref{'HREF'}; + + if ($config{'BROWSER'} eq 'MECH') { + # $mech->get('https://zinomedia.de/dl/customHTML.html'); + $mech->get('https://service.berlin.de/terminvereinbarung/termin/time/1576747440/109/'); + say $mech->status(); + if ($mech->success()) { + say $mech->content(); + my $tree = HTML::TreeBuilder::XPath->new_from_content($mech->content()); + my $success = &checkSuccess($tree); + if ($success) { + &submitRegister($mech); + } + else { + die; + } + } + else { + if ($mech->status() == 429) { + say $mech->content(); + my $treeRateLimit = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $success = &rateLimitHandler($treeRateLimit); + } + die; + } + } +} + +sub checkSuccess { + &Delimiter((caller(0))[3]); + my $tree = shift; + + my $xpath_alert = '//div[contains(concat(" ", normalize-space(@class), " "), " alert")]/p[1]'; + my $xpath_success = '//div[@class="submit-success-message"]'; + + if ($tree->exists($xpath_alert)) { + say trim($tree->findvalue($xpath_alert)); + + if (trim($tree->findvalue($xpath_alert)) eq 'Sie haben schon einen Termin reserviert. Soll diese Reservierung gelöscht werden oder möchten Sie mit der vorhandenen Reservierung fortfahren?') { + say 'Another reservation is already in progress.'; + return 0; + # my $url_restart = $config{'URL_ROOT'} . $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " alert")]/ul/li[2]/a/@href'); + # my $url_continue = $config{'URL_ROOT'} . $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " alert")]/ul/li[1]/a/@href'); + # say $url_restart; + # $mech->get($url_restart); + } + elsif (trim($tree->findvalue($xpath_alert)) eq 'Zu ihrer Suche konnten keine Daten ermittelt werden. Dies kann unterschiedliche Gründe haben:') { + say 'Cookie seems outdated'; + return 0; + } + elsif (trim($tree->findvalue($xpath_alert)) eq 'Sie haben Ihre Terminvereinbarung abgeschlossen. Sie können nun eine neue Terminssuche starten!') { + say 'Reservation already completed'; + return 0; + } + elsif (trim($tree->findvalue($xpath_alert)) eq 'Ihr Termin ist eingetragen worden. Bitte drucken Sie diese Seite aus, damit Sie alle Informationen zur Hand haben. Wichtig für Ihren Besuch ist die Vorlage Ihrer Vorgangsnummer.') { + say 'submitRegister successfull.'; + say trim($tree->findvalue($xpath_success)); + say $tree->findvalue('//span[@class="summary_authKey"]'); + say $tree->findvalue('//span[contains(concat(" ", normalize-space(@class), " "), " summary_processId ")]'); + return 1; + } + else { + say 'NO XPATH SPECIFIED DETECTED'; + } + + } + # elsif ($tree->exists($xpath_success)) { + # say 'submitRegister successfull.'; + # say trim($tree->findvalue($xpath_success)); + # say $tree->findvalue('//span[@class="summary_authKey"]'); + # say $tree->findvalue('//span[contains(concat(" ", normalize-space(@class), " "), " summary_processId ")]'); + # return 1; + # } + else { + say 'NO XPATH SPECIFIED DETECTED. Everything fine?'; + return 1; + } + +} + +sub submitRegister { + &Delimiter((caller(0))[3]); + my $mech = shift; + + + # print Dumper $mech->all_form_with_fields( ['familyName'] ); + my $form = $mech->form_number(2); + # print Dumper $result; + $mech->set_fields( 'familyName' => 'John Smith', 'email' => 'shoty.at.cs@gmx.net', ); + $mech->tick('agbgelesen', 1); + $mech->click_button(id => 'register_submit'); + + say $mech->status(); + if ($mech->success()) { + say $mech->content; + &dumpToFile('./output/success', $mech->content); + my $tree_success = HTML::TreeBuilder::XPath->new_from_content($mech->content()); + &checkSuccess($tree_success); + } + else { + if ($mech->status() == 404) { + say $mech->content(); + say 'PAGE NOT FOUND'; + } + die; + } +} + +sub programLoop { + &Delimiter((caller(0))[3]); + + for (;;) { + # INIT + &init(); + + # MAIN + &getStandorte() if $config{'RUN_VENUES'}; + &getDienstleistungen() if $config{'RUN_SERVICES'}; + &db_request(); + &Summary(); + + &sleepCountdown($config{'SLEEP_LOOP_MIN'} * 60); + } +} + +sub db_createRequestDienstleister { + &Delimiter((caller(0))[3]); + my $request_id = shift; + my $values; + + my $string = &db_query("Select terminsnipe.services.HREF From terminsnipe.request Inner Join terminsnipe.services On terminsnipe.services.ID = terminsnipe.request.SERVICE_ID Where terminsnipe.request.ID = $request_id", $connection, 'fetchrow'); + my @dienstleister = getDienstleisterlist($string); + + foreach my $id (@dienstleister) { + $values .= "('$request_id', '$id'), "; + } + my $sql = substr("INSERT INTO `request_dienstleister` (`REQUEST_ID`, `VENUE_ID`) VALUES $values", 0, -2); + # say $sql; + + &db_query("DELETE FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id", $connection, 'do'); + &db_query($sql, $connection, 'do'); +} + +sub getDienstleisterlist { + &Delimiter((caller(0))[3]); + my $string = shift; + + + $string =~ m/dienstleisterlist=(.+?)&/; + return split /,/, $1; +} + +sub db_request { + &Delimiter((caller(0))[3]); + my %request; + + my $response = &db_query("SELECT `ID`, `USER_ID`, `SERVICE_ID`, `DATETIME_FROM`, `DATETIME_TO`, `VENUE_ID`, `TIMESTAMP` FROM `request` WHERE `EXPIRED` = 0;", $connection, 'selectall_arrayref'); + if (@$response) { + for my $i (0 .. $#{$response}) { + my $dataset = $$response[$i]; + $request{$$dataset[2]}{$$dataset[0]}{'ID'} = $$dataset[0]; + $request{$$dataset[2]}{$$dataset[0]}{'USER_ID'} = $$dataset[1]; + $request{$$dataset[2]}{$$dataset[0]}{'SERVICE_ID'} = $$dataset[2]; + $request{$$dataset[2]}{$$dataset[0]}{'DATETIME_FROM'} = $$dataset[3]; + $request{$$dataset[2]}{$$dataset[0]}{'DATETIME_TO'} = $$dataset[4]; + $request{$$dataset[2]}{$$dataset[0]}{'VENUE_ID'} = $$dataset[5]; + $request{$$dataset[2]}{$$dataset[0]}{'TIMESTAMP'} = $$dataset[6]; + $services{$$dataset[2]}{'HREF_GENERATED'} = &db_generateHREF($$dataset[0], $$dataset[2]); + } + print Dumper \%request; + + foreach my $service_id (keys %request) { + &startFork($service_id, \%request); + } + $pm->wait_all_children; + say 'All childs are done. Continuing with parent...'; + foreach my $service_id (keys %request) { + &db_find_appointment($service_id, \%request); + } + } + else { + say 'No requests in db.'; + } +} + +sub db_generateHREF { + &Delimiter((caller(0))[3]); + my ($request_id, $service_id) = @_; + + my $href_generated = $services{$service_id}{'HREF'}; + my $response = &db_query("SELECT `VENUE_ID` FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id;", $connection, 'selectall_arrayref'); + + my $dienstleisterlist; + for my $i (0 .. $#{$response}) { + $dienstleisterlist .= "$$response[$i][0],"; + } + $dienstleisterlist = substr($dienstleisterlist, 0, -1); + + + $href_generated =~ m/dienstleisterlist=(.+?)&/; + $href_generated =~ s/$1/$dienstleisterlist/; + + return $href_generated; +} + +sub startFork { + &Delimiter((caller(0))[3]); + my ($service_id, $request_ref) = @_; + + my $pid = $pm->start($service_id) and next; + + &run_from_request($service_id); + &db_sync($service_id); + # &db_find_appointment($service_id, $request_ref); + + $pm->finish; +} + +sub run_from_request { + &Delimiter((caller(0))[3]); + my $service_id = shift; + + my $service = $services{$service_id}{'NAME'}; + my $href = $services{$service_id}{'HREF_GENERATED'}; + + say "START RUN FOR SERVICE '$service'..."; + &getTerminDay($href, $service_id); +} + +sub db_find_appointment { + &Delimiter((caller(0))[3]); + my ($service_id, $request_ref) = @_; + + foreach my $id (keys %{$$request_ref{$service_id}}) { + my $query = "Select terminsnipe.datetime.DATETIME, terminsnipe.mapping.SERVICE_ID, terminsnipe.mapping.VENUE_ID, terminsnipe.mapping.HREF As HREF, terminsnipe.venues.NAME As VENUE_NAME, terminsnipe.datetime.EPOCHTIME, terminsnipe.mapping.ID As MAPPING_ID From terminsnipe.datetime Inner Join terminsnipe.mapping On terminsnipe.mapping.DATETIME_ID = terminsnipe.datetime.EPOCHTIME Inner Join terminsnipe.venues On terminsnipe.mapping.VENUE_ID = terminsnipe.venues.ID Where Not terminsnipe.datetime.DATETIME > '$$request_ref{$service_id}{$id}{'DATETIME_TO'}' And Not terminsnipe.datetime.DATETIME < '$$request_ref{$service_id}{$id}{'DATETIME_FROM'}' And terminsnipe.mapping.SERVICE_ID = $service_id;"; + say $query; + my $response = &db_query($query, $connection, 'selectall_hashref', 'EPOCHTIME'); + print Dumper $response; + + say 'Possible appointments: ' . scalar(keys(%$response)); + if (%$response) { + say 'defined'; + + &db_insertAppointment($response, $$request_ref{$service_id}{$id}); + } + } +} + +sub db_insertAppointment { + &Delimiter((caller(0))[3]); + my ($response, $dataset_ref) = @_; + my $user_id = $$dataset_ref{'USER_ID'}; + my $request_id = $$dataset_ref{'ID'}; + + foreach my $epochtime (keys %{$response}) { + if (!$$response{$epochtime}{'RESERVED'}) { + say "Reserving appointment $$response{$epochtime}{MAPPING_ID} for USER_ID $user_id now..."; + + &getRegister($$response{$epochtime}, $user_id); + + &db_query("INSERT INTO `appointments` (`MAPPING_ID`, `USER_ID`) VALUES ('$$response{$epochtime}{MAPPING_ID}', '$user_id');", $connection, 'do'); + $$response{$epochtime}{'RESERVED'} = 1; + + say 'Deleting request with ID $request_id from request and request_dienstleister...'; + &db_query("DELETE FROM `request` WHERE `ID` = $request_id;", $connection, 'do'); + &db_query("DELETE FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id;", $connection, 'do'); + last; + } + else { + say "Appointment $epochtime is available but already reserved in loop by another user."; + } + } +} + +sub db_query { + # &Delimiter((caller(0))[3]); + my ($query, $connection, $switch, @rest) = @_; + my $response; + + # say 'DATABASE CONNECTION STATE: ' . $connection->ping; + + # $query = decode("utf-8", $query); + # $query = encode( "iso-8859-1", $query ); + + my $statement; + if ($switch eq 'fetchrow') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchrow(); + } + elsif ($switch eq 'fetchall_hashref') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchall_hashref($rest[0]); + } + elsif ($switch eq 'fetchall_arrayref') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchall_arrayref(); + } + elsif ($switch eq 'do') { + $response = $connection->do($query) or die $connection->errstr; + } + elsif ($switch eq 'selectall_hashref') { + $response = $connection->selectall_hashref($query, $rest[0]) or warn $connection->errstr; + } + elsif ($switch eq 'selectall_array') { + my @response = $connection->selectall_array($query) or die $connection->errstr; + return \@response; + } + elsif ($switch eq 'selectall_arrayref') { + $response = $connection->selectall_arrayref($query) or die $connection->errstr; + } + elsif ($switch eq 'selectrow_hashref') { + $response = $connection->selectrow_hashref($query) or die $connection->errstr; + } + + $statement->finish if $statement; + return $response; +} + + +sub connectToMySql { + &Delimiter((caller(0))[3]); + my ($db) = @_; + + # assign the values to your connection variable + my $connectionInfo = "dbi:mysql:$db;$config{'MYSQL'}{'ACCESS'}{'hostname'}"; + + # make connection to database + # my $l_connection = DBI->connect($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, $config{'MYSQL'}{'OPTIONS'}) || warn "MySQL conncet failed: $DBI::errstr"; + # return $l_connection; + + my $l_connection = DBIx::Connector->new($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, { + RaiseError => 1, + AutoCommit => 1, + }); + return $l_connection->dbh; +} + +sub run { + &Delimiter((caller(0))[3]); + + for my $service (keys %{$config{'SERVICES'}}) { + if ($config{'SERVICES'}{$service}{'RUN'}) { + my $service_id = $config{'SERVICES'}{$service}{'ID'}; + say "START RUN FOR SERVICE '$service'..."; + &getTerminDay($services{$service_id}{'HREF'}, $service_id); + &db_sync($service_id); + } + else { + # say "DISABLED $service"; + } + } +} + +sub db_sync { + &Delimiter((caller(0))[3]); + my $service_id = shift; + + &db_query("DELETE FROM `mapping` WHERE `SERVICE_ID` = '$service_id';", $connection, 'do'); + + foreach my $day (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}) { + foreach my $datetime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}}) { + my $epochtime = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}; + my $venue_id = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'VENUE_ID'}; + my $href = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'HREF'}; + + say "$service_id\t$venue_id\t$epochtime"; + + # say "INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) VALUES ('$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}', '$datetime') ON DUPLICATE KEY UPDATE `DATETIME` = `DATETIME`;"; + # say "INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) SELECT $epochtime, '$datetime' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `datetime` WHERE `EPOCHTIME`= $epochtime);"; + # say "INSERT INTO `mapping` (`SERVICE_ID`, `VENUE_ID`, `DATETIME_ID`, `HREF`) SELECT '$service_id', '$venue_id', '$epochtime', '$href' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `mapping` WHERE `SERVICE_ID` = '$service_id' AND `VENUE_ID` = '$venue_id' AND `DATETIME_ID` = '$epochtime');"; + + # &db_query("INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) VALUES ('$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}', '$datetime') ON DUPLICATE KEY UPDATE `DATETIME` = `DATETIME`;", $connection, 'do'); + &db_query("INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) SELECT $epochtime, '$datetime' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `datetime` WHERE `EPOCHTIME`= $epochtime);", $connection, 'do'); + &db_query("INSERT INTO `mapping` (`SERVICE_ID`, `VENUE_ID`, `DATETIME_ID`, `HREF`) SELECT '$service_id', '$venue_id', '$epochtime', '$href' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `mapping` WHERE `SERVICE_ID` = '$service_id' AND `VENUE_ID` = '$venue_id' AND `DATETIME_ID` = '$epochtime');", $connection, 'do'); + } + } + +} + +sub createMysqlDateTime { + # &Delimiter((caller(0))[3]); + my $string = shift; + + my $dt = $strp->parse_datetime($string); + return DateTime::Format::MySQL->format_datetime($dt); +} + +sub getDienstleistungen { + &Delimiter((caller(0))[3]); + + print "GETTING $config{'URL_DIENSTLEISTUNGEN'}... "; + + if ($config{'BROWSER'} eq 'MECH') { + $mech->get( $config{'URL_DIENSTLEISTUNGEN'} ); + say $mech->status(); + if ($mech->success()) { + # my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $tree = HTML::TreeBuilder::XPath->new_from_content($mech->content()); + &parseDienstleistungen($tree); + } + else { + if ($mech->status() == 429) { + say $mech->content(); + my $treeRateLimit = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $success = &rateLimitHandler($treeRateLimit); + } + die; + } + } + + if ($config{'FILTER_SERVICES_TO_OUTPUT'} || $config{'DB_REFRESH_SERVICES'}) { + &filterDienstleistungen(); + &db_insertServicesVenues('services') if $config{'DB_REFRESH_SERVICES'}; + } + + if ($config{'DUMP_SERVICES_TO_FILE'}) { + &dumpToFile($config{'SERVICES_PATH'}, \%services); + } +} + + +sub db_insertServicesVenues { + &Delimiter((caller(0))[3]); + my $switch = shift; + my ($database, $hash_ref); + + if ($switch eq 'services') { + $database = 'services'; + $hash_ref = \%services; + } + elsif ($switch eq 'venues') { + $database = 'venues'; + $hash_ref = \%venues; + } + + my $response = &db_query("delete FROM $database", $connection, 'do'); + + foreach my $id (sort keys %{$hash_ref}) { + my $query = "INSERT INTO `$database` (`ID`, `NAME`, `HREF`) VALUES ('$$hash_ref{$id}{'ID'}', '$$hash_ref{$id}{'NAME'}', '$$hash_ref{$id}{'HREF'}');"; + my $response = &db_query($query, $connection, 'do'); + } +} + +sub filterDienstleistungen { + &Delimiter((caller(0))[3]); + + # DATA DUMPER UTF8 HACK + local *Data::Dumper::qquote = sub { qq["${\(shift)}"] }; + local $Data::Dumper::Useperl = 1; + + open my $FILE, '>>:encoding(UTF-8)', $config{'FILTER_SERVICES_PATH'}; + foreach my $id (keys %services) { + if ($config{'BROWSER'} eq 'MECH') { + + print "GETTING $services{$id}{'HREF'}... "; + $mech->get( $services{$id}{'HREF'} ); + say $mech->status(); + + if ($mech->success()) { + my $name = $services{$id}{'NAME'}; + + print "\t$name => "; + # my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $tree = HTML::TreeBuilder::XPath->new_from_content($mech->content()); + if (!$tree->exists('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]')) { + say '0'; + delete $services{$id}; + } + else { + my $href = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]/a/@href'); + my $entry = "'$name' => { 'RUN' => 0, 'ID' => $services{$id}{'ID'} },"; + print '1 => ' . $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]/a') . " | $entry\n"; + say $FILE $entry if $config{'FILTER_SERVICES_TO_OUTPUT'}; + $services{$id}{'HREF'} = $href; + } + } + else { + if ($mech->status() == 429) { + say $mech->content(); + my $treeRateLimit = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $success = &rateLimitHandler($treeRateLimit); + } + die; + } + } + } + close $FILE; +} + +sub parseDienstleistungen { + &Delimiter((caller(0))[3]); + my $tree = shift; + + foreach my $dataset ($tree->findnodes('//li[contains(concat(" ", normalize-space(@class), " "), " topic-dls row-fluid ")]')) { + my $id = basename($dataset->findvalue('./a/@href')); + my $href = $dataset->findvalue('./a/@href'); + my $name = trim($dataset->findvalue('./a')); + + $services{$id}{'ID'} = $id; + $services{$id}{'HREF'} = "$config{'URL_ROOT'}$href"; + $services{$id}{'NAME'} = $name; + } +} + +sub getStandorte { + &Delimiter((caller(0))[3]); + + say "GETTING $config{'URL_STANDORTE'}"; + + my $tree; + if ($config{'BROWSER'} eq 'MECH') { + $mech->get( $config{'URL_STANDORTE'} ); + say $mech->status(); + if ($mech->success()) { + # $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + $tree = HTML::TreeBuilder::XPath->new_from_content($mech->content()); + &parseStandorte($tree); + &db_insertServicesVenues('venues') if $config{'DB_REFRESH_VENUES'}; + + if ($config{'VENUES_TO_FILE'}) { + dumpToFile($config{'VENUES_PATH'}, \%venues); + } + + } + else { + if ($mech->status() == 429) { + say $mech->content(); + my $treeRateLimit = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $success = &rateLimitHandler($treeRateLimit); + } + die; + } + } +} + +sub dumpToFile { + &Delimiter((caller(0))[3]); + my ($filepath, $ref) = @_; + + # DATA DUMPER UTF8 HACK + local *Data::Dumper::qquote = sub { qq["${\(shift)}"] }; + local $Data::Dumper::Useperl = 1; + + open my $FILE, '>:encoding(UTF-8)', $filepath; + print $FILE Dumper $ref; + close $FILE; +} + +sub parseStandorte { + &Delimiter((caller(0))[3]); + my $tree = shift; + + foreach my $dataset ($tree->findnodes('//li[contains(concat(" ", normalize-space(@class), " "), " topic-dls ")]')) { + my $id = basename($dataset->findvalue('./a/@href')); + my $href = $dataset->findvalue('./a/@href'); + my $name = trim($dataset->findvalue('./a')); + + say $name; + + $venues{$id}{'ID'} = $id; + $venues{$id}{'HREF'} = "$config{'URL_ROOT'}$href"; + $venues{$id}{'NAME'} = $name; + } +} + +sub init { + &Delimiter((caller(0))[3]); + + ($zsmappointment, $ts, $mech, $ua, $cookie_jar, $strp, $connection, $strp_datetime, $pm) = (undef) x 9; + undef %data; + + # COOKIES & ARGS + $cookie_jar = HTTP::Cookies->new(ignore_discard => 1 ); + if (@ARGV) { + $cookie_jar->set_cookie( 0, 'Zmsappointment', $ARGV[0], '/', 'service.berlin.de', undef, 1, undef, undef, 1, ) if $ARGV[0]; + $cookie_jar->set_cookie( 0, 'TS01fa49ef', $ARGV[1], '/', 'service.berlin.de', undef, 1, undef, undef, 1, ) if $ARGV[1]; + } + elsif (!@ARGV && -e $config{'COOKIE_PATH'}) { + say "Loading $config{'COOKIE_PATH'}..."; + $cookie_jar->load($config{'COOKIE_PATH'}); + } + + # STRP DATETIME + $strp = DateTime::Format::Strptime->new( + pattern => '%d %b %Y %H:%M', + locale => 'de_DE', + ); + + $strp_datetime = DateTime::Format::Strptime->new( + pattern => '%Y-%m-%d %T', + locale => 'de_DE', + ); + + + # BROWSER + if ($config{'BROWSER'} eq 'MECH') { + use WWW::Mechanize (); + $mech = WWW::Mechanize->new( + cookie_jar => $cookie_jar, + agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + autocheck => 0, + ); + } + elsif ($config{'BROWSER'} eq 'LWP') { + use LWP::UserAgent; + $ua = LWP::UserAgent->new( + ssl_opts => { + SSL_version => 'TLSv12:!SSLv2:!SSLv3:!TLSv1:!TLSv11', + }, + cookie_jar => $cookie_jar, + agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + ); + } + else { + die "$config{'BROWSER'} not defined."; + } + + # MYSQL + $connection = &connectToMySql($config{'MYSQL'}{'ACCESS'}{'database'}); + + # SERVICE AND VENUE HASH + $connection->do('set names utf8'); + %venues = %{&db_query("SELECT `ID`, `NAME`, `HREF` FROM `venues`", $connection, 'fetchall_hashref', 'ID')}; + %services = %{&db_query("SELECT `ID`, `NAME`, `HREF` FROM `services`", $connection, 'fetchall_hashref', 'ID')}; + + # EMPTY DIRECTORIES + # unlink glob "'$config{'CAPTCHA_DIR'}/*'"; + unlink glob "'$config{'OUTPUT_DIR'}/*'"; + + # CLEAN OLD DATETIMES, FLAG EXPIRED REQUESTS + &db_query("DELETE FROM `datetime` WHERE `DATETIME` < NOW();", $connection, 'do'); + &db_query("UPDATE request SET `EXPIRED` = 1 WHERE `DATETIME_TO` < NOW();", $connection, 'do'); + + # FORK MANAGER + $pm = Parallel::ForkManager->new(scalar(keys(%services))); + # $pm = Parallel::ForkManager->new(1); + $pm->run_on_start( + sub { + my ($pid, $ident)=@_; + say "\n** $ident started, pid: $pid"; + }); + $pm->run_on_finish( + sub { + my ($pid, $exit_code, $ident) = @_; + say "\n** $ident just got out of the pool with PID $pid and exit code: $exit_code"; + }); + $pm->run_on_wait( + sub { + # my ($pid, $ident) = @_; + say "\n** a child is waiting now..."; + }); +} + +sub Summary { + &Delimiter((caller(0))[3]); + + &dumpToFile($config{'OUTPUT_PATH'}, \%data); + $connection->disconnect(); +} + +sub getTerminDay { + &Delimiter((caller(0))[3]); + my ($url, $service_id) = @_; + + say "GETTING $url"; + + if ($config{'BROWSER'} eq 'MECH') { + $mech->get( $url ); + say $mech->status(); + if ($mech->success()) { + # say $mech->content(); + my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $success = &parseTerminDay($tree, $service_id); + + if ($tree->exists('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')) { + say 'NEXT EXISTS'; + if ($config{'FOLLOW_NEXT'}) { + my $epochtime = basename($tree->findvalue('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')); + my $url = "$config{'URL_TERMIN_DAY'}/$epochtime"; + &getTerminDay($url, $service_id); + } + } + } + else { + if ($mech->status() == 429) { + say $mech->content(); + my $treeRateLimit = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $success = &rateLimitHandler($treeRateLimit); + } + die; + } + } +} + +sub parseTerminDay { + &Delimiter((caller(0))[3]); + my ($tree, $service_id) = @_; + + my $xpath = '//div[contains(concat(" ", normalize-space(@class), " "), " calendar-month-table ")]'; + my $xpath_restart = '//div[contains(concat(" ", normalize-space(@class), " "), " alert alert-error noprint textile ")]/ul/li[2]/a/@href'; + if ($tree->exists($xpath)) { + foreach my $table ($tree->findnodes($xpath)) { + my ($month, $year) = split(/ /, $table->findvalue('./table/thead/tr/th[contains(concat(" ", normalize-space(@class), " "), " month ")]')); + foreach my $buchbar ($table->findnodes('./table/tbody/tr/td[contains(concat(" ", normalize-space(@class), " "), " buchbar ")]')) { + my $id = basename($buchbar->findvalue('./a/@href')); + + if ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}) { + say 'ALREADY PARSED'; + next; + } + else { + my $day = $buchbar->findvalue('./a'); + # ($data{'SERVICES'}{$service_id}{'SERVICE_ID'}) = grep{ $services{$_}{'NAME'} eq $service } keys %services; + $data{'SERVICES'}{$service_id}{'SERVICE_ID'} = $service_id; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} = $month; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} = $year; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} = $day; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'ID'} = $id; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIME'} = &createMysqlDateTime("$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} 00:00"); + + my $url = "$config{'URL_TERMIN_TIME'}/$id"; + &getTerminTime($url, $id, $service_id); + } + } + } + } + elsif ($tree->exists($xpath_restart)) { + say 'XPATH ALERT DETECTED'; + my $url = $config{'URL_ROOT'} . $tree->findvalue($xpath_restart); + say $url; + my $tree_restart = &getRestart($url); + &parseTerminDay($tree_restart, $service_id); + } + else { + say 'Parsing error.'; + say $tree->as_XML_compact; + die; + } +} + +sub getRestart { + &Delimiter((caller(0))[3]); + my $url = shift; + + say "GETTING $url"; + + if ($config{'BROWSER'} eq 'MECH') { + $mech->get( $url ); + say $mech->status(); + if ($mech->success()) { + my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + return $tree; + } + else { + if ($mech->status() == 429) { + say $mech->content(); + my $treeRateLimit = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $success = &rateLimitHandler($treeRateLimit); + } + else { + say $mech->content(); + say 'Parsing error'; + die; + } + } + } + +} + +sub getTerminTime { + &Delimiter((caller(0))[3]); + my ($url, $id, $service_id) = @_; + + say "GETTING $url"; + + if ($config{'BROWSER'} eq 'MECH') { + $mech->get( $url ); + say $mech->status(); + if ($mech->success()) { + my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $success = &parseTerminTime($tree, $id, $service_id); + if (!$success) { + say $mech->content(); + say 'No success in parseTerminTime. Retrying...'; + &getTerminTime($url, $id, $service_id); # RETRY + } + } + else { + if ($mech->status() == 428) { + my $treeCaptcha = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $success = &captchaHandler($treeCaptcha); + &getTerminTime($url, $id, $service_id); # RETRY + } + elsif ($mech->status() == 429) { + say $mech->content(); + # my $treeRateLimit = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $treeRateLimit = HTML::TreeBuilder::XPath->new_from_content($mech->content()); + my $success = &rateLimitHandler($treeRateLimit); + &getTerminTime($url, $id, $service_id); # RETRY + } + } + } +} + +sub rateLimitHandler { + &Delimiter((caller(0))[3]); + my $tree = shift; + + my $error = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " offset2 ")]/h1'); + say $error; + # my @list = $tree->findvalues'//'); + # print Dumper \@list; + + if ($error eq 'Zu viele Zugriffe') { + say 'Rate Limit detected.'; + &sleepCountdown($config{'RATE_LIMIT_SLEEP_MIN'} * 60); + return 1; + } + else { + die 'Could not resolve error.'; + } +} + +sub sleepCountdown { + &Delimiter((caller(0))[3]); + my $countdown = shift; + + $|++; + for (my $i=$countdown; $i >= 0; $i--) { + printf "Sleeping for %s seconds...\r", $i; + sleep(1); + } + return 1; +} + +sub captchaHandler { + &Delimiter((caller(0))[3]); + my $tree = shift; + + my $captchaBase64 = $tree->findvalue('//fieldset[contains(concat(" ", normalize-space(@class), " "), " well ")]/p/img/@src'); + my $filename = &base64ToFile($captchaBase64); + # system("cacaview $filename"); + system("tiv -0 $filename"); + + say 'Please solve captcha ' . basename($filename) . ' to proceed: '; + my $captcha_text = ; + chomp $captcha_text; + my $urlCaptcha = "$config{'URL_HUMAN'}/?captcha_text=$captcha_text"; + + if ($config{'BROWSER'} eq 'MECH') { + $mech->get( $urlCaptcha ); + say $mech->status(); + if ($mech->success()) { + say 'Captcha successfully solved. Saving cookie...'; + $cookie_jar->save($config{'COOKIE_PATH'}); + return 1; + } + else { + say 'Captcha could not be solved. Try again...'; + if ($mech->status() == 428) { + return 0; + } + if ($mech->status() == 429) { + say $mech->content(); + my $treeRateLimit = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $success = &rateLimitHandler($treeRateLimit); + } + } + } +} + +sub trim { + my $string = shift; + $string =~ s/^\s+|\s+$//g; + return $string; +} + +sub parseTerminTime { + &Delimiter((caller(0))[3]); + my ($tree, $id, $service_id) = @_; + + my $xpath = '//div[contains(concat(" ", normalize-space(@class), " "), " timetable ")]/table/tbody/tr'; + my $xpath_stop = '//link[contains(concat(" ", normalize-space(@rel), " "), " alternate ")]/@href'; + + if ($tree->exists($xpath)) { + my $time; + foreach my $buergeramt_dataset ($tree->findnodes($xpath)) { + if ($buergeramt_dataset->findvalue('./th[1]')) { + $time = trim($buergeramt_dataset->findvalue('./th[1]')); + $time = &createMysqlDateTime("$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} $time"); + # print "Parsing for $time..."; + } + foreach my $buergeramt ($buergeramt_dataset->findnodes('./td[contains(concat(" ", normalize-space(@class), " "), " frei ")]')) { + my $name = trim($buergeramt->findvalue('./a')); + my $href = $buergeramt->findvalue('./a/@href'); + say $time; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'VENUE'} = $name; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'HREF'} = "$config{'URL_ROOT'}$href"; + + # say $name . ' needs match: ' . grep{ $venues{$_}{'NAME'} eq $name} keys %venues; + ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'VENUE_ID'}) = grep{ $venues{$_}{'NAME'} eq $name} keys %venues; + + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'EPOCHTIME'} = DateTime::Format::DateParse->parse_datetime($time)->epoch(); + } + } + return 1; + } + elsif ($tree->findvalue($xpath_stop) eq '/terminvereinbarung/termin/stop/') { + say "XPATH_STOP DETECTED"; + die; + } + else { + say 'Parsing error.'; + return 0; + } +} + +sub base64ToFile { + &Delimiter((caller(0))[3]); + my $base64 = shift; + + my $timestamp = &GetTimestamp('YMDHMS'); + $base64 =~ s/data:image\/(.+?)\;base64,//; + my $decoded= decode_base64($base64); + my $filename = "$config{'CAPTCHA_DIR'}/$timestamp.png"; + open my $fh, '>', $filename or die $!; + binmode $fh; + print $fh $decoded; + close $fh; + return $filename; +} + +sub Delimiter { + my $SubName = shift; + print "\n" . "-" x 80 . "\nSUB " . $SubName . "\n" . '-' x 80 . "\n"; +} + +sub GetTimestamp { + #&Delimiter((caller(0))[3]); + my $switch = shift; + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time); + + my $nice_timestamp; + if ($switch eq 'YMDHMS') { + $nice_timestamp = sprintf ( "%04d%02d%02d_%02d%02d%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec); + } + elsif ($switch eq 'YMD') { + $nice_timestamp = sprintf ( "%04d%02d%02d", $year+1900,$mon+1,$mday); + } + elsif ($switch eq 'year') { + $nice_timestamp = $year+1900; + } + elsif ($switch eq 'month') { + $nice_timestamp = $mon+10; + } + else { + print "Invalid/no switch detected. Use: 'YMDHMS' / 'YMD'\n"; + } + + return $nice_timestamp; +} \ No newline at end of file diff --git a/src/server/service.berlin.de.2.pl b/src/server/service.berlin.de.2.pl new file mode 100755 index 0000000..c33bdc8 --- /dev/null +++ b/src/server/service.berlin.de.2.pl @@ -0,0 +1,1295 @@ +#!usr/bin/perl +use strict; +use warnings; +no warnings 'redefine'; +# use diagnostics; +use Data::Dumper; +use HTML::TreeBuilder::XPath; +use feature qw(say); +use File::Basename; +use HTTP::Cookies; +use MIME::Base64 qw(decode_base64); +use Encode; +use utf8; +use DateTime::Format::Strptime; +use DateTime::Format::MySQL; +use DBI; +use DBD::MariaDB; +use DateTime::Format::DateParse; +use autodie qw(:all); +use Parallel::ForkManager; +use DBIx::Connector; +# use Storable; +# use Data::Printer; +# use DateTime; +# use HTML::Entities; +# binmode STDOUT, ':encoding(utf-8)'; +# binmode STDIN, ':encoding(utf-8)'; +# use open ':std', ':encoding(UTF-8)'; + +my ($zsmappointment, $ts, $mech, $ua, $cookie_jar, $strp, %data, %services, %venues, $connection, $strp_datetime, $pm); + +my %config = ( + 'MYSQL' => { + 'ACCESS' => { + 'database' => 'terminsnipe', + 'hostname' => 'zinomedia.de', + 'user' => 'terminsnipe', + 'pw' => 'dIHB6JLk0CBBx1p7', + 'port' => 3306, + }, + 'OPTIONS' => { + 'PrintError' => 0, + 'RaiseError' => 1, + 'AutoCommit' => 1, + } + }, + 'URL_ROOT' => 'https://service.berlin.de', + 'URL_TERMIN_DAY' => 'https://service.berlin.de/terminvereinbarung/termin/day', + 'URL_TERMIN_TIME' => 'https://service.berlin.de/terminvereinbarung/termin/time', + 'URL_HUMAN' => 'https://service.berlin.de/terminvereinbarung/termin/human', + 'URL_STANDORTE' => 'https://service.berlin.de/standorte', + 'URL_DIENSTLEISTUNGEN' => 'https://service.berlin.de/dienstleistungen', + 'BROWSER' => 'MECH', + 'CAPTCHA_DIR' => './captcha', + 'COOKIE_DIR' => './cookie', + 'COOKIE_PATH' => './cookie/.cookies.txt', + 'FOLLOW_NEXT' => 1, + 'OUTPUT_PATH' => './output/data', + 'FILTER_SERVICES_TO_OUTPUT' => 1, + 'FILTER_SERVICES_PATH' => './output/filter_services', + 'DB_REFRESH_SERVICES' => 0, + 'RUN_SERVICES' => 0, + 'RUN_VENUES' => 0, + 'VENUES_TO_FILE' => 1, + 'VENUES_PATH' => './output/venues', + 'DB_REFRESH_VENUES' => 0, + 'DUMP_SERVICES_TO_FILE' => 1, + 'SERVICES_PATH' => './output/services', + 'OUTPUT_DIR' => './output', + 'DB_CLEAN_DATETIMES' => 1, + 'RATE_LIMIT_SLEEP_MIN' => 5, + 'SLEEP_LOOP_MIN' => 1, + 'SERVICES' => { + 'Melderegisterauskunft sperren' => { 'RUN' => 0, 'ID' => 120678 }, + 'Erweiterung einer Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121629 }, + 'Ersatzführerschein nach Verlust/Diebstahl' => { 'RUN' => 0, 'ID' => 121593 }, + 'Kinderreisepass beantragen / verlängern / aktualisieren' => { 'RUN' => 0, 'ID' => 121469 }, + 'Namensänderung in den Fahrzeugpapieren' => { 'RUN' => 0, 'ID' => 324173 }, + 'Änderung/Wechsel der Hauptwohnung' => { 'RUN' => 0, 'ID' => 120697 }, + 'Umschreibung einer ausländischen Fahrerlaubnis aus einem Nicht-EU/EWR-Land (Drittstaat/Anlage 11)' => { 'RUN' => 0, 'ID' => 327537 }, + 'Historisches Kennzeichen beantragen' => { 'RUN' => 0, 'ID' => 121478 }, + 'Verlängerung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121634 }, + 'Adressänderung in der Zulassungsbescheinigung Teil I (ZBI) bzw. in dem Fahrzeugschein' => { 'RUN' => 0, 'ID' => 120658 }, + 'Blaue Karte EU zu einem neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 326798 }, + 'Neufahrzeug anmelden' => { 'RUN' => 0, 'ID' => 120882 }, + 'Zulassung eines aus dem EU-Ausland eingeführten fabrikneuen Fahrzeuges mit Zulassungsbescheinigung Teil II' => { 'RUN' => 0, 'ID' => 324199 }, + 'Führungszeugnis' => { 'RUN' => 0, 'ID' => 120926 }, + 'Aufenthaltserlaubnis für im Bundesgebiet geborene Kinder - Erteilung' => { 'RUN' => 0, 'ID' => 324269 }, + 'Kraftfahrzeug – technische Änderung melden' => { 'RUN' => 0, 'ID' => 120904 }, + 'Reisepass beantragen' => { 'RUN' => 0, 'ID' => 121151 }, + 'Meldebescheinigung beantragen' => { 'RUN' => 0, 'ID' => 120702 }, + 'Umtausch eines Kartenführerscheins' => { 'RUN' => 0, 'ID' => 121616 }, + 'Umschreibung einer Dienstfahrerlaubnis' => { 'RUN' => 0, 'ID' => 121615 }, + 'Bescheinigung über ein unbefristetes Aufenthaltsrecht' => { 'RUN' => 0, 'ID' => 324921 }, + 'Elektrokennzeichen beantragen' => { 'RUN' => 0, 'ID' => 327084 }, + 'Abmeldung einer Wohnung' => { 'RUN' => 0, 'ID' => 120335 }, + 'Zulassungsbescheinigung Teil II für KFZ wegen Verlust oder Diebstahl ersetzen' => { 'RUN' => 0, 'ID' => 121584 }, + 'Beglaubigung von Unterschriften' => { 'RUN' => 0, 'ID' => 158142 }, + 'Kraftfahrzeugkennzeichen wegen Unleserlichkeit ersetzen' => { 'RUN' => 0, 'ID' => 324907 }, + 'Zulassung von Taxen (Kraftdroschken)' => { 'RUN' => 0, 'ID' => 326033 }, + 'Fahrerlaubnis - Erweiterung auf die Klassen D1, D1E, D und DE' => { 'RUN' => 0, 'ID' => 324450 }, + 'Wiederzulassung eines Kraftfahrzeuges ohne Halterwechsel beantragen' => { 'RUN' => 0, 'ID' => 120905 }, + 'Ersterteilung einer Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121627 }, + 'Beglaubigung von Kopien' => { 'RUN' => 0, 'ID' => 121701 }, + 'Saisonkennzeichen für KFZ beantragen' => { 'RUN' => 0, 'ID' => 121480 }, + 'Anmeldung einer Wohnung' => { 'RUN' => 0, 'ID' => 120686 }, + 'Zulassung eines aus dem EU-Ausland eingeführten fabrikneuen Fahrzeuges ohne Zulassungsbescheinigung Teil II' => { 'RUN' => 0, 'ID' => 324200 }, + 'Kraftfahrzeug ummelden – nach einem Umzug nach Berlin' => { 'RUN' => 0, 'ID' => 120918 }, + 'Umschreibung eines zugelassenen Fahrzeuges mit Berliner Kennzeichen auf einen anderen Halter' => { 'RUN' => 0, 'ID' => 120906 }, + 'Reisepass beantragen (vorläufiger Reisepass)' => { 'RUN' => 0, 'ID' => 121153 }, + 'Kraftfahrzeug außer Betrieb setzen (Kfz abmelden)' => { 'RUN' => 0, 'ID' => 120877 }, + 'Umstellung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 124556 }, + 'Widerspruchsrechte gegen Datenübermittlungen und Melderegisterauskünfte' => { 'RUN' => 0, 'ID' => 319141 }, + 'Gewerbezentralregister - Auskunft beantragen' => { 'RUN' => 0, 'ID' => 327835 }, + 'Wechselkennzeichen beantragen' => { 'RUN' => 0, 'ID' => 324587 }, + 'Personalausweis vorläufig beantragen' => { 'RUN' => 0, 'ID' => 120682 }, + 'Personalausweis beantragen' => { 'RUN' => 0, 'ID' => 120703 }, + 'Kraftfahrzeugkennzeichen ersetzen – nach Diebstahl oder Verlust eines Nummernschildes' => { 'RUN' => 0, 'ID' => 324196 }, + 'Umschreibung einer ausländischen Fahrerlaubnis aus einem EU-/EWR-Staat' => { 'RUN' => 0, 'ID' => 121598 }, + 'Selbstfahrermietfahrzeug – Beginn/Beendigung der Nutzung' => { 'RUN' => 0, 'ID' => 326039 }, + 'Zulassungsbescheinigung Teil I für KFZ wegen Verlust oder Diebstahl ersetzen' => { 'RUN' => 0, 'ID' => 121575 }, + 'Kraftfahrzeugkennzeichen auf Wunsch ändern' => { 'RUN' => 0, 'ID' => 121473 }, + 'Begleitetes Fahren mit 17' => { 'RUN' => 0, 'ID' => 121589 }, + 'Fahrerlaubnis zur Fahrgastbeförderung (P-Schein) - Verlängerung' => { 'RUN' => 0, 'ID' => 324389 }, + 'Zulassung eines außer Betrieb gesetzten Fahrzeuges mit Berliner Kennzeichen auf einen anderen Halter' => { 'RUN' => 0, 'ID' => 324169 }, + 'Befreiung von der Ausweispflicht' => { 'RUN' => 0, 'ID' => 327044 }, + 'Grünes Kennzeichen beantragen' => { 'RUN' => 0, 'ID' => 121482 }, + 'Aufenthaltserlaubnis in einen neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 121874 }, + 'Kraftfahrzeug außer Betrieb setzen, unvollständige Unterlagen (Kfz abmelden)' => { 'RUN' => 0, 'ID' => 325881 }, + 'Führerschein: Ausstellung – Internationaler Führerschein' => { 'RUN' => 0, 'ID' => 121591 }, + 'Neuerteilung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121637 }, + 'Niederlassungserlaubnis oder Erlaubnis zum Daueraufenthalt-EU in einen neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 324280 }, + 'Fahrerlaubnis zur Fahrgastbeförderung (P-Schein) - Erteilung' => { 'RUN' => 0, 'ID' => 121622 }, + } +); + + +# # INIT +&init(); +# &getRegister(); +&getRegisterControls(); +# db_createRequestDienstleister(20); +# &getStandorte(); +# &getDienstleistungen(); +die; + +# # MAIN +# &getStandorte() if $config{'RUN_VENUES'}; +# &getDienstleistungen() if $config{'RUN_SERVICES'}; +# # &run(); +# &db_request(); +# &Summary(); + + + +&programLoop(); + +# SUBS + +sub responseHandlerMech { + &Delimiter((caller(0))[3]); + my ($url, $switch) = @_; + $switch = '' if !$switch; + + print "GETTING $url... "; + + $mech->get($url); + say $mech->status(); + + # my $tree = HTML::TreeBuilder::XPath->new_from_content($mech->content()); + my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my ($success, $code) = &checkSuccess($tree, $mech); # returns 1 or 0 for $success and error/success code for $code + if (!$success && !$code) { + &dumpToFile('./output/successcodeundefined', $mech->content); + die '$success and $code undefined'; + } + say "success: $success\tcode: $code"; + + if ($mech->success()) { + if ($code == -6) { + say 'Mech->code 200 and success_checkSuccess ' . $code . ' : Starting new appointment search...'; + my $response = &restartHandler($tree); + return $response; + } + + if ($switch eq 'getRegister') { + return { 'success' => 1, 'tree' => $tree, 'status' => $mech->status, 'success_checkSuccess' => $success, 'mech' => $mech, 'code_checkSuccess' => $code }; + } + elsif ($switch eq 'getDienstleistungen' || $switch eq 'filterDienstleistungen' || $switch eq 'getStandorte' || $switch eq 'getTerminDay' || $switch eq 'getRestart' || $switch eq 'getTerminTime' || $switch eq 'captchaHandler') { + return { 'success' => 1, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + else { + return { 'success' => 1, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + } + else { + if ($mech->status() == 404) { + if ($switch eq 'getRegister') { + return { 'success' => 1, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + else { + say $mech->content(); + die 'No handler'; + } + } + elsif ($mech->status() == 429) { + if ($switch eq 'getDiensleistungen' || $switch eq 'filterDienstleistungen' || $switch eq 'getStandorte' || $switch eq 'getTerminDay' || $switch eq 'getTerminTime') { + &dumpToFile('./output/429', $mech->content); + my $treeRateLimit = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + my $successRate = &rateLimitHandler($treeRateLimit); + return { 'success' => 0, 'status' => $mech->status, 'success_rateLimitHandler' => $successRate, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + } + elsif ($mech->status() == 428) { + # if ($switch eq 'getTerminTime' || $switch eq 'captchaHandler') { + # # say $mech->content(); + # return { 'success' => 0, 'status' => $mech->status, 'tree' => $tree, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + # } + # say $mech->content(); + if ($code == -5 || $code == -7) { + say 'Mech->code 428 and success_checkSuccess ' . $code . ' : Renewing captcha...'; + my $response = &captchaHandler($tree); + return $response; + } + + } + else { + return { 'success' => 0, 'status' => $mech->status, 'tree' => $tree, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + } +} + +sub restartHandler { + &Delimiter((caller(0))[3]); + my $tree = shift; + + my $url = '//div[contains(concat(" ", normalize-space(@class), " "), " alert alert-error noprint textile ")]/ul/li[2]/a/@href'; + my $response = &responseHandlerMech($url); + if ($response->{'success'}) { + say 'New appointment search successfully started.'; + } + elsif (!$response->{'success'}) { + say 'New appointment search not successfull.'; + print Dumper $response; + } + return $response; +} + +sub getRegister { + &Delimiter((caller(0))[3]); + my ($dataset_ref, $user_id) = @_; + + my $href = $$dataset_ref{'HREF'}; + + # my $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1576747440/109/', 'getRegister'); + my $response = &responseHandlerMech('https://zinomedia.de/dl/customHTML2.html', 'getRegister'); + + if ($response->{'success_checkSuccess'}) { + &getRegisterControls($mech); + # &submitRegister($mech); + } + else { + die; + } +} + +sub getRegisterControls { + &Delimiter((caller(0))[3]); + use Digest::MD5 qw(md5 md5_hex); + use Data::Structure::Util qw( unbless ); + my %controls; + + for my $service (sort keys %{$config{'SERVICES'}}) { + my $service_id = $config{'SERVICES'}{$service}{'ID'}; + say "\nSTART GETTING CONTROLS FOR SERVICE '$service'..."; + my $href = "https://service.berlin.de/dienstleistung/$service_id/"; + + my $response = &responseHandlerMech($href); + if ($response->{'success'}) { + say 'success'; + + my @nodes = $response->{'tree'}->findnodes('//div[@class="behoerdenitem"]/div[@class="row"]'); + say $#nodes; + # print Dumper $nodes; + + foreach my $node (@nodes) { + if ($node->exists('./div[@class="span5" and (strong)]') || !$node->exists('./div[@class="span2"]/p/strong/a/@href')) { + next; + } + my $venue = $node->findvalue('./div[@class="span5"]'); + my $terminvereinbarung_url = $node->findvalue('./div[@class="span2"]/p/strong/a/@href'); + my $last = 0; + + say "$venue => $terminvereinbarung_url"; + + my $success = &getTerminDay($terminvereinbarung_url, $service_id, 'getRegisterControls'); + if ($success) { + say 'We have appointments.'; + # print Dumper $data{'SERVICES'}{$service_id}{'AVAILABLE'}; + + foreach my $epochtime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}) { + foreach my $datetime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$epochtime}{'DATETIMES'}}) { + my $register_url = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$epochtime}{'DATETIMES'}{$datetime}{'HREF'}; + say $datetime; + say $register_url; + + + my $response = &responseHandlerMech($register_url, 'getRegister'); + if ($response->{'success_checkSuccess'}) { + my $form = $mech->form_number(2); + my @inputs = $mech->find_all_inputs( + # type => 'textarea', + # name_regex => qr/^customer/, + ); + + my $url = $mech->uri()->as_string; + + foreach my $i (0 .. $#inputs) { + my $digest; + my $contains_pflichtfeld = 0; + foreach my $key (sort keys %{$inputs[$i]}) { + # print "KEY $key VALUE "; + # say $inputs[0]{$key}; + $digest .= "$key$inputs[$i]{$key}"; + $contains_pflichtfeld = 1 if $inputs[$i]{$key} =~ m/\*$/; + } + $digest = md5_hex($digest); + if (!$controls{$digest}) { + # $controls{$digest}{'HASH'} = unbless(\%{$inputs[$i]}); + $controls{$digest}{'HASH'} = \%{$inputs[$i]}; + $controls{$digest}{'FIRST_LVL_PFLICHTFELD'} = $contains_pflichtfeld; + $controls{$digest}{'URI'}{$url} = 1; + push(@{$controls{$digest}{'VENUES'}}, "$venue => $service"); + } + else { + push(@{$controls{$digest}{'VENUES'}}, "$venue => $service"); + } + + } + + say 'Aborting now...'; + my $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/abort/'); + sleep(5); + $last = 1; + last if $last; + # say 'Clicking abort now...'; + # $mech->click_button(id => 'register_abort'); + # sleep(10); + } + + # print Dumper \%controls; + # die; + # last; + last if $last; + } + last if $last; + } + + } + + # print Dumper \%controls; + # die; + } + + + die; + } + + + # &getTerminDay($services{$service_id}{'HREF'}, $service_id); + # &db_sync($service_id); + } + + die; + + + + + + # say $digest; + # print Dumper \@inputs; + +} + +sub checkSuccess { + &Delimiter((caller(0))[3]); + my ($tree, $mech) = @_; + + my $xpath_alert = '//div[contains(concat(" ", normalize-space(@class), " "), " alert")]/p[1]'; + my $xpath_alert2 = '//div[contains(concat(" ", normalize-space(@class), " "), " alert")]/div/p'; + my $xpath_alert3 = '//div[@class="zms"]/div[@class="html5-header header"]/h1[@class="title"]'; + + my $xpath_success = '//div[@class="submit-success-message"]'; + + if ($tree->exists($xpath_alert)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert))); + say "Alert: $alert"; + + if ($alert eq 'Sie haben schon einen Termin reserviert. Soll diese Reservierung gelöscht werden oder möchten Sie mit der vorhandenen Reservierung fortfahren?') { + say 'Code -1: Another reservation is already in progress.'; + &dumpToFile('./output/-1', $mech->content); + return (0, -1); + } + elsif ($alert eq 'Zu ihrer Suche konnten keine Daten ermittelt werden. Dies kann unterschiedliche Gründe haben:') { + say 'Code -2: Cookie seems outdated'; + &dumpToFile('./output/-2', $mech->content); + return (0, -2); + } + elsif ($alert eq 'Sie haben Ihre Terminvereinbarung abgeschlossen. Sie können nun eine neue Terminssuche starten!') { + say 'Code -3: Reservation already completed'; + &dumpToFile('./output/-3', $mech->content); + return (0, -3); + } + elsif ($alert eq 'Ihr Termin ist eingetragen worden. Bitte drucken Sie diese Seite aus, damit Sie alle Informationen zur Hand haben. Wichtig für Ihren Besuch ist die Vorlage Ihrer Vorgangsnummer.') { + say 'Code 1: submitRegister successfull.'; + &dumpToFile('./output/1', $mech->content); + say trim($tree->findvalue($xpath_success)); + say $tree->findvalue('//span[@class="summary_authKey"]'); + say $tree->findvalue('//span[contains(concat(" ", normalize-space(@class), " "), " summary_processId ")]'); + return (1, 1); + } + elsif ($alert eq 'Der eingegebene Text stimmte nicht mit dem Bild überein. Versuchen Sie es noch einmal.') { + say 'Code -5: Text / Picture mismatch'; + &dumpToFile('./output/-5', $mech->content); + return (0, -5); + } + elsif ($alert eq 'Ihre Auswahl von Standort und Diensteistung hat sich geändert. Möchten Sie mit Ihrer vorherigen Suche fortfahren oder eine neue Terminsuche starten?') { + say 'Code -6: Location and venue changed. New search or continue.'; + &dumpToFile('./output/-6', $mech->content); + return (0, -6); + } + else { + say $mech->content(); + say 'NO XPATH SPECIFIED DETECTED'; + &dumpToFile('./output/noxpathspecified', $mech->content); + # my $string = trim($tree->findvalue($xpath_alert)); + say $alert; + # $string = decode('utf-8', $string); + # say $string; + # say utf8::is_utf8($string); # since Perl 5.8.1 + # say utf8::valid($string); + die; + } + + } + elsif ($tree->exists($xpath_alert2)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert2))); + say "Alert2: $alert"; + + if ($alert eq 'Leider konnte der ausgewählte Termin nicht reserviert werden. Da war jemand schneller als Sie. Bitte versuchen Sie es erneut.') { + say 'Code -4: Appointment not available anymore and booked by another person.'; + &dumpToFile('./output/-4', $mech->content); + return (0, -4); + } + } + elsif ($tree->exists($xpath_alert3)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert3))); + say "Alert 3: $alert"; + + if ($alert eq 'Bitte verifizieren sie sich') { + say 'Code -7: Verification needed.'; + &dumpToFile('./output/-7', $mech->content); + return (0, -7); + } + elsif ($alert eq 'Terminvereinbarung') { + say 'Code 3: Normal Terminvereinbarung'; + &dumpToFile('./output/-7', $mech->content); + return (1, 3); + } + } + else { + say 'Code 2: NO XPATH SPECIFIED DETECTED. Everything fine?'; + &dumpToFile('./output/2', $mech->content); + return (1, 2); + } + +} + +sub submitRegister { + &Delimiter((caller(0))[3]); + my $mech = shift; + + + # print Dumper $mech->all_form_with_fields( ['familyName'] ); + my $form = $mech->form_number(2); + # print Dumper $result; + $mech->set_fields( 'familyName' => 'John Smith', 'email' => 'shoty.at.cs@gmx.net', ); + $mech->tick('agbgelesen', 1); + $mech->click_button(id => 'register_submit'); + + say $mech->status(); + if ($mech->success()) { + say $mech->content; + &dumpToFile('./output/success', $mech->content); + my $tree_success = HTML::TreeBuilder::XPath->new_from_content($mech->content()); + &checkSuccess($tree_success); + } + else { + if ($mech->status() == 404) { + say $mech->content(); + say 'PAGE NOT FOUND'; + } + die; + } +} + +sub programLoop { + &Delimiter((caller(0))[3]); + + for (;;) { + # INIT + &init(); + + # MAIN + &getStandorte() if $config{'RUN_VENUES'}; + &getDienstleistungen() if $config{'RUN_SERVICES'}; + &db_request(); + &Summary(); + + &sleepCountdown($config{'SLEEP_LOOP_MIN'} * 60); + } +} + +sub db_createRequestDienstleister { + &Delimiter((caller(0))[3]); + my $request_id = shift; + my $values; + + my $string = &db_query("Select terminsnipe.services.HREF From terminsnipe.request Inner Join terminsnipe.services On terminsnipe.services.ID = terminsnipe.request.SERVICE_ID Where terminsnipe.request.ID = $request_id", $connection, 'fetchrow'); + my @dienstleister = getDienstleisterlist($string); + + foreach my $id (@dienstleister) { + $values .= "('$request_id', '$id'), "; + } + my $sql = substr("INSERT INTO `request_dienstleister` (`REQUEST_ID`, `VENUE_ID`) VALUES $values", 0, -2); + # say $sql; + + &db_query("DELETE FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id", $connection, 'do'); + &db_query($sql, $connection, 'do'); +} + +sub getDienstleisterlist { + &Delimiter((caller(0))[3]); + my $string = shift; + + + $string =~ m/dienstleisterlist=(.+?)&/; + return split /,/, $1; +} + +sub db_request { + &Delimiter((caller(0))[3]); + my %request; + + my $response = &db_query("SELECT `ID`, `USER_ID`, `SERVICE_ID`, `DATETIME_FROM`, `DATETIME_TO`, `TIMESTAMP` FROM `request` WHERE `EXPIRED` = 0;", $connection, 'selectall_arrayref'); + if (@$response) { + for my $i (0 .. $#{$response}) { + my $dataset = $$response[$i]; + $request{$$dataset[2]}{$$dataset[0]}{'ID'} = $$dataset[0]; + $request{$$dataset[2]}{$$dataset[0]}{'USER_ID'} = $$dataset[1]; + $request{$$dataset[2]}{$$dataset[0]}{'SERVICE_ID'} = $$dataset[2]; + $request{$$dataset[2]}{$$dataset[0]}{'DATETIME_FROM'} = $$dataset[3]; + $request{$$dataset[2]}{$$dataset[0]}{'DATETIME_TO'} = $$dataset[4]; + $request{$$dataset[2]}{$$dataset[0]}{'TIMESTAMP'} = $$dataset[5]; + $services{$$dataset[2]}{'HREF_GENERATED'} = &db_generateHREF($$dataset[0], $$dataset[2]); + } + print Dumper \%request; + + foreach my $service_id (keys %request) { + &startFork($service_id, \%request); + } + $pm->wait_all_children; + say 'All childs are done. Continuing with parent...'; + foreach my $service_id (keys %request) { + &db_find_appointment($service_id, \%request); + } + } + else { + say 'No requests in db.'; + } +} + +sub db_generateHREF { + &Delimiter((caller(0))[3]); + my ($request_id, $service_id) = @_; + + my $href_generated = $services{$service_id}{'HREF'}; + my $response = &db_query("SELECT `VENUE_ID` FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id;", $connection, 'selectall_arrayref'); + + my $dienstleisterlist; + for my $i (0 .. $#{$response}) { + $dienstleisterlist .= "$$response[$i][0],"; + } + $dienstleisterlist = substr($dienstleisterlist, 0, -1); + $href_generated =~ m/dienstleisterlist=(.+?)&/; + $href_generated =~ s/$1/$dienstleisterlist/; + + return $href_generated; +} + +sub startFork { + &Delimiter((caller(0))[3]); + my ($service_id, $request_ref) = @_; + + my $pid = $pm->start($service_id) and next; + + &run_from_request($service_id); + &db_sync($service_id); + # &db_find_appointment($service_id, $request_ref); + + $pm->finish; +} + +sub run_from_request { + &Delimiter((caller(0))[3]); + my $service_id = shift; + + my $service = $services{$service_id}{'NAME'}; + my $href = $services{$service_id}{'HREF_GENERATED'}; + + say "START RUN FOR SERVICE '$service'..."; + &getTerminDay($href, $service_id); +} + +sub db_find_appointment { + &Delimiter((caller(0))[3]); + my ($service_id, $request_ref) = @_; + + foreach my $id (keys %{$$request_ref{$service_id}}) { + my $query = "Select terminsnipe.datetime.DATETIME, terminsnipe.mapping.SERVICE_ID, terminsnipe.mapping.VENUE_ID, terminsnipe.mapping.HREF As HREF, terminsnipe.venues.NAME As VENUE_NAME, terminsnipe.datetime.EPOCHTIME, terminsnipe.mapping.ID As MAPPING_ID From terminsnipe.datetime Inner Join terminsnipe.mapping On terminsnipe.mapping.DATETIME_ID = terminsnipe.datetime.EPOCHTIME Inner Join terminsnipe.venues On terminsnipe.mapping.VENUE_ID = terminsnipe.venues.ID Where Not terminsnipe.datetime.DATETIME > '$$request_ref{$service_id}{$id}{'DATETIME_TO'}' And Not terminsnipe.datetime.DATETIME < '$$request_ref{$service_id}{$id}{'DATETIME_FROM'}' And terminsnipe.mapping.SERVICE_ID = $service_id;"; + say $query; + my $response = &db_query($query, $connection, 'selectall_hashref', 'EPOCHTIME'); + print Dumper $response; + + say 'Possible appointments: ' . scalar(keys(%$response)); + if (%$response) { + say 'defined'; + + &db_insertAppointment($response, $$request_ref{$service_id}{$id}); + } + } +} + +sub db_insertAppointment { + &Delimiter((caller(0))[3]); + my ($response, $dataset_ref) = @_; + my $user_id = $$dataset_ref{'USER_ID'}; + my $request_id = $$dataset_ref{'ID'}; + + foreach my $epochtime (keys %{$response}) { + if (!$$response{$epochtime}{'RESERVED'}) { + say "Reserving appointment $$response{$epochtime}{MAPPING_ID} for USER_ID $user_id now..."; + + &getRegister($$response{$epochtime}, $user_id); + + &db_query("INSERT INTO `appointments` (`MAPPING_ID`, `USER_ID`) VALUES ('$$response{$epochtime}{MAPPING_ID}', '$user_id');", $connection, 'do'); + $$response{$epochtime}{'RESERVED'} = 1; + + say 'Deleting request with ID $request_id from request and request_dienstleister...'; + &db_query("DELETE FROM `request` WHERE `ID` = $request_id;", $connection, 'do'); + &db_query("DELETE FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id;", $connection, 'do'); + last; + } + else { + say "Appointment $epochtime is available but already reserved in loop by another user."; + } + } +} + +sub db_query { + # &Delimiter((caller(0))[3]); + my ($query, $connection, $switch, @rest) = @_; + my $response; + + # say 'DATABASE CONNECTION STATE: ' . $connection->ping; + + # $query = decode("utf-8", $query); + # $query = encode( "iso-8859-1", $query ); + + my $statement; + if ($switch eq 'fetchrow') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchrow(); + } + elsif ($switch eq 'fetchall_hashref') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchall_hashref($rest[0]); + } + elsif ($switch eq 'fetchall_arrayref') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchall_arrayref(); + } + elsif ($switch eq 'do') { + $response = $connection->do($query) or die $connection->errstr; + } + elsif ($switch eq 'selectall_hashref') { + $response = $connection->selectall_hashref($query, $rest[0]) or warn $connection->errstr; + } + elsif ($switch eq 'selectall_array') { + my @response = $connection->selectall_array($query) or die $connection->errstr; + return \@response; + } + elsif ($switch eq 'selectall_arrayref') { + $response = $connection->selectall_arrayref($query) or die $connection->errstr; + } + elsif ($switch eq 'selectrow_hashref') { + $response = $connection->selectrow_hashref($query) or die $connection->errstr; + } + + $statement->finish if $statement; + return $response; +} + +sub connectToMySql { + &Delimiter((caller(0))[3]); + my ($db) = @_; + + # assign the values to your connection variable + my $connectionInfo = "dbi:mysql:$db;$config{'MYSQL'}{'ACCESS'}{'hostname'}"; + + # make connection to database + # my $l_connection = DBI->connect($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, $config{'MYSQL'}{'OPTIONS'}) || warn "MySQL conncet failed: $DBI::errstr"; + # return $l_connection; + + my $l_connection = DBIx::Connector->new($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, { + RaiseError => 1, + AutoCommit => 1, + }); + return $l_connection->dbh; +} + +sub run { + &Delimiter((caller(0))[3]); + + for my $service (keys %{$config{'SERVICES'}}) { + if ($config{'SERVICES'}{$service}{'RUN'}) { + my $service_id = $config{'SERVICES'}{$service}{'ID'}; + say "START RUN FOR SERVICE '$service'..."; + &getTerminDay($services{$service_id}{'HREF'}, $service_id); + &db_sync($service_id); + } + else { + # say "DISABLED $service"; + } + } +} + +sub db_sync { + &Delimiter((caller(0))[3]); + my $service_id = shift; + + &db_query("DELETE FROM `mapping` WHERE `SERVICE_ID` = '$service_id';", $connection, 'do'); + + foreach my $day (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}) { + foreach my $datetime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}}) { + my $epochtime = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}; + my $venue_id = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'VENUE_ID'}; + my $href = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'HREF'}; + + say "$service_id\t$venue_id\t$epochtime"; + + # say "INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) VALUES ('$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}', '$datetime') ON DUPLICATE KEY UPDATE `DATETIME` = `DATETIME`;"; + # say "INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) SELECT $epochtime, '$datetime' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `datetime` WHERE `EPOCHTIME`= $epochtime);"; + # say "INSERT INTO `mapping` (`SERVICE_ID`, `VENUE_ID`, `DATETIME_ID`, `HREF`) SELECT '$service_id', '$venue_id', '$epochtime', '$href' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `mapping` WHERE `SERVICE_ID` = '$service_id' AND `VENUE_ID` = '$venue_id' AND `DATETIME_ID` = '$epochtime');"; + + # &db_query("INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) VALUES ('$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}', '$datetime') ON DUPLICATE KEY UPDATE `DATETIME` = `DATETIME`;", $connection, 'do'); + &db_query("INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) SELECT $epochtime, '$datetime' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `datetime` WHERE `EPOCHTIME`= $epochtime);", $connection, 'do'); + &db_query("INSERT INTO `mapping` (`SERVICE_ID`, `VENUE_ID`, `DATETIME_ID`, `HREF`) SELECT '$service_id', '$venue_id', '$epochtime', '$href' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `mapping` WHERE `SERVICE_ID` = '$service_id' AND `VENUE_ID` = '$venue_id' AND `DATETIME_ID` = '$epochtime');", $connection, 'do'); + } + } + +} + +sub createMysqlDateTime { + # &Delimiter((caller(0))[3]); + my $string = shift; + + my $dt = $strp->parse_datetime($string); + return DateTime::Format::MySQL->format_datetime($dt); +} + +sub getDienstleistungen { + &Delimiter((caller(0))[3]); + + my $response = &responseHandlerMech($config{'URL_DIENSTLEISTUNGEN'}, 'getDienstleistungen'); + + if ($response->{'success'}) { + &parseDienstleistungen($response->{'tree'}); + } + elsif (!$response->{'success'} && $response->{'success_rateLimitHandler'}) { + say 'No success in getDienstleistungen and rateLimitHandler successfull. Retrying...'; + &getDienstleistungen(); + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in getDienstleistungen and rateLimitHandler unsuccessfull.'; + } + + if ($config{'FILTER_SERVICES_TO_OUTPUT'} || $config{'DB_REFRESH_SERVICES'}) { + &filterDienstleistungen(); + &db_insertServicesVenues('services') if $config{'DB_REFRESH_SERVICES'}; + } + + if ($config{'DUMP_SERVICES_TO_FILE'}) { + &dumpToFile($config{'SERVICES_PATH'}, \%services); + } +} + +sub db_insertServicesVenues { + &Delimiter((caller(0))[3]); + my $switch = shift; + my ($database, $hash_ref); + + if ($switch eq 'services') { + $database = 'services'; + $hash_ref = \%services; + } + elsif ($switch eq 'venues') { + $database = 'venues'; + $hash_ref = \%venues; + } + + my $response = &db_query("delete FROM $database", $connection, 'do'); + + foreach my $id (sort keys %{$hash_ref}) { + my $query = "INSERT INTO `$database` (`ID`, `NAME`, `HREF`) VALUES ('$$hash_ref{$id}{'ID'}', '$$hash_ref{$id}{'NAME'}', '$$hash_ref{$id}{'HREF'}');"; + my $response = &db_query($query, $connection, 'do'); + } +} + +sub filterDienstleistungen { + &Delimiter((caller(0))[3]); + + # DATA DUMPER UTF8 HACK + local *Data::Dumper::qquote = sub { qq["${\(shift)}"] }; + local $Data::Dumper::Useperl = 1; + + open my $FILE, '>>:encoding(UTF-8)', $config{'FILTER_SERVICES_PATH'}; + foreach my $id (keys %services) { + + my $response = &responseHandlerMech($services{$id}{'HREF'}, 'filterDienstleistungen'); + + if ($response->{'success'}) { + my $tree = $response->{'tree'}; + my $name = $services{$id}{'NAME'}; + + print "\t$name => "; + + if (!$tree->exists('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]')) { + say '0'; + delete $services{$id}; + } + else { + my $href = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]/a/@href'); + my $entry = "'$name' => { 'RUN' => 0, 'ID' => $services{$id}{'ID'} },"; + print '1 => ' . $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]/a') . " | $entry\n"; + say $FILE $entry if $config{'FILTER_SERVICES_TO_OUTPUT'}; + $services{$id}{'HREF'} = $href; + } + } + elsif (!$response->{'success'} && $response->{'success_rateLimitHandler'}) { + say 'No success in filterDienstleistungen and rateLimitHandler successfull. Retry here?'; + die; + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in filterDienstleistungen and rateLimitHandler unsuccessfull.'; + } + } + close $FILE; +} + +sub parseDienstleistungen { + &Delimiter((caller(0))[3]); + my $tree = shift; + + foreach my $dataset ($tree->findnodes('//li[contains(concat(" ", normalize-space(@class), " "), " topic-dls row-fluid ")]')) { + my $id = basename($dataset->findvalue('./a/@href')); + my $href = $dataset->findvalue('./a/@href'); + my $name = trim($dataset->findvalue('./a')); + + $services{$id}{'ID'} = $id; + $services{$id}{'HREF'} = "$config{'URL_ROOT'}$href"; + $services{$id}{'NAME'} = $name; + } +} + +sub getStandorte { + &Delimiter((caller(0))[3]); + + my $response = &responseHandlerMech($config{'URL_STANDORTE'}, 'getStandorte'); + + if ($response->{'success'}) { + &parseStandorte($response->{'tree'}); + &db_insertServicesVenues('venues') if $config{'DB_REFRESH_VENUES'}; + dumpToFile($config{'VENUES_PATH'}, \%venues) if $config{'VENUES_TO_FILE'}; + } + elsif (!$response->{'success'} && $response->{'success_rateLimitHandler'}) { + say 'No success in getStandorte and rateLimitHandler successfull. Retry?'; + die; + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in getStandorte and rateLimitHandler unsuccessfull.'; + } +} + +sub dumpToFile { + &Delimiter((caller(0))[3]); + my ($filepath, $ref) = @_; + + # DATA DUMPER UTF8 HACK + # no warnings 'redefine'; + local *Data::Dumper::qquote = sub { qq["${\(shift)}"] }; + local $Data::Dumper::Useperl = 1; + + open my $FILE, '>:encoding(UTF-8)', $filepath; + print $FILE Dumper $ref; + close $FILE; +} + +sub parseStandorte { + &Delimiter((caller(0))[3]); + my $tree = shift; + + foreach my $dataset ($tree->findnodes('//li[contains(concat(" ", normalize-space(@class), " "), " topic-dls ")]')) { + my $id = basename($dataset->findvalue('./a/@href')); + my $href = $dataset->findvalue('./a/@href'); + my $name = trim($dataset->findvalue('./a')); + + say $name; + + $venues{$id}{'ID'} = $id; + $venues{$id}{'HREF'} = "$config{'URL_ROOT'}$href"; + $venues{$id}{'NAME'} = $name; + } +} + +sub init { + &Delimiter((caller(0))[3]); + + ($zsmappointment, $ts, $mech, $ua, $cookie_jar, $strp, $connection, $strp_datetime, $pm) = (undef) x 9; + undef %data; + + # COOKIES & ARGS + $cookie_jar = HTTP::Cookies->new(ignore_discard => 1 ); + if (@ARGV) { + $cookie_jar->set_cookie( 0, 'Zmsappointment', $ARGV[0], '/', 'service.berlin.de', undef, 1, undef, undef, 1, ) if $ARGV[0]; + $cookie_jar->set_cookie( 0, 'TS01fa49ef', $ARGV[1], '/', 'service.berlin.de', undef, 1, undef, undef, 1, ) if $ARGV[1]; + } + elsif (!@ARGV && -e $config{'COOKIE_PATH'}) { + say "Loading $config{'COOKIE_PATH'}..."; + $cookie_jar->load($config{'COOKIE_PATH'}); + } + + # STRP DATETIME + $strp = DateTime::Format::Strptime->new( + pattern => '%d %b %Y %H:%M', + locale => 'de_DE', + ); + + $strp_datetime = DateTime::Format::Strptime->new( + pattern => '%Y-%m-%d %T', + locale => 'de_DE', + ); + + + # BROWSER + if ($config{'BROWSER'} eq 'MECH') { + use WWW::Mechanize (); + $mech = WWW::Mechanize->new( + cookie_jar => $cookie_jar, + agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + autocheck => 0, + ); + } + elsif ($config{'BROWSER'} eq 'LWP') { + use LWP::UserAgent; + $ua = LWP::UserAgent->new( + ssl_opts => { + SSL_version => 'TLSv12:!SSLv2:!SSLv3:!TLSv1:!TLSv11', + }, + cookie_jar => $cookie_jar, + agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + ); + } + else { + die "$config{'BROWSER'} not defined."; + } + + # MYSQL + $connection = &connectToMySql($config{'MYSQL'}{'ACCESS'}{'database'}); + + # SERVICE AND VENUE HASH + $connection->do('set names utf8'); + %venues = %{&db_query("SELECT `ID`, `NAME`, `HREF` FROM `venues`", $connection, 'fetchall_hashref', 'ID')}; + %services = %{&db_query("SELECT `ID`, `NAME`, `HREF` FROM `services`", $connection, 'fetchall_hashref', 'ID')}; + + # EMPTY DIRECTORIES + # unlink glob "'$config{'CAPTCHA_DIR'}/*'"; + unlink glob "'$config{'OUTPUT_DIR'}/*'"; + + # CLEAN OLD DATETIMES, FLAG EXPIRED REQUESTS + &db_query("DELETE FROM `datetime` WHERE `DATETIME` < NOW();", $connection, 'do'); + &db_query("UPDATE request SET `EXPIRED` = 1 WHERE `DATETIME_TO` < NOW();", $connection, 'do'); + + # FORK MANAGER + $pm = Parallel::ForkManager->new(scalar(keys(%services))); + # $pm = Parallel::ForkManager->new(1); + $pm->run_on_start( + sub { + my ($pid, $ident)=@_; + say "\n** $ident started, pid: $pid"; + }); + $pm->run_on_finish( + sub { + my ($pid, $exit_code, $ident) = @_; + say "\n** $ident just got out of the pool with PID $pid and exit code: $exit_code"; + }); + $pm->run_on_wait( + sub { + # my ($pid, $ident) = @_; + say "\n** a child is waiting now..."; + }); +} + +sub Summary { + &Delimiter((caller(0))[3]); + + &dumpToFile($config{'OUTPUT_PATH'}, \%data); + $connection->disconnect(); +} + +sub getTerminDay { + &Delimiter((caller(0))[3]); + my ($url, $service_id, $switch) = @_; + + my $response = &responseHandlerMech($url, 'getTerminDay'); + + if ($response->{'success'}) { + my $success = &parseTerminDay($response->{'tree'}, $service_id, $switch); + + if ($switch eq 'getRegisterControls' && $success) { + say 'No need to go next. Returning...'; + return 1; + } + elsif (!$success) { + say 'No success in getTerminDay'; + # say $response->{'mech'}->content(); + # die; + } + + if ($response->{'tree'}->exists('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')) { + say 'NEXT EXISTS'; + if ($config{'FOLLOW_NEXT'}) { + my $epochtime = basename($response->{'tree'}->findvalue('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')); + my $url = "$config{'URL_TERMIN_DAY'}/$epochtime"; + &getTerminDay($url, $service_id, $switch); + } + } + } + elsif (!$response->{'success'} && $response->{'success_rateLimitHandler'}) { + say 'No success in getTerminDay and rateLimitHandler successfull. Retry?'; + die; + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in getTerminDay and rateLimitHandler unsuccessfull.'; + } +} + +sub parseTerminDay { + &Delimiter((caller(0))[3]); + my ($tree, $service_id, $switch) = @_; + + my $xpath = '//div[contains(concat(" ", normalize-space(@class), " "), " calendar-month-table ")]'; + my $xpath_restart = '//div[contains(concat(" ", normalize-space(@class), " "), " alert alert-error noprint textile ")]/ul/li[2]/a/@href'; + if ($tree->exists($xpath)) { + foreach my $table ($tree->findnodes($xpath)) { + my ($month, $year) = split(/ /, $table->findvalue('./table/thead/tr/th[contains(concat(" ", normalize-space(@class), " "), " month ")]')); + foreach my $buchbar ($table->findnodes('./table/tbody/tr/td[contains(concat(" ", normalize-space(@class), " "), " buchbar ")]')) { + my $id = basename($buchbar->findvalue('./a/@href')); + + if ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}) { + say 'ALREADY PARSED'; + next; + } + else { + my $day = $buchbar->findvalue('./a'); + $data{'SERVICES'}{$service_id}{'SERVICE_ID'} = $service_id; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} = $month; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} = $year; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} = $day; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'ID'} = $id; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIME'} = &createMysqlDateTime("$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} 00:00"); + + my $url = "$config{'URL_TERMIN_TIME'}/$id"; + &getTerminTime($url, $id, $service_id); + + if ($switch eq 'getRegisterControls') { + if (scalar keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}} > 0) { + say 'Successfully parsed at least 1 appointment. Returning...'; + return 1; + } + } + } + } + } + # return 1; + } + elsif ($tree->exists($xpath_restart)) { + say 'XPATH ALERT DETECTED'; + my $url = $config{'URL_ROOT'} . $tree->findvalue($xpath_restart); + say $url; + + my $response = &responseHandlerMech($url, 'getRestart'); + if ($response->{'success'}) { + &parseTerminDay($response->{'tree'}, $service_id, $switch); + } + } + else { + say 'Parsing error.'; + return 0; + } +} + +sub getTerminTime { + &Delimiter((caller(0))[3]); + my ($url, $id, $service_id, $switch) = @_; + + my $response = &responseHandlerMech($url, 'getTerminTime'); + if ($response->{'success'}) { + my $success = &parseTerminTime($response->{'tree'}, $id, $service_id); + if (!$success) { + say $mech->content(); + say 'No success in parseTerminTime. Retrying...'; + &getTerminTime($url, $id, $service_id); # RETRY + } + } + # elsif (!$response->{'success'} && $response->{'status'} == 428) { + # say 'No success in parseTerminTime. Renewing captcha...'; + # my $success = &captchaHandler($response->{'tree'}); + # &getTerminTime($url, $id, $service_id); # RETRY + # } + # elsif (!$response->{'success'} && $response->{'status'} == 429) { + # say 'No success in getStandorte and rateLimitHandler detected. Retrying...'; + # &getTerminTime($url, $id, $service_id); # RETRY + # } +} + +sub rateLimitHandler { + &Delimiter((caller(0))[3]); + my $tree = shift; + + my $error = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " offset2 ")]/h1'); + say $error; + + if ($error eq 'Zu viele Zugriffe') { + say 'Rate Limit detected.'; + &sleepCountdown($config{'RATE_LIMIT_SLEEP_MIN'} * 60); + return 1; + } + else { + say 'Could not resolve error.'; + return 0; + } +} + +sub sleepCountdown { + &Delimiter((caller(0))[3]); + my $countdown = shift; + + $|++; + for (my $i=$countdown; $i >= 0; $i--) { + printf "Sleeping for %s seconds...\r", $i; + sleep(1); + } + return 1; +} + +sub captchaHandler { + &Delimiter((caller(0))[3]); + my $tree = shift; + + my $captchaBase64 = $tree->findvalue('//fieldset[contains(concat(" ", normalize-space(@class), " "), " well ")]/p/img/@src'); + my $filename = &base64ToFile($captchaBase64); + # system("cacaview $filename"); + system("tiv -0 $filename"); + + say 'Please solve captcha ' . basename($filename) . ' to proceed: '; + my $captcha_text = ; + chomp $captcha_text; + my $urlCaptcha = "$config{'URL_HUMAN'}/?captcha_text=$captcha_text"; + + my $response = &responseHandlerMech($urlCaptcha, 'captchaHandler'); + if ($response->{'success'}) { + say 'Captcha successfully solved. Saving cookie...'; + $cookie_jar->save($config{'COOKIE_PATH'}); + } + return $response; + # elsif (!$response->{'success'} && $response->{'status'} == 428) { + # # return 0; + # return $response; + # } +} + +sub trim { + my $string = shift; + $string =~ s/^\s+|\s+$//g; + return $string; +} + +sub parseTerminTime { + &Delimiter((caller(0))[3]); + my ($tree, $id, $service_id) = @_; + + my $xpath = '//div[contains(concat(" ", normalize-space(@class), " "), " timetable ")]/table/tbody/tr'; + my $xpath_stop = '//link[contains(concat(" ", normalize-space(@rel), " "), " alternate ")]/@href'; + + if ($tree->exists($xpath)) { + my $time; + foreach my $buergeramt_dataset ($tree->findnodes($xpath)) { + if ($buergeramt_dataset->findvalue('./th[1]')) { + $time = trim($buergeramt_dataset->findvalue('./th[1]')); + $time = &createMysqlDateTime("$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} $time"); + # print "Parsing for $time..."; + } + foreach my $buergeramt ($buergeramt_dataset->findnodes('./td[contains(concat(" ", normalize-space(@class), " "), " frei ")]')) { + my $name = trim($buergeramt->findvalue('./a')); + my $href = $buergeramt->findvalue('./a/@href'); + say $time; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'VENUE'} = $name; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'HREF'} = "$config{'URL_ROOT'}$href"; + + # say $name . ' needs match: ' . grep{ $venues{$_}{'NAME'} eq $name} keys %venues; + ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'VENUE_ID'}) = grep{ $venues{$_}{'NAME'} eq $name} keys %venues; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'EPOCHTIME'} = DateTime::Format::DateParse->parse_datetime($time)->epoch(); + } + } + return 1; + } + elsif ($tree->findvalue($xpath_stop) eq '/terminvereinbarung/termin/stop/') { + say "XPATH_STOP DETECTED"; + die; + } + else { + say 'Parsing error.'; + return 0; + } +} + +sub base64ToFile { + &Delimiter((caller(0))[3]); + my $base64 = shift; + + my $timestamp = &GetTimestamp('YMDHMS'); + $base64 =~ s/data:image\/(.+?)\;base64,//; + my $decoded= decode_base64($base64); + my $filename = "$config{'CAPTCHA_DIR'}/$timestamp.png"; + open my $fh, '>', $filename or die $!; + binmode $fh; + print $fh $decoded; + close $fh; + return $filename; +} + +sub Delimiter { + my $SubName = shift; + print "\n" . "-" x 80 . "\nSUB " . $SubName . "\n" . '-' x 80 . "\n"; +} + +sub GetTimestamp { + #&Delimiter((caller(0))[3]); + my $switch = shift; + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time); + + my $nice_timestamp; + if ($switch eq 'YMDHMS') { + $nice_timestamp = sprintf ( "%04d%02d%02d_%02d%02d%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec); + } + elsif ($switch eq 'YMD') { + $nice_timestamp = sprintf ( "%04d%02d%02d", $year+1900,$mon+1,$mday); + } + elsif ($switch eq 'year') { + $nice_timestamp = $year+1900; + } + elsif ($switch eq 'month') { + $nice_timestamp = $mon+10; + } + else { + print "Invalid/no switch detected. Use: 'YMDHMS' / 'YMD'\n"; + } + + return $nice_timestamp; +} \ No newline at end of file diff --git a/src/server/service.berlin.de.3.pl b/src/server/service.berlin.de.3.pl new file mode 100755 index 0000000..e332e78 --- /dev/null +++ b/src/server/service.berlin.de.3.pl @@ -0,0 +1,1438 @@ +#!usr/bin/perl +use strict; +use warnings; +no warnings 'redefine'; +# use diagnostics; +use Data::Dumper; +use HTML::TreeBuilder::XPath; +use feature qw(say); +use File::Basename; +use HTTP::Cookies; +use MIME::Base64 qw(decode_base64); +use Encode; +use utf8; +use DateTime::Format::Strptime; +use DateTime::Format::MySQL; +use DBI; +use DBD::MariaDB; +use DateTime::Format::DateParse; +use autodie qw(:all); +use Parallel::ForkManager; +use DBIx::Connector; +use WWW::Mechanize (); +# use Storable; +# use Data::Printer; +# use DateTime; +# use HTML::Entities; +# binmode STDOUT, ':encoding(utf-8)'; +# binmode STDIN, ':encoding(utf-8)'; +# use open ':std', ':encoding(UTF-8)'; + +my ($zsmappointment, $ts, $mech, $ua, $cookie_jar, $strp, %data, %services, %venues, $connection, $strp_datetime, $pm); + +my %config = ( + 'MYSQL' => { + 'ACCESS' => { + 'database' => 'terminsnipe', + 'hostname' => 'zinomedia.de', + 'user' => 'terminsnipe', + 'pw' => 'dIHB6JLk0CBBx1p7', + 'port' => 3306, + }, + 'OPTIONS' => { + 'PrintError' => 0, + 'RaiseError' => 1, + 'AutoCommit' => 1, + } + }, + 'URL_ROOT' => 'https://service.berlin.de', + 'URL_TERMIN_DAY' => 'https://service.berlin.de/terminvereinbarung/termin/day', + 'URL_TERMIN_TIME' => 'https://service.berlin.de/terminvereinbarung/termin/time', + 'URL_HUMAN' => 'https://service.berlin.de/terminvereinbarung/termin/human', + 'URL_STANDORTE' => 'https://service.berlin.de/standorte', + 'URL_DIENSTLEISTUNGEN' => 'https://service.berlin.de/dienstleistungen', + 'CAPTCHA_DIR' => './captcha', + 'COOKIE_DIR' => './cookie', + 'COOKIE_PATH' => './cookie/.cookies.txt', + 'FOLLOW_NEXT' => 1, + 'OUTPUT_PATH' => './output/data', + 'FILTER_SERVICES_TO_OUTPUT' => 1, + 'FILTER_SERVICES_PATH' => './output/filter_services', + 'DB_REFRESH_SERVICES' => 0, + 'RUN_SERVICES' => 0, + 'RUN_VENUES' => 0, + 'VENUES_TO_FILE' => 0, + 'VENUES_PATH' => './output/venues', + 'DB_REFRESH_VENUES' => 0, + 'DUMP_SERVICES_TO_FILE' => 1, + 'SERVICES_PATH' => './output/services', + 'OUTPUT_DIR' => './output', + 'DB_CLEAN_DATETIMES' => 1, + 'RATE_LIMIT_SLEEP_MIN' => 5, + 'SLEEP_LOOP_MIN' => 1, + 'SERVICES' => { + 'Melderegisterauskunft sperren' => { 'RUN' => 0, 'ID' => 120678 }, + 'Erweiterung einer Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121629 }, + 'Ersatzführerschein nach Verlust/Diebstahl' => { 'RUN' => 0, 'ID' => 121593 }, + 'Kinderreisepass beantragen / verlängern / aktualisieren' => { 'RUN' => 0, 'ID' => 121469 }, + 'Namensänderung in den Fahrzeugpapieren' => { 'RUN' => 0, 'ID' => 324173 }, + 'Änderung/Wechsel der Hauptwohnung' => { 'RUN' => 0, 'ID' => 120697 }, + 'Umschreibung einer ausländischen Fahrerlaubnis aus einem Nicht-EU/EWR-Land (Drittstaat/Anlage 11)' => { 'RUN' => 0, 'ID' => 327537 }, + 'Historisches Kennzeichen beantragen' => { 'RUN' => 0, 'ID' => 121478 }, + 'Verlängerung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121634 }, + 'Adressänderung in der Zulassungsbescheinigung Teil I (ZBI) bzw. in dem Fahrzeugschein' => { 'RUN' => 0, 'ID' => 120658 }, + 'Blaue Karte EU zu einem neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 326798 }, + 'Neufahrzeug anmelden' => { 'RUN' => 0, 'ID' => 120882 }, + 'Zulassung eines aus dem EU-Ausland eingeführten fabrikneuen Fahrzeuges mit Zulassungsbescheinigung Teil II' => { 'RUN' => 0, 'ID' => 324199 }, + 'Führungszeugnis' => { 'RUN' => 0, 'ID' => 120926 }, + 'Aufenthaltserlaubnis für im Bundesgebiet geborene Kinder - Erteilung' => { 'RUN' => 0, 'ID' => 324269 }, + 'Kraftfahrzeug – technische Änderung melden' => { 'RUN' => 0, 'ID' => 120904 }, + 'Reisepass beantragen' => { 'RUN' => 0, 'ID' => 121151 }, + 'Meldebescheinigung beantragen' => { 'RUN' => 0, 'ID' => 120702 }, + 'Umtausch eines Kartenführerscheins' => { 'RUN' => 0, 'ID' => 121616 }, + 'Umschreibung einer Dienstfahrerlaubnis' => { 'RUN' => 0, 'ID' => 121615 }, + 'Bescheinigung über ein unbefristetes Aufenthaltsrecht' => { 'RUN' => 0, 'ID' => 324921 }, + 'Elektrokennzeichen beantragen' => { 'RUN' => 0, 'ID' => 327084 }, + 'Abmeldung einer Wohnung' => { 'RUN' => 0, 'ID' => 120335 }, + 'Zulassungsbescheinigung Teil II für KFZ wegen Verlust oder Diebstahl ersetzen' => { 'RUN' => 0, 'ID' => 121584 }, + 'Beglaubigung von Unterschriften' => { 'RUN' => 0, 'ID' => 158142 }, + 'Kraftfahrzeugkennzeichen wegen Unleserlichkeit ersetzen' => { 'RUN' => 0, 'ID' => 324907 }, + 'Zulassung von Taxen (Kraftdroschken)' => { 'RUN' => 0, 'ID' => 326033 }, + 'Fahrerlaubnis - Erweiterung auf die Klassen D1, D1E, D und DE' => { 'RUN' => 0, 'ID' => 324450 }, + 'Wiederzulassung eines Kraftfahrzeuges ohne Halterwechsel beantragen' => { 'RUN' => 0, 'ID' => 120905 }, + 'Ersterteilung einer Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121627 }, + 'Beglaubigung von Kopien' => { 'RUN' => 0, 'ID' => 121701 }, + 'Saisonkennzeichen für KFZ beantragen' => { 'RUN' => 0, 'ID' => 121480 }, + 'Anmeldung einer Wohnung' => { 'RUN' => 0, 'ID' => 120686 }, + 'Zulassung eines aus dem EU-Ausland eingeführten fabrikneuen Fahrzeuges ohne Zulassungsbescheinigung Teil II' => { 'RUN' => 0, 'ID' => 324200 }, + 'Kraftfahrzeug ummelden – nach einem Umzug nach Berlin' => { 'RUN' => 0, 'ID' => 120918 }, + 'Umschreibung eines zugelassenen Fahrzeuges mit Berliner Kennzeichen auf einen anderen Halter' => { 'RUN' => 0, 'ID' => 120906 }, + 'Reisepass beantragen (vorläufiger Reisepass)' => { 'RUN' => 0, 'ID' => 121153 }, + 'Kraftfahrzeug außer Betrieb setzen (Kfz abmelden)' => { 'RUN' => 0, 'ID' => 120877 }, + 'Umstellung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 124556 }, + 'Widerspruchsrechte gegen Datenübermittlungen und Melderegisterauskünfte' => { 'RUN' => 0, 'ID' => 319141 }, + 'Gewerbezentralregister - Auskunft beantragen' => { 'RUN' => 0, 'ID' => 327835 }, + 'Wechselkennzeichen beantragen' => { 'RUN' => 0, 'ID' => 324587 }, + 'Personalausweis vorläufig beantragen' => { 'RUN' => 0, 'ID' => 120682 }, + 'Personalausweis beantragen' => { 'RUN' => 0, 'ID' => 120703 }, + 'Kraftfahrzeugkennzeichen ersetzen – nach Diebstahl oder Verlust eines Nummernschildes' => { 'RUN' => 0, 'ID' => 324196 }, + 'Umschreibung einer ausländischen Fahrerlaubnis aus einem EU-/EWR-Staat' => { 'RUN' => 0, 'ID' => 121598 }, + 'Selbstfahrermietfahrzeug – Beginn/Beendigung der Nutzung' => { 'RUN' => 0, 'ID' => 326039 }, + 'Zulassungsbescheinigung Teil I für KFZ wegen Verlust oder Diebstahl ersetzen' => { 'RUN' => 0, 'ID' => 121575 }, + 'Kraftfahrzeugkennzeichen auf Wunsch ändern' => { 'RUN' => 0, 'ID' => 121473 }, + 'Begleitetes Fahren mit 17' => { 'RUN' => 0, 'ID' => 121589 }, + 'Fahrerlaubnis zur Fahrgastbeförderung (P-Schein) - Verlängerung' => { 'RUN' => 0, 'ID' => 324389 }, + 'Zulassung eines außer Betrieb gesetzten Fahrzeuges mit Berliner Kennzeichen auf einen anderen Halter' => { 'RUN' => 0, 'ID' => 324169 }, + 'Befreiung von der Ausweispflicht' => { 'RUN' => 0, 'ID' => 327044 }, + 'Grünes Kennzeichen beantragen' => { 'RUN' => 0, 'ID' => 121482 }, + 'Aufenthaltserlaubnis in einen neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 121874 }, + 'Kraftfahrzeug außer Betrieb setzen, unvollständige Unterlagen (Kfz abmelden)' => { 'RUN' => 0, 'ID' => 325881 }, + 'Führerschein: Ausstellung – Internationaler Führerschein' => { 'RUN' => 0, 'ID' => 121591 }, + 'Neuerteilung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121637 }, + 'Niederlassungserlaubnis oder Erlaubnis zum Daueraufenthalt-EU in einen neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 324280 }, + 'Fahrerlaubnis zur Fahrgastbeförderung (P-Schein) - Erteilung' => { 'RUN' => 0, 'ID' => 121622 }, + } +); + + +# # INIT +&init(); +# &submitRegister(); +# &getRegister('', 2); +# &getRegisterControls(); +# db_createRequestDienstleister(24); +# &getStandorte(); +# &getDienstleistungen(); +# die; + +# # MAIN +# &getStandorte() if $config{'RUN_VENUES'}; +# &getDienstleistungen() if $config{'RUN_SERVICES'}; +# # &run(); +# &db_request(); +# &Summary(); + + + +&programLoop(); + +# SUBS + +sub responseHandlerMech { + # &Delimiter((caller(0))[3]); + my $url = shift; + + print "\nGETTING $url... "; + + $mech->get($url); + say $mech->status(); + + # my $tree = HTML::TreeBuilder::XPath->new_from_content($mech->content()); + my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + + my ($success, $code) = &checkSuccess($tree, $mech); # returns 1 or 0 for $success and error/success code for $code + if (!$success && !$code) { + &dumpToFile('./output/successcodeundefined', $mech->content); + die '$success and $code undefined'; + } + # say "success: $success\tcode: $code"; + + if ($mech->success()) { + if ($code == -6) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Starting new appointment search...'; + my $success = &restartHandler($tree, $code, $mech); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restartHandler_success' => 1, 'restart' => 1 } if $success; + # return $response if $response->code == 200; + # &responseHandlerMech($url) if $response->code != 200; + } + elsif ($code == -1) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Starting new appointment search...'; + my $success = &restartHandler($tree, $code, $mech); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restartHandler_success' => 1, 'restart' => 1 } if $success; + # return $response if $response->code == 200; + # &responseHandlerMech($url) if $response->code != 200; + } + elsif ($code == -9) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Timeout after 30 minutes. What to do here? Renewing cookie...'; + getCookie(); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restart' => 1 }; + } + elsif ($code == -2) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : XPATH_STOP detected: Restarting.'; + &getCookie(); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restart' => 1 }; + } + else { + return { 'success' => 1, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + } + else { + if ($mech->status == 404) { + &dumpToFile('./output/404', $mech->content); + if ($code == -8) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Error occrured, retrying...'; + &responseHandlerMech($url); # RETRY + } + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + # return { 'success' => 1, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + elsif ($mech->status == 429) { + if ($code == -12) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Too many requests...'; + &dumpToFile('./output/429', $mech->content); + my $success = &rateLimitHandler($tree, $url); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'rateLimit_success' => 1, 'restart' => 1 } if $success; + } + } + elsif ($mech->status == 428) { + if ($code == -5 || $code == -7) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Renewing captcha...'; + my $response = &captchaHandler($tree); + &responseHandlerMech($url) if !$response; # CAPTCHA NOT SOLVED SUCCESSFULLY, RETRY + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'captcha_success' => 1, 'restart' => 1 }; + } + } + elsif ($mech->status == 500) { + if ($code == -10) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Site currently in maintenance mode.'; + die; + } + } + else { + say 'Mech->status ' . $mech->status . ' not handled yet.'; + die; + # return { 'success' => 0, 'status' => $mech->status, 'tree' => $tree, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + } +} + +sub checkSuccess { + &Delimiter((caller(0))[3]); + my ($tree, $mech) = @_; + + my $xpath_alert = '//div[contains(concat(" ", normalize-space(@class), " "), " alert")]/p[1]'; + my $xpath_alert2 = '//div[contains(concat(" ", normalize-space(@class), " "), " alert")]/div/p'; + my $xpath_alert3 = '//div[@class="zms"]/div[@class="html5-header header"]/h1[@class="title"]'; + my $xpath_alert4 = '//div[@class="offset2 span7"]/h2'; + my $xpath_alert5 = '//div[contains(concat(" ", normalize-space(@class), " "), " offset2 ")]/h1'; + + my $xpath_success = '//div[@class="submit-success-message"]'; + + if ($tree->exists($xpath_alert)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert))); + say "Alert: $alert"; + + if ($alert eq 'Sie haben schon einen Termin reserviert. Soll diese Reservierung gelöscht werden oder möchten Sie mit der vorhandenen Reservierung fortfahren?') { + say 'Code -1: Another reservation is already in progress.'; + &dumpToFile('./output/-1', $mech->content); + return (0, -1); + } + elsif ($alert eq 'Zu ihrer Suche konnten keine Daten ermittelt werden. Dies kann unterschiedliche Gründe haben:') { + say 'Code -2: Cookie seems outdated'; + &dumpToFile('./output/-2', $mech->content); + return (0, -2); + } + elsif ($alert eq 'Sie haben Ihre Terminvereinbarung abgeschlossen. Sie können nun eine neue Terminssuche starten!') { + say 'Code -3: Reservation already completed'; + &dumpToFile('./output/-3', $mech->content); + return (0, -3); + } + elsif ($alert eq 'Ihr Termin ist eingetragen worden. Bitte drucken Sie diese Seite aus, damit Sie alle Informationen zur Hand haben. Wichtig für Ihren Besuch ist die Vorlage Ihrer Vorgangsnummer.') { + say 'Code 1: submitRegister successfull.'; + &dumpToFile('./output/1', $mech->content); + say trim($tree->findvalue($xpath_success)); + say $tree->findvalue('//span[@class="summary_authKey"]'); + say $tree->findvalue('//span[contains(concat(" ", normalize-space(@class), " "), " summary_processId ")]'); + return (1, 1); + } + elsif ($alert eq 'Der eingegebene Text stimmte nicht mit dem Bild überein. Versuchen Sie es noch einmal.') { + say 'Code -5: Text / Picture mismatch'; + &dumpToFile('./output/-5', $mech->content); + return (0, -5); + } + elsif ($alert eq 'Ihre Auswahl von Standort und Diensteistung hat sich geändert. Möchten Sie mit Ihrer vorherigen Suche fortfahren oder eine neue Terminsuche starten?') { + say 'Code -6: Location and venue changed. New search or continue.'; + &dumpToFile('./output/-6', $mech->content); + return (0, -6); + } + elsif ($alert eq 'Ihre Vorherige Reservierung wird mit der Auswahl eines neuen Termins entfernt. Fahren Sie nur fort, wenn Sie Ihren bisherigen Termin wirklich ändern wollen.') { + say 'Code -11: Old appointment removed for new one.'; + &dumpToFile('./output/-11', $mech->content); + return (0, -11); + } + elsif ($alert eq 'Bei der Eingabe der Daten scheint Ihnen ein Fehler unterlaufen zu sein. Bitte prüfen Sie die Hinweise an den markierten Formularfeldern.') { + say 'Code -13: Form input error'; + &dumpToFile('./output/-13', $mech->content); + return (0, -13); + } + else { + # say $mech->content(); + say 'NO XPATH SPECIFIED DETECTED'; + say $alert; + &dumpToFile('./output/noxpathspecified', $mech->content); + # my $string = trim($tree->findvalue($xpath_alert)); + + # $string = decode('utf-8', $string); + # say $string; + # say utf8::is_utf8($string); # since Perl 5.8.1 + # say utf8::valid($string); + die; + } + + } + elsif ($tree->exists($xpath_alert2)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert2))); + say "Alert2: $alert"; + + if ($alert eq 'Leider konnte der ausgewählte Termin nicht reserviert werden. Da war jemand schneller als Sie. Bitte versuchen Sie es erneut.') { + say 'Code -4: Appointment not available anymore and booked by another person.'; + &dumpToFile('./output/-4', $mech->content); + return (0, -4); + } + } + elsif ($tree->exists($xpath_alert3)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert3))); + say "Alert 3: $alert"; + + if ($alert eq 'Bitte verifizieren sie sich') { + say 'Code -7: Verification needed.'; + &dumpToFile('./output/-7', $mech->content); + return (0, -7); + } + elsif ($alert eq 'Terminvereinbarung') { + say 'Code 3: Normal Terminvereinbarung'; + &dumpToFile('./output/-7', $mech->content); + return (1, 3); + } + elsif ($alert eq 'Bitte entschuldigen Sie den Fehler') { + say 'Code -8: An error has occured'; + &dumpToFile('./output/-8', $mech->content); + return (1, -8); + } + elsif ($alert eq 'Zeitüberschreitung: Bitte versuchen Sie es erneut') { + say 'Code -9: Timeout: Request older than 30 minutes.'; + &dumpToFile('./output/-9', $mech->content); + return (1, -9); + } + + } + elsif ($tree->exists($xpath_alert4)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert4))); + say "Alert 4: $alert"; + + if ($alert eq 'Die Terminvereinbarung ist zur Zeit nicht möglich.') { + say 'Code -10: Maintenance mode.'; + &dumpToFile('./output/-10', $mech->content); + return (0, -10); + } + elsif ($alert eq 'Service-Portal Berlin' && $tree->findvalue($xpath_alert5) eq 'Zu viele Zugriffe') { + say 'Code -12: Too many requests.'; + &dumpToFile('./output/-12', $mech->content); + return (0, -12); + } + } + else { + say 'Code 2: NO XPATH SPECIFIED DETECTED. Everything fine?'; + &dumpToFile('./output/2', $mech->content); + return (1, 2); + } + +} + +sub restartHandler { + &Delimiter((caller(0))[3]); + my ($tree, $code, $mech) = @_; + + my $url; + if ($code == -6) { + $url = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " alert alert-error noprint textile ")]/ul/li[2]/a/@href'); + } + elsif ($code == -1) { + $url = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " alert alert-info noprint textile ")]/ul/li[2]/a/@href'); + } + $url = $config{'URL_ROOT'} . $url; + say "URL: $url"; + + my $response = $mech->get($url); + say $response->code; + if ($mech->success) { + say "Restart with code $code successfull."; + return 1; + } + else { + say "Restart with code $code unsuccessfull."; + &dumpToFile('./output/restartunsuccessfull', $mech->content); + &responseHandlerMech($url); # LET MECH HANDLE RESPONSE + # print Dumper $response; + # return 0; + # die; + } +} + +sub getRegister { + &Delimiter((caller(0))[3]); + my ($dataset_ref, $user_id) = @_; + + my $response_user = &db_query("SELECT * FROM `reg_users` WHERE `ID` = $user_id", $connection, 'selectall_hashref', 'ID'); + + my $url = $$dataset_ref{'HREF'}; + my $response = &responseHandlerMech($url); + # my $response = &responseHandlerMech('https://zinomedia.de/dl/customHTML2.html'); + + if (!$response->{'success'} && $response->{'restart'}) { + &getRegister($dataset_ref, $user_id); + } + elsif (!$response->{'success'}) { + return 0; + } + elsif ($response->{'success'}) { + &submitRegister($mech, $$response_user{$user_id}); + return 1; + } +} + +sub getRegisterControls { + &Delimiter((caller(0))[3]); + my $switch = shift; + $switch = '' if !$switch; + + use Data::Structure::Util qw( unbless ); + my %controls; + + + + # my $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/tag.php?termin=1&dienstleister=122210&anliegen[]=120335&herkunft=1'); # ABmeldung einer Wohnung Halemweg + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579561200/'); # Termin Zeiten + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579260240/141/'); # Terminbuchung + + # say 'SWITCHING SERVICE NOW'; + + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/tag.php?termin=1&dienstleister=122217&anliegen[]=120335&herkunft=1'); # ABmeldung einer Wohnung Heerstraße + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579215600/'); # Termin Zeiten + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579244400/141/'); # Terminbuchung + + # die; + + + for my $service (sort keys %{$config{'SERVICES'}}) { + my $service_id = $config{'SERVICES'}{$service}{'ID'}; + say "\nSTART GETTING CONTROLS FOR SERVICE '$service'..."; + my $href = "https://service.berlin.de/dienstleistung/$service_id/"; + + my $response = &responseHandlerMech($href); + if ($response->{'success'}) { + say 'success'; + + my @nodes = $response->{'tree'}->findnodes('//div[@class="behoerdenitem"]/div[@class="row"]'); + say $#nodes; + # print Dumper $nodes; + + foreach my $node (sort @nodes) { + if ($node->exists('./div[@class="span5" and (strong)]') || !$node->exists('./div[@class="span2"]/p/strong/a/@href')) { + next; + } + my $venue = $node->findvalue('./div[@class="span5"]'); + my $terminvereinbarung_url = $node->findvalue('./div[@class="span2"]/p/strong/a/@href'); + my $last = 0; + + say "$venue => $terminvereinbarung_url"; + + my $success = &getTerminDay($terminvereinbarung_url, $service_id, 'getRegisterControls'); + return if $switch eq 'getCookie'; + if ($success) { + say 'We have appointments.'; + print Dumper $data{'SERVICES'}{$service_id}{'AVAILABLE'}; + + foreach my $epochtime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}) { + foreach my $datetime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$epochtime}{'DATETIMES'}}) { + my $register_url = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$epochtime}{'DATETIMES'}{$datetime}{'HREF'}; + say $datetime; + say $register_url; + + my $response = &responseHandlerMech($register_url); + if ($response->{'success_checkSuccess'}) { + my $form = $mech->form_number(2); + my @inputs = $mech->find_all_inputs( + # type => 'textarea', + # name_regex => qr/^customer/, + ); + + my $url = $mech->uri()->as_string; + + foreach my $i (0 .. $#inputs) { + # my $digest; + my $contains_pflichtfeld = 0; + foreach my $key2 (sort keys %{$inputs[$i]}) { + # print "KEY $key VALUE "; + # say $inputs[0]{$key}; + # $digest .= "$key$inputs[$i]{$key}"; + $contains_pflichtfeld = 1 if $inputs[$i]{$key2} =~ m/\*$/; + } + + my $key; + if ($inputs[$i]{'name'}) { + $key = 'name'; + } + elsif ($inputs[$i]{'id'}) { + $key = 'id'; + } + else { + print Dumper $inputs[$i]; + die; + } + + # $digest = md5_hex($digest); + # if (!$controls{$digest}) { + # # $controls{$digest}{'HASH'} = unbless(\%{$inputs[$i]}); + # $controls{$digest}{'HASH'} = \%{$inputs[$i]}; + # $controls{$digest}{'FIRST_LVL_PFLICHTFELD'} = $contains_pflichtfeld; + # $controls{$digest}{'URI'}{$url} = 1; + # push(@{$controls{$digest}{'VENUES'}}, "$venue => $service"); + # } + # else { + # push(@{$controls{$digest}{'VENUES'}}, "$venue => $service"); + # } + + + if (!$controls{$key}) { + $controls{$inputs[$i]{$key}}{'HASH'} = unbless(\%{$inputs[$i]}); + # $controls{$digest}{'HASH'} = \%{$inputs[$i]}; + $controls{$inputs[$i]{$key}}{'FIRST_LVL_PFLICHTFELD'} = $contains_pflichtfeld; + $controls{$inputs[$i]{$key}}{'URI'}{$url} = 1; + push(@{$controls{$inputs[$i]{$key}}{'VENUES'}}, "$venue => $service"); + } + else { + push(@{$controls{$inputs[$i]{$key}}{'VENUES'}}, "$venue => $service"); + } + + } + + # say 'Aborting now...'; + # my $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/abort/'); + # sleep(5); + undef %data; + $last = 1; + last if $last; + } + + # print Dumper \%controls; + # die; + last if $last; + } + last if $last; + } + + } + + # print Dumper \%controls; + # die; + } + } + &dumpToFile('./output/controls', \%controls); + print Dumper \%controls; + die; + } +} + +sub submitRegister { + &Delimiter((caller(0))[3]); + my ($mech, $dataset_ref) = @_; + + my $form = $mech->form_number(2); + my @inputs = $mech->find_all_inputs(); + + my $exist_surveyAccepted = 0; + my $exist_telephone = 0; + # print Dumper \@inputs; + for my $i (0 .. $#inputs) { + $exist_surveyAccepted = 1 if defined $inputs[$i]{'name'} && $inputs[$i]{'name'} eq 'surveyAccepted'; + $exist_telephone = 1 if defined $inputs[$i]{'name'} && $inputs[$i]{'name'} eq 'telephone'; + } + # say $exist_surveyAccepted; + + my $familyName = "$$dataset_ref{'FIRST_NAME'} $$dataset_ref{'LAST_NAME'}"; + my $email = $$dataset_ref{'EMAIL'}; + my $telephone = $$dataset_ref{'PHONE'}; + # say $familyName; + # say $email; + # die; + + $mech->set_fields( 'familyName' => $familyName, 'email' => $email, ); + $mech->set_fields( 'telephone' => $telephone ) if $exist_telephone; + $mech->tick('agbgelesen', 1); + $mech->select('surveyAccepted', '0') if $exist_surveyAccepted; + $mech->click_button(id => 'register_submit'); + + + # my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + say $mech->status(); + if ($mech->success()) { + &dumpToFile('./output/successSubmitRegister', $mech->content); + my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + + my ($success, $code) = &checkSuccess($tree, $mech); + say "success: $success\tcode: $code"; + if (!$success && !$code) { + &dumpToFile('./output/successcodeundefined', $mech->content); + die '$success and $code undefined'; + } + elsif ($code == -13) { + &dumpToFile('./output/-13inputs', \@inputs); + die; + } + } + else { + &dumpToFile('./output/unsuccessSubmitRegister', $mech->content); + die; + } +} + +sub programLoop { + &Delimiter((caller(0))[3]); + + for (;;) { + # INIT + &init(); + + # MAIN + # &getCookie(); # GET COOKIE, CHECK IF COOKIE IS VALID OR RENEW + &getStandorte() if $config{'RUN_VENUES'}; + &getDienstleistungen() if $config{'RUN_SERVICES'}; + &db_request(); + &Summary(); + + &sleepCountdown($config{'SLEEP_LOOP_MIN'} * 60); + } +} + +sub db_createRequestDienstleister { + &Delimiter((caller(0))[3]); + my $request_id = shift; + my $values; + + my $string = &db_query("Select terminsnipe.services.HREF From terminsnipe.request Inner Join terminsnipe.services On terminsnipe.services.ID = terminsnipe.request.SERVICE_ID Where terminsnipe.request.ID = $request_id", $connection, 'fetchrow'); + my @dienstleister = getDienstleisterlist($string); + + foreach my $id (@dienstleister) { + $values .= "('$request_id', '$id'), "; + } + my $sql = substr("INSERT INTO `request_dienstleister` (`REQUEST_ID`, `VENUE_ID`) VALUES $values", 0, -2); + # say $sql; + + &db_query("DELETE FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id", $connection, 'do'); + &db_query($sql, $connection, 'do'); +} + +sub getDienstleisterlist { + &Delimiter((caller(0))[3]); + my $string = shift; + + $string =~ m/dienstleisterlist=(.+?)&/; + return split /,/, $1; +} + +sub db_request { + &Delimiter((caller(0))[3]); + my %request; + + my $response = &db_query("SELECT `ID`, `USER_ID`, `SERVICE_ID`, `DATETIME_FROM`, `DATETIME_TO`, `TIMESTAMP` FROM `request` WHERE `EXPIRED` = 0;", $connection, 'selectall_arrayref'); + if (@$response) { + for my $i (0 .. $#{$response}) { + my $dataset = $$response[$i]; + $request{$$dataset[2]}{$$dataset[0]}{'ID'} = $$dataset[0]; + $request{$$dataset[2]}{$$dataset[0]}{'USER_ID'} = $$dataset[1]; + $request{$$dataset[2]}{$$dataset[0]}{'SERVICE_ID'} = $$dataset[2]; + $request{$$dataset[2]}{$$dataset[0]}{'DATETIME_FROM'} = $$dataset[3]; + $request{$$dataset[2]}{$$dataset[0]}{'DATETIME_TO'} = $$dataset[4]; + $request{$$dataset[2]}{$$dataset[0]}{'TIMESTAMP'} = $$dataset[5]; + $services{$$dataset[2]}{'HREF_GENERATED'} = &db_generateHREF($$dataset[0], $$dataset[2]); # ALSO CREATES $services{$$dataset[2]}{'REQUEST_DIENSTLEISTER_VENUES'} + + # print Dumper $data{'SERVICES'}{$$dataset[2]}{'REQUEST_VENUE_IDS'}; + + # die; + } + print Dumper \%request; + + foreach my $service_id (keys %request) { + &startFork($service_id, \%request); + } + $pm->wait_all_children; + say 'All childs are done. Continuing with parent...'; + foreach my $service_id (keys %request) { + &db_find_appointment($service_id, \%request); + } + } + else { + say 'No requests in db.'; + } +} + +sub db_generateHREF { + &Delimiter((caller(0))[3]); + my ($request_id, $service_id) = @_; + + my $href_generated = $services{$service_id}{'HREF'}; + my $response = &db_query("SELECT `VENUE_ID` FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id;", $connection, 'selectall_arrayref'); + + my $dienstleisterlist; + for my $i (0 .. $#{$response}) { + $dienstleisterlist .= "$$response[$i][0],"; + push(@{$data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'}}, [ $$response[$i][0], $venues{$$response[$i][0]}{'NAME'}, $venues{$$response[$i][0]}{'HREF'} ]); + } + $dienstleisterlist = substr($dienstleisterlist, 0, -1); + $href_generated =~ m/dienstleisterlist=(.+?)&/; + $href_generated =~ s/$1/$dienstleisterlist/; + + return $href_generated; +} + +sub startFork { + &Delimiter((caller(0))[3]); + my ($service_id, $request_ref) = @_; + + my $pid = $pm->start($service_id) and next; + + &run_from_request($service_id); + &db_sync($service_id); + # &db_find_appointment($service_id, $request_ref); + + $pm->finish; +} + +sub run_from_request { + &Delimiter((caller(0))[3]); + my $service_id = shift; + + my $service = $services{$service_id}{'NAME'}; + my $href = $services{$service_id}{'HREF_GENERATED'}; + + say "START RUN FOR SERVICE '$service'..."; + &getTerminDay($href, $service_id); +} + +sub db_find_appointment { + &Delimiter((caller(0))[3]); + my ($service_id, $request_ref) = @_; + + foreach my $id (keys %{$$request_ref{$service_id}}) { + my $query = "Select terminsnipe.datetime.DATETIME, terminsnipe.mapping.SERVICE_ID, terminsnipe.mapping.VENUE_ID, terminsnipe.mapping.HREF As HREF, terminsnipe.venues.NAME As VENUE_NAME, terminsnipe.datetime.EPOCHTIME, terminsnipe.mapping.ID As MAPPING_ID From terminsnipe.datetime Inner Join terminsnipe.mapping On terminsnipe.mapping.DATETIME_ID = terminsnipe.datetime.EPOCHTIME Inner Join terminsnipe.venues On terminsnipe.mapping.VENUE_ID = terminsnipe.venues.ID Where Not terminsnipe.datetime.DATETIME > '$$request_ref{$service_id}{$id}{'DATETIME_TO'}' And Not terminsnipe.datetime.DATETIME < '$$request_ref{$service_id}{$id}{'DATETIME_FROM'}' And terminsnipe.mapping.SERVICE_ID = $service_id;"; + say $query; + my $response = &db_query($query, $connection, 'selectall_hashref', 'EPOCHTIME'); + # print Dumper $response; + + say 'Possible appointments: ' . scalar(keys(%$response)); + if (%$response) { + say 'defined'; + + &db_insertAppointment($response, $$request_ref{$service_id}{$id}); + } + } +} + +sub db_insertAppointment { + &Delimiter((caller(0))[3]); + my ($response, $dataset_ref) = @_; + my $user_id = $$dataset_ref{'USER_ID'}; + my $request_id = $$dataset_ref{'ID'}; + + foreach my $epochtime (keys %{$response}) { + if (!$$response{$epochtime}{'RESERVED'}) { + say "Reserving appointment $$response{$epochtime}{MAPPING_ID} for USER_ID $user_id now..."; + + my $success = &getRegister($$response{$epochtime}, $user_id); + + if ($success) { + &db_query("INSERT INTO `appointments` (`MAPPING_ID`, `USER_ID`) VALUES ('$$response{$epochtime}{MAPPING_ID}', '$user_id');", $connection, 'do'); + $$response{$epochtime}{'RESERVED'} = 1; + + say 'Deleting request with ID $request_id from request and request_dienstleister...'; + &db_query("DELETE FROM `request` WHERE `ID` = $request_id;", $connection, 'do'); + &db_query("DELETE FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id;", $connection, 'do'); + last; + } + elsif (!$success) { + say 'Could not book appointment.'; + } + else { + say 'Unknown return value'; + die; + } + } + else { + say "Appointment $epochtime is available but already reserved in loop by another user."; + } + } +} + +sub db_query { + # &Delimiter((caller(0))[3]); + my ($query, $connection, $switch, @rest) = @_; + my $response; + + # say 'DATABASE CONNECTION STATE: ' . $connection->ping; + + # $query = decode("utf-8", $query); + # $query = encode( "iso-8859-1", $query ); + + my $statement; + if ($switch eq 'fetchrow') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchrow(); + } + elsif ($switch eq 'fetchall_hashref') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchall_hashref($rest[0]); + } + elsif ($switch eq 'fetchall_arrayref') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchall_arrayref(); + } + elsif ($switch eq 'do') { + $response = $connection->do($query) or die $connection->errstr; + } + elsif ($switch eq 'selectall_hashref') { + $response = $connection->selectall_hashref($query, $rest[0]) or die $connection->errstr; + } + elsif ($switch eq 'selectall_array') { + my @response = $connection->selectall_array($query) or die $connection->errstr; + return \@response; + } + elsif ($switch eq 'selectall_arrayref') { + $response = $connection->selectall_arrayref($query) or die $connection->errstr; + } + elsif ($switch eq 'selectrow_hashref') { + $response = $connection->selectrow_hashref($query) or die $connection->errstr; + } + $statement->finish if $statement; + + return $response; +} + +sub connectToMySql { + &Delimiter((caller(0))[3]); + my ($db) = @_; + + # assign the values to your connection variable + my $connectionInfo = "dbi:mysql:$db;$config{'MYSQL'}{'ACCESS'}{'hostname'}"; + + # make connection to database + # my $l_connection = DBI->connect($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, $config{'MYSQL'}{'OPTIONS'}) || warn "MySQL conncet failed: $DBI::errstr"; + # return $l_connection; + + my $l_connection = DBIx::Connector->new($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, { + RaiseError => 1, + AutoCommit => 1, + }); + return $l_connection->dbh; +} + +sub run { + &Delimiter((caller(0))[3]); + + for my $service (keys %{$config{'SERVICES'}}) { + if ($config{'SERVICES'}{$service}{'RUN'}) { + my $service_id = $config{'SERVICES'}{$service}{'ID'}; + say "START RUN FOR SERVICE '$service'..."; + &getTerminDay($services{$service_id}{'HREF'}, $service_id); + &db_sync($service_id); + } + else { + # say "DISABLED $service"; + } + } +} + +sub db_sync { + &Delimiter((caller(0))[3]); + my $service_id = shift; + + &db_query("DELETE FROM `mapping` WHERE `SERVICE_ID` = '$service_id';", $connection, 'do'); + + foreach my $day (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}) { + foreach my $datetime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}}) { + my $epochtime = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}; + my $venue_id = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'VENUE_ID'}; + my $href = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'HREF'}; + + say "$service_id\t$venue_id\t$epochtime"; + + # say "INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) VALUES ('$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}', '$datetime') ON DUPLICATE KEY UPDATE `DATETIME` = `DATETIME`;"; + # say "INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) SELECT $epochtime, '$datetime' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `datetime` WHERE `EPOCHTIME`= $epochtime);"; + # say "INSERT INTO `mapping` (`SERVICE_ID`, `VENUE_ID`, `DATETIME_ID`, `HREF`) SELECT '$service_id', '$venue_id', '$epochtime', '$href' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `mapping` WHERE `SERVICE_ID` = '$service_id' AND `VENUE_ID` = '$venue_id' AND `DATETIME_ID` = '$epochtime');"; + + # &db_query("INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) VALUES ('$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}', '$datetime') ON DUPLICATE KEY UPDATE `DATETIME` = `DATETIME`;", $connection, 'do'); + &db_query("INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) SELECT $epochtime, '$datetime' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `datetime` WHERE `EPOCHTIME`= $epochtime);", $connection, 'do'); + &db_query("INSERT INTO `mapping` (`SERVICE_ID`, `VENUE_ID`, `DATETIME_ID`, `HREF`) SELECT '$service_id', '$venue_id', '$epochtime', '$href' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `mapping` WHERE `SERVICE_ID` = '$service_id' AND `VENUE_ID` = '$venue_id' AND `DATETIME_ID` = '$epochtime');", $connection, 'do'); + } + } + +} + +sub createMysqlDateTime { + # &Delimiter((caller(0))[3]); + my $string = shift; + + my $dt = $strp->parse_datetime($string); + return DateTime::Format::MySQL->format_datetime($dt); +} + +sub getDienstleistungen { + &Delimiter((caller(0))[3]); + + my $response = &responseHandlerMech($config{'URL_DIENSTLEISTUNGEN'}); + + if (!$response->{'success'} && $response->{'restart'}) { + &getDienstleistungen(); + } + elsif ($response->{'success'}) { + &parseDienstleistungen($response->{'tree'}); + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in getDienstleistungen and rateLimitHandler unsuccessfull.'; + } + + if ($config{'FILTER_SERVICES_TO_OUTPUT'} || $config{'DB_REFRESH_SERVICES'}) { + &filterDienstleistungen(); + &db_insertServicesVenues('services') if $config{'DB_REFRESH_SERVICES'}; + } + + if ($config{'DUMP_SERVICES_TO_FILE'}) { + &dumpToFile($config{'SERVICES_PATH'}, \%services); + } +} + +sub db_insertServicesVenues { + &Delimiter((caller(0))[3]); + my $switch = shift; + my ($database, $hash_ref); + + if ($switch eq 'services') { + $database = 'services'; + $hash_ref = \%services; + } + elsif ($switch eq 'venues') { + $database = 'venues'; + $hash_ref = \%venues; + } + + my $response = &db_query("delete FROM $database", $connection, 'do'); + + foreach my $id (sort keys %{$hash_ref}) { + my $query = "INSERT INTO `$database` (`ID`, `NAME`, `HREF`) VALUES ('$$hash_ref{$id}{'ID'}', '$$hash_ref{$id}{'NAME'}', '$$hash_ref{$id}{'HREF'}');"; + my $response = &db_query($query, $connection, 'do'); + } +} + +sub filterDienstleistungen { + &Delimiter((caller(0))[3]); + + # DATA DUMPER UTF8 HACK + local *Data::Dumper::qquote = sub { qq["${\(shift)}"] }; + local $Data::Dumper::Useperl = 1; + + open my $FILE, '>>:encoding(UTF-8)', $config{'FILTER_SERVICES_PATH'}; + foreach my $id (keys %services) { + + my $response = &responseHandlerMech($services{$id}{'HREF'}); + + if (!$response->{'success'} && $response->{'restart'}) { + &filterDienstleistungen(); + } + elsif ($response->{'success'}) { + my $tree = $response->{'tree'}; + my $name = $services{$id}{'NAME'}; + + print "\t$name => "; + + if (!$tree->exists('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]')) { + say '0'; + delete $services{$id}; + } + else { + my $href = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]/a/@href'); + my $entry = "'$name' => { 'RUN' => 0, 'ID' => $services{$id}{'ID'} },"; + print '1 => ' . $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]/a') . " | $entry\n"; + say $FILE $entry if $config{'FILTER_SERVICES_TO_OUTPUT'}; + $services{$id}{'HREF'} = $href; + } + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in filterDienstleistungen and rateLimitHandler unsuccessfull.'; + } + } + close $FILE; +} + +sub parseDienstleistungen { + &Delimiter((caller(0))[3]); + my $tree = shift; + + foreach my $dataset ($tree->findnodes('//li[contains(concat(" ", normalize-space(@class), " "), " topic-dls row-fluid ")]')) { + my $id = basename($dataset->findvalue('./a/@href')); + my $href = $dataset->findvalue('./a/@href'); + my $name = trim($dataset->findvalue('./a')); + + $services{$id}{'ID'} = $id; + $services{$id}{'HREF'} = "$config{'URL_ROOT'}$href"; + $services{$id}{'NAME'} = $name; + } +} + +sub getStandorte { + &Delimiter((caller(0))[3]); + + my $response = &responseHandlerMech($config{'URL_STANDORTE'}); + + if (!$response->{'success'} && $response->{'restart'}) { + &getStandorte(); + } + if ($response->{'success'}) { + &parseStandorte($response->{'tree'}); + &db_insertServicesVenues('venues') if $config{'DB_REFRESH_VENUES'}; + dumpToFile($config{'VENUES_PATH'}, \%venues) if $config{'VENUES_TO_FILE'}; + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in getStandorte and rateLimitHandler unsuccessfull.'; + } +} + +sub dumpToFile { + # &Delimiter((caller(0))[3]); + my ($filepath, $ref) = @_; + + # DATA DUMPER UTF8 HACK + # no warnings 'redefine'; + local *Data::Dumper::qquote = sub { qq["${\(shift)}"] }; + local $Data::Dumper::Useperl = 1; + + open my $FILE, '>:encoding(UTF-8)', $filepath; + print $FILE Dumper $ref; + close $FILE; +} + +sub parseStandorte { + &Delimiter((caller(0))[3]); + my $tree = shift; + + foreach my $dataset ($tree->findnodes('//li[contains(concat(" ", normalize-space(@class), " "), " topic-dls ")]')) { + my $id = basename($dataset->findvalue('./a/@href')); + my $href = $dataset->findvalue('./a/@href'); + my $name = trim($dataset->findvalue('./a')); + + say $name; + + $venues{$id}{'ID'} = $id; + $venues{$id}{'HREF'} = "$config{'URL_ROOT'}$href"; + $venues{$id}{'NAME'} = $name; + } +} + +sub getCookie { + &Delimiter((caller(0))[3]); + + # my $url; + # foreach my $id (sort keys %services) { + # $url = $services{$id}{'HREF'}; + # last; + # } + + $mech = WWW::Mechanize->new( + agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + autocheck => 0, + ); + &getRegisterControls('getCookie'); +} + +sub init { + &Delimiter((caller(0))[3]); + + ($zsmappointment, $ts, $mech, $ua, $cookie_jar, $strp, $connection, $strp_datetime, $pm) = (undef) x 9; + undef %data; + + # STRP DATETIME + $strp = DateTime::Format::Strptime->new( + pattern => '%d %b %Y %H:%M', + locale => 'de_DE', + ); + + $strp_datetime = DateTime::Format::Strptime->new( + pattern => '%Y-%m-%d %T', + locale => 'de_DE', + ); + + # MYSQL + $connection = &connectToMySql($config{'MYSQL'}{'ACCESS'}{'database'}); + + # SERVICE AND VENUE HASH + $connection->do('set names utf8'); + %venues = %{&db_query("SELECT `ID`, `NAME`, `HREF` FROM `venues`", $connection, 'fetchall_hashref', 'ID')}; + %services = %{&db_query("SELECT `ID`, `NAME`, `HREF` FROM `services`", $connection, 'fetchall_hashref', 'ID')}; + + # COOKIES + $cookie_jar = HTTP::Cookies->new(ignore_discard => 1 ); + if (-e $config{'COOKIE_PATH'}) { + my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($config{'COOKIE_PATH'}); + # say "mtime: $mtime\tatime: $atime\tctime: $ctime"; + # print scalar localtime $mtime . "\n"; + + # my $age = (int((-M $config{'COOKIE_PATH'})*1440)||1); + # say "COOKIE AGE: $age minutes"; + # if ($age > 30) { + # unlink $config{'COOKIE_PATH'}; + # } + # else { + # say "Loading $config{'COOKIE_PATH'}..."; + # $cookie_jar->load($config{'COOKIE_PATH'}); + # } + my $dt_epoch = DateTime->from_epoch( epoch => $ctime ); + my $duration = $dt_epoch->delta_ms( DateTime->now ); + # say $duration->minutes; + if ($duration->minutes > 30) { + unlink $config{'COOKIE_PATH'}; + } + else { + say "Loading $config{'COOKIE_PATH'}..."; + $cookie_jar->load($config{'COOKIE_PATH'}); + } + } + + # BROWSER + $mech = WWW::Mechanize->new( + cookie_jar => $cookie_jar, + agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + autocheck => 0, + ); + # use LWP::UserAgent; + # $ua = LWP::UserAgent->new( + # ssl_opts => { + # SSL_version => 'TLSv12:!SSLv2:!SSLv3:!TLSv1:!TLSv11', + # }, + # cookie_jar => $cookie_jar, + # agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + # ); + + # EMPTY DIRECTORIES + # unlink glob "'$config{'CAPTCHA_DIR'}/*'"; + unlink glob "'$config{'OUTPUT_DIR'}/*'"; + # unlink glob "'$config{'COOKIE_DIR'}/*'"; + + # CLEAN OLD DATETIMES, FLAG EXPIRED REQUESTS + &db_query("DELETE FROM `datetime` WHERE `DATETIME` < NOW();", $connection, 'do'); + &db_query("UPDATE request SET `EXPIRED` = 1 WHERE `DATETIME_TO` < NOW();", $connection, 'do'); + + # FORK MANAGER + $pm = Parallel::ForkManager->new(scalar(keys(%services))); + # $pm = Parallel::ForkManager->new(1); + $pm->run_on_start( + sub { + my ($pid, $ident)=@_; + say "\n** $ident started, pid: $pid"; + }); + $pm->run_on_finish( + sub { + my ($pid, $exit_code, $ident) = @_; + say "\n** $ident just got out of the pool with PID $pid and exit code: $exit_code"; + }); + $pm->run_on_wait( + sub { + # my ($pid, $ident) = @_; + say "\n** a child is waiting now..."; + }); +} + +sub Summary { + &Delimiter((caller(0))[3]); + + &dumpToFile($config{'OUTPUT_PATH'}, \%data); + $connection->disconnect(); +} + +sub getTerminDay { + &Delimiter((caller(0))[3]); + my ($url, $service_id, $switch) = @_; + $switch = '' if !$switch; + + my $response = &responseHandlerMech($url); + + if (!$response->{'success'} && $response->{'restart'}) { + &getTerminDay($url, $service_id, $switch); + } + elsif ($response->{'success'}) { + my $success = &parseTerminDay($response->{'tree'}, $service_id, $switch, $response); + + if ($success && $switch eq 'getRegisterControls') { + say 'No need to go next. Returning...'; + return 1; + } + + if ($response->{'tree'}->exists('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')) { + say 'NEXT EXISTS'; + if ($config{'FOLLOW_NEXT'}) { + my $epochtime = basename($response->{'tree'}->findvalue('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')); + my $url = "$config{'URL_TERMIN_DAY'}/$epochtime"; + &getTerminDay($url, $service_id, $switch); + } + } + } +} + +sub parseTerminDay { + &Delimiter((caller(0))[3]); + my ($tree, $service_id, $switch, $response) = @_; + + my $xpath = '//div[contains(concat(" ", normalize-space(@class), " "), " calendar-month-table ")]'; + if ($tree->exists($xpath)) { + foreach my $table ($tree->findnodes($xpath)) { + my ($month, $year) = split(/ /, $table->findvalue('./table/thead/tr/th[contains(concat(" ", normalize-space(@class), " "), " month ")]')); + foreach my $buchbar ($table->findnodes('./table/tbody/tr/td[contains(concat(" ", normalize-space(@class), " "), " buchbar ")]')) { + my $id = basename($buchbar->findvalue('./a/@href')); + + if ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}) { + say 'ALREADY PARSED'; + next; + } + else { + my $day = $buchbar->findvalue('./a'); + $data{'SERVICES'}{$service_id}{'SERVICE_ID'} = $service_id; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} = $month; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} = $year; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} = $day; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'ID'} = $id; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIME'} = &createMysqlDateTime("$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} 00:00"); + + my $url = "$config{'URL_TERMIN_TIME'}/$id"; + &getTerminTime($url, $id, $service_id); + + if ($switch eq 'getRegisterControls') { + if (scalar keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}} > 0) { + say 'Successfully parsed at least 1 appointment. Returning...'; + return 1; + } + } + } + } + } + # return 1; + } + else { + say 'Parsing error.'; + &dumpToFile('./output/parsingerror_parseTerminDay', $response->{'mech'}->content); + die; + } +} + +sub getTerminTime { + &Delimiter((caller(0))[3]); + my ($url, $id, $service_id, $switch) = @_; + + my $response = &responseHandlerMech($url); + + if (!$response->{'success'} && $response->{'restart'}) { + &getTerminTime($url, $id, $service_id, $switch); + } + elsif ($response->{'success'}) { + my $success = &parseTerminTime($response->{'tree'}, $id, $service_id); + if (!$success) { + # say $mech->content(); + &dumpToFile('./output/!success_parseTerminTime', $mech->content); + say 'No success in parseTerminTime. Just continue?'; + # &getTerminTime($url, $id, $service_id); # RETRY + } + } +} + +sub rateLimitHandler { + &Delimiter((caller(0))[3]); + my ($tree, $url) = @_; + + my $error = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " offset2 ")]/h1'); + say $error; + + if ($error eq 'Zu viele Zugriffe') { + say 'Rate Limit detected.'; + &sleepCountdown($config{'RATE_LIMIT_SLEEP_MIN'} * 60); + # my $response = &responseHandlerMech($url); + return 1; + } + else { + say 'Could not resolve error in rateLimitHandler'; + die; + } +} + +sub sleepCountdown { + &Delimiter((caller(0))[3]); + my $countdown = shift; + + $|++; + for (my $i=$countdown; $i >= 0; $i--) { + printf "Sleeping for %s seconds...\r", $i; + sleep(1); + } + return 1; +} + +sub captchaHandler { + &Delimiter((caller(0))[3]); + my $tree = shift; + + my $captchaBase64 = $tree->findvalue('//fieldset[contains(concat(" ", normalize-space(@class), " "), " well ")]/p/img/@src'); + my $filename = &base64ToFile($captchaBase64); + # system("cacaview $filename"); + system("tiv -0 $filename"); + + say 'Please solve captcha ' . basename($filename) . ' to proceed: '; + my $captcha_text = ; + chomp $captcha_text; + my $urlCaptcha = "$config{'URL_HUMAN'}/?captcha_text=$captcha_text"; + + # my $response = &responseHandlerMech($urlCaptcha); + my $response = $mech->get($urlCaptcha); + if ($mech->success) { + say 'Captcha successfully solved. Saving cookie...'; + $cookie_jar->save($config{'COOKIE_PATH'}); + return 1; + } + elsif ($mech->status == 428) { + say 'Captcha not successfully solved. Try again...'; + return 0; + } +} + +sub trim { + my $string = shift; + $string =~ s/^\s+|\s+$//g; + return $string; +} + +sub parseTerminTime { + &Delimiter((caller(0))[3]); + my ($tree, $id, $service_id) = @_; + + my $xpath = '//div[contains(concat(" ", normalize-space(@class), " "), " timetable ")]/table/tbody/tr'; + my $xpath_stop = '//link[contains(concat(" ", normalize-space(@rel), " "), " alternate ")]/@href'; + + if ($tree->exists($xpath)) { + my $time; + foreach my $buergeramt_dataset ($tree->findnodes($xpath)) { + if ($buergeramt_dataset->findvalue('./th[1]')) { + $time = trim($buergeramt_dataset->findvalue('./th[1]')); + $time = &createMysqlDateTime("$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} $time"); + # print "Parsing for $time..."; + } + foreach my $buergeramt ($buergeramt_dataset->findnodes('./td[contains(concat(" ", normalize-space(@class), " "), " frei ")]')) { + my $name = trim($buergeramt->findvalue('./a')); + + if ($name eq 'Diesen Termin buchen' && defined $data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'} && scalar @{$data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'}} == 1) { + $name = $data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'}[0][1]; + # say "Single venue: Correct name: $name"; + } + + my $href = $buergeramt->findvalue('./a/@href'); + say $time; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'VENUE'} = $name; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'HREF'} = "$config{'URL_ROOT'}$href"; + # say $name . ' needs match: ' . grep{ $venues{$_}{'NAME'} eq $name} keys %venues; + ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'VENUE_ID'}) = grep{ $venues{$_}{'NAME'} eq $name} keys %venues; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'EPOCHTIME'} = DateTime::Format::DateParse->parse_datetime($time)->epoch(); + } + } + return 1; + } + elsif ($tree->findvalue($xpath_stop) eq '/terminvereinbarung/termin/stop/') { # CAN BE REMOVED BECAUSE OF HANDLER? + say "XPATH_STOP DETECTED"; + } + else { + say 'Parsing error.'; + return 0; + } +} + +sub base64ToFile { + &Delimiter((caller(0))[3]); + my $base64 = shift; + + my $timestamp = &GetTimestamp('YMDHMS'); + $base64 =~ s/data:image\/(.+?)\;base64,//; + my $decoded= decode_base64($base64); + my $filename = "$config{'CAPTCHA_DIR'}/$timestamp.png"; + open my $fh, '>', $filename or die $!; + binmode $fh; + print $fh $decoded; + close $fh; + return $filename; +} + +sub Delimiter { + my $SubName = shift; + print "\n" . "-" x 80 . "\nSUB " . $SubName . "\n" . '-' x 80 . "\n"; +} + +sub GetTimestamp { + #&Delimiter((caller(0))[3]); + my $switch = shift; + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time); + + my $nice_timestamp; + if ($switch eq 'YMDHMS') { + $nice_timestamp = sprintf ( "%04d%02d%02d_%02d%02d%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec); + } + elsif ($switch eq 'YMD') { + $nice_timestamp = sprintf ( "%04d%02d%02d", $year+1900,$mon+1,$mday); + } + elsif ($switch eq 'year') { + $nice_timestamp = $year+1900; + } + elsif ($switch eq 'month') { + $nice_timestamp = $mon+10; + } + else { + print "Invalid/no switch detected. Use: 'YMDHMS' / 'YMD'\n"; + } + + return $nice_timestamp; +} \ No newline at end of file diff --git a/src/server/service.berlin.de.4.pl b/src/server/service.berlin.de.4.pl new file mode 100755 index 0000000..3372102 --- /dev/null +++ b/src/server/service.berlin.de.4.pl @@ -0,0 +1,1550 @@ +#!usr/bin/perl +use strict; +use warnings; +no warnings 'redefine'; +# use diagnostics; +use Data::Dumper; +use HTML::TreeBuilder::XPath; +use feature qw(say); +use File::Basename; +use HTTP::Cookies; +use MIME::Base64 qw(decode_base64); +use Encode; +use utf8; +use DateTime::Format::Strptime; +use DateTime::Format::MySQL; +use DBI; +use DBD::MariaDB; +use DateTime::Format::DateParse; +use autodie qw(:all); +use Parallel::ForkManager; +use DBIx::Connector; +use WWW::Mechanize (); +# use AnyEvent::DBI::MySQL; +# use Storable; +# use Data::Printer; +# use DateTime; +# use HTML::Entities; +# binmode STDOUT, ':encoding(utf-8)'; +# binmode STDIN, ':encoding(utf-8)'; +# use open ':std', ':encoding(UTF-8)'; + +my ($zsmappointment, $ts, $mech, $ua, $cookie_jar, $strp, %data, %services, %venues, $connection, $strp_datetime, $pm, %request, $service_id_fork); + +my %config = ( + 'MYSQL' => { + 'ACCESS' => { + 'database' => 'terminsnipe', + 'hostname' => 'zinomedia.de', + 'user' => 'terminsnipe', + 'pw' => 'dIHB6JLk0CBBx1p7', + 'port' => 3306, + }, + 'OPTIONS' => { + 'PrintError' => 0, + 'RaiseError' => 1, + 'AutoCommit' => 1, + } + }, + 'URL_ROOT' => 'https://service.berlin.de', + 'URL_TERMIN_DAY' => 'https://service.berlin.de/terminvereinbarung/termin/day', + 'URL_TERMIN_TIME' => 'https://service.berlin.de/terminvereinbarung/termin/time', + 'URL_HUMAN' => 'https://service.berlin.de/terminvereinbarung/termin/human', + 'URL_STANDORTE' => 'https://service.berlin.de/standorte', + 'URL_DIENSTLEISTUNGEN' => 'https://service.berlin.de/dienstleistungen', + 'CAPTCHA_DIR' => './captcha', + 'COOKIE_DIR' => './cookie', + 'COOKIE_PATH' => './cookie/.cookies.txt', + 'FOLLOW_NEXT' => 1, + 'OUTPUT_PATH' => './output/data', + 'FILTER_SERVICES_TO_OUTPUT' => 1, + 'FILTER_SERVICES_PATH' => './output/filter_services', + 'DB_REFRESH_SERVICES' => 0, + 'RUN_SERVICES' => 0, + 'RUN_VENUES' => 0, + 'VENUES_TO_FILE' => 0, + 'VENUES_PATH' => './output/venues', + 'DB_REFRESH_VENUES' => 0, + 'DUMP_SERVICES_TO_FILE' => 1, + 'SERVICES_PATH' => './output/services', + 'OUTPUT_DIR' => './output', + 'DB_CLEAN_DATETIMES' => 1, + 'RATE_LIMIT_SLEEP_MIN' => 5, + 'SLEEP_LOOP_MIN' => 1, + 'SERVICES' => { + 'Melderegisterauskunft sperren' => { 'RUN' => 0, 'ID' => 120678 }, + 'Erweiterung einer Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121629 }, + 'Ersatzführerschein nach Verlust/Diebstahl' => { 'RUN' => 0, 'ID' => 121593 }, + 'Kinderreisepass beantragen / verlängern / aktualisieren' => { 'RUN' => 0, 'ID' => 121469 }, + 'Namensänderung in den Fahrzeugpapieren' => { 'RUN' => 0, 'ID' => 324173 }, + 'Änderung/Wechsel der Hauptwohnung' => { 'RUN' => 0, 'ID' => 120697 }, + 'Umschreibung einer ausländischen Fahrerlaubnis aus einem Nicht-EU/EWR-Land (Drittstaat/Anlage 11)' => { 'RUN' => 0, 'ID' => 327537 }, + 'Historisches Kennzeichen beantragen' => { 'RUN' => 0, 'ID' => 121478 }, + 'Verlängerung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121634 }, + 'Adressänderung in der Zulassungsbescheinigung Teil I (ZBI) bzw. in dem Fahrzeugschein' => { 'RUN' => 0, 'ID' => 120658 }, + 'Blaue Karte EU zu einem neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 326798 }, + 'Neufahrzeug anmelden' => { 'RUN' => 0, 'ID' => 120882 }, + 'Zulassung eines aus dem EU-Ausland eingeführten fabrikneuen Fahrzeuges mit Zulassungsbescheinigung Teil II' => { 'RUN' => 0, 'ID' => 324199 }, + 'Führungszeugnis' => { 'RUN' => 0, 'ID' => 120926 }, + 'Aufenthaltserlaubnis für im Bundesgebiet geborene Kinder - Erteilung' => { 'RUN' => 0, 'ID' => 324269 }, + 'Kraftfahrzeug – technische Änderung melden' => { 'RUN' => 0, 'ID' => 120904 }, + 'Reisepass beantragen' => { 'RUN' => 0, 'ID' => 121151 }, + 'Meldebescheinigung beantragen' => { 'RUN' => 0, 'ID' => 120702 }, + 'Umtausch eines Kartenführerscheins' => { 'RUN' => 0, 'ID' => 121616 }, + 'Umschreibung einer Dienstfahrerlaubnis' => { 'RUN' => 0, 'ID' => 121615 }, + 'Bescheinigung über ein unbefristetes Aufenthaltsrecht' => { 'RUN' => 0, 'ID' => 324921 }, + 'Elektrokennzeichen beantragen' => { 'RUN' => 0, 'ID' => 327084 }, + 'Abmeldung einer Wohnung' => { 'RUN' => 0, 'ID' => 120335 }, + 'Zulassungsbescheinigung Teil II für KFZ wegen Verlust oder Diebstahl ersetzen' => { 'RUN' => 0, 'ID' => 121584 }, + 'Beglaubigung von Unterschriften' => { 'RUN' => 0, 'ID' => 158142 }, + 'Kraftfahrzeugkennzeichen wegen Unleserlichkeit ersetzen' => { 'RUN' => 0, 'ID' => 324907 }, + 'Zulassung von Taxen (Kraftdroschken)' => { 'RUN' => 0, 'ID' => 326033 }, + 'Fahrerlaubnis - Erweiterung auf die Klassen D1, D1E, D und DE' => { 'RUN' => 0, 'ID' => 324450 }, + 'Wiederzulassung eines Kraftfahrzeuges ohne Halterwechsel beantragen' => { 'RUN' => 0, 'ID' => 120905 }, + 'Ersterteilung einer Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121627 }, + 'Beglaubigung von Kopien' => { 'RUN' => 0, 'ID' => 121701 }, + 'Saisonkennzeichen für KFZ beantragen' => { 'RUN' => 0, 'ID' => 121480 }, + 'Anmeldung einer Wohnung' => { 'RUN' => 0, 'ID' => 120686 }, + 'Zulassung eines aus dem EU-Ausland eingeführten fabrikneuen Fahrzeuges ohne Zulassungsbescheinigung Teil II' => { 'RUN' => 0, 'ID' => 324200 }, + 'Kraftfahrzeug ummelden – nach einem Umzug nach Berlin' => { 'RUN' => 0, 'ID' => 120918 }, + 'Umschreibung eines zugelassenen Fahrzeuges mit Berliner Kennzeichen auf einen anderen Halter' => { 'RUN' => 0, 'ID' => 120906 }, + 'Reisepass beantragen (vorläufiger Reisepass)' => { 'RUN' => 0, 'ID' => 121153 }, + 'Kraftfahrzeug außer Betrieb setzen (Kfz abmelden)' => { 'RUN' => 0, 'ID' => 120877 }, + 'Umstellung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 124556 }, + 'Widerspruchsrechte gegen Datenübermittlungen und Melderegisterauskünfte' => { 'RUN' => 0, 'ID' => 319141 }, + 'Gewerbezentralregister - Auskunft beantragen' => { 'RUN' => 0, 'ID' => 327835 }, + 'Wechselkennzeichen beantragen' => { 'RUN' => 0, 'ID' => 324587 }, + 'Personalausweis vorläufig beantragen' => { 'RUN' => 0, 'ID' => 120682 }, + 'Personalausweis beantragen' => { 'RUN' => 0, 'ID' => 120703 }, + 'Kraftfahrzeugkennzeichen ersetzen – nach Diebstahl oder Verlust eines Nummernschildes' => { 'RUN' => 0, 'ID' => 324196 }, + 'Umschreibung einer ausländischen Fahrerlaubnis aus einem EU-/EWR-Staat' => { 'RUN' => 0, 'ID' => 121598 }, + 'Selbstfahrermietfahrzeug – Beginn/Beendigung der Nutzung' => { 'RUN' => 0, 'ID' => 326039 }, + 'Zulassungsbescheinigung Teil I für KFZ wegen Verlust oder Diebstahl ersetzen' => { 'RUN' => 0, 'ID' => 121575 }, + 'Kraftfahrzeugkennzeichen auf Wunsch ändern' => { 'RUN' => 0, 'ID' => 121473 }, + 'Begleitetes Fahren mit 17' => { 'RUN' => 0, 'ID' => 121589 }, + 'Fahrerlaubnis zur Fahrgastbeförderung (P-Schein) - Verlängerung' => { 'RUN' => 0, 'ID' => 324389 }, + 'Zulassung eines außer Betrieb gesetzten Fahrzeuges mit Berliner Kennzeichen auf einen anderen Halter' => { 'RUN' => 0, 'ID' => 324169 }, + 'Befreiung von der Ausweispflicht' => { 'RUN' => 0, 'ID' => 327044 }, + 'Grünes Kennzeichen beantragen' => { 'RUN' => 0, 'ID' => 121482 }, + 'Aufenthaltserlaubnis in einen neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 121874 }, + 'Kraftfahrzeug außer Betrieb setzen, unvollständige Unterlagen (Kfz abmelden)' => { 'RUN' => 0, 'ID' => 325881 }, + 'Führerschein: Ausstellung – Internationaler Führerschein' => { 'RUN' => 0, 'ID' => 121591 }, + 'Neuerteilung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121637 }, + 'Niederlassungserlaubnis oder Erlaubnis zum Daueraufenthalt-EU in einen neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 324280 }, + 'Fahrerlaubnis zur Fahrgastbeförderung (P-Schein) - Erteilung' => { 'RUN' => 0, 'ID' => 121622 }, + } +); + + +# # INIT +# &init(); +# db_createRequestDienstleister(31); +# die; +# &submitRegister(); +# &getRegister('', 2); +# &getRegisterControls(); +# db_createRequestDienstleister(24); +# &getStandorte(); +# &getDienstleistungen(); +# die; + +# # MAIN +# &getStandorte() if $config{'RUN_VENUES'}; +# &getDienstleistungen() if $config{'RUN_SERVICES'}; +# # &run(); +# &db_request(); +# &Summary(); + +&programLoop(); + +# SUBS + +sub responseHandlerMech { + # &Delimiter((caller(0))[3]); + my $url = shift; + + print "\nGETTING $url... "; + + $mech->get($url); + say $mech->status(); + + # my $tree = HTML::TreeBuilder::XPath->new_from_content($mech->content()); + my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + + my ($success, $code) = &checkSuccess($tree, $mech); # returns 1 or 0 for $success and error/success code for $code + if (!$success && !$code) { + &dumpToFile('./output/successcodeundefined', $mech->content); + die '$success and $code undefined'; + } + say "success: $success\tcode: $code\t\tstatus: " . $mech->status; + + if ($mech->success()) { + if ($code == -6) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Starting new appointment search...'; + my $success = &restartHandler($tree, $code, $mech); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restartHandler_success' => 1, 'restart' => 1 } if $success; + # return $response if $response->code == 200; + # &responseHandlerMech($url) if $response->code != 200; + } + elsif ($code == -1) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Starting new appointment search...'; + my $success = &restartHandler($tree, $code, $mech); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restartHandler_success' => 1, 'restart' => 1 } if $success; + # return $response if $response->code == 200; + # &responseHandlerMech($url) if $response->code != 200; + } + elsif ($code == -9) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Timeout after 30 minutes. What to do here? Renewing cookie...'; + getCookie(); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restart' => 1 }; + } + elsif ($code == -2) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : XPATH_STOP detected: Restarting.'; + &getCookie(); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restart' => 1 }; + } + else { + return { 'success' => 1, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + } + else { + if ($mech->status == 404) { + &dumpToFile('./output/404', $mech->content); + if ($code == -8) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Error occrured, retrying...'; + &responseHandlerMech($url); # RETRY + } + elsif ($code == -14) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : No venue. Check request in db or its generation.'; + die; + } + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + # return { 'success' => 1, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + elsif ($mech->status == 429) { + if ($code == -12) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Too many requests...'; + &dumpToFile('./output/429', $mech->content); + my $success = &rateLimitHandler($tree, $url); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'rateLimit_success' => 1, 'restart' => 1 } if $success; + } + } + elsif ($mech->status == 428) { + if ($code == -5 || $code == -7) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Renewing captcha...'; + my $response = &captchaHandler($tree); + &responseHandlerMech($url) if !$response; # CAPTCHA NOT SOLVED SUCCESSFULLY, RETRY + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'captcha_success' => 1, 'restart' => 1 }; + } + } + elsif ($mech->status == 500) { + if ($code == -10) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Site currently in maintenance mode.'; + die; + } + } + else { + say 'Mech->status ' . $mech->status . ' not handled yet.'; + die; + # return { 'success' => 0, 'status' => $mech->status, 'tree' => $tree, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + } +} + +sub checkSuccess { + &Delimiter((caller(0))[3]); + my ($tree, $mech) = @_; + + my $xpath_alert = '//div[contains(concat(" ", normalize-space(@class), " "), " alert")]/p[1]'; + my $xpath_alert2 = '//div[contains(concat(" ", normalize-space(@class), " "), " alert")]/div/p'; + my $xpath_alert3 = '//div[@class="zms"]/div[@class="html5-header header"]/h1[@class="title"]'; + my $xpath_alert4 = '//div[@class="offset2 span7"]/h2'; + my $xpath_alert5 = '//div[contains(concat(" ", normalize-space(@class), " "), " offset2 ")]/h1'; + + my $xpath_success = '//div[@class="submit-success-message"]'; + + if ($tree->exists($xpath_alert)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert))); + say "Alert: $alert"; + + if ($alert eq 'Sie haben schon einen Termin reserviert. Soll diese Reservierung gelöscht werden oder möchten Sie mit der vorhandenen Reservierung fortfahren?') { + say 'Code -1: Another reservation is already in progress.'; + &dumpToFile('./output/-1', $mech->content); + return (0, -1); + } + elsif ($alert eq 'Zu ihrer Suche konnten keine Daten ermittelt werden. Dies kann unterschiedliche Gründe haben:') { + say 'Code -2: Cookie seems outdated'; + &dumpToFile('./output/-2', $mech->content); + return (0, -2); + } + elsif ($alert eq 'Sie haben Ihre Terminvereinbarung abgeschlossen. Sie können nun eine neue Terminssuche starten!') { + say 'Code -3: Reservation already completed'; + &dumpToFile('./output/-3', $mech->content); + return (0, -3); + } + elsif ($alert eq 'Ihr Termin ist eingetragen worden. Bitte drucken Sie diese Seite aus, damit Sie alle Informationen zur Hand haben. Wichtig für Ihren Besuch ist die Vorlage Ihrer Vorgangsnummer.') { + say 'Code 1: submitRegister successfull.'; + &dumpToFile('./output/1', $mech->content); + say trim($tree->findvalue($xpath_success)); + say $tree->findvalue('//span[@class="summary_authKey"]'); + say $tree->findvalue('//span[contains(concat(" ", normalize-space(@class), " "), " summary_processId ")]'); + return (1, 1); + } + elsif ($alert eq 'Der eingegebene Text stimmte nicht mit dem Bild überein. Versuchen Sie es noch einmal.') { + say 'Code -5: Text / Picture mismatch'; + &dumpToFile('./output/-5', $mech->content); + return (0, -5); + } + elsif ($alert eq 'Ihre Auswahl von Standort und Diensteistung hat sich geändert. Möchten Sie mit Ihrer vorherigen Suche fortfahren oder eine neue Terminsuche starten?') { + say 'Code -6: Location and venue changed. New search or continue.'; + &dumpToFile('./output/-6', $mech->content); + return (0, -6); + } + elsif ($alert eq 'Ihre Vorherige Reservierung wird mit der Auswahl eines neuen Termins entfernt. Fahren Sie nur fort, wenn Sie Ihren bisherigen Termin wirklich ändern wollen.') { + say 'Code -11: Old appointment removed for new one.'; + &dumpToFile('./output/-11', $mech->content); + return (0, -11); + } + elsif ($alert eq 'Bei der Eingabe der Daten scheint Ihnen ein Fehler unterlaufen zu sein. Bitte prüfen Sie die Hinweise an den markierten Formularfeldern.') { + say 'Code -13: Form input error'; + &dumpToFile('./output/-13', $mech->content); + return (0, -13); + } + else { + # say $mech->content(); + say 'NO XPATH SPECIFIED DETECTED'; + say $alert; + &dumpToFile('./output/noxpathspecified', $mech->content); + # my $string = trim($tree->findvalue($xpath_alert)); + + # $string = decode('utf-8', $string); + # say $string; + # say utf8::is_utf8($string); # since Perl 5.8.1 + # say utf8::valid($string); + die; + } + + } + elsif ($tree->exists($xpath_alert2)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert2))); + say "Alert2: $alert"; + + if ($alert eq 'Leider konnte der ausgewählte Termin nicht reserviert werden. Da war jemand schneller als Sie. Bitte versuchen Sie es erneut.') { + say 'Code -4: Appointment not available anymore and booked by another person.'; + &dumpToFile('./output/-4', $mech->content); + return (0, -4); + } + elsif ($alert eq 'Es wurde kein Standort ausgewählt. Bitte wählen Sie mindestens einen Standort aus.') { + say 'Code -14: No venue selected'; + &dumpToFile('./output/-14', $mech->content); + return (0, -14); + } + } + elsif ($tree->exists($xpath_alert3)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert3))); + say "Alert 3: $alert"; + + if ($alert eq 'Bitte verifizieren sie sich') { + say 'Code -7: Verification needed.'; + &dumpToFile('./output/-7', $mech->content); + return (0, -7); + } + elsif ($alert eq 'Terminvereinbarung') { + say 'Code 3: Normal Terminvereinbarung'; + &dumpToFile('./output/-7', $mech->content); + return (1, 3); + } + elsif ($alert eq 'Bitte entschuldigen Sie den Fehler') { + say 'Code -8: An error has occured'; + &dumpToFile('./output/-8', $mech->content); + return (1, -8); + } + elsif ($alert eq 'Zeitüberschreitung: Bitte versuchen Sie es erneut') { + say 'Code -9: Timeout: Request older than 30 minutes.'; + &dumpToFile('./output/-9', $mech->content); + return (1, -9); + } + + } + elsif ($tree->exists($xpath_alert4)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert4))); + say "Alert 4: $alert"; + + if ($alert eq 'Die Terminvereinbarung ist zur Zeit nicht möglich.') { + say 'Code -10: Maintenance mode.'; + &dumpToFile('./output/-10', $mech->content); + return (0, -10); + } + elsif ($alert eq 'Service-Portal Berlin' && $tree->findvalue($xpath_alert5) eq 'Zu viele Zugriffe') { + say 'Code -12: Too many requests.'; + &dumpToFile('./output/-12', $mech->content); + return (0, -12); + } + } + else { + say 'Code 2: NO XPATH SPECIFIED DETECTED. Everything fine?'; + &dumpToFile('./output/2', $mech->content); + return (1, 2); + } + +} + +sub restartHandler { + &Delimiter((caller(0))[3]); + my ($tree, $code, $mech) = @_; + + my $url; + if ($code == -6) { + $url = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " alert alert-error noprint textile ")]/ul/li[2]/a/@href'); + } + elsif ($code == -1) { + $url = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " alert alert-info noprint textile ")]/ul/li[2]/a/@href'); + } + $url = $config{'URL_ROOT'} . $url; + say "URL: $url"; + + my $response = $mech->get($url); + say $response->code; + if ($mech->success) { + say "Restart with code $code successfull."; + return 1; + } + else { + say "Restart with code $code unsuccessfull."; + &dumpToFile('./output/restartunsuccessfull', $mech->content); + &responseHandlerMech($url); # LET MECH HANDLE RESPONSE + # print Dumper $response; + # return 0; + # die; + } +} + +sub getRegister { + &Delimiter((caller(0))[3]); + my ($dataset_ref, $user_id) = @_; + + my $response_user = &db_query("SELECT * FROM `reg_users` WHERE `ID` = $user_id", $connection, 'selectall_hashref', 'ID'); + + my $url = $$dataset_ref{'HREF'}; + my $response = &responseHandlerMech($url); + # my $response = &responseHandlerMech('https://zinomedia.de/dl/customHTML2.html'); + + if (!$response->{'success'} && $response->{'restart'}) { + &getRegister($dataset_ref, $user_id); + } + elsif (!$response->{'success'}) { + return 0; + } + elsif ($response->{'success'}) { + &submitRegister($mech, $$response_user{$user_id}); + return 1; + } +} + +sub getRegisterControls { + &Delimiter((caller(0))[3]); + my $switch = shift; + $switch = '' if !$switch; + + use Data::Structure::Util qw( unbless ); + my %controls; + + + + # my $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/tag.php?termin=1&dienstleister=122210&anliegen[]=120335&herkunft=1'); # ABmeldung einer Wohnung Halemweg + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579561200/'); # Termin Zeiten + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579260240/141/'); # Terminbuchung + + # say 'SWITCHING SERVICE NOW'; + + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/tag.php?termin=1&dienstleister=122217&anliegen[]=120335&herkunft=1'); # ABmeldung einer Wohnung Heerstraße + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579215600/'); # Termin Zeiten + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579244400/141/'); # Terminbuchung + + # die; + + + for my $service (sort keys %{$config{'SERVICES'}}) { + my $service_id = $config{'SERVICES'}{$service}{'ID'}; + say "\nSTART GETTING CONTROLS FOR SERVICE '$service'..."; + my $href = "https://service.berlin.de/dienstleistung/$service_id/"; + + my $response = &responseHandlerMech($href); + if ($response->{'success'}) { + say 'success'; + + my @nodes = $response->{'tree'}->findnodes('//div[@class="behoerdenitem"]/div[@class="row"]'); + say $#nodes; + # print Dumper $nodes; + + foreach my $node (sort @nodes) { + if ($node->exists('./div[@class="span5" and (strong)]') || !$node->exists('./div[@class="span2"]/p/strong/a/@href')) { + next; + } + my $venue = $node->findvalue('./div[@class="span5"]'); + my $terminvereinbarung_url = $node->findvalue('./div[@class="span2"]/p/strong/a/@href'); + my $last = 0; + + say "$venue => $terminvereinbarung_url"; + + my $success = &getTerminDay($terminvereinbarung_url, $service_id, 'getRegisterControls'); + return if $switch eq 'getCookie'; + if ($success) { + say 'We have appointments.'; + print Dumper $data{'SERVICES'}{$service_id}{'AVAILABLE'}; + + foreach my $epochtime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}) { + foreach my $datetime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$epochtime}{'DATETIMES'}}) { + my $register_url = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$epochtime}{'DATETIMES'}{$datetime}{'HREF'}; + say $datetime; + say $register_url; + + my $response = &responseHandlerMech($register_url); + if ($response->{'success_checkSuccess'}) { + my $form = $mech->form_number(2); + my @inputs = $mech->find_all_inputs( + # type => 'textarea', + # name_regex => qr/^customer/, + ); + + my $url = $mech->uri()->as_string; + + foreach my $i (0 .. $#inputs) { + # my $digest; + my $contains_pflichtfeld = 0; + foreach my $key2 (sort keys %{$inputs[$i]}) { + # print "KEY $key VALUE "; + # say $inputs[0]{$key}; + # $digest .= "$key$inputs[$i]{$key}"; + $contains_pflichtfeld = 1 if $inputs[$i]{$key2} =~ m/\*$/; + } + + my $key; + if ($inputs[$i]{'name'}) { + $key = 'name'; + } + elsif ($inputs[$i]{'id'}) { + $key = 'id'; + } + else { + print Dumper $inputs[$i]; + die; + } + + # $digest = md5_hex($digest); + # if (!$controls{$digest}) { + # # $controls{$digest}{'HASH'} = unbless(\%{$inputs[$i]}); + # $controls{$digest}{'HASH'} = \%{$inputs[$i]}; + # $controls{$digest}{'FIRST_LVL_PFLICHTFELD'} = $contains_pflichtfeld; + # $controls{$digest}{'URI'}{$url} = 1; + # push(@{$controls{$digest}{'VENUES'}}, "$venue => $service"); + # } + # else { + # push(@{$controls{$digest}{'VENUES'}}, "$venue => $service"); + # } + + + if (!$controls{$key}) { + $controls{$inputs[$i]{$key}}{'HASH'} = unbless(\%{$inputs[$i]}); + # $controls{$digest}{'HASH'} = \%{$inputs[$i]}; + $controls{$inputs[$i]{$key}}{'FIRST_LVL_PFLICHTFELD'} = $contains_pflichtfeld; + $controls{$inputs[$i]{$key}}{'URI'}{$url} = 1; + push(@{$controls{$inputs[$i]{$key}}{'VENUES'}}, "$venue => $service"); + } + else { + push(@{$controls{$inputs[$i]{$key}}{'VENUES'}}, "$venue => $service"); + } + + } + + # say 'Aborting now...'; + # my $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/abort/'); + # sleep(5); + undef %data; + $last = 1; + last if $last; + } + + # print Dumper \%controls; + # die; + last if $last; + } + last if $last; + } + + } + + # print Dumper \%controls; + # die; + } + } + &dumpToFile('./output/controls', \%controls); + print Dumper \%controls; + die; + } +} + +sub submitRegister { + &Delimiter((caller(0))[3]); + my ($mech, $dataset_ref) = @_; + + my $form = $mech->form_number(2); + my @inputs = $mech->find_all_inputs(); + + my $exist_surveyAccepted = 0; + my $exist_telephone = 0; + # print Dumper \@inputs; + for my $i (0 .. $#inputs) { + $exist_surveyAccepted = 1 if defined $inputs[$i]{'name'} && $inputs[$i]{'name'} eq 'surveyAccepted'; + $exist_telephone = 1 if defined $inputs[$i]{'name'} && $inputs[$i]{'name'} eq 'telephone'; + } + # say $exist_surveyAccepted; + + my $familyName = "$$dataset_ref{'FIRST_NAME'} $$dataset_ref{'LAST_NAME'}"; + my $email = $$dataset_ref{'EMAIL'}; + my $telephone = $$dataset_ref{'PHONE'}; + # say $familyName; + # say $email; + # die; + + $mech->set_fields( 'familyName' => $familyName, 'email' => $email, ); + $mech->set_fields( 'telephone' => $telephone ) if $exist_telephone; + $mech->tick('agbgelesen', 1); + $mech->select('surveyAccepted', '0') if $exist_surveyAccepted; + $mech->click_button(id => 'register_submit'); + + + # my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + say $mech->status(); + if ($mech->success()) { + &dumpToFile('./output/successSubmitRegister', $mech->content); + my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + + my ($success, $code) = &checkSuccess($tree, $mech); + say "success: $success\tcode: $code"; + if (!$success && !$code) { + &dumpToFile('./output/successcodeundefined', $mech->content); + die '$success and $code undefined'; + } + elsif ($code == -13) { + &dumpToFile('./output/-13inputs', \@inputs); + die; + } + } + else { + &dumpToFile('./output/unsuccessSubmitRegister', $mech->content); + die; + } +} + +sub programLoop { + &Delimiter((caller(0))[3]); + + for (;;) { + # INIT + &init(); + + # MAIN + # &getCookie(); # GET COOKIE, CHECK IF COOKIE IS VALID OR RENEW + &getStandorte() if $config{'RUN_VENUES'}; + &getDienstleistungen() if $config{'RUN_SERVICES'}; + &db_request(); + &Summary(); + + &sleepCountdown($config{'SLEEP_LOOP_MIN'} * 60); + } +} + +sub db_createRequestDienstleister { + &Delimiter((caller(0))[3]); + my $request_id = shift; + my $values; + + my $string = &db_query("Select terminsnipe.services.HREF From terminsnipe.request Inner Join terminsnipe.services On terminsnipe.services.ID = terminsnipe.request.SERVICE_ID Where terminsnipe.request.ID = $request_id", $connection, 'fetchrow'); + my @dienstleister = getDienstleisterlist($string); + + foreach my $id (@dienstleister) { + $values .= "('$request_id', '$id'), "; + } + my $sql = substr("INSERT INTO `request_dienstleister` (`REQUEST_ID`, `VENUE_ID`) VALUES $values", 0, -2); + # say $sql; + + &db_query("DELETE FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id", $connection, 'do'); + &db_query($sql, $connection, 'do'); +} + +sub getDienstleisterlist { + &Delimiter((caller(0))[3]); + my $string = shift; + + $string =~ m/dienstleisterlist=(.+?)&/; + return split /,/, $1; +} + +sub db_request { + &Delimiter((caller(0))[3]); + + my $response = &db_query("SELECT `ID`, `USER_ID`, `SERVICE_ID`, `DATETIME_FROM`, `DATETIME_TO`, `TIMESTAMP` FROM `request` WHERE `EXPIRED` = 0;", $connection, 'selectall_arrayref'); + + + + if (@$response) { + for my $i (0 .. $#{$response}) { + my $dataset = $$response[$i]; + $request{$$dataset[2]}{$$dataset[0]}{'ID'} = $$dataset[0]; + $request{$$dataset[2]}{$$dataset[0]}{'USER_ID'} = $$dataset[1]; + $request{$$dataset[2]}{$$dataset[0]}{'SERVICE_ID'} = $$dataset[2]; + $request{$$dataset[2]}{$$dataset[0]}{'DATETIME_FROM'} = $$dataset[3]; + $request{$$dataset[2]}{$$dataset[0]}{'DATETIME_TO'} = $$dataset[4]; + $request{$$dataset[2]}{$$dataset[0]}{'TIMESTAMP'} = $$dataset[5]; + $services{$$dataset[2]}{'HREF_GENERATED'} = &db_generateHREF($$dataset[0], $$dataset[2]); # ALSO CREATES $services{$$dataset[2]}{'REQUEST_DIENSTLEISTER_VENUES'} + + # print Dumper $data{'SERVICES'}{$$dataset[2]}{'REQUEST_VENUE_IDS'}; + + # die; + } + print Dumper \%request; + + # DISCONNECT PARENT DB + $connection->disconnect(); + + foreach my $service_id (keys %request) { + &startFork($service_id); + } + $pm->wait_all_children; + say 'All childs are done. Continuing with parent...'; + + # foreach my $service_id (keys %request) { + # &db_find_appointment($service_id, \%request); + # } + } + else { + say 'No requests in db.'; + } +} + +sub db_generateHREF { + &Delimiter((caller(0))[3]); + my ($request_id, $service_id) = @_; + + my $href_generated = $services{$service_id}{'HREF'}; + my $response = &db_query("SELECT `VENUE_ID` FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id;", $connection, 'selectall_arrayref'); + + my $dienstleisterlist; + for my $i (0 .. $#{$response}) { + $dienstleisterlist .= "$$response[$i][0],"; + push(@{$data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'}}, [ $$response[$i][0], $venues{$$response[$i][0]}{'NAME'}, $venues{$$response[$i][0]}{'HREF'} ]); + } + $dienstleisterlist = substr($dienstleisterlist, 0, -1); + $href_generated =~ m/dienstleisterlist=(.+?)&/; + $href_generated =~ s/$1/$dienstleisterlist/; + + return $href_generated; +} + +sub startFork { + &Delimiter((caller(0))[3]); + my ($service_id) = @_; + + my $pid = $pm->start($service_id) and next; + + # CHILD START + my $service = $services{$service_id}{'NAME'}; + my $href = $services{$service_id}{'HREF_GENERATED'}; + $service_id_fork = $service_id; + + # MYSQL + $connection = &connectToMySql($config{'MYSQL'}{'ACCESS'}{'database'}); + $connection->do('set names utf8'); + + say "START RUN FOR SERVICE '$service'..."; + &getTerminDay($href, $service_id); + + # &run_from_request($service_id); + # &db_sync($service_id); + # &db_find_appointment($service_id, $request_ref); + $connection->disconnect(); + $pm->finish; +} + +sub db_find_appointment { + &Delimiter((caller(0))[3]); + my ($dataset_ref, $service_id) = @_; + + foreach my $id (keys %{$dataset_ref}) { + my $query = "Select terminsnipe.datetime.DATETIME, terminsnipe.mapping.SERVICE_ID, terminsnipe.mapping.VENUE_ID, terminsnipe.mapping.HREF As HREF, terminsnipe.venues.NAME As VENUE_NAME, terminsnipe.datetime.EPOCHTIME, terminsnipe.mapping.ID As MAPPING_ID From terminsnipe.datetime Inner Join terminsnipe.mapping On terminsnipe.mapping.DATETIME_ID = terminsnipe.datetime.EPOCHTIME Inner Join terminsnipe.venues On terminsnipe.mapping.VENUE_ID = terminsnipe.venues.ID Where Not terminsnipe.datetime.DATETIME > '$$dataset_ref{$id}{'DATETIME_TO'}' And Not terminsnipe.datetime.DATETIME < '$$dataset_ref{$id}{'DATETIME_FROM'}' And terminsnipe.mapping.SERVICE_ID = $service_id;"; + say $query; + my $response = &db_query($query, $connection, 'selectall_hashref', 'EPOCHTIME'); + # print Dumper $response; + + say 'Possible appointments: ' . scalar(keys(%{$response})); + if (%{$response}) { + &db_insertAppointment($response, $$dataset_ref{$id}); + } + } +} + +sub db_insertAppointment { + &Delimiter((caller(0))[3]); + my ($response, $dataset_ref) = @_; + my $user_id = $$dataset_ref{'USER_ID'}; + my $request_id = $$dataset_ref{'ID'}; + + foreach my $epochtime (keys %{$response}) { + if (!$$response{$epochtime}{'RESERVED'}) { + say "Reserving appointment $$response{$epochtime}{MAPPING_ID} for USER_ID $user_id now..."; + + my $success = &getRegister($$response{$epochtime}, $user_id); + + if ($success) { + &db_query("INSERT INTO `appointments` (`MAPPING_ID`, `USER_ID`) VALUES ('$$response{$epochtime}{MAPPING_ID}', '$user_id');", $connection, 'do'); + $$response{$epochtime}{'RESERVED'} = 1; + + say 'Deleting request with ID $request_id from request and request_dienstleister...'; + &db_query("DELETE FROM `request` WHERE `ID` = $request_id;", $connection, 'do'); + &db_query("DELETE FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id;", $connection, 'do'); + last; + } + elsif (!$success) { + say 'Could not book appointment.'; + } + else { + say 'Unknown return value'; + die; + } + } + else { + say "Appointment $epochtime is available but already reserved in loop by another user."; + } + } +} + +# sub db_query { +# # &Delimiter((caller(0))[3]); +# my ($query, $connection, $switch, @rest) = @_; +# my $response; + +# # say 'DATABASE CONNECTION STATE: ' . $connection->ping; +# # my $connectionInfo = "dbi:mysql:terminsnipe;$config{'MYSQL'}{'ACCESS'}{'hostname'}, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}"; +# # my $connection = AnyEvent::DBI::MySQL->connect($connectionInfo); + +# my $statement; +# if ($switch eq 'fetchrow') { +# $statement = $connection->prepare($query); +# $statement->execute(); +# $response = $statement->fetchrow(); +# } +# elsif ($switch eq 'do') { +# $response = $connection->do($query) or die $connection->errstr; +# $connection->do($query, sub { +# my ($rv, $dbh) = @_; +# print Dumper $rv; +# print Dumper $dbh; +# }); +# } +# elsif ($switch eq 'selectall_hashref') { +# # $connection->selectall_hashref($query, $rest[0], { async => 1 }) or die $connection->errstr; + +# $statement = $connection->prepare($query, { async => 1 }); +# $statement->execute(); +# $response = $statement->fetchall_hashref($rest[0]); + +# until($connection->mysql_async_ready) { +# say 'not ready yet!'; +# sleep 1; +# } +# $response = $connection->mysql_async_result; + +# } +# elsif ($switch eq 'selectall_arrayref') { +# # $response = $connection->selectall_arrayref($query) or die $connection->errstr; +# $connection->selectall_arrayref($query, sub { +# ($response) = @_; +# }); +# } +# $statement->finish if $statement; + +# return $response; +# } + +sub db_query { + # &Delimiter((caller(0))[3]); + my ($query, $connection, $switch, @rest) = @_; + my $response; + + # my $connection = &connectToMySql($config{'MYSQL'}{'ACCESS'}{'database'}); + + # say 'DATABASE CONNECTION STATE: ' . $connection->ping; + + # $query = decode("utf-8", $query); + # $query = encode( "iso-8859-1", $query ); + + my $statement; + if ($switch eq 'fetchrow') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchrow(); + } + elsif ($switch eq 'fetchall_hashref') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchall_hashref($rest[0]); + } + elsif ($switch eq 'fetchall_arrayref') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchall_arrayref(); + } + elsif ($switch eq 'do') { + $response = $connection->do($query) or die $connection->errstr; + } + elsif ($switch eq 'selectall_hashref') { + $response = $connection->selectall_hashref($query, $rest[0]) or die $connection->errstr; + } + elsif ($switch eq 'selectall_array') { + my @response = $connection->selectall_array($query) or die $connection->errstr; + return \@response; + } + elsif ($switch eq 'selectall_arrayref') { + $response = $connection->selectall_arrayref($query) or die $connection->errstr; + } + elsif ($switch eq 'selectrow_hashref') { + $response = $connection->selectrow_hashref($query) or die $connection->errstr; + } + $statement->finish if $statement; + + # $connection->disconnect(); + + return $response; +} + +sub connectToMySql { + &Delimiter((caller(0))[3]); + my ($db) = @_; + + # assign the values to your connection variable + my $connectionInfo = "dbi:mysql:$db;$config{'MYSQL'}{'ACCESS'}{'hostname'}"; + + # make connection to database + # my $l_connection = DBI->connect($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, $config{'MYSQL'}{'OPTIONS'}) || warn "MySQL conncet failed: $DBI::errstr"; + # return $l_connection; + # my $l_connection = DBI->connect($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, { + # RaiseError => 0, # SUGGESTED BY AnyEvent::DBI::MySQL + # AutoCommit => 1, + # mysql_auto_reconnect => 1, + # }); + # return $l_connection; + + # my $l_connection = AnyEvent::DBI::MySQL->connect($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, { + # RaiseError => 0, # SUGGESTED BY AnyEvent::DBI::MySQL + # AutoCommit => 1, + # }); + + my $l_connection = DBIx::Connector->new($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, { + RaiseError => 1, # SUGGESTED BY AnyEvent::DBI::MySQL + AutoCommit => 1, + }); + return $l_connection->dbh; +} + +sub run { + &Delimiter((caller(0))[3]); + + for my $service (keys %{$config{'SERVICES'}}) { + if ($config{'SERVICES'}{$service}{'RUN'}) { + my $service_id = $config{'SERVICES'}{$service}{'ID'}; + say "START RUN FOR SERVICE '$service'..."; + &getTerminDay($services{$service_id}{'HREF'}, $service_id); + &db_sync($service_id); + } + else { + # say "DISABLED $service"; + } + } +} + +sub db_sync { + &Delimiter((caller(0))[3]); + my $service_id = shift; + + # &db_query("DELETE FROM `mapping` WHERE `SERVICE_ID` = '$service_id';", $connection, 'do'); + + foreach my $day (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}) { + foreach my $datetime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}}) { + if ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'SYNCED'} == 0) { + my $epochtime = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}; + my $venue_id = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'VENUE_ID'}; + my $href = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'HREF'}; + + say "$service_id\t$venue_id\t$epochtime"; + + # say "INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) VALUES ('$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}', '$datetime') ON DUPLICATE KEY UPDATE `DATETIME` = `DATETIME`;"; + # say "INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) SELECT $epochtime, '$datetime' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `datetime` WHERE `EPOCHTIME`= $epochtime);"; + # say "INSERT INTO `mapping` (`SERVICE_ID`, `VENUE_ID`, `DATETIME_ID`, `HREF`) SELECT '$service_id', '$venue_id', '$epochtime', '$href' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `mapping` WHERE `SERVICE_ID` = '$service_id' AND `VENUE_ID` = '$venue_id' AND `DATETIME_ID` = '$epochtime');"; + + # &db_query("INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) VALUES ('$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}', '$datetime') ON DUPLICATE KEY UPDATE `DATETIME` = `DATETIME`;", $connection, 'do'); + &db_query("INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) SELECT $epochtime, '$datetime' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `datetime` WHERE `EPOCHTIME`= $epochtime);", $connection, 'do'); + &db_query("INSERT INTO `mapping` (`SERVICE_ID`, `VENUE_ID`, `DATETIME_ID`, `HREF`) SELECT '$service_id', '$venue_id', '$epochtime', '$href' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `mapping` WHERE `SERVICE_ID` = '$service_id' AND `VENUE_ID` = '$venue_id' AND `DATETIME_ID` = '$epochtime');", $connection, 'do'); + + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'SYNCED'} = 1; + } + # else { + # say "ALREADY SYNCED: $datetime"; + # } + } + } + +} + +sub createMysqlDateTime { + # &Delimiter((caller(0))[3]); + my $string = shift; + + my $dt = $strp->parse_datetime($string); + return DateTime::Format::MySQL->format_datetime($dt); +} + +sub getDienstleistungen { + &Delimiter((caller(0))[3]); + + my $response = &responseHandlerMech($config{'URL_DIENSTLEISTUNGEN'}); + + if (!$response->{'success'} && $response->{'restart'}) { + &getDienstleistungen(); + } + elsif ($response->{'success'}) { + &parseDienstleistungen($response->{'tree'}); + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in getDienstleistungen and rateLimitHandler unsuccessfull.'; + } + + if ($config{'FILTER_SERVICES_TO_OUTPUT'} || $config{'DB_REFRESH_SERVICES'}) { + &filterDienstleistungen(); + &db_insertServicesVenues('services') if $config{'DB_REFRESH_SERVICES'}; + } + + if ($config{'DUMP_SERVICES_TO_FILE'}) { + &dumpToFile($config{'SERVICES_PATH'}, \%services); + } +} + +sub db_insertServicesVenues { + &Delimiter((caller(0))[3]); + my $switch = shift; + my ($database, $hash_ref); + + if ($switch eq 'services') { + $database = 'services'; + $hash_ref = \%services; + } + elsif ($switch eq 'venues') { + $database = 'venues'; + $hash_ref = \%venues; + } + + my $response = &db_query("delete FROM $database", $connection, 'do'); + + foreach my $id (sort keys %{$hash_ref}) { + my $query = "INSERT INTO `$database` (`ID`, `NAME`, `HREF`) VALUES ('$$hash_ref{$id}{'ID'}', '$$hash_ref{$id}{'NAME'}', '$$hash_ref{$id}{'HREF'}');"; + my $response = &db_query($query, $connection, 'do'); + } +} + +sub filterDienstleistungen { + &Delimiter((caller(0))[3]); + + # DATA DUMPER UTF8 HACK + local *Data::Dumper::qquote = sub { qq["${\(shift)}"] }; + local $Data::Dumper::Useperl = 1; + + open my $FILE, '>>:encoding(UTF-8)', $config{'FILTER_SERVICES_PATH'}; + foreach my $id (keys %services) { + + my $response = &responseHandlerMech($services{$id}{'HREF'}); + + if (!$response->{'success'} && $response->{'restart'}) { + &filterDienstleistungen(); + } + elsif ($response->{'success'}) { + my $tree = $response->{'tree'}; + my $name = $services{$id}{'NAME'}; + + print "\t$name => "; + + if (!$tree->exists('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]')) { + say '0'; + delete $services{$id}; + } + else { + my $href = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]/a/@href'); + my $entry = "'$name' => { 'RUN' => 0, 'ID' => $services{$id}{'ID'} },"; + print '1 => ' . $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]/a') . " | $entry\n"; + say $FILE $entry if $config{'FILTER_SERVICES_TO_OUTPUT'}; + $services{$id}{'HREF'} = $href; + } + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in filterDienstleistungen and rateLimitHandler unsuccessfull.'; + } + } + close $FILE; +} + +sub parseDienstleistungen { + &Delimiter((caller(0))[3]); + my $tree = shift; + + foreach my $dataset ($tree->findnodes('//li[contains(concat(" ", normalize-space(@class), " "), " topic-dls row-fluid ")]')) { + my $id = basename($dataset->findvalue('./a/@href')); + my $href = $dataset->findvalue('./a/@href'); + my $name = trim($dataset->findvalue('./a')); + + $services{$id}{'ID'} = $id; + $services{$id}{'HREF'} = "$config{'URL_ROOT'}$href"; + $services{$id}{'NAME'} = $name; + } +} + +sub getStandorte { + &Delimiter((caller(0))[3]); + + my $response = &responseHandlerMech($config{'URL_STANDORTE'}); + + if (!$response->{'success'} && $response->{'restart'}) { + &getStandorte(); + } + if ($response->{'success'}) { + &parseStandorte($response->{'tree'}); + &db_insertServicesVenues('venues') if $config{'DB_REFRESH_VENUES'}; + dumpToFile($config{'VENUES_PATH'}, \%venues) if $config{'VENUES_TO_FILE'}; + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in getStandorte and rateLimitHandler unsuccessfull.'; + } +} + +sub dumpToFile { + # &Delimiter((caller(0))[3]); + my ($filepath, $ref) = @_; + + # DATA DUMPER UTF8 HACK + # no warnings 'redefine'; + local *Data::Dumper::qquote = sub { qq["${\(shift)}"] }; + local $Data::Dumper::Useperl = 1; + + open my $FILE, '>:encoding(UTF-8)', $filepath; + print $FILE Dumper $ref; + close $FILE; +} + +sub parseStandorte { + &Delimiter((caller(0))[3]); + my $tree = shift; + + foreach my $dataset ($tree->findnodes('//li[contains(concat(" ", normalize-space(@class), " "), " topic-dls ")]')) { + my $id = basename($dataset->findvalue('./a/@href')); + my $href = $dataset->findvalue('./a/@href'); + my $name = trim($dataset->findvalue('./a')); + + say $name; + + $venues{$id}{'ID'} = $id; + $venues{$id}{'HREF'} = "$config{'URL_ROOT'}$href"; + $venues{$id}{'NAME'} = $name; + } +} + +sub getCookie { + &Delimiter((caller(0))[3]); + + # my $url; + # foreach my $id (sort keys %services) { + # $url = $services{$id}{'HREF'}; + # last; + # } + + $mech = WWW::Mechanize->new( + agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + autocheck => 0, + ); + &getRegisterControls('getCookie'); +} + +sub init { + &Delimiter((caller(0))[3]); + + ($zsmappointment, $ts, $mech, $ua, $cookie_jar, $strp, $connection, $strp_datetime, $pm) = (undef) x 9; + undef %data; + + # STRP DATETIME + $strp = DateTime::Format::Strptime->new( + pattern => '%d %b %Y %H:%M', + locale => 'de_DE', + ); + + $strp_datetime = DateTime::Format::Strptime->new( + pattern => '%Y-%m-%d %T', + locale => 'de_DE', + ); + + # MYSQL + $connection = &connectToMySql($config{'MYSQL'}{'ACCESS'}{'database'}); + + # SERVICE AND VENUE HASH + $connection->do('set names utf8'); + %venues = %{&db_query("SELECT `ID`, `NAME`, `HREF` FROM `venues`", $connection, 'selectall_hashref', 'ID')}; + %services = %{&db_query("SELECT `ID`, `NAME`, `HREF` FROM `services`", $connection, 'selectall_hashref', 'ID')}; + + # COOKIES + $cookie_jar = HTTP::Cookies->new(ignore_discard => 1 ); + if (-e $config{'COOKIE_PATH'}) { + my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($config{'COOKIE_PATH'}); + # say "mtime: $mtime\tatime: $atime\tctime: $ctime"; + # print scalar localtime $mtime . "\n"; + + # my $age = (int((-M $config{'COOKIE_PATH'})*1440)||1); + # say "COOKIE AGE: $age minutes"; + # if ($age > 30) { + # unlink $config{'COOKIE_PATH'}; + # } + # else { + # say "Loading $config{'COOKIE_PATH'}..."; + # $cookie_jar->load($config{'COOKIE_PATH'}); + # } + my $dt_epoch = DateTime->from_epoch( epoch => $ctime ); + my $duration = $dt_epoch->delta_ms( DateTime->now ); + # say $duration->minutes; + if ($duration->minutes > 30) { + unlink $config{'COOKIE_PATH'}; + } + else { + say "Loading $config{'COOKIE_PATH'}..."; + $cookie_jar->load($config{'COOKIE_PATH'}); + } + } + + # BROWSER + $mech = WWW::Mechanize->new( + cookie_jar => $cookie_jar, + agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + autocheck => 0, + ); + # use LWP::UserAgent; + # $ua = LWP::UserAgent->new( + # ssl_opts => { + # SSL_version => 'TLSv12:!SSLv2:!SSLv3:!TLSv1:!TLSv11', + # }, + # cookie_jar => $cookie_jar, + # agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + # ); + + # EMPTY DIRECTORIES + # unlink glob "'$config{'CAPTCHA_DIR'}/*'"; + unlink glob "'$config{'OUTPUT_DIR'}/*'"; + # unlink glob "'$config{'COOKIE_DIR'}/*'"; + + # CLEAN OLD DATETIMES, FLAG EXPIRED REQUESTS + &db_query("DELETE FROM `datetime` WHERE `DATETIME` < NOW();", $connection, 'do'); + &db_query("UPDATE request SET `EXPIRED` = 1 WHERE `DATETIME_TO` < NOW();", $connection, 'do'); + + # FORK MANAGER + $pm = Parallel::ForkManager->new(scalar(keys(%services))); + # $pm = Parallel::ForkManager->new(1); + $pm->run_on_start( + sub { + my ($pid, $ident)=@_; + say "\n** $ident started, pid: $pid"; + }); + $pm->run_on_finish( + sub { + my ($pid, $exit_code, $ident) = @_; + say "\n** $ident just got out of the pool with PID $pid and exit code: $exit_code"; + }); + $pm->run_on_wait( + sub { + # my ($pid, $ident) = @_; + say "\n** a child is waiting now..."; + }); + + # DISCONNECT DB + # $connection->disconnect(); +} + +sub Summary { + &Delimiter((caller(0))[3]); + + &dumpToFile($config{'OUTPUT_PATH'}, \%data); + # $connection->disconnect(); +} + +sub getTerminDay { + &Delimiter((caller(0))[3]); + my ($url, $service_id, $switch) = @_; + $switch = '' if !$switch; + + my $response = &responseHandlerMech($url); + + if (!$response->{'success'} && $response->{'restart'}) { + &getTerminDay($url, $service_id, $switch); + } + elsif ($response->{'success'}) { + my $success = &parseTerminDay($response->{'tree'}, $service_id, $switch, $response); + + if ($success && $switch eq 'getRegisterControls') { + say 'No need to go next. Returning...'; + return 1; + } + + if ($response->{'tree'}->exists('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')) { + say 'NEXT EXISTS'; + if ($config{'FOLLOW_NEXT'}) { + my $epochtime = basename($response->{'tree'}->findvalue('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')); + my $url = "$config{'URL_TERMIN_DAY'}/$epochtime"; + &getTerminDay($url, $service_id, $switch); + } + } + } +} + +sub parseTerminDay { + &Delimiter((caller(0))[3]); + my ($tree, $service_id, $switch, $response) = @_; + + my $xpath = '//div[contains(concat(" ", normalize-space(@class), " "), " calendar-month-table ")]'; + if ($tree->exists($xpath)) { + foreach my $table ($tree->findnodes($xpath)) { + my ($month, $year) = split(/ /, $table->findvalue('./table/thead/tr/th[contains(concat(" ", normalize-space(@class), " "), " month ")]')); + foreach my $buchbar ($table->findnodes('./table/tbody/tr/td[contains(concat(" ", normalize-space(@class), " "), " buchbar ")]')) { + my $id = basename($buchbar->findvalue('./a/@href')); + + if ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}) { + say 'ALREADY PARSED'; + next; + } + else { + my $day = $buchbar->findvalue('./a'); + $data{'SERVICES'}{$service_id}{'SERVICE_ID'} = $service_id; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} = $month; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} = $year; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} = $day; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'ID'} = $id; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIME'} = &createMysqlDateTime("$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} 00:00"); + + my $url = "$config{'URL_TERMIN_TIME'}/$id"; + &getTerminTime($url, $id, $service_id); + + if ($switch eq 'getRegisterControls') { + if (scalar keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}} > 0) { + say 'Successfully parsed at least 1 appointment. Returning...'; + return 1; + } + } + } + } + + my $days_avail = scalar keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}; + say "DAYS AVAILABLE: $days_avail"; + if ($days_avail >= 1) { + &sync_find_book_appointment($service_id); + } + + } + # return 1; + } + else { + say 'Parsing error.'; + &dumpToFile('./output/parsingerror_parseTerminDay', $response->{'mech'}->content); + die; + } +} + +sub sync_find_book_appointment { + &Delimiter((caller(0))[3]); + my ($service_id) = @_; + + # SYNC + &db_query("DELETE FROM `mapping` WHERE `SERVICE_ID` = '$service_id';", $connection, 'do'); + &db_sync($service_id); + + # FIND & BOOK + &db_find_appointment($request{$service_id}, $service_id); # FIND APPOINTMENT ONLY FOR THE SERVICE WORKER IS HANDLING +} + +sub getTerminTime { + &Delimiter((caller(0))[3]); + my ($url, $id, $service_id, $switch) = @_; + + my $response = &responseHandlerMech($url); + + if (!$response->{'success'} && $response->{'restart'}) { + &getTerminTime($url, $id, $service_id, $switch); + } + elsif ($response->{'success'}) { + my $success = &parseTerminTime($response->{'tree'}, $id, $service_id); + if (!$success) { + # say $mech->content(); + &dumpToFile('./output/!success_parseTerminTime', $mech->content); + say 'No success in parseTerminTime. Just continue?'; + # &getTerminTime($url, $id, $service_id); # RETRY + } + } +} + +sub rateLimitHandler { + &Delimiter((caller(0))[3]); + my ($tree, $url) = @_; + + my $error = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " offset2 ")]/h1'); + say $error; + + if ($error eq 'Zu viele Zugriffe') { + say 'Rate Limit detected.'; + &sleepCountdown($config{'RATE_LIMIT_SLEEP_MIN'} * 60); + # my $response = &responseHandlerMech($url); + return 1; + } + else { + say 'Could not resolve error in rateLimitHandler'; + die; + } +} + +sub sleepCountdown { + &Delimiter((caller(0))[3]); + my $countdown = shift; + + $|++; + for (my $i=$countdown; $i >= 0; $i--) { + printf "Sleeping for %s seconds...\r", $i; + sleep(1); + } + return 1; +} + +sub captchaHandler { + &Delimiter((caller(0))[3]); + my $tree = shift; + + my $captchaBase64 = $tree->findvalue('//fieldset[contains(concat(" ", normalize-space(@class), " "), " well ")]/p/img/@src'); + my $filename = &base64ToFile($captchaBase64); + # system("cacaview $filename"); + system("tiv -0 $filename"); + + say 'Please solve captcha ' . basename($filename) . ' to proceed: '; + my $captcha_text = ; + chomp $captcha_text; + my $urlCaptcha = "$config{'URL_HUMAN'}/?captcha_text=$captcha_text"; + + # my $response = &responseHandlerMech($urlCaptcha); + my $response = $mech->get($urlCaptcha); + if ($mech->success) { + say 'Captcha successfully solved. Saving cookie...'; + $cookie_jar->save($config{'COOKIE_PATH'}); + return 1; + } + elsif ($mech->status == 428) { + say 'Captcha not successfully solved. Try again...'; + &dumpToFile('./output/captchaNotSuccessfullySolved', $mech->content); + return 0; + } +} + +sub trim { + my $string = shift; + $string =~ s/^\s+|\s+$//g; + return $string; +} + +sub parseTerminTime { + &Delimiter((caller(0))[3]); + my ($tree, $id, $service_id) = @_; + + my $xpath = '//div[contains(concat(" ", normalize-space(@class), " "), " timetable ")]/table/tbody/tr'; + my $xpath_stop = '//link[contains(concat(" ", normalize-space(@rel), " "), " alternate ")]/@href'; + + if ($tree->exists($xpath)) { + my $time; + foreach my $buergeramt_dataset ($tree->findnodes($xpath)) { + if ($buergeramt_dataset->findvalue('./th[1]')) { + $time = trim($buergeramt_dataset->findvalue('./th[1]')); + $time = &createMysqlDateTime("$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} $time"); + # print "Parsing for $time..."; + } + foreach my $buergeramt ($buergeramt_dataset->findnodes('./td[contains(concat(" ", normalize-space(@class), " "), " frei ")]')) { + my $name = trim($buergeramt->findvalue('./a')); + + if ($name eq 'Diesen Termin buchen' && defined $data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'} && scalar @{$data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'}} == 1) { + $name = $data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'}[0][1]; + # say "Single venue: Correct name: $name"; + } + + my $href = $buergeramt->findvalue('./a/@href'); + say $time; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'VENUE'} = $name; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'HREF'} = "$config{'URL_ROOT'}$href"; + # say $name . ' needs match: ' . grep{ $venues{$_}{'NAME'} eq $name} keys %venues; + ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'VENUE_ID'}) = grep{ $venues{$_}{'NAME'} eq $name} keys %venues; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'EPOCHTIME'} = DateTime::Format::DateParse->parse_datetime($time)->epoch(); + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'SYNCED'} = 0; + } + } + return 1; + } + elsif ($tree->findvalue($xpath_stop) eq '/terminvereinbarung/termin/stop/') { # CAN BE REMOVED BECAUSE OF HANDLER? + say "XPATH_STOP DETECTED"; + } + else { + say 'Parsing error.'; + return 0; + } +} + +sub base64ToFile { + &Delimiter((caller(0))[3]); + my $base64 = shift; + + my $timestamp = &GetTimestamp('YMDHMS'); + $base64 =~ s/data:image\/(.+?)\;base64,//; + my $decoded= decode_base64($base64); + my $filename = "$config{'CAPTCHA_DIR'}/$timestamp.png"; + open my $fh, '>', $filename or die $!; + binmode $fh; + print $fh $decoded; + close $fh; + return $filename; +} + +sub Delimiter { + my $SubName = shift; + print "\n" . "-" x 80 . "\n"; + print "> " . $SubName; + print " | CHILD: $service_id_fork" if ($service_id_fork); + print "\n" . '-' x 80 . "\n"; +} + +sub GetTimestamp { + #&Delimiter((caller(0))[3]); + my $switch = shift; + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time); + + my $nice_timestamp; + if ($switch eq 'YMDHMS') { + $nice_timestamp = sprintf ( "%04d%02d%02d_%02d%02d%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec); + } + elsif ($switch eq 'YMD') { + $nice_timestamp = sprintf ( "%04d%02d%02d", $year+1900,$mon+1,$mday); + } + elsif ($switch eq 'year') { + $nice_timestamp = $year+1900; + } + elsif ($switch eq 'month') { + $nice_timestamp = $mon+10; + } + else { + print "Invalid/no switch detected. Use: 'YMDHMS' / 'YMD'\n"; + } + + return $nice_timestamp; +} \ No newline at end of file diff --git a/src/server/service.berlin.de.5.pl b/src/server/service.berlin.de.5.pl new file mode 100755 index 0000000..752a2f6 --- /dev/null +++ b/src/server/service.berlin.de.5.pl @@ -0,0 +1,1637 @@ +#!usr/bin/perl +use strict; +use warnings; +no warnings 'redefine'; +# use diagnostics; +use Data::Dumper; +use HTML::TreeBuilder::XPath; +use feature qw(say); +use File::Basename; +use HTTP::Cookies; +use MIME::Base64 qw(decode_base64); +use Encode; +use utf8; +use DateTime::Format::Strptime; +use DateTime::Format::MySQL; +use DBI; +use DBD::MariaDB; +use DateTime::Format::DateParse; +use autodie qw(:all); +use Parallel::ForkManager; +use DBIx::Connector; +use WWW::Mechanize (); +# use AnyEvent::DBI::MySQL; +# use Storable; +# use Data::Printer; +# use DateTime; +# use HTML::Entities; +# binmode STDOUT, ':encoding(utf-8)'; +# binmode STDIN, ':encoding(utf-8)'; +# use open ':std', ':encoding(UTF-8)'; + +my ($zsmappointment, $ts, $mech, $ua, $cookie_jar, $strp, %data, %services, %venues, $connection, $strp_datetime, $pm, %request, $service_id_fork); + +my %config = ( + 'MON2NUM' => { + 'Januar' => 1, + 'Februar' => 2, + 'März' => 3, + 'April' => 4, + 'Mai' => 5, + 'Juni' => 6, + 'Juli' => 7, + 'August' => 8, + 'September' => 9, + 'Oktober' => 10, + 'November' => 11, + 'Dezember' => 12, + }, + 'MYSQL' => { + 'ACCESS' => { + 'database' => 'terminsnipe', + 'hostname' => 'zinomedia.de', + 'user' => 'terminsnipe', + 'pw' => 'dIHB6JLk0CBBx1p7', + 'port' => 3306, + }, + 'OPTIONS' => { + 'PrintError' => 0, + 'RaiseError' => 1, + 'AutoCommit' => 1, + } + }, + 'URL_ROOT' => 'https://service.berlin.de', + 'URL_TERMIN_DAY' => 'https://service.berlin.de/terminvereinbarung/termin/day', + 'URL_TERMIN_TIME' => 'https://service.berlin.de/terminvereinbarung/termin/time', + 'URL_HUMAN' => 'https://service.berlin.de/terminvereinbarung/termin/human', + 'URL_STANDORTE' => 'https://service.berlin.de/standorte', + 'URL_DIENSTLEISTUNGEN' => 'https://service.berlin.de/dienstleistungen', + 'CAPTCHA_DIR' => './captcha', + 'COOKIE_DIR' => './cookie', + 'COOKIE_PATH' => './cookie/.cookies.txt', + 'FOLLOW_NEXT' => 1, + 'OUTPUT_PATH' => './output/data', + 'OUTPUT_DIR' => './output', + 'FILTER_SERVICES_TO_OUTPUT' => 1, + 'FILTER_SERVICES_PATH' => './output/filter_services', + 'RUN_SERVICES' => 0, + 'DB_REFRESH_SERVICES' => 1, + 'RUN_VENUES' => 0, + 'DB_REFRESH_VENUES' => 1, + 'VENUES_TO_FILE' => 0, + 'VENUES_PATH' => './output/venues', + 'DUMP_SERVICES_TO_FILE' => 1, + 'SERVICES_PATH' => './output/services', + 'OUTPUT_DIR' => './output', + 'DB_CLEAN_DATETIMES' => 1, + 'RATE_LIMIT_SLEEP_MIN' => 5, + 'SLEEP_LOOP_MIN' => 1, + 'SERVICES' => { + 'Melderegisterauskunft sperren' => { 'RUN' => 0, 'ID' => 120678 }, + 'Erweiterung einer Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121629 }, + 'Ersatzführerschein nach Verlust/Diebstahl' => { 'RUN' => 0, 'ID' => 121593 }, + 'Kinderreisepass beantragen / verlängern / aktualisieren' => { 'RUN' => 0, 'ID' => 121469 }, + 'Namensänderung in den Fahrzeugpapieren' => { 'RUN' => 0, 'ID' => 324173 }, + 'Änderung/Wechsel der Hauptwohnung' => { 'RUN' => 0, 'ID' => 120697 }, + 'Umschreibung einer ausländischen Fahrerlaubnis aus einem Nicht-EU/EWR-Land (Drittstaat/Anlage 11)' => { 'RUN' => 0, 'ID' => 327537 }, + 'Historisches Kennzeichen beantragen' => { 'RUN' => 0, 'ID' => 121478 }, + 'Verlängerung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121634 }, + 'Adressänderung in der Zulassungsbescheinigung Teil I (ZBI) bzw. in dem Fahrzeugschein' => { 'RUN' => 0, 'ID' => 120658 }, + 'Blaue Karte EU zu einem neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 326798 }, + 'Neufahrzeug anmelden' => { 'RUN' => 0, 'ID' => 120882 }, + 'Zulassung eines aus dem EU-Ausland eingeführten fabrikneuen Fahrzeuges mit Zulassungsbescheinigung Teil II' => { 'RUN' => 0, 'ID' => 324199 }, + 'Führungszeugnis' => { 'RUN' => 0, 'ID' => 120926 }, + 'Aufenthaltserlaubnis für im Bundesgebiet geborene Kinder - Erteilung' => { 'RUN' => 0, 'ID' => 324269 }, + 'Kraftfahrzeug – technische Änderung melden' => { 'RUN' => 0, 'ID' => 120904 }, + 'Reisepass beantragen' => { 'RUN' => 0, 'ID' => 121151 }, + 'Meldebescheinigung beantragen' => { 'RUN' => 0, 'ID' => 120702 }, + 'Umtausch eines Kartenführerscheins' => { 'RUN' => 0, 'ID' => 121616 }, + 'Umschreibung einer Dienstfahrerlaubnis' => { 'RUN' => 0, 'ID' => 121615 }, + 'Bescheinigung über ein unbefristetes Aufenthaltsrecht' => { 'RUN' => 0, 'ID' => 324921 }, + 'Elektrokennzeichen beantragen' => { 'RUN' => 0, 'ID' => 327084 }, + 'Abmeldung einer Wohnung' => { 'RUN' => 0, 'ID' => 120335 }, + 'Zulassungsbescheinigung Teil II für KFZ wegen Verlust oder Diebstahl ersetzen' => { 'RUN' => 0, 'ID' => 121584 }, + 'Beglaubigung von Unterschriften' => { 'RUN' => 0, 'ID' => 158142 }, + 'Kraftfahrzeugkennzeichen wegen Unleserlichkeit ersetzen' => { 'RUN' => 0, 'ID' => 324907 }, + 'Zulassung von Taxen (Kraftdroschken)' => { 'RUN' => 0, 'ID' => 326033 }, + 'Fahrerlaubnis - Erweiterung auf die Klassen D1, D1E, D und DE' => { 'RUN' => 0, 'ID' => 324450 }, + 'Wiederzulassung eines Kraftfahrzeuges ohne Halterwechsel beantragen' => { 'RUN' => 0, 'ID' => 120905 }, + 'Ersterteilung einer Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121627 }, + 'Beglaubigung von Kopien' => { 'RUN' => 0, 'ID' => 121701 }, + 'Saisonkennzeichen für KFZ beantragen' => { 'RUN' => 0, 'ID' => 121480 }, + 'Anmeldung einer Wohnung' => { 'RUN' => 0, 'ID' => 120686 }, + 'Zulassung eines aus dem EU-Ausland eingeführten fabrikneuen Fahrzeuges ohne Zulassungsbescheinigung Teil II' => { 'RUN' => 0, 'ID' => 324200 }, + 'Kraftfahrzeug ummelden – nach einem Umzug nach Berlin' => { 'RUN' => 0, 'ID' => 120918 }, + 'Umschreibung eines zugelassenen Fahrzeuges mit Berliner Kennzeichen auf einen anderen Halter' => { 'RUN' => 0, 'ID' => 120906 }, + 'Reisepass beantragen (vorläufiger Reisepass)' => { 'RUN' => 0, 'ID' => 121153 }, + 'Kraftfahrzeug außer Betrieb setzen (Kfz abmelden)' => { 'RUN' => 0, 'ID' => 120877 }, + 'Umstellung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 124556 }, + 'Widerspruchsrechte gegen Datenübermittlungen und Melderegisterauskünfte' => { 'RUN' => 0, 'ID' => 319141 }, + 'Gewerbezentralregister - Auskunft beantragen' => { 'RUN' => 0, 'ID' => 327835 }, + 'Wechselkennzeichen beantragen' => { 'RUN' => 0, 'ID' => 324587 }, + 'Personalausweis vorläufig beantragen' => { 'RUN' => 0, 'ID' => 120682 }, + 'Personalausweis beantragen' => { 'RUN' => 0, 'ID' => 120703 }, + 'Kraftfahrzeugkennzeichen ersetzen – nach Diebstahl oder Verlust eines Nummernschildes' => { 'RUN' => 0, 'ID' => 324196 }, + 'Umschreibung einer ausländischen Fahrerlaubnis aus einem EU-/EWR-Staat' => { 'RUN' => 0, 'ID' => 121598 }, + 'Selbstfahrermietfahrzeug – Beginn/Beendigung der Nutzung' => { 'RUN' => 0, 'ID' => 326039 }, + 'Zulassungsbescheinigung Teil I für KFZ wegen Verlust oder Diebstahl ersetzen' => { 'RUN' => 0, 'ID' => 121575 }, + 'Kraftfahrzeugkennzeichen auf Wunsch ändern' => { 'RUN' => 0, 'ID' => 121473 }, + 'Begleitetes Fahren mit 17' => { 'RUN' => 0, 'ID' => 121589 }, + 'Fahrerlaubnis zur Fahrgastbeförderung (P-Schein) - Verlängerung' => { 'RUN' => 0, 'ID' => 324389 }, + 'Zulassung eines außer Betrieb gesetzten Fahrzeuges mit Berliner Kennzeichen auf einen anderen Halter' => { 'RUN' => 0, 'ID' => 324169 }, + 'Befreiung von der Ausweispflicht' => { 'RUN' => 0, 'ID' => 327044 }, + 'Grünes Kennzeichen beantragen' => { 'RUN' => 0, 'ID' => 121482 }, + 'Aufenthaltserlaubnis in einen neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 121874 }, + 'Kraftfahrzeug außer Betrieb setzen, unvollständige Unterlagen (Kfz abmelden)' => { 'RUN' => 0, 'ID' => 325881 }, + 'Führerschein: Ausstellung – Internationaler Führerschein' => { 'RUN' => 0, 'ID' => 121591 }, + 'Neuerteilung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121637 }, + 'Niederlassungserlaubnis oder Erlaubnis zum Daueraufenthalt-EU in einen neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 324280 }, + 'Fahrerlaubnis zur Fahrgastbeförderung (P-Schein) - Erteilung' => { 'RUN' => 0, 'ID' => 121622 }, + } +); + + +# my $string = 'https://service.berlin.de/terminvereinbarung/termin/tag.php?termin=1&anliegen[]=158142&dienstleisterlist=122217,122219,122227,122231,122238,122243,122252,122260,122262,122254,122271,122273,122277,122280,122282,122284,122291,122285,122286,122296,327262,325657,150230,122301,122297,122294,122312,122314,122304,122311,122309,122281,324414,122283,122279,122276,122274,122267,122246,122251,122257,122208,122226&herkunft=http%3A%2F%2Fservice.berlin.de%2Fdienstleistung%2F158142%2F'; +# $string =~ m/dienstleisterlist=(.*?)\&/; +# say $1; +# my @venue_ids = split(/,/, $1); +# print Dumper \@venue_ids; +# die; + +# # INIT +# &init(); +# db_createRequestDienstleister(31); +# die; +# &submitRegister(); +# &getRegister('', 2); +# &getRegisterControls(); +# db_createRequestDienstleister(24); +# &getStandorte(); +# &getDienstleistungen(); +# die; + +# # MAIN +# &getStandorte() if $config{'RUN_VENUES'}; +# &getDienstleistungen() if $config{'RUN_SERVICES'}; +# # &run(); +# &db_request(); +# &Summary(); + +&programLoop(); + +# SUBS + +sub responseHandlerMech { + # &Delimiter((caller(0))[3]); + my $url = shift; + + print "\nGETTING $url... "; + + $mech->get($url); + say $mech->status(); + + # my $tree = HTML::TreeBuilder::XPath->new_from_content($mech->content()); + my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + + my ($success, $code) = &checkSuccess($tree, $mech); # returns 1 or 0 for $success and error/success code for $code + if (!$success && !$code) { + &dumpToFile('./output/successcodeundefined', $mech->content); + die '$success and $code undefined'; + } + say "success: $success\tcode: $code\t\tstatus: " . $mech->status; + + if ($mech->success()) { + if ($code == -6) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Starting new appointment search...'; + my $success = &restartHandler($tree, $code, $mech); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restartHandler_success' => 1, 'restart' => 1 } if $success; + # return $response if $response->code == 200; + # &responseHandlerMech($url) if $response->code != 200; + } + elsif ($code == -1) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Starting new appointment search...'; + my $success = &restartHandler($tree, $code, $mech); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restartHandler_success' => 1, 'restart' => 1 } if $success; + # return $response if $response->code == 200; + # &responseHandlerMech($url) if $response->code != 200; + } + elsif ($code == -9) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Timeout after 30 minutes. What to do here? Renewing cookie...'; + getCookie(); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restart' => 1 }; + } + elsif ($code == -2) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : XPATH_STOP detected: Restarting.'; + &getCookie(); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restart' => 1 }; + } + else { + return { 'success' => 1, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + } + else { + if ($mech->status == 404) { + &dumpToFile('./output/404', $mech->content); + if ($code == -8) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Error occrured, retrying...'; + &responseHandlerMech($url); # RETRY + } + elsif ($code == -14) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : No venue. Check request in db or its generation.'; + die; + } + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + # return { 'success' => 1, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + elsif ($mech->status == 429) { + if ($code == -12) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Too many requests...'; + &dumpToFile('./output/429', $mech->content); + my $success = &rateLimitHandler($tree, $url); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'rateLimit_success' => 1, 'restart' => 1 } if $success; + } + } + elsif ($mech->status == 428) { + if ($code == -5 || $code == -7) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Renewing captcha...'; + my $response = &captchaHandler($tree); + &responseHandlerMech($url) if !$response; # CAPTCHA NOT SOLVED SUCCESSFULLY, RETRY + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'captcha_success' => 1, 'restart' => 1 }; + } + } + elsif ($mech->status == 500) { + if ($code == -10) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Site currently in maintenance mode.'; + die; + } + } + else { + say 'Mech->status ' . $mech->status . ' not handled yet.'; + die; + # return { 'success' => 0, 'status' => $mech->status, 'tree' => $tree, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + } +} + +sub checkSuccess { + &Delimiter((caller(0))[3]); + my ($tree, $mech) = @_; + + my $xpath_alert = '//div[contains(concat(" ", normalize-space(@class), " "), " alert")]/p[1]'; + my $xpath_alert2 = '//div[contains(concat(" ", normalize-space(@class), " "), " alert")]/div/p'; + my $xpath_alert3 = '//div[@class="zms"]/div[@class="html5-header header"]/h1[@class="title"]'; + my $xpath_alert4 = '//div[@class="offset2 span7"]/h2'; + my $xpath_alert5 = '//div[contains(concat(" ", normalize-space(@class), " "), " offset2 ")]/h1'; + + my $xpath_success = '//div[@class="submit-success-message"]'; + + if ($tree->exists($xpath_alert)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert))); + say "Alert: $alert"; + + if ($alert eq 'Sie haben schon einen Termin reserviert. Soll diese Reservierung gelöscht werden oder möchten Sie mit der vorhandenen Reservierung fortfahren?') { + say 'Code -1: Another reservation is already in progress.'; + &dumpToFile('./output/-1', $mech->content); + return (0, -1); + } + elsif ($alert eq 'Zu ihrer Suche konnten keine Daten ermittelt werden. Dies kann unterschiedliche Gründe haben:') { + say 'Code -2: Cookie seems outdated'; + &dumpToFile('./output/-2', $mech->content); + return (0, -2); + } + elsif ($alert eq 'Sie haben Ihre Terminvereinbarung abgeschlossen. Sie können nun eine neue Terminssuche starten!') { + say 'Code -3: Reservation already completed'; + &dumpToFile('./output/-3', $mech->content); + return (0, -3); + } + elsif ($alert eq 'Ihr Termin ist eingetragen worden. Bitte drucken Sie diese Seite aus, damit Sie alle Informationen zur Hand haben. Wichtig für Ihren Besuch ist die Vorlage Ihrer Vorgangsnummer.') { + say 'Code 1: submitRegister successfull.'; + &dumpToFile('./output/1', $mech->content); + say trim($tree->findvalue($xpath_success)); + + my %return_data; + $return_data{'AUTH_KEY'} = $tree->findvalue('//span[@class="summary_authKey"]'); + $return_data{'PROCESS_ID'} = $tree->findvalue('//span[contains(concat(" ", normalize-space(@class), " "), " summary_processId ")]'); + say $tree->findvalue('//span[@class="summary_authKey"]'); + say $tree->findvalue('//span[contains(concat(" ", normalize-space(@class), " "), " summary_processId ")]'); + return (1, 1, \%return_data); + } + elsif ($alert eq 'Der eingegebene Text stimmte nicht mit dem Bild überein. Versuchen Sie es noch einmal.') { + say 'Code -5: Text / Picture mismatch'; + &dumpToFile('./output/-5', $mech->content); + return (0, -5); + } + elsif ($alert eq 'Ihre Auswahl von Standort und Diensteistung hat sich geändert. Möchten Sie mit Ihrer vorherigen Suche fortfahren oder eine neue Terminsuche starten?') { + say 'Code -6: Location and venue changed. New search or continue.'; + &dumpToFile('./output/-6', $mech->content); + return (0, -6); + } + elsif ($alert eq 'Ihre Vorherige Reservierung wird mit der Auswahl eines neuen Termins entfernt. Fahren Sie nur fort, wenn Sie Ihren bisherigen Termin wirklich ändern wollen.') { + say 'Code -11: Old appointment removed for new one.'; + &dumpToFile('./output/-11', $mech->content); + return (0, -11); + } + elsif ($alert eq 'Bei der Eingabe der Daten scheint Ihnen ein Fehler unterlaufen zu sein. Bitte prüfen Sie die Hinweise an den markierten Formularfeldern.') { + say 'Code -13: Form input error'; + &dumpToFile('./output/-13', $mech->content); + return (0, -13); + } + else { + # say $mech->content(); + say 'NO XPATH SPECIFIED DETECTED'; + say $alert; + &dumpToFile('./output/noxpathspecified', $mech->content); + # my $string = trim($tree->findvalue($xpath_alert)); + + # $string = decode('utf-8', $string); + # say $string; + # say utf8::is_utf8($string); # since Perl 5.8.1 + # say utf8::valid($string); + die; + } + + } + elsif ($tree->exists($xpath_alert2)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert2))); + say "Alert2: $alert"; + + if ($alert eq 'Leider konnte der ausgewählte Termin nicht reserviert werden. Da war jemand schneller als Sie. Bitte versuchen Sie es erneut.') { + say 'Code -4: Appointment not available anymore and booked by another person.'; + &dumpToFile('./output/-4', $mech->content); + return (0, -4); + } + elsif ($alert eq 'Es wurde kein Standort ausgewählt. Bitte wählen Sie mindestens einen Standort aus.') { + say 'Code -14: No venue selected'; + &dumpToFile('./output/-14', $mech->content); + return (0, -14); + } + } + elsif ($tree->exists($xpath_alert3)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert3))); + say "Alert 3: $alert"; + + if ($alert eq 'Bitte verifizieren sie sich') { + say 'Code -7: Verification needed.'; + &dumpToFile('./output/-7', $mech->content); + return (0, -7); + } + elsif ($alert eq 'Terminvereinbarung') { + say 'Code 3: Normal Terminvereinbarung'; + &dumpToFile('./output/-7', $mech->content); + return (1, 3); + } + elsif ($alert eq 'Bitte entschuldigen Sie den Fehler') { + say 'Code -8: An error has occured'; + &dumpToFile('./output/-8', $mech->content); + return (1, -8); + } + elsif ($alert eq 'Zeitüberschreitung: Bitte versuchen Sie es erneut') { + say 'Code -9: Timeout: Request older than 30 minutes.'; + &dumpToFile('./output/-9', $mech->content); + return (1, -9); + } + + } + elsif ($tree->exists($xpath_alert4)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert4))); + say "Alert 4: $alert"; + + if ($alert eq 'Die Terminvereinbarung ist zur Zeit nicht möglich.') { + say 'Code -10: Maintenance mode.'; + &dumpToFile('./output/-10', $mech->content); + return (0, -10); + } + elsif ($alert eq 'Service-Portal Berlin' && $tree->findvalue($xpath_alert5) eq 'Zu viele Zugriffe') { + say 'Code -12: Too many requests.'; + &dumpToFile('./output/-12', $mech->content); + return (0, -12); + } + } + else { + say 'Code 2: NO XPATH SPECIFIED DETECTED. Everything fine?'; + &dumpToFile('./output/2', $mech->content); + return (1, 2); + } + +} + +sub restartHandler { + &Delimiter((caller(0))[3]); + my ($tree, $code, $mech) = @_; + + my $url; + if ($code == -6) { + $url = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " alert alert-error noprint textile ")]/ul/li[2]/a/@href'); + } + elsif ($code == -1) { + $url = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " alert alert-info noprint textile ")]/ul/li[2]/a/@href'); + } + $url = $config{'URL_ROOT'} . $url; + say "URL: $url"; + + my $response = $mech->get($url); + say $response->code; + if ($mech->success) { + say "Restart with code $code successfull."; + return 1; + } + else { + say "Restart with code $code unsuccessfull."; + &dumpToFile('./output/restartunsuccessfull', $mech->content); + &responseHandlerMech($url); # LET MECH HANDLE RESPONSE + # print Dumper $response; + # return 0; + # die; + } +} + +sub getRegister { + &Delimiter((caller(0))[3]); + my ($dataset_ref, $user_id) = @_; + + my $response_user = &db_query("SELECT * FROM `reg_users` WHERE `ID` = $user_id", $connection, 'selectall_hashref', 'ID'); + + my $url = $$dataset_ref{'HREF'}; + my $response = &responseHandlerMech($url); + # my $response = &responseHandlerMech('https://zinomedia.de/dl/customHTML2.html'); + + if (!$response->{'success'} && $response->{'restart'}) { + &getRegister($dataset_ref, $user_id); + } + elsif (!$response->{'success'}) { + return 0; + } + elsif ($response->{'success'}) { + my $return_data_ref = &submitRegister($mech, $$response_user{$user_id}); + return (1, $return_data_ref); + } +} + +sub getRegisterControls { + &Delimiter((caller(0))[3]); + my $switch = shift; + $switch = '' if !$switch; + + use Data::Structure::Util qw( unbless ); + my %controls; + + + + # my $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/tag.php?termin=1&dienstleister=122210&anliegen[]=120335&herkunft=1'); # ABmeldung einer Wohnung Halemweg + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579561200/'); # Termin Zeiten + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579260240/141/'); # Terminbuchung + + # say 'SWITCHING SERVICE NOW'; + + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/tag.php?termin=1&dienstleister=122217&anliegen[]=120335&herkunft=1'); # ABmeldung einer Wohnung Heerstraße + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579215600/'); # Termin Zeiten + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579244400/141/'); # Terminbuchung + + # die; + + + for my $service (sort keys %{$config{'SERVICES'}}) { + my $service_id = $config{'SERVICES'}{$service}{'ID'}; + say "\nSTART GETTING CONTROLS FOR SERVICE '$service'..."; + my $href = "https://service.berlin.de/dienstleistung/$service_id/"; + + my $response = &responseHandlerMech($href); + if ($response->{'success'}) { + say 'success'; + + my @nodes = $response->{'tree'}->findnodes('//div[@class="behoerdenitem"]/div[@class="row"]'); + say $#nodes; + # print Dumper $nodes; + + foreach my $node (sort @nodes) { + if ($node->exists('./div[@class="span5" and (strong)]') || !$node->exists('./div[@class="span2"]/p/strong/a/@href')) { + next; + } + my $venue = $node->findvalue('./div[@class="span5"]'); + my $terminvereinbarung_url = $node->findvalue('./div[@class="span2"]/p/strong/a/@href'); + my $last = 0; + + say "$venue => $terminvereinbarung_url"; + + my $success = &getTerminDay($terminvereinbarung_url, $service_id, 'getRegisterControls'); + return if $switch eq 'getCookie'; + if ($success) { + say 'We have appointments.'; + print Dumper $data{'SERVICES'}{$service_id}{'AVAILABLE'}; + + foreach my $epochtime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}) { + foreach my $datetime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$epochtime}{'DATETIMES'}}) { + my $register_url = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$epochtime}{'DATETIMES'}{$datetime}{'HREF'}; + say $datetime; + say $register_url; + + my $response = &responseHandlerMech($register_url); + if ($response->{'success_checkSuccess'}) { + my $form = $mech->form_number(2); + my @inputs = $mech->find_all_inputs( + # type => 'textarea', + # name_regex => qr/^customer/, + ); + + my $url = $mech->uri()->as_string; + + foreach my $i (0 .. $#inputs) { + # my $digest; + my $contains_pflichtfeld = 0; + foreach my $key2 (sort keys %{$inputs[$i]}) { + # print "KEY $key VALUE "; + # say $inputs[0]{$key}; + # $digest .= "$key$inputs[$i]{$key}"; + $contains_pflichtfeld = 1 if $inputs[$i]{$key2} =~ m/\*$/; + } + + my $key; + if ($inputs[$i]{'name'}) { + $key = 'name'; + } + elsif ($inputs[$i]{'id'}) { + $key = 'id'; + } + else { + print Dumper $inputs[$i]; + die; + } + + # $digest = md5_hex($digest); + # if (!$controls{$digest}) { + # # $controls{$digest}{'HASH'} = unbless(\%{$inputs[$i]}); + # $controls{$digest}{'HASH'} = \%{$inputs[$i]}; + # $controls{$digest}{'FIRST_LVL_PFLICHTFELD'} = $contains_pflichtfeld; + # $controls{$digest}{'URI'}{$url} = 1; + # push(@{$controls{$digest}{'VENUES'}}, "$venue => $service"); + # } + # else { + # push(@{$controls{$digest}{'VENUES'}}, "$venue => $service"); + # } + + + if (!$controls{$key}) { + $controls{$inputs[$i]{$key}}{'HASH'} = unbless(\%{$inputs[$i]}); + # $controls{$digest}{'HASH'} = \%{$inputs[$i]}; + $controls{$inputs[$i]{$key}}{'FIRST_LVL_PFLICHTFELD'} = $contains_pflichtfeld; + $controls{$inputs[$i]{$key}}{'URI'}{$url} = 1; + push(@{$controls{$inputs[$i]{$key}}{'VENUES'}}, "$venue => $service"); + } + else { + push(@{$controls{$inputs[$i]{$key}}{'VENUES'}}, "$venue => $service"); + } + + } + + # say 'Aborting now...'; + # my $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/abort/'); + # sleep(5); + undef %data; + $last = 1; + last if $last; + } + + # print Dumper \%controls; + # die; + last if $last; + } + last if $last; + } + + } + + # print Dumper \%controls; + # die; + } + } + &dumpToFile('./output/controls', \%controls); + print Dumper \%controls; + die; + } +} + +sub submitRegister { + &Delimiter((caller(0))[3]); + my ($mech, $dataset_ref) = @_; + + my $form = $mech->form_number(2); + my @inputs = $mech->find_all_inputs(); + + my $exist_surveyAccepted = 0; + my $exist_telephone = 0; + # print Dumper \@inputs; + for my $i (0 .. $#inputs) { + $exist_surveyAccepted = 1 if defined $inputs[$i]{'name'} && $inputs[$i]{'name'} eq 'surveyAccepted'; + $exist_telephone = 1 if defined $inputs[$i]{'name'} && $inputs[$i]{'name'} eq 'telephone'; + } + # say $exist_surveyAccepted; + + my $familyName = "$$dataset_ref{'FIRST_NAME'} $$dataset_ref{'LAST_NAME'}"; + my $email = $$dataset_ref{'EMAIL'}; + my $telephone = $$dataset_ref{'PHONE'}; + # say $familyName; + # say $email; + # die; + + $mech->set_fields( 'familyName' => $familyName, 'email' => $email, ); + $mech->set_fields( 'telephone' => $telephone ) if $exist_telephone; + $mech->tick('agbgelesen', 1); + $mech->select('surveyAccepted', '0') if $exist_surveyAccepted; + $mech->click_button(id => 'register_submit'); + + + # my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + say $mech->status(); + if ($mech->success()) { + &dumpToFile('./output/successSubmitRegister', $mech->content); + my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + + my ($success, $code, $return_data_ref) = &checkSuccess($tree, $mech); + say "success: $success\tcode: $code"; + if (!$success && !$code) { + &dumpToFile('./output/successcodeundefined', $mech->content); + die '$success and $code undefined'; + } + elsif ($code == -13) { + &dumpToFile('./output/-13inputs', \@inputs); + die; + } + elsif ($code && $success) { + say 'Successfully registered appointment'; + return $return_data_ref; + } + } + else { + &dumpToFile('./output/unsuccessSubmitRegister', $mech->content); + die; + } +} + +sub programLoop { + &Delimiter((caller(0))[3]); + + for (;;) { + # INIT + &init(); + + # MAIN + # &getCookie(); # GET COOKIE, CHECK IF COOKIE IS VALID OR RENEW + &getStandorte() if $config{'RUN_VENUES'}; + &getDienstleistungen() if $config{'RUN_SERVICES'}; + &db_request(); + &Summary(); + + &sleepCountdown($config{'SLEEP_LOOP_MIN'} * 60); + } +} + +sub db_createRequestDienstleister { + &Delimiter((caller(0))[3]); + my $request_id = shift; + my $values; + + my $string = &db_query("Select terminsnipe.services.HREF From terminsnipe.request Inner Join terminsnipe.services On terminsnipe.services.ID = terminsnipe.request.SERVICE_ID Where terminsnipe.request.ID = $request_id", $connection, 'fetchrow'); + my @dienstleister = getDienstleisterlist($string); + + foreach my $id (@dienstleister) { + $values .= "('$request_id', '$id'), "; + } + my $sql = substr("INSERT INTO `request_dienstleister` (`REQUEST_ID`, `VENUE_ID`) VALUES $values", 0, -2); + # say $sql; + + &db_query("DELETE FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id", $connection, 'do'); + &db_query($sql, $connection, 'do'); +} + +sub getDienstleisterlist { + &Delimiter((caller(0))[3]); + my $string = shift; + + $string =~ m/dienstleisterlist=(.+?)&/; + return split /,/, $1; +} + +sub db_request { + &Delimiter((caller(0))[3]); + + my $response = &db_query("SELECT `ID`, `USER_ID`, `SERVICE_ID`, `DATETIME_FROM`, `DATETIME_TO`, `TIMESTAMP` FROM `request` WHERE `EXPIRED` = 0;", $connection, 'selectall_arrayref'); + + + + if (@$response) { + for my $i (0 .. $#{$response}) { + my $dataset = $$response[$i]; + $request{$$dataset[2]}{$$dataset[0]}{'ID'} = $$dataset[0]; + $request{$$dataset[2]}{$$dataset[0]}{'USER_ID'} = $$dataset[1]; + $request{$$dataset[2]}{$$dataset[0]}{'SERVICE_ID'} = $$dataset[2]; + $request{$$dataset[2]}{$$dataset[0]}{'DATETIME_FROM'} = $$dataset[3]; + $request{$$dataset[2]}{$$dataset[0]}{'DATETIME_TO'} = $$dataset[4]; + $request{$$dataset[2]}{$$dataset[0]}{'TIMESTAMP'} = $$dataset[5]; + $services{$$dataset[2]}{'HREF_GENERATED'} = &db_generateHREF($$dataset[0], $$dataset[2]); # ALSO CREATES $services{$$dataset[2]}{'REQUEST_DIENSTLEISTER_VENUES'} + + # print Dumper $data{'SERVICES'}{$$dataset[2]}{'REQUEST_VENUE_IDS'}; + + # die; + } + print Dumper \%request; + + # DISCONNECT PARENT DB + $connection->disconnect(); + + foreach my $service_id (keys %request) { + &startFork($service_id); + } + $pm->wait_all_children; + say 'All childs are done. Continuing with parent...'; + + # foreach my $service_id (keys %request) { + # &db_find_appointment($service_id, \%request); + # } + } + else { + say 'No requests in db.'; + } +} + +sub db_generateHREF { + &Delimiter((caller(0))[3]); + my ($request_id, $service_id) = @_; + + my $href_generated = $services{$service_id}{'HREF'}; + my $response = &db_query("SELECT `VENUE_ID` FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id;", $connection, 'selectall_arrayref'); + + my $dienstleisterlist; + for my $i (0 .. $#{$response}) { + $dienstleisterlist .= "$$response[$i][0],"; + push(@{$data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'}}, [ $$response[$i][0], $venues{$$response[$i][0]}{'NAME'}, $venues{$$response[$i][0]}{'HREF'} ]); + } + $dienstleisterlist = substr($dienstleisterlist, 0, -1); + $href_generated =~ m/dienstleisterlist=(.+?)&/; + $href_generated =~ s/$1/$dienstleisterlist/; + + return $href_generated; +} + +sub startFork { + &Delimiter((caller(0))[3]); + my ($service_id) = @_; + + my $pid = $pm->start($service_id) and next; + + # CHILD START + my $service = $services{$service_id}{'NAME'}; + my $href = $services{$service_id}{'HREF_GENERATED'}; + $service_id_fork = $service_id; + + say "START RUN FOR SERVICE '$service'..."; + + # MYSQL + $connection = &connectToMySql($config{'MYSQL'}{'ACCESS'}{'database'}); + $connection->do('set names utf8'); + + $cookie_jar = &checkCookie("$config{'COOKIE_DIR'}/$service_id"); + &newBrowser($cookie_jar); + &getTerminDay($href, $service_id); + + $connection->disconnect(); + $pm->finish; +} + +sub checkCookie { + &Delimiter((caller(0))[3]); + my $cookie = shift; + + say "Checking cookie $cookie..."; + + my $cookie_jar = HTTP::Cookies->new(ignore_discard => 1 ); + + if (-e $cookie) { + my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($cookie); + my $dt_epoch = DateTime->from_epoch(epoch => $ctime); + my $duration = $dt_epoch->delta_ms(DateTime->now); + print "Cookie age in minutes: " . $duration->minutes . "\n"; + if ($duration->minutes > 30) { + unlink $cookie; + } + else { + say "Loading $cookie..."; + $cookie_jar->load($cookie); + } + } + else { + say "Cookie not existent."; + } + + return $cookie_jar; +} + +sub db_find_appointment { + &Delimiter((caller(0))[3]); + my ($dataset_ref, $service_id) = @_; + + foreach my $id (keys %{$dataset_ref}) { + my $query = "Select terminsnipe.datetime.DATETIME, terminsnipe.mapping.SERVICE_ID, terminsnipe.mapping.VENUE_ID, terminsnipe.mapping.HREF As HREF, terminsnipe.venues.NAME As VENUE_NAME, terminsnipe.datetime.EPOCHTIME, terminsnipe.mapping.ID As MAPPING_ID From terminsnipe.datetime Inner Join terminsnipe.mapping On terminsnipe.mapping.DATETIME_ID = terminsnipe.datetime.EPOCHTIME Inner Join terminsnipe.venues On terminsnipe.mapping.VENUE_ID = terminsnipe.venues.ID Where Not terminsnipe.datetime.DATETIME > '$$dataset_ref{$id}{'DATETIME_TO'}' And Not terminsnipe.datetime.DATETIME < '$$dataset_ref{$id}{'DATETIME_FROM'}' And terminsnipe.mapping.SERVICE_ID = $service_id;"; + say $query; + my $response = &db_query($query, $connection, 'selectall_hashref', 'EPOCHTIME'); + # print Dumper $response; + + say 'Possible appointments: ' . scalar(keys(%{$response})); + if (%{$response}) { + &db_insertAppointment($response, $$dataset_ref{$id}); + } + } +} + +sub db_insertAppointment { + &Delimiter((caller(0))[3]); + my ($response, $dataset_ref) = @_; # $response = possible appointments from db, $dataset_ref = request data for user, delete after successfull insert? + my $user_id = $$dataset_ref{'USER_ID'}; + my $request_id = $$dataset_ref{'ID'}; + + foreach my $epochtime (keys %{$response}) { + if (!$$response{$epochtime}{'RESERVED'}) { + say "Starting reservation with MAPPIND_ID $$response{$epochtime}{MAPPING_ID} for USER_ID $user_id..."; + + my ($success, $return_data_ref) = &getRegister($$response{$epochtime}, $user_id); + + if ($success) { + print Dumper $return_data_ref; + say 'Inserting new appointment into db now...'; + my $query = "INSERT INTO `appointments` (`MAPPING_ID`, `USER_ID`, `PROCESS_ID`, `AUTH_KEY`) VALUES ('$$response{$epochtime}{MAPPING_ID}', '$user_id', '$$return_data_ref{PROCESS_ID}', '$$return_data_ref{AUTH_KEY}')"; + say $query; + &db_query($query, $connection, 'do'); + $$response{$epochtime}{'RESERVED'} = 1; + + say "Deleting request with REQUEST_ID $request_id from request and request_dienstleister..."; + &db_query("DELETE FROM `request` WHERE `ID` = $request_id;", $connection, 'do'); + &db_query("DELETE FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id;", $connection, 'do'); + last; + } + elsif (!$success) { + say 'Could not book appointment.'; + } + else { + say 'Unknown return value'; + die; + } + } + else { + say "Appointment $epochtime is available but already reserved in loop by another user."; + } + } +} + +# sub db_query { +# # &Delimiter((caller(0))[3]); +# my ($query, $connection, $switch, @rest) = @_; +# my $response; + +# # say 'DATABASE CONNECTION STATE: ' . $connection->ping; +# # my $connectionInfo = "dbi:mysql:terminsnipe;$config{'MYSQL'}{'ACCESS'}{'hostname'}, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}"; +# # my $connection = AnyEvent::DBI::MySQL->connect($connectionInfo); + +# my $statement; +# if ($switch eq 'fetchrow') { +# $statement = $connection->prepare($query); +# $statement->execute(); +# $response = $statement->fetchrow(); +# } +# elsif ($switch eq 'do') { +# $response = $connection->do($query) or die $connection->errstr; +# $connection->do($query, sub { +# my ($rv, $dbh) = @_; +# print Dumper $rv; +# print Dumper $dbh; +# }); +# } +# elsif ($switch eq 'selectall_hashref') { +# # $connection->selectall_hashref($query, $rest[0], { async => 1 }) or die $connection->errstr; + +# $statement = $connection->prepare($query, { async => 1 }); +# $statement->execute(); +# $response = $statement->fetchall_hashref($rest[0]); + +# until($connection->mysql_async_ready) { +# say 'not ready yet!'; +# sleep 1; +# } +# $response = $connection->mysql_async_result; + +# } +# elsif ($switch eq 'selectall_arrayref') { +# # $response = $connection->selectall_arrayref($query) or die $connection->errstr; +# $connection->selectall_arrayref($query, sub { +# ($response) = @_; +# }); +# } +# $statement->finish if $statement; + +# return $response; +# } + +sub db_query { + # &Delimiter((caller(0))[3]); + my ($query, $connection, $switch, @rest) = @_; + my $response; + + # my $connection = &connectToMySql($config{'MYSQL'}{'ACCESS'}{'database'}); + + # say 'DATABASE CONNECTION STATE: ' . $connection->ping; + + my $statement; + if ($switch eq 'fetchrow') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchrow(); + } + elsif ($switch eq 'fetchall_hashref') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchall_hashref($rest[0]); + } + elsif ($switch eq 'fetchall_arrayref') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchall_arrayref(); + } + elsif ($switch eq 'do') { + $response = $connection->do($query) or die $connection->errstr; + } + elsif ($switch eq 'selectall_hashref') { + $response = $connection->selectall_hashref($query, $rest[0]) or die $connection->errstr; + } + elsif ($switch eq 'selectall_array') { + my @response = $connection->selectall_array($query) or die $connection->errstr; + return \@response; + } + elsif ($switch eq 'selectall_arrayref') { + $response = $connection->selectall_arrayref($query) or die $connection->errstr; + } + elsif ($switch eq 'selectrow_hashref') { + $response = $connection->selectrow_hashref($query) or die $connection->errstr; + } + $statement->finish if $statement; + + # $connection->disconnect(); + + return $response; +} + +sub connectToMySql { + &Delimiter((caller(0))[3]); + my ($db) = @_; + + # assign the values to your connection variable + my $connectionInfo = "dbi:mysql:$db;$config{'MYSQL'}{'ACCESS'}{'hostname'}"; + + # make connection to database + # my $l_connection = DBI->connect($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, $config{'MYSQL'}{'OPTIONS'}) || warn "MySQL conncet failed: $DBI::errstr"; + # return $l_connection; + # my $l_connection = DBI->connect($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, { + # RaiseError => 0, # SUGGESTED BY AnyEvent::DBI::MySQL + # AutoCommit => 1, + # mysql_auto_reconnect => 1, + # }); + # return $l_connection; + + # my $l_connection = AnyEvent::DBI::MySQL->connect($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, { + # RaiseError => 0, # SUGGESTED BY AnyEvent::DBI::MySQL + # AutoCommit => 1, + # }); + + my $l_connection = DBIx::Connector->new($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, { + RaiseError => 1, # SUGGESTED BY AnyEvent::DBI::MySQL + AutoCommit => 1, + mysql_auto_reconnect => 1, + }); + return $l_connection->dbh; +} + +sub run { + &Delimiter((caller(0))[3]); + + for my $service (keys %{$config{'SERVICES'}}) { + if ($config{'SERVICES'}{$service}{'RUN'}) { + my $service_id = $config{'SERVICES'}{$service}{'ID'}; + say "START RUN FOR SERVICE '$service'..."; + &getTerminDay($services{$service_id}{'HREF'}, $service_id); + &db_sync($service_id); + } + else { + # say "DISABLED $service"; + } + } +} + +sub db_sync { + &Delimiter((caller(0))[3]); + my $service_id = shift; + + # &db_query("DELETE FROM `mapping` WHERE `SERVICE_ID` = '$service_id';", $connection, 'do'); + + foreach my $day (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}) { + foreach my $datetime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}}) { + if ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'SYNCED'} == 0) { + my $epochtime = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}; + my $venue_id = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'VENUE_ID'}; + my $href = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'HREF'}; + + say "$service_id\t$venue_id\t$epochtime"; + &db_query("INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) SELECT $epochtime, '$datetime' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `datetime` WHERE `EPOCHTIME`= $epochtime);", $connection, 'do'); + &db_query("INSERT INTO `mapping` (`SERVICE_ID`, `VENUE_ID`, `DATETIME_ID`, `HREF`) SELECT '$service_id', '$venue_id', '$epochtime', '$href' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `mapping` WHERE `SERVICE_ID` = '$service_id' AND `VENUE_ID` = '$venue_id' AND `DATETIME_ID` = '$epochtime');", $connection, 'do'); + + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'SYNCED'} = 1; + } + # else { + # say "ALREADY SYNCED: $datetime"; + # } + } + } + +} + +sub createMysqlDateTime { + # &Delimiter((caller(0))[3]); + my $string = shift; + + my $dt = $strp->parse_datetime($string); + return DateTime::Format::MySQL->format_datetime($dt); +} + +sub getDienstleistungen { + &Delimiter((caller(0))[3]); + + my $response = &responseHandlerMech($config{'URL_DIENSTLEISTUNGEN'}); + + if (!$response->{'success'} && $response->{'restart'}) { + &getDienstleistungen(); + } + elsif ($response->{'success'}) { + &parseDienstleistungen($response->{'tree'}); + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in getDienstleistungen and rateLimitHandler unsuccessfull.'; + } + + if ($config{'FILTER_SERVICES_TO_OUTPUT'} || $config{'DB_REFRESH_SERVICES'}) { + &filterDienstleistungen(); + &db_insertServicesVenues('services') if $config{'DB_REFRESH_SERVICES'}; + } + + if ($config{'DUMP_SERVICES_TO_FILE'}) { + &dumpToFile($config{'SERVICES_PATH'}, \%services); + } +} + +sub db_insertServicesVenues { + &Delimiter((caller(0))[3]); + my $switch = shift; + my ($database, $hash_ref); + + if ($switch eq 'services') { + $database = 'services'; + $hash_ref = \%services; + } + elsif ($switch eq 'venues') { + $database = 'venues'; + $hash_ref = \%venues; + } + + my $response = &db_query("delete FROM $database", $connection, 'do'); + $response = &db_query("delete FROM `mapping_services`", $connection, 'do') if $switch eq 'services'; + + foreach my $id (sort keys %{$hash_ref}) { + my $query = "INSERT INTO `$database` (`ID`, `NAME`, `HREF`) VALUES ('$$hash_ref{$id}{'ID'}', '$$hash_ref{$id}{'NAME'}', '$$hash_ref{$id}{'HREF'}');"; + my $response = &db_query($query, $connection, 'do'); + + if ($switch eq 'services') { + foreach my $venue_id (@{$hash_ref->{$id}->{'MAPPING_SERVICES'}}) { + $query = "INSERT INTO `mapping_services` (`SERVICE_ID`, `VENUE_ID`) VALUES ('$$hash_ref{$id}{'ID'}', '$venue_id');"; + $response = &db_query($query, $connection, 'do'); + } + } + } +} + +sub filterDienstleistungen { + &Delimiter((caller(0))[3]); + + # DATA DUMPER UTF8 HACK + local *Data::Dumper::qquote = sub { qq["${\(shift)}"] }; + local $Data::Dumper::Useperl = 1; + + open my $FILE, '>>:encoding(UTF-8)', $config{'FILTER_SERVICES_PATH'}; + foreach my $id (keys %services) { + + my $response = &responseHandlerMech($services{$id}{'HREF'}); + + if (!$response->{'success'} && $response->{'restart'}) { + &filterDienstleistungen(); + } + elsif ($response->{'success'}) { + # &dumpToFile("$config{'OUTPUT_DIR'}/service_$id", $response->{'mech'}->content()); + my $tree = $response->{'tree'}; + my $name = $services{$id}{'NAME'}; + + print "\t$name => "; + + if (!$tree->exists('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]')) { + say '0'; + delete $services{$id}; + } + else { + my $href = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]/a/@href'); # e.g. https://service.berlin.de/terminvereinbarung/termin/tag.php?termin=1&anliegen[]=120335&dienstleisterlist=122210,122217,122219,122227,122231,122238,122243,122252,122260,122262,122254,122271,122273,122277,122280,122282,122284,122291,122285,122286,122296,327262,325657,150230,122301,122297,122294,122312,122304,122311,122309,317869,324434,122281,122279,122276,122274,122267,122246,122251,122257,122208,122226&herkunft=http%3A%2F%2Fservice.berlin.de%2Fdienstleistung%2F120335%2F + my $entry = "'$name' => { 'RUN' => 0, 'ID' => $services{$id}{'ID'} },"; + + print '1 => ' . $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]/a') . " | $entry\n"; + say $FILE $entry if $config{'FILTER_SERVICES_TO_OUTPUT'}; + $services{$id}{'HREF'} = $href; + + $href =~ m/dienstleisterlist=(.*?)\&/; + @{$services{$id}{'MAPPING_SERVICES'}} = split(/,/, $1); # contains array of dienstleistlist ID's + # print Dumper $services{$id}; + } + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in filterDienstleistungen and rateLimitHandler unsuccessfull.'; + } + } + close $FILE; +} + +sub parseDienstleistungen { + &Delimiter((caller(0))[3]); + my $tree = shift; + + foreach my $dataset ($tree->findnodes('//li[contains(concat(" ", normalize-space(@class), " "), " topic-dls row-fluid ")]')) { + my $id = basename($dataset->findvalue('./a/@href')); + my $href = $dataset->findvalue('./a/@href'); + my $name = trim($dataset->findvalue('./a')); + + $services{$id}{'ID'} = $id; + $services{$id}{'HREF'} = "$config{'URL_ROOT'}$href"; + $services{$id}{'NAME'} = $name; + } +} + +sub getStandorte { + &Delimiter((caller(0))[3]); + + my $response = &responseHandlerMech($config{'URL_STANDORTE'}); + + if (!$response->{'success'} && $response->{'restart'}) { + &getStandorte(); + } + if ($response->{'success'}) { + &parseStandorte($response->{'tree'}); + &db_insertServicesVenues('venues') if $config{'DB_REFRESH_VENUES'}; + dumpToFile($config{'VENUES_PATH'}, \%venues) if $config{'VENUES_TO_FILE'}; + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in getStandorte and rateLimitHandler unsuccessfull.'; + } +} + +sub dumpToFile { + # &Delimiter((caller(0))[3]); + my ($filepath, $ref) = @_; + + # DATA DUMPER UTF8 HACK + # no warnings 'redefine'; + local *Data::Dumper::qquote = sub { qq["${\(shift)}"] }; + local $Data::Dumper::Useperl = 1; + + open my $FILE, '>:encoding(UTF-8)', $filepath; + print $FILE Dumper $ref; + close $FILE; +} + +sub parseStandorte { + &Delimiter((caller(0))[3]); + my $tree = shift; + + foreach my $dataset ($tree->findnodes('//li[contains(concat(" ", normalize-space(@class), " "), " topic-dls ")]')) { + my $id = basename($dataset->findvalue('./a/@href')); + my $href = $dataset->findvalue('./a/@href'); + my $name = trim($dataset->findvalue('./a')); + + say $name; + + $venues{$id}{'ID'} = $id; + $venues{$id}{'HREF'} = "$config{'URL_ROOT'}$href"; + $venues{$id}{'NAME'} = $name; + } +} + +sub getCookie { + &Delimiter((caller(0))[3]); + + # my $url; + # foreach my $id (sort keys %services) { + # $url = $services{$id}{'HREF'}; + # last; + # } + + $mech = WWW::Mechanize->new( + agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + autocheck => 0, + ); + &getRegisterControls('getCookie'); +} + +sub init { + &Delimiter((caller(0))[3]); + + ($zsmappointment, $ts, $mech, $ua, $cookie_jar, $strp, $connection, $strp_datetime, $pm) = (undef) x 9; + undef %data; + + # STRP DATETIME + $strp = DateTime::Format::Strptime->new( + # pattern => '%d %b %Y %H:%M', # Before umlaut month fix + pattern => '%d %m %Y %H:%M', + locale => 'de_DE', + ); + + $strp_datetime = DateTime::Format::Strptime->new( + pattern => '%Y-%m-%d %T', + locale => 'de_DE', + ); + + # MYSQL + $connection = &connectToMySql($config{'MYSQL'}{'ACCESS'}{'database'}); + + # SERVICE AND VENUE HASH + $connection->do('set names utf8'); + %venues = %{&db_query("SELECT `ID`, `NAME`, `HREF` FROM `venues`", $connection, 'selectall_hashref', 'ID')}; + %services = %{&db_query("SELECT `ID`, `NAME`, `HREF` FROM `services`", $connection, 'selectall_hashref', 'ID')}; + + # COOKIES + $cookie_jar = checkCookie($config{'COOKIE_PATH'}); + # $cookie_jar = HTTP::Cookies->new(ignore_discard => 1 ); + # if (-e $config{'COOKIE_PATH'}) { + # my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($config{'COOKIE_PATH'}); + # my $dt_epoch = DateTime->from_epoch(epoch => $ctime); + # my $duration = $dt_epoch->delta_ms(DateTime->now); + # # say $duration->minutes; + # if ($duration->minutes > 30) { + # unlink $config{'COOKIE_PATH'}; + # } + # else { + # say "Loading $config{'COOKIE_PATH'}..."; + # $cookie_jar->load($config{'COOKIE_PATH'}); + # } + # } + + # BROWSER + &newBrowser($cookie_jar); + # $mech = WWW::Mechanize->new( + # cookie_jar => $cookie_jar, + # agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + # autocheck => 0, + # ); + # use LWP::UserAgent; + # $ua = LWP::UserAgent->new( + # ssl_opts => { + # SSL_version => 'TLSv12:!SSLv2:!SSLv3:!TLSv1:!TLSv11', + # }, + # cookie_jar => $cookie_jar, + # agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + # ); + + # EMPTY DIRECTORIES + # unlink glob "'$config{'CAPTCHA_DIR'}/*'"; + unlink glob "'$config{'OUTPUT_DIR'}/*'"; + # unlink glob "'$config{'COOKIE_DIR'}/*'"; + + # CLEAN OLD DATETIMES, FLAG EXPIRED REQUESTS + &db_query("DELETE FROM `datetime` WHERE `DATETIME` < NOW();", $connection, 'do'); + &db_query("UPDATE request SET `EXPIRED` = 1 WHERE `DATETIME_TO` < NOW();", $connection, 'do'); + + # FORK MANAGER + $pm = Parallel::ForkManager->new(scalar(keys(%services))); + # $pm = Parallel::ForkManager->new(1); + $pm->run_on_start( + sub { + my ($pid, $ident)=@_; + say "\n** $ident started, pid: $pid"; + }); + $pm->run_on_finish( + sub { + my ($pid, $exit_code, $ident) = @_; + say "\n** $ident just got out of the pool with PID $pid and exit code: $exit_code"; + }); + $pm->run_on_wait( + sub { + # my ($pid, $ident) = @_; + say "\n** a child is waiting now..."; + }); + + # DISCONNECT DB + # $connection->disconnect(); +} + +sub newBrowser { + &Delimiter((caller(0))[3]); + my $cookie_jar = shift; + + # BROWSER + $mech = WWW::Mechanize->new( + cookie_jar => $cookie_jar, + agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + autocheck => 0, + ); +} + +sub Summary { + &Delimiter((caller(0))[3]); + + &dumpToFile($config{'OUTPUT_PATH'}, \%data) if %data; + &dumpToFile("$config{'OUTPUT_DIR'}/venues", \%venues) if %venues; + &dumpToFile("$config{'OUTPUT_DIR'}/services", \%services) if %services; + # $connection->disconnect(); +} + +sub getTerminDay { + &Delimiter((caller(0))[3]); + my ($url, $service_id, $switch) = @_; + $switch = '' if !$switch; + + my $response = &responseHandlerMech($url); + + if (!$response->{'success'} && $response->{'restart'}) { + &getTerminDay($url, $service_id, $switch); + } + elsif ($response->{'success'}) { + my $success = &parseTerminDay($response->{'tree'}, $service_id, $switch, $response); + + if ($success && $switch eq 'getRegisterControls') { + say 'No need to go next. Returning...'; + return 1; + } + + if ($response->{'tree'}->exists('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')) { + say 'NEXT EXISTS'; + if ($config{'FOLLOW_NEXT'}) { + my $epochtime = basename($response->{'tree'}->findvalue('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')); + my $url = "$config{'URL_TERMIN_DAY'}/$epochtime"; + &getTerminDay($url, $service_id, $switch); + } + } + } +} + +sub parseTerminDay { + &Delimiter((caller(0))[3]); + my ($tree, $service_id, $switch, $response) = @_; + + my $xpath = '//div[contains(concat(" ", normalize-space(@class), " "), " calendar-month-table ")]'; + if ($tree->exists($xpath)) { + foreach my $table ($tree->findnodes($xpath)) { + my ($month, $year) = split(/ /, $table->findvalue('./table/thead/tr/th[contains(concat(" ", normalize-space(@class), " "), " month ")]')); + # FIX MONTH UMLAUT: CONVERT TO NUMBER + $month=Encode::decode_utf8($month); + $month = $config{'MON2NUM'}{$month}; + + foreach my $buchbar ($table->findnodes('./table/tbody/tr/td[contains(concat(" ", normalize-space(@class), " "), " buchbar ")]')) { + my $id = basename($buchbar->findvalue('./a/@href')); + + if ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}) { + say 'ALREADY PARSED'; + next; + } + else { + my $day = $buchbar->findvalue('./a'); + + $data{'SERVICES'}{$service_id}{'SERVICE_ID'} = $service_id; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} = $month; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} = $year; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} = $day; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'ID'} = $id; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIME'} = &createMysqlDateTime("$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} 00:00"); + + my $url = "$config{'URL_TERMIN_TIME'}/$id"; + &getTerminTime($url, $id, $service_id); + + if ($switch eq 'getRegisterControls') { + if (scalar keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}} > 0) { + say 'Successfully parsed at least 1 appointment. Returning...'; + return 1; + } + } + } + } + + my $days_avail = scalar keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}; + say "DAYS AVAILABLE: $days_avail"; + if ($days_avail >= 1) { + &sync_find_book_appointment($service_id); + } + + } + # return 1; + } + else { + say 'Parsing error.'; + &dumpToFile('./output/parsingerror_parseTerminDay', $response->{'mech'}->content); + die; + } +} + +sub sync_find_book_appointment { + &Delimiter((caller(0))[3]); + my ($service_id) = @_; + + # SYNC + # Why delete here? Results in link not found db error + # &db_query("DELETE FROM `mapping` WHERE `SERVICE_ID` = '$service_id';", $connection, 'do'); + + &db_sync($service_id); + + # FIND & BOOK + &db_find_appointment($request{$service_id}, $service_id); # FIND APPOINTMENT ONLY FOR THE SERVICE WORKER IS HANDLING +} + +sub getTerminTime { + &Delimiter((caller(0))[3]); + my ($url, $id, $service_id, $switch) = @_; + + my $response = &responseHandlerMech($url); + + if (!$response->{'success'} && $response->{'restart'}) { + &getTerminTime($url, $id, $service_id, $switch); + } + elsif ($response->{'success'}) { + my $success = &parseTerminTime($response->{'tree'}, $id, $service_id); + if (!$success) { + # say $mech->content(); + &dumpToFile('./output/!success_parseTerminTime', $mech->content); + say 'No success in parseTerminTime. Just continue?'; + # &getTerminTime($url, $id, $service_id); # RETRY + } + } +} + +sub rateLimitHandler { + &Delimiter((caller(0))[3]); + my ($tree, $url) = @_; + + my $error = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " offset2 ")]/h1'); + say $error; + + if ($error eq 'Zu viele Zugriffe') { + say 'Rate Limit detected.'; + &sleepCountdown($config{'RATE_LIMIT_SLEEP_MIN'} * 60); + # my $response = &responseHandlerMech($url); + return 1; + } + else { + say 'Could not resolve error in rateLimitHandler'; + die; + } +} + +sub sleepCountdown { + &Delimiter((caller(0))[3]); + my $countdown = shift; + + $|++; + for (my $i=$countdown; $i >= 0; $i--) { + printf "Sleeping for %s seconds...\r", $i; + sleep(1); + } + return 1; +} + +sub captchaHandler { + &Delimiter((caller(0))[3]); + my $tree = shift; + + my $captchaBase64 = $tree->findvalue('//fieldset[contains(concat(" ", normalize-space(@class), " "), " well ")]/p/img/@src'); + my $filename = &base64ToFile($captchaBase64); + # system("cacaview $filename"); + system("tiv -0 $filename"); + + say 'Please solve captcha ' . basename($filename) . ' to proceed: '; + my $captcha_text = ; + chomp $captcha_text; + my $urlCaptcha = "$config{'URL_HUMAN'}/?captcha_text=$captcha_text"; + + # my $response = &responseHandlerMech($urlCaptcha); + my $response = $mech->get($urlCaptcha); + if ($mech->success) { + say 'Captcha successfully solved. Saving cookie...'; + + if ($service_id_fork) { + say "$config{'COOKIE_DIR'}/$service_id_fork"; + $cookie_jar->save("$config{'COOKIE_DIR'}/$service_id_fork"); + } + else { + $cookie_jar->save($config{'COOKIE_PATH'}); + } + return 1; + } + elsif ($mech->status == 428) { + say 'Captcha not successfully solved. Try again...'; + &dumpToFile('./output/captchaNotSuccessfullySolved', $mech->content); + return 0; + } +} + +sub trim { + my $string = shift; + $string =~ s/^\s+|\s+$//g; + return $string; +} + +sub parseTerminTime { + &Delimiter((caller(0))[3]); + my ($tree, $id, $service_id) = @_; + + my $xpath = '//div[contains(concat(" ", normalize-space(@class), " "), " timetable ")]/table/tbody/tr'; + my $xpath_stop = '//link[contains(concat(" ", normalize-space(@rel), " "), " alternate ")]/@href'; + + if ($tree->exists($xpath)) { + my $time; + foreach my $buergeramt_dataset ($tree->findnodes($xpath)) { + if ($buergeramt_dataset->findvalue('./th[1]')) { + $time = trim($buergeramt_dataset->findvalue('./th[1]')); + $time = &createMysqlDateTime("$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} $time"); + # print "Parsing for $time..."; + } + foreach my $buergeramt ($buergeramt_dataset->findnodes('./td[contains(concat(" ", normalize-space(@class), " "), " frei ")]')) { + my $name = trim($buergeramt->findvalue('./a')); + + if ($name eq 'Diesen Termin buchen' && defined $data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'} && scalar @{$data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'}} == 1) { + $name = $data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'}[0][1]; + # say "Single venue: Correct name: $name"; + } + + my $href = $buergeramt->findvalue('./a/@href'); + say $time; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'VENUE'} = $name; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'HREF'} = "$config{'URL_ROOT'}$href"; + # say $name . ' needs match: ' . grep{ $venues{$_}{'NAME'} eq $name} keys %venues; + ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'VENUE_ID'}) = grep{ $venues{$_}{'NAME'} eq $name} keys %venues; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'EPOCHTIME'} = DateTime::Format::DateParse->parse_datetime($time)->epoch(); + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'SYNCED'} = 0; + } + } + return 1; + } + elsif ($tree->findvalue($xpath_stop) eq '/terminvereinbarung/termin/stop/') { # CAN BE REMOVED BECAUSE OF HANDLER? + say "XPATH_STOP DETECTED"; + } + else { + say 'Parsing error.'; + return 0; + } +} + +sub base64ToFile { + &Delimiter((caller(0))[3]); + my $base64 = shift; + + my $timestamp = &GetTimestamp('YMDHMS'); + $base64 =~ s/data:image\/(.+?)\;base64,//; + my $decoded= decode_base64($base64); + my $filename = "$config{'CAPTCHA_DIR'}/$timestamp.png"; + open my $fh, '>', $filename or die $!; + binmode $fh; + print $fh $decoded; + close $fh; + return $filename; +} + +sub Delimiter { + my $SubName = shift; + print "\n" . "-" x 80 . "\n"; + print "> " . $SubName; + print " | CHILD: $service_id_fork" if ($service_id_fork); + print "\n" . '-' x 80 . "\n"; +} + +sub GetTimestamp { + #&Delimiter((caller(0))[3]); + my $switch = shift; + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time); + + my $nice_timestamp; + if ($switch eq 'YMDHMS') { + $nice_timestamp = sprintf ( "%04d%02d%02d_%02d%02d%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec); + } + elsif ($switch eq 'YMD') { + $nice_timestamp = sprintf ( "%04d%02d%02d", $year+1900,$mon+1,$mday); + } + elsif ($switch eq 'year') { + $nice_timestamp = $year+1900; + } + elsif ($switch eq 'month') { + $nice_timestamp = $mon+10; + } + else { + print "Invalid/no switch detected\n"; + } + + return $nice_timestamp; +} \ No newline at end of file diff --git a/src/server/service.berlin.de_iteration.pl b/src/server/service.berlin.de_iteration.pl new file mode 100755 index 0000000..afe39eb --- /dev/null +++ b/src/server/service.berlin.de_iteration.pl @@ -0,0 +1,1551 @@ +#!usr/bin/perl +use strict; +use warnings; +no warnings 'redefine'; +# use diagnostics; +use Data::Dumper; +use HTML::TreeBuilder::XPath; +use feature qw(say); +use File::Basename; +use HTTP::Cookies; +use MIME::Base64 qw(decode_base64); +use Encode; +use utf8; +use DateTime::Format::Strptime; +use DateTime::Format::MySQL; +use DBI; +use DBD::MariaDB; +use DateTime::Format::DateParse; +use autodie qw(:all); +use Parallel::ForkManager; +use DBIx::Connector; +use WWW::Mechanize (); +# use Storable; +# use Data::Printer; +# use DateTime; +# use HTML::Entities; +# binmode STDOUT, ':encoding(utf-8)'; +# binmode STDIN, ':encoding(utf-8)'; +# use open ':std', ':encoding(UTF-8)'; + +my ($zsmappointment, $ts, $mech, $ua, $cookie_jar, $strp, %data, %services, %venues, $connection, $strp_datetime, $pm); + +my %config = ( + 'MYSQL' => { + 'ACCESS' => { + 'database' => 'terminsnipe', + 'hostname' => 'zinomedia.de', + 'user' => 'terminsnipe', + 'pw' => 'dIHB6JLk0CBBx1p7', + 'port' => 3306, + }, + 'OPTIONS' => { + 'PrintError' => 0, + 'RaiseError' => 1, + 'AutoCommit' => 1, + } + }, + 'URL_ROOT' => 'https://service.berlin.de', + 'URL_TERMIN_DAY' => 'https://service.berlin.de/terminvereinbarung/termin/day', + 'URL_TERMIN_TIME' => 'https://service.berlin.de/terminvereinbarung/termin/time', + 'URL_HUMAN' => 'https://service.berlin.de/terminvereinbarung/termin/human', + 'URL_STANDORTE' => 'https://service.berlin.de/standorte', + 'URL_DIENSTLEISTUNGEN' => 'https://service.berlin.de/dienstleistungen', + 'CAPTCHA_DIR' => './captcha', + 'COOKIE_DIR' => './cookie', + 'COOKIE_PATH' => './cookie/.cookies.txt', + 'FOLLOW_NEXT' => 1, + 'OUTPUT_PATH' => './output/data', + 'FILTER_SERVICES_TO_OUTPUT' => 1, + 'FILTER_SERVICES_PATH' => './output/filter_services', + 'DB_REFRESH_SERVICES' => 0, + 'RUN_SERVICES' => 0, + 'RUN_VENUES' => 0, + 'VENUES_TO_FILE' => 0, + 'VENUES_PATH' => './output/venues', + 'DB_REFRESH_VENUES' => 0, + 'DUMP_SERVICES_TO_FILE' => 1, + 'SERVICES_PATH' => './output/services', + 'OUTPUT_DIR' => './output', + 'DB_CLEAN_DATETIMES' => 1, + 'RATE_LIMIT_SLEEP_MIN' => 5, + 'SLEEP_LOOP_MIN' => 1, + 'SERVICES' => { + 'Melderegisterauskunft sperren' => { 'RUN' => 0, 'ID' => 120678 }, + 'Erweiterung einer Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121629 }, + 'Ersatzführerschein nach Verlust/Diebstahl' => { 'RUN' => 0, 'ID' => 121593 }, + 'Kinderreisepass beantragen / verlängern / aktualisieren' => { 'RUN' => 0, 'ID' => 121469 }, + 'Namensänderung in den Fahrzeugpapieren' => { 'RUN' => 0, 'ID' => 324173 }, + 'Änderung/Wechsel der Hauptwohnung' => { 'RUN' => 0, 'ID' => 120697 }, + 'Umschreibung einer ausländischen Fahrerlaubnis aus einem Nicht-EU/EWR-Land (Drittstaat/Anlage 11)' => { 'RUN' => 0, 'ID' => 327537 }, + 'Historisches Kennzeichen beantragen' => { 'RUN' => 0, 'ID' => 121478 }, + 'Verlängerung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121634 }, + 'Adressänderung in der Zulassungsbescheinigung Teil I (ZBI) bzw. in dem Fahrzeugschein' => { 'RUN' => 0, 'ID' => 120658 }, + 'Blaue Karte EU zu einem neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 326798 }, + 'Neufahrzeug anmelden' => { 'RUN' => 0, 'ID' => 120882 }, + 'Zulassung eines aus dem EU-Ausland eingeführten fabrikneuen Fahrzeuges mit Zulassungsbescheinigung Teil II' => { 'RUN' => 0, 'ID' => 324199 }, + 'Führungszeugnis' => { 'RUN' => 0, 'ID' => 120926 }, + 'Aufenthaltserlaubnis für im Bundesgebiet geborene Kinder - Erteilung' => { 'RUN' => 0, 'ID' => 324269 }, + 'Kraftfahrzeug – technische Änderung melden' => { 'RUN' => 0, 'ID' => 120904 }, + 'Reisepass beantragen' => { 'RUN' => 0, 'ID' => 121151 }, + 'Meldebescheinigung beantragen' => { 'RUN' => 0, 'ID' => 120702 }, + 'Umtausch eines Kartenführerscheins' => { 'RUN' => 0, 'ID' => 121616 }, + 'Umschreibung einer Dienstfahrerlaubnis' => { 'RUN' => 0, 'ID' => 121615 }, + 'Bescheinigung über ein unbefristetes Aufenthaltsrecht' => { 'RUN' => 0, 'ID' => 324921 }, + 'Elektrokennzeichen beantragen' => { 'RUN' => 0, 'ID' => 327084 }, + 'Abmeldung einer Wohnung' => { 'RUN' => 0, 'ID' => 120335 }, + 'Zulassungsbescheinigung Teil II für KFZ wegen Verlust oder Diebstahl ersetzen' => { 'RUN' => 0, 'ID' => 121584 }, + 'Beglaubigung von Unterschriften' => { 'RUN' => 0, 'ID' => 158142 }, + 'Kraftfahrzeugkennzeichen wegen Unleserlichkeit ersetzen' => { 'RUN' => 0, 'ID' => 324907 }, + 'Zulassung von Taxen (Kraftdroschken)' => { 'RUN' => 0, 'ID' => 326033 }, + 'Fahrerlaubnis - Erweiterung auf die Klassen D1, D1E, D und DE' => { 'RUN' => 0, 'ID' => 324450 }, + 'Wiederzulassung eines Kraftfahrzeuges ohne Halterwechsel beantragen' => { 'RUN' => 0, 'ID' => 120905 }, + 'Ersterteilung einer Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121627 }, + 'Beglaubigung von Kopien' => { 'RUN' => 0, 'ID' => 121701 }, + 'Saisonkennzeichen für KFZ beantragen' => { 'RUN' => 0, 'ID' => 121480 }, + 'Anmeldung einer Wohnung' => { 'RUN' => 0, 'ID' => 120686 }, + 'Zulassung eines aus dem EU-Ausland eingeführten fabrikneuen Fahrzeuges ohne Zulassungsbescheinigung Teil II' => { 'RUN' => 0, 'ID' => 324200 }, + 'Kraftfahrzeug ummelden – nach einem Umzug nach Berlin' => { 'RUN' => 0, 'ID' => 120918 }, + 'Umschreibung eines zugelassenen Fahrzeuges mit Berliner Kennzeichen auf einen anderen Halter' => { 'RUN' => 0, 'ID' => 120906 }, + 'Reisepass beantragen (vorläufiger Reisepass)' => { 'RUN' => 0, 'ID' => 121153 }, + 'Kraftfahrzeug außer Betrieb setzen (Kfz abmelden)' => { 'RUN' => 0, 'ID' => 120877 }, + 'Umstellung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 124556 }, + 'Widerspruchsrechte gegen Datenübermittlungen und Melderegisterauskünfte' => { 'RUN' => 0, 'ID' => 319141 }, + 'Gewerbezentralregister - Auskunft beantragen' => { 'RUN' => 0, 'ID' => 327835 }, + 'Wechselkennzeichen beantragen' => { 'RUN' => 0, 'ID' => 324587 }, + 'Personalausweis vorläufig beantragen' => { 'RUN' => 0, 'ID' => 120682 }, + 'Personalausweis beantragen' => { 'RUN' => 0, 'ID' => 120703 }, + 'Kraftfahrzeugkennzeichen ersetzen – nach Diebstahl oder Verlust eines Nummernschildes' => { 'RUN' => 0, 'ID' => 324196 }, + 'Umschreibung einer ausländischen Fahrerlaubnis aus einem EU-/EWR-Staat' => { 'RUN' => 0, 'ID' => 121598 }, + 'Selbstfahrermietfahrzeug – Beginn/Beendigung der Nutzung' => { 'RUN' => 0, 'ID' => 326039 }, + 'Zulassungsbescheinigung Teil I für KFZ wegen Verlust oder Diebstahl ersetzen' => { 'RUN' => 0, 'ID' => 121575 }, + 'Kraftfahrzeugkennzeichen auf Wunsch ändern' => { 'RUN' => 0, 'ID' => 121473 }, + 'Begleitetes Fahren mit 17' => { 'RUN' => 0, 'ID' => 121589 }, + 'Fahrerlaubnis zur Fahrgastbeförderung (P-Schein) - Verlängerung' => { 'RUN' => 0, 'ID' => 324389 }, + 'Zulassung eines außer Betrieb gesetzten Fahrzeuges mit Berliner Kennzeichen auf einen anderen Halter' => { 'RUN' => 0, 'ID' => 324169 }, + 'Befreiung von der Ausweispflicht' => { 'RUN' => 0, 'ID' => 327044 }, + 'Grünes Kennzeichen beantragen' => { 'RUN' => 0, 'ID' => 121482 }, + 'Aufenthaltserlaubnis in einen neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 121874 }, + 'Kraftfahrzeug außer Betrieb setzen, unvollständige Unterlagen (Kfz abmelden)' => { 'RUN' => 0, 'ID' => 325881 }, + 'Führerschein: Ausstellung – Internationaler Führerschein' => { 'RUN' => 0, 'ID' => 121591 }, + 'Neuerteilung der Fahrerlaubnis' => { 'RUN' => 0, 'ID' => 121637 }, + 'Niederlassungserlaubnis oder Erlaubnis zum Daueraufenthalt-EU in einen neuen Pass übertragen' => { 'RUN' => 0, 'ID' => 324280 }, + 'Fahrerlaubnis zur Fahrgastbeförderung (P-Schein) - Erteilung' => { 'RUN' => 0, 'ID' => 121622 }, + } +); + + +# # INIT +# &init(); +# &submitRegister(); +# &getRegister('', 2); +# &getRegisterControls(); +# db_createRequestDienstleister(24); +# &getStandorte(); +# &getDienstleistungen(); +# die; + +# # MAIN +# &getStandorte() if $config{'RUN_VENUES'}; +# &getDienstleistungen() if $config{'RUN_SERVICES'}; +# # &run(); +# &db_request(); +# &Summary(); + + + +&programLoop(); + +# SUBS + +sub responseHandlerMech { + # &Delimiter((caller(0))[3]); + my $url = shift; + + print "\nGETTING $url... "; + + $mech->get($url); + say $mech->status(); + + # my $tree = HTML::TreeBuilder::XPath->new_from_content($mech->content()); + my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + + my ($success, $code) = &checkSuccess($tree, $mech); # returns 1 or 0 for $success and error/success code for $code + if (!$success && !$code) { + &dumpToFile('./output/successcodeundefined', $mech->content); + die '$success and $code undefined'; + } + # say "success: $success\tcode: $code"; + + if ($mech->success()) { + if ($code == -6) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Starting new appointment search...'; + my $success = &restartHandler($tree, $code, $mech); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restartHandler_success' => 1, 'restart' => 1 } if $success; + # return $response if $response->code == 200; + # &responseHandlerMech($url) if $response->code != 200; + } + elsif ($code == -1) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Starting new appointment search...'; + my $success = &restartHandler($tree, $code, $mech); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restartHandler_success' => 1, 'restart' => 1 } if $success; + # return $response if $response->code == 200; + # &responseHandlerMech($url) if $response->code != 200; + } + elsif ($code == -9) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Timeout after 30 minutes. What to do here? Renewing cookie...'; + getCookie(); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restart' => 1 }; + } + elsif ($code == -2) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : XPATH_STOP detected: Restarting.'; + &getCookie(); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'restart' => 1 }; + } + else { + return { 'success' => 1, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + } + else { + if ($mech->status == 404) { + &dumpToFile('./output/404', $mech->content); + if ($code == -8) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Error occrured, retrying...'; + &responseHandlerMech($url); # RETRY + } + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + # return { 'success' => 1, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + elsif ($mech->status == 429) { + if ($code == -12) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Too many requests...'; + &dumpToFile('./output/429', $mech->content); + my $success = &rateLimitHandler($tree, $url); + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'rateLimit_success' => 1, 'restart' => 1 } if $success; + } + } + elsif ($mech->status == 428) { + if ($code == -5 || $code == -7) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Renewing captcha...'; + my $response = &captchaHandler($tree); + &responseHandlerMech($url) if !$response; # CAPTCHA NOT SOLVED SUCCESSFULLY, RETRY + return { 'success' => 0, 'tree' => $tree, 'status' => $mech->status, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code, 'captcha_success' => 1, 'restart' => 1 }; + } + } + elsif ($mech->status == 500) { + if ($code == -10) { + say 'Mech->status ' . $mech->status . ' and success_checkSuccess ' . $code . ' : Site currently in maintenance mode.'; + die; + } + } + else { + say 'Mech->status ' . $mech->status . ' not handled yet.'; + die; + # return { 'success' => 0, 'status' => $mech->status, 'tree' => $tree, 'mech' => $mech, 'success_checkSuccess' => $success, 'code_checkSuccess' => $code }; + } + } +} + +sub checkSuccess { + &Delimiter((caller(0))[3]); + my ($tree, $mech) = @_; + + my $xpath_alert = '//div[contains(concat(" ", normalize-space(@class), " "), " alert")]/p[1]'; + my $xpath_alert2 = '//div[contains(concat(" ", normalize-space(@class), " "), " alert")]/div/p'; + my $xpath_alert3 = '//div[@class="zms"]/div[@class="html5-header header"]/h1[@class="title"]'; + my $xpath_alert4 = '//div[@class="offset2 span7"]/h2'; + my $xpath_alert5 = '//div[contains(concat(" ", normalize-space(@class), " "), " offset2 ")]/h1'; + + my $xpath_success = '//div[@class="submit-success-message"]'; + + if ($tree->exists($xpath_alert)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert))); + say "Alert: $alert"; + + if ($alert eq 'Sie haben schon einen Termin reserviert. Soll diese Reservierung gelöscht werden oder möchten Sie mit der vorhandenen Reservierung fortfahren?') { + say 'Code -1: Another reservation is already in progress.'; + &dumpToFile('./output/-1', $mech->content); + return (0, -1); + } + elsif ($alert eq 'Zu ihrer Suche konnten keine Daten ermittelt werden. Dies kann unterschiedliche Gründe haben:') { + say 'Code -2: Cookie seems outdated'; + &dumpToFile('./output/-2', $mech->content); + return (0, -2); + } + elsif ($alert eq 'Sie haben Ihre Terminvereinbarung abgeschlossen. Sie können nun eine neue Terminssuche starten!') { + say 'Code -3: Reservation already completed'; + &dumpToFile('./output/-3', $mech->content); + return (0, -3); + } + elsif ($alert eq 'Ihr Termin ist eingetragen worden. Bitte drucken Sie diese Seite aus, damit Sie alle Informationen zur Hand haben. Wichtig für Ihren Besuch ist die Vorlage Ihrer Vorgangsnummer.') { + say 'Code 1: submitRegister successfull.'; + &dumpToFile('./output/1', $mech->content); + say trim($tree->findvalue($xpath_success)); + say $tree->findvalue('//span[@class="summary_authKey"]'); + say $tree->findvalue('//span[contains(concat(" ", normalize-space(@class), " "), " summary_processId ")]'); + return (1, 1); + } + elsif ($alert eq 'Der eingegebene Text stimmte nicht mit dem Bild überein. Versuchen Sie es noch einmal.') { + say 'Code -5: Text / Picture mismatch'; + &dumpToFile('./output/-5', $mech->content); + return (0, -5); + } + elsif ($alert eq 'Ihre Auswahl von Standort und Diensteistung hat sich geändert. Möchten Sie mit Ihrer vorherigen Suche fortfahren oder eine neue Terminsuche starten?') { + say 'Code -6: Location and venue changed. New search or continue.'; + &dumpToFile('./output/-6', $mech->content); + return (0, -6); + } + elsif ($alert eq 'Ihre Vorherige Reservierung wird mit der Auswahl eines neuen Termins entfernt. Fahren Sie nur fort, wenn Sie Ihren bisherigen Termin wirklich ändern wollen.') { + say 'Code -11: Old appointment removed for new one.'; + &dumpToFile('./output/-11', $mech->content); + return (0, -11); + } + elsif ($alert eq 'Bei der Eingabe der Daten scheint Ihnen ein Fehler unterlaufen zu sein. Bitte prüfen Sie die Hinweise an den markierten Formularfeldern.') { + say 'Code -13: Form input error'; + &dumpToFile('./output/-13', $mech->content); + return (0, -13); + } + else { + # say $mech->content(); + say 'NO XPATH SPECIFIED DETECTED'; + say $alert; + &dumpToFile('./output/noxpathspecified', $mech->content); + # my $string = trim($tree->findvalue($xpath_alert)); + + # $string = decode('utf-8', $string); + # say $string; + # say utf8::is_utf8($string); # since Perl 5.8.1 + # say utf8::valid($string); + die; + } + + } + elsif ($tree->exists($xpath_alert2)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert2))); + say "Alert2: $alert"; + + if ($alert eq 'Leider konnte der ausgewählte Termin nicht reserviert werden. Da war jemand schneller als Sie. Bitte versuchen Sie es erneut.') { + say 'Code -4: Appointment not available anymore and booked by another person.'; + &dumpToFile('./output/-4', $mech->content); + return (0, -4); + } + } + elsif ($tree->exists($xpath_alert3)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert3))); + say "Alert 3: $alert"; + + if ($alert eq 'Bitte verifizieren sie sich') { + say 'Code -7: Verification needed.'; + &dumpToFile('./output/-7', $mech->content); + return (0, -7); + } + elsif ($alert eq 'Terminvereinbarung') { + say 'Code 3: Normal Terminvereinbarung'; + &dumpToFile('./output/-7', $mech->content); + return (1, 3); + } + elsif ($alert eq 'Bitte entschuldigen Sie den Fehler') { + say 'Code -8: An error has occured'; + &dumpToFile('./output/-8', $mech->content); + return (1, -8); + } + elsif ($alert eq 'Zeitüberschreitung: Bitte versuchen Sie es erneut') { + say 'Code -9: Timeout: Request older than 30 minutes.'; + &dumpToFile('./output/-9', $mech->content); + return (1, -9); + } + + } + elsif ($tree->exists($xpath_alert4)) { + my $alert = decode('utf-8', trim($tree->findvalue($xpath_alert4))); + say "Alert 4: $alert"; + + if ($alert eq 'Die Terminvereinbarung ist zur Zeit nicht möglich.') { + say 'Code -10: Maintenance mode.'; + &dumpToFile('./output/-10', $mech->content); + return (0, -10); + } + elsif ($alert eq 'Service-Portal Berlin' && $tree->findvalue($xpath_alert5) eq 'Zu viele Zugriffe') { + say 'Code -12: Too many requests.'; + &dumpToFile('./output/-12', $mech->content); + return (0, -12); + } + } + else { + say 'Code 2: NO XPATH SPECIFIED DETECTED. Everything fine?'; + &dumpToFile('./output/2', $mech->content); + return (1, 2); + } + +} + +sub restartHandler { + &Delimiter((caller(0))[3]); + my ($tree, $code, $mech) = @_; + + my $url; + if ($code == -6) { + $url = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " alert alert-error noprint textile ")]/ul/li[2]/a/@href'); + } + elsif ($code == -1) { + $url = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " alert alert-info noprint textile ")]/ul/li[2]/a/@href'); + } + $url = $config{'URL_ROOT'} . $url; + say "URL: $url"; + + my $response = $mech->get($url); + say $response->code; + if ($mech->success) { + say "Restart with code $code successfull."; + return 1; + } + else { + say "Restart with code $code unsuccessfull."; + &dumpToFile('./output/restartunsuccessfull', $mech->content); + &responseHandlerMech($url); # LET MECH HANDLE RESPONSE + # print Dumper $response; + # return 0; + # die; + } +} + +sub getRegister { + &Delimiter((caller(0))[3]); + my ($dataset_ref, $user_id) = @_; + + my $response_user = &db_query("SELECT * FROM `reg_users` WHERE `ID` = $user_id", $connection, 'selectall_hashref', 'ID'); + + my $url = $$dataset_ref{'HREF'}; + my $response = &responseHandlerMech($url); + # my $response = &responseHandlerMech('https://zinomedia.de/dl/customHTML2.html'); + + if (!$response->{'success'} && $response->{'restart'}) { + &getRegister($dataset_ref, $user_id); + } + elsif (!$response->{'success'}) { + return 0; + } + elsif ($response->{'success'}) { + &submitRegister($mech, $$response_user{$user_id}); + return 1; + } +} + +sub getRegisterControls { + &Delimiter((caller(0))[3]); + my $switch = shift; + $switch = '' if !$switch; + + use Data::Structure::Util qw( unbless ); + my %controls; + + + + # my $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/tag.php?termin=1&dienstleister=122210&anliegen[]=120335&herkunft=1'); # ABmeldung einer Wohnung Halemweg + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579561200/'); # Termin Zeiten + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579260240/141/'); # Terminbuchung + + # say 'SWITCHING SERVICE NOW'; + + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/tag.php?termin=1&dienstleister=122217&anliegen[]=120335&herkunft=1'); # ABmeldung einer Wohnung Heerstraße + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579215600/'); # Termin Zeiten + # $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/time/1579244400/141/'); # Terminbuchung + + # die; + + + for my $service (sort keys %{$config{'SERVICES'}}) { + my $service_id = $config{'SERVICES'}{$service}{'ID'}; + say "\nSTART GETTING CONTROLS FOR SERVICE '$service'..."; + my $href = "https://service.berlin.de/dienstleistung/$service_id/"; + + my $response = &responseHandlerMech($href); + if ($response->{'success'}) { + say 'success'; + + my @nodes = $response->{'tree'}->findnodes('//div[@class="behoerdenitem"]/div[@class="row"]'); + say $#nodes; + # print Dumper $nodes; + + foreach my $node (sort @nodes) { + if ($node->exists('./div[@class="span5" and (strong)]') || !$node->exists('./div[@class="span2"]/p/strong/a/@href')) { + next; + } + my $venue = $node->findvalue('./div[@class="span5"]'); + my $terminvereinbarung_url = $node->findvalue('./div[@class="span2"]/p/strong/a/@href'); + my $last = 0; + + say "$venue => $terminvereinbarung_url"; + + my $success = &getTerminDay($terminvereinbarung_url, $service_id, 'getRegisterControls', 0); + return if $switch eq 'getCookie'; + if ($success) { + say 'We have appointments.'; + print Dumper $data{'SERVICES'}{$service_id}{'AVAILABLE'}; + + foreach my $epochtime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}) { + foreach my $datetime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$epochtime}{'DATETIMES'}}) { + my $register_url = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$epochtime}{'DATETIMES'}{$datetime}{'HREF'}; + say $datetime; + say $register_url; + + my $response = &responseHandlerMech($register_url); + if ($response->{'success_checkSuccess'}) { + my $form = $mech->form_number(2); + my @inputs = $mech->find_all_inputs( + # type => 'textarea', + # name_regex => qr/^customer/, + ); + + my $url = $mech->uri()->as_string; + + foreach my $i (0 .. $#inputs) { + # my $digest; + my $contains_pflichtfeld = 0; + foreach my $key2 (sort keys %{$inputs[$i]}) { + # print "KEY $key VALUE "; + # say $inputs[0]{$key}; + # $digest .= "$key$inputs[$i]{$key}"; + $contains_pflichtfeld = 1 if $inputs[$i]{$key2} =~ m/\*$/; + } + + my $key; + if ($inputs[$i]{'name'}) { + $key = 'name'; + } + elsif ($inputs[$i]{'id'}) { + $key = 'id'; + } + else { + print Dumper $inputs[$i]; + die; + } + + # $digest = md5_hex($digest); + # if (!$controls{$digest}) { + # # $controls{$digest}{'HASH'} = unbless(\%{$inputs[$i]}); + # $controls{$digest}{'HASH'} = \%{$inputs[$i]}; + # $controls{$digest}{'FIRST_LVL_PFLICHTFELD'} = $contains_pflichtfeld; + # $controls{$digest}{'URI'}{$url} = 1; + # push(@{$controls{$digest}{'VENUES'}}, "$venue => $service"); + # } + # else { + # push(@{$controls{$digest}{'VENUES'}}, "$venue => $service"); + # } + + + if (!$controls{$key}) { + $controls{$inputs[$i]{$key}}{'HASH'} = unbless(\%{$inputs[$i]}); + # $controls{$digest}{'HASH'} = \%{$inputs[$i]}; + $controls{$inputs[$i]{$key}}{'FIRST_LVL_PFLICHTFELD'} = $contains_pflichtfeld; + $controls{$inputs[$i]{$key}}{'URI'}{$url} = 1; + push(@{$controls{$inputs[$i]{$key}}{'VENUES'}}, "$venue => $service"); + } + else { + push(@{$controls{$inputs[$i]{$key}}{'VENUES'}}, "$venue => $service"); + } + + } + + # say 'Aborting now...'; + # my $response = &responseHandlerMech('https://service.berlin.de/terminvereinbarung/termin/abort/'); + # sleep(5); + undef %data; + $last = 1; + last if $last; + } + + # print Dumper \%controls; + # die; + last if $last; + } + last if $last; + } + + } + + # print Dumper \%controls; + # die; + } + } + &dumpToFile('./output/controls', \%controls); + print Dumper \%controls; + die; + } +} + +sub submitRegister { + &Delimiter((caller(0))[3]); + my ($mech, $dataset_ref) = @_; + + my $form = $mech->form_number(2); + my @inputs = $mech->find_all_inputs(); + + my $exist_surveyAccepted = 0; + my $exist_telephone = 0; + # print Dumper \@inputs; + for my $i (0 .. $#inputs) { + $exist_surveyAccepted = 1 if defined $inputs[$i]{'name'} && $inputs[$i]{'name'} eq 'surveyAccepted'; + $exist_telephone = 1 if defined $inputs[$i]{'name'} && $inputs[$i]{'name'} eq 'telephone'; + } + # say $exist_surveyAccepted; + + my $familyName = "$$dataset_ref{'FIRST_NAME'} $$dataset_ref{'LAST_NAME'}"; + my $email = $$dataset_ref{'EMAIL'}; + my $telephone = $$dataset_ref{'PHONE'}; + # say $familyName; + # say $email; + # die; + + $mech->set_fields( 'familyName' => $familyName, 'email' => $email, ); + $mech->set_fields( 'telephone' => $telephone ) if $exist_telephone; + $mech->tick('agbgelesen', 1); + $mech->select('surveyAccepted', '0') if $exist_surveyAccepted; + $mech->click_button(id => 'register_submit'); + + + # my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + say $mech->status(); + if ($mech->success()) { + &dumpToFile('./output/successSubmitRegister', $mech->content); + my $tree = HTML::TreeBuilder::XPath->new_from_content(encode('utf-8', $mech->content())); + + my ($success, $code) = &checkSuccess($tree, $mech); + say "success: $success\tcode: $code"; + if (!$success && !$code) { + &dumpToFile('./output/successcodeundefined', $mech->content); + die '$success and $code undefined'; + } + elsif ($code == -13) { + &dumpToFile('./output/-13inputs', \@inputs); + die; + } + } + else { + &dumpToFile('./output/unsuccessSubmitRegister', $mech->content); + die; + } +} + +sub programLoop { + &Delimiter((caller(0))[3]); + + for (;;) { + # INIT + &init(); + + # MAIN + # &getCookie(); # GET COOKIE, CHECK IF COOKIE IS VALID OR RENEW + &getStandorte() if $config{'RUN_VENUES'}; + &getDienstleistungen() if $config{'RUN_SERVICES'}; + &db_request(); + &Summary(); + + &sleepCountdown($config{'SLEEP_LOOP_MIN'} * 60); + } +} + +sub db_createRequestDienstleister { + &Delimiter((caller(0))[3]); + my $request_id = shift; + my $values; + + my $string = &db_query("Select terminsnipe.services.HREF From terminsnipe.request Inner Join terminsnipe.services On terminsnipe.services.ID = terminsnipe.request.SERVICE_ID Where terminsnipe.request.ID = $request_id", $connection, 'fetchrow'); + my @dienstleister = getDienstleisterlist($string); + + foreach my $id (@dienstleister) { + $values .= "('$request_id', '$id'), "; + } + my $sql = substr("INSERT INTO `request_dienstleister` (`REQUEST_ID`, `VENUE_ID`) VALUES $values", 0, -2); + # say $sql; + + &db_query("DELETE FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id", $connection, 'do'); + &db_query($sql, $connection, 'do'); +} + +sub getDienstleisterlist { + &Delimiter((caller(0))[3]); + my $string = shift; + + $string =~ m/dienstleisterlist=(.+?)&/; + return split /,/, $1; +} + +sub db_request { + &Delimiter((caller(0))[3]); + my %request; + + my $response = &db_query("SELECT `ID`, `USER_ID`, `SERVICE_ID`, `DATETIME_FROM`, `DATETIME_TO`, `TIMESTAMP` FROM `request` WHERE `EXPIRED` = 0;", $connection, 'selectall_arrayref'); + if (@$response) { + for my $i (0 .. $#{$response}) { + my $dataset = $$response[$i]; + $request{$$dataset[2]}{$$dataset[0]}{'ID'} = $$dataset[0]; + $request{$$dataset[2]}{$$dataset[0]}{'USER_ID'} = $$dataset[1]; + $request{$$dataset[2]}{$$dataset[0]}{'SERVICE_ID'} = $$dataset[2]; + $request{$$dataset[2]}{$$dataset[0]}{'DATETIME_FROM'} = $$dataset[3]; + $request{$$dataset[2]}{$$dataset[0]}{'DATETIME_TO'} = $$dataset[4]; + $request{$$dataset[2]}{$$dataset[0]}{'TIMESTAMP'} = $$dataset[5]; + $services{$$dataset[2]}{'HREF_GENERATED'} = &db_generateHREF($$dataset[0], $$dataset[2]); # ALSO CREATES $services{$$dataset[2]}{'REQUEST_DIENSTLEISTER_VENUES'} + # print Dumper $data{'SERVICES'}{$$dataset[2]}{'REQUEST_VENUE_IDS'}; + # die; + } + print Dumper \%request; + + # foreach my $service_id (keys %request) { + # my $url = $services{$service_id}{'HREF_GENERATED'}; + # &get_next_urls($url, $service_id, \%request); + # } + # print Dumper \%request; + # die; + + for my $iteration (0 .. 2) { + say "Iteration: $iteration"; + + foreach my $service_id (keys %request) { + if ($iteration == 0) { + &db_query("DELETE FROM `mapping` WHERE `SERVICE_ID` = '$service_id';", $connection, 'do'); # REMOVE ALL MAPPINGS FOR THIS SERVICE ID + } + &startFork($service_id, \%request, $iteration); + } + $pm->wait_all_children; + say 'All childs are done. Continuing with parent...'; + + foreach my $service_id (keys %request) { + &db_find_appointment($service_id, \%request); + } + } + } + else { + say 'No requests in db.'; + } +} + +sub get_next_urls { + &Delimiter((caller(0))[3]); + my ($url, $service_id, $request_ref) = @_; + + my $response = &responseHandlerMech($url); + + if (!$response->{'success'} && $response->{'restart'}) { + &get_next_urls($url, $service_id, $request_ref); + } + elsif ($response->{'success'}) { + if ($response->{'tree'}->exists('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')) { + say 'NEXT EXISTS'; + $$request_ref{$service_id}{'NEXT_FOLLOW'}++; + $$request_ref{'HIGHEST_NEXT_FOLLOW'} = $$request_ref{$service_id}{'NEXT_FOLLOW'} if $$request_ref{$service_id}{'NEXT_FOLLOW'} > $$request_ref{'HIGHEST_NEXT_FOLLOW'}; + my $epochtime = basename($response->{'tree'}->findvalue('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')); + my $url_next = "$config{'URL_TERMIN_DAY'}/$epochtime"; + &get_next_urls($url_next, $service_id, $request_ref); + } + } +} + +sub db_generateHREF { + &Delimiter((caller(0))[3]); + my ($request_id, $service_id) = @_; + + my $href_generated = $services{$service_id}{'HREF'}; + my $response = &db_query("SELECT `VENUE_ID` FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id;", $connection, 'selectall_arrayref'); + + my $dienstleisterlist; + for my $i (0 .. $#{$response}) { + $dienstleisterlist .= "$$response[$i][0],"; + push(@{$data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'}}, [ $$response[$i][0], $venues{$$response[$i][0]}{'NAME'}, $venues{$$response[$i][0]}{'HREF'} ]); + } + $dienstleisterlist = substr($dienstleisterlist, 0, -1); + $href_generated =~ m/dienstleisterlist=(.+?)&/; + $href_generated =~ s/$1/$dienstleisterlist/; + + return $href_generated; +} + +sub startFork { + &Delimiter((caller(0))[3]); + my ($service_id, $request_ref, $iteration) = @_; + + my $pid = $pm->start($service_id) and next; + + say "CHILD: START RUN FOR SERVICE '$services{$service_id}{'NAME'}'..."; + my $href = $services{$service_id}{'HREF_GENERATED'}; + + my $next = &getTerminDay($href, $service_id, '' , $iteration); + if ($next->{'success'} && $next->{'appointments_available'} > 0) { + &db_sync($$next{'service_id'}); + } + + $pm->finish; +} + +sub db_find_appointment { + &Delimiter((caller(0))[3]); + my ($service_id, $request_ref) = @_; + + foreach my $id (keys %{$$request_ref{$service_id}}) { + my $query = "Select terminsnipe.datetime.DATETIME, terminsnipe.mapping.SERVICE_ID, terminsnipe.mapping.VENUE_ID, terminsnipe.mapping.HREF As HREF, terminsnipe.venues.NAME As VENUE_NAME, terminsnipe.datetime.EPOCHTIME, terminsnipe.mapping.ID As MAPPING_ID From terminsnipe.datetime Inner Join terminsnipe.mapping On terminsnipe.mapping.DATETIME_ID = terminsnipe.datetime.EPOCHTIME Inner Join terminsnipe.venues On terminsnipe.mapping.VENUE_ID = terminsnipe.venues.ID Where Not terminsnipe.datetime.DATETIME > '$$request_ref{$service_id}{$id}{'DATETIME_TO'}' And Not terminsnipe.datetime.DATETIME < '$$request_ref{$service_id}{$id}{'DATETIME_FROM'}' And terminsnipe.mapping.SERVICE_ID = $service_id;"; + say $query; + my $response = &db_query($query, $connection, 'selectall_hashref', 'EPOCHTIME'); + # print Dumper $response; + + say 'Possible appointments: ' . scalar(keys(%$response)); + if (%$response) { + say 'defined'; + + &db_insertAppointment($response, $$request_ref{$service_id}{$id}); + } + } +} + +sub db_insertAppointment { + &Delimiter((caller(0))[3]); + my ($response, $dataset_ref) = @_; + my $user_id = $$dataset_ref{'USER_ID'}; + my $request_id = $$dataset_ref{'ID'}; + + foreach my $epochtime (keys %{$response}) { + if (!$$response{$epochtime}{'RESERVED'}) { + say "Reserving appointment $$response{$epochtime}{MAPPING_ID} for USER_ID $user_id now..."; + + my $success = &getRegister($$response{$epochtime}, $user_id); + + if ($success) { + &db_query("INSERT INTO `appointments` (`MAPPING_ID`, `USER_ID`) VALUES ('$$response{$epochtime}{MAPPING_ID}', '$user_id');", $connection, 'do'); + $$response{$epochtime}{'RESERVED'} = 1; + + say 'Deleting request with ID $request_id from request and request_dienstleister...'; + &db_query("DELETE FROM `request` WHERE `ID` = $request_id;", $connection, 'do'); + &db_query("DELETE FROM `request_dienstleister` WHERE `REQUEST_ID` = $request_id;", $connection, 'do'); + last; + } + elsif (!$success) { + say 'Could not book appointment.'; + } + else { + say 'Unknown return value'; + die; + } + } + else { + say "Appointment $epochtime is available but already reserved in loop by another user."; + } + } +} + +sub db_query { + # &Delimiter((caller(0))[3]); + my ($query, $connection, $switch, @rest) = @_; + my $response; + + # say 'DATABASE CONNECTION STATE: ' . $connection->ping; + + # $query = decode("utf-8", $query); + # $query = encode( "iso-8859-1", $query ); + + my $statement; + if ($switch eq 'fetchrow') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchrow(); + } + elsif ($switch eq 'fetchall_hashref') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchall_hashref($rest[0]); + } + elsif ($switch eq 'fetchall_arrayref') { + $statement = $connection->prepare($query); + $statement->execute(); + $response = $statement->fetchall_arrayref(); + } + elsif ($switch eq 'do') { + $response = $connection->do($query) or die $connection->errstr; + } + elsif ($switch eq 'selectall_hashref') { + $response = $connection->selectall_hashref($query, $rest[0]) or die $connection->errstr; + } + elsif ($switch eq 'selectall_array') { + my @response = $connection->selectall_array($query) or die $connection->errstr; + return \@response; + } + elsif ($switch eq 'selectall_arrayref') { + $response = $connection->selectall_arrayref($query) or die $connection->errstr; + } + elsif ($switch eq 'selectrow_hashref') { + $response = $connection->selectrow_hashref($query) or die $connection->errstr; + } + $statement->finish if $statement; + + return $response; +} + +sub connectToMySql { + &Delimiter((caller(0))[3]); + my ($db) = @_; + + # assign the values to your connection variable + my $connectionInfo = "dbi:mysql:$db;$config{'MYSQL'}{'ACCESS'}{'hostname'}"; + + # make connection to database + # my $l_connection = DBI->connect($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, $config{'MYSQL'}{'OPTIONS'}) || warn "MySQL conncet failed: $DBI::errstr"; + # return $l_connection; + + my $l_connection = DBIx::Connector->new($connectionInfo, $config{'MYSQL'}{'ACCESS'}{'user'}, $config{'MYSQL'}{'ACCESS'}{'pw'}, { + RaiseError => 1, + AutoCommit => 1, + }); + return $l_connection->dbh; +} + +sub run { + &Delimiter((caller(0))[3]); + + for my $service (keys %{$config{'SERVICES'}}) { + if ($config{'SERVICES'}{$service}{'RUN'}) { + my $service_id = $config{'SERVICES'}{$service}{'ID'}; + say "START RUN FOR SERVICE '$service'..."; + &getTerminDay($services{$service_id}{'HREF'}, $service_id, 0); + &db_sync($service_id); + } + else { + # say "DISABLED $service"; + } + } +} + +sub db_sync { + &Delimiter((caller(0))[3]); + my $service_id = shift; + + # &db_query("DELETE FROM `mapping` WHERE `SERVICE_ID` = '$service_id';", $connection, 'do'); + + foreach my $day (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}) { + + # TABLE DATETIME_DAY + # my $datetime_day = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIME'}; + # &db_query("INSERT INTO `datetime_day` (`EPOCHTIME`, `DATETIME`) SELECT $day, '$datetime_day' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `datetime_day` WHERE `EPOCHTIME`= $day);", $connection, 'do'); + + + foreach my $datetime (keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}}) { + my $epochtime = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}; + my $venue_id = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'VENUE_ID'}; + my $href = $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'HREF'}; + + say "$service_id\t$venue_id\t$epochtime"; + + # say "INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) VALUES ('$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}', '$datetime') ON DUPLICATE KEY UPDATE `DATETIME` = `DATETIME`;"; + # say "INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) SELECT $epochtime, '$datetime' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `datetime` WHERE `EPOCHTIME`= $epochtime);"; + # say "INSERT INTO `mapping` (`SERVICE_ID`, `VENUE_ID`, `DATETIME_ID`, `HREF`) SELECT '$service_id', '$venue_id', '$epochtime', '$href' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `mapping` WHERE `SERVICE_ID` = '$service_id' AND `VENUE_ID` = '$venue_id' AND `DATETIME_ID` = '$epochtime');"; + + # &db_query("INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) VALUES ('$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$day}{'DATETIMES'}{$datetime}{'EPOCHTIME'}', '$datetime') ON DUPLICATE KEY UPDATE `DATETIME` = `DATETIME`;", $connection, 'do'); + &db_query("INSERT INTO `datetime` (`EPOCHTIME`, `DATETIME`) SELECT $epochtime, '$datetime' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `datetime` WHERE `EPOCHTIME`= $epochtime);", $connection, 'do'); + &db_query("INSERT INTO `mapping` (`SERVICE_ID`, `VENUE_ID`, `DATETIME_ID`, `HREF`) SELECT '$service_id', '$venue_id', '$epochtime', '$href' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `mapping` WHERE `SERVICE_ID` = '$service_id' AND `VENUE_ID` = '$venue_id' AND `DATETIME_ID` = '$epochtime');", $connection, 'do'); + + # TABLE DATETIME_DAY + # my $mapping_id = $connection->last_insert_id(); + # print Dumper $mapping_id; + # &db_query("INSERT INTO `mapping_day` (`EPOCHTIME_DAY`, `MAPPING_ID`) SELECT '$day', '$mapping_id' FROM dual WHERE NOT EXISTS (SELECT 1 FROM `mapping_day` WHERE `EPOCHTIME_DAY` = '$day' AND `MAPPING_ID` = '$mapping_id');", $connection, 'do') if $mapping_id != 0; + } + } + +} + +sub createMysqlDateTime { + # &Delimiter((caller(0))[3]); + my $string = shift; + + my $dt = $strp->parse_datetime($string); + return DateTime::Format::MySQL->format_datetime($dt); +} + +sub getDienstleistungen { + &Delimiter((caller(0))[3]); + + my $response = &responseHandlerMech($config{'URL_DIENSTLEISTUNGEN'}); + + if (!$response->{'success'} && $response->{'restart'}) { + &getDienstleistungen(); + } + elsif ($response->{'success'}) { + &parseDienstleistungen($response->{'tree'}); + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in getDienstleistungen and rateLimitHandler unsuccessfull.'; + } + + if ($config{'FILTER_SERVICES_TO_OUTPUT'} || $config{'DB_REFRESH_SERVICES'}) { + &filterDienstleistungen(); + &db_insertServicesVenues('services') if $config{'DB_REFRESH_SERVICES'}; + } + + if ($config{'DUMP_SERVICES_TO_FILE'}) { + &dumpToFile($config{'SERVICES_PATH'}, \%services); + } +} + +sub db_insertServicesVenues { + &Delimiter((caller(0))[3]); + my $switch = shift; + my ($database, $hash_ref); + + if ($switch eq 'services') { + $database = 'services'; + $hash_ref = \%services; + } + elsif ($switch eq 'venues') { + $database = 'venues'; + $hash_ref = \%venues; + } + + my $response = &db_query("delete FROM $database", $connection, 'do'); + + foreach my $id (sort keys %{$hash_ref}) { + my $query = "INSERT INTO `$database` (`ID`, `NAME`, `HREF`) VALUES ('$$hash_ref{$id}{'ID'}', '$$hash_ref{$id}{'NAME'}', '$$hash_ref{$id}{'HREF'}');"; + my $response = &db_query($query, $connection, 'do'); + } +} + +sub filterDienstleistungen { + &Delimiter((caller(0))[3]); + + # DATA DUMPER UTF8 HACK + local *Data::Dumper::qquote = sub { qq["${\(shift)}"] }; + local $Data::Dumper::Useperl = 1; + + open my $FILE, '>>:encoding(UTF-8)', $config{'FILTER_SERVICES_PATH'}; + foreach my $id (keys %services) { + + my $response = &responseHandlerMech($services{$id}{'HREF'}); + + if (!$response->{'success'} && $response->{'restart'}) { + &filterDienstleistungen(); + } + elsif ($response->{'success'}) { + my $tree = $response->{'tree'}; + my $name = $services{$id}{'NAME'}; + + print "\t$name => "; + + if (!$tree->exists('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]')) { + say '0'; + delete $services{$id}; + } + else { + my $href = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]/a/@href'); + my $entry = "'$name' => { 'RUN' => 0, 'ID' => $services{$id}{'ID'} },"; + print '1 => ' . $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " zmstermin-multi inner ")]/a') . " | $entry\n"; + say $FILE $entry if $config{'FILTER_SERVICES_TO_OUTPUT'}; + $services{$id}{'HREF'} = $href; + } + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in filterDienstleistungen and rateLimitHandler unsuccessfull.'; + } + } + close $FILE; +} + +sub parseDienstleistungen { + &Delimiter((caller(0))[3]); + my $tree = shift; + + foreach my $dataset ($tree->findnodes('//li[contains(concat(" ", normalize-space(@class), " "), " topic-dls row-fluid ")]')) { + my $id = basename($dataset->findvalue('./a/@href')); + my $href = $dataset->findvalue('./a/@href'); + my $name = trim($dataset->findvalue('./a')); + + $services{$id}{'ID'} = $id; + $services{$id}{'HREF'} = "$config{'URL_ROOT'}$href"; + $services{$id}{'NAME'} = $name; + } +} + +sub getStandorte { + &Delimiter((caller(0))[3]); + + my $response = &responseHandlerMech($config{'URL_STANDORTE'}); + + if (!$response->{'success'} && $response->{'restart'}) { + &getStandorte(); + } + if ($response->{'success'}) { + &parseStandorte($response->{'tree'}); + &db_insertServicesVenues('venues') if $config{'DB_REFRESH_VENUES'}; + dumpToFile($config{'VENUES_PATH'}, \%venues) if $config{'VENUES_TO_FILE'}; + } + elsif (!$response->{'success'} && !$response->{'success_rateLimitHandler'}) { + die 'No success in getStandorte and rateLimitHandler unsuccessfull.'; + } +} + +sub dumpToFile { + # &Delimiter((caller(0))[3]); + my ($filepath, $ref) = @_; + + # DATA DUMPER UTF8 HACK + # no warnings 'redefine'; + local *Data::Dumper::qquote = sub { qq["${\(shift)}"] }; + local $Data::Dumper::Useperl = 1; + + open my $FILE, '>:encoding(UTF-8)', $filepath; + print $FILE Dumper $ref; + close $FILE; +} + +sub parseStandorte { + &Delimiter((caller(0))[3]); + my $tree = shift; + + foreach my $dataset ($tree->findnodes('//li[contains(concat(" ", normalize-space(@class), " "), " topic-dls ")]')) { + my $id = basename($dataset->findvalue('./a/@href')); + my $href = $dataset->findvalue('./a/@href'); + my $name = trim($dataset->findvalue('./a')); + + say $name; + + $venues{$id}{'ID'} = $id; + $venues{$id}{'HREF'} = "$config{'URL_ROOT'}$href"; + $venues{$id}{'NAME'} = $name; + } +} + +sub getCookie { + &Delimiter((caller(0))[3]); + + # my $url; + # foreach my $id (sort keys %services) { + # $url = $services{$id}{'HREF'}; + # last; + # } + + $mech = WWW::Mechanize->new( + agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + autocheck => 0, + ); + &getRegisterControls('getCookie'); +} + +sub init { + &Delimiter((caller(0))[3]); + + ($zsmappointment, $ts, $mech, $ua, $cookie_jar, $strp, $connection, $strp_datetime, $pm) = (undef) x 9; + undef %data; + + # STRP DATETIME + $strp = DateTime::Format::Strptime->new( + pattern => '%d %b %Y %H:%M', + locale => 'de_DE', + ); + + $strp_datetime = DateTime::Format::Strptime->new( + pattern => '%Y-%m-%d %T', + locale => 'de_DE', + ); + + # MYSQL + $connection = &connectToMySql($config{'MYSQL'}{'ACCESS'}{'database'}); + + # SERVICE AND VENUE HASH + $connection->do('set names utf8'); + %venues = %{&db_query("SELECT `ID`, `NAME`, `HREF` FROM `venues`", $connection, 'fetchall_hashref', 'ID')}; + %services = %{&db_query("SELECT `ID`, `NAME`, `HREF` FROM `services`", $connection, 'fetchall_hashref', 'ID')}; + + # COOKIES + $cookie_jar = HTTP::Cookies->new(ignore_discard => 1 ); + if (-e $config{'COOKIE_PATH'}) { + my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($config{'COOKIE_PATH'}); + # say "mtime: $mtime\tatime: $atime\tctime: $ctime"; + # print scalar localtime $mtime . "\n"; + + # my $age = (int((-M $config{'COOKIE_PATH'})*1440)||1); + # say "COOKIE AGE: $age minutes"; + # if ($age > 30) { + # unlink $config{'COOKIE_PATH'}; + # } + # else { + # say "Loading $config{'COOKIE_PATH'}..."; + # $cookie_jar->load($config{'COOKIE_PATH'}); + # } + my $dt_epoch = DateTime->from_epoch( epoch => $ctime ); + my $duration = $dt_epoch->delta_ms( DateTime->now ); + # say $duration->minutes; + if ($duration->minutes > 30) { + unlink $config{'COOKIE_PATH'}; + } + else { + say "Loading $config{'COOKIE_PATH'}..."; + $cookie_jar->load($config{'COOKIE_PATH'}); + } + } + + # BROWSER + $mech = WWW::Mechanize->new( + cookie_jar => $cookie_jar, + agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + autocheck => 0, + ); + # use LWP::UserAgent; + # $ua = LWP::UserAgent->new( + # ssl_opts => { + # SSL_version => 'TLSv12:!SSLv2:!SSLv3:!TLSv1:!TLSv11', + # }, + # cookie_jar => $cookie_jar, + # agent => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36', + # ); + + # EMPTY DIRECTORIES + # unlink glob "'$config{'CAPTCHA_DIR'}/*'"; + unlink glob "'$config{'OUTPUT_DIR'}/*'"; + # unlink glob "'$config{'COOKIE_DIR'}/*'"; + + # CLEAN OLD DATETIMES, FLAG EXPIRED REQUESTS + &db_query("DELETE FROM `datetime` WHERE `DATETIME` < NOW();", $connection, 'do'); + &db_query("UPDATE request SET `EXPIRED` = 1 WHERE `DATETIME_TO` < NOW();", $connection, 'do'); + + # FORK MANAGER + $pm = Parallel::ForkManager->new(scalar(keys(%services))); + # $pm = Parallel::ForkManager->new(1); + $pm->run_on_start( + sub { + my ($pid, $ident)=@_; + say "\n** $ident started, pid: $pid"; + }); + $pm->run_on_finish( + sub { + my ($pid, $exit_code, $ident) = @_; + say "\n** $ident just got out of the pool with PID $pid and exit code: $exit_code"; + }); + $pm->run_on_wait( + sub { + # my ($pid, $ident) = @_; + say "\n** a child is waiting now..."; + }); +} + +sub Summary { + &Delimiter((caller(0))[3]); + + &dumpToFile($config{'OUTPUT_PATH'}, \%data); + $connection->disconnect(); +} + +sub getTerminDay { + &Delimiter((caller(0))[3]); + my ($url, $service_id, $switch, $iteration) = @_; + $switch = '' if !$switch; + + + say "Iteration is $iteration"; + + my $response; + my $url_old = ''; + for (my $i=0; $i <= $iteration; $i++) { + say "\nLoop $i"; + say "url_old:\t$url_old\nurl:\t\t$url"; + if ($url_old eq $url) { + say 'NEXT DOES NOT EXIST'; + return { 'success' => 0, 'url' => $url, 'service_id' => $service_id, 'switch' => $switch}; + } + + $response = &responseHandlerMech($url); + if (!$response->{'success'} && $response->{'restart'}) { + redo; + } + + + $url_old = $url; + if ($response->{'tree'}->exists('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')) { + say 'NEXT EXISTS'; + my $epochtime = basename($response->{'tree'}->findvalue('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')); + $url = "$config{'URL_TERMIN_DAY'}/$epochtime"; + } + } + + + # my $response = &responseHandlerMech($url); + + # if (!$response->{'success'} && $response->{'restart'}) { + # &getTerminDay($url, $service_id, $switch, $iteration); + # } + if ($response->{'success'}) { + my $success = &parseTerminDay($response->{'tree'}, $service_id, $switch, $response); + + if ($success && $switch eq 'getRegisterControls') { + say 'No need to go next. Returning...'; + return 1; + } + + my $appointments_available = scalar keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}; + return { 'success' => 1, 'url' => $url, 'service_id' => $service_id, 'switch' => $switch, 'appointments_available' => $appointments_available }; + } + + + + + # if ($response->{'tree'}->exists('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')) { + # say 'NEXT EXISTS'; + # if ($config{'FOLLOW_NEXT'}) { + # my $epochtime = basename($response->{'tree'}->findvalue('//th[contains(concat(" ", normalize-space(@class), " "), " next ")]/a/@href')); + # my $url = "$config{'URL_TERMIN_DAY'}/$epochtime"; + # # &getTerminDay($url, $service_id, $switch); + # return { 'next' => 1, 'url' => $url, 'service_id' => $service_id, 'switch' => $switch, 'appointments_available' => $appointments_available }; + # } + # return { 'next' => 0, 'service_id' => $service_id, 'switch' => $switch, 'appointments_available' => $appointments_available }; + # } + # else { + # return { 'next' => 0, 'service_id' => $service_id, 'switch' => $switch, 'appointments_available' => $appointments_available }; + # } + + +} + +sub db_datetime_day { + &Delimiter((caller(0))[3]); + my $service_id = shift; + + print 'GETTING ALREADY PARSED & SYNCED ID\'S FORM MAPPING.. '; + my $response_mapping = &db_query("Select terminsnipe.datetime_day.EPOCHTIME As EPOCHTIME_DAY From terminsnipe.datetime_day Inner Join terminsnipe.mapping_day On terminsnipe.datetime_day.EPOCHTIME = terminsnipe.mapping_day.EPOCHTIME_DAY Inner Join terminsnipe.mapping On terminsnipe.mapping_day.MAPPING_ID = terminsnipe.mapping.ID Where terminsnipe.mapping.SERVICE_ID = 120703 Group By terminsnipe.datetime_day.EPOCHTIME", $connection, 'fetchall_hashref', 'EPOCHTIME_DAY'); + my $keys = scalar keys %{$response_mapping}; + say "GOT: $keys"; + + my @datetime_day; + if (%{$response_mapping}) { + %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}} = %{$response_mapping}; + @datetime_day = ( keys %{$response_mapping} ); + return { 'success' => 1, 'already_parsed_num' => $keys, 'datetime_day_ref' => \@datetime_day }; + } + else { + return { 'success' => 0 }; + } +} + +sub parseTerminDay { + &Delimiter((caller(0))[3]); + my ($tree, $service_id, $switch, $response) = @_; + + my $response_datetime_day = &db_datetime_day($service_id); + + + # print Dumper $data{'SERVICES'}{$service_id}{'AVAILABLE'}; + + # die; + + my $xpath = '//div[contains(concat(" ", normalize-space(@class), " "), " calendar-month-table ")]'; + if ($tree->exists($xpath)) { + foreach my $table ($tree->findnodes($xpath)) { + my ($month, $year) = split(/ /, $table->findvalue('./table/thead/tr/th[contains(concat(" ", normalize-space(@class), " "), " month ")]')); + foreach my $buchbar ($table->findnodes('./table/tbody/tr/td[contains(concat(" ", normalize-space(@class), " "), " buchbar ")]')) { + my $id = basename($buchbar->findvalue('./a/@href')); + if ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}) { + say 'ALREADY PARSED'; + next; + } + else { + my $day = $buchbar->findvalue('./a'); + $data{'SERVICES'}{$service_id}{'SERVICE_ID'} = $service_id; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} = $month; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} = $year; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} = $day; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'ID'} = $id; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIME'} = &createMysqlDateTime("$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} 00:00"); + + my $url = "$config{'URL_TERMIN_TIME'}/$id"; + &getTerminTime($url, $id, $service_id); + + if ($switch eq 'getRegisterControls') { + if (scalar keys %{$data{'SERVICES'}{$service_id}{'AVAILABLE'}} > 0) { + say 'Successfully parsed at least 1 appointment. Returning...'; + return 1; + } + } + } + } + } + # return 1; + + + if ($response_datetime_day->{'success'}) { + say "DELETING ALREADY PARSED AND SYNCED DATASETS FROM DATA"; + my $ref = $response_datetime_day->{'datetime_day_ref'}; + delete @{$data{'SERVICES'}{$service_id}{'AVAILABLE'}}{@{$ref}} + } + } + else { + say 'Parsing error.'; + &dumpToFile('./output/parsingerror_parseTerminDay', $response->{'mech'}->content); + die; + } +} + +sub getTerminTime { + &Delimiter((caller(0))[3]); + my ($url, $id, $service_id, $switch) = @_; + + my $response = &responseHandlerMech($url); + + if (!$response->{'success'} && $response->{'restart'}) { + &getTerminTime($url, $id, $service_id, $switch); + } + elsif ($response->{'success'}) { + my $success = &parseTerminTime($response->{'tree'}, $id, $service_id); + if (!$success) { + # say $mech->content(); + &dumpToFile('./output/!success_parseTerminTime', $mech->content); + say 'No success in parseTerminTime. Just continue?'; + # &getTerminTime($url, $id, $service_id); # RETRY + } + } +} + +sub rateLimitHandler { + &Delimiter((caller(0))[3]); + my ($tree, $url) = @_; + + my $error = $tree->findvalue('//div[contains(concat(" ", normalize-space(@class), " "), " offset2 ")]/h1'); + say $error; + + if ($error eq 'Zu viele Zugriffe') { + say 'Rate Limit detected.'; + &sleepCountdown($config{'RATE_LIMIT_SLEEP_MIN'} * 60); + # my $response = &responseHandlerMech($url); + return 1; + } + else { + say 'Could not resolve error in rateLimitHandler'; + die; + } +} + +sub sleepCountdown { + &Delimiter((caller(0))[3]); + my $countdown = shift; + + $|++; + for (my $i=$countdown; $i >= 0; $i--) { + printf "Sleeping for %s seconds...\r", $i; + sleep(1); + } + return 1; +} + +sub captchaHandler { + &Delimiter((caller(0))[3]); + my $tree = shift; + + my $captchaBase64 = $tree->findvalue('//fieldset[contains(concat(" ", normalize-space(@class), " "), " well ")]/p/img/@src'); + my $filename = &base64ToFile($captchaBase64); + # system("cacaview $filename"); + system("tiv -0 $filename"); + + say 'Please solve captcha ' . basename($filename) . ' to proceed: '; + my $captcha_text = ; + chomp $captcha_text; + my $urlCaptcha = "$config{'URL_HUMAN'}/?captcha_text=$captcha_text"; + + # my $response = &responseHandlerMech($urlCaptcha); + my $response = $mech->get($urlCaptcha); + if ($mech->success) { + say 'Captcha successfully solved. Saving cookie...'; + $cookie_jar->save($config{'COOKIE_PATH'}); + return 1; + } + elsif ($mech->status == 428) { + say 'Captcha not successfully solved. Try again...'; + return 0; + } +} + +sub trim { + my $string = shift; + $string =~ s/^\s+|\s+$//g; + return $string; +} + +sub parseTerminTime { + &Delimiter((caller(0))[3]); + my ($tree, $id, $service_id) = @_; + + my $xpath = '//div[contains(concat(" ", normalize-space(@class), " "), " timetable ")]/table/tbody/tr'; + my $xpath_stop = '//link[contains(concat(" ", normalize-space(@rel), " "), " alternate ")]/@href'; + + if ($tree->exists($xpath)) { + my $time; + foreach my $buergeramt_dataset ($tree->findnodes($xpath)) { + if ($buergeramt_dataset->findvalue('./th[1]')) { + $time = trim($buergeramt_dataset->findvalue('./th[1]')); + $time = &createMysqlDateTime("$data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DAY'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'MONTH'} $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'YEAR'} $time"); + # print "Parsing for $time..."; + } + foreach my $buergeramt ($buergeramt_dataset->findnodes('./td[contains(concat(" ", normalize-space(@class), " "), " frei ")]')) { + my $name = trim($buergeramt->findvalue('./a')); + + if ($name eq 'Diesen Termin buchen' && defined $data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'} && scalar @{$data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'}} == 1) { + $name = $data{'SERVICES'}{$service_id}{'REQUEST_VENUE_IDS'}[0][1]; + # say "Single venue: Correct name: $name"; + } + + my $href = $buergeramt->findvalue('./a/@href'); + say $time; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'VENUE'} = $name; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'HREF'} = "$config{'URL_ROOT'}$href"; + # say $name . ' needs match: ' . grep{ $venues{$_}{'NAME'} eq $name} keys %venues; + ($data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'VENUE_ID'}) = grep{ $venues{$_}{'NAME'} eq $name} keys %venues; + $data{'SERVICES'}{$service_id}{'AVAILABLE'}{$id}{'DATETIMES'}{$time}{'EPOCHTIME'} = DateTime::Format::DateParse->parse_datetime($time)->epoch(); + } + } + &dumpToFile('./output/data', \%data); + return 1; + } + elsif ($tree->findvalue($xpath_stop) eq '/terminvereinbarung/termin/stop/') { # CAN BE REMOVED BECAUSE OF HANDLER? + say "XPATH_STOP DETECTED"; + } + else { + say 'Parsing error.'; + return 0; + } +} + +sub base64ToFile { + &Delimiter((caller(0))[3]); + my $base64 = shift; + + my $timestamp = &GetTimestamp('YMDHMS'); + $base64 =~ s/data:image\/(.+?)\;base64,//; + my $decoded= decode_base64($base64); + my $filename = "$config{'CAPTCHA_DIR'}/$timestamp.png"; + open my $fh, '>', $filename or die $!; + binmode $fh; + print $fh $decoded; + close $fh; + return $filename; +} + +sub Delimiter { + my $SubName = shift; + print "\n" . "-" x 80 . "\nSUB " . $SubName . "\n" . '-' x 80 . "\n"; +} + +sub GetTimestamp { + #&Delimiter((caller(0))[3]); + my $switch = shift; + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time); + + my $nice_timestamp; + if ($switch eq 'YMDHMS') { + $nice_timestamp = sprintf ( "%04d%02d%02d_%02d%02d%02d", $year+1900,$mon+1,$mday,$hour,$min,$sec); + } + elsif ($switch eq 'YMD') { + $nice_timestamp = sprintf ( "%04d%02d%02d", $year+1900,$mon+1,$mday); + } + elsif ($switch eq 'year') { + $nice_timestamp = $year+1900; + } + elsif ($switch eq 'month') { + $nice_timestamp = $mon+10; + } + else { + print "Invalid/no switch detected. Use: 'YMDHMS' / 'YMD'\n"; + } + + return $nice_timestamp; +} \ No newline at end of file