]> Raphaël G. Git Repositories - airbundle/blob - Command/WeatherCommand.php
Use checker for isGranted
[airbundle] / Command / WeatherCommand.php
1 <?php declare(strict_types=1);
2
3 /*
4 * This file is part of the Rapsys AirBundle package.
5 *
6 * (c) Raphaël Gertz <symfony@rapsys.eu>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Rapsys\AirBundle\Command;
13
14 use Doctrine\Bundle\DoctrineBundle\Command\DoctrineCommand;
15 use Doctrine\Persistence\ManagerRegistry;
16
17 use Symfony\Component\Console\Input\InputInterface;
18 use Symfony\Component\Console\Output\OutputInterface;
19 use Symfony\Component\Filesystem\Exception\IOException;
20 use Symfony\Component\Filesystem\Filesystem;
21
22 use Rapsys\AirBundle\Entity\Session;
23
24 class WeatherCommand extends DoctrineCommand {
25 //Set failure constant
26 const FAILURE = 1;
27
28 ///Set success constant
29 const SUCCESS = 0;
30
31 ///Set Tidy config
32 private $config = [
33 //Mostly useless in fact
34 'indent' => true,
35 //Required to simplify simplexml transition
36 'output-xml' => true,
37 //Required to avoid xml errors
38 'quote-nbsp' => false,
39 //Required to fix code
40 'clean' => true
41 ];
42
43 ///Set accuweather uris
44 private $accuweather = [
45 //Hourly uri
46 'hourly' => [
47 75001 => 'https://www.accuweather.com/en/fr/paris-01-louvre/75001/hourly-weather-forecast/179142_pc?day=',
48 75004 => 'https://www.accuweather.com/en/fr/paris-04-hotel-de-ville/75004/hourly-weather-forecast/179145_pc?day=',
49 75005 => 'https://www.accuweather.com/en/fr/paris-05-pantheon/75005/hourly-weather-forecast/179146_pc?day=',
50 75006 => 'https://www.accuweather.com/fr/fr/paris-06-luxembourg/75006/hourly-weather-forecast/179147_pc?day=',
51 75007 => 'https://www.accuweather.com/en/fr/paris-07-palais-bourbon/75007/hourly-weather-forecast/179148_pc?day=',
52 75009 => 'https://www.accuweather.com/en/fr/paris-09-opera/75009/hourly-weather-forecast/179150_pc?day=',
53 75010 => 'https://www.accuweather.com/en/fr/paris-10-entrepot/75010/hourly-weather-forecast/179151_pc?day=',
54 75012 => 'https://www.accuweather.com/en/fr/paris-12-reuilly/75012/hourly-weather-forecast/179153_pc?day=',
55 75013 => 'https://www.accuweather.com/en/fr/paris-13-gobelins/75013/hourly-weather-forecast/179154_pc?day=',
56 75015 => 'https://www.accuweather.com/en/fr/paris-15-vaugirard/75015/hourly-weather-forecast/179156_pc?day=',
57 75019 => 'https://www.accuweather.com/en/fr/paris-19-buttes-chaumont/75019/hourly-weather-forecast/179160_pc?day=',
58 75116 => 'https://www.accuweather.com/en/fr/paris-16-passy/75116/hourly-weather-forecast/179246_pc?day='
59 ],
60 //Daily uri
61 'daily' => [
62 75001 => 'https://www.accuweather.com/en/fr/paris-01-louvre/75001/daily-weather-forecast/179142_pc',
63 75004 => 'https://www.accuweather.com/en/fr/paris-04-hotel-de-ville/75004/daily-weather-forecast/179145_pc',
64 75005 => 'https://www.accuweather.com/en/fr/paris-05-pantheon/75005/daily-weather-forecast/179146_pc',
65 75006 => 'https://www.accuweather.com/fr/fr/paris-06-luxembourg/75006/daily-weather-forecast/179147_pc',
66 75007 => 'https://www.accuweather.com/en/fr/paris-07-palais-bourbon/75007/daily-weather-forecast/179148_pc',
67 75009 => 'https://www.accuweather.com/en/fr/paris-09-opera/75009/daily-weather-forecast/179150_pc',
68 75010 => 'https://www.accuweather.com/en/fr/paris-10-entrepot/75010/daily-weather-forecast/179151_pc',
69 75012 => 'https://www.accuweather.com/en/fr/paris-12-reuilly/75012/daily-weather-forecast/179153_pc',
70 75013 => 'https://www.accuweather.com/en/fr/paris-13-gobelins/75013/daily-weather-forecast/179154_pc',
71 75015 => 'https://www.accuweather.com/en/fr/paris-15-vaugirard/75015/daily-weather-forecast/179156_pc',
72 75019 => 'https://www.accuweather.com/en/fr/paris-19-buttes-chaumont/75019/daily-weather-forecast/179160_pc',
73 75116 => 'https://www.accuweather.com/en/fr/paris-16-passy/75116/daily-weather-forecast/179246_pc'
74 ]
75 ];
76
77 ///Set curl handler
78 private $ch = null;
79
80 ///Set manager registry
81 private $doctrine;
82
83 ///Set filesystem
84 private $filesystem;
85
86 ///Weather command constructor
87 public function __construct(ManagerRegistry $doctrine, Filesystem $filesystem) {
88 parent::__construct($doctrine);
89
90 //Set entity manager
91 $this->doctrine = $doctrine;
92
93 //Set filesystem
94 $this->filesystem = $filesystem;
95 }
96
97 ///Configure attribute command
98 protected function configure() {
99 //Configure the class
100 $this
101 //Set name
102 ->setName('rapsysair:weather')
103 //Set description shown with bin/console list
104 ->setDescription('Updates session rain and temperature fields')
105 //Set description shown with bin/console --help airlibre:attribute
106 ->setHelp('This command updates session rain and temperature fields in next three days')
107 //Add daily and hourly aliases
108 ->setAliases(['rapsysair:weather:daily', 'rapsysair:weather:hourly']);
109 }
110
111 ///Process the attribution
112 protected function execute(InputInterface $input, OutputInterface $output): int {
113 //Kernel object
114 $kernel = $this->getApplication()->getKernel();
115
116 //Tmp directory
117 $tmpdir = $kernel->getContainer()->getParameter('kernel.project_dir').'/var/cache/weather';
118
119 //Set tmpdir
120 //XXX: worst case scenario we have 3 files per zipcode plus daily
121 if (!is_dir($tmpdir)) {
122 try {
123 //Create dir
124 $this->filesystem->mkdir($tmpdir, 0775);
125 } catch (IOException $exception) {
126 //Display error
127 echo 'Create dir '.$exception->getPath().' failed'."\n";
128
129 //Exit with failure
130 exit(self::FAILURE);
131 }
132 }
133
134 //Cleanup kernel
135 unset($kernel);
136
137 //Tidy object
138 $tidy = new \tidy();
139
140 //Init zipcodes array
141 $zipcodes = [];
142
143 //Init types
144 $types = [];
145
146 //Process hourly accuweather
147 if (($command = $input->getFirstArgument()) == 'rapsysair:weather:hourly' || $command == 'rapsysair:weather') {
148 //Fetch hourly sessions to attribute
149 $types['hourly'] = $this->doctrine->getRepository(Session::class)->findAllPendingHourlyWeather();
150
151 //Iterate on each session
152 foreach($types['hourly'] as $sessionId => $session) {
153 //Get zipcode
154 $zipcode = $session->getLocation()->getZipcode();
155
156 //Get start
157 $start = $session->getStart();
158
159 //Set start day
160 $day = $start->diff((new \DateTime('now'))->setTime(0, 0, 0))->d + 1;
161
162 //Check if zipcode is set
163 if (!isset($zipcodes[$zipcode])) {
164 $zipcodes[$zipcode] = [];
165 }
166
167 //Check if zipcode date is set
168 if (!isset($zipcodes[$zipcode][$day])) {
169 $zipcodes[$zipcode][$day] = [ $sessionId => $sessionId ];
170 } else {
171 $zipcodes[$zipcode][$day][$sessionId] = $sessionId;
172 }
173
174 //Get stop
175 $stop = $session->getStop();
176
177 //Set stop day
178 $day = $stop->diff((new \DateTime('now'))->setTime(0, 0, 0))->d + 1;
179
180 //Check if zipcode date is set
181 if (!isset($zipcodes[$zipcode][$day])) {
182 $zipcodes[$zipcode][$day] = [ $sessionId => $sessionId ];
183 } else {
184 $zipcodes[$zipcode][$day][$sessionId] = $sessionId;
185 }
186 }
187 }
188
189 //Process daily accuweather
190 if ($command == 'rapsysair:weather:daily' || $command == 'rapsysair:weather') {
191 //Fetch daily sessions to attribute
192 $types['daily'] = $this->doctrine->getRepository(Session::class)->findAllPendingDailyWeather();
193
194 //Iterate on each session
195 foreach($types['daily'] as $sessionId => $session) {
196 //Get zipcode
197 $zipcode = $session->getLocation()->getZipcode();
198
199 //Get start
200 $start = $session->getStart();
201
202 //Set start day
203 $day = 'daily';
204
205 //Check if zipcode is set
206 if (!isset($zipcodes[$zipcode])) {
207 $zipcodes[$zipcode] = [];
208 }
209
210 //Check if zipcode date is set
211 if (!isset($zipcodes[$zipcode][$day])) {
212 $zipcodes[$zipcode][$day] = [ $sessionId => $sessionId ];
213 } else {
214 $zipcodes[$zipcode][$day][$sessionId] = $sessionId;
215 }
216 }
217 }
218
219 //Init curl
220 $this->curl_init();
221
222 //Init data array
223 $data = [];
224
225 //Iterate on zipcodes
226 foreach($zipcodes as $zipcode => $days) {
227 //Iterate on days
228 foreach($days as $day => $null) {
229 //Try to load content from cache
230 if (!is_file($file = $tmpdir.'/'.$zipcode.'.'.$day.'.html') || stat($file)['ctime'] <= (time() - ($day == 'daily' ? 4 : 2)*3600) || ($content = file_get_contents($file)) === false) {
231 //Prevent timing detection
232 //XXX: from 0.1 to 5 seconds
233 usleep(rand(1,50) * 100000);
234
235 //Get content
236 //TODO: for daily we may load data for requested quarter of the day
237 $content = $this->curl_get($day == 'daily' ? $this->accuweather['daily'][$zipcode] : $this->accuweather['hourly'][$zipcode].$day);
238
239 //Store it
240 if (file_put_contents($tmpdir.'/'.$zipcode.'.'.$day.'.html', $content) === false) {
241 //Display error
242 echo 'Write to '.$tmpdir.'/'.$zipcode.'.'.$day.'.html failed'."\n";
243
244 //Exit with failure
245 exit(self::FAILURE);
246 }
247 }
248
249 //Parse string
250 $tidy->parseString($content, $this->config, 'utf8');
251
252 //Fix error buffer
253 //XXX: don't care about theses errors, tidy is here to fix...
254 #if (!empty($tidy->errorBuffer)) {
255 # var_dump($tidy->errorBuffer);
256 # die('Tidy errors');
257 #}
258
259 //Load simplexml
260 //XXX: trash all xmlns= broken tags
261 $sx = new \SimpleXMLElement(str_replace(['xmlns=', 'xlink:href='], ['xns=', 'href='], $tidy));
262
263 //Process daily
264 if ($day == 'daily') {
265 //Iterate on each link containing data
266 foreach($sx->xpath('//a[contains(@class,"daily-forecast-card")]') as $node) {
267 //Get date
268 $dsm = trim($node->div[0]->h2[0]->span[1]);
269
270 //Get temperature
271 $temperature = str_replace('°', '', $node->div[0]->div[0]->span[0]);
272
273 //Get rainrisk
274 $rainrisk = trim(str_replace('%', '', $node->div[1]))/100;
275
276 //Store data
277 $data[$zipcode][$dsm]['daily'] = [
278 'temperature' => $temperature,
279 'rainrisk' => $rainrisk
280 ];
281 }
282 //Process hourly
283 } else {
284 //Iterate on each div containing data
285 #(string)$sx->xpath('//div[@class="hourly-card-nfl"]')[0]->attributes()->value
286 #/html/body/div[1]/div[5]/div[1]/div[1]/div[1]/div[1]/div[1]/div/h2/span[1]
287 foreach($sx->xpath('//div[@data-shared="false"]') as $node) {
288 //Get hour
289 $hour = trim(str_replace(' h', '', $node->div[0]->div[0]->div[0]->div[0]->div[0]->h2[0]));
290
291 //Compute dsm from day (1=d,2=d+1,3=d+2)
292 $dsm = (new \DateTime('+'.($day - 1).' day'))->format('d/m');
293
294 //Get temperature
295 $temperature = str_replace('°', '', $node->div[0]->div[0]->div[0]->div[0]->div[1]);
296
297 //Get realfeel
298 $realfeel = trim(str_replace(['RealFeel®', '°'], '', $node->div[0]->div[0]->div[0]->div[1]->div[0]->div[0]->div[0]));
299
300 //Get rainrisk
301 $rainrisk = floatval(str_replace('%', '', trim($node->div[0]->div[0]->div[0]->div[2]->div[0]))/100);
302
303 //Set rainfall to 0 (mm)
304 $rainfall = 0;
305
306 //Iterate on each entry
307 //TODO: wind and other infos are present in $node->div[1]->div[0]->div[1]->div[0]->p
308 foreach($node->div[1]->div[0]->div[1]->div[0]->p as $p) {
309 //Lookup for rain entry if present
310 if (in_array(trim($p), ['Rain', 'Pluie'])) {
311 //Get rainfall
312 $rainfall = floatval(str_replace(' mm', '', $p->span[0]));
313 }
314 }
315
316 //Store data
317 $data[$zipcode][$dsm][$hour] = [
318 'temperature' => $temperature,
319 'realfeel' => $realfeel,
320 'rainrisk' => $rainrisk,
321 'rainfall' => $rainfall
322 ];
323 }
324 }
325
326 //Cleanup
327 unset($sx);
328 }
329 }
330
331 //Iterate on types
332 foreach($types as $type => $sessions) {
333 //Iterate on each type
334 foreach($sessions as $sessionId => $session) {
335 //Get zipcode
336 $zipcode = $session->getLocation()->getZipcode();
337
338 //Get start
339 $start = $session->getStart();
340
341 //Daily type
342 if ($type == 'daily') {
343 //Set period
344 $period = [ $start ];
345 //Hourly type
346 } else {
347 //Get stop
348 $stop = $session->getStop();
349
350 //Compute period
351 $period = new \DatePeriod(
352 //Start from begin
353 $start,
354 //Iterate on each hour
355 new \DateInterval('PT1H'),
356 //End with begin + length
357 $stop
358 );
359 }
360
361 //Set meteo
362 $meteo = [
363 'rainfall' => null,
364 'rainrisk' => null,
365 'realfeel' => [],
366 'realfeelmin' => null,
367 'realfeelmax' => null,
368 'temperature' => [],
369 'temperaturemin' => null,
370 'temperaturemax' => null
371 ];
372
373 //Iterate on the period
374 foreach($period as $time) {
375 //Set dsm
376 $dsm = $time->format('d/m');
377
378 //Set hour
379 $hour = $type=='daily'?$type:$time->format('H');
380
381 //Check data availability
382 //XXX: sometimes startup delay causes weather data to be unavailable for session first hour
383 if (!isset($data[$zipcode][$dsm][$hour])) {
384 //Skip unavailable data
385 continue;
386 }
387
388 //Set info alias
389 $info = $data[$zipcode][$dsm][$hour];
390
391 //Check if rainrisk is higher
392 if ($meteo['rainrisk'] === null || $info['rainrisk'] > $meteo['rainrisk']) {
393 //Set highest rain risk
394 $meteo['rainrisk'] = floatval($info['rainrisk']);
395 }
396
397 //Check if rainfall is set
398 if (isset($info['rainfall'])) {
399 //Set rainfall sum
400 $meteo['rainfall'] += floatval($info['rainfall']);
401 }
402
403 //Add temperature
404 $meteo['temperature'][$hour] = $info['temperature'];
405
406 //Hourly type
407 if ($type != 'daily') {
408 //Check min temperature
409 if ($meteo['temperaturemin'] === null || $info['temperature'] < $meteo['temperaturemin']) {
410 $meteo['temperaturemin'] = floatval($info['temperature']);
411 }
412
413 //Check max temperature
414 if ($meteo['temperaturemax'] === null || $info['temperature'] > $meteo['temperaturemax']) {
415 $meteo['temperaturemax'] = floatval($info['temperature']);
416 }
417 }
418
419 //Check if realfeel is set
420 if (isset($info['realfeel'])) {
421 //Add realfeel
422 $meteo['realfeel'][$hour] = $info['realfeel'];
423
424 //Check min realfeel
425 if ($meteo['realfeelmin'] === null || $info['realfeel'] < $meteo['realfeelmin']) {
426 $meteo['realfeelmin'] = floatval($info['realfeel']);
427 }
428
429 //Check max realfeel
430 if ($meteo['realfeelmax'] === null || $info['realfeel'] > $meteo['realfeelmax']) {
431 $meteo['realfeelmax'] = floatval($info['realfeel']);
432 }
433 }
434 }
435
436 //Check if rainfall is set and differ
437 if ($session->getRainfall() !== $meteo['rainfall']) {
438 //Set rainfall
439 $session->setRainfall($meteo['rainfall']);
440 }
441
442 //Check if rainrisk differ
443 if ($session->getRainrisk() !== $meteo['rainrisk']) {
444 //Set rainrisk
445 $session->setRainrisk($meteo['rainrisk']);
446 }
447
448 //Check realfeel array
449 if ($meteo['realfeel'] !== []) {
450 //Compute realfeel
451 $realfeel = floatval(round(array_sum($meteo['realfeel'])/count($meteo['realfeel']),1));
452
453 //Check if realfeel differ
454 if ($session->getRealfeel() !== $realfeel) {
455 //Set average realfeel
456 $session->setRealfeel($realfeel);
457 }
458
459 //Check if realfeelmin differ
460 if ($session->getRealfeelmin() !== $meteo['realfeelmin']) {
461 //Set realfeelmin
462 $session->setRealfeelmin($meteo['realfeelmin']);
463 }
464
465 //Check if realfeelmax differ
466 if ($session->getRealfeelmax() !== $meteo['realfeelmax']) {
467 //Set realfeelmax
468 $session->setRealfeelmax($meteo['realfeelmax']);
469 }
470 }
471
472 //Check temperature array
473 if ($meteo['temperature'] !== []) {
474 //Compute temperature
475 $temperature = floatval(round(array_sum($meteo['temperature'])/count($meteo['temperature']),1));
476
477 //Check if temperature differ
478 if ($session->getTemperature() !== $temperature) {
479 //Set average temperature
480 $session->setTemperature($temperature);
481 }
482
483 //Check if temperaturemin differ
484 if ($session->getTemperaturemin() !== $meteo['temperaturemin']) {
485 //Set temperaturemin
486 $session->setTemperaturemin($meteo['temperaturemin']);
487 }
488
489 //Check if temperaturemax differ
490 if ($session->getTemperaturemax() !== $meteo['temperaturemax']) {
491 //Set temperaturemax
492 $session->setTemperaturemax($meteo['temperaturemax']);
493 }
494 }
495 }
496 }
497
498 //Flush to get the ids
499 $this->doctrine->getManager()->flush();
500
501 //Close curl handler
502 $this->curl_close();
503
504 //Return success
505 return self::SUCCESS;
506 }
507
508 /**
509 * Init curl handler
510 *
511 * @return bool|void Return success or exit
512 */
513 function curl_init() {
514 //Init curl
515 if (($this->ch = curl_init()) === false) {
516 //Display error
517 echo 'Curl init failed: '.curl_error($this->ch)."\n";
518 //Exit with failure
519 exit(self::FAILURE);
520 }
521
522 //Set curl options
523 if (
524 curl_setopt_array(
525 $this->ch,
526 [
527 //Force http2
528 CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,
529 //Set http headers
530 CURLOPT_HTTPHEADER => [
531 //XXX: it seems that you can disable akamai fucking protection with Pragma: akamai-x-cache-off
532 //XXX: see https://support.globaldots.com/hc/en-us/articles/115003996705-Akamai-Pragma-Headers-overview
533 #'Pragma: akamai-x-cache-off',
534 //XXX: working curl command
535 #curl --http2 --cookie file.jar --cookie-jar file.jar -v -i -k -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' -H 'Accept-Language: en-GB,en;q=0.9' -H 'Cache-Control: no-cache' -H 'Connection: keep-alive' -H 'Host: www.accuweather.com' -H 'Pragma: no-cache' -H 'Upgrade-Insecure-Requests: 1' -H 'User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36' 'https://www.accuweather.com/'
536 //Set accept
537 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
538 //Set accept language
539 'Accept-Language: en-GB,en;q=0.9',
540 //Disable cache
541 'Cache-Control: no-cache',
542 //Keep connection alive
543 'Connection: keep-alive',
544 //Disable cache
545 'Pragma: no-cache',
546 //Force secure requests
547 'Upgrade-Insecure-Requests: 1',
548 //Set user agent
549 'User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
550 //Force akamai cookie
551 //XXX: seems to come from http request
552 'Cookie: AKA_A2=A',
553 ],
554 //Enable cookie
555 CURLOPT_COOKIEFILE => '',
556 //Disable location following
557 CURLOPT_FOLLOWLOCATION => false,
558 //Set url
559 #CURLOPT_URL => $url = 'https://www.accuweather.com/',
560 //Return headers too
561 CURLOPT_HEADER => true,
562 //Return content
563 CURLOPT_RETURNTRANSFER => true,
564
565 //XXX: debug
566 CURLINFO_HEADER_OUT => true
567 ]
568 ) === false
569 ) {
570 //Display error
571 echo 'Curl setopt array failed: '.curl_error($this->ch)."\n";
572 //Exit with failure
573 exit(self::FAILURE);
574 }
575
576 //Return success
577 return true;
578 }
579
580 /**
581 * Get url
582 *
583 * @return string|void Return url content or exit
584 */
585 function curl_get($url) {
586 //Set url to fetch
587 if (curl_setopt($this->ch, CURLOPT_URL, $url) === false) {
588 //Display error
589 echo 'Setopt for '.$url.' failed: '.curl_error($this->ch)."\n";
590
591 //Close curl handler
592 curl_close($this->ch);
593
594 //Exit with failure
595 exit(self::FAILURE);
596 }
597
598 //Check return status
599 if (($response = curl_exec($this->ch)) === false) {
600 //Display error
601 echo 'Get for '.$url.' failed: '.curl_error($this->ch)."\n";
602
603 //Display sent headers
604 var_dump(curl_getinfo($this->ch, CURLINFO_HEADER_OUT));
605
606 //Display response
607 var_dump($response);
608
609 //Close curl handler
610 curl_close($this->ch);
611
612 //Exit with failure
613 exit(self::FAILURE);
614 }
615
616 //Get header size
617 if (empty($hs = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE))) {
618 //Display error
619 echo 'Getinfo for '.$url.' failed: '.curl_error($this->ch)."\n";
620
621 //Display sent headers
622 var_dump(curl_getinfo($this->ch, CURLINFO_HEADER_OUT));
623
624 //Display response
625 var_dump($response);
626
627 //Close curl handler
628 curl_close($this->ch);
629
630 //Exit with failure
631 exit(self::FAILURE);
632 }
633
634 //Get header
635 if (empty($header = substr($response, 0, $hs))) {
636 //Display error
637 echo 'Header for '.$url.' empty: '.curl_error($this->ch)."\n";
638
639 //Display sent headers
640 var_dump(curl_getinfo($this->ch, CURLINFO_HEADER_OUT));
641
642 //Display response
643 var_dump($response);
644
645 //Close curl handler
646 curl_close($this->ch);
647
648 //Exit with failure
649 exit(self::FAILURE);
650 }
651
652 //Check request success
653 if (strlen($header) <= 10 || substr($header, 0, 10) !== 'HTTP/2 200') {
654 //Display error
655 echo 'Status for '.$url.' failed: '.curl_error($this->ch)."\n";
656
657 //Display sent headers
658 var_dump(curl_getinfo($this->ch, CURLINFO_HEADER_OUT));
659
660 //Display response
661 var_dump($header);
662
663 //Close curl handler
664 curl_close($this->ch);
665
666 //Exit with failure
667 exit(self::FAILURE);
668 }
669
670 //Return content
671 return substr($response, $hs);
672 }
673
674 /**
675 * Close curl handler
676 *
677 * @return bool Return success or failure
678 */
679 function curl_close() {
680 return curl_close($this->ch);
681 }
682 }