浏览代码

add the ot_admin status and scan commands

Olivier Massot 4 年之前
父节点
当前提交
8651cb45d1

+ 69 - 0
ot_admin/Classes/Command/GetSiteStatusCommand.php

@@ -0,0 +1,69 @@
+<?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;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Object\ObjectManager;
+
+/**
+ * This CLI command returns a status code representing the current state of a given website
+ *
+ * @package Opentalent\OtAdmin\Command
+ */
+class GetSiteStatusCommand 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:status")
+            ->setDescription("Displays the current state of a given website")
+            ->setHelp("This CLI command returns a status code representing the current state of a given website")
+            ->addOption(
+                'full',
+                null,
+                InputOption::VALUE_NONE,
+                "Performs a full scan"
+            )
+            ->addArgument(
+                'organization_id',
+                InputArgument::REQUIRED,
+                "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
+     * @throws \Exception
+     */
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $org_id = $input->getArgument('organization_id');
+        $full = $input->getOption('full');
+
+        $io = new SymfonyStyle($input, $output);
+
+        $siteController = GeneralUtility::makeInstance(ObjectManager::class)->get(SiteController::class);
+        $status = $siteController->getSiteStatusAction($org_id, $full);
+
+        $io->success(sprintf("Status of the website: " . var_dump($status)));
+    }
+
+}

+ 40 - 0
ot_admin/Classes/Controller/ScanController.php

@@ -0,0 +1,40 @@
+<?php
+
+
+namespace Opentalent\OtAdmin\Controller;
+
+
+use Opentalent\OtCore\Controller\ActionController;
+use Opentalent\OtCore\Domain\Repository\OrganizationRepository;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Object\ObjectManager;
+
+/**
+ * The ScanController allows to scan the Typo3 DB to find potential issues
+ */
+class ScanController extends ActionController
+{
+    /**
+     * Perform a full scan of the Typo3 DB, and confront it to the
+     * Opentalent organizations list.
+     *
+     * @param bool $fullScan If true, a 'warnings' entry will be added to the result of each site status,
+     *                        and a full scan of the website pages will be performed.
+     * @return array
+     */
+    public function scanAllAction(bool $fullScan = false): array
+    {
+        $organizationRepository = GeneralUtility::makeInstance(ObjectManager::class)->get(OrganizationRepository::class);
+        $siteController = GeneralUtility::makeInstance(ObjectManager::class)->get(SiteController::class);
+
+        $results = [];
+
+        $organizationsCollection = $organizationRepository->getAll();
+        foreach ($organizationsCollection->getMembers() as $organization) {
+            $status = $siteController->getSiteStatusAction($organization->getId(), $fullScan);
+            $results[$organization->getId()] = $status;
+        }
+
+        return $results;
+    }
+}

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

@@ -3,7 +3,9 @@
 namespace Opentalent\OtAdmin\Controller;
 
 use http\Exception\RuntimeException;
+use Opentalent\OtAdmin\Exception\NoSuchWebsiteException;
 use Opentalent\OtCore\Cache\OtCacheManager;
+use Opentalent\OtCore\Controller\ActionController;
 use Opentalent\OtCore\Domain\Model\Organization;
 use Opentalent\OtCore\Domain\Repository\OrganizationRepository;
 use Opentalent\OtCore\Exception\ApiRequestException;
@@ -12,7 +14,6 @@ use PDO;
 use Symfony\Component\Yaml\Yaml;
 use TYPO3\CMS\Core\Crypto\Random;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
 use TYPO3\CMS\Extbase\Object\ObjectManager;
 
 
@@ -63,6 +64,12 @@ class SiteController extends ActionController
         "manager" => 3, // Association writer full
     ];
 
+    // Websites statuses
+    const STATUS_NO_SUCH_WEBSITE = 0;
+    const STATUS_EXISTING = 1;
+    const STATUS_EXISTING_DELETED = 2;
+    const STATUS_EXISTING_HIDDEN = 3;
+
     const MODE_PROD = 1;
     const MODE_DEV = 1;
 
