<?php

/**
 * @copyright  Copyright (c) 2010-2013 Nicholas K. Dionysopoulos / AkeebaBackup.com
 * @copyright  Copyright (c) 2009-2019 Ryan Demmer. All rights reserved
 * @license    GNU LGPLv3 or later <http://www.gnu.org/copyleft/lesser.html>
 *
 * Based on the LiveUpdateDownloadHelper class from Akeeba LiveUpdate
 */
defined('_JEXEC') or die();

/**
 * Check for and download updates from a remote server.
 */
class UpdatesHelper
{
    private static function applyCACert(&$ch)
    {
        $cacert = dirname(__FILE__).'/cacert.pem';

        if (file_exists($cacert)) {
            @curl_setopt($ch, CURLOPT_CAINFO, $cacert);

            return true;
        }

        return false;
    }

    /**
     * Fetches update information from the server using cURL or fopen.
     *
     * @param string $url  The URL to check
     * @param string $data Data to send via POST
     *
     * @return mixed Result string on success, false on failure
     */
    public function check($url, $data)
    {
        $result = false;

        if (self::hasCURL()) {
            $ch = curl_init($url);

            self::applyCACert($ch);

            curl_setopt($ch, CURLOPT_HEADER, 0);
            // Pretend we are Firefox, so that webservers play nice with us
            curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.14) Gecko/20110105 Firefox/3.6.14');
            curl_setopt($ch, CURLOPT_ENCODING, 'gzip');
            curl_setopt($ch, CURLOPT_TIMEOUT, 30);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            // needed to display errors
            curl_setopt($ch, CURLOPT_FAILONERROR, true);
            // The @ sign allows the next line to fail if open_basedir is set or if safe mode is enabled
            @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
            @curl_setopt($ch, CURLOPT_MAXREDIRS, 20);

            // add post data
            if ($data) {
                curl_setopt($ch, CURLOPT_POST, 1);
                curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
            }

            $result = curl_exec($ch);

            // file download
            if ($result === false) {
                $error = curl_error($ch);
                $num = (int) curl_errno($ch);

                if (!$error && $num === 7) {
                    $error = 'Failed to connect() to host or proxy';
                }

                curl_close($ch);

                return array('error' => 'CURL ERROR : '.$num.' - '.$error);
            }

            curl_close($ch);
        } elseif (self::hasFOPEN()) {
            $options = array('http' => array('method' => 'POST', 'timeout' => 10, 'content' => $data));

            $context = stream_context_create($options);
            $result = @file_get_contents($url, false, $context);

            if ($result === false) {
                return array('error' => WFText::_('Update check failed : Invalid response from update server'));
            }
        }

