Abstract.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • library/
  • P4/
  • Connection/
  • Abstract.php
  • View
  • Commits
  • Open Download .zip Download (27 KB)
<?php
/**
 * Abstract class for Perforce Connection implementations.
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 * @todo        verify need to call disconnect() on setClient/Password/etc.
 */
abstract class P4_Connection_Abstract implements P4_Connection_Interface
{
    const               LOG_MAX_STRING_LENGTH   = 1024;
    const               DEFAULT_CHARSET         = 'utf8unchecked';
    const               OPTION_LIMIT            = 256;

    protected           $_client;
    protected           $_info;
    protected           $_password;
    protected           $_port;
    protected           $_ticket;
    protected           $_user;
    protected           $_charset;
    protected           $_host;
    protected           $_appName;
    protected           $_disconnectCallbacks   = array();

    /**
     * Create a P4_Connection_Interface instance.
     *
     * @param   string  $port        optional - the port to connect to.
     * @param   string  $user        optional - the user to connect as.
     * @param   string  $client      optional - the client spec to use.
     * @param   string  $password    optional - the password to use.
     * @param   string  $ticket      optional - a ticket to use.
     */
    public function __construct(
        $port       = null,
        $user       = null,
        $client     = null,
        $password   = null,
        $ticket     = null)
    {
        $this->setPort($port);
        $this->setUser($user);
        $this->setClient($client);
        $this->setPassword($password);
        $this->setTicket($ticket);

        // ensure we disconnect on shutdown.
        P4_Environment::addShutdownCallback(
            array($this, 'disconnect')
        );
    }

    /**
     * Return the p4 port.
     *
     * @return  string  the port.
     */
    public function getPort()
    {
        return $this->_port;
    }

    /**
     * Set the p4 port.
     * Forces a disconnect if already connected.
     *
     * @param   string  $port               the port to connect to.
     * @return  P4_Connection_Interface     provides fluent interface.
     * @todo    validate port using port validator - make validator work with 'rsh:' ports.
     */
    public function setPort($port)
    {
        $this->_port = (string) $port;

        // disconnect on port change.
        $this->disconnect();

        return $this;
    }

    /**
     * Return the name of the p4 user.
     *
     * @return  string  the user.
     */
    public function getUser()
    {
        return $this->_user;
    }

    /**
     * Set the name of the p4 user.
     * Forces a disconnect if already connected.
     *
     * @param   string  $user               the user to connect as.
     * @return  P4_Connection_Interface     provides fluent interface.
     */
    public function setUser($user)
    {
        $validator = new P4_Validate_SpecName;

        if ($user !== null && !$validator->isValid($user)) {
            throw new P4_Exception("Username: " . implode("\n", $validator->getMessages()));
        }

        $this->_user = $user;

        // disconnect on user change.
        $this->disconnect();

        return $this;
    }

    /**
     * Return the p4 user's client.
     *
     * @return  string  the client.
     */
    public function getClient()
    {
        return $this->_client;
    }

    /**
     * Set the p4 user's client.
     * Forces a disconnect if already connected.
     *
     * @param   string  $client             the name of the client workspace to use.
     * @return  P4_Connection_Interface     provides fluent interface.
     */
    public function setClient($client)
    {
        $validator = new P4_Validate_SpecName;

        if ($client !== null && !$validator->isValid($client)) {
            throw new P4_Exception("Client name: " . implode("\n", $validator->getMessages()));
        }

        $this->_client = $client;

        // clear cached p4 info
        $this->_info = null;

        return $this;
    }

    /**
     * Retrieves the password set for this perforce connection.
     *
     * @return  string  password used to authenticate against perforce server.
     */
    public function getPassword()
    {
        return $this->_password;
    }

    /**
     * Sets the password to use for this perforce connection.
     *
     * @param   string  $password           the password to use as authentication.
     * @return  P4_Connection_Interface     provides fluent interface.
     */
    public function setPassword($password)
    {
        $this->_password = $password;

        return $this;
    }

    /**
     * Retrieves the ticket set for this perforce connection.
     *
     * @return  string  ticket as generated by perforce server.
     */
    public function getTicket()
    {
        return $this->_ticket;
    }

    /**
     * Sets the ticket to use for this perforce connection.
     * Forces a disconnect if already connected.
     *
     * @param   string  $ticket             the ticket to use as authentication.
     * @return  P4_Connection_Interface     provides fluent interface.
     */
    public function setTicket($ticket)
    {
        $this->_ticket = $ticket;

        // disconnect on ticket change.
        $this->disconnect();

        return $this;
    }

