<?php
/**
 * Validates p4 port value for valid hostname (if provided) and the presence
 * of a numeric port.
 *
 * The value should be in one of the following forms:
 *
 *            port only:  1666
 *    hostname and port:  perforce:1666
 *  ip address and port:  192.168.0.100:1666
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */

namespace P4\Validate;

class Port extends AbstractValidate
{
    const INVALID_PORT          = 'invalidPort';
    const INVALID_HOST          = 'invalidHost';
    const INVALID_RSH           = 'invalidRsh';

    protected $messageTemplates = array(
        self::INVALID_PORT  => "'%value%' does not appear to contain a valid numeric port.",
        self::INVALID_HOST  => "'%value%' appears to have a invalid hostname component.",
        self::INVALID_RSH   => "Value does not appear to be a valid 'rsh' port."
    );

    /**
     * Checks value for valid hostname (if present) and the
     * presence of a numeric port.
     *
     * @param   string   $value  port value to validate.
     * @return  boolean  true if port is in a valid format.
     */
    public function isValid($value)
    {
        $this->set($value);

        // split out protocol/host/port
        // the protocol is optional and only one of host/port need be present.
        $parts = explode(':', $value);

        // if we lead with a recognized protocol; strip it off
        // and continue validating the other components.
        if (in_array($parts[0], array("tcp", "tcp4", "tcp6", "ssl", "ssl4", "ssl6"))) {
            array_shift($parts);
        }

        // if we only have a single part it should be a port; pull it out.
        // if more parts are present we have an rsh or port/host style.
        if (count($parts) == 1) {
            $port = $parts[0];
        } else {
            // allow directly (inetd) invoked p4d
            if ($parts[0] == 'rsh') {
                // attempt to seperate out the 'p4d' executable path
                // if we get no result this is an invalid rsh port.
                $subParts = str_getcsv($parts[1], ' ');
                if (!isset($subParts[0])) {
                    $this->error(self::INVALID_RSH);
                    return false;
                }

                // run the specified executable and verify we get
                // back 'p4d' style response data
                exec('"' . escapeshellcmd($subParts[0]) . '" -V', $output);
                $output = implode("\n", $output);
                if (!preg_match("#^Rev. P4D/#m", $output)) {
                    $this->error(self::INVALID_RSH);
                    return false;
                }

                return true;
            }

            $host = $parts[0];
            $port = $parts[1];
        }

        // check that port is numeric.
        if (!is_numeric($port)) {
            $this->error(self::INVALID_PORT);
            return false;
        }

        // validate host if present.
        if (isset($host)) {
            // @see http://stackoverflow.com/questions/1418423/the-hostname-regex
            $pattern = '/^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}'
                     . '[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-)'
                     . '{0,61}[0-9A-Za-z])?)*\.?$/';

            if (!preg_match($pattern, $host)) {
                $this->error(self::INVALID_HOST);
                return false;
            } else {
                return true;
            }
        } else {
            return true;
        }
    }
}