TikiWiki/tiki-manager

View on GitHub
src/Access/SSH.php

Summary

Maintainability
F
3 days
Test Coverage
<?php
// Copyright (c) 2016, Avan.Tech, et. al.
// Copyright (c) 2008, Luis Argerich, Garland Foster, Eduardo Polidor, et. al.
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.

namespace TikiManager\Access;

use TikiManager\Application\Tiki\Versions\TikiRequirements;
use TikiManager\Config\Environment as Env;
use TikiManager\Libs\Host\SSH as SSHHost;
use TikiManager\Libs\Host\Command;
use TikiManager\Application\Instance;
use TikiManager\Command\Helper\CommandHelper;

class SSH extends Access implements ShellPrompt
{
    private $location;
    private $env = [];
    private $changeLocation = null;

    public function __construct(Instance $instance)
    {
        parent::__construct($instance, 'ssh');
        $this->port = 22;
    }

    public function getHost()
    {
        $host = new SSHHost($this->host, $this->user, $this->port);

        // change cwd before executing commands, for instance in CoreOS it may influence what
        // php interpreter version is used to execute commands, if the dir is not available
        // try the parent directory
        if ($this->changeLocation === null && !empty($this->instance->webroot)) {
            $output = $host->runCommands(['cd ' . $this->instance->webroot . ' && echo EXISTS']);
            if (trim($output) == "EXISTS") {
                $this->changeLocation = $this->instance->webroot;
            } else {
                $output = $host->runCommands(['cd ' . dirname($this->instance->webroot) . ' && echo EXISTS']);
                if (trim($output) == "EXISTS") {
                    $this->changeLocation = dirname($this->instance->webroot);
                }
            }
            if ($this->changeLocation === null) {
                $this->changeLocation = false;
            }
        }
        if ($this->changeLocation) {
            $host->chdir($this->changeLocation);
        }

        return $host;
    }

    public function firstConnect()
    {
        $host = $this->getHost();
        $host->setupKey($_ENV['SSH_PUBLIC_KEY']);

        $this->io->writeln("Testing connection...");

        $host->runCommands('exit');

        $answer = $this->io->confirm('After successfully entering your password, were you asked for a password again?', false);

        if ($answer == 'yes') {
            $this->changeType('ssh::nokey');
        }

        return true;
    }

    // FIXME: Expect all remote to be Unix-like machines
    public function getInterpreterPath(TikiRequirements $requirements = null)
    {
        $instance = $this->instance;
        $detectedBinaries = $instance->phpexec ? [$instance->phpexec] : $instance->getDiscovery()->detectPHP();

        $valid = [];

        foreach ($detectedBinaries as $binary) {
            try {
                $version = $this->getInterpreterVersion($binary);
            } catch (\Exception $e) {
                continue;
            }

            $formattedVersion = CommandHelper::formatPhpVersion($version);
            if (($version >= 50300 && !$requirements) ||
                $requirements->getPhpVersion()->isValidVersion($formattedVersion)) {
                $valid[$formattedVersion] = $binary;
            }
        }

        if (count($valid) == 1) {
            return reset($valid);
        }

        // Instance current PHPExec no longer valid, re-detect again!
        if ($instance->phpexec) {
            $instance->phpexec = null;
            return $this->getInterpreterPath($requirements);
        }

        if (empty($valid)) {
            throw new \Exception("No suitable php interpreter was found on {$instance->name} instance");
        }

        // Assume that the first in the list should be the default one;
        $defaultVersion = key($valid);

        // List available options for user
        krsort($valid);

        $question = 'Multiple PHP interpreters available on host, which version do you want to use?';
        $options = array_keys($valid);
        $pickedVersion = $this->io->choice($question, $options, $defaultVersion);

        return $valid[$pickedVersion];
    }

