Dotclear

source: inc/core/class.dc.update.php @ 3350:7faafc3b6250

Revision 3350:7faafc3b6250, 12.1 KB checked in by franck <carnet.franck.paul@…>, 9 years ago (diff)

Better this way (using closure → less code)

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

Sites map