PHP cURL: una classe per controllare link interrotti, status code e nxdomain in parallelo

PHP cURL e status code

PHP cURL e status code

Questa classe nasce dall’esigenza di revisionare i numerosi bookmarks accumulati in anni di navigazione su Internet. Purtroppo uno degli inconvenienti di Internet è proprio la scarsa affidabilità sulla persistenza dei link. Succede spesso che i link collezionati anni prima non siano più attivi oppure siano stati ridirezionati su altri siti.

Questa classe utilizza le librerie cURL che nelle ultime versioni del PHP, sono state integrate nel pacchetto. Ho utilizzato in particolare la famiglia di comandi curl_multi* in modo da poter evadere richieste multiple parallelamente e velocizzare notevolmente il processo. Oltre a poter verificare le url, che vengono fornite al costruttore della classe come array, per particolari status code o per intere famiglie di status code, è possibile anche verificare che la risposta non sia un cosiddetto hit-nxdomain cioè un server che intercetta un nxdomain e propone un redirect ad una pagina di ricerca di domini dal nome simile. In pratica alcuni DNS (p.e. OpenDNS) in caso di dominio inesistente producono redirect pubblicitari attraverso i loro hit-nxdomain.

Le proprietà pubbliche della classe sono:

NomeDescrizione
$errorsArray che contiene la lista di url che hanno determinato un errore cURL
$maxUrlsIl numero massimo di url da processare parallelamente
$statusCodeTypeStringa filtro per gli status code e le famiglie di status code (p.e. 3xx) Possono essere abbinate e devono essere separate da , p.e. 3xx,404,403.
$timeoutTimeout in secondi per l’attesa della risposta del server
$uaStringa che identifica lo user agent con la quale viene formalizzata la richiesta cURL

Per la gestione delle risposte multiple alle chiamate cURL HTTP, ho abbondantemente utilizzato le ottime risorse del manuale PHP….!
Ed ecco il codice della classe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
<?php
/**
 * class: CheckUrls
 * Check status code in a list of Urls with Curl Multi thread library
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *      
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 * 
 * @author Mario Spada <spadamar@spadamar.com>
 * @copyright Copyright (c) 2013 Mario Spada
 * @package CheckUrls
 */
class CheckUrls {
	/**
	 * timeout (default = 30s)
	 * @var int
	 */	
	public  $timeout = 30;
	/**
	 * Max number of urls to check (default = 50)
	 * @var int
	 */	
	public $maxUrls = 50;
	/**
	 * User Agent string (default = Chrome 27 on Windows XP )
	 * @var string
	 */	
	public  $ua = "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.94 Safari/537.36";
	/**
	 * Array filled with Curl scan errors 
	 * @var array
	 */	
	public	$errors = array();
	/**
	 * String filtering status code, If more than one, separate it with comma. 
	 * Type a group e.g. 4xx and/or a single code e.g. 404. 
	 * Example "3xx,404" means filter all 3xx and 404.
	 * Default: "1xx,2xx,3xx,4xx,5xx" (all)
	 * 
	 * @var string
	 */		
	public 	$statusCodeType = "1xx,2xx,3xx,4xx,5xx";
	private $urls2check = array();
	private $nUrls = 0;
	private $ch = NULL;
	private $mh = NULL;
	private $still_running = NULL;
	private $hitNxdomainList = array("67.215.65.132");
 
	/**
	 * @param array $urlList (list of Urls to check)
	 */	
	public function __construct($urlList) {
		if (count($urlList) < 1) {
			die ("Empty urls list!");
		}
		if (!function_exists("curl_multi_init")) {
			die ("Sorry, cURL Libraries aren't available");
		}
		$this->urls2check = (array) $urlList;
		$this->nUrls = count($this->urls2check);
		if ($this->nUrls > $this->maxUrls) {
			die ("Sorry, the number of urls to check exceed MAX: ".$this->maxUrls);
		}
	}
 
	private function initCurl () {
		for ($n = 1; $n <= $this->nUrls; $n++){
			$this->ch[$n] = curl_init();
		}
	}
 
	private function curlSetOptions () {
		$n = 1;
		foreach ($this->urls2check as $value) {
			curl_setopt($this->ch[$n], CURLOPT_URL,			$value);
			curl_setopt($this->ch[$n], CURLOPT_NOBODY,		true);
			curl_setopt($this->ch[$n], CURLOPT_HEADER, 		true);
			curl_setopt($this->ch[$n], CURLOPT_USERAGENT,		$this->ua);
			curl_setopt($this->ch[$n], CURLOPT_RETURNTRANSFER,	true);
			curl_setopt($this->ch[$n], CURLOPT_TIMEOUT,		$this->timeout);
			$n++;
		}
	}
 