    /**
     * Retrieves the character set used by this connection.
     *
     * @return  string  charset used for this connection.
     */
    public function getCharset()
    {
        return $this->_charset;
    }

    /**
     * Sets the character set to use for this perforce connection.
     *
     * You should only set a character set when connecting to a
     * 'unicode enabled' server, or when setting the special value
     * of 'none'.
     *
     * @param   string  $charset            the charset to use (e.g. 'utf8').
     * @return  P4_Connection_Interface     provides fluent interface.
     */
    public function setCharset($charset)
    {
        $this->_charset = $charset;

        return $this;
    }

    /**
     * Retrieves the client host set for this connection.
     *
     * @return  string  host name used for this connection.
     */
    public function getHost()
    {
        return $this->_host;
    }

    /**
     * Sets the client host name overriding the environment.
     *
     * @param   string|null $host           the host name to use.
     * @return  P4_Connection_Interface     provides fluent interface.
     */
    public function setHost($host)
    {
        $this->_host = $host;

        return $this;
    }

    /**
     * Set the name of the application that is using this connection.
     *
     * The application name will be reported to the server and might
     * be necessary to satisfy certain licensing restrictions.
     *
     * @param   string|null     $name       the app name to report to the server.
     * @return  P4_Connection_Interface     provides fluent interface.
     */
    public function setAppName($name)
    {
        $this->_appName = is_null($name) ? $name : (string) $name;

        return $this;
    }

    /**
     * Get the application name being reported to the server.
     *
     * @return  string|null     the app name reported to the server.
     */
    public function getAppName()
    {
        return $this->_appName;
    }

    /**
     * Get the current client's root directory with no trailing slash.
     *
     * @return  string  the full path to the current client's root.
     */
    public function getClientRoot()
    {
        $info = $this->getInfo();
        if (isset($info['clientRoot'])) {
            return rtrim($info['clientRoot'], '/\\');
        }
        return false;
    }

    /**
     * Return an array of connection information.
     * Due to caching, server date may be stale.
     *
     * @return  array   the connection information ('p4 info').
     */
    public function getInfo()
    {
        // if info cache is populated and connection is up, return cached info.
        if (isset($this->_info) && $this->isConnected()) {
            return $this->_info;
        }

        // run p4 info.
        $result      = $this->run("info");
        $this->_info = array();

        // gather all data (multiple arrays returned when connecting through broker).
        foreach ($result->getData() as $data) {
            $this->_info += $data;
        }

        return $this->_info;
    }

    /**
     * Clear the info cache. This method is primarily used during testing,
     * and would not normally be used.
     *
     * @return  P4_Connection_Interface     provides fluent interface.
     */
    public function clearInfo()
    {
        $this->_info = null;

        return $this;
    }

    /**
     * Authenticate the user with 'p4 login'.
     *
     * @return  string|null     the ticket issued by the server or null if
     *                          no ticket issued (ie. user has no password).
     * @throws  P4_Connection_LoginException    if login fails.
     */
    public function login()
    {
        // ensure user name is set.
        if (!strlen($this->getUser())) {
            throw new P4_Connection_LoginException(
                "Login failed. Username is empty.",
                P4_Connection_LoginException::IDENTITY_AMBIGUOUS
            );
        }

        // try to login.
        try {
            $result = $this->run('login', '-p', $this->_password ?: '');
        } catch (P4_Connection_CommandException $e) {

            // user doesn't exist.
            if (stristr($e->getMessage(), "doesn't exist") ||
                stristr($e->getMessage(), "has not been enabled by 'p4 protect'")
            ) {
                throw new P4_Connection_LoginException(
                    "Login failed. " . $e->getMessage(),
                     P4_Connection_LoginException::IDENTITY_NOT_FOUND
                );
            }

            // invalid password.
            if (stristr($e->getMessage(), "password invalid")) {
                throw new P4_Connection_LoginException(
                    "Login failed. " . $e->getMessage(),
                     P4_Connection_LoginException::CREDENTIAL_INVALID
                );
            }

            // generic login exception.
            throw new P4_Connection_LoginException(
                "Login failed. " . $e->getMessage()
            );
        }

        // check if no password set for this user.
        // fail if a password was provided - succeed otherwise.
        if (stristr($result->getData(0), "no password set for this user")) {
            if ($this->_password) {
                throw new P4_Connection_LoginException(
                    "Login failed. " . $result->getData(0),
                     P4_Connection_LoginException::CREDENTIAL_INVALID
                );
            } else {
                return null;
            }
        }

        // capture ticket from output.
        $this->_ticket = $result->getData(0);

        // if ticket wasn't captured correctly, fail with unknown code.
        if (!$this->_ticket) {
            throw new P4_Connection_LoginException(
                "Login failed. Unable to capture login ticket."
            );
        }

        return $this->_ticket;
    }

