Advertisement
p4geoff

<swarm-835154-root>/library/P4/Connection/AbstractConnection.php

Jun 26th, 2014
211
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 41.81 KB | None | 0 0
  1. <?php
  2. /**
  3.  * Abstract class for Perforce Connection implementations.
  4.  *
  5.  * @copyright   2011 Perforce Software. All rights reserved.
  6.  * @license     Please see LICENSE.txt in top-level folder of this distribution.
  7.  * @version     <release>/<patch>
  8.  * @todo        verify need to call disconnect() on setClient/Password/etc.
  9.  */
  10.  
  11. namespace P4\Connection;
  12.  
  13. use P4;
  14. use P4\Connection\Exception\ServiceNotFoundException;
  15. use P4\Counter\Counter;
  16. use P4\Environment\Environment;
  17. use P4\Log\Logger;
  18. use P4\Time\Time;
  19. use P4\Validate;
  20.  
  21. abstract class AbstractConnection implements ConnectionInterface
  22. {
  23.     const LOG_MAX_STRING_LENGTH     = 1024;
  24.     const DEFAULT_CHARSET           = 'utf8unchecked';
  25.  
  26.     protected $client;
  27.     protected $info;
  28.     protected $password;
  29.     protected $port;
  30.     protected $ticket;
  31.     protected $user;
  32.     protected $charset;
  33.     protected $host;
  34.     protected $appName;
  35.     protected $progName;
  36.     protected $progVersion;
  37.     protected $services;
  38.     protected $disconnectCallbacks  = array();
  39.     protected $slowCommandLogging   = array();
  40.  
  41.     /**
  42.      * Create an Interface instance.
  43.      *
  44.      * @param   string  $port        optional - the port to connect to.
  45.      * @param   string  $user        optional - the user to connect as.
  46.      * @param   string  $client      optional - the client spec to use.
  47.      * @param   string  $password    optional - the password to use.
  48.      * @param   string  $ticket      optional - a ticket to use.
  49.      */
  50.     public function __construct(
  51.         $port = null,
  52.         $user = null,
  53.         $client = null,
  54.         $password = null,
  55.         $ticket = null
  56.     ) {
  57.         $this->setPort($port);
  58.         $this->setUser($user);
  59.         $this->setClient($client);
  60.         $this->setPassword($password);
  61.         $this->setTicket($ticket);
  62.  
  63.         // ensure we disconnect on shutdown.
  64.         Environment::addShutdownCallback(
  65.             array($this, 'disconnect')
  66.         );
  67.     }
  68.  
  69.     /**
  70.      * Return the p4 port.
  71.      *
  72.      * @return  string  the port.
  73.      */
  74.     public function getPort()
  75.     {
  76.         return $this->port;
  77.     }
  78.  
  79.     /**
  80.      * Set the p4 port.
  81.      * Forces a disconnect if already connected.
  82.      *
  83.      * @param   string  $port           the port to connect to.
  84.      * @return  ConnectionInterface     provides fluent interface.
  85.      * @todo    validate port using port validator - make validator work with 'rsh:' ports.
  86.      */
  87.     public function setPort($port)
  88.     {
  89.         $this->port = (string) $port;
  90.  
  91.         // disconnect on port change.
  92.         $this->disconnect();
  93.  
  94.         return $this;
  95.     }
  96.  
  97.     /**
  98.      * Return the name of the p4 user.
  99.      *
  100.      * @return  string  the user.
  101.      */
  102.     public function getUser()
  103.     {
  104.         return $this->user;
  105.     }
  106.  
  107.     /**
  108.      * Set the name of the p4 user.
  109.      * Forces a disconnect if already connected.
  110.      *
  111.      * @param   string  $user           the user to connect as.
  112.      * @return  ConnectionInterface     provides fluent interface.
  113.      * @throws  P4\Exception            if the user is not valid.
  114.      */
  115.     public function setUser($user)
  116.     {
  117.         $validator = new Validate\UserName;
  118.  
  119.         if ($user !== null && !$validator->isValid($user)) {
  120.             throw new P4\Exception("Username: " . implode("\n", $validator->getMessages()));
  121.         }
  122.  
  123.         $this->user = $user;
  124.  
  125.         // disconnect on user change.
  126.         $this->disconnect();
  127.  
  128.         return $this;
  129.     }
  130.  
  131.     /**
  132.      * Return the p4 user's client.
  133.      *
  134.      * @return  string  the client.
  135.      */
  136.     public function getClient()
  137.     {
  138.         return $this->client;
  139.     }
  140.  
  141.     /**
  142.      * Set the p4 user's client.
  143.      * Forces a disconnect if already connected.
  144.      *
  145.      * @param   string  $client         the name of the client workspace to use.
  146.      * @return  ConnectionInterface     provides fluent interface.
  147.      * @throws  P4\Exception            if the client is not valid.
  148.      */
  149.     public function setClient($client)
  150.     {
  151.         $validator = new Validate\SpecName;
  152.  
  153.         if ($client !== null && !$validator->isValid($client)) {
  154.             throw new P4\Exception("Client name: " . implode("\n", $validator->getMessages()));
  155.         }
  156.  
  157.         $this->client = $client;
  158.  
  159.         // clear cached p4 info
  160.         $this->info = null;
  161.  
  162.         return $this;
  163.     }
  164.  
  165.     /**
  166.      * Retrieves the password set for this perforce connection.
  167.      *
  168.      * @return  string  password used to authenticate against perforce server.
  169.      */
  170.     public function getPassword()
  171.     {
  172.         return $this->password;
  173.     }
  174.  
  175.     /**
  176.      * Sets the password to use for this perforce connection.
  177.      *
  178.      * @param   string  $password       the password to use as authentication.
  179.      * @return  ConnectionInterface     provides fluent interface.
  180.      */
  181.     public function setPassword($password)
  182.     {
  183.         $this->password = $password;
  184.  
  185.         return $this;
  186.     }
  187.  
  188.     /**
  189.      * Retrieves the ticket set for this perforce connection.
  190.      *
  191.      * @return  string  ticket as generated by perforce server.
  192.      */
  193.     public function getTicket()
  194.     {
  195.         return $this->ticket;
  196.     }
  197.  
  198.     /**
  199.      * Sets the ticket to use for this perforce connection.
  200.      * Forces a disconnect if already connected.
  201.      *
  202.      * @param   string  $ticket         the ticket to use as authentication.
  203.      * @return  ConnectionInterface     provides fluent interface.
  204.      */
  205.     public function setTicket($ticket)
  206.     {
  207.         $this->ticket = $ticket;
  208.  
  209.         // disconnect on ticket change.
  210.         $this->disconnect();
  211.  
  212.         return $this;
  213.     }
  214.  
  215.     /**
  216.      * Retrieves the character set used by this connection.
  217.      *
  218.      * @return  string  charset used for this connection.
  219.      */
  220.     public function getCharset()
  221.     {
  222.         return $this->charset;
  223.     }
  224.  
  225.     /**
  226.      * Sets the character set to use for this perforce connection.
  227.      *
  228.      * You should only set a character set when connecting to a
  229.      * 'unicode enabled' server, or when setting the special value
  230.      * of 'none'.
  231.      *
  232.      * @param   string  $charset        the charset to use (e.g. 'utf8').
  233.      * @return  ConnectionInterface     provides fluent interface.
  234.      */
  235.     public function setCharset($charset)
  236.     {
  237.         $this->charset = $charset;
  238.  
  239.         return $this;
  240.     }
  241.  
  242.     /**
  243.      * Retrieves the client host set for this connection.
  244.      *
  245.      * @return  string  host name used for this connection.
  246.      */
  247.     public function getHost()
  248.     {
  249.         return $this->host;
  250.     }
  251.  
  252.     /**
  253.      * Sets the client host name overriding the environment.
  254.      *
  255.      * @param   string|null $host       the host name to use.
  256.      * @return  ConnectionInterface     provides fluent interface.
  257.      */
  258.     public function setHost($host)
  259.     {
  260.         $this->host = $host;
  261.  
  262.         return $this;
  263.     }
  264.  
  265.     /**
  266.      * Set the name of the application that is using this connection.
  267.      *
  268.      * The application name will be reported to the server and might
  269.      * be necessary to satisfy certain licensing restrictions.
  270.      *
  271.      * @param   string|null     $name   the app name to report to the server.
  272.      * @return  ConnectionInterface     provides fluent interface.
  273.      */
  274.     public function setAppName($name)
  275.     {
  276.         $this->appName = is_null($name) ? $name : (string) $name;
  277.  
  278.         return $this;
  279.     }
  280.  
  281.     /**
  282.      * Get the application name being reported to the server.
  283.      *
  284.      * @return  string|null     the app name reported to the server.
  285.      */
  286.     public function getAppName()
  287.     {
  288.         return $this->appName;
  289.     }
  290.  
  291.     /**
  292.      * Set the name of the program that is using this connection.
  293.      *
  294.      * The program name will be reported in the server logs
  295.      *
  296.      * @param   string|null     $name   the program name to report to the server.
  297.      * @return  ConnectionInterface     provides fluent interface.
  298.      */
  299.     public function setProgName($name)
  300.     {
  301.         $this->progName = is_null($name) ? $name : (string) $name;
  302.  
  303.         return $this;
  304.     }
  305.  
  306.     /**
  307.      * Get the program name being reported to the server.
  308.      *
  309.      * @return  string|null     the program name reported to the server.
  310.      */
  311.     public function getProgName()
  312.     {
  313.         return $this->progName;
  314.     }
  315.  
  316.     /**
  317.      * Set the program version of the program that is using this connection.
  318.      *
  319.      * The program version will be reported in the server logs
  320.      *
  321.      * @param   string|null     $version the program version to report to the server.
  322.      * @return  ConnectionInterface      provides fluent interface.
  323.      */
  324.     public function setProgVersion($version)
  325.     {
  326.         $this->progVersion = is_null($version) ? $version : (string) $version;
  327.  
  328.         return $this;
  329.     }
  330.  
  331.     /**
  332.      * Get the program version being reported to the server.
  333.      *
  334.      * @return  string|null     the program version reported to the server.
  335.      */
  336.     public function getProgVersion()
  337.     {
  338.         return $this->progVersion;
  339.     }
  340.  
  341.     /**
  342.      * Get the current client's root directory with no trailing slash.
  343.      *
  344.      * @return  string  the full path to the current client's root.
  345.      */
  346.     public function getClientRoot()
  347.     {
  348.         $info = $this->getInfo();
  349.         if (isset($info['clientRoot'])) {
  350.             return rtrim($info['clientRoot'], '/\\');
  351.         }
  352.         return false;
  353.     }
  354.  
  355.     /**
  356.      * Return an array of connection information.
  357.      * Due to caching, server date may be stale.
  358.      *
  359.      * @return  array   the connection information ('p4 info').
  360.      */
  361.     public function getInfo()
  362.     {
  363.         // if info cache is populated and connection is up, return cached info.
  364.         if (isset($this->info) && $this->isConnected()) {
  365.             return $this->info;
  366.         }
  367.  
  368.         // run p4 info.
  369.         $result      = $this->run("info");
  370.         $this->info = array();
  371.  
  372.         // gather all data (multiple arrays returned when connecting through broker).
  373.         foreach ($result->getData() as $data) {
  374.             $this->info += $data;
  375.         }
  376.  
  377.         return $this->info;
  378.     }
  379.  
  380.     /**
  381.      * Clear the info cache. This method is primarily used during testing,
  382.      * and would not normally be used.
  383.      *
  384.      * @return  ConnectionInterface     provides fluent interface.
  385.      */
  386.     public function clearInfo()
  387.     {
  388.         $this->info = null;
  389.  
  390.         return $this;
  391.     }
  392.  
  393.     /**
  394.      * Get the server identity of this connection.
  395.      * Resulting array will contain:
  396.      *  - name
  397.      *  - platform
  398.      *  - version
  399.      *  - build
  400.      *  - apiversion (same value as version, included for consistency)
  401.      *  - apibuild   (same value as build, included for consistency)
  402.      *  - date
  403.      *  - original   (all text for server version from 'info' response)
  404.      *
  405.      * @return  array           an array of server information for this connection
  406.      * @throws  P4\Exception    if the returned server version string is invalid
  407.      */
  408.     public function getServerIdentity()
  409.     {
  410.         $info  = $this->getInfo();
  411.         $parts = isset($info['serverVersion'])
  412.             ? preg_split('/\/| \(|\)/', $info['serverVersion'])
  413.             : null;
  414.         if (count($parts) < 6) {
  415.             $message = 'p4 info returned an invalid server version string';
  416.             throw new P4\Exception($message);
  417.         }
  418.  
  419.         // build server identity array of version components, including original string
  420.         return array(
  421.             'name'       => $parts[0],
  422.             'platform'   => $parts[1],
  423.             'version'    => $parts[2],
  424.             'build'      => $parts[3],
  425.             'apiversion' => $parts[2],
  426.             'apibuild'   => $parts[3],
  427.             'date'       => $parts[4] . '/' . $parts[5] . '/' . $parts[6],
  428.             'original'   => $info['serverVersion']
  429.         );
  430.     }
  431.  
  432.     /**
  433.      * Return perforce server version in the form of '<year>.<release>'.
  434.      *
  435.      * @return  string          server version as '<year>.<release>'
  436.      * @throws  P4\Exception    if server version cannot be determined
  437.      */
  438.     public function getServerVersion()
  439.     {
  440.         $identity = $this->getServerIdentity();
  441.         $version  = $identity['version'];
  442.  
  443.         // keep only '<year>.<release>' of the version
  444.         $parts = explode('.', $version);
  445.         if (count($parts) < 2) {
  446.             throw new P4\Exception(
  447.                 'Cannot get version from server identity: unknown version format.'
  448.             );
  449.         }
  450.         $version = implode('.', array_slice($parts, 0, 2));
  451.  
  452.         return $version;
  453.     }
  454.  
  455.     /**
  456.      * Check if the server version for this connection is same or higher than
  457.      * the version passed in the parameter.
  458.      *
  459.      * @param   string  $version    version to compare in format <year>.<release>
  460.      * @return  bool    true if server version is same or higher than $version
  461.      */
  462.     public function isServerMinVersion($version)
  463.     {
  464.         return version_compare($this->getServerVersion(), $version) >= 0;
  465.     }
  466.  
  467.     /**
  468.      * Check if the P4API version for this connection is same or higher than
  469.      * the version passed in the parameter.
  470.      *
  471.      * @param   string  $version    version to compare in format <year>.<release>
  472.      * @return  bool    true if P4API version is same or higher than $version
  473.      * @throws  P4\Exception        if the apiVersion string is invalid
  474.      */
  475.     public function isApiMinVersion($version)
  476.     {
  477.         $identity   = $this->getConnectionIdentity();
  478.         $apiVersion = isset($identity['apiversion']) ? $identity['apiversion'] : '';
  479.  
  480.         // keep only '<year>.<release>' of the apiVersion
  481.         $parts = explode('.', $apiVersion);
  482.         if (count($parts) < 2) {
  483.             throw new P4\Exception(
  484.                 'Cannot get version from connection identity: unknown version format.'
  485.             );
  486.         }
  487.         $apiVersion = implode('.', array_slice($parts, 0, 2));
  488.  
  489.         return version_compare($apiVersion, $version) >= 0;
  490.     }
  491.  
  492.     /**
  493.      * Return option limit (server-side limit on the number of flags)
  494.      * based on the server version.
  495.      *
  496.      * @return  int     option limit
  497.      */
  498.     public function getOptionLimit()
  499.     {
  500.         $limit = 20;
  501.         if ($this->isServerMinVersion('2012.1')) {
  502.             $limit = 256;
  503.         }
  504.  
  505.         return $limit;
  506.     }
  507.  
  508.     /**
  509.      * Authenticate the user with 'p4 login'.
  510.      *
  511.      * @return  string|null     the ticket issued by the server or null if
  512.      *                          no ticket issued (ie. user has no password).
  513.      * @throws  Exception\LoginException    if login fails.
  514.      */
  515.     public function login()
  516.     {
  517.         // ensure user name is set.
  518.         if (!strlen($this->getUser())) {
  519.             throw new Exception\LoginException(
  520.                 "Login failed. Username is empty.",
  521.                 Exception\LoginException::IDENTITY_AMBIGUOUS
  522.             );
  523.         }
  524.  
  525.         // try to login.
  526.         try {
  527.             $result = $this->run('login', '-p', $this->password ?: '');
  528.         } catch (Exception\CommandException $e) {
  529.  
  530.             // user doesn't exist.
  531.             if (stristr($e->getMessage(), "doesn't exist") ||
  532.                 stristr($e->getMessage(), "has not been enabled by 'p4 protect'")
  533.             ) {
  534.                 throw new Exception\LoginException(
  535.                     "Login failed. " . $e->getMessage(),
  536.                     Exception\LoginException::IDENTITY_NOT_FOUND
  537.                 );
  538.             }
  539.  
  540.             // invalid password.
  541.             if (stristr($e->getMessage(), "password invalid")) {
  542.                 throw new Exception\LoginException(
  543.                     "Login failed. " . $e->getMessage(),
  544.                     Exception\LoginException::CREDENTIAL_INVALID
  545.                 );
  546.             }
  547.  
  548.             // generic login exception.
  549.             throw new Exception\LoginException(
  550.                 "Login failed. " . $e->getMessage()
  551.             );
  552.         }
  553.  
  554.         // we can get several output blocks
  555.         // we want the first block that looks like a ticket
  556.         // if user has no password, the last block will be a message
  557.         // if using external auth, early blocks could be trigger output
  558.         // if talking to a replica, the last block will be a ticket for the master
  559.         $response = end($result->getData());
  560.         foreach ($result->getData() as $data) {
  561.             if (preg_match('/^[A-F0-9]{32}$/', $data)) {
  562.                 $response = $data;
  563.                 break;
  564.             }
  565.         }
  566.  
  567.         // check if no password set for this user.
  568.         // fail if a password was provided - succeed otherwise.
  569.         if (stristr($response, "no password set for this user")) {
  570.             if ($this->password) {
  571.                 throw new Exception\LoginException(
  572.                     "Login failed. " . $response,
  573.                     Exception\LoginException::CREDENTIAL_INVALID
  574.                 );
  575.             } else {
  576.                 return null;
  577.             }
  578.         }
  579.  
  580.         // capture ticket from output.
  581.         $this->ticket = $response;
  582.  
  583.         // if ticket wasn't captured correctly, fail with unknown code.
  584.         if (!$this->ticket) {
  585.             throw new Exception\LoginException(
  586.                 "Login failed. Unable to capture login ticket."
  587.             );
  588.         }
  589.  
  590.         return $this->ticket;
  591.     }
  592.  
  593.     /**
  594.      * Executes the specified command and returns a perforce result object.
  595.      * No need to call connect() first. Run will connect automatically.
  596.      *
  597.      * Performs common pre/post-run work. Hands off to doRun() for the
  598.      * actual mechanics of running commands.
  599.      *
  600.      * @param   string          $command        the command to run.
  601.      * @param   array|string    $params         optional - one or more arguments.
  602.      * @param   array|string    $input          optional - input for the command - should be provided
  603.      *                                          in array form when writing perforce spec records.
  604.      * @param   boolean         $tagged         optional - true/false to enable/disable tagged output.
  605.      *                                          defaults to true.
  606.      * @param   boolean         $ignoreErrors   optional - true/false to ignore errors - default false
  607.      * @return  P4\Connection\CommandResult     the perforce result object.
  608.      */
  609.     public function run(
  610.         $command,
  611.         $params = array(),
  612.         $input = null,
  613.         $tagged = true,
  614.         $ignoreErrors = false
  615.     ) {
  616.         // establish connection to perforce server.
  617.         if (!$this->isConnected()) {
  618.             $this->connect();
  619.         }
  620.  
  621.         // ensure params is an array.
  622.         if (!is_array($params)) {
  623.             if (!empty($params)) {
  624.                 $params = array($params);
  625.             } else {
  626.                 $params = array();
  627.             }
  628.         }
  629.  
  630.         // log the start of the command w. params.
  631.         $message = "P4 (" . spl_object_hash($this) . ") start command: "
  632.                  . $command . " " . implode(" ", $params);
  633.         Logger::log(
  634.             Logger::DEBUG,
  635.             substr($message, 0, static::LOG_MAX_STRING_LENGTH)
  636.         );
  637.  
  638.         // prepare input for passing to perforce.
  639.         $input = $this->prepareInput($input, $command);
  640.  
  641.         // defer to sub-classes to actually issue the command.
  642.         $start  = microtime(true);
  643.         $result = $this->doRun($command, $params, $input, $tagged);
  644.         $lapse  = microtime(true) - $start;
  645.  
  646.         // if the command was slow, log a warning
  647.         // we determine the threshold for slow based on the command being run
  648.         $slow = 0;
  649.         foreach ($this->getSlowCommandLogging() as $key => $value) {
  650.             if (ctype_digit((string) $value)) {
  651.                 $slow = max($slow, $value);
  652.             } elseif (in_array($command, (array) $value)) {
  653.                 $slow = max($slow, (int) $key);
  654.             }
  655.         }
  656.         if ($slow && $lapse >= $slow) {
  657.             $message = "P4 (" . spl_object_hash($this) . ") slow command (" . round($lapse, 3) . "s): "
  658.                      . $command . " " . implode(" ", $params);
  659.             Logger::log(
  660.                 Logger::WARN,
  661.                 substr($message, 0, static::LOG_MAX_STRING_LENGTH)
  662.             );
  663.         }
  664.  
  665.         // log errors - log them and throw an exception.
  666.         if ($result->hasErrors() && !$ignoreErrors) {
  667.             $message = "P4 (" . spl_object_hash($this) . ") command failed: "
  668.                        . implode("\n", $result->getErrors());
  669.             Logger::log(
  670.                 Logger::DEBUG,
  671.                 substr($message, 0, static::LOG_MAX_STRING_LENGTH)
  672.             );
  673.  
  674.             // if we have no charset, and the command failed because we are
  675.             // talking to a unicode server, automatically use the default
  676.             // charset and run the command again.
  677.             $errors = $result->getErrors();
  678.             $needle = 'Unicode server permits only unicode enabled clients.';
  679.             if (!$this->getCharset() && stripos($errors[0], $needle) !== false) {
  680.                 Logger::log(
  681.                     Logger::DEBUG,
  682.                     "P4 (" . spl_object_hash($this) . ") automatically enabling unicode and re-trying."
  683.                 );
  684.  
  685.                 $this->setCharset(static::DEFAULT_CHARSET);
  686.  
  687.                 // run the command again now that we have a charset.
  688.                 return call_user_func_array(
  689.                     array($this, 'run'),
  690.                     func_get_args()
  691.                 );
  692.             }
  693.  
  694.             // if connect failed due to an untrusted server, trust it and retry
  695.             $needle = "To allow connection use the 'p4 trust' command";
  696.             if (stripos($errors[0], $needle) !== false && !$this->hasTrusted) {
  697.                 Logger::log(
  698.                     Logger::DEBUG,
  699.                     "P4 (" . spl_object_hash($this) . ") automatically trusting and re-trying."
  700.                 );
  701.  
  702.                 // add a property to avoid re-recursing on this test
  703.                 $this->hasTrusted = true;
  704.  
  705.                 // work around @job066722 by disconnecting to clear the argument buffer
  706.                 // -fixed in P4API 2013.2
  707.                 if (!$this->isApiMinVersion('2013.2')) {
  708.                     $this->disconnect();
  709.                 }
  710.  
  711.                 // trust the connection as this is the first time we have seen it
  712.                 $this->run('trust', '-y');
  713.  
  714.                 // run the command again now that we have trusted it
  715.                 return call_user_func_array(
  716.                     array($this, 'run'),
  717.                     func_get_args()
  718.                 );
  719.             }
  720.  
  721.             $this->handleError($result);
  722.         }
  723.  
  724.         return $result;
  725.     }
  726.  
  727.     /**
  728.      * Runs the specified command using the passed output handler.
  729.      * Ensures the output handler is turned back off at completion.
  730.      *
  731.      * If the handler has a 'reset' method it will be called. This is intended
  732.      * to give the handler an opportunity to prepare itself for a fresh run.
  733.      *
  734.      * @param   P4_OutputHandlerAbstract    $handler        the output handler to use
  735.      * @param   string                      $command        the command to run.
  736.      * @param   array|string                $params         optional - one or more arguments.
  737.      * @param   array|string                $input          optional - input for the command - should be provided
  738.      *                                                      in array form when writing perforce spec records.
  739.      * @param   boolean                     $tagged         optional - true/false to enable/disable tagged output.
  740.      *                                                      defaults to true.
  741.      * @param   boolean                     $ignoreErrors   optional - true/false to ignore errors - default false
  742.      * @throws  \P4\Exception                               if the implementation doesn't define a runHandler
  743.      * @return  \P4\Connection\CommandResult                the perforce result object.
  744.      */
  745.     public function runHandler(
  746.         $handler,
  747.         $command,
  748.         $params = array(),
  749.         $input = null,
  750.         $tagged = true,
  751.         $ignoreErrors = false
  752.     ) {
  753.         throw new P4\Exception('Implementing class must define a runHandler implementation!');
  754.     }
  755.  
  756.     /**
  757.      * Check if the user we are connected as has super user privileges.
  758.      *
  759.      * @return  bool    true if the user has super, false otherwise.
  760.      * @throws  Exception\CommandException  if unanticipated error from protects -m.
  761.      */
  762.     public function isSuperUser()
  763.     {
  764.         return $this->getMaxAccess() === 'super';
  765.     }
  766.  
  767.     /**
  768.      * Check if the user we are connected as has admin user privileges.
  769.      * By default, 'super' connection will return false on this check.
  770.      * This behaviour can be modified by optional $allowSuper flag
  771.      * to also include 'super' users.
  772.      *
  773.      * @param   bool    $allowSuper     optional - if true, then this check will
  774.      *                                  return true also if the connection is super
  775.      * @return  bool    true if the user is admin (or super if $allowSuper is true),
  776.      *                  false otherwise.
  777.      * @throws  Exception\CommandException  if unanticipated error from protects -m.
  778.      */
  779.     public function isAdminUser($allowSuper = false)
  780.     {
  781.         $maxAccess = $this->getMaxAccess();
  782.         return ($allowSuper && $maxAccess === 'super') || $maxAccess === 'admin';
  783.     }
  784.  
  785.     /**
  786.      * Check if the server we are connected to is case sensitive.
  787.      *
  788.      * @return  bool            true if the server is case sensitive, false otherwise.
  789.      * @throws  P4\Exception    if unable to determine server case handling.
  790.      */
  791.     public function isCaseSensitive()
  792.     {
  793.         $info = $this->getInfo();
  794.  
  795.         // throw exception if case handling unknown.
  796.         if (!isset($info['caseHandling'])) {
  797.             throw new P4\Exception("Cannot determine server case-handling.");
  798.         }
  799.  
  800.         return $info['caseHandling'] === 'sensitive';
  801.     }
  802.  
  803.     /**
  804.      * Check if the server we are connected to is using external authentication
  805.      *
  806.      * @return  bool    true if the server is using external authentication, false otherwise.
  807.      */
  808.     public function hasExternalAuth()
  809.     {
  810.         $info = $this->getInfo();
  811.  
  812.         if (isset($info['externalAuth']) && ($info['externalAuth'] === 'enabled')) {
  813.             return true;
  814.         }
  815.         return false;
  816.     }
  817.  
  818.     /**
  819.      * Check if the server we are connected to has a auth-set trigger configured.
  820.      *
  821.      * @return  bool    true, if the server has configured an auth-set trigger,
  822.      *                  false, otherwise.
  823.      */
  824.     public function hasAuthSetTrigger()
  825.     {
  826.         // exit early if the server is not using external authentication
  827.         if (!$this->hasExternalAuth()) {
  828.             return false;
  829.         }
  830.  
  831.         try {
  832.             // try to set the password, the server without an auth-set trigger
  833.             // throws a Exception\CommandException with the error message:
  834.             //   "Command unavailable: external authentication 'auth-set' trigger not found."
  835.             $this->run('passwd');
  836.         } catch (Exception\CommandException $e) {
  837.             if (stristr($e->getMessage(), "'auth-set' trigger not found.")) {
  838.                 return false;
  839.             }
  840.         }
  841.  
  842.         return true;
  843.     }
  844.  
  845.     /**
  846.      * Connect to a Perforce Server.
  847.      * Hands off to doConnect() for the actual mechanics of connecting.
  848.      *
  849.      * @return  ConnectionInterface         provides fluent interface.
  850.      * @throws  Exception\ConnectException  if the connection fails.
  851.      */
  852.     public function connect()
  853.     {
  854.         if (!$this->isConnected()) {
  855.  
  856.             // refuse to connect if no port or no user set.
  857.             if (!strlen($this->getPort()) || !strlen($this->getUser())) {
  858.                 throw new Exception\ConnectException(
  859.                     "Cannot connect. You must specify both a port and a user."
  860.                 );
  861.             }
  862.  
  863.             $this->doConnect();
  864.         }
  865.  
  866.         return $this;
  867.     }
  868.  
  869.     /**
  870.      * Run disconnect callbacks.
  871.      *
  872.      * @return  ConnectionInterface     provides fluent interface.
  873.      */
  874.     public function disconnect()
  875.     {
  876.         return $this->runDisconnectCallbacks();
  877.     }
  878.  
  879.     /**
  880.      * Add a function to run when connection is closed.
  881.      * Callbacks are removed after they are executed
  882.      * unless persistent is set to true.
  883.      *
  884.      * @param   callable    $callback   the function to execute on disconnect
  885.      *                                  (will be passed connection).
  886.      * @param   bool        $persistent optional - defaults to false - set to true to
  887.      *                                  run callback on repeated disconnects.
  888.      * @return  ConnectionInterface     provides fluent interface.
  889.      * @throws  \InvalidArgumentException  if callback supplied is not callable
  890.      */
  891.     public function addDisconnectCallback($callback, $persistent = false)
  892.     {
  893.         if (!is_callable($callback)) {
  894.             throw new \InvalidArgumentException(
  895.                 "Cannot add disconnect callback. Not callable"
  896.             );
  897.         }
  898.  
  899.         $this->disconnectCallbacks[] = array(
  900.             'callback'      => $callback,
  901.             'persistent'    => $persistent
  902.         );
  903.  
  904.         return $this;
  905.     }
  906.  
  907.     /**
  908.      * Clear disconnect callbacks.
  909.      *
  910.      * @return  ConnectionInterface     provides fluent interface.
  911.      */
  912.     public function clearDisconnectCallbacks()
  913.     {
  914.         $this->disconnectCallbacks = array();
  915.         return $this;
  916.     }
  917.  
  918.     /**
  919.      * Run disconnect callbacks.
  920.      *
  921.      * @return  ConnectionInterface     provides fluent interface.
  922.      */
  923.     public function runDisconnectCallbacks()
  924.     {
  925.         foreach ($this->disconnectCallbacks as $key => $callback) {
  926.             call_user_func($callback['callback'], $this);
  927.             if (!$callback['persistent']) {
  928.                 unset($this->disconnectCallbacks[$key]);
  929.             }
  930.         }
  931.  
  932.         return $this;
  933.     }
  934.  
  935.     /**
  936.      * Get the server's security level.
  937.      *
  938.      * @return  int     the security level of the server (e.g. 0, 1, 2, 3)
  939.      */
  940.     public function getSecurityLevel()
  941.     {
  942.         if (!Counter::exists('security', $this)) {
  943.             return 0;
  944.         }
  945.  
  946.         return (int) Counter::fetch('security', $this)->get();
  947.     }
  948.  
  949.     /**
  950.      * This function will throw the appropriate exception for the error(s) found
  951.      * in the passed result object.
  952.      *
  953.      * @param   P4\Connection\CommandResult     $result     The result containing errors
  954.      * @throws  Exception\ConflictException     if there are file conflicts to resolve.
  955.      * @throws  Exception\CommandException      if there are any other command errors.
  956.      */
  957.     public function handleError($result)
  958.     {
  959.         $message = "Command failed: " . implode("\n", $result->getErrors());
  960.  
  961.         // create appropriate exception based on error condition
  962.         if (preg_match("/must (sync\/ )?(be )?resolved?/", $message) ||
  963.             preg_match("/Merges still pending/", $message)) {
  964.             $e = new Exception\ConflictException($message);
  965.         } else {
  966.             $e = new Exception\CommandException($message);
  967.         }
  968.  
  969.         $e->setConnection($this);
  970.         $e->setResult($result);
  971.         throw $e;
  972.     }
  973.  
  974.     /**
  975.      * Get the maximum allowable length of all command arguments.
  976.      *
  977.      * @return  int     the max length of combined arguments - zero for no limit
  978.      */
  979.     public function getArgMax()
  980.     {
  981.         return 0;
  982.     }
  983.  
  984.     /**
  985.      * Return arguments split into chunks (batches) where each batch contains as many
  986.      * arguments as possible to not exceed ARG_MAX or option limit.
  987.      *
  988.      * ARG_MAX is a character limit that affects command line programs (p4).
  989.      * Option limit is a server-side limit on the number of flags (e.g. '-n').
  990.      *
  991.      * @param   array       $arguments  list of arguments to split into chunks.
  992.      * @param   array|null  $prefixArgs arguments to begin all batches with.
  993.      * @param   array|null  $suffixArgs arguments to end all batches with.
  994.      * @param   int         $groupSize  keep arguments together in groups of this size
  995.      *                                  for example, when clearing attributes you want to
  996.      *                                  keep pairs of -n and attr-name together.
  997.      * @return  array                   list of batches of arguments where every batch contains as many
  998.      *                                  arguments as possible and arg-max is not exceeded.
  999.      * @throws  P4\Exception            if a argument (or set of arguments) exceed arg-max.
  1000.      */
  1001.     public function batchArgs(array $arguments, array $prefixArgs = null, array $suffixArgs = null, $groupSize = 1)
  1002.     {
  1003.         // determine size of leading and trailing arguments.
  1004.         $initialLength  = 0;
  1005.         $initialOptions = 0;
  1006.         $argMax         = $this->getArgMax();
  1007.         $optionLimit    = $this->getOptionLimit();
  1008.         $prefixArgs     = (array) $prefixArgs;
  1009.         $suffixArgs     = (array) $suffixArgs;
  1010.         foreach (array_merge($prefixArgs, $suffixArgs) as $argument) {
  1011.             // if we have an arg-max limit, determine length of common args.
  1012.             // compute length by adding length of escaped argument + 1 space
  1013.             if ($argMax) {
  1014.                 $initialLength += strlen(static::escapeArg($argument)) + 1;
  1015.             }
  1016.  
  1017.             // if the first character is a dash ('-'), it's an option
  1018.             if (substr($argument, 0, 1) === '-') {
  1019.                 $initialOptions++;
  1020.             }
  1021.         }
  1022.  
  1023.         $batches = array();
  1024.         while (!empty($arguments)) {
  1025.             // determine how many arguments we can move into this batch.
  1026.             $count   = 0;
  1027.             $length  = $initialLength;
  1028.             $options = $initialOptions;
  1029.             foreach ($arguments as $argument) {
  1030.  
  1031.                 // if we have an arg-max limit, enforce it.
  1032.                 // compute length by adding length of escaped argument + 1 space
  1033.                 if ($argMax) {
  1034.                     $length += strlen(static::escapeArg($argument)) + 1;
  1035.  
  1036.                     // if we exceed arg-max, break
  1037.                     if ($length >= $argMax) {
  1038.                         break;
  1039.                     }
  1040.                 }
  1041.  
  1042.                 // if we exceed the option-limit, break
  1043.                 if ($options > $optionLimit) {
  1044.                     break;
  1045.                 }
  1046.  
  1047.                 // if the first character is a dash ('-'), it's an option
  1048.                 if (substr($argument, 0, 1) === '-') {
  1049.                     $options++;
  1050.                 }
  1051.  
  1052.                 $count++;
  1053.             }
  1054.  
  1055.             // adjust count down to largest divisible group size
  1056.             // and move that number of arguments into this batch.
  1057.             $count    -= $count % $groupSize;
  1058.             $batches[] = array_merge($prefixArgs, array_splice($arguments, 0, $count), $suffixArgs);
  1059.  
  1060.             // handle the case of a given argument group not fitting in a batch
  1061.             // this informs the caller of indivisble args and avoids infinite loops
  1062.             if (!empty($arguments) && $count < $groupSize) {
  1063.                 throw new P4\Exception(
  1064.                     "Cannot batch arguments. Arguments exceed arg-max and/or option-limit."
  1065.                 );
  1066.             }
  1067.         }
  1068.  
  1069.         return $batches;
  1070.     }
  1071.  
  1072.     /**
  1073.      * Escape a string for use as a command argument.
  1074.      * Escaping is a no-op for the abstract implementation,
  1075.      * but is needed by batchArgs.
  1076.      *
  1077.      * @param   string  $arg    the string to escape
  1078.      * @return  string          the escaped string
  1079.      */
  1080.     public static function escapeArg($arg)
  1081.     {
  1082.         return $arg;
  1083.     }
  1084.  
  1085.     /**
  1086.      * Get the server's timezone.
  1087.      *
  1088.      * @return DateTimeZone the server's timezone
  1089.      * @throws \Exception   if the server's timezone isn't parsable
  1090.      */
  1091.     public function getTimeZone()
  1092.     {
  1093.         // the 'serverDate' object lists the offset in the format -0800 and the short timezone name as the
  1094.         // last two components. strip these off and convert to a long name if possible.
  1095.         $info = $this->getInfo();
  1096.         if (isset($info['serverDate'])
  1097.             && preg_match('#^[0-9/]+ [0-9:]+ (?P<offset>[0-9-+]+) (?P<name>.+)$#', $info['serverDate'], $timezone)
  1098.         ) {
  1099.             // converting the string to a DateTimeZone is tricky; outsource the heavy lifting
  1100.             return Time::toDateTimeZone($timezone['name'], $timezone['offset']);
  1101.         }
  1102.  
  1103.         // if we couldn't preg match out the details; throw
  1104.         throw new \Exception('Unable to get timezone, p4 info does not contain a parsable serverDate');
  1105.     }
  1106.  
  1107.     /**
  1108.      * Attach a service to this connection.
  1109.      * Allows the connection to act as a service locator (e.g. for logging, caching, etc.)
  1110.      *
  1111.      * @param   string                  $name       the name of the service to set (e.g. 'cache')
  1112.      * @param   object|callable|null    $service    the service instance or factory function (or null to clear)
  1113.      *                                              factory is called with the connection and service name
  1114.      * @return  ConnectionInterface to maintain a fluent interface
  1115.      * @throws  \InvalidArgumentException   if the name or service is invalid
  1116.      */
  1117.     public function setService($name, $service)
  1118.     {
  1119.         if (!is_string($name) || !strlen($name)) {
  1120.             throw new \InvalidArgumentException("Cannot set service. Name must be a non-empty string.");
  1121.         }
  1122.  
  1123.         // if service is null, remove it
  1124.         if ($service === null) {
  1125.             unset($this->services[$name]);
  1126.             return $this;
  1127.         }
  1128.  
  1129.         if (!is_object($service) && !is_callable($service)) {
  1130.             throw new \InvalidArgumentException("Cannot set service. Service must be an object or callable.");
  1131.         }
  1132.  
  1133.         $this->services[$name] = array(
  1134.             'instance' => is_callable($service) ? null     : $service,
  1135.             'factory'  => is_callable($service) ? $service : null
  1136.         );
  1137.  
  1138.         return $this;
  1139.     }
  1140.  
  1141.     /**
  1142.      * Retrieve a service from this connection.
  1143.      *
  1144.      * @param   string  $name       the name of the service to get (e.g. 'cache')
  1145.      * @return  object  the service instance (factory functions are resolved automatically)
  1146.      * @throws  ServiceNotFoundException    if the requested service does not exist
  1147.      */
  1148.     public function getService($name)
  1149.     {
  1150.         if (!isset($this->services[$name])) {
  1151.             throw new ServiceNotFoundException("Cannot get service. No such service ('$name').");
  1152.         }
  1153.  
  1154.         // construct the service instance if necessary
  1155.         $service = $this->services[$name];
  1156.         if (!isset($service['instance'])) {
  1157.             $service['instance']   = $service['factory']($this, $name);
  1158.             $this->services[$name] = $service;
  1159.         }
  1160.  
  1161.         return $service['instance'];
  1162.     }
  1163.  
  1164.     /**
  1165.      * Set the threshold(s) for logging slow commands.
  1166.      * Pass false or an empty array to disable logging.
  1167.      *
  1168.      * You may specify a default limit (in seconds) as well as limits that
  1169.      * apply to only specific commands. The longest applicable limit is used
  1170.      * for a given command if more than one candidate occurs.
  1171.      *
  1172.      * The format is:
  1173.      * $limits => array(
  1174.      *    3,                                // numeric value is a default (any command) limit
  1175.      *    30 => array('print', 'submit')    // seconds as key with command(s) as value for command specific limit
  1176.      *    60 => 'unshelve'
  1177.      * );
  1178.      *
  1179.      * In the above example, the command fstat would have a limit of 3, print 30 and unshelve 60.
  1180.      *
  1181.      * @param   array|bool $thresholds  the limit(s) to trigger slow command logging or false
  1182.      * @return  ConnectionInterface     to maintain a fluent interface
  1183.      */
  1184.     public function setSlowCommandLogging($thresholds)
  1185.     {
  1186.         $this->slowCommandLogging = (array) $thresholds;
  1187.         return $this;
  1188.     }
  1189.  
  1190.     /**
  1191.      * Return the currently specified slow command thresholds.
  1192.      *
  1193.      * @return  array   the slow command thresholds, see setSlowCommandLimits for format details
  1194.      */
  1195.     public function getSlowCommandLogging()
  1196.     {
  1197.         return (array) $this->slowCommandLogging;
  1198.     }
  1199.  
  1200.     /**
  1201.      * Get maximum access level for this connection.
  1202.      *
  1203.      * @param   string|null     $host   optional - if set, max access level will be determined
  1204.      *                                  for the given host
  1205.      * @return  string|false    maximum access level or false
  1206.      */
  1207.     public function getMaxAccess($host = null)
  1208.     {
  1209.         // get max access level from Perforce
  1210.         $flags = array('-m');
  1211.         if ($host) {
  1212.             $flags[] = '-h';
  1213.             $flags[] = $host;
  1214.         }
  1215.  
  1216.         try {
  1217.             $result = $this->run("protects", $flags);
  1218.         } catch (Exception\CommandException $e) {
  1219.             // if protections table is empty, everyone is super
  1220.             $errors = $e->getResult()->getErrors();
  1221.             if (stristr($errors[0], "empty")) {
  1222.                 return 'super';
  1223.             } elseif (stristr($errors[0], "password must be set")) {
  1224.                 return false;
  1225.             }
  1226.  
  1227.             throw $e;
  1228.         }
  1229.  
  1230.         return $result->getData(0, "permMax");
  1231.     }
  1232.  
  1233.     /**
  1234.      * Actually issues a command. Called by run() to perform the dirty work.
  1235.      *
  1236.      * @param   string          $command    the command to run.
  1237.      * @param   array           $params     optional - arguments.
  1238.      * @param   array|string    $input      optional - input for the command - should be provided
  1239.      *                                      in array form when writing perforce spec records.
  1240.      * @param   boolean         $tagged     optional - true/false to enable/disable tagged output.
  1241.      *                                      defaults to true.
  1242.      * @return  P4\Connection\CommandResult     the perforce result object.
  1243.      */
  1244.     abstract protected function doRun($command, $params = array(), $input = null, $tagged = true);
  1245.  
  1246.     /**
  1247.      * Prepare input for passing to Perforce.
  1248.      *
  1249.      * @param   string|array    $input      the input to prepare for p4.
  1250.      * @param   string          $command    the command to prepare input for.
  1251.      * @return  string|array    the prepared input.
  1252.      */
  1253.     abstract protected function prepareInput($input, $command);
  1254.  
  1255.     /**
  1256.      * Does real work of establishing connection. Called by connect().
  1257.      *
  1258.      * @throws  Exception\ConnectException  if the connection fails.
  1259.      */
  1260.     abstract protected function doConnect();
  1261. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement