Bläddra i källkod

Merge branch 'release/1.1.0'

Olivier Massot 10 månader sedan
förälder
incheckning
76270e8f9a

+ 2 - 1
composer.json

@@ -40,7 +40,8 @@
         "opentalent/ot_optimizer": "^1.0",
         "opentalent/ot_connect": "^1.0",
         "typo3/cms-linkvalidator": "^12.4",
-        "typo3/cms-t3editor": "^12.4"
+        "typo3/cms-t3editor": "^12.4",
+        "phpstan/phpdoc-parser": "^1.0"
     },
     "repositories": [
         {

+ 10 - 10
doc/be_users.md

@@ -44,10 +44,10 @@ Ce paragraphe présente les droits des be_users sur les pages et contenus du sit
 
 | Page                                               | Show               | Edit content       | Edit page          | Delete             | New                | Code |
 |----------------------------------------------------|--------------------|--------------------|--------------------|--------------------|--------------------|------|
-| Home                                               | ![v](images/v.svg) | ![v](images/v.svg) | ![x](images/x.svg) | ![x](images/x.svg) | ![x](images/x.svg) | *17* |
-| footer/* (mentions légales, contact, plan du site) | ![v](images/v.svg) | ![x](images/x.svg) | ![x](images/x.svg) | ![x](images/x.svg) | ![x](images/x.svg) | *1*  |
-| Menu Présentation (*)                              | ![v](images/v.svg) | ![x](images/x.svg) | ![x](images/x.svg) | ![x](images/x.svg) | ![x](images/x.svg) | *1*  |
-| Autres pages                                       | ![v](images/v.svg) | ![v](images/v.svg) | ![v](images/v.svg) | ![x](images/x.svg) | ![v](images/v.svg) | *27* |
+| Home                                               | ![v](images/v.svg) | ![v](images/v.svg) | ![v](images/v.svg) | ![x](images/x.svg) | ![v](images/v.svg) | *27* |
+| footer/* (mentions légales, contact, plan du site) | ![v](images/v.svg) | ![x](images/x.svg) | ![x](images/x.svg) | ![x](images/x.svg) | ![v](images/v.svg) | *9*  |
+| Menu Présentation (*)                              | ![v](images/v.svg) | ![x](images/x.svg) | ![v](images/v.svg) | ![x](images/x.svg) | ![v](images/v.svg) | *11* |
+| Autres pages                                       | ![v](images/v.svg) | ![v](images/v.svg) | ![v](images/v.svg) | ![v](images/v.svg) | ![v](images/v.svg) | *31* |
 
 ### Licence Standard
 
@@ -64,10 +64,10 @@ Ce paragraphe présente les droits des be_users sur les pages et contenus du sit
 
 | Page                                               | Show               | Edit content       | Edit page          | Delete             | New                | Code |
 |----------------------------------------------------|--------------------|--------------------|--------------------|--------------------|--------------------|------|
-| Home                                               | ![v](images/v.svg) | ![v](images/v.svg) | ![x](images/x.svg) | ![x](images/x.svg) | ![x](images/x.svg) | *17* |
+| Home                                               | ![v](images/v.svg) | ![v](images/v.svg) | ![v](images/v.svg) | ![x](images/x.svg) | ![x](images/x.svg) | *19* |
 | footer/* (mentions légales, contact, plan du site) | ![v](images/v.svg) | ![x](images/x.svg) | ![x](images/x.svg) | ![x](images/x.svg) | ![x](images/x.svg) | *1*  |
 | Menu Présentation (*)                              | ![v](images/v.svg) | ![x](images/x.svg) | ![x](images/x.svg) | ![x](images/x.svg) | ![x](images/x.svg) | *1*  |
-| Autres pages                                       | ![v](images/v.svg) | ![v](images/v.svg) | ![v](images/v.svg) | ![x](images/x.svg) | ![x](images/x.svg) | *19* |
+| Autres pages                                       | ![v](images/v.svg) | ![v](images/v.svg) | ![v](images/v.svg) | ![x](images/x.svg) | ![x](images/x.svg) | *27* |
 
 
 ## Accès aux menus
@@ -77,15 +77,15 @@ Ce paragraphe présente les menus du backend disponibles selon le type de compte
 | Page                                           | Admin Premium      | Editor Premium     |   | Admin Standard     | Editor Standard    |
 |------------------------------------------------|--------------------|--------------------|---|--------------------|--------------------|
 | Web - Page                                     | ![v](images/v.svg) | ![v](images/v.svg) |   | ![v](images/v.svg) | ![v](images/v.svg) |
-| Web - Liste                                    | ![v](images/v.svg) | ![x](images/x.svg) |   | ![x](images/x.svg) | ![x](images/x.svg) |
+| Web - Liste                                    | ![v](images/v.svg) | ![v](images/v.svg) |   | ![v](images/v.svg) | ![v](images/v.svg) |
 | Web - Formulaires                              | ![v](images/v.svg) | ![v](images/v.svg) |   | ![v](images/v.svg) | ![v](images/v.svg) |
 | Web - Corbeille                                | ![x](images/x.svg) | ![x](images/x.svg) |   | ![x](images/x.svg) | ![x](images/x.svg) |
 | Web - Info                                     | ![x](images/x.svg) | ![x](images/x.svg) |   | ![x](images/x.svg) | ![x](images/x.svg) |
-| Web - Personnaliser                            | ![v](images/v.svg) | ![x](images/x.svg) |   | ![v](images/v.svg) | ![x](images/x.svg) |
-| Web - Statistiques                             | ![v](images/v.svg) | ![x](images/x.svg) |   | ![v](images/v.svg) | ![x](images/x.svg) |
+| Web - Personnaliser                            | ![v](images/v.svg) | ![v](images/v.svg) |   | ![v](images/v.svg) | ![v](images/v.svg) |
+| Web - Statistiques                             | ![v](images/v.svg) | ![v](images/v.svg) |   | ![v](images/v.svg) | ![v](images/v.svg) |
 | Web - Gestion des actualités                   | ![v](images/v.svg) | ![v](images/v.svg) |   | ![v](images/v.svg) | ![v](images/v.svg) |
 | Gestionnaire de site - Redirects               | ![x](images/x.svg) | ![x](images/x.svg) |   | ![x](images/x.svg) | ![x](images/x.svg) |
-| Fichier - Fichiers                             | ![v](images/v.svg) | ![x](images/x.svg) |   | ![v](images/v.svg) | ![x](images/x.svg) |
+| Fichier - Fichiers                             | ![v](images/v.svg) | ![v](images/v.svg) |   | ![v](images/v.svg) | ![v](images/v.svg) |
 | Outils Utilisateur - Configuration utilisateur | ![x](images/x.svg) | ![x](images/x.svg) |   | ![x](images/x.svg) | ![x](images/x.svg) |
 | Aide - A propos de...                          | ![x](images/x.svg) | ![x](images/x.svg) |   | ![x](images/x.svg) | ![x](images/x.svg) |
 | Aide -Manuel TYPO3                             | ![x](images/x.svg) | ![x](images/x.svg) |   | ![x](images/x.svg) | ![x](images/x.svg) |

+ 3 - 1
ot_admin/Classes/Command/ClearSiteCacheCommand.php

@@ -25,7 +25,9 @@ class ClearSiteCacheCommand extends Command
 {
     public function __construct(
         private readonly SiteController $siteController
-    ) {}
+    ) {
+        parent::__construct();
+    }
 
     /**
      * -- This method is expected by Typo3, do not rename ou remove --

+ 79 - 0
ot_admin/Classes/Command/DeleteUserCreatedPagesCommand.php

@@ -0,0 +1,79 @@
+<?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;
+
+/**
+ * This CLI command provoke the definitive deletion of all the pages
+ * created by the users, leaving only the starting mandatory pages.
+ *
+ * /!\ Warning: this is a destructive operation
+ *
+ * @package Opentalent\OtAdmin\Command
+ */
+class DeleteUserCreatedPagesCommand extends Command
+{
+    public function __construct(
+        private readonly SiteController $siteController
+    ) {
+        parent::__construct();
+    }
+
+    /**
+     * -- 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(): void
+    {
+        $this
+            ->setName("ot:site:delete-user-created-pages")
+            ->setDescription("Delete all the pages created by the users, leaving only the starting mandatory pages.")
+            ->setHelp("This CLI command provoke the definitive deletion of all the pages 
+                            created by the users, leaving only the starting mandatory pages. 
+                            /!\ Warning: this is a destructive operation.")
+            ->addArgument(
+                'organization-id',
+                InputArgument::OPTIONAL,
+                "The organization's id in the opentalent DB"
+            );
+    }
+
+    /**
+     * -- This method is expected by Typo3, do not rename ou remove --
+     *
+     * @param InputInterface $input
+     * @param OutputInterface $output
+     * @return int
+     * @throws \Exception
+     */
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $org_id = $input->getArgument('organization-id');
+
+        $io = new SymfonyStyle($input, $output);
+
+        if (
+            !$io->confirm("Are you sure you want to delete all the pages created " .
+                "by the users? (organization id: " . $org_id . ")")
+        ) {
+            $io->error("Aborting.");
+            return 1;
+        }
+
+        $rootUid = $this->siteController->deleteUserCreatedPagesAction($org_id);
+
+        $io->success(sprintf("The website with root uid " . $rootUid . " had its user-created pages deleted."));
+
+        return 0;
+    }
+
+}

+ 1 - 3
ot_admin/Classes/Controller/ScanController.php

@@ -17,9 +17,7 @@ class ScanController extends ActionController
     public function __construct(
         private readonly SiteController $siteController,
         private readonly OrganizationRepository $organizationRepository
-    ) {
-        parent::__construct();
-    }
+    ) {}
 
     /**
      * Perform a full scan of the Typo3 DB, and confront it to the

+ 82 - 31
ot_admin/Classes/Controller/SiteController.php

@@ -5,6 +5,7 @@ namespace Opentalent\OtAdmin\Controller;
 use Doctrine\DBAL\Driver\Exception;
 use Opentalent\OtAdmin\Domain\Entity\SiteInfos;
 use Opentalent\OtAdmin\Domain\Entity\SiteStatus;
+use Opentalent\OtAdmin\Enum\OtPageTypeEnum;
 use Opentalent\OtCore\Exception\InvalidWebsiteConfigurationException;
 use Opentalent\OtCore\Exception\NoSuchOrganizationException;
 use Opentalent\OtCore\Exception\NoSuchRecordException;
@@ -19,6 +20,7 @@ use Opentalent\OtCore\Utility\FileUtility;
 use PDO;
 use RuntimeException;
 use Symfony\Component\Yaml\Yaml;
+use Throwable;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException;
 use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
@@ -37,7 +39,7 @@ class SiteController extends ActionController
     // Templates names
     private const TEMPLATE_HOME = "OpenTalent.OtTemplating->home";
     private const TEMPLATE_1COL = "OpenTalent.OtTemplating->1Col";
-    private const TEMPLATE_3COL = "OpenTalent.OtTemplating->home";
+    private const TEMPLATE_3COL = "OpenTalent.OtTemplating->3Col";
     private const TEMPLATE_EVENTS = "OpenTalent.OtTemplating->events";
     private const TEMPLATE_STRUCTURESEVENTS = "OpenTalent.OtTemplating->structuresEvents";
     private const TEMPLATE_STRUCTURES = "OpenTalent.OtTemplating->structures";
@@ -290,6 +292,7 @@ class SiteController extends ActionController
                 $rootUid,
                 'Accueil',
                 '/accueil',
+                OtPageTypeEnum::MANDATORY_EDITABLE,
                 '',
                 [
                     'dokType' => self::DOK_SHORTCUT,
@@ -302,7 +305,8 @@ class SiteController extends ActionController
                 $websiteUid,
                 $rootUid,
                 'Présentation',
-                '/presentation'
+                '/presentation',
+                OtPageTypeEnum::MANDATORY_EDITABLE
             );
 
             // > 'Présentation > Qui sommes-nous ?' page (hidden by default)
@@ -311,6 +315,7 @@ class SiteController extends ActionController
                 $this->createdPagesIndex['/presentation'],
                 'Qui sommes nous?',
                 '/qui-sommes-nous',
+                OtPageTypeEnum::MANDATORY_EDITABLE,
                 '',
                 ['hidden' => 1]
             );
@@ -321,6 +326,7 @@ class SiteController extends ActionController
                 $this->createdPagesIndex['/presentation'],
                 'Les adhérents',
                 '/les-adherents',
+                OtPageTypeEnum::MANDATORY_EDITABLE,
                 self::TEMPLATE_MEMBERS
             );
 
@@ -330,6 +336,7 @@ class SiteController extends ActionController
                 $this->createdPagesIndex['/presentation'],
                 'Les membres du CA',
                 '/les-membres-du-ca',
+                OtPageTypeEnum::MANDATORY_EDITABLE,
                 self::TEMPLATE_MEMBERSCA
             );
 
@@ -339,6 +346,7 @@ class SiteController extends ActionController
                 $this->createdPagesIndex['/presentation'],
                 'Informations Pratiques',
                 '/informations-pratiques',
+                OtPageTypeEnum::MANDATORY_EDITABLE,
                 self::TEMPLATE_STRUCTUREDETAILS
             );
 
@@ -349,6 +357,7 @@ class SiteController extends ActionController
                     $this->createdPagesIndex['/presentation'],
                     'Les sociétés adhérentes',
                     '/societes-adherentes',
+                    OtPageTypeEnum::MANDATORY_EDITABLE,
                     self::TEMPLATE_STRUCTURES
                 );
             }
@@ -359,6 +368,7 @@ class SiteController extends ActionController
                 $this->createdPagesIndex['/presentation'],
                 'Historique',
                 '/historique',
+                OtPageTypeEnum::MANDATORY_EDITABLE,
                 '',
                 ['hidden' => 1]
             );
@@ -371,6 +381,7 @@ class SiteController extends ActionController
                 $rootUid,
                 'Actualités',
                 '/actualites',
+                OtPageTypeEnum::MANDATORY_EDITABLE,
                 self::TEMPLATE_NEWS,
                 ['hidden' => 1]
             );
@@ -389,16 +400,18 @@ class SiteController extends ActionController
                 $this->createdPagesIndex['/saison-en-cours'],
                 'Les évènements',
                 '/les-evenements',
+                OtPageTypeEnum::MANDATORY_EDITABLE,
                 self::TEMPLATE_EVENTS
             );
 
             if ($isNetwork) {
-                // > 'Présentation > Les sociétés adhérentes' page
+                // > 'Présentation > Evènements des structures' page
                 $this->insertPage(
                     $websiteUid,
                     $this->createdPagesIndex['/presentation'],
                     'Évènements des structures',
                     '/evenements-des-structures',
+                    OtPageTypeEnum::MANDATORY_EDITABLE,
                     self::TEMPLATE_STRUCTURESEVENTS
                 );
             }
@@ -409,6 +422,7 @@ class SiteController extends ActionController
                 $rootUid,
                 'Vie interne',
                 '/vie-interne',
+                OtPageTypeEnum::NON_MANDATORY,
                 '',
                 [
                     'hidden' => 1,
@@ -422,6 +436,7 @@ class SiteController extends ActionController
                 $rootUid,
                 'Footer',
                 '/footer',
+                OtPageTypeEnum::MANDATORY_NON_EDITABLE,
                 '',
                 [
                     'dokType' => self::DOK_FOLDER,
@@ -435,6 +450,7 @@ class SiteController extends ActionController
                 $this->createdPagesIndex['/footer'],
                 'Contact',
                 '/contact',
+                OtPageTypeEnum::MANDATORY_NON_EDITABLE,
                 self::TEMPLATE_CONTACT
             );
 
@@ -443,7 +459,8 @@ class SiteController extends ActionController
                 $websiteUid,
                 $this->createdPagesIndex['/footer'],
                 'Plan du site',
-                '/plan-du-site'
+                '/plan-du-site',
+                OtPageTypeEnum::MANDATORY_NON_EDITABLE
             );
 
             // > 'Footer > Mentions légales' page
@@ -452,6 +469,7 @@ class SiteController extends ActionController
                 $this->createdPagesIndex['/footer'],
                 'Mentions légales',
                 '/mentions-legales',
+                OtPageTypeEnum::MANDATORY_NON_EDITABLE,
                 self::TEMPLATE_LEGAL
             );
 
@@ -461,6 +479,7 @@ class SiteController extends ActionController
                 $this->createdPagesIndex['/presentation'],
                 'Contact',
                 '/ecrivez-nous',
+                OtPageTypeEnum::MANDATORY_EDITABLE,
                 '',
                 [
                     'dokType' => self::DOK_SHORTCUT,
@@ -474,6 +493,7 @@ class SiteController extends ActionController
                 $rootUid,
                 'Page introuvable',
                 '/page-introuvable',
+                OtPageTypeEnum::NON_MANDATORY,
                 '',
                 [
                     'nav_hide' => 1,
@@ -1723,42 +1743,33 @@ class SiteController extends ActionController
         $pages = $this->otPageRepository->getPageWithSubpages($rootUid);
 
         // To understand how the rights levels are computed:
-        // @see https://ressources.opentalent.fr/display/EX/Droits+des+BE+Users
+        // @see https://gitlab.2iopenservice.com/opentalent/ot_typo3/-/blob/master/doc/be_users.md?ref_type=heads
         foreach ($pages as $page) {
 
-            if ($page['is_siteroot']) {
+            if ($page['ot_page_type'] === OtPageTypeEnum::ROOT) {
 
                 $adminPerms = self::PERM_SHOW + self::PERM_EDIT_CONTENT + self::PERM_EDIT_PAGE;
+                $editorsPerms = self::PERM_SHOW + self::PERM_EDIT_CONTENT + self::PERM_EDIT_PAGE;
                 if ($isPremium)  {
                     $adminPerms += self::PERM_NEW;
+                    $editorsPerms += self::PERM_NEW;
                 }
-                $editorsPerms = self::PERM_SHOW + self::PERM_EDIT_CONTENT;
-
-            } else if (
-                $page['slug'] === '/footer' ||
-                $page['tx_fed_page_controller_action'] === 'OpenTalent.OtTemplating->legal' ||
-                $page['tx_fed_page_controller_action'] === 'OpenTalent.OtTemplating->contact' ||
-                $page['slug'] === '/plan-du-site'
-            ) {
+
+            } else if ($page['ot_page_type'] === OtPageTypeEnum::MANDATORY_NON_EDITABLE) {
                 $adminPerms = self::PERM_SHOW;
+                $editorsPerms = self::PERM_SHOW;
                 if ($isPremium)  {
                     $adminPerms += self::PERM_NEW;
+                    $editorsPerms += self::PERM_NEW;
                 }
-                $editorsPerms = self::PERM_SHOW;
 
-            } else if (
-                $page['tx_fed_page_controller_action'] === 'OpenTalent.OtTemplating->members' ||
-                $page['tx_fed_page_controller_action'] === 'OpenTalent.OtTemplating->membersCa' ||
-                $page['tx_fed_page_controller_action'] === 'OpenTalent.OtTemplating->structures' ||
-                $page['tx_fed_page_controller_action'] === 'OpenTalent.OtTemplating->events' ||
-                $page['tx_fed_page_controller_action'] === 'OpenTalent.OtTemplating->structuresEvents' ||
-                $page['tx_fed_page_controller_action'] === 'OpenTalent.OtTemplating->structureDetails'
-            ) {
+            } else if ($page['ot_page_type'] === OtPageTypeEnum::MANDATORY_EDITABLE) {
                 $adminPerms = self::PERM_SHOW;
+                $editorsPerms = self::PERM_SHOW;
                 if ($isPremium)  {
                     $adminPerms += self::PERM_NEW + self::PERM_EDIT_PAGE;
+                    $editorsPerms += self::PERM_NEW + self::PERM_EDIT_PAGE;
                 }
-                $editorsPerms = self::PERM_SHOW;
 
             } else {
                 $adminPerms = self::PERM_SHOW + self::PERM_EDIT_CONTENT + self::PERM_EDIT_PAGE;
@@ -1768,7 +1779,7 @@ class SiteController extends ActionController
 
                 $editorsPerms = self::PERM_SHOW + self::PERM_EDIT_CONTENT + self::PERM_EDIT_PAGE;
                 if ($isPremium)  {
-                    $editorsPerms += self::PERM_NEW;
+                    $editorsPerms += self::PERM_DELETE + self::PERM_NEW;
                 }
             }
 
@@ -1817,6 +1828,42 @@ class SiteController extends ActionController
         }
     }
 
+    /**
+     * Delete all the pages created by the users, leaving only the starting mandatory pages.
+     *
+     * @param int $organizationId
+     * @return int
+     * @throws NoSuchRecordException
+     * @throws NoSuchWebsiteException
+     * @throws \Doctrine\DBAL\Exception
+     * @throws Throwable
+     */
+    public function deleteUserCreatedPagesAction(int $organizationId): int
+    {
+        $website = $this->otWebsiteRepository->getWebsiteByOrganizationId($organizationId);
+        $rootUid = $this->otWebsiteRepository->getWebsiteRootUid($website['uid']);
+
+        $pages = $this->otPageRepository->getPageWithSubpages($rootUid);
+
+        $this->connectionPool->getConnectionByName('Default')->beginTransaction();
+
+        try {
+            foreach($pages as $page) {
+                if ($page['ot_page_type'] === OtPageTypeEnum::USER_CREATED->value) {
+                    $this->delete('tt_content', 'pid', $page['uid'], true);
+                    $this->delete('pages', 'uid', $page['uid'], true);
+                }
+            }
+
+            $this->connectionPool->getConnectionByName('Default')->commit();
+        } catch (\Throwable $e) {
+            $this->connectionPool->getConnectionByName('Default')->rollBack();
+            throw $e;
+        }
+
+        return $rootUid;
+    }
+
     /**
      * Delete then regenerate all the typo3 sites yaml config files
      *
@@ -1995,16 +2042,18 @@ class SiteController extends ActionController
      * @param int $pid
      * @param string $title
      * @param string $slug
+     * @param OtPageTypeEnum $otPageType
      * @param string $template
      * @param array $moreValues
      * @return int
      */
