1 <?php 
declare(strict_types
=1); 
   4  * This file is part of the Rapsys AirBundle package. 
   6  * (c) Raphaël Gertz <symfony@rapsys.eu> 
   8  * For the full copyright and license information, please view the LICENSE 
   9  * file that was distributed with this source code. 
  12 namespace Rapsys\AirBundle\Command
; 
  14 use Doctrine\Bundle\DoctrineBundle\Command\DoctrineCommand
; 
  15 use Doctrine\Persistence\ManagerRegistry
; 
  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
; 
  22 use Rapsys\AirBundle\Entity\Session
; 
  24 class WeatherCommand 
extends DoctrineCommand 
{ 
  25         //Set failure constant 
  28         ///Set success constant 
  33                 //Mostly useless in fact 
  35                 //Required to simplify simplexml transition 
  37                 //Required to avoid xml errors 
  38                 'quote-nbsp' => false, 
  39                 //Required to fix code 
  43         ///Set accuweather uris 
  44         private $accuweather = [ 
  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=' 
  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' 
  80         ///Set manager registry 
  86         ///Weather command constructor 
  87         public function __construct(ManagerRegistry 
$doctrine, Filesystem 
$filesystem) { 
  88                 parent
::__construct($doctrine); 
  91                 $this->doctrine 
= $doctrine; 
  94                 $this->filesystem 
= $filesystem; 
  97         ///Configure attribute command 
  98         protected function configure() { 
 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']); 
 111         ///Process the attribution 
 112         protected function execute(InputInterface 
$input, OutputInterface 
$output): int { 
 114                 $kernel = $this->getApplication()->getKernel(); 
 117                 $tmpdir = $kernel->getContainer()->getParameter('kernel.project_dir').'/var/cache/weather'; 
 120                 //XXX: worst case scenario we have 3 files per zipcode plus daily 
 121                 if (!is_dir($tmpdir)) { 
 124                                 $this->filesystem
->mkdir($tmpdir, 0775); 
 125                         } catch (IOException 
$exception) { 
 127                                 echo 'Create dir '.$exception->getPath().' failed'."\n"; 
 140                 //Init zipcodes array 
 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(); 
 151                         //Iterate on each session 
 152                         foreach($types['hourly'] as $sessionId => $session) { 
 154                                 $zipcode = $session->getLocation()->getZipcode(); 
 157                                 $start = $session->getStart(); 
 160                                 $day = $start->diff((new \
DateTime('now'))->setTime(0, 0, 0))->d + 
1; 
 162                                 //Check if zipcode is set 
 163                                 if (!isset($zipcodes[$zipcode])) { 
 164                                         $zipcodes[$zipcode] = []; 
 167                                 //Check if zipcode date is set 
 168                                 if (!isset($zipcodes[$zipcode][$day])) { 
 169                                         $zipcodes[$zipcode][$day] = [ $sessionId => $sessionId ]; 
 171                                         $zipcodes[$zipcode][$day][$sessionId] = $sessionId; 
 175                                 $stop = $session->getStop(); 
 178                                 $day = $stop->diff((new \
DateTime('now'))->setTime(0, 0, 0))->d + 
1; 
 181                                 //XXX: accuweather only allow until 3rd day 
 186                                 //Check if zipcode date is set 
 187                                 if (!isset($zipcodes[$zipcode][$day])) { 
 188                                         $zipcodes[$zipcode][$day] = [ $sessionId => $sessionId ]; 
 190                                         $zipcodes[$zipcode][$day][$sessionId] = $sessionId; 
 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(); 
 200                         //Iterate on each session 
 201                         foreach($types['daily'] as $sessionId => $session) { 
 203                                 $zipcode = $session->getLocation()->getZipcode(); 
 206                                 $start = $session->getStart(); 
 211                                 //Check if zipcode is set 
 212                                 if (!isset($zipcodes[$zipcode])) { 
 213                                         $zipcodes[$zipcode] = []; 
 216                                 //Check if zipcode date is set 
 217                                 if (!isset($zipcodes[$zipcode][$day])) { 
 218                                         $zipcodes[$zipcode][$day] = [ $sessionId => $sessionId ]; 
 220                                         $zipcodes[$zipcode][$day][$sessionId] = $sessionId; 
 231                 //Iterate on zipcodes 
 232                 foreach($zipcodes as $zipcode => $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);  
 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); 
 246                                         if (file_put_contents($tmpdir.'/'.$zipcode.'.'.$day.'.html', $content) === false) { 
 248                                                 echo 'Write to '.$tmpdir.'/'.$zipcode.'.'.$day.'.html failed'."\n"; 
 256                                 $tidy->parseString($content, $this->config
, 'utf8'); 
 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'); 
 266                                 //XXX: trash all xmlns= broken tags 
 267                                 $sx = new \
SimpleXMLElement(str_replace(['xmlns=', 'xlink:href='], ['xns=', 'href='], (string)$tidy)); 
 270                                 if ($day == 'daily') { 
 271                                         //Iterate on each link containing data 
 272                                         foreach($sx->xpath('//a[contains(@class,"daily-forecast-card")]') as $node) { 
 274                                                 $dsm = trim((string)$node->div
[0]->h2
[0]->span
[1]); 
 277                                                 $temperature = str_replace('°', '', (string)$node->div
[0]->div
[0]->span
[0]); 
 280                                                 $rainrisk = trim(str_replace('%', '', (string)$node->div
[1]))/100; 
 283                                                 $data[$zipcode][$dsm]['daily'] = [ 
 284                                                         'temperature' => $temperature, 
 285                                                         'rainrisk' => $rainrisk 
 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) { 
 295                                                 $hour = trim(str_replace(' h', '', (string)$node->div
[0]->div
[0]->div
[0]->div
[0]->div
[0]->h2
[0])); 
 297                                                 //Compute dsm from day (1=d,2=d+1,3=d+2) 
 298                                                 $dsm = (new \
DateTime('+'.($day - 1).' day'))->format('d/m'); 
 301                                                 $temperature = str_replace('°', '', (string)$node->div
[0]->div
[0]->div
[0]->div
[0]->div
[1]); 
 304                                                 $realfeel = trim(str_replace(['RealFeel®', '°'], '', (string)$node->div
[0]->div
[0]->div
[0]->div
[1]->div
[0]->div
[0]->div
[0])); 
 307                                                 $rainrisk = floatval(str_replace('%', '', trim((string)$node->div
[0]->div
[0]->div
[0]->div
[2]->div
[0]))/100); 
 309                                                 //Set rainfall to 0 (mm) 
 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'])) { 
 318                                                                 $rainfall = floatval(str_replace(' mm', '', (string)$p->span
[0])); 
 323                                                 $data[$zipcode][$dsm][$hour] = [ 
 324                                                         'temperature' => $temperature, 
 325                                                         'realfeel' => $realfeel, 
 326                                                         'rainrisk' => $rainrisk, 
 327                                                         'rainfall' => $rainfall 
 338                 foreach($types as $type => $sessions) { 
 339                         //Iterate on each type 
 340                         foreach($sessions as $sessionId => $session) { 
 342                                 $zipcode = $session->getLocation()->getZipcode(); 
 345                                 $start = $session->getStart(); 
 348                                 if ($type == 'daily') { 
 350                                         $period = [ $start ]; 
 354                                         $stop = $session->getStop(); 
 357                                         $period = new \
DatePeriod( 
 360                                                 //Iterate on each hour 
 361                                                 new \
DateInterval('PT1H'), 
 362                                                 //End with begin + length 
 372                                         'realfeelmin' => null, 
 373                                         'realfeelmax' => null, 
 375                                         'temperaturemin' => null, 
 376                                         'temperaturemax' => null 
 379                                 //Iterate on the period 
 380                                 foreach($period as $time) { 
 382                                         $dsm = $time->format('d/m'); 
 385                                         $hour = $type=='daily'?$type:$time->format('H'); 
 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 
 395                                         $info = $data[$zipcode][$dsm][$hour]; 
 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']); 
 403                                         //Check if rainfall is set 
 404                                         if (isset($info['rainfall'])) { 
 406                                                 $meteo['rainfall'] +
= floatval($info['rainfall']); 
 410                                         $meteo['temperature'][$hour] = $info['temperature']; 
 413                                         if ($type != 'daily') { 
 414                                                 //Check min temperature 
 415                                                 if ($meteo['temperaturemin'] === null || $info['temperature'] < $meteo['temperaturemin']) { 
 416                                                         $meteo['temperaturemin'] = floatval($info['temperature']); 
 419                                                 //Check max temperature 
 420                                                 if ($meteo['temperaturemax'] === null || $info['temperature'] > $meteo['temperaturemax']) { 
 421                                                         $meteo['temperaturemax'] = floatval($info['temperature']); 
 425                                         //Check if realfeel is set 
 426                                         if (isset($info['realfeel'])) { 
 428                                                 $meteo['realfeel'][$hour] = $info['realfeel']; 
 431                                                 if ($meteo['realfeelmin'] === null || $info['realfeel'] < $meteo['realfeelmin']) { 
 432                                                         $meteo['realfeelmin'] = floatval($info['realfeel']); 
 436                                                 if ($meteo['realfeelmax'] === null || $info['realfeel'] > $meteo['realfeelmax']) { 
 437                                                         $meteo['realfeelmax'] = floatval($info['realfeel']); 
 442                                 //Check if rainfall is set and differ 
 443                                 if ($session->getRainfall() !== $meteo['rainfall']) { 
 445                                         $session->setRainfall($meteo['rainfall']); 
 448                                 //Check if rainrisk differ 
 449                                 if ($session->getRainrisk() !== $meteo['rainrisk']) { 
 451                                         $session->setRainrisk($meteo['rainrisk']); 
 454                                 //Check realfeel array 
 455                                 if ($meteo['realfeel'] !== []) { 
 457                                         $realfeel = floatval(round(array_sum($meteo['realfeel'])/count($meteo['realfeel']),1)); 
 459                                         //Check if realfeel differ 
 460                                         if ($session->getRealfeel() !== $realfeel) { 
 461                                                 //Set average realfeel 
 462                                                 $session->setRealfeel($realfeel); 
 465                                         //Check if realfeelmin differ 
 466                                         if ($session->getRealfeelmin() !== $meteo['realfeelmin']) { 
 468                                                 $session->setRealfeelmin($meteo['realfeelmin']); 
 471                                         //Check if realfeelmax differ 
 472                                         if ($session->getRealfeelmax() !== $meteo['realfeelmax']) { 
 474                                                 $session->setRealfeelmax($meteo['realfeelmax']); 
 478                                 //Check temperature array 
 479                                 if ($meteo['temperature'] !== []) { 
 480                                         //Compute temperature 
 481                                         $temperature = floatval(round(array_sum($meteo['temperature'])/count($meteo['temperature']),1)); 
 483                                         //Check if temperature differ 
 484                                         if ($session->getTemperature() !== $temperature) { 
 485                                                 //Set average temperature 
 486                                                 $session->setTemperature($temperature); 
 489                                         //Check if temperaturemin differ 
 490                                         if ($session->getTemperaturemin() !== $meteo['temperaturemin']) { 
 492                                                 $session->setTemperaturemin($meteo['temperaturemin']); 
 495                                         //Check if temperaturemax differ 
 496                                         if ($session->getTemperaturemax() !== $meteo['temperaturemax']) { 
 498                                                 $session->setTemperaturemax($meteo['temperaturemax']); 
 504                 //Flush to get the ids 
 505                 $this->doctrine
->getManager()->flush(); 
 511                 return self
::SUCCESS
; 
 517          * @return bool|void Return success or exit 
 519         function curl_init() { 
 521                 if (($this->ch 
= curl_init()) === false) { 
 523                         echo 'Curl init failed: '.curl_error($this->ch
)."\n"; 
 534                                         CURLOPT_HTTP_VERSION 
=> CURL_HTTP_VERSION_2_0
, 
 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/' 
 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', 
 547                                                 'Cache-Control: no-cache', 
 548                                                 //Keep connection alive 
 549                                                 'Connection: keep-alive', 
 552                                                 //Force secure requests 
 553                                                 'Upgrade-Insecure-Requests: 1', 
 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 
 561                                         CURLOPT_COOKIEFILE 
=> '', 
 562                                         //Disable location following 
 563                                         CURLOPT_FOLLOWLOCATION 
=> false, 
 565                                         #CURLOPT_URL => $url = 'https://www.accuweather.com/', 
 567                                         CURLOPT_HEADER 
=> true, 
 569                                         CURLOPT_RETURNTRANSFER 
=> true, 
 572                                         CURLINFO_HEADER_OUT 
=> true 
 577                         echo 'Curl setopt array failed: '.curl_error($this->ch
)."\n"; 
 589          * @return string|void Return url content or exit 
 591         function curl_get($url) { 
 593                 if (curl_setopt($this->ch
, CURLOPT_URL
, $url) === false) { 
 595                         echo 'Setopt for '.$url.' failed: '.curl_error($this->ch
)."\n"; 
 598                         curl_close($this->ch
); 
 604                 //Check return status 
 605                 if (($response = curl_exec($this->ch
)) === false) { 
 607                         echo 'Get for '.$url.' failed: '.curl_error($this->ch
)."\n"; 
 609                         //Display sent headers 
 610                         var_dump(curl_getinfo($this->ch
, CURLINFO_HEADER_OUT
)); 
 616                         curl_close($this->ch
); 
 623                 if (empty($hs = curl_getinfo($this->ch
, CURLINFO_HEADER_SIZE
))) { 
 625                         echo 'Getinfo for '.$url.' failed: '.curl_error($this->ch
)."\n"; 
 627                         //Display sent headers 
 628                         var_dump(curl_getinfo($this->ch
, CURLINFO_HEADER_OUT
)); 
 634                         curl_close($this->ch
); 
 641                 if (empty($header = substr($response, 0, $hs))) { 
 643                         echo 'Header for '.$url.' empty: '.curl_error($this->ch
)."\n"; 
 645                         //Display sent headers 
 646                         var_dump(curl_getinfo($this->ch
, CURLINFO_HEADER_OUT
)); 
 652                         curl_close($this->ch
); 
 658                 //Check request success 
 659                 if (strlen($header) <= 10 || substr($header, 0, 10) !== 'HTTP/2 200') { 
 661                         echo 'Status for '.$url.' failed: '.curl_error($this->ch
)."\n"; 
 663                         //Display sent headers 
 664                         var_dump(curl_getinfo($this->ch
, CURLINFO_HEADER_OUT
)); 
 670                         curl_close($this->ch
); 
 677                 return substr($response, $hs); 
 683          * @return bool Return success or failure 
 685         function curl_close() { 
 686                 return curl_close($this->ch
);