    /**
     * Executes the specified command and returns a perforce result object.
     * No need to call connect() first. Run will connect automatically.
     *
     * Performs common pre/post-run work. Hands off to _run() for the
     * actual mechanics of running commands.
     *
     * @param   string          $command    the command to run.
     * @param   array|string    $params     optional - one or more arguments.
     * @param   array|string    $input      optional - input for the command - should be provided
     *                                      in array form when writing perforce spec records.
     * @param   boolean         $tagged     optional - true/false to enable/disable tagged output.
     *                                      defaults to true.
     * @return  P4_Result the perforce result object.
     */
    public function run($command, $params = array(), $input = null, $tagged = true)
    {
        // establish connection to perforce server.
        if (!$this->isConnected()) {
            $this->connect();
        }

        // ensure params is an array.
        if (!is_array($params)) {
            if (!empty($params)) {
                $params = array($params);
            } else {
                $params = array();
            }
        }

        // log the start of the command w. params.
        $message = "P4 (" . spl_object_hash($this) . ") start command: "
                 . $command . " " . implode(" ", $params);
        P4_Log::log(
            substr($message, 0, static::LOG_MAX_STRING_LENGTH),
            P4_Log::DEBUG
        );

        // prepare input for passing to perforce.
        $input = $this->_prepareInput($input, $command);

        // defer to sub-classes to actually issue the command.
        $result = $this->_run($command, $params, $input, $tagged);

        // log errors - log them and throw an exception.
        if ($result->hasErrors()) {

            // if we have no charset, and the command failed because we are
            // talking to a unicode server, automatically use the default
            // charset and run the command again.
            $errors = $result->getErrors();
            $needle = 'Unicode server permits only unicode enabled clients.';
            if (!$this->getCharset() && stripos($errors[0], $needle) !== false) {
                $this->setCharset(static::DEFAULT_CHARSET);

                // run the command again now that we have a charset.
                return call_user_func_array(
                    array($this, 'run'),
                    func_get_args()
                );
            }

            // if connect failed due to an untrusted server, trust it and retry
            $needle = "To allow connection use the 'p4 trust' command";
            if (stripos($errors[0], $needle) !== false && !$this->_hasTrusted) {
                // add a property to avoid re-recursing on this test
                $this->_hasTrusted = true;

                // trust the connection as this is the first time we have seen it
                $this->run('trust', '-y');

                // run the command again now that we have trusted it
                return call_user_func_array(
                    array($this, 'run'),
                    func_get_args()
                );
            }

            $message = "P4 (" . spl_object_hash($this) . ") command failed: "
                     . implode("\n", $result->getErrors());
            P4_Log::log(
                substr($message, 0, static::LOG_MAX_STRING_LENGTH),
                P4_Log::ERR
            );

            $this->_handleError($result);
        }

        return $result;
    }

    /**
     * Check if the user we are connected as has super user privileges.
     *
     * @return  bool    true if the user has super, false otherwise.
     */
    public function isSuperUser()
    {
        try {
            $result = $this->run("protects", "-m");
        } catch (P4_Connection_CommandException $e) {

            // if protections table is empty, everyone is super.
            $errors = $e->getResult()->getErrors();
            if (stristr($errors[0], "empty")) {
                return true;
            } else if (stristr($errors[0], "password must be set")) {
                return false;
            }

            throw $e;
        }

        if ($result->getData(0, "permMax") == "super") {
            return true;
        } else {
            return false;
        }
    }

   /**
     * Check if the server we are connected to is case sensitive.
     *
     * @return  bool            true if the server is case sensitive, false otherwise.
     * @throws  P4_Exception    if unable to determine server case handling.
     */
    public function isCaseSensitive()
    {
        $info = $this->getInfo();

        // throw exception if case handling unknown.
        if (!isset($info['caseHandling'])) {
            throw new P4_Exception("Cannot determine server case-handling.");
        }

        return $info['caseHandling'] === 'sensitive';
    }