    public function getSVNPath()
    {
        $host = $this->getHost();

        $sets = [
            ['which svn'],
        ];
        $svn_name='svn';

        foreach ($sets as $attempt) {
            // Get possible paths
            $svns = $host->runCommands($attempt);
            $svns = explode("\n", $svns);

            // Check different versions
            $valid = [];
            foreach ($svns as $interpreter) {
                if (! in_array(basename($interpreter), [$svn_name])) {
                    continue;
                }

                $versionInfo = $host->runCommands("$interpreter --version");
                if (preg_match('/svn, version (\d+\.\d+\.\d+)/', $versionInfo, $matches)) {
                    $valid[$matches[1]] = $interpreter;
                }
            }

            // Handle easy cases
            if (count($valid) == 0) {
                continue;
            }
            if (count($valid) == 1) {
                return reset($valid);
            }

            // List available options for user
            $this->io->writeln("Multiple SVN'es available on host :");
            $counter = 0;
            krsort($valid);
            $versions = array_keys($valid);
            foreach ($valid as $version => $path) {
                $this->io->writeln("[$counter] $path ($version)");
                $counter++;
            }

            // Ask user
            $counter--;
            $selection = -1;
            while (! array_key_exists($selection, $versions)) {
                $selection = $this->io->ask("Which version do you want to use? (0-$counter) : ");
            }

            $version = $versions[$selection];
            return $valid[$version];
        }
    }

    public function getInterpreterVersion($interpreter)
    {
        return $this->instance->getDiscovery()->detectPHPVersion($interpreter);
    }

    public function getDistributionName($interpreter)
    {
        $host = $this->getHost();
        $command = file_get_contents(
            sprintf('%s/../getlinuxdistro.php', dirname(__FILE__))
        );
        $linuxName = $host->runCommands("$interpreter -r '$command'");

        return $linuxName;
    }

    public function createDirectory($path)
    {
        $options = ['-m', '777', '-p', $path];
        $command = $this->createCommand('mkdir', $options);

        return $command->run()->getReturn() == 0;
    }

    public function fileExists($filename)
    {
        if ($filename[0] != '/') {
            $filename = $this->instance->getWebPath($filename);
        }

        $command = $this->createCommand('test', ['-f', $filename]);
        $isFile = $command->run()->getReturn() == 0;

        $command = $this->createCommand('test', ['-d', $filename]);
        $isDir = $command->run()->getReturn() == 0;

        return $isFile || $isDir;
    }

    public function fileGetContents($filename)
    {
        $host = $this->getHost();
        $filename = escapeshellarg($filename);

        return $host->runCommands("cat $filename");
    }

    public function fileModificationDate($filename)
    {
        $host = $this->getHost();
        $root = escapeshellarg($filename);
        $data = $host->runCommands("ls -l $root");

        if (preg_match('/\d{4}-\d{2}-\d{2}/', $data, $matches)) {
            return $matches[0];
        } else {
            return null;
        }
    }

    public function runPHP($localFile, $args = [])
    {
        $host = $this->getHost();

        $remoteName = md5($localFile);
        $remoteFile = $this->instance->getWorkPath($remoteName);
        $host->runCommands(
            'mkdir -p ' . escapeshellarg($this->instance->tempdir)
        );

        $host->sendFile($localFile, $remoteFile);
        $arg = implode(' ', array_map('escapeshellarg', $args));
        $output = $host->runCommands(
            "{$this->instance->phpexec} -q -d memory_limit=256M {$remoteFile} {$arg}",
            "rm {$remoteFile}"
        );

        return $output;
    }

    /**
     * @param $filename
     * @param string $target
     * @return string
     */
    public function downloadFile($filename, $target = ''): string
    {
        if ($filename[0] != '/') {
            $filename = $this->instance->getWebPath($filename);
        }

        $dot = strrpos($filename, '.');
        $ext = substr($filename, $dot);

        $tempFolder = Env::get('TEMP_FOLDER');
        $local = $target ?: tempnam($tempFolder, 'trim');

        $this->getHost()->receiveFile($filename, $local);

        if (!$target) {
            $target = $local . $ext;
            rename($local, $target);
            chmod($target, 0644);
        }

        return $target;
    }

