VYPR
Medium severity6.5OSV Advisory· Published Oct 3, 2025· Updated Apr 15, 2026

CVE-2025-57423

CVE-2025-57423

Description

A SQL injection vulnerability was discovered in the /articles endpoint of MyClub 0.5, affecting the query parameters Content, GroupName, PersonName, lastUpdate, pool, and title. Due to insufficient input sanitisation, an unauthenticated remote attacker could inject arbitrary SQL commands via a crafted GET request, potentially leading to information disclosure or manipulation of the database.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

An unauthenticated SQL injection vulnerability in MyClub 0.5 allows attackers to execute arbitrary SQL commands via the /articles endpoint, leading to full database compromise.

Vulnerability

Analysis

A SQL injection vulnerability (CVE-2025-57423) exists in MyClub version 0.5, specifically within the /articles endpoint. The vulnerability affects six GET parameters: Content, GroupName, PersonName, lastUpdate, pool, and title [1]. Due to insufficient input sanitization, unauthenticated remote attackers can inject arbitrary SQL commands by crafting a malicious GET request [1].

Exploitation

Details

An attacker can exploit this vulnerability with no authentication required. A simple proof-of-concept injection using a single quote (') in the PersonName parameter immediately reveals the SQL error, exposing the query structure, file paths, and database engine (SQLite) [1]. Adding a second quote bypasses the error, confirming that arbitrary SQL commands can be injected [1]. The vulnerability allows attackers to extract the entire database schema, revealing 28 sensitive tables such as Person, Authorization, Group, Settings, and Message [1].

Impact

Successful exploitation can lead to full database access, including potential information disclosure, data manipulation, privilege escalation, and even denial of service [1].

Mitigation

The vulnerability was responsibly disclosed by William Fieldhouse of Aardwolf Security and has been patched in subsequent commits [2][3][4]. The developer addressed the issue by removing unsanitized Fluent query patterns and implementing parameterized queries in affected endpoints, as shown in commits 5741f39 and f067bb6 [2][3]. Users are strongly advised to update to the latest patched version of MyClub.

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Jebissey/MyclubOSV2 versions
    V0.1+ 1 more
    • (no CPE)range: V0.1
    • (no CPE)range: = 0.5

Patches

2
f067bb63ac7d

big refactoring fixes (work in progress)

https://github.com/jebissey/myclubjebisseyAug 2, 2025via osv
1 file changed · +11 9
  • WebSite/app/helpers/LogDataHelper.php+11 9 modified
    @@ -682,17 +682,19 @@ public function getVisitedPages($perPage, $logPage)
             return [$query->fetchAll(), ceil($total / $perPage)];
         }
     
    -    public function getPersons($filteredPersonEmails)
    +    public function getPersons(array $filteredPersonEmails): array
         {
    -        $filteredPersonEmails = array_filter($filteredPersonEmails);
    -        $query = $this->fluent->from('Person')
    -            ->select(null)
    -            ->select('LOWER(Email) AS Email, FirstName, LastName');
    -        if (!empty($filteredPersonEmails)) {
    -            $placeholders = implode(',', array_fill(0, count($filteredPersonEmails), '?'));
    -            $query->where("Email COLLATE NOCASE IN ($placeholders)", $filteredPersonEmails);
    +        $emails = array_filter(array_map('trim', array_values($filteredPersonEmails)));
    +        $sql = "SELECT LOWER(Email) AS Email, FirstName, LastName FROM Person";
    +        $params = [];
    +        if (!empty($emails)) {
    +            $placeholders = implode(',', array_fill(0, count($emails), '?'));
    +            $sql .= " WHERE LOWER(Email) IN ($placeholders)";
    +            $params = array_map('strtolower', $emails);
             }
    -        return $query->fetchAll();
    +        $stmt = $this->pdo->prepare($sql);
    +        $stmt->execute($params);
    +        return $stmt->fetchAll();
         }
     
         public function getTopArticles(string $dateCondition, int $top): array
    
5741f39cf022

Big refactoring fixes and fluent removing (work in progress)

https://github.com/jebissey/myclubjebisseyAug 2, 2025via osv
22 files changed · +364 485
  • dev/detect_circular_dependencies.php+0 116 removed
    @@ -1,116 +0,0 @@
    -<?php
    -
    -$root = __DIR__ . '/../WebSite/app';
    -
    -function getPhpFiles($dir)
    -{
    -    $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
    -    $files = [];
    -    foreach ($rii as $file) {
    -        if (!$file->isDir() && pathinfo($file, PATHINFO_EXTENSION) === 'php') $files[] = $file->getPathname();
    -    }
    -    return $files;
    -}
    -
    -function parseFileDependencies($filePath, $root)
    -{
    -    $content = file_get_contents($filePath);
    -    $dependencies = [];
    -
    -    // Normalize slashes
    -    $normalizedPath = str_replace($root . '/', '', $filePath);
    -
    -    // Match `use Some\Namespace\ClassName;`
    -    if (preg_match_all('/use\s+([a-zA-Z0-9_\\\\]+)\s*;/', $content, $matches)) {
    -        foreach ($matches[1] as $match) {
    -            $dependencies[] = $match;
    -        }
    -    }
    -
    -    // Match new ClassName() or \Fully\Qualified\ClassName
    -    if (preg_match_all('/new\s+(\\\?[a-zA-Z0-9_\\\\]+)/', $content, $matches2)) {
    -        foreach ($matches2[1] as $match) {
    -            $dependencies[] = ltrim($match, '\\');
    -        }
    -    }
    -
    -    // Also match static calls like ClassName::method()
    -    if (preg_match_all('/([a-zA-Z0-9_\\\\]+)::/', $content, $matches3)) {
    -        foreach ($matches3[1] as $match) {
    -            if (!in_array($match, $dependencies)) {
    -                $dependencies[] = $match;
    -            }
    -        }
    -    }
    -
    -    return [$normalizedPath, array_unique($dependencies)];
    -}
    -
    -function buildDependencyGraph($files, $root)
    -{
    -    $graph = [];
    -
    -    foreach ($files as $file) {
    -        list($filename, $deps) = parseFileDependencies($file, $root);
    -        $graph[$filename] = [];
    -
    -        foreach ($deps as $dep) {
    -            $graph[$filename][] = $dep;
    -        }
    -    }
    -
    -    return $graph;
    -}
    -
    -function detectCycles($graph)
    -{
    -    $visited = [];
    -    $recStack = [];
    -    $cycles = [];
    -
    -    foreach (array_keys($graph) as $node) {
    -        dfs($node, $graph, $visited, $recStack, [], $cycles);
    -    }
    -
    -    return $cycles;
    -}
    -
    -function dfs($node, $graph, &$visited, &$recStack, $path, &$cycles)
    -{
    -    if (isset($recStack[$node]) && $recStack[$node]) {
    -        $cycleStart = array_search($node, $path);
    -        if ($cycleStart !== false) {
    -            $cycle = array_slice($path, $cycleStart);
    -            $cycles[] = $cycle;
    -        }
    -        return;
    -    }
    -
    -    if (isset($visited[$node])) return;
    -
    -    $visited[$node] = true;
    -    $recStack[$node] = true;
    -    $path[] = $node;
    -
    -    foreach ($graph[$node] ?? [] as $neighbor) {
    -        dfs($neighbor, $graph, $visited, $recStack, $path, $cycles);
    -    }
    -
    -    $recStack[$node] = false;
    -}
    -
    -// --- Main ---
    -echo "🔍 Analyse des dépendances...\n";
    -
    -$files = getPhpFiles($root);
    -$graph = buildDependencyGraph($files, $root);
    -$cycles = detectCycles($graph);
    -
    -if (empty($cycles)) {
    -    echo "✅ Aucune dépendance circulaire détectée.\n";
    -} else {
    -    echo "⚠️ Dépendances circulaires détectées :\n";
    -    foreach ($cycles as $cycle) {
    -        echo " - " . implode(' → ', $cycle) . " → " . $cycle[0] . "\n";
    -    }
    -}
    
  • dev/LinesByFiles.txt+95 60 modified
    @@ -1,5 +1,5 @@
     === Détail par fichier ===
    -   331 ../WebSite/index.php
    +   326 ../WebSite/index.php
         65 ../WebSite/app/views/dbbrowser/edit.latte
         43 ../WebSite/app/views/dbbrowser/index.latte
        181 ../WebSite/app/views/dbbrowser/table.latte
    @@ -17,8 +17,8 @@
         40 ../WebSite/app/views/eventType/attributes-list.latte
         27 ../WebSite/app/views/persons/index.latte
         32 ../WebSite/app/views/navbar/redactor.latte
    -    52 ../WebSite/app/views/navbar/user.latte
    -    73 ../WebSite/app/views/navbar/home.latte
    +    66 ../WebSite/app/views/navbar/user.latte
    +    87 ../WebSite/app/views/navbar/home.latte
        128 ../WebSite/app/views/navbar/index.latte
         32 ../WebSite/app/views/navbar/eventManager.latte
         29 ../WebSite/app/views/navbar/personManager.latte
    @@ -34,9 +34,9 @@
        190 ../WebSite/app/views/event/needs.latte
        354 ../WebSite/app/views/event/nextEvents.latte
        121 ../WebSite/app/views/event/chat.latte
    -   188 ../WebSite/app/views/event/weekEvents.latte
    +   187 ../WebSite/app/views/event/weekEvents.latte
         63 ../WebSite/app/views/contact.latte
    -    60 ../WebSite/app/views/logs/referer.latte
    +    60 ../WebSite/app/views/logs/referent.latte
        153 ../WebSite/app/views/logs/crossTab.latte
         80 ../WebSite/app/views/logs/topPages.latte
        178 ../WebSite/app/views/logs/analytics.latte
    @@ -55,7 +55,7 @@
        124 ../WebSite/app/views/user/presentation.latte
        142 ../WebSite/app/views/user/ffaSearch.latte
          4 ../WebSite/app/views/user/user.latte
    -    98 ../WebSite/app/views/user/articles.latte
    +   100 ../WebSite/app/views/user/articles.latte
         30 ../WebSite/app/views/user/groups.latte
         43 ../WebSite/app/views/user/map.latte
         97 ../WebSite/app/views/user/editPresentation.latte
    @@ -74,35 +74,42 @@
         43 ../WebSite/app/views/media/upload.latte
         27 ../WebSite/app/views/emails/copyToClipBoard.latte
         76 ../WebSite/app/views/emails/getEmails.latte
    -   129 ../WebSite/app/views/common/dataTable.latte
    +   132 ../WebSite/app/views/common/dataTable.latte
        156 ../WebSite/app/views/common/crosstab.latte
         12 ../WebSite/app/views/info.latte
        106 ../WebSite/app/views/import/form.latte
         60 ../WebSite/app/views/contact/registration-success.latte
    -   164 ../WebSite/app/api/WebmasterApi.php
    -   105 ../WebSite/app/api/CarouselApi.php
    -   268 ../WebSite/app/api/ArticleApi.php
    -   706 ../WebSite/app/api/EventApi.php
    -   101 ../WebSite/app/controllers/EmailController.php
    -   319 ../WebSite/app/controllers/DbBrowserController.php
    -   299 ../WebSite/app/controllers/BaseController.php
    -   699 ../WebSite/app/controllers/UserController.php
    -    97 ../WebSite/app/controllers/NavBarController.php
    -   280 ../WebSite/app/controllers/WebmasterController.php
    -   124 ../WebSite/app/controllers/SurveyController.php
    -    39 ../WebSite/app/controllers/FFAController.php
    -   128 ../WebSite/app/controllers/EventTypeController.php
    -   161 ../WebSite/app/controllers/MediaController.php
    -   414 ../WebSite/app/controllers/EventController.php
    -   148 ../WebSite/app/controllers/ImportController.php
    -   323 ../WebSite/app/controllers/PersonController.php
    -   340 ../WebSite/app/controllers/LogController.php
    -   139 ../WebSite/app/controllers/RegistrationController.php
    -   351 ../WebSite/app/controllers/ArticleController.php
    -    44 ../WebSite/app/controllers/TableController.php
    -    85 ../WebSite/app/controllers/DesignController.php
    -    11 ../WebSite/app/controllers/CrudControllerInterface.php
    -   165 ../WebSite/app/controllers/GroupController.php
    +    10 ../WebSite/app/enums/EventAudience.php
    +    12 ../WebSite/app/enums/Authorization.php
    +    16 ../WebSite/app/enums/ApplicationError.php
    +    80 ../WebSite/app/controllers/EmailController.php
    +   107 ../WebSite/app/controllers/DbBrowserController.php
    +   100 ../WebSite/app/controllers/BaseController.php
    +   526 ../WebSite/app/controllers/UserController.php
    +    72 ../WebSite/app/controllers/NavBarController.php
    +   233 ../WebSite/app/controllers/WebmasterController.php
    +   105 ../WebSite/app/controllers/SurveyController.php
    +    43 ../WebSite/app/controllers/FFAController.php
    +    91 ../WebSite/app/controllers/EventTypeController.php
    +   130 ../WebSite/app/controllers/MediaController.php
    +   349 ../WebSite/app/controllers/EventController.php
    +    88 ../WebSite/app/controllers/ImportController.php
    +   234 ../WebSite/app/controllers/PersonController.php
    +   161 ../WebSite/app/controllers/LogController.php
    +    63 ../WebSite/app/controllers/RegistrationController.php
    +   250 ../WebSite/app/controllers/ArticleController.php
    +    58 ../WebSite/app/controllers/TableController.php
    +    59 ../WebSite/app/controllers/DesignController.php
    +    99 ../WebSite/app/controllers/GroupController.php
    +     9 ../WebSite/app/interfaces/NewsProviderInterface.php
    +    10 ../WebSite/app/interfaces/DatabaseMigratorInterface.php
    +    11 ../WebSite/app/interfaces/CrudControllerInterface.php
    +   120 ../WebSite/app/apis/WebmasterApi.php
    +    19 ../WebSite/app/apis/ImportApi.php
    +    84 ../WebSite/app/apis/CarouselApi.php
    +   140 ../WebSite/app/apis/ArticleApi.php
    +    45 ../WebSite/app/apis/BaseApi.php
    +   405 ../WebSite/app/apis/EventApi.php
         69 ../WebSite/app/js/layout.js
         55 ../WebSite/app/js/designs/index.js
         48 ../WebSite/app/js/designs/create.js
    @@ -117,7 +124,7 @@
        143 ../WebSite/app/js/event/needs.js
        539 ../WebSite/app/js/event/nextEvents.js
         23 ../WebSite/app/js/event/guest.js
    -    46 ../WebSite/app/js/logs/topArticles.js
    +    45 ../WebSite/app/js/logs/topArticles.js
        127 ../WebSite/app/js/admin/arwards.js
         39 ../WebSite/app/js/survey/add.js
         75 ../WebSite/app/js/user/preferences.js
    @@ -127,44 +134,72 @@
         22 ../WebSite/app/js/user/presentation.js
         55 ../WebSite/app/js/user/map.js
         86 ../WebSite/app/js/user/signIn.js
    -    27 ../WebSite/app/js/user/account.js
    +    23 ../WebSite/app/js/user/account.js
        119 ../WebSite/app/js/user/article.js
         55 ../WebSite/app/js/user/statistics.js
         52 ../WebSite/app/js/media/upload.js
         73 ../WebSite/app/js/media/list.js
         87 ../WebSite/app/js/import/form.js
    -    61 ../WebSite/app/helpers/Message.php
    -    10 ../WebSite/app/helpers/EventAudience.php
    -    28 ../WebSite/app/helpers/Settings.php
    -   148 ../WebSite/app/helpers/Authorization.php
    -    19 ../WebSite/app/helpers/PasswordManager.php
    -    52 ../WebSite/app/helpers/Arwards.php
    -    92 ../WebSite/app/helpers/database/Database.php
    -    17 ../WebSite/app/helpers/database/File.php
    -    82 ../WebSite/app/helpers/TranslationManager.php
    -    66 ../WebSite/app/helpers/Alert.php
    +    22 ../WebSite/app/helpers/NeedDataHelper.php
    +   221 ../WebSite/app/helpers/PersonDataHelper.php
    +   743 ../WebSite/app/helpers/LogDataHelper.php
    +   593 ../WebSite/app/helpers/EventDataHelper.php
    +    59 ../WebSite/app/helpers/CarouselDataHelper.php
    +    16 ../WebSite/app/helpers/Generic.php
    +    45 ../WebSite/app/helpers/ReplyDataHelper.php
    +    88 ../WebSite/app/helpers/PageDataHelper.php
    +    90 ../WebSite/app/helpers/Database.php
    +    60 ../WebSite/app/helpers/ImportDataHelper.php
    +   192 ../WebSite/app/helpers/GroupDataHelper.php
    +    32 ../WebSite/app/helpers/TableControllerHelper.php
    +    11 ../WebSite/app/helpers/DataHelper.php
    +   302 ../WebSite/app/helpers/ArticleDataHelper.php
    +   186 ../WebSite/app/helpers/Data.php
    +    78 ../WebSite/app/helpers/Period.php
    +    47 ../WebSite/app/helpers/ArwardsDataHelper.php
    +    47 ../WebSite/app/helpers/ApiNeedTypeDataHelper.php
    +    37 ../WebSite/app/helpers/database/migrators/V1ToV2Migrator.php
    +    85 ../WebSite/app/helpers/AttributeDataHelper.php
    +    68 ../WebSite/app/helpers/TranslationManager.php
        140 ../WebSite/app/helpers/FFAScraper.php
    -    75 ../WebSite/app/helpers/Client.php
    +    74 ../WebSite/app/helpers/Client.php
    +   228 ../WebSite/app/helpers/DbBrowserHelper.php
        102 ../WebSite/app/helpers/Backup.php
    -   192 ../WebSite/app/helpers/News.php
    +    99 ../WebSite/app/helpers/SurveyDataHelper.php
    +    26 ../WebSite/app/helpers/ParticipantDataHelper.php
    +    28 ../WebSite/app/helpers/News.php
         36 ../WebSite/app/helpers/GravatarHandler.php
    -   504 ../WebSite/app/helpers/PersonStatistics.php
    -   138 ../WebSite/app/helpers/Application.php
    -    38 ../WebSite/app/helpers/BaseHelper.php
    -   214 ../WebSite/app/helpers/Article.php
    -   647 ../WebSite/app/helpers/Log.php
    -    96 ../WebSite/app/helpers/Crosstab.php
    -   119 ../WebSite/app/helpers/PersonPreferences.php
    -    14 ../WebSite/app/helpers/Params.php
    -   430 ../WebSite/app/helpers/Event.php
    -    41 ../WebSite/app/helpers/Person.php
    -   127 ../WebSite/app/helpers/Email.php
    - 19484 total
    +   498 ../WebSite/app/helpers/PersonStatistics.php
    +   115 ../WebSite/app/helpers/Application.php
    +    86 ../WebSite/app/helpers/Sign.php
    +   111 ../WebSite/app/helpers/CrosstabDataHelper.php
    +    61 ../WebSite/app/helpers/ArticleTableDataHelper.php
    +    24 ../WebSite/app/helpers/File.php
    +    97 ../WebSite/app/helpers/AuthorizationDataHelper.php
    +   113 ../WebSite/app/helpers/ConnectedUser.php
    +   107 ../WebSite/app/helpers/MessageDataHelper.php
    +    52 ../WebSite/app/helpers/WebApp.php
    +    28 ../WebSite/app/helpers/ApiImportHelper.php
    +    42 ../WebSite/app/helpers/ApiNeedDataHelper.php
    +    90 ../WebSite/app/helpers/DesignDataHelper.php
    +    82 ../WebSite/app/helpers/ApiEventDataHelper.php
    +    19 ../WebSite/app/helpers/LanguagesDataHelper.php
    +    40 ../WebSite/app/helpers/PersonGroupDataHelper.php
    +    76 ../WebSite/app/helpers/Media.php
    +    69 ../WebSite/app/helpers/PersonPreferences.php
    +    42 ../WebSite/app/helpers/ArticleCrosstabDataHelper.php
    +    51 ../WebSite/app/helpers/Params.php
    +    31 ../WebSite/app/helpers/EventTypeDataHelper.php
    +    19 ../WebSite/app/helpers/Password.php
    +    21 ../WebSite/app/helpers/EventNeedHelper.php
    +    78 ../WebSite/app/helpers/ErrorManager.php
    +    53 ../WebSite/app/helpers/Email.php
    + 19997 total
     
     === Total par type de fichier ===
     Fichiers PHP :
    -9329
    +9815
     Fichiers Latte :
    -7064
    +7096
     Fichiers Javascript :
    -3091
    +3086
    
  • WebSite/app/apis/CarouselApi.php+8 12 modified
    @@ -7,28 +7,25 @@
     
     use app\helpers\Application;
     use app\helpers\CarouselDataHelper;
    +use app\helpers\DataHelper;
     use app\helpers\WebApp;
     
     class CarouselApi extends BaseApi
     {
    -    private AuthorizationDataHelper $authorizationDataHelper;
    -    private CarouselDataHelper $carouselDataHelper;
     
         public function __construct(Application $application)
         {
             parent::__construct($application);
    -        $this->authorizationDataHelper = new AuthorizationDataHelper($application);
    -        $this->carouselDataHelper = new CarouselDataHelper($this->application);
         }
     
         public function getItems($idArticle)
         {
             $person = $this->connectedUser->get()->person ?? false;
    -        if (!$person || !$this->authorizationDataHelper->getArticle($idArticle, $person)) {
    +        if (!$person || !(new AuthorizationDataHelper($this->application))->getArticle($idArticle, $person)) {
                 $this->renderJson(['error' => 'Accès non autorisé'], 403);
                 return;
             }
    -        $items = $this->carouselDataHelper->getByArticle($idArticle);
    +        $items = $this->dataHelper->gets('Carousel', ['IdArticle' => $idArticle]);
             $this->renderJson(['success' => true, 'items' => $items]);
         }
     
    @@ -44,14 +41,13 @@ public function saveItem()
                 $this->renderJson(['error' => 'Données invalides'], 400);
                 return;
             }
    -        if (!$this->authorizationDataHelper->getArticle($data['idArticle'], $person)) {
    +        if (!(new AuthorizationDataHelper($this->application))->getArticle($data['idArticle'], $person)) {
                 $this->renderJson(['error' => 'Vous n\'êtes pas autorisé à modifier cet article'], 403);
                 return;
             }
    -
             $item = WebApp::sanitizeHtml($data['item']);
             try {
    -            $message = $this->carouselDataHelper->set_($data, $item);
    +            $message = (new CarouselDataHelper($this->application))->set_($data, $item);
                 $this->renderJson(['success' => true, 'message' => $message]);
             } catch (Throwable $e) {
                 $this->renderJson(['error' => 'Erreur lors de l\'enregistrement: ' . $e->getMessage()], 500);
    @@ -65,17 +61,17 @@ public function deleteItem($id)
                 $this->renderJson(['error' => 'Utilisateur non connecté'], 401);
                 return;
             }
    -        $item = $this->carouselDataHelper->get_($id);
    +        $item = (new DataHelper($this->application))->get('Carousel', ['Id' => $id]);
             if (!$item) {
                 $this->renderJson(['error' => 'Élément non trouvé'], 404);
                 return;
             }
    -        if (!$this->authorizationDataHelper->getArticle($item->IdArticle, $person)) {
    +        if (!(new AuthorizationDataHelper($this->application))->getArticle($item->IdArticle, $person)) {
                 $this->renderJson(['error' => 'Vous n\'êtes pas autorisé à modifier cet article'], 403);
                 return;
             }
             try {
    -            $this->carouselDataHelper->delete_($id);
    +            (new DataHelper($this->application))->delete('Carousel', ['Id' => $id]);
                 $this->renderJson(['success' => true, 'message' => 'Élément supprimé avec succès']);
             } catch (Throwable $e) {
                 $this->renderJson(['error' => 'Erreur lors de la suppression: ' . $e->getMessage()], 500);
    
  • WebSite/app/apis/WebmasterApi.php+1 1 modified
    @@ -32,7 +32,7 @@ public function addToGroup($personId, $groupId)
             } else $this->renderJson(['success' => false, 'message' => 'User not allowed'], 403);
         }
     
    -    public function getPersonsInGroup($id)
    +    public function getPersonsInGroup(?int $id): void
         {
             if ($this->connectedUser->get()->person ?? false) {
                 if ($_SERVER['REQUEST_METHOD'] === 'GET') {
    
  • WebSite/app/controllers/ArticleController.php+3 3 modified
    @@ -11,7 +11,7 @@
     use app\helpers\ArticleTableDataHelper;
     use app\helpers\AuthorizationDataHelper;
     use app\helpers\Backup;
    -use app\helpers\CarouselDataHelper;
    +use app\helpers\DataHelper;
     use app\helpers\Params;
     use app\helpers\Period;
     use app\helpers\PersonDataHelper;
    @@ -121,13 +121,13 @@ public function show($id): void
                     'latestArticles' => $this->articleDataHelper->getLatestArticles_($articleIds),
                     'canEdit' => $canEdit,
                     'groups' => $this->dataHelper->gets('Group', ['Inactivated' => 0], 'Id, Name', 'Name'),
    -                'hasSurvey' => $this->dataHelper->get('Survey',['IdArticle' => $id]),
    +                'hasSurvey' => $this->dataHelper->get('Survey', ['IdArticle' => $id]),
                     'id' => $id,
                     'userConnected' => $this->connectedUser->person,
                     'navItems' => $this->getNavItems($this->connectedUser->person),
                     'publishedBy' => $chosenArticle->PublishedBy && $chosenArticle->PublishedBy != $chosenArticle->CreatedBy ? (new PersonDataHelper($this->application))->getPublisher($chosenArticle->PublishedBy) : '',
                     'canReadPool' => $this->authorizationDatahelper->canPersonReadSurveyResults($chosenArticle, $this->connectedUser->person),
    -                'carouselItems' => (new CarouselDataHelper($this->application))->getsForArticle($id),
    +                'carouselItems' => (new DataHelper($this->application))->gets('Carousel', ['IdArticle' => $id]),
                     'message' => $messages,
                 ]));
             } else if ($this->connectedUser->person == '') $this->application->getErrorManager()->raise(ApplicationError::NotAllowed, 'Il faut être connecté pour pouvoir consulter cet article', 5000);
    
  • WebSite/app/controllers/BaseController.php+4 0 modified
    @@ -96,5 +96,9 @@ private function addLatteFilters(): void
                 elseif ($bytes >= 1024)    return number_format($bytes / 1024, 2) . ' KB';
                 else                       return $bytes . ' bytes';
             });
    +
    +        $this->latte->addFilter('readableDuration', function ($duration) {
    +            return TranslationManager::getReadableDuration($duration);
    +        });
         }
     }
    
  • WebSite/app/controllers/EmailController.php+3 3 modified
    @@ -21,8 +21,8 @@ public function fetchEmails()
         {
             if ($this->connectedUser->get()->isEventManager() ?? false) {
                 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    -                $idGroup = $_POST['idGroup'] ?? '';
    -                $idEventType = $_POST['idEventType'] ?? '';
    +                $idGroup = isset($_POST['idGroup']) && is_numeric($_POST['idGroup']) ? (int)$_POST['idGroup'] : null;
    +                $idEventType = isset($_POST['idGroup']) && is_numeric($_POST['idGroup']) ? (int)$_POST['idEventType'] : null;
                     $dayOfWeek = $_POST['dayOfWeek'] ?? '';
                     $timeOfDay = $_POST['timeOfDay'] ?? '';
                     $filteredEmails = (new PersonDataHelper($this->application))->getEmailsOfInterestedPeople($idGroup, $idEventType, $dayOfWeek, $timeOfDay);
    @@ -34,7 +34,7 @@ public function fetchEmails()
                         'emailsJson' => json_encode($filteredEmails),
                         'emails' => $filteredEmails,
                         'filters' => "$groupName / $eventTypeName / $dayOfWeekName / $timeOfDay",
    -                    'phones' => $this->dataHelper->gets('Person', ['Inactivated' => 0], "Email', 'Phone'"),
    +                    'people' => $this->dataHelper->gets('Person', ['Inactivated' => 0], 'Email, Phone, FirstName, LastName', '', true),
                     ]));
                 } else if ($_SERVER['REQUEST_METHOD'] === 'GET') {
                     $this->render('app/views/emails/getEmails.latte', Params::getAll([
    
  • WebSite/app/controllers/EventController.php+7 7 modified
    @@ -37,9 +37,9 @@ public function nextEvents(): void
                 'navItems' => $this->getNavItems($this->connectedUser->get()->person ?? false),
                 'events' => $this->eventDataHelper->getEvents($this->connectedUser->person, $mode, $offset, $filterByPreferences),
                 'person' => $this->connectedUser->person,
    -            'eventTypes' => $this->dataHelper->gets('EventType', ['Inactivated' => 0], "'Id', 'Name'", 'Name'),
    -            'needTypes' => $this->dataHelper->gets('NeedType', [], "'Id', 'Name'", 'Name'),
    -            'eventAttributes' => $this->dataHelper->gets('Attribute', [], "'Id', 'Name, Detail, Color'"),
    +            'eventTypes' => $this->dataHelper->gets('EventType', ['Inactivated' => 0], 'Id, Name'),
    +            'needTypes' => $this->dataHelper->gets('NeedType', [], 'Id, Name'),
    +            'eventAttributes' => $this->dataHelper->gets('Attribute', [], 'Id, Name, Detail, Color'),
                 'offset' => $offset,
                 'mode' => $mode,
                 'filterByPreferences' => $filterByPreferences,
    @@ -51,8 +51,8 @@ public function weekEvents(): void
         {
             $this->render('app/views/event/weekEvents.latte', Params::getAll([
                 'events' => $this->eventDataHelper->getNextWeekEvents(),
    -            'eventTypes' => $this->dataHelper->gets('EventType', ['Inactivated', 0], "'Id', 'Name'", 'Name'),
    -            'eventAttributes' => $this->dataHelper->gets('Attribute', [], "'Id', 'Name, Detail, Color'"),
    +            'eventTypes' => $this->dataHelper->gets('EventType', ['Inactivated' => 0], 'Id, Name'),
    +            'eventAttributes' => $this->dataHelper->gets('Attribute', [], 'Id, Name, Detail, Color'),
                 'navItems' => $this->getNavItems($this->connectedUser->get()->person ?? false),
                 'layout' => WebApp::getLayout()
             ]));
    @@ -182,7 +182,7 @@ public function guestInvite()
             } else $this->application->getErrorManager()->raise(ApplicationError::NotAllowed, 'Page not allowed in file ' . __FILE__ . ' at line ' . __LINE__);
         }
     
    -    public function show($eventId, $message = null, $messageType = null): void
    +    public function show(int $eventId, string $message = '', string $messageType = ''): void
         {
             $person = $this->connectedUser->get()->person ?? false;
             $userEmail = $person->Email ?? '';
    @@ -196,7 +196,7 @@ public function show($eventId, $message = null, $messageType = null): void
                     'isRegistered' => $this->eventDataHelper->isUserRegistered($eventId, $userEmail),
                     'navItems' => $this->getNavItems($person),
                     'countOfMessages' => count($this->dataHelper->gets('Message', [
    -                    'From' => 'User',
    +                    '"From"' => 'User',
                         'EventId' => $eventId
                     ])),
                     'eventNeeds' => $this->eventDataHelper->getEventNeeds($eventId),
    
  • WebSite/app/controllers/LogController.php+1 1 modified
    @@ -129,7 +129,7 @@ public function crossTab()
                 $emailFilter = $_GET['email'] ?? '';
                 $groupFilter = $_GET['group'] ?? '';
                 $period = $_GET['period'] ?? 'today';
    -            [$sortedCrossTabData, $filteredPersons, $columnTotals] = (new CrosstabDataHelper($this->application))->getPersons(Period::getDateConditions($period));
    +            [$sortedCrossTabData, $filteredPersons, $columnTotals] = (new CrosstabDataHelper($this->application))->getPersons(Period::getDateConditions($period), $uriFilter, $emailFilter, $groupFilter);
     
                 $this->render('app/views/logs/crossTab.latte', Params::getAll([
                     'title' => 'Tableau croisé dynamique des visites',
    
  • WebSite/app/helpers/AttributeDataHelper.php+20 29 modified
    @@ -15,13 +15,8 @@ public function delete_($id)
         {
             try {
                 $this->pdo->beginTransaction();
    -            $this->fluent->deleteFrom('EventTypeAttribute')
    -                ->where('IdAttribute', $id)
    -                ->execute();
    -            $this->fluent->deleteFrom('Attribute')
    -                ->where('Id', $id)
    -                ->execute();
    -
    +            $this->delete('EventTypeAttribute', ['IdAttribute' => $id]);
    +            $this->delete('Attribute', ['Id' => $id]);
                 $this->pdo->commit();
                 return [['success' => true], 200];
             } catch (Throwable $e) {
    @@ -34,11 +29,11 @@ public function insert($data)
         {
             try {
                 $this->pdo->beginTransaction();
    -            $this->fluent->insertInto('Attribute', [
    +            $this->set('Attribute', [
                     'Name'   => $data['name'],
                     'Detail' => $data['detail'],
                     'Color'  => $data['color']
    -            ])->execute();
    +            ]);
                 $this->pdo->commit();
                 return [['success' => true], 200];
             } catch (Throwable $e) {
    @@ -47,34 +42,30 @@ public function insert($data)
             }
         }
     
    -    public function getAttributesOf($eventTypeId)
    +    public function getAttributesOf(int $eventTypeId): array
         {
    -        return $this->fluent->from('EventTypeAttribute')
    -            ->select('Attribute.*')
    -            ->join('Attribute ON EventTypeAttribute.IdAttribute = Attribute.Id')
    -            ->where('EventTypeAttribute.IdEventType', $eventTypeId)
    -            ->fetchall();
    +        $sql = '
    +            SELECT Attribute.*
    +            FROM EventTypeAttribute
    +            INNER JOIN Attribute ON EventTypeAttribute.IdAttribute = Attribute.Id
    +            WHERE EventTypeAttribute.IdEventType = :id
    +        ';
    +        $stmt = $this->pdo->prepare($sql);
    +        $stmt->execute([':id' => $eventTypeId]);
    +        return $stmt->fetchAll();
         }
     
    -    public function gets_()
    -    {
    -        return  $this->fluent->from('Attribute')
    -            ->orderBy('Name')
    -            ->fetchAll();
    -    }
    +
     
         public function update($data)
         {
             try {
                 $this->pdo->beginTransaction();
    -            $this->fluent->update('Attribute')
    -                ->set([
    -                    'Name'   => $data['name'],
    -                    'Detail' => $data['detail'],
    -                    'Color'  => $data['color']
    -                ])
    -                ->where('Id', $data['id'])
    -                ->execute();
    +            $this->set('Attribute', [
    +                'Name'   => $data['name'],
    +                'Detail' => $data['detail'],
    +                'Color'  => $data['color']
    +            ], ['Id' => $data['id']]);
                 $this->pdo->commit();
                 return ['success' => true];
             } catch (Throwable $e) {
    
  • WebSite/app/helpers/AuthorizationDataHelper.php+20 28 modified
    @@ -24,52 +24,47 @@ public function getsFor(ConnectedUser $connectedUser): array
             return array_column($query->fetchAll(), 'Name');
         }
     
    -    public function canPersonReadSurveyResults($article, $person): bool
    +    public function canPersonReadSurveyResults(object $article, object $person): bool
         {
    -        $survey = $this->fluent->from('Survey')->where('IdArticle', $article->Id)->fetch();
    -        if (!$survey || !$person) {
    -            return false;
    -        }
    -
    +        $survey = $this->get('Survey', ['IdArticle' => $article->Id]);
    +        if (!$survey || !$person) return false;
             $now = (new DateTime())->format('Y-m-d');
             $closingDate = $survey->ClosingDate;
    -
             if (
                 $article->CreatedBy == $person->Id
                 || $survey->Visibility == 'all'
                 || $survey->Visibility == 'allAfterClosing' && $closingDate < $now
    -        ) {
    -            return true;
    -        }
    -
    +        ) return true;
             $stmt = $this->pdo->prepare('SELECT COUNT(*) FROM Reply WHERE IdSurvey = ? AND IdPerson = ?');
             $stmt->execute([$survey->Id, $person->Id]);
             $hasVoted = $stmt->fetchColumn() > 0;
    -        if ($hasVoted && ($survey->Visibility == 'voters' || ($survey->Visibility == 'votersAfterClosing' && $closingDate < $now))) {
    +        if ($hasVoted && ($survey->Visibility == 'voters' || ($survey->Visibility == 'votersAfterClosing' && $closingDate < $now)))
                 return true;
    -        }
             return false;
         }
     
    -    public function getArticle($id, $connectedUser)
    +    public function getArticle($id, $connectedUser): object|false
         {
    -        $article = $this->fluent->from('Article')->where('Id', $id)->fetch();
    -        if (!$this->canReadArticle($article, $connectedUser)) {
    -            return false;
    -        }
    +        $article = $this->get('Article', ['Id' => $id]);
    +        if (!$this->canReadArticle($article, $connectedUser)) return false;
             return $article;
         }
     
         public function getUserGroups(string $userEmail): array
         {
    -        $rows = $this->fluent->from('PersonGroup')
    -            ->select('PersonGroup.IdGroup AS IdGroup')
    -            ->leftJoin('Person ON Person.Id = PersonGroup.IdPerson')
    -            ->where('Person.Email', $userEmail)
    -            ->fetchAll();
    +        $sql = '
    +            SELECT PersonGroup.IdGroup AS IdGroup
    +            FROM PersonGroup
    +            LEFT JOIN Person ON Person.Id = PersonGroup.IdPerson
    +            WHERE Person.Email COLLATE NOCASE = :email
    +        ';
    +        $stmt = $this->pdo->prepare($sql);
    +        $stmt->execute([':email' => $userEmail]);
    +        $rows = $stmt->fetchAll();
             return array_column($rows, 'IdGroup');
         }
     
    +
         public function isUserInGroup($personEmail, $groupsFilter)
         {
             return !empty(array_intersect($this->getGroups($groupsFilter), $this->getUserGroups($personEmail)));
    @@ -88,10 +83,7 @@ private function canReadArticle($article, ConnectedUser $connectedUser)
     
         private function getGroups($groupsFilter): array
         {
    -        $rows = $this->fluent->from('"Group"')
    -            ->select('Id AS IdGroup')
    -            ->where('Name LIKE "%' . $groupsFilter . '%"')
    -            ->fetchAll();
    -        return array_column($rows, 'IdGroup');
    +        $rows = $this->gets('Group', ['Name LIKE "%' . $groupsFilter . '%"' => null]);
    +        return array_column($rows, 'Id');
         }
     }
    
  • WebSite/app/helpers/CarouselDataHelper.php+5 41 modified
    @@ -10,50 +10,14 @@ public function __construct(Application $application)
             parent::__construct($application);
         }
     
    -    public function delete_($id)
    -    {
    -        $this->fluent->deleteFrom('Carousel')
    -            ->where('Id', $id)
    -            ->execute();
    -    }
    -
    -    public function get_($id)
    -    {
    -        return $this->fluent->from('Carousel')
    -            ->where('Id', $id)
    -            ->fetch();
    -    }
    -
    -    public function getsForArticle($id)
    -    {
    -        $this->fluent->from('Carousel')->where('IdArticle', $id)->fetchAll();
    -    }
    -
    -    public function getByArticle($idArticle)
    -    {
    -        return $this->fluent->from('Carousel')->where('IdArticle', $idArticle)->fetchAll();
    -    }
    -
    -    public function set_($data, $item): string
    +    public function set_(array $data, string $item): string
         {
             if (!empty($data['id'])) {
    -            $this->fluent->update('Carousel')
    -                ->set([
    -                    'Item' => $item
    -                ])
    -                ->where('Id', $data['id'])
    -                ->where('IdArticle', $data['idArticle'])
    -                ->execute();
    -            $message = 'Élément mis à jour avec succès';
    +            $this->set('Carousel', ['Item' => $item], ['id' => $data['id'], 'IdArticle' => $data['idArticle']]);
    +            return 'Élément mis à jour avec succès';
             } else {
    -            $this->fluent->insertInto('Carousel')
    -                ->values([
    -                    'IdArticle' => $data['idArticle'],
    -                    'Item' => $item
    -                ])
    -                ->execute();
    -            $message = 'Élément ajouté avec succès';
    +            $this->set('Carousel', ['Item' => $item, 'IdArticle' => $data['idArticle']]);
    +            return 'Élément ajouté avec succès';
             }
    -        return $message;
         }
     }
    
  • WebSite/app/helpers/CrosstabDataHelper.php+37 27 modified
    @@ -55,23 +55,23 @@ public function generateCrosstab($sql, $params = [], $rowsTitle = 'Lignes', $col
         public function getevents($period)
         {
             $sql = "
    -                SELECT 
    -                    p.FirstName || ' ' || p.LastName || 
    -                    CASE 
    -                        WHEN p.NickName IS NOT NULL AND p.NickName != '' THEN ' (' || p.NickName || ')'
    -                        ELSE ''
    -                    END AS columnForCrosstab,
    -                    et.Name AS rowForCrosstab,
    -                    COUNT(DISTINCT e.Id) AS countForCrosstab,
    -                    COUNT(part.Id) AS count2ForCrosstab
    -                FROM Person p
    -                JOIN Event e ON p.Id = e.CreatedBy
    -                JOIN EventType et ON e.IdEventType = et.Id
    -                LEFT JOIN Participant part ON part.IdEvent = e.Id
    -                WHERE e.LastUpdate BETWEEN :start AND :end
    -                GROUP BY p.Id, et.Id
    -                ORDER BY p.LastName, p.FirstName
    -            ";
    +            SELECT 
    +                p.FirstName || ' ' || p.LastName || 
    +                CASE 
    +                    WHEN p.NickName IS NOT NULL AND p.NickName != '' THEN ' (' || p.NickName || ')'
    +                    ELSE ''
    +                END AS columnForCrosstab,
    +                et.Name AS rowForCrosstab,
    +                COUNT(DISTINCT e.Id) AS countForCrosstab,
    +                COUNT(part.Id) AS count2ForCrosstab
    +            FROM Person p
    +            JOIN Event e ON p.Id = e.CreatedBy
    +            JOIN EventType et ON e.IdEventType = et.Id
    +            LEFT JOIN Participant part ON part.IdEvent = e.Id
    +            WHERE e.LastUpdate BETWEEN :start AND :end
    +            GROUP BY p.Id, et.Id
    +            ORDER BY p.LastName, p.FirstName
    +        ";
             $dateRange = Period::getDateRangeFor($period);
             $crosstabData = $this->generateCrosstab(
                 $sql,
    @@ -82,24 +82,34 @@ public function getevents($period)
             return [$dateRange, $crosstabData];
         }
     
    -    public function getPersons($dateCondition)
    +    public function getPersons(string $dateCondition, ?string $uriFilter = null, ?string $emailFilter = null, ?string $groupFilter = null): array
         {
    -        $crossTabQuery = $this->fluentForLog->from('Log')
    -            ->select(null)
    -            ->select('Uri, Who, COUNT(*) as count')
    -            ->where($dateCondition)
    -            ->groupBy('Uri, Who');
    -        if (!empty($uriFilter)) $crossTabQuery->where('Uri LIKE ?', "%$uriFilter%");
    -        if (!empty($emailFilter)) $crossTabQuery->where('Who LIKE ?', "%$emailFilter%");
    -        $crossTabData = $crossTabQuery->fetchAll();
    +        $sql = '
    +            SELECT Uri, Who, COUNT(*) as count
    +            FROM Log
    +            WHERE ' . $dateCondition . '
    +        ';
    +        $params = [];
    +        if (!empty($uriFilter)) {
    +            $sql .= ' AND Uri LIKE :uriFilter';
    +            $params[':uriFilter'] = "%$uriFilter%";
    +        }
    +        if (!empty($emailFilter)) {
    +            $sql .= ' AND Who LIKE :emailFilter';
    +            $params[':emailFilter'] = "%$emailFilter%";
    +        }
    +        $sql .= ' GROUP BY Uri, Who';
    +        $stmt = $this->pdoForLog->prepare($sql);
    +        $stmt->execute($params);
    +        $crossTabData = $stmt->fetchAll();
             $filteredPersons = array_unique(array_column($crossTabData, 'Who'));
             $sortedCrossTabData = [];
             $columnTotals = [];
             foreach ($crossTabData as $row) {
                 $uri = $row->Uri;
                 $who = $row->Who;
                 if (!empty($groupFilter) && !(new AuthorizationDataHelper($this->application))->isUserInGroup($who, $groupFilter)) continue;
    -            $count = $row->count;
    +            $count = (int) $row->count;
                 if (!isset($sortedCrossTabData[$uri])) $sortedCrossTabData[$uri] = ['visits' => [], 'total' => 0];
                 $sortedCrossTabData[$uri]['visits'][$who] = $count;
                 $sortedCrossTabData[$uri]['total'] += $count;
    
  • WebSite/app/helpers/Data.php+28 13 modified
    @@ -2,10 +2,12 @@
     
     namespace app\helpers;
     
    -use app\enums\ApplicationError;
    +use InvalidArgumentException;
     use PDO;
     use PDOException;
     
    +use app\enums\ApplicationError;
    +
     /*
     Examples
     ========
    @@ -70,7 +72,7 @@ public function get(string $table, array $where = [], $fields = '*'): object|fal
                 if (is_array($fields)) $fieldsStr = implode(', ', $fields);
                 else                   $fieldsStr = $fields;
     
    -            $sql = "SELECT {$fieldsStr} FROM '{$table}'";
    +            $sql = "SELECT {$fieldsStr} FROM \"{$table}\"";
                 $params = [];
                 if (!empty($where)) {
                     $conditions = [];
    @@ -90,29 +92,42 @@ public function get(string $table, array $where = [], $fields = '*'): object|fal
             }
         }
     
    -    public function gets(string $table, array $where = [], $fields = '*', string $orderBy = ''): array
    +    public function gets(string $table, array $where = [], string $fields = '*', string $orderBy = '', bool $keyPair = false): array
         {
             try {
    -            if (is_array($fields)) $fieldsStr = implode(', ', $fields);
    -            else                   $fieldsStr = $fields;
    -
    -            $sql = "SELECT {$fieldsStr} FROM '{$table}'";
    +            $firstField = null;
    +            if ($keyPair) {
    +                if ($fields === '*') throw new InvalidArgumentException("Cannot use keyPair with fields='*'");
    +                $fieldParts = array_map('trim', explode(',', $fields));
    +                $firstField = $fieldParts[0];
    +            }
    +            $sql = "SELECT {$fields} FROM \"{$table}\"";
                 $params = [];
                 if (!empty($where)) {
                     $conditions = [];
                     foreach ($where as $field => $value) {
    -                    if ($value == null) $conditions[] = "{$field}";
    +                    if ($value === null)                    $conditions[] = "{$field}";
                         else {
    -                        $conditions[] = "{$field} = :{$field}";
    -                        $params[":{$field}"] = $value;
    +                        if (strtolower($field) === 'email') $conditions[] = "{$field} COLLATE NOCASE = ?";
    +                        else                                $conditions[] = "{$field} = ?";
    +                        $params[] = $value;
                         }
                     }
                     $sql .= " WHERE " . implode(' AND ', $conditions);
    -                if ($orderBy !== '')  $sql .= " ORDER BY " . $orderBy;
                 }
    +            if ($orderBy !== '') $sql .= " ORDER BY " . $orderBy;
                 $stmt = $this->pdo->prepare($sql);
                 $stmt->execute($params);
    -            return $stmt->fetchall();
    +            if ($keyPair) {
    +                $results = $stmt->fetchAll(PDO::FETCH_OBJ);
    +                $keyPairArray = [];
    +                foreach ($results as $result) {
    +                    $key = $result->{$firstField};
    +                    $keyPairArray[$key] = $result;
    +                }
    +                return $keyPairArray;
    +            }
    +            return $stmt->fetchAll();
             } catch (PDOException $e) {
                 $this->application->getErrorManager()->raise(ApplicationError::Error, 'Database error: ' . $e->getMessage() . ' in file ' . __FILE__ . ' at line ' . __LINE__);
                 throw $e;
    @@ -129,7 +144,7 @@ public function query(string $sql, array $parameters = []): mixed
                 $queryType = strtoupper(substr(trim($sql), 0, 6));
                 switch ($queryType) {
                     case 'SELECT':
    -                    return $stmt->fetchAll(PDO::FETCH_OBJ);
    +                    return $stmt->fetchAll();
                     case 'INSERT':
                         return $this->pdo->lastInsertId();
                     case 'UPDATE':
    
  • WebSite/app/helpers/DbBrowserHelper.php+1 1 modified
    @@ -86,7 +86,7 @@ public function getTableColumnsDetails($table)
             $stmt->execute();
     
             $columns = [];
    -        while ($row = $stmt->fetch(PDO::FETCH_OBJ)) {
    +        while ($row = $stmt->fetch()) {
                 $columns[] = [
                     'name' => $row->name,
                     'notnull' => $row->notnull
    
  • WebSite/app/helpers/ErrorManager.php+1 3 modified
    @@ -48,16 +48,14 @@ private function log(int $code, string $message): void
             $client = new Client();;
     
             try {
    -            $email = filter_var($_SESSION['user'] ?? '', FILTER_VALIDATE_EMAIL) ?: 'anonymous';
    -
    +            $email = filter_var($_SESSION['user'] ?? '', FILTER_VALIDATE_EMAIL) ?: '';
                 $stmt = $this->pdoForLog->prepare("
                     INSERT INTO Log (
                         IpAddress, Referer, Os, Browser, ScreenResolution,
                         Type, Uri, Token, Who, Code, Message, CreatedAt
                     )
                     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
                 ");
    -
                 $stmt->execute([
                     $client->getIp(),
                     $client->getReferer(),
    
  • WebSite/app/helpers/EventDataHelper.php+2 2 modified
    @@ -127,7 +127,7 @@ public function getEventAttributes($eventId)
             ";
             $stmt = $this->pdo->prepare($sql);
             $stmt->execute(['eventId' => $eventId]);
    -        $result = $stmt->fetchAll(PDO::FETCH_OBJ);
    +        $result = $stmt->fetchAll();
     
             return $result;
         }
    @@ -316,7 +316,7 @@ public function getEventNeeds($eventId): array
             ";
             $stmt = $this->pdo->prepare($sql);
             $stmt->execute([$eventId, $eventId, $eventId]);
    -        return $stmt->fetchAll(PDO::FETCH_OBJ);
    +        return $stmt->fetchAll();
         }
     
         public function getNews($person, $searchFrom): array
    
  • WebSite/app/helpers/LogDataHelper.php+93 85 modified
    @@ -19,47 +19,53 @@ public function __construct(Application $application)
             $this->host = Application::$root . '%';
         }
     
    -    public function add($code, $message)
    +    public function add(string $code, string $message): void
         {
    -        $this->fluentForLog
    -            ->insertInto('Log', [
    -                'IpAddress'        => $_SERVER['REMOTE_ADDR'],
    -                'Referer'          => $_SERVER['HTTP_REFERER'] ?? '',
    -                'Os'               => '',
    -                'Browser'          => '',
    -                'ScreenResolution' => '',
    -                'Type'             => '',
    -                'Uri'              => $_SERVER['REQUEST_URI'],
    -                'Token'            => '',
    -                'Who'              => gethostbyaddr($_SERVER['REMOTE_ADDR']) ?? '',
    -                'Code'             => $code,
    -                'Message'          => $message
    -            ])
    -            ->execute();
    +        $sql = '
    +            INSERT INTO Log (IpAddress, Referer, Os, Browser, ScreenResolution, Type, Uri, Token, Who, Code, Message) 
    +            VALUES (:ipAddress, :referer, :os, :browser, :screenResolution, :type, :uri, :token, :who, :code, :message)
    +        ';
    +        $params = [
    +            ':ipAddress'        => $_SERVER['REMOTE_ADDR'],
    +            ':referer'          => $_SERVER['HTTP_REFERER'] ?? '',
    +            ':os'               => '',
    +            ':browser'          => '',
    +            ':screenResolution' => '',
    +            ':type'             => '',
    +            ':uri'              => $_SERVER['REQUEST_URI'],
    +            ':token'            => '',
    +            ':who'              => gethostbyaddr($_SERVER['REMOTE_ADDR']) ?? '',
    +            ':code'             => $code,
    +            ':message'          => $message,
    +        ];
    +        $stmt = $this->pdoForLog->prepare($sql);
    +        $stmt->execute($params);
         }
     
    -    public function getOsDistribution()
    +    public function getOsDistribution(): array
         {
    -        $query = $this->fluentForLog
    -            ->from('Log')
    -            ->select('Os, COUNT(*) as count')
    -            ->groupBy('Os')
    -            ->orderBy('count DESC');
    -        $results = $query->fetchAll();
    -
    +        $sql = '
    +            SELECT Os, COUNT(*) AS count
    +            FROM Log
    +            GROUP BY Os
    +            ORDER BY count DESC
    +        ';
    +        $stmt = $this->pdoForLog->prepare($sql);
    +        $stmt->execute();
    +        $results = $stmt->fetchAll();
             $labels = [];
             $data = [];
    -
             foreach ($results as $row) {
                 $labels[] = $row->Os ?: 'Inconnu';
    -            $data[] = $row->count;
    +            $data[] = (int) $row->count;
             }
             return [
                 'labels' => $labels,
                 'data' => $data
             ];
         }
     
    +
         public function getBrowserDistribution()
         {
             $query = $this->pdoForLog->query("
    @@ -85,14 +91,12 @@ public function getBrowserDistribution()
                 GROUP BY word
                 ORDER BY count DESC");
             $results = $query->fetchAll();
    -
             $labels = [];
             $data = [];
             foreach ($results as $row) {
                 $labels[] = $row->Browser ?? 'Inconnu';
                 $data[] = $row->count;
             }
    -
             return [
                 'labels' => $labels,
                 'data' => $data
    @@ -101,15 +105,15 @@ public function getBrowserDistribution()
     
         public function getScreenResolutionDistribution()
         {
    -        $query = $this->fluentForLog
    -            ->from('Log')
    -            ->select(null)
    -            ->select('ScreenResolution, COUNT(*) as count')
    -            ->groupBy('ScreenResolution')
    -            ->orderBy('count DESC');
    -
    -        $results = $query->fetchAll();
    -
    +        $sql = '
    +            SELECT ScreenResolution, COUNT(*) AS count
    +            FROM Log
    +            GROUP BY ScreenResolution
    +            ORDER BY count DESC
    +        ';
    +        $stmt = $this->pdoForLog->prepare($sql);
    +        $stmt->execute();
    +        $results = $stmt->fetchAll();
             $typeGroups = [];
             $typeResolutions = [];
     
    @@ -124,15 +128,13 @@ public function getScreenResolutionDistribution()
                     $typeResolutions[$typeKey] = [];
                 }
                 $typeGroups[$typeKey] += $row->count;
    -
                 if ($row->ScreenResolution && $row->ScreenResolution !== 'Inconnu') {
                     $typeResolutions[$typeKey][] = $row->ScreenResolution;
                 }
             }
             arsort($typeGroups);
             $labels = [];
             $data = [];
    -
             foreach ($typeGroups as $typeKey => $count) {
                 $label = $typeKey;
                 if (isset($typeResolutions[$typeKey]) && !empty($typeResolutions[$typeKey])) {
    @@ -142,7 +144,6 @@ public function getScreenResolutionDistribution()
                 $labels[] = $label;
                 $data[] = $count;
             }
    -
             return [
                 'labels' => $labels,
                 'data' => $data
    @@ -251,22 +252,21 @@ private function getResolutionType($resolution)
     
         public function getTypeDistribution()
         {
    -        $query = $this->fluentForLog
    -            ->from('Log')
    -            ->select('Type, COUNT(*) as count')
    -            ->groupBy('Type')
    -            ->orderBy('count DESC');
    -
    -        $results = $query->fetchAll();
    -
    +        $sql = '
    +            SELECT Type, COUNT(*) AS count
    +            FROM Log
    +            GROUP BY Type
    +            ORDER BY count DESC
    +        ';
    +        $stmt = $this->pdoForLog->prepare($sql);
    +        $stmt->execute();
    +        $results = $stmt->fetchAll();
             $labels = [];
             $data = [];
    -
             foreach ($results as $row) {
                 $labels[] = $row->Type ?: 'Inconnu';
                 $data[] = $row->count;
             }
    -
             return [
                 'labels' => $labels,
                 'data' => $data
    @@ -588,27 +588,31 @@ public function getLastVisitPerActivePersonWithTimeAgo($activePersons)
             }
             return $visits;
         }
    -    private function getLastVisitPerActivePerson($activePersons)
    +    private function getLastVisitPerActivePerson(array $activePersons): array
         {
             $result = [];
    -        foreach ($activePersons as $person) {
    -            $lastLog = $this->fluentForLog->from('Log')
    -                ->select('Uri, CreatedAt, Os, Browser')
    -                ->where('Who COLLATE NOCASE', $person->Email)
    -                ->orderBy('CreatedAt DESC')
    -                ->limit(1)
    -                ->fetch();
     
    +        $sql = '
    +            SELECT Uri, CreatedAt, Os, Browser
    +            FROM Log
    +            WHERE Who COLLATE NOCASE = :email
    +            ORDER BY CreatedAt DESC
    +            LIMIT 1
    +        ';
    +        $stmt = $this->pdoForLog->prepare($sql);
    +        foreach ($activePersons as $person) {
    +            $stmt->execute([':email' => $person->Email]);
    +            $lastLog = $stmt->fetch();
                 if ($lastLog) {
                     $result[] = (object)[
    -                    'PersonId' => $person->Id,
    -                    'FullName' => $person->FirstName . ' ' . $person->LastName,
    -                    'Email' => $person->Email,
    -                    'Avatar' => $person->Avatar,
    -                    'LastPage' => $lastLog->Uri,
    +                    'PersonId'     => $person->Id,
    +                    'FullName'     => $person->FirstName . ' ' . $person->LastName,
    +                    'Email'        => $person->Email,
    +                    'Avatar'       => $person->Avatar,
    +                    'LastPage'     => $lastLog->Uri,
                         'LastActivity' => $lastLog->CreatedAt,
    -                    'Os' => $lastLog->Os,
    -                    'Browser' => $lastLog->Browser
    +                    'Os'           => $lastLog->Os,
    +                    'Browser'      => $lastLog->Browser
                     ];
                 }
             }
    @@ -618,6 +622,7 @@ private function getLastVisitPerActivePerson($activePersons)
             return $result;
         }
     
    +
         private function calculateTimeAgo($dateTime)
         {
             $datetime = new DateTime($dateTime, new DateTimeZone('UTC'));
    @@ -690,28 +695,31 @@ public function getPersons($filteredPersonEmails)
             return $query->fetchAll();
         }
     
    -    public function getTopArticles($dateCondition, $top)
    +    public function getTopArticles(string $dateCondition, int $top): array
         {
    -        $query = $this->fluentForLog
    -            ->from('Log')
    -            ->select('
    -                    Uri, 
    -                    COUNT(*) AS visits,
    -                    CASE 
    -                        WHEN Uri LIKE "/articles/%" THEN CAST(substr(Uri, 11) AS INTEGER)
    -                        WHEN Uri LIKE "/navbar/show/article/%" THEN CAST(substr(Uri, 22) AS INTEGER)
    -                        ELSE NULL
    -                    END AS articleId')
    -            ->where($dateCondition)
    -            ->where('(
    -                    (Uri LIKE "/articles/%" AND Uri GLOB "/articles/[0-9]*" AND Uri NOT LIKE "/articles/%/%") 
    -                    OR 
    +        $sql = '
    +            SELECT
    +                Uri,
    +                COUNT(*) AS visits,
    +                CASE
    +                    WHEN Uri LIKE "/articles/%" THEN CAST(substr(Uri, 11) AS INTEGER)
    +                    WHEN Uri LIKE "/navbar/show/article/%" THEN CAST(substr(Uri, 22) AS INTEGER)
    +                    ELSE NULL
    +                END AS articleId
    +            FROM Log
    +            WHERE ' . $dateCondition . '
    +                AND (
    +                    (Uri LIKE "/articles/%" AND Uri GLOB "/articles/[0-9]*" AND Uri NOT LIKE "/articles/%/%")
    +                    OR
                         (Uri LIKE "/navbar/show/article/%" AND Uri GLOB "/navbar/show/article/[0-9]*" AND Uri NOT LIKE "/navbar/show/article/%/%")
    -                )')
    -            ->groupBy('Uri')
    -            ->orderBy('visits DESC')
    -            ->limit($top);
    -        return $query->fetchAll();
    +                )
    +            GROUP BY Uri
    +            ORDER BY visits DESC
    +            LIMIT :top
    +        ';
    +        $stmt = $this->pdoForLog->prepare($sql);
    +        $stmt->execute([':top' => $top]);
    +        return $stmt->fetchAll();
         }
     
         public function getTopPages($dateCondition, $top)
    
  • WebSite/app/helpers/PageDataHelper.php+17 28 modified
    @@ -34,37 +34,27 @@ public function insertOrUpdate($data)
             if (empty($data['id'])) {
                 $maxPosition = $this->fluent->from('Page')->select('MAX(Position) AS MaxPos')->fetch();
                 $newPosition = ($maxPosition && $maxPosition->MaxPos) ? $maxPosition->MaxPos + 1 : 1;
    -            $this->fluent->insertInto('Page')
    -                ->values([
    -                    'Name' => $data['name'],
    -                    'Route' => $data['route'],
    -                    'Position' => $newPosition,
    -                    'IdGroup' => $data['idGroup'],
    -                    'ForMembers' => $data['forMembers'],
    -                    'ForAnonymous' => $data['forAnonymous'],
    -                ])
    -                ->execute();
    -        } else {
    -            $this->fluent->update('Page')
    -                ->set([
    -                    'Name' => $data['name'],
    -                    'Route' => $data['route'],
    -                    'IdGroup' => $data['idGroup'],
    -                    'ForMembers' => $data['forMembers'],
    -                    'ForAnonymous' => $data['forAnonymous'],
    -                ])
    -                ->where('Id', $data['id'])
    -                ->execute();
    -        }
    +            $this->set('Page', [
    +                'Name' => $data['name'],
    +                'Route' => $data['route'],
    +                'Position' => $newPosition,
    +                'IdGroup' => $data['idGroup'],
    +                'ForMembers' => $data['forMembers'],
    +                'ForAnonymous' => $data['forAnonymous']
    +            ]);
    +        } else $this->set('Page', [
    +            'Name' => $data['name'],
    +            'Route' => $data['route'],
    +            'IdGroup' => $data['idGroup'],
    +            'ForMembers' => $data['forMembers'],
    +            'ForAnonymous' => $data['forAnonymous'],
    +        ], ['Id' => $data['id']]);
         }
     
         public function updates($positions)
         {
             foreach ($positions as $id => $position) {
    -            $this->fluent->update('Page')
    -                ->set(['Position' => $position])
    -                ->where('Id', $id)
    -                ->execute();
    +            $this->set('Page', ['Position' => $position], ['Id' => $id]);
             }
         }
     
    @@ -77,8 +67,7 @@ public function authorizedUser($page, $person): bool
                 ->fetch();
             if (!$pageData) return false;
             if (!$pageData->IdGroup) {
    -            if (!$person && $pageData->ForAnonymous) return true;
    -            if ($person && $pageData->ForMembers)    return true;
    +            if ((!$person && $pageData->ForAnonymous) || ($person && $pageData->ForMembers)) return true;
                 return false;
             }
             if (!$person) return false;
    
  • WebSite/app/helpers/PersonDataHelper.php+16 23 modified
    @@ -13,21 +13,20 @@ public function __construct(Application $application)
             parent::__construct($application);
         }
     
    -    public function getPersonsInGroup(int $idGroup, bool $everybodyIfNoGroup = false): array
    +    public function getPersonsInGroup(?int $idGroup): array
         {
             $innerJoin = $and = '';
    -        if (!empty($idGroup) || $everybodyIfNoGroup) {
    +        if ($idGroup !== null) {
                 $innerJoin = 'INNER JOIN PersonGroup on PersonGroup.IdPerson = Person.Id';
                 $and = 'AND PersonGroup.IdGroup = ' . $idGroup;
             }
    -        $query = $this->pdo->query("
    +        return $this->pdo->query("
                 SELECT Person.Id, FirstName, LastName, Email, Preferences, Availabilities
                 FROM Person
                 $innerJoin
                 WHERE Person.Inactivated = 0 $and
                 ORDER BY FirstName, LastName
    -        ");
    -        return $query->fetchAll();
    +        ")->fetchAll();
         }
     
         public function getPersonsInGroupForDirectory($groupId)
    @@ -50,8 +49,6 @@ public function getPublisher($id): string|null
             return "publié par " . $person->FirstName . " " . $person->LastName;
         }
     
    -
    -
         public function create()
         {
             $query = $this->pdo->prepare("SELECT Id FROM Person WHERE Email = ''");
    @@ -137,8 +134,8 @@ public function getNews($person, $searchFrom): array
     
         public function getPersonWantedToBeAlerted($idArticle): array
         {
    -        $idGroup = $this->fluent->from('Article')->where('Id', $idArticle)->fetch('IdGroup');
    -        $idSurvey = $this->fluent->from('Survey')->where('IdArticle', $idArticle)->fetch('Id');
    +        $idGroup = $this->get('Article', ['Id' => $idArticle], 'IdGroup')->IdGroup;
    +        $idSurvey = $this->get('Survey', ['IdArticle' => $idArticle], 'Id')->Id;
             $persons = (new PersonDataHelper($this->application))->getPersonsInGroup($idGroup);
             $filteredEmails = [];
             foreach ($persons as $person) {
    @@ -147,28 +144,24 @@ public function getPersonWantedToBeAlerted($idArticle): array
                     $preferences = json_decode($person->Preferences ?? '', true);
                     if ($preferences != '' && isset($preferences['eventTypes']['newArticle'])) {
                         if (isset($preferences['eventTypes']['newArticle']['pollOnly'])) {
    -                        if ($idSurvey) {
    -                            $include = true;
    -                        }
    +                        if ($idSurvey) $include = true;
                         } else $include = true;
                     }
                 }
                 if ($include) {
                     $filteredEmails[] = $person->Email;
    -                $this->fluent->insertInto('Message')
    -                    ->values([
    -                        'EventId' => null,
    -                        'PersonId' => $person->Id,
    -                        'Text' =>  "New article \n\n /articles/" . $idArticle,
    -                        '"From"' => 'Webapp'
    -                    ])
    -                    ->execute();
    +                $this->set('Message', [
    +                    'EventId' => null,
    +                    'PersonId' => $person->Id,
    +                    'Text' =>  "New article \n\n /articles/" . $idArticle,
    +                    '"From"' => 'Webapp'
    +                ]);
                 }
             }
             return $filteredEmails;
         }
     
    -    public function getEmailsOfInterestedPeople($idGroup, $idEventType, $dayOfWeek, $timeOfDay)
    +    public function getEmailsOfInterestedPeople(?int $idGroup, ?int $idEventType, string $dayOfWeek, string $timeOfDay): array
         {
             $persons = $this->getInterestedPeople($idGroup, $idEventType, $dayOfWeek, $timeOfDay);
             $filteredEmails = [];
    @@ -178,9 +171,9 @@ public function getEmailsOfInterestedPeople($idGroup, $idEventType, $dayOfWeek,
             return $filteredEmails;
         }
     
    -    public function getInterestedPeople($idGroup, $idEventType, $dayOfWeek, $timeOfDay): array
    +    public function getInterestedPeople(?int $idGroup, ?int $idEventType, string $dayOfWeek, string $timeOfDay): array
         {
    -        $persons = (new PersonDataHelper($this->application))->getPersonsInGroup($idGroup, true);
    +        $persons = (new PersonDataHelper($this->application))->getPersonsInGroup($idGroup);
             $filteredPeople = [];
             foreach ($persons as $person) {
                 if ((new PersonPreferences())->isPersonInterested($person, $idEventType, $dayOfWeek, $timeOfDay)) $filteredPeople[] = $person;
    
  • WebSite/app/views/emails/copyToClipBoard.latte+1 1 modified
    @@ -6,7 +6,7 @@
         <h2>Liste des adresses email ({count($emails)}) avec {$filters}</h2>
         <ul>
             {foreach $emails as $email}
    -            <li>{$email} ({$phones[$email]->Phone})</li>
    +            <li>{$email} (📞{$people[$email]->Phone} 👤{$people[$email]->FirstName} {$people[$email]->LastName})</li>
             {/foreach}
         </ul>
     </div>
    
  • WebSite/index.php+1 1 modified
    @@ -300,7 +300,7 @@
         $application->getErrorManager()->raise(ApplicationError::Error, 'Error ' . $ex->getMessage() . ' in file ' . $ex->getFile() . ' at line ' . $ex->getLine());
     });
     $flight->after('start', function () use ($logDataHelper, $flight) {
    -    $logDataHelper->add($flight->getData('code'), $flight->getData('message'));
    +    $logDataHelper->add($flight->getData('code') ?? '', $flight->getData('message') ?? '');
     });
     
     $flight->start();
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.