nextcloud/apps/files_external/3rdparty/php-opencloud/lib/OpenCloud/Common/Service.php

490 lines
15 KiB
PHP

<?php
/**
* PHP OpenCloud library.
*
* @copyright Copyright 2013 Rackspace US, Inc. See COPYING for licensing information.
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache 2.0
* @version 1.6.0
* @author Jamie Hannaford <jamie.hannaford@rackspace.com>
*/
namespace OpenCloud\Common;
use OpenCloud\Common\Base;
use OpenCloud\Common\Lang;
use OpenCloud\OpenStack;
use OpenCloud\Common\Exceptions;
/**
* This class defines a cloud service; a relationship between a specific OpenStack
* and a provided service, represented by a URL in the service catalog.
*
* Because Service is an abstract class, it cannot be called directly. Provider
* services such as Rackspace Cloud Servers or OpenStack Swift are each
* subclassed from Service.
*
* @author Glen Campbell <glen.campbell@rackspace.com>
*/
abstract class Service extends Base
{
protected $conn;
private $service_type;
private $service_name;
private $service_region;
private $service_url;
protected $_namespaces = array();
/**
* Creates a service on the specified connection
*
* Usage: `$x = new Service($conn, $type, $name, $region, $urltype);`
* The service's URL is defined in the OpenStack's serviceCatalog; it
* uses the $type, $name, $region, and $urltype to find the proper URL
* and set it. If it cannot find a URL in the service catalog that matches
* the criteria, then an exception is thrown.
*
* @param OpenStack $conn - a Connection object
* @param string $type - the service type (e.g., "compute")
* @param string $name - the service name (e.g., "cloudServersOpenStack")
* @param string $region - the region (e.g., "ORD")
* @param string $urltype - the specified URL from the catalog
* (e.g., "publicURL")
*/
public function __construct(
OpenStack $conn,
$type,
$name,
$region,
$urltype = RAXSDK_URL_PUBLIC,
$customServiceUrl = null
) {
$this->setConnection($conn);
$this->service_type = $type;
$this->service_name = $name;
$this->service_region = $region;
$this->service_url = $customServiceUrl ?: $this->getEndpoint($type, $name, $region, $urltype);
}
/**
* Set this service's connection.
*
* @param type $connection
*/
public function setConnection($connection)
{
$this->conn = $connection;
}
/**
* Get this service's connection.
*
* @return type
*/
public function getConnection()
{
return $this->conn;
}
/**
* Returns the URL for the Service
*
* @param string $resource optional sub-resource
* @param array $query optional k/v pairs for query strings
* @return string
*/
public function url($resource = '', array $param = array())
{
$baseurl = $this->service_url;
// use strlen instead of boolean test because '0' is a valid name
if (strlen($resource) > 0) {
$baseurl = Lang::noslash($baseurl).'/'.$resource;
}
if (!empty($param)) {
$baseurl .= '?'.$this->MakeQueryString($param);
}
return $baseurl;
}
/**
* Returns the /extensions for the service
*
* @api
* @return array of objects
*/
public function extensions()
{
$ext = $this->getMetaUrl('extensions');
return (is_object($ext) && isset($ext->extensions)) ? $ext->extensions : array();
}
/**
* Returns the /limits for the service
*
* @api
* @return array of limits
*/
public function limits()
{
$limits = $this->getMetaUrl('limits');
return (is_object($limits)) ? $limits->limits : array();
}
/**
* Performs an authenticated request
*
* This method handles the addition of authentication headers to each
* request. It always adds the X-Auth-Token: header and will add the
* X-Auth-Project-Id: header if there is a tenant defined on the
* connection.
*
* @param string $url The URL of the request
* @param string $method The HTTP method (defaults to "GET")
* @param array $headers An associative array of headers
* @param string $body An optional body for POST/PUT requests
* @return \OpenCloud\HttpResult
*/
public function request(
$url,
$method = 'GET',
array $headers = array(),
$body = null
) {
$headers['X-Auth-Token'] = $this->conn->Token();
if ($tenant = $this->conn->Tenant()) {
$headers['X-Auth-Project-Id'] = $tenant;
}
return $this->conn->request($url, $method, $headers, $body);
}
/**
* returns a collection of objects
*
* @param string $class the class of objects to fetch
* @param string $url (optional) the URL to retrieve
* @param mixed $parent (optional) the parent service/object
* @return OpenCloud\Common\Collection
*/
public function collection($class, $url = null, $parent = null)
{
// Set the element names
$collectionName = $class::JsonCollectionName();
$elementName = $class::JsonCollectionElement();
// Set the parent if empty
if (!$parent) {
$parent = $this;
}
// Set the URL if empty
if (!$url) {
$url = $parent->url($class::ResourceName());
}
// Save debug info
$this->getLogger()->info(
'{class}:Collection({url}, {collectionClass}, {collectionName})',
array(
'class' => get_class($this),
'url' => $url,
'collectionClass' => $class,
'collectionName' => $collectionName
)
);
// Fetch the list
$response = $this->request($url);
$this->getLogger()->info('Response {status} [{body}]', array(
'status' => $response->httpStatus(),
'body' => $response->httpBody()
));
// Check return code
if ($response->httpStatus() > 204) {
throw new Exceptions\CollectionError(sprintf(
Lang::translate('Unable to retrieve [%s] list from [%s], status [%d] response [%s]'),
$class,
$url,
$response->httpStatus(),
$response->httpBody()
));
}
// Handle empty response
if (strlen($response->httpBody()) == 0) {
return new Collection($parent, $class, array());
}
// Parse the return
$object = json_decode($response->httpBody());
$this->checkJsonError();
// See if there's a "next" link
// Note: not sure if the current API offers links as top-level structures;
// might have to refactor to allow $nextPageUrl as method argument
// @codeCoverageIgnoreStart
if (isset($object->links) && is_array($object->links)) {
foreach($object->links as $link) {
if (isset($link->rel) && $link->rel == 'next') {
if (isset($link->href)) {
$nextPageUrl = $link->href;
} else {
$this->getLogger()->warning(
'Unexpected [links] found with no [href]'
);
}
}
}
}
// @codeCoverageIgnoreEnd
// How should we populate the collection?
$data = array();
if (!$collectionName) {
// No element name, just a plain object
// @codeCoverageIgnoreStart
$data = $object;
// @codeCoverageIgnoreEnd
} elseif (isset($object->$collectionName)) {
if (!$elementName) {
// The object has a top-level collection name only
$data = $object->$collectionName;
} else {
// The object has element levels which need to be iterated over
$data = array();
foreach($object->$collectionName as $item) {
$subValues = $item->$elementName;
unset($item->$elementName);
$data[] = array_merge((array)$item, (array)$subValues);
}
}
}
$collectionObject = new Collection($parent, $class, $data);
// if there's a $nextPageUrl, then we need to establish a callback
// @codeCoverageIgnoreStart
if (!empty($nextPageUrl)) {
$collectionObject->setNextPageCallback(array($this, 'Collection'), $nextPageUrl);
}
// @codeCoverageIgnoreEnd
return $collectionObject;
}
/**
* returns the Region associated with the service
*
* @api
* @return string
*/
public function region()
{
return $this->service_region;
}
/**
* returns the serviceName associated with the service
*
* This is used by DNS for PTR record lookups
*
* @api
* @return string
*/
public function name()
{
return $this->service_name;
}
/**
* Returns a list of supported namespaces
*
* @return array
*/
public function namespaces()
{
return (isset($this->_namespaces) && is_array($this->_namespaces)) ? $this->_namespaces : array();
}
/**
* Given a service type, name, and region, return the url
*
* This function ensures that services are represented by an entry in the
* service catalog, and NOT by an arbitrarily-constructed URL.
*
* Note that it will always return the first match found in the
* service catalog (there *should* be only one, but you never know...)
*
* @param string $type The OpenStack service type ("compute" or
* "object-store", for example
* @param string $name The name of the service in the service catlog
* @param string $region The region of the service
* @param string $urltype The URL type; defaults to "publicURL"
* @return string The URL of the service
*/
private function getEndpoint($type, $name, $region, $urltype = 'publicURL')
{
$catalog = $this->getConnection()->serviceCatalog();
// Search each service to find The One
foreach ($catalog as $service) {
// Find the service by comparing the type ("compute") and name ("openstack")
if (!strcasecmp($service->type, $type) && !strcasecmp($service->name, $name)) {
foreach($service->endpoints as $endpoint) {
// Only set the URL if:
// a. It is a regionless service (i.e. no region key set)
// b. The region matches the one we want
if (isset($endpoint->$urltype) &&
(!isset($endpoint->region) || !strcasecmp($endpoint->region, $region))
) {
$url = $endpoint->$urltype;
}
}
}
}
// error if not found
if (empty($url)) {
throw new Exceptions\EndpointError(sprintf(
'No endpoints for service type [%s], name [%s], region [%s] and urlType [%s]',
$type,
$name,
$region,
$urltype
));
}
return $url;
}
/**
* Constructs a specified URL from the subresource
*
* Given a subresource (e.g., "extensions"), this constructs the proper
* URL and retrieves the resource.
*
* @param string $resource The resource requested; should NOT have slashes
* at the beginning or end
* @return \stdClass object
*/
private function getMetaUrl($resource)
{
$urlBase = $this->getEndpoint(
$this->service_type,
$this->service_name,
$this->service_region,
RAXSDK_URL_PUBLIC
);
$url = Lang::noslash($urlBase) . '/' . $resource;
$response = $this->request($url);
// check for NOT FOUND response
if ($response->httpStatus() == 404) {
return array();
}
// @codeCoverageIgnoreStart
if ($response->httpStatus() >= 300) {
throw new Exceptions\HttpError(sprintf(
Lang::translate('Error accessing [%s] - status [%d], response [%s]'),
$urlBase,
$response->httpStatus(),
$response->httpBody()
));
}
// @codeCoverageIgnoreEnd
// we're good; proceed
$object = json_decode($response->httpBody());
$this->checkJsonError();
return $object;
}
/**
* Get all associated resources for this service.
*
* @access public
* @return void
*/
public function getResources()
{
return $this->resources;
}
/**
* Internal method for accessing child namespace from parent scope.
*
* @return type
*/
protected function getCurrentNamespace()
{
$namespace = get_class($this);
return substr($namespace, 0, strrpos($namespace, '\\'));
}
/**
* Resolves fully-qualified classname for associated local resource.
*
* @param string $resourceName
* @return string
*/
protected function resolveResourceClass($resourceName)
{
$className = substr_count($resourceName, '\\')
? $resourceName
: $this->getCurrentNamespace() . '\\Resource\\' . ucfirst($resourceName);
if (!class_exists($className)) {
throw new Exceptions\UnrecognizedServiceError(sprintf(
'%s resource does not exist, please try one of the following: %s',
$resourceName,
implode(', ', $this->getResources())
));
}
return $className;
}
/**
* Factory method for instantiating resource objects.
*
* @access public
* @param string $resourceName
* @param mixed $info (default: null)
* @return object
*/
public function resource($resourceName, $info = null)
{
$className = $this->resolveResourceClass($resourceName);
return new $className($this, $info);
}
/**
* Factory method for instantiate a resource collection.
*
* @param string $resourceName
* @param string|null $url
* @return Collection
*/
public function resourceList($resourceName, $url = null, $service = null)
{
$className = $this->resolveResourceClass($resourceName);
return $this->collection($className, $url, $service);
}
}