Advertisement
p4geoff

library/P4/Connection/AbstractConnection.php

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