Site.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • library/
  • P4Cms/
  • Site.php
  • View
  • Commits
  • Open Download .zip Download (36 KB)
<?php
/**
 * Sites are used to group website configurations and contents.
 * Site configuration and content are stored in Perforce.
 *
 * This class models a particular branch (a stream) of a site.
 * The identifier for a site branch corresponds directly with
 * the id of a stream in Perforce.
 *
 * Arguably this class should not be called 'P4Cms_Site' as it
 * does not actually model a site, but rather a specific branch
 * of a site (a site contains many branches). We chose to treat
 * this as a implementation detail because in most cases the
 * calling code that interfaces with the site object does not
 * care that it is actually a branch. It is simpler to think of
 * it as a site.
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */
class P4Cms_Site extends P4Cms_Model
{
    const               ACL_RECORD_ID           = 'config/acl';
    const               SITE_PREFIX             = 'chronicle-';
    const               DEFAULT_BRANCH          = 'live';
    const               CACHE_KEY               = 'sites';

    const               FETCH_BY_SITE           = 'site';
    const               FETCH_BY_ACL            = 'acl';
    const               FETCH_SORT_FLAT         = 'flat';

    protected           $_acl                   = null;
    protected           $_adapter               = null;
    protected           $_config                = null;
    protected           $_connection            = null;
    protected           $_templateConnection    = null;
    protected           $_stream                = null;
    protected           $_parent                = null;

    protected static    $_idField               = 'id';
    protected static    $_sitesPackagesPath     = null;
    protected static    $_sitesDataPath         = null;
    protected static    $_activeSite            = null;

    /**
     * We need a custom sleep to exclude the adapter and connections.
     * Connection objects cannot be serialized.
     *
     * @return  array   list of properties to serialize
     */
    public function __sleep()
    {
        return array_diff(
            array_keys(get_object_vars($this)),
            array('_connection', '_templateConnection', '_adapter')
        );
    }

    /**
     * We need a custom wakeup to provide any unserialized connected
     * objects with valid connections.
     *
     * We use deferred connections to avoid actually creating the
     * connections until they are needed and to ensure that the
     * connected objects always use the same connection as the site.
     *
     * Note: the site config object takes care of itself. It always
     * defers to its associated site's adapter (e.g. in save()).
     */
    public function __wakeup()
    {
        if ($this->_stream) {
            $this->_stream->setConnection($this->getDeferredConnection());
        }
        if ($this->_acl) {
            $this->_acl->getRecord()->setAdapter($this->getDeferredAdapter());
        }
    }

    /**
     * Get the id of the site that this branch belongs to.
     *
     * @return  string|null     the site id for this branch.
     */
    public function getSiteId()
    {
        $id = $this->getId();

        if (!$id) {
            return null;
        }

        // try to extract the site id from the branch id.
        if (preg_match("#^//([^/]+)#", $id, $matches)) {
            return $matches[1];
        }

        throw new P4Cms_Site_Exception("Failed to get site id from site branch id.");
    }

    /**
     * Get the sub-folder of this site branch in the site depot.
     *
     * @return  string|null     the site branch sub-folder.
     */
    public function getBranchBasename()
    {
        return basename($this->getId()) ?: null;
    }

    /**
     * Check if a site exists with the given id.
     *
     * @param   mixed                       $id             the id to check for.
     * @param   P4_Connection_Interface     $connection     optional - a specific connection to use.
     * @return  bool                        true if the given id matches an existing site.
     */
    public static function exists($id, P4_Connection_Interface $connection = null)
    {
        try {
            static::fetch($id, $connection);
            return true;
        } catch (P4Cms_Model_NotFoundException $e) {
            return false;
        }
    }

    /**
     * Fetch a single site by id from the local sites list.
     *
     * @param   string                      $id             the id of the site to fetch.
     * @param   P4_Connection_Interface     $connection     optional - a specific connection to use.
     * @return  P4Cms_Site                  the matching site if one exists.
     * @throws  P4Cms_Model_NotFoundException   if the requested site can't be found.
     */
    public static function fetch($id, P4_Connection_Interface $connection = null)
    {
        // throw exception if no id given.
        if (!is_string($id) || !$id) {
            throw new InvalidArgumentException("No site id given.");
        }

        // find the identified site.
        $sites = static::fetchAll(null, $connection);
        if (!isset($sites[$id])) {
            throw new P4Cms_Model_NotFoundException("Cannot find the specified site.");
        }

        return $sites[$id];
    }

