瀏覽代碼

ot_stats: add matomo widgets, controller ok

Olivier Massot 5 年之前
父節點
當前提交
8204b052aa

+ 18 - 0
ot_core/Classes/Exception/NoSiteSelected.php

@@ -0,0 +1,18 @@
+<?php
+
+
+namespace Opentalent\OtCore\Exception;
+
+
+use Exception;
+
+/**
+ * Class NoSiteSelected
+ * Raise this exception in a BE module if no site is selected while it should
+ *
+ * @package Opentalent\OtCore\Exception
+ */
+class NoSiteSelected extends Exception
+{
+
+}

+ 34 - 2
ot_core/Classes/Page/OtPageRepository.php

@@ -5,6 +5,7 @@ namespace Opentalent\OtCore\Page;
 use FluidTYPO3\Vhs\Service\PageService;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
+use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Object\ObjectManager;
@@ -35,6 +36,7 @@ class OtPageRepository extends PageRepository
         for (end($rootLine); key($rootLine)!==null; prev($rootLine)){
             $page = current($rootLine);
             if ($page['is_siteroot'] == 1) {
+                $page = $this->getPage($page['uid']);
                 return $page;
             }
         }
@@ -48,7 +50,7 @@ class OtPageRepository extends PageRepository
      * @param bool $withRestrictions Set to true to add the standard restrictions (deleted, forbidden...etc.)
      * @return array
      */
-    public function getAllSubpagesForPage($pageUid, bool $withRestrictions=false) {
+    public function getAllSubpagesForPage(int $pageUid, bool $withRestrictions=false) {
         $subpages = [];
 
         $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
@@ -74,8 +76,23 @@ class OtPageRepository extends PageRepository
     }
 
     /**
-     * Returns the typo3 site matching the current request
+     * Return the Site object for the given page
      *
+     * @param int $pageUid
+     * @return Site
+     */
+    public function getSiteFor(int $pageUid) {
+        $rootPage = $this->getRootPageFor($pageUid);
+        $rootUid = $rootPage['uid'];
+
+        $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
+        return $siteFinder->getSiteByRootPageId($rootUid);
+    }
+
+    /**
+     * Returns the typo3 site matching the current request (FE only)
+     *
+     * @return Site
      */
     public function getCurrentSite() {
         $request = $GLOBALS['TYPO3_REQUEST'];
@@ -85,7 +102,9 @@ class OtPageRepository extends PageRepository
     }
 
     /**
+     * Returns the current site's rootpage uid (FE only)
      *
+     * @return int
      */
     public function getCurrentSiteRootPageId() {
         $site = $this->getCurrentSite();
@@ -93,7 +112,9 @@ class OtPageRepository extends PageRepository
     }
 
     /**
+     * Returns the current site's rootpage URI (FE only)
      *
+     * @return string
      */
     public function getCurrentSiteRootPageUri() {
         $site = $this->getCurrentSite();
@@ -101,11 +122,22 @@ class OtPageRepository extends PageRepository
     }
 
     /**
+     * Returns the current site's rootpage array (FE only)
      *
+     * @return array
      */
     public function getCurrentSiteRootPage() {
         $uid = $this->getCurrentSiteRootPageUri();
         return $this->getPage($uid);
     }
 
+    /**
+     * Returns the page currently selected in the backend if any
+     *
+     * @return int|null
+     */
+    public function getCurrentBackendPage() {
+        return (int)GeneralUtility::_GP('id');
+    }
+
 }

+ 12 - 0
ot_core/Resources/Private/Language/locallang.xlf

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<xliff version="1.0">
+	<file source-language="fr" datatype="plaintext" original="messages" date="2020-04-03T15:44:11Z" product-name="ot_templating">
+		<header/>
+		<body>
+			<!--  -->
+			<trans-unit id="noPageSelected">
+				<source>Veuillez sélectionner une des pages du site à personnaliser pour accéder à cette rubrique.</source>
+			</trans-unit>
+		</body>
+	</file>
+</xliff>

+ 44 - 0
ot_core/Resources/Public/assets/Backend/style/ot_be_module.css

@@ -0,0 +1,44 @@
+
+.ot-be-module {
+}
+
+.ot-be-module h3 {
+    font-weight: 500;
+    font-size: 1.35em;
+    line-height: 1.2em;
+}
+
+.ot-be-module .ot-btn {
+    padding: 6px;
+
+    border: solid 1px #bbb;
+    border-radius: 2px;
+    background-color: #eee;
+    font-weight: 600;
+}
+
+.ot-be-module .ot-btn:hover {
+    text-decoration: none;
+    cursor: pointer;
+
+    color: #333;
+    background-color: #d5d4d4;
+    border-color: #9c9c9c;
+}
+
+.ot-be-module .success-msg {
+    color: #009933;
+    width: 100%;
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    font-size: 14px;
+}
+
+.ot-be-module .no-page-warning {
+    margin: 36px;
+    padding: 12px;
+    background-color: #e6e6e6;
+    border: solid 1px #666666;
+}
+

