crystalservice/samba

View on GitHub
src/Samba/SambaClient.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace Samba;

class SambaClient
{
    const SOCKET_OPTIONS = "TCP_NODELAY IPTOS_LOWDELAY SO_KEEPALIVE SO_RCVBUF=8192 SO_SNDBUF=8192";
    const CLIENT = "smbclient";

    /**
     * @var array
     */
    protected $regexp = array(
        '^added interface ip=(.*) bcast=(.*) nmask=(.*)$' => 'skip',
        'Anonymous login successful' => 'skip',
        '^Domain=\[(.*)\] OS=\[(.*)\] Server=\[(.*)\]$' => 'skip',
        '^\tSharename[ ]+Type[ ]+Comment$' => 'shares',
        '^\t---------[ ]+----[ ]+-------$' => 'skip',
        '^\tServer   [ ]+Comment$' => 'servers',
        '^\t---------[ ]+-------$' => 'skip',
        '^\tWorkgroup[ ]+Master$' => 'workgroups',
        '^\t(.*)[ ]+(Disk|IPC)[ ]+IPC.*$' => 'skip',
        '^\tIPC\\\$(.*)[ ]+IPC' => 'skip',
        '^\t(.*)[ ]+(Disk)[ ]+(.*)$' => 'share',
        '^\t(.*)[ ]+(Printer)[ ]+(.*)$' => 'skip',
        '([0-9]+) blocks of size ([0-9]+)\. ([0-9]+) blocks available' => 'skip',
        'Got a positive name query response from ' => 'skip',
        '^(session setup failed): (.*)$' => 'error',
        '^(.*): ERRSRV - ERRbadpw' => 'error',
        '^Error returning browse list: (.*)$' => 'error',
        '^tree connect failed: (.*)$' => 'error',
        '^(Connection to .* failed)$' => 'error',
        '^NT_STATUS_(.*) ' => 'error',
        '^NT_STATUS_(.*)\$' => 'error',
        'ERRDOS - ERRbadpath \((.*).\)' => 'error',
        'cd (.*): (.*)$' => 'error',
        '^cd (.*): NT_STATUS_(.*)' => 'error',
        '^\t(.*)$' => 'srvorwg',
        '^([0-9]+)[ ]+([0-9]+)[ ]+(.*)$' => 'skip',
        '^Job ([0-9]+) cancelled' => 'skip',
        '^[ ]+(.*)[ ]+([0-9]+)[ ]+(Mon|Tue|Wed|Thu|Fri|Sat|Sun)[ ](Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[ ]+([0-9]+)[ ]+([0-9]{2}:[0-9]{2}:[0-9]{2})[ ]([0-9]{4})$' => 'files',
        '^message start: ERRSRV - (ERRmsgoff)' => 'error',
        '^Connection to (.+) failed \(Error (.+)\)$' => 'error',
    );

    /**
     * @param string $url
     * @return SambaUrl
     */
    public function parseUrl($url)
    {
        return new SambaUrl($url);
    }

    /**
     * @param SambaUrl $url
     * @return array
     */
    public function look(SambaUrl $url)
    {
        return $this->client('-L ' . escapeshellarg($url->getHost()), $url);
    }

    /**
     * @param string $command
     * @param SambaUrl $url
     * @return array
     */
    public function execute($command, SambaUrl $url)
    {
        return $this->client(
            '-d 0 '
            . escapeshellarg($url->getHostShare())
            . ' -c ' . escapeshellarg($command),
            $url
        );
    }

    /**
     * @param $line
     * @return array
     */
    protected function getTag($line)
    {
        $tag = 'skip';
        $regs = array();
        foreach ($this->regexp as $regexp => $t) {
            if (preg_match('/' . $regexp . '/', $line, $regs)) {
                $tag = $t;
                break;
            }
        }
        return array($tag, $regs);
    }

    /**
     * @param string $params
     * @param SambaUrl $url
     * @throws SambaException
     * @return array
     */
    public function client($params, SambaUrl $url)
    {
        $options = $this->createOptions($url);
        $output = $this->getProcessResource($params, $options);

        try {
            $info = $this->parseOutput($output);
            $this->closeProcessResource($output);
            return $info;
        } catch (SambaException $e) {
            $this->closeProcessResource($output);
            throw $e;
        }
    }