    /**
     * Get all sites/branches from Perforce as site models.
     * Makes heavy use of caching as this gets called numerous times per-request.
     *
     * @param   array|null               $options        optional - options to limit results
     *                                                       FETCH_BY_ACL - set to an array containing
     *                                                                      resource and/or privilige
     *                                                      FETCH_BY_SITE - set to site id to limit
     *                                                                      to branches of that site
     *                                                    FETCH_SORT_FLAT - set to sort by stream name
     *                                                                      ignoring hierachy
     * @param   P4_Connection_Interface  $connection     optional - a specific connection to use.
     * @return  P4Cms_Model_Iterator     all sites/branches.
     */
    public static function fetchAll(array $options = null, P4_Connection_Interface $connection = null)
    {
        // read sites from the global cache if possible.
        $sites = P4Cms_Cache::load(static::CACHE_KEY, 'global');
        if (!$sites instanceof P4Cms_Model_Iterator) {

            // failed to read sites out of cache, we need to read sites
            // out of perforce which means we need a connection.
            $connection = $connection ?: P4_Connection::getDefaultConnection();

            // fetch sites by querying streams in Perforce
            // chronicle sites are prefixed with 'chronicle-'
            // to distinguish them from other streams
            $streams = P4_Stream::fetchAll(
                array(
                    P4_Stream::FETCH_BY_PATH  => '//' . static::SITE_PREFIX . '*/*',
                    P4_Stream::SORT_RECURSIVE => true
                ),
                $connection
            );

            // each site will make use of this one connection
            // and in so doing change its client, we remember
            // the client here so we can restore it afterwards.
            $client = $connection->getClient();

            // generate site objects for each stream
            // we preload the site object with the stream, config and acl
            // so that next time the site object is read from cache it will
            // already have these objects set on it.
            $sites = new P4Cms_Model_Iterator;
            foreach ($streams as $stream) {
                $site = new static;
                $site->setId($stream->getId());

                // tell the site to use/customize the given connection
                // this will change the connection's client, but we will
                // set it back to the original value below.
                $site->setConnection($connection);

                // read in the stream by getting values and set it on the site.
                $stream->getValues();
                $site->_setStream($stream);

                // read in all of this site's config information.
                $site->getConfig()->getValues();

                // read in the acl (also primes acl roles).
                $site->getAcl();

                // the connection will be useless after this point as the
                // client will be changed; clear site's reference to it
                $site->setConnection(null);

                // give each site/branch a reference to its parent (if it has one)
                $parent = $site->getStream()->getParent();
                if ($parent && isset($sites[$parent])) {
                    $site->setParent($sites[$parent]);
                }

                // add this site to our list.
                $sites[$site->getId()] = $site;
            }

            // restore connection's original client.
            $connection->setClient($client);

            // sort by title of first mainline for each site
            $sites = static::_sortBySiteTitle($sites);

            // save sites to global cache.
            P4Cms_Cache::save($sites, static::CACHE_KEY, array(), null, null, 'global');
        }

        // normalize our options
        $options = (array) $options + array(
            static::FETCH_BY_SITE   => null,
            static::FETCH_BY_ACL    => null,
            static::FETCH_SORT_FLAT => null
        );

        // filter our result by site if requested
        if ($options[static::FETCH_BY_SITE]) {
            $sites->filterByCallback(
                function($site) use ($options)
                {
                    return $site->getSiteId() == $options[$site::FETCH_BY_SITE];
                }
            );
        }

        // filter by ACL if requested
        $user      = P4Cms_User::hasActive() ? P4Cms_User::fetchActive() : null;
        $acl       = (array) $options[static::FETCH_BY_ACL];
        $resource  = array_shift($acl);
        $privilege = array_shift($acl);
        if ($user && ($resource || $privilege)) {
            $sites->filterByCallback(
                function($site) use ($user, $resource, $privilege)
                {
                    return $user->isAllowed($resource, $privilege, $site->getAcl());
                }
            );
        }

        // re-sort by stream name (ignoring depth) if requested
        if ($options[static::FETCH_SORT_FLAT]) {
            $sites->sortByCallback(
                function($a, $b)
                {
                    return strnatcasecmp($a->getStream()->getName(), $b->getStream()->getName());
                }
            );
        }

        // in order for site getConnection() to work we want to give
        // each site a usable template connection if we can.
        if (!$connection && P4_Connection::hasDefaultConnection()) {
            $connection = P4_Connection::getDefaultConnection();
        }
        $sites->invoke('setTemplateConnection', array($connection));

        return $sites;
    }

