]> Raphaël G. Git Repositories - airbundle/blob - Command/WeatherCommand.php
List dance sessions
[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 //Skip 4th day
181 //XXX: accuweather only allow until 3rd day
182 if ($day >= 4) {
183 continue;
184 }
185
186 //Check if zipcode date is set
187 if (!isset($zipcodes[$zipcode][$day])) {
188 $zipcodes[$zipcode][$day] = [ $sessionId => $sessionId ];
189 } else {
190 $zipcodes[$zipcode][$day][$sessionId] = $sessionId;
191 }
192 }
193 }
194
195 //Process daily accuweather
196 if ($command == 'rapsysair:weather:daily' || $command == 'rapsysair:weather') {
197 //Fetch daily sessions to attribute
198 $types['daily'] = $this->doctrine->getRepository(Session::class)->findAllPendingDailyWeather();
199
200 //Iterate on each session
201 foreach($types['daily'] as $sessionId => $session) {
202 //Get zipcode
203 $zipcode = $session->getLocation()->getZipcode();
204
205 //Get start
206 $start = $session->getStart();
207
208 //Set start day
209 $day = 'daily';
210
211 //Check if zipcode is set
212 if (!isset($zipcodes[$zipcode])) {
213 $zipcodes[$zipcode] = [];
214 }
215
216 //Check if zipcode date is set
217 if (!isset($zipcodes[$zipcode][$day])) {
218 $zipcodes[$zipcode][$day] = [ $sessionId => $sessionId ];
219 } else {
220 $zipcodes[$zipcode][$day][$sessionId] = $sessionId;
221 }
222 }
223 }
224
225 //Init curl
226 $this->curl_init();
227
228 //Init data array
229 $data = [];
230
231 //Iterate on zipcodes
232 foreach($zipcodes as $zipcode => $days) {
233 //Iterate on days
234 foreach($days as $day => $null) {
235 //Try to load content from cache
236 if (!is_file($file = $tmpdir.'/'.$zipcode.'.'.$day.'.html') || stat($file)['ctime'] <= (time() - ($day == 'daily' ? 4 : 2)*3600) || ($content = file_get_contents($file)) === false) {
237 //Prevent timing detection
238 //XXX: from 0.1 to 5 seconds
239 usleep(rand(1,50) * 100000);
240
241 //Get content
242 //TODO: for daily we may load data for requested quarter of the day
243 $content = $this->curl_get($day == 'daily' ? $this->accuweather['daily'][$zipcode] : $this->accuweather['hourly'][$zipcode].$day);
244
245 //Store it
246 if (file_put_contents($tmpdir.'/'.$zipcode.'.'.$day.'.html', $content) === false) {
247 //Display error
248 echo 'Write to '.$tmpdir.'/'.$zipcode.'.'.$day.'.html failed'."\n";
249
250 //Exit with failure
251 exit(self::FAILURE);
252 }
253 }
254
255 //Parse string
256 $tidy->parseString($content, $this->config, 'utf8');
257
258 //Fix error buffer
259 //XXX: don't care about theses errors, tidy is here to fix...
260 #if (!empty($tidy->errorBuffer)) {
261 # var_dump($tidy->errorBuffer);
262 # die('Tidy errors');
263 #}
264
265 //Load simplexml
266 //XXX: trash all xmlns= broken tags
267 $sx = new \SimpleXMLElement(str_replace(['xmlns=', 'xlink:href='], ['xns=', 'href='], (string)$tidy));
268
269 //Process daily
270 if ($day == 'daily') {
271 //Iterate on each link containing data
272 foreach($sx->xpath('//a[contains(@class,"daily-forecast-card")]') as $node) {
273 //Get date
274 $dsm = trim((string)$node->div[0]->h2[0]->span[1]);
275
276 //Get temperature
277 $temperature = str_replace('°', '', (string)$node->div[0]->div[0]->span[0]);
278
279 //Get rainrisk
280 $rainrisk = trim(str_replace('%', '', (string)$node->div[1]))/100;
281
282 //Store data
283 $data[$zipcode][$dsm]['daily'] = [
284 'temperature' => $temperature,
285 'rainrisk' => $rainrisk
286 ];
287 }
288 //Process hourly
289 } else {
290 //Iterate on each div containing data
291 #(string)$sx->xpath('//div[@class="hourly-card-nfl"]')[0]->attributes()->value
292 #/html/body/div[1]/div[5]/div[1]/div[1]/div[1]/div[1]/div[1]/div/h2/span[1]
293 foreach($sx->xpath('//div[@data-shared="false"]') as $node) {
294 //Get hour
295 $hour = trim(str_replace(' h', '', (string)$node->div[0]->div[0]->div[0]->div[0]->div[0]->h2[0]));
296
297 //Compute dsm from day (1=d,2=d+1,3=d+2)
298 $dsm = (new \DateTime('+'.($day - 1).' day'))->format('d/m');
299
300 //Get temperature
301 $temperature = str_replace('°', '', (string)$node->div[0]->div[0]->div[0]->div[0]->div[1]);
302
303 //Get realfeel
304 $realfeel = trim(str_replace(['RealFeel®', '°'], '', (string)$node->div[0]->div[0]->div[0]->div[1]->div[0]->div[0]->div[0]));
305
306 //Get rainrisk
307 $rainrisk = floatval(str_replace('%', '', trim((string)$node->div[0]->div[0]->div[0]->div[2]->div[0]))/100);
308
309 //Set rainfall to 0 (mm)
310 $rainfall = 0;
311
312 //Iterate on each entry
313 //TODO: wind and other infos are present in $node->div[1]->div[0]->div[1]->div[0]->p
314 foreach($node->div[1]->div[0]->div[1]->div[0]->p as $p) {
315 //Lookup for rain entry if present
316 if (in_array(trim((string)$p), ['Rain', 'Pluie'])) {
317 //Get rainfall
318 $rainfall = floatval(str_replace(' mm', '', (string)$p->span[0]));
319 }
320 }
321
322 //Store data
323 $data[$zipcode][$dsm][$hour] = [
324 'temperature' => $temperature,
325 'realfeel' => $realfeel,
326 'rainrisk' => $rainrisk,
327 'rainfall' => $rainfall
328 ];
329 }
330 }
331
332 //Cleanup
333 unset($sx);
334 }
335 }
336
337 //Iterate on types
338 foreach($types as $type => $sessions) {
339 //Iterate on each type
340 foreach($sessions as $sessionId => $session) {
341 //Get zipcode
342 $zipcode = $session->getLocation()->getZipcode();
343
344 //Get start
345 $start = $session->getStart();
346
347 //Daily type
348 if ($type == 'daily') {
349 //Set period
350 $period = [ $start ];
351 //Hourly type
352 } else {
353 //Get stop
354 $stop = $session->getStop();
355
356 //Compute period
357 $period = new \DatePeriod(
358 //Start from begin
359 $start,
360 //Iterate on each hour
361 new \DateInterval('PT1H'),
362 //End with begin + length
363 $stop
364 );
365 }
366
367 //Set meteo
368 $meteo = [
369 'rainfall' => null,
370 'rainrisk' => null,
371 'realfeel' => [],
372 'realfeelmin' => null,
373 'realfeelmax' => null,
374 'temperature' => [],
375 'temperaturemin' => null,
376 'temperaturemax' => null
377 ];
378
379 //Iterate on the period
380 foreach($period as $time) {
381 //Set dsm
382 $dsm = $time->format('d/m');
383
384 //Set hour
385 $hour = $type=='daily'?$type:$time->format('H');
386
387 //Check data availability
388 //XXX: sometimes startup delay causes weather data to be unavailable for session first hour
389 if (!isset($data[$zipcode][$dsm][$hour])) {
390 //Skip unavailable data
391 continue;
392 }
393
394 //Set info alias
395 $info = $data[$zipcode][$dsm][$hour];
396
397 //Check if rainrisk is higher
398 if ($meteo['rainrisk'] === null || $info['rainrisk'] > $meteo['rainrisk']) {
399 //Set highest rain risk
400 $meteo['rainrisk'] = floatval($info['rainrisk']);
401 }
402
403 //Check if rainfall is set
404 if (isset($info['rainfall'])) {
405 //Set rainfall sum
406 $meteo['rainfall'] += floatval($info['rainfall']);
407 }
408
409 //Add temperature
410 $meteo['temperature'][$hour] = $info['temperature'];
411
412 //Hourly type
413 if ($type != 'daily') {
414 //Check min temperature
415 if ($meteo['temperaturemin'] === null || $info['temperature'] < $meteo['temperaturemin']) {
416 $meteo['temperaturemin'] = floatval($info['temperature']);
417 }
418
419 //Check max temperature
420 if ($meteo['temperaturemax'] === null || $info['temperature'] > $meteo['temperaturemax']) {
421 $meteo['temperaturemax'] = floatval($info['temperature']);
422 }
423 }
424
425 //Check if realfeel is set
426 if (isset($info['realfeel'])) {
427 //Add realfeel
428 $meteo['realfeel'][$hour] = $info['realfeel'];
429
430 //Check min realfeel
431 if ($meteo['realfeelmin'] === null || $info['realfeel'] < $meteo['realfeelmin']) {
432 $meteo['realfeelmin'] = floatval($info['realfeel']);
433 }
434
435 //Check max realfeel
436 if ($meteo['realfeelmax'] === null || $info['realfeel'] > $meteo['realfeelmax']) {
437 $meteo['realfeelmax'] = floatval($info['realfeel']);
438 }
439 }
440 }
441
442 //Check if rainfall is set and differ
443 if ($session->getRainfall() !== $meteo['rainfall']) {
444 //Set rainfall
445 $session->setRainfall($meteo['rainfall']);
446 }
447
448 //Check if rainrisk differ
449 if ($session->getRainrisk() !== $meteo['rainrisk']) {
450 //Set rainrisk
451 $session->setRainrisk($meteo['rainrisk']);
452 }
453
454 //Check realfeel array
455 if ($meteo['realfeel'] !== []) {
456 //Compute realfeel
457 $realfeel = floatval(round(array_sum($meteo['realfeel'])/count($meteo['realfeel']),1));
458
459 //Check if realfeel differ
460 if ($session->getRealfeel() !== $realfeel) {
461 //Set average realfeel
462 $session->setRealfeel($realfeel);
463 }
464
465 //Check if realfeelmin differ
466 if ($session->getRealfeelmin() !== $meteo['realfeelmin']) {
467 //Set realfeelmin
468 $session->setRealfeelmin($meteo['realfeelmin']);
469 }
470
471 //Check if realfeelmax differ
472 if ($session->getRealfeelmax() !== $meteo['realfeelmax']) {
473 //Set realfeelmax
474 $session->setRealfeelmax($meteo['realfeelmax']);
475 }
476 }
477
478 //Check temperature array
479 if ($meteo['temperature'] !== []) {
480 //Compute temperature
481 $temperature = floatval(round(array_sum($meteo['temperature'])/count($meteo['temperature']),1));
482
483 //Check if temperature differ
484 if ($session->getTemperature() !== $temperature) {
485 //Set average temperature
486 $session->setTemperature($temperature);
487 }
488
489 //Check if temperaturemin differ
490 if ($session->getTemperaturemin() !== $meteo['temperaturemin']) {
491 //Set temperaturemin
492 $session->setTemperaturemin($meteo['temperaturemin']);
493 }
494
495 //Check if temperaturemax differ
496 if ($session->getTemperaturemax() !== $meteo['temperaturemax']) {
497 //Set temperaturemax
498 $session->setTemperaturemax($meteo['temperaturemax']);
499 }
500 }
501 }
502 }
503
504 //Flush to get the ids
505 $this->doctrine->getManager()->flush();
506
507 //Close curl handler
508 $this->curl_close();
509
510 //Return success
511 return self::SUCCESS;
512 }
513
514 /**
515 * Init curl handler
516 *
517 * @return bool|void Return success or exit
518 */
519 function curl_init() {
520 //Init curl
521 if (($this->ch = curl_init()) === false) {
522 //Display error
523 echo 'Curl init failed: '.curl_error($this->ch)."\n";
524 //Exit with failure
525 exit(self::FAILURE);
526 }
527
528 //Set curl options
529 if (
530 curl_setopt_array(
531 $this->ch,
532 [
533 //Force http2
534 CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,
535 //Set http headers
536 CURLOPT_HTTPHEADER => [
537 //XXX: it seems that you can disable akamai fucking protection with Pragma: akamai-x-cache-off
538 //XXX: see https://support.globaldots.com/hc/en-us/articles/115003996705-Akamai-Pragma-Headers-overview
539 #'Pragma: akamai-x-cache-off',
540 //XXX: working curl command
541 #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/'
542 //Set accept
543 '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',
544 //Set accept language
545 'Accept-Language: en-GB,en;q=0.9',
546 //Disable cache
547 'Cache-Control: no-cache',
548 //Keep connection alive
549 'Connection: keep-alive',
550 //Disable cache
551 'Pragma: no-cache',
552 //Force secure requests
553 'Upgrade-Insecure-Requests: 1',
554 //Set user agent
555 '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',
556 //Force akamai cookie
557 //XXX: seems to come from http request
558 'Cookie: AKA_A2=A',
559 ],
560 //Enable cookie
561 CURLOPT_COOKIEFILE => '',
562 //Disable location following
563 CURLOPT_FOLLOWLOCATION => false,
564 //Set url
565 #CURLOPT_URL => $url = 'https://www.accuweather.com/',
566 //Return headers too
567 CURLOPT_HEADER => true,
568 //Return content
569 CURLOPT_RETURNTRANSFER => true,
570
571 //XXX: debug
572 CURLINFO_HEADER_OUT => true
573 ]
574 ) === false
575 ) {
576 //Display error
577 echo 'Curl setopt array failed: '.curl_error($this->ch)."\n";
578 //Exit with failure
579 exit(self::FAILURE);
580 }
581
582 //Return success
583 return true;
584 }
585
586 /**
587 * Get url
588 *
589 * @return string|void Return url content or exit
590 */
591 function curl_get($url) {
592 //Set url to fetch
593 if (curl_setopt($this->ch, CURLOPT_URL, $url) === false) {
594 //Display error
595 echo 'Setopt for '.$url.' failed: '.curl_error($this->ch)."\n";
596
597 //Close curl handler
598 curl_close($this->ch);
599
600 //Exit with failure
601 exit(self::FAILURE);
602 }
603
604 //Check return status
605 if (($response = curl_exec($this->ch)) === false) {
606 //Display error
607 echo 'Get for '.$url.' failed: '.curl_error($this->ch)."\n";
608
609 //Display sent headers
610 var_dump(curl_getinfo($this->ch, CURLINFO_HEADER_OUT));
611
612 //Display response
613 var_dump($response);
614
615 //Close curl handler
616 curl_close($this->ch);
617
618 //Exit with failure
619 exit(self::FAILURE);
620 }
621
622 //Get header size
623 if (empty($hs = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE))) {
624 //Display error
625 echo 'Getinfo for '.$url.' failed: '.curl_error($this->ch)."\n";
626
627 //Display sent headers
628 var_dump(curl_getinfo($this->ch, CURLINFO_HEADER_OUT));
629
630 //Display response
631 var_dump($response);
632
633 //Close curl handler
634 curl_close($this->ch);
635
636 //Exit with failure
637 exit(self::FAILURE);
638 }
639
640 //Get header
641 if (empty($header = substr($response, 0, $hs))) {
642 //Display error
643 echo 'Header for '.$url.' empty: '.curl_error($this->ch)."\n";
644
645 //Display sent headers
646 var_dump(curl_getinfo($this->ch, CURLINFO_HEADER_OUT));
647
648 //Display response
649 var_dump($response);
650
651 //Close curl handler
652 curl_close($this->ch);
653
654 //Exit with failure
655 exit(self::FAILURE);
656 }
657
658 //Check request success
659 if (strlen($header) <= 10 || substr($header, 0, 10) !== 'HTTP/2 200') {
660 //Display error
661 echo 'Status for '.$url.' failed: '.curl_error($this->ch)."\n";
662
663 //Display sent headers
664 var_dump(curl_getinfo($this->ch, CURLINFO_HEADER_OUT));
665
666 //Display response
667 var_dump($header);
668
669 //Close curl handler
670 curl_close($this->ch);
671
672 //Exit with failure
673 exit(self::FAILURE);
674 }
675
676 //Return content
677 return substr($response, $hs);
678 }
679
680 /**
681 * Close curl handler
682 *
683 * @return bool Return success or failure
684 */
685 function curl_close() {
686 return curl_close($this->ch);
687 }
688 }