Seditio Source
Root |
./othercms/dotclear-2.22/inc/core/class.dc.update.php
<?php
/**
 * @package Dotclear
 * @subpackage Core
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright GPL-2.0-only
 */
if (!defined('DC_RC_PATH')) {
    return;
}

class
dcUpdate
{
    public const
ERR_FILES_CHANGED    = 101;
    public const
ERR_FILES_UNREADABLE = 102;
    public const
ERR_FILES_UNWRITALBE = 103;

    protected
$url;
    protected
$subject;
    protected
$version;
    protected
$cache_file;

    protected
$version_info = [
       
'version'  => null,
       
'href'     => null,
       
'checksum' => null,
       
'info'     => null,
       
'php'      => '7.4',
       
'notify'   => true,
    ];

    protected
$cache_ttl    = '-6 hours';
    protected
$forced_files = [];

   
/**
     * Constructor
     *
     * @param string $url           Versions file URL
     * @param string $subject       Subject to check
     * @param string $version       Version type
     * @param string $cache_dir     Directory cache path
     */
   
public function __construct($url, $subject, $version, $cache_dir)
    {
       
$this->url        = $url;
       
$this->subject    = $subject;
       
$this->version    = $version;
       
$this->cache_file = $cache_dir . '/' . $subject . '-' . $version;
    }

   
/**
     * Checks for Dotclear updates.
     * Returns latest version if available or false.
     *
     * @param   string  $version    Current version to compare
     * @param   boolean $nocache    Force checking
     *
     * @return  mixed   Latest version if available
     */
   
public function check($version, $nocache = false)
    {
       
$this->getVersionInfo($nocache);
       
$v = $this->getVersion();
        if (
$v && version_compare($version, $v, '<')) {
            return
$v;
        }

        return
false;
    }

    public function
getVersionInfo($nocache = false)
    {
       
# Check cached file
       
if (is_readable($this->cache_file) && filemtime($this->cache_file) > strtotime($this->cache_ttl) && !$nocache) {
           
$c = @file_get_contents($this->cache_file);
           
$c = @unserialize($c);
            if (
is_array($c)) {
               
$this->version_info = $c;

                return;
            }
        }

       
$cache_dir = dirname($this->cache_file);
       
$can_write = (!is_dir($cache_dir) && is_writable(dirname($cache_dir)))
        || (!
file_exists($this->cache_file) && is_writable($cache_dir))
        ||
is_writable($this->cache_file);

       
# If we can't write file, don't bug host with queries
       
if (!$can_write) {
            return;
        }

        if (!
is_dir($cache_dir)) {
            try {
               
files::makeDir($cache_dir);
            } catch (
Exception $e) {
                return;
            }
        }

       
# Try to get latest version number
       
try {
           
$path   = '';
           
$status = 0;

           
$http_get = function ($http_url) use (&$status, $path) {
               
$client = netHttp::initClient($http_url, $path);
                if (
$client !== false) {
                   
$client->setTimeout(DC_QUERY_TIMEOUT);
                   
$client->setUserAgent($_SERVER['HTTP_USER_AGENT']);
                   
$client->get($path);
                   
$status = (int) $client->getStatus();
                }

                return
$client;
            };

           
$client = $http_get($this->url);
            if (
$status >= 400) {
               
// If original URL uses HTTPS, try with HTTP
               
$url_parts = parse_url($client->getRequestURL());
                if (isset(
$url_parts['scheme']) && $url_parts['scheme'] == 'https') {
                   
// Replace https by http in url
                   
$this->url = preg_replace('/^https(?=:\/\/)/i', 'http', $this->url);
                   
$client    = $http_get($this->url);
                }
            }
            if (!
$status || $status >= 400) {
                throw new
Exception();
            }
           
$this->readVersion($client->getContent());
        } catch (
Exception $e) {
            return;
        }

       
# Create cache
       
file_put_contents($this->cache_file, serialize($this->version_info));
    }

    public function
getVersion()
    {
        return
$this->version_info['version'];
    }

    public function
getFileURL()
    {
        return
$this->version_info['href'];
    }

    public function
getInfoURL()
    {
        return
$this->version_info['info'];
    }

    public function
getChecksum()
    {
        return
$this->version_info['checksum'];
    }

    public function
getPHPVersion()
    {
        return
$this->version_info['php'];
    }

    public function
getNotify()
    {
        return
$this->version_info['notify'];
    }

    public function
getForcedFiles()
    {
        return
$this->forced_files;
    }

    public function
setForcedFiles(...$args)
    {
       
$this->forced_files = $args;
    }

   
/**
     * Sets notification flag.
     */
   
public function setNotify($n)
    {
        if (!
is_writable($this->cache_file)) {
            return;
        }

       
$this->version_info['notify'] = (bool) $n;
       
file_put_contents($this->cache_file, serialize($this->version_info));
    }

    public function
checkIntegrity($digests_file, $root)
    {
        if (!
$digests_file) {
            throw new
Exception(__('Digests file not found.'));
        }

       
$changes = $this->md5sum($root, $digests_file);

        if (!empty(
$changes)) {
           
$e            = new Exception('Some files have changed.', self::ERR_FILES_CHANGED);
           
$e->bad_files = $changes;   // @phpstan-ignore-line

           
throw $e;
        }

        return
true;
    }

   
/**
     * Downloads new version to destination $dest.
     */
   
public function download($dest)
    {
       
$url = $this->getFileURL();

        if (!
$url) {
            throw new
Exception(__('No file to download'));
        }

        if (!
is_writable(dirname($dest))) {
            throw new
Exception(__('Root directory is not writable.'));
        }

        try {
           
$path   = '';
           
$status = 0;

           
$http_get = function ($http_url) use (&$status, $dest, $path) {
               
$client = netHttp::initClient($http_url, $path);
                if (
$client !== false) {
                   
$client->setTimeout(DC_QUERY_TIMEOUT);
                   
$client->setUserAgent($_SERVER['HTTP_USER_AGENT']);
                   
$client->useGzip(false);
                   
$client->setPersistReferers(false);
                   
$client->setOutput($dest);
                   
$client->get($path);
                   
$status = (int) $client->getStatus();
                }

                return
$client;
            };

           
$client = $http_get($url);
            if (
$status >= 400) {
               
// If original URL uses HTTPS, try with HTTP
               
$url_parts = parse_url($client->getRequestURL());
                if (isset(
$url_parts['scheme']) && $url_parts['scheme'] == 'https') {
                   
// Replace https by http in url
                   
$url    = preg_replace('/^https(?=:\/\/)/i', 'http', $url);
                   
$client = $http_get($url);
                }
            }
            if (
$status != 200) {
                @
unlink($dest);

                throw new
Exception();
            }
        } catch (
Exception $e) {
            throw new
Exception(__('An error occurred while downloading archive.'));
        }
    }

   
/**
     * Checks if archive was successfully downloaded.
     */
   
public function checkDownload($zip)
    {
       
$cs = $this->getChecksum();

        return
$cs && is_readable($zip) && md5_file($zip) == $cs;
    }

   
/**
     * Backups changed files before an update.
     */
   
public function backup($zip_file, $zip_digests, $root, $root_digests, $dest)
    {
        if (!
is_readable($zip_file)) {
            throw new
Exception(__('Archive not found.'));
        }

        if (!
is_readable($root_digests)) {
            @
unlink($zip_file);

            throw new
Exception(__('Unable to read current digests file.'));
        }

       
# Stop everything if a backup already exists and can not be overrided
       
if (!is_writable(dirname($dest)) && !file_exists($dest)) {
            throw new
Exception(__('Root directory is not writable.'));
        }

        if (
file_exists($dest) && !is_writable($dest)) {
            return
false;
        }

       
$b_fp = @fopen($dest, 'wb');
        if (
$b_fp === false) {
            return
false;
        }

       
$zip   = new fileUnzip($zip_file);
       
$b_zip = new fileZip($b_fp);

        if (!
$zip->hasFile($zip_digests)) {
            @
unlink($zip_file);

            throw new
Exception(__('Downloaded file does not seem to be a valid archive.'));
        }

       
$opts        = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES;
       
$cur_digests = file($root_digests, $opts);
       
$new_digests = explode("\n", $zip->unzip($zip_digests));
       
$new_files   = $this->getNewFiles($cur_digests, $new_digests);
       
$zip->close();
        unset(
$opts, $cur_digests, $new_digests, $zip);

       
$not_readable = [];

        if (!empty(
$this->forced_files)) {
           
$new_files = array_merge($new_files, $this->forced_files);
        }

        foreach (
$new_files as $file) {
            if (!
$file || !file_exists($root . '/' . $file)) {
                continue;
            }

            try {
               
$b_zip->addFile($root . '/' . $file, $file);
            } catch (
Exception $e) {
               
$not_readable[] = $file;
            }
        }

       
# If only one file is not readable, stop everything now
       
if (!empty($not_readable)) {
           
$e            = new Exception('Some files are not readable.', self::ERR_FILES_UNREADABLE);
           
$e->bad_files = $not_readable;  // @phpstan-ignore-line

           
throw $e;
        }

       
$b_zip->write();
       
fclose($b_fp);
       
$b_zip->close();

        return
true;
    }

   
/**
     * Upgrade process.
     */
   
public function performUpgrade($zip_file, $zip_digests, $zip_root, $root, $root_digests)
    {
        if (!
is_readable($zip_file)) {
            throw new
Exception(__('Archive not found.'));
        }

        if (!
is_readable($root_digests)) {
            @
unlink($zip_file);

            throw new
Exception(__('Unable to read current digests file.'));
        }

       
$zip = new fileUnzip($zip_file);

        if (!
$zip->hasFile($zip_digests)) {
            @
unlink($zip_file);

            throw new
Exception(__('Downloaded file does not seem to be a valid archive.'));
        }

       
$opts        = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES;
       
$cur_digests = file($root_digests, $opts);
       
$new_digests = explode("\n", $zip->unzip($zip_digests));
       
$new_files   = self::getNewFiles($cur_digests, $new_digests);

        if (!empty(
$this->forced_files)) {
           
$new_files = array_merge($new_files, $this->forced_files);
        }

       
$zip_files    = [];
       
$not_writable = [];

        foreach (
$new_files as $file) {
            if (!
$file) {
                continue;
            }

            if (!
$zip->hasFile($zip_root . '/' . $file)) {
                @
unlink($zip_file);

                throw new
Exception(__('Incomplete archive.'));
            }

           
$dest = $dest_dir = $root . '/' . $file;
            while (!
is_dir($dest_dir = dirname($dest_dir)));

            if ((
file_exists($dest) && !is_writable($dest)) || (!file_exists($dest) && !is_writable($dest_dir))) {
               
$not_writable[] = $file;

                continue;
            }

           
$zip_files[] = $file;
        }

       
# If only one file is not writable, stop everything now
       
if (!empty($not_writable)) {
           
$e            = new Exception('Some files are not writable', self::ERR_FILES_UNWRITALBE);
           
$e->bad_files = $not_writable;  // @phpstan-ignore-line

           
throw $e;
        }

       
# Everything's fine, we can write files, then do it now
       
$can_touch = function_exists('touch');
        foreach (
$zip_files as $file) {
           
$zip->unzip($zip_root . '/' . $file, $root . '/' . $file);
            if (
$can_touch) {
                @
touch($root . '/' . $file);
            }
        }
        @
unlink($zip_file);
    }

    protected function
getNewFiles($cur_digests, $new_digests)
    {
       
$cur_md5 = $cur_path = $cur_digests;
       
$new_md5 = $new_path = $new_digests;

       
array_walk($cur_md5, [$this, 'parseLine'], 1);
       
array_walk($cur_path, [$this, 'parseLine'], 2);
       
array_walk($new_md5, [$this, 'parseLine'], 1);
       
array_walk($new_path, [$this, 'parseLine'], 2);

       
$cur = array_combine($cur_md5, $cur_path);
       
$new = array_combine($new_md5, $new_path);

        return
array_values(array_diff_key($new, $cur));
    }

    protected function
readVersion($str)
    {
        try {
           
$xml = new SimpleXMLElement($str, LIBXML_NOERROR);
           
$r   = $xml->xpath("/versions/subject[@name='" . $this->subject . "']/release[@name='" . $this->version . "']");

            if (!empty(
$r) && is_array($r)) {
               
$r                              = $r[0];
               
$this->version_info['version']  = isset($r['version']) ? (string) $r['version'] : null;
               
$this->version_info['href']     = isset($r['href']) ? (string) $r['href'] : null;
               
$this->version_info['checksum'] = isset($r['checksum']) ? (string) $r['checksum'] : null;
               
$this->version_info['info']     = isset($r['info']) ? (string) $r['info'] : null;
               
$this->version_info['php']      = isset($r['php']) ? (string) $r['php'] : null;
            }
        } catch (
Exception $e) {
            throw
$e;
        }
    }

    protected function
md5sum($root, $digests_file)
    {
        if (!
is_readable($digests_file)) {
            throw new
Exception(__('Unable to read digests file.'));
        }

       
$opts     = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES;
       
$contents = file($digests_file, $opts);

       
$changes = [];

        foreach (
$contents as $digest) {
            if (!
preg_match('#^([\da-f]{32})\s+(.+?)$#', $digest, $m)) {
                continue;
            }

           
$md5      = $m[1];
           
$filename = $root . '/' . $m[2];

           
# Invalid checksum
           
if (!is_readable($filename) || !self::md5_check($filename, $md5)) {
               
$changes[] = substr($m[2], 2);
            }
        }

       
# No checksum found in digests file
       
if (empty($md5)) {
            throw new
Exception(__('Invalid digests file.'));
        }

        return
$changes;
    }

    protected function
parseLine(&$v, $k, $n)
    {
        if (!
preg_match('#^([\da-f]{32})\s+(.+?)$#', $v, $m)) {
            return;
        }

       
$v = $n == 1 ? md5($m[2] . $m[1]) : substr($m[2], 2);
    }

    protected static function
md5_check($filename, $md5)
    {
        if (
md5_file($filename) == $md5) {
            return
true;
        }
       
$filecontent = file_get_contents($filename);
       
$filecontent = str_replace("\r\n", "\n", $filecontent);
       
$filecontent = str_replace("\r", "\n", $filecontent);
        if (
md5($filecontent) == $md5) {
            return
true;
        }

        return
false;
    }
}