<?php

namespace MODX\Revolution\Transport;

use MODX\Revolution\modX;
use Throwable;
use xPDO\Om\xPDOObject;
use xPDO\Transport\xPDOTransport;
use xPDO\xPDO;

/**
 * Represents an xPDOTransport package as required for MODX Providers and package installation
 *
 * @property string  $signature     The full signature of the package
 * @property string  $created       The time this package was created or added
 * @property string  $updated       The time this package was last update
 * @property string  $installed     The time this package was installed
 * @property int     $state         The state of the package; packed/unpacked/etc
 * @property int     $workspace     The workspace this package is installed into.
 * @property int     $provider      The provider ID of the package, if any.
 * @property boolean $disabled      Whether or not this package is disabled (not currently used)
 * @property string  $source        The source data of the package
 * @property string  $manifest      The manifest of the package, containing transport information and methods
 * @property string  $attributes    Any package-level attributes
 * @property string  $package_name  The name of the package
 * @property string  $metadata      Any metadata transmitted with the package
 * @property int     $version_major The major version number of the package
 * @property int     $version_minor The minor version number of the package
 * @property int     $version_patch The patch version number of the package
 * @property int     $release_index The release index of the release. Optional.
 *
 * @package MODX\Revolution\Transport
 */
class modTransportPackage extends xPDOObject
{
    /** @var xPDO|modX */
    public $xpdo = null;
    /**
     * @var string The unique identifier of a package.
     */
    public $identifier = null;
    /**
     * @var string The version number of a package.
     */
    public $version = null;
    /**
     * @var int The major version number of a package.
     */
    public $version_major = 0;
    /**
     * @var int The minor version number of a package.
     */
    public $version_minor = 0;
    /**
     * @var int The patch version number of a package.
     */
    public $version_patch = 0;
    /**
     * @var string The release number of a package; eg, pl, beta, alpha, dev
     */
    public $release = '';
    /**
     * @var int The release index of a package
     */
    public $release_index = 0;
    /**
     * @var xPDOTransport The package to transport.
     */
    public $package = null;

    /**
     * List the packages from this transport package
     *
     * @static
     *
     * @param modX   $modx      A reference to the modX instance
     * @param int    $workspace The current active workspace ID
     * @param int    $limit     The limit of packages to return
     * @param int    $offset    The offset on which to list by
     * @param string $search    An optional search value
     *
     * @return array
     */
    public static function listPackages(modX &$modx, $workspace, $limit = 0, $offset = 0, $search = '')
    {
        return ['collection' => [], 'total' => 0];
    }

    /**
     * Overrides xPDOObject::save to set a default created time if new.
     *
     * @param boolean $cacheFlag
     *
     * @return boolean True if the save was successful
     */
    public function save($cacheFlag = null)
    {
        if ($this->_new && !$this->get('created')) {
            $this->set('created', date('Y-m-d H:i:s'));
        }
        $saved = parent:: save($cacheFlag);

        return $saved;
    }

    /**
     * Overrides xPDOObject::set. Checks if signature is set, and if so,
     * parses it and sets the source if is a new package.
     *
     * @param string $k     The key to set
     * @param mixed  $v     The value to set
     * @param string $vType The validation type to set against
     *
     * @return boolean True if successfully set
     */
    public function set($k, $v = null, $vType = '')
    {
        $set = parent::set($k, $v, $vType);
        if ($k == 'signature') {
            $this->parseSignature();
            if ($this->isNew() && !$this->get('source')) {
                $this->set('source', $this->get('signature') . '.transport.zip');
            }
        }

        return $set;
    }

    /**
     * Parses the signature.
     *
     * @return boolean True if successful.
     */
    public function parseSignature()
    {
        $parsed = false;
        $sig = $this->get('signature');
        if ($sig != null) {
            $parsedSig = xPDOTransport::parseSignature($sig);
            if (count($parsedSig) === 2 && !empty($parsedSig[0]) && !empty($parsedSig[1])) {
                $this->identifier = $parsedSig[0];
                $parsedVersion = explode('-', $parsedSig[1], 2);
                $parsedVersionEls = count($parsedVersion);
                if ($parsedVersionEls === 2) {
                    $this->version = $parsedVersion[0];
                    $releaseChars = [];
                    parse_str($parsedVersion[1], $releaseChars);
                    $release = '';
                    $releaseIndex = '';
                    $char = reset($releaseChars);
                    while ($char !== false && !is_numeric($char)) {
                        $release .= $char;
                        $char = next($releaseChars);
                    }
                    while ($char !== false && is_numeric($char)) {
                        $releaseIndex .= $char;
                    }
                    $this->release = $release;
                    $this->release_index = (int)$releaseIndex;
                    $parsed = true;
                } elseif ($parsedVersionEls === 1) {
                    $this->version = $parsedVersion[0];
                    $this->release = '';
                    $this->release_index = 0;
                    $parsed = true;
                }
                $version = explode('.', $this->version);
                if (count($version) === 2) {
                    $version[] = '0';
                }
                list($this->version_major, $this->version_minor, $this->version_patch) = $version;
            }
        }

        return $parsed;
    }