        return $result;
    }

    /**
     * Downloads from a URL and saves the result as a local file.
     *
     * @param string $url    The URL to fetch
     * @param string $target Where to save the file
     *
     * @return bool True on success
     */
    public static function download($url, $target)
    {
        // Import Joomla! libraries
        JLoader::import('joomla.filesystem.file');

        /* @var bool Did we try to force permissions? */
        $hackPermissions = false;

        // Make sure the target does not exist
        if (JFile::exists($target)) {
            if (!@unlink($target)) {
                JFile::delete($target);
            }
        }

        // Try to open the output file for writing
        $fp = @fopen($target, 'wb');

        if ($fp === false) {
            // The file can not be opened for writing. Let's try a hack.
            $empty = '';
            if (JFile::write($target, $empty)) {
                if (self::chmod($target, 511)) {
                    $fp = @fopen($target, 'wb');
                    $hackPermissions = true;
                }
            }
        }

        $result = false;

        if ($fp !== false) {
            // First try to download directly to file if $fp !== false
            $adapters = self::getAdapters();
            $result = false;

            while (!empty($adapters) && ($result === false)) {
                // Run the current download method
                $method = 'get'.strtoupper(array_shift($adapters));
                $result = self::$method($url, $fp);

                // Check if we have a download
                if ($result === true) {
                    // The download is complete, close the file pointer
                    @fclose($fp);

                    // If the filesize is not at least 1 byte, we consider it failed.
                    clearstatcache();
                    $filesize = @filesize($target);

                    if ($filesize <= 0) {
                        $result = false;
                        $fp = @fopen($target, 'wb');
                    }
                }
            }

            // If we have no download, close the file pointer
            if ($result === false) {
                @fclose($fp);
            }
        }

        if ($result === false) {
            // Delete the target file if it exists
            if (file_exists($target)) {
                if (!@unlink($target)) {
                    JFile::delete($target);
                }
            }
            // Download and write using JFile::write();
            $result = JFile::write($target, self::downloadAndReturn($url));
        }

        return $result;
    }

    /**
     * Downloads from a URL and returns the result as a string.
     *
     * @param string $url The URL to download from
     *
     * @return mixed Result string on success, false on failure
     */
    public static function downloadAndReturn($url)
    {
        $adapters = self::getAdapters();
        $result = false;

        while (!empty($adapters) && ($result === false)) {
            // Run the current download method
            $method = 'get'.strtoupper(array_shift($adapters));
            $result = self::$method($url, null);
        }

        return $result;
    }

    /**
     * Does the server support PHP's cURL extension?
     *
     * @return bool True if it is supported
     */
    public static function hasCURL()
    {
        static $result = null;

        if (is_null($result)) {
            $result = function_exists('curl_init');

            if ($result) {
                $cacert = dirname(__FILE__).'/cacert.pem';

                // check for SSL support
                $version = curl_version();
                $ssl_supported = ($version['features'] & CURL_VERSION_SSL);

                $result = (bool) $ssl_supported && file_exists($cacert);
            }
        }

        return $result;
    }

    /**
     * Downloads the contents of a URL and writes them to disk (if $fp is not null)
     * or returns them as a string (if $fp is null) using cURL.
     *
     * @param string   $url The URL to download from
     * @param resource $fp  The file pointer to download to. Omit to return the contents
     *
     * @return bool|string False on failure, true on success ($fp not null) or the URL contents (if $fp is null)
     */
    private static function &getCURL($url, $fp = null, $nofollow = false)
    {
        $result = false;

        $ch = curl_init($url);

        self::applyCACert($ch);

        if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1) && !$nofollow) {
            // Safe Mode is enabled. We have to fetch the headers and
            // parse any redirections present in there.
            curl_setopt($ch, CURLOPT_AUTOREFERER, true);
            curl_setopt($ch, CURLOPT_FAILONERROR, true);
            curl_setopt($ch, CURLOPT_HEADER, true);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
            curl_setopt($ch, CURLOPT_TIMEOUT, 30);

            // Get the headers
            $data = curl_exec($ch);
            curl_close($ch);

            // Init
            $newURL = $url;

            // Parse the headers
            $lines = explode("\n", $data);

            foreach ($lines as $line) {
                if (substr($line, 0, 9) == 'Location:') {
                    $newURL = trim(substr($line, 9));
                }
            }

            // Download from the new URL
            if ($url != $newURL) {
                return self::getCURL($newURL, $fp);
            } else {
                return self::getCURL($newURL, $fp, true);
            }
        } else {
            @curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
        }

        curl_setopt($ch, CURLOPT_AUTOREFERER, true);
        curl_setopt($ch, CURLOPT_FAILONERROR, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        // Pretend we are IE7, so that webservers play nice with us
        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.0.3705; .NET CLR 1.1.4322; Media Center PC 4.0)');

        if (is_resource($fp)) {
            curl_setopt($ch, CURLOPT_FILE, $fp);
        }

        $result = curl_exec($ch);
        curl_close($ch);

        return $result;
    }

    /**
     * Does the server support URL fopen() wrappers?
     *
     * @return bool
     */
    public static function hasFOPEN()
    {
        static $result = null;

        if (is_null($result)) {
            // If we are not allowed to use ini_get, we assume that URL fopen is
            // disabled.
            if (!function_exists('ini_get')) {
                $result = false;
            } else {
                // get wrappers
                $wrappers = stream_get_wrappers();

                $result = ini_get('allow_url_fopen') && in_array('https', $wrappers);
            }
        }

        return $result;
    }

    /**
     * Downloads the contents of a URL and writes them to disk (if $fp is not null)
     * or returns them as a string (if $fp is null) using fopen() URL wrappers.
     *
     * @param string   $url The URL to download from
     * @param resource $fp  The file pointer to download to. Omit to return the contents
     *
     * @return bool|string False on failure, true on success ($fp not null) or the URL contents (if $fp is null)
     */
    private static function &getFOPEN($url, $fp = null)
    {
        $result = false;

        // Track errors
        if (function_exists('ini_set')) {
            $track_errors = ini_set('track_errors', true);
        }

        // Open the URL for reading
        if (function_exists('stream_context_create')) {
            // PHP 5+ way (best)
            $httpopts = array(
                'user_agent' => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.0.3705; .NET CLR 1.1.4322; Media Center PC 4.0)',
                'timeout' => 10.0,
            );
            $context = stream_context_create(array('http' => $httpopts));
            $ih = @fopen($url, 'r', false, $context);
        } else {
            // PHP 4 way (actually, it's just a fallback as we can't run this code in PHP4)
            if (function_exists('ini_set')) {
                ini_set('user_agent', 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.0.3705; .NET CLR 1.1.4322; Media Center PC 4.0)');
            }
            $ih = @fopen($url, 'r');
        }

        // If fopen() fails, abort
        if (!is_resource($ih)) {
            return $result;
        }

        // Try to download
        $bytes = 0;
        $result = true;
        $return = '';
        while (!feof($ih) && $result) {
            $contents = fread($ih, 4096);
            if ($contents === false) {
                @fclose($ih);
                $result = false;

                return $result;
            } else {
                $bytes += strlen($contents);
                if (is_resource($fp)) {
                    $result = @fwrite($fp, $contents);
                } else {
                    $return .= $contents;
                    unset($contents);
                }
            }
        }

        @fclose($ih);

        if (is_resource($fp)) {
            return $result;
        } elseif ($result === true) {
            return $return;
        } else {
            return $result;
        }
    }

    /**
     * Detect and return available download methods.
     *
     * @return array
     */
    private static function getAdapters()
    {
        // Detect available adapters
        $adapters = array();
        if (self::hasCURL()) {
            $adapters[] = 'curl';
        }
        if (self::hasFOPEN()) {
            $adapters[] = 'fopen';
        }

        return $adapters;
    }

    /**
     * Change the permissions of a file, optionally using FTP.
     *
     * @param string $file Absolute path to file
     * @param int    $mode Permissions, e.g. 0755
     *
     * @return bool Ture if successful
     */
    private static function chmod($path, $mode)
    {
        if (is_string($mode)) {
            $mode = octdec($mode);
            if (($mode < 0600) || ($mode > 0777)) {
                $mode = 0755;
            }
        }

        // Initialize variables
        JLoader::import('joomla.client.helper');
        $ftpOptions = JClientHelper::getCredentials('ftp');

        // Check to make sure the path valid and clean
        $path = JPath::clean($path);

        if ($ftpOptions['enabled'] == 1) {
            // Connect the FTP client
            JLoader::import('joomla.client.ftp');
            if (version_compare(JVERSION, '3.0', 'ge')) {
                $ftp = JClientFTP::getInstance(
                                $ftpOptions['host'], $ftpOptions['port'], array(), $ftpOptions['user'], $ftpOptions['pass']
                );
            } else {
                if (version_compare(JVERSION, '3.0', 'ge')) {
                    $ftp = JClientFTP::getInstance(
                                    $ftpOptions['host'], $ftpOptions['port'], array(), $ftpOptions['user'], $ftpOptions['pass']
                    );
                } else {
                    $ftp = JFTP::getInstance(
                                    $ftpOptions['host'], $ftpOptions['port'], array(), $ftpOptions['user'], $ftpOptions['pass']
                    );
                }
            }
        }

        if (@chmod($path, $mode)) {
            $ret = true;
        } elseif ($ftpOptions['enabled'] == 1) {
            // Translate path and delete
            JLoader::import('joomla.client.ftp');
            $path = JPath::clean(str_replace(JPATH_ROOT, $ftpOptions['root'], $path), '/');
            // FTP connector throws an error
            $ret = $ftp->chmod($path, $mode);
        } else {
            return false;
        }
    }
}
