Dotclear

source: inc/core/class.dc.update.php @ 3874:ab8368569446

Revision 3874:ab8368569446, 15.1 KB checked in by franck <carnet.franck.paul@…>, 7 years ago (diff)

short notation for array (array() → [])

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

Sites map