	private function addCurlHandles () {
		$this->mh = curl_multi_init();
		for ($n = 1; $n <= $this->nUrls; $n++){
			curl_multi_add_handle($this->mh,$this->ch[$n]);
		}		
	}
 
	/**
	 * Parse urls and return an array of status code.
	 * @return array
	 */	
	public function parse() {
		$statusCodePattern = $this->composeStatusCodePattern();
		$res = array();
		$this->errors = array();
		$this->initCurl();
		$this->curlSetOptions();
		$this->addCurlHandles();
		$this->still_running = NULL;
		$this->full_curl_multi_exec(); 		// start requests
		do { 					// "wait for completion"-loop
			curl_multi_select($this->mh); 	// non-busy (!) wait for state change
			$this->full_curl_multi_exec(); 	// get new state
			while ($info = curl_multi_info_read($this->mh)) {
				$headers = curl_multi_getcontent($info['handle']);
				$url = curl_getinfo($info['handle'],CURLINFO_EFFECTIVE_URL);
				$status = curl_getinfo($info['handle'],CURLINFO_HTTP_CODE);
				$redir_url = "NA";
				// is_NXDOMAIN ?
				$isnxdomain = $this->is_nxdomain($url);
				if ( $info['result'] ==  CURLE_OK ) {
					if (preg_match($statusCodePattern,$status)) {
						if($status === 301 || $status === 302) {
							if (preg_match("!\r\n(?:Location|URI): *(.*?) *\r\n!", $headers, $matches)) {
								$redir_url = $matches[1];
							}
						}
					}
				} else {
					array_push($this->errors, $url);
				}
				array_push($res,array('url'=>$url, 
								'status'=>$status, 
								'redirect'=>$redir_url, 
								'nxdomain'=>$isnxdomain));
			}
		} while ($this->still_running);
		  $this->closeAll();
		return $res;
	}
 
	private function full_curl_multi_exec() {
		do {
		  $rv = curl_multi_exec($this->mh, $this->still_running);
		} while ($rv == CURLM_CALL_MULTI_PERFORM);
		return $rv;
	}
 
	private function closeAll () {
		for ($n = 1; $n <=$this->nUrls; $n++){
			curl_multi_remove_handle($this->mh, $this->ch[$n]);
		}
		curl_multi_close($this->mh);
	}
 
	private function composeStatusCodePattern () {
		$res = "";
		$this->statusCodeType = preg_replace('/\s+/', '',$this->statusCodeType);
		$codesFilter = explode(",",$this->statusCodeType);
 
		foreach ($codesFilter as $code) {
			if (is_numeric($code)) {
				$res .= $code."|";
			} else {
				$res .= str_replace("x","\d",$code)."|";
			}
		}
		$res = substr($res, 0, -1);
		$res = "/(".$res.")/";
		return $res;
	}
 
	private function is_nxdomain ($url) {
		$hostfromurl = parse_url($url, PHP_URL_HOST);
		$res = gethostbyname($hostfromurl);
		if ($res == $hostfromurl || in_array($res,$this->hitNxdomainList)) {
			return true;
		}
		return false;
	}
 
}
?>

E questo è un esempio di utilizzo della classe:

<?php
require_once("CheckUrls.class.php");
$urls = array("www.google.com",
	"www.facebook.com",
	"www.questaeunaprova.it");
$check = new CheckUrls($urls);
$result = $check->parse();
echo "<p>Risultati</p>\n";
print_r($result);
echo "<p>Errori</p>\n";
print_r($check->errors);
?>

Questo è l’output:

Risultati

Array
(
    [0] => Array
        (
            [url] => HTTP://www.questaeunaprova.it
            [status] => 0
            [redirect] => NA
            [nxdomain] => 1
        )

    [1] => Array
        (
            [url] => HTTP://www.google.com
            [status] => 302
            [redirect] => http://www.google.it/
            [nxdomain] => 
        )

    [2] => Array
        (
            [url] => HTTP://www.facebook.com
            [status] => 301
            [redirect] => https://www.facebook.com/
            [nxdomain] => 
        )

)

Errori

Array
(
    [0] => HTTP://www.questaeunaprova.it
)

Ho preparato anche la documentazione del codice generata con phpSimpleDoc

Al momento ho potuto constatare che solo OpenDNS utilizza un redirect per i nxdomanin, e ho inserito nella proprietà: $hitNxdomainList l’indirizzo IP del suo hit-nxdomain. Non ho utilizzato un metodo per gestire il contenuto di questo array, perché ritengo che sarà utilizzato poco. Ovviamente, se fosse necessario aggiungerne altri, è possibile modificare direttamente il codice alla riga: 60.

Il pacchetto con la classe e il file di esempio è disponibile qui.

Questo articolo è stato pubblicato in Informatica e contrassegnato con , . Aggiungi ai segnalibri il permalink.