grund; } /** * Holt die Adresse direkt in eine Datei - fuer Archive, die nicht als Zeichenkette in den * Speicher passen sollen. Ohne curl geht es ueber get(); dann ist der Speicher die Grenze. */ public function toFile(string $url, string $path): bool { $this->grund = ''; if (!str_starts_with($url, 'https://')) { $this->grund = 'Nur HTTPS-Adressen werden geholt.'; return false; } if (function_exists('curl_init')) { $datei = @fopen($path, 'w'); if (!is_resource($datei)) { $this->grund = 'Die Zieldatei ließ sich nicht anlegen: '.$path; return false; } $curl = curl_init($url); curl_setopt_array($curl, [ CURLOPT_FILE => $datei, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_CONNECTTIMEOUT => $this->connectTimeout, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_USERAGENT => 'KantSites-Updater', ]); $ok = curl_exec($curl); $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); $fehler = curl_errno($curl); $text = curl_error($curl); curl_close($curl); fclose($datei); if (true === $ok && 200 === $status && (int) filesize($path) > 0) { return true; } @unlink($path); if (!in_array($fehler, self::ZERTIFIKATSFEHLER, true)) { $this->grund = match (true) { 0 !== $fehler => 'Die Verbindung kam nicht zustande: '.$text, 0 === $status => 'Der Server hat nicht geantwortet.', default => sprintf('Die Adresse antwortet mit %d statt 200.', $status), }; return false; } } $inhalt = $this->get($url); if (null === $inhalt || '' === $inhalt) { return false; } return false !== file_put_contents($path, $inhalt); } /** Den Inhalt der Adresse, oder null wenn sie nicht mit 200 antwortet. */ public function get(string $url): ?string { $this->grund = ''; // Kein http, kein file, kein ftp: ein Update, das über eine unverschlüsselte Verbindung // kommt, kann unterwegs ausgetauscht worden sein. if (!str_starts_with($url, 'https://')) { $this->grund = 'Nur HTTPS-Adressen werden geholt.'; return null; } if (function_exists('curl_init')) { $curl = curl_init($url); curl_setopt_array($curl, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, // Auch eine Weiterleitung bleibt bei HTTPS: sonst pruefte die erste Zeile nur die // erste Adresse, und die Gegenstelle koennte auf http:// umlenken. CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_CONNECTTIMEOUT => $this->connectTimeout, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_USERAGENT => 'KantSites-Updater', ]); $antwort = curl_exec($curl); $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); $fehler = curl_errno($curl); $text = curl_error($curl); curl_close($curl); if (is_string($antwort) && 200 === $status) { return $antwort; } // Ein fehlender Zertifikatsspeicher ist keine kaputte Leitung, sondern eine // Einstellung - und er sieht sonst genauso aus wie eine gesperrte Verbindung. $this->grund = match (true) { in_array($fehler, self::ZERTIFIKATSFEHLER, true) => 'Das Zertifikat der Gegenstelle lässt sich nicht prüfen: Diesem PHP fehlt ein Zertifikatsspeicher (curl.cainfo in der php.ini). Geholt wird trotzdem nur mit Prüfung - ein Update ohne sie könnte unterwegs ausgetauscht worden sein.', 0 !== $fehler => 'Die Verbindung kam nicht zustande: '.$text, 0 === $status => 'Der Server hat nicht geantwortet.', default => sprintf('Die Adresse antwortet mit %d statt 200.', $status), }; // Bei einem Zertifikatsproblem lohnt der zweite Weg: Der Dateizeiger benutzt die // Einstellungen von OpenSSL, und die sind manchmal gesetzt, wo curl nichts findet. if (!in_array($fehler, self::ZERTIFIKATSFEHLER, true)) { return null; } } if (!filter_var(ini_get('allow_url_fopen'), FILTER_VALIDATE_BOOL)) { $this->grund = '' !== $this->grund ? $this->grund : 'Dieser Server darf keine Adressen abrufen: curl fehlt und allow_url_fopen ist abgeschaltet.'; return null; } // Der Dateizeiger folgt Weiterleitungen auch auf http://; deshalb hier von Hand, und // jede Station muss HTTPS sein. $antwort = false; for ($hop = 0; $hop <= 3 && str_starts_with($url, 'https://'); ++$hop) { $context = stream_context_create([ 'http' => ['timeout' => $this->timeout, 'follow_location' => 0, 'ignore_errors' => true, 'header' => "User-Agent: KantSites-Updater\r\n"], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true], ]); $antwort = @file_get_contents($url, false, $context); $kopf = implode("\n", $http_response_header ?? []); if (!is_string($antwort) || 1 !== preg_match('/^HTTP\/\S+ 3\d\d/m', $kopf) || 1 !== preg_match('/^Location:\s*(\S+)/mi', $kopf, $ziel)) { break; } $url = $ziel[1]; $antwort = false; } if (is_string($antwort) && 1 === preg_match('/^HTTP\/\S+ 200/m', implode("\n", $http_response_header ?? []))) { $this->grund = ''; return $antwort; } if ('' === $this->grund) { $this->grund = 'Die Adresse war über den Dateizeiger nicht erreichbar.'; } return null; } }