    /**
     * Check if the server we are connected to is using external authentication
     *
     * @return  bool    true if the server is using external authentication, false otherwise.
     */
    public function hasExternalAuth()
    {
        $info = $this->getInfo();

        if (isset($info['externalAuth']) && ($info['externalAuth'] === 'enabled')) {
            return true;
        }
        return false;
    }

    /**
     * Check if the server we are connected to has a auth-set trigger configured.
     *
     * @return  bool    true, if the server has configured an auth-set trigger,
     *                  false, otherwise.
     */
    public function hasAuthSetTrigger()
    {
        // exit early if the server is not using external authentication
        if (!$this->hasExternalAuth()) {
            return false;
        }

        try {
            // try to set the password, the server without an auth-set trigger
            // throws a P4_Connection_CommandException with the error message:
            //   "Command unavailable: external authentication 'auth-set' trigger not found."
            $this->run('passwd');
        } catch (P4_Connection_CommandException $e) {
            if (stristr($e->getMessage(), "'auth-set' trigger not found.")) {
                return false;
            }
        }

        return true;
    }

    /**
     * Connect to a Perforce Server.
     * Hands off to _connect() for the actual mechanics of connecting.
     *
     * @return  P4_Connection_Interface     provides fluent interface.
     * @throws  P4_Connection_ConnectException  if the connection fails.
     */
    public function connect()
    {
        if (!$this->isConnected()) {

            // refuse to connect if no port or no user set.
            if (!strlen($this->getPort()) || !strlen($this->getUser())) {
                throw new P4_Connection_ConnectException(
                    "Cannot connect. You must specify both a port and a user."
                );
            }

            $this->_connect();
        }

        return $this;
    }

    /**
     * Run disconnect callbacks.
     *
     * @return  P4_Connection_Interface     provides fluent interface.
     */
    public function disconnect()
    {
        return $this->runDisconnectCallbacks();
    }

    /**
     * Add a function to run when connection is closed.
     * Callbacks are removed after they are executed
     * unless persistent is set to true.
     *
     * @param   callable    $callback   the function to execute on disconnect
     *                                  (will be passed connection).
     * @param   bool        $persistent optional - defaults to false - set to true to
     *                                  run callback on repeated disconnects.
     * @return  P4_Connection_Interface     provides fluent interface.
     */
    public function addDisconnectCallback($callback, $persistent = false)
    {
        if (!is_callable($callback)) {
            throw new InvalidArgumentException(
                "Cannot add disconnect callback. Not callable"
            );
        }

        $this->_disconnectCallbacks[] = array(
            'callback'      => $callback,
            'persistent'    => $persistent
        );

        return $this;
    }

    /**
     * Clear disconnect callbacks.
     *
     * @return  P4_Connection_Interface     provides fluent interface.
     */
    public function clearDisconnectCallbacks()
    {
        $this->_disconnectCallbacks = array();
        return $this;
    }

    /**
     * Run disconnect callbacks.
     *
     * @return  P4_Connection_Interface     provides fluent interface.
     */
    public function runDisconnectCallbacks()
    {
        foreach ($this->_disconnectCallbacks as $key => $callback) {
            call_user_func($callback['callback'], $this);
            if (!$callback['persistent']) {
                unset($this->_disconnectCallbacks[$key]);
            }
        }

        return $this;
    }

    /**
     * Get the server's security level.
     *
     * @return  int     the security level of the server (e.g. 0, 1, 2, 3)
     */
    public function getSecurityLevel()
    {
        if (!P4_Counter::exists('security', $this)) {
            return 0;
        }

        return (int) P4_Counter::fetch('security', $this)->getValue();
    }

    /**
     * This function will throw the appropriate exception for the error(s) found
     * in the passed result object.
     *
     * @param P4_Result     $result     The result containing errors
     */
    public function _handleError($result)
    {
        $message = "Command failed: " . implode("\n", $result->getErrors());

        // create appropriate exception based on error condition
        if (preg_match("/must (sync\/ )?(be )?resolved?/", $message) ||
            preg_match("/Merges still pending/", $message)) {
            $e = new P4_Connection_ConflictException($message);
        } else {
            $e = new P4_Connection_CommandException($message);
        }

        $e->setConnection($this);
        $e->setResult($result);
        throw $e;
    }

    /**
     * Get the maximum allowable length of all command arguments.
     *
     * @return  int     the max length of combined arguments - zero for no limit
     */
    public function getArgMax()
    {
        return 0;
    }

