[FEATURE] Introduce native support for Symfony Console 81/45581/8
authorBenni Mack <benni@typo3.org>
Mon, 4 Jan 2016 20:34:58 +0000 (21:34 +0100)
committerGeorg Ringer <georg.ringer@gmail.com>
Mon, 1 Feb 2016 09:22:40 +0000 (10:22 +0100)
This feature allows to add Commands based on Symfony
Console, and also introduces a new binary located in
typo3/sysext/core/bin/t3console.

This effectively removes the need for typo3/cli_dispatch.phpsh
which is not part of any system extension.

Resolves: #73042
Releases: master
Change-Id: I01c2c600e379c314d7b9dd99d4716a280bfbb105
Reviewed-on: https://review.typo3.org/45581
Reviewed-by: Morton Jonuschat <m.jonuschat@mojocode.de>
Tested-by: Morton Jonuschat <m.jonuschat@mojocode.de>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
composer.json
composer.lock
typo3/sysext/backend/Classes/Command/LockBackendCommand.php [new file with mode: 0644]
typo3/sysext/backend/Configuration/Commands.php [new file with mode: 0644]
typo3/sysext/core/Classes/Console/CommandApplication.php [new file with mode: 0644]
typo3/sysext/core/Classes/Console/CommandRequestHandler.php [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-73042-IntroduceNativeSupportForSymfonyConsole.rst [new file with mode: 0644]
typo3/sysext/core/bin/t3console [new file with mode: 0755]
typo3/sysext/core/composer.json
typo3/sysext/lowlevel/Classes/AdminCommand.php [deleted file]
typo3/sysext/lowlevel/ext_localconf.php

index 89ccc0e..0911665 100644 (file)
@@ -27,6 +27,9 @@
                "optimize-autoloader": true,
                "bin-dir": "bin"
        },
