Skip to content
Snippets Groups Projects
DataResolveService.php 8.68 KiB
Newer Older
<?php

namespace SGalinski\SgRest\Service;

/***************************************************************
 *  Copyright notice
 *
 *  (c) sgalinski Internet Services (http://www.sgalinski.de)
 *
 *  All rights reserved
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
 *  free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
 *
 *  This script is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/

use SGalinski\SgRest\Utility\PathUtility;
use TYPO3\CMS\Core\Resource\AbstractFile;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter;

/**
 * The DataResolverService converts the data representations of the REST requests. For instance from objects to array
 * or the way around.
 */
class DataResolveService implements SingletonInterface {
	/**
	 * @inject
	 * @var \TYPO3\CMS\Extbase\Object\ObjectManager
	 */
	protected $objectManager;

	/**
	 * @inject
	 * @var \SGalinski\SgRest\Service\RegistrationService
	 */
	protected $registrationService;

	/**
	 * @var bool
	 */
	protected $resolveResourceLinks = FALSE;

	/**
	 * @var int
	 */
	protected $resolveNestingLevel = 1;

	/**
	 * @var string
	 */
	protected $apiKey = '';

	/**
	 * @var string
	 */
	protected $dateFormat = DateTimeConverter::DEFAULT_DATE_FORMAT;

	/**
	 * @var array
	 */
	protected $objectCache = [];

	/**
	 * Setter for the nesting level property.
	 *
	 * @param int $resolveNestingLevel
	 * @return void
	 */
	public function setResolveNestingLevel($resolveNestingLevel) {
		$this->resolveNestingLevel = $resolveNestingLevel;
	}

	/**
	 * Setter for the resolve resource links flag.
	 *
	 * @param boolean $resolveResourceLinks
	 * @return void
	 */
	public function setResolveResourceLinks($resolveResourceLinks) {
		$this->resolveResourceLinks = $resolveResourceLinks;
	}

	/**
	 * Setter for the apiKey.
	 *
	 * @param string $apiKey
	 * @return void
	 */
	public function setApiKey($apiKey) {
		$this->apiKey = $apiKey;
	}

	/**
	 * Setter for the date format.
	 *
	 * @param string $format
	 * @return void
	 */
	public function setDateFormat($format) {
		$this->dateFormat = $format;
	}