    /**
     * Get the first site/branch that matches the request
     *
     * Each site has a list of urls that it will respond to. We begin by finding
     * the first site/branch that matches the request url. If no site matches,
     * we return the first site/branch. If there are no sites, returns false.
     *
     * Additionally, we support a convention of embedding the name of a specific
     * branch in the request url as the first path component (after the base url).
     * Such as: http://example.com/-dev- This permits multiple site branches
     * without configuring DNS and/or the web server with different URLs for each
     * branch.
     *
     * If the request url specifies a particular branch, we fetch and return that
     * site branch. If no such branch exists, an exception is thrown.
     *
     * @param   Zend_Controller_Request_Http    $request    a request to examine to determine
     *                                                      which site/branch to load.
     * @param   array|null                      $limit      optional - a whitelist of site/branch ids
     *                                                      that can be safely exposed, all other sites
     *                                                      will be ignored - null to allow all.
     * @param   P4_Connection_Interface         $connection optional - a specific connection to use.
     * @return  P4Cms_Site|false                the first matching site/branch
     */
    public static function fetchByRequest(
        Zend_Controller_Request_Http $request,
        array $limit = null,
        P4_Connection_Interface $connection = null)
    {
        // compose url from http host and request uri to find site.
        $requestUrl = Zend_Uri_Http::fromString(
            $request->getScheme()
            . '://'
            . $request->getHttpHost()
            . $request->getRequestUri()
        );

        // loop over urls in site/branches to find a matching prefix.
        $found = false;
        $sites = static::fetchAll(null, $connection);
        foreach ($sites as $site) {

            // skip sites that aren't in the limit whitelist.
            if ($limit && !in_array($site->getId(), $limit)) {
                continue;
            }

            // loop over urls served by this site.
            foreach ($site->getConfig()->getUrls() as $url) {
                // trim whitespace to improve our chances of a match
                $url = trim($url);

                // if url has no scheme - assume scheme of request
                if (!preg_match('#[a-z]+://#i', $url)) {
                    $url = $request->getScheme() . '://' . $url;
                }

                // convert url to Zend_Uri_Http object and skip invalid urls
                try {
                    $url = Zend_Uri_Http::fromString($url);
                } catch (Exception $e) {
                    continue;
                }

                // request scheme (protocol) must match.
                if ($url->getScheme() != $requestUrl->getScheme()) {
                    continue;
                }

                // http host must match.
                if ($url->getHost() != $requestUrl->getHost()) {
                    continue;
                }

                // if path specified - request url must start with path.
                if ($url->getPath() && strpos($requestUrl->getPath(), $url->getPath()) !== 0) {
                    continue;
                }

                // still here? site matches!
                $found = $site;
                break 2;
            }
        }

        // if we failed to find a precise match, assume the first site.
        $found = $found ?: $sites->first();

        // if still no site, or no specific branch requested, all done.
        if (!$found
            || !$request instanceof P4Cms_Controller_Request_Http
            || !$request->getBranchName()
        ) {
            return $found;
        }

        // url specifies a particular branch -- if it's not allowed by
        // the whitelist, simply return what we have found so far.
        $branch = '//' . $found->getSiteId() . '/' . $request->getBranchName();
        if ($limit && !in_array($branch, $limit)) {
            return $found;
        }

        // attempt to fetch the specified branch, if that branch cannot
        // be found, fallback to whatever branch we found previously.
        try {
            return static::fetch($branch, $connection);
        } catch (P4Cms_Model_NotFoundException $e) {
            return $found;
        }
    }

    /**
     * Fetch the active (currently loaded) site.
     * Guaranteed to return the active site model or throw an exception.
     *
     * @return  P4Cms_Site              the currently active site.
     * @throws  P4Cms_Site_Exception    if there is no currently active site.
     */
    public static function fetchActive()
    {
        if (!static::$_activeSite || !static::$_activeSite instanceof P4Cms_Site) {
            throw new P4Cms_Site_Exception("There is no active (currently loaded) site.");
        }
        return static::$_activeSite;
    }