    /**
     * Set package version data based on the signature
     *
     * @return boolean
     */
    public function setPackageVersionData()
    {
        $sig = explode('-', $this->signature);
        if (is_array($sig)) {
            if (!empty($sig[1])) {
                $v = explode('.', $sig[1]);
                if (isset($v[0])) {
                    $this->set('version_major', $v[0]);
                }
                if (isset($v[1])) {
                    $this->set('version_minor', $v[1]);
                }
                if (isset($v[2])) {
                    $this->set('version_patch', $v[2]);
                }
            }
            if (!empty($sig[2])) {
                $r = preg_split('/([0-9]+)/', $sig[2], -1, PREG_SPLIT_DELIM_CAPTURE);
                if (is_array($r) && !empty($r)) {
                    $this->set('release', $r[0]);
                    $this->set('release_index', (isset($r[1]) ? $r[1] : '0'));
                } else {
                    $this->set('release', $sig[2]);
                }
            }
        }

        return true;
    }

    /**
     * Gets the package's transport mechanism.
     *
     * @access public
     *
     * @param integer $state The state of the package.
     *
     * @return mixed The package.
     */
    public function getTransport($state = -1)
    {
        if (!is_object($this->package) || !($this->package instanceof xPDOTransport)) {
            $workspace = $this->getOne('Workspace');
            if ($workspace) {
                $packageDir = $workspace->get('path') . 'packages/';
                $sourceFile = $this->get('source');
                if ($sourceFile) {
                    $transferred = file_exists($packageDir . $sourceFile);
                    if (!$transferred) { /* if no transport zip, attempt to get it */
                        if (!$transferred = $this->transferPackage($sourceFile, $packageDir)) {
                            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $this->xpdo->lexicon('package_err_transfer', [
                                'sourceFile' => $sourceFile,
                                'packageDir' => $packageDir,
                            ]));
                        } else {
                            $sourceFile = basename($sourceFile);
                        }
                    }
                    if ($transferred) {
                        if ($state < 0) {
                            /* if directory is missing but zip exists, and DB state value is incorrect, fix here */
                            $targetDir = basename($sourceFile, '.transport.zip');
                            $state = is_dir($packageDir . $targetDir) ? $this->get('state') : xPDOTransport::STATE_PACKED;
                        }
                        /* retrieve the package */
                        $this->package = xPDOTransport::retrieve($this->xpdo, $packageDir . $sourceFile, $packageDir, $state);
                        if ($this->package) {
                            /* set to unpacked state */
                            if ($state == xPDOTransport::STATE_PACKED) {
                                $this->set('state', xPDOTransport::STATE_UNPACKED);
                            }
                            $this->set('source', $sourceFile);
                            $this->set('attributes', $this->package->attributes);
                            $this->save();
                        }
                    }
                } else {
                    $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $this->xpdo->lexicon('package_err_source_nf'));
                }
            }
        }

        return $this->package;
    }

    /**
     * Removes and uninstalls the package.
     *
     * @param boolean $force     Indicates if removal should be forced even if currently installed.
     * @param boolean $uninstall Indicates if the package should be uninstalled before removal.
     *
     * @return boolean True if the package was successfully removed.
     */
    public function removePackage($force = false, $uninstall = true)
    {
        $removed = false;
        $uninstalled = false;
        if ($this->get('installed') == null || $this->get('installed') == '0000-00-00 00:00:00') {
            $uninstalled = true;
        } else {
            if ($uninstall) {
                $uninstalled = $this->uninstall();
            }
        }

        if ($uninstalled || $force) {
            $removed = $this->remove();
        }

        return $removed;
    }

    /**
     * Installs or upgrades the package.
     *
     * @access public
     *
     * @param array $options An array of installation options
     *
     * @return boolean True if successful.
     */
    public function install(array $options = [])
    {
        if (!$this->getTransport()) {
            return false;
        }

        $this->xpdo->log(xPDO::LOG_LEVEL_INFO, $this->xpdo->lexicon('workspace_grabbing'));
        $this->getOne('Workspace');
        $wc = isset($this->Workspace->config) && is_array($this->Workspace->config) ? $this->Workspace->config : [];
        $at = is_array($this->get('attributes')) ? $this->get('attributes') : [];
        $attributes = array_merge($wc, $at);
        $attributes = array_merge($attributes, $options);
        $attributes[xPDOTransport::PACKAGE_ACTION] = $this->previousVersionInstalled() ? xPDOTransport::ACTION_UPGRADE : xPDOTransport::ACTION_INSTALL;
        @ini_set('max_execution_time', 0);
        $this->xpdo->log(xPDO::LOG_LEVEL_INFO, $this->xpdo->lexicon('package_installing'));
        $requires = isset($attributes['requires']) && is_array($attributes['requires'])
            ? $attributes['requires']
            : [];
        $unsatisfied = $this->checkDependencies($requires);
        if (!empty($unsatisfied)) {
            $unsatisfied = $this->resolveDependencies($unsatisfied);
            if (!empty($unsatisfied)) {
                foreach ($unsatisfied as $dependency => $constraint) {
                    $this->xpdo->log(
                        xPDO::LOG_LEVEL_ERROR,
                        $this->xpdo->lexicon(
                            'package_dependency_unsatisfied',
                            [
                                'signature' => $this->get('signature'),
                                'requires' => "{$dependency} @ {$constraint}",
                            ]
                        )
                    );
                }
                if ($this->getOption('abort_install_on_unsatisfied_dependency', $attributes, true)) {
                    return false;
                }
            }
        }

        $installed = false;
        try {
            $installed = $this->package->install($attributes);
        } catch (Throwable $e) {
            $this->xpdo->log(
                xPDO::LOG_LEVEL_ERROR,
                $this->xpdo->lexicon('package_err_caught', [
                    'type' => $e->getPrevious() ? get_class($e->getPrevious()) : get_class($e),
                    'message' => $e->getMessage(),
                    'in' => str_replace(MODX_BASE_PATH, '/', $e->getFile()) . ':' . $e->getLine(),
                ])
            );
        }
        if ($installed) {
            $this->set('installed', date('Y-m-d H:i:s'));
            $this->set('attributes', $attributes);
            $this->save();
        }

        return $installed;
    }

    /**
     * Uninstalls the package.
     *
     * @access public
     *
     * @param array $options An array of uninstallation options
     *
     * @return boolean True if successful.
     */
    public function uninstall(array $options = [])
    {
        $uninstalled = false;
        if (!$this->getTransport()) {
            /* files have already been removed, so ignore this */
            return true;
        }
        if ($this->package) {
            $this->getOne('Workspace');
            $wc = isset($this->Workspace->config) && is_array($this->Workspace->config) ? $this->Workspace->config : [];
            $at = is_array($this->get('attributes')) ? $this->get('attributes') : [];
            $attributes = array_merge($wc, $at);
            $attributes = array_merge($attributes, $options);
            @ini_set('max_execution_time', 0);
            if ($this->package->uninstall($attributes)) {
                $uninstalled = true;
                $this->set('installed', null);
                $this->set('attributes', $attributes);
                $this->save();
            } else {
                $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $this->xpdo->lexicon('package_err_uninstall', [
                    'signature' => $this->get('signature'),
                ]));
            }
        } else {
            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $this->xpdo->lexicon('package_err_load'));
        }

        return $uninstalled;
    }

    /**
     * Transfers the package from one directory to another.
     *
     * @access public
     *
     * @param string $sourceFile The file to transfer.
     * @param string $targetDir  The directory to transfer into.
     *
     * @return boolean True if successful.
     */
    public function transferPackage($sourceFile, $targetDir)
    {
        $transferred = false;
        $content = '';
        if (is_dir($targetDir) && is_writable($targetDir)) {
            if (!is_array($this->xpdo->version)) {
                $this->xpdo->getVersionData();
            }
            $productVersion = $this->xpdo->version['code_name'] . '-' . $this->xpdo->version['full_version'];

            $source = $this->get('service_url') . $sourceFile . (
                strpos($sourceFile, '?') !== false ? '&' : '?') . 'revolution_version=' . $productVersion;

            /* see if user has allow_url_fopen on and is not behind a proxy */
            $proxyHost = $this->xpdo->getOption('proxy_host', null, '');
            if (ini_get('allow_url_fopen') && empty($proxyHost)) {
                if ($handle = @ fopen($source, 'rb')) {
                    $filesize = @ filesize($source);
                    $memory_limit = @ ini_get('memory_limit');
                    if (!$memory_limit) {
                        $memory_limit = '8M';
                    }
                    $byte_limit = $this->_bytes($memory_limit) * .5;
                    if (strpos($source, '://') !== false || $filesize > $byte_limit) {
                        $content = @ file_get_contents($source);
                    } else {
                        $content = @ fread($handle, $filesize);
                    }
                    @ fclose($handle);
                } else {
                    $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $this->xpdo->lexicon('package_err_file_read', [
                        'source' => $source,
                    ]));
                }
            }

            /* if not, try curl */
            if (empty($content) && function_exists('curl_init')) {
                $ch = curl_init();
                curl_setopt($ch, CURLOPT_URL, $source);
                curl_setopt($ch, CURLOPT_HEADER, 0);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
                curl_setopt($ch, CURLOPT_TIMEOUT, 180);
                $openBasedir = @ini_get('open_basedir');
                if (empty($openBasedir)) {
                    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
                }

                $proxyHost = $this->xpdo->getOption('proxy_host', null, '');
                if (!empty($proxyHost)) {
                    $proxyPort = $this->xpdo->getOption('proxy_port', null, '');
                    curl_setopt($ch, CURLOPT_PROXY, $proxyHost);
                    curl_setopt($ch, CURLOPT_PROXYPORT, $proxyPort);

                    $proxyUsername = $this->xpdo->getOption('proxy_username', null, '');
                    if (!empty($proxyUsername)) {
                        $proxyAuth = $this->xpdo->getOption('proxy_auth_type', null, 'BASIC');
                        $proxyAuth = $proxyAuth == 'NTLM' ? CURLAUTH_NTLM : CURLAUTH_BASIC;
                        curl_setopt($ch, CURLOPT_PROXYAUTH, $proxyAuth);

                        $proxyPassword = $this->xpdo->getOption('proxy_password', null, '');
                        $up = $proxyUsername . (!empty($proxyPassword) ? ':' . $proxyPassword : '');
                        curl_setopt($ch, CURLOPT_PROXYUSERPWD, $up);
                    }
                }
                $content = curl_exec($ch);
                curl_close($ch);
            }

            /* and as last-ditch resort, try fsockopen */
            if (empty($content)) {
                $content = $this->_getByFsockopen($source);
            }

            if ($content) {
                if ($cacheManager = $this->xpdo->getCacheManager()) {
                    $filename = $this->signature . '.transport.zip';
                    $target = $targetDir . $filename;
                    $transferred = $cacheManager->writeFile($target, $content);
                }
            } else {
                $this->xpdo->log(
                    xPDO::LOG_LEVEL_ERROR,
                    'MODX could not download the file. You must enable allow_url_fopen, cURL or fsockopen to use remote transport packaging.'
                );
            }
        } else {
            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $this->xpdo->lexicon('package_err_target_write', [
                'targetDir' => $targetDir,
            ]));
        }

        return $transferred;
    }

    /**
     * Check dependency constraints for the package.
     *
     * @param array $dependencies An array of dependencies to check.
     *
     * @return array An array of unsatisfied dependencies.
     */
    public function checkDependencies(array $dependencies)
    {
        $unsatisfied = [];
        $dependencies = xPDOTransport::checkPlatformDependencies($dependencies);
        foreach ($dependencies as $package => $constraint) {
            if (strtolower($package) === strtolower($this->identifier)) {
                continue;
            }
            switch (strtolower($package)) {
                case 'modx':
                    $versionData = $this->xpdo->getVersionData();
                    if (!xPDOTransport::satisfies($versionData['full_version'], $constraint)) {
                        $unsatisfied[$package] = $constraint;
                    }
                    break;
                default:
                    /* get latest installed package version */
                    $latestQuery = $this->xpdo->newQuery(
                        modTransportPackage::class,
                        [
                            [
                                "UCASE({$this->xpdo->escape('package_name')}) LIKE UCASE({$this->xpdo->quote($package)})",
                            ],
                            'installed:IS NOT' => null,
                        ]
                    );
                    $latestQuery->sortby('installed', 'DESC');
                    /** @var modTransportPackage $latest */
                    $latest = $this->xpdo->getObject(modTransportPackage::class, $latestQuery);
                    if ($latest) {
                        $latest->parseSignature();
                        if (xPDOTransport::satisfies($latest->version, $constraint)) {
                            unset($latest);
                            break;
                        }
                    }
                    $unsatisfied[$package] = $constraint;
                    break;
            }
        }

        return $unsatisfied;
    }

    public function checkDownloadedDependencies(array $dependencies)
    {
        $satisfied = [];
        foreach ($dependencies as $package => $constraint) {
            if (strtolower($package) === strtolower($this->identifier) || $package === 'php' || $package === 'modx') {
                continue;
            }

            /* get latest installed package version */
            $latestQuery = $this->xpdo->newQuery(
                modTransportPackage::class,
                [
                    [
                        "UCASE({$this->xpdo->escape('package_name')}) LIKE UCASE({$this->xpdo->quote($package)})",
                        'OR:signature:LIKE' => $package . '-%',
                    ],
                    'installed:IS' => null,
                ]
            );
            $latestQuery->sortby('installed', 'DESC');
            /** @var modTransportPackage $latest */
            $latest = $this->xpdo->getObject(modTransportPackage::class, $latestQuery);
            if ($latest) {
                $latest->parseSignature();
                if (xPDOTransport::satisfies($latest->version, $constraint)) {
                    $satisfied[strtolower($package)] = $latest->signature;
                    continue;
                }
            }
        }

        return $satisfied;
    }

    /**
     * Resolve unsatisfied dependencies defined for a package.
     *
     * @param array $requirements An array of unsatisfied package names and their constraints
     *
     * @return array Any unresolvable dependent package names and their failed constraints.
     */
    public function resolveDependencies(array $requirements)
    {
        $unresolved = [];
        foreach ($requirements as $dependency => $constraint) {
            if (!$this->resolveDependency($dependency, $constraint)) {
                $unresolved[$dependency] = $constraint;
            }
        }

        return $unresolved;
    }

    /**
     * Resolve an unsatisfied dependency defined for a package.
     *
     * @param string $package    The dependent package name.
     * @param string $constraint A valid version constraint for the dependent package.
     *
     * @return bool TRUE if the dependency was resolved.
     */
    public function resolveDependency($package, $constraint)
    {
        $resolved = false;
        /** @var modTransportProvider|null $provider */
        $provider = null;
        $resolution = $this->findResolution($package, $constraint, $provider);
        if ($resolution !== false) {
            /** @var modTransportPackage $transport */
            $transport = isset($resolution['transport']) ? $resolution['transport'] : null;
            if ($provider !== null) {
                $transport = $provider->transfer($resolution['signature']);
            }
            if ($transport) {
                $installed = $transport->install();
                if (!$installed) {
                    $this->xpdo->log(
                        xPDO::LOG_LEVEL_ERROR,
                        "Error installing dependency {$package} from {$transport->source}",
                        '',
                        __METHOD__,
                        __FILE__,
                        __LINE__
                    );
                }
            }
        }
        return $resolved;
    }

    /**
     * Search for package to satisfy a dependency.
     *
     * @param string                    $package    The name of the dependent package.
     * @param string                    $constraint The version constraint the package must satisfy.
     * @param modTransportProvider|null $provider   A reference which is set to the
     *                                              modTransportProvider which satisfies the dependency.
     *
     * @return array|bool The metadata for the package version which satisfies the dependency, or FALSE.
     */
    public function findResolution($package, $constraint, &$provider = null)
    {
        $resolution = false;
        $conditions = [
            'active' => true,
        ];
        switch (strtolower($package)) {
            case 'php':
                /* you must resolve php dependencies manually */
                $this->xpdo->log(
                    xPDO::LOG_LEVEL_WARN,
                    "PHP version dependencies must be resolved manually",
                    '',
                    __METHOD__,
                    __FILE__,
                    __LINE__
                );
                break;
            case 'modx':
            case 'revo':
            case 'revolution':
                /* resolve core dependencies manually for now */
                $this->xpdo->log(
                    xPDO::LOG_LEVEL_ERROR,
                    "MODX core version dependencies must be resolved manually",
                    '',
                    __METHOD__,
                    __FILE__,
                    __LINE__
                );
                break;
            default:
                /* TODO: scan for local packages to satisfy dependency */

                /* see if current provider can satisfy dependency */
                /** @var modTransportProvider $provider */
                $provider = $this->Provider;
                if ($provider) {
                    $resolution = $provider->latest($package, $constraint);
                }
                /* loop through active providers if all else fails */
                if (empty($resolution)) {
                    $query = $this->xpdo->newQuery(modTransportProvider::class, $conditions);
                    $query->sortby('priority', 'ASC');
                    /** @var modTransportProvider $p */
                    foreach ($this->xpdo->getIterator(modTransportProvider::class, $query) as $p) {
                        $resolution = $p->latest($package, $constraint);
                        if ($resolution) {
                            $provider = $p;
                            break;
                        }
                    }
                }
                if (empty($resolution)) {
                    $this->xpdo->log(
                        xPDO::LOG_LEVEL_ERROR,
                        "Could not find package to satisfy dependency {$package} @ {$constraint} from your currently active providers",
                        '',
                        __METHOD__,
                        __FILE__,
                        __LINE__
                    );
                }
                break;
        }

        return $resolution;
    }

    /**
     * Converts to bytes from PHP ini_get() format.
     *
     * PHP ini modifiers for byte values:
     * <ul>
     *  <li>G = gigabytes</li>
     *  <li>M = megabytes</li>
     *  <li>K = kilobytes</li>
     * </ul>
     *
     * @access protected
     *
     * @param string $value Number of bytes represented in PHP ini value format.
     *
     * @return integer The value converted to bytes.
     */
    protected function _bytes($value)
    {
        return ini_parse_quantity(trim($value));
    }

    /**
     * If for some reason the server does not have allow_url_fopen or cURL
     * enabled, use this function to get the file via fsockopen.
     *
     * @access protected
     *
     * @param string $url The source URL to retrieve
     *
     * @return string The response from the server
     */
    protected function _getByFsockopen($url)
    {
        $purl = parse_url($url);
        $host = $purl['host'];
        $path = !empty($purl['path']) ? $purl['path'] : '/';
        if (!empty($purl['query'])) {
            $path .= '?' . $purl['query'];
        }
        $port = !empty($purl['port']) ? $purl['port'] : '80';

        $timeout = 10;
        $response = '';
        $fp = @fsockopen($host, $port, $errno, $errstr, $timeout);

        if (!$fp) {
            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Could not retrieve from ' . $url);
        } else {
            fwrite($fp, "GET $path " . $_SERVER['SERVER_PROTOCOL'] . "\r\n" .
                "Host: $host\r\n" .
                "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3\r\n" .
                "Accept: */*\r\n" .
                "Accept-Language: en-us,en;q=0.5\r\n" .
                "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" .
                "Keep-Alive: 300\r\n" .
                "Connection: keep-alive\r\n" .
                "Referer: http://$host\r\n\r\n");

            while ($line = fread($fp, 4096)) {
                $response .= $line;
            }
            fclose($fp);

            $pos = strpos($response, "\r\n\r\n");
            $response = substr($response, $pos + 4);
        }

        return $response;
    }

    /**
     * Gets a version string able to be used by version_compare for checking
     *
     * @return string The properly formatted string.
     */
    public function getComparableVersion()
    {
        $v = explode('-', $this->get('signature'));
        array_shift($v);
        $v = implode('-', $v);
        $v = str_replace('-ga', '-pl', $v);

        return $v;
    }

    /**
     * Compares this version of the package to another
     *
     * @param string $version   The version to compare to. Must be a PHP-
     *                          standardized version.
     * @param string $direction The direction to compare. Defaults to <=
     *
     * @return boolean Result of comparison.
     */
    public function compareVersion($version, $direction = '<=')
    {
        $v = $this->getComparableVersion();

        return version_compare($version, $v, $direction);
    }

    /**
     * Indicates if a previous version of the package is installed.
     *
     * @return boolean True if a previous version of the package is installed.
     */
    public function previousVersionInstalled()
    {
        $this->parseSignature();
        $count = $this->xpdo->getCount(modTransportPackage::class, [
            ["UCASE({$this->xpdo->escape('package_name')}) LIKE UCASE({$this->xpdo->quote($this->identifier)})"],
            'installed:IS NOT' => null,
            'signature:!=' => $this->get('signature'),
        ]);

        return $count > 0;
    }
}
