FriendsOfPHP/pickle

View on GitHub
src/Package/PHP/Command/Install/Windows/Binary.php

Summary

Maintainability
B
6 hrs
Test Coverage
<?php

/*
 * Pickle
 *
 *
 * @license
 *
 * New BSD License
 *
 * Copyright © 2015-2015, Pickle community. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Hoa nor the names of its contributors may be
 *       used to endorse or promote products derived from this software without
 *       specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

namespace Pickle\Package\PHP\Command\Install\Windows;

use DOMDocument;
use Exception;
use Pickle\Base\Archive;
use Pickle\Base\Pecl\WebsiteFactory;
use Pickle\Base\Util;
use Pickle\Base\Util\FileOps;
use Pickle\Engine;
use Symfony\Component\Console\Output\OutputInterface as OutputInterface;

class Binary
{
    use FileOps;

    private $php;

    private $extName;

    private $extVersion;

    private $progress;

    private $output;

    private $extDll;

    /**
     * @param string $ext
     */
    public function __construct($ext)
    {
        // used only if only the extension name is given
        if (strpos('//', $ext) !== false) {
            $this->extensionPeclExists();
        }

        $this->extName = $ext;
        $this->php = Engine::factory();
    }

    public function setProgress($progress)
    {
        $this->progress = $progress;
    }

    public function setOutput(OutputInterface $output)
    {
        $this->output = $output;
    }

    /**
     *  1. check if ext exists
     *  2. check if given version requested
     *  2.1 yes? check if builds available
     *  2.2 no? get latest version+build.
     *
     * @throws Exception
     */
    public function install()
    {
        [$this->extName, $this->extVersion] = $this->getInfoFromPecl();
        $url = $this->fetchZipName();
        $pathArchive = $this->download($url);
        $this->uncompress($pathArchive);
        $this->copyFiles();
        $this->cleanup();
        $this->updateIni();
    }

    public function getExtDllPaths()
    {
        $ret = [];

        foreach ($this->extDll as $dll) {
            $ret[] = $this->php->getExtensionDir() . DIRECTORY_SEPARATOR . $dll;
        }

        return $ret;
    }

    private function extensionPeclExists()
    {
        $url = WebsiteFactory::getWebsite()->getBaseUrl() . '/get/' . $this->extName;
        $headers = get_headers($url, 1);
        $status = $headers[0];
        if (strpos($status, '404')) {
            throw new Exception("Extension <{$this->extName}> cannot be found");
        }
    }

    /**
     * @param string $url
     */
    private function findInLinks($url, $toFind)
    {
        $page = @file_get_contents($url);
        $opts = [
            'http' => [
                'header' => 'User-Agent: pickle',
            ],
        ];
        $context = stream_context_create($opts);
        $page = @file_get_contents($url, false, $context);
        if (!$page) {
            return false;
        }
        $dom = new DOMDocument();
        $dom->loadHTML($page);
        $links = $dom->getElementsByTagName('a');
        if (!$links) {
            return false;
        }

        foreach ($links as $link) {
            if ($link->nodeValue[0] == '[') {
                continue;
            }
            $value = trim($link->nodeValue);
            if ($toFind == $value) {
                return $value;
            }
        }

        return false;
    }

    /**
     * @throws Exception
     *
     * @return string
     */
    private function fetchZipName()
    {
        $phpVc = $this->php->getCompiler();
        $phpArch = $this->php->getArchitecture();
        $phpZts = $this->php->getZts() ? '-ts' : '-nts';
        $phpVersion = $this->php->getMajorVersion() . '.' . $this->php->getMinorVersion();
        $pkgVersion = $this->extVersion;
        $extName = strtolower($this->extName);
        $baseUrl = 'https://windows.php.net/downloads/pecl/releases/';

        if ($this->findInLinks($baseUrl . $extName, $pkgVersion) === false) {
            throw new Exception('Binary for <' . $extName . '-' . $pkgVersion . '> cannot be found');
        }

        $fileToFind = 'php_' . $extName . '-' . $pkgVersion . '-' . $phpVersion . $phpZts . '-' . $phpVc . '-' . $phpArch . '.zip';
        $fileUrl = $this->findInLinks($baseUrl . $extName . '/' . $pkgVersion, $fileToFind);

        if (!$fileUrl) {
            throw new Exception('Binary for <' . $fileToFind . '> cannot be found');
        }
        return $baseUrl . $extName . '/' . $pkgVersion . '/' . $fileToFind;
    }

    /**
     * @param string $zipFile
     *
     * @throws Exception
     */
    private function uncompress($zipFile)
    {
        $this->createTempDir($this->extName);
        $this->cleanup();
        $zipClass = Archive\Factory::getUnzipperClassName();
        $zipArchive = $zipClass($zipFile);
        /** @var \Pickle\Base\Interfaces\Archive\Unzipper $zipArchive */
        $this->output->writeln('Extracting archives...');
        $zipArchive->extractTo($this->tempDir);
    }

    /**
     * @param string $url
     *
     * @throws Exception
     *
     * @return string
     */
    private function download($url)
    {
        $progress = $this->progress;
        $progress->setOverwrite(true);
        $ctx = stream_context_create(
            [
                'http' => [
                    'header' => 'User-Agent: pickle',
                ],
            ],
            [
                'notification' => function ($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax) use ($progress) {
                    switch ($notificationCode) {
                        case STREAM_NOTIFY_RESOLVE:
                        case STREAM_NOTIFY_AUTH_REQUIRED:
                        case STREAM_NOTIFY_COMPLETED:
                        case STREAM_NOTIFY_FAILURE:
                        case STREAM_NOTIFY_AUTH_RESULT:
                            break;

                        case STREAM_NOTIFY_REDIRECTED:
                            break;

                        case STREAM_NOTIFY_CONNECT:
                            break;

                        case STREAM_NOTIFY_FILE_SIZE_IS:
                            $progress->start($bytesMax);
                            break;

                        case STREAM_NOTIFY_MIME_TYPE_IS:
                            break;

                        case STREAM_NOTIFY_PROGRESS:
                            $progress->setProgress($bytesTransferred);
                            break;
                    }
                },
            ]
        );
        if ($this->output) {
            $this->output->writeln("downloading {$url} ");
        }
        $fileContents = file_get_contents($url, false, $ctx);
        $progress->finish();
        if (!$fileContents) {
            throw new Exception('Cannot fetch <' . $url . '>');
        }
        $tmpdir = Util\TmpDir::get();
        $path = $tmpdir . '/' . $this->extName . '.zip';
        if (!file_put_contents($path, $fileContents)) {
            throw new Exception('Cannot save temporary file <' . $path . '>');
        }

        return $path;
    }

    /**
     * @throws Exception
     */
    private function copyFiles()
    {
        $DLLs = glob($this->tempDir . '/*.dll');
        $this->extDll = [];
        foreach ($DLLs as $dll) {
            $dll = realpath($dll);
            $basename = basename($dll);
            $dest = $this->php->getExtensionDir() . DIRECTORY_SEPARATOR . $basename;
            if (substr($basename, 0, 4) == 'php_') {
                $this->extDll[] = $basename;
                $this->output->writeln("copying {$dll} to " . $dest . "\n");
                $success = copy($dll, $this->php->getExtensionDir() . '/' . $basename);
                if (!$success) {
                    throw new Exception('Cannot copy DLL <' . $dll . '> to <' . $dest . '>');
                }
            } else {
                $success = copy($dll, dirname($this->php->getPath()) . '/' . $basename);
                if (!$success) {
                    throw new Exception('Cannot copy DLL <' . $dll . '> to <' . $dest . '>');
                }
            }
        }
    }

    /**
     * @throws Exception
     */
    private function updateIni()
    {
        $ini = \Pickle\Engine\Ini::factory($this->php);
        $ini->updatePickleSection($this->extDll);
    }

    /**
     * @throws Exception
     *
     * @return array
     */
    private function getInfoFromPecl()
    {
        $baseUrl = WebsiteFactory::getWebsite()->getBaseUrl();
        $url = $baseUrl . '/get/' . $this->extName;
        $headers = get_headers($url);

        if (strpos($headers[0], '404') !== false) {
            throw new Exception('Cannot find extension <' . $this->extName . '>');
        }
        $headerPkg = null;
        foreach ($headers as $header) {
            if (strpos($header, 'tgz') !== false) {
                $headerPkg = $header;
                break;
            }
        }
        if ($headerPkg === null) {
            throw new Exception('Cannot find extension <' . $this->extName . '>');
        }
        $m = null;
        if (!preg_match('|=(.*)\\.[a-z0-9]{2,3}$|', $headerPkg, $m)) {
            throw new Exception("Invalid response from {$baseUrl}");
        }
        $packageFullname = $m[1];

        [$name, $version] = explode('-', $packageFullname);
        if ($name == '' || $version == '') {
            throw new Exception("Invalid response from {$baseUrl}");
        }

        return [$name, $version];
    }
}

/* vim: set tabstop=4 shiftwidth=4 expandtab: fdm=marker */