    /**
     * Determine if there is an active (currently loaded) site.
     *
     * @return  boolean     true if there is an active site.
     */
    public static function hasActive()
    {
        try {
            static::fetchActive();
            return true;
        } catch (P4Cms_Site_Exception $e) {
            return false;
        }
    }

    /**
     * Clear the active site.
     */
    public static function clearActive()
    {
        static::$_activeSite = null;
    }

    /**
     * Warning: this method changes the client of the given connection.
     * Sets the perforce connection to use for this site and configures
     * it with a new client configured to use this site's stream.
     *
     * @param   P4_Connection_Interface|null    $connection     the connection to use or null.
     * @return  P4Cms_Site                      provides fluent interface.
     */
    public function setConnection(P4_Connection_Interface $connection = null)
    {
        $this->_connection = $connection
            ? $this->_customizeConnection($connection)
            : null;

        // wipe out the storage adapter anytime the connection changes.
        // this ensures that subsequent calls to getStorageAdapter()
        // will get the same connection that we have been handed here.
        $this->_adapter = null;

        // ensure that the stream always uses the same connection as the site.
        // if null was given a connection might be dynamically generated later.
        // we use a deferred connection to ensure the connection stays in sync.
        if ($this->_stream) {
            $this->_stream->setConnection($this->getDeferredConnection());
        }

        // the acl record also needs to be updated. it has an associated
        // p4 file object that needs to be cleared if the connection changes
        // (otherwise, it will have the old connection and related properties).
        // we use a 'deferred' adapter to delay creating a connection (in case
        // null was given) and to ensure the adapter stays in sync with the site.
        if ($this->_acl) {
            $this->_acl->getRecord()->setAdapter($this->getDeferredAdapter());
        }

        return $this;
    }

    /**
     * The connection to use as a template when generating a new connection.
     * If no template is set, the default connection is used.
     *
     * @param   P4_Connection_Interface|null    $connection     the template connection or null.
     * @return  P4Cms_Site                      provides fluent interface.
     */
    public function setTemplateConnection(P4_Connection_Interface $connection = null)
    {
        $this->_templateConnection = $connection;

        return $this;
    }

    /**
     * Check if this site/branch already has a perforce connection.
     * You cannot use getConnection() for this because it will always
     * try to return a connection.
     *
     * @return  bool    true if this site/branch already has a connection.
     */
    public function hasConnection()
    {
        return (bool) $this->_connection;
    }

    /**
     * Get the perforce connection for this site.
     *
     * If no connection has been explicitly set, a new connection will
     * be made using the current template (or the default connection as
     * a template) customized for the site.
     *
     * @return  P4_Connection_Interface     a connection to this site's perforce server.
     * @throws  P4Cms_Site_Exception        if no explicit, template or default connection is set.
     */
    public function getConnection()
    {
        // check for existing connection.
        if ($this->_connection instanceof P4_Connection_Interface) {
            return $this->_connection;
        }

        if (!$this->_templateConnection && !P4_Connection::hasDefaultConnection()) {
            throw new P4Cms_Site_Exception(
                "Cannot get connection. No explicit or default connection set."
            );
        }

        // if we don't have an existing connection create a
        // custom version of the template or default connection
        $template   = $this->_templateConnection ?: P4_Connection::getDefaultConnection();
        $connection = P4_Connection::factory(
            $template->getPort(),
            $template->getUser(),
            $template->getClient(),
            $template->getPassword(),
            $template->getTicket(),
            get_class($template)
        );

        // attempt to login if we don't already have a ticket.
        if (!$connection->getTicket()) {
            $connection->login();
        }

        // set connection will record this connection for future
        // calls and customize it to use the site client
        $this->setConnection($connection);

        return $connection;
    }

    /**
     * Get a 'deferred' connection. This can be used anywhere a regular
     * connection can be used.
     *
     * Getting a deferred connection will not cause the site to create
     * a connection until it is actually used. It will always link to
     * the site's current connection even if it is changed.
     *
     * @return  P4_Connection_Deferred  a connection linked to this site's connection.
     */
    public function getDeferredConnection()
    {
        $site = $this;
        return new P4_Connection_Deferred(
            function() use ($site)
            {
                return $site->getConnection();
            }
        );
    }

