From 54d6f826f5353ef0eb765978d42ee81c33d6257b Mon Sep 17 00:00:00 2001 From: Stefano Kowalke Date: Mon, 2 Jun 2014 19:15:54 +0200 Subject: [PATCH 1/4] Add support for TYPO3 6.2 and drop support for all other version below * Introduce namespaces * Adjust doc comments * Remove closing php tag * Fix typo * Remove the legacy Dispatcher interface close #18 close #47 --- Classes/Cli/Dispatcher.php | 649 ------------------ Classes/Command/CacheApiCommandController.php | 42 +- .../Command/DatabaseApiCommandController.php | 28 +- .../Command/ExtensionApiCommandController.php | 110 +-- Classes/Command/SiteApiCommandController.php | 39 +- Classes/Service/CacheApiService.php | 39 +- Classes/Service/DatabaseApiService.php | 56 +- Classes/Service/ExtensionApiService.php | 200 +++--- Classes/Service/SiteApiService.php | 39 +- Scripts/Cli.php | 10 - ext_autoload.php | 21 +- ext_emconf.php | 18 +- ext_localconf.php | 12 +- 13 files changed, 332 insertions(+), 931 deletions(-) delete mode 100644 Classes/Cli/Dispatcher.php delete mode 100644 Scripts/Cli.php diff --git a/Classes/Cli/Dispatcher.php b/Classes/Cli/Dispatcher.php deleted file mode 100644 index 2560c30..0000000 --- a/Classes/Cli/Dispatcher.php +++ /dev/null @@ -1,649 +0,0 @@ - - * @package TYPO3 - * @subpackage tx_coreapi - */ -class Tx_Coreapi_Cli_Dispatcher extends t3lib_cli { - - var $cli_help = array( - 'name' => 'coreapi', - 'synopsis' => './cli_dispatch.phpsh coreapi service:command [options] arguments', - 'description' => ' -Coreapi provides a set of services/commands for doing the most common admin task in TYPO3 by CLI instead of doing it in the backend/browser. -Currently the following commands are supported: - -###COMMANDS### -', - 'examples' => ' -./cli_dispatch.phpsh coreapi site:info -./cli_dispatch.phpsh coreapi cache:clearallcaches -./cli_dispatch.phpsh coreapi extension:info rtehtmlarea -./cli_dispatch.phpsh coreapi site:createsysnews "title" "text"', - 'options' => 'use "./cli_dispatch.phpsh coreapi help service:command" to get help on the arguments and options of a specific command - ', - 'license' => 'GNU GPL - free software!', - 'author' => 'Tobias Liebig', - ); - - const MAXIMUM_LINE_LENGTH = 79; - - var $commandMethodPattern = '/([a-z][a-z0-9]*)([A-Z][a-zA-Z0-9]*)Command/'; - - /** - * @var string service - */ - protected $service = ''; - - /** - * @var string command - */ - protected $command = ''; - - /** - * Constructor with basic checks - */ - public function __construct() { - parent::__construct(); - - if (!isset($this->cli_args['_DEFAULT'][1]) || $this->cli_args['_DEFAULT'][1] === 'help') { - $this->cli_help(); - die(); - } - - $split = explode(':', $this->cli_args['_DEFAULT'][1]); - if (count($split) === 1) { - $this->error('CLI calls need to be like coreapi cache:clearallcaches'); - } elseif (count($split) !== 2) { - $this->error('Only one : is allowed in first argument'); - } - - $this->service = strtolower($split[0]); - $this->command = strtolower($split[1]); - } - - /** - * Starts the script - * @return void - */ - public function start() { - try { - $command = $this->service . ucfirst($this->command) . 'Command'; - $this->runCommand($command); - - } catch (Exception $e) { - $errorMessage = sprintf('ERROR: Error in service "%s" and command "%s"": %s!', $this->service, $this->command, $e->getMessage()); - $this->outputLine($errorMessage); - } - } - - /** - * @param $command - * @throws InvalidArgumentException - */ - protected function runCommand($command) { - if (method_exists($this, $command)) { - $args = array_slice($this->cli_args['_DEFAULT'], 2); - $method = new ReflectionMethod(get_class($this), $command); - - //check number of required arguments - if ($method->getNumberOfRequiredParameters() !== count($args)) { - throw new InvalidArgumentException('Wrong number of arguments'); - } - - foreach ($method->getParameters() as $param) { - if ($param->isOptional()) { - $name = $param->getName(); - if ($this->cli_isArg('--' . $name)) { - $args[] = $this->cli_argValue('--' . $name); - } else { - $args[] = $param->getDefaultValue(); - } - } - } - //invoke command with given args and options - $method->invokeArgs($this, $args); - } else { - throw new InvalidArgumentException('Service does not exist or command not supported'); - } - } - - /** - * Clear all caches - * - * Clears all TYPO3 caches - * - * @return void - * @example ./cli_dispatch.phpsh coreapi cache:clearallcaches - */ - public function cacheClearallcachesCommand() { - $cacheApiService = $this->getCacheApiService(); - $cacheApiService->clearAllCaches(); - $this->outputLine('All caches cleared'); - } - - - /** - * Clear configuration cache (temp_CACHED_..) - * - * Deletes the temp_CACHED_* files in /typo3conf - * - * @return void - * @example ./cli_dispatch.phpsh coreapi cache:clearconfigurationcache - */ - public function cacheClearconfigurationcacheCommand() { - $cacheApiService = $this->getCacheApiService(); - $cacheApiService->clearConfigurationCache(); - $this->outputLine('Configuration cache cleared'); - } - - /** - * Clear page cache - * - * Clears the page cache in TYPO3 - * - * @return void - * @example ./cli_dispatch.phpsh coreapi cache:clearpagecache - */ - public function cacheClearpagecacheCommand() { - $cacheApiService = $this->getCacheApiService(); - $cacheApiService->clearPageCache(); - $this->outputLine('Page cache cleared'); - } - - /** - * Clear all caches except the page cache. - * This is especially useful on big sites when you can't just drop the page cache - * - * @example ./cli_dispatch.phpsh coreapi cache:clearallexceptpagecache - * @return void - */ - public function cacheClearallexceptpagecacheCommand() { - $cacheApiService = $this->getCacheApiService(); - $clearedCaches = $cacheApiService->clearAllExceptPageCache(); - - $this->outputLine('Cleared caches: ' . implode(', ', $clearedCaches)); - } - - /** - * Database compare - * - * Leave the argument 'actions' empty or use "help" to see the available ones - * - * @param string $actions List of actions which will be executed - * @return void - * @example ./cli_dispatch.phpsh coreapi database:databasecompare 2 - */ - public function databaseDatabasecompareCommand($actions) { - $databaseApiService = $this->getDatabaseApiService(); - if ($actions === 'help') { - $actions = $databaseApiService->databaseCompareAvailableActions(); - $this->outputTable($actions); - } else { - $databaseApiService->databaseCompare($actions); - } - } - - /** - * Information about an extension - * - * Echo's out a table with information about a specific extension - * - * @param string $extkey extension key - * @return void - * @example ./cli_dispatch.phpsh coreapi extension:info rtehtmlarea - */ - public function extensionInfoCommand($extkey) { - $extensionApiService = $this->getExtensionApiService(); - $data = $extensionApiService->getExtensionInformation($extkey); - $this->outputLine(''); - $this->outputLine('EXTENSION "%s": %s %s', array(strtoupper($extkey), $data['em_conf']['version'], $data['em_conf']['state'])); - $this->outputLine(str_repeat('-', self::MAXIMUM_LINE_LENGTH)); - - $outputInformation = array(); - $outputInformation['is installed'] = ($data['is_installed'] ? 'yes' : 'no'); - foreach ($data['em_conf'] as $emConfKey => $emConfValue) { - // Skip empty properties - if (empty($emConfValue)) { - continue; - } - // Skip properties which are already handled - if ($emConfKey === 'title' || $emConfKey === 'version' || $emConfKey === 'state') { - continue; - } - $outputInformation[$emConfKey] = $emConfValue; - } - - foreach ($outputInformation as $outputKey => $outputValue) { - $description = ''; - if (is_array($outputValue)) { - foreach ($outputValue as $additionalKey => $additionalValue) { - if (is_array($additionalValue)) { - - if (empty($additionalValue)) { - continue; - } - $description .= LF . str_repeat(' ', 28) . $additionalKey; - $description .= LF; - foreach ($additionalValue as $ak => $av) { - $description .= str_repeat(' ', 30) . $ak . ': ' . $av . LF; - } - } else { - $description .= LF . str_repeat(' ', 28) . $additionalKey . ': ' . $additionalValue; - } - } - } else { - $description = wordwrap($outputValue, self::MAXIMUM_LINE_LENGTH - 28, PHP_EOL . str_repeat(' ', 28), TRUE); - } - $this->outputLine('%-2s%-25s %s', array(' ', $outputKey, $description)); - } - } - - /** - * Update list - * - * Update the list of available extensions in the TER - * - * @return void - * @example ./cli_dispatch.phpsh coreapi extension:updatelist - */ - public function extensionUpdatelistCommand() { - $extensionApiService = $this->getExtensionApiService(); - $extensionApiService->updateMirrors(); - $this->outputLine('Extension list has been updated.'); - } - - /** - * List all installed (loaded) extensions - * - * @param string $type Extension type, can either be L for local, S for system or G for global. Leave it empty for all - * @return void - * @example ./cli_dispatch.phpsh coreapi extension:listinstalled --type=S - */ - public function extensionListinstalledCommand($type = '') { - $extensionApiService = $this->getExtensionApiService(); - $extensions = $extensionApiService->getInstalledExtensions($type); - $out = array(); - - foreach ($extensions as $key => $details) { - $title = $key . ' - ' . $details['version'] . '/' . $details['state']; - $out[$title] = $details['title']; - } - $this->outputTable($out); - } - - /** - * Install(activate) an extension - * - * @param string $key extension key - * @return void - */ - public function extensionInstallCommand($key) { - $extensionApiService = $this->getExtensionApiService(); - $data = $extensionApiService->installExtension($key); - $this->outputLine(sprintf('Extension "%s" is now installed!', $key)); - } - - /** - * UnInstall(deactivate) an extension - * - * @param string $key extension key - * @return void - */ - public function extensionUninstallCommand($key) { - $extensionApiService = $this->getExtensionApiService(); - $data = $extensionApiService->uninstallExtension($key); - $this->outputLine(sprintf('Extension "%s" is now uninstalled!', $key)); - } - - /** - * Configure an extension - * - * This command enables you to configure an extension. - * - * examples: - * - * [1] Using a standard formatted ini-file - * ./cli_dispatch.phpsh coreapi extension:configure rtehtmlarea --configfile=C:\rteconf.txt - * - * [2] Adding configuration settings directly on the command line - * ./cli_dispatch.phpsh coreapi extension:configure rtehtmlarea --settings="enableImages=1;allowStyleAttribute=0" - * - * [3] A combination of [1] and [2] - * ./cli_dispatch.phpsh extbase extension:configure rtehtmlarea --configfile=C:\rteconf.txt --settings="enableImages=1;allowStyleAttribute=0" - * - * @param string $key extension key - * @param string $configfile path to file containing configuration settings. Must be formatted as a standard ini-file - * @param string $settings string containing configuration settings separated on the form "k1=v1;k2=v2;" - * @return void - * @throws InvalidArgumentException - */ - public function extensionConfigureCommand($key, $configfile = '', $settings = '') { - global $TYPO3_CONF_VARS; - $extensionApiService = $this->getExtensionApiService(); - $conf = array(); - - if (is_file($configfile)) { - $conf = parse_ini_file($configfile); - } - - if (strlen($settings)) { - $arr = explode(';', $settings); - foreach ($arr as $v) { - if (strpos($v, '=') === FALSE) { - throw new InvalidArgumentException(sprintf('Ill-formed setting "%s"!', $v)); - } - $parts = t3lib_div::trimExplode('=', $v, FALSE, 2); - - if (!empty($parts[0])) { - $conf[$parts[0]] = $parts[1]; - } - } - } - - if (empty($conf)) { - throw new InvalidArgumentException(sprintf('No configuration settings!', $key)); - } - - $extensionApiService->configureExtension($key, $conf); - $this->outputLine(sprintf('Extension "%s" has been configured!', $key)); - - } - - - /** - * Fetch an extension from TER - * - * @param string $key extension key - * @param string $version the exact version of the extension, otherwise the latest will be picked - * @param string $location where to put the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext - * @param string|bool $overwrite overwrite the extension if it already exists - * @param string $mirror mirror to fetch the extension from, otherwise a random mirror will be selected - * @return void - */ - public function extensionFetchCommand($key, $version = '', $location = 'L', $overwrite = FALSE, $mirror = '') { - $extensionApiService = $this->getExtensionApiService(); - $data = $extensionApiService->fetchExtension($key, $version, $location, $overwrite, $mirror); - $this->outputLine(sprintf('Extension "%s" version %s has been fetched from repository!', $data['extKey'], $data['version'])); - } - - - /** - * Import extension from file - * - * @param string $file path to t3x file - * @param string $location where to import the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext - * @param boolean $overwrite overwrite the extension if it already exists - * @return void - */ - public function extensionImportCommand($file, $location = 'L', $overwrite = FALSE) { - $extensionApiService = $this->getExtensionApiService(); - $data = $extensionApiService->importExtension($file, $location, $overwrite); - $this->outputLine(sprintf('Extension "%s" has been imported!', $data['extKey'])); - } - - /** - * Ensure upload folders of installed extensions exist - * @return void - */ - public function extensionCreateuploadfoldersCommand() { - $extensionApiService = $this->getExtensionApiService(); - $messages = $extensionApiService->createUploadFolders(); - if (sizeof($messages)) { - foreach ($messages as $message) { - $this->outputLine($message); - } - } else { - $this->outputLine('no uploadFolder created'); - } - } - - /** - * Site info - * - * Basic information about the system - * - * @return void - * @example ./cli_dispatch.phpsh coreapi site:info - */ - public function siteInfoCommand() { - $siteApiService = $this->getSiteApiService(); - $infos = $siteApiService->getSiteInfo(); - $this->outputTable($infos); - } - - /** - * Create a sys news - * - * Sys news record is displayed at the login page - * - * @param string $header Header text - * @param string $text Basic text - * @return void - * @example ./cli_dispatch.phpsh coreapi site:createsysnews "The header" "The news text" - */ - public function siteCreatesysnewsCommand($header, $text) { - $siteApiService = $this->getSiteApiService(); - $siteApiService->createSysNews($header, $text); - } - - /** - * Output a single line - * - * @param string $text text - * @param array $arguments optional arguments - * @return void - */ - protected function outputLine($text, array $arguments = array()) { - if ($arguments !== array()) { - $text = vsprintf($text, $arguments); - } - $this->cli_echo($text . PHP_EOL); - } - - /** - * Output a whole table, maximum 2 cols - * - * @param array $input input table - * @return void - */ - protected function outputTable(array $input) { - $this->outputLine(str_repeat('-', self::MAXIMUM_LINE_LENGTH)); - foreach ($input as $key => $value) { - $line = wordwrap($value, self::MAXIMUM_LINE_LENGTH - 43, PHP_EOL . str_repeat(' ', 43), TRUE); - $this->outputLine('%-2s%-40s %s', array(' ', $key, $line)); - } - $this->outputLine(str_repeat('-', self::MAXIMUM_LINE_LENGTH)); - } - - /** - * End call - * - * @param string $message Error message - * @return void - */ - protected function error($message) { - $this->cli_echo('ERROR: ' . $message, FALSE); - die(); - } - - /** - * Display help - * Overridden from parent - * - * @return void - */ - public function cli_help() { - if (isset($this->cli_args['_DEFAULT'][1]) && - $this->cli_args['_DEFAULT'][1] === 'help' && - isset($this->cli_args['_DEFAULT'][2]) && - strpos($this->cli_args['_DEFAULT'][2], ':') !== FALSE - ) { - $this->setHelpFromDocComment($this->cli_args['_DEFAULT'][2]); - } else { - $class = new ReflectionClass(get_class($this)); - $commands = array(); - foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { - if (preg_match($this->commandMethodPattern, $method->getName(), $matches)) { - $commands[] = strtolower($matches[1]) . ':' . strtolower($matches[2]); - } - } - $this->cli_help['description'] = str_replace('###COMMANDS###', implode(PHP_EOL, $commands), $this->cli_help['description']); - } - parent::cli_help(); - } - - /** - * Extract help texts from doc comments - * - * @param $operation - */ - protected function setHelpFromDocComment($operation) { - list($service, $command) = explode(':', $operation, 2); - $commandMethod = strtolower($service) . ucfirst($command) . 'Command'; - if (method_exists($this, $commandMethod)) { - $this->cli_help['options'] = ''; - //extract doc comment - $ref = new ReflectionMethod(get_class($this), $commandMethod); - $comment = $ref->getDocComment(); - $comment = preg_replace('/\/\*\*\s*(.*)\s*\*\//s', '$1', $comment); - $lines = explode(PHP_EOL, $comment); - - //get name - $name = preg_replace('/^\s*\*\s*(.*)\s*$/i', '$1', array_shift($lines)); - $this->cli_help['name'] = $name; - - $description = array(); - $examples = array(); - $params = array(); - foreach ($lines as $n => $l) { - if (!preg_match('/^\s*\*\s*@/i', $l)) { - //add to description - $description[] = preg_replace('/^\s*\*\s*(.*)\s*$/i', '$1', $l); - continue; - } - - //params - if (preg_match('/^\s*\*\s*@param\s*(?P[a-z0-9_]*)\s+\$(?P[a-z0-9_]*)\s+(?P.*)/i', $l, $matches)) { - $params[$matches['name']] = array( - 'type' => $matches['name'], - 'description' => $matches['description'] - ); - continue; - } - - // examples - if (preg_match('/^\s*\*\s*@example\s+(?P.*)/i', $l, $matches)) { - $examples[] = $matches['text']; - } - } - - $this->cli_help['description'] = trim(implode(PHP_EOL, $description)); - if (!empty($examples)) { - $this->cli_help['examples'] = implode(PHP_EOL, $examples); - } else { - unset($this->cli_help['examples']); - } - - //get params - $parameters = $ref->getParameters(); - $args = array(); - foreach ($parameters as $param) { - $name = $param->getName(); - $description = isset($params[$name]) ? $params[$name]['description'] : ''; - if ($param->isOptional()) { - $this->cli_options[] = array('--' . $name, $description); - } else { - $args[strtoupper($name)] = $description; - } - } - - //compile arguments section - if (!empty($args)) { - $maxLen = 0; - foreach (array_keys($args) as $argname) { - if (strlen($argname) > $maxLen) { - $maxLen = strlen($argname); - } - } - - $tmp = array(); - foreach ($args as $argname => $description) { - $tmp[] = $argname . substr($this->cli_indent(rtrim($description), $maxLen + 4), strlen($argname)); - } - - $offset = array_search('options', array_keys($this->cli_help)); - $this->cli_help = array_slice($this->cli_help, 0, $offset, true) + - array('arguments' => LF . implode(LF, $tmp)) + - array_slice($this->cli_help, $offset, NULL, true); - } - - //set synopsis for this - $this->cli_help['synopsis'] = './cli_dispatch.phpsh coreapi ' . $operation . ' ###OPTIONS### ' . implode(' ', array_keys($args)); - - } else { - $this->error(sprintf('No help available for "%s"', $operation) . PHP_EOL); - } - } - - /** - * @return Tx_Coreapi_Service_CacheApiService - */ - protected function getCacheApiService() { - $cacheApiService = t3lib_div::makeInstance('Tx_Coreapi_Service_CacheApiService'); - $cacheApiService->initializeObject(); - return $cacheApiService; - } - - /** - * @return Tx_Coreapi_Service_DatabaseApiService - */ - protected function getDatabaseApiService() { - $databaseApiService = t3lib_div::makeInstance('Tx_Coreapi_Service_DatabaseApiService'); - return $databaseApiService; - } - - /** - * @return Tx_Coreapi_Service_ExtensionApiService - */ - protected function getExtensionApiService() { - $extensionApiService = t3lib_div::makeInstance('Tx_Coreapi_Service_ExtensionApiService'); - return $extensionApiService; - } - - /** - * @return Tx_Coreapi_Service_SiteApiService - */ - protected function getSiteApiService() { - $siteApiService = t3lib_div::makeInstance('Tx_Coreapi_Service_SiteApiService'); - return $siteApiService; - } -} - -?> \ No newline at end of file diff --git a/Classes/Command/CacheApiCommandController.php b/Classes/Command/CacheApiCommandController.php index 5b0071c..2954fb3 100644 --- a/Classes/Command/CacheApiCommandController.php +++ b/Classes/Command/CacheApiCommandController.php @@ -1,8 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -21,49 +24,48 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use TYPO3\CMS\Extbase\Mvc\Controller\CommandController; /** * API Command Controller * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Command_CacheApiCommandController extends Tx_Extbase_MVC_Controller_CommandController { +class CacheApiCommandController extends CommandController { /** - * Clear all caches + * Clear all caches. * * @return void */ public function clearAllCachesCommand() { - /** @var $service Tx_Coreapi_Service_CacheApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_CacheApiService'); + $service = $this->getService(); $service->clearAllCaches(); $this->outputLine('All caches have been cleared.'); } /** - * Clear configuration cache (temp_CACHED_..) + * Clear configuration cache (temp_CACHED_..). * * @return void */ public function clearConfigurationCacheCommand() { - /** @var $service Tx_Coreapi_Service_CacheApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_CacheApiService'); + $service = $this->getService(); $service->clearConfigurationCache(); $this->outputLine('Configuration cache has been cleared.'); } /** - * Clear page cache + * Clear page cache. * * @return void */ public function clearPageCacheCommand() { - /** @var $service Tx_Coreapi_Service_CacheApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_CacheApiService'); + $service = $this->getService(); $service->clearPageCache(); $this->outputLine('Page cache has been cleared.'); @@ -71,17 +73,23 @@ public function clearPageCacheCommand() { /** * Clear all caches except the page cache. - * This is especially useful on big sites when you can't just drop the page cache + * This is especially useful on big sites when you can't just drop the page cache. * * @return void */ public function clearAllExceptPageCacheCommand() { - /** @var $service Tx_Coreapi_Service_CacheApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_CacheApiService'); + $service = $this->getService(); $clearedCaches = $service->clearAllExceptPageCache(); $this->outputLine('Cleared caches: ' . implode(', ', $clearedCaches)); } -} -?> \ No newline at end of file + /** + * Returns the service object. + * + * @return \Etobi\CoreAPI\Service\CacheApiService object + */ + private function getService() { + return $this->objectManager->get('Etobi\\CoreAPI\\Service\\CacheApiService'); + } +} \ No newline at end of file diff --git a/Classes/Command/DatabaseApiCommandController.php b/Classes/Command/DatabaseApiCommandController.php index 8bbbe3e..6777cb5 100644 --- a/Classes/Command/DatabaseApiCommandController.php +++ b/Classes/Command/DatabaseApiCommandController.php @@ -1,8 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -21,25 +24,25 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use TYPO3\CMS\Extbase\Mvc\Controller\CommandController; /** * API Command Controller * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Command_DatabaseApiCommandController extends Tx_Extbase_MVC_Controller_CommandController { +class DatabaseApiCommandController extends CommandController { /** - * Database compare - * + * Database compare. * Leave the argument 'actions' empty or use "help" to see the available ones * * @param string $actions List of actions which will be executed */ public function databaseCompareCommand($actions = '') { - /** @var $service Tx_Coreapi_Service_DatabaseApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_DatabaseApiService'); + $service = $this->getService(); if ($actions === 'help' || strlen($actions) === 0) { $actions = $service->databaseCompareAvailableActions(); @@ -57,6 +60,13 @@ public function databaseCompareCommand($actions = '') { $this->quit(); } } -} -?> \ No newline at end of file + /** + * Returns the service object. + * + * @return \Etobi\CoreAPI\Service\DatabaseApiService object + */ + private function getService() { + return $this->objectManager->get('Etobi\\CoreAPI\\Service\\DatabaseApiService'); + } +} \ No newline at end of file diff --git a/Classes/Command/ExtensionApiCommandController.php b/Classes/Command/ExtensionApiCommandController.php index 31f8f6a..8299f4a 100644 --- a/Classes/Command/ExtensionApiCommandController.php +++ b/Classes/Command/ExtensionApiCommandController.php @@ -1,9 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -22,26 +24,31 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use Exception; +use InvalidArgumentException; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Mvc\Controller\CommandController; /** * Extension API Command Controller * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Command_ExtensionApiCommandController extends Tx_Extbase_MVC_Controller_CommandController { +class ExtensionApiCommandController extends CommandController { /** - * Information about an extension + * Information about an extension. * * @param string $key extension key + * * @return void */ public function infoCommand($key) { $data = array(); try { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); + $service = $this->getService(); $data = $service->getExtensionInformation($key); } catch (Exception $e) { $this->outputLine($e->getMessage()); @@ -92,9 +99,10 @@ public function infoCommand($key) { } /** - * List all installed extensions + * List all installed extensions. * * @param string $type Extension type, can either be L for local, S for system or G for global. Leave it empty for all + * * @return void */ public function listInstalledCommand($type = '') { @@ -104,8 +112,7 @@ public function listInstalledCommand($type = '') { $this->quit(); } - /** @var $extensions Tx_Coreapi_Service_ExtensionApiService */ - $extensions = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService')->getInstalledExtensions($type); + $extensions = $this->getService()->getInstalledExtensions($type); foreach ($extensions as $key => $details) { $title = $key . ' - ' . $details['version'] . '/' . $details['state']; @@ -119,29 +126,28 @@ public function listInstalledCommand($type = '') { } /** - * Update list + * Update list. * * @return void */ public function updateListCommand() { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); + $service = $this->getService(); $service->updateMirrors(); $this->outputLine('Extension list has been updated.'); } /** - * Install(activate) an extension + * Install(activate) an extension. * * @param string $key extension key + * * @return void */ public function installCommand($key) { try { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); - $data = $service->installExtension($key); + $service = $this->getService(); + $service->installExtension($key); } catch (Exception $e) { $this->outputLine($e->getMessage()); $this->quit(); @@ -150,16 +156,16 @@ public function installCommand($key) { } /** - * UnInstall(deactivate) an extension + * UnInstall(deactivate) an extension. * * @param string $key extension key + * * @return void */ public function uninstallCommand($key) { try { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); - $data = $service->uninstallExtension($key); + $service = $this->getService(); + $service->uninstallExtension($key); } catch (Exception $e) { $this->outputLine($e->getMessage()); $this->quit(); @@ -168,12 +174,10 @@ public function uninstallCommand($key) { } /** - * Configure an extension - * + * Configure an extension. * This command enables you to configure an extension. * - * examples: - * + * * [1] Using a standard formatted ini-file * ./cli_dispatch.phpsh extbase extensionapi:configure rtehtmlarea --configfile=C:\rteconf.txt * @@ -182,17 +186,17 @@ public function uninstallCommand($key) { * * [3] A combination of [1] and [2] * ./cli_dispatch.phpsh extbase extensionapi:configure rtehtmlarea --configfile=C:\rteconf.txt --settings="enableImages=1;allowStyleAttribute=0" + * * - * @param string $key extension key + * @param string $key extension key * @param string $configfile path to file containing configuration settings. Must be formatted as a standard ini-file - * @param string $settings string containing configuration settings separated on the form "k1=v1;k2=v2;" + * @param string $settings string containing configuration settings separated on the form "k1=v1;k2=v2;" + * * @return void */ public function configureCommand($key, $configfile = '', $settings = '') { - global $TYPO3_CONF_VARS; try { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); + $service = $this->getService(); $conf = array(); if (is_file($configfile)) { $conf = parse_ini_file($configfile); @@ -204,7 +208,7 @@ public function configureCommand($key, $configfile = '', $settings = '') { if (strpos($v, '=') === FALSE) { throw new InvalidArgumentException(sprintf('Ill-formed setting "%s"!', $v)); } - $parts = t3lib_div::trimExplode('=', $v, FALSE, 2); + $parts = GeneralUtility::trimExplode('=', $v, FALSE, 2); if (!empty($parts[0])) { $conf[$parts[0]] = $parts[1]; } @@ -214,7 +218,8 @@ public function configureCommand($key, $configfile = '', $settings = '') { if (empty($conf)) { throw new InvalidArgumentException(sprintf('No configuration settings!', $key)); } - $data = $service->configureExtension($key, $conf); + + $service->configureExtension($key, $conf); } catch (Exception $e) { $this->outputLine($e->getMessage()); @@ -224,19 +229,19 @@ public function configureCommand($key, $configfile = '', $settings = '') { } /** - * Fetch an extension from TER + * Fetch an extension from TER. + * + * @param string $key extension key + * @param string $version the exact version of the extension, otherwise the latest will be picked + * @param string $location where to put the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext + * @param bool $overwrite overwrite the extension if it already exists + * @param string $mirror mirror to fetch the extension from, otherwise a random mirror will be selected * - * @param string $key extension key - * @param string $version the exact version of the extension, otherwise the latest will be picked - * @param string $location where to put the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext - * @param bool $overwrite overwrite the extension if it already exists - * @param string $mirror mirror to fetch the extension from, otherwise a random mirror will be selected * @return void */ public function fetchCommand($key, $version = '', $location = 'L', $overwrite = FALSE, $mirror = '') { try { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); + $service = $this->getService(); $data = $service->fetchExtension($key, $version, $location, $overwrite, $mirror); $this->outputLine(sprintf('Extension "%s" version %s has been fetched from repository!', $data['extKey'], $data['version'])); } catch (Exception $e) { @@ -246,20 +251,19 @@ public function fetchCommand($key, $version = '', $location = 'L', $overwrite = } /** - * Import extension from file + * Import extension from file. * - * @param string $file path to t3x file - * @param string $location where to import the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext + * @param string $file path to t3x file + * @param string $location where to import the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext * @param boolean $overwrite overwrite the extension if it already exists + * * @return void */ public function importCommand($file, $location = 'L', $overwrite = FALSE) { try { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); + $service = $this->getService(); $data = $service->importExtension($file, $location, $overwrite); $this->outputLine(sprintf('Extension "%s" has been imported!', $data['extKey'])); - } catch (Exception $e) { $this->outputLine($e->getMessage()); $this->quit(); @@ -267,13 +271,12 @@ public function importCommand($file, $location = 'L', $overwrite = FALSE) { } /** - * createUploadFoldersCommand + * Creates the upload folders of an extension. * * @return void */ public function createUploadFoldersCommand() { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); + $service = $this->getService(); $messages = $service->createUploadFolders(); if (sizeof($messages)) { @@ -284,6 +287,13 @@ public function createUploadFoldersCommand() { $this->outputLine('no uploadFolder created'); } } -} -?> \ No newline at end of file + /** + * Returns the service object. + * + * @return \Etobi\CoreAPI\Service\ExtensionApiService object + */ + private function getService() { + return $this->objectManager->get('Etobi\\CoreAPI\\Service\\ExtensionApiService'); + } +} \ No newline at end of file diff --git a/Classes/Command/SiteApiCommandController.php b/Classes/Command/SiteApiCommandController.php index b965830..7f6f1c3 100644 --- a/Classes/Command/SiteApiCommandController.php +++ b/Classes/Command/SiteApiCommandController.php @@ -1,8 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -21,25 +24,24 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use TYPO3\CMS\Extbase\Mvc\Controller\CommandController; /** * Site API Command Controller * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Command_SiteApiCommandController extends Tx_Extbase_MVC_Controller_CommandController { +class SiteApiCommandController extends CommandController { /** - * Site info - * - * Basic information about the system + * Basic information about the system. * * @return void */ public function infoCommand() { - /** @var $service Tx_Coreapi_Service_SiteApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_SiteApiService'); + $service = $this->getService(); $data = $service->getSiteInfo(); foreach ($data as $key => $value) { @@ -49,19 +51,24 @@ public function infoCommand() { } /** - * Create a sys news - * - * Sys news record is displayed at the login page + * Sys news record is displayed at the login page. * * @param string $header Header text - * @param string $text Basic text + * @param string $text Basic text + * * @return void */ public function createSysNewsCommand($header, $text = '') { - /** @var $service Tx_Coreapi_Service_SiteApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_SiteApiService'); + $service = $this->getService(); $service->createSysNews($header, $text); } -} -?> \ No newline at end of file + /** + * Returns the service object. + * + * @return \Etobi\CoreAPI\Service\SiteApiService object + */ + private function getService() { + return $this->objectManager->get('Etobi\\CoreAPI\\Service\\SiteApiService'); + } +} \ No newline at end of file diff --git a/Classes/Service/CacheApiService.php b/Classes/Service/CacheApiService.php index 7d8b38b..f1f011e 100644 --- a/Classes/Service/CacheApiService.php +++ b/Classes/Service/CacheApiService.php @@ -1,8 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -21,66 +24,72 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Cache API service * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Service_CacheApiService { +class CacheApiService { /** - * @var t3lib_TCEmain + * @var \TYPO3\CMS\Core\DataHandling\DataHandler */ protected $tce; /** + * Initialize the object. * + * @return void */ public function initializeObject() { // Create a fake admin user - $adminUser = new t3lib_beUserAuth(); + $adminUser = new BackendUserAuthentication(); $adminUser->user['uid'] = $GLOBALS['BE_USER']->user['uid']; $adminUser->user['username'] = '_CLI_lowlevel'; $adminUser->user['admin'] = 1; $adminUser->workspace = 0; - $this->tce = t3lib_div::makeInstance('t3lib_TCEmain'); + $this->tce = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\DataHandling\\DataHandler'); $this->tce->start(Array(), Array()); $this->tce->start(Array(), Array(), $adminUser); } /** - * Clear all caches + * Clear all caches. + * + * @return void */ public function clearAllCaches() { - if (version_compare(TYPO3_version, '6.0.0', '<')) { - t3lib_extMgm::removeCacheFiles(); - } $this->tce->clear_cacheCmd('all'); } /** + * Clear the page cache. * + * @return void */ public function clearPageCache() { $this->tce->clear_cacheCmd('pages'); } /** + * Clears the configuration cache. * + * @return void */ public function clearConfigurationCache() { - if (version_compare(TYPO3_version, '6.0.0', '<')) { - t3lib_extMgm::removeCacheFiles(); - } $this->tce->clear_cacheCmd('temp_cached'); } /** * Clear all caches except the page cache. - * This is especially useful on big sites when you can't just drop the page cache + * This is especially useful on big sites when you can't just drop the page cache. * * @return array with list of cleared caches */ @@ -104,5 +113,3 @@ public function clearAllExceptPageCache() { return $toBeFlushed; } } - -?> diff --git a/Classes/Service/DatabaseApiService.php b/Classes/Service/DatabaseApiService.php index cefe97d..d68cd7b 100644 --- a/Classes/Service/DatabaseApiService.php +++ b/Classes/Service/DatabaseApiService.php @@ -1,9 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -22,14 +24,18 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use InvalidArgumentException; +use TYPO3\CMS\Core\Cache\Cache; +use TYPO3\CMS\Core\Utility\GeneralUtility; /** * DB API service * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Service_DatabaseApiService { +class DatabaseApiService { const ACTION_UPDATE_CLEAR_TABLE = 1; const ACTION_UPDATE_ADD = 2; const ACTION_UPDATE_CHANGE = 3; @@ -40,41 +46,36 @@ class Tx_Coreapi_Service_DatabaseApiService { const ACTION_REMOVE_DROP_TABLE = 8; /** - * @var t3lib_install_Sql Instance of SQL handler + * @var \TYPO3\CMS\Install\Service\SqlSchemaMigrationService Instance of SQL handler */ protected $sqlHandler = NULL; /** - * Constructor function + * Constructor function. */ public function __construct() { - if (class_exists('TYPO3\\CMS\\Install\\Sql\\SchemaMigrator')) { - $this->sqlHandler = t3lib_div::makeInstance('TYPO3\\CMS\\Install\\Sql\\SchemaMigrator'); - } elseif (class_exists('t3lib_install_Sql')) { - $this->sqlHandler = t3lib_div::makeInstance('t3lib_install_Sql'); - } elseif (class_exists('t3lib_install')) { - $this->sqlHandler = t3lib_div::makeInstance('t3lib_install'); - } + $this->sqlHandler = GeneralUtility::makeInstance('TYPO3\\CMS\\Install\\Sql\\SchemaMigrator'); } /** - * Database compare + * Database compare. * * @param string $actions comma separated list of IDs - * @return array + * * @throws InvalidArgumentException + * @return array */ public function databaseCompare($actions) { $errors = array(); - $availableActions = array_flip(t3lib_div::makeInstance('Tx_Extbase_Reflection_ClassReflection', 'Tx_Coreapi_Service_DatabaseApiService')->getConstants()); + $availableActions = array_flip(GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', 'Etobi\\CoreAPI\\Service\\DatabaseApiService')->getConstants()); if (empty($actions)) { throw new InvalidArgumentException('No compare modes defined'); } $allowedActions = array(); - $actionSplit = t3lib_div::trimExplode(',', $actions); + $actionSplit = GeneralUtility::trimExplode(',', $actions); foreach ($actionSplit as $split) { if (!isset($availableActions[$split])) { throw new InvalidArgumentException(sprintf('Action "%s" is not available!', $split)); @@ -83,17 +84,17 @@ public function databaseCompare($actions) { } - $tblFileContent = t3lib_div::getUrl(PATH_t3lib . 'stddb/tables.sql'); + $tblFileContent = GeneralUtility::getUrl(PATH_t3lib . 'stddb/tables.sql'); foreach ($GLOBALS['TYPO3_LOADED_EXT'] as $loadedExtConf) { if (is_array($loadedExtConf) && $loadedExtConf['ext_tables.sql']) { - $extensionSqlContent = t3lib_div::getUrl($loadedExtConf['ext_tables.sql']); + $extensionSqlContent = GeneralUtility::getUrl($loadedExtConf['ext_tables.sql']); $tblFileContent .= LF . LF . LF . LF . $extensionSqlContent; } } - if (is_callable('t3lib_cache::getDatabaseTableDefinitions')) { - $tblFileContent .= t3lib_cache::getDatabaseTableDefinitions(); + if (is_callable('TYPO3\\CMS\\Core\\Cache\\Cache::getDatabaseTableDefinitions')) { + $tblFileContent .= Cache::getDatabaseTableDefinitions(); } if (class_exists('TYPO3\\CMS\\Core\\Category\\CategoryRegistry')) { @@ -160,14 +161,15 @@ public function databaseCompare($actions) { } /** - * Get all available actions + * Get all available actions. + * * @return array */ public function databaseCompareAvailableActions() { - $availableActions = array_flip(t3lib_div::makeInstance('Tx_Extbase_Reflection_ClassReflection', 'Tx_Coreapi_Service_DatabaseApiService')->getConstants()); + $availableActions = array_flip(GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', 'Etobi\\CoreAPI\\Service\\DatabaseApiService')->getConstants()); foreach ($availableActions as $number => $action) { - if (!t3lib_div::isFirstPartOfStr($action, 'ACTION_')) { + if (!GeneralUtility::isFirstPartOfStr($action, 'ACTION_')) { unset($availableActions[$number]); } } @@ -175,10 +177,11 @@ public function databaseCompareAvailableActions() { } /** - * Get all request keys, even for those requests which are not used + * Get all request keys, even for those requests which are not used. * * @param array $update * @param array $remove + * * @return array */ protected function getRequestKeys(array $update, array $remove) { @@ -207,7 +210,4 @@ protected function getRequestKeys(array $update, array $remove) { } return $finalKeys; } - -} - -?> \ No newline at end of file +} \ No newline at end of file diff --git a/Classes/Service/ExtensionApiService.php b/Classes/Service/ExtensionApiService.php index 37b2edd..da6aae0 100644 --- a/Classes/Service/ExtensionApiService.php +++ b/Classes/Service/ExtensionApiService.php @@ -1,8 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -21,14 +24,19 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use InvalidArgumentException; +use RuntimeException; +use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Extension API service * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Service_ExtensionApiService { +class ExtensionApiService { /* * some ExtensionManager Objects require public access to these objects @@ -45,12 +53,23 @@ class Tx_Coreapi_Service_ExtensionApiService { /** @var tx_em_Extensions_Details */ public $extensionDetails; + /** @var $configurationManager \TYPO3\CMS\Core\Configuration\ConfigurationManager */ + protected $configurationManager; + + /** + * The Constructor. + */ + public function __construct() { + $this->configurationManager = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Configuration\\ConfigurationManager'); + } + /** - * Get information about an extension + * Get information about an extension. * * @param string $extensionKey extension key - * @return array + * * @throws InvalidArgumentException + * @return array */ public function getExtensionInformation($extensionKey) { if (strlen($extensionKey) === 0) { @@ -60,21 +79,22 @@ public function getExtensionInformation($extensionKey) { throw new InvalidArgumentException(sprintf('Extension "%s" not found!', $extensionKey)); } - include_once(t3lib_extMgm::extPath($extensionKey) . 'ext_emconf.php'); + include_once(ExtensionManagementUtility::extPath($extensionKey) . 'ext_emconf.php'); $information = array( 'em_conf' => $EM_CONF[''], - 'is_installed' => t3lib_extMgm::isLoaded($extensionKey) + 'is_installed' => ExtensionManagementUtility::isLoaded($extensionKey) ); return $information; } /** - * Get array of installed extensions + * Get array of installed extensions. * * @param string $type L, S, G or empty (for all) - * @return array + * * @throws InvalidArgumentException + * @return array */ public function getInstalledExtensions($type = '') { $type = strtoupper($type); @@ -90,7 +110,7 @@ public function getInstalledExtensions($type = '') { continue; } - include_once(t3lib_extMgm::extPath($key) . 'ext_emconf.php'); + include_once(ExtensionManagementUtility::extPath($key) . 'ext_emconf.php'); $list[$key] = $EM_CONF['']; } @@ -99,14 +119,14 @@ public function getInstalledExtensions($type = '') { } /** - * Update the mirrors, using the scheduler task of EXT:em + * Update the mirrors, using the scheduler task of EXT:em. * - * @return void * @see tx_em_Tasks_UpdateExtensionList * @throws RuntimeException + * @return void */ public function updateMirrors() { - if (t3lib_div::compat_version('6.0.0')) { + if (version_compare(TYPO3_version, '4.7.0', '>')) { throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); } @@ -118,64 +138,49 @@ public function updateMirrors() { // update all repositories foreach ($repositories as $repository) { - /* @var $objRepository tx_em_Repository */ - $objRepository = t3lib_div::makeInstance('tx_em_Repository', $repository['uid']); - /* @var $objRepositoryUtility tx_em_Repository_Utility */ - $objRepositoryUtility = t3lib_div::makeInstance('tx_em_Repository_Utility', $objRepository); + /* @var $objRepository \TYPO3\CMS\Extensionmanager\Domain\Model\Repository */ + $objRepository = GeneralUtility::makeInstance('TYPO3\\CMS\\Extensionmanager\\Domain\\Model\\Repository', $repository['uid']); + /* @var $objRepositoryUtility \TYPO3\CMS\Extensionmanager\Utility\Repository\Helper */ + $objRepositoryUtility = GeneralUtility::makeInstance('TYPO3\\CMS\\Extensionmanager\\Utility\\Repository\\Helper', $objRepository); $count = $objRepositoryUtility->updateExtList(FALSE); unset($objRepository, $objRepositoryUtility); } } /** - * createUploadFolders + * Creates the upload folders of an extension. * * @return array */ public function createUploadFolders() { $extensions = $this->getInstalledExtensions(); - // 6.0 creates also Dirs + // 6.2 creates also Dirs + $result = array(); if (class_exists('\TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility')) { - $fileHandlingUtility = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility'); + $fileHandlingUtility = GeneralUtility::makeInstance('TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility'); foreach ($extensions AS $key => $extension) { $extension['key'] = $key; $fileHandlingUtility->ensureConfiguredDirectoriesExist($extension); } - return array('done with \TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility->ensureConfiguredDirectoriesExist'); - } - - // < 6.0 creates no Dirs - $messages = array(); - foreach ($extensions as $extKey => $extInfo) { - $uploadFolder = PATH_site . tx_em_Tools::uploadFolder($extKey); - if ($extInfo['uploadfolder'] && !@is_dir($uploadFolder)) { - t3lib_div::mkdir($uploadFolder); - $messages[] = 'mkdir ' . $uploadFolder; - $indexContent = ' - - - - - - '; - t3lib_div::writeFile($uploadFolder . 'index.html', $indexContent); - } + $result[] = 'done with \TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility->ensureConfiguredDirectoriesExist'; } - return $messages; + + return $result; } /** - * Install (load) an extension + * Install (load) an extension. * * @param string $extensionKey extension key - * @return void + * * @throws RuntimeException * @throws InvalidArgumentException + * @return void */ public function installExtension($extensionKey) { - if (t3lib_div::compat_version('6.0.0')) { + if (version_compare(TYPO3_version, '4.7.0', '>')) { throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); } @@ -185,12 +190,13 @@ public function installExtension($extensionKey) { } // check if extension is already loaded - if (t3lib_extMgm::isLoaded($extensionKey)) { + if (ExtensionManagementUtility::isLoaded($extensionKey)) { throw new InvalidArgumentException(sprintf('Extension "%s" already installed!', $extensionKey)); } // check if localconf.php is writable - if (!t3lib_extMgm::isLocalconfWritable()) { + + if (!$this->configurationManager->canWriteConfiguration()) { throw new RuntimeException('Localconf.php is not writeable!'); } @@ -218,15 +224,16 @@ public function installExtension($extensionKey) { } /** - * Uninstall (unload) an extension + * Uninstall (unload) an extension. * * @param string $extensionKey extension key - * @return void + * * @throws RuntimeException * @throws InvalidArgumentException + * @return void */ public function uninstallExtension($extensionKey) { - if (t3lib_div::compat_version('6.0.0')) { + if (version_compare(TYPO3_version, '4.7.0', '>')) { throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); } @@ -241,18 +248,18 @@ public function uninstallExtension($extensionKey) { } // check if extension is loaded - if (!t3lib_extMgm::isLoaded($extensionKey)) { + if (!ExtensionManagementUtility::isLoaded($extensionKey)) { throw new InvalidArgumentException(sprintf('Extension "%s" is not installed!', $extensionKey)); } // check if this is a required extension (such as "cms") that cannot be uninstalled - $requiredExtList = t3lib_div::trimExplode(',', t3lib_extMgm::getRequiredExtensionList()); + $requiredExtList = GeneralUtility::trimExplode(',', REQUIRED_EXTENSIONS); if (in_array($extensionKey, $requiredExtList)) { throw new InvalidArgumentException(sprintf('Extension "%s" is a required extension and cannot be uninstalled!', $extensionKey)); } // check if localconf.php is writable - if (!t3lib_extMgm::isLocalconfWritable()) { + if (!$this->configurationManager->canWriteConfiguration()) { throw new RuntimeException('Localconf.php is not writeable!'); } @@ -274,18 +281,19 @@ public function uninstallExtension($extensionKey) { } /** - * Configure an extension + * Configure an extension. + * + * @param string $extensionKey The extension key + * @param array $extensionConfiguration * - * @param string $extensionKey extension key - * @param array $extensionConfiguration - * @return void * @throws RuntimeException * @throws InvalidArgumentException + * @return void */ public function configureExtension($extensionKey, $extensionConfiguration = array()) { global $TYPO3_CONF_VARS; - if (t3lib_div::compat_version('6.0.0')) { + if (version_compare(TYPO3_version, '4.7.0', '>')) { throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); } @@ -295,12 +303,12 @@ public function configureExtension($extensionKey, $extensionConfiguration = arra } // check if extension is loaded - if (!t3lib_extMgm::isLoaded($extensionKey)) { + if (!ExtensionManagementUtility::isLoaded($extensionKey)) { throw new InvalidArgumentException(sprintf('Extension "%s" is not installed!', $extensionKey)); } // check if extension can be configured - $extAbsPath = t3lib_extMgm::extPath($extensionKey); + $extAbsPath = ExtensionManagementUtility::extPath($extensionKey); $extConfTemplateFile = $extAbsPath . 'ext_conf_template.txt'; if (!file_exists($extConfTemplateFile)) { @@ -313,12 +321,12 @@ public function configureExtension($extensionKey, $extensionConfiguration = arra } // Load tsStyleConfig class and parse configuration template: - $extRelPath = t3lib_extmgm::extRelPath($extensionKey); + $extRelPath = ExtensionManagementUtility::extRelPath($extensionKey); - $tsStyleConfig = t3lib_div::makeInstance('t3lib_tsStyleConfig'); + $tsStyleConfig = GeneralUtility::makeInstance('t3lib_tsStyleConfig'); $tsStyleConfig->doNotSortCategoriesBeforeMakingForm = TRUE; $constants = $tsStyleConfig->ext_initTSstyleConfig( - t3lib_div::getUrl($extConfTemplateFile), + GeneralUtility::getUrl($extConfTemplateFile), $extRelPath, $extAbsPath, $GLOBALS['BACK_PATH'] @@ -366,19 +374,20 @@ public function configureExtension($extensionKey, $extensionConfiguration = arra } /** - * Fetch an extension from TER + * Fetch an extension from TER. * - * @param $extensionKey + * @param string $extensionKey * @param string $version * @param string $location - * @param bool $overwrite + * @param bool $overwrite * @param string $mirror - * @return array + * * @throws RuntimeException * @throws InvalidArgumentException + * @return array */ public function fetchExtension($extensionKey, $version = '', $location = 'L', $overwrite = FALSE, $mirror = '') { - if (t3lib_div::compat_version('6.0.0')) { + if (version_compare(TYPO3_version, '4.7.0', '>')) { throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); } @@ -426,16 +435,16 @@ public function fetchExtension($extensionKey, $version = '', $location = 'L', $o // get mirrors $mirrors = array(); - $mirrorsTmpFile = t3lib_div::tempnam('mirrors'); - $mirrorsFile = t3lib_div::getUrl($GLOBALS['TYPO3_CONF_VARS']['EXT']['em_mirrorListURL'], 0); + $mirrorsTmpFile = GeneralUtility::tempnam('mirrors'); + $mirrorsFile = GeneralUtility::getUrl($GLOBALS['TYPO3_CONF_VARS']['EXT']['em_mirrorListURL'], 0); if ($mirrorsFile === FALSE) { - t3lib_div::unlink_tempfile($mirrorsTmpFile); + GeneralUtility::unlink_tempfile($mirrorsTmpFile); throw new RuntimeException('Could not retrieve the list of mirrors!'); } else { - t3lib_div::writeFile($mirrorsTmpFile, $mirrorsFile); + GeneralUtility::writeFile($mirrorsTmpFile, $mirrorsFile); $mirrorsXml = implode('', gzfile($mirrorsTmpFile)); - t3lib_div::unlink_tempfile($mirrorsTmpFile); + GeneralUtility::unlink_tempfile($mirrorsTmpFile); $mirrors = $this->xmlHandler->parseMirrorsXML($mirrorsXml); } @@ -474,15 +483,21 @@ public function fetchExtension($extensionKey, $version = '', $location = 'L', $o } /** - * Imports extension from file + * Imports extension from file. * - * @param string $file path to t3x file - * @param string $location where to import the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext - * @param bool $overwrite overwrite the extension if it already exists - * @return void - * @throws InvalidArgumentException + * @param string $file path to t3x file + * @param string $location where to import the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext + * @param bool $overwrite overwrite the extension if it already exists + * + * @throws \RuntimeException + * @throws \InvalidArgumentException + * @return array */ public function importExtension($file, $location = 'L', $overwrite = FALSE) { + if (version_compare(TYPO3_version, '4.7.0', '>')) { + throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); + } + $return = array(); if (!is_file($file)) { throw new InvalidArgumentException(sprintf('File "%s" does not exist!', $file)); @@ -501,7 +516,7 @@ public function importExtension($file, $location = 'L', $overwrite = FALSE) { throw new InvalidArgumentException(sprintf('Unknown location "%s"!', $location)); } - $fileContent = t3lib_div::getUrl($file); + $fileContent = GeneralUtility::getUrl($file); if (!$fileContent) { throw new InvalidArgumentException(sprintf('File "%s" is empty!', $file)); } @@ -535,10 +550,11 @@ public function importExtension($file, $location = 'L', $overwrite = FALSE) { /** - * Check if an extension exists + * Check if an extension exists. * * @param string $extensionKey extension key - * @return void + * + * @return boolean */ protected function extensionExists($extensionKey) { $this->initializeExtensionManagerObjects(); @@ -554,32 +570,34 @@ protected function extensionExists($extensionKey) { } /** - * initialize ExtensionManager Objects + * Initialize ExtensionManager Objects. + * + * @return void */ protected function initializeExtensionManagerObjects() { - $this->xmlHandler = t3lib_div::makeInstance('tx_em_Tools_XmlHandler'); - $this->extensionList = t3lib_div::makeInstance('tx_em_Extensions_List', $this); - $this->terConnection = t3lib_div::makeInstance('tx_em_Connection_Ter', $this); - $this->extensionDetails = t3lib_div::makeInstance('tx_em_Extensions_Details', $this); + $this->xmlHandler = GeneralUtility::makeInstance('tx_em_Tools_XmlHandler'); + $this->extensionList = GeneralUtility::makeInstance('tx_em_Extensions_List', $this); + $this->terConnection = GeneralUtility::makeInstance('tx_em_Connection_Ter', $this); + $this->extensionDetails = GeneralUtility::makeInstance('tx_em_Extensions_Details', $this); } /** * @return tx_em_Install */ protected function getEmInstall() { - $install = t3lib_div::makeInstance('tx_em_Install', $this); + $install = GeneralUtility::makeInstance('tx_em_Install', $this); $install->setSilentMode(TRUE); return $install; } /** - * Clear the caches + * Clear the caches. + * + * @return void */ protected function clearCaches() { - $cacheApiService = t3lib_div::makeInstance('Tx_Coreapi_Service_CacheApiService'); + $cacheApiService = GeneralUtility::makeInstance('Etobi\\CoreAPI\\Service\\CacheApiService'); $cacheApiService->initializeObject(); $cacheApiService->clearAllCaches(); } -} - -?> \ No newline at end of file +} \ No newline at end of file diff --git a/Classes/Service/SiteApiService.php b/Classes/Service/SiteApiService.php index d363a98..b406d28 100644 --- a/Classes/Service/SiteApiService.php +++ b/Classes/Service/SiteApiService.php @@ -1,8 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -21,17 +24,20 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use InvalidArgumentException; +use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Site API service * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Service_SiteApiService { +class SiteApiService { /** - * Get some basic site information + * Get some basic site information. * * @return array */ @@ -49,12 +55,13 @@ public function getSiteInfo() { } /** - * Create a sys news record + * Create a sys news record. * * @param string $header header - * @param string $text text - * @return void + * @param string $text text + * * @throws InvalidArgumentException + * @return void */ public function createSysNews($header, $text) { if (strlen($header) === 0) { @@ -70,10 +77,11 @@ public function createSysNews($header, $text) { } /** - * Get disku usage + * Get disk usage. * * @author Claus Due , Wildside A/S * @param array $data + * * @return void */ protected function getDiskUsage(&$data) { @@ -83,10 +91,11 @@ protected function getDiskUsage(&$data) { } /** - * Get database size + * Get database size. * * @author Claus Due , Wildside A/S * @param array $data + * * @return void */ protected function getDatabaseInformation(&$data) { @@ -98,18 +107,16 @@ protected function getDatabaseInformation(&$data) { } /** - * Get count of local installed extensions + * Get count of local installed extensions. * * @param array $data + * * @return void */ protected function getCountOfExtensions(&$data) { - /** @var Tx_Coreapi_Service_ExtensionApiService $extensionService */ - $extensionService = t3lib_div::makeInstance('Tx_Coreapi_Service_ExtensionApiService'); + /** @var \Etobi\CoreAPI\Service\ExtensionApiService $extensionService */ + $extensionService = GeneralUtility::makeInstance('Etobi\\CoreAPI\\Service\\ExtensionApiService'); $extensions = $extensionService->getInstalledExtensions('L'); $data['Count local installed extensions'] = count($extensions); } - -} - -?> \ No newline at end of file +} \ No newline at end of file diff --git a/Scripts/Cli.php b/Scripts/Cli.php deleted file mode 100644 index 6432181..0000000 --- a/Scripts/Cli.php +++ /dev/null @@ -1,10 +0,0 @@ -start(); -} else { - die('This script must be included by the "CLI module dispatcher"'); -} - -?> diff --git a/ext_autoload.php b/ext_autoload.php index 4969edd..221b0e9 100644 --- a/ext_autoload.php +++ b/ext_autoload.php @@ -1,17 +1,14 @@ $extensionClassesPath . 'Command/DatabaseApiCommandController.php', - 'tx_coreapi_command_siteapicommandcontroller' => $extensionClassesPath . 'Command/SiteApiCommandController.php', - 'tx_coreapi_command_cacheapicommandcontroller' => $extensionClassesPath . 'Command/CacheApiCommandController.php', - 'tx_coreapi_service_cacheapiservice' => $extensionClassesPath . 'Service/CacheApiService.php', - 'tx_coreapi_service_siteapiservice' => $extensionClassesPath . 'Service/SiteApiService.php', - 'tx_coreapi_service_databaseapiservice' => $extensionClassesPath . 'Service/DatabaseApiService.php', - 'tx_coreapi_service_extensionapiservice' => $extensionClassesPath . 'Service/ExtensionApiService.php', - 'tx_coreapi_cli_dispatcher' => $extensionClassesPath .'Cli/Dispatcher.php', -); - -?> \ No newline at end of file + 'Etobi\CoreAPI\Command\DatabaseApiCommandController' => $extensionClassesPath . 'Command/DatabaseApiCommandController.php', + 'Etobi\CoreAPI\Command\SiteApiCommandController' => $extensionClassesPath . 'Command/SiteApiCommandController.php', + 'Etobi\CoreAPI\Command\CacheApiCommandController' => $extensionClassesPath . 'Command/CacheApiCommandController.php', + 'Etobi\CoreAPI\Service\CacheApiService' => $extensionClassesPath . 'Service/CacheApiService.php', + 'Etobi\CoreAPI\Service\SiteApiService' => $extensionClassesPath . 'Service/SiteApiService.php', + 'Etobi\CoreAPI\Service\DatabaseApiService' => $extensionClassesPath . 'Service/DatabaseApiService.php', + 'Etobi\CoreAPI\Service\ExtensionApiService' => $extensionClassesPath . 'Service/ExtensionApiService.php' +); \ No newline at end of file diff --git a/ext_emconf.php b/ext_emconf.php index 6423fd0..16ecf3e 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -4,31 +4,29 @@ 'title' => 'coreapi', 'description' => 'coreapi', 'category' => 'plugin', - 'author' => 'Tobias Liebig,Georg Ringer', - 'author_email' => 'tobias.liebig@typo3.org,georg.ringer@cyberhouse.at', + 'author' => 'Tobias Liebig,Georg Ringer,Stefano Kowalke', + 'author_email' => 'tobias.liebig@typo3.org,georg.ringer@cyberhouse.at,blueduck@gmx.net', 'author_company' => '', 'shy' => '', 'priority' => '', 'module' => '', - 'state' => 'beta', + 'state' => 'alpha', 'internal' => '', 'uploadfolder' => '0', 'createDirs' => '', 'modify_tables' => '', 'clearCacheOnLoad' => 0, 'lockType' => '', - 'version' => '0.0.1', + 'version' => '1.0.0', 'constraints' => array( 'depends' => array( - 'typo3' => '4.5.0-6.1.99', - 'extbase' => '1.3.0-6.1.99', - 'fluid' => '1.3.0-6.1.99', + 'typo3' => '6.2.0-6.2.99', + 'extbase' => '6.2.0-6.2.99', + 'fluid' => '6.2.0-6.2.99', ), 'conflicts' => array( ), 'suggests' => array( ), ), -); - -?> \ No newline at end of file +); \ No newline at end of file diff --git a/ext_localconf.php b/ext_localconf.php index 27117c8..da4611c 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -3,15 +3,13 @@ if (TYPO3_MODE === 'BE') { // Register commands - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Tx_Coreapi_Command_DatabaseApiCommandController'; - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Tx_Coreapi_Command_CacheApiCommandController'; - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Tx_Coreapi_Command_SiteApiCommandController'; - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Tx_Coreapi_Command_ExtensionApiCommandController'; + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Etobi\CoreAPI\Command\DatabaseApiCommandController'; + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Etobi\CoreAPI\Command\CacheApiCommandController'; + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Etobi\CoreAPI\Command\SiteApiCommandController'; + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Etobi\CoreAPI\Command\ExtensionApiCommandController'; } // Register the CLI dispatcher $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['cliKeys'][$_EXTKEY] = array( 'EXT:' . $_EXTKEY . '/Scripts/Cli.php', '_CLI_lowlevel' -); - -?> \ No newline at end of file +); \ No newline at end of file From e339ef2a6731f8d7bd845adcd783a5c0e520b0c0 Mon Sep 17 00:00:00 2001 From: Stefano Kowalke Date: Mon, 2 Jun 2014 19:15:54 +0200 Subject: [PATCH 2/4] Add support for TYPO3 6.2 and drop support for all other version below * Introduce namespaces * Adjust doc comments * Remove closing php tag * Fix typo * Remove the legacy Dispatcher interface close #18 close #47 --- Classes/Cli/Dispatcher.php | 649 ------------------ Classes/Command/CacheApiCommandController.php | 42 +- .../Command/DatabaseApiCommandController.php | 28 +- .../Command/ExtensionApiCommandController.php | 110 +-- Classes/Command/SiteApiCommandController.php | 39 +- Classes/Service/CacheApiService.php | 39 +- Classes/Service/DatabaseApiService.php | 56 +- Classes/Service/ExtensionApiService.php | 200 +++--- Classes/Service/SiteApiService.php | 39 +- Scripts/Cli.php | 10 - ext_autoload.php | 21 +- ext_emconf.php | 18 +- ext_localconf.php | 12 +- 13 files changed, 332 insertions(+), 931 deletions(-) delete mode 100644 Classes/Cli/Dispatcher.php delete mode 100644 Scripts/Cli.php diff --git a/Classes/Cli/Dispatcher.php b/Classes/Cli/Dispatcher.php deleted file mode 100644 index 2560c30..0000000 --- a/Classes/Cli/Dispatcher.php +++ /dev/null @@ -1,649 +0,0 @@ - - * @package TYPO3 - * @subpackage tx_coreapi - */ -class Tx_Coreapi_Cli_Dispatcher extends t3lib_cli { - - var $cli_help = array( - 'name' => 'coreapi', - 'synopsis' => './cli_dispatch.phpsh coreapi service:command [options] arguments', - 'description' => ' -Coreapi provides a set of services/commands for doing the most common admin task in TYPO3 by CLI instead of doing it in the backend/browser. -Currently the following commands are supported: - -###COMMANDS### -', - 'examples' => ' -./cli_dispatch.phpsh coreapi site:info -./cli_dispatch.phpsh coreapi cache:clearallcaches -./cli_dispatch.phpsh coreapi extension:info rtehtmlarea -./cli_dispatch.phpsh coreapi site:createsysnews "title" "text"', - 'options' => 'use "./cli_dispatch.phpsh coreapi help service:command" to get help on the arguments and options of a specific command - ', - 'license' => 'GNU GPL - free software!', - 'author' => 'Tobias Liebig', - ); - - const MAXIMUM_LINE_LENGTH = 79; - - var $commandMethodPattern = '/([a-z][a-z0-9]*)([A-Z][a-zA-Z0-9]*)Command/'; - - /** - * @var string service - */ - protected $service = ''; - - /** - * @var string command - */ - protected $command = ''; - - /** - * Constructor with basic checks - */ - public function __construct() { - parent::__construct(); - - if (!isset($this->cli_args['_DEFAULT'][1]) || $this->cli_args['_DEFAULT'][1] === 'help') { - $this->cli_help(); - die(); - } - - $split = explode(':', $this->cli_args['_DEFAULT'][1]); - if (count($split) === 1) { - $this->error('CLI calls need to be like coreapi cache:clearallcaches'); - } elseif (count($split) !== 2) { - $this->error('Only one : is allowed in first argument'); - } - - $this->service = strtolower($split[0]); - $this->command = strtolower($split[1]); - } - - /** - * Starts the script - * @return void - */ - public function start() { - try { - $command = $this->service . ucfirst($this->command) . 'Command'; - $this->runCommand($command); - - } catch (Exception $e) { - $errorMessage = sprintf('ERROR: Error in service "%s" and command "%s"": %s!', $this->service, $this->command, $e->getMessage()); - $this->outputLine($errorMessage); - } - } - - /** - * @param $command - * @throws InvalidArgumentException - */ - protected function runCommand($command) { - if (method_exists($this, $command)) { - $args = array_slice($this->cli_args['_DEFAULT'], 2); - $method = new ReflectionMethod(get_class($this), $command); - - //check number of required arguments - if ($method->getNumberOfRequiredParameters() !== count($args)) { - throw new InvalidArgumentException('Wrong number of arguments'); - } - - foreach ($method->getParameters() as $param) { - if ($param->isOptional()) { - $name = $param->getName(); - if ($this->cli_isArg('--' . $name)) { - $args[] = $this->cli_argValue('--' . $name); - } else { - $args[] = $param->getDefaultValue(); - } - } - } - //invoke command with given args and options - $method->invokeArgs($this, $args); - } else { - throw new InvalidArgumentException('Service does not exist or command not supported'); - } - } - - /** - * Clear all caches - * - * Clears all TYPO3 caches - * - * @return void - * @example ./cli_dispatch.phpsh coreapi cache:clearallcaches - */ - public function cacheClearallcachesCommand() { - $cacheApiService = $this->getCacheApiService(); - $cacheApiService->clearAllCaches(); - $this->outputLine('All caches cleared'); - } - - - /** - * Clear configuration cache (temp_CACHED_..) - * - * Deletes the temp_CACHED_* files in /typo3conf - * - * @return void - * @example ./cli_dispatch.phpsh coreapi cache:clearconfigurationcache - */ - public function cacheClearconfigurationcacheCommand() { - $cacheApiService = $this->getCacheApiService(); - $cacheApiService->clearConfigurationCache(); - $this->outputLine('Configuration cache cleared'); - } - - /** - * Clear page cache - * - * Clears the page cache in TYPO3 - * - * @return void - * @example ./cli_dispatch.phpsh coreapi cache:clearpagecache - */ - public function cacheClearpagecacheCommand() { - $cacheApiService = $this->getCacheApiService(); - $cacheApiService->clearPageCache(); - $this->outputLine('Page cache cleared'); - } - - /** - * Clear all caches except the page cache. - * This is especially useful on big sites when you can't just drop the page cache - * - * @example ./cli_dispatch.phpsh coreapi cache:clearallexceptpagecache - * @return void - */ - public function cacheClearallexceptpagecacheCommand() { - $cacheApiService = $this->getCacheApiService(); - $clearedCaches = $cacheApiService->clearAllExceptPageCache(); - - $this->outputLine('Cleared caches: ' . implode(', ', $clearedCaches)); - } - - /** - * Database compare - * - * Leave the argument 'actions' empty or use "help" to see the available ones - * - * @param string $actions List of actions which will be executed - * @return void - * @example ./cli_dispatch.phpsh coreapi database:databasecompare 2 - */ - public function databaseDatabasecompareCommand($actions) { - $databaseApiService = $this->getDatabaseApiService(); - if ($actions === 'help') { - $actions = $databaseApiService->databaseCompareAvailableActions(); - $this->outputTable($actions); - } else { - $databaseApiService->databaseCompare($actions); - } - } - - /** - * Information about an extension - * - * Echo's out a table with information about a specific extension - * - * @param string $extkey extension key - * @return void - * @example ./cli_dispatch.phpsh coreapi extension:info rtehtmlarea - */ - public function extensionInfoCommand($extkey) { - $extensionApiService = $this->getExtensionApiService(); - $data = $extensionApiService->getExtensionInformation($extkey); - $this->outputLine(''); - $this->outputLine('EXTENSION "%s": %s %s', array(strtoupper($extkey), $data['em_conf']['version'], $data['em_conf']['state'])); - $this->outputLine(str_repeat('-', self::MAXIMUM_LINE_LENGTH)); - - $outputInformation = array(); - $outputInformation['is installed'] = ($data['is_installed'] ? 'yes' : 'no'); - foreach ($data['em_conf'] as $emConfKey => $emConfValue) { - // Skip empty properties - if (empty($emConfValue)) { - continue; - } - // Skip properties which are already handled - if ($emConfKey === 'title' || $emConfKey === 'version' || $emConfKey === 'state') { - continue; - } - $outputInformation[$emConfKey] = $emConfValue; - } - - foreach ($outputInformation as $outputKey => $outputValue) { - $description = ''; - if (is_array($outputValue)) { - foreach ($outputValue as $additionalKey => $additionalValue) { - if (is_array($additionalValue)) { - - if (empty($additionalValue)) { - continue; - } - $description .= LF . str_repeat(' ', 28) . $additionalKey; - $description .= LF; - foreach ($additionalValue as $ak => $av) { - $description .= str_repeat(' ', 30) . $ak . ': ' . $av . LF; - } - } else { - $description .= LF . str_repeat(' ', 28) . $additionalKey . ': ' . $additionalValue; - } - } - } else { - $description = wordwrap($outputValue, self::MAXIMUM_LINE_LENGTH - 28, PHP_EOL . str_repeat(' ', 28), TRUE); - } - $this->outputLine('%-2s%-25s %s', array(' ', $outputKey, $description)); - } - } - - /** - * Update list - * - * Update the list of available extensions in the TER - * - * @return void - * @example ./cli_dispatch.phpsh coreapi extension:updatelist - */ - public function extensionUpdatelistCommand() { - $extensionApiService = $this->getExtensionApiService(); - $extensionApiService->updateMirrors(); - $this->outputLine('Extension list has been updated.'); - } - - /** - * List all installed (loaded) extensions - * - * @param string $type Extension type, can either be L for local, S for system or G for global. Leave it empty for all - * @return void - * @example ./cli_dispatch.phpsh coreapi extension:listinstalled --type=S - */ - public function extensionListinstalledCommand($type = '') { - $extensionApiService = $this->getExtensionApiService(); - $extensions = $extensionApiService->getInstalledExtensions($type); - $out = array(); - - foreach ($extensions as $key => $details) { - $title = $key . ' - ' . $details['version'] . '/' . $details['state']; - $out[$title] = $details['title']; - } - $this->outputTable($out); - } - - /** - * Install(activate) an extension - * - * @param string $key extension key - * @return void - */ - public function extensionInstallCommand($key) { - $extensionApiService = $this->getExtensionApiService(); - $data = $extensionApiService->installExtension($key); - $this->outputLine(sprintf('Extension "%s" is now installed!', $key)); - } - - /** - * UnInstall(deactivate) an extension - * - * @param string $key extension key - * @return void - */ - public function extensionUninstallCommand($key) { - $extensionApiService = $this->getExtensionApiService(); - $data = $extensionApiService->uninstallExtension($key); - $this->outputLine(sprintf('Extension "%s" is now uninstalled!', $key)); - } - - /** - * Configure an extension - * - * This command enables you to configure an extension. - * - * examples: - * - * [1] Using a standard formatted ini-file - * ./cli_dispatch.phpsh coreapi extension:configure rtehtmlarea --configfile=C:\rteconf.txt - * - * [2] Adding configuration settings directly on the command line - * ./cli_dispatch.phpsh coreapi extension:configure rtehtmlarea --settings="enableImages=1;allowStyleAttribute=0" - * - * [3] A combination of [1] and [2] - * ./cli_dispatch.phpsh extbase extension:configure rtehtmlarea --configfile=C:\rteconf.txt --settings="enableImages=1;allowStyleAttribute=0" - * - * @param string $key extension key - * @param string $configfile path to file containing configuration settings. Must be formatted as a standard ini-file - * @param string $settings string containing configuration settings separated on the form "k1=v1;k2=v2;" - * @return void - * @throws InvalidArgumentException - */ - public function extensionConfigureCommand($key, $configfile = '', $settings = '') { - global $TYPO3_CONF_VARS; - $extensionApiService = $this->getExtensionApiService(); - $conf = array(); - - if (is_file($configfile)) { - $conf = parse_ini_file($configfile); - } - - if (strlen($settings)) { - $arr = explode(';', $settings); - foreach ($arr as $v) { - if (strpos($v, '=') === FALSE) { - throw new InvalidArgumentException(sprintf('Ill-formed setting "%s"!', $v)); - } - $parts = t3lib_div::trimExplode('=', $v, FALSE, 2); - - if (!empty($parts[0])) { - $conf[$parts[0]] = $parts[1]; - } - } - } - - if (empty($conf)) { - throw new InvalidArgumentException(sprintf('No configuration settings!', $key)); - } - - $extensionApiService->configureExtension($key, $conf); - $this->outputLine(sprintf('Extension "%s" has been configured!', $key)); - - } - - - /** - * Fetch an extension from TER - * - * @param string $key extension key - * @param string $version the exact version of the extension, otherwise the latest will be picked - * @param string $location where to put the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext - * @param string|bool $overwrite overwrite the extension if it already exists - * @param string $mirror mirror to fetch the extension from, otherwise a random mirror will be selected - * @return void - */ - public function extensionFetchCommand($key, $version = '', $location = 'L', $overwrite = FALSE, $mirror = '') { - $extensionApiService = $this->getExtensionApiService(); - $data = $extensionApiService->fetchExtension($key, $version, $location, $overwrite, $mirror); - $this->outputLine(sprintf('Extension "%s" version %s has been fetched from repository!', $data['extKey'], $data['version'])); - } - - - /** - * Import extension from file - * - * @param string $file path to t3x file - * @param string $location where to import the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext - * @param boolean $overwrite overwrite the extension if it already exists - * @return void - */ - public function extensionImportCommand($file, $location = 'L', $overwrite = FALSE) { - $extensionApiService = $this->getExtensionApiService(); - $data = $extensionApiService->importExtension($file, $location, $overwrite); - $this->outputLine(sprintf('Extension "%s" has been imported!', $data['extKey'])); - } - - /** - * Ensure upload folders of installed extensions exist - * @return void - */ - public function extensionCreateuploadfoldersCommand() { - $extensionApiService = $this->getExtensionApiService(); - $messages = $extensionApiService->createUploadFolders(); - if (sizeof($messages)) { - foreach ($messages as $message) { - $this->outputLine($message); - } - } else { - $this->outputLine('no uploadFolder created'); - } - } - - /** - * Site info - * - * Basic information about the system - * - * @return void - * @example ./cli_dispatch.phpsh coreapi site:info - */ - public function siteInfoCommand() { - $siteApiService = $this->getSiteApiService(); - $infos = $siteApiService->getSiteInfo(); - $this->outputTable($infos); - } - - /** - * Create a sys news - * - * Sys news record is displayed at the login page - * - * @param string $header Header text - * @param string $text Basic text - * @return void - * @example ./cli_dispatch.phpsh coreapi site:createsysnews "The header" "The news text" - */ - public function siteCreatesysnewsCommand($header, $text) { - $siteApiService = $this->getSiteApiService(); - $siteApiService->createSysNews($header, $text); - } - - /** - * Output a single line - * - * @param string $text text - * @param array $arguments optional arguments - * @return void - */ - protected function outputLine($text, array $arguments = array()) { - if ($arguments !== array()) { - $text = vsprintf($text, $arguments); - } - $this->cli_echo($text . PHP_EOL); - } - - /** - * Output a whole table, maximum 2 cols - * - * @param array $input input table - * @return void - */ - protected function outputTable(array $input) { - $this->outputLine(str_repeat('-', self::MAXIMUM_LINE_LENGTH)); - foreach ($input as $key => $value) { - $line = wordwrap($value, self::MAXIMUM_LINE_LENGTH - 43, PHP_EOL . str_repeat(' ', 43), TRUE); - $this->outputLine('%-2s%-40s %s', array(' ', $key, $line)); - } - $this->outputLine(str_repeat('-', self::MAXIMUM_LINE_LENGTH)); - } - - /** - * End call - * - * @param string $message Error message - * @return void - */ - protected function error($message) { - $this->cli_echo('ERROR: ' . $message, FALSE); - die(); - } - - /** - * Display help - * Overridden from parent - * - * @return void - */ - public function cli_help() { - if (isset($this->cli_args['_DEFAULT'][1]) && - $this->cli_args['_DEFAULT'][1] === 'help' && - isset($this->cli_args['_DEFAULT'][2]) && - strpos($this->cli_args['_DEFAULT'][2], ':') !== FALSE - ) { - $this->setHelpFromDocComment($this->cli_args['_DEFAULT'][2]); - } else { - $class = new ReflectionClass(get_class($this)); - $commands = array(); - foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { - if (preg_match($this->commandMethodPattern, $method->getName(), $matches)) { - $commands[] = strtolower($matches[1]) . ':' . strtolower($matches[2]); - } - } - $this->cli_help['description'] = str_replace('###COMMANDS###', implode(PHP_EOL, $commands), $this->cli_help['description']); - } - parent::cli_help(); - } - - /** - * Extract help texts from doc comments - * - * @param $operation - */ - protected function setHelpFromDocComment($operation) { - list($service, $command) = explode(':', $operation, 2); - $commandMethod = strtolower($service) . ucfirst($command) . 'Command'; - if (method_exists($this, $commandMethod)) { - $this->cli_help['options'] = ''; - //extract doc comment - $ref = new ReflectionMethod(get_class($this), $commandMethod); - $comment = $ref->getDocComment(); - $comment = preg_replace('/\/\*\*\s*(.*)\s*\*\//s', '$1', $comment); - $lines = explode(PHP_EOL, $comment); - - //get name - $name = preg_replace('/^\s*\*\s*(.*)\s*$/i', '$1', array_shift($lines)); - $this->cli_help['name'] = $name; - - $description = array(); - $examples = array(); - $params = array(); - foreach ($lines as $n => $l) { - if (!preg_match('/^\s*\*\s*@/i', $l)) { - //add to description - $description[] = preg_replace('/^\s*\*\s*(.*)\s*$/i', '$1', $l); - continue; - } - - //params - if (preg_match('/^\s*\*\s*@param\s*(?P[a-z0-9_]*)\s+\$(?P[a-z0-9_]*)\s+(?P.*)/i', $l, $matches)) { - $params[$matches['name']] = array( - 'type' => $matches['name'], - 'description' => $matches['description'] - ); - continue; - } - - // examples - if (preg_match('/^\s*\*\s*@example\s+(?P.*)/i', $l, $matches)) { - $examples[] = $matches['text']; - } - } - - $this->cli_help['description'] = trim(implode(PHP_EOL, $description)); - if (!empty($examples)) { - $this->cli_help['examples'] = implode(PHP_EOL, $examples); - } else { - unset($this->cli_help['examples']); - } - - //get params - $parameters = $ref->getParameters(); - $args = array(); - foreach ($parameters as $param) { - $name = $param->getName(); - $description = isset($params[$name]) ? $params[$name]['description'] : ''; - if ($param->isOptional()) { - $this->cli_options[] = array('--' . $name, $description); - } else { - $args[strtoupper($name)] = $description; - } - } - - //compile arguments section - if (!empty($args)) { - $maxLen = 0; - foreach (array_keys($args) as $argname) { - if (strlen($argname) > $maxLen) { - $maxLen = strlen($argname); - } - } - - $tmp = array(); - foreach ($args as $argname => $description) { - $tmp[] = $argname . substr($this->cli_indent(rtrim($description), $maxLen + 4), strlen($argname)); - } - - $offset = array_search('options', array_keys($this->cli_help)); - $this->cli_help = array_slice($this->cli_help, 0, $offset, true) + - array('arguments' => LF . implode(LF, $tmp)) + - array_slice($this->cli_help, $offset, NULL, true); - } - - //set synopsis for this - $this->cli_help['synopsis'] = './cli_dispatch.phpsh coreapi ' . $operation . ' ###OPTIONS### ' . implode(' ', array_keys($args)); - - } else { - $this->error(sprintf('No help available for "%s"', $operation) . PHP_EOL); - } - } - - /** - * @return Tx_Coreapi_Service_CacheApiService - */ - protected function getCacheApiService() { - $cacheApiService = t3lib_div::makeInstance('Tx_Coreapi_Service_CacheApiService'); - $cacheApiService->initializeObject(); - return $cacheApiService; - } - - /** - * @return Tx_Coreapi_Service_DatabaseApiService - */ - protected function getDatabaseApiService() { - $databaseApiService = t3lib_div::makeInstance('Tx_Coreapi_Service_DatabaseApiService'); - return $databaseApiService; - } - - /** - * @return Tx_Coreapi_Service_ExtensionApiService - */ - protected function getExtensionApiService() { - $extensionApiService = t3lib_div::makeInstance('Tx_Coreapi_Service_ExtensionApiService'); - return $extensionApiService; - } - - /** - * @return Tx_Coreapi_Service_SiteApiService - */ - protected function getSiteApiService() { - $siteApiService = t3lib_div::makeInstance('Tx_Coreapi_Service_SiteApiService'); - return $siteApiService; - } -} - -?> \ No newline at end of file diff --git a/Classes/Command/CacheApiCommandController.php b/Classes/Command/CacheApiCommandController.php index 5b0071c..2954fb3 100644 --- a/Classes/Command/CacheApiCommandController.php +++ b/Classes/Command/CacheApiCommandController.php @@ -1,8 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -21,49 +24,48 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use TYPO3\CMS\Extbase\Mvc\Controller\CommandController; /** * API Command Controller * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Command_CacheApiCommandController extends Tx_Extbase_MVC_Controller_CommandController { +class CacheApiCommandController extends CommandController { /** - * Clear all caches + * Clear all caches. * * @return void */ public function clearAllCachesCommand() { - /** @var $service Tx_Coreapi_Service_CacheApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_CacheApiService'); + $service = $this->getService(); $service->clearAllCaches(); $this->outputLine('All caches have been cleared.'); } /** - * Clear configuration cache (temp_CACHED_..) + * Clear configuration cache (temp_CACHED_..). * * @return void */ public function clearConfigurationCacheCommand() { - /** @var $service Tx_Coreapi_Service_CacheApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_CacheApiService'); + $service = $this->getService(); $service->clearConfigurationCache(); $this->outputLine('Configuration cache has been cleared.'); } /** - * Clear page cache + * Clear page cache. * * @return void */ public function clearPageCacheCommand() { - /** @var $service Tx_Coreapi_Service_CacheApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_CacheApiService'); + $service = $this->getService(); $service->clearPageCache(); $this->outputLine('Page cache has been cleared.'); @@ -71,17 +73,23 @@ public function clearPageCacheCommand() { /** * Clear all caches except the page cache. - * This is especially useful on big sites when you can't just drop the page cache + * This is especially useful on big sites when you can't just drop the page cache. * * @return void */ public function clearAllExceptPageCacheCommand() { - /** @var $service Tx_Coreapi_Service_CacheApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_CacheApiService'); + $service = $this->getService(); $clearedCaches = $service->clearAllExceptPageCache(); $this->outputLine('Cleared caches: ' . implode(', ', $clearedCaches)); } -} -?> \ No newline at end of file + /** + * Returns the service object. + * + * @return \Etobi\CoreAPI\Service\CacheApiService object + */ + private function getService() { + return $this->objectManager->get('Etobi\\CoreAPI\\Service\\CacheApiService'); + } +} \ No newline at end of file diff --git a/Classes/Command/DatabaseApiCommandController.php b/Classes/Command/DatabaseApiCommandController.php index 8bbbe3e..6777cb5 100644 --- a/Classes/Command/DatabaseApiCommandController.php +++ b/Classes/Command/DatabaseApiCommandController.php @@ -1,8 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -21,25 +24,25 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use TYPO3\CMS\Extbase\Mvc\Controller\CommandController; /** * API Command Controller * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Command_DatabaseApiCommandController extends Tx_Extbase_MVC_Controller_CommandController { +class DatabaseApiCommandController extends CommandController { /** - * Database compare - * + * Database compare. * Leave the argument 'actions' empty or use "help" to see the available ones * * @param string $actions List of actions which will be executed */ public function databaseCompareCommand($actions = '') { - /** @var $service Tx_Coreapi_Service_DatabaseApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_DatabaseApiService'); + $service = $this->getService(); if ($actions === 'help' || strlen($actions) === 0) { $actions = $service->databaseCompareAvailableActions(); @@ -57,6 +60,13 @@ public function databaseCompareCommand($actions = '') { $this->quit(); } } -} -?> \ No newline at end of file + /** + * Returns the service object. + * + * @return \Etobi\CoreAPI\Service\DatabaseApiService object + */ + private function getService() { + return $this->objectManager->get('Etobi\\CoreAPI\\Service\\DatabaseApiService'); + } +} \ No newline at end of file diff --git a/Classes/Command/ExtensionApiCommandController.php b/Classes/Command/ExtensionApiCommandController.php index 31f8f6a..8299f4a 100644 --- a/Classes/Command/ExtensionApiCommandController.php +++ b/Classes/Command/ExtensionApiCommandController.php @@ -1,9 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -22,26 +24,31 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use Exception; +use InvalidArgumentException; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Mvc\Controller\CommandController; /** * Extension API Command Controller * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Command_ExtensionApiCommandController extends Tx_Extbase_MVC_Controller_CommandController { +class ExtensionApiCommandController extends CommandController { /** - * Information about an extension + * Information about an extension. * * @param string $key extension key + * * @return void */ public function infoCommand($key) { $data = array(); try { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); + $service = $this->getService(); $data = $service->getExtensionInformation($key); } catch (Exception $e) { $this->outputLine($e->getMessage()); @@ -92,9 +99,10 @@ public function infoCommand($key) { } /** - * List all installed extensions + * List all installed extensions. * * @param string $type Extension type, can either be L for local, S for system or G for global. Leave it empty for all + * * @return void */ public function listInstalledCommand($type = '') { @@ -104,8 +112,7 @@ public function listInstalledCommand($type = '') { $this->quit(); } - /** @var $extensions Tx_Coreapi_Service_ExtensionApiService */ - $extensions = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService')->getInstalledExtensions($type); + $extensions = $this->getService()->getInstalledExtensions($type); foreach ($extensions as $key => $details) { $title = $key . ' - ' . $details['version'] . '/' . $details['state']; @@ -119,29 +126,28 @@ public function listInstalledCommand($type = '') { } /** - * Update list + * Update list. * * @return void */ public function updateListCommand() { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); + $service = $this->getService(); $service->updateMirrors(); $this->outputLine('Extension list has been updated.'); } /** - * Install(activate) an extension + * Install(activate) an extension. * * @param string $key extension key + * * @return void */ public function installCommand($key) { try { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); - $data = $service->installExtension($key); + $service = $this->getService(); + $service->installExtension($key); } catch (Exception $e) { $this->outputLine($e->getMessage()); $this->quit(); @@ -150,16 +156,16 @@ public function installCommand($key) { } /** - * UnInstall(deactivate) an extension + * UnInstall(deactivate) an extension. * * @param string $key extension key + * * @return void */ public function uninstallCommand($key) { try { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); - $data = $service->uninstallExtension($key); + $service = $this->getService(); + $service->uninstallExtension($key); } catch (Exception $e) { $this->outputLine($e->getMessage()); $this->quit(); @@ -168,12 +174,10 @@ public function uninstallCommand($key) { } /** - * Configure an extension - * + * Configure an extension. * This command enables you to configure an extension. * - * examples: - * + * * [1] Using a standard formatted ini-file * ./cli_dispatch.phpsh extbase extensionapi:configure rtehtmlarea --configfile=C:\rteconf.txt * @@ -182,17 +186,17 @@ public function uninstallCommand($key) { * * [3] A combination of [1] and [2] * ./cli_dispatch.phpsh extbase extensionapi:configure rtehtmlarea --configfile=C:\rteconf.txt --settings="enableImages=1;allowStyleAttribute=0" + * * - * @param string $key extension key + * @param string $key extension key * @param string $configfile path to file containing configuration settings. Must be formatted as a standard ini-file - * @param string $settings string containing configuration settings separated on the form "k1=v1;k2=v2;" + * @param string $settings string containing configuration settings separated on the form "k1=v1;k2=v2;" + * * @return void */ public function configureCommand($key, $configfile = '', $settings = '') { - global $TYPO3_CONF_VARS; try { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); + $service = $this->getService(); $conf = array(); if (is_file($configfile)) { $conf = parse_ini_file($configfile); @@ -204,7 +208,7 @@ public function configureCommand($key, $configfile = '', $settings = '') { if (strpos($v, '=') === FALSE) { throw new InvalidArgumentException(sprintf('Ill-formed setting "%s"!', $v)); } - $parts = t3lib_div::trimExplode('=', $v, FALSE, 2); + $parts = GeneralUtility::trimExplode('=', $v, FALSE, 2); if (!empty($parts[0])) { $conf[$parts[0]] = $parts[1]; } @@ -214,7 +218,8 @@ public function configureCommand($key, $configfile = '', $settings = '') { if (empty($conf)) { throw new InvalidArgumentException(sprintf('No configuration settings!', $key)); } - $data = $service->configureExtension($key, $conf); + + $service->configureExtension($key, $conf); } catch (Exception $e) { $this->outputLine($e->getMessage()); @@ -224,19 +229,19 @@ public function configureCommand($key, $configfile = '', $settings = '') { } /** - * Fetch an extension from TER + * Fetch an extension from TER. + * + * @param string $key extension key + * @param string $version the exact version of the extension, otherwise the latest will be picked + * @param string $location where to put the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext + * @param bool $overwrite overwrite the extension if it already exists + * @param string $mirror mirror to fetch the extension from, otherwise a random mirror will be selected * - * @param string $key extension key - * @param string $version the exact version of the extension, otherwise the latest will be picked - * @param string $location where to put the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext - * @param bool $overwrite overwrite the extension if it already exists - * @param string $mirror mirror to fetch the extension from, otherwise a random mirror will be selected * @return void */ public function fetchCommand($key, $version = '', $location = 'L', $overwrite = FALSE, $mirror = '') { try { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); + $service = $this->getService(); $data = $service->fetchExtension($key, $version, $location, $overwrite, $mirror); $this->outputLine(sprintf('Extension "%s" version %s has been fetched from repository!', $data['extKey'], $data['version'])); } catch (Exception $e) { @@ -246,20 +251,19 @@ public function fetchCommand($key, $version = '', $location = 'L', $overwrite = } /** - * Import extension from file + * Import extension from file. * - * @param string $file path to t3x file - * @param string $location where to import the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext + * @param string $file path to t3x file + * @param string $location where to import the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext * @param boolean $overwrite overwrite the extension if it already exists + * * @return void */ public function importCommand($file, $location = 'L', $overwrite = FALSE) { try { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); + $service = $this->getService(); $data = $service->importExtension($file, $location, $overwrite); $this->outputLine(sprintf('Extension "%s" has been imported!', $data['extKey'])); - } catch (Exception $e) { $this->outputLine($e->getMessage()); $this->quit(); @@ -267,13 +271,12 @@ public function importCommand($file, $location = 'L', $overwrite = FALSE) { } /** - * createUploadFoldersCommand + * Creates the upload folders of an extension. * * @return void */ public function createUploadFoldersCommand() { - /** @var $service Tx_Coreapi_Service_ExtensionApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_ExtensionApiService'); + $service = $this->getService(); $messages = $service->createUploadFolders(); if (sizeof($messages)) { @@ -284,6 +287,13 @@ public function createUploadFoldersCommand() { $this->outputLine('no uploadFolder created'); } } -} -?> \ No newline at end of file + /** + * Returns the service object. + * + * @return \Etobi\CoreAPI\Service\ExtensionApiService object + */ + private function getService() { + return $this->objectManager->get('Etobi\\CoreAPI\\Service\\ExtensionApiService'); + } +} \ No newline at end of file diff --git a/Classes/Command/SiteApiCommandController.php b/Classes/Command/SiteApiCommandController.php index b965830..7f6f1c3 100644 --- a/Classes/Command/SiteApiCommandController.php +++ b/Classes/Command/SiteApiCommandController.php @@ -1,8 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -21,25 +24,24 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use TYPO3\CMS\Extbase\Mvc\Controller\CommandController; /** * Site API Command Controller * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Command_SiteApiCommandController extends Tx_Extbase_MVC_Controller_CommandController { +class SiteApiCommandController extends CommandController { /** - * Site info - * - * Basic information about the system + * Basic information about the system. * * @return void */ public function infoCommand() { - /** @var $service Tx_Coreapi_Service_SiteApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_SiteApiService'); + $service = $this->getService(); $data = $service->getSiteInfo(); foreach ($data as $key => $value) { @@ -49,19 +51,24 @@ public function infoCommand() { } /** - * Create a sys news - * - * Sys news record is displayed at the login page + * Sys news record is displayed at the login page. * * @param string $header Header text - * @param string $text Basic text + * @param string $text Basic text + * * @return void */ public function createSysNewsCommand($header, $text = '') { - /** @var $service Tx_Coreapi_Service_SiteApiService */ - $service = $this->objectManager->get('Tx_Coreapi_Service_SiteApiService'); + $service = $this->getService(); $service->createSysNews($header, $text); } -} -?> \ No newline at end of file + /** + * Returns the service object. + * + * @return \Etobi\CoreAPI\Service\SiteApiService object + */ + private function getService() { + return $this->objectManager->get('Etobi\\CoreAPI\\Service\\SiteApiService'); + } +} \ No newline at end of file diff --git a/Classes/Service/CacheApiService.php b/Classes/Service/CacheApiService.php index 7d8b38b..f1f011e 100644 --- a/Classes/Service/CacheApiService.php +++ b/Classes/Service/CacheApiService.php @@ -1,8 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -21,66 +24,72 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Cache API service * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Service_CacheApiService { +class CacheApiService { /** - * @var t3lib_TCEmain + * @var \TYPO3\CMS\Core\DataHandling\DataHandler */ protected $tce; /** + * Initialize the object. * + * @return void */ public function initializeObject() { // Create a fake admin user - $adminUser = new t3lib_beUserAuth(); + $adminUser = new BackendUserAuthentication(); $adminUser->user['uid'] = $GLOBALS['BE_USER']->user['uid']; $adminUser->user['username'] = '_CLI_lowlevel'; $adminUser->user['admin'] = 1; $adminUser->workspace = 0; - $this->tce = t3lib_div::makeInstance('t3lib_TCEmain'); + $this->tce = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\DataHandling\\DataHandler'); $this->tce->start(Array(), Array()); $this->tce->start(Array(), Array(), $adminUser); } /** - * Clear all caches + * Clear all caches. + * + * @return void */ public function clearAllCaches() { - if (version_compare(TYPO3_version, '6.0.0', '<')) { - t3lib_extMgm::removeCacheFiles(); - } $this->tce->clear_cacheCmd('all'); } /** + * Clear the page cache. * + * @return void */ public function clearPageCache() { $this->tce->clear_cacheCmd('pages'); } /** + * Clears the configuration cache. * + * @return void */ public function clearConfigurationCache() { - if (version_compare(TYPO3_version, '6.0.0', '<')) { - t3lib_extMgm::removeCacheFiles(); - } $this->tce->clear_cacheCmd('temp_cached'); } /** * Clear all caches except the page cache. - * This is especially useful on big sites when you can't just drop the page cache + * This is especially useful on big sites when you can't just drop the page cache. * * @return array with list of cleared caches */ @@ -104,5 +113,3 @@ public function clearAllExceptPageCache() { return $toBeFlushed; } } - -?> diff --git a/Classes/Service/DatabaseApiService.php b/Classes/Service/DatabaseApiService.php index cefe97d..d68cd7b 100644 --- a/Classes/Service/DatabaseApiService.php +++ b/Classes/Service/DatabaseApiService.php @@ -1,9 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -22,14 +24,18 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use InvalidArgumentException; +use TYPO3\CMS\Core\Cache\Cache; +use TYPO3\CMS\Core\Utility\GeneralUtility; /** * DB API service * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Service_DatabaseApiService { +class DatabaseApiService { const ACTION_UPDATE_CLEAR_TABLE = 1; const ACTION_UPDATE_ADD = 2; const ACTION_UPDATE_CHANGE = 3; @@ -40,41 +46,36 @@ class Tx_Coreapi_Service_DatabaseApiService { const ACTION_REMOVE_DROP_TABLE = 8; /** - * @var t3lib_install_Sql Instance of SQL handler + * @var \TYPO3\CMS\Install\Service\SqlSchemaMigrationService Instance of SQL handler */ protected $sqlHandler = NULL; /** - * Constructor function + * Constructor function. */ public function __construct() { - if (class_exists('TYPO3\\CMS\\Install\\Sql\\SchemaMigrator')) { - $this->sqlHandler = t3lib_div::makeInstance('TYPO3\\CMS\\Install\\Sql\\SchemaMigrator'); - } elseif (class_exists('t3lib_install_Sql')) { - $this->sqlHandler = t3lib_div::makeInstance('t3lib_install_Sql'); - } elseif (class_exists('t3lib_install')) { - $this->sqlHandler = t3lib_div::makeInstance('t3lib_install'); - } + $this->sqlHandler = GeneralUtility::makeInstance('TYPO3\\CMS\\Install\\Sql\\SchemaMigrator'); } /** - * Database compare + * Database compare. * * @param string $actions comma separated list of IDs - * @return array + * * @throws InvalidArgumentException + * @return array */ public function databaseCompare($actions) { $errors = array(); - $availableActions = array_flip(t3lib_div::makeInstance('Tx_Extbase_Reflection_ClassReflection', 'Tx_Coreapi_Service_DatabaseApiService')->getConstants()); + $availableActions = array_flip(GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', 'Etobi\\CoreAPI\\Service\\DatabaseApiService')->getConstants()); if (empty($actions)) { throw new InvalidArgumentException('No compare modes defined'); } $allowedActions = array(); - $actionSplit = t3lib_div::trimExplode(',', $actions); + $actionSplit = GeneralUtility::trimExplode(',', $actions); foreach ($actionSplit as $split) { if (!isset($availableActions[$split])) { throw new InvalidArgumentException(sprintf('Action "%s" is not available!', $split)); @@ -83,17 +84,17 @@ public function databaseCompare($actions) { } - $tblFileContent = t3lib_div::getUrl(PATH_t3lib . 'stddb/tables.sql'); + $tblFileContent = GeneralUtility::getUrl(PATH_t3lib . 'stddb/tables.sql'); foreach ($GLOBALS['TYPO3_LOADED_EXT'] as $loadedExtConf) { if (is_array($loadedExtConf) && $loadedExtConf['ext_tables.sql']) { - $extensionSqlContent = t3lib_div::getUrl($loadedExtConf['ext_tables.sql']); + $extensionSqlContent = GeneralUtility::getUrl($loadedExtConf['ext_tables.sql']); $tblFileContent .= LF . LF . LF . LF . $extensionSqlContent; } } - if (is_callable('t3lib_cache::getDatabaseTableDefinitions')) { - $tblFileContent .= t3lib_cache::getDatabaseTableDefinitions(); + if (is_callable('TYPO3\\CMS\\Core\\Cache\\Cache::getDatabaseTableDefinitions')) { + $tblFileContent .= Cache::getDatabaseTableDefinitions(); } if (class_exists('TYPO3\\CMS\\Core\\Category\\CategoryRegistry')) { @@ -160,14 +161,15 @@ public function databaseCompare($actions) { } /** - * Get all available actions + * Get all available actions. + * * @return array */ public function databaseCompareAvailableActions() { - $availableActions = array_flip(t3lib_div::makeInstance('Tx_Extbase_Reflection_ClassReflection', 'Tx_Coreapi_Service_DatabaseApiService')->getConstants()); + $availableActions = array_flip(GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', 'Etobi\\CoreAPI\\Service\\DatabaseApiService')->getConstants()); foreach ($availableActions as $number => $action) { - if (!t3lib_div::isFirstPartOfStr($action, 'ACTION_')) { + if (!GeneralUtility::isFirstPartOfStr($action, 'ACTION_')) { unset($availableActions[$number]); } } @@ -175,10 +177,11 @@ public function databaseCompareAvailableActions() { } /** - * Get all request keys, even for those requests which are not used + * Get all request keys, even for those requests which are not used. * * @param array $update * @param array $remove + * * @return array */ protected function getRequestKeys(array $update, array $remove) { @@ -207,7 +210,4 @@ protected function getRequestKeys(array $update, array $remove) { } return $finalKeys; } - -} - -?> \ No newline at end of file +} \ No newline at end of file diff --git a/Classes/Service/ExtensionApiService.php b/Classes/Service/ExtensionApiService.php index 37b2edd..da6aae0 100644 --- a/Classes/Service/ExtensionApiService.php +++ b/Classes/Service/ExtensionApiService.php @@ -1,8 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -21,14 +24,19 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use InvalidArgumentException; +use RuntimeException; +use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Extension API service * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Service_ExtensionApiService { +class ExtensionApiService { /* * some ExtensionManager Objects require public access to these objects @@ -45,12 +53,23 @@ class Tx_Coreapi_Service_ExtensionApiService { /** @var tx_em_Extensions_Details */ public $extensionDetails; + /** @var $configurationManager \TYPO3\CMS\Core\Configuration\ConfigurationManager */ + protected $configurationManager; + + /** + * The Constructor. + */ + public function __construct() { + $this->configurationManager = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Configuration\\ConfigurationManager'); + } + /** - * Get information about an extension + * Get information about an extension. * * @param string $extensionKey extension key - * @return array + * * @throws InvalidArgumentException + * @return array */ public function getExtensionInformation($extensionKey) { if (strlen($extensionKey) === 0) { @@ -60,21 +79,22 @@ public function getExtensionInformation($extensionKey) { throw new InvalidArgumentException(sprintf('Extension "%s" not found!', $extensionKey)); } - include_once(t3lib_extMgm::extPath($extensionKey) . 'ext_emconf.php'); + include_once(ExtensionManagementUtility::extPath($extensionKey) . 'ext_emconf.php'); $information = array( 'em_conf' => $EM_CONF[''], - 'is_installed' => t3lib_extMgm::isLoaded($extensionKey) + 'is_installed' => ExtensionManagementUtility::isLoaded($extensionKey) ); return $information; } /** - * Get array of installed extensions + * Get array of installed extensions. * * @param string $type L, S, G or empty (for all) - * @return array + * * @throws InvalidArgumentException + * @return array */ public function getInstalledExtensions($type = '') { $type = strtoupper($type); @@ -90,7 +110,7 @@ public function getInstalledExtensions($type = '') { continue; } - include_once(t3lib_extMgm::extPath($key) . 'ext_emconf.php'); + include_once(ExtensionManagementUtility::extPath($key) . 'ext_emconf.php'); $list[$key] = $EM_CONF['']; } @@ -99,14 +119,14 @@ public function getInstalledExtensions($type = '') { } /** - * Update the mirrors, using the scheduler task of EXT:em + * Update the mirrors, using the scheduler task of EXT:em. * - * @return void * @see tx_em_Tasks_UpdateExtensionList * @throws RuntimeException + * @return void */ public function updateMirrors() { - if (t3lib_div::compat_version('6.0.0')) { + if (version_compare(TYPO3_version, '4.7.0', '>')) { throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); } @@ -118,64 +138,49 @@ public function updateMirrors() { // update all repositories foreach ($repositories as $repository) { - /* @var $objRepository tx_em_Repository */ - $objRepository = t3lib_div::makeInstance('tx_em_Repository', $repository['uid']); - /* @var $objRepositoryUtility tx_em_Repository_Utility */ - $objRepositoryUtility = t3lib_div::makeInstance('tx_em_Repository_Utility', $objRepository); + /* @var $objRepository \TYPO3\CMS\Extensionmanager\Domain\Model\Repository */ + $objRepository = GeneralUtility::makeInstance('TYPO3\\CMS\\Extensionmanager\\Domain\\Model\\Repository', $repository['uid']); + /* @var $objRepositoryUtility \TYPO3\CMS\Extensionmanager\Utility\Repository\Helper */ + $objRepositoryUtility = GeneralUtility::makeInstance('TYPO3\\CMS\\Extensionmanager\\Utility\\Repository\\Helper', $objRepository); $count = $objRepositoryUtility->updateExtList(FALSE); unset($objRepository, $objRepositoryUtility); } } /** - * createUploadFolders + * Creates the upload folders of an extension. * * @return array */ public function createUploadFolders() { $extensions = $this->getInstalledExtensions(); - // 6.0 creates also Dirs + // 6.2 creates also Dirs + $result = array(); if (class_exists('\TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility')) { - $fileHandlingUtility = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility'); + $fileHandlingUtility = GeneralUtility::makeInstance('TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility'); foreach ($extensions AS $key => $extension) { $extension['key'] = $key; $fileHandlingUtility->ensureConfiguredDirectoriesExist($extension); } - return array('done with \TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility->ensureConfiguredDirectoriesExist'); - } - - // < 6.0 creates no Dirs - $messages = array(); - foreach ($extensions as $extKey => $extInfo) { - $uploadFolder = PATH_site . tx_em_Tools::uploadFolder($extKey); - if ($extInfo['uploadfolder'] && !@is_dir($uploadFolder)) { - t3lib_div::mkdir($uploadFolder); - $messages[] = 'mkdir ' . $uploadFolder; - $indexContent = ' - - - - - - '; - t3lib_div::writeFile($uploadFolder . 'index.html', $indexContent); - } + $result[] = 'done with \TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility->ensureConfiguredDirectoriesExist'; } - return $messages; + + return $result; } /** - * Install (load) an extension + * Install (load) an extension. * * @param string $extensionKey extension key - * @return void + * * @throws RuntimeException * @throws InvalidArgumentException + * @return void */ public function installExtension($extensionKey) { - if (t3lib_div::compat_version('6.0.0')) { + if (version_compare(TYPO3_version, '4.7.0', '>')) { throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); } @@ -185,12 +190,13 @@ public function installExtension($extensionKey) { } // check if extension is already loaded - if (t3lib_extMgm::isLoaded($extensionKey)) { + if (ExtensionManagementUtility::isLoaded($extensionKey)) { throw new InvalidArgumentException(sprintf('Extension "%s" already installed!', $extensionKey)); } // check if localconf.php is writable - if (!t3lib_extMgm::isLocalconfWritable()) { + + if (!$this->configurationManager->canWriteConfiguration()) { throw new RuntimeException('Localconf.php is not writeable!'); } @@ -218,15 +224,16 @@ public function installExtension($extensionKey) { } /** - * Uninstall (unload) an extension + * Uninstall (unload) an extension. * * @param string $extensionKey extension key - * @return void + * * @throws RuntimeException * @throws InvalidArgumentException + * @return void */ public function uninstallExtension($extensionKey) { - if (t3lib_div::compat_version('6.0.0')) { + if (version_compare(TYPO3_version, '4.7.0', '>')) { throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); } @@ -241,18 +248,18 @@ public function uninstallExtension($extensionKey) { } // check if extension is loaded - if (!t3lib_extMgm::isLoaded($extensionKey)) { + if (!ExtensionManagementUtility::isLoaded($extensionKey)) { throw new InvalidArgumentException(sprintf('Extension "%s" is not installed!', $extensionKey)); } // check if this is a required extension (such as "cms") that cannot be uninstalled - $requiredExtList = t3lib_div::trimExplode(',', t3lib_extMgm::getRequiredExtensionList()); + $requiredExtList = GeneralUtility::trimExplode(',', REQUIRED_EXTENSIONS); if (in_array($extensionKey, $requiredExtList)) { throw new InvalidArgumentException(sprintf('Extension "%s" is a required extension and cannot be uninstalled!', $extensionKey)); } // check if localconf.php is writable - if (!t3lib_extMgm::isLocalconfWritable()) { + if (!$this->configurationManager->canWriteConfiguration()) { throw new RuntimeException('Localconf.php is not writeable!'); } @@ -274,18 +281,19 @@ public function uninstallExtension($extensionKey) { } /** - * Configure an extension + * Configure an extension. + * + * @param string $extensionKey The extension key + * @param array $extensionConfiguration * - * @param string $extensionKey extension key - * @param array $extensionConfiguration - * @return void * @throws RuntimeException * @throws InvalidArgumentException + * @return void */ public function configureExtension($extensionKey, $extensionConfiguration = array()) { global $TYPO3_CONF_VARS; - if (t3lib_div::compat_version('6.0.0')) { + if (version_compare(TYPO3_version, '4.7.0', '>')) { throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); } @@ -295,12 +303,12 @@ public function configureExtension($extensionKey, $extensionConfiguration = arra } // check if extension is loaded - if (!t3lib_extMgm::isLoaded($extensionKey)) { + if (!ExtensionManagementUtility::isLoaded($extensionKey)) { throw new InvalidArgumentException(sprintf('Extension "%s" is not installed!', $extensionKey)); } // check if extension can be configured - $extAbsPath = t3lib_extMgm::extPath($extensionKey); + $extAbsPath = ExtensionManagementUtility::extPath($extensionKey); $extConfTemplateFile = $extAbsPath . 'ext_conf_template.txt'; if (!file_exists($extConfTemplateFile)) { @@ -313,12 +321,12 @@ public function configureExtension($extensionKey, $extensionConfiguration = arra } // Load tsStyleConfig class and parse configuration template: - $extRelPath = t3lib_extmgm::extRelPath($extensionKey); + $extRelPath = ExtensionManagementUtility::extRelPath($extensionKey); - $tsStyleConfig = t3lib_div::makeInstance('t3lib_tsStyleConfig'); + $tsStyleConfig = GeneralUtility::makeInstance('t3lib_tsStyleConfig'); $tsStyleConfig->doNotSortCategoriesBeforeMakingForm = TRUE; $constants = $tsStyleConfig->ext_initTSstyleConfig( - t3lib_div::getUrl($extConfTemplateFile), + GeneralUtility::getUrl($extConfTemplateFile), $extRelPath, $extAbsPath, $GLOBALS['BACK_PATH'] @@ -366,19 +374,20 @@ public function configureExtension($extensionKey, $extensionConfiguration = arra } /** - * Fetch an extension from TER + * Fetch an extension from TER. * - * @param $extensionKey + * @param string $extensionKey * @param string $version * @param string $location - * @param bool $overwrite + * @param bool $overwrite * @param string $mirror - * @return array + * * @throws RuntimeException * @throws InvalidArgumentException + * @return array */ public function fetchExtension($extensionKey, $version = '', $location = 'L', $overwrite = FALSE, $mirror = '') { - if (t3lib_div::compat_version('6.0.0')) { + if (version_compare(TYPO3_version, '4.7.0', '>')) { throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); } @@ -426,16 +435,16 @@ public function fetchExtension($extensionKey, $version = '', $location = 'L', $o // get mirrors $mirrors = array(); - $mirrorsTmpFile = t3lib_div::tempnam('mirrors'); - $mirrorsFile = t3lib_div::getUrl($GLOBALS['TYPO3_CONF_VARS']['EXT']['em_mirrorListURL'], 0); + $mirrorsTmpFile = GeneralUtility::tempnam('mirrors'); + $mirrorsFile = GeneralUtility::getUrl($GLOBALS['TYPO3_CONF_VARS']['EXT']['em_mirrorListURL'], 0); if ($mirrorsFile === FALSE) { - t3lib_div::unlink_tempfile($mirrorsTmpFile); + GeneralUtility::unlink_tempfile($mirrorsTmpFile); throw new RuntimeException('Could not retrieve the list of mirrors!'); } else { - t3lib_div::writeFile($mirrorsTmpFile, $mirrorsFile); + GeneralUtility::writeFile($mirrorsTmpFile, $mirrorsFile); $mirrorsXml = implode('', gzfile($mirrorsTmpFile)); - t3lib_div::unlink_tempfile($mirrorsTmpFile); + GeneralUtility::unlink_tempfile($mirrorsTmpFile); $mirrors = $this->xmlHandler->parseMirrorsXML($mirrorsXml); } @@ -474,15 +483,21 @@ public function fetchExtension($extensionKey, $version = '', $location = 'L', $o } /** - * Imports extension from file + * Imports extension from file. * - * @param string $file path to t3x file - * @param string $location where to import the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext - * @param bool $overwrite overwrite the extension if it already exists - * @return void - * @throws InvalidArgumentException + * @param string $file path to t3x file + * @param string $location where to import the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext + * @param bool $overwrite overwrite the extension if it already exists + * + * @throws \RuntimeException + * @throws \InvalidArgumentException + * @return array */ public function importExtension($file, $location = 'L', $overwrite = FALSE) { + if (version_compare(TYPO3_version, '4.7.0', '>')) { + throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); + } + $return = array(); if (!is_file($file)) { throw new InvalidArgumentException(sprintf('File "%s" does not exist!', $file)); @@ -501,7 +516,7 @@ public function importExtension($file, $location = 'L', $overwrite = FALSE) { throw new InvalidArgumentException(sprintf('Unknown location "%s"!', $location)); } - $fileContent = t3lib_div::getUrl($file); + $fileContent = GeneralUtility::getUrl($file); if (!$fileContent) { throw new InvalidArgumentException(sprintf('File "%s" is empty!', $file)); } @@ -535,10 +550,11 @@ public function importExtension($file, $location = 'L', $overwrite = FALSE) { /** - * Check if an extension exists + * Check if an extension exists. * * @param string $extensionKey extension key - * @return void + * + * @return boolean */ protected function extensionExists($extensionKey) { $this->initializeExtensionManagerObjects(); @@ -554,32 +570,34 @@ protected function extensionExists($extensionKey) { } /** - * initialize ExtensionManager Objects + * Initialize ExtensionManager Objects. + * + * @return void */ protected function initializeExtensionManagerObjects() { - $this->xmlHandler = t3lib_div::makeInstance('tx_em_Tools_XmlHandler'); - $this->extensionList = t3lib_div::makeInstance('tx_em_Extensions_List', $this); - $this->terConnection = t3lib_div::makeInstance('tx_em_Connection_Ter', $this); - $this->extensionDetails = t3lib_div::makeInstance('tx_em_Extensions_Details', $this); + $this->xmlHandler = GeneralUtility::makeInstance('tx_em_Tools_XmlHandler'); + $this->extensionList = GeneralUtility::makeInstance('tx_em_Extensions_List', $this); + $this->terConnection = GeneralUtility::makeInstance('tx_em_Connection_Ter', $this); + $this->extensionDetails = GeneralUtility::makeInstance('tx_em_Extensions_Details', $this); } /** * @return tx_em_Install */ protected function getEmInstall() { - $install = t3lib_div::makeInstance('tx_em_Install', $this); + $install = GeneralUtility::makeInstance('tx_em_Install', $this); $install->setSilentMode(TRUE); return $install; } /** - * Clear the caches + * Clear the caches. + * + * @return void */ protected function clearCaches() { - $cacheApiService = t3lib_div::makeInstance('Tx_Coreapi_Service_CacheApiService'); + $cacheApiService = GeneralUtility::makeInstance('Etobi\\CoreAPI\\Service\\CacheApiService'); $cacheApiService->initializeObject(); $cacheApiService->clearAllCaches(); } -} - -?> \ No newline at end of file +} \ No newline at end of file diff --git a/Classes/Service/SiteApiService.php b/Classes/Service/SiteApiService.php index d363a98..b406d28 100644 --- a/Classes/Service/SiteApiService.php +++ b/Classes/Service/SiteApiService.php @@ -1,8 +1,11 @@ + * (c) 2014 Stefano Kowalke * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -21,17 +24,20 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use InvalidArgumentException; +use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Site API service * - * @package TYPO3 - * @subpackage tx_coreapi + * @author Georg Ringer + * @author Stefano Kowalke + * @package Etobi\CoreAPI\Service\SiteApiService */ -class Tx_Coreapi_Service_SiteApiService { +class SiteApiService { /** - * Get some basic site information + * Get some basic site information. * * @return array */ @@ -49,12 +55,13 @@ public function getSiteInfo() { } /** - * Create a sys news record + * Create a sys news record. * * @param string $header header - * @param string $text text - * @return void + * @param string $text text + * * @throws InvalidArgumentException + * @return void */ public function createSysNews($header, $text) { if (strlen($header) === 0) { @@ -70,10 +77,11 @@ public function createSysNews($header, $text) { } /** - * Get disku usage + * Get disk usage. * * @author Claus Due , Wildside A/S * @param array $data + * * @return void */ protected function getDiskUsage(&$data) { @@ -83,10 +91,11 @@ protected function getDiskUsage(&$data) { } /** - * Get database size + * Get database size. * * @author Claus Due , Wildside A/S * @param array $data + * * @return void */ protected function getDatabaseInformation(&$data) { @@ -98,18 +107,16 @@ protected function getDatabaseInformation(&$data) { } /** - * Get count of local installed extensions + * Get count of local installed extensions. * * @param array $data + * * @return void */ protected function getCountOfExtensions(&$data) { - /** @var Tx_Coreapi_Service_ExtensionApiService $extensionService */ - $extensionService = t3lib_div::makeInstance('Tx_Coreapi_Service_ExtensionApiService'); + /** @var \Etobi\CoreAPI\Service\ExtensionApiService $extensionService */ + $extensionService = GeneralUtility::makeInstance('Etobi\\CoreAPI\\Service\\ExtensionApiService'); $extensions = $extensionService->getInstalledExtensions('L'); $data['Count local installed extensions'] = count($extensions); } - -} - -?> \ No newline at end of file +} \ No newline at end of file diff --git a/Scripts/Cli.php b/Scripts/Cli.php deleted file mode 100644 index 6432181..0000000 --- a/Scripts/Cli.php +++ /dev/null @@ -1,10 +0,0 @@ -start(); -} else { - die('This script must be included by the "CLI module dispatcher"'); -} - -?> diff --git a/ext_autoload.php b/ext_autoload.php index 4969edd..221b0e9 100644 --- a/ext_autoload.php +++ b/ext_autoload.php @@ -1,17 +1,14 @@ $extensionClassesPath . 'Command/DatabaseApiCommandController.php', - 'tx_coreapi_command_siteapicommandcontroller' => $extensionClassesPath . 'Command/SiteApiCommandController.php', - 'tx_coreapi_command_cacheapicommandcontroller' => $extensionClassesPath . 'Command/CacheApiCommandController.php', - 'tx_coreapi_service_cacheapiservice' => $extensionClassesPath . 'Service/CacheApiService.php', - 'tx_coreapi_service_siteapiservice' => $extensionClassesPath . 'Service/SiteApiService.php', - 'tx_coreapi_service_databaseapiservice' => $extensionClassesPath . 'Service/DatabaseApiService.php', - 'tx_coreapi_service_extensionapiservice' => $extensionClassesPath . 'Service/ExtensionApiService.php', - 'tx_coreapi_cli_dispatcher' => $extensionClassesPath .'Cli/Dispatcher.php', -); - -?> \ No newline at end of file + 'Etobi\CoreAPI\Command\DatabaseApiCommandController' => $extensionClassesPath . 'Command/DatabaseApiCommandController.php', + 'Etobi\CoreAPI\Command\SiteApiCommandController' => $extensionClassesPath . 'Command/SiteApiCommandController.php', + 'Etobi\CoreAPI\Command\CacheApiCommandController' => $extensionClassesPath . 'Command/CacheApiCommandController.php', + 'Etobi\CoreAPI\Service\CacheApiService' => $extensionClassesPath . 'Service/CacheApiService.php', + 'Etobi\CoreAPI\Service\SiteApiService' => $extensionClassesPath . 'Service/SiteApiService.php', + 'Etobi\CoreAPI\Service\DatabaseApiService' => $extensionClassesPath . 'Service/DatabaseApiService.php', + 'Etobi\CoreAPI\Service\ExtensionApiService' => $extensionClassesPath . 'Service/ExtensionApiService.php' +); \ No newline at end of file diff --git a/ext_emconf.php b/ext_emconf.php index 6423fd0..16ecf3e 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -4,31 +4,29 @@ 'title' => 'coreapi', 'description' => 'coreapi', 'category' => 'plugin', - 'author' => 'Tobias Liebig,Georg Ringer', - 'author_email' => 'tobias.liebig@typo3.org,georg.ringer@cyberhouse.at', + 'author' => 'Tobias Liebig,Georg Ringer,Stefano Kowalke', + 'author_email' => 'tobias.liebig@typo3.org,georg.ringer@cyberhouse.at,blueduck@gmx.net', 'author_company' => '', 'shy' => '', 'priority' => '', 'module' => '', - 'state' => 'beta', + 'state' => 'alpha', 'internal' => '', 'uploadfolder' => '0', 'createDirs' => '', 'modify_tables' => '', 'clearCacheOnLoad' => 0, 'lockType' => '', - 'version' => '0.0.1', + 'version' => '1.0.0', 'constraints' => array( 'depends' => array( - 'typo3' => '4.5.0-6.1.99', - 'extbase' => '1.3.0-6.1.99', - 'fluid' => '1.3.0-6.1.99', + 'typo3' => '6.2.0-6.2.99', + 'extbase' => '6.2.0-6.2.99', + 'fluid' => '6.2.0-6.2.99', ), 'conflicts' => array( ), 'suggests' => array( ), ), -); - -?> \ No newline at end of file +); \ No newline at end of file diff --git a/ext_localconf.php b/ext_localconf.php index 27117c8..da4611c 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -3,15 +3,13 @@ if (TYPO3_MODE === 'BE') { // Register commands - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Tx_Coreapi_Command_DatabaseApiCommandController'; - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Tx_Coreapi_Command_CacheApiCommandController'; - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Tx_Coreapi_Command_SiteApiCommandController'; - $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Tx_Coreapi_Command_ExtensionApiCommandController'; + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Etobi\CoreAPI\Command\DatabaseApiCommandController'; + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Etobi\CoreAPI\Command\CacheApiCommandController'; + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Etobi\CoreAPI\Command\SiteApiCommandController'; + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Etobi\CoreAPI\Command\ExtensionApiCommandController'; } // Register the CLI dispatcher $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['cliKeys'][$_EXTKEY] = array( 'EXT:' . $_EXTKEY . '/Scripts/Cli.php', '_CLI_lowlevel' -); - -?> \ No newline at end of file +); \ No newline at end of file From 4661c159767decb481a389d0107d0e2b00adad1d Mon Sep 17 00:00:00 2001 From: Stefano Kowalke Date: Sat, 7 Jun 2014 00:04:30 +0200 Subject: [PATCH 3/4] [WIP] Make the ExtensionAPI compatible to TYPO3 CMS 6.2 * updateList * fetch * listMirrors * import * add Unit Tests * vfsStream is necessary for Unit Tests. But we don't add this as a dependency to the extension. Instead we use the one which is defined in cores composer file. For installation copy the composer.json from the core package to your webroot and execute `composer install`. This will install some dependencies into Packages/Libraries. * Add Travis CI configuration file and status image * Add Scrutinizer configuration and status images * Update README.md Resolve #51 Resolve #55 Resolve #69 Resolve #72 Resolve #73 Resolve #74 Resolve #76 Resolve #77 Resolve #78 Resolve #79 Resolve #80 Resolve #82 Resolve #83 Resolve #84 Resolve #85 --- .scrutinizer.yml | 49 + .travis.yml | 52 + Classes/Command/CacheApiCommandController.php | 39 +- .../Command/ExtensionApiCommandController.php | 129 +- Classes/Command/SiteApiCommandController.php | 43 +- Classes/Service/CacheApiService.php | 45 +- Classes/Service/DatabaseApiService.php | 127 +- Classes/Service/ExtensionApiService.php | 695 +++--- Classes/Service/SiteApiService.php | 68 +- .../ExtensionApi/ExtensionApiFetch.graphml | 364 ++++ .../Images/ExtensionApi/ExtensionApiFetch.png | Bin 0 -> 40295 bytes .../Images/ExtensionApi/uploadfolder.graphml | 233 ++ .../Images/ExtensionApi/uploadfolder.png | Bin 0 -> 25170 bytes README.md | 32 +- .../path/to/importfolder/realurl_1.12.8.t3x | Bin 0 -> 128603 bytes Tests/Unit/Service/CacheApiServiceTest.php | 127 ++ Tests/Unit/Service/DatabaseApiServiceTest.php | 165 ++ .../Unit/Service/ExtensionApiServiceTest.php | 1926 +++++++++++++++++ Tests/Unit/Service/SiteApiServiceTest.php | 321 +++ composer.json | 10 +- ext_autoload.php | 3 +- ext_emconf.php | 2 +- ext_localconf.php | 2 +- 23 files changed, 3895 insertions(+), 537 deletions(-) create mode 100644 .scrutinizer.yml create mode 100644 .travis.yml create mode 100644 Documentation/Images/ExtensionApi/ExtensionApiFetch.graphml create mode 100644 Documentation/Images/ExtensionApi/ExtensionApiFetch.png create mode 100644 Documentation/Images/ExtensionApi/uploadfolder.graphml create mode 100644 Documentation/Images/ExtensionApi/uploadfolder.png create mode 100644 Tests/Unit/Resources/vfsStream/importCommand/path/to/importfolder/realurl_1.12.8.t3x create mode 100644 Tests/Unit/Service/CacheApiServiceTest.php create mode 100644 Tests/Unit/Service/DatabaseApiServiceTest.php create mode 100644 Tests/Unit/Service/ExtensionApiServiceTest.php create mode 100644 Tests/Unit/Service/SiteApiServiceTest.php diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..4d0ec2f --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,49 @@ +filter: + excluded_paths: + - 'Resources/Public/vendor/*' + - 'Tests/*' + +tools: + + external_code_coverage: + timeout: 700 + + php_sim: true + + php_code_sniffer: + enabled: true + command: phpcs -n + config: + standard: TYPO3CMS + + php_cs_fixer: false + + php_mess_detector: + enabled: true + config: + code_size_rules: + cyclomatic_complexity: true + npath_complexity: true + excessive_class_complexity: true + controversial_rules: + superglobals: false + + php_pdepend: true + + php_analyzer: + enabled: true + config: + basic_semantic_checks: + enabled: true + property_on_interface: true + missing_abstract_methods: true + deprecation_checks: + enabled: true + simplify_boolean_return: + enabled: true + metrics_lack_of_cohesion_methods: + enabled: true + dead_assignments: + enabled: true + + sensiolabs_security_checker: true \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..656f1bb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,52 @@ +language: php +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + +env: + - DB=mysql TYPO3=coreapi/develop INTEGRATION=master COVERAGE=0 + +services: + - memcached + - redis-server + +notifications: + email: + - blueduck@gmx.net + slack: + rooms: + - typo3:DHkQdCNWc6x2znPAYA5T2LXO#gsoc-coreapi + +before_script: + # Get latest git version cause of travis issues (https://github.com/travis-ci/travis-ci/issues/1710) + - sudo apt-get update && sudo apt-get install git parallel tree + - > + if [[ "$TRAVIS_PHP_VERSION" = "5.3" || "$TRAVIS_PHP_VERSION" = "5.4" ]]; then + echo "extension = apc.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; + echo "apc.enable_cli=1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; + echo "apc.slam_defense=0" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; + pecl install igbinary > /dev/null; + fi + - echo "extension = memcache.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - echo "extension = redis.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - cd .. + - git clone --single-branch --branch $INTEGRATION --depth 1 git://github.com/TYPO3-coreapi/TYPO3-Travis-Integration.git build-environment + - source build-environment/install-helper.sh + - git clone --single-branch --branch $TYPO3 --depth 1 https://github.com/TYPO3-coreapi/TYPO3CMS.git core + - mv core/* . + - composer self-update + - composer --dev install + - mkdir -p fileadmin uploads typo3temp typo3conf/ext/ + - mv ext-coreapi typo3conf/ext/coreapi + +script: + - phpLint typo3conf/ext/coreapi + - > + echo; + echo "Running unit tests"; + ./bin/phpunit --colors --coverage-clover=coverage.clover -c typo3/sysext/core/Build/UnitTests.xml typo3conf/ext/coreapi/Tests/Unit/ + - wget https://scrutinizer-ci.com/ocular.phar + - cp -R typo3conf/ext/coreapi/.git . + - php ocular.phar code-coverage:upload --format=php-clover coverage.clover \ No newline at end of file diff --git a/Classes/Command/CacheApiCommandController.php b/Classes/Command/CacheApiCommandController.php index 2954fb3..1a3bf80 100644 --- a/Classes/Command/CacheApiCommandController.php +++ b/Classes/Command/CacheApiCommandController.php @@ -35,15 +35,27 @@ */ class CacheApiCommandController extends CommandController { + /** + * @var \Etobi\CoreAPI\Service\CacheApiService + */ + protected $cacheApiService; + + /** + * Inject the CacheApiService + * + * @param \Etobi\CoreAPI\Service\CacheApiService $cacheApiService + */ + public function injectCacheApiService(\Etobi\CoreAPI\Service\CacheApiService $cacheApiService) { + $this->cacheApiService = $cacheApiService; + } + /** * Clear all caches. * * @return void */ public function clearAllCachesCommand() { - $service = $this->getService(); - $service->clearAllCaches(); - + $this->cacheApiService->clearAllCaches(); $this->outputLine('All caches have been cleared.'); } @@ -53,9 +65,7 @@ public function clearAllCachesCommand() { * @return void */ public function clearConfigurationCacheCommand() { - $service = $this->getService(); - $service->clearConfigurationCache(); - + $this->cacheApiService->clearConfigurationCache(); $this->outputLine('Configuration cache has been cleared.'); } @@ -65,9 +75,7 @@ public function clearConfigurationCacheCommand() { * @return void */ public function clearPageCacheCommand() { - $service = $this->getService(); - $service->clearPageCache(); - + $this->cacheApiService->clearPageCache(); $this->outputLine('Page cache has been cleared.'); } @@ -78,18 +86,7 @@ public function clearPageCacheCommand() { * @return void */ public function clearAllExceptPageCacheCommand() { - $service = $this->getService(); - $clearedCaches = $service->clearAllExceptPageCache(); - + $clearedCaches = $this->cacheApiService->clearAllExceptPageCache(); $this->outputLine('Cleared caches: ' . implode(', ', $clearedCaches)); } - - /** - * Returns the service object. - * - * @return \Etobi\CoreAPI\Service\CacheApiService object - */ - private function getService() { - return $this->objectManager->get('Etobi\\CoreAPI\\Service\\CacheApiService'); - } } \ No newline at end of file diff --git a/Classes/Command/ExtensionApiCommandController.php b/Classes/Command/ExtensionApiCommandController.php index 8299f4a..43fc7da 100644 --- a/Classes/Command/ExtensionApiCommandController.php +++ b/Classes/Command/ExtensionApiCommandController.php @@ -38,18 +38,29 @@ */ class ExtensionApiCommandController extends CommandController { + /** + * @var \Etobi\CoreAPI\Service\ExtensionApiService + * @inject + */ + protected $extensionApiService; + + /** + * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher + * @inject + */ + protected $signalSlotDispatcher; + /** * Information about an extension. * - * @param string $key extension key + * @param string $key The extension key * * @return void */ public function infoCommand($key) { $data = array(); try { - $service = $this->getService(); - $data = $service->getExtensionInformation($key); + $data = $this->extensionApiService->getExtensionInformation($key); } catch (Exception $e) { $this->outputLine($e->getMessage()); $this->quit(); @@ -101,18 +112,19 @@ public function infoCommand($key) { /** * List all installed extensions. * - * @param string $type Extension type, can either be L for local, S for system or G for global. Leave it empty for all + * @param string $type Extension type, can either be "Local", + * "System" or "Global". Leave it empty for all * * @return void */ public function listInstalledCommand($type = '') { - $type = strtoupper($type); - if (!empty($type) && $type !== 'L' && $type !== 'G' && $type !== 'S') { - $this->outputLine('Only "L", "S" and "G" are supported as type (or nothing)'); + $type = ucfirst(strtolower($type)); + if (!empty($type) && $type !== 'Local' && $type !== 'Global' && $type !== 'System') { + $this->outputLine('Only "Local", "System" and "Global" are supported as type (or nothing)'); $this->quit(); } - $extensions = $this->getService()->getInstalledExtensions($type); + $extensions = $this->extensionApiService->listExtensions($type); foreach ($extensions as $key => $details) { $title = $key . ' - ' . $details['version'] . '/' . $details['state']; @@ -131,23 +143,27 @@ public function listInstalledCommand($type = '') { * @return void */ public function updateListCommand() { - $service = $this->getService(); - $service->updateMirrors(); + $this->outputLine('This may take a while...'); + $result = $this->extensionApiService->updateMirrors(); - $this->outputLine('Extension list has been updated.'); + if ($result) { + $this->outputLine('Extension list has been updated.'); + } else { + $this->outputLine('Extension list already up-to-date.'); + } } /** * Install(activate) an extension. * - * @param string $key extension key + * @param string $key The extension key * * @return void */ public function installCommand($key) { try { - $service = $this->getService(); - $service->installExtension($key); + $this->emitPackagesMayHaveChangedSignal(); + $this->extensionApiService->installExtension($key); } catch (Exception $e) { $this->outputLine($e->getMessage()); $this->quit(); @@ -158,14 +174,13 @@ public function installCommand($key) { /** * UnInstall(deactivate) an extension. * - * @param string $key extension key + * @param string $key The extension key * * @return void */ public function uninstallCommand($key) { try { - $service = $this->getService(); - $service->uninstallExtension($key); + $this->extensionApiService->uninstallExtension($key); } catch (Exception $e) { $this->outputLine($e->getMessage()); $this->quit(); @@ -188,18 +203,17 @@ public function uninstallCommand($key) { * ./cli_dispatch.phpsh extbase extensionapi:configure rtehtmlarea --configfile=C:\rteconf.txt --settings="enableImages=1;allowStyleAttribute=0" * * - * @param string $key extension key - * @param string $configfile path to file containing configuration settings. Must be formatted as a standard ini-file - * @param string $settings string containing configuration settings separated on the form "k1=v1;k2=v2;" + * @param string $key The extension key + * @param string $configFile Path to file containing configuration settings. Must be formatted as a standard ini-file + * @param string $settings String containing configuration settings separated on the form "k1=v1;k2=v2;" * * @return void */ - public function configureCommand($key, $configfile = '', $settings = '') { + public function configureCommand($key, $configFile = '', $settings = '') { try { - $service = $this->getService(); $conf = array(); - if (is_file($configfile)) { - $conf = parse_ini_file($configfile); + if (is_file($configFile)) { + $conf = parse_ini_file($configFile); } if (strlen($settings)) { @@ -219,7 +233,7 @@ public function configureCommand($key, $configfile = '', $settings = '') { throw new InvalidArgumentException(sprintf('No configuration settings!', $key)); } - $service->configureExtension($key, $conf); + $this->extensionApiService->configureExtension($key, $conf); } catch (Exception $e) { $this->outputLine($e->getMessage()); @@ -231,19 +245,18 @@ public function configureCommand($key, $configfile = '', $settings = '') { /** * Fetch an extension from TER. * - * @param string $key extension key - * @param string $version the exact version of the extension, otherwise the latest will be picked - * @param string $location where to put the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext - * @param bool $overwrite overwrite the extension if it already exists - * @param string $mirror mirror to fetch the extension from, otherwise a random mirror will be selected + * @param string $key The extension key + * @param string $version The exact version of the extension, otherwise the latest will be picked + * @param string $location Where to put the extension. System = typo3/sysext, Global = typo3/ext, Local = typo3conf/ext + * @param bool $overwrite Overwrite the extension if already exists + * @param int $mirror Mirror to fetch the extension from. Run extensionapi:listmirrors to get the list of all available repositories, otherwise a random mirror will be selected * * @return void */ - public function fetchCommand($key, $version = '', $location = 'L', $overwrite = FALSE, $mirror = '') { + public function fetchCommand($key, $version = '', $location = 'Local', $overwrite = FALSE, $mirror = -1) { try { - $service = $this->getService(); - $data = $service->fetchExtension($key, $version, $location, $overwrite, $mirror); - $this->outputLine(sprintf('Extension "%s" version %s has been fetched from repository!', $data['extKey'], $data['version'])); + $data = $this->extensionApiService->fetchExtension($key, $version, $location, $overwrite, $mirror); + $this->outputLine(sprintf('Extension "%s" version %s has been fetched from repository! Dependencies were not resolved.', $data['main']['extKey'], $data['main']['version'])); } catch (Exception $e) { $this->outputLine($e->getMessage()); $this->quit(); @@ -251,19 +264,18 @@ public function fetchCommand($key, $version = '', $location = 'L', $overwrite = } /** - * Import extension from file. - * - * @param string $file path to t3x file - * @param string $location where to import the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext - * @param boolean $overwrite overwrite the extension if it already exists + * Lists the possible mirrors * * @return void */ - public function importCommand($file, $location = 'L', $overwrite = FALSE) { + public function listMirrorsCommand() { try { - $service = $this->getService(); - $data = $service->importExtension($file, $location, $overwrite); - $this->outputLine(sprintf('Extension "%s" has been imported!', $data['extKey'])); + $mirros = $this->extensionApiService->listMirrors(); + $key = 0; + foreach ($mirros as $mirror) { + $this->outputLine($key . ' = ' . $mirror['title'] . ' ' . $mirror['host']); + ++$key; + } } catch (Exception $e) { $this->outputLine($e->getMessage()); $this->quit(); @@ -271,29 +283,30 @@ public function importCommand($file, $location = 'L', $overwrite = FALSE) { } /** - * Creates the upload folders of an extension. + * Import extension from file. + * + * @param string $file Path to t3x file + * @param string $location Where to import the extension. System = typo3/sysext, Global = typo3/ext, Local = typo3conf/ext + * @param boolean $overwrite Overwrite the extension if already exists * * @return void */ - public function createUploadFoldersCommand() { - $service = $this->getService(); - $messages = $service->createUploadFolders(); - - if (sizeof($messages)) { - foreach ($messages as $message) { - $this->outputLine($message); - } - } else { - $this->outputLine('no uploadFolder created'); + public function importCommand($file, $location = 'Local', $overwrite = FALSE) { + try { + $data = $this->extensionApiService->importExtension($file, $location, $overwrite); + $this->outputLine(sprintf('Extension "%s" has been imported!', $data['extKey'])); + } catch (Exception $e) { + $this->outputLine($e->getMessage()); + $this->quit(); } } /** - * Returns the service object. + * Emits packages may have changed signal * - * @return \Etobi\CoreAPI\Service\ExtensionApiService object + * @return void */ - private function getService() { - return $this->objectManager->get('Etobi\\CoreAPI\\Service\\ExtensionApiService'); + protected function emitPackagesMayHaveChangedSignal() { + $this->signalSlotDispatcher->dispatch('PackageManagement', 'packagesMayHaveChanged'); } } \ No newline at end of file diff --git a/Classes/Command/SiteApiCommandController.php b/Classes/Command/SiteApiCommandController.php index 7f6f1c3..6456519 100644 --- a/Classes/Command/SiteApiCommandController.php +++ b/Classes/Command/SiteApiCommandController.php @@ -24,6 +24,7 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use SebastianBergmann\Exporter\Exception; use TYPO3\CMS\Extbase\Mvc\Controller\CommandController; /** @@ -35,14 +36,29 @@ */ class SiteApiCommandController extends CommandController { + /** + * @var \Etobi\CoreAPI\Service\SiteApiService + */ + protected $siteApiService; + + /** + * Inject the SiteApiService + * + * @param \Etobi\CoreAPI\Service\SiteApiService $siteApiService + * + * @return void + */ + public function injectSiteApiService(\Etobi\CoreAPI\Service\SiteApiService $siteApiService) { + $this->siteApiService = $siteApiService; + } + /** * Basic information about the system. * * @return void */ public function infoCommand() { - $service = $this->getService(); - $data = $service->getSiteInfo(); + $data = $this->siteApiService->getSiteInfo(); foreach ($data as $key => $value) { $line = wordwrap($value, self::MAXIMUM_LINE_LENGTH - 43, PHP_EOL . str_repeat(' ', 43), TRUE); @@ -59,16 +75,19 @@ public function infoCommand() { * @return void */ public function createSysNewsCommand($header, $text = '') { - $service = $this->getService(); - $service->createSysNews($header, $text); - } + $result = FALSE; - /** - * Returns the service object. - * - * @return \Etobi\CoreAPI\Service\SiteApiService object - */ - private function getService() { - return $this->objectManager->get('Etobi\\CoreAPI\\Service\\SiteApiService'); + try { + $result = $this->siteApiService->createSysNews($header, $text); + } catch (Exception $e) { + $this->outputLine($e->getMessage()); + $this->quit(); + } + + if ($result) { + $this->outputLine('News entry successfully created.'); + } else { + $this->outputLine('News entry NOT created.'); + } } } \ No newline at end of file diff --git a/Classes/Service/CacheApiService.php b/Classes/Service/CacheApiService.php index f1f011e..d01c9ec 100644 --- a/Classes/Service/CacheApiService.php +++ b/Classes/Service/CacheApiService.php @@ -24,9 +24,6 @@ * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ -use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; -use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; -use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Cache API service @@ -40,7 +37,30 @@ class CacheApiService { /** * @var \TYPO3\CMS\Core\DataHandling\DataHandler */ - protected $tce; + protected $dataHandler; + + /** + * @var \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager + */ + protected $objectManager; + + /** + * @param \TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler + * + * @return void + */ + public function injectDataHandler(\TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler) { + $this->dataHandler = $dataHandler; + } + + /** + * @param \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager + * + * @return void + */ + public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManager $objectManager) { + $this->objectManager = $objectManager; + } /** * Initialize the object. @@ -49,15 +69,13 @@ class CacheApiService { */ public function initializeObject() { // Create a fake admin user - $adminUser = new BackendUserAuthentication(); + $adminUser = $this->objectManager->get('TYPO3\\CMS\\Core\\Authentication\\BackendUserAuthentication'); $adminUser->user['uid'] = $GLOBALS['BE_USER']->user['uid']; $adminUser->user['username'] = '_CLI_lowlevel'; $adminUser->user['admin'] = 1; $adminUser->workspace = 0; - $this->tce = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\DataHandling\\DataHandler'); - $this->tce->start(Array(), Array()); - $this->tce->start(Array(), Array(), $adminUser); + $this->dataHandler->start(Array(), Array(), $adminUser); } /** @@ -66,7 +84,7 @@ public function initializeObject() { * @return void */ public function clearAllCaches() { - $this->tce->clear_cacheCmd('all'); + $this->dataHandler->clear_cacheCmd('all'); } /** @@ -75,7 +93,7 @@ public function clearAllCaches() { * @return void */ public function clearPageCache() { - $this->tce->clear_cacheCmd('pages'); + $this->dataHandler->clear_cacheCmd('pages'); } /** @@ -84,12 +102,13 @@ public function clearPageCache() { * @return void */ public function clearConfigurationCache() { - $this->tce->clear_cacheCmd('temp_cached'); + $this->dataHandler->clear_cacheCmd('temp_cached'); } /** * Clear all caches except the page cache. - * This is especially useful on big sites when you can't just drop the page cache. + * This is especially useful on big sites when you can't + * just drop the page cache. * * @return array with list of cleared caches */ @@ -102,7 +121,7 @@ public function clearAllExceptPageCache() { /** @var \TYPO3\CMS\Core\Cache\CacheManager $cacheManager */ $cacheManager = $GLOBALS['typo3CacheManager']; - foreach($cacheKeys as $cacheKey) { + foreach ($cacheKeys as $cacheKey) { if ($cacheManager->hasCache($cacheKey)) { $out[] = $cacheKey; $singleCache = $cacheManager->getCache($cacheKey); diff --git a/Classes/Service/DatabaseApiService.php b/Classes/Service/DatabaseApiService.php index d68cd7b..7df4f67 100644 --- a/Classes/Service/DatabaseApiService.php +++ b/Classes/Service/DatabaseApiService.php @@ -46,15 +46,31 @@ class DatabaseApiService { const ACTION_REMOVE_DROP_TABLE = 8; /** - * @var \TYPO3\CMS\Install\Service\SqlSchemaMigrationService Instance of SQL handler + * @var \TYPO3\CMS\Install\Service\SqlSchemaMigrationService $schemaMigrationService Instance of SQL handler */ - protected $sqlHandler = NULL; + protected $schemaMigrationService; /** - * Constructor function. + * @var \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager */ - public function __construct() { - $this->sqlHandler = GeneralUtility::makeInstance('TYPO3\\CMS\\Install\\Sql\\SchemaMigrator'); + protected $objectManager; + + /** + * Inject the ObjectManager + * + * @param \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager + */ + public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManager $objectManager) { + $this->objectManager = $objectManager; + } + + /** + * Inject the SchemaMigrationService + * + * @param \TYPO3\CMS\Install\Service\SqlSchemaMigrationService $schemaMigrationService + */ + public function injectSchemaMigrationService(\TYPO3\CMS\Install\Service\SqlSchemaMigrationService $schemaMigrationService) { + $this->schemaMigrationService = $schemaMigrationService; } /** @@ -68,14 +84,15 @@ public function __construct() { public function databaseCompare($actions) { $errors = array(); - $availableActions = array_flip(GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', 'Etobi\\CoreAPI\\Service\\DatabaseApiService')->getConstants()); + $test = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', 'Etobi\\CoreAPI\\Service\\DatabaseApiService'); + $availableActions = array_flip($test->getConstants()); if (empty($actions)) { throw new InvalidArgumentException('No compare modes defined'); } $allowedActions = array(); - $actionSplit = GeneralUtility::trimExplode(',', $actions); + $actionSplit = $this->trimExplode($actions); foreach ($actionSplit as $split) { if (!isset($availableActions[$split])) { throw new InvalidArgumentException(sprintf('Action "%s" is not available!', $split)); @@ -83,69 +100,68 @@ public function databaseCompare($actions) { $allowedActions[$split] = 1; } - - $tblFileContent = GeneralUtility::getUrl(PATH_t3lib . 'stddb/tables.sql'); + $tblFileContent = ''; foreach ($GLOBALS['TYPO3_LOADED_EXT'] as $loadedExtConf) { if (is_array($loadedExtConf) && $loadedExtConf['ext_tables.sql']) { - $extensionSqlContent = GeneralUtility::getUrl($loadedExtConf['ext_tables.sql']); + $extensionSqlContent = $this->getUrl($loadedExtConf['ext_tables.sql']); $tblFileContent .= LF . LF . LF . LF . $extensionSqlContent; } } if (is_callable('TYPO3\\CMS\\Core\\Cache\\Cache::getDatabaseTableDefinitions')) { - $tblFileContent .= Cache::getDatabaseTableDefinitions(); + $tblFileContent .= $this->getDatabaseTableDefinitionsFromCache(); } if (class_exists('TYPO3\\CMS\\Core\\Category\\CategoryRegistry')) { - $tblFileContent .= \TYPO3\CMS\Core\Category\CategoryRegistry::getInstance()->getDatabaseTableDefinitions(); + $tblFileContent .= $this->getCategoryRegistry()->getDatabaseTableDefinitions(); } if ($tblFileContent) { - $fileContent = implode(LF, $this->sqlHandler->getStatementArray($tblFileContent, 1, '^CREATE TABLE ')); - $FDfile = $this->sqlHandler->getFieldDefinitions_fileContent($fileContent); + $fileContent = implode(LF, $this->schemaMigrationService->getStatementArray($tblFileContent, 1, '^CREATE TABLE ')); + $fieldDefinitionsFromFile = $this->schemaMigrationService->getFieldDefinitions_fileContent($fileContent); - $FDdb = $this->sqlHandler->getFieldDefinitions_database(); + $fieldDefinitionsFromDb = $this->schemaMigrationService->getFieldDefinitions_database(); - $diff = $this->sqlHandler->getDatabaseExtra($FDfile, $FDdb); - $update_statements = $this->sqlHandler->getUpdateSuggestions($diff); + $diff = $this->schemaMigrationService->getDatabaseExtra($fieldDefinitionsFromFile, $fieldDefinitionsFromDb); + $updateStatements = $this->schemaMigrationService->getUpdateSuggestions($diff); - $diff = $this->sqlHandler->getDatabaseExtra($FDdb, $FDfile); - $remove_statements = $this->sqlHandler->getUpdateSuggestions($diff, 'remove'); + $diff = $this->schemaMigrationService->getDatabaseExtra($fieldDefinitionsFromDb, $fieldDefinitionsFromFile); + $removeStatements = $this->schemaMigrationService->getUpdateSuggestions($diff, 'remove'); - $allowedRequestKeys = $this->getRequestKeys($update_statements, $remove_statements); + $allowedRequestKeys = $this->getRequestKeys($updateStatements, $removeStatements); $results = array(); if ($allowedActions[self::ACTION_UPDATE_CLEAR_TABLE] == 1) { - $results[] = $this->sqlHandler->performUpdateQueries($update_statements['clear_table'], $allowedRequestKeys); + $results[] = $this->schemaMigrationService->performUpdateQueries($updateStatements['clear_table'], $allowedRequestKeys); } if ($allowedActions[self::ACTION_UPDATE_ADD] == 1) { - $results[] = $this->sqlHandler->performUpdateQueries($update_statements['add'], $allowedRequestKeys); + $results[] = $this->schemaMigrationService->performUpdateQueries($updateStatements['add'], $allowedRequestKeys); } if ($allowedActions[self::ACTION_UPDATE_CHANGE] == 1) { - $results[] = $this->sqlHandler->performUpdateQueries($update_statements['change'], $allowedRequestKeys); + $results[] = $this->schemaMigrationService->performUpdateQueries($updateStatements['change'], $allowedRequestKeys); } if ($allowedActions[self::ACTION_REMOVE_CHANGE] == 1) { - $results[] = $this->sqlHandler->performUpdateQueries($remove_statements['change'], $allowedRequestKeys); + $results[] = $this->schemaMigrationService->performUpdateQueries($removeStatements['change'], $allowedRequestKeys); } if ($allowedActions[self::ACTION_REMOVE_DROP] == 1) { - $results[] = $this->sqlHandler->performUpdateQueries($remove_statements['drop'], $allowedRequestKeys); + $results[] = $this->schemaMigrationService->performUpdateQueries($removeStatements['drop'], $allowedRequestKeys); } if ($allowedActions[self::ACTION_UPDATE_CREATE_TABLE] == 1) { - $results[] = $this->sqlHandler->performUpdateQueries($update_statements['create_table'], $allowedRequestKeys); + $results[] = $this->schemaMigrationService->performUpdateQueries($updateStatements['create_table'], $allowedRequestKeys); } if ($allowedActions[self::ACTION_REMOVE_CHANGE_TABLE] == 1) { - $results[] = $this->sqlHandler->performUpdateQueries($remove_statements['change_table'], $allowedRequestKeys); + $results[] = $this->schemaMigrationService->performUpdateQueries($removeStatements['change_table'], $allowedRequestKeys); } if ($allowedActions[self::ACTION_REMOVE_DROP_TABLE] == 1) { - $results[] = $this->sqlHandler->performUpdateQueries($remove_statements['drop_table'], $allowedRequestKeys); + $results[] = $this->schemaMigrationService->performUpdateQueries($removeStatements['drop_table'], $allowedRequestKeys); } foreach ($results as $resultSet) { @@ -166,10 +182,10 @@ public function databaseCompare($actions) { * @return array */ public function databaseCompareAvailableActions() { - $availableActions = array_flip(GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', 'Etobi\\CoreAPI\\Service\\DatabaseApiService')->getConstants()); + $availableActions = array_flip($this->objectManager->get('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', 'Etobi\\CoreAPI\\Service\\DatabaseApiService')->getConstants()); foreach ($availableActions as $number => $action) { - if (!GeneralUtility::isFirstPartOfStr($action, 'ACTION_')) { + if (!$this->isFirstPartOfString($action, 'ACTION_')) { unset($availableActions[$number]); } } @@ -210,4 +226,55 @@ protected function getRequestKeys(array $update, array $remove) { } return $finalKeys; } + + /** + * Wrapper around GeneralUtility::trimExplode + * + * @param string $values The values to explode + * + * @return array + */ + protected function trimExplode($values) { + return GeneralUtility::trimExplode(',', $values); + } + + /** + * Wrapper around GeneralUtility::getUrl() + * @param $url + * + * @return mixed + */ + protected function getUrl($url) { + return GeneralUtility::getUrl($url); + } + + /** + * Wrapper around Cache::getDatabaseTableDefinitions() + * + * @return string + */ + protected function getDatabaseTableDefinitionsFromCache() { + return Cache::getDatabaseTableDefinitions(); + } + + /** + * Wrapper around \TYPO3\CMS\Core\Category\CategoryRegistry::getInstance() + * + * @return \TYPO3\CMS\Core\Category\CategoryRegistry + */ + protected function getCategoryRegistry() { + return \TYPO3\CMS\Core\Category\CategoryRegistry::getInstance(); + } + + /** + * Wrapper around GeneralUtility::isFirstPartOfStr() + * + * @param string $str + * @param string $partStr + * + * @return bool + */ + protected function isFirstPartOfString($str, $partStr) { + return GeneralUtility::isFirstPartOfStr($str, $partStr); + } } \ No newline at end of file diff --git a/Classes/Service/ExtensionApiService.php b/Classes/Service/ExtensionApiService.php index da6aae0..8493285 100644 --- a/Classes/Service/ExtensionApiService.php +++ b/Classes/Service/ExtensionApiService.php @@ -26,8 +26,19 @@ ***************************************************************/ use InvalidArgumentException; use RuntimeException; +use SebastianBergmann\Exporter\Exception; +use TYPO3\CMS\Core\Configuration\ConfigurationManager; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; +use TYPO3\CMS\Extensionmanager\Domain\Model\Extension; +use TYPO3\CMS\Extensionmanager\Domain\Repository\ExtensionRepository; +use TYPO3\CMS\Extensionmanager\Domain\Repository\RepositoryRepository; +use TYPO3\CMS\Extensionmanager\Service\ExtensionManagementService; +use TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility; +use TYPO3\CMS\Extensionmanager\Utility\InstallUtility; +use TYPO3\CMS\Extensionmanager\Utility\ListUtility; +use TYPO3\CMS\Extensionmanager\Utility\Repository\Helper; /** * Extension API service @@ -38,26 +49,126 @@ */ class ExtensionApiService { - /* - * some ExtensionManager Objects require public access to these objects + /** + * @var \TYPO3\CMS\Extensionmanager\Utility\Connection\TerUtility $terConnection */ - /** @var tx_em_Tools_XmlHandler */ - public $xmlHandler; + public $terConnection; - /** @var tx_em_Extensions_List */ - public $extensionList; + /** + * @var \TYPO3\CMS\Core\Configuration\ConfigurationManager $configurationManager + */ + protected $configurationManager; - /** @var tx_em_Connection_Ter */ - public $terConnection; + /** + * @var \TYPO3\CMS\Extensionmanager\Utility\ListUtility $extensionList + */ + public $listUtility; - /** @var tx_em_Extensions_Details */ - public $extensionDetails; + /** + * @var \TYPO3\CMS\Extensionmanager\Utility\InstallUtility $installUtility + */ + protected $installUtility; - /** @var $configurationManager \TYPO3\CMS\Core\Configuration\ConfigurationManager */ - protected $configurationManager; + /** + * @var \TYPO3\CMS\Extensionmanager\Domain\Repository\RepositoryRepository $repositoryRepository + */ + protected $repositoryRepository; + + /** + * @var Helper $repositoryHelper + */ + protected $repositoryHelper; + + /** + * @var ExtensionRepository $extensionRepository + */ + protected $extensionRepository; + + /** + * @var ExtensionManagementService $extensionManagementService + */ + protected $extensionManagementService; + + /** + * @var ObjectManagerInterface $objectManager + */ + protected $objectManager; + + /** + * @var \TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility $fileHandlingUtility + */ + protected $fileHandlingUtility; + + /** + * @var \TYPO3\CMS\Extensionmanager\Utility\EmConfUtility $emConfUtility + */ + protected $emConfUtility; + + /** + * @param \TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility $fileHandlingUtility + * + * @return void + */ + public function injectFileHandlingUtility(FileHandlingUtility $fileHandlingUtility) { + $this->fileHandlingUtility = $fileHandlingUtility; + } + + /** + * @param \TYPO3\CMS\Extensionmanager\Utility\InstallUtility $installUtility + * + * @return void + */ + public function injectInstallUtility(InstallUtility $installUtility) { + $this->installUtility = $installUtility; + } + + /** + * @param RepositoryRepository $repositoryRepository + * + * @return void + */ + public function injectRepositoryRepository(RepositoryRepository $repositoryRepository) { + $this->repositoryRepository = $repositoryRepository; + } + + /** + * @param Helper $repositoryHelper + * + * @return void + */ + public function injectRepositoryHelper(Helper $repositoryHelper) { + $this->repositoryHelper = $repositoryHelper; + } + + /** + * @param ExtensionRepository $extensionRepository + * + * @return void + */ + public function injectExtensionRepository(ExtensionRepository $extensionRepository){ + $this->extensionRepository = $extensionRepository; + } + + /** + * @param ExtensionManagementService $extensionManagementService + * + * @return void + */ + public function injectExtensionManagementService(ExtensionManagementService $extensionManagementService) { + $this->extensionManagementService = $extensionManagementService; + } + + /** + * @param ObjectManagerInterface $objectManager + * + * @return void + */ + public function injectObjectManager(ObjectManagerInterface $objectManager) { + $this->objectManager = $objectManager; + } /** - * The Constructor. + * The constructor */ public function __construct() { $this->configurationManager = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Configuration\\ConfigurationManager'); @@ -75,14 +186,14 @@ public function getExtensionInformation($extensionKey) { if (strlen($extensionKey) === 0) { throw new InvalidArgumentException('No extension key given!'); } - if (!$GLOBALS['TYPO3_LOADED_EXT'][$extensionKey]) { - throw new InvalidArgumentException(sprintf('Extension "%s" not found!', $extensionKey)); - } - include_once(ExtensionManagementUtility::extPath($extensionKey) . 'ext_emconf.php'); + $this->checkExtensionExists($extensionKey); + + $extensions = $this->listExtensions(); + $information = array( - 'em_conf' => $EM_CONF[''], - 'is_installed' => ExtensionManagementUtility::isLoaded($extensionKey) + 'em_conf' => $extensions[$extensionKey], + 'is_installed' => $this->installUtility->isLoaded($extensionKey) ); return $information; @@ -91,85 +202,62 @@ public function getExtensionInformation($extensionKey) { /** * Get array of installed extensions. * - * @param string $type L, S, G or empty (for all) + * @param string $type Local, System, Global or empty (for all) * * @throws InvalidArgumentException * @return array */ - public function getInstalledExtensions($type = '') { - $type = strtoupper($type); - if (!empty($type) && $type !== 'L' && $type !== 'G' && $type !== 'S') { - throw new InvalidArgumentException('Only "L", "S", "G" and "" (all) are supported as type'); + public function listExtensions($type = '') { + $type = ucfirst(strtolower($type)); + if (!empty($type) && $type !== 'Local' && $type !== 'Global' && $type !== 'System') { + throw new InvalidArgumentException('Only "Local", "System", "Global" and "" (all) are supported as type'); } - $extensions = $GLOBALS['TYPO3_LOADED_EXT']; + $this->initializeExtensionManagerObjects(); + + // TODO: Make listUtlity local var + $extensions = $this->listUtility->getAvailableExtensions(); $list = array(); + foreach ($extensions as $key => $extension) { - if (!empty($type) && $type !== $extension['type']) { + if ((!empty($type) && $type !== $extension['type']) + || (!$this->installUtility->isLoaded($extension['key'])) + ) { continue; } - include_once(ExtensionManagementUtility::extPath($key) . 'ext_emconf.php'); - $list[$key] = $EM_CONF['']; + // TODO: Make emConfUtility local var + $configuration = $this->emConfUtility->includeEmConf($extension); + if (!empty($configuration)) { + $list[$key] = $configuration; + } } - ksort($list); + return $list; } /** * Update the mirrors, using the scheduler task of EXT:em. * - * @see tx_em_Tasks_UpdateExtensionList * @throws RuntimeException - * @return void + * @return boolean */ public function updateMirrors() { - if (version_compare(TYPO3_version, '4.7.0', '>')) { - throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); - } + $result = FALSE; + $repositories = $this->repositoryRepository->findAll(); - // get repositories - $repositories = tx_em_Database::getRepositories(); - if (!is_array($repositories)) { - return; - } - - // update all repositories + // update all repositories foreach ($repositories as $repository) { - /* @var $objRepository \TYPO3\CMS\Extensionmanager\Domain\Model\Repository */ - $objRepository = GeneralUtility::makeInstance('TYPO3\\CMS\\Extensionmanager\\Domain\\Model\\Repository', $repository['uid']); - /* @var $objRepositoryUtility \TYPO3\CMS\Extensionmanager\Utility\Repository\Helper */ - $objRepositoryUtility = GeneralUtility::makeInstance('TYPO3\\CMS\\Extensionmanager\\Utility\\Repository\\Helper', $objRepository); - $count = $objRepositoryUtility->updateExtList(FALSE); - unset($objRepository, $objRepositoryUtility); - } - } - - /** - * Creates the upload folders of an extension. - * - * @return array - */ - public function createUploadFolders() { - $extensions = $this->getInstalledExtensions(); - - // 6.2 creates also Dirs - $result = array(); - if (class_exists('\TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility')) { - $fileHandlingUtility = GeneralUtility::makeInstance('TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility'); - foreach ($extensions AS $key => $extension) { - $extension['key'] = $key; - $fileHandlingUtility->ensureConfiguredDirectoriesExist($extension); - } - $result[] = 'done with \TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility->ensureConfiguredDirectoriesExist'; + $this->repositoryHelper->setRepository($repository); + $result = $this->repositoryHelper->updateExtList(); + unset($objRepository, $this->repositoryHelper); } return $result; } - /** * Install (load) an extension. * @@ -180,47 +268,9 @@ public function createUploadFolders() { * @return void */ public function installExtension($extensionKey) { - if (version_compare(TYPO3_version, '4.7.0', '>')) { - throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); - } - - // checks if extension exists - if (!$this->extensionExists($extensionKey)) { - throw new InvalidArgumentException(sprintf('Extension "%s" does not exist!', $extensionKey)); - } + $this->checkExtensionExists($extensionKey); - // check if extension is already loaded - if (ExtensionManagementUtility::isLoaded($extensionKey)) { - throw new InvalidArgumentException(sprintf('Extension "%s" already installed!', $extensionKey)); - } - - // check if localconf.php is writable - - if (!$this->configurationManager->canWriteConfiguration()) { - throw new RuntimeException('Localconf.php is not writeable!'); - } - - $this->initializeExtensionManagerObjects(); - list($currentList,) = $this->extensionList->getInstalledExtensions(); - - // add extension to list of loaded extensions - $newList = $this->extensionList->addExtToList($extensionKey, $currentList); - if ($newList === -1) { - throw new RuntimeException(sprintf('Extension "%s" could not be installed!', $extensionKey)); - } - - // update typo3conf/localconf.php - $install = $this->getEmInstall(); - $install->writeNewExtensionList($newList); - - tx_em_Tools::refreshGlobalExtList(); - - // make database changes - // TODO make this optional - $install->forceDBupdates($extensionKey, $newList[$extensionKey]); - - // TODO make this optional - $this->clearCaches(); + $this->installUtility->install($extensionKey); } /** @@ -233,340 +283,274 @@ public function installExtension($extensionKey) { * @return void */ public function uninstallExtension($extensionKey) { - if (version_compare(TYPO3_version, '4.7.0', '>')) { - throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); - } - - // check if extension is this extension (coreapi) - if ($extensionKey == 'coreapi') { - throw new InvalidArgumentException(sprintf('Extension "%s" cannot be uninstalled!', $extensionKey)); - } - - // checks if extension exists - if (!$this->extensionExists($extensionKey)) { - throw new InvalidArgumentException(sprintf('Extension "%s" does not exist!', $extensionKey)); - } - - // check if extension is loaded - if (!ExtensionManagementUtility::isLoaded($extensionKey)) { - throw new InvalidArgumentException(sprintf('Extension "%s" is not installed!', $extensionKey)); - } - - // check if this is a required extension (such as "cms") that cannot be uninstalled - $requiredExtList = GeneralUtility::trimExplode(',', REQUIRED_EXTENSIONS); - if (in_array($extensionKey, $requiredExtList)) { - throw new InvalidArgumentException(sprintf('Extension "%s" is a required extension and cannot be uninstalled!', $extensionKey)); - } - - // check if localconf.php is writable - if (!$this->configurationManager->canWriteConfiguration()) { - throw new RuntimeException('Localconf.php is not writeable!'); - } - - $this->initializeExtensionManagerObjects(); - list($currentList,) = $this->extensionList->getInstalledExtensions(); - $newList = $this->extensionList->removeExtFromList($extensionKey, $currentList); - if ($newList === -1) { - throw new RuntimeException(sprintf('Extension "%s" could not be installed!', $extensionKey)); + if ($extensionKey === 'coreapi') { + throw new InvalidArgumentException('Extension "coreapi" cannot be uninstalled!'); } - // update typo3conf/localconf.php - $install = $this->getEmInstall(); - $install->writeNewExtensionList($newList); + $this->checkExtensionExists($extensionKey); + $this->checkExtensionLoaded($extensionKey); - tx_em_Tools::refreshGlobalExtList(); - - // TODO make this optional - $this->clearCaches(); + $this->installUtility->uninstall($extensionKey); } + /** * Configure an extension. * - * @param string $extensionKey The extension key - * @param array $extensionConfiguration + * @param string $extensionKey The extension key + * @param array $newExtensionConfiguration * * @throws RuntimeException * @throws InvalidArgumentException * @return void */ - public function configureExtension($extensionKey, $extensionConfiguration = array()) { - global $TYPO3_CONF_VARS; - - if (version_compare(TYPO3_version, '4.7.0', '>')) { - throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); - } + public function configureExtension($extensionKey, $newExtensionConfiguration = array()) { + $this->checkExtensionExists($extensionKey); + $this->checkExtensionLoaded($extensionKey); - // check if extension exists - if (!$this->extensionExists($extensionKey)) { - throw new InvalidArgumentException(sprintf('Extension "%s" does not exist!', $extensionKey)); - } - - // check if extension is loaded - if (!ExtensionManagementUtility::isLoaded($extensionKey)) { - throw new InvalidArgumentException(sprintf('Extension "%s" is not installed!', $extensionKey)); + // checks if conf array is empty + if (empty($newExtensionConfiguration)) { + throw new InvalidArgumentException(sprintf('No configuration provided for extension "%s"!', $extensionKey)); } // check if extension can be configured - $extAbsPath = ExtensionManagementUtility::extPath($extensionKey); - + $extAbsPath = $this->getExtensionPath($extensionKey); $extConfTemplateFile = $extAbsPath . 'ext_conf_template.txt'; if (!file_exists($extConfTemplateFile)) { throw new InvalidArgumentException(sprintf('Extension "%s" has no configuration options!', $extensionKey)); } - // checks if conf array is empty - if (empty($extensionConfiguration)) { - throw new InvalidArgumentException(sprintf('No configuration for extension "%s"!', $extensionKey)); - } - - // Load tsStyleConfig class and parse configuration template: - $extRelPath = ExtensionManagementUtility::extRelPath($extensionKey); - - $tsStyleConfig = GeneralUtility::makeInstance('t3lib_tsStyleConfig'); - $tsStyleConfig->doNotSortCategoriesBeforeMakingForm = TRUE; - $constants = $tsStyleConfig->ext_initTSstyleConfig( - GeneralUtility::getUrl($extConfTemplateFile), - $extRelPath, - $extAbsPath, - $GLOBALS['BACK_PATH'] - ); + /** @var $configurationUtility \TYPO3\CMS\Extensionmanager\Utility\ConfigurationUtility */ + $configurationUtility = $this->objectManager->get('TYPO3\\CMS\\Extensionmanager\\Utility\\ConfigurationUtility'); + // get existing configuration + $currentExtensionConfig = $configurationUtility->getCurrentConfiguration($extensionKey); // check for unknown configuration settings - foreach ($extensionConfiguration as $key => $value) { - if (!isset($constants[$key])) { + foreach ($newExtensionConfiguration as $key => $_) { + if (!isset($currentExtensionConfig[$key])) { throw new InvalidArgumentException(sprintf('No configuration setting with name "%s" for extension "%s"!', $key, $extensionKey)); } } - // get existing configuration - $configurationArray = unserialize($TYPO3_CONF_VARS['EXT']['extConf'][$extensionKey]); - $configurationArray = is_array($configurationArray) ? $configurationArray : array(); - // fill with missing values - foreach (array_keys($constants) as $key) { - if (!isset($extensionConfiguration[$key])) { - if (isset($configurationArray[$key])) { - $extensionConfiguration[$key] = $configurationArray[$key]; - } else { - if (!empty($constants[$key]['value'])) { - $extensionConfiguration[$key] = $constants[$key]['value']; - } else { - $extensionConfiguration[$key] = $constants[$key]['default_value']; - } - } - } - } - - // process incoming configuration - // values are checked against types in $constants - $tsStyleConfig->ext_procesInput(array('data' => $extensionConfiguration), array(), $constants, array()); - - // current configuration is merged with incoming configuration - $configurationArray = $tsStyleConfig->ext_mergeIncomingWithExisting($configurationArray); - - // write configuration to typo3conf/localconf.php - $install = $this->getEmInstall(); - $install->writeTsStyleConfig($extensionKey, $configurationArray); + $newExtensionConfiguration = $this->mergeNewExtensionConfiguratonWithCurrentConfiguration( + $newExtensionConfiguration, + $currentExtensionConfig + ); - // TODO make this optional - $this->clearCaches(); + // write configuration to typo3conf/LocalConfiguration.php + $configurationUtility->writeConfiguration($newExtensionConfiguration, $extensionKey); } /** * Fetch an extension from TER. * - * @param string $extensionKey - * @param string $version - * @param string $location - * @param bool $overwrite - * @param string $mirror + * @param string $extensionKey The extension key + * @param string $location Where to import the extension. System = typo3/sysext, Global = typo3/ext, Local = typo3conf/ext + * @param bool $overwrite Overwrite the extension if it already exists + * @param int $mirror The mirror to fetch the extension from * - * @throws RuntimeException - * @throws InvalidArgumentException + * @throws \RuntimeException + * @throws \InvalidArgumentException * @return array */ - public function fetchExtension($extensionKey, $version = '', $location = 'L', $overwrite = FALSE, $mirror = '') { - if (version_compare(TYPO3_version, '4.7.0', '>')) { - throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); + public function fetchExtension($extensionKey, $version = '', $location = 'Local', $overwrite = FALSE, $mirror = -1) { + if (!is_numeric($mirror)) { + throw new InvalidArgumentException('Option --mirror must be a number. Run the command extensionapi:listmirrors to get the list of all available repositories'); } - $return = array(); - if (!tx_em_Tools::importAsType($location)) { - if ($location === 'G') { - throw new InvalidArgumentException(sprintf('Global installation (%s) is not allowed!', $location)); + if ($version === '') { + $extension = $this->extensionRepository->findHighestAvailableVersion($extensionKey); + if ($extension === NULL) { + throw new InvalidArgumentException(sprintf('Extension "%s" was not found on TER', $extensionKey)); } - if ($location === 'L') { - throw new InvalidArgumentException(sprintf('Local installation (%s) is not allowed!', $location)); - } - if ($location === 'S') { - throw new InvalidArgumentException(sprintf('System installation (%s) is not allowed!', $location)); + } else { + $extension = $this->extensionRepository->findOneByExtensionKeyAndVersion($extensionKey, $version); + if ($extension === NULL) { + throw new InvalidArgumentException(sprintf('Version %s of extension "%s" does not exist', $version, $extensionKey)); } - throw new InvalidArgumentException(sprintf('Unknown location "%s"!', $location)); } if (!$overwrite) { - $location = ($location === 'G' || $location === 'S') ? $location : 'L'; - $comingExtPath = tx_em_Tools::typePath($location) . $extensionKey . '/'; + $comingExtPath = $this->fileHandlingUtility->getExtensionDir($extensionKey, $location); if (@is_dir($comingExtPath)) { throw new InvalidArgumentException(sprintf('Extension "%s" already exists at "%s"!', $extensionKey, $comingExtPath)); } } - // check extension list - $this->initializeExtensionManagerObjects(); - $this->xmlHandler->searchExtensionsXMLExact($extensionKey, '', '', TRUE, TRUE); - if (!isset($this->xmlHandler->extensionsXML[$extensionKey])) { - throw new InvalidArgumentException(sprintf('Extension "%s" was not found', $extensionKey)); - } - - // get latest version - if (!strlen($version)) { - $versions = array_keys($this->xmlHandler->extensionsXML[$extensionKey]['versions']); - // sort version numbers ascending to pick the highest version - natsort($versions); - $version = end($versions); - } - - // check if version exists - if (!isset($this->xmlHandler->extensionsXML[$extensionKey]['versions'][$version])) { - throw new InvalidArgumentException(sprintf('Version %s of extension "%s" does not exist', $version, $extensionKey)); - } - - // get mirrors - $mirrors = array(); - $mirrorsTmpFile = GeneralUtility::tempnam('mirrors'); - $mirrorsFile = GeneralUtility::getUrl($GLOBALS['TYPO3_CONF_VARS']['EXT']['em_mirrorListURL'], 0); + $mirrors = $this->repositoryHelper->getMirrors(); - if ($mirrorsFile === FALSE) { - GeneralUtility::unlink_tempfile($mirrorsTmpFile); - throw new RuntimeException('Could not retrieve the list of mirrors!'); - } else { - GeneralUtility::writeFile($mirrorsTmpFile, $mirrorsFile); - $mirrorsXml = implode('', gzfile($mirrorsTmpFile)); - GeneralUtility::unlink_tempfile($mirrorsTmpFile); - $mirrors = $this->xmlHandler->parseMirrorsXML($mirrorsXml); - } - - if ((!is_array($mirrors)) || (count($mirrors) < 1)) { + if ($mirrors === NULL) { throw new RuntimeException('No mirrors found!'); } - $mirrorUrl = ''; - if (!strlen($mirror)) { - $rand = array_rand($mirrors); - $mirrorUrl = 'http://' . $mirrors[$rand]['host'] . $mirrors[$rand]['path']; - } elseif (isset($mirrors[$mirror])) { - $mirrorUrl = 'http://' . $mirrors[$mirror]['host'] . $mirrors[$mirror]['path']; + if ($mirror === -1) { + $mirrors->setSelect(); + } elseif ($mirror > 0 && $mirror <= count($mirrors->getMirrors())) { + $mirrors->setSelect($mirror); } else { throw new InvalidArgumentException(sprintf('Mirror "%s" does not exist', $mirror)); } - $fetchData = $this->terConnection->fetchExtension($extensionKey, $version, $this->xmlHandler->extensionsXML[$extensionKey]['versions'][$version]['t3xfilemd5'], $mirrorUrl); - if (!is_array($fetchData)) { - throw new RuntimeException($fetchData); - } + /** + * @var \TYPO3\CMS\Extensionmanager\Utility\DownloadUtility $downloadUtility + */ + $downloadUtility = $this->objectManager->get('TYPO3\\CMS\\Extensionmanager\\Utility\\DownloadUtility'); + $downloadUtility->setDownloadPath($location); - $extKey = $fetchData[0]['extKey']; - if (!$extKey) { - throw new RuntimeException($fetchData); + $this->extensionManagementService->downloadMainExtension($extension); + + $return = array(); + $extensionDir = $this->fileHandlingUtility->getExtensionDir($extensionKey, $location); + if (is_dir($extensionDir)) { + $return['main']['extKey'] = $extension->getExtensionKey(); + $return['main']['version'] = $extension->getVersion(); + } else { + throw new RuntimeException( + sprintf('Extension "%s" version %s could not installed!', $extensionKey, $extension->getVersion()) + ); } - $return['extKey'] = $extKey; - $return['version'] = $fetchData[0]['EM_CONF']['version']; + return $return; + } - // TODO make this optional - $install = $this->getEmInstall(); - $content = $install->installExtension($fetchData, $location, null, '', !$overwrite); + /** + * Lists the possible mirrors + * + * @return array + */ + public function listMirrors() { + /** @var $repositoryHelper Helper */ + $repositoryHelper = $this->objectManager->get('TYPO3\\CMS\\Extensionmanager\\Utility\\Repository\\Helper'); + $mirrors = $repositoryHelper->getMirrors(); - return $return; + return $mirrors->getMirrors(); + } + + /** + * Extracts and returns the file content of the given file + * + * @param string $file The file with file path + * + * @return array + */ + protected function getFileContentFromUrl($file) { + return GeneralUtility::getUrl($file); } /** * Imports extension from file. * - * @param string $file path to t3x file - * @param string $location where to import the extension. S = typo3/sysext, G = typo3/ext, L = typo3conf/ext - * @param bool $overwrite overwrite the extension if it already exists + * @param string $file Path to t3x file + * @param string $location Where to import the extension. System = typo3/sysext, Global = typo3/ext, Local = typo3conf/ext + * @param bool $overwrite Overwrite the extension if it already exists * * @throws \RuntimeException * @throws \InvalidArgumentException - * @return array + * @return array The extension data */ - public function importExtension($file, $location = 'L', $overwrite = FALSE) { - if (version_compare(TYPO3_version, '4.7.0', '>')) { - throw new RuntimeException('This feature is not available in TYPO3 versions > 4.7 (yet)!'); - } - - $return = array(); + public function importExtension($file, $location = 'Local', $overwrite = FALSE) { if (!is_file($file)) { throw new InvalidArgumentException(sprintf('File "%s" does not exist!', $file)); } - if (!tx_em_Tools::importAsType($location)) { - if ($location === 'G') { - throw new InvalidArgumentException(sprintf('Global installation (%s) is not allowed!', $location)); - } - if ($location === 'L') { - throw new InvalidArgumentException(sprintf('Local installation (%s) is not allowed!', $location)); - } - if ($location === 'S') { - throw new InvalidArgumentException(sprintf('System installation (%s) is not allowed!', $location)); - } - throw new InvalidArgumentException(sprintf('Unknown location "%s"!', $location)); - } + $this->checkInstallLocation($location); - $fileContent = GeneralUtility::getUrl($file); - if (!$fileContent) { - throw new InvalidArgumentException(sprintf('File "%s" is empty!', $file)); - } + $uploadExtensionFileController = $this->objectManager->get('TYPO3\\CMS\\Extensionmanager\\Controller\\UploadExtensionFileController'); - $fetchData = $this->terConnection->decodeExchangeData($fileContent); - if (!is_array($fetchData)) { - throw new InvalidArgumentException(sprintf('File "%s" is of a wrong format!', $file)); + $filename = pathinfo($file, PATHINFO_BASENAME); + $return = $uploadExtensionFileController->extractExtensionFromFile($file, $filename, $overwrite, FALSE); + + return $return; + } + + /** + * Checks if the function exists in the system + * + * @param string $extensionKey The extension key + * + * @return void + * @throws \InvalidArgumentException + */ + protected function checkExtensionExists($extensionKey) { + if (!$this->installUtility->isAvailable($extensionKey)) { + throw new InvalidArgumentException(sprintf('Extension "%s" does not exist!', $extensionKey)); } + } - $extensionKey = $fetchData[0]['extKey']; - if (!$extensionKey) { - throw new InvalidArgumentException(sprintf('File "%s" is of a wrong format!', $file)); + /** + * Check if an extension is loaded. + * + * @param string $extensionKey The extension key + * + * @throws \InvalidArgumentException + * @return void + */ + protected function checkExtensionLoaded($extensionKey) { + if (!$this->installUtility->isLoaded($extensionKey)) { + throw new InvalidArgumentException(sprintf('Extension "%s" is not installed!', $extensionKey)); } + } - $return['extKey'] = $extensionKey; - $return['version'] = $fetchData[0]['EM_CONF']['version']; + /** + * Returns the absolute extension path. + * Wrapper around the static method. This makes the method mockable. + * + * @param string $extensionKey The extension key + * + * @return string + */ + protected function getExtensionPath($extensionKey) { + return ExtensionManagementUtility::extPath($extensionKey); + } - if (!$overwrite) { - $location = ($location === 'G' || $location === 'S') ? $location : 'L'; - $destinationPath = tx_em_Tools::typePath($location) . $extensionKey . '/'; - if (@is_dir($destinationPath)) { - throw new InvalidArgumentException(sprintf('Extension "%s" already exists at "%s"!', $extensionKey, $destinationPath)); + /** + * Add missing values from current configuration to the new configuration + * + * @param array $newExtensionConfiguration The new configuration which was provided as argument + * @param array $currentExtensionConfig The current configuration of the extension + * + * @return array The merged configuration + */ + protected function mergeNewExtensionConfiguratonWithCurrentConfiguration($newExtensionConfiguration, $currentExtensionConfig) { + foreach (array_keys($currentExtensionConfig) as $key) { + if (!isset($newExtensionConfiguration[$key])) { + if (!empty($currentExtensionConfig[$key]['value'])) { + $newExtensionConfiguration[$key] = $currentExtensionConfig[$key]['value']; + } else { + $newExtensionConfiguration[$key] = $currentExtensionConfig[$key]['default_value']; + } } } - $install = $this->getEmInstall(); - $content = $install->installExtension($fetchData, $location, null, $file, !$overwrite); - - return $return; + return $newExtensionConfiguration; } - /** - * Check if an extension exists. + * Checks if the extension is able to install at the demanded location * - * @param string $extensionKey extension key + * @param string $location The location + * @param array $allowedInstallTypes The allowed locations * - * @return boolean + * @return void + * @throws \InvalidArgumentException */ - protected function extensionExists($extensionKey) { - $this->initializeExtensionManagerObjects(); - list($list,) = $this->extensionList->getInstalledExtensions(); - $extensionExists = FALSE; - foreach ($list as $values) { - if ($values['extkey'] === $extensionKey) { - $extensionExists = TRUE; - break; + protected function checkInstallLocation($location) { + $allowedInstallTypes = Extension::returnAllowedInstallTypes(); + $location = ucfirst(strtolower($location)); + + if (!in_array($location, $allowedInstallTypes)) { + if ($location === 'Global') { + throw new InvalidArgumentException('Global installation is not allowed!'); } + if ($location === 'Local') { + throw new InvalidArgumentException('Local installation is not allowed!'); + } + if ($location === 'System') { + throw new InvalidArgumentException('System installation is not allowed!'); + } + throw new InvalidArgumentException(sprintf('Unknown location "%s"!', $location)); } - return $extensionExists; } /** @@ -575,19 +559,8 @@ protected function extensionExists($extensionKey) { * @return void */ protected function initializeExtensionManagerObjects() { - $this->xmlHandler = GeneralUtility::makeInstance('tx_em_Tools_XmlHandler'); - $this->extensionList = GeneralUtility::makeInstance('tx_em_Extensions_List', $this); - $this->terConnection = GeneralUtility::makeInstance('tx_em_Connection_Ter', $this); - $this->extensionDetails = GeneralUtility::makeInstance('tx_em_Extensions_Details', $this); - } - - /** - * @return tx_em_Install - */ - protected function getEmInstall() { - $install = GeneralUtility::makeInstance('tx_em_Install', $this); - $install->setSilentMode(TRUE); - return $install; + $this->listUtility = $this->objectManager->get('TYPO3\\CMS\\Extensionmanager\\Utility\\ListUtility'); + $this->emConfUtility = $this->objectManager->get('TYPO3\\CMS\\Extensionmanager\\Utility\\EmConfUtility'); } /** diff --git a/Classes/Service/SiteApiService.php b/Classes/Service/SiteApiService.php index b406d28..0b0c6d1 100644 --- a/Classes/Service/SiteApiService.php +++ b/Classes/Service/SiteApiService.php @@ -6,6 +6,7 @@ * * (c) 2012 Georg Ringer * (c) 2014 Stefano Kowalke + * (c) 2013 Claus Due , Wildside A/S * All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is @@ -25,17 +26,33 @@ * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ use InvalidArgumentException; -use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Site API service * * @author Georg Ringer * @author Stefano Kowalke + * @author Claus Due , Wildside A/S * @package Etobi\CoreAPI\Service\SiteApiService */ class SiteApiService { + /** + * @var \TYPO3\CMS\Extbase\Object\ObjectManager + */ + protected $objectManager; + + /** + * Inject the ObjectManager + * + * @param \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager + * + * @return void + */ + public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManager $objectManager) { + $this->objectManager = $objectManager; + } + /** * Get some basic site information. * @@ -47,9 +64,9 @@ public function getSiteInfo() { 'Site name' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], ); - $this->getDiskUsage($data); - $this->getDatabaseInformation($data); - $this->getCountOfExtensions($data); + $data = $this->getDiskUsage($data); + $data = $this->getDatabaseSize($data); + $data = $this->getCountOfExtensions($data); return $data; } @@ -61,13 +78,14 @@ public function getSiteInfo() { * @param string $text text * * @throws InvalidArgumentException - * @return void + * @return boolean */ public function createSysNews($header, $text) { if (strlen($header) === 0) { throw new InvalidArgumentException('No header given'); } - $GLOBALS['TYPO3_DB']->exec_INSERTquery('sys_news', array( + + return $this->getDatabaseHandler()->exec_INSERTquery('sys_news', array( 'title' => $header, 'content' => $text, 'tstamp' => $GLOBALS['EXEC_TIME'], @@ -79,31 +97,34 @@ public function createSysNews($header, $text) { /** * Get disk usage. * - * @author Claus Due , Wildside A/S * @param array $data * - * @return void + * @return array */ - protected function getDiskUsage(&$data) { + public function getDiskUsage($data) { if (TYPO3_OS !== 'WIN') { $data['Combined disk usage'] = trim(array_shift(explode("\t", shell_exec('du -sh ' . PATH_site)))); } + + return $data; } /** * Get database size. * - * @author Claus Due , Wildside A/S * @param array $data * - * @return void + * @return array */ - protected function getDatabaseInformation(&$data) { - $databaseSizeResult = $GLOBALS['TYPO3_DB']->sql_query("SELECT SUM( data_length + index_length ) / 1024 / 1024 AS size FROM information_schema.TABLES WHERE table_schema = '" . TYPO3_db . "'"); - $databaseSizeRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($databaseSizeResult); + public function getDatabaseSize($data) { + $databaseHandler = $this->getDatabaseHandler(); + $databaseSizeResult = $databaseHandler->sql_query("SELECT SUM( data_length + index_length ) / 1024 / 1024 AS size FROM information_schema.TABLES WHERE table_schema = '" . TYPO3_db . "'"); + $databaseSizeRow = $databaseHandler->sql_fetch_assoc($databaseSizeResult); $databaseSize = array_pop($databaseSizeRow); $value = number_format($databaseSize, ($databaseSize > 10 ? 0 : 1)) . 'M'; $data['Database size'] = $value; + + return $data; } /** @@ -111,12 +132,23 @@ protected function getDatabaseInformation(&$data) { * * @param array $data * - * @return void + * @return array */ - protected function getCountOfExtensions(&$data) { + public function getCountOfExtensions($data) { /** @var \Etobi\CoreAPI\Service\ExtensionApiService $extensionService */ - $extensionService = GeneralUtility::makeInstance('Etobi\\CoreAPI\\Service\\ExtensionApiService'); - $extensions = $extensionService->getInstalledExtensions('L'); + $extensionService = $this->objectManager->get('Etobi\\CoreAPI\\Service\\ExtensionApiService'); + $extensions = $extensionService->listExtensions('Local'); $data['Count local installed extensions'] = count($extensions); + + return $data; + } + + /** + * Returns the DatabaseConnection + * + * @return \TYPO3\CMS\Core\Database\DatabaseConnection + */ + protected function getDatabaseHandler() { + return $GLOBALS['TYPO3_DB']; } } \ No newline at end of file diff --git a/Documentation/Images/ExtensionApi/ExtensionApiFetch.graphml b/Documentation/Images/ExtensionApi/ExtensionApiFetch.graphml new file mode 100644 index 0000000..389ce75 --- /dev/null +++ b/Documentation/Images/ExtensionApi/ExtensionApiFetch.graphml @@ -0,0 +1,364 @@ + + + + + + + + + + + + + + + + + + + + + + + S + + + + + + + + + + + + + + + + extensionapi:fetch + + + + + + + + + + + + + + + + Extension already installed? + + + + + + + + + + + + + + + + Extension already downloaded? + + + + + + + + + + + + + + + + override? + + + + + + + + + + + + + + + + Extension available at TER? + + + + + + + + + + + + + + + + download + + + + + + + + + + + + + + + + E + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + no + + + + + + + + + + + + + + + + + + + + yes + + + + + + + + + + + + + + + + + + + + no + + + + + + + + + + + + + + + + + + + + yes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + no + + + + + + + + + + + + + + + + + + + + + yes + + + + + + + + + + + + + + + + + + + + + no + + + + + + + + + + + + + + + + + + + + + + yes + + + + + + + + + + + + + + + + diff --git a/Documentation/Images/ExtensionApi/ExtensionApiFetch.png b/Documentation/Images/ExtensionApi/ExtensionApiFetch.png new file mode 100644 index 0000000000000000000000000000000000000000..92923e8b917952bb3931b0696a156bb2204470f1 GIT binary patch literal 40295 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

