Преглед на файлове

implement new ot_admin CLI commands

Olivier Massot преди 5 години
родител
ревизия
2cc35b282d

+ 1 - 1
ot_admin/Classes/Command/CreateOrganizationCommand.php → ot_admin/Classes/Command/CreateSiteCommand.php

@@ -16,7 +16,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
  *
  * @package Opentalent\OtAdmin\Command
  */
-class CreateOrganizationCommand extends Command
+class CreateSiteCommand extends Command
 {
 
 

+ 81 - 0
ot_admin/Classes/Command/DeleteSiteCommand.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace Opentalent\OtAdmin\Command;
+
+
+use Opentalent\OtAdmin\Controller\SiteController;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+/**
+ * This CLI command delete an existing organization's website
+ *
+ * @package Opentalent\OtAdmin\Command
+ */
+class DeleteSiteCommand extends Command
+{
+
+
+    /**
+     * -- This method is expected by Typo3, do not rename ou remove --
+     *
+     * Allows to configure the command.
+     * Allows to add a description, a help text, and / or define arguments.
+     *
+     */
+    protected function configure()
+    {
+        $this
+            ->setName("ot:site:delete")
+            ->setDescription("Delete an organization website")
+            ->setHelp("Call this method by giving it the organization's id in the Opentalent DB. 
+                            By default, it proceed to a soft deletion, marking the records as deleted 
+                            and renaming files: this kind of deletion can then be undone 
+                            with the ot:site:undelete command.
+                            If the --hard option is passed, the records and files will be permanently deleted,
+                            with no possibility of undoing. A confirmation will be demanded.")
+            ->addOption(
+                'hard',
+                null,
+                InputOption::VALUE_NONE,
+                "Permanently delete the records and files. Use with caution."
+            )
+            ->addArgument(
+                'organization_id',
+                InputArgument::REQUIRED,
+                "The organization's id in the opentalent DB"
+            );
+    }
+
+    /**
+     * -- This method is expected by Typo3, do not rename ou remove --
+     *
+     * Executes the command for creating the new organization
+     *
+     * @param InputInterface $input
+     * @param OutputInterface $output
+     * @throws \Exception
+     */
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $org_id = $input->getArgument('organization_id');
+        $hard = $input->getOption('hard');
+
+        $io = new SymfonyStyle($input, $output);
+
+        $siteController = new SiteController();
+        $rootUid = $siteController->deleteSiteAction($org_id, $hard);
+
+        if ($hard) {
+            $io->success(sprintf("The website with root uid " . $rootUid . " has been permanently deleted"));
+        } else {
+            $io->success(sprintf("The website with root uid " . $rootUid . " has been soft-deleted. Use ot:site:undelete to restore it."));
+        }
+
+    }
+
+}

+ 0 - 76
ot_admin/Classes/Command/EnableFeEditingCommand.php

@@ -1,76 +0,0 @@
-<?php
-
-namespace Opentalent\OtAdmin\Command;
-
-
-use Opentalent\OtAdmin\Controller\SiteController;
-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\Database\ConnectionPool;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-
-/**
- * This CLI command enable front-end editing for all be-users
- *
- * @package Opentalent\OtAdmin\Command
- */
-class EnableFeEditingCommand extends Command
-{
-
-    /**
-     * -- This method is expected by Typo3, do not rename ou remove --
-     *
-     * Allows to configure the command.
-     * Allows to add a description, a help text, and / or define arguments.
-     *
-     */
-    protected function configure()
-    {
-        $this
-            ->setName("ot:users:enfeedit")
-            ->setDescription("Enable front-end editing for all be-users");
-    }
-
-    /**
-     * -- This method is expected by Typo3, do not rename ou remove --
-     *
-     * Executes the command for creating the new organization
-     *
-     * @param InputInterface $input
-     * @param OutputInterface $output
-     */
-    protected function execute(InputInterface $input, OutputInterface $output)
-    {
-        $io = new SymfonyStyle($input, $output);
-
-        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-            ->getQueryBuilderForTable('be_users');
-        $queryBuilder->getRestrictions()->removeAll();
-
-        $statement = $queryBuilder
-            ->select('be_users.*')
-            ->from('be_users')
-            ->where(
-                $queryBuilder->expr()->eq('be_users.usergroup', $queryBuilder->createNamedParameter(3, \PDO::PARAM_INT))
-            )
-            ->execute()
-            ;
-
-        while ($row = $statement->fetch()) {
-            $id = (int)$row['uid'];
-            $BE_USER = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Authentication\\BackendUserAuthentication');
-            $user = $BE_USER->getRawUserByUid($id);
-            $BE_USER->user = $user;
-            $BE_USER->backendSetUC();
-            $BE_USER->uc['frontend_editing'] = 1;
-            $BE_USER->uc['frontend_editing_overlay'] = 1;
-            $BE_USER->writeUC($BE_USER->uc);
-        }
-
-        $io->success(sprintf("-- done --"));
-    }
-
-}

+ 68 - 0
ot_admin/Classes/Command/UndeleteSiteCommand.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace Opentalent\OtAdmin\Command;
+
+
+use Opentalent\OtAdmin\Controller\SiteController;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+/**
+ * This CLI command undo the soft-deletion an existing organization's website
+ *
+ * @package Opentalent\OtAdmin\Command
+ */
+class UndeleteSiteCommand extends Command
+{
+
+
+    /**
+     * -- This method is expected by Typo3, do not rename ou remove --
+     *
+     * Allows to configure the command.
+     * Allows to add a description, a help text, and / or define arguments.
+     *
+     */
+    protected function configure()
+    {
+        $this
+            ->setName("ot:site:undelete")
+            ->setDescription("Undo the soft-deletion of an organization website")
+            ->setHelp("Call this method by giving it the organization's id in the Opentalent DB. 
+                            If an organization website has been deleted with the ot:site:delete
+                            command and without the --hard option, you can undo the deletion with
+                            this command.")
+            ->addArgument(
+                'organization_id',
+                InputArgument::REQUIRED,
+                "The organization's id in the opentalent DB"
+            );
+    }
+
+    /**
+     * -- This method is expected by Typo3, do not rename ou remove --
+     *
+     * Executes the command for creating the new organization
+     *
+     * @param InputInterface $input
+     * @param OutputInterface $output
+     * @throws \Exception
+     */
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $org_id = $input->getArgument('organization_id');
+
+        $io = new SymfonyStyle($input, $output);
+
+        $siteController = new SiteController();
+        $rootUid = $siteController->undeleteSiteAction($org_id);
+
+        $io->success(sprintf("The website with root uid " . $rootUid . " has been restored"));
+
+    }
+
+}

+ 64 - 0
ot_admin/Classes/Command/UpdateSiteCommand.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace Opentalent\OtAdmin\Command;
+
+
+use Opentalent\OtAdmin\Controller\SiteController;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+/**
+ * This CLI command update an existing organization's website
+ * with the latest data from the Opentalent DB
+ *
+ * @package Opentalent\OtAdmin\Command
+ */
+class UpdateSiteCommand extends Command
+{
+    /**
+     * -- This method is expected by Typo3, do not rename ou remove --
+     *
+     * Allows to configure the command.
+     * Allows to add a description, a help text, and / or define arguments.
+     *
+     */
+    protected function configure()
+    {
+        $this
+            ->setName("ot:site:update")
+            ->setDescription("Update an organization website")
+            ->setHelp("This CLI command update an existing organization's website
+                            with the latest data from the Opentalent DB")
+            ->addArgument(
+                'organization_id',
+                InputArgument::REQUIRED,
+                "The organization's id in the opentalent DB"
+            );
+    }
+
+    /**
+     * -- This method is expected by Typo3, do not rename ou remove --
+     *
+     * Executes the command for creating the new organization
+     *
+     * @param InputInterface $input
+     * @param OutputInterface $output
+     * @throws \Exception
+     */
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $org_id = $input->getArgument('organization_id');
+
+        $io = new SymfonyStyle($input, $output);
+
+        $siteController = new SiteController();
+        $rootUid = $siteController->updateSiteConstantsAction($org_id);
+
+        $io->success(sprintf("The website with root uid " . $rootUid . " has been updated"));
+    }
+
+}

+ 781 - 81
ot_admin/Classes/Controller/SiteController.php

@@ -5,8 +5,11 @@ namespace Opentalent\OtAdmin\Controller;
 use Opentalent\OtTemplating\Domain\Model\Organization;
 use Opentalent\OtTemplating\Domain\Repository\OrganizationRepository;
 use Opentalent\OtTemplating\Exception\ApiRequestException;
+use Opentalent\OtTemplating\Page\OtPageRepository;
+use PDO;
 use Psr\Log\LoggerAwareInterface;
 use Symfony\Component\Yaml\Yaml;
+use TYPO3\CMS\Core\Crypto\Random;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
@@ -48,13 +51,19 @@ class SiteController extends ActionController
     const CTYPE_LIST = 'list';
     const CTYPE_SITEMAP = 'menu_sitemap';
 
-//    const DEFAULT_ROOT_PID = 134833;
-    const DEFAULT_ROOT_PID = 11;
-
     // Default values
     const DEFAULT_THEME = 'Classic';
     const DEFAULT_COLOR = 'light-blue';
 
+    // BE rights
+    CONST PRODUCT_MAPPING = [
+        "school-standard" => 1, // Association writer basic
+        "artist-standard" => 1, // Association writer basic
+        "school-premium" => 3, // Association writer full
+        "artist-premium" => 3, // Association writer full
+        "manager" => 3, // Association writer full
+    ];
+
     /**
      * Doctrine connection pool
      * @var object|LoggerAwareInterface|\TYPO3\CMS\Core\SingletonInterface
@@ -62,17 +71,31 @@ class SiteController extends ActionController
     private $cnnPool;
 
     /**
-     * Index of the pages created
+     * Index of the pages created during the process
      * >> [slug => uid]
      * @var array
      */
     private $createdPagesIndex;
 
+    /**
+     * List of the directories created in the process (for rollback purposes)
+     * @var array
+     */
+    private $createdDirs;
+
+    /**
+     * List of the files created in the process (for rollback purposes)
+     * @var array
+     */
+    private $createdFiles;
+
     public function __construct()
     {
         parent::__construct();
         $this->cnnPool = GeneralUtility::makeInstance(ConnectionPool::class);
         $this->createdPagesIndex = [];
+        $this->createdDirs = [];
+        $this->createdFiles = [];
     }
 
     /**
@@ -81,21 +104,11 @@ class SiteController extends ActionController
      *
      * @param int $organizationId
      * @return int Uid of the root page of the newly created website
-     * @throws \RuntimeException
+     * @throws \RuntimeException|\Exception
      */
     public function createSiteAction(int $organizationId) {
 
-        // ** Get the organization object from the Opentalent API
-        $manager = GeneralUtility::makeInstance(ObjectManager::class);
-        $organizationRepository = GeneralUtility::makeInstance(
-            OrganizationRepository::class,
-            $manager
-        );
-        try {
-            $organization = $organizationRepository->findById($organizationId);
-        } catch (ApiRequestException $e) {
-            throw new \RuntimeException('Unable to fetch the organization with id: ' . $organizationId);
-        }
+        $organization = $this->fetchOrganization($organizationId);
 
         // ** Test the existence of a website with this name and or organization id
 
@@ -127,11 +140,9 @@ class SiteController extends ActionController
         // ** Create the new website
 
         // start transactions
-        $tables = ['be_users', 'pages', 'sys_domain', 'sys_template', 'tt_content'];
-        foreach ($tables as $table) {
-            $this->cnnPool->getConnectionForTable($table)->beginTransaction();
-        }
+        $this->cnnPool->getConnectionByName('Default')->beginTransaction();
 
+        // keep tracks of the created folders and files to be able to remove them during a rollback
         try {
             // Create the site pages:
             // > Root page
@@ -322,7 +333,7 @@ class SiteController extends ActionController
             $this->insertContent(
                 $this->createdPagesIndex['/mentions-legales'],
                 self::CTYPE_TEXT,
-                '<p style="margin-bottom: 0cm"><b>Mentions Légales</b></p>',
+                '<p style="margin-bottom: 0"><b>Mentions Légales</b></p>',
                 0
             );
 
@@ -336,26 +347,8 @@ class SiteController extends ActionController
                 ])
                 ->execute();
 
-            // Create the template
-
-            $constants = "plugin.tx_ottemplating {" .
-                         "    settings {" .
-                         "        organization {" .
-                         "            id = " . $organization->getId() .
-                         "            name = " . $organization->getName() .
-                         "            is_network = " . (1 ? $organization->getIsNetwork() : 0) .
-                         "            email = " . $organization->getEmail() .
-                         "            logoid = " . explode('/', $organization->getLogo(), -1)[0] .
-                         "            twitter = " . $organization->getTwitter() .
-                         "            facebook = " . $organization->getFacebook() .
-                         "        }" .
-                         "    network {" .
-                         "            logo = " . $organization->getNetworkLogo() .
-                         "            name = CMF" . $organization->getNetworkName() .
-                         "            url = " . $organization->getNetworkUrl() .
-                         "        }" .
-                         "    }" .
-                         "}";
+            // update sys_template
+            $constants = $this->genTemplateConstants($organizationId);
 
             $queryBuilder = $this->cnnPool->getQueryBuilderForTable('sys_template');
             $queryBuilder->insert('sys_template')
@@ -372,61 +365,527 @@ class SiteController extends ActionController
                 ->execute();
 
             // Create the site config.yaml file
-            $config = ['base'=> 'https://' . $domain,
-                'baseVariants'=>[],
-                'errorHandling'=>[
-                    ['errorCode'=>'404',
-                     'errorHandler'=>'Page',
-                     'errorContentSource'=>'t3://page?uid=' . $this->createdPagesIndex['/page-introuvable']],
-                    ['errorCode'=>'403',
-                     'errorHandler'=>'Page',
-                     'errorContentSource'=>'t3://page?uid=' . $this->createdPagesIndex['/page-introuvable']]
-                ],
-                'flux_content_types'=>'',
-                'flux_page_templates'=>'',
-                'languages'=>[[
-                    'title'=>'Fr',
-                    'enabled'=>True,
-                    'base'=>'/',
-                    'typo3Language'=>'fr',
-                    'locale'=>'fr_FR',
-                    'iso-639-1'=>'fr',
-                    'navigationTitle'=>'Fr',
-                    'hreflang'=>'fr-FR',
-                    'direction'=>'ltr',
-                    'flag'=>'fr',
-                    'languageId'=>'0',
-                ]],
-                'rootPageId'=>$rootUid,
-                'routes'=>[]
-                ];
-            $yamlConfig = Yaml::dump($config, 4);
-
-            // Create the contact form
-            $contactForm = "";
+            $this->writeConfigFile($organizationId, $rootUid, $domain);
 
             // Create the BE user
+            // -- BE user will be auto-updated by the ot_connect extension --
+
+            // Create the user_upload directory and update the sys_filemounts table
+            $uploadRelPath = "/user_upload/" . $organizationId;
+            $uploadDir = $_ENV['TYPO3_PATH_APP'] . "/public/fileadmin" . $uploadRelPath;
+            if (file_exists($uploadDir)) {
+                throw new \RuntimeException("A directory or file " . $uploadDir . " already exists. Abort.");
+            }
 
-            // Create the user_upload directory (sys_filemounts)
+            $this->mkDir($uploadDir);
+            $this->mkDir($uploadDir . '/images');
+            $this->mkDir($uploadDir . '/Forms');
+
+            // Insert the filemounts points (sys_filemounts)
+            $queryBuilder = $this->cnnPool->getQueryBuilderForTable('sys_filemounts');
+            $queryBuilder->insert('sys_filemounts')
+                ->values([
+                    'title' => 'Documents',
+                    'path' => $uploadRelPath . '/images',
+                    'base' => 1
+                ])
+                ->execute();
+
+            $queryBuilder = $this->cnnPool->getQueryBuilderForTable('sys_filemounts');
+            $queryBuilder->insert('sys_filemounts')
+                ->values([
+                    'title' => 'Forms_' . $organizationId,
+                    'path' => $uploadRelPath . '/Forms',
+                    'base' => 1
+                ])
+                ->execute();
+
+            // Create the BE User
+            $beUid = $this->createBeUser($organizationId, $rootUid, $domain);
+
+            // Give the keys of the website to this user (makes him the owner)
+            $queryBuilder = $this->cnnPool->getQueryBuilderForTable('pages');
+            foreach($this->createdPagesIndex as $slug => $uid) {
+                $queryBuilder
+                    ->update('pages')
+                    ->where($queryBuilder->expr()->eq('uid', $uid))
+                    ->set('perms_userid', $beUid)
+                    ->execute();
+            }
 
             // Try to commit the result
-            foreach ($tables as $table) {
-                $commitSuccess = $this->cnnPool->getConnectionForTable($table)->commit();
-                if (!$commitSuccess) {
-                    throw new \RuntimeException('Something went wrong while commiting the result');
+            $commitSuccess = $this->cnnPool->getConnectionByName('Default')->commit();
+            if (!$commitSuccess) {
+                throw new \RuntimeException('Something went wrong while commiting the result');
+            }
+            return $rootUid;
+
+        } catch(\Exception $e) {
+            // rollback
+            $this->cnnPool->getConnectionByName('Default')->rollback();
+
+            // remove created files and dirs
+            foreach (array_reverse($this->createdFiles) as $filename) {
+                unlink($filename);
+            }
+            $this->createdFiles = [];
+
+            foreach (array_reverse($this->createdDirs) as $dirname) {
+                rmdir($dirname);
+            }
+            $this->createdDirs = [];
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Update the `sys_template`.`constants` field of the given
+     * organization's website with the data fetched from the opentalent DB.
+     *
+     * @param int $organizationId
+     * @return int
+     */
+    public function updateSiteConstantsAction(int $organizationId) {
+        $rootUid = $this->findRootUidFor($organizationId);
+
+        $constants = $this->genTemplateConstants($organizationId);
+
+        $queryBuilder = $this->cnnPool->getQueryBuilderForTable('sys_template');
+        $queryBuilder
+            ->update('sys_template')
+            ->set('constants', $constants)
+            ->where($queryBuilder->expr()->eq('uid', $rootUid))
+            ->execute();
+        return $rootUid;
+    }
+
+    /**
+     * Delete the website with all its pages, contents and related records
+     *
+     * If the hard parameter is false, the records' `deleted` field will be set to true and
+     * the files and directories will be renamed. This kind of 'soft' deletion can be undone.
+     *
+     * Otherwise, if hard is set to true, the records and files will be permanently removed,
+     * with no possibility of undoing anything. In this case, you'll have to confirm your intention
+     * by creating a file in the Typo3 root directory, named 'DEL####' (#### is the organization id)
+     *
+     * @param int $organizationId
+     * @param bool $hard
+     * @return int
+     * @throws \Exception
+     */
+    public function deleteSiteAction(int $organizationId, bool $hard=false) {
+        $rootUid = $this->findRootUidFor($organizationId);
+
+        $queryBuilder = $this->cnnPool->getQueryBuilderForTable('pages');
+        $isDeleted = $queryBuilder
+            ->select('deleted')
+            ->from('pages')
+            ->where($queryBuilder->expr()->eq('uid', $rootUid))
+            ->execute()
+            ->fetchColumn(0) == 1;
+
+        $confirm_file = $_ENV['TYPO3_PATH_APP'] . "/DEL" . $organizationId;
+        if ($hard && !file_exists($confirm_file)) {
+            throw new \RuntimeException(
+                "You are going to completely delete the website with root uid " . $rootUid .
+                ", and all of its pages, files, contents...etc. If you are sure, create a file named '" .
+                $confirm_file . "', and launch this command again."
+            );
+        }
+
+        // start transactions
+        $this->cnnPool->getConnectionByName('Default')->beginTransaction();
+
+        // keep track of renamed file for an eventual rollback
+        $renamed = [];
+
+        try {
+            $repository = new OtPageRepository();
+            $pages = $repository->getAllSubpagesForPage($rootUid);
+            foreach($pages as $page) {
+                $this->delete('tt_content', 'pid', $page['uid'], $hard);
+                $this->delete('pages', 'uid', $page['uid'], $hard);
+            }
+            $this->delete('tt_content', 'pid', $rootUid, $hard);
+            $this->delete('pages', 'uid', $rootUid, $hard);
+            $this->delete('sys_template', 'pid', $rootUid, $hard);
+
+            // remove filemounts
+            $this->delete('sys_filemounts',
+                'path',
+                "'/user_upload/" . $organizationId . "/images'",
+                $hard);
+            $this->delete('sys_filemounts',
+                'path',
+                "'/user_upload/" . $organizationId . "/Forms'",
+                $hard);
+
+            $this->delete('be_users', 'db_mountpoints', $rootUid, $hard);
+
+            // Look up for the config.yaml file of the website
+            $configMainDir = $_ENV['TYPO3_PATH_APP'] . '/config/sites';
+            $configYamlFile = "";
+            foreach (glob($configMainDir . '/*',  GLOB_ONLYDIR) as $subdir) {
+                if (!$isDeleted) {
+                    $yamlFile = $subdir . '/config.yaml';
+                } else {
+                    $yamlFile = $subdir . '/config.yaml.deleted';
+                }
+
+                if (is_file($yamlFile)) {
+                    $conf = Yaml::parseFile($yamlFile);
+
+                    if ($conf['rootPageId'] == $rootUid) {
+                        $configYamlFile = $yamlFile;
+                        break;
+                    }
+                }
+            }
+            if (!$isDeleted) {
+                $uploadDir = $_ENV['TYPO3_PATH_APP'] . '/public/fileadmin/user_upload/' . $organizationId . '/';
+            } else {
+                $uploadDir = $_ENV['TYPO3_PATH_APP'] . '/public/fileadmin/user_upload/deleted_' . $organizationId . '/';
+            }
+
+            // If hard deletion, verify that upload dirs are empty
+            if ($hard && is_file($uploadDir)) {
+                foreach (scandir($uploadDir) as $subdir) {
+                    if (!is_dir($subdir)) {
+                        throw new \RuntimeException(
+                            'The directory ' . $uploadDir . ' contains non-directory files' .
+                            ', this humble script prefers not to take care of them automatically. Cancel.');
+                    }
+                    if (is_readable($subdir) && count(scandir($subdir)) > 0) {
+                        throw new \RuntimeException(
+                            'The directory ' . $subdir . ' is not empty, ' .
+                            'this humble script prefers not to take care of them automatically. Cancel.');
+                    }
                 }
             }
+
+            // If soft deletion, check that no deleted file or directory exist
+            if (!$hard) {
+                $toRename = [];
+                if (!$hard) {
+                    if (is_file($configYamlFile)) {
+                        $toRename[$configYamlFile] = $configYamlFile . '.deleted';
+                    }
+                    if (is_dir($uploadDir)) {
+                        $toRename[$uploadDir] = dirname($uploadDir) . '/deleted_' . basename($uploadDir);
+                    }
+                }
+
+                foreach ($toRename as $initialPath => $newPath) {
+                    if (is_file($newPath)) {
+                        throw new \RuntimeException(
+                            'A file or directory named ' . $newPath . ' already exists, what happened?. Cancel.');
+                    }
+                }
+            }
+
+            // Delete or rename files and dirs
+            if ($hard) {
+                if (is_file($configYamlFile)) {
+                    unlink($configYamlFile);
+                }
+                if (is_dir(dirname($configYamlFile))) {
+                    rmdir(dirname($configYamlFile));
+                }
+                if (is_dir(dirname($uploadDir . 'images'))) {
+                    rmdir($uploadDir . 'images');
+                }
+                if (is_dir(dirname($uploadDir . 'Forms'))) {
+                    rmdir($uploadDir . 'Forms');
+                }
+                if (is_dir(dirname($uploadDir))) {
+                    rmdir($uploadDir);
+                }
+            } else {
+                $renamed = [];
+                foreach ($toRename as $initialPath => $newPath) {
+                    rename($initialPath, $newPath);
+                    $renamed[$initialPath] = $newPath;
+                }
+            }
+
+            // Try to commit the result (before any eventual file deletion or renaming)
+            $commitSuccess = $this->cnnPool->getConnectionByName('Default')->commit();
+            if (!$commitSuccess) {
+                throw new \RuntimeException('Something went wrong while commiting the result');
+            }
             return $rootUid;
 
         } catch(\Exception $e) {
             // rollback
-            foreach ($tables as $table) {
-                $this->cnnPool->getConnectionForTable($table)->rollback();
+            $this->cnnPool->getConnectionByName('Default')->rollback();
+            if ($hard) {
+                foreach ($renamed as $initialPath => $newPath) {
+                    rename($newPath, $initialPath);
+                }
+            }
+            if (file_exists($confirm_file)) {
+                unlink($confirm_file);
             }
+
             throw $e;
         }
     }
 
+    /**
+     * @param string $table
+     * @param string $whereKey
+     * @param $whereValue
+     * @param int $hard
+     */
+    private function delete(string $table, string $whereKey, $whereValue, $hard=0) {
+        $queryBuilder = $this->cnnPool->getQueryBuilderForTable($table);
+        if (!$hard) {
+            $queryBuilder
+                ->update($table)
+                ->set('deleted', 1)
+                ->where($queryBuilder->expr()->eq($whereKey, $whereValue))
+                ->execute();
+        } else {
+            $queryBuilder
+                ->delete($table)
+                ->where($queryBuilder->expr()->eq($whereKey, $whereValue))
+                ->execute();
+        }
+    }
+
+
+    public function undeleteSiteAction(int $organizationId) {
+        $rootUid = $this->findRootUidFor($organizationId);
+
+        // start transactions
+        $this->cnnPool->getConnectionByName('Default')->beginTransaction();
+
+        // keep track of renamed file for an eventual rollback
+        $renamed = [];
+
+        try {
+            $repository = new OtPageRepository();
+            $pages = $repository->getAllSubpagesForPage($rootUid);
+
+            foreach($pages as $page) {
+                $queryBuilder = $this->cnnPool->getQueryBuilderForTable('tt_content');
+                $queryBuilder
+                    ->update('tt_content')
+                    ->set('deleted', 0)
+                    ->where($queryBuilder->expr()->eq('pid', $page['uid']))
+                    ->execute();
+
+                $queryBuilder = $this->cnnPool->getQueryBuilderForTable('pages');
+                $queryBuilder
+                    ->update('pages')
+                    ->set('deleted', 0)
+                    ->where($queryBuilder->expr()->eq('uid', $page['uid']))
+                    ->execute();
+            }
+            $queryBuilder = $this->cnnPool->getQueryBuilderForTable('tt_content');
+            $queryBuilder
+                ->update('tt_content')
+                ->set('deleted', 0)
+                ->where($queryBuilder->expr()->eq('pid', $rootUid))
+                ->execute();
+
+            $queryBuilder = $this->cnnPool->getQueryBuilderForTable('pages');
+            $queryBuilder
+                ->update('pages')
+                ->set('deleted', 0)
+                ->where($queryBuilder->expr()->eq('uid', $rootUid))
+                ->execute();
+
+            $queryBuilder = $this->cnnPool->getQueryBuilderForTable('sys_template');
+            $queryBuilder
+                ->update('sys_template')
+                ->set('deleted', 0)
+                ->where($queryBuilder->expr()->eq('pid', $rootUid))
+                ->execute();
+
+            // remove filemounts
+            $queryBuilder = $this->cnnPool->getQueryBuilderForTable('sys_filemounts');
+            $queryBuilder
+                ->update('sys_filemounts')
+                ->set('deleted', 0)
+                ->where($queryBuilder->expr()->eq('path', "'/user_upload/" . $organizationId . "/images'"))
+                ->execute();
+
+            $queryBuilder
+                ->update('sys_filemounts')
+                ->set('deleted', 0)
+                ->where($queryBuilder->expr()->eq('path', "'/user_upload/" . $organizationId . "/Forms'"))
+                ->execute();
+
+            $queryBuilder = $this->cnnPool->getQueryBuilderForTable('be_users');
+            $queryBuilder
+                ->update('be_users')
+                ->set('deleted', 0)
+                ->where($queryBuilder->expr()->eq('db_mountpoints', $rootUid))
+                ->execute();
+
+            // Look up for the config.yaml file of the website
+            $configMainDir = $_ENV['TYPO3_PATH_APP'] . '/config/sites';
+            $configYamlFile = "";
+            foreach (glob($configMainDir . '/*',  GLOB_ONLYDIR) as $subdir) {
+                $yamlFile = $subdir . '/config.yaml.deleted';
+
+                if (is_file($yamlFile)) {
+                    $conf = Yaml::parseFile($yamlFile);
+
+                    if ($conf['rootPageId'] == $rootUid) {
+                        $configYamlFile = $yamlFile;
+                        break;
+                    }
+                }
+            }
+            $uploadDir = $_ENV['TYPO3_PATH_APP'] . '/public/fileadmin/user_upload/deleted_' . $organizationId . '/';
+
+            $toRename = [];
+            if (is_file($configYamlFile)) {
+                $toRename[$configYamlFile] = dirname($configYamlFile) . '/config.yaml';
+            }
+            if (is_dir($uploadDir)) {
+                $toRename[$uploadDir] = dirname($uploadDir) . '/' . $organizationId;
+            }
+
+            foreach ($toRename as $initialPath => $newPath) {
+                if (is_file($newPath)) {
+                    throw new \RuntimeException(
+                        'A file or directory named ' . $newPath . ' already exists, what happened?. Cancel.');
+                }
+            }
+
+            $renamed = [];
+            foreach ($toRename as $initialPath => $newPath) {
+                rename($initialPath, $newPath);
+                $renamed[$initialPath] = $newPath;
+            }
+
+            // Try to commit the result
+            $commitSuccess = $this->cnnPool->getConnectionByName('Default')->commit();
+            if (!$commitSuccess) {
+                throw new \RuntimeException('Something went wrong while commiting the result');
+            }
+            return $rootUid;
+
+        } catch(\Exception $e) {
+            // rollback
+            $this->cnnPool->getConnectionByName('Default')->rollback();
+            foreach ($renamed as $initialPath => $newPath) {
+                rename($newPath, $initialPath);
+            }
+            throw $e;
+        }
+    }
+
+    /**
+     * Retrieve the Organization object from the repository and then,
+     * from the Opentalent API
+     * @param $organizationId
+     * @return Organization
+     */
+    private function fetchOrganization($organizationId) {
+        $manager = GeneralUtility::makeInstance(ObjectManager::class);
+        $organizationRepository = GeneralUtility::makeInstance(
+            OrganizationRepository::class,
+            $manager
+        );
+        try {
+            return $organizationRepository->findById($organizationId);
+        } catch (ApiRequestException $e) {
+            throw new \RuntimeException('Unable to fetch the organization with id: ' . $organizationId);
+        }
+    }
+
+    /**
+     * Try to find the root page uid of the organization's website and return it.
+     *
+     * @param $organizationId
+     * @return int
+     */
+    private function findRootUidFor($organizationId) {
+        $queryBuilder = $this->cnnPool->getQueryBuilderForTable('pages');
+        $queryBuilder->getRestrictions()->removeAll();
+        $rootUid = $queryBuilder
+            ->select('uid')
+            ->from('pages')
+            ->where('is_siteroot=1')
+            ->andWhere($queryBuilder->expr()->eq('tx_opentalent_structure_id', $organizationId))
+            ->execute()
+            ->fetchColumn(0);
+
+        if ($rootUid > 0) {
+            return $rootUid;
+        }
+
+        $organization = $this->fetchOrganization($organizationId);
+        $queryBuilder = $this->cnnPool->getQueryBuilderForTable('pages');
+        $rootUid = $queryBuilder
+            ->count('uid')
+            ->from('pages')
+            ->where('is_siteroot=1')
+            ->andWhere($queryBuilder->expr()->eq('title', $queryBuilder->createNamedParameter($organization->getName())))
+            ->andWhere('tx_opentalent_structure_id=null')
+            ->execute()
+            ->fetchColumn(0);
+
+        if ($rootUid > 0) {
+            return $rootUid;
+        }
+        throw new \RuntimeException("The website of this organization can not be found");
+    }
+
+    /**
+     * Determine which folder-type Typo3 page should contain the new website
+     * CREATES IT if needed, and return its uid
+     *
+     * @return int
+     */
+    private function getParentFolderUid() {
+
+        $queryBuilder = $this->cnnPool->getQueryBuilderForTable('pages');
+        $siteCount = $queryBuilder
+            ->count('uid')
+            ->from('pages')
+            ->where('is_siteroot=1')
+            ->execute()
+            ->fetchColumn(0);
+
+        $thousand = (int)(($siteCount + 1) / 1000);
+
+        $folderName = "Web Sites " . (1000 * $thousand) . " - " . ((1000 * $thousand) + 999);
+
+        $queryBuilder = $this->cnnPool->getQueryBuilderForTable('pages');
+        $uid = $queryBuilder
+            ->select('uid')
+            ->from('pages')
+            ->where($queryBuilder->expr()->eq('title', $queryBuilder->createNamedParameter($folderName)))
+            ->andWhere('dokType=254')
+            ->execute()
+            ->fetchColumn(0);
+
+        if ($uid == null) {
+            $queryBuilder = $this->cnnPool->getQueryBuilderForTable('pages');
+            $queryBuilder->insert('pages')
+                ->values([
+                    'pid' => 0,
+                    'title' => $folderName,
+                    'dokType' => 254,
+                    'sorting' => 11264,
+                    'perms_userid' => 1,
+                    'perms_groupid' => 31,
+                    'perms_group' => 27,
+
+                ])
+                ->execute();
+            $uid = $queryBuilder->getConnection()->lastInsertId();
+        }
+        return $uid;
+    }
+
     /**
      * Insert a new row in the 'pages' table of the Typo3 DB
      *
@@ -486,7 +945,7 @@ class SiteController extends ActionController
     private function insertRootPage(Organization $organization) {
         return $this->insertPage(
             $organization,
-            self::DEFAULT_ROOT_PID,
+            $this->getParentFolderUid(),
             $organization->getName(),
             '/',
             self::TEMPLATE_HOME,
@@ -529,4 +988,245 @@ class SiteController extends ActionController
             ->execute();
     }
 
+    /**
+     * Return the content of `sys_template`.`constants` of
+     * the website of the given organization
+     *
+     * @param int $organizationId
+     * @return string
+     */
+    private function genTemplateConstants(int $organizationId) {
+        $rootUid = null;
+
+        $cnn = new PDO(
+            "mysql:host=prod-back;dbname=opentalent",
+            'dbcloner',
+            'wWZ4hYcrmHLW2mUK'
+        );
+        $cnn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+        $stmt = $cnn->prepare(
+            "SELECT o.id, o.name, o.facebook, o.twitter, 
+                                 o.category, o.logo_id, p.logoDonorsMove
+                          FROM opentalent.Organization o INNER JOIN opentalent.Parameters p 
+                          ON o.parameters_id = p.id
+                          WHERE o.id=" . $organizationId . ";"
+        );
+        $stmt->execute();
+        $stmt->setFetchMode(PDO::FETCH_ASSOC);
+        $org_data = $stmt->fetch();
+
+        $stmt = $cnn->prepare(
+            "SELECT c.email 
+                          FROM opentalent.ContactPoint c
+                            INNER JOIN opentalent.organization_contactpoint o 
+                            ON o.contactPoint_id = c.id
+                          WHERE c.contactType = 'PRINCIPAL' 
+                            AND o.organization_id = " . $organizationId . ";"
+        );
+        $stmt->execute();
+        $stmt->setFetchMode(PDO::FETCH_ASSOC);
+        $contact = $stmt->fetch();
+
+        $stmt = $cnn->prepare(
+            "SELECT n.name, n.logo, n.url 
+                          FROM opentalent.Network n
+                          INNER JOIN 
+                              (opentalent.NetworkOrganization l
+                              INNER JOIN opentalent.Organization o
+                              ON l.organization_id = o.id)
+                          ON l.network_id = n.id
+                          WHERE l.endDate is NULL
+                            AND o.id=" . $organizationId . ";"
+        );
+        $stmt->execute();
+        $stmt->setFetchMode(PDO::FETCH_ASSOC);
+        $network = $stmt->fetch();
+
+        return "plugin.tx_ottemplating {\n" .
+            "    settings {\n" .
+            "        organization {\n" .
+            "            id = " . $organizationId . "\n" .
+            "            name = " . $org_data['name'] . "\n" .
+            "            is_network = " . ($org_data['category'] == 'NETWORK' ? '1' : '0') . "\n" .
+            "            email = " . $contact['email'] . "\n" .
+            "            logoid = " . $org_data['logo_id'] . "\n" .
+            "            twitter = " . $org_data['twitter'] . "\n" .
+            "            facebook = " . $org_data['facebook'] . "\n" .
+            "        }\n" .
+            "    network {\n" .
+            "            logo = " . $network['logo'] . "\n" .
+            "            name = " . $network['name'] . "\n" .
+            "            url = " . $network['url'] . "\n" .
+            "        }\n" .
+            "    }\n" .
+            "}";
+    }
+
+    /**
+     * Create the given directory, give its property to the www-data group and
+     * record it as a newly created dir (for an eventual rollback)
+     *
+     * @param string $dirPath
+     */
+    private function mkDir(string $dirPath) {
+        mkdir($dirPath);
+        $this->createdDirs[] = $dirPath;
+        chgrp($dirPath, 'www-data');
+    }
+
+    /**
+     * Write the given file with content, give its property to the www-data group and
+     * record it as a newly created file (for an eventual rollback)
+     *
+     * @param string $path
+     * @param string $content
+     */
+    private function writeFile(string $path, string $content) {
+        $f = fopen($path, "w");
+        try
+        {
+            fwrite($f, $content);
+            $this->createdFiles[] = $path;
+            chgrp($path, 'www-data');
+        } finally {
+            fclose($f);
+        }
+    }
+
+    /**
+     * Write the .../sites/.../config.yml file of the given site
+     *
+     * @param int $organizationId
+     * @param int $rootUid
+     * @param string $domain
+     */
+    private function writeConfigFile(int $organizationId, int $rootUid, string $domain) {
+        $folder_id = explode('.', $domain)[0] . '_' . $organizationId;
+        $config_dir = $_ENV['TYPO3_PATH_APP'] . "/config/sites/" . $folder_id;
+
+        $config_filename = $config_dir . "/config.yaml";
+        if (file_exists($config_filename)) {
+            throw new \RuntimeException("A file " . $config_filename . " already exists. Abort.");
+        }
+
+        $config = ['base'=> 'https://' . $domain,
+            'baseVariants'=>[],
+            'errorHandling'=>[
+                ['errorCode'=>'404',
+                    'errorHandler'=>'Page',
+                    'errorContentSource'=>'t3://page?uid=' . $this->createdPagesIndex['/page-introuvable']],
+                ['errorCode'=>'403',
+                    'errorHandler'=>'Page',
+                    'errorContentSource'=>'t3://page?uid=' . $this->createdPagesIndex['/page-introuvable']]
+            ],
+            'flux_content_types'=>'',
+            'flux_page_templates'=>'',
+            'languages'=>[[
+                'title'=>'Fr',
+                'enabled'=>True,
+                'base'=>'/',
+                'typo3Language'=>'fr',
+                'locale'=>'fr_FR',
+                'iso-639-1'=>'fr',
+                'navigationTitle'=>'Fr',
+                'hreflang'=>'fr-FR',
+                'direction'=>'ltr',
+                'flag'=>'fr',
+                'languageId'=>'0',
+            ]],
+            'rootPageId'=>$rootUid,
+            'routes'=>[]
+        ];
+        $yamlConfig = Yaml::dump($config, 4);
+
+        if (!file_exists($config_dir)) {
+            $this->mkDir($config_dir);
+        }
+        $this->writeFile($config_filename, $yamlConfig);
+    }
+
+
+    /**
+     * Create the BE user for the website
+     * The user shall be already created in the Opentalent DB
+     *
+     * @param int $organizationId
+     * @param int $rootUid
+     * @param string $domain
+     * @return int
+     */
+    private function createBeUser(int $organizationId, int $rootUid, string $domain) {
+        $cnn = new PDO(
+            "mysql:host=prod-back;dbname=opentalent",
+            'dbcloner',
+            'wWZ4hYcrmHLW2mUK'
+        );
+        $cnn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+        $stmt = $cnn->prepare(
+            "SELECT p.username, a.id, s.product
+                      FROM opentalent.Person p 
+                        INNER JOIN Access a ON p.id = a.person_id
+                        INNER JOIN Settings s on a.organization_id = s.organization_id
+                      where a.organization_id=" . $organizationId . " AND  a.adminAccess=1;"
+        );
+        $stmt->execute();
+        $stmt->setFetchMode(PDO::FETCH_ASSOC);
+        $user_data = $stmt->fetch();
+
+        if (!$user_data) {
+            throw new \RuntimeException('Can not find any user with admin access in the Opentalent DB. Abort.');
+        }
+
+        // Since we don't want to store the password in the TYPO3 DB, we store a random string instead
+        $randomStr = (new Random)->generateRandomHexString(20);
+
+        // get the existing filemounts
+        $queryBuilder = $this->cnnPool->getQueryBuilderForTable('sys_filemounts');
+        $queryBuilder
+            ->select('uid')
+            ->from('sys_filemounts')
+            ->where("path LIKE '%user_upload/" . $organizationId . "/%'");
+        $statement = $queryBuilder->execute();
+        $rows = $statement->fetchAll(3) ?: [];
+        $files = [];
+        foreach ($rows as $row) {
+            $files[] = $row[0];
+        }
+
+        $values = [
+            'username' => $user_data['username'],
+            'password' => $randomStr,
+            'description' => '[ATTENTION: enregistrement auto-généré, ne pas modifier directement] BE Admin for ' . $domain . ' (id: ' . $user_data['id'] . ')',
+            'deleted' => 0,
+            'lang' => 'fr',
+            'usergroup' => isset(self::PRODUCT_MAPPING[$user_data['product']]) ? self::PRODUCT_MAPPING[$user_data['product']] : 1,
+            'db_mountpoints' => $rootUid,
+            'file_mountPoints' => join(',', $files),
+            'options' => 2,
+            'file_permissions' => 'readFolder,writeFolder,addFolder,renameFolder,moveFolder,deleteFolder,readFile,writeFile,addFile,renameFile,replaceFile,moveFile,copyFile,deleteFile',
+            'tx_opentalent_opentalentId' => $user_data['id'],
+            'tx_opentalent_organizationId' => $organizationId,
+            'tx_opentalent_generationDate' => date('Y/m/d H:i:s')
+        ];
+
+        $queryBuilder = $this->cnnPool->getQueryBuilderForTable('be_users');
+        $queryBuilder->insert('be_users')
+            ->values($values)
+            ->execute();
+
+        $beUid = $queryBuilder->getConnection()->lastInsertId();
+
+        // enable frontend editing
+        $BE_USER = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Authentication\\BackendUserAuthentication');
+        $user = $BE_USER->getRawUserByUid($beUid);
+        $BE_USER->user = $user;
+        $BE_USER->backendSetUC();
+        $BE_USER->uc['frontend_editing'] = 1;
+        $BE_USER->uc['frontend_editing_overlay'] = 1;
+        $BE_USER->writeUC($BE_USER->uc);
+
+        return $beUid;
+    }
 }

+ 9 - 3
ot_admin/Configuration/Commands.php

@@ -11,10 +11,16 @@
 
 return [
     'ot:site:create' => [
-        'class' => Opentalent\OtAdmin\Command\CreateOrganizationCommand::class
+        'class' => Opentalent\OtAdmin\Command\CreateSiteCommand::class
     ],
-    'ot:users:enfeedit' => [
-        'class' => Opentalent\OtAdmin\Command\EnableFeEditingCommand::class
+    'ot:site:delete' => [
+        'class' => Opentalent\OtAdmin\Command\DeleteSiteCommand::class
+    ],
+    'ot:site:undelete' => [
+        'class' => Opentalent\OtAdmin\Command\UndeleteSiteCommand::class
+    ],
+    'ot:site:update' => [
+        'class' => Opentalent\OtAdmin\Command\UpdateSiteCommand::class
     ]
 ];
 

+ 9 - 3
ot_admin/Configuration/Services.yaml

@@ -13,8 +13,14 @@ services:
         command: 'ot:site:create'
         schedulable: false
 
-  Opentalent\OtAdmin\Command\EnableFeEditingCommand:
+  Opentalent\OtAdmin\Command\DeleteOrganizationCommand:
     tags:
-      - name: 'ot:users:enfeedit'
-        command: 'ot:users:enfeedit'
+      - name: 'ot:site:delete'
+        command: 'ot:site:delete'
+        schedulable: false
+
+  Opentalent\OtAdmin\Command\UndeleteOrganizationCommand:
+    tags:
+      - name: 'ot:site:undelete'
+        command: 'ot:site:undelete'
         schedulable: false