    /**
     * Return arguments split into chunks (batches) where each batch contains as many
     * arguments as possible to not exceed ARG_MAX or OPTION_LIMIT.
     *
     * ARG_MAX is a character limit that affects command line programs (p4).
     * OPTION_LIMIT is a server-side limit on the number of flags (e.g. '-n').
     *
     * @param   array       $arguments  list of arguments to split into chunks.
     * @param   array|null  $prefixArgs arguments to begin all batches with.
     * @param   array|null  $suffixArgs arguments to end all batches with.
     * @param   int         $groupSize  keep arguments together in groups of this size
     *                                  for example, when clearing attributes you want to
     *                                  keep pairs of -n and attr-name together.
     * @return  array                   list of batches of arguments where every batch contains as many
     *                                  arguments as possible and arg-max is not exceeded.
     * @throws  P4_Exception            if a argument (or set of arguments) exceed arg-max.
     */
    public function batchArgs(array $arguments, array $prefixArgs = null, array $suffixArgs = null, $groupSize = 1)
    {
        $argMax  = $this->getArgMax();

        // determine size of leading and trailing arguments.
        $initialLength  = 0;
        $initialOptions = 0;
        $prefixArgs     = (array) $prefixArgs;
        $suffixArgs     = (array) $suffixArgs;
        foreach (array_merge($prefixArgs, $suffixArgs) as $argument) {
            // if we have an arg-max limit, determine length of common args.
            // compute length by adding length of escaped argument + 1 space
            if ($argMax) {
                $initialLength += strlen(static::escapeArg($argument)) + 1;
            }

            // if the first character is a dash ('-'), it's an option
            if (substr($argument, 0, 1) === '-') {
                $initialOptions++;
            }
        }

        $batches = array();
        while (!empty($arguments)) {
            // determine how many arguments we can move into this batch.
            $count   = 0;
            $length  = $initialLength;
            $options = $initialOptions;
            foreach ($arguments as $argument) {

                // if we have an arg-max limit, enforce it.
                // compute length by adding length of escaped argument + 1 space
                if ($argMax) {
                    $length += strlen(static::escapeArg($argument)) + 1;

                    // if we exceed arg-max, break
                    if ($length >= $argMax) {
                        break;
                    }
                }

                // if we exceed the option-limit, break
                if ($options > static::OPTION_LIMIT) {
                    break;
                }

                // if the first character is a dash ('-'), it's an option
                if (substr($argument, 0, 1) === '-') {
                    $options++;
                }

                $count++;
            }

            // adjust count down to largest divisible group size
            // and move that number of arguments into this batch.
            $count    -= $count % $groupSize;
            $batches[] = array_merge($prefixArgs, array_splice($arguments, 0, $count), $suffixArgs);

            // handle the case of a given argument group not fitting in a batch
            // this informs the caller of indivisble args and avoids infinite loops
            if (!empty($arguments) && $count < $groupSize) {
                throw new P4_Exception(
                    "Cannot batch arguments. Arguments exceed arg-max and/or option-limit."
                );
            }
        }

        return $batches;
    }

    /**
     * Escape a string for use as a command argument.
     * Escaping is a no-op for the abstract implementation,
     * but is needed by batchArgs.
     *
     * @param   string  $arg    the string to escape
     * @return  string          the escaped string
     */
    public static function escapeArg($arg)
    {
        return $arg;
    }

    /**
     * Actually issues a command. Called by run() to perform the dirty work.
     *
     * @param   string          $command    the command to run.
     * @param   array           $params     optional - arguments.
     * @param   array|string    $input      optional - input for the command - should be provided
     *                                      in array form when writing perforce spec records.
     * @param   boolean         $tagged     optional - true/false to enable/disable tagged output.
     *                                      defaults to true.
     * @return  P4_Result       the perforce result object.
     */
    abstract protected function _run($command, $params = array(), $input = null, $tagged = true);

    /**
     * Prepare input for passing to Perforce.
     *
     * @param   string|array    $input      the input to prepare for p4.
     * @param   string          $command    the command to prepare input for.
     * @return  string|array    the prepared input.
     */
    abstract protected function _prepareInput($input, $command);

    /**
     * Does real work of establishing connection. Called by connect().
     *
     * @throws  P4_Connection_ConnectException  if the connection fails.
     */
    abstract protected function _connect();
}
# Change User Description Committed
#1 16170 perforce_software Move Chronicle files to follow new path scheme for branching.
//guest/perforce_software/chronicle/library/P4/Connection/Abstract.php
#1 8972 Matt Attaway Initial add of the Chronicle source code