    /**
     * @param SambaUrl $url
     * @return array
     */
    protected function createOptions(SambaUrl $url)
    {
        $options = array();
        $options['-O'] = self::SOCKET_OPTIONS;
        if ($url->getUser()) {
            $options['-U'] = "{$url->getUser()}%{$url->getPass()}";
        }
        if ($url->getDomain()) {
            $options['-W'] = $url->getDomain();
        }
        if (!$url->isDefaultPort()) {
            $options['-p'] = $url->getPort();
        }
        return $options;
    }

    # stats

    /**
     * @param SambaUrl $url
     * @return array
     */
    public function info(SambaUrl $url)
    {
        switch ($url->getType()) {
            case SambaUrl::TYPE_HOST:
                $info = $this->hostInfo($url);
                break;
            case SambaUrl::TYPE_SHARE:
                $info = $this->shareInfo($url);
                break;
            case SambaUrl::TYPE_PATH:
                $info = $this->pathInfo($url);
                break;
            default:
                throw new SambaException('error in URL');
        }

        return $info;
    }

    /**
     * @param SambaUrl $url
     * @return array
     * @throws SambaException
     */
    protected function hostInfo(SambaUrl $url)
    {
        if ($lookInfo = $this->look($url)) {
            return array(
                'attr' => 'D',
                'size' => 0,
                'time' => 0,
            );
        }

        throw new SambaException("url_stat(): list failed for host '{$url->getHost()}'");
    }

    /**
     * @param SambaUrl $url
     * @return array
     * @throws SambaException
     */
    protected function pathInfo(SambaUrl $url)
    {
        if ($output = $this->dir($url)) {
            $name = $url->getLastPath();
            if (isset($output['info'][$name])) {
                return $output['info'][$name];
            }
        }

        throw new SambaException("url_stat(): dir failed for path '{$url->getPath()}'");
    }

    /**
     * @param SambaUrl $url
     * @return array
     * @throws SambaException
     */
    protected function shareInfo(SambaUrl $url)
    {
        $lowerShare = strtolower($url->getShare()); # fix by Eric Leung

        if ($lookInfo = $this->look($url)) {
            foreach ($lookInfo['disk'] as $share) {
                if ($lowerShare == strtolower($share)) {
                    return array(
                        'attr' => 'D',
                        'size' => 0,
                        'time' => 0,
                    );
                }
            }
        }

        throw new SambaException(
            "url_stat(): disk resource '{$lowerShare}' not found in '{$url->getHost()}'"
        );
    }

    /**
     * @param string $params
     * @param array $options
     * @return resource
     */
    public function getProcessResource($params, array $options)
    {
        $args = '';
        foreach ($options as $key => $value) {
            $args .= ' ' . $key . ' ' . escapeshellarg($value);
        }
        $command = sprintf("%s -N %s %s 2>/dev/null", self::CLIENT, $args, $params);

        return popen($command, 'r');
    }

    /**
     * @param $output
     */
    public function closeProcessResource($output)
    {
        pclose($output);
    }

    /**
     * Commands
     */

    /**
     * @param SambaUrl $url
     * @param string $file
     * @return array
     */
    public function get(SambaUrl $url, $file)
    {
        $command = sprintf('get "%s" "%s"', $url->getPath(), $file);
        return $this->execute($command, $url);
    }

    /**
     * @param SambaUrl $url
     * @param string $file
     * @return array
     */
    public function put(SambaUrl $url, $file)
    {
        $command = sprintf('put "%s" "%s"', $file, $url->getPath());
        return $this->execute($command, $url);
    }

    /**
     * @param SambaUrl $url
     * @param string $mask
     * @return array
     */
    public function dir(SambaUrl $url, $mask = '')
    {
        $command = sprintf('dir "%s%s"', $url->getPath(), $mask);
        return $this->execute($command, $url);
    }