+ 76 - 16
ot_stats/Classes/Controller/OtStatsController.php

@@ -2,10 +2,13 @@
 
 namespace Opentalent\OtStats\Controller;
 
+use Opentalent\OtCore\Exception\NoSiteSelected;
+use Opentalent\OtStats\Domain\Repository\MatomoWebsiteRepository;
 use Opentalent\OtStats\Settings\StatsSettingsRepository;
 use Opentalent\OtCore\Page\OtPageRepository;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
+use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
 
 /**
  * Controller for the OtStats backend submodule
@@ -20,26 +23,19 @@ class OtStatsController extends ActionController {
      */
     public function indexAction() {
 
-        // Get the selected page uid
-        $pageId = (int) GeneralUtility::_GP('id');
-
-        // Get the root page of the site
-        $otPageRepository = GeneralUtility::makeInstance(OtPageRepository::class);
-        $rootPage = $otPageRepository->getRootPageFor($pageId);
-
-        $rootPageUid = (int)($rootPage['uid'] ?? 0);
-
-        // If the current page is not a root page or a subpage of one, abort
-        $pageSelected = ($rootPageUid !== null);
-        $this->view->assign('pageSelected', (int)$pageSelected);
-        if (!$pageSelected) {
+        // Get the current page uid
+        try {
+            $rootUid = $this->getCurrentRootUidOrAbort();
+        } catch (NoSiteSelected $e) {
+            $this->view->assign('pageSelected', 0);
             return;
         }
 
-        $this->view->assign('rootPage', $rootPageUid);
+        $this->view->assign('pageSelected', 1);
+        $this->view->assign('rootPage', $rootUid);
 
         $statsSettingsRepository = GeneralUtility::makeInstance(StatsSettingsRepository::class);
-        $matomoId = $statsSettingsRepository->getMatomoSiteId($rootPageUid);
+        $matomoId = $statsSettingsRepository->getMatomoSiteId($rootUid);
 
         $statsActivated = ($matomoId !== null);
         $this->view->assign('statsActivated', (int)$statsActivated);
@@ -55,13 +51,77 @@ class OtStatsController extends ActionController {
      * save its id
      */
     public function enableStatsAction() {
+        // Get the current page uid
+        try {
+            $rootUid = $this->getCurrentRootUidOrAbort();
+        } catch (NoSiteSelected $e) {
+            $this->forward('index');
+        }
+
+        $matomoRepository = GeneralUtility::makeInstance(MatomoWebsiteRepository::class);
 
+        try {
+            $matomoRepository->createFor($rootUid);
+        } catch (\RuntimeException $e) {
+            $this->view->assign(
+                'errorMsg',
+                "Une erreur s'est produite lors de l'opération, veuillez contacter un administrateur"
+            );
+        }
+        $this->forward('index');
     }
 
     /**
      * Disable the stats monitoring
+     *
+     * @param int $rootUid    Pass the rootUid of the site as an argument to confirm of the decision
+     *                          of disabling the website. If no rootUid is passed, the user will be redirected to a
+     *                          confirmation page.
+     * @throws StopActionException
      */
-    public function disableStatsAction() {
+    public function disableStatsAction(int $rootUid) {
+
+        $matomoRepository = GeneralUtility::makeInstance(MatomoWebsiteRepository::class);
+
+        try {
+            $matomoRepository->disableFor($rootUid);
+        } catch (\RuntimeException $e) {
+            $this->view->assign(
+                'errorMsg',
+                "Une erreur s'est produite lors de l'opération, veuillez contacter un administrateur"
+            );
+        }
+        $this->forward('index');
+    }
 
+    /**
+     * Display a confirmation page
+     */
+    public function confirmDeletionAction() {
+        try {
+            $rootUid = $this->getCurrentRootUidOrAbort();
+        } catch (NoSiteSelected $e) {
+            $this->forward('index');
+        }
+        $this->view->assign('rootUid', $rootUid);
+    }
+
+    /**
+     * Return the root uid of the currently selected website if any,
+     * or throw a NoSiteSelected exception
+     *
+     * @return int|mixed
+     * @throws NoSiteSelected
+     */
+    private function getCurrentRootUidOrAbort() {
+        $rootUid = (int)GeneralUtility::_GP('id');
+
+        $otPageRepository = GeneralUtility::makeInstance(OtPageRepository::class);
+        $rootPage = $otPageRepository->getRootPageFor($rootUid);
+        $rootUid = $rootPage['uid'] ?? 0;
+        if (!$rootUid > 0) {
+            throw new NoSiteSelected();
+        }
+        return $rootUid;
     }
 }

+ 144 - 0
ot_stats/Classes/Domain/Model/MatomoWebsite.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace Opentalent\OtStats\Domain\Model;
+
+class MatomoWebsite
+{
+    /**
+     * Website's id in the matomo DB
+     * @var int
+     */
+    public $id;
+
+    /**
+     * Name of the website in the matomo db
+     *
+     * @var string
+     */
+    public $name;
+
+    /**
+     * Main url of the website to monitor
+     *
+     * @var string
+     */
+    public $mainUrl;
+
+    /**
+     * Ips to ignore
+     *
+     * @var
+     */
+    public $excludedIps;
+
+    /**
+     * Matomo type (always 'website')
+     *
+     * @var string
+     */
+    public $type;
+
+    /**
+     * Login of the record creator (always 'matomo')
+     *
+     * @var string
+     */
+    public $creatorLogin;
+
+    /**
+     * @return int
+     */
+    public function getId(): int
+    {
+        return $this->id;
+    }
+
+    /**
+     * @param int $id
+     */
+    public function setId(int $id): void
+    {
+        $this->id = $id;
+    }
+
+    /**
+     * @return string
+     */
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    /**
+     * @param string $name
+     */
+    public function setName(string $name): void
+    {
+        $this->name = $name;
+    }
+
+    /**
+     * @return string
+     */
+    public function getMainUrl(): string
+    {
+        return $this->mainUrl;
+    }
+
+    /**
+     * @param string $mainUrl
+     */
+    public function setMainUrl(string $mainUrl): void
+    {
+        $this->mainUrl = $mainUrl;
+    }
+
+    /**
+     * @return mixed
+     */
+    public function getExcludedIps()
+    {
+        return $this->excludedIps;
+    }
+
+    /**
+     * @param mixed $excludedIps
+     */
+    public function setExcludedIps($excludedIps): void
+    {
+        $this->excludedIps = $excludedIps;
+    }
+
+    /**
+     * @return string
+     */
+    public function getType(): string
+    {
+        return $this->type;
+    }
+
+    /**
+     * @param string $type
+     */
+    public function setType(string $type): void
+    {
+        $this->type = $type;
+    }
+
+    /**
+     * @return string
+     */
+    public function getCreatorLogin(): string
+    {
+        return $this->creatorLogin;
+    }
+
+    /**
+     * @param string $creatorLogin
+     */
+    public function setCreatorLogin(string $creatorLogin): void
+    {
+        $this->creatorLogin = $creatorLogin;
+    }
+
+}

+ 244 - 0
ot_stats/Classes/Domain/Repository/MatomoWebsiteRepository.php

@@ -0,0 +1,244 @@
+<?php
+
+namespace Opentalent\OtStats\Domain\Repository;
+
+use Opentalent\OtCore\Page\OtPageRepository;
+use Opentalent\OtStats\Domain\Model\MatomoWebsite;
+use PDO;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Persistence\Repository;
+
+class MatomoWebsiteRepository
+{
+    const MATOMO_DB_HOST = 'tools';
+    const MATOMO_DB_NAME = 'matomo';
+    const MATOMO_DB_USER = 'root';
+    const MATOMO_DB_PWD = 'mysql2iopenservice369566';
+
+    const DOMAIN_VALIDATION = '/^(https?:\/\/)?([a-zA-Z0-9]+\.)+[a-zA-Z]{2,}$/';
+
+
+    /**
+     * Connection to the matomo DB
+     *
+     * @var PDO
+     */
+    private $matomoCnn;
+
+    public function __construct()
+    {
+        // Connection to the Matomo DB
+        $this->matomoCnn = new PDO(
+            "mysql:host=" . self::MATOMO_DB_HOST . ";dbname=" . self::MATOMO_DB_NAME,
+            self::MATOMO_DB_USER,
+            self::MATOMO_DB_PWD,
+            array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8')
+        );
+        $this->matomoCnn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+    }
+
+    /**
+     * Create a new MatomoWebsite from an array
+     *
+     * @param array $data
+     * @return MatomoWebsite
+     */
+    private function fromArray(array $data) {
+        $matomoSite = new MatomoWebsite();
+        $matomoSite->setId($data['idsite']);
+        $matomoSite->setName($data['name']);
+        $matomoSite->setMainUrl($data['main_url']);
+        $matomoSite->setExcludedIps($data['exluded_ips']);
+        $matomoSite->setType($data['type']);
+        $matomoSite->setCreatorLogin($data['creator_login']);
+        return $matomoSite;
+    }
+
+    /**
+     * Retrieve the MatomoWebsite object by id, otherwise, returns Null
+     *
+     * @param int $id
+     * @return MatomoWebsite|null
+     */
+    public function findById(int $id) {
+        $stmt = $this->matomoCnn->prepare(
+            "SELECT s.idsite, s.name, s.main_url, s.excluded_ips, s.type, s.creator_login
+                      FROM matomo.matomo_site s
+                      WHERE s.idsite=" . $id . ";"
+        );
+        $stmt->execute();
+        $stmt->setFetchMode(PDO::FETCH_ASSOC);
+        $data = $stmt->fetch();
+        if (!$data) {
+            return null;
+        }
+        return $this->fromArray($data);
+    }
+
+    /**
+     * Retrieve the MatomoWebsite object by uri, otherwise, returns Null
+     *
+     * @param string $uri
+     * @return MatomoWebsite|null
+     */
+    public function findByUri(string $uri) {
+        $stmt = $this->matomoCnn->prepare(
+            "SELECT s.idsite, s.name, s.main_url, s.excluded_ips, s.type, s.creator_login
+                      FROM matomo.matomo_site s
+                      WHERE s.main_url='" . $uri . "';"
+        );
+        $stmt->execute();
+        $stmt->setFetchMode(PDO::FETCH_ASSOC);
+        $data = $stmt->fetch();
+        if (!$data) {
+            return null;
+        }
+        return $this->fromArray($data);
+    }
+
+    /**
+     * Retrieve the MatomoWebsite object for the typo3 website with
+     * the given rootUid, otherwise, returns Null
+     *
+     * @param int $rootUid
+     * @return MatomoWebsite|null
+     */
+    public function findByRootUid(int $rootUid) {
+        $otPageRepository = GeneralUtility::makeInstance(OtPageRepository::class);
+
+        // Just to make sure this page is actually the root page
+        $rootPage = $otPageRepository->getRootPageFor($rootUid);
+
+        if ($rootPage['tx_opentalent_matomo_id'] == null) {
+            return null;
+        }
+        return $this->findById($rootPage['tx_opentalent_matomo_id']);
+    }
+
+    /**
+     * Create a new matomo's website for the Typo3 site (or retrieve the existing if any),
+     * store its id in the Typo3 DB and return it
+     *
+     * @param int $rootUid
+     * @return int|string The matomo site's id
+     * @throws \Exception
+     */
+    public function createFor(int $rootUid) {
+        $otPageRepository = GeneralUtility::makeInstance(OtPageRepository::class);
+
+        // Just to make sure this page is actually the root page
+        $rootPage = $otPageRepository->getRootPageFor($rootUid);
+        $rootUid = $rootPage['uid'];
+
+        // Retrieve current site informations
+        $site = $otPageRepository->getSiteFor($rootUid);
+        $title = $site->getIdentifier();
+        $uri = $site->getConfiguration()['base'];
+        if (!preg_match(self::DOMAIN_VALIDATION, $uri)) {
+            throw new \RuntimeException("The domain's name is not valid: " . $uri);
+        }
+        if (!preg_match('/https?:\/\//', $uri)) {
+            $uri = 'https://' . $uri;
+        }
+
+        // Make sure the current typo3 website has no existing configured matomo website
+        if ($rootPage['tx_opentalent_matomo_id'] != null) {
+            // Check if the registered matomo site still exist. If it does, throw an error.
+            if ($this->findById($rootPage['tx_opentalent_matomo_id']) != null) {
+                throw new \RuntimeException('This website has already been registered (matomo id: ' . $rootPage['tx_opentalent_matomo_id'] . ')');
+            }
+        }
+
+        // Check if there is already an existing matomo record for this url
+        $existingMatomoSite = $this->findByUri($uri);
+        if ($existingMatomoSite) {
+            // Verify that the names match (just to be sure)
+            if (!$existingMatomoSite->getName() != $title) {
+                throw new \RuntimeException('A website with this URL has already been registered: ' . $uri);
+            }
+            $matomoSiteId = $existingMatomoSite->getId();
+        }
+
+        // Set up a connection to the typo3 DB
+        $cnnPool = GeneralUtility::makeInstance(ConnectionPool::class);
+
+        // Create a new website record in the matomo DB, and store its id
+        $this->matomoCnn->beginTransaction();
+        $cnnPool->getConnectionByName('Default')->beginTransaction();
+        try {
+
+            if ($existingMatomoSite == null) {
+                // Create the new website
+                $stmt = $this->matomoCnn->prepare(
+                    "INSERT INTO matomo.matomo_site
+                          (`name`, main_url, ts_created, ecommerce, sitesearch, sitesearch_keyword_parameters, 
+                           sitesearch_category_parameters, timezone, 
+                           currency, excluded_ips, excluded_parameters, excluded_user_agents, `group`, `type`, creator_login)
+                          VALUES ('" . $title . "', '" . $uri . "', NOW(), 0, 1, '', '', 'Europe/Paris', 
+                                  'EUR', '10.8.0.*', '', '', '', 'website', 'typo3');");
+                $stmt->execute();
+
+                // Get the new site's id
+                $matomoSiteId = $this->matomoCnn->lastInsertId();
+            }
+
+            // Update typo3 DB
+            $queryBuilder = $cnnPool->getQueryBuilderForTable('pages');
+            $queryBuilder
+                ->update('pages')
+                ->where($queryBuilder->expr()->eq('uid', $rootUid))
+                ->set('tx_opentalent_matomo_id', $matomoSiteId)
+                ->execute();
+
+            // finalize
+            $this->matomoCnn->commit();
+            $cnnPool->getConnectionByName('Default')->commit();
+
+            return $matomoSiteId;
+
+        } catch (\Exception $e) {
+            // An error happened, rollback and throw
+            $this->matomoCnn->rollBack();
+            $cnnPool->getConnectionByName('Default')->rollback();
+            throw $e;
+        }
+    }
+
+    /**
+     * Disable the matomo monitoring for the website
+     * > Data wont be deleted from the matomo DB, but no more data will
+     *   be collected unless it is reactivated with the 'createFor' method
+     *
+     * @param int $rootUid
+     * @throws \Exception
+     */
+    public function disableFor(int $rootUid) {
+        $matomoSite = $this->findByRootUid($rootUid);
+        if ($matomoSite == null) {
+            throw new \RuntimeException('Activity monitoring is already disabled for this website');
+        }
+
+        // Set up a connection to the typo3 DB
+        $cnnPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $cnnPool->getConnectionByName('Default')->beginTransaction();
+        try {
+            // Update typo3 DB
+            $queryBuilder = $cnnPool->getQueryBuilderForTable('pages');
+            $queryBuilder
+                ->update('pages')
+                ->where($queryBuilder->expr()->eq('uid', $rootUid))
+                ->set('tx_opentalent_matomo_id', null)
+                ->execute();
+
+            // finalize
+            $cnnPool->getConnectionByName('Default')->commit();
+
+        } catch (\Exception $e) {
+            // An error happened, rollback and throw
+            $cnnPool->getConnectionByName('Default')->rollback();
+            throw $e;
+        }
+    }
+
+}

+ 71 - 0
ot_stats/Classes/Middleware/RequestHandler.php

@@ -0,0 +1,71 @@
+<?php
+namespace Opentalent\OtStats\Middleware;
+
+use Opentalent\OtCore\Page\OtPageRepository;
+use Opentalent\OtStats\Settings\StatsSettingsRepository;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use TYPO3\CMS\Core\Http\Stream;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+
+/**
+ * Hooks into the frontend request to add the matomo script into
+ * the request's header
+ *
+ * @internal
+ */
+class RequestHandler implements MiddlewareInterface
+{
+    const TEMPLATES_ROOT_PATHS = 'EXT:ot_stats/Resources/Private/Templates';
+    const LAYOUTS_ROOT_PATHS = 'EXT:ot_stats/Resources/Private/Layouts';
+    const TEMPLATE_FILE = self::TEMPLATES_ROOT_PATHS . '/Header/Matomo.html';
+
+    /**
+     *
+     * @param ServerRequestInterface $request
+     * @param RequestHandlerInterface $handler
+     * @return ResponseInterface
+     */
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+
+        // Retrieve the current matomo site's id if set
+        $otPageRepository = GeneralUtility::makeInstance(OtPageRepository::class);
+        $rootPageUid = $otPageRepository->getCurrentSiteRootPageId();
+
+        $statsSettingsRepository = GeneralUtility::makeInstance(StatsSettingsRepository::class);
+        $matomoSiteId = $statsSettingsRepository->getMatomoSiteId($rootPageUid);
+
+        if (!$matomoSiteId > 0) {
+            // stats are not enabled, continue
+            return $handler->handle($request);
+        }
+
+        // render view
+        $view = GeneralUtility::makeInstance(StandaloneView::class);
+        $view->setTemplateRootPaths([self::TEMPLATES_ROOT_PATHS]);
+        $view->setLayoutRootPaths([self::LAYOUTS_ROOT_PATHS]);
+        $view->setTemplatePathAndFilename(
+            GeneralUtility::getFileAbsFileName(self::TEMPLATE_FILE)
+        );
+
+        // Build the response
+        $response = $handler->handle($request);
+
+        // insert the rendered view in the body, just before the closing </html> tag
+        $newSection = $view->render();
+
+        $bodyContent = (string)$response->getBody();
+        $bodyContent = str_replace('</head>', $newSection . '</head>', $bodyContent);
+
+        $newBody = new Stream('php://temp', 'wb+');
+        $newBody->write($bodyContent);
+        $newBody->rewind();
+        $response = $response->withBody($newBody);
+
+        return $response;
+    }
+}

+ 3 - 4
ot_stats/Classes/ViewHelpers/MatomoSiteIdViewHelper.php

@@ -1,12 +1,11 @@
 <?php
 
-namespace Opentalent\OtTemplating\ViewHelpers;
+namespace Opentalent\OtStats\ViewHelpers;
 
 
 use Closure;
-use Opentalent\OtStats\Page\StatsSettingsRepository;
-use Opentalent\OtTemplating\Page\OtPageRepository;
-use Opentalent\OtTemplating\ViewHelpers\RootPage\GetIdViewHelper;
+use Opentalent\OtCore\Page\OtPageRepository;
+use Opentalent\OtStats\Settings\StatsSettingsRepository;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
 use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;

+ 17 - 0
ot_stats/Configuration/RequestMiddlewares.php

@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * Register middlewares, which will be triggered at each request
+ */
+return [
+    'frontend' => [
+        'matomo-integration' => [
+            'target' => Opentalent\OtStats\Middleware\RequestHandler::class,
+            'before' => [
+            ],
+            'after' => [
+                'typo3/cms-frontend/output-compression'
+            ],
+        ],
+    ],
+];

+ 3 - 0
ot_stats/Readme.md

@@ -3,6 +3,8 @@
 
 Extension de suivi des statistiques typo3.
 
+**Pré-requis**: Cette extension a été développée pour fonctionner avec Matomo 4.0.0. 
+
 |||
 |---|---|
 | Extension key | ot_stats |
@@ -19,3 +21,4 @@ Cette Id est ensuite stocké dans un champs dédié de la table 'pages'.
 Si un Id de site matomo est enregistré, l'extension intégrera alors le script de suivi matomo aux
 pages du site. Une bannière sera également présentée aux nouveaux visiteurs les avertissant de l'usage 
 de cookies de suivi et demandant leur accord. 
+

+ 3 - 6
ot_stats/Resources/Private/Layouts/Backend/Default.html

@@ -1,9 +1,6 @@
 {namespace v=FluidTYPO3\Vhs\ViewHelpers}
 
-<f:be.container includeCssFiles="{ot_customizer: '{f:uri.resource(path:\'assets/Backend/style/ot_customizer.css\')}'}">
-    <div class="ot-templating">
-        <f:render section="content" />
-    </div>
+<f:be.container includeCssFiles="{ot_core: '{f:uri.resource(path:\'assets/Backend/style/ot_be_module.css\', extensionName:\'ot_core\')}',
+                                  ot_stats: '{f:uri.resource(path:\'assets/Backend/style/ot_stats.css\')}'}">
+    <f:render section="content" />
 </f:be.container>
-
-

+ 28 - 0
ot_stats/Resources/Private/Templates/Header/Matomo.html

@@ -0,0 +1,28 @@
+{namespace v=FluidTYPO3\Vhs\ViewHelpers}
+{namespace st=Opentalent\OtStats\ViewHelpers}
+
+<f:comment>
+    <!-- render the matomo script for the current website (if stats are enabled) -->
+</f:comment>
+
+<f:comment><!--Matomo partial: contains the integration code of matomo ...--></f:comment>
+
+<v:variable.set name="matomo_site_id" value="{st:matomoSiteId()}" />
+
+<f:comment><!-- Matomo --></f:comment>
+<script type="text/javascript">
+    var _paq = window._paq || [];
+    _paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
+    _paq.push(["setDomains", ["*.opentalent.fr"]]);
+    _paq.push(['trackPageView']);
+    _paq.push(['enableLinkTracking']);
+    (function() <f:format.raw>{</f:format.raw>
+    var u="https://stats.2iopenservice.com/";
+    _paq.push(['setTrackerUrl', u+'matomo.php']);
+    _paq.push(['setSiteId', '{matomo_site_id}']);
+    var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
+    g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
+    <f:format.raw>}</f:format.raw>)();
+</script>
+<noscript><p><img src="https://stats.2iopenservice.com/matomo.php?idsite={matomo_site_id}&amp;rec=1" style="border:0;" alt="" /></p></noscript>
+<f:comment><!-- End Matomo Code --></f:comment>

+ 28 - 0
ot_stats/Resources/Private/Templates/OtStats/ConfirmDeletion.html

@@ -0,0 +1,28 @@
+<f:layout name="Backend/Default" />
+
+<f:section name="content">
+
+    <div class="ot-be-module ot-stats">
+        <div class="statsConfirmationPanel">
+
+            <p>
+                Vous vous apprêtez à désactiver le suivi de l'utilisation du site. Si vous confirmez, plus aucune
+                donnée de suivi ne sera enregistrée tant que vous ne l'aurez pas réactivé.
+            </p>
+            <p>
+                <i>Notez que les données collectées jusqu'ici ne seront pas supprimées.</i>
+            </p>
+
+            <f:link.action
+                    action="disableStats"
+                    title="select"
+                    class="ot-btn"
+                    arguments="{rootUid: rootUid}"
+            >
+                Désactiver le suivi de l'utilisation
+            </f:link.action>
+
+        </div>
+    </div>
+
+</f:section>

+ 73 - 11
ot_stats/Resources/Private/Templates/OtStats/Index.html

@@ -5,29 +5,91 @@
 
 <f:section name="content">
 
-    <div class="ot-stats">
-        <f:if condition="{pageSelected}">
+    <div class="ot-be-module ot-stats">
+        <p class="warning">{errorMsg}</p>
+
+        <f:if condition="{pageSelected}>0">
             <f:then>
                 <h3>Statistiques d'utilisation</h3>
 
                 <f:if condition="{statsActivated}">
                     <f:then>
+                        <div id="matomoReport">
+
+                            <h4>Récapitulatif des visites</h4>
+                            <div class="reportSection">
+
+                                <div class="reportSubSection">
+                                    <f:comment><!-- Real time visits--></f:comment>
+                                    <div class="widgetIframe" id="realTimeVisits"><iframe width="450" height="320" src="https://stats.2iopenservice.com/index.php?module=Widgetize&action=iframe&disableLink=0&widget=1&moduleToWidgetize=Live&actionToWidgetize=widget&idSite=5&period=month&date=today&disableLink=1&widget=1" scrolling="yes" frameborder="0" marginheight="0" marginwidth="0"></iframe></div>
+
+                                    <f:comment><!-- Last visits graph--></f:comment>
+                                    <div class="widgetIframe" id="lastVisitsGraph"><iframe width="450" height="260" src="https://stats.2iopenservice.com/index.php?module=Widgetize&action=iframe&forceView=1&viewDataTable=graphEvolution&disableLink=0&widget=1&moduleToWidgetize=VisitsSummary&actionToWidgetize=getEvolutionGraph&idSite=5&period=month&date=today&disableLink=1&widget=1" scrolling="yes" frameborder="0" marginheight="0" marginwidth="0"></iframe></div>
+                                </div>
+
+                                <f:comment><!-- Visits Summary --></f:comment>
+                                <div class="widgetIframe" id="visitsSummary"><iframe width="450" height="600" src="https://stats.2iopenservice.com/index.php?module=Widgetize&action=iframe&forceView=1&viewDataTable=sparklines&disableLink=0&widget=1&moduleToWidgetize=VisitsSummary&actionToWidgetize=get&idSite=5&period=month&date=today&disableLink=1&widget=1" scrolling="yes" frameborder="0" marginheight="0" marginwidth="0"></iframe></div>
+                            </div>
+
+                            <h4>Répartition géographique et horaire</h4>
+                            <div class="reportSection">
+                                <f:comment><!-- By weekday --></f:comment>
+                                <div class="widgetIframe" id="visitsByWeekday"><iframe width="500" height="450" src="https://stats.2iopenservice.com/index.php?module=Widgetize&action=iframe&disableLink=0&widget=1&moduleToWidgetize=VisitTime&actionToWidgetize=getByDayOfWeek&idSite=5&period=month&date=today&disableLink=1&widget=1" scrolling="yes" frameborder="0" marginheight="0" marginwidth="0"></iframe></div>
+
+                                <f:comment><!-- By local hour --></f:comment>
+                                <div class="widgetIframe" id="visitsByHour"><iframe width="500" height="450" src="https://stats.2iopenservice.com/index.php?module=Widgetize&action=iframe&disableLink=0&widget=1&moduleToWidgetize=VisitTime&actionToWidgetize=getByDayOfWeek&idSite=5&period=month&date=today&disableLink=1&widget=1" scrolling="yes" frameborder="0" marginheight="0" marginwidth="0"></iframe></div>
 
-                        <iframe></iframe>
+                                <f:comment><!-- Map --></f:comment>
+                                <div class="widgetIframe" id="visitsMap"><iframe width="400" height="450" src="https://stats.2iopenservice.com/index.php?module=Widgetize&action=iframe&disableLink=0&widget=1&moduleToWidgetize=UserCountryMap&actionToWidgetize=visitorMap&idSite=5&period=month&date=today&disableLink=1&widget=1" scrolling="yes" frameborder="0" marginheight="0" marginwidth="0"></iframe></div>
+                            </div>
 
+                            <h4>Supports et référents</h4>
+                            <div class="reportSection">
+                                <f:comment><!-- Refferers --></f:comment>
+                                <div class="widgetIframe" id="visitsRefferers"><iframe width="360" height="450" src="https://stats.2iopenservice.com/index.php?module=Widgetize&action=iframe&disableLink=0&widget=1&moduleToWidgetize=Referrers&actionToWidgetize=getAll&idSite=5&period=month&date=today&disableLink=1&widget=1" scrolling="yes" frameborder="0" marginheight="0" marginwidth="0"></iframe></div>
+
+                                <f:comment><!-- By web-browsers   --></f:comment>
+                                <div class="widgetIframe" id="visitsByBrowsers"><iframe width="360" height="450" src="https://stats.2iopenservice.com/index.php?module=Widgetize&action=iframe&disableLink=0&widget=1&moduleToWidgetize=DevicesDetection&actionToWidgetize=getBrowsers&idSite=5&period=month&date=today&disableLink=1&widget=1" scrolling="yes" frameborder="0" marginheight="0" marginwidth="0"></iframe></div>
+                            </div>
+
+                            <h4>Comportements des visiteurs</h4>
+                            <div class="reportSection">
+                                <f:comment><!-- By page   --></f:comment>
+                                <div class="widgetIframe" id="visitsByPage"><iframe width="360" height="450" src="https://stats.2iopenservice.com/index.php?module=Widgetize&action=iframe&disableLink=0&widget=1&moduleToWidgetize=Actions&actionToWidgetize=getPageUrls&idSite=5&period=month&date=today&disableLink=1&widget=1" scrolling="yes" frameborder="0" marginheight="0" marginwidth="0"></iframe></div>
+
+                                <f:comment><!-- By entry page   --></f:comment>
+                                <div class="widgetIframe" id="visitorsEntryPoints"><iframe width="360" height="450" src="https://stats.2iopenservice.com/index.php?module=Widgetize&action=iframe&disableLink=0&widget=1&moduleToWidgetize=Actions&actionToWidgetize=getEntryPageUrls&idSite=5&period=month&date=today&disableLink=1&widget=1" scrolling="yes" frameborder="0" marginheight="0" marginwidth="0"></iframe></div>
+
+                                <f:comment><!-- By visits duration   --></f:comment>
+                                <div class="widgetIframe" id="visitsByDuration"><iframe width="360" height="450" src="https://stats.2iopenservice.com/index.php?module=Widgetize&action=iframe&disableLink=0&widget=1&moduleToWidgetize=VisitorInterest&actionToWidgetize=getNumberOfVisitsPerVisitDuration&idSite=5&period=month&date=today&disableLink=1&widget=1" scrolling="yes" frameborder="0" marginheight="0" marginwidth="0"></iframe></div>
+                            </div>
+
+                            <div class="reportFooter">
+                                <f:link.action
+                                        action="confirmDeletion"
+                                        title="select"
+                                        id="disableLink"
+                                        class="warning"
+                                >
+                                    Désactiver le suivi de l'utilisation
+                                </f:link.action>
+                            </div>
+
+                        </div>
                     </f:then>
                     <f:else>
-                        <div class="statsActivationPanel">
+                        <div class="statsConfirmationPanel">
                             <p>
-                                Le suivi des statistiques d'utilisation n'est pas activé.
-                                Si vous voulez activer le suivi de l'utilisation, cliquez sur le bouton ci-dessous.
+                                Le suivi des statistiques d'utilisation n'est pas encore activé.
+                                Pour l'activer, cliquez sur le bouton ci-dessous.
                             </p>
                             <p class="warning">
-                                Attention: Bien que les données collectées ici soient anonymisées, la loi impose
-                                malgré tout de demander l'accord des utilisateurs avant l'installation de cookies de suivi.
-                                Si vous activez le suivi des statistiques, une bannière sera présentée à chaque nouveau
-                                visiteur, qui pourra alors donner son accord ou s'opposer au suivi en question.
+                                <b>Attention:</b> La loi européenne impose de demander l'accord des
+                                utilisateurs avant l'installation de cookies de suivi. Dès lors que le suivi sera activé,
+                                une bannière sera présentée à chaque nouveau visiteur pour lui demander son accord
+                                quand à l'utilisation ou non de cookies de suivi.
                             </p>
+                            <i style="margin-bottom: 12px;"></i>
 
                             <f:link.action
                                     action="enableStats"
@@ -44,7 +106,7 @@
             <f:else>
                 <div class="no-page-warning">
                     <f:comment><!-- No page selected --></f:comment>
-                    <f:translate key="noPageSelected" />
+                    <f:translate key="noPageSelected" extensionName="ot_core" />
                 </div>
             </f:else>
         </f:if>

+ 39 - 9
ot_stats/Resources/Public/assets/Backend/style/ot_stats.css

@@ -1,20 +1,50 @@
 
 .ot-stats {
+    margin: 20px 3%;
+}
+
+.statsConfirmationPanel {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    border: solid 1px #cccccc;
+    padding: 22px;
+    margin: 0 15%;
+}
+
+.warning {
+    color: #800000;
+}
+
+#matomoReport {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+}
+
+#matomoReport .reportSection {
     display: flex;
     flex-direction: row;
+    flex-wrap: wrap;
 }
 
-.ot-stats h3 {
-    font-weight: 500;
-    font-size: 1.35em;
-    line-height: 1.2em;
+#matomoReport .reportSubSection {
+    display: flex;
+    flex-direction: column;
 }
 
+#matomoReport .widgetIframe {
+    margin: 0 24px;
+}
 
-.ot-stats .no-page-warning {
-    margin: 36px;
-    padding: 12px;
-    background-color: #e6e6e6;
-    border: solid 1px #666666;
+.reportFooter {
+    display: flex;
+    flex-direction: row;
+    justify-content: flex-end;
+    margin-top: 54px;
 }
 
+#disableLink {
+    color: #800000;
+    text-decoration: underline;
+}

+ 3 - 3
ot_stats/ext_tables.php

@@ -13,10 +13,10 @@ call_user_func(
             \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule(
                 'Opentalent.OtStats',
                 'web', // Make module a submodule of 'web'
-                'ot_stats', // Submodule key
-                'after:ot_customizer', // Position
+                'otstats', // Submodule key
+                'after:OtTemplatingOtcustomizer', // Position
                 array(
-                    'OtStats' => 'index,enableStats,disableStats',
+                    'OtStats' => 'index,enableStats,disableStats,confirmDeletion',
                 ),
                 array(
                     'access' => 'user,group',