@@ -592,8 +599,7 @@ class SiteController extends ActionController
         $renamed = [];
 
         try {
-            $repository = GeneralUtility::makeInstance(ObjectManager::class)->get(OtPageRepository::class);
-            $pages = $repository->getAllSubpagesForPage($rootUid);
+            $pages = $this->otPageRepository->getAllSubpagesForPage($rootUid);
             foreach($pages as $page) {
                 $this->delete('tt_content', 'pid', $page['uid'], $hard);
                 $this->delete('pages', 'uid', $page['uid'], $hard);
@@ -768,8 +774,7 @@ class SiteController extends ActionController
         $renamed = [];
 
         try {
-            $repository = GeneralUtility::makeInstance(ObjectManager::class)->get(OtPageRepository::class);
-            $pages = $repository->getAllSubpagesForPage($rootUid);
+            $pages = $this->otPageRepository->getAllSubpagesForPage($rootUid);
 
             foreach($pages as $page) {
                 $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content');
@@ -889,8 +894,10 @@ class SiteController extends ActionController
      * @param int $organizationId the organization's id whom site cache should be cleared
      * @param bool $clearAll if true, all caches will be cleared, and not only the frontend one
      * @return int
+     * @throws NoSuchWebsiteException
      */
-    public function clearSiteCacheAction($organizationId, $clearAll=false) {
+    public function clearSiteCacheAction(int $organizationId, $clearAll=false): int
+    {
         $rootUid = $this->findRootUidFor($organizationId);
 
         OtCacheManager::clearSiteCache($rootUid, $clearAll);
@@ -898,6 +905,152 @@ class SiteController extends ActionController
         return $rootUid;
     }
 
+    /**
+     * Pack the status informations into an array for the getSiteStatusAction method
+     *
+     * @param int $organizationId
+     * @param int $code
+     * @param int|null $rootUid
+     * @param array|null $warnings
+     * @return array
+     */
+    private function buildStatusResult(
+        int $organizationId,
+        int $code,
+        ?int $rootUid = null,
+        ?array $warnings = null): array
+    {
+        $message = "";
+        if($code == self::STATUS_NO_SUCH_WEBSITE) {
+            $message = 'No website were found for organization ' . $organizationId;
+        } elseif ($code == self::STATUS_EXISTING) {
+            $message = 'A website exists for organization ' . $organizationId;
+        }
+        elseif ($code == self::STATUS_EXISTING_DELETED) {
+            $message = 'A website exists for organization ' . $organizationId . ', but has been deleted';
+        }
+        elseif ($code == self::STATUS_EXISTING_HIDDEN) {
+            $message = 'A website exists for organization ' . $organizationId . ', but is hidden or has a restricted access';
+        }
+
+        $title = '';
+        if ($rootUid !== null) {
+            $title = $this->otPageRepository->getPage($rootUid)['title'];
+        }
+
+        $status = [
+            'organization_id' => $organizationId,
+            'code' => $code,
+            'message' => $message,
+            'root_uid' => $rootUid,
+            'title' => $title
+        ];
+        if ($warnings !== null) {
+            $status['warnings'] = $warnings;
+        }
+        return $status;
+    }
+
+    /**
+     * Perform a full scan of the website and returns a list of warnings
+     *
+     * @param int $organizationId
+     * @param int $rootUid
+     * @return array
+     */
+    private function scanSite(int $organizationId, int $rootUid) {
+
+        $warnings = [];
+
+        // Who is the expected owner among the be_users? there should be only one.
+        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
+        $queryBuilder->getRestrictions()->removeAll();
+        $beUsers = $queryBuilder
+            ->select('uid', 'username')
+            ->from('be_users')
+            ->where('FIND_IN_SET(' . $rootUid . ', db_mountpoints) > 0')
+            ->execute()
+            ->fetchAll();
+
+        $owner = null;
+        if (count($beUsers) > 1) {
+            $warnings[] = 'Website is mounted on more than one be_user: ' .
+                join(', ', array_map(function($u) { return $u['username']; } ,$beUsers));
+        } elseif (count($beUsers) == 0) {
+            $warnings[] = 'Website is not mounted on any be_user';
+        } else {
+            $owner = $beUsers[0];
+        }
+
+        // scan pages
+        $pages = $this->otPageRepository->getAllSitePages($rootUid);
+
+        foreach ($pages as $page) {
+
+            // Is it the correct owner?
+            if ($owner !== null && $page['perms_userid'] != $owner['uid']) {
+                $warnings[] = 'Page ' . $page['uid'] . ' has wrong owner';
+            }
+        }
+
+
+
+        return $warnings;
+    }
+
+    /**
+     * Get the current status of the organization's website
+     *
+     * The result is an array of the form:
+     *
+     *  [
+     *     'organization_id' => int,
+     *     'code' => int,
+     *     'message' => string,
+     *     'root_uid' => ?int,
+     *     'site_title' => ?string,
+     *     ('warnings' => array)
+     *  ]
+     *
+     * If $statusOnly is true, the warnings entry won't be added
+     *
+     * The code value is among:
+     *
+     *   - STATUS_NO_SUCH_WEBSITE
+     *   - STATUS_EXISTING
+     *   - STATUS_EXISTING_DELETED
+     *   - STATUS_EXISTING_HIDDEN
+     *
+     * @param int $organizationId the organization's id whom site cache should be cleared
+     * @param bool $fullScan If true, a 'warnings' entry will be added to the result, and a full scan of
+     *                       the website pages will be performed.
+     * @return array
+     */
+    public function getSiteStatusAction(int $organizationId, bool $fullScan = false): array
+    {
+        try {
+            $rootUid = $this->findRootUidFor($organizationId);
+        } catch (NoSuchWebsiteException $e) {
+            return $this->buildStatusResult($organizationId, self::STATUS_NO_SUCH_WEBSITE);
+        }
+
+        if ($rootUid['deleted']) {
+            return $this->buildStatusResult($organizationId, self::STATUS_EXISTING_DELETED, $rootUid);
+        }
+        if ($rootUid['hidden'] || ($rootUid['fe_group'] < 0)) {
+            return $this->buildStatusResult($organizationId, self::STATUS_EXISTING_HIDDEN, $rootUid);
+        }
+
+        $warnings = null;
+        if ($fullScan) {
+            // ** Look for potential issues
+            $warnings = $this->scanSite($organizationId, $rootUid);
+        }
+        return $this->buildStatusResult($organizationId, self::STATUS_EXISTING, $rootUid, $warnings);
+    }
+
+
+
     /**
      * Retrieve the Organization object from the repository and then,
      * from the Opentalent API
@@ -915,9 +1068,11 @@ class SiteController extends ActionController
 
     /**
      * Try to find the root page uid of the organization's website and return it.
+     * Throw a Opentalent\OtAdmin\NoSuchWebsiteException exception if the website does not exist.
      *
      * @param $organizationId
      * @return int
+     * @throws NoSuchWebsiteException
      */
     private function findRootUidFor($organizationId) {
         $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
@@ -933,22 +1088,7 @@ class SiteController extends ActionController
         if ($rootUid > 0) {
             return $rootUid;
         }
-
-        $organization = $this->fetchOrganization($organizationId);
-        $queryBuilder = $this->connectionPool->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");
+        throw new NoSuchWebsiteException("The website of this organization can not be found");
     }
 
     /**
@@ -1235,23 +1375,28 @@ class SiteController extends ActionController
      * @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;
 
+        $subdomain = explode('.', $domain)[0];
+
+        $config_dir = $_ENV['TYPO3_PATH_APP'] . "/config/sites/" . $subdomain . '_' . $organizationId;
         $config_filename = $config_dir . "/config.yaml";
+
         if (file_exists($config_filename)) {
-            throw new \RuntimeException("A file " . $config_filename . " already exists. Abort.");
+            throw new \RuntimeException("A file named " . $config_filename . " already exists. Abort.");
         }
 
-        $config = ['base'=> 'https://' . $domain,
-            'baseVariants'=>[],
+        $config = ['base' => 'https://' . $domain,
+            'baseVariants'=>[
+                ['base' => $subdomain . '/',
+                 'condition' => 'applicationContext == "Development"']
+            ],
             'errorHandling'=>[
                 ['errorCode'=>'404',
-                    'errorHandler'=>'PHP',
-                    'errorPhpClassFQCN'=>'Opentalent\OtTemplating\Page\ErrorHandler'],
+                 'errorHandler'=>'PHP',
+                 'errorPhpClassFQCN'=>'Opentalent\OtTemplating\Page\ErrorHandler'],
                 ['errorCode'=>'403',
-                    'errorHandler'=>'PHP',
-                    'errorPhpClassFQCN'=>'Opentalent\OtTemplating\Page\ErrorHandler'],
+                 'errorHandler'=>'PHP',
+                 'errorPhpClassFQCN'=>'Opentalent\OtTemplating\Page\ErrorHandler'],
             ],
             'flux_content_types'=>'',
             'flux_page_templates'=>'',

+ 15 - 0
ot_admin/Classes/Exception/NoSuchWebsiteException.php

@@ -0,0 +1,15 @@
+<?php
+
+
+namespace Opentalent\OtAdmin\Exception;
+
+
+use Exception;
+
+/**
+ * Class NoSuchWebsite
+ * Raise this exception when a non-existing website is fetched
+ *
+ * @package Opentalent\OtCore\Exception
+ */
+class NoSuchWebsiteException extends Exception {}

+ 46 - 0
ot_admin/Classes/Http/ApiController.php

@@ -3,6 +3,7 @@
 namespace Opentalent\OtAdmin\Http;
 
 
+use Opentalent\OtAdmin\Controller\ScanController;
 use Opentalent\OtAdmin\Controller\SiteController;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
@@ -223,4 +224,49 @@ class ApiController implements LoggerAwareInterface
             ]
         );
     }
+
+    /**
+     * -- Target of the route 'site_status' --
+     * >> Requires a query param named 'organization-id' (int)
+     *
+     * Returns the current status of the website
+     *
+     * @param ServerRequest $request
+     * @return JsonResponse
+     * @throws \Exception
+     */
+    public function getSiteStatusAction(ServerRequest $request) {
+        $this->assertIpAllowed();
+
+        $organizationId = $this->getOrganizationId($request);
+
+        $controller = GeneralUtility::makeInstance(ObjectManager::class)->get(SiteController::class);
+
+        $queryParams = $request->getQueryParams();
+        $full = (isset($queryParams['full']) && $queryParams['full']);
+        $status = $controller->getSiteStatusAction($organizationId, $full);
+
+        return new JsonResponse($status);
+    }
+
+    /**
+     * -- Target of the route 'scan' --
+     *
+     * Scan the whole Typo3 database and return the results
+     *
+     * @param ServerRequest $request
+     * @return JsonResponse
+     * @throws \Exception
+     */
+    public function scanAllAction(ServerRequest $request) {
+        $this->assertIpAllowed();
+
+        $controller = GeneralUtility::makeInstance(ObjectManager::class)->get(ScanController::class);
+
+        $queryParams = $request->getQueryParams();
+        $full = (isset($queryParams['full']) && $queryParams['full']);
+        $results = $controller->scanAllAction($full);
+
+        return new JsonResponse($results);
+    }
 }

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

@@ -32,4 +32,14 @@ return [
         'target' => ApiController::class . '::clearSiteCacheAction',
         'access' => 'public'
     ],
+    'site_status' => [
+        'path' => '/otadmin/site/status',
+        'target' => ApiController::class . '::getSiteStatusAction',
+        'access' => 'public'
+    ],
+    'scan' => [
+        'path' => '/otadmin/scan',
+        'target' => ApiController::class . '::scanAllAction',
+        'access' => 'public'
+    ]
 ];

