media_acquiadam-8.x-1.46/src/Client.php
src/Client.php
<?php
namespace Drupal\media_acquiadam;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Routing\RouteObjectInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\media_acquiadam\Entity\Asset;
use Drupal\media_acquiadam\Entity\Category;
use Drupal\media_acquiadam\Exception\UploadAssetException;
use Drupal\user\UserDataInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\RequestOptions;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Provides the integration with Acquia DAM.
* @phpcs:disable Drupal.Commenting.Deprecated
*/
class Client {
/**
* The Guzzle client to use for communication with the Acquia DAM API.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $client;
/**
* The base URL of the Acquia DAM API.
*
* @var string
*/
protected $baseUrl = "https://api.widencollective.com/v2";
/**
* Datastore for the specific metadata fields.
*
* @var array
*/
protected $specificMetadataFields;
/**
* The user data factory service.
*
* @var \Drupal\user\UserDataInterface
*/
protected $userData;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Drupal config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Acquia DAM config.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The version of this client. Used in User-Agent string for API requests.
*
* @var string
*/
const CLIENTVERSION = "2.x";
/**
* Client constructor.
*
* @param \GuzzleHttp\ClientInterface $client
* The Guzzle client interface.
* @param \Drupal\user\UserDataInterface $user_data
* The user data interface.
* @param \Drupal\Core\Session\AccountInterface $account
* The account interface.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config interface.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(ClientInterface $client, UserDataInterface $user_data, AccountInterface $account, ConfigFactoryInterface $configFactory, RequestStack $request_stack) {
$this->client = $client;
$this->userData = $user_data;
$this->account = $account;
$this->configFactory = $configFactory;
$this->config = $configFactory->get('media_acquiadam.settings');
$this->requestStack = $request_stack;
}
/**
* Check if the current user is authenticated on Acquia DAM.
*
* In case of anonymous user, check the generic token has been configured.
*
* @return bool
* TRUE if the authentication details are available. FALSE otherwise.
*/
public function checkAuth(): bool {
$request = $this->requestStack->getCurrentRequest();
if ($this->account->isAuthenticated()) {
$account = $this->userData->get('media_acquiadam', $this->account->id(), 'account');
if (isset($account['acquiadam_username']) && isset($account['acquiadam_token'])) {
return TRUE;
}
}
elseif (!$this->account->isAuthenticated() && !empty($this->config->get('token'))
&& (PHP_SAPI === 'cli' || $request->attributes->get(RouteObjectInterface::ROUTE_NAME) === 'system.cron_settings')) {
return TRUE;
}
return FALSE;
}
/**
* Get internal auth state details.
*
* @return array
* An array with the auth state details (username and token).
*/
public function getAuthState(): array {
$state = ['valid_token' => FALSE];
$account = $this->userData->get('media_acquiadam', $this->account->id(), 'account');
if (isset($account['acquiadam_username']) || isset($account['acquiadam_token'])) {
$state = [
'valid_token' => TRUE,
'username' => $account['acquiadam_username'],
'access_token' => $account['acquiadam_token'],
];
}
return $state;
}
/**
* Return an array of headers to add to every authenticated request.
*
* @return array
* A list of headers to be used in API calls.
*/
protected function getDefaultHeaders(): array {
$token = NULL;
$request = $this->requestStack->getCurrentRequest();
if ($this->account->isAuthenticated()) {
$account = $this->userData->get('media_acquiadam', $this->account->id(), 'account');
if (isset($account['acquiadam_token'])) {
$token = $account['acquiadam_token'];
}
}
elseif (!$this->account->isAuthenticated() && !empty($this->config->get('token'))
&& (PHP_SAPI === 'cli' || $request->attributes->get(RouteObjectInterface::ROUTE_NAME) === 'system.cron_settings'
|| $request->attributes->get(RouteObjectInterface::ROUTE_NAME) === 'entity.ultimate_cron_job.run')) {
$token = $this->config->get('token');
}
return [
'User-Agent' => 'drupal/acquiadam ' . self::CLIENTVERSION,
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $token,
];
}
/**
* Get a Category given a Category Name.
*
* @param string $categoryName
* The Acquia DAM Category Name.
*
* @return \Drupal\media_acquiadam\Entity\Category
* The category object.
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function getCategoryByName(string $categoryName): Category {
$this->checkAuth();
$response = $this->client->request(
"GET",
$this->baseUrl . '/categories/' . $categoryName,
['headers' => $this->getDefaultHeaders()]
);
$category = Category::fromJson((string) $response->getBody());
return $category;
}
/**
* Load subcategories by Category link or parts (used in breadcrumb).
*
* @param \Drupal\media_acquiadam\Entity\Category $category
* Category object.
*
* @return \Drupal\media_acquiadam\Entity\Category[]
* A list of sub-categories (ie: child categories).
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function getCategoryData(Category $category): array {
$this->checkAuth();
$url = $this->baseUrl . '/categories';
// If category is not set, it will load the root category.
if (isset($category->links->categories)) {
$url = $category->links->categories;
}
elseif (!empty($category->parts)) {
$cats = "";
foreach ($category->parts as $part) {
$cats .= "/" . $part;
}
$url .= $cats;
}
$response = $this->client->request(
"GET",
$url,
['headers' => $this->getDefaultHeaders()]
);
$category = Category::fromJson((string) $response->getBody());
return $category;
}
/**
* Get top level categories.
*
* @return Drupal\media_acquiadam\Entity\Category[]
* A list of top level categories (ie: root categories).
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function getTopLevelCategories(): array {
$this->checkAuth();
$response = $this->client->request(
"GET",
$this->baseUrl . '/categories',
['headers' => $this->getDefaultHeaders()]
);
$categories_data = json_decode($response->getBody());
$categories = [];
foreach ($categories_data->items as $category) {
$category->items = $this->getCategoryByName($category->name);
$categories[] = Category::fromJson($category);
}
return $categories;
}
/**
* Get an Asset given an Asset ID.
*
* @param string $assetId
* The Acquia DAM Asset ID.
* @param array $expands
* The additional properties to be included.
*
* @return \Drupal\media_acquiadam\Entity\Asset
* The asset entity.
*
* @throws \GuzzleHttp\Exception\RequestException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function getAsset(string $assetId, array $expands = []): Asset {
$this->checkAuth();
$required_expands = Asset::getRequiredExpands();
$allowed_expands = Asset::getAllowedExpands();
$expands = array_intersect(array_unique($expands + $required_expands), $allowed_expands);
$response = $this->client->request(
"GET",
$this->baseUrl . '/assets/' . $assetId . '?expand=' . implode('%2C', $expands),
['headers' => $this->getDefaultHeaders()]
);
return Asset::fromJson((string) $response->getBody());
}
/**
* Gets presigned url from AWS S3.
*
* @param string $file_type
* The File Content Type.
* @param string $file_name
* The File filename.
* @param string $file_size
* The File size.
* @param string $folderID
* The folder ID to upload the file to.
*
* @return mixed
* Presigned url needed for next step + PID.
*/
protected function getPresignUrl($file_type, $file_name, $file_size, $folderID) {
$this->checkAuth();
$file_data = [
'filesize' => $file_size,
'filename' => $file_name,
'contenttype' => $file_type,
'folderid' => $folderID,
];
$response = $this->client->request(
"GET",
$this->baseUrl . '/ws/awss3/generateupload',
[
'headers' => $this->getDefaultHeaders(),
'query' => $file_data,
]
);
return json_decode($response->getBody());
}
/**
* Uploads file to Acquia DAM AWS S3.
*
* @param mixed $presignedUrl
* The presigned URL we got in previous step from AWS.
* @param string $file_uri
* The file URI.
* @param string $file_type
* The File Content Type.
*
* @return array
* Response Status 100 / 200
*
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \Drupal\media_acquiadam\Exception\InvalidCredentialsException
*/
protected function uploadPresigned($presignedUrl, $file_uri, $file_type) {
$this->checkAuth();
$file = fopen($file_uri, 'r');
$response = $this->client->request(
"PUT",
$presignedUrl,
[
'headers' => ['Content-Type' => $file_type],
'body' => stream_get_contents($file),
RequestOptions::TIMEOUT => 0,
]
);
return [
'status' => json_decode($response->getStatusCode(), TRUE),
];
}
/**
* Confirms the upload to Acquia DAM.
*
* @param string $pid
* The Process ID we got in first step.
*
* @return string
* The uploaded/edited asset ID.
*/
protected function uploadConfirmed($pid) {
$this->checkAuth();
$response = $this->client->request(
"PUT",
$this->baseUrl . '/ws/awss3/finishupload/' . $pid,
['headers' => $this->getDefaultHeaders()]
);
return (string) json_decode($response->getBody(), TRUE)['id'];
}
/**
* Uploads Assets to Acquia DAM using the previously defined methods.
*
* @param string $file_uri
* The file URI.
* @param string $file_name
* The File filename.
* @param int $folderID
* The Acquia DAM folder ID.
*
* @return string
* Acquia DAM response (asset id).
*
* @throws \Drupal\media_acquiadam\Exception\UploadAssetException
* If uploadAsset fails we throw an instance of UploadAssetException
* that contains a message for the caller.
*/
public function uploadAsset(string $file_uri, string $file_name, int $folderID): string {
$this->checkAuth();
// Getting file data from file_uri.
$file_type = mime_content_type($file_uri);
$file_size = filesize($file_uri);
$response = [];
// Getting Pre-sign URL.
$presign = $this->getPresignUrl($file_type, $file_name, $file_size, $folderID);
if (property_exists($presign, 'presignedUrl')) {
// Post-sign upload.
$postsign = $this->uploadPresigned($presign->presignedUrl, $file_uri, $file_type);
if ($postsign['status'] == '200' || $postsign['status'] == '100') {
// Getting Asset ID.
$response = $this->uploadConfirmed($presign->processId);
}
else {
// If we got presignedUrl but upload not confirmed, we throw exception.
throw new UploadAssetException('Failed to upload file after presigning.');
}
}
else {
// If we couldn't retrieve presignedUrl, we throw exception.
throw new UploadAssetException('Failed to obtain presigned URL from AWS.');
}
return $response;
}
/**
* Get a list of Assets given a Category ID.
*
* @param string $category_name
* Category name.
* @param array $params
* Additional query parameters for the request.
* @param bool $exact_search
* Use exact search instead of phrase search.
*
* @return array
* Contains the following keys:
* - total_count: The total number of assets in the result set across all
* pages.
* - assets: an array of Asset objects.
*
* @link https://community.widen.com/collective/s/article/How-do-I-search-for-assets
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function getAssetsByCategory(string $category_name, array $params = [], bool $exact_search = TRUE): array {
if ($category_name) {
if ($exact_search) {
$params['query'] = 'category:({' . $category_name . '})';
}
else {
$params['query'] = 'category:' . $category_name;
}
}
// Fetch all assets of current category.
$assets = $this->searchAssets($params);
return $assets;
}
/**
* Get a list of Assets given an array of Asset ID's.
*
* @param array $assetIds
* The Acquia DAM Asset ID's.
* @param array $expand
* A list of dta items to expand on the result set.
*
* @return array
* A list of assets.
*/
public function getAssetMultiple(array $assetIds, array $expand = []): array {
$this->checkAuth();
if (empty($assetIds)) {
return [];
}
$assets = [];
foreach ($assetIds as $assetId) {
$assets[] = $this->getAsset($assetId, $expand);
}
return $assets;
}
/**
* Search for assets using the Acquia DAM search API.
*
* @param array $params
* An array used as query parameter. Valid parameters are documented on
* https://widenv2.docs.apiary.io/#reference/assets/assets/list-by-search-query.
* @param bool $released_not_expired
* Add filter to verify if asset is released and not expired.
*
* @return array
* A list of assets.
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function searchAssets(array $params, bool $released_not_expired = TRUE): array {
$this->checkAuth();
// By default the UI only shows by day, but this should be by hour.
$date = date('m/d/Y');
if ($released_not_expired) {
$params['query'] = $params['query'] ? $params['query'] . ' AND ' : '';
$params['query'] .= 'rd:([before ' . $date . '] OR [' . $date . ']) AND ed:((isEmpty) OR [after ' . $date . '])';
}
$response = $this->client->request(
"GET",
$this->baseUrl . '/assets/search',
[
'headers' => $this->getDefaultHeaders(),
'query' => $params,
]
);
$status_code = $response->getStatusCode();
if ($status_code !== 200) {
\Drupal::logger('media_acquiadam')->error('Unknown HTTP error happened while communicating with the Widen API. Status code: ' . $status_code);
return [
'total_count' => 0,
];
}
$response = json_decode((string) $response->getBody());
$results = [
'total_count' => $response->total_count,
];
foreach ($response->items as $asset) {
// Filter out any results that aren't released or are expired.
$asset_entity = Asset::fromJson($asset);
if ($asset_entity->released_and_not_expired && !empty($asset_entity->links->download)) {
$results['assets'][] = $asset_entity;
}
else {
// Decrement the total count if the asset isn't being included.
$results['total_count']--;
}
}
return $results;
}
/**
* Download file asset from Acquia DAM.
*
* @param string $assetID
* Asset ID to be fetched.
*
* @return string
* Contents of the file as a string.
*/
public function downloadAsset(string $assetID): string {
$this->checkAuth();
$response = $this->getAsset($assetID);
if ($response === NULL) {
return '';
}
$url = str_replace('&download=true', '', $response->embeds->original->url);
$response = $this->client->request('GET', $url, [
'allow_redirects' => [
'track_redirects' => TRUE,
],
]);
$size = $response->getBody()->getSize();
if ($size === NULL || $size === 0) {
return '';
}
return (string) $response->getBody();
}
/**
* Queue custom asset conversions for download.
*
* This is a 2 step process:
* 1. Queue assets.
* 2. Download From Queue.
*
* This step will allow users to queue an asset for download by specifying an
* AssetID and a Preset ID or custom conversion parameters. If a valid
* PresetID is defined, the other conversions parameters will be ignored
* (format, resolution, size, orientation, colorspace).
*
* @param array|int $assetIDs
* A single or list of asset IDs.
* @param array $options
* Asset preset or conversion options.
*
* @return array
* An array of response data.
*
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \Drupal\media_acquiadam\Exception\InvalidCredentialsException
*/
public function queueAssetDownload($assetIDs, array $options): array {
$this->checkAuth();
if (!is_array($assetIDs)) {
$assetIDs = [$assetIDs];
}
$data = ['items' => []];
foreach ($assetIDs as $assetID) {
$data['items'][] = ['id' => $assetID] + $options;
}
$response = $this->client->request(
'POST',
$this->baseUrl . '/assets/queuedownload',
[
'headers' => $this->getDefaultHeaders(),
RequestOptions::JSON => $data,
]
);
$response = json_decode((string) $response->getBody(), TRUE);
return $response;
}
/**
* Gets asset download queue information.
*
* This is a 2 step process:
* 1. Queue assets.
* 2. Download From Queue.
*
* This step will allow users to download the queued asset using the download
* key returned from step1 (Queue asset process). The output of this step will
* be a download URL to the asset or the download status, if the asset is not
* ready for download.
*
* @param string $downloadKey
* The download key to check the status of.
*
* @return array
* An array of response data.
*
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \Drupal\media_acquiadam\Exception\InvalidCredentialsException
*/
public function downloadFromQueue($downloadKey): array {
$this->checkAuth();
$response = $this->client->request(
'GET',
$this->baseUrl . '/downloadfromqueue/' . $downloadKey,
['headers' => $this->getDefaultHeaders()]
);
$response = json_decode((string) $response->getBody(), TRUE);
return $response;
}
/**
* Edit an asset metadata.
*
* If an asset is uploaded and its required fields are not filled in, the
* asset is in onhold status and cannot be activated until all required fields
* are supplied. Any attempt to change the status to 'active' for assets that
* still require metadata will return back 409.
*
* @param string $assetID
* The asset to edit.
* @param array $data
* Contains lists of fields and data.
*
* The $data should be an array keyed by metadata field names with an array
* of values.
*
* @code
* $data = [
* 'Color' => ['Red'],
* ];
* @endcode
*
* @return \Drupal\media_acquiadam\Entity\Asset|bool
* An asset object on success, or FALSE on failure.
*
* @throws \GuzzleHttp\Exception\GuzzleException
* @throws \Drupal\media_acquiadam\Exception\InvalidCredentialsException
*/
public function editAssetMetadata(string $assetID, array $data) {
$this->checkAuth();
$response = $this->client->request(
'PUT',
$this->baseUrl . "/assets/$assetID/metadata",
[
'headers' => $this->getDefaultHeaders(),
RequestOptions::JSON => ['fields' => $data],
]
);
if (409 == $response->getStatusCode()) {
return FALSE;
}
$asset = Asset::fromJson((string) $response->getBody());
return $asset;
}
/**
* Edit an asset.
*
* @deprecated in media_acquia_dam:2.0.7 and will not be replaced. Call
* `editAssetMetadata` directly.
*
* @see Client::editAssetMetadata()
*
* @throws \GuzzleHttp\Exception\GuzzleException
*
* @throws \Drupal\media_acquiadam\Exception\InvalidCredentialsException
*/
public function editAsset(string $assetID, array $data) {
// phpcs:ignore Drupal.Semantics.FunctionTriggerError
@trigger_error('Client::editAsset() is deprecated in media_acquia_dam:2.0.7 and will not be replaced. Call Client::editAssetMetadata directly.', E_USER_DEPRECATED);
return $this->editAssetMetadata($assetID, $data);
}
/**
* Get a list of metadata.
*
* @return array
* A list of metadata fields.
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function getSpecificMetadataFields(): array {
if (!empty($this->specificMetadataFields)) {
return $this->specificMetadataFields;
}
try {
$this->checkAuth();
}
catch (\Exception $e) {
\Drupal::logger('media_acquiadam')->error('Unable to authenticate to retrieve metadata fields. Exception message: %message', [
'%message' => $e->getMessage(),
]);
$this->specificMetadataFields = [];
return $this->specificMetadataFields;
}
try {
$response = $this->client->request(
'GET',
'https://' . $this->config->get('domain') . '/api/rest/metadata/types',
[
'headers' => $this->getDefaultHeaders(),
]
);
}
catch (\Exception $e) {
\Drupal::logger('media_acquiadam')->error('Unable to retrieve metadata fields. Exception message: %message', [
'%message' => $e->getMessage(),
]);
$this->specificMetadataFields = [];
return $this->specificMetadataFields;
}
$response = json_decode((string) $response->getBody());
$this->specificMetadataFields = [];
foreach ($response->types as $type) {
foreach ($type->fields as $field) {
switch ($field->discriminator) {
case 'TextArea':
$type = 'text_long';
break;
case 'Date':
$type = 'datetime';
break;
default:
$type = 'string';
}
$this->specificMetadataFields[$field->displayKey] = [
'label' => $field->displayName,
'type' => $type,
];
}
}
return $this->specificMetadataFields;
}
/**
* Register integration link on Acquia DAM via API.
*
* @param array $data
* The body of the POST request.
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function registerIntegrationLink(array $data) {
try {
$this->checkAuth();
}
catch (\Exception $e) {
\Drupal::logger('media_acquiadam')->error('Unable to authenticate to register integration link for asset @uuid. Exception message: %message', [
'@uuid' => $data['assetUuid'],
'%message' => $e->getMessage(),
]);
return FALSE;
}
try {
$response = $this->client->request(
'POST',
'https://' . $this->config->get('domain') . '/api/rest/integrationlink',
[
'headers' => $this->getDefaultHeaders(),
RequestOptions::JSON => $data,
]
);
$response = json_decode((string) $response->getBody(), TRUE);
}
catch (\Exception $e) {
\Drupal::logger('media_acquiadam')->error('Unable to register integration link for asset @uuid. Exception message: %message', [
'@uuid' => $data['assetUuid'],
'%message' => $e->getMessage(),
]);
return FALSE;
}
return $response;
}
/**
* Get all the integration links which have been registered on Acquia DAM.
*
* @return array
* All the integration links which are registered on Acquia DAM.
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function getIntegrationLinks(): array {
$this->checkAuth();
$response = $this->client->request(
'GET',
'https://' . $this->config->get('domain') . '/api/rest/integrationlink',
[
'headers' => $this->getDefaultHeaders(),
]
);
$response = json_decode((string) $response->getBody(), TRUE);
return $response->integrationLinks;
}
/**
* Get a specific integration link by its uuid.
*
* @param string $uuid
* The uuid of the integration link to fetch.
*
* @return mixed
* The integration link if found, NULL otherwise.
*/
public function getIntegrationLink(string $uuid) {
foreach ($this->getIntegrationLinks() as $link) {
if ($link->uuid === $uuid) {
return $link;
}
}
return NULL;
}
/**
* Get all the integration links which have been registered for an asset.
*
* @param string $asset_uuid
* The uuid of the asset to check.
*
* @return array
* The integration links of the asset.
*/
public function getAssetIntegrationLinks(string $asset_uuid): array {
$links = [];
foreach ($this->getIntegrationLinks() as $link) {
if ($link->assetUuid === $asset_uuid) {
$links[] = $link;
}
}
return $links;
}
}