-    private function insertPage(int $website_uid,
-                                int $pid,
-                                string $title,
-                                string $slug,
-                                string $template = '',
-                                array $moreValues = []
+    private function insertPage(int            $website_uid,
+                                int            $pid,
+                                string         $title,
+                                string         $slug,
+                                OtPageTypeEnum $otPageType = OtPageTypeEnum::MANDATORY_EDITABLE,
+                                string         $template = '',
+                                array          $moreValues = []
                                 ): int
     {
         $defaultValues = [
@@ -2017,6 +2066,7 @@ class SiteController extends ActionController
             'backend_layout' => 'flux__grid',
             'backend_layout_next_level' => 'flux__grid',
             'ot_website_uid' => $website_uid,
+            'ot_page_type' => $otPageType->value
         ];
 
         if ($template) {
@@ -2052,6 +2102,7 @@ class SiteController extends ActionController
             $this->getParentFolderUid(),
             $title,
             '/',
+            OtPageTypeEnum::ROOT,
             self::TEMPLATE_HOME,
             [
                 'is_siteroot' => 1,

+ 13 - 0
ot_admin/Classes/Enum/OtPageTypeEnum.php

@@ -0,0 +1,13 @@
+<?php
+declare(strict_types=1);
+
+namespace Opentalent\OtAdmin\Enum;
+
+enum OtPageTypeEnum: string
+{
+    case ROOT = 'ROOT';
+    case MANDATORY_EDITABLE = 'MANDATORY_EDITABLE';
+    case MANDATORY_NON_EDITABLE = 'MANDATORY_NON_EDITABLE';
+    case NON_MANDATORY = 'NON_MANDATORY';
+    case USER_CREATED = 'USER_CREATED';
+}

+ 96 - 2
ot_admin/Classes/Http/ApiController.php

@@ -12,6 +12,7 @@ use Opentalent\OtCore\Exception\NoSuchRecordException;
 use Opentalent\OtCore\Exception\NoSuchWebsiteException;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
+use Psr\Log\LoggerInterface;
 use TYPO3\CMS\Core\Http\JsonResponse;
 use TYPO3\CMS\Core\Http\ServerRequest;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -25,6 +26,11 @@ class ApiController implements LoggerAwareInterface
 {
     use LoggerAwareTrait;
 
+    const PROD_FRONT_IP = "172.16.0.68";
+    const PROD_V2_IP = "172.16.0.35";
+    const PUBLIC_PRODFRONT_IP = "141.94.117.38";
+    const PUBLIC_PROD_V2_IP = "141.94.117.35";
+
     const array ALLOWED_IPS = [
         '/^127\.0\.0\.[0-1]$/', // Localhost
         '/^localhost$/',  // Localhost
@@ -74,6 +80,46 @@ class ApiController implements LoggerAwareInterface
         return true;
     }
 
+    /**
+     * Lève une erreur si l'environnement est la prod et que la requête provient d'un autre environnement, car
+     * cette requête a probablement été envoyée à la prod par erreur.
+     *
+     * Permet de sécuriser certaines opérations destructives, comme la suppression d'organisation.
+     *
+     * @return void
+     */
+    private function preventIfIsDubious(): void
+    {
+        if (
+            $_SERVER &&
+            (
+                ($_SERVER['SERVER_ADDR'] === self::PROD_FRONT_IP && $_SERVER['REMOTE_ADDR'] !== self::PROD_V2_IP) ||
+                ($_SERVER['SERVER_ADDR'] === self::PUBLIC_PRODFRONT_IP && $_SERVER['REMOTE_ADDR'] !== self::PUBLIC_PROD_V2_IP)
+            )
+        ) {
+            throw new \RuntimeException("Invalid client ip");
+        }
+    }
+
+    /**
+     * Lève une erreur si le token de confirmation n'a pas était ajouté, ou si sa valeur est invalide.
+     *
+     * Permet de sécuriser certaines opérations destructives, comme la suppression d'organisation.
+     *
+     * @param int $organizationId
+     * @return void
+     */
+    private function preventOnMissingConfirmationToken(int $organizationId): void
+    {
+        $headers = getallheaders();
+        if (
+            !isset($headers['Confirmation-Token']) ||
+            $headers['Confirmation-Token'] !== 'DEL-'.$organizationId.'-'.date('Ymd')
+        ) {
+            throw new \RuntimeException("Missing or invalid confirmation token");
+        }
+    }
+
     /**
      * Retrieve the organization's id from the given request parameters
      *
@@ -218,6 +264,12 @@ class ApiController implements LoggerAwareInterface
      *
      * Proceeds to a soft-deletion of the organization's website
      *
+     * In the case of a hard deletion, a special header is requested as a confirmation token. The header
+     * shall be named 'Confirmation-Token' and its value shall be DEL-XXXX-YYYYMMDD, where XXXX is the id of
+     * the organization owning the website, and YYYYMMDD is the date of the current day.
+     *
+     * /!\ Warning: this is a destructive operation
+     *
      * @param ServerRequest $request
      * @return JsonResponse
      * @throws \Exception
@@ -229,17 +281,27 @@ class ApiController implements LoggerAwareInterface
         $organizationId = $this->getOrganizationId($request);
 
         $params = $request->getQueryParams();
+        $hard = (isset($params['hard']) && $params['hard']);
+
+        if ($hard) {
+            $this->preventIfIsDubious();
+            $this->preventOnMissingConfirmationToken($organizationId);
+        }
 
-        $rootUid = $this->siteController->deleteSiteAction($organizationId);
+        $rootUid = $this->siteController->deleteSiteAction($organizationId, $hard, true, true);
 
         $this->logger->info(sprintf(
             "OtAdmin API: The website with root uid " . $rootUid . " has been soft-deleted " .
             " (organization: " . $organizationId . ")"));
 
+        $msg = $hard ?
+            "The website with root uid " . $rootUid . " has been hard-deleted." :
+            "The website with root uid " . $rootUid . " has been soft-deleted. Use the /site/undelete route to restore it.";
+
         return new JsonResponse(
             [
                 'organization_id' => $organizationId,
-                'msg' => "The website with root uid " . $rootUid . " has been soft-deleted. Use the /site/undelete route to restore it.",
+                'msg' => $msg,
                 'root_uid' => $rootUid
             ]
         );
@@ -418,4 +480,36 @@ class ApiController implements LoggerAwareInterface
 
         return new JsonResponse($results);
     }
+
+    /**
+     * -- Target of the route 'delete-user-created-pages' --
+     * >> Requires a query param named 'organization-id' (int)
+     *
+     * Delete all user-created pages for the organization's website
+     *
+     * /!\ Warning: this is a destructive operation
+     *
+     * @param ServerRequest $request
+     * @return JsonResponse
+     * @throws \Exception
+     */
+    public function deleteUserCreatedPagesAction(ServerRequest $request): JsonResponse
+    {
+        $this->assertIpAllowed();
+
+        $organizationId = $this->getOrganizationId($request);
+
+        $this->preventIfIsDubious();
+        $this->preventOnMissingConfirmationToken($organizationId);
+
+        $rootUid = $this->siteController->deleteUserCreatedPagesAction($organizationId);
+
+        return new JsonResponse(
+            [
+                'organization_id' => $organizationId,
+                'msg' => "The website with root uid " . $rootUid . " had its user-created pages deleted.",
+                'root_uid' => $rootUid
+            ]
+        );
+    }
 }

+ 5 - 0
ot_admin/Configuration/Backend/Routes.php

@@ -56,5 +56,10 @@ return [
         'path' => '/otadmin/scan',
         'target' => ApiController::class . '::scanAllAction',
         'access' => 'public'
+    ],
+    'delete-user-created-pages' => [
+        'path' => '/otadmin/site/delete-user-created-pages',
+        'target' => ApiController::class . '::deleteUserCreatedPagesAction',
+        'access' => 'public'
     ]
 ];

+ 6 - 0
ot_admin/Configuration/Services.yaml

@@ -72,3 +72,9 @@ services:
       - name: console.command
         command: 'ot:site:scan'
         schedulable: true
+
+  Opentalent\OtAdmin\Command\DeleteUserCreatedPagesCommand:
+    tags:
+      - name: console.command
+        command: 'ot:site:delete-user-created-pages'
+        schedulable: false

+ 3 - 1
ot_admin/ext_tables.sql

@@ -2,9 +2,11 @@
 
 #
 # Table structure for table 'pages'
+# Possible values for ot_page_type are : ROOT, MANDATORY_EDITABLE, MANDATORY_NON_EDITABLE, NON_MANDATORY, USER_CREATED
 #
 CREATE TABLE pages (
-   manually_deleted smallint(5) unsigned NOT NULL DEFAULT 0
+    manually_deleted smallint(5) unsigned NOT NULL DEFAULT 0,
+    ot_page_type text DEFAULT 'USER_CREATED'
 );
 
 #

+ 71 - 27
ot_connect/Classes/Service/OtAuthenticationService.php

@@ -31,6 +31,8 @@ class OtAuthenticationService extends AbstractAuthenticationService
     CONST ISAUTH_URI = 'api/user/isauthenticated';
     CONST LOGOUT_URI = 'logout';
     CONST GROUP_FE_ALL_UID = 18076;
+    CONST GROUP_ADMIN_STANDARD_UID = 1;
+    CONST GROUP_ADMIN_PREMIUM_UID = 3;
 
     // Cookies' domain needs to be the same that the api's cookies, or guzzle will ignore them.
     CONST COOKIE_DOMAIN = 'opentalent.fr';
@@ -118,7 +120,9 @@ class OtAuthenticationService extends AbstractAuthenticationService
         // Does the user already have a session on the Opentalent API?
         $username = $this->getAuthenticatedUsername();
 
-        if ($username != null && $this->authInfo['loginType'] === 'FE' && $this->login['status'] === 'logout') {
+        $isBackend = $this->authInfo['loginType'] === 'BE';
+
+        if ($username != null && !$isBackend && $this->login['status'] === 'logout') {
             // This is a logout request
             $this->logout();
             return false;
@@ -154,8 +158,10 @@ class OtAuthenticationService extends AbstractAuthenticationService
         // Request the latest data for the user and write it in the Typo3 DB
         //   * The shouldUserBeUpdated() method checks if the user was already
         //   generated in the last minutes, to avoid unnecessary operations *
-        if ($this->shouldUserBeUpdated($username)) {
-            $wasUpdated = $this->createOrUpdateUser();
+
+        if ($this->shouldUserBeUpdated($username, $isBackend)) {
+            $wasUpdated = $this->createOrUpdateUser($isBackend);
+
             if (!$wasUpdated) {
                 // An error happened during the update of the user's data
                 // since its data may have changed (credentials, rights, rôles...)
@@ -293,12 +299,12 @@ class OtAuthenticationService extends AbstractAuthenticationService
      * @param string $username
      * @return bool
      */
-    protected function shouldUserBeUpdated(string $username): bool
+    protected function shouldUserBeUpdated(string $username, bool $isBackend = false): bool
     {
-
-        $cnn = $this->connectionPool->getConnectionForTable('fe_users');
-        $q = $cnn->select(['tx_opentalent_generationDate'], 'fe_users', ['username' => $username]);
-        $strGenDate = $q->fetch(3)[0];
+        $table = $isBackend ? 'be_users' : 'fe_users';
+        $cnn = $this->connectionPool->getConnectionForTable($table);
+        $q = $cnn->select(['tx_opentalent_generationDate'], $table, ['username' => $username]);
+        $strGenDate = $q->fetch(3)[0] ?? '1970-01-01 00:00:00';
 
         $genDate = DateTime::createFromFormat("Y-m-d H:i:s", $strGenDate);
         if ($genDate == null) {
@@ -316,8 +322,12 @@ class OtAuthenticationService extends AbstractAuthenticationService
      *
      * @return bool
      */
-    protected function createOrUpdateUser(): bool
+    protected function createOrUpdateUser(bool $isBackend = false): bool
     {
+        $table = $isBackend ? 'be_users' : 'fe_users';
+        $group_table = $isBackend ? 'be_groups' : 'fe_groups';
+        $prefix = $isBackend ? 'BE' : 'FE';
+
         // Get user's data from the API
         $userApiData = $this->getUserData();
 
@@ -327,34 +337,60 @@ class OtAuthenticationService extends AbstractAuthenticationService
             return false;
         }
 
-        $connection = $this->connectionPool->getConnectionForTable('fe_users');
+        $connection = $this->connectionPool->getConnectionForTable($table);
 
         // Since we don't want to store the password in the TYPO3 DB, we store a random string instead
-        $randomStr = (new Random)->generateRandomHexString(20);
+        $randomStr = (new Random)->generateRandomHexString(30);
 
         // Front-end user
-        $fe_row = [
+        $user_row = [
             'username' => $userApiData['username'],
             'password' => $randomStr,
-            'name' => $userApiData['name'],
-            'first_name' => $userApiData['first_name'],
-            'description' => '[Warning: auto-generated record, do not modify] FE User',
+            'description' => "[Warning: auto-generated record, do not modify] $prefix User",
             'deleted' => 0,
             'tx_opentalent_opentalentId' => $userApiData['id'],
             'tx_opentalent_generationDate' => date('Y/m/d H:i:s')
         ];
 
-        $groupsUid = [self::GROUP_FE_ALL_UID];
+        if ($isBackend) {
+            $user_row['lang'] = 'fr';
+            $user_row['options'] = "3";
+            $user_row['TSconfig'] = "options.uploadFieldsInTopOfEB = 1";
+        } else {
+            $user_row['name'] = $userApiData['name'];
+            $user_row['first_name'] = $userApiData['first_name'];
+        }
+
+        $groupsUid = [];
+
+        if (!$isBackend) {
+            $groupsUid[] = self::GROUP_FE_ALL_UID;
+        }
+
+        // Loop over the accesses of the user to list the matching organization groups
         if ($userApiData['accesses']) {
             foreach ($userApiData['accesses'] as $accessData) {
+                if ($isBackend && !$accessData['isEditor'] && !$accessData['admin_access']) {
+                    continue;
+                }
+
+                if ($isBackend && $accessData['admin_access']) {
+                    $adminGroupUid = $accessData['product'] === 'artist_premium' ?
+                        self::GROUP_ADMIN_PREMIUM_UID :
+                        self::GROUP_ADMIN_STANDARD_UID;
+                    if (!in_array($adminGroupUid, $groupsUid)) {
+                        $groupsUid[] = $adminGroupUid;
+                    }
+                }
+
                 $organizationId = $accessData['organizationId'];
 
-                // get the fe_group for this organization
+                // get the group for this organization
                 $groupUid = $connection->fetchOne(
                     "select g.uid
-                     from typo3.fe_groups g
+                     from typo3.$group_table g
                      inner join (select uid, ot_website_uid from typo3.pages where is_siteroot) p 
-                     on g.pid = p.uid
+                     on g." . ($isBackend ? 'db_mountpoints' : 'pid') . " = p.uid
                      inner join typo3.ot_websites w on p.ot_website_uid = w.uid
                      where w.organization_id=:organizationId;",
                     ['organizationId' => $organizationId]
@@ -363,31 +399,39 @@ class OtAuthenticationService extends AbstractAuthenticationService
                 if ($groupUid) {
                     $groupsUid[] = $groupUid;
                 } else {
-                    OtLogger::warning("Warning: no fe_group found for organization " . $organizationId);
+                    OtLogger::warning("Warning: no " . strtolower($prefix) . "_group found for organization " . $organizationId);
                 }
             }
         }
-        $fe_row['usergroup'] = join(',', $groupsUid);
+
+        if ($isBackend && empty($groupsUid)) {
+            throw new \Exception("No BE_group found for user " . $userApiData['username']);
+        }
+
+        $user_row['usergroup'] = join(',', $groupsUid);
 
         // TODO: log a warning if a user with the same opentalentId exists (the user might have changed of username)
         $q = $connection->select(
             ['uid', 'tx_opentalent_opentalentId'],
-            'fe_users',
+            $table,
             ['username' => $userApiData['username']]
         );
         $row = $q->fetch(3);
-        $uid = $row[0];
-        $tx_opentalent_opentalentId = $row[1];
+        $uid = $row[0] ?? null;
+        $tx_opentalent_opentalentId = $row[1] ?? null;
 
         if (!$uid) {
             // No existing user: create
-            $connection->insert('fe_users', $fe_row);
+            $connection->insert($table, $user_row);
         } else {
             // User exists: update
             if (!$tx_opentalent_opentalentId > 0) {
-                OtLogger::warning('WARNING: FE user ' . $userApiData['username'] . ' has been replaced by an auto-generated version.');
+                OtLogger::warning(
+                    "WARNING: $prefix user " . $userApiData['username'] .
+                    ' has been replaced by an auto-generated version.'
+                );
             }
-            $connection->update('fe_users', $fe_row, ['uid' => $uid]);
+            $connection->update($table, $user_row, ['uid' => $uid]);
         }
 
         return true;

+ 1 - 1
ot_core/Classes/Domain/Repository/DonorRepository.php

@@ -65,7 +65,7 @@ class DonorRepository extends BaseApiRepository
         $donor->setWebsite($record['website']);
         $donor->setWording($record['wording']);
         $donor->setDisplayedOn($record['displayedOn']);
-        $donor->setLogo($record['logo']);
+        $donor->setLogo($record['logo'] ?? null);
 
         return $donor;
     }

+ 1 - 0
ot_core/Classes/Service/OpentalentApiService.php

@@ -82,6 +82,7 @@ class OpentalentApiService
     ): array
     {
         $body = $this->getBody($uri, $params);
+
         $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
         if ($data !== null) {
             return json_decode($body, true, 512, JSON_THROW_ON_ERROR);