    public function uploadFile($filename, $remoteLocation)
    {
        $host = $this->getHost();
        if ($remoteLocation[0] == '/' || strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
            $host->sendFile($filename, $remoteLocation);
        } else {
            $host->sendFile($filename, $this->instance->getWebPath($remoteLocation));
        }
    }

    public function deleteFile($filename)
    {
        if ($filename[0] != '/' || strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
            $filename = $this->instance->getWebPath($filename);
        }

        $path = escapeshellarg($filename);

        $host = $this->getHost();
        $host->runCommands("rm $path");
    }

    public function moveFile($remoteSource, $remoteTarget)
    {
        if ($remoteSource[0] != '/' && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
            $remoteSource = $this->instance->getWebPath($remoteSource);
        }
        if ($remoteTarget[0] != '/' && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
            $remoteTarget = $this->instance->getWebPath($remoteTarget);
        }

        $a = escapeshellarg($remoteSource);
        $b = escapeshellarg($remoteTarget);

        $this->shellExec("mv $a $b");
    }

    public function copyFile($remoteSource, $remoteTarget)
    {
        if ($remoteSource[0] != '/' && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
            $remoteSource = $this->instance->getWebPath($remoteSource);
        }
        if ($remoteTarget[0] != '/' && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
            $remoteTarget = $this->instance->getWebPath($remoteTarget);
        }

        $a = escapeshellarg($remoteSource);
        $b = escapeshellarg($remoteTarget);

        $this->shellExec("cp $a $b");
    }

    public function chdir($location)
    {
        $this->location = $location;
    }

    public function setenv($var, $value)
    {
        $this->env[$var] = $value;
    }

    public function shellExec($commands, $output = false)
    {
        if (!is_array($commands)) {
            $argv = func_get_args();
            $argc = count($argv);
            $commands = $argv;
            $commands = array_filter($commands, 'is_string');
            $commands = array_filter($commands, 'strlen');
            $output = is_bool($argv[$argc - 1]) && $argv[$argc - 1];
        }

        $host = $this->getHost();
        if ($this->location) {
            $host->chdir($this->location);
        }

        foreach ($this->env as $key => $value) {
            $host->setenv($key, $value);
        }

        return $host->runCommands($commands, $output);
    }

    public function createCommand($bin, $args = [], $stdin = '')
    {
        $options = [];

        if ($this->location) {
            $options['cwd'] = $this->location;
        }
        if ($this->env) {
            $options['env'] = $this->env;
        }

        $command = new Command($bin, $args, $stdin);
        $command->setOptions($options);
        $command->setHost($this->getHost());
        return $command;
    }

    public function runCommand($command, $options = [])
    {
        $host = $this->getHost();

        if ($this->location) {
            $options['cwd'] = $this->location;
        }
        if ($this->env) {
            $options['env'] = $this->env;
        }

        return $command->run($host, $options);
    }

    public function openShell($workingDir = '')
    {
        $host = $this->getHost();
        return $host->openShell($workingDir);
    }

    public function hasExecutable($command)
    {
        $command = escapeshellcmd($command);
        $exists = $this->shellExec("which $command");

        return ! empty($exists);
    }

    public function localizeFolder($remoteLocation, $localMirror)
    {
        $host = $this->getHost();
        return $host->rsync([
            'src' => $remoteLocation,
            'dest' => $localMirror,
            'download' => true
        ]);
    }

    public function isEmptyDir($path)
    {
        $phpexec = $this->getInterpreterPath();

        $script = sprintf("echo serialize(scandir('%s'));", $path);

        $command = $this->createCommand($phpexec, ["-r {$script}"]);
        $output = $command->run()->getStdoutContent();

        $dirContents = unserialize($output);
        $dirContents = array_diff($dirContents, ['.', '..']);

        return array_values($dirContents);
    }

    /**
     * Get generic rsync prefix for the access to the files of this SSH host
     * @return string
     */
    public function getRsyncPrefix()
    {
        return $this->user . "@" . $this->host . ":";
    }
}