+ 3 - 0
ot_admin/Configuration/Commands.php

@@ -24,6 +24,9 @@ return [
     ],
     'ot:site:clear-cache' => [
         'class' => Opentalent\OtAdmin\Command\ClearSiteCacheCommand::class
+    ],
+    'ot:site:status' => [
+        'class' => Opentalent\OtAdmin\Command\GetSiteStatusCommand::class
     ]
 ];
 

+ 6 - 0
ot_admin/Configuration/Services.yaml

@@ -30,3 +30,9 @@ services:
       - name: 'ot:site:clear-cache'
         command: 'ot:site:clear-cache'
         schedulable: true
+
+  Opentalent\OtAdmin\Command\GetSiteStatusCommand:
+    tags:
+      - name: 'ot:site:status'
+        command: 'ot:site:status'
+        schedulable: false

+ 2 - 0
ot_admin/Readme.md

@@ -50,4 +50,6 @@ Les commandes disponibles sont:
 | Soft-delete an organization | `<typo3_host>/typo3/index.php?route=/otadmin/site/delete&organization-id=<organization_id>` |
 | Restore a soft-deleted organization | `<typo3_host>/typo3/index.php?route=/otadmin/site/undelete&organization-id=<organization_id>` |
 | Clear the website's cache | `<typo3_host>/typo3/index.php?route=/otadmin/site/clear-cache&organization-id=<organization_id>` |