	/**
	 * Method converts a given object to an array.
	 *
	 * @param AbstractEntity $object
	 * @param bool $returnReference
	 * @param int $nestingLevel
	 * @return array
	 */
	public function getArrayFromObject(AbstractEntity $object, $returnReference = FALSE, $nestingLevel = -1) {
		$entityName = $this->getEntityName($object);
		$identifier = (method_exists($object, 'getUid') ? $object->getUid() : 0);

		$objectCacheKey = $entityName . '-' . $identifier . '-' . ($returnReference ? 1 : 0);
		if (isset($this->objectCache[$objectCacheKey])) {
			return $this->objectCache[$objectCacheKey];
		}

		$properties = $object->_getCleanProperties();

		if ($object instanceof FileReference && $identifier) {
			return [
				'href' => PathUtility::createFileUrl($object),
				'uid' => $identifier,
				'metadata' => $this->getFileMetaData($object)
		if ($returnReference && $identifier) {
			return [
				'href' => PathUtility::createRestEntityUrl($this->apiKey, $entityName, $identifier),
				'uid' => $identifier
			];
		}

		// Initialize nesting level
		if ($this->resolveResourceLinks && $nestingLevel === -1) {
			$nestingLevel = $this->resolveNestingLevel;
		}

		$allowedProperties = [];
		if ($this->apiKey !== '' && $this->registrationService->isApiKeyAvailable($this->apiKey)) {
			$accessGroups = $this->registrationService->getAccessGroups();
			$allowedProperties = $accessGroups[$this->apiKey]['entities'][$entityName]['read'];
		}

		$properties = array_intersect_key($properties, array_flip($allowedProperties));
		foreach ($properties as $propertyName => $propertyValues) {
			if (!is_object($propertyValues)) {
				continue;
			}

			if ($propertyValues instanceof \DateTime) {
				/** @var \DateTime $date */
				$date = $propertyValues;
				$properties[$propertyName] = $date->format($this->dateFormat);
			} else {
				$properties[$propertyName] = $this->convertSubPropertyToArray($object, $propertyName, $nestingLevel);
			}
		}

		$this->objectCache[$objectCacheKey] = $properties;
		return $properties;
	}

	/**
	 * Method returns an array with relations to the sub property as rest urls.
	 *
	 * @param AbstractEntity $parentObject
	 * @param string $subProperty
	 * @param int $nestingLevel
	 * @throws \Exception
	 * @return array
	 */
	protected function convertSubPropertyToArray(AbstractEntity $parentObject, $subProperty, $nestingLevel) {
		$result = [];

		$getMethod = 'get' . ucfirst($subProperty);
		if (!method_exists($parentObject, $getMethod)) {
			$message = 'The sub property ' . $subProperty . ' of the object you requested could not be found.';
			throw new \Exception($message, 404);
		}

		$subObject = $parentObject->{$getMethod}();
		$returnReference = ($this->resolveResourceLinks && $nestingLevel >= 1);

		if ($subObject instanceof LazyLoadingProxy) {
			$subObject->_loadRealInstance();
		}

		if ($subObject instanceof LazyObjectStorage) {
			$subObject->toArray();
		}

		if ($subObject instanceof ObjectStorage || $subObject instanceof \Traversable) {
			// toArray() is necessary, because an ObjectStorage is losing it's index, if the child object references to
			// the same storage. So it can happen, that not all objects are iterated.
			foreach ($subObject->toArray() as $item) {
				$result[] = $this->getArrayFromObject($item, !$returnReference, $nestingLevel);
			}
		} else {
			$entityName = (($subObject instanceof AbstractEntity) ? $this->getEntityName($subObject) : '');

			if (!$this->registrationService->isEntityAvailable($entityName, $this->apiKey)) {
				$message = 'You are not allowed to access the sub property: ' . $subProperty;
				throw new \Exception($message, 403);
			}

			if ($subObject instanceof AbstractEntity && $returnReference) {
				$nestingLevel = ($nestingLevel > 0 ? $nestingLevel - 1 : 0);
				$result = $this->getArrayFromObject($subObject, FALSE, $nestingLevel);
			}

			$identifier = (method_exists($subObject, 'getUid') ? $subObject->getUid() : 0);
			if ($subObject instanceof FileReference && $identifier) {
				$result['href'] = PathUtility::createFileUrl($subObject);
			} else {
				$result['href'] = PathUtility::createRestEntityUrl($this->apiKey, $entityName, $identifier);
			}

			$result['uid'] = $identifier;
		}

		return $result;
	}

	/**
	 * Returns lowercase representation of the entity name.
	 *
	 * @param AbstractEntity $object
	 * @return string
	 */
	public function getEntityName(AbstractEntity $object) {
		return $this->getEntityNameByClassName(get_class($object));
	}

	/**
	 * Returns lowercase representation of the entity name.
	 *
	 * @param string $className
	 * @return string
	 */
	public function getEntityNameByClassName($className) {
		$classPath = GeneralUtility::trimExplode('\\', $className);

		return lcfirst(array_pop($classPath));
	}

	/**
	 * Returns an array with meta data of the file.
	 * - fileSize
	 * - mime type
	 * - name
	 *
	 * And for images also:
	 * - height
	 * - width
	 * - aspect ratio
	 *
	 * @param FileReference $fileReference
	 * @return array
	 */
	protected function getFileMetaData(FileReference $fileReference) {
		$originalResource = $fileReference->getOriginalResource();
		if (!$originalResource) {
			return [];
		}

		/** @var File $originalFile */
		$originalFile = $originalResource->getOriginalFile();
		if (!$originalFile) {
			return [];
		}

		$metadata = [
			'fileSize' => $originalFile->getSize(),
			'mimeType' => $originalFile->getMimeType(),
			'name' => $originalFile->getName(),
		];

		if ($originalFile->getType() === AbstractFile::FILETYPE_IMAGE) {
			$height = $originalFile->getProperty('height');
			$width = $originalFile->getProperty('width');

			$metadata['height'] = $height;
			$metadata['width'] = $width;
			$metadata['aspectRatio'] = $height / $width;
		}

		return $metadata;
	}