Browse Source

https://assistance.opentalent.fr/browse/V8-1923

Olivier Massot 4 years ago
parent
commit
a1a4ba1c97

+ 16 - 1
ot_admin/Classes/Command/GetSiteStatusCommand.php

@@ -63,7 +63,22 @@ class GetSiteStatusCommand extends Command
         $siteController = GeneralUtility::makeInstance(ObjectManager::class)->get(SiteController::class);
         $status = $siteController->getSiteStatusAction($org_id, $full);
 
-        $io->success(sprintf("Status of the website: " . var_dump($status)));
+        $output = "## Status of the website: \n" .
+            "Organization Id: " . $status['organization_id'] . "\n" .
+            "Status: " . $status['status'] . "\n" .
+            "Message: " . $status['message'] . "\n" .
+            "Site root uid: " . $status['root_uid'] . "\n" .
+            "Site's title: " . $status['title'] . "";
+
+        if (isset($status['warnings'])) {
+            if ($status['warnings']) {
+                $output .= "\nWarnings: \n - " . join("\n - ", $status['warnings']);
+            } else {
+                $output .= "\nWarnings: None";
+            }
+        }
+
+        $io->success(sprintf($output));
     }
 
 }

+ 89 - 0
ot_admin/Classes/Command/ScanCommand.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace Opentalent\OtAdmin\Command;
+
+
+use Opentalent\OtAdmin\Controller\ScanController;
+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 performs a full scan on the Typo3 db
+ *
+ * @package Opentalent\OtAdmin\Command
+ */
+class ScanCommand 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:scan")
+            ->setDescription("Scan the typo3 DB")
+            ->setHelp("This CLI command performs a full scan on the Typo3 db.")
+            ->addOption(
+                'report',
+                'r',
+                InputOption::VALUE_OPTIONAL,
+                'Build an html report file with the given name.'
+            );
+    }
+
+    /**
+     * -- 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)
+    {
+        $io = new SymfonyStyle($input, $output);
+
+        // evaluate the report path
+        $report_path = $input->getOption('report');
+        if ($report_path == null) {
+            $report_path = getcwd() . '/scan_report.html';
+        } else {
+            $info = pathinfo($report_path);
+            if ($info['extension'] != '.html') {
+                $report_path .= '.html';
+            }
+        }
+
+        // perform the scan
+        $scanController = GeneralUtility::makeInstance(ObjectManager::class)->get(ScanController::class);
+        $scan = $scanController->scanAllAction(true);
+
+        // render the twig template
+        $loader = new \Twig\Loader\FilesystemLoader(dirname(__FILE__) . '/../../templates');
+        $twig = new \Twig\Environment($loader);
+        $template = $twig->load('scan_report.twig');
+
+        $html_report = $template->render(['scan' => $scan]);
+
+        $f = fopen($report_path, 'w+');
+        try {
+            fwrite($f, $html_report);
+            $io->success(sprintf("Report file was created at: " . $report_path));
+        } finally {
+            fclose($f);
+        }
+    }
+
+}

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

@@ -18,6 +18,21 @@ class ScanController extends ActionController
      * Perform a full scan of the Typo3 DB, and confront it to the
      * Opentalent organizations list.
      *
+     * Result is an array of the form:
+     *
+     * [
+     *   'date' => Datetime,
+     *   'stats' => [
+     *                status => count,
+     *                ...
+     *              ]
+     *   'results' => [
+     *                  organization_id => status,
+     *                  ...
+     *                ]
+     * ]
+     *
+     *
      * @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
@@ -28,13 +43,23 @@ class ScanController extends ActionController
         $siteController = GeneralUtility::makeInstance(ObjectManager::class)->get(SiteController::class);
 
         $results = [];
+        $stats = [];
 
         $organizationsCollection = $organizationRepository->getAll();
         foreach ($organizationsCollection->getMembers() as $organization) {
             $status = $siteController->getSiteStatusAction($organization->getId(), $fullScan);
             $results[$organization->getId()] = $status;
+
+            if (!isset($stats[$status['status']])) {
+                $stats[$status['status']] = 0;
+            }
+            $stats[$status['status']] += 1;
         }
 
-        return $results;
+        return [
+            'date' => new \DateTime(),
+            'stats' => $stats,
+            'results' => $results
+        ];
     }
 }

+ 89 - 7
ot_admin/Classes/Controller/SiteController.php

@@ -69,6 +69,16 @@ class SiteController extends ActionController
     const STATUS_EXISTING = 1;
     const STATUS_EXISTING_DELETED = 2;
     const STATUS_EXISTING_HIDDEN = 3;
+    const STATUS_EXISTING_WITH_WARNINGS = 4;
+
+    const STATUS_LBL = [
+        self::STATUS_NO_SUCH_WEBSITE => 'Does not exist',
+        self::STATUS_EXISTING => 'Existing',
+        self::STATUS_EXISTING_DELETED => 'Existing, deleted',
+        self::STATUS_EXISTING_HIDDEN => 'Existing, hidden',
+        self::STATUS_EXISTING_WITH_WARNINGS => 'Existing with warnings',
+    ];
+
 
     const MODE_PROD = 1;
     const MODE_DEV = 1;
@@ -931,6 +941,8 @@ class SiteController extends ActionController
         }
         elseif ($code == self::STATUS_EXISTING_HIDDEN) {
             $message = 'A website exists for organization ' . $organizationId . ', but is hidden or has a restricted access';
+        } elseif ($code == self::STATUS_EXISTING_WITH_WARNINGS) {
+            $message = 'A website exists for organization ' . $organizationId . ', but warnings were raised';
         }
 
         $title = '';
@@ -941,6 +953,7 @@ class SiteController extends ActionController
         $status = [
             'organization_id' => $organizationId,
             'code' => $code,
+            'status' => self::STATUS_LBL[$code],
             'message' => $message,
             'root_uid' => $rootUid,
             'title' => $title
@@ -962,6 +975,29 @@ class SiteController extends ActionController
 
         $warnings = [];
 
+        // fetch pages and root page
+        $pages = $this->otPageRepository->getAllSitePages($rootUid);
+
+        $rootPage = null;
+        $pageIndex = [];
+        foreach ($pages as $page) {
+            $pageIndex[$page['uid']] = $page;
+            if ($page['is_siteroot'] == 1) { $rootPage = $page; }
+        }
+
+        // fetch organization and extradata
+        $organization = $this->fetchOrganization($organizationId);
+        $extraData = $this->fetchOrganizationExtraData($organizationId);
+
+        // load site's settings (uncomment if needed)
+        // $config_dir = $_ENV['TYPO3_PATH_APP'] . "/config/sites/" . $organization->getSubDomain() . '_' . $organizationId;
+        // $config = Yaml::parseFile($config_dir . "/config.yaml");
+
+        // Check site's title
+        if (trim($rootPage['title']) != trim($organization->getName())) {
+            $warnings[] = "Site's title does not match the organization name";
+        }
+
         // Who is the expected owner among the be_users? there should be only one.
         $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
         $queryBuilder->getRestrictions()->removeAll();
@@ -982,18 +1018,59 @@ class SiteController extends ActionController
             $owner = $beUsers[0];
         }
 
-        // scan pages
-        $pages = $this->otPageRepository->getAllSitePages($rootUid);
+        // are template constants up to date?
+        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
+        $actual_constants = $queryBuilder
+            ->select('constants')
+            ->from('sys_template')
+            ->where($queryBuilder->expr()->eq('pid', $rootUid))
+            ->execute()
+            ->fetchColumn(0);
+        $expected_constants = $this->getTemplateConstants($organizationId, $extraData);
+        $norm = function ($s) {
+            return strtolower(preg_replace('/\s/', '', $s));
+        };
+        if ($norm($expected_constants) != $norm($actual_constants)) {
+            $warnings[] = 'Template constants need an update';
+        }
 
-        foreach ($pages as $page) {
+        $expected_templates = [
+            "OpenTalent.OtTemplating->home" => 0,
+            "OpenTalent.OtTemplating->legal" => 0,
+            "OpenTalent.OtTemplating->contact" => 0
+        ];
 
+        foreach ($pages as $page) {
             // Is it the correct owner?
             if ($owner !== null && $page['perms_userid'] != $owner['uid']) {
                 $warnings[] = 'Page ' . $page['uid'] . ' has wrong owner';
             }
-        }
 
+            if (!$page['is_siteroot']) {
+                // is the parent page state (deleted, hidden, restricted) the same as this page?
+                $parent = $pageIndex[$page['pid']];
 
+                if ($parent['deleted'] && !$page['deleted']) {
+                    $warnings[] = 'The non-deleted page ' . $page['uid'] . ' has a deleted parent page';
+                }
+                if ($parent['hidden'] && !$page['hidden']) {
+                    $warnings[] = 'The non-hidden page ' . $page['uid'] . ' has a hidden parent page';
+                }
+                if ($parent['fe_group'] < 0 && !$page['fe_group'] >= 0) {
+                    $warnings[] = 'The non-restricted page ' . $page['uid'] . ' has a restricted parent page';
+                }
+            }
+
+            // an expected template was found, remove it from the list of expected
+            if (in_array($page['tx_fed_page_controller_action'], $expected_templates) &&
+                !$page['deleted'] && !$page['hidden']) {
+                unset($expected_templates[$page['tx_fed_page_controller_action']]);
+            }
+        }
+
+        foreach ($expected_templates as $template => $_) {
+            $warnings[] = 'No page with template ' . $template;
+        }
 
         return $warnings;
     }
@@ -1006,6 +1083,7 @@ class SiteController extends ActionController
      *  [
      *     'organization_id' => int,
      *     'code' => int,
+     *     'status' => int,
      *     'message' => string,
      *     'root_uid' => ?int,
      *     'site_title' => ?string,
@@ -1020,6 +1098,7 @@ class SiteController extends ActionController
      *   - STATUS_EXISTING
      *   - STATUS_EXISTING_DELETED
      *   - STATUS_EXISTING_HIDDEN
+     *   - STATUS_EXISTING_WITH_WARNINGS
      *
      * @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
@@ -1046,10 +1125,13 @@ class SiteController extends ActionController
             // ** Look for potential issues
             $warnings = $this->scanSite($organizationId, $rootUid);
         }
-        return $this->buildStatusResult($organizationId, self::STATUS_EXISTING, $rootUid, $warnings);
-    }
-
 
+        return $this->buildStatusResult(
+            $organizationId,
+            $warnings ? self::STATUS_EXISTING_WITH_WARNINGS : self::STATUS_EXISTING,
+            $rootUid,
+            $warnings);
+    }
 
     /**
      * Retrieve the Organization object from the repository and then,

+ 3 - 0
ot_admin/Configuration/Commands.php

@@ -27,6 +27,9 @@ return [
     ],
     'ot:site:status' => [
         'class' => Opentalent\OtAdmin\Command\GetSiteStatusCommand::class
+    ],
+    'ot:scan' => [
+        'class' => Opentalent\OtAdmin\Command\ScanCommand::class
     ]
 ];
 

+ 6 - 0
ot_admin/Configuration/Services.yaml

@@ -36,3 +36,9 @@ services:
       - name: 'ot:site:status'
         command: 'ot:site:status'
         schedulable: false
+
+  Opentalent\OtAdmin\Command\ScanCommand:
+    tags:
+      - name: 'ot:scan'
+        command: 'ot:scan'
+        schedulable: true

+ 1 - 1
ot_admin/Readme.md

@@ -50,6 +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]` |
+| Get the current status of the website | `<typo3_host>/typo3/index.php?route=/otadmin/site/status&organization-id=<organization_id>[&full=1]` |
 | Scan the whole Typo3 DB | `<typo3_host>/typo3/index.php?route=/otadmin/scan[&full=1]` |
 

+ 112 - 0
ot_admin/templates/scan_report.twig

@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>OtAdmin Scan Report</title>
+
+        <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.23/css/jquery.dataTables.css">
+
+        <script type="text/javascript" charset="utf8" src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
+        <script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.23/js/jquery.dataTables.js"></script>
+        <script type="text/javascript" charset="utf8" src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
+
+        <style>
+            body {
+                margin: 0 10%;
+            }
+            .summary, .details {
+                margin: 24px 0;
+            }
+            .chart-div {
+                height: 400px;
+            }
+
+        </style>
+    </head>
+    <body>
+
+        <h1>OtAdmin Scan Report</h1>
+
+        <div class="summary">
+            <div class="chart-div">
+                <canvas id="chart_stats" width="400" height="400"></canvas>
+            </div>
+        </div>
+
+        <div class="details">
+            <table id="results" class="display">
+                <thead>
+                    <tr>
+                        <th>Organization Id</th>
+                        <th>Status</th>
+                        <th>Root uid</th>
+                        <th>Site's title</th>
+                        <th>Warnings</th>
+                    </tr>
+                </thead>
+                <tbody>
+                {% for status in scan.results %}
+                    <tr>
+                        <td>{{ status.organization_id }}</td>
+                        <td>{{ status.status }}</td>
+                        <td>{{ status.root_uid }}</td>
+                        <td>{{ status.title }}</td>
+                        <td><ul>{% for warning in status.warnings %}<li>{{ warning }}</li>{% endfor %}</ul></td>
+                    </tr>
+                {% endfor %}
+                </tbody>
+            </table>
+        </div>
+
+        <script>
+            $(document).ready( function () {
+                // Datatable
+                $('#results').DataTable();
+
+
+            } );
+
+            // Charts
+            var labels = [];
+            var values = [];
+            {% for lbl, val in scan.stats %}
+            labels.push('{{ lbl }}');
+            values.push({{ val }});
+            {% endfor %}
+
+            {% autoescape 'js' %}
+            var ctx = document.getElementById('chart_stats').getContext('2d');
+            var myChart = new Chart(ctx, {
+                type: 'bar',
+                data: {
+                    labels: labels,
+                    datasets: [{
+                        label: 'Stats',
+                        data: values,
+                        backgroundColor: [
+                            'rgba(255, 99, 132, 0.2)',
+                            'rgba(54, 162, 235, 0.2)',
+                            'rgba(255, 206, 86, 0.2)',
+                            'rgba(75, 192, 192, 0.2)',
+                            'rgba(153, 102, 255, 0.2)',
+                            'rgba(255, 159, 64, 0.2)'
+                        ],
+                        borderColor: [
+                            'rgba(255, 99, 132, 1)',
+                            'rgba(54, 162, 235, 1)',
+                            'rgba(255, 206, 86, 1)',
+                            'rgba(75, 192, 192, 1)',
+                            'rgba(153, 102, 255, 1)',
+                            'rgba(255, 159, 64, 1)'
+                        ],
+                        borderWidth: 1
+                    }]
+                },
+                options: {
+                    responsive: true,
+                    maintainAspectRatio: false
+                }
+            });
+            {% endautoescape %}
+        </script>
+    </body>
+</html>