Dotclear

source: inc/core/class.dc.update.php @ 3390:8cc44296f7b0

Revision 3390:8cc44296f7b0, 12.2 KB checked in by franck <carnet.franck.paul@…>, 9 years ago (diff)

Check minimum version of PHP required before giving access to auto-update

RevLine 
[0]1<?php
2# -- BEGIN LICENSE BLOCK ---------------------------------------
3#
4# This file is part of Dotclear 2.
5#
[1179]6# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
[0]7# Licensed under the GPL version 2.0 license.
8# See LICENSE file or
9# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
10#
11# -- END LICENSE BLOCK -----------------------------------------
12if (!defined('DC_RC_PATH')) { return; }
13
14class dcUpdate
15{
16     const ERR_FILES_CHANGED = 101;
17     const ERR_FILES_UNREADABLE = 102;
18     const ERR_FILES_UNWRITALBE = 103;
[2566]19
[0]20     protected $url;
21     protected $subject;
22     protected $version;
23     protected $cache_file;
[2566]24
[0]25     protected $version_info = array(
26          'version' => null,
27          'href' => null,
28          'checksum' => null,
29          'info' => null,
[3390]30          'php' => '5.3',
[0]31          'notify' => true
32     );
[2566]33
[0]34     protected $cache_ttl = '-6 hours';
35     protected $forced_files = array();
[2566]36
[0]37     /**
[765]38      * Constructor
[2566]39      *
[767]40      * @param url            string    Versions file URL
41      * @param subject        string    Subject to check
42      * @param version        string    Version type
43      * @param cache_dir      string    Directory cache path
[765]44      */
[0]45     public function __construct($url,$subject,$version,$cache_dir)
46     {
47          $this->url = $url;
48          $this->subject = $subject;
49          $this->version = $version;
50          $this->cache_file = $cache_dir.'/'.$subject.'-'.$version;
51     }
[2566]52
[0]53     /**
[765]54      * Checks for Dotclear updates.
55      * Returns latest version if available or false.
[2566]56      *
[765]57      * @param version        string    Current version to compare
[2517]58      * @param nocache        boolean   Force checking
[765]59      * @return string                  Latest version if available
60      */
[3390]61     public function check($version,$nocache=false)
[0]62     {
[2517]63          $this->getVersionInfo($nocache);
[0]64          $v = $this->getVersion();
65          if ($v && version_compare($version,$v,'<')) {
66               return $v;
67          }
[2566]68
[0]69          return false;
70     }
[2566]71
[2517]72     public function getVersionInfo($nocache=false)
[0]73     {
74          # Check cached file
[2517]75          if (is_readable($this->cache_file) && filemtime($this->cache_file) > strtotime($this->cache_ttl) && !$nocache)
[0]76          {
77               $c = @file_get_contents($this->cache_file);
78               $c = @unserialize($c);
79               if (is_array($c)) {
80                    $this->version_info = $c;
81                    return;
82               }
83          }
[2566]84
[0]85          $cache_dir = dirname($this->cache_file);
86          $can_write = (!is_dir($cache_dir) && is_writable(dirname($cache_dir)))
87          || (!file_exists($this->cache_file) && is_writable($cache_dir))
88          || is_writable($this->cache_file);
[2566]89
[0]90          # If we can't write file, don't bug host with queries
91          if (!$can_write) {
92               return;
93          }
[2566]94
[0]95          if (!is_dir($cache_dir)) {
96               try {
97                    files::makeDir($cache_dir);
98               } catch (Exception $e) {
99                    return;
100               }
101          }
[2566]102
[0]103          # Try to get latest version number
104          try
105          {
106               $path = '';
[3350]107               $status = 0;
[2566]108
[3350]109               $http_get = function ($http_url) use (&$status,$path) {
110                    $client = netHttp::initClient($http_url,$path);
111                    if ($client !== false) {
112                         $client->setTimeout(4);
113                         $client->setUserAgent($_SERVER['HTTP_USER_AGENT']);
114                         $client->get($path);
115                         $status = (int) $client->getStatus();
116                    }
117                    return $client;
118               };
[3349]119
[3350]120               $client = $http_get($this->url);
121               if ($status >= 400) {
122                    // If original URL uses HTTPS, try with HTTP
123                    $url_parts = parse_url($client->getRequestURL());
124                    if (isset($url_parts['scheme']) && $url_parts['scheme'] == 'https') {
125                         // Replace https by http in url
126                         $this->url = preg_replace('/^https(?=:\/\/)/i','http',$this->url);
127                         $client = $http_get($this->url);
[3349]128                    }
[0]129               }
[3350]130               if (!$status || $status >= 400) {
131                    throw new Exception();
132               }
133               $this->readVersion($client->getContent());
[0]134          }
[3350]135          catch (Exception $e) {
136               return;
137          }
[2566]138
[0]139          # Create cache
140          file_put_contents($this->cache_file,serialize($this->version_info));
141     }
[2566]142
[0]143     public function getVersion()
[767]144     {
[0]145          return $this->version_info['version'];
146     }
[2566]147
[0]148     public function getFileURL()
149     {
150          return $this->version_info['href'];
151     }
[2566]152
[0]153     public function getInfoURL()
154     {
155          return $this->version_info['info'];
156     }
[2566]157
[0]158     public function getChecksum()
159     {
160          return $this->version_info['checksum'];
161     }
[2566]162
[3390]163     public function getPHPVersion()
164     {
165          return $this->version_info['php'];
166     }
167
[0]168     public function getNotify()
169     {
170          return $this->version_info['notify'];
171     }
[2566]172
[0]173     public function getForcedFiles()
174     {
175          return $this->forced_files;
176     }
[2566]177
[0]178     public function setForcedFiles()
179     {
180          $this->forced_files = func_get_args();
181     }
[2566]182
[0]183     /**
[765]184      * Sets notification flag.
185      */
[0]186     public function setNotify($n)
187     {
[2566]188
[0]189          if (!is_writable($this->cache_file)) {
190               return;
191          }
[2566]192
[0]193          $this->version_info['notify'] = (boolean) $n;
194          file_put_contents($this->cache_file,serialize($this->version_info));
195     }
[2566]196
[0]197     public function checkIntegrity($digests_file,$root)
198     {
199          if (!$digests_file) {
200               throw new Exception(__('Digests file not found.'));
201          }
[2566]202
[0]203          $changes = $this->md5sum($root,$digests_file);
[2566]204
[0]205          if (!empty($changes)) {
206               $e = new Exception('Some files have changed.',self::ERR_FILES_CHANGED);
207               $e->bad_files = $changes;
208               throw $e;
209          }
[2566]210
[0]211          return true;
212     }
[2566]213
[0]214     /**
[765]215      * Downloads new version to destination $dest.
216      */
[0]217     public function download($dest)
218     {
219          $url = $this->getFileURL();
[2566]220
[0]221          if (!$url) {
222               throw new Exception(__('No file to download'));
223          }
[2566]224
[0]225          if (!is_writable(dirname($dest))) {
226               throw new Exception(__('Root directory is not writable.'));
227          }
[2566]228
[0]229          try
230          {
[3350]231               $path = '';
232               $status = 0;
[2566]233
[3350]234               $http_get = function ($http_url) use (&$status,$dest,$path) {
235                    $client = netHttp::initClient($http_url,$path);
236                    if ($client !== false) {
[3349]237                         $client->setTimeout(4);
238                         $client->setUserAgent($_SERVER['HTTP_USER_AGENT']);
239                         $client->useGzip(false);
240                         $client->setPersistReferers(false);
241                         $client->setOutput($dest);
242                         $client->get($path);
243                         $status = (int) $client->getStatus();
244                    }
[3350]245                    return $client;
246               };
247
248               $client = $http_get($url);
249               if ($status >= 400) {
250                    // If original URL uses HTTPS, try with HTTP
251                    $url_parts = parse_url($client->getRequestURL());
252                    if (isset($url_parts['scheme']) && $url_parts['scheme'] == 'https') {
253                         // Replace https by http in url
254                         $url = preg_replace('/^https(?=:\/\/)/i','http',$url);
255                         $client = $http_get($url);
256                    }
[3349]257               }
258               if ($status != 200) {
[0]259                    @unlink($dest);
260                    throw new Exception();
261               }
262          }
263          catch (Exception $e)
264          {
265               throw new Exception(__('An error occurred while downloading archive.'));
266          }
267     }
[2566]268
[0]269     /**
[765]270      * Checks if archive was successfully downloaded.
271      */
[0]272     public function checkDownload($zip)
273     {
274          $cs = $this->getChecksum();
[2566]275
[0]276          return $cs && is_readable($zip) && md5_file($zip) == $cs;
277     }
[2566]278
[0]279     /**
[765]280      * Backups changed files before an update.
281      */
[0]282     public function backup($zip_file,$zip_digests,$root,$root_digests,$dest)
283     {
284          if (!is_readable($zip_file)) {
285               throw new Exception(__('Archive not found.'));
286          }
[2566]287
[0]288          if (!is_readable($root_digests)) {
289               @unlink($zip_file);
290               throw new Exception(__('Unable to read current digests file.'));
291          }
[2566]292
[0]293          # Stop everything if a backup already exists and can not be overrided
294          if (!is_writable(dirname($dest)) && !file_exists($dest)) {
295               throw new Exception(__('Root directory is not writable.'));
296          }
[2566]297
[0]298          if (file_exists($dest) && !is_writable($dest)) {
299               return false;
300          }
[2566]301
[0]302          $b_fp = @fopen($dest,'wb');
303          if ($b_fp === false) {
304               return false;
305          }
[2566]306
[0]307          $zip = new fileUnzip($zip_file);
308          $b_zip = new fileZip($b_fp);
[2566]309
[0]310          if (!$zip->hasFile($zip_digests))
311          {
312               @unlink($zip_file);
313               throw new Exception(__('Downloaded file does not seem to be a valid archive.'));
314          }
[2566]315
[0]316          $opts = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES;
317          $cur_digests = file($root_digests,$opts);
318          $new_digests = explode("\n",$zip->unzip($zip_digests));
319          $new_files = $this->getNewFiles($cur_digests,$new_digests);
320          $zip->close();
321          unset($opts,$cur_digests,$new_digests,$zip);
[2566]322
[0]323          $not_readable = array();
[2566]324
[0]325          if (!empty($this->forced_files)) {
326               $new_files = array_merge($new_files,$this->forced_files);
327          }
[2566]328
[0]329          foreach ($new_files as $file)
330          {
331               if (!$file || !file_exists($root.'/'.$file)) {
332                    continue;
333               }
[2566]334
[0]335               try {
336                    $b_zip->addFile($root.'/'.$file,$file);
337               } catch (Exception $e) {
338                    $not_readable[] = $file;
339               }
340          }
[2566]341
[0]342          # If only one file is not readable, stop everything now
343          if (!empty($not_readable)) {
344               $e = new Exception('Some files are not readable.',self::ERR_FILES_UNREADABLE);
345               $e->bad_files = $not_readable;
346               throw $e;
347          }
[2566]348
[0]349          $b_zip->write();
350          fclose($b_fp);
351          $b_zip->close();
[2566]352
[0]353          return true;
354     }
[2566]355
[0]356     /**
[765]357      * Upgrade process.
358      */
[0]359     public function performUpgrade($zip_file,$zip_digests,$zip_root,$root,$root_digests)
360     {
361          if (!is_readable($zip_file)) {
362               throw new Exception(__('Archive not found.'));
363          }
[2566]364
[0]365          if (!is_readable($root_digests)) {
366               @unlink($zip_file);
367               throw new Exception(__('Unable to read current digests file.'));
368          }
[2566]369
[0]370          $zip = new fileUnzip($zip_file);
[2566]371
[0]372          if (!$zip->hasFile($zip_digests))
373          {
374               @unlink($zip_file);
375               throw new Exception(__('Downloaded file does not seem to be a valid archive.'));
376          }
[2566]377
[0]378          $opts = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES;
379          $cur_digests = file($root_digests,$opts);
380          $new_digests = explode("\n",$zip->unzip($zip_digests));
381          $new_files = self::getNewFiles($cur_digests,$new_digests);
[2566]382
[0]383          if (!empty($this->forced_files)) {
384               $new_files = array_merge($new_files,$this->forced_files);
385          }
[2566]386
[0]387          $zip_files = array();
388          $not_writable = array();
[2566]389
[0]390          foreach ($new_files as $file)
391          {
392               if (!$file) {
393                    continue;
394               }
[2566]395
[0]396               if (!$zip->hasFile($zip_root.'/'.$file)) {
397                    @unlink($zip_file);
398                    throw new Exception(__('Incomplete archive.'));
399               }
[2566]400
[0]401               $dest = $dest_dir = $root.'/'.$file;
402               while (!is_dir($dest_dir = dirname($dest_dir)));
[2566]403
[0]404               if ((file_exists($dest) && !is_writable($dest)) ||
405               (!file_exists($dest) && !is_writable($dest_dir))) {
406                    $not_writable[] = $file;
407                    continue;
408               }
[2566]409
[0]410               $zip_files[] = $file;
411          }
[2566]412
[0]413          # If only one file is not writable, stop everything now
414          if (!empty($not_writable)) {
415               $e = new Exception('Some files are not writable',self::ERR_FILES_UNWRITALBE);
416               $e->bad_files = $not_writable;
417               throw $e;
418          }
[2566]419
[0]420          # Everything's fine, we can write files, then do it now
421          $can_touch = function_exists('touch');
422          foreach ($zip_files as $file) {
423               $zip->unzip($zip_root.'/'.$file, $root.'/'.$file);
424               if ($can_touch) {
425                    @touch($root.'/'.$file);
426               }
427          }
428          @unlink($zip_file);
429     }
[2566]430
[0]431     protected function getNewFiles($cur_digests,$new_digests)
432     {
433          $cur_md5 = $cur_path = $cur_digests;
434          $new_md5 = $new_path = $new_digests;
[2566]435
[0]436          array_walk($cur_md5, array($this,'parseLine'),1);
437          array_walk($cur_path,array($this,'parseLine'),2);
438          array_walk($new_md5, array($this,'parseLine'),1);
439          array_walk($new_path,array($this,'parseLine'),2);
[2566]440
[0]441          $cur = array_combine($cur_md5,$cur_path);
442          $new = array_combine($new_md5,$new_path);
[2566]443
[0]444          return array_values(array_diff_key($new,$cur));
445     }
[2566]446
[0]447     protected function readVersion($str)
448     {
449          try
450          {
451               $xml = new SimpleXMLElement($str,LIBXML_NOERROR);
452               $r = $xml->xpath("/versions/subject[@name='".$this->subject."']/release[@name='".$this->version."']");
[2566]453
[0]454               if (!empty($r) && is_array($r))
455               {
456                    $r = $r[0];
457                    $this->version_info['version'] = isset($r['version']) ? (string) $r['version'] : null;
458                    $this->version_info['href'] = isset($r['href']) ? (string) $r['href'] : null;
459                    $this->version_info['checksum'] = isset($r['checksum']) ? (string) $r['checksum'] : null;
460                    $this->version_info['info'] = isset($r['info']) ? (string) $r['info'] : null;
[3390]461                    $this->version_info['php'] = isset($r['php']) ? (string) $r['php'] : null;
[0]462               }
463          }
464          catch (Exception $e)
465          {
466               throw $e;
467          }
468     }
[2566]469
[0]470     protected function md5sum($root,$digests_file)
471     {
472          if (!is_readable($digests_file)) {
473               throw new Exception(__('Unable to read digests file.'));
474          }
[2566]475
[0]476          $opts = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES;
477          $contents = file($digests_file,$opts);
[2566]478
[0]479          $changes = array();
[2566]480
[0]481          foreach ($contents as $digest)
482          {
483               if (!preg_match('#^([\da-f]{32})\s+(.+?)$#',$digest,$m)) {
484                    continue;
485               }
[2566]486
[0]487               $md5 = $m[1];
488               $filename = $root.'/'.$m[2];
[2566]489
[0]490               # Invalid checksum
491               if (!is_readable($filename) || !self::md5_check($filename, $md5)) {
492                    $changes[] = substr($m[2],2);
493               }
494          }
[2566]495
[0]496          # No checksum found in digests file
497          if (empty($md5)) {
498               throw new Exception(__('Invalid digests file.'));
499          }
[2566]500
[0]501          return $changes;
502     }
[2566]503
[0]504     protected function parseLine(&$v,$k,$n)
505     {
506          if (!preg_match('#^([\da-f]{32})\s+(.+?)$#',$v,$m)) {
507               return;
508          }
[2566]509
[0]510          $v = $n == 1 ? md5($m[2].$m[1]) : substr($m[2],2);
511     }
[2566]512
[0]513     protected static function md5_check($filename,$md5)
514     {
515          if (md5_file($filename) == $md5) {
516               return true;
517          } else {
518               $filecontent = file_get_contents($filename);
519               $filecontent = str_replace ("\r\n","\n",$filecontent);
520               $filecontent = str_replace ("\r","\n",$filecontent);
521               if (md5($filecontent) == $md5) return true;
522          }
523          return false;
524     }
525}
Note: See TracBrowser for help on using the repository browser.

Sites map