    /**
     * @param SambaUrl $url
     * @return array
     */
    public function del(SambaUrl $url)
    {
        $this->checkUrlIsPath($url, 'del');
        $command = sprintf('del "%s"', $url->getPath());
        return $this->execute($command, $url);
    }

    /**
     * @param SambaUrl $from purl
     * @param SambaUrl $to purl
     * @return array
     */
    public function rename(SambaUrl $from, SambaUrl $to)
    {
        $this->checkUrlIsPath($from, 'rename');
        $this->checkUrlIsPath($to, 'rename');

        if (!$from->isFromSameUserShare($to)) {
            throw new SambaException('rename: FROM & TO must be in same server-share-user-pass-domain');
        }

        $command = sprintf('rename "%s" "%s"', $from->getPath(), $to->getPath());
        return $this->execute($command, $to);
    }

    /**
     * @param SambaUrl $url
     * @return array
     */
    public function mkdir(SambaUrl $url)
    {
        $this->checkUrlIsPath($url, 'mkdir');
        $command = sprintf('mkdir "%s"', $url->getPath());
        return $this->execute($command, $url);
    }

    /**
     * @param SambaUrl $url
     * @return array
     */
    public function rmdir(SambaUrl $url)
    {
        $this->checkUrlIsPath($url, 'rmdir');

        $command = sprintf('rmdir "%s"', $url->getPath());
        return $this->execute($command, $url);
    }

    /**
     * @param SambaUrl $url
     * @param string $command
     * @throws SambaException
     */
    protected function checkUrlIsPath(SambaUrl $url, $command)
    {
        if (!$url->isPath()) {
            throw new SambaException($command . ': error - URL should be path');
        }
    }

    /**
     * @param resource $output
     * @return array
     */
    protected function parseOutput($output)
    {
        $info = array();

        while (($line = fgets($output)) !== false) {
            $i = array();

            list($tag, $regs) = $this->getTag($line);

            switch ($tag) {
                case 'skip':
                    continue;
                case 'shares':
                    $mode = 'shares';
                    break;
                case 'servers':
                    $mode = 'servers';
                    break;
                case 'workgroups':
                    $mode = 'workgroups';
                    break;
                case 'share':
                    list($name, $type) = array(
                        trim(substr($line, 1, 15)),
                        trim(strtolower(substr($line, 17, 10)))
                    );
                    $i = ($type != 'disk' && preg_match('/^(.*) Disk/', $line, $regs))
                        ? array(trim($regs[1]), 'disk')
                        : array($name, 'disk');
                    break;
                case 'srvorwg':
                    list ($name, $master) = array(
                        strtolower(trim(substr($line, 1, 21))),
                        strtolower(trim(substr($line, 22)))
                    );
                    $i = (isset($mode) && $mode == 'servers')
                        ? array($name, "server")
                        : array($name, "workgroup", $master);
                    break;
                case 'files':
                    list ($attr, $name) = preg_match("/^(.*) +([VDAHSNRdtsrcone]{1})$/", trim($regs[1]), $regs2)
                        ? array(trim($regs2[2]), trim($regs2[1]))
                        : array('', trim($regs[1]));
                    list ($his, $im) = array(
                        explode(':', $regs[6]),
                        1 + strpos("JanFebMarAprMayJunJulAugSepOctNovDec", $regs[4]) / 3
                    );
                    $i = ($name != '.' && $name != '..')
                        ? array(
                            $name,
                            (strpos($attr, 'D') === false) ? 'file' : 'folder',
                            'attr' => $attr,
                            'size' => intval($regs[2]),
                            'time' => mktime($his[0], $his[1], $his[2], $im, $regs[5], $regs[7])
                        )
                        : array();
                    break;
                case 'error':
                    throw new SambaException($regs[0]);
            }
            if ($i) {
                switch ($i[1]) {
                    case 'file':
                    case 'folder':
                        $info['info'][$i[0]] = $i;
                        $info[$i[1]][] = $i[0];
                        break;
                    case 'disk':
                    case 'server':
                    case 'workgroup':
                        $info[$i[1]][] = $i[0];
                        break;
                }
            }
        }

        return $info;
    }
}