    /**
     * Load this site into the environment and set it as the active site.
     *
     * Establish a connection and record adapter for this site and set them
     * as the default. Also, updates package paths to point at site resources.
     *
     * @return  P4Cms_Site  provides fluent interface.
     */
    public function load()
    {
        // ensure paths we need to write to exist and are writable.
        P4Cms_FileUtility::createWritablePath($this->getDataPath());
        P4Cms_FileUtility::createWritablePath($this->getWorkspacesPath());

        // set this site's connection as the default connection for the environment.
        P4_Connection::setDefaultConnection($this->getConnection());

        // set this site's storage adapter as the default.
        P4Cms_Record::setDefaultAdapter($this->getStorageAdapter());

        // add the appropriate themes paths for this site.
        P4Cms_Theme::clearPackagesPaths();
        P4Cms_Theme::addPackagesPath(static::getSitesPackagesPath() . '/all/themes');
        P4Cms_Theme::addPackagesPath($this->getThemesPath());

        // add the appropriate modules paths for this site.
        P4Cms_Module::clearPackagesPaths();
        P4Cms_Module::addPackagesPath(static::getSitesPackagesPath() . '/all/modules');
        P4Cms_Module::addPackagesPath($this->getModulesPath());

        // set this instance as the active site.
        static::$_activeSite = $this;

        return $this;
    }

    /**
     * Get the path to this site's packages folder (not branch specific)
     *
     * @return  string  the path to this site's packages folder.
     */
    public function getPackagesPath()
    {
        return static::getSitesPackagesPath($this->getSiteId());
    }

    /**
     * Get the path to this site branch's data folder.
     *
     * @return string   the path to this site branch's data folder.
     */
    public function getDataPath()
    {
        return static::getSitesDataPath($this->getSiteId())
            . '/' . $this->getBranchBasename();
    }

    /**
     * Get the path to this site's p4 workspaces.
     *
     * @return  string  the path to the site workspaces.
     */
    public function getWorkspacesPath()
    {
        return $this->getDataPath() . '/workspaces';
    }

    /**
     * Get the path to this site's modules.
     *
     * @return  string  the path to this site's modules folder.
     */
    public function getModulesPath()
    {
        return $this->getPackagesPath() . '/modules';
    }

    /**
     * Get the path to this site's themes.
     *
     * @return  string  the path to this site's themes folder.
     */
    public function getThemesPath()
    {
        return $this->getPackagesPath() . '/themes';
    }

    /**
     * Get the path to this site's (writable) public resources.
     *
     * @return  string  the path to the site's (writable) public resources.
     */
    public function getResourcesPath()
    {
        return $this->getDataPath() . '/resources';
    }

    /**
     * Get the storage adapter to use when reading records from
     * and writing records to this site.
     *
     * @return  P4Cms_Record_Adapter    the storage adapter to use for this site branch.
     */
    public function getStorageAdapter()
    {
        if ($this->_adapter) {
            return $this->_adapter;
        }

        // no site adapter prepared, make a new one.
        $adapter = new P4Cms_Record_Adapter;

        // the adapter should use this site branch's connection
        // this will ensure it uses the appropriate stream client
        $adapter->setConnection($this->getConnection());

        // when composing record paths, use client-syntax as the base
        // this will ensure that paths resolve through the view.
        $adapter->setBasePath("//" . $this->getConnection()->getClient());

        // set the name of this site's 'umbrella' group in Perforce.
        // this is the parent group for all site roles and gives its
        // members read/write permission to this site's depot files
        // (it is site global, not branch specific).
        $adapter->setProperty(P4Cms_Acl_Role::PARENT_GROUP,  $this->getSiteId());

        // volatile records need to share a non-temp client to see
        // records because they store them as pending files - pick
        // a client name based on the site-branch id.
        $adapter->setProperty(
            P4Cms_Record_Volatile::CLIENT,
            str_replace('/', '-', trim($this->getId(), '/'))
        );

        // only make the adapter once.
        $this->_adapter = $adapter;

        return $adapter;
    }

    /**
     * Get a 'deferred' storage adapter. This can be used anywhere a regular
     * record adapter can be used.
     *
     * Getting a deferred adapter will not cause the site to create a storage
     * adapter until it is actually used. It will always link to the site's
     * current storage adapter even if it is changed.
     *
     * @return  P4Cms_Record_DeferredAdapter    an adapter linked to this site's adapter.
     */
    public function getDeferredAdapter()
    {
        $site = $this;
        return new P4Cms_Record_DeferredAdapter(
            function() use ($site)
            {
                return $site->getStorageAdapter();
            }
        );
    }