XkOPRgAEkcu~ zmDl@ShsA7e>^8O;z0+Sgof=R_v9us*;2J(-C z&8oWT)8={e#w(R1t^!}Ob=YrdqW!MNF!v{eRzWlKk+{vii#!?RF~lUBF1u!hthWBs zo_%koSgi5?H47oI|!LYA$E(o&YS(nc5O0^D}%i@ z%{DnOl)$sesjML?rmDaEog?TIn?!tFn(0kck2oxus9M^Dv};TkFH+WwX>MYIFCTpa z;sr5rb-XcemjA5Gb=*T3Wh;i<_%e1&YMB z&gmHR*G5sA??_TIEf=j{(1i(s`ULCHq%?1DadXT@WO&7B%bvbJYptr z5KA_QS*MoQYLa)S9<23|k>mBTviXMn)uBsM=j+zg)G45k%ZtOh?5KHAvgwKqVvH7Z zxk*qT39Dj2h3|tH)r+079pVll*qJw~hZmm5bo>R$F|o)hZB*@tJ*b(;oRyU|H#__4 z<;!P5`+Iv{-rgRk>s;E}ljGy#p`j13v9ZB{y%jz{(%0&>AOtRslKl7|g22jCqWW%z z1IfPsYiPZ(P2JGq8Mm_nD047IrU$&j23CL_&0#k9`6l*vfB&XGfBq7~RaKt=`m#JI zl1WRVMnhRSvaql)JiI?iGzf&g8Wr8=zbj4d37f5zC((A&7(M>SW&?B4h@G43$87s_ zPu;UC1a%fxSF;im^R1@%&CJYp^1v~}^V36*?OEX}51ThOv$ry~Fi{IE&PksV-xmxF zg*L0Xv^Z*yIqpf7^mw9=K@}qB0EJAfc|6RkYg!IdcX_`0mpo+%|2c&{08a z*RBR3DNRc${gb|Tw7fJ=q|wCpRr9TN_4TO+HK$$5Ob0S;=9&vGBL4XK`uZj&PH)Zp zHtPH#9^nTH>b$&ez}uJm(p+X?ARyVv2n#Q`S9yaSeR1>3neC3bLxTcvXF{;OK=emx zeArX!sF9~o9_gWvOR_9xs}riQ&zn&d6%|!gE?HcI0|O=M@8G*=)aCiq<$m`fSXxV= zzP`7I@HRl{h>wqOWR5ynMDzeF$9>4e9mHlP^zR?m#f2QdRx{4^$jA`#oZOk&SuYOa zqgqhrchGH%gS_c!0Zu|v2)I{+ss~*{!lYIOEoN=zmmd3jN;gT^MH2*C8*mZmpEa(b z$7@R@%yV>X6&2W>v0Ti&yl0>$@QCyC<}c_cJ-y3q8Q{sg`EPscZd$9#%S&j%Izr|; zGm2-^^4zVEx7o25i*K@XrmD&bteG*G&F9y^s&ln0zGhO6(&ydKt;Lmf68 ztCt>*Hlc?LHp@>@_d`iP%hr>|xwL8FLl^bEck+jTnn9$wb8yOx7*2J$K;&ddsgmNY%P*B7roI*JDj2kfShg&-a zr{eforYuIVuhlBTc$UX`;XOufkS=NuAB1FfwYpkQ*!4n%}$?jQMBIa&4EKas}B3Cv6OKa>~+8JQ9V4d-X-~kuImo# zI3na6En%tO#PV$I_F3W#XaJ#aHR)O%56UpQA@zhyb|;fHoT2jadd2n#p12QPbQ!tl zNhWXb5JadR2;>_HRvV>~_glU6(P*}*vo_SV2aqv`5&S3he2S8;(O&YcMUE`36{Bw+ z0>Vhjs1rq>@zYdqHkmFe_ZSs##q{Eh{=jIUqP1qVgQymaL7=Ywg?~hU=J%i5;be|a z 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

lbwfo-2o=;+y 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 CeeV?j literal 0 HcmV?d00001 diff --git a/Documentation/Images/ExtensionApi/uploadfolder.graphml b/Documentation/Images/ExtensionApi/uploadfolder.graphml new file mode 100644 index 0000000..203ff95 --- /dev/null +++ b/Documentation/Images/ExtensionApi/uploadfolder.graphml @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + S + + + + + + + + + + + + + + + + + upload folder already exists? + + + + + + + + + + + + + + + + + Create upload folder + + + + + + + + + + + + + + + + + upload folder exists now? + + + + + + + + + + + + + + + + + E + + + + + + + + + + + + + + + + + Error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Yes + + + + + + + + + + + + + + + + + + + + + Yes + + + + + + + + + + + + + + + + + + + + + No + + + + + + + + + + + + + + + + diff --git a/Documentation/Images/ExtensionApi/uploadfolder.png b/Documentation/Images/ExtensionApi/uploadfolder.png new file mode 100644 index 0000000000000000000000000000000000000000..dab22dd2ddaad105079d94676aa562fe58556de4 GIT binary patch literal 25170 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 bk0hFJOubuv*2M?d!&elZtH_i}y$bvvrB_a` literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 16350c9..bd675a7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![Stories in Ready](https://badge.waffle.io/typo3-coreapi/ext-coreapi.png?label=ready&title=Ready)](https://waffle.io/typo3-coreapi/ext-coreapi) +[![Stories in Ready](https://badge.waffle.io/typo3-coreapi/ext-coreapi.png?label=ready&title=Ready)](https://waffle.io/typo3-coreapi/ext-coreapi) [![Build Status](https://travis-ci.org/TYPO3-coreapi/ext-coreapi.svg?branch=feature%2FMakeExtensionApiCompatibleTo62)](https://travis-ci.org/TYPO3-coreapi/ext-coreapi) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/TYPO3-coreapi/ext-coreapi/badges/quality-score.png?b=feature%2FMakeExtensionApiCompatibleTo62)](https://scrutinizer-ci.com/g/TYPO3-coreapi/ext-coreapi/?branch=feature%2FMakeExtensionApiCompatibleTo62) [![Code Coverage](https://scrutinizer-ci.com/g/TYPO3-coreapi/ext-coreapi/badges/coverage.png?b=feature%2FMakeExtensionApiCompatibleTo62)](https://scrutinizer-ci.com/g/TYPO3-coreapi/ext-coreapi/?branch=feature%2FMakeExtensionApiCompatibleTo62) + ## TYPO3 Extension 'coreapi' The EXT:coreapi should provide a simple to use API for common core features. Goal is to be able to do the most common tasks by CLI instead of doing it in the backend/browser. @@ -54,19 +55,15 @@ If you want to use the cache clearing commands, you need to add the following sn options.clearCache.all=1 options.clearCache.pages=1 -#### TYPO3 4.7+ #### -If you are using TYPO3 4.7+, you can use the awesome CommandController of Extbase. +#### TYPO3 6.2 and below #### +Support for TYPO3 CMS below 6.2 was removed with version 0.2.0 of this extension. In case you need to use ext:coreapi in combination with lower version of TYPO3 CMS use version [0.1.0-beta](https://github.com/TYPO3-coreapi/ext-coreapi/releases/tag/0.1.0-beta "0.1.0-beta"). + +#### TYPO3 6.2+ #### +If you are using TYPO3 6.2+, you can use the awesome CommandController of Extbase. This will show you all available calls ./typo3/cli_dispatch.phpsh extbase help -#### TYPO3 4.6 and below #### -If you are using 4.5 or 4.6, you can still use the extension with a call like - ./typo3/cli_dispatch.phpsh coreapi cache:clearallcaches - - # list all available commands: - ./typo3/cli_dispatch.phpsh coreapi help - ### Usage in Composer ### { @@ -78,12 +75,6 @@ If you are using 4.5 or 4.6, you can still use the extension with a call like "typo3core/cms": "*", "etobi/coreapi": "dev-master", }, - "repositories": [ - { - "type": "vcs", - "url": "git://github.com/typo3-coreapi/ext-coreapi.git" - } - ], "extra": { "installer-paths": { "typo3conf/ext/{$name}": [ @@ -96,4 +87,13 @@ If you are using 4.5 or 4.6, you can still use the extension with a call like "scripts": {} } +### Running the unit tests + +The Unit Tests rely on [vfsStream](https://github.com/mikey179/vfsStream "vfsStream"). For some reasons ext:coreapi don't add this dependencies by itself but uses the one which is alread defined for Core Unit Tests. +To install vfsStream copy the composer.json from the TYPO3 CMS package into you webroot folder and execute the command `composer install`. This will install all dependencies into Packages/Libraries/. + +Then run the Unit Tests. + cp typo3_src/composer.json . + composer install + ./bin/phpunit --colors -c typo3/sysext/core/Build/UnitTests.xml typo3conf/ext/coreapi/Tests/Unit/ diff --git a/Tests/Unit/Resources/vfsStream/importCommand/path/to/importfolder/realurl_1.12.8.t3x b/Tests/Unit/Resources/vfsStream/importCommand/path/to/importfolder/realurl_1.12.8.t3x new file mode 100644 index 0000000000000000000000000000000000000000..1a1e6d3516ec30434a3a53878d0048536b85610b GIT binary patch literal 128603 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

Is)NWAY5`@{A-SP%=2l?ebTy+i2* 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 + * 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 2 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. + * A copy is found in the textfile GPL.txt and important notices to the license + * from the author is found in LICENSE.txt distributed with these scripts. + * + * + * 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 TYPO3\CMS\Core\Tests\UnitTestCase; + +/** + * Class CacheApiServiceTest + * + * @package Etobi\CoreApi\Tests\Unit\Service + * @author Stefano Kowalke + * @coversDefaultClass \Etobi\CoreAPI\Service\CacheApiService + */ +class CacheApiServiceTest extends UnitTestCase { + /** + * @var \Etobi\CoreApi\Service\CacheApiService|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $subject + */ + protected $subject; + + /** + * @var \TYPO3\CMS\Core\DataHandling\DataHandler|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $subject + */ + protected $dataHandlerMock; + + /** + * @var \TYPO3\CMS\Core\Authentication\BackendUserAuthentication|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $subject + */ + protected $backendUserAuthenticationMock; + + /** + * @var \TYPO3\CMS\Extbase\Object\ObjectManager|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $subject + */ + protected $objectManagerMock; + + /** + * Setup the test + */ + public function setup() { + $this->subject = $this->getMock('Etobi\\CoreApi\\Service\\CacheApiService', array('clear_cacheCmd')); + $this->dataHandlerMock = $this->getMock('TYPO3\\CMS\\Core\\DataHandling\\DataHandler', array('clear_cacheCmd')); + $this->objectManagerMock = $this->getMock('TYPO3\\CMS\\Extbase\\Object\\ObjectManager'); + $this->backendUserAuthenticationMock = $this->getMock('TYPO3\\CMS\\Core\\Authentication\\BackendUserAuthentication', array('dummy')); + $this->objectManagerMock->expects($this->once())->method('get')->will($this->returnValue($this->backendUserAuthenticationMock)); + + $this->subject->injectDataHandler($this->dataHandlerMock); + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->initializeObject(); + } + + /** + * @test + * @covers ::clearAllCaches + */ + public function clearAllCachesClearAllCaches() { + $this->dataHandlerMock->expects($this->once())->method('clear_cacheCmd')->with('all'); + $this->subject->clearAllCaches(); + } + + /** + * @test + * @covers ::clearPageCache + */ + public function clearPageCacheClearPageCache() { + $this->dataHandlerMock->expects($this->once())->method('clear_cacheCmd')->with('pages'); + $this->subject->clearPageCache(); + } + + /** + * @test + * @covers ::clearConfigurationCache + */ + public function clearConfigurationCacheClearsConfigurationCache() { + $this->dataHandlerMock->expects($this->once())->method('clear_cacheCmd')->with('temp_cached'); + $this->subject->clearConfigurationCache(); + } + + /** + * @test + * @covers ::clearAllExceptPageCache + */ + public function clearAllExceptPageCacheClearsAllExceptPageCache() { + $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'] = array( + 0 => 'cache_core', + 1 => 'cache_classes', + 2 => 'cache_hash', + 3 => 'cache_pages', + 4 => 'cache_pagesection', + 5 => 'cache_phpcode', + 6 => 'cache_runtime', + 7 => 'cache_rootline', + 8 => 'l10n', + 9 => 'extbase_object', + 10 => 'extbase_reflection', + ); + + $cacheManager = $this->getMock('TYPO3\\CMS\\Core\\Cache\\CacheManager'); + $cacheManager->expects($this->exactly(11))->method('hasCache'); + $GLOBALS['typo3CacheManager'] = $cacheManager; + $this->subject->clearAllExceptPageCache(); + } +} \ No newline at end of file diff --git a/Tests/Unit/Service/DatabaseApiServiceTest.php b/Tests/Unit/Service/DatabaseApiServiceTest.php new file mode 100644 index 0000000..86468ed --- /dev/null +++ b/Tests/Unit/Service/DatabaseApiServiceTest.php @@ -0,0 +1,165 @@ + + * 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 2 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. + * A copy is found in the textfile GPL.txt and important notices to the license + * from the author is found in LICENSE.txt distributed with these scripts. + * + * + * 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 TYPO3\CMS\Core\Tests\UnitTestCase; + +/** + * Class DatabaseApiServiceTest + * + * @package Etobi\CoreApi\Tests\Unit\Service + * @author Stefano Kowalke + * @coversDefaultClass \Etobi\CoreAPI\Service\DatabaseApiService + */ +class DatabaseApiServiceTest extends UnitTestCase { + + /** + * @var \Etobi\CoreApi\Service\DatabaseApiService|\PHPUnit_Framework_MockObject_MockObject $subject + */ + protected $subject; + + /** + * Setup the test + */ + public function setup() { + $this->subject = $this->getMock('Etobi\\CoreApi\\Service\\DatabaseApiService', array('dummy')); + } + + /** + * Tears the test down + */ + public function tearDown() { + unset($this->subject); + } + + /** + * @test + * @covers ::databaseCompare + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage No compare modes defined + */ + public function databaseCompareNoCompareModesDefinedThrowsException() { + $objectManagerMock = $this->getMock('TYPO3\\CMS\\Extbase\\Object\\ObjectManager', array('get')); + $classReflectionMock = $this->getMock('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', array(), array(new \Etobi\CoreAPI\Service\DatabaseApiService())); + + $classReflectionMock->expects($this->once())->method('getConstants')->will($this->returnValue($this->getAvailableActions())); + $objectManagerMock->expects($this->once())->method('get')->with('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', 'Etobi\\CoreAPI\\Service\\DatabaseApiService')->will($this->returnValue($classReflectionMock)); + $this->subject->injectObjectManager($objectManagerMock); + + $this->subject->databaseCompare(''); + } + + /** + * @test + * @covers ::databaseCompare + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Action "10" is not available! + */ + public function databaseCompareActionsNoDefinedThrowsException() { + $objectManagerMock = $this->getMock('TYPO3\\CMS\\Extbase\\Object\\ObjectManager', array('get')); + $classReflectionMock = $this->getMock('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', array(), array(new \Etobi\CoreAPI\Service\DatabaseApiService())); + $this->subject = $this->getMock('Etobi\\CoreApi\\Service\\DatabaseApiService', array('trimExplode')); + + $classReflectionMock->expects($this->once())->method('getConstants')->will($this->returnValue($this->getAvailableActions())); + $objectManagerMock->expects($this->once())->method('get')->with('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', 'Etobi\\CoreAPI\\Service\\DatabaseApiService')->will($this->returnValue($classReflectionMock)); + $this->subject->expects($this->once())->method('trimExplode')->with('10')->will($this->returnValue(array('10'))); + $this->subject->injectObjectManager($objectManagerMock); + + $this->subject->databaseCompare('10'); + } + + /** + * @test + * @covers ::databaseCompare + */ + public function databaseCompareOneAction() { + $this->markTestIncomplete(); + $objectManagerMock = $this->getMock('TYPO3\\CMS\\Extbase\\Object\\ObjectManager', array('get')); + $classReflectionMock = $this->getMock('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', array(), array(new \Etobi\CoreAPI\Service\DatabaseApiService())); + $this->subject = $this->getMock('Etobi\\CoreApi\\Service\\DatabaseApiService', array('trimExplode')); + + $classReflectionMock->expects($this->once())->method('getConstants')->will($this->returnValue($this->getAvailableActions())); + $objectManagerMock->expects($this->once())->method('get')->with('TYPO3\\CMS\\Extbase\\Reflection\\ClassReflection', 'Etobi\\CoreAPI\\Service\\DatabaseApiService')->will($this->returnValue($classReflectionMock)); + $this->subject->expects($this->once())->method('trimExplode')->with('1')->will($this->returnValue(array('1'))); + $this->subject->injectObjectManager($objectManagerMock); + + $this->subject->databaseCompare('1'); + } + + /** + * Returns the complete available actions + * + * @return array + */ + protected function getAvailableActions() { + return array( + 'ACTION_UPDATE_CLEAR_TABLE' => 1, + 'ACTION_UPDATE_ADD' => 2, + 'ACTION_UPDATE_CHANGE' => 3, + 'ACTION_UPDATE_CREATE_TABLE' => 4, + 'ACTION_REMOVE_CHANGE' => 5, + 'ACTION_REMOVE_DROP' => 6, + 'ACTION_REMOVE_CHANGE_TABLE' => 7, + 'ACTION_REMOVE_DROP_TABLE' => 8 + ); + } + + protected function getLoadedExtensions() { + return array( + 'core' => array( + 'type' => 'S', + 'siteRelPath' => 'typo3/sysext/core/', + 'typo3RelPath' => 'sysext/core/', + 'ext_localconf.php' => '/Volumes/HDD/Users/sok/Sites/TYPO3/www.coreapi.dev/http/typo3/sysext/core/ext_localconf.php', + 'ext_tables.php' => '/Volumes/HDD/Users/sok/Sites/TYPO3/www.coreapi.dev/http/typo3/sysext/core/ext_tables.php', + 'ext_tables.sql' => '/Volumes/HDD/Users/sok/Sites/TYPO3/www.coreapi.dev/http/typo3/sysext/core/ext_tables.sql', + 'ext_icon' => 'ext_icon.png' + ), + 'backend' => array( + 'type' => 'S', + 'siteRelPath' => 'typo3/sysext/backend/', + 'typo3RelPath' => 'sysext/backend/', + 'ext_localconf.php' => '/Volumes/HDD/Users/sok/Sites/TYPO3/www.coreapi.dev/http/typo3/sysext/backend/ext_localconf.php', + 'ext_tables.php' => '/Volumes/HDD/Users/sok/Sites/TYPO3/www.coreapi.dev/http/typo3/sysext/backend/ext_tables.php', + 'ext_icon' => 'ext_icon.png' + ), + 'extbase' => array( + 'type' => 'S', + 'siteRelPath' => 'typo3/sysext/extbase/', + 'typo3RelPath' => 'sysext/extbase/', + 'ext_localconf.php' => '/Volumes/HDD/Users/sok/Sites/TYPO3/www.coreapi.dev/http/typo3/sysext/extbase/ext_localconf.php', + 'ext_tables.php' => '/Volumes/HDD/Users/sok/Sites/TYPO3/www.coreapi.dev/http/typo3/sysext/extbase/ext_tables.php', + 'ext_tables.sql' => '/Volumes/HDD/Users/sok/Sites/TYPO3/www.coreapi.dev/http/typo3/sysext/extbase/ext_tables.sql', + 'ext_typoscript_setup.txt' => '/Volumes/HDD/Users/sok/Sites/TYPO3/www.coreapi.dev/http/typo3/sysext/extbase/ext_typoscript_setup.txt', + 'ext_icon' => 'ext_icon.png' + ), + ); + } +} + diff --git a/Tests/Unit/Service/ExtensionApiServiceTest.php b/Tests/Unit/Service/ExtensionApiServiceTest.php new file mode 100644 index 0000000..12f66b5 --- /dev/null +++ b/Tests/Unit/Service/ExtensionApiServiceTest.php @@ -0,0 +1,1926 @@ + + * 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 2 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. + * A copy is found in the textfile GPL.txt and important notices to the license + * from the author is found in LICENSE.txt distributed with these scripts. + * + * + * 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 InvalidArgumentException; +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; +use TYPO3\CMS\Core\Tests\UnitTestCase; + +/** + * Class ExtensionApiServiceTest + * + * @package Etobi\CoreApi\Tests\Unit\Service + * @author Stefano Kowalke + * @coversDefaultClass \Etobi\CoreAPI\Service\ExtensionApiService + */ +class ExtensionApiServiceTest extends UnitTestCase { + + /** + * @var \Etobi\CoreApi\Service\ExtensionApiService|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $subject + */ + protected $subject; + + /** + * @var \TYPO3\CMS\Extensionmanager\Utility\Repository\Helper|\PHPUnit_Framework_MockObject_MockObject $repositoryHelperMock + */ + protected $repositoryHelperMock; + + /** + * @var \TYPO3\CMS\Extensionmanager\Domain\Model\Mirrors|\PHPUnit_Framework_MockObject_MockObject + */ + protected $mirrorsMock; + + /** + * @var \TYPO3\CMS\Extensionmanager\Domain\Model\Extension|\PHPUnit_Framework_MockObject_MockObject $extensionMock + */ + protected $extensionMock; + + /** + * @var |\PHPUnit_Framework_MockObject_MockObject $extensionRepositoryMock + */ + protected $extensionRepositoryMock; + + /** + * @var \TYPO3\CMS\Extensionmanager\Domain\Repository\RepositoryRepository|\PHPUnit_Framework_MockObject_MockObject $repositoryRepositoryMock + */ + protected $repositoryRepositoryMock; + + /** + * @var \TYPO3\CMS\Extensionmanager\Utility\ConfigurationUtility|\PHPUnit_Framework_MockObject_MockObject $repositoryRepositoryMock + */ + protected $configurationMock; + + /** + * @var \TYPO3\CMS\Extensionmanager\Service\ExtensionManagementService $extensionManagementService + */ + protected $extensionManagementServiceMock; + + /** + * @var \TYPO3\CMS\Extbase\Object\ObjectManager $objectManagerMock + */ + protected $objectManagerMock; + + /** + * @var string $installPath + */ + protected $installPath = 'root/coreapi/'; + + /** + * Set the test up + */ + public function setup() { + $this->subject = $this->getAccessibleMock('Etobi\\CoreApi\\Service\\ExtensionApiService', array('dummy')); + $this->objectManagerMock = $this->getMock('TYPO3\\CMS\\Extbase\\Object\\ObjectManager', array('get')); + $this->extensionMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Model\\Extension', array('dummy')); + + $fileHandlingUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\FileHandlingUtility'); + $this->repositoryHelperMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\Repository\\Helper', array(), array(), '', FALSE); + $this->mirrorsMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Model\\Mirrors'); + + $this->subject->injectFileHandlingUtility($fileHandlingUtility); + } + + // + // Tests for importExtension() + // + + /** + * @test + * @covers ::importExtension + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage File "vfs://root/path/to/importfolder/dummy.t3x" does not exist! + */ + public function importExtensionDemandedFileNotExistsThrowsException() { + vfsStream::setup('root'); + vfsStream::create(array( + 'path' => array( + 'to' => array( + 'importfolder' => array( + ) + ) + ) + )); + + $this->subject->importExtension(vfsStream::url('root/path/to/importfolder/dummy.t3x')); + } + + /** + * @test + * @covers ::importExtension + * @expectedException InvalidArgumentException + * @expectedExceptionMessage System installation is not allowed! + */ + public function importExtensionDemandedLocationNotAllowed() { + vfsStream::setup('root'); + vfsStream::create(array( + 'path' => array( + 'to' => array( + 'importfolder' => array( + 'dummy.t3x' => 'File exists' + ) + ) + ) + )); + + $this->subject->importExtension(vfsStream::url('root/path/to/importfolder/dummy.t3x'), 'System'); + } + + /** + * @test + * @covers ::importExtension + * @expectedException \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException + * @expectedExceptionMessage File had no or wrong content. + */ + public function importExtensionDemandedFileIsEmpty() { + vfsStream::setup('root'); + vfsStream::create(array( + 'path' => array( + 'to' => array( + 'importfolder' => array( + 'dummy.t3x' => '' + ) + ) + ) + )); + + $uploadExtensionFileController = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Extensionmanager\\Controller\\UploadExtensionFileController'); + + $this->objectManagerMock->expects($this->once())->method('get')->will($this->returnValue($uploadExtensionFileController)); + + $this->subject->injectObjectManager($this->objectManagerMock); + + $this->subject->importExtension(vfsStream::url('root/path/to/importfolder/dummy.t3x'), 'Local'); + } + + /** + * @test + * @covers ::importExtension + * @expectedException \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException + * @expectedExceptionMessage Decoding the file went wrong. No extension key found + */ + public function importExtensionDemandedFileDataIsNotAnArray() { + vfsStream::setup('root'); + vfsStream::copyFromFileSystem(PATH_site . 'typo3conf/ext/coreapi/Tests/Unit/Resources/vfsStream/importCommand/'); + + $importFile = 'root/path/to/importfolder/realurl_1.12.8.t3x'; + $fetchData = ''; + + $uploadExtensionFileController = $this->getAccessibleMock('TYPO3\\CMS\\Extensionmanager\\Controller\\UploadExtensionFileController', array('dummy')); + $terUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\Connection\\TerUtility', array('decodeExchangeData')); + + $terUtilityMock->expects($this->once())->method('decodeExchangeData')->will($this->returnValue($fetchData)); + + $uploadExtensionFileController->_set('terUtility', $terUtilityMock); + $this->objectManagerMock->expects($this->once())->method('get')->will($this->returnValue($uploadExtensionFileController)); + + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->importExtension(vfsStream::url('root/path/to/importfolder/realurl_1.12.8.t3x'), 'Local'); + } + + /** + * @test + * @covers ::importExtension + * @expectedException \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException + * @expectedExceptionMessage Decoding the file went wrong. No extension key found + */ + public function importExtensionDemandedFileDataMissesKey() { + vfsStream::setup('root'); + vfsStream::copyFromFileSystem(PATH_site . 'typo3conf/ext/coreapi/Tests/Unit/Resources/vfsStream/importCommand/'); + + $importFile = 'root/path/to/importfolder/realurl_1.12.8.t3x'; + $fetchData = array( + 'EM_CONF' => array( + 'title' => 'RealURL: speaking paths for TYPO3', + 'description' => 'Creates nice looking URLs for TYPO3 pages: converts http://example.com/index.phpid=12345&L=2 to http://example.com/path/to/your/page/. Please, ask for free support in TYPO3 mailing lists or contact the maintainer for paid support.', + 'category' => 'fe', + 'shy' => 0, + 'version' => '1.12.8', + 'dependencies' => '', + 'conflicts' => '', + 'priority' => '', + 'loadOrder' => '', + 'TYPO3_version' => '4.5.0-6.2.999', + 'PHP_version' => '5.3.2-5.999.999', + 'module' => '', + 'state' => 'stable', + 'uploadfolder' => 0, + 'createDirs' => '', + 'modifiy_tables' => 'pages,sys_domain,pages_language_overlay,sys_template', + 'clearcacheonload' => 1, + 'lockType' => '', + 'author' => 'Dmitry Dulepov', + 'author_email' => 'dmitry.dulepov@gmail.com', + 'author_company' => '', + 'CGLcompliance' => NULL, + 'CGLcompliance_note' => NULL + ), + 'misc' => array(), + 'techInfo' => array(), + 'FILES' => array() + ); + + $uploadExtensionFileController = $this->getAccessibleMock('TYPO3\\CMS\\Extensionmanager\\Controller\\UploadExtensionFileController', array('dummy')); + $terUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\Connection\\TerUtility', array('decodeExchangeData')); + + $terUtilityMock->expects($this->once())->method('decodeExchangeData')->will($this->returnValue($fetchData)); + + $uploadExtensionFileController->_set('terUtility', $terUtilityMock); + $this->objectManagerMock->expects($this->once())->method('get')->will($this->returnValue($uploadExtensionFileController)); + + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->importExtension(vfsStream::url('root/path/to/importfolder/realurl_1.12.8.t3x'), 'Local'); + } + + /** + * @test + * @covers ::importExtension + * @expectedException \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException + */ + public function importExtensionOptionOverrideFalseExtensionAlreadyExists() { + vfsStream::setup('root'); + vfsStream::copyFromFileSystem(PATH_site . 'typo3conf/ext/coreapi/Tests/Unit/Resources/vfsStream/importCommand/'); + vfsStream::create(array('typo3conf' => array('ext' => array('realurl' => array())))); + + $importFile = 'root/path/to/importfolder/realurl_1.12.8.t3x'; + $fetchData = array( + 'extKey' => 'realurl', + 'EM_CONF' => array( + 'title' => 'RealURL: speaking paths for TYPO3', + 'description' => 'Creates nice looking URLs for TYPO3 pages: converts http://example.com/index.phpid=12345&L=2 to http://example.com/path/to/your/page/. Please, ask for free support in TYPO3 mailing lists or contact the maintainer for paid support.', + 'category' => 'fe', + 'shy' => 0, + 'version' => '1.12.8', + 'dependencies' => '', + 'conflicts' => '', + 'priority' => '', + 'loadOrder' => '', + 'TYPO3_version' => '4.5.0-6.2.999', + 'PHP_version' => '5.3.2-5.999.999', + 'module' => '', + 'state' => 'stable', + 'uploadfolder' => 0, + 'createDirs' => '', + 'modifiy_tables' => 'pages,sys_domain,pages_language_overlay,sys_template', + 'clearcacheonload' => 1, + 'lockType' => '', + 'author' => 'Dmitry Dulepov', + 'author_email' => 'dmitry.dulepov@gmail.com', + 'author_company' => '', + 'CGLcompliance' => NULL, + 'CGLcompliance_note' => NULL + ), + 'misc' => array(), + 'techInfo' => array(), + 'FILES' => array() + ); + + $uploadExtensionFileController = $this->getAccessibleMock('TYPO3\\CMS\\Extensionmanager\\Controller\\UploadExtensionFileController', array('translate')); + $terUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\Connection\\TerUtility', array('decodeExchangeData')); + $installUtilityMock = $this->getMock('TYPO3\\CMS\Extensionmanager\\Utility\\InstallUtility', array('isAvailable')); + + $terUtilityMock->expects($this->once())->method('decodeExchangeData')->will($this->returnValue($fetchData)); + $installUtilityMock->expects($this->once())->method('isAvailable')->will($this->returnValue(TRUE)); + + $uploadExtensionFileController->_set('terUtility', $terUtilityMock); + $uploadExtensionFileController->_set('installUtility', $installUtilityMock); + $this->objectManagerMock->expects($this->once())->method('get')->will($this->returnValue($uploadExtensionFileController)); + + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->importExtension(vfsStream::url('root/path/to/importfolder/realurl_1.12.8.t3x'), 'Local'); + } + + /** + * @test + * @covers ::importExtension + */ + public function importExtensionOptionOverrideTrueExtensionAlreadyExists() { + vfsStream::setup('root'); + vfsStream::copyFromFileSystem(PATH_site . 'typo3conf/ext/coreapi/Tests/Unit/Resources/vfsStream/importCommand/'); + vfsStream::create(array('typo3conf' => array('ext' => array('realurl' => array())))); + + $importFile = 'root/path/to/importfolder/realurl_1.12.8.t3x'; + $fetchData = array( + 'extKey' => 'realurl', + 'EM_CONF' => array( + 'title' => 'RealURL: speaking paths for TYPO3', + 'description' => 'Creates nice looking URLs for TYPO3 pages: converts http://example.com/index.phpid=12345&L=2 to http://example.com/path/to/your/page/. Please, ask for free support in TYPO3 mailing lists or contact the maintainer for paid support.', + 'category' => 'fe', + 'shy' => 0, + 'version' => '1.12.8', + 'dependencies' => '', + 'conflicts' => '', + 'priority' => '', + 'loadOrder' => '', + 'TYPO3_version' => '4.5.0-6.2.999', + 'PHP_version' => '5.3.2-5.999.999', + 'module' => '', + 'state' => 'stable', + 'uploadfolder' => 0, + 'createDirs' => '', + 'modifiy_tables' => 'pages,sys_domain,pages_language_overlay,sys_template', + 'clearcacheonload' => 1, + 'lockType' => '', + 'author' => 'Dmitry Dulepov', + 'author_email' => 'dmitry.dulepov@gmail.com', + 'author_company' => '', + 'CGLcompliance' => NULL, + 'CGLcompliance_note' => NULL + ), + 'misc' => array(), + 'techInfo' => array(), + 'FILES' => array() + ); + + $uploadExtensionFileController = $this->getAccessibleMock('TYPO3\\CMS\\Extensionmanager\\Controller\\UploadExtensionFileController', array('translate', 'copyExtensionFolderToTempFolder')); + $terUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\Connection\\TerUtility', array('decodeExchangeData')); + $installUtilityMock = $this->getMock('TYPO3\\CMS\Extensionmanager\\Utility\\InstallUtility', array('isAvailable')); + $fileHandlingUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\FileHandlingUtility', array('unpackExtensionFromExtensionDataArray')); + + $terUtilityMock->expects($this->once())->method('decodeExchangeData')->will($this->returnValue($fetchData)); + $installUtilityMock->expects($this->once())->method('isAvailable')->will($this->returnValue(TRUE)); + $fileHandlingUtility->expects($this->once())->method('unpackExtensionFromExtensionDataArray')->with($fetchData); + $uploadExtensionFileController->expects($this->once())->method('copyExtensionFolderToTempFolder')->with('realurl'); + + $uploadExtensionFileController->_set('terUtility', $terUtilityMock); + $uploadExtensionFileController->_set('installUtility', $installUtilityMock); + $uploadExtensionFileController->_set('fileHandlingUtility', $fileHandlingUtility); + $this->objectManagerMock->expects($this->once())->method('get')->will($this->returnValue($uploadExtensionFileController)); + + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->importExtension(vfsStream::url('root/path/to/importfolder/realurl_1.12.8.t3x'), 'Local', TRUE); + } + /** + * @test + * @covers ::importExtension + */ + public function importExtensionOptionOverrideFalseExtensionNotExists() { + vfsStream::setup('root'); + vfsStream::copyFromFileSystem(PATH_site . 'typo3conf/ext/coreapi/Tests/Unit/Resources/vfsStream/importCommand/'); + vfsStream::create(array('typo3conf' => array('ext' => array('realurl' => array())))); + + $importFile = 'root/path/to/importfolder/realurl_1.12.8.t3x'; + $fetchData = array( + 'extKey' => 'realurl', + 'EM_CONF' => array( + 'title' => 'RealURL: speaking paths for TYPO3', + 'description' => 'Creates nice looking URLs for TYPO3 pages: converts http://example.com/index.phpid=12345&L=2 to http://example.com/path/to/your/page/. Please, ask for free support in TYPO3 mailing lists or contact the maintainer for paid support.', + 'category' => 'fe', + 'shy' => 0, + 'version' => '1.12.8', + 'dependencies' => '', + 'conflicts' => '', + 'priority' => '', + 'loadOrder' => '', + 'TYPO3_version' => '4.5.0-6.2.999', + 'PHP_version' => '5.3.2-5.999.999', + 'module' => '', + 'state' => 'stable', + 'uploadfolder' => 0, + 'createDirs' => '', + 'modifiy_tables' => 'pages,sys_domain,pages_language_overlay,sys_template', + 'clearcacheonload' => 1, + 'lockType' => '', + 'author' => 'Dmitry Dulepov', + 'author_email' => 'dmitry.dulepov@gmail.com', + 'author_company' => '', + 'CGLcompliance' => NULL, + 'CGLcompliance_note' => NULL + ), + 'misc' => array(), + 'techInfo' => array(), + 'FILES' => array() + ); + + $uploadExtensionFileController = $this->getAccessibleMock('TYPO3\\CMS\\Extensionmanager\\Controller\\UploadExtensionFileController', array('translate', 'copyExtensionFolderToTempFolder')); + $terUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\Connection\\TerUtility', array('decodeExchangeData')); + $installUtilityMock = $this->getMock('TYPO3\\CMS\Extensionmanager\\Utility\\InstallUtility', array('isAvailable')); + $fileHandlingUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\FileHandlingUtility', array('unpackExtensionFromExtensionDataArray')); + + $terUtilityMock->expects($this->once())->method('decodeExchangeData')->will($this->returnValue($fetchData)); + $installUtilityMock->expects($this->once())->method('isAvailable')->will($this->returnValue(FALSE)); + $fileHandlingUtility->expects($this->once())->method('unpackExtensionFromExtensionDataArray')->with($fetchData); + $uploadExtensionFileController->expects($this->never())->method('translate'); + $uploadExtensionFileController->expects($this->never())->method('copyExtensionFolderToTempFolder')->with('realurl'); + + $uploadExtensionFileController->_set('terUtility', $terUtilityMock); + $uploadExtensionFileController->_set('installUtility', $installUtilityMock); + $uploadExtensionFileController->_set('fileHandlingUtility', $fileHandlingUtility); + $this->objectManagerMock->expects($this->once())->method('get')->will($this->returnValue($uploadExtensionFileController)); + + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->importExtension(vfsStream::url('root/path/to/importfolder/realurl_1.12.8.t3x'), 'Local'); + } + + // + // Tests for getExtensionInformation() + // + + /** + * @test + * @covers ::getExtensionInformation + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage No extension key given! + */ + public function getExtensionInformationNoExtensionKeyGivenThrowsException(){ + $this->subject->getExtensionInformation(''); + } + + /** + * @test + * @covers ::getExtensionInformation + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Extension "dummy" does not exist! + */ + public function getExtensionInformationExtensionNotFoundThrowsException() { + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isAvailable')); + + $installUtilityMock->expects($this->once())->method('isAvailable')->with('dummy')->will($this->returnValue(FALSE)); + + $this->subject->injectInstallUtility($installUtilityMock); + + $this->subject->getExtensionInformation('dummy'); + } + + /** + * @test + * @covers ::getExtensionInformation + */ + public function getExtensionInformationReturnsInformation() { + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isAvailable', 'isLoaded')); + $this->subject = $this->getAccessibleMock('Etobi\\CoreApi\\Service\\ExtensionApiService', array('listExtensions')); + + $installUtilityMock->expects($this->once())->method('isAvailable')->with('dummy')->will($this->returnValue(TRUE)); + $installUtilityMock->expects($this->once())->method('isLoaded')->with('dummy')->will($this->returnValue(TRUE)); + $this->subject->expects($this->once())->method('listExtensions')->will($this->returnValue($this->getExtensionArrayForCreateUploadFolders())); + + $this->subject->injectInstallUtility($installUtilityMock); + + $currentExtensionInformation = $this->subject->getExtensionInformation('dummy'); + + $this->assertArrayHasKey('em_conf', $currentExtensionInformation); + $this->assertArrayHasKey('is_installed', $currentExtensionInformation); + $this->assertSame($currentExtensionInformation['em_conf']['title'], 'Dummy Extension for testing'); + $this->assertSame($currentExtensionInformation['is_installed'], TRUE); + } + + /** + * @test + * @covers ::listExtensions + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Only "Local", "System", "Global" and "" (all) are supported as type + */ + public function getInstalledExtensionWrongTypeGivenThrowsException() { + $this->subject->listExtensions('42'); + } + + /** + * @test + * @covers ::listExtensions + */ + public function getInstalledExtensionReturnsListOfLocalExtensions() { + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isLoaded')); + $listUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\ListUtility', array('getAvailableExtensions')); + $emConfUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\EmConfUtility', array('includeEmConf')); + + $installUtilityMock->expects($this->any())->method('isLoaded')->will($this->returnValue(TRUE)); + $extensions = $this->getFakeInstalledExtensionArray(); + $listUtility->expects($this->exactly(2))->method('getAvailableExtensions')->will($this->returnValue($extensions)); + $emConfUtility->expects($this->at(0))->method('includeEmConf'); + $this->objectManagerMock->expects($this->at(0))->method('get')->will($this->returnValue($listUtility)); + $this->objectManagerMock->expects($this->at(1))->method('get')->will($this->returnValue($emConfUtility)); + $this->objectManagerMock->expects($this->at(2))->method('get')->will($this->returnValue($listUtility)); + $emConfUtility->expects($this->at(2))->method('includeEmConf')->will($this->returnValue($extensions['coreapi'])); + $this->objectManagerMock->expects($this->at(3))->method('get')->will($this->returnValue($emConfUtility)); + + $this->subject->injectInstallUtility($installUtilityMock); + $this->subject->injectObjectManager($this->objectManagerMock); + + $listLocal = $this->subject->listExtensions('Local'); + $this->assertTrue(count($listLocal) === 0); + $listLocal = $this->subject->listExtensions('Local'); + $this->assertTrue(count($listLocal) === 1); + } + + /** + * @test + * @covers ::listExtensions + */ + public function getInstalledExtensionReturnsListOfGlobalExtensions() { + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isLoaded')); + $listUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\ListUtility', array('getAvailableExtensions')); + $emConfUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\EmConfUtility', array('includeEmConf')); + + $installUtilityMock->expects($this->any())->method('isLoaded')->will($this->returnValue(TRUE)); + $extensions = $this->getFakeInstalledExtensionArray(); + $listUtility->expects($this->exactly(2))->method('getAvailableExtensions')->will($this->returnValue($extensions)); + $emConfUtility->expects($this->at(0))->method('includeEmConf'); + $this->objectManagerMock->expects($this->at(0))->method('get')->will($this->returnValue($listUtility)); + $this->objectManagerMock->expects($this->at(1))->method('get')->will($this->returnValue($emConfUtility)); + $this->objectManagerMock->expects($this->at(2))->method('get')->will($this->returnValue($listUtility)); + $emConfUtility->expects($this->at(1))->method('includeEmConf')->will($this->returnValue($extensions['coreapi'])); + $this->objectManagerMock->expects($this->at(3))->method('get')->will($this->returnValue($emConfUtility)); + + $this->subject->injectInstallUtility($installUtilityMock); + $this->subject->injectObjectManager($this->objectManagerMock); + + $listGlobal = $this->subject->listExtensions('Global'); + $this->assertTrue(count($listGlobal) === 0); + $listGlobal = $this->subject->listExtensions('Global'); + $this->assertTrue(count($listGlobal) === 1); + } + + /** + * @test + * @covers ::listExtensions + */ + public function getInstalledExtensionReturnsListOfSystemExtensions() { + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isLoaded')); + $listUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\ListUtility', array('getAvailableExtensions')); + $emConfUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\EmConfUtility', array('includeEmConf')); + + $installUtilityMock->expects($this->any())->method('isLoaded')->will($this->returnValue(TRUE)); + $listUtility->expects($this->exactly(2))->method('getAvailableExtensions')->will($this->returnValue($this->getFakeInstalledExtensionArray())); + $this->objectManagerMock->expects($this->at(0))->method('get')->will($this->returnValue($listUtility)); + $this->objectManagerMock->expects($this->at(2))->method('get')->will($this->returnValue($listUtility)); + $emConfUtility->expects($this->at(0))->method('includeEmConf')->will($this->returnValue(array())); + $emConfUtility->expects($this->at(1))->method('includeEmConf')->will($this->returnValue(array())); + + $extensions = $this->getExtensionArrayForCreateUploadFolders(); + $emConfUtility->expects($this->at(2))->method('includeEmConf')->will($this->returnValue($extensions['core'])); + $emConfUtility->expects($this->at(3))->method('includeEmConf')->will($this->returnValue($extensions['backend'])); + $this->objectManagerMock->expects($this->at(1))->method('get')->will($this->returnValue($emConfUtility)); + $this->objectManagerMock->expects($this->at(3))->method('get')->will($this->returnValue($emConfUtility)); + + $this->subject->injectInstallUtility($installUtilityMock); + $this->subject->injectObjectManager($this->objectManagerMock); + + $listSystem = $this->subject->listExtensions('System'); + $this->assertTrue(count($listSystem) === 0); + + $listSystem = $this->subject->listExtensions('System'); + $this->assertTrue(count($listSystem) === 2); + } + + // + // Tests for updateMirrors() + // + /** + * Creates and returns a repository object + * + * @return \TYPO3\CMS\ExtensionManager\Domain\Model\Repository + */ + public function getRepositoryData() { + $repository = new \TYPO3\CMS\ExtensionManager\Domain\Model\Repository(); + $repository->setTitle('TYPO3.org Main Repository'); + $repository->setDescription('Main repository on typo3.org. This repository has some mirrors configured which are available with the mirror url.'); + $repository->setMirrorListUrl('http://repositories.typo3.org/mirrors.xml.gz'); + $repository->setWsdlUrl('http://typo3.org/wsdl/tx_ter_wsdl.php'); + $repository->setLastUpdate(new \DateTime('now')); + $repository->setExtensionCount(42); + $repository->setPid(0); + + return $repository; + } + + /** + * @test + * @covers ::updateMirrors + */ + public function updateMirrorsReturnsFalse() { + $repositoryRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\RepositoryRepository', array('findAll'), array(), '', FALSE); + $repositoryRepositoryMock->expects($this->once())->method('findAll')->will($this->returnValue(array($this->getRepositoryData()))); + $this->repositoryHelperMock->expects($this->once())->method('updateExtList')->will($this->returnValue(FALSE)); + + $this->subject->injectRepositoryHelper($this->repositoryHelperMock); + $this->subject->injectRepositoryRepository($repositoryRepositoryMock); + + $this->assertFalse($this->subject->updateMirrors()); + } + + /** + * @test + * @covers ::updateMirrors + */ + public function updateMirrorsReturnsTrue() { + $repositoryRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\RepositoryRepository', array('findAll'), array(), '', FALSE); + $repositoryRepositoryMock->expects($this->once())->method('findAll')->will($this->returnValue(array($this->getRepositoryData()))); + $this->repositoryHelperMock->expects($this->once())->method('updateExtList')->will($this->returnValue(TRUE)); + + $this->subject->injectRepositoryHelper($this->repositoryHelperMock); + $this->subject->injectRepositoryRepository($repositoryRepositoryMock); + + $this->assertTrue($this->subject->updateMirrors()); + } + + // + // Tests for createUpdateFolders() + // + + /** + * Provides the array which is return value from ExtensionServiceApi::getInstalledExtensions() + * + * @return array + */ + public function getExtensionArrayForCreateUploadFolders() { + return array( + 'about' => array( + 'title' => 'Help>About', + 'description' => 'Shows info about TYPO3 and installed extensions', + 'category' => 'module', + 'shy' => 1, + 'dependencies' => '', + 'conflicts' => '', + 'priority' => '', + 'loadOrder' => '', + 'module' => 'mod', + 'state' => 'stable', + 'internal' => 0, + 'uploadfolder' => 0, + 'createDirs' => '', + 'modify_tables' => '', + 'clearCacheOnLoad' => '', + 'lockType' => '', + 'author' => 'Kasper Skaarhoj', + 'author_email' => 'kasperYYYY@typo3.com', + 'author_company' => 'Curby Soft Multimedia', + 'CGLcompilance' => '', + 'CGLcompilance_note' => '', + 'version' => '6.2.0', + '_md5_values_when_last_written' => '', + 'constraints' => array(), + 'suggests' => array(), + 'key' => 'about' + ), + 'backend' => array( + 'title' => 'TYPO3 Backend', + 'description' => 'Classes for the TYPO3 backend.', + 'category' => 'be', + 'shy' => 1, + 'dependencies' => '', + 'conflicts' => '', + 'priority' => 'top', + 'loadOrder' => '', + 'module' => '', + 'state' => 'stable', + 'internal' => 1, + 'uploadfolder' => 0, + 'createDirs' => '', + 'modify_tables' => '', + 'clearCacheOnLoad' => 0, + 'lockType' => 'S', + 'author' => 'Kasper Skaarhoj', + 'author_email' => 'kasperYYYY@typo3.com', + 'author_company' => 'Curby Soft Multimedia', + 'CGLcompilance' => '', + 'CGLcompilance_note' => '', + 'version' => '6.2.0', + '_md5_values_when_last_written' => '', + 'constraints' => array(), + 'suggests' => array(), + 'key' => 'about' + ), + 'core' => array( + 'title' => 'TYPO3 Core', + 'description' => 'Classes for the TYPO3 backend.', + 'category' => 'be', + 'shy' => 1, + 'dependencies' => '', + 'conflicts' => '', + 'priority' => 'top', + 'loadOrder' => '', + 'module' => '', + 'state' => 'stable', + 'internal' => 1, + 'uploadfolder' => 0, + 'createDirs' => '', + 'modify_tables' => '', + 'clearCacheOnLoad' => 0, + 'lockType' => 'S', + 'author' => 'Kasper Skaarhoj', + 'author_email' => 'kasperYYYY@typo3.com', + 'author_company' => 'Curby Soft Multimedia', + 'CGLcompilance' => '', + 'CGLcompilance_note' => '', + 'version' => '6.2.0', + '_md5_values_when_last_written' => '', + 'constraints' => array(), + 'suggests' => array(), + 'key' => 'about' + ), + 'dummy' => array( + 'title' => 'Dummy Extension for testing', + 'description' => 'This is just a dummy extension', + 'category' => 'experimental', + 'shy' => 1, + 'dependencies' => '', + 'conflicts' => '', + 'priority' => '', + 'loadOrder' => '', + 'module' => 'mod', + 'state' => 'stable', + 'internal' => 0, + 'uploadfolder' => 0, + 'createDirs' => '', + 'modify_tables' => '', + 'clearCacheOnLoad' => '', + 'lockType' => '', + 'author' => 'Stefano Kowalke', + 'author_email' => 'blueduck@gmx.net', + 'author_company' => 'Arroba IT', + 'CGLcompilance' => '', + 'CGLcompilance_note' => '', + 'version' => '6.2.0', + '_md5_values_when_last_written' => '', + 'constraints' => array(), + 'suggests' => array(), + 'key' => 'about' + ) + ); + } + + // + // Tests for installExtension() + // + + /** + * @test + * @covers ::installExtension + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Extension "dummy" does not exist! + */ + public function installExtensionNonExistentExtensionThrowsException() { + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isAvailable')); + + $installUtilityMock->expects($this->once())->method('isAvailable')->with('dummy')->will($this->returnValue(FALSE)); + + $this->subject->injectInstallUtility($installUtilityMock); + + $this->subject->installExtension('dummy'); + } + + /** + * @test + * @covers ::installExtension + */ + public function installExtensionInstallsExtension() { + $cacheManagerMock = $this->getMock('TYPO3\\CMS\\Core\\Cache\\CacheManager'); + $installUtilityMock = $this->getAccessibleMock( + 'TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', + array( + 'enrichExtensionWithDetails', + 'processDatabaseUpdates', + 'ensureConfiguredDirectoriesExist', + 'importInitialFiles', + 'isLoaded', + 'loadExtension', + 'reloadCaches', + 'processRuntimeDatabaseUpdates', + 'saveDefaultConfiguration', + 'isAvailable' + )); + + $extensions = $this->getExtensionArrayForCreateUploadFolders(); + $installUtilityMock + ->expects($this->once()) + ->method('enrichExtensionWithDetails') + ->with('about') + ->will($this->returnValue($extensions['about']) + ); + $installUtilityMock->expects($this->once())->method('processDatabaseUpdates')->with($extensions['about']); + $installUtilityMock->expects($this->once())->method('ensureConfiguredDirectoriesExist'); + $installUtilityMock->expects($this->once())->method('importInitialFiles'); + $installUtilityMock->expects($this->once())->method('isLoaded'); + $installUtilityMock->expects($this->once())->method('loadExtension'); + $installUtilityMock->expects($this->once())->method('reloadCaches'); + $installUtilityMock->expects($this->once())->method('processRuntimeDatabaseUpdates'); + $installUtilityMock->expects($this->once())->method('saveDefaultConfiguration'); + $installUtilityMock->expects($this->once())->method('isAvailable')->with('about')->will($this->returnValue(TRUE)); + + $installUtilityMock->_set('cacheManager', $cacheManagerMock); + $this->subject->injectInstallUtility($installUtilityMock); + + $this->subject->installExtension('about'); + } + + // + // Tests for uninstallExtension() + // + /** + * @test + * @covers ::uninstallExtension + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Extension "coreapi" cannot be uninstalled! + */ + public function uninstallExtensionCoreApiThrowsException() { + $this->subject->uninstallExtension('coreapi'); + } + + /** + * @test + * @covers ::uninstallExtension + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Extension "dummy" does not exist! + */ + public function uninstallExtensionWhichNotExistsThrowsException() { + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isAvailable')); + + $installUtilityMock->expects($this->once())->method('isAvailable')->with('dummy')->will($this->returnValue(FALSE)); + + $this->subject->injectInstallUtility($installUtilityMock); + + $this->subject->uninstallExtension('dummy'); + } + + /** + * @test + * @covers ::uninstallExtension + */ + public function uninstallExtensionUninstallExtension() { + $installUtilityMock = $this->getAccessibleMock( + 'TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', + array('unloadExtension', 'isAvailable', 'isLoaded') + ); + $dependencyManagerMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\DependencyUtility'); + + $dependencyManagerMock->expects($this->once())->method('findInstalledExtensionsThatDependOnMe')->will($this->returnValue(array())); + $installUtilityMock->expects($this->once())->method('unloadExtension'); + $installUtilityMock->expects($this->once())->method('isAvailable')->with('core')->will($this->returnValue(TRUE)); + $installUtilityMock->expects($this->once())->method('isLoaded')->with('core')->will($this->returnValue(TRUE)); + + $installUtilityMock->_set('dependencyUtility', $dependencyManagerMock); + $this->subject->injectInstallUtility($installUtilityMock); + + $this->subject->uninstallExtension('core'); + } + + // + // Tests for configureExtension() + // + + /** + * Creates the needed mocks for the test + */ + public function prepareConfigureExtensionTest(array $methodsToMock, $createConfigurationMock = TRUE) { + if ($createConfigurationMock) { + $this->configurationMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\ConfigurationUtility'); + $this->objectManagerMock->expects($this->once())->method('get')->will($this->returnValue($this->configurationMock)); + } + + $this->subject = $this->getAccessibleMock('Etobi\\CoreApi\\Service\\ExtensionApiService', $methodsToMock); + $this->subject->injectObjectManager($this->objectManagerMock); + + vfsStream::setup('root'); + vfsStream::create(array( + 'absolute' => array( + 'path' => array( + 'to' => array( + 'extension' => array('dummy' => array( + 'ext_conf_template.txt' => ' + # cat=basic; type=string; label=Excluded extensions: You can exclude extensions from being search for tests by writing their extension key here. Seperate the entries with comma. + excludeextensions = lib, div + # cat=basic; type=string; label=Path to Composer: Path to Composer installation which includes the vendor directory. Please remind to configure Composer to install the packages "phpunit/phpunit", "phpunit/phpunit-selenium" and "mikey179/vfsStream". Setting this will have preference over provided Composer packages. + composerpath = + # cat=selenium; type=small; label=Host of the Selenium RC server + selenium_host = localhost + # cat=selenium; type=int+; label=Port of the Selenium RC server + selenium_port = 4444 + # cat=selenium; type=small; label=Browser that should be used to run Selenium tests: Allowed values are *firefox, *mock, *firefoxproxy, *pifirefox, *chrome, *iexploreproxy, *iexplore, *firefox3, *safariproxy, *googlechrome, *konqueror, *firefox2, *safari, *piiexplore, *firefoxchrome, *opera, *iehta, *custom + selenium_browser = *firefox + # cat=selenium; type=small; label=Default Selenium Browser URL: Leave empty to use domain of this TYPO3 installation (TYPO3_SITE_URL) + selenium_browserurl = + ') + ) + ) + ) + ) + )); + } + + /** + * @test + * @covers ::configureExtension + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Extension "dummy" does not exist! + */ + public function configureExtensionWhenExtensionNotExistsThrowsException() { + $this->prepareConfigureExtensionTest(array('checkExtensionExists'), FALSE); + $this->subject->expects($this->once())->method('checkExtensionExists')->will($this->throwException(new InvalidArgumentException(sprintf('Extension "dummy" does not exist!')))); + + $this->subject->configureExtension('dummy'); + } + + /** + * @test + * @covers ::configureExtension + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Extension "dummy" is not installed! + */ + public function configureExtensionWhenExtensionNotLoadedThrowsException() { + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isAvailable', 'isLoaded')); + + $installUtilityMock->expects($this->once())->method('isAvailable')->with('dummy')->will($this->returnValue(TRUE)); + $installUtilityMock->expects($this->once())->method('isLoaded')->with('dummy')->will($this->returnValue(FALSE)); + + $this->subject->injectInstallUtility($installUtilityMock); + + $this->subject->configureExtension('dummy'); + } + + /** + * @test + * @covers ::configureExtension + * @expectedException InvalidArgumentException + * @expectedExceptionMessage No configuration provided for extension "dummy"! + */ + public function configureExtensionWhenNoExtensionConfigurationProvidedAsArgumentCommandLineThrowsException() { + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isAvailable', 'isLoaded')); + + $installUtilityMock->expects($this->once())->method('isAvailable')->with('dummy')->will($this->returnValue(TRUE)); + $installUtilityMock->expects($this->once())->method('isLoaded')->with('dummy')->will($this->returnValue(TRUE)); + + $this->subject->injectInstallUtility($installUtilityMock); + + $this->subject->configureExtension('dummy'); + } + + /** + * @test + * @covers ::configureExtension + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Extension "dummy" has no configuration options! + */ + public function configureExtensionNoDefaultExtensionsSettingsTemplateFileFoundThrowsException() { + $this->prepareConfigureExtensionTest(array('getExtensionPath'), FALSE); + + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isAvailable', 'isLoaded')); + + $installUtilityMock->expects($this->once())->method('isAvailable')->with('dummy')->will($this->returnValue(TRUE)); + $installUtilityMock->expects($this->once())->method('isLoaded')->with('dummy')->will($this->returnValue(TRUE)); + $this->subject->expects($this->once())->method('getExtensionPath')->will($this->returnValue('/test/string')); + + $this->subject->injectInstallUtility($installUtilityMock); + + $this->subject->configureExtension('dummy', array('foo' => 'bar')); + } + + /** + * @test + * @covers ::configureExtension + * @expectedException InvalidArgumentException + * @expectedExceptionMessage No configuration setting with name "excludeextensions" for extension "dummy"! + */ + public function configureExtensionNewExtensionConfigurationSettingDefinesUnknownSettingsThrowsException() { + $this->prepareConfigureExtensionTest(array('getExtensionPath')); + + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isAvailable', 'isLoaded')); + + $installUtilityMock->expects($this->once())->method('isAvailable')->with('dummy')->will($this->returnValue(TRUE)); + $installUtilityMock->expects($this->once())->method('isLoaded')->with('dummy')->will($this->returnValue(TRUE)); + + $newExtensionConfiguration = array( + 'excludeextensions' => 'lib, div', + 'composerpath' => '', + 'selenium_host' => 'localhost', + 'selenium_port' => '4444', + 'selenium_browser' => '*firefox', + 'selenium_browserurl' => '' + ); + + $currentExtensionConfig = array( + 'composerpath' => array( + 'cat' => 'basic', + 'subcat' => 'x/z', + 'type' => 'string', + 'label' => 'Path to Composer: Path to Composer installation which includes the vendor directory. Please remind to configure Composer to install the packages "phpunit/phpunit", "phpunit/phpunit-selenium" and "mikey179/vfsStream". Setting this will have preference over provided Composer packages.', + 'name' => 'composerpath', + 'value' => '', + 'default_value' => '' + ), + 'selenium_host' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'small', + 'label' => 'Host of the Selenium RC server', + 'name' => 'selenium_host', + 'value' => 'localhost', + 'default_value' => 'localhost' + ), + 'selenium_port' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'int+', + 'label' => 'Port of the Selenium RC server', + 'name' => 'selenium_port', + 'value' => '4444', + 'default_value' => '4444' + ), + 'selenium_browser' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'small', + 'label' => 'Browser that should be used to run Selenium tests: Allowed values are *firefox, *mock, *firefoxproxy, *pifirefox, *chrome, *iexploreproxy, *iexplore, *firefox3, *safariproxy, *googlechrome, *konqueror, *firefox2, *safari, *piiexplore, *firefoxchrome, *opera, *iehta, *custom', + 'name' => 'selenium_browser', + 'value' => '*firefox', + 'default_value' => '*firefox' + ), + 'selenium_browserurl' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'small', + 'label' => 'Default Selenium Browser URL: Leave empty to use domain of this TYPO3 installation (TYPO3_SITE_URL)', + 'name' => 'selenium_browserurl', + 'value' => '', + 'default_value' => '' + ) + ); + + $this->subject->expects($this->once())->method('getExtensionPath')->will($this->returnValue(vfsStream::url('root/absolute/path/to/extension/dummy/'))); + $this->configurationMock->expects($this->once())->method('getCurrentConfiguration')->will($this->returnValue($currentExtensionConfig)); + + $this->subject->injectInstallUtility($installUtilityMock); + + $this->subject->configureExtension('dummy', $newExtensionConfiguration); + } + + /** + * @test + * @covers ::configureExtension + */ + public function configureExtensionNewExtensionConfigurationMissesSomeKeyWritesConfiguration() { + $this->prepareConfigureExtensionTest(array('getExtensionPath', 'writeConfiguration')); + + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isAvailable', 'isLoaded')); + + $installUtilityMock->expects($this->once())->method('isAvailable')->with('dummy')->will($this->returnValue(TRUE)); + $installUtilityMock->expects($this->once())->method('isLoaded')->with('dummy')->will($this->returnValue(TRUE)); + + + $newExtensionConfiguration = array( + 'composerpath' => '', + 'selenium_host' => 'localhost', + 'selenium_port' => '4444', + 'selenium_browser' => '*firefox', + 'selenium_browserurl' => '' + ); + + $this->subject->expects($this->once())->method('getExtensionPath')->will( + $this->returnValue(vfsStream::url('root/absolute/path/to/extension/dummy/')) + ); + + $currentExtensionConfig = array( + 'excludeextensions' => array( + 'cat' => 'basic', + 'subcat' => 'x/z', + 'type' => 'string', + 'label' => 'Demo', + 'name' => 'excludeextensions', + 'value' => 'lib,div', + 'default_value' => 'lib, div' + ), + 'composerpath' => array( + 'cat' => 'basic', + 'subcat' => 'x/z', + 'type' => 'string', + 'label' => 'Path to Composer: Path to Composer installation which includes the vendor directory. Please remind to configure Composer to install the packages "phpunit/phpunit", "phpunit/phpunit-selenium" and "mikey179/vfsStream". Setting this will have preference over provided Composer packages.', + 'name' => 'composerpath', + 'value' => '', + 'default_value' => '' + ), + 'selenium_host' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'small', + 'label' => 'Host of the Selenium RC server', + 'name' => 'selenium_host', + 'value' => 'localhost', + 'default_value' => 'localhost' + ), + 'selenium_port' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'int+', + 'label' => 'Port of the Selenium RC server', + 'name' => 'selenium_port', + 'value' => '4444', + 'default_value' => '4444' + ), + 'selenium_browser' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'small', + 'label' => 'Browser that should be used to run Selenium tests: Allowed values are *firefox, *mock, *firefoxproxy, *pifirefox, *chrome, *iexploreproxy, *iexplore, *firefox3, *safariproxy, *googlechrome, *konqueror, *firefox2, *safari, *piiexplore, *firefoxchrome, *opera, *iehta, *custom', + 'name' => 'selenium_browser', + 'value' => '*firefox', + 'default_value' => '*firefox' + ), + 'selenium_browserurl' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'small', + 'label' => 'Default Selenium Browser URL: Leave empty to use domain of this TYPO3 installation (TYPO3_SITE_URL)', + 'name' => 'selenium_browserurl', + 'value' => '', + 'default_value' => '' + ) + ); + $this->configurationMock + ->expects($this->once()) + ->method('getCurrentConfiguration') + ->will($this->returnValue($currentExtensionConfig)); + + $expectedExtensionConfiguration = array( + 'composerpath' => '', + 'selenium_host' => 'localhost', + 'selenium_port' => '4444', + 'selenium_browser' => '*firefox', + 'selenium_browserurl' => '', + 'excludeextensions' => 'lib,div' + ); + + $this->configurationMock + ->expects($this->once()) + ->method('writeConfiguration') + ->with($expectedExtensionConfiguration, 'dummy'); + + $this->subject->injectInstallUtility($installUtilityMock); + + $this->subject->configureExtension('dummy', $newExtensionConfiguration); + } + + /** + * @test + * @covers ::configureExtension + */ + public function configureExtensionNewExtensionConfigurationMissesSomeKeyAndCurrentConfigurationValueIsEmptyWritesConfiguration() { + $this->prepareConfigureExtensionTest(array('getExtensionPath', 'writeConfiguration')); + + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isAvailable', 'isLoaded')); + $installUtilityMock->expects($this->once())->method('isAvailable')->with('dummy')->will($this->returnValue(TRUE)); + $installUtilityMock->expects($this->once())->method('isLoaded')->with('dummy')->will($this->returnValue(TRUE)); + $this->subject->expects($this->once())->method('getExtensionPath')->will( + $this->returnValue(vfsStream::url('root/absolute/path/to/extension/dummy/')) + ); + + $currentExtensionConfig = array( + 'excludeextensions' => array( + 'cat' => 'basic', + 'subcat' => 'x/z', + 'type' => 'string', + 'label' => 'Demo', + 'name' => 'excludeextensions', + 'value' => '', + 'default_value' => 'lib,div' + ), + 'composerpath' => array( + 'cat' => 'basic', + 'subcat' => 'x/z', + 'type' => 'string', + 'label' => 'Path to Composer: Path to Composer installation which includes the vendor directory. Please remind to configure Composer to install the packages "phpunit/phpunit", "phpunit/phpunit-selenium" and "mikey179/vfsStream". Setting this will have preference over provided Composer packages.', + 'name' => 'composerpath', + 'value' => '', + 'default_value' => '' + ), + 'selenium_host' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'small', + 'label' => 'Host of the Selenium RC server', + 'name' => 'selenium_host', + 'value' => 'localhost', + 'default_value' => 'localhost' + ), + 'selenium_port' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'int+', + 'label' => 'Port of the Selenium RC server', + 'name' => 'selenium_port', + 'value' => '4444', + 'default_value' => '4444' + ), + 'selenium_browser' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'small', + 'label' => 'Browser that should be used to run Selenium tests: Allowed values are *firefox, *mock, *firefoxproxy, *pifirefox, *chrome, *iexploreproxy, *iexplore, *firefox3, *safariproxy, *googlechrome, *konqueror, *firefox2, *safari, *piiexplore, *firefoxchrome, *opera, *iehta, *custom', + 'name' => 'selenium_browser', + 'value' => '*firefox', + 'default_value' => '*firefox' + ), + 'selenium_browserurl' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'small', + 'label' => 'Default Selenium Browser URL: Leave empty to use domain of this TYPO3 installation (TYPO3_SITE_URL)', + 'name' => 'selenium_browserurl', + 'value' => '', + 'default_value' => '' + ) + ); + $this->configurationMock + ->expects($this->once()) + ->method('getCurrentConfiguration') + ->will($this->returnValue($currentExtensionConfig)); + + $expectedExtensionConfiguration = array( + 'composerpath' => '', + 'selenium_host' => 'localhost', + 'selenium_port' => '4444', + 'selenium_browser' => '*firefox', + 'selenium_browserurl' => '', + 'excludeextensions' => 'lib,div' + ); + + $this->configurationMock + ->expects($this->once()) + ->method('writeConfiguration') + ->with($expectedExtensionConfiguration, 'dummy'); + + $newExtensionConfiguration = array( + 'composerpath' => '', + 'selenium_host' => 'localhost', + 'selenium_port' => '4444', + 'selenium_browser' => '*firefox', + 'selenium_browserurl' => '' + ); + + $this->subject->injectInstallUtility($installUtilityMock); + + $this->subject->configureExtension('dummy', $newExtensionConfiguration); + } + + /** + * @test + * @covers ::configureExtension + */ + public function configureExtensionWhenExtensionWritesConfiguration() { + $this->prepareConfigureExtensionTest(array('getExtensionPath', 'writeConfiguration')); + + $installUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\InstallUtility', array('isAvailable', 'isLoaded')); + $installUtilityMock->expects($this->once())->method('isAvailable')->with('dummy')->will($this->returnValue(TRUE)); + $installUtilityMock->expects($this->once())->method('isLoaded')->with('dummy')->will($this->returnValue(TRUE)); + $this->subject->expects($this->once())->method('getExtensionPath')->will( + $this->returnValue(vfsStream::url('root/absolute/path/to/extension/dummy/')) + ); + + $currentExtensionConfig = array( + 'excludeextensions' => array( + 'cat' => 'basic', + 'subcat' => 'x/z', + 'type' => 'string', + 'label' => 'Demo', + 'name' => 'excludeextensions', + 'value' => 'lib,div', + 'default_value' => 'lib, div' + ), + 'composerpath' => array( + 'cat' => 'basic', + 'subcat' => 'x/z', + 'type' => 'string', + 'label' => 'Path to Composer: Path to Composer installation which includes the vendor directory. Please remind to configure Composer to install the packages "phpunit/phpunit", "phpunit/phpunit-selenium" and "mikey179/vfsStream". Setting this will have preference over provided Composer packages.', + 'name' => 'composerpath', + 'value' => '', + 'default_value' => '' + ), + 'selenium_host' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'small', + 'label' => 'Host of the Selenium RC server', + 'name' => 'selenium_host', + 'value' => 'localhost', + 'default_value' => 'localhost' + ), + 'selenium_port' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'int+', + 'label' => 'Port of the Selenium RC server', + 'name' => 'selenium_port', + 'value' => '4444', + 'default_value' => '4444' + ), + 'selenium_browser' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'small', + 'label' => 'Browser that should be used to run Selenium tests: Allowed values are *firefox, *mock, *firefoxproxy, *pifirefox, *chrome, *iexploreproxy, *iexplore, *firefox3, *safariproxy, *googlechrome, *konqueror, *firefox2, *safari, *piiexplore, *firefoxchrome, *opera, *iehta, *custom', + 'name' => 'selenium_browser', + 'value' => '*firefox', + 'default_value' => '*firefox' + ), + 'selenium_browserurl' => array( + 'cat' => 'selenium', + 'subcat' => 'x/z', + 'type' => 'small', + 'label' => 'Default Selenium Browser URL: Leave empty to use domain of this TYPO3 installation (TYPO3_SITE_URL)', + 'name' => 'selenium_browserurl', + 'value' => '', + 'default_value' => '' + ) + ); + + $this->configurationMock + ->expects($this->once()) + ->method('getCurrentConfiguration') + ->will($this->returnValue($currentExtensionConfig)); + + $newExtensionConfiguration = array( + 'excludeextensions' => 'lib,div', + 'composerpath' => '', + 'selenium_host' => 'localhost', + 'selenium_port' => '4444', + 'selenium_browser' => '*firefox', + 'selenium_browserurl' => '' + ); + + $this->configurationMock + ->expects($this->once()) + ->method('writeConfiguration') + ->with($newExtensionConfiguration, 'dummy'); + + $this->subject->injectInstallUtility($installUtilityMock); + + $this->subject->configureExtension('dummy', $newExtensionConfiguration); + } + + // + // Tests for fetchExtension() + // + + /** + * @test + * @covers ::fetchExtension + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Extension "dummy" was not found on TER + */ + public function fetchExtensionNoVersionSetNoExtensionFound() { + $this->extensionRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', array('findHighestAvailableVersion'), array(), '', FALSE); + + $this->extensionRepositoryMock->expects($this->once())->method('findHighestAvailableVersion')->with('dummy')->will($this->returnValue(NULL)); + + $this->subject->injectExtensionRepository($this->extensionRepositoryMock); + + $this->subject->fetchExtension('dummy'); + } + + /** + * @test + * @covers ::fetchExtension + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Version 1.0.0 of extension "dummy" does not exist + */ + public function fetchExtensionVersionSetNoExtensionFound() { + $this->extensionRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', array('findOneByExtensionKeyAndVersion'), array(), '', FALSE); + + $this->extensionRepositoryMock->expects($this->once())->method('findOneByExtensionKeyAndVersion')->with('dummy')->will($this->returnValue(NULL)); + + $this->subject->injectExtensionRepository($this->extensionRepositoryMock); + + $this->subject->fetchExtension('dummy', '1.0.0'); + } + + /** + * @test + * @covers ::fetchExtension + */ + public function fetchExtensionOptionVersionNotSetDownloadLatestVersion() { + vfsStream::setup('root'); + vfsStream::create(array('typo3conf' => array('ext' => array('coreapi' => array())))); + + $this->extensionMock->setExtensionKey('coreapi'); + $this->extensionMock->setVersion('2.03'); + + // Create the mock objects + $extensionRepositoryMock = $this->getMock( + 'TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', + array('findHighestAvailableVersion'), + array(), + '', + FALSE + ); + + $extensionManagementServiceMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Service\\ExtensionManagementService'); + $downloadUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\DownloadUtility', array('dummy')); + $fileHandlingUtility = $this->getMock( + 'TYPO3\\CMS\\Extensionmanager\\Utility\\FileHandlingUtility', + array('getExtensionDir') + ); + + // Add behavior to the mock objects + $this->objectManagerMock->expects($this->once()) + ->method('get') + ->will($this->returnValue($downloadUtilityMock) + ); + + $extensionRepositoryMock->expects($this->once()) + ->method('findHighestAvailableVersion') + ->with('coreapi') + ->will($this->returnValue($this->extensionMock) + ); + + $extensionManagementServiceMock->expects($this->once()) + ->method('downloadMainExtension') + ->with($this->extensionMock); + + $this->repositoryHelperMock->expects($this->once()) + ->method('getMirrors') + ->will($this->returnValue($this->mirrorsMock) + ); + + $fileHandlingUtility->expects($this->at(1)) + ->method('getExtensionDir') + ->with('coreapi', 'Local') + ->will($this->returnValue(vfsStream::url('root/typo3conf/ext/coreapi/')) + ); + + // Inject the mock objects + $this->subject->injectExtensionRepository($extensionRepositoryMock); + $this->subject->injectExtensionManagementService($extensionManagementServiceMock); + $this->subject->injectRepositoryHelper($this->repositoryHelperMock); + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->injectFileHandlingUtility($fileHandlingUtility); + + $this->subject->fetchExtension('coreapi'); + } + + /** + * @test + * @covers ::fetchExtension + */ + public function fetchExtensionOptionVersionSetDownloadsDemandedVersion() { + vfsStream::setup('root'); + vfsStream::create(array('typo3conf' => array('ext' => array('coreapi' => array())))); + + $this->extensionMock->setExtensionKey('coreapi'); + $this->extensionMock->setVersion('1.0'); + + // Create the mock objects + $downloadUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\DownloadUtility', array('dummy')); + $fileHandlingUtility = $this->getMock( + 'TYPO3\\CMS\\Extensionmanager\\Utility\\FileHandlingUtility', + array('getExtensionDir') + ); + $extensionRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', array('findOneByExtensionKeyAndVersion'), array(), '', FALSE); + $extensionManagementServiceMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Service\\ExtensionManagementService'); + + // Add behavior to the mock objects + $this->objectManagerMock->expects($this->once()) + ->method('get') + ->will($this->returnValue($downloadUtilityMock) + ); + + $fileHandlingUtility->expects($this->at(1)) + ->method('getExtensionDir') + ->with('coreapi', 'Local') + ->will($this->returnValue(vfsStream::url('root/typo3conf/ext/coreapi/')) + ); + + $extensionRepositoryMock->expects($this->once())->method('findOneByExtensionKeyAndVersion')->with('coreapi', '1.0')->will($this->returnValue($this->extensionMock)); + $extensionManagementServiceMock->expects($this->once())->method('downloadMainExtension')->with($this->extensionMock); + $this->repositoryHelperMock->expects($this->once())->method('getMirrors')->will($this->returnValue($this->mirrorsMock)); + + // Inject the mock objects + $this->subject->injectExtensionRepository($extensionRepositoryMock); + $this->subject->injectExtensionManagementService($extensionManagementServiceMock); + $this->subject->injectRepositoryHelper($this->repositoryHelperMock); + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->injectFileHandlingUtility($fileHandlingUtility); + + $this->subject->fetchExtension('coreapi', '1.0'); + } + + /** + * @test + * @covers ::fetchExtension + * @expectedException TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException + * @expectedExceptionMessage System not in allowed download paths + */ + public function fetchExtensionOptionLocationSystemThrowsException() { + unset($GLOBALS['TYPO3_CONF_VARS']['EXT']['allowSystemInstall']); + unset($GLOBALS['TYPO3_CONF_VARS']['EXT']['allowGlobalInstall']); + + // Create the mock objects + $downloadUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\DownloadUtility', array('dummy')); + $extensionRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', array('findOneByExtensionKeyAndVersion'), array(), '', FALSE); + + // Add behavior to the mock objects + $this->objectManagerMock->expects($this->once()) + ->method('get') + ->will($this->returnValue($downloadUtilityMock) + ); + $extensionRepositoryMock->expects($this->once())->method('findOneByExtensionKeyAndVersion')->with('coreapi', '1.0')->will($this->returnValue($this->extensionMock)); + $this->repositoryHelperMock->expects($this->once())->method('getMirrors')->will($this->returnValue($this->mirrorsMock)); + + // Inject the mock objects + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->injectRepositoryHelper($this->repositoryHelperMock); + $this->subject->injectExtensionRepository($extensionRepositoryMock); + + $this->subject->fetchExtension('coreapi', '1.0', 'System'); + } + + /** + * @test + * @covers ::fetchExtension + * @expectedException TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException + * @expectedExceptionMessage Global not in allowed download paths + */ + public function fetchExtensionOptionLocationGlobalThrowsException() { + unset($GLOBALS['TYPO3_CONF_VARS']['EXT']['allowSystemInstall']); + unset($GLOBALS['TYPO3_CONF_VARS']['EXT']['allowGlobalInstall']); + + // Create the mock objects + $downloadUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\DownloadUtility', array('dummy')); + $extensionRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', array('findOneByExtensionKeyAndVersion'), array(), '', FALSE); + + // Add behavior to the mock objects + $this->objectManagerMock->expects($this->once())->method('get')->will($this->returnValue($downloadUtilityMock)); + $extensionRepositoryMock->expects($this->once())->method('findOneByExtensionKeyAndVersion')->with('coreapi', '1.0')->will($this->returnValue($this->extensionMock)); + $this->repositoryHelperMock->expects($this->once())->method('getMirrors')->will($this->returnValue($this->mirrorsMock)); + + // Inject the mock objects + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->injectRepositoryHelper($this->repositoryHelperMock); + $this->subject->injectExtensionRepository($extensionRepositoryMock); + + $this->subject->fetchExtension('coreapi', '1.0', 'Global'); + } + + /** + * @test + * @covers ::fetchExtension + * @expectedException TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException + * @expectedExceptionMessage Local not in allowed download paths + */ + public function fetchExtensionOptionLocationLocalThrowsException() { + unset($GLOBALS['TYPO3_CONF_VARS']['EXT']['allowLocalInstall']); + + // Create the mock objects + $downloadUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\DownloadUtility', array('dummy')); + $extensionRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', array('findOneByExtensionKeyAndVersion'), array(), '', FALSE); + + // Add behavior to the mock objects + $this->objectManagerMock->expects($this->once())->method('get')->will($this->returnValue($downloadUtilityMock)); + $extensionRepositoryMock->expects($this->once())->method('findOneByExtensionKeyAndVersion')->with('coreapi', '1.0')->will($this->returnValue($this->extensionMock)); + $this->repositoryHelperMock->expects($this->once())->method('getMirrors')->will($this->returnValue($this->mirrorsMock)); + + // Inject the mock objects + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->injectExtensionRepository($extensionRepositoryMock); + $this->subject->injectRepositoryHelper($this->repositoryHelperMock); + + $this->subject->fetchExtension('coreapi', '1.0', 'Local'); + } + + /** + * @test + * @covers ::fetchExtension + * @expectedException TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException + * @expectedExceptionMessage location not in allowed download paths + */ + public function fetchExtensionOptionLocationWithWrongDataThrowsException() { + // Create the mock objects + $extensionRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', array('findOneByExtensionKeyAndVersion'), array(), '', FALSE); + $downloadUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\DownloadUtility', array('dummy')); + + // Add behavior to the mock objects + $this->objectManagerMock->expects($this->once())->method('get')->will($this->returnValue($downloadUtilityMock)); + $extensionRepositoryMock->expects($this->once())->method('findOneByExtensionKeyAndVersion')->with('coreapi', '1.0')->will($this->returnValue($this->extensionMock)); + $this->repositoryHelperMock->expects($this->once())->method('getMirrors')->will($this->returnValue($this->mirrorsMock)); + + // Inject the mock objects + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->injectExtensionRepository($extensionRepositoryMock); + $this->subject->injectRepositoryHelper($this->repositoryHelperMock); + + $this->subject->fetchExtension('coreapi', '1.0', 'location'); + } + + /** + * @test + * @covers ::fetchExtension + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Option --mirror must be a number. Run the command extensionapi:listmirrors to get the list of all available repositories + */ + public function fetchExtensionOptionMirrorIsNotANumber() { + // Create the mock objects + $extensionRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', array('findOneByExtensionKeyAndVersion'), array(), '', FALSE); + + // Add behavior to the mock objects + $extensionRepositoryMock->expects($this->never())->method('findOneByExtensionKeyAndVersion'); + + // Inject the mock objects + $this->subject->injectExtensionRepository($extensionRepositoryMock); + + $this->subject->fetchExtension('coreapi', '', '', FALSE, 'test'); + } + + /** + * @test + * @covers ::fetchExtension + * @expectedException RuntimeException + * @expectedExceptionMessage No mirrors found! + */ + public function fetchExtensionNoMirrorsFoundThrowsException() { + vfsStream::setup('root'); + vfsStream::create(array('dummy' => array())); + + $this->extensionMock->setExtensionKey('coreapi'); + $this->extensionMock->setVersion('1.0'); + + // Create the mock objects + $fileHandlingUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\FileHandlingUtility'); + $extensionRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', array('findHighestAvailableVersion'), array(), '', FALSE); + + // Add behavior to the mock objects + $fileHandlingUtility->expects($this->once())->method('getExtensionDir')->will($this->returnValue(vfsStream::url($this->installPath))); + $extensionRepositoryMock->expects($this->once())->method('findHighestAvailableVersion')->with('coreapi')->will($this->returnValue($this->extensionMock)); + $this->repositoryHelperMock->expects($this->once())->method('getMirrors')->will($this->returnValue(NULL)); + + // Inject the mock objects + $this->subject->injectExtensionRepository($extensionRepositoryMock); + $this->subject->injectRepositoryHelper($this->repositoryHelperMock); + $this->subject->injectFileHandlingUtility($fileHandlingUtility); + + $this->subject->fetchExtension('coreapi'); + } + + /** + * @test + * @covers ::fetchExtension + */ + public function fetchExtensionSetDemandedMirror() { + vfsStream::setup('root'); + vfsStream::create(array('typo3conf' => array('ext' => array('coreapi' => array())))); + + $this->extensionMock->setExtensionKey('coreapi'); + $this->extensionMock->setVersion('1.0'); + + // Create the mock objects + $downloadUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\DownloadUtility', array('dummy')); + $fileHandlingUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\FileHandlingUtility'); + $extensionManagementServiceMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Service\\ExtensionManagementService'); + $extensionRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', array('findHighestAvailableVersion'), array(), '', FALSE); + + // Add behavior to the mock objects + $this->objectManagerMock->expects($this->once()) + ->method('get') + ->will($this->returnValue($downloadUtilityMock) + ); + $fileHandlingUtility->expects($this->at(1))->method('getExtensionDir')->will($this->returnValue(vfsStream::url('root/typo3conf/ext/coreapi/'))); + $extensionRepositoryMock->expects($this->once())->method('findHighestAvailableVersion')->with('coreapi')->will($this->returnValue($this->extensionMock)); + $repository = array( + 'title' => 'TYPO3.org Main Repository', + 'host' => 'typo3.org', + 'path' => '/fileadmin/ter/', + 'country' => 'DEU', + 'sponsorname' => 'punkt.de GmbH', + 'sponsorlink' => 'http://punkt.de/', + 'sponsorlogo' => 'http://repositories.typo3.org/sponsors/logo-punktde.gif' + ); + $this->mirrorsMock->setMirrors($repository); + $this->mirrorsMock->expects($this->once())->method('getMirrors')->will($this->returnValue($repository)); + $this->mirrorsMock->expects($this->once())->method('setSelect')->with(1); + $this->repositoryHelperMock->expects($this->once())->method('getMirrors')->will($this->returnValue($this->mirrorsMock)); + $extensionManagementServiceMock->expects($this->once())->method('downloadMainExtension')->with($this->extensionMock); + + // Inject the mock objects + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->injectExtensionRepository($extensionRepositoryMock); + $this->subject->injectExtensionManagementService($extensionManagementServiceMock); + $this->subject->injectRepositoryHelper($this->repositoryHelperMock); + $this->subject->injectFileHandlingUtility($fileHandlingUtility); + + $this->subject->fetchExtension('coreapi', '', 'Local', FALSE, 1); + } + + /** + * @test + * @covers ::fetchExtension + * @expectedException RuntimeException + * @expectedExceptionMessage Extension "dummy" version 1.0 could not installed! + */ + public function fetchExtensionFoundDependencies() { + $this->markTestSkipped(); + vfsStream::setup('root'); + vfsStream::create(array('dummy' => array())); + + $this->extensionMock->setExtensionKey('dummy'); + $this->extensionMock->setVersion('1.0'); + + // Create the mock objects + $downloadUtilityMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\DownloadUtility', array('dummy')); + $fileHandlingUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\FileHandlingUtility'); + $this->extensionManagementServiceMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Service\\ExtensionManagementService'); + $extensionRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', array('findOneByExtensionKeyAndVersion'), array(), '', FALSE); + + // Add behavior to the mock objects + $this->objectManagerMock->expects($this->once()) + ->method('get') + ->will($this->returnValue($downloadUtilityMock) + ); + $fileHandlingUtility->expects($this->once())->method('getExtensionDir')->will($this->returnValue(vfsStream::url($this->installPath))); + $extensionRepositoryMock->expects($this->once())->method('findOneByExtensionKeyAndVersion')->with('dummy', '1.0')->will($this->returnValue($this->extensionMock)); + $repository = array( + 'title' => 'TYPO3.org Main Repository', + 'host' => 'typo3.org', + 'path' => '/fileadmin/ter/', + 'country' => 'DEU', + 'sponsorname' => 'punkt.de GmbH', + 'sponsorlink' => 'http://punkt.de/', + 'sponsorlogo' => 'http://repositories.typo3.org/sponsors/logo-punktde.gif' + ); + + $this->mirrorsMock->setMirrors($repository); + $this->mirrorsMock->expects($this->once())->method('getMirrors')->will($this->returnValue($repository)); + $this->mirrorsMock->expects($this->once())->method('setSelect')->with(1); + $this->repositoryHelperMock->expects($this->once())->method('getMirrors')->will($this->returnValue($this->mirrorsMock)); + $this->extensionManagementServiceMock->expects($this->once())->method('downloadMainExtension')->with($this->extensionMock); + + // Inject the mock objects + $this->subject->injectObjectManager($this->objectManagerMock); + $this->subject->injectExtensionRepository($extensionRepositoryMock); + $this->subject->injectExtensionManagementService($this->extensionManagementServiceMock); + $this->subject->injectRepositoryHelper($this->repositoryHelperMock); + $this->subject->injectFileHandlingUtility($fileHandlingUtility); + + $this->subject->fetchExtension('dummy', '1.0', 'Local', FALSE, 1); + } + + /** + * @test + * @covers ::fetchExtension + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Mirror "5" does not exist + */ + public function fetchExtensionDemandedMirrorOutOfRange() { + vfsStream::setup('root'); + vfsStream::create(array('dummy' => array())); + + $this->extensionMock->setExtensionKey('coreapi'); + $this->extensionMock->setVersion('1.0'); + + // Create the mock objects + $fileHandlingUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\FileHandlingUtility'); + $extensionRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', array('findHighestAvailableVersion'), array(), '', FALSE); + + // Add behavior to the mock objects + $fileHandlingUtility->expects($this->once())->method('getExtensionDir')->will($this->returnValue(vfsStream::url($this->installPath))); + $extensionRepositoryMock->expects($this->once())->method('findHighestAvailableVersion')->with('coreapi')->will($this->returnValue($this->extensionMock)); + $this->mirrorsMock->setMirrors( + array( + 'title' => 'TYPO3.org Main Repository', + 'host' => 'typo3.org', + 'path' => '/fileadmin/ter/', + 'country' => 'DEU', + 'sponsorname' => 'punkt.de GmbH', + 'sponsorlink' => 'http://punkt.de/', + 'sponsorlogo' => 'http://repositories.typo3.org/sponsors/logo-punktde.gif' + ) + ); + $this->repositoryHelperMock->expects($this->once())->method('getMirrors')->will($this->returnValue($this->mirrorsMock)); + + // Inject the mock objects + $this->subject->injectExtensionRepository($extensionRepositoryMock); + $this->subject->injectRepositoryHelper($this->repositoryHelperMock); + $this->subject->injectFileHandlingUtility($fileHandlingUtility); + + $this->subject->fetchExtension('coreapi', '', 'Local', FALSE, 5); + } + + /** + * @test + * @covers ::fetchExtension + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Extension "coreapi" already exists at "vfs://root/coreapi/"! + */ + public function fetchExtensionOptionOverrideNotSetExtensionExistsAlready() { + vfsStream::setup('root'); + vfsStream::create(array('coreapi' => array())); + + $this->extensionMock->setExtensionKey('coreapi'); + $this->extensionMock->setVersion('1.0'); + + // Create the mock objects + $fileHandlingUtility = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Utility\\FileHandlingUtility'); + $extensionRepositoryMock = $this->getMock('TYPO3\\CMS\\Extensionmanager\\Domain\\Repository\\ExtensionRepository', array('findHighestAvailableVersion'), array(), '', FALSE); + + // Add behavior to the mock objects + $fileHandlingUtility->expects($this->once())->method('getExtensionDir')->will($this->returnValue(vfsStream::url($this->installPath))); + $extensionRepositoryMock->expects($this->once())->method('findHighestAvailableVersion')->with('coreapi')->will($this->returnValue($this->extensionMock)); + $this->repositoryHelperMock->expects($this->never())->method('getMirrors'); + + // Inject the mock objects + $this->subject->injectExtensionRepository($extensionRepositoryMock); + $this->subject->injectRepositoryHelper($this->repositoryHelperMock); + $this->subject->injectFileHandlingUtility($fileHandlingUtility); + + $this->subject->fetchExtension('coreapi'); + } + + // + // Tests for listMirrors() + // + + /** + * @test + * @covers ::listMirrors + */ + public function listMirrorsListMirrors() { + $repository = array( + 'title' => 'TYPO3.org Main Repository', + 'host' => 'typo3.org', + 'path' => '/fileadmin/ter/', + 'country' => 'DEU', + 'sponsorname' => 'punkt.de GmbH', + 'sponsorlink' => 'http://punkt.de/', + 'sponsorlogo' => 'http://repositories.typo3.org/sponsors/logo-punktde.gif' + ); + + $this->mirrorsMock->expects($this->once())->method('getMirrors')->will($this->returnValue($repository)); + $this->repositoryHelperMock->expects($this->once())->method('getMirrors')->will($this->returnValue($this->mirrorsMock)); + $this->objectManagerMock->expects($this->once())->method('get')->will($this->returnValue($this->repositoryHelperMock)); + $this->subject->injectObjectManager($this->objectManagerMock); + + $this->subject->listMirrors(); + } + + /** + * @param string $amount + * + * @return array + */ + protected function getFakeInstalledExtensionArray() { + return array( + 'core' => array( + 'type' => 'System', + 'siteRelPath' => 'typo3/sysext/core/', + 'typo3RelPath' => 'sysext/core/', + 'ext_localconf.php' => 'path/to/core/ext_localconf.php', + 'ext_tables.php' => 'path/to/core/ext_tables.php', + 'ext_tables.sql' => 'path/to/core/ext_tables.sql', + 'ext_icon' => 'ext_icon.png' + ), + 'about' => array(), + 'backend' => array( + 'type' => 'System', + 'siteRelPath' => 'typo3/sysext/backend/', + 'typo3RelPath' => 'sysext/backend/', + 'ext_localconf.php' => 'path/to/backend/ext_localconf.php', + 'ext_tables.php' => 'path/to/backend/ext_tables.php', + 'ext_icon' => 'ext_icon.png' + ), + 'cms' => array( + 'type' => 'Global', + 'siteRelPath' => 'typo3/sysext/cms/', + 'typo3RelPath' => 'sysext/cms/', + 'ext_localconf.php' => 'path/to/cms/ext_localconf.php', + 'ext_tables.php' => 'path/to/cms/ext_tables.php', + 'ext_icon' => 'ext_icon.png' + ), + 'coreapi' => array( + 'type' => 'Local', + 'siteRelPath' => 'typo3conf/ext/coreapi/', + 'typo3RelPath' => '../typo3conf/ext/coreapi/', + 'ext_localconf.php' => 'path/to/coreapi/ext_localconf.php', + 'ext_icon' => 'ext_icon.png' + ), + 'dummy' => array( + 'type' => 'Local', + 'siteRelPath' => 'typo3conf/ext/dummy/', + 'typo3RelPath' => '../typo3conf/ext/dummy/', + 'ext_localconf.php' => 'path/to/dummy/ext_localconf.php', + 'ext_icon' => 'ext_icon.png' + ), + ); + } +} + diff --git a/Tests/Unit/Service/SiteApiServiceTest.php b/Tests/Unit/Service/SiteApiServiceTest.php new file mode 100644 index 0000000..246015b --- /dev/null +++ b/Tests/Unit/Service/SiteApiServiceTest.php @@ -0,0 +1,321 @@ + + * 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 2 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. + * A copy is found in the textfile GPL.txt and important notices to the license + * from the author is found in LICENSE.txt distributed with these scripts. + * + * + * 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 TYPO3\CMS\Core\Tests\UnitTestCase; + +/** + * Class SiteApiServiceTest + * + * @package Etobi\CoreApi\Tests\Unit\Service + * @author Stefano Kowalke + * @coversDefaultClass \Etobi\CoreAPI\Service\SiteApiService + */ +class SiteApiServiceTest extends UnitTestCase { + + /** + * @var \Etobi\CoreApi\Service\SiteApiService|\PHPUnit_Framework_MockObject_MockObject $subject + */ + protected $subject; + + /** + * Setup the test + */ + public function setUp() { + $this->subject = $this->getMock('Etobi\\CoreApi\\Service\\SiteApiService', array('dummy')); + } + + /** + * Tears the test down + */ + public function tearDown() { + unset($this->subject); + } + + /** + * @test + * @covers ::getSiteInfo + */ + public function getSiteInfoReturnsSiteInfo() { + $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] = 'CoreApi Testpage'; + + $expectedData = array( + 'TYPO3 version' => TYPO3_version, + 'Site name' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], + 'Combined disk usage' => '42M', + 'Database size' => '23M', + 'Count local installed extensions' => 4 + ); + + $data1 = array( + 'TYPO3 version' => TYPO3_version, + 'Site name' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], + ); + + $data2 = array( + 'TYPO3 version' => TYPO3_version, + 'Site name' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], + 'Combined disk usage' => '42M', + ); + + $data3 = array( + 'TYPO3 version' => TYPO3_version, + 'Site name' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], + 'Combined disk usage' => '42M', + 'Database size' => '23M', + + ); + + $data4 = array( + 'TYPO3 version' => TYPO3_version, + 'Site name' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], + 'Combined disk usage' => '42M', + 'Database size' => '23M', + 'Count local installed extensions' => 4 + ); + + /** @var \Etobi\CoreApi\Service\SiteApiService|\PHPUnit_Framework_MockObject_MockObject $subject */ + $subject = $this->getMock('Etobi\\CoreApi\\Service\\SiteApiService', array('getDiskUsage', 'getDatabaseSize', 'getCountOfExtensions')); + + $subject->expects($this->once())->method('getDiskUsage')->with($data1)->will($this->returnValue($data2)); + $subject->expects($this->once())->method('getDatabaseSize')->with($data2)->will($this->returnValue($data3)); + $subject->expects($this->once())->method('getCountOfExtensions')->with($data3)->will($this->returnValue($data4)); + $calculatedData = $subject->getSiteInfo(); + $this->assertEquals($expectedData, $calculatedData); + } + + /** + * @test + * @covers ::createSysNews + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage No header given + */ + public function createSysNewsNoHeaderGivenThrowsException() { + $this->subject->createSysNews('', 'Foo'); + } + + /** + * @test + * @covers ::createSysNews + */ + public function createSysNewsCreateNewsEntry() { + $databaseConnectionMock = $this->getMock('TYPO3\CMS\Core\Database\DatabaseConnection', array('exec_INSERTquery'), array(), '', FALSE); + /** @var \Etobi\CoreApi\Service\SiteApiService|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $subject */ + $subject = $this->getAccessibleMock('Etobi\\CoreApi\\Service\\SiteApiService', array('getDatabaseHandler')); + + $databaseConnectionMock->expects($this->once())->method('exec_INSERTquery')->with('sys_news', array('title' => 'Foo', 'content' => 'Bar', 'tstamp' => $GLOBALS['EXEC_TIME'], 'crdate' => $GLOBALS['EXEC_TIME'], 'cruser_id' => $GLOBALS['BE_USER']->user['uid'])); + $subject->expects($this->once())->method('getDatabaseHandler')->will($this->returnValue($databaseConnectionMock)); + + $subject->createSysNews('Foo', 'Bar'); + } + + /** + * @test + * @covers ::getDiskUsage + */ + public function getDiskUsageCalculatesDiskUsageWhenOsIsNotWindows() { + if (TYPO3_OS === 'WIN') { + $this->markTestSkipped('Test is only valid for Unix based platforms'); + } + $data = array(); + $diskUsage = trim(array_shift(explode("\t", shell_exec('du -sh ' . PATH_site)))); + + $this->assertEquals(array('Combined disk usage' => $diskUsage), $this->subject->getDiskUsage($data)); + } + + /** + * @test + * @covers ::getDiskUsage + */ + public function getDiskUsageCalculatesNoDiskUsageWhenOsIsWindows() { + if (TYPO3_OS === 'WIN') { + $data = array(); + $this->assertEquals(array(), $this->subject->getDiskUsage($data)); + } else { + $this->markTestSkipped('Test is only valid for Windows based platforms'); + } + } + + /** + * @test + * @covers ::getDatabaseSize + */ + public function getDatabaseSizeReturnsDatabaseSize() { + $data = array(); + + $databaseConnectionMock = $this->getMock('TYPO3\CMS\Core\Database\DatabaseConnection', array('sql_query', 'sql_fetch_assoc'), array(), '', FALSE); + /** @var \Etobi\CoreApi\Service\SiteApiService|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $subject */ + $subject = $this->getAccessibleMock('Etobi\\CoreApi\\Service\\SiteApiService', array('getDatabaseHandler')); + + $databaseConnectionMock->expects($this->once())->method('sql_query')->with("SELECT SUM( data_length + index_length ) / 1024 / 1024 AS size FROM information_schema.TABLES WHERE table_schema = '" . TYPO3_db . "'"); + $databaseConnectionMock->expects($this->once())->method('sql_fetch_assoc')->will($this->returnValue(array('size' => 30.06250000))); + $subject->expects($this->once())->method('getDatabaseHandler')->will($this->returnValue($databaseConnectionMock)); + + $data = $subject->getDatabaseSize($data); + $this->assertEquals('30M', $data['Database size']); + } + + /** + * @test + * @covers ::getCountOfExtensions + */ + public function getCountOfExtensionsCountsLocalInstalledExtensions() { + $data = array(); + $extensionApiServiceMock = $this->getMock('Etobi\\CoreAPI\\Service\\ExtensionApiService', array('listExtensions')); + $extensionApiServiceMock->expects($this->once())->method('listExtensions')->with('Local')->will($this->returnValue($this->getExtensionArrayForCreateUploadFolders())); + $objectManagerMock = $this->getMock('TYPO3\\CMS\\Extbase\\Object\\ObjectManager', array('get')); + $objectManagerMock->expects($this->once())->method('get')->will($this->returnValue($extensionApiServiceMock)); + $this->subject->injectObjectManager($objectManagerMock); + $data = $this->subject->getCountOfExtensions($data); + $this->assertEquals(4, $data['Count local installed extensions']); + } + + /** + * Provides the array which is return value from ExtensionServiceApi::getInstalledExtensions() + * + * @return array + */ + public function getExtensionArrayForCreateUploadFolders() { + return array( + 'about' => array( + 'title' => 'Help>About', + 'description' => 'Shows info about TYPO3 and installed extensions', + 'category' => 'module', + 'shy' => 1, + 'dependencies' => '', + 'conflicts' => '', + 'priority' => '', + 'loadOrder' => '', + 'module' => 'mod', + 'state' => 'stable', + 'internal' => 0, + 'uploadfolder' => 0, + 'createDirs' => '', + 'modify_tables' => '', + 'clearCacheOnLoad' => '', + 'lockType' => '', + 'author' => 'Kasper Skaarhoj', + 'author_email' => 'kasperYYYY@typo3.com', + 'author_company' => 'Curby Soft Multimedia', + 'CGLcompilance' => '', + 'CGLcompilance_note' => '', + 'version' => '6.2.0', + '_md5_values_when_last_written' => '', + 'constraints' => array(), + 'suggests' => array(), + 'key' => 'about' + ), + 'backend' => array( + 'title' => 'TYPO3 Backend', + 'description' => 'Classes for the TYPO3 backend.', + 'category' => 'be', + 'shy' => 1, + 'dependencies' => '', + 'conflicts' => '', + 'priority' => 'top', + 'loadOrder' => '', + 'module' => '', + 'state' => 'stable', + 'internal' => 1, + 'uploadfolder' => 0, + 'createDirs' => '', + 'modify_tables' => '', + 'clearCacheOnLoad' => 0, + 'lockType' => 'S', + 'author' => 'Kasper Skaarhoj', + 'author_email' => 'kasperYYYY@typo3.com', + 'author_company' => 'Curby Soft Multimedia', + 'CGLcompilance' => '', + 'CGLcompilance_note' => '', + 'version' => '6.2.0', + '_md5_values_when_last_written' => '', + 'constraints' => array(), + 'suggests' => array(), + 'key' => 'about' + ), + 'core' => array( + 'title' => 'TYPO3 Core', + 'description' => 'Classes for the TYPO3 backend.', + 'category' => 'be', + 'shy' => 1, + 'dependencies' => '', + 'conflicts' => '', + 'priority' => 'top', + 'loadOrder' => '', + 'module' => '', + 'state' => 'stable', + 'internal' => 1, + 'uploadfolder' => 0, + 'createDirs' => '', + 'modify_tables' => '', + 'clearCacheOnLoad' => 0, + 'lockType' => 'S', + 'author' => 'Kasper Skaarhoj', + 'author_email' => 'kasperYYYY@typo3.com', + 'author_company' => 'Curby Soft Multimedia', + 'CGLcompilance' => '', + 'CGLcompilance_note' => '', + 'version' => '6.2.0', + '_md5_values_when_last_written' => '', + 'constraints' => array(), + 'suggests' => array(), + 'key' => 'about' + ), + 'dummy' => array( + 'title' => 'Dummy Extension for testing', + 'description' => 'This is just a dummy extension', + 'category' => 'experimental', + 'shy' => 1, + 'dependencies' => '', + 'conflicts' => '', + 'priority' => '', + 'loadOrder' => '', + 'module' => 'mod', + 'state' => 'stable', + 'internal' => 0, + 'uploadfolder' => 0, + 'createDirs' => '', + 'modify_tables' => '', + 'clearCacheOnLoad' => '', + 'lockType' => '', + 'author' => 'Stefano Kowalke', + 'author_email' => 'blueduck@gmx.net', + 'author_company' => 'Arroba IT', + 'CGLcompilance' => '', + 'CGLcompilance_note' => '', + 'version' => '6.2.0', + '_md5_values_when_last_written' => '', + 'constraints' => array(), + 'suggests' => array(), + 'key' => 'about' + ) + ); + } +} + diff --git a/composer.json b/composer.json index 9ffbd2b..9c2e8c2 100644 --- a/composer.json +++ b/composer.json @@ -2,19 +2,19 @@ "name": "etobi/coreapi", "description": "Provides a simple to use API for common core features. Goal is to be able to do the most common tasks by CLI instead of doing it in the backend/browser.", "type": "typo3-cms-extension", - + "version": "0.2.0", "keywords": ["typo3", "extension", "deployment", "commandline", "cli"], - "authors": [ { "name": "Tobias Liebig", "homepage": "http://etobi.de" + }, + { + "name": "Stefano Kowalke" } ], - "require": { "composer/installers": "~1.0" }, - "minimum-stability": "dev" -} +} \ No newline at end of file diff --git a/ext_autoload.php b/ext_autoload.php index 221b0e9..c341b5c 100644 --- a/ext_autoload.php +++ b/ext_autoload.php @@ -7,8 +7,9 @@ 'Etobi\CoreAPI\Command\DatabaseApiCommandController' => $extensionClassesPath . 'Command/DatabaseApiCommandController.php', 'Etobi\CoreAPI\Command\SiteApiCommandController' => $extensionClassesPath . 'Command/SiteApiCommandController.php', 'Etobi\CoreAPI\Command\CacheApiCommandController' => $extensionClassesPath . 'Command/CacheApiCommandController.php', + 'Etobi\CoreAPI\Command\ExtensionApiCommandController' => $extensionClassesPath . 'Command/ExtensionApiCommandController.php', 'Etobi\CoreAPI\Service\CacheApiService' => $extensionClassesPath . 'Service/CacheApiService.php', 'Etobi\CoreAPI\Service\SiteApiService' => $extensionClassesPath . 'Service/SiteApiService.php', 'Etobi\CoreAPI\Service\DatabaseApiService' => $extensionClassesPath . 'Service/DatabaseApiService.php', - 'Etobi\CoreAPI\Service\ExtensionApiService' => $extensionClassesPath . 'Service/ExtensionApiService.php' + 'Etobi\CoreAPI\Service\ExtensionApiService' => $extensionClassesPath . 'Service/ExtensionApiService.php', ); \ No newline at end of file diff --git a/ext_emconf.php b/ext_emconf.php index 16ecf3e..c4587f9 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -1,7 +1,7 @@ 'coreapi', + 'title' => 'Coreapi', 'description' => 'coreapi', 'category' => 'plugin', 'author' => 'Tobias Liebig,Georg Ringer,Stefano Kowalke', diff --git a/ext_localconf.php b/ext_localconf.php index da4611c..e9bdd37 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -9,7 +9,7 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['extbase']['commandControllers'][] = 'Etobi\CoreAPI\Command\ExtensionApiCommandController'; } - // Register the CLI dispatcher +// Register the CLI dispatcher $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['cliKeys'][$_EXTKEY] = array( 'EXT:' . $_EXTKEY . '/Scripts/Cli.php', '_CLI_lowlevel' ); \ No newline at end of file From f18081b8d62fcf43120eb07f5484d0b1b243bd8b Mon Sep 17 00:00:00 2001 From: Stefano Kowalke Date: Sun, 13 Jul 2014 16:58:58 +0200 Subject: [PATCH 4/4] Raise the version number --- composer.json | 2 +- ext_emconf.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 9c2e8c2..12bf043 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "etobi/coreapi", "description": "Provides a simple to use API for common core features. Goal is to be able to do the most common tasks by CLI instead of doing it in the backend/browser.", "type": "typo3-cms-extension", - "version": "0.2.0", + "version": "1.0.0-beta", "keywords": ["typo3", "extension", "deployment", "commandline", "cli"], "authors": [ { diff --git a/ext_emconf.php b/ext_emconf.php index c4587f9..b97d8c8 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -10,7 +10,7 @@ 'shy' => '', 'priority' => '', 'module' => '', - 'state' => 'alpha', + 'state' => 'beta', 'internal' => '', 'uploadfolder' => '0', 'createDirs' => '',