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

Line 
1<?php
2# -- BEGIN LICENSE BLOCK ---------------------------------------
3#
4# This file is part of Dotclear 2.
5#
6# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
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;
19
20     protected $url;
21     protected $subject;
22     protected $version;
23     protected $cache_file;
24
25     protected $version_info = array(
26          'version' => null,
27          'href' => null,
28          'checksum' => null,
29          'info' => null,
30          'php' => '5.3',
31          'notify' => true
32     );
33
34     protected $cache_ttl = '-6 hours';
35     protected $forced_files = array();
36
37     /**
38      * Constructor
39      *
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
44      */
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     }
52
53     /**
54      * Checks for Dotclear updates.
55      * Returns latest version if available or false.
56      *
57      * @param version        string    Current version to compare
58      * @param nocache        boolean   Force checking
59      * @return string                  Latest version if available
60      */
61     public function check($version,$nocache=false)
62     {
63          $this->getVersionInfo($nocache);
64          $v = $this->getVersion();
65          if ($v && version_compare($version,$v,'<')) {
66               return $v;
67          }
68
69          return false;
70     }
71
72     public function getVersionInfo($nocache=false)
73     {
74          # Check cached file
75          if (is_readable($this->cache_file) && filemtime($this->cache_file) > strtotime($this->cache_ttl) && !$nocache)
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          }
84
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);
89
90          # If we can't write file, don't bug host with queries
91          if (!$can_write) {
92               return;
93          }
94
95          if (!is_dir($cache_dir)) {
96               try {
97                    files::makeDir($cache_dir);
98               } catch (Exception $e) {
99                    return;
100               }
101          }
102
103          # Try to get latest version number
104          try
105          {
106               $path = '';
107               $status = 0;
108
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               };
119
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);
128                    }
129               }
130               if (!$status || $status >= 400) {
131                    throw new Exception();
132               }
133               $this->readVersion($client->getContent());
134          }
135          catch (Exception $e) {
136               return;
137          }
138
139          # Create cache
140          file_put_contents($this->cache_file,serialize($this->version_info));
141     }
142
143     public function getVersion()
144     {
145          return $this->version_info['version'];
146     }
147
148     public function getFileURL()
149     {
150          return $this->version_info['href'];
151     }
152
153     public function getInfoURL()
154     {
155          return $this->version_info['info'];
156     }
157
158     public function getChecksum()
159     {
160          return $this->version_info['checksum'];
161     }
162
163     public function getPHPVersion()
164     {
165          return $this->version_info['php'];
166     }
167
168     public function getNotify()
169     {
170          return $this->version_info['notify'];
171     }
172
173     public function getForcedFiles()
174     {
175          return $this->forced_files;
176     }
177
178     public function setForcedFiles()
179     {
180          $this->forced_files = func_get_args();
181     }
182
183     /**
184      * Sets notification flag.
185      */
186     public function setNotify($n)
187     {
188
189          if (!is_writable($this->cache_file)) {
190               return;
191          }
192
193          $this->version_info['notify'] = (boolean) $n;
194          file_put_contents($this->cache_file,serialize($this->version_info));
195     }
196
197     public function checkIntegrity($digests_file,$root)
198     {
199          if (!$digests_file) {
200               throw new Exception(__('Digests file not found.'));
201          }
202
203          $changes = $this->md5sum($root,$digests_file);
204
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          }
210
211          return true;
212     }
213
214     /**
215      * Downloads new version to destination $dest.
216      */
217     public function download($dest)
218     {
219          $url = $this->getFileURL();
220
221          if (!$url) {
222               throw new Exception(__('No file to download'));
223          }
224
225          if (!is_writable(dirname($dest))) {
226               throw new Exception(__('Root directory is not writable.'));
227          }
228
229          try
230          {
231               $path = '';
232               $status = 0;
233
234               $http_get = function ($http_url) use (&$status,$dest,$path) {
235                    $client = netHttp::initClient($http_url,$path);
236                    if ($client !== false) {
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                    }
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                    }
257               }
258               if ($status != 200) {
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     }
268
269     /**
270      * Checks if archive was successfully downloaded.
271      */
272     public function checkDownload($zip)
273     {
274          $cs = $this->getChecksum();
275
276          return $cs && is_readable($zip) && md5_file($zip) == $cs;
277     }
278
279     /**
280      * Backups changed files before an update.
281      */
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          }
287
288          if (!is_readable($root_digests)) {
289               @unlink($zip_file);
290               throw new Exception(__('Unable to read current digests file.'));
291          }
292
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          }
297
298          if (file_exists($dest) && !is_writable($dest)) {
299               return false;
300          }
301
302          $b_fp = @fopen($dest,'wb');
303          if ($b_fp === false) {
304               return false;
305          }
306
307          $zip = new fileUnzip($zip_file);
308          $b_zip = new fileZip($b_fp);
309
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          }
315
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);
322
323          $not_readable = array();
324
325          if (!empty($this->forced_files)) {
326               $new_files = array_merge($new_files,$this->forced_files);
327          }
328
329          foreach ($new_files as $file)
330          {
331               if (!$file || !file_exists($root.'/'.$file)) {
332                    continue;
333               }
334
335               try {
336                    $b_zip->addFile($root.'/'.$file,$file);
337               } catch (Exception $e) {
338                    $not_readable[] = $file;
339               }
340          }
341
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          }
348
349          $b_zip->write();
350          fclose($b_fp);
351          $b_zip->close();
352
353          return true;
354     }
355
356     /**
357      * Upgrade process.
358      */
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          }
364
365          if (!is_readable($root_digests)) {
366               @unlink($zip_file);
367               throw new Exception(__('Unable to read current digests file.'));
368          }
369
370          $zip = new fileUnzip($zip_file);
371
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          }
377
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);
382
383          if (!empty($this->forced_files)) {
384               $new_files = array_merge($new_files,$this->forced_files);
385          }
386
387          $zip_files = array();
388          $not_writable = array();
389
390          foreach ($new_files as $file)
391          {
392               if (!$file) {
393                    continue;
394               }
395
396               if (!$zip->hasFile($zip_root.'/'.$file)) {
397                    @unlink($zip_file);
398                    throw new Exception(__('Incomplete archive.'));
399               }
400
401               $dest = $dest_dir = $root.'/'.$file;
402               while (!is_dir($dest_dir = dirname($dest_dir)));
403
404               if ((file_exists($dest) && !is_writable($dest)) ||
405               (!file_exists($dest) && !is_writable($dest_dir))) {
406                    $not_writable[] = $file;
407                    continue;
408               }
409
410               $zip_files[] = $file;
411          }
412
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          }
419
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     }
430
431     protected function getNewFiles($cur_digests,$new_digests)
432     {
433          $cur_md5 = $cur_path = $cur_digests;
434          $new_md5 = $new_path = $new_digests;
435
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);
440
441          $cur = array_combine($cur_md5,$cur_path);
442          $new = array_combine($new_md5,$new_path);
443
444          return array_values(array_diff_key($new,$cur));
445     }
446
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."']");
453
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;
461                    $this->version_info['php'] = isset($r['php']) ? (string) $r['php'] : null;
462               }
463          }
464          catch (Exception $e)
465          {
466               throw $e;
467          }
468     }
469
470     protected function md5sum($root,$digests_file)
471     {
472          if (!is_readable($digests_file)) {
473               throw new Exception(__('Unable to read digests file.'));
474          }
475
476          $opts = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES;
477          $contents = file($digests_file,$opts);
478
479          $changes = array();
480
481          foreach ($contents as $digest)
482          {
483               if (!preg_match('#^([\da-f]{32})\s+(.+?)$#',$digest,$m)) {
484                    continue;
485               }
486
487               $md5 = $m[1];
488               $filename = $root.'/'.$m[2];
489
490               # Invalid checksum
491               if (!is_readable($filename) || !self::md5_check($filename, $md5)) {
492                    $changes[] = substr($m[2],2);
493               }
494          }
495
496          # No checksum found in digests file
497          if (empty($md5)) {
498               throw new Exception(__('Invalid digests file.'));
499          }
500
501          return $changes;
502     }
503
504     protected function parseLine(&$v,$k,$n)
505     {
506          if (!preg_match('#^([\da-f]{32})\s+(.+?)$#',$v,$m)) {
507               return;
508          }
509
510          $v = $n == 1 ? md5($m[2].$m[1]) : substr($m[2],2);
511     }
512
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