    /**
     * Get the stream object for this site branch.
     *
     * @return  P4_Stream|null  the stream for this site branch or null if we don't have an id.
     */
    public function getStream()
    {
        if ($this->_stream || !$this->getId()) {
            return $this->_stream;
        }

        $this->_stream = P4_Stream::fetch($this->getId(), $this->getConnection());

        return $this->_stream;
    }

    /**
     * Get the configuration object for this site branch.
     *
     * @return  P4Cms_Site_Config               the configuration record for this site branch.
     * @throws  P4Cms_Model_NotFoundException   if an invalid revision is given.
     */
    public function getConfig()
    {
        if (!$this->_config) {
            $this->_config = new P4Cms_Site_Config($this);
        }

        return $this->_config;
    }

    /**
     * Get the ACL for this site.
     *
     * @return  P4Cms_Acl   the acl defined for this site.
     */
    public function getAcl()
    {
        // load acl from storage if we haven't already done so.
        if (!$this->_acl instanceof P4Cms_Acl) {
            $adapter = $this->getStorageAdapter();
            try {
                $acl = P4Cms_Acl::fetch(static::ACL_RECORD_ID, $adapter);
            } catch (P4Cms_Model_NotFoundException $e) {

                // setup record storage for acl.
                $record = new P4Cms_Record;
                $record->setId(static::ACL_RECORD_ID)
                       ->setAdapter($adapter);

                // create new, empty, acl.
                $acl = new P4Cms_Acl;
                $acl->setRecord($record);

            }

            // load roles into acl.
            $acl->setRoles(P4Cms_Acl_Role::fetchAll(null, $adapter));

            $this->_acl = $acl;
        }

        return $this->_acl;
    }

    /**
     * Set a reference to this site/branch's parent branch.
     *
     * @param   P4Cms_Site|null     $parent     a reference to this branch's parent branch
     * @return  P4Cms_Site          provides fluent interface.
     */
    public function setParent(P4Cms_Site $parent = null)
    {
        $this->_parent = $parent;
        return $this;
    }

    /**
     * Get a reference to this site/branch's parent branch (if it has one).
     *
     * @return  P4Cms_Site|null     this branch's parent branch or null if no parent.
     */
    public function getParent()
    {
        return $this->_parent;
    }

    /**
     * Set the path to the sites packages folder.
     * See getSitesPackagesPath for details.
     *
     * @param   string  $path   the path to the sites folder.
     */
    public static function setSitesPackagesPath($path)
    {
        static::$_sitesPackagesPath = rtrim($path, '/');
    }

    /**
     * Get the path to the sites packages folder.
     *
     * This folder contains a sub-folder for each site (plus an all folder)
     * under which theme and module packages reside for each specific site.
     * The 'all' folder contains themes and modules available to all sites.
     *
     * If a site id is given this method will return the path to that specific
     * site's packages folder.
     *
     * @param   string|null     $siteId     optional - the id of a site to get its
     *                                      specific package path
     * @return  string                      the path to the sites folder.
     * @throws  P4Cms_Site_Exception        if the sites path has not been set.
     */
    public static function getSitesPackagesPath($siteId = null)
    {
        if (!strlen(static::$_sitesPackagesPath)) {
            throw new P4Cms_Site_Exception("The sites packages path has not been set.");
        }

        // if no site id given, simply return top-level sites packages path.
        if (!$siteId) {
            return static::$_sitesPackagesPath;
        }

        $validator = new P4Cms_Validate_SiteId;
        if (!$validator->isValid($siteId)) {
            throw new InvalidArgumentException(
                "Cannot get sites packages path. Given site id is malformed."
            );
        }

        // we strip the site id prefix to shorten the path.
        return static::$_sitesPackagesPath . '/' . substr($siteId, strlen(static::SITE_PREFIX));
    }

    /**
     * Set the path to the sites data folder (where sites data is stored).
     *
     * @param   string  $path   the path to the sites data folder.
     */
    public static function setSitesDataPath($path)
    {
        static::$_sitesDataPath = rtrim($path, '/');
    }