+| Get the current status of the website | `<typo3_host>/typo3/index.php?route=/otadmin/site/clear-cache&organization-id=<organization_id>[&full=1]` |
+| Scan the whole Typo3 DB | `<typo3_host>/typo3/index.php?route=/otadmin/scan[&full=1]` |
 

+ 18 - 3
ot_core/Classes/Domain/Repository/OrganizationRepository.php

@@ -18,7 +18,8 @@ class OrganizationRepository extends BaseApiRepository
      * @return array
      * @throws ApiRequestException
      */
-    public function findById($id) {
+    public function findById($id): array
+    {
         $params = [];
         $params['filter[where][id]'] = $id;
         $organization = $this->getApiFirstRecord($this::URI, $params);
@@ -36,7 +37,7 @@ class OrganizationRepository extends BaseApiRepository
      * @throws \Exception
      * @throws ApiRequestException
      */
-    public function findByName($name)
+    public function findByName($name): array
     {
         $params = [];
         $params['filter[where][name]'] = $name;
@@ -56,7 +57,7 @@ class OrganizationRepository extends BaseApiRepository
      * @return ApiPagedCollection
      * @throws ApiRequestException
      */
-    public function findChildrenById(int $id, $searchParams = [], $page = 1)
+    public function findChildrenById(int $id, $searchParams = [], $page = 1): ApiPagedCollection
     {
         $params = [];
         $params['parentId'] = $id;
@@ -68,6 +69,20 @@ class OrganizationRepository extends BaseApiRepository
         return $this->getApiRecords($this::URI, $params);
     }
 
+    /**
+     * Get all organizations
+     *
+     * @return ApiPagedCollection
+     * @throws ApiRequestException
+     */
+    public function getAll(): ApiPagedCollection
+    {
+        $params = [];
+        $params['itemsPerPage'] = 999999;
+
+        return $this->getApiRecords($this::URI, $params);
+    }
+
     /**
      * Returns an Organization object from an Api record
      *

+ 13 - 0
ot_core/Classes/Page/OtPageRepository.php

@@ -114,6 +114,19 @@ class OtPageRepository
         return $subpages;
     }
 
+    /**
+     * Returns all the pages of the given page's website, starting from the root page
+     *
+     * @param int $pageUid The uid of the page
+     * @param bool $withRestrictions Set to true to add the standard restrictions (deleted, forbidden...etc.)
+     * @return array
+     */
+    public function getAllSitePages(int $pageUid, bool $withRestrictions=false) {
+        $rootPage = $this->getRootPageFor($pageUid);
+        return array_merge([$rootPage], $this->getAllSubpagesForPage($rootPage['uid']));
+
+    }
+
     /**
      * Return the Site object for the given page
      *