+       "bin": [
+               "typo3/sysext/core/bin/t3console"
+       ],
        "require": {
                "php": ">=5.5.0",
                "ext-fileinfo": "*",
index 8f4be59..df48783 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "b3ee1a2e920605bac9c105764365761e",
+    "hash": "1746425a3334192ec0cdc3c2ef31b0af",
     "content-hash": "d06ab9b6a8fe495278326559effe7788",
     "packages": [
         {
diff --git a/typo3/sysext/backend/Classes/Command/LockBackendCommand.php b/typo3/sysext/backend/Classes/Command/LockBackendCommand.php
new file mode 100644 (file)
index 0000000..c24aa37
--- /dev/null
@@ -0,0 +1,117 @@
+<?php
+namespace TYPO3\CMS\Backend\Command;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Core function for locking and unlocking the TYPO3 Backend
+ */
+class LockBackendCommand extends Command
+{
+    /**
+     * Configure the command by defining the name, options and arguments
+     */
+    protected function configure()
+    {
+        if ($this->getName() === 'backend:unlock') {
+            $this
+                ->setDescription('Unlock the TYPO3 Backend');
+        } else {
+            $this
+                ->setDescription('Lock the TYPO3 Backend')
+                ->addArgument(
+                    'redirect',
+                    InputArgument::OPTIONAL,
+                    'If set, then the TYPO3 Backend will redirect to the locking state (only used when locking the TYPO3 Backend'
+                );
+        }
+    }
+
+    /**
+     * Executes the command for adding or removing the lock file
+     * @param InputInterface $input
+     * @param OutputInterface $output
+     * @return void
+     */
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $io = new SymfonyStyle($input, $output);
+        $io->title($this->getDescription());
+        if ($this->getName() === 'backend:unlock') {
+            $this->unlock($io);
+        } else {
+            $this->lock($io, $input);
+        }
+    }
+
+    /**
+     * Unlock the TYPO3 Backend by removing the lock file
+     *
+     * @param SymfonyStyle $io
+     */
+    protected function unlock(SymfonyStyle $io)
+    {
+        $lockFile = $this->getLockFileName();
+        if (@is_file($lockFile)) {
+            unlink($lockFile);
+            if (@is_file($lockFile)) {
+                $io->caution('Could not remove lock file "' . $lockFile . '"!');
+            } else {
+                $io->success('Removed lock file "' . $lockFile . '".');
+            }
+        } else {
+            $io->note('No lock file "' . $lockFile . '" was found.' . LF . 'Hence no lock can be removed.');
+        }
+    }
+
+    /**
+     * Lock the TYPO3 Backend
+     *
+     * @param SymfonyStyle $io
+     * @param InputInterface $input
+     */
+    protected function lock(SymfonyStyle $io, InputInterface $input)
+    {
+        $lockFile = $this->getLockFileName();
+        if (@is_file($lockFile)) {
+            $io->note('A lock file already exists. Overwriting it.');
+        }
+        $output = 'Wrote lock file to "' . $lockFile . '"';
+        if ($input->getArgument('redirect')) {
+            $lockFileContent = $input->getArgument('redirect');
+            $output .= LF . 'with content "' . $lockFileContent . '".';
+        } else {
+            $lockFileContent = '';
+            $output .= '.';
+        }
+        GeneralUtility::writeFile($lockFile, $lockFileContent);
+        $io->success($output);
+    }
+
+    /**
+     * Location of the file name
+     *
+     * @return string
+     */
+    protected function getLockFileName()
+    {
+        return PATH_typo3conf . 'LOCK_BACKEND';
+    }
+}
diff --git a/typo3/sysext/backend/Configuration/Commands.php b/typo3/sysext/backend/Configuration/Commands.php
new file mode 100644 (file)
index 0000000..708ae12
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+/**
+ * Commands to be executed by t3console, where the key of the array
+ * is the name of the command (to be called as the first argument after t3console).
+ * Required parameter is the "class" of the command which needs to be a subclass
+ * of Symfony/Console/Command. An optional parameter is "user" that logs in
+ * a Backend user via CLI.
+ */
+return [
+    'backend:lock' => [
+        'class' => \TYPO3\CMS\Backend\Command\LockBackendCommand::class
+    ],
+    'backend:unlock' => [
+        'class' => \TYPO3\CMS\Backend\Command\LockBackendCommand::class
+    ]
+];
diff --git a/typo3/sysext/core/Classes/Console/CommandApplication.php b/typo3/sysext/core/Classes/Console/CommandApplication.php
new file mode 100644 (file)
index 0000000..a7ba5eb
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+namespace TYPO3\CMS\Core\Console;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+use Symfony\Component\Console\Input\ArgvInput;
+use TYPO3\CMS\Core\Core\ApplicationInterface;
+use TYPO3\CMS\Core\Core\Bootstrap;
+
+/**
+ * Entry point for the TYPO3 Command Line for Commands
+ * Does not run the RequestHandler as this already runs an Application inside an Application which
+ * is just way too much logic around simple CLI calls
+ */
+class CommandApplication implements ApplicationInterface
+{
+    /**
+     * @var Bootstrap
+     */
+    protected $bootstrap;
+
+    /**
+     * @var string
+     */
+    protected $entryPointPath = 'typo3/sysext/core/bin/';
+
+    /**
+     * All available request handlers that can deal with a CLI Request
+     * @var array
+     */
+    protected $availableRequestHandlers = array(
+        \TYPO3\CMS\Core\Console\CommandRequestHandler::class,
+        \TYPO3\CMS\Backend\Console\CliRequestHandler::class
+    );
+
+    /**
+     * Constructor setting up legacy constants and register available Request Handlers
+     *
+     * @param \Composer\Autoload\ClassLoader $classLoader an instance of the class loader
+     */
+    public function __construct($classLoader)
+    {
+        $this->checkEnvironmentOrDie();
+        $this->defineLegacyConstants();
+        $this->bootstrap = Bootstrap::getInstance()
+            ->initializeClassLoader($classLoader)
+            ->setRequestType(TYPO3_REQUESTTYPE_BE | TYPO3_REQUESTTYPE_CLI)
+            ->baseSetup($this->entryPointPath);
+
+        foreach ($this->availableRequestHandlers as $requestHandler) {
+            $this->bootstrap->registerRequestHandlerImplementation($requestHandler);
+        }
+
+        $this->bootstrap->configure();
+    }
+
+    /**
+     * Run the Symfony Console application in this TYPO3 application
+     *
+     * @param callable $execute
+     * @return void
+     */
+    public function run(callable $execute = null)
+    {
+        $this->bootstrap->handleRequest(new ArgvInput());
+
+        if ($execute !== null) {
+            call_user_func($execute);
+        }
+
+        $this->bootstrap->shutdown();
+    }
+
+    /**
+     * Define constants and variables
+     */
+    protected function defineLegacyConstants()
+    {
+        define('TYPO3_MODE', 'BE');
+    }
+
+    /**
+     * Check the script is called from a cli environment.
+     *
+     * @return void
+     */
+    protected function checkEnvironmentOrDie()
+    {
+        if (php_sapi_name() !== 'cli') {
+            die('Not called from a command line interface (e.g. a shell or scheduler).' . LF);
+        }
+    }
+}
diff --git a/typo3/sysext/core/Classes/Console/CommandRequestHandler.php b/typo3/sysext/core/Classes/Console/CommandRequestHandler.php
new file mode 100644 (file)
index 0000000..daeaf32
--- /dev/null
@@ -0,0 +1,181 @@
+<?php
+namespace TYPO3\CMS\Core\Console;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\ConsoleOutput;
+use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Core\Package\PackageManager;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Command Line Interface Request Handler dealing with registered commands.
+ */
+class CommandRequestHandler implements RequestHandlerInterface
+{
+    /**
+     * Instance of the current TYPO3 bootstrap
+     * @var Bootstrap
+     */
+    protected $bootstrap;
+
+    /**
+     * Instance of the symfony application
+     * @var Application
+     */
+    protected $application;
+
+    /**
+     * @var []
+     */
+    protected $availableCommands;
+
+    /**
+     * Constructor handing over the bootstrap
+     *
+     * @param Bootstrap $bootstrap
+     */
+    public function __construct(Bootstrap $bootstrap)
+    {
+        $this->bootstrap = $bootstrap;
+        $this->application = new Application('TYPO3 CMS', TYPO3_version);
+    }
+
+    /**
+     * Handles any commandline request
+     *
+     * @param InputInterface $input
+     * @return void
+     */
+    public function handleRequest(InputInterface $input)
+    {
+        $output = new ConsoleOutput();
+
+        $this->bootstrap->loadExtensionTables();
+
+        // Check if the command to run needs a backend user to be loaded
+        $command = $this->getCommandToRun($input);
+        foreach ($this->availableCommands as $data) {
+            if ($data['command'] !== $command) {
+                continue;
+            }
+            if (isset($data['user'])) {
+                $this->initializeBackendUser($data['user']);
+            }
+        }
+
+        // Make sure output is not buffered, so command-line output and interaction can take place
+        GeneralUtility::flushOutputBuffers();
+        $exitCode = $this->application->run($input, $output);
+        exit($exitCode);
+    }
+
+    /**
+     * If the backend script is in CLI mode, it will try to load a backend user named by the CLI module name (in lowercase)
+     *
+     * @param string $userName the name of the module registered inside $TYPO3_CONF_VARS[SC_OPTIONS][GLOBAL][cliKeys] as second parameter
+     * @throws \RuntimeException if a non-admin Backend user could not be loaded
+     */
+    protected function initializeBackendUser($userName)
+    {
+        $this->bootstrap->initializeBackendUser();
+
+        $GLOBALS['BE_USER']->setBeUserByName($userName);
+        if (!$GLOBALS['BE_USER']->user['uid']) {
+            throw new \RuntimeException('No backend user named "' . $userName . '" was found!', 3);
+        }
+
+        $this->bootstrap
+            ->initializeBackendAuthentication()
+            ->initializeLanguageObject();
+    }
+
+    /**
+     * This request handler can handle any CLI request, but checks for
+     *
+     * @param InputInterface $input
+     * @return bool Always TRUE
+     */
+    public function canHandleRequest(InputInterface $input)
+    {
+        $this->populateAvailableCommands();
+        return $this->getCommandToRun($input) !== false;
+    }
+
+    /**
+     * Returns the priority - how eager the handler is to actually handle the request.
+     *
+     * @return int The priority of the request handler.
+     */
+    public function getPriority()
+    {
+        return 50;
+    }
+
+    /**
+     *
+     * @param InputInterface $input
+     * @return bool|Command
+     */
+    protected function getCommandToRun(InputInterface $input)
+    {
+        $firstArgument = $input->getFirstArgument();
+        try {
+            return $this->application->find($firstArgument);
+        } catch (\InvalidArgumentException $e) {
+            return false;
+        }
+    }
+
+    /**
+     * put all available commands inside the application
+     */
+    protected function populateAvailableCommands()
+    {
+        $this->availableCommands = $this->getAvailableCommands();
+        foreach ($this->availableCommands as $name => $data) {
+            /** @var Command $cmd */
+            $cmd = GeneralUtility::makeInstance($data['class'], $name);
+            $this->application->add($cmd);
+            $this->availableCommands[$name]['command'] = $cmd;
+        }
+    }
+
+    /**
+     * Fetches all commands registered via Commands.php of all active packages
+     *
+     * @return array
+     */
+    protected function getAvailableCommands()
+    {
+        /** @var PackageManager $packageManager */
+        $packageManager = Bootstrap::getInstance()->getEarlyInstance(PackageManager::class);
+        $availableCommands = [];
+
+        foreach ($packageManager->getActivePackages() as $package) {
+            $commandsOfExtension = $package->getPackagePath() . 'Configuration/Commands.php';
+            if (@is_file($commandsOfExtension)) {
+                $commands = require_once $commandsOfExtension;
+                if (is_array($commands)) {
+                    $availableCommands = array_merge($availableCommands, $commands);
+                }
+            }
+        }
+
+        return $availableCommands;
+    }
+}
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-73042-IntroduceNativeSupportForSymfonyConsole.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-73042-IntroduceNativeSupportForSymfonyConsole.rst
new file mode 100644 (file)
index 0000000..5b6ed1b
--- /dev/null
@@ -0,0 +1,49 @@
+==============================================================
+Feature: #73042 - Introduce native support for Symfony Console
+==============================================================
+
+Description
+===========
+
+TYPO3 supports the Symfony Console component out-of-the-box now by providing a new Command Line script
+located in typo3/sysext/core/bin/t3console. On TYPO3 instances installed via Composer, the binary can be
+linked into bin/t3console.
+
+The new binary still supports the existing command-line arguments when no proper Symfony Console command
+was found as a fallback.
+
+Registering a command to be available via the ``t3console`` command line tool works by putting a
+``Configuration/Commands.php`` file into any installed extension. This lists the Symfony/Console/Command classes
+to be executed by t3console in an associative array. The key of the is the name of the command to be called as
+the first argument after ``t3console``.
+
+A required parameter when registering a command is the "class" property. Optionally the "user" parameter can be
+set so a Backend user is logged in when calling the command.
+
+The extensions' ``Configuration/Commands.php`` could look like this:
+
+.. code-block:: php
+
+    return [
+        'backend:lock' => [
+            'class' => \TYPO3\CMS\Backend\Command\LockBackendCommand::class
+        ],
+        'referenceindex:update' => [
+            'class' => \TYPO3\CMS\Backend\Command\ReferenceIndexUpdateCommand::class,
+            'user' => '_cli_lowlevel'
+        ]
+    ];
+
+
+An example call could look like:
+
+.. code-block:: sh
+
+       typo3/sysext/core/bin/t3console backend:lock http://www.mydomain.com/maintenance.html
+
+
+Impact
+======
+
+Using Symfony Commands and calling ``t3console`` instead of using ``typo3/cli_dispatch.phpsh`` is
+now the preferred way for writing Command Line code.
diff --git a/typo3/sysext/core/bin/t3console b/typo3/sysext/core/bin/t3console
new file mode 100755 (executable)
index 0000000..7223e87
--- /dev/null
@@ -0,0 +1,23 @@
+#! /usr/bin/env php
+<?php
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Command Line Interface module dispatcher
+ * that executes commands
+ */
+call_user_func(function() {
+    $classLoader = require __DIR__ . '/../../../../vendor/autoload.php';
+    (new \TYPO3\CMS\Core\Console\CommandApplication($classLoader))->run();
+});
index 522b374..b029984 100644 (file)
@@ -11,6 +11,9 @@
        "replace": {
                "core": "*"
        },
+       "bin": [
+               "bin/t3console"
+       ],
        "extra": {
                "typo3/cms": {
                        "Package": {
diff --git a/typo3/sysext/lowlevel/Classes/AdminCommand.php b/typo3/sysext/lowlevel/Classes/AdminCommand.php
deleted file mode 100644 (file)
index 8191cf6..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-namespace TYPO3\CMS\Lowlevel;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-/**
- * Core functions for administration
- */
-class AdminCommand extends \TYPO3\CMS\Core\Controller\CommandLineController
-{
-    /**
-     * @var array
-     */
-    public $adminModules = array(
-        'setBElock' => 'Set the Backend Lock',
-        'clearBElock' => 'Clears the Backend Lock',
-        'msg' => 1
-    );
-
-    /**
-     * Constructor
-     */
-    public function __construct()
-    {
-        // Running parent class constructor
-        parent::__construct();
-        // Adding options to help archive:
-        $this->cli_options[] = array('--redirect=[URL]', 'For toolkey "setBElock": The URL to which the redirection will occur.');
-        // Setting help texts:
-        $this->cli_help['name'] = 'lowlevel_admin -- Various functions for administration and maintenance of TYPO3 from the command line';
-        $this->cli_help['synopsis'] = 'toolkey ###OPTIONS###';
-        $this->cli_help['description'] = 'The \'toolkey\' keywords are:
-
-  ' . implode('
-  ', array_keys($this->adminModules));
-        $this->cli_help['examples'] = '/.../cli_dispatch.phpsh lowlevel_admin setBElock --redirect=http://url_which_explains_why.com/';
-        $this->cli_help['author'] = 'Kasper Skaarhoej, (c) 2009';
-    }
-
-    /**************************
-     *
-     * CLI functionality
-     *
-     *************************/
-    /**
-     * CLI engine
-     *
-     * @param array $argv Command line arguments
-     * @return string
-     */
-    public function cli_main($argv)
-    {
-        // Force user to admin state and set workspace to "Live":
-        $GLOBALS['BE_USER']->user['admin'] = 1;
-        $GLOBALS['BE_USER']->setWorkspace(0);
-        // Print help
-        $analysisType = (string)$this->cli_args['_DEFAULT'][1];
-        if (!$analysisType) {
-            $this->cli_validateArgs();
-            $this->cli_help();
-            die;
-        }
-        // Analysis type:
-        switch ((string)$analysisType) {
-            case 'setBElock':
-                if (@is_file((PATH_typo3conf . 'LOCK_BACKEND'))) {
-                    $this->cli_echo('A lockfile already exists. Overwriting it...
-');
-                }
-                $lockFileContent = $this->cli_argValue('--redirect');
-                \TYPO3\CMS\Core\Utility\GeneralUtility::writeFile(PATH_typo3conf . 'LOCK_BACKEND', $lockFileContent);
-                $this->cli_echo('Wrote lock-file to \'' . PATH_typo3conf . 'LOCK_BACKEND\' with content \'' . $lockFileContent . '\'');
-                break;
-            case 'clearBElock':
-                if (@is_file((PATH_typo3conf . 'LOCK_BACKEND'))) {
-                    unlink(PATH_typo3conf . 'LOCK_BACKEND');
-                    if (@is_file((PATH_typo3conf . 'LOCK_BACKEND'))) {
-                        $this->cli_echo('ERROR: Could not remove lock file \'' . PATH_typo3conf . 'LOCK_BACKEND\'!!
-', 1);
-                    } else {
-                        $this->cli_echo('Removed lock file \'' . PATH_typo3conf . 'LOCK_BACKEND\'
-');
-                    }
-                } else {
-                    $this->cli_echo('No lock file \'' . PATH_typo3conf . 'LOCK_BACKEND\' was found; hence no lock can be removed.\'
-');
-                }
-                break;
-            default:
-                $this->cli_echo('Unknown toolkey, \'' . $analysisType . '\'');
-        }
-        $this->cli_echo(LF);
-    }
-}
index 9786f31..9fd21ba 100644 (file)
@@ -30,13 +30,6 @@ if (TYPO3_MODE === 'BE') {
         },
         '_CLI_lowlevel'
     );
-    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['cliKeys']['lowlevel_admin'] = array(
-        function () {
-            $adminObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Lowlevel\AdminCommand::class);
-            $adminObj->cli_main($_SERVER['argv']);
-        },
-        '_CLI_lowlevel'
-    );
     $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules']['missing_files'] = array(\TYPO3\CMS\Lowlevel\MissingFilesCommand::class);
     $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules']['missing_relations'] = array(\TYPO3\CMS\Lowlevel\MissingRelationsCommand::class);
     $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules']['double_files'] = array(\TYPO3\CMS\Lowlevel\DoubleFilesCommand::class);