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() → [])

RevLine 
[0]1<?php
[3731]2/**
3 * @package Dotclear
4 * @subpackage Core
5 *
6 * @copyright Olivier Meunier & Association Dotclear
7 * @copyright GPL-2.0-only
8 */
9
[3730]10if (!defined('DC_RC_PATH')) {return;}
[0]11
12class dcUpdate
13{
[3730]14    const ERR_FILES_CHANGED    = 101;
15    const ERR_FILES_UNREADABLE = 102;
16    const ERR_FILES_UNWRITALBE = 103;
[2566]17
[3730]18    protected $url;
19    protected $subject;
20    protected $version;
21    protected $cache_file;
[2566]22
[3874]23    protected $version_info = [
[3730]24        'version'  => null,
25        'href'     => null,
26        'checksum' => null,
27        'info'     => null,
[3795]28        'php'      => '5.6',
[3730]29        'notify'   => true
[3874]30    ];
[2566]31
[3730]32    protected $cache_ttl    = '-6 hours';
[3874]33    protected $forced_files = [];
[2566]34
[3730]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    }
[2566]50
[3730]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        }
[2566]66
[3730]67        return false;
68    }
[2566]69
[3730]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        }
[2566]81
[3730]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);
[2566]86
[3730]87        # If we can't write file, don't bug host with queries
88        if (!$can_write) {
89            return;
90        }
[2566]91
[3730]92        if (!is_dir($cache_dir)) {
93            try {
94                files::makeDir($cache_dir);
95            } catch (Exception $e) {
96                return;
97            }
98        }
[2566]99
[3730]100        # Try to get latest version number
101        try
102        {
103            $path   = '';
104            $status = 0;
[2566]105
[3730]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            };
[3349]116
[3730]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        }
[2566]134
[3730]135        # Create cache
136        file_put_contents($this->cache_file, serialize($this->version_info));
137    }
[2566]138
[3730]139    public function getVersion()
140    {
141        return $this->version_info['version'];
142    }
[2566]143
[3730]144    public function getFileURL()
145    {
146        return $this->version_info['href'];
147    }
[2566]148
[3730]149    public function getInfoURL()
150    {
151        return $this->version_info['info'];
152    }
[2566]153
[3730]154    public function getChecksum()
155    {
156        return $this->version_info['checksum'];
157    }
[2566]158
[3730]159    public function getPHPVersion()
160    {
161        return $this->version_info['php'];
162    }
[3390]163
[3730]164    public function getNotify()
165    {
166        return $this->version_info['notify'];
167    }
[2566]168
[3730]169    public function getForcedFiles()
170    {
171        return $this->forced_files;
172    }
[2566]173
[3872]174    public function setForcedFiles(...$args)
[3730]175    {
[3872]176        $this->forced_files = $args;
[3730]177    }
[2566]178
[3730]179    /**
180     * Sets notification flag.
181     */
182    public function setNotify($n)
183    {
[2566]184
[3730]185        if (!is_writable($this->cache_file)) {
186            return;
187        }
[2566]188
[3730]189        $this->version_info['notify'] = (boolean) $n;
190        file_put_contents($this->cache_file, serialize($this->version_info));
191    }
[2566]192
[3730]193    public function checkIntegrity($digests_file, $root)
194    {
195        if (!$digests_file) {
196            throw new Exception(__('Digests file not found.'));
197        }
[2566]198
[3730]199        $changes = $this->md5sum($root, $digests_file);
[2566]200
[3730]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        }
[2566]206
[3730]207        return true;
208    }
[2566]209
[3730]210    /**
211     * Downloads new version to destination $dest.
212     */
213    public function download($dest)
214    {
215        $url = $this->getFileURL();
[2566]216
[3730]217        if (!$url) {
218            throw new Exception(__('No file to download'));
219        }
[2566]220
[3730]221        if (!is_writable(dirname($dest))) {
222            throw new Exception(__('Root directory is not writable.'));
223        }
[2566]224
[3730]225        try
226        {
227            $path   = '';
228            $status = 0;
[2566]229
[3730]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            };
[3350]243
[3730]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    }
[2566]262
[3730]263    /**
264     * Checks if archive was successfully downloaded.
265     */
266    public function checkDownload($zip)
267    {
268        $cs = $this->getChecksum();
[2566]269
[3730]270        return $cs && is_readable($zip) && md5_file($zip) == $cs;
271    }
[2566]272
[3730]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        }
[2566]281
[3730]282        if (!is_readable($root_digests)) {
283            @unlink($zip_file);
284            throw new Exception(__('Unable to read current digests file.'));
285        }
[2566]286
[3730]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        }
[2566]291
[3730]292        if (file_exists($dest) && !is_writable($dest)) {
293            return false;
294        }
[2566]295
[3730]296        $b_fp = @fopen($dest, 'wb');
297        if ($b_fp === false) {
298            return false;
299        }
[2566]300
[3730]301        $zip   = new fileUnzip($zip_file);
302        $b_zip = new fileZip($b_fp);
[2566]303
[3730]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        }
[2566]308
[3730]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);
[2566]315
[3874]316        $not_readable = [];
[2566]317
[3730]318        if (!empty($this->forced_files)) {
319            $new_files = array_merge($new_files, $this->forced_files);
320        }
[2566]321
[3730]322        foreach ($new_files as $file) {
323            if (!$file || !file_exists($root . '/' . $file)) {
324                continue;
325            }
[2566]326
[3730]327            try {
328                $b_zip->addFile($root . '/' . $file, $file);
329            } catch (Exception $e) {
330                $not_readable[] = $file;
331            }
332        }
[2566]333
[3730]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        }
[2566]340
[3730]341        $b_zip->write();
342        fclose($b_fp);
343        $b_zip->close();
[2566]344
[3730]345        return true;
346    }
[2566]347
[3730]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        }
[2566]356
[3730]357        if (!is_readable($root_digests)) {
358            @unlink($zip_file);
359            throw new Exception(__('Unable to read current digests file.'));
360        }
[2566]361
[3730]362        $zip = new fileUnzip($zip_file);
[2566]363
[3730]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        }
[2566]368
[3730]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);
[2566]373
[3730]374        if (!empty($this->forced_files)) {
375            $new_files = array_merge($new_files, $this->forced_files);
376        }
[2566]377
[3874]378        $zip_files    = [];
379        $not_writable = [];
[2566]380
[3730]381        foreach ($new_files as $file) {
382            if (!$file) {
383                continue;
384            }
[2566]385
[3730]386            if (!$zip->hasFile($zip_root . '/' . $file)) {
387                @unlink($zip_file);
388                throw new Exception(__('Incomplete archive.'));
389            }
[2566]390
[3730]391            $dest = $dest_dir = $root . '/' . $file;
392            while (!is_dir($dest_dir = dirname($dest_dir)));
[2566]393
[3730]394            if ((file_exists($dest) && !is_writable($dest)) ||
395                (!file_exists($dest) && !is_writable($dest_dir))) {
396                $not_writable[] = $file;
397                continue;
398            }
[2566]399
[3730]400            $zip_files[] = $file;
401        }
[2566]402
[3730]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        }
[2566]409
[3730]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    }
[2566]420
[3730]421    protected function getNewFiles($cur_digests, $new_digests)
422    {
423        $cur_md5 = $cur_path = $cur_digests;
424        $new_md5 = $new_path = $new_digests;
[2566]425
[3874]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);
[2566]430
[3730]431        $cur = array_combine($cur_md5, $cur_path);
432        $new = array_combine($new_md5, $new_path);
[2566]433
[3730]434        return array_values(array_diff_key($new, $cur));
435    }
[2566]436
[3730]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 . "']");
[2566]443
[3730]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    }
[2566]456
[3730]457    protected function md5sum($root, $digests_file)
458    {
459        if (!is_readable($digests_file)) {
460            throw new Exception(__('Unable to read digests file.'));
461        }
[2566]462
[3730]463        $opts     = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES;
464        $contents = file($digests_file, $opts);
[2566]465
[3874]466        $changes = [];
[2566]467
[3730]468        foreach ($contents as $digest) {
469            if (!preg_match('#^([\da-f]{32})\s+(.+?)$#', $digest, $m)) {
470                continue;
471            }
[2566]472
[3730]473            $md5      = $m[1];
474            $filename = $root . '/' . $m[2];
[2566]475
[3730]476            # Invalid checksum
477            if (!is_readable($filename) || !self::md5_check($filename, $md5)) {
478                $changes[] = substr($m[2], 2);
479            }
480        }
[2566]481
[3730]482        # No checksum found in digests file
483        if (empty($md5)) {
484            throw new Exception(__('Invalid digests file.'));
485        }
[2566]486
[3730]487        return $changes;
488    }
[2566]489
[3730]490    protected function parseLine(&$v, $k, $n)
491    {
492        if (!preg_match('#^([\da-f]{32})\s+(.+?)$#', $v, $m)) {
493            return;
494        }
[2566]495
[3730]496        $v = $n == 1 ? md5($m[2] . $m[1]) : substr($m[2], 2);
497    }
[2566]498
[3730]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    }
[0]514}
Note: See TracBrowser for help on using the repository browser.

Sites map