    /**
     * Get the path to the sites data folder.
     *
     * This writable folder contains a sub-folder for each site under which
     * site data is stored.
     *
     * If a site id is given this method will return the path to that specific
     * site's data folder.
     *
     * @param   string|null     $siteId     optional - the id of a site to get its
     *                                      specific package path
     * @return  string                      the path to the sites data folder.
     * @throws  P4Cms_Site_Exception        if the sites data path has not been set.
     * @throws  InvalidArgumentException    if an malformed site id is given.
     */
    public static function getSitesDataPath($siteId = null)
    {
        if (!strlen(static::$_sitesDataPath)) {
            throw new P4Cms_Site_Exception("The sites data path has not been set.");
        }

        // if no site id given, simply return top-level sites data path.
        if (!$siteId) {
            return static::$_sitesDataPath;
        }

        $validator = new P4Cms_Validate_SiteId;
        if (!$validator->isValid($siteId)) {
            throw new InvalidArgumentException(
                "Cannot get sites data path. Given site id is malformed."
            );
        }

        // we strip the site id prefix to shorten the path.
        return static::$_sitesDataPath . '/' . substr($siteId, strlen(static::SITE_PREFIX));
    }

    /**
     * Sort sites by the title of the first mainline within each site.
     * Maintains the existing order for the branches within each site.
     *
     * @param   P4Cms_Model_Iterator    $sites  sites already sorted by stream name/depth
     * @return  P4Cms_Model_Iterator    sorted result
     */
    protected static function _sortBySiteTitle(P4Cms_Model_Iterator $sites)
    {
        // create a model for each site which has the mainline's title and
        // holds an iterator of all the site's branches in the correct order
        $bySite = new P4Cms_Model_Iterator;
        foreach ($sites as $site) {
            $siteId = $site->getSiteId();
            if (!isset($bySite[$siteId])) {
                $bySite[$siteId] = new P4Cms_Model;
                $bySite[$siteId]->setValue('Title', $site->getConfig()->getTitle());
                $bySite[$siteId]->branches = new P4Cms_Model_Iterator;
            }
            $bySite[$siteId]->branches[$site->getId()] = $site;
        }

        // sort the sites by the title of the mainline
        $bySite->sortBy('Title', array(P4Cms_Model_Iterator::SORT_NATURAL));

        // glue all of the branches back into a single result now
        // that they are sorted by their associated site's title
        $result = new P4Cms_Model_Iterator;
        foreach ($bySite as $site) {
            $result->merge($site->branches);
        }

        return $result;
    }

    /**
     * Used by fetchAll to set the stream on a new instance.
     *
     * @param   P4_Stream|null  $stream     stream to use for this site
     * @return  P4Cms_Site                  provides fluent interface.
     */
    protected function _setStream(P4_Stream $stream = null)
    {
        $this->_stream = $stream;

        return $this;
    }

    /**
     * Customize the given connection for this site.
     *
     * Creates a new client configured to use this site's stream and
     * configures the given connection to use the new client.
     *
     * @param   P4_Connection_Interface     $connection     the connection to customize
     * @return  P4_Connection_Interface     the customized connection.
     */
    protected function _customizeConnection(P4_Connection_Interface $connection)
    {
        // we cannot customize the connection if we don't have an id (aka a stream id)
        if (!$this->getId()) {
            throw new P4Cms_Site_Exception(
                "Cannot customize connection. No stream id has been set."
            );
        }

        // to avoid problems that result from multiple processes
        // sharing one client (namely race conditions), we generate
        // a temporary client for each request.
        $tempClientId = P4_Client::makeTempId();

        // setup our temp client to use the site's stream.
        $root   = $this->getWorkspacesPath() . "/" . $tempClientId;
        $client = new P4_Client($connection);
        $client->setId($tempClientId)
               ->setStream($this->getId())
               ->setRoot($root);

        // create the client with the values we've setup above, using
        // makeTemp() so that it will be destroyed automatically.
        // provide a custom clean-up callback to delete the workspace folder.
        $cleanup = function($entry, $defaultCallback) use ($root)
        {
            $defaultCallback($entry);
            P4Cms_FileUtility::deleteRecursive($root);
        };
        P4_Client::makeTemp($client->getValues(), $cleanup, $connection);

        // use our newly created client.
        $connection->setClient($tempClientId);

        return $connection;
    }
}
# Change User Description Committed
#1 16170 perforce_software Move Chronicle files to follow new path scheme for branching.
//guest/perforce_software/chronicle/library/P4Cms/Site.php
#1 8972 Matt Attaway Initial add of the Chronicle source code