Skip to content

Conversation

@d0ubIeU
Copy link
Contributor

@d0ubIeU d0ubIeU commented Jan 7, 2026

Feature: Extension of the plugin system to include configuration and activation options

Summary by CodeRabbit

  • New Features

    • Admin UI: plugin list with enable/disable toggles, configuration modal with dynamic config forms; initialization on load.
    • Admin APIs: endpoints to toggle plugins and save plugin configurations.
    • Sample HelloWorld plugin with configurable greeting and username.
  • Chores

    • Persist plugin activation state and configs to a new plugins table; installer/update creates the table.
    • Plugin manager: load/save plugin state, activation/config APIs, asset and translation handling.
    • Database drivers and core DB interface extended to support prepared statements.
  • Documentation

    • Plugin docs expanded; English and German translations updated.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 7, 2026

📝 Walkthrough

Walkthrough

Introduces a plugin subsystem: DB table/schema for plugins, PluginManager enhancements (activation, config persistence, prepared-statement DB APIs), admin API endpoints and UI for toggling/saving plugin configs, admin Twig template/modal, frontend JS for plugin UI, plugin example classes, and update routines to create plugin table.

Changes

Cohort / File(s) Summary
Database schema & migrations
phpmyfaq/src/phpMyFAQ/Setup/Update.php, phpmyfaq/src/phpMyFAQ/Instance/Database/*, phpmyfaq/src/phpMyFAQ/Instance/Database/*/*, phpmyfaq/src/phpMyFAQ/Instance/Database/*
Adds create-table statements for faqplugins across all drivers and a 4.2 alpha updater to create the table.
Database drivers & API
phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php, phpmyfaq/src/phpMyFAQ/Database/*.php (Mysqli, Pgsql, Sqlite3, Sqlsrv, Pdo*)
Adds prepare/execute statement API and statement wrapper classes to drivers; updates fetchAll to unwrap wrappers; adds faqplugins to table name lists. Review for duplicated logic and driver-specific parameter binding.
Plugin persistence & manager
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php, phpmyfaq/src/services.php, phpmyfaq/src/phpMyFAQ/Configuration.php
PluginManager now depends on Configuration, loads DB-backed plugin active/config state, exposes activate/deactivate/isPluginActive/savePluginConfig/getAllPluginStylesheets/getAllPluginScripts; service wiring changed to factory. Review constructor injection effects and DB merge/coercion logic.
Admin controllers & routes
phpmyfaq/src/admin-routes.php, phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php
Adds two POST API endpoints /api/plugin/toggle and /api/plugin/config with CSRF checks, JSON parsing, status/config persistence and error handling. Verify CSRF token names and error responses.
Admin UI & templates
phpmyfaq/assets/templates/admin/configuration/plugins.twig, phpmyfaq/src/phpMyFAQ/Controller/Administration/PluginController.php
Adds Active column, Actions column with config modal, data attributes, and passes activePlugins array to template. Review Twig markup and data attributes for XSS/sanitization.
Frontend JS modules
phpmyfaq/admin/assets/src/api/plugins.ts, phpmyfaq/admin/assets/src/api/index.ts, phpmyfaq/admin/assets/src/configuration/plugins.ts, phpmyfaq/admin/assets/src/index.ts
New API helpers togglePluginStatus/savePluginConfig, re-exports, and configuration UI initializer handlePlugins bound on DOMContentLoaded. Check CSRF handling, error flows, and modal/form field generation.
Plugin example & configuration classes
phpmyfaq/content/plugins/HelloWorld/*
Adds HelloWorldPluginConfiguration, updates HelloWorldPlugin (version bump, config-driven greeting, new events) and introduces configuration property usage. Review lazy config loading and event handlers.
Translations & docs
phpmyfaq/translations/language_en.php, phpmyfaq/translations/language_de.php, docs/plugins.md
Adds/adjusts plugin-related translation keys and docs example methods. Ensure translation keys used in UI/JS/controllers match files.
Twig extension minor change
phpmyfaq/src/phpMyFAQ/Twig/Extensions/PluginTwigExtension.php
Coerces null event data to empty string when triggering plugin events; check for downstream assumptions.

Sequence Diagram(s)

sequenceDiagram
  actor AdminBrowser as Admin (Browser)
  participant JS as Admin JS
  participant API as PluginController (Server)
  participant PM as PluginManager
  participant DB as Database

  AdminBrowser->>JS: click toggle / open config modal / save config
  JS->>API: POST /api/plugin/toggle or /api/plugin/config (JSON, X-CSRF-Token)
  API->>PM: loadPlugins() / validate plugin / activate/deactivate or savePluginConfig(...)
  PM->>DB: getPluginsFromDatabase() / update plugin row or persist config
  DB-->>PM: return plugin rows / ack update
  PM-->>API: operation result (success / error)
  API-->>JS: JSON response
  JS-->>AdminBrowser: update UI / show notification / reload
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

"I hopped through code with tiny paws,
new plugins snug in database jaws.
A modal, toggle, token too—
I nibble bugs and sew them through.
🥕 — rabbit dev, plugin cheer!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: extending the experimental plugin system with configuration and activation capabilities, which is the core objective of all modifications across admin UI, API, database, and plugin management.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (1)

1-541: Fix formatting issues flagged by CI pipeline.

The pipeline reports that mago format --dry-run found formatting changes needed in this file. Please run the formatter to fix these issues:

mago format phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
🤖 Fix all issues with AI agents
In @phpmyfaq/admin/assets/src/api/plugins.ts:
- Around line 45-58: The savePluginConfig function lacks CSRF protection and
HTTP error handling; update savePluginConfig to read the CSRF token (e.g., from
the existing helper or meta tag used by togglePluginStatus) and include it in
the request headers (e.g., 'X-CSRF-Token'), and after fetch verify response.ok
and throw or return a rejected error (including status/text) before parsing
JSON; reference the savePluginConfig function and mirror the header and
response.ok checks implemented in togglePluginStatus so the request is protected
and failures are surfaced properly.
- Around line 24-37: The togglePluginStatus function currently parses JSON
without checking HTTP status; update togglePluginStatus to inspect response.ok
after fetch and handle non-OK responses (e.g., throw an error or return a
rejected Promise with a descriptive message and include status/statusText and
parsed error body if present) before calling response.json(); ensure the
function signature still returns the same Response type on success and that
error paths propagate useful information for callers.
- Around line 24-37: The togglePluginStatus function is missing CSRF protection;
update the function signature (togglePluginStatus) to accept a csrfToken
(string) parameter, add the CSRF token to the fetch request (include it in
headers using the same header name your codebase uses for admin endpoints, e.g.,
'X-CSRF-Token' or the project's configured header), and ensure the body remains
unchanged; then update all callers to pass the current CSRF token when invoking
togglePluginStatus so admin API requests include the token for server
validation.

In @phpmyfaq/admin/assets/src/configuration/plugins.ts:
- Line 36: Calls to togglePluginStatus and savePluginConfig do not include a
CSRF token; update the code to fetch the CSRF token (e.g. from a meta tag like
document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') or an
existing getCsrfToken() helper) and pass that token as an additional argument to
togglePluginStatus(pluginName, active, csrfToken) and to
savePluginConfig(pluginName, config, csrfToken) so the API layer receives CSRF
protection; apply the same change for the savePluginConfig invocation mentioned
in the comment.
- Around line 168-181: The form serialization loop in configuration/plugins.ts
collects inputs from container and treats non-checkboxs as text, so numeric
fields like wordsPerMinute lack HTML5 number inputs and select/textarea/radio
options are never generated; update the generation logic (where inputs are
created) to infer field types from the plugin schema or a type map and create
appropriate elements (e.g., HTMLInputElement with type="number" for integers,
type="email"/"date" as needed, select/textarea/radio for enumerations), and
adjust the serialization loop (the inputs NodeList and usage of configData) to
read value/checked consistently for these element types so numbers are preserved
(parseInt/parseFloat where appropriate) and select/textarea values are
supported.

In @phpmyfaq/assets/templates/admin/configuration/plugins.twig:
- Around line 36-37: The HTML id uses plugin.name directly (id="active_{{
plugin.name }}"), which can produce invalid IDs if plugin names contain spaces
or special chars; update the template to sanitize plugin.name when building the
id (e.g., replace dots/spaces/unsafe chars with underscores or use a regex
replace) so the label's for attribute and the checkbox id remain valid and
accessible; locate the id and corresponding for attribute in plugins.twig
(references: plugin.name, class "plugin-toggle", label for="active_...") and
apply a Twig replace/regex filter to produce a safe identifier consistently.
- Around line 50-53: The data-plugin-config attribute uses plugin.config with
json_encode but lacks HTML attribute escaping; update the template to pipe the
json_encode output through the Twig escaper for attributes (use e('html_attr'))
so the JSON string emitted by plugin.config is safe inside the HTML attribute;
keep the existing default handling for plugin.getAdvDescription() and
plugin.getImplementation() as-is.

In @phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php:
- Around line 58-60: The JSON decoding of $request->getContent() in
PluginController (where $data = json_decode(...), $name = $data['name'] ?? null,
$config = (array) ($data['config'] ?? [])) lacks error handling: detect
json_last_error() / json_last_error_msg() after json_decode and treat malformed
JSON as a bad request; return an appropriate error response (HTTP 400) or throw
the same kind of exception used in toggleStatus for invalid JSON, so invalid
payloads don't produce null-driven notices—ensure you validate $data is an array
before accessing ['name'] or ['config'].
- Around line 35-37: The code uses json_decode($request->getContent(), true) and
then reads $data['name'] and $data['active'] without checking for JSON parse
errors; detect malformed JSON by verifying that json_decode returned an array
and/or checking json_last_error() after the decode in the PluginController
method, and if invalid respond with a 400 Bad Request (e.g., throw
BadRequestHttpException or return a JsonResponse with status 400) before
accessing $data; keep references to $data, $name, $active and the json_decode
call so the check is added immediately after that call.
- Line 66: The saveConfig() handler must validate CSRF and handle save errors:
extract the CSRF token from the request payload (e.g. $data['csrf']) and call
Token::getInstance($this->session)->verifyToken($csrf, 'admin-plugin-config'
/*or appropriate page id*/) before proceeding; then wrap the call to
$pluginManager->savePluginConfig($name, $config) in a try-catch that catches
database/exception types and returns a JSON error response (with an error
message and non-200 status) when an exception occurs, otherwise return success.

In @phpmyfaq/src/phpMyFAQ/Instance/Database/PdoSqlsrv.php:
- Around line 176-180: The config column for the faqdata_plugins table uses
NVARCHAR(MAX) in PdoSqlsrv.php which creates a capacity/behavior mismatch versus
other drivers; update the declaration in the 'faqdata_plugins' entry in
PdoSqlsrv.php so the config column uses a TEXT-like type consistent with other
drivers (use TEXT where supported, or VARCHAR(MAX) for SQL Server if you must
stay with single-byte semantics) and adjust any schema migration/creation code
accordingly so all drivers present a functionally equivalent unlimited-text
column for config.

In @phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php:
- Around line 407-410: Remove the unused private method getPluginWebPath(string
$pluginName) from the PluginManager class; since stylesheets and scripts
registration already construct plugin paths inline (see the registration code
around the stylesheet and script registration blocks), simply delete the
getPluginWebPath method and ensure no other methods call it (if any callers
exist, replace them with the inline 'content/plugins/' . $pluginName
construction or refactor callers to use the same inline pattern).

In @phpmyfaq/src/phpMyFAQ/Setup/Update.php:
- Around line 1128-1179: The file containing the method applyUpdates420Alpha
needs to be formatted with the mago formatter; run the mago format tool (e.g.,
mago format at the repo root or on the file containing applyUpdates420Alpha),
stage and commit the resulting changes, and push the commit so the pipeline no
longer flags the formatting error.

In @phpmyfaq/translations/language_de.php:
- Around line 1543-1557: This file contains duplicate plugin translation keys:
remove the second block that redefines keys like msgPlugins, msgPluginListing,
msgPluginName, msgPluginVersion, msgPluginAuthor, msgPluginDescription, and
msgPluginReason (the duplicates at the later block) and merge any missing keys
from that block (msgActions, msgConfig, msgIncompatiblePlugins,
msgIncompatiblePluginsInfo, msgNoPluginConfig, msgPluginImplementation) into the
original plugin translation group (the earlier definitions around
msgPlugins/msgPluginListing) so all plugin-related keys are defined once with
the correct, consistent values.

In @phpmyfaq/translations/language_en.php:
- Around line 1543-1556: Remove the duplicated translation keys in the later
block: delete the second definitions of msgPlugins, msgPluginListing,
msgPluginName, msgPluginVersion, msgPluginAuthor, msgPluginDescription,
msgIncompatiblePlugins, msgIncompatiblePluginsInfo, and msgPluginReason so they
do not override the originals defined earlier; then merge the unique keys from
this block (msgActions, msgConfig, msgNoPluginConfig, msgPluginImplementation)
into the earlier translation section (the block added in v4.1.0-alpha) so all
plugin-related keys exist exactly once.
🧹 Nitpick comments (9)
phpmyfaq/src/phpMyFAQ/Instance/Database/Mysqli.php (1)

183-188: Consider using TINYINT for the active column.

Line 185 uses INT(1) for the active flag. While this is consistent with other boolean columns in this file, TINYINT would be more appropriate for boolean flags and avoids the deprecated display width syntax in MySQL 8.0.17+.

♻️ Suggested improvement
         'faqdata_plugins' => 'CREATE TABLE %sfaqdata_plugins (
             name VARCHAR(255) NOT NULL,
-            active INT(1) NOT NULL DEFAULT 0,
+            active TINYINT NOT NULL DEFAULT 0,
             config LONGTEXT DEFAULT NULL,
             PRIMARY KEY (name)) 
             DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB',
phpmyfaq/src/phpMyFAQ/Instance/Database/PdoMysql.php (1)

182-187: LGTM! Consider using TINYINT for the boolean column.

The table structure is well-defined for plugin management. However, for better MySQL convention, consider using TINYINT(1) instead of INT(1) for the active column, as it's more memory-efficient and semantically clearer for boolean values.

♻️ Optional refinement for the active column type
        'faqdata_plugins' => 'CREATE TABLE %sfaqdata_plugins (
            name VARCHAR(255) NOT NULL,
-            active INT(1) NOT NULL DEFAULT 0,
+            active TINYINT(1) NOT NULL DEFAULT 0,
            config LONGTEXT DEFAULT NULL,
            PRIMARY KEY (name)) 
            DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB',
phpmyfaq/content/plugins/HelloWorld/HelloWorldPluginConfiguration.php (1)

24-31: Consider readonly properties for configuration immutability.

The configuration class uses public mutable properties. For better encapsulation and to prevent accidental modification after initialization, consider making the properties readonly. However, if the plugin system requires mutable configuration at runtime, the current approach is acceptable.

♻️ Optional refinement for immutability
class HelloWorldPluginConfiguration implements PluginConfigurationInterface
{
    public function __construct(
-        public string $greeting = 'Hello, World!',
-        public string $username = 'John Doe',
+        public readonly string $greeting = 'Hello, World!',
+        public readonly string $username = 'John Doe',
    ) {
    }
}
phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php (1)

39-41: Consider stricter plugin name validation.

While checking plugin existence is good, there's no validation of the $name format itself. An empty string would pass the null check but might cause issues.

♻️ Suggested validation enhancement
     if ($name === null || !array_key_exists($name, $pluginManager->getPlugins())) {
         return new JsonResponse(['success' => false, 'message' => 'Plugin not found'], 404);
     }
+
+    if (empty(trim($name))) {
+        return new JsonResponse(['success' => false, 'message' => 'Invalid plugin name'], 400);
+    }

Or combine into a single validation:

-    if ($name === null || !array_key_exists($name, $pluginManager->getPlugins())) {
+    if (empty($name) || !array_key_exists($name, $pluginManager->getPlugins())) {
         return new JsonResponse(['success' => false, 'message' => 'Plugin not found'], 404);
     }
phpmyfaq/admin/assets/src/configuration/plugins.ts (1)

189-189: Consider UX improvement: avoid full page reload.

While window.location.reload() works, it disrupts the user experience. Consider updating only the affected UI elements or closing the modal with a success notification instead.

♻️ Optional UX improvement
     if (result.success) {
-        window.location.reload();
+        // Close modal
+        const modal = bootstrap.Modal.getInstance(configModalEl);
+        modal?.hide();
+        pushNotification('Configuration saved successfully.');
+        // Optionally refresh just the plugin table row
     } else {
         pushErrorNotification('Failed to save configuration: ' + (result.message || 'Unknown error'));
     }
phpmyfaq/content/plugins/HelloWorld/HelloWorldPlugin.php (1)

24-25: Redundant require_once at file top level.

The require_once at line 24 loads HelloWorldPluginConfiguration.php unconditionally. However, getConfig() at lines 74-76 also has a class_exists guard with another require_once. Since line 24 always executes first, the guard will never trigger.

Consider removing one of them for clarity:

♻️ Option 1: Remove top-level require (preferred for lazy loading)
-require_once __DIR__ . '/HelloWorldPluginConfiguration.php';
-
 use phpMyFAQ\Plugin\PluginEvent;

This keeps the lazy loading pattern in getConfig().

♻️ Option 2: Remove the guard in getConfig()
 public function getConfig(): ?PluginConfigurationInterface
 {
     if ($this->config === null) {
-        if (!class_exists(HelloWorldPluginConfiguration::class)) {
-            require_once __DIR__ . '/HelloWorldPluginConfiguration.php';
-        }
         $this->config = new HelloWorldPluginConfiguration();
     }
     return $this->config;
 }

This is simpler if eager loading is acceptable.

phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (3)

201-203: Remove development comments before merging.

These appear to be development notes rather than useful code documentation:

// Remove plugin from the plugins array since it's incompatible (actually, maybe keep it but mark inactive?)
// Original code removed it. I'll stick to original behavior for incompatible ones.
♻️ Proposed fix
             $this->incompatiblePlugins[$plugin->getName()] = [
                 'plugin' => $plugin,
                 'reason' => sprintf('Missing dependencies: %s', implode(', ', $missingDeps)),
             ];
-            // Remove plugin from the plugins array since it's incompatible (actually, maybe keep it but mark inactive?)
-            // Original code removed it. I'll stick to original behavior for incompatible ones.
             unset($this->plugins[$plugin->getName()]);
         }

304-309: Consider catching a more specific exception type.

Catching the generic \Exception could mask legitimate errors unrelated to table existence. Consider using a more specific exception type or explicitly checking if the table exists first.

♻️ Suggested improvement
         // Ensure table exists to avoid crashes during update/install if not yet run
         try {
             $result = $db->query("SELECT name, active, config FROM $table");
-        } catch (\Exception $e) {
+        } catch (\PDOException|\mysqli_sql_exception $e) {
             // Table might not exist yet
             return [];
         }

Alternatively, you could check table existence explicitly before querying if the database driver supports it.


145-148: Clean up development notes.

This comment reads like a development note rather than documentation:

// I will default to inactive (false) for new plugins found on disk but not in DB.
$isActive = false; 
♻️ Proposed fix
             }
         } else {
-
-            // I will default to inactive (false) for new plugins found on disk but not in DB.
-            $isActive = false; 
+            // New plugins default to inactive until explicitly activated
+            $isActive = false;
         }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8549f2 and 0b1f72b.

📒 Files selected for processing (35)
  • phpmyfaq/admin/assets/src/api/index.ts
  • phpmyfaq/admin/assets/src/api/plugins.ts
  • phpmyfaq/admin/assets/src/configuration/index.ts
  • phpmyfaq/admin/assets/src/configuration/plugins.ts
  • phpmyfaq/admin/assets/src/index.ts
  • phpmyfaq/assets/templates/admin/configuration/plugins.twig
  • phpmyfaq/content/plugins/HelloWorld/HelloWorldConfiguration.php
  • phpmyfaq/content/plugins/HelloWorld/HelloWorldPlugin.php
  • phpmyfaq/content/plugins/HelloWorld/HelloWorldPluginConfiguration.php
  • phpmyfaq/src/admin-routes.php
  • phpmyfaq/src/phpMyFAQ/Configuration.php
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/PluginController.php
  • phpmyfaq/src/phpMyFAQ/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoMysql.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoPgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoSqlite.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoSqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Database/Pgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoMysql.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoPgsql.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoSqlite.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoSqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Pgsql.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlite3.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
  • phpmyfaq/src/phpMyFAQ/Setup/Update.php
  • phpmyfaq/src/phpMyFAQ/Twig/Extensions/PluginTwigExtension.php
  • phpmyfaq/src/services.php
  • phpmyfaq/translations/language_de.php
  • phpmyfaq/translations/language_en.php
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript coding standards for TypeScript code
Use semicolons at the end of each statement
Use single quotes for strings
Use arrow functions for callbacks

Files:

  • phpmyfaq/admin/assets/src/configuration/index.ts
  • phpmyfaq/admin/assets/src/configuration/plugins.ts
  • phpmyfaq/admin/assets/src/api/index.ts
  • phpmyfaq/admin/assets/src/api/plugins.ts
  • phpmyfaq/admin/assets/src/index.ts
**/phpmyfaq/src/**/*.php

📄 CodeRabbit inference engine (AGENTS.md)

Use PER Coding Style 3.0 for PHP code

Files:

  • phpmyfaq/src/phpMyFAQ/Database/PdoPgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoSqlite.php
  • phpmyfaq/src/phpMyFAQ/Twig/Extensions/PluginTwigExtension.php
  • phpmyfaq/src/phpMyFAQ/Database/Pgsql.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoSqlite.php
  • phpmyfaq/src/admin-routes.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlite3.php
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Pgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoSqlsrv.php
  • phpmyfaq/src/services.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoPgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoSqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
  • phpmyfaq/src/phpMyFAQ/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Setup/Update.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Configuration.php
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/PluginController.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoMysql.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoMysql.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlsrv.php
**/translations/**

📄 CodeRabbit inference engine (AGENTS.md)

Use UTF-8 encoding for all translation files

Files:

  • phpmyfaq/translations/language_en.php
  • phpmyfaq/translations/language_de.php
🧬 Code graph analysis (11)
phpmyfaq/src/phpMyFAQ/Twig/Extensions/PluginTwigExtension.php (2)
phpmyfaq/src/phpMyFAQ/Configuration.php (4)
  • Configuration (47-530)
  • getConfigurationInstance (96-99)
  • getPluginManager (516-519)
  • triggerEvent (521-524)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (1)
  • triggerEvent (326-332)
phpmyfaq/admin/assets/src/configuration/plugins.ts (2)
phpmyfaq/admin/assets/src/api/plugins.ts (2)
  • togglePluginStatus (24-37)
  • savePluginConfig (45-58)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (1)
  • savePluginConfig (235-262)
phpmyfaq/src/admin-routes.php (1)
phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php (1)
  • PluginController (27-70)
phpmyfaq/admin/assets/src/api/plugins.ts (3)
phpmyfaq/admin/assets/src/interfaces/response.ts (1)
  • Response (1-8)
phpmyfaq/admin/assets/js/configuration.js (1)
  • response (49-56)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (1)
  • savePluginConfig (235-262)
phpmyfaq/admin/assets/src/index.ts (1)
phpmyfaq/admin/assets/src/configuration/plugins.ts (1)
  • handlePlugins (22-199)
phpmyfaq/content/plugins/HelloWorld/HelloWorldPluginConfiguration.php (2)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (1)
  • __construct (55-60)
phpmyfaq/src/phpMyFAQ/Configuration.php (1)
  • __construct (73-94)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (5)
phpmyfaq/src/phpMyFAQ/Configuration.php (2)
  • Configuration (47-530)
  • getDb (134-137)
phpmyfaq/src/phpMyFAQ/Plugin/PluginInterface.php (6)
  • getName (29-29)
  • getConfig (54-54)
  • registerEvents (59-59)
  • getTranslationsPath (75-75)
  • getStylesheets (67-67)
  • getScripts (83-83)
phpmyfaq/src/phpMyFAQ/Translation.php (2)
  • Translation (25-436)
  • registerPluginTranslations (292-322)
phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php (1)
  • escape (83-86)
phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php (1)
  • escape (55-55)
phpmyfaq/src/phpMyFAQ/Setup/Update.php (1)
phpmyfaq/src/phpMyFAQ/Database.php (1)
  • getTablePrefix (138-141)
phpmyfaq/src/phpMyFAQ/Configuration.php (1)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (1)
  • PluginManager (31-541)
phpmyfaq/src/phpMyFAQ/Controller/Administration/PluginController.php (5)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (2)
  • getPlugins (350-353)
  • isPluginActive (227-230)
phpmyfaq/content/plugins/HelloWorld/HelloWorldPlugin.php (1)
  • getName (34-37)
tests/phpMyFAQ/Plugin/PluginManagerTest.php (1)
  • getName (249-252)
phpmyfaq/src/phpMyFAQ/Plugin/PluginInterface.php (1)
  • getName (29-29)
tests/phpMyFAQ/Plugin/MockPlugin.php (1)
  • getName (9-12)
phpmyfaq/content/plugins/HelloWorld/HelloWorldPlugin.php (3)
phpmyfaq/src/phpMyFAQ/Plugin/PluginInterface.php (4)
  • getDependencies (49-49)
  • getConfig (54-54)
  • getTranslationsPath (75-75)
  • registerEvents (59-59)
phpmyfaq/content/plugins/HelloWorld/HelloWorldPluginConfiguration.php (1)
  • HelloWorldPluginConfiguration (24-31)
phpmyfaq/src/phpMyFAQ/Plugin/PluginEvent.php (3)
  • PluginEvent (26-49)
  • getData (35-38)
  • setOutput (40-43)
🪛 ast-grep (0.40.3)
phpmyfaq/admin/assets/src/configuration/plugins.ts

[warning] 90-90: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: container.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

🪛 GitHub Actions: phpMyFAQ Build
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php

[error] 1-1: mago format --dry-run reported formatting changes needed in this file. Found 2 file(s) that need formatting.

phpmyfaq/src/phpMyFAQ/Setup/Update.php

[error] 1-1: mago format --dry-run reported formatting changes needed in this file. Found 2 file(s) that need formatting.

🪛 PHPMD (2.15.0)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php

407-410: Avoid unused private methods such as 'getPluginWebPath'. (undefined)

(UnusedPrivateMethod)

🔇 Additional comments (30)
phpmyfaq/src/phpMyFAQ/Database/PdoSqlsrv.php (1)

222-222: LGTM! Clean addition to support plugin system.

The addition of faqdata_plugins to the table names array is correct and follows the established naming convention. This properly extends the MS SQL Server driver to recognize the new plugin data table.

phpmyfaq/src/phpMyFAQ/Database/Pgsql.php (1)

317-317: LGTM: Plugin table correctly registered.

The addition of faqdata_plugins to the table registry is correct and consistent with the plugin management feature being introduced across all database drivers.

phpmyfaq/src/phpMyFAQ/Database/PdoMysql.php (1)

222-222: LGTM: Plugin table correctly registered.

The addition of faqdata_plugins to the table registry is correct and aligns with the broader plugin management implementation across all database backends.

phpmyfaq/src/phpMyFAQ/Database/Mysqli.php (1)

255-255: LGTM: Plugin table correctly registered.

The addition of faqdata_plugins to the table registry is correct and maintains consistency with the plugin data persistence layer being added across all database drivers.

phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php (1)

273-273: LGTM: Plugin table correctly registered.

The addition of faqdata_plugins to the table registry is correct and consistent with the plugin management feature implementation across all supported database backends.

phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php (1)

312-312: LGTM: Plugin table correctly registered.

The addition of faqdata_plugins to the table registry is correct and completes the consistent multi-driver update for the new plugin management feature.

phpmyfaq/content/plugins/HelloWorld/HelloWorldConfiguration.php (1)

24-27: LGTM!

The plugin configuration class follows a clean structure with proper interface implementation. The public property pattern is appropriate for configuration data.

phpmyfaq/src/phpMyFAQ/Instance/Database/Pgsql.php (1)

180-184: LGTM!

The table definition follows PostgreSQL conventions and is consistent with other table definitions in this driver.

phpmyfaq/src/phpMyFAQ/Instance/Database/PdoPgsql.php (1)

179-183: LGTM!

The table definition is consistent with the legacy Pgsql driver and follows proper PostgreSQL syntax.

phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlsrv.php (1)

177-181: LGTM!

The table definition correctly uses SQL Server data types with proper Unicode support (NVARCHAR) and is consistent with other table definitions in this driver.

phpmyfaq/src/phpMyFAQ/Database/PdoPgsql.php (1)

221-221: LGTM!

The table name addition is consistent with the plugin infrastructure being introduced across all database drivers.

phpmyfaq/src/phpMyFAQ/Database/PdoSqlite.php (1)

246-246: LGTM!

The table name addition maintains consistency with the plugin infrastructure across all database drivers.

phpmyfaq/admin/assets/src/index.ts (2)

39-39: LGTM!

The import is correctly placed with other configuration-related imports and follows the existing code structure.


171-172: LGTM!

The plugin initialization is properly integrated into the DOMContentLoaded flow. The placement after Elasticsearch/OpenSearch configuration ensures core services are set up before plugin management is initialized.

phpmyfaq/admin/assets/src/api/index.ts (1)

15-15: LGTM!

The new re-export follows the established barrel pattern and TypeScript coding standards (semicolons, single quotes). This correctly exposes the plugin API module to consumers of the API index.

phpmyfaq/admin/assets/src/configuration/index.ts (1)

6-6: LGTM!

The re-export correctly exposes the plugins configuration module, maintaining consistency with the existing barrel pattern and TypeScript coding standards.

phpmyfaq/src/phpMyFAQ/Controller/Administration/PluginController.php (1)

42-45: LGTM!

The activePlugins computation correctly builds a map of plugin names to their active status by iterating through all plugins and querying isPluginActive() for each. This data structure aligns with the template's needs (as referenced in the AI summary) and follows a clear, straightforward pattern.

Also applies to: 51-51

phpmyfaq/src/phpMyFAQ/Twig/Extensions/PluginTwigExtension.php (1)

31-31: The null-to-empty-string coercion appears safe for existing plugins.

Examined all three example plugins (EnhancedExamplePlugin, HelloWorldPlugin, ReadingTimePlugin) and found none differentiate between null and '' when handling event data. All plugins simply retrieve data via $event->getData() and use it directly without null checks. A repository-wide search found no plugins use is_null() or explicit null comparisons on event data. Since strip_tags(null) and strip_tags('') produce identical results, and other plugins use the data in string contexts where both behave equivalently, the change to coerce null to an empty string poses no risk to existing plugins in this codebase.

phpmyfaq/src/phpMyFAQ/Configuration.php (1)

507-514: LGTM!

The change correctly passes the Configuration instance to the PluginManager constructor, which aligns with the updated constructor signature shown in the relevant code snippets. This enables the PluginManager to access configuration and database services for plugin lifecycle management and persistence.

phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlite3.php (1)

177-181: LGTM!

The new faqdata_plugins table schema is well-designed for storing plugin metadata:

  • Plugin name as primary key ensures uniqueness
  • Default inactive state (0) is a safe default for new plugins
  • TEXT column for config provides flexibility for JSON-based configuration

The structure is consistent with other table definitions in this file.

phpmyfaq/src/phpMyFAQ/Instance/Database/PdoSqlite.php (1)

176-180: LGTM!

The faqdata_plugins table structure is identical to the Sqlite3.php implementation, ensuring consistency across SQLite database drivers. The schema design is appropriate for plugin persistence.

phpmyfaq/assets/templates/admin/configuration/plugins.twig (1)

64-104: Well-structured plugin configuration modal.

The modal implementation is comprehensive and well-organized:

  • Provides clear sections for description, implementation code, and configuration
  • Handles the no-config case gracefully with an informative message
  • Uses proper Bootstrap 5 modal patterns with accessibility attributes
  • Dynamic form field generation aligns with the frontend plugin management API

The structure integrates well with the plugin management features added in this PR.

phpmyfaq/src/services.php (1)

300-303: LGTM! Clean factory-based DI pattern.

The change to use a factory method for PluginManager instantiation properly delegates creation to the Configuration service, following dependency injection best practices.

phpmyfaq/src/phpMyFAQ/Setup/Update.php (2)

1128-1179: LGTM! Database schema is correctly defined across all supported drivers.

The plugin table creation properly handles type differences across MySQL/MariaDB, PostgreSQL, SQLite, and SQL Server:

  • Uses appropriate text types (LONGTEXT, TEXT, NVARCHAR(MAX))
  • Applies correct integer types and defaults
  • Maintains consistent PRIMARY KEY definitions

187-189: No ordering issues identified in the update flow.

The execution order is safe. applyUpdates420Alpha() only adds queries to the array without executing them, optimizeTables() queries existing tables (read-only, safe before execution), and executeQueries() executes all accumulated queries. The new faqdata_plugins table won't be included in the OPTIMIZE TABLE list during this upgrade, but that's harmless—it will be optimized in future updates. The version check in applyUpdates420Alpha() (version < '4.2.0-alpha') already handles recovery from incomplete previous updates.

phpmyfaq/admin/assets/src/configuration/plugins.ts (2)

90-91: Safe innerHTML usage - static analysis false positive.

The static analysis tool flagged this line, but clearing innerHTML with an empty string is safe and standard practice. This is not an XSS vulnerability.


22-50: Good error recovery pattern.

The toggle handler properly reverts the UI state on failure and provides user feedback. This prevents UI state from becoming out of sync with the server state.

phpmyfaq/content/plugins/HelloWorld/HelloWorldPlugin.php (1)

87-107: LGTM!

The event registration and handlers are well-structured. The getConfig() method guarantees a non-null return value for this plugin, so accessing ->greeting and ->username is safe.

phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (2)

227-230: isPluginActive may not reflect DB state after toggle.

This method checks $this->loadedPlugins which is populated during loadPlugins(). However, after calling activatePlugin() or deactivatePlugin(), the DB is updated but $this->loadedPlugins is not refreshed until the next request.

Consider if this is the intended behavior, or if you need to also update the in-memory state.


235-262: LGTM - SQL escaping is properly applied.

The savePluginConfig method correctly uses $db->escape() for all user-controlled values before interpolating them into SQL queries. The table name is safely constructed from internal constants.

Copy link
Owner

@thorsten thorsten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting is not correct in some files.

* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/.
*
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File header data is not correct

},
body: JSON.stringify({
name,
active,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSRF protection is missing


if (!result.success) {
target.checked = !active; // Revert
pushErrorNotification('Failed to update plugin status: ' + (result.message || 'Unknown error'));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TranslationService could be used to have multi-lingual notifications.

});

// Configuration Modal
const configModalEl = document.getElementById('pluginConfigModal');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would name it pluginConfigModal


Object.keys(configData).forEach((key) => {
const value = configData[key];
const div = document.createElement('div');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use addElement() to create HTML elements.

$prefix . 'faquser_right',
$prefix . 'faqvisits',
$prefix . 'faqvoting',
$prefix . 'faqdata_plugins',
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would name the table "faqplugins"

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (1)

1-1: Fix formatting issues detected by pipeline.

The pipeline detected formatting changes. Run the formatter to fix:

mago format phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
🤖 Fix all issues with AI agents
In @phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php:
- Around line 1-98: The file has styling/formatting violations causing the
pipeline to fail; run the repository formatter (mago) on
phpMyFAQ/Controller/Administration/Api/PluginController.php or apply the
project's PHP coding standard (e.g., PSR-12) so the class PluginController and
its methods toggleStatus and saveConfig are consistently formatted (spacing,
indentation, docblocks, casts, and array syntax), then stage and commit the
formatted file so the CI passes.

In @phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php:
- Around line 267-293: The updatePluginStatus method currently builds SQL with
string concatenation and escape() (same issue as savePluginConfig); replace the
SELECT/UPDATE/INSERT calls in updatePluginStatus to use prepared statements with
bound parameters on $db (e.g. prepare + execute) instead of sprintf/escape,
binding the plugin name and active flag as parameters, then use the
prepared-statement result (or rowCount) to decide whether to run an UPDATE or
INSERT, and execute the appropriate prepared statement with bound values; ensure
you use the same DB API conventions used elsewhere in this class for
prepare/execute and error handling.
- Around line 235-262: The savePluginConfig method currently builds SQL with
sprintf and $db->escape (which is a no-op in PdoMysql), creating an SQL
injection risk; change savePluginConfig to use parameterized queries for the
SELECT, UPDATE and INSERT (use placeholders for name and config) and bind
$pluginName and the JSON-encoded $configData when executing; if the Database
driver does not expose PDO prepare/execute, add or use an abstraction method
(e.g., prepare/execute or queryWithParams) on the Database/driver so you can
call prepared statements from savePluginConfig instead of sprintf, or
alternatively harden PdoMysql::escape to actually escape input—prefer the
prepared-statement approach to fix the vulnerability.
🧹 Nitpick comments (4)
phpmyfaq/admin/assets/src/configuration/plugins.ts (1)

74-76: Use translated string for modal title.

The modal title uses a hard-coded "Configuration: " prefix. This should be translated for consistency with the rest of the application.

♻️ Recommended refactor
+const translate = new TranslationService();
 if (modalTitle && pluginName) {
-    modalTitle.textContent = 'Configuration: ' + pluginName;
+    modalTitle.textContent = translate.get('msgConfig') + ': ' + pluginName;
 }

As per coding guidelines, use translation features for user-facing text.

phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (3)

146-147: Use professional tone in code comments.

The comment uses first-person narrative ("I will default"). Consider rephrasing to: "Defaults to inactive (false) for new plugins found on disk but not in DB."


201-202: Remove uncertain/informal comment.

The comment expresses uncertainty ("actually, maybe keep it?") which doesn't belong in production code. If the current behavior is correct, update the comment to explain why incompatible plugins are removed from the array.


304-309: Consider more specific exception handling.

Catching a generic \Exception will suppress all errors, not just missing table errors. If the database layer provides specific exceptions for "table not found" errors, catch those specifically.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b1f72b and 2ea3fe2.

📒 Files selected for processing (9)
  • phpmyfaq/admin/assets/src/api/plugins.ts
  • phpmyfaq/admin/assets/src/configuration/plugins.ts
  • phpmyfaq/assets/templates/admin/configuration/plugins.twig
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoSqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
  • phpmyfaq/src/phpMyFAQ/Setup/Update.php
  • phpmyfaq/translations/language_de.php
  • phpmyfaq/translations/language_en.php
🚧 Files skipped from review as they are similar to previous changes (3)
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoSqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Setup/Update.php
  • phpmyfaq/translations/language_de.php
🧰 Additional context used
📓 Path-based instructions (3)
**/phpmyfaq/src/**/*.php

📄 CodeRabbit inference engine (AGENTS.md)

Use PER Coding Style 3.0 for PHP code

Files:

  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php
  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript coding standards for TypeScript code
Use semicolons at the end of each statement
Use single quotes for strings
Use arrow functions for callbacks

Files:

  • phpmyfaq/admin/assets/src/api/plugins.ts
  • phpmyfaq/admin/assets/src/configuration/plugins.ts
**/translations/**

📄 CodeRabbit inference engine (AGENTS.md)

Use UTF-8 encoding for all translation files

Files:

  • phpmyfaq/translations/language_en.php
🧠 Learnings (1)
📚 Learning: 2025-12-26T21:59:16.058Z
Learnt from: CR
Repo: thorsten/phpMyFAQ PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-26T21:59:16.058Z
Learning: Avoid hard-coded strings in code; use translation features for user-facing text

Applied to files:

  • phpmyfaq/admin/assets/src/configuration/plugins.ts
🧬 Code graph analysis (3)
phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php (2)
phpmyfaq/src/phpMyFAQ/Session/Token.php (2)
  • Token (27-219)
  • verifyToken (132-162)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (5)
  • loadPlugins (87-206)
  • getPlugins (350-353)
  • activatePlugin (211-214)
  • deactivatePlugin (219-222)
  • savePluginConfig (235-262)
phpmyfaq/admin/assets/src/api/plugins.ts (3)
phpmyfaq/admin/assets/src/interfaces/response.ts (1)
  • Response (1-8)
phpmyfaq/admin/assets/js/configuration.js (1)
  • response (49-56)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (1)
  • savePluginConfig (235-262)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (3)
phpmyfaq/src/phpMyFAQ/Configuration.php (2)
  • Configuration (47-530)
  • getDb (134-137)
phpmyfaq/src/phpMyFAQ/Plugin/PluginInterface.php (6)
  • getName (29-29)
  • getConfig (54-54)
  • registerEvents (59-59)
  • getTranslationsPath (75-75)
  • getStylesheets (67-67)
  • getScripts (83-83)
phpmyfaq/src/phpMyFAQ/Translation.php (2)
  • Translation (25-436)
  • registerPluginTranslations (292-322)
🪛 ast-grep (0.40.3)
phpmyfaq/admin/assets/src/configuration/plugins.ts

[warning] 93-93: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: container.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

🪛 GitHub Actions: phpMyFAQ Build
phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php

[error] 1-1: mago format --dry-run detected formatting changes for the file. Apply fixes with the formatter. Command: mago format --dry-run

phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php

[error] 1-1: mago format --dry-run detected formatting changes for the file. Apply fixes with the formatter. Command: mago format --dry-run

🔇 Additional comments (17)
phpmyfaq/translations/language_en.php (1)

1463-1474: LGTM! Translation updates support the new plugin management features.

The translation key updates are consistent and well-structured. The changes from "Plugin name" to "Plugin Name" and "List of..." to "Plugin Listing" improve consistency, and the new keys (msgActions, msgConfig, msgNoPluginConfig, msgPluginImplementation, msgPluginReason) properly support the new UI components introduced in the template.

phpmyfaq/assets/templates/admin/configuration/plugins.twig (3)

34-42: Well-implemented plugin toggle with proper sanitization.

The Active column correctly uses a sanitized plugin name for HTML IDs, preventing invalid IDs from plugins with special characters. The form-check-switch pattern is appropriate for toggle functionality, and the visually-hidden label maintains accessibility.


45-59: Configuration button properly passes plugin metadata.

The button correctly includes all necessary data attributes (name, description, implementation, config) with proper HTML attribute escaping applied to the JSON config. The e('html_attr') filter ensures special characters in JSON are safely encoded for use in HTML attributes.


65-105: Modal structure is well-organized and accessible.

The plugin configuration modal follows Bootstrap best practices with proper ARIA attributes, a scrollable dialog for long content, and a clear structure separating description, implementation display, configuration form, and action buttons.

phpmyfaq/admin/assets/src/api/plugins.ts (2)

25-44: Excellent error handling and CSRF protection.

The function properly includes CSRF protection via the X-CSRF-Token header and implements robust error handling by checking response.ok before parsing JSON. The error message extraction with fallback ensures useful feedback is provided to callers.


53-77: Configuration save function is well-implemented.

The function mirrors the error handling pattern from togglePluginStatus and correctly includes the CSRF token in both the header and body (as noted in the comment on line 67) for backend validation flexibility.

phpmyfaq/admin/assets/src/configuration/plugins.ts (3)

93-93: innerHTML clearing is safe in this context.

The static analysis tool flagged this line as a potential XSS risk, but setting innerHTML = '' to clear the container is safe since no user input is being inserted. The subsequent dynamic content generation uses createElement and appendChild, which are XSS-safe methods.


121-167: Excellent input type inference and form field generation.

The dynamic form field generation now properly detects data types and creates appropriate HTML input elements (checkbox for booleans, number inputs for numeric values, email/date inputs based on heuristics, and textarea for long strings). This is a significant improvement over the basic text-only approach.


204-219: Configuration serialization handles multiple input types correctly.

The form data collection properly distinguishes between checkboxes (boolean), number inputs (parsed as int/float), and text inputs, ensuring the configuration data is serialized with correct types for backend storage.

phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php (2)

34-47: Excellent CSRF protection and input validation.

The endpoint properly validates the CSRF token before processing and includes comprehensive JSON decode error handling with appropriate HTTP status codes (403 for auth failure, 400 for invalid input).


66-94: Configuration save endpoint is secure and robust.

The endpoint implements proper security controls (CSRF validation accepting token from either body or header) and wraps the database operation in exception handling to return meaningful error responses (404 for missing plugin, 500 for save failures).

phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (6)

55-60: LGTM: Constructor properly initializes dependencies.

The dependency injection of Configuration is appropriate for the DB-driven plugin management features.


89-91: Early return prevents re-initialization per request.

This ensures plugins are loaded once per request. Note that activation/deactivation changes will only take effect on the next request.


227-230: LGTM: Correct implementation with strict comparison.


407-429: Good security hardening for path traversal prevention.

The path normalization and case-insensitive directory checks effectively prevent path traversal attacks while maintaining Windows compatibility.


458-480: Good security hardening for script registration.

Consistent with stylesheet registration, this properly prevents path traversal with realpath() canonicalization and case-insensitive checks.


211-222: No changes needed. The methods function as designed: the controller validates plugin existence before invocation, and updatePluginStatus includes database-level existence checking.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (1)

1-1: Resolve the formatting issue flagged by the pipeline.

The pipeline reports that mago format --dry-run detects formatting changes needed for this file. Please run the formatter to ensure PER Coding Style 3.0 compliance.

#!/bin/bash
# Run the formatter to fix the issue
mago format phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php

As per coding guidelines, PER Coding Style 3.0 should be used for PHP code in this directory.

🤖 Fix all issues with AI agents
In @phpmyfaq/src/phpMyFAQ/Database/Pgsql.php:
- Around line 321-347: The prepare method uses a deterministic statement name
('pmf_stmt_' . md5($query)) which can collide when the same query is prepared
multiple times; update prepare(string $query, array $options = []) and related
execute(mixed $statement, array $params = []) to avoid collisions by tracking
prepared names or generating unique names per prepare call (e.g., append a
unique suffix or incrementing counter) and store the mapping on the Pgsql
instance (use a property like $preparedStatements to map query->stmtName or
stmtName->query); ensure prepare returns the unique statement name and execute
casts and uses that name as it currently does ((string)$statement) and keep the
$options parameter untouched for interface compatibility.

In @phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php:
- Around line 316-342: The execute() method currently ignores $params; fix by
handling both prepared resources and direct-query execution: keep sqlsrv_prepare
usage in prepare() for cases without runtime params, but have prepare() return
the original query string (or allow callers to pass a query string) when you
intend to supply parameters at execution time; then change execute(mixed
$statement, array $params = []) to detect if $statement is a resource and
$params is empty -> call sqlsrv_execute($statement), otherwise treat $statement
as a SQL string and call sqlsrv_query($this->conn, $statement, $params) so
parameters are actually passed to SQL Server; ensure you handle types (resource
vs string) and return boolean consistent with other drivers.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ea3fe2 and 27ab9dd.

📒 Files selected for processing (24)
  • phpmyfaq/admin/assets/src/api/plugins.ts
  • phpmyfaq/admin/assets/src/configuration/plugins.ts
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php
  • phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php
  • phpmyfaq/src/phpMyFAQ/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoMysql.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoPgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoSqlite.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoSqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Database/Pgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoMysql.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoPgsql.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoSqlite.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoSqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Pgsql.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlite3.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
  • phpmyfaq/src/phpMyFAQ/Setup/Update.php
  • phpmyfaq/translations/language_de.php
  • phpmyfaq/translations/language_en.php
🚧 Files skipped from review as they are similar to previous changes (13)
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoSqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlite3.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoPgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoPgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoMysql.php
  • phpmyfaq/translations/language_en.php
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoSqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoMysql.php
  • phpmyfaq/src/phpMyFAQ/Database/PdoSqlite.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlsrv.php
  • phpmyfaq/translations/language_de.php
🧰 Additional context used
📓 Path-based instructions (2)
**/phpmyfaq/src/**/*.php

📄 CodeRabbit inference engine (AGENTS.md)

Use PER Coding Style 3.0 for PHP code

Files:

  • phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/PdoSqlite.php
  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
  • phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php
  • phpmyfaq/src/phpMyFAQ/Database/Pgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Setup/Update.php
  • phpmyfaq/src/phpMyFAQ/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Instance/Database/Pgsql.php
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript coding standards for TypeScript code
Use semicolons at the end of each statement
Use single quotes for strings
Use arrow functions for callbacks

Files:

  • phpmyfaq/admin/assets/src/api/plugins.ts
  • phpmyfaq/admin/assets/src/configuration/plugins.ts
🧠 Learnings (2)
📚 Learning: 2025-12-26T21:59:16.059Z
Learnt from: CR
Repo: thorsten/phpMyFAQ PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-26T21:59:16.059Z
Learning: Applies to **/phpmyfaq/src/**/*.php : Use PER Coding Style 3.0 for PHP code

Applied to files:

  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
📚 Learning: 2025-12-26T21:59:16.058Z
Learnt from: CR
Repo: thorsten/phpMyFAQ PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-26T21:59:16.058Z
Learning: Avoid hard-coded strings in code; use translation features for user-facing text

Applied to files:

  • phpmyfaq/admin/assets/src/configuration/plugins.ts
🧬 Code graph analysis (7)
phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php (2)
phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php (3)
  • prepare (148-148)
  • query (50-50)
  • execute (157-157)
phpmyfaq/src/phpMyFAQ/Database/PdoSqlite.php (3)
  • prepare (316-319)
  • query (289-308)
  • execute (327-330)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (4)
phpmyfaq/src/phpMyFAQ/Configuration.php (1)
  • Configuration (47-530)
phpmyfaq/src/phpMyFAQ/Plugin/PluginInterface.php (6)
  • getName (29-29)
  • getConfig (54-54)
  • registerEvents (59-59)
  • getTranslationsPath (75-75)
  • getStylesheets (67-67)
  • getScripts (83-83)
phpmyfaq/src/phpMyFAQ/Translation.php (2)
  • Translation (25-436)
  • registerPluginTranslations (292-322)
phpmyfaq/src/phpMyFAQ/Database.php (1)
  • getTablePrefix (138-141)
phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php (6)
phpmyfaq/src/phpMyFAQ/Database/Mysqli.php (3)
  • prepare (326-329)
  • query (298-317)
  • execute (338-361)
phpmyfaq/src/phpMyFAQ/Database/PdoMysql.php (3)
  • prepare (291-294)
  • query (264-283)
  • execute (302-305)
phpmyfaq/src/phpMyFAQ/Database/PdoPgsql.php (3)
  • prepare (291-294)
  • query (264-283)
  • execute (302-305)
phpmyfaq/src/phpMyFAQ/Database/PdoSqlite.php (3)
  • prepare (316-319)
  • query (289-308)
  • execute (327-330)
phpmyfaq/src/phpMyFAQ/Database/Pgsql.php (3)
  • prepare (328-335)
  • query (103-122)
  • execute (344-347)
phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php (3)
  • prepare (323-326)
  • query (204-221)
  • execute (335-342)
phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php (1)
phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php (3)
  • prepare (148-148)
  • query (50-50)
  • execute (157-157)
phpmyfaq/src/phpMyFAQ/Setup/Update.php (1)
phpmyfaq/src/phpMyFAQ/Database.php (1)
  • getTablePrefix (138-141)
phpmyfaq/admin/assets/src/api/plugins.ts (1)
phpmyfaq/admin/assets/src/interfaces/response.ts (1)
  • Response (1-8)
phpmyfaq/admin/assets/src/configuration/plugins.ts (4)
phpmyfaq/assets/src/utils/TranslationService.ts (1)
  • TranslationService (3-19)
phpmyfaq/admin/assets/src/api/plugins.ts (2)
  • togglePluginStatus (25-44)
  • savePluginConfig (53-77)
phpmyfaq/assets/src/utils/helper.ts (1)
  • addElement (34-50)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (1)
  • savePluginConfig (235-256)
🪛 ast-grep (0.40.4)
phpmyfaq/admin/assets/src/configuration/plugins.ts

[warning] 98-98: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: container.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

🪛 GitHub Actions: phpMyFAQ Build
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php

[error] 1-1: Mago format --dry-run reported formatting changes needed for this file.

🪛 PHPMD (2.15.0)
phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php

314-314: Avoid unused parameters such as '$options'. (undefined)

(UnusedFormalParameter)

phpmyfaq/src/phpMyFAQ/Database/Pgsql.php

328-328: Avoid unused parameters such as '$options'. (undefined)

(UnusedFormalParameter)

phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php

335-335: Avoid unused parameters such as '$params'. (undefined)

(UnusedFormalParameter)

phpmyfaq/src/phpMyFAQ/Database/Mysqli.php

326-326: Avoid unused parameters such as '$options'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (26)
phpmyfaq/src/phpMyFAQ/Instance/Database/PdoSqlite.php (1)

176-180: LGTM! Plugin table schema is well-designed.

The faqplugins table schema is appropriate for plugin management with a string primary key, boolean flag, and JSON config storage. The schema is consistent with SQLite conventions.

phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php (2)

141-148: LGTM! Prepared statement support properly abstracted.

The prepare() method appropriately uses mixed return type to accommodate different driver implementations (PDOStatement, mysqli_stmt, string for pg_prepare, resource for sqlsrv_prepare). This is a necessary enhancement for SQL injection prevention.


150-157: LGTM! Execute method completes the prepared statement API.

The execute() method design is sound, accepting the mixed-type statement from prepare() and parameter array, returning a boolean. This enables secure parameterized queries across all database drivers.

phpmyfaq/src/phpMyFAQ/Instance/Database/Pgsql.php (1)

180-184: LGTM! Consistent plugin table schema for PostgreSQL.

The faqplugins table definition is consistent with the SQLite implementation and uses appropriate PostgreSQL data types. The schema correctly supports plugin state and configuration persistence.

phpmyfaq/src/phpMyFAQ/Setup/Update.php (2)

187-188: LGTM! Correct placement in update sequence.

The 4.2 alpha updates are appropriately positioned before table optimization, consistent with the update pattern for other versions.


1128-1179: Resolve the formatting issue flagged by the pipeline.

The pipeline reports that mago format --dry-run still detects formatting changes needed for this file, despite a past review comment indicating this was addressed. Please run the formatter to ensure compliance.

#!/bin/bash
# Run the formatter to fix the issue
mago format phpmyfaq/src/phpMyFAQ/Setup/Update.php

Likely an incorrect or invalid review comment.

phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (9)

55-60: LGTM! Proper dependency injection.

The constructor now accepts Configuration as a dependency, enabling database-backed plugin state management. The readonly modifier ensures immutability.


89-91: LGTM! Idempotency guard prevents duplicate loading.

The early return when plugins are already loaded prevents duplicate registration and unnecessary database queries.


104-148: LGTM! Robust DB-backed plugin state integration.

The database integration correctly:

  • Fetches plugin states and configs
  • Applies activation status
  • Merges JSON config with type coercion via reflection
  • Handles missing config gracefully with is_array() check
  • Defaults new plugins to inactive

The reflection-based type coercion for int/bool/float is well-guarded with try-catch.


153-205: LGTM! Activation logic properly gates plugin loading.

The activation check correctly ensures only active plugins with satisfied dependencies are loaded. The method_exists guards for translations, stylesheets, and scripts provide good defensive programming against incomplete plugin implementations.


208-230: LGTM! Clean lifecycle API.

The activation/deactivation methods correctly update database state. Note that activation changes take effect on the next loadPlugins() call, which is appropriate for maintaining system stability without runtime plugin reloading.


232-256: Excellent! SQL injection vulnerability has been fixed.

The critical security issue flagged in past reviews has been properly addressed. The code now uses prepared statements with parameter binding (prepare() with ? placeholders and execute() with parameter arrays) instead of string concatenation with escape(). This is the correct and secure approach.


258-282: Excellent! SQL injection vulnerability has been fixed here too.

The critical security issue in updatePluginStatus has been properly resolved using prepared statements with parameter binding, matching the secure approach in savePluginConfig.


284-308: LGTM! Safe database query with good error handling.

The direct query is safe since $table is constructed from the internal table prefix, not user input. The try-catch appropriately handles scenarios where the table doesn't exist yet (e.g., during installation/upgrade).


396-469: LGTM! Asset registration with appropriate security controls.

The stylesheet and script registration methods properly:

  • Normalize paths and handle Windows/Unix differences
  • Prevent directory traversal with realpath() and case-insensitive str_starts_with() checks
  • Validate file existence before registration
  • Store web-relative paths for template use

The realpath() fallback using the ?: operator handles edge cases gracefully.

phpmyfaq/src/phpMyFAQ/Database/Pgsql.php (1)

317-317: LGTM!

The faqplugins table is correctly added to the list of table names.

phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php (1)

312-312: LGTM!

The faqplugins table is correctly added to the list of table names.

phpmyfaq/src/phpMyFAQ/Database/Mysqli.php (2)

255-255: LGTM!

The faqplugins table is correctly added to the list of table names.


319-361: Well-implemented prepared statement support with automatic type binding.

The implementation correctly handles parameter type detection and binding. The fallback to 'b' (blob) for non-standard types is reasonable for edge cases.

Minor note: The unused $options parameter is acceptable as it's required by the DatabaseDriver interface.

phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php (2)

273-273: LGTM!

The faqplugins table is correctly added to the list of table names.


307-345: Well-implemented prepared statement support with proper SQLite type binding.

The implementation correctly uses 1-based indexing for bindValue() and properly detects SQLite data types. The unused $options parameter is acceptable as it's required by the DatabaseDriver interface.

phpmyfaq/admin/assets/src/api/plugins.ts (2)

1-44: LGTM - CSRF protection and error handling properly implemented.

The function correctly includes the CSRF token in the header and checks response.ok before parsing the response. The error handling properly attempts to parse error details from the response body.

As per coding guidelines, single quotes and semicolons are correctly used.


46-77: LGTM with a minor observation on duplicate CSRF token.

The CSRF token is sent in both the header (X-CSRF-Token) and the body (csrf). The comment on line 67 indicates this is intentional for backend validation. If only one location is needed, consider removing the redundancy.

phpmyfaq/admin/assets/src/configuration/plugins.ts (3)

22-58: Well-implemented plugin toggle functionality with proper error handling.

The implementation correctly:

  • Uses the TranslationService for localized notifications (as suggested in past comments)
  • Passes CSRF token to API calls
  • Reverts checkbox state on failure
  • Handles both API errors and exceptions

60-172: Good modal implementation with dynamic form field generation.

The configuration modal properly:

  • Uses addElement() helper as suggested
  • Generates appropriate input types (checkbox for boolean, number for numeric values, email/date for matching key names)
  • Handles textarea for long text values
  • Shows/hides the save button based on config presence

The static analysis warning about container.innerHTML = '' on line 99 is a false positive - setting innerHTML to an empty string to clear the container is safe.


175-226: Solid form serialization and save implementation.

The save handler correctly:

  • Parses checkbox values as booleans
  • Parses number inputs as integers or floats
  • Handles NaN edge case by defaulting to 0
  • Reloads the page on success to reflect changes

@d0ubIeU d0ubIeU requested a review from thorsten January 7, 2026 19:36
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27ab9dd and c52c3ec.

📒 Files selected for processing (2)
  • phpmyfaq/src/phpMyFAQ/Database/Pgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php
🧰 Additional context used
📓 Path-based instructions (1)
**/phpmyfaq/src/**/*.php

📄 CodeRabbit inference engine (AGENTS.md)

Use PER Coding Style 3.0 for PHP code

Files:

  • phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Database/Pgsql.php
🧬 Code graph analysis (1)
phpmyfaq/src/phpMyFAQ/Database/Pgsql.php (2)
phpmyfaq/src/phpMyFAQ/Database/PdoPgsql.php (3)
  • prepare (291-294)
  • query (264-283)
  • execute (302-305)
phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php (3)
  • prepare (148-148)
  • query (50-50)
  • execute (157-157)
🪛 PHPMD (2.15.0)
phpmyfaq/src/phpMyFAQ/Database/Pgsql.php

333-333: Avoid unused parameters such as '$options'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (5)
phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php (2)

312-312: LGTM!

The addition of the faqplugins table to the table names array is correct and aligns with the plugin system feature.


316-350: Implementation correctly addresses SQL Server parameter binding constraints and complies with PER Coding Style 3.0.

The revised approach properly handles SQL Server's requirement that parameters must be passed as references at prepare-time. By returning the query string when placeholders exist and deferring binding to sqlsrv_query() in execute(), parameters are now correctly bound.

Consistency check across drivers confirms this architecture is justified: Mysqli, Sqlite3, and Pgsql each use their underlying PHP extension's native API, which allow parameter binding after prepare. SQL Server's API requires a different pattern, hence the conditional return type. All implementations correctly use their extension's capabilities.

One edge case: passing parameters to execute() with a prepared resource (no placeholders) silently returns false. This is correct behavior—you cannot bind parameters to a statement without placeholders—though consider adding a warning log if unexpected.

phpmyfaq/src/phpMyFAQ/Database/Pgsql.php (3)

55-58: LGTM! Property is well-defined.

The $preparedStatements property provides a clean mechanism to track prepared statements and ensure unique naming.


322-322: LGTM! Table name addition is correct.

The faqplugins table is properly added to support the plugin management functionality.


326-341: Implementation resolves the past review concern about statement name collisions.

The counter-based approach (count($this->preparedStatements) + 1) ensures each prepare() call generates a unique statement name, eliminating the collision issue raised in the previous review where md5($query) would produce identical names when preparing the same query multiple times.

The unused $options parameter is acceptable—it's required by the DatabaseDriver interface for cross-driver consistency.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (2)

398-420: Path traversal vulnerability and code duplication.

Critical security issue: The case-insensitive path validation (lines 412, 462) using strtolower() is intended for Windows compatibility but introduces a vulnerability on case-sensitive filesystems. An attacker could potentially craft paths that bypass the security check using mixed case (e.g., ../../Etc/passwd).

Refactoring: The path normalization and validation logic is duplicated between registerPluginStylesheets and registerPluginScripts. Extract this into a private helper method.

🔒 Proposed security fix
-        // Normalize the path for comparison (slashes and case)
-        $realPluginDir = str_replace('\\', '/', realpath($pluginDir) ?: $pluginDir);
-        $realPluginDir = rtrim($realPluginDir, '/') . '/';
-
-        foreach ($stylesheets as $stylesheet) {
-            $fullPath = $pluginDir . '/' . $stylesheet;
-            $absolutePath = str_replace('\\', '/', realpath($fullPath) ?: $fullPath);
-
-            // Security check: Must be inside the plugin directory (case-insensitive for Windows)
-            if (str_starts_with(strtolower($absolutePath), strtolower($realPluginDir)) && file_exists($fullPath)) {
-                $validatedStylesheets[] = 'content/plugins/' . $pluginName . '/' . $stylesheet;
-            }
-        }
+        foreach ($stylesheets as $stylesheet) {
+            if ($validatedPath = $this->validatePluginAssetPath($pluginName, $pluginDir, $stylesheet)) {
+                $validatedStylesheets[] = $validatedPath;
+            }
+        }

Add a new private method:

/**
 * Validates and normalizes a plugin asset path
 * 
 * @return string|null Returns web-relative path if valid, null otherwise
 */
private function validatePluginAssetPath(string $pluginName, string $pluginDir, string $assetPath): ?string
{
    // Normalize slashes
    $pluginDir = str_replace('\\', '/', $pluginDir);
    $assetPath = str_replace('\\', '/', $assetPath);
    
    // Build full path and resolve it
    $fullPath = $pluginDir . '/' . $assetPath;
    $realFullPath = realpath($fullPath);
    $realPluginDir = realpath($pluginDir);
    
    // Both must resolve successfully
    if ($realFullPath === false || $realPluginDir === false) {
        return null;
    }
    
    // Normalize for comparison
    $realFullPath = str_replace('\\', '/', $realFullPath);
    $realPluginDir = str_replace('\\', '/', rtrim($realPluginDir, '/')) . '/';
    
    // Security check: case-sensitive prefix match
    if (!str_starts_with($realFullPath, $realPluginDir)) {
        return null;
    }
    
    return 'content/plugins/' . $pluginName . '/' . $assetPath;
}

This fix:

  1. Uses case-sensitive comparison (safer)
  2. Requires both paths to resolve via realpath() (prevents path traversal)
  3. Eliminates code duplication

Also applies to: 448-470


1-524: Pipeline failure: Code formatting required.

The pipeline reports that Mago formatting is required. Run mago format to fix code style issues before merging.

#!/bin/bash
# Run Mago formatter to fix code style
mago format phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
phpmyfaq/src/phpMyFAQ/Database/Pgsql.php (1)

147-158: Potential null dereference when calling pg_result_status().

If pg_query() returns false on line 147, pg_result_status($result) on line 153 will be called with false, which may cause unexpected behavior or a warning.

Proposed fix
         $result = pg_query($this->conn, $query);

         if (!$result) {
             $this->sqlLog .= $this->error();
+            return false;
         }

         if (pg_result_status($result) === PGSQL_COMMAND_OK) {
             return true;
         }

         return $result;
🤖 Fix all issues with AI agents
In @phpmyfaq/src/phpMyFAQ/Database/Mysqli.php:
- Around line 394-397: When $statement->stmt->execute() succeeds, do not
directly assign the return of $statement->stmt->get_result() to
$statement->result because get_result() can return false; instead call
get_result(), check if it === false and handle that case by setting
$statement->result to null (or an appropriate empty value) and/or falling back
to non-result metadata (e.g. $statement->stmt->affected_rows or store_result()
as applicable); update the code around $statement->stmt->execute() /
$statement->stmt->get_result() so downstream callers of $statement->result can
safely handle no-result queries or environments without mysqlnd.

In @phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php:
- Around line 366-376: The binding loop in Sqlite3.php treats booleans as text
because there is no is_bool check; update the foreach over $params to detect
is_bool($param) and bind it as SQLITE3_INTEGER (casting the value to int) when
calling $statement->stmt->bindValue so booleans are stored as 0/1; keep existing
branches for is_int/is_float/is_null and ensure the boolean branch uses
SQLITE3_INTEGER and (int)$param.

In @phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php:
- Around line 351-364: The file contains two consecutive PHPDoc blocks for the
prepare() method; remove the duplicate block so only one PHPDoc remains
immediately above the prepare() method declaration—prefer keeping the more
specific block that returns "SqlsrvStatement|false" and delete the earlier,
redundant block to avoid duplicate documentation.

In @phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php:
- Around line 114-143: The JSON decode of $dbPlugins[$pluginName]['config'] may
fail silently; after calling json_decode(...) assign its result to $configArray,
then check json_last_error() and log or throw via the plugin manager logger when
decoding fails (include the raw config and json_last_error_msg()); only proceed
if json_decode returned an array. Also extend the ReflectionProperty type
handling in the loop over $configArray (where you use $rp = new
\ReflectionProperty($configObject, $key) and $typeName = $type->getName()) to
explicitly handle 'string' (cast to (string)) and 'array' (ensure value is an
array, e.g., by json_decoding the individual value or validating/is_array and
falling back to an empty array) and keep the existing int/bool/float cases, so
property assignments on $configObject->$key preserve expected types and
malformed inputs are reported.
- Around line 287-308: The getPluginsFromDatabase method builds a query with
direct table-name interpolation and catches a broad \Exception; change it to
build/execute the SELECT using the database API's safe methods (e.g., prepared
statement or the DB wrapper's safe table quoting) instead of raw string
interpolation of \phpMyFAQ\Database::getTablePrefix(), and replace the
catch(\Exception $e) with the specific database exception type thrown by the DB
layer so only missing-table errors are handled (rethrow or let others bubble).
Locate getPluginsFromDatabase, the $table construction that uses
\phpMyFAQ\Database::getTablePrefix(), and the $db->query / $db->fetchObject
usage to update the query execution and exception type.
- Around line 211-230: activatePlugin and deactivatePlugin only change DB via
updatePluginStatus but don't update runtime $this->loadedPlugins, so
isPluginActive can return stale data; fix by mutating $this->loadedPlugins
immediately after updating the DB: in activatePlugin(string $pluginName) call
updatePluginStatus($pluginName, true) then add $pluginName to
$this->loadedPlugins if not present, and in deactivatePlugin(string $pluginName)
call updatePluginStatus($pluginName, false) then remove $pluginName from
$this->loadedPlugins if present; alternatively (or additionally) provide a
public reloadPlugins() method that calls loadPlugins() and document that admins
must reload for changes to take effect—use the existing updatePluginStatus,
activatePlugin, deactivatePlugin, isPluginActive, loadPlugins and
$this->loadedPlugins symbols to locate where to apply the change.
🧹 Nitpick comments (8)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (3)

89-91: Consider thread-safety implications.

The early return check is not atomic. If the application architecture evolves to support concurrent plugin loading (e.g., async workers), this could lead to race conditions. For now, PHP's request-scoped execution model makes this safe.


235-256: Add input validation and consider extracting upsert logic.

The method lacks validation for the $pluginName parameter (could contain unexpected characters) and the $configData array (could be too large or contain non-serializable data).

Additionally, the "check existence then INSERT or UPDATE" pattern is duplicated with updatePluginStatus(). Consider extracting this into a private upsertPlugin() helper method to reduce duplication.

♻️ Suggested improvements

Add validation:

 public function savePluginConfig(string $pluginName, array $configData): void
 {
+    if (empty($pluginName) || preg_match('/[^a-zA-Z0-9_-]/', $pluginName)) {
+        throw new \InvalidArgumentException('Invalid plugin name');
+    }
+    
     $jsonConfig = json_encode($configData);
+    if ($jsonConfig === false) {
+        throw new \RuntimeException('Failed to encode config data: ' . json_last_error_msg());
+    }

Extract upsert pattern to reduce duplication with updatePluginStatus().


261-282: Consider transaction wrapping for atomic DB updates.

DB operations that check-then-update/insert should ideally be wrapped in a transaction to prevent race conditions if multiple processes attempt to modify the same plugin simultaneously.

💡 Example with transaction
 private function updatePluginStatus(string $pluginName, bool $active): void
 {
     $db = $this->configuration->getDb();
     $table = \phpMyFAQ\Database::getTablePrefix() . 'faqplugins';
     $activeInt = $active ? 1 : 0;
+    
+    $db->beginTransaction();
+    try {
         // Check if exists
         $select = sprintf('SELECT name FROM %s WHERE name = ?', $table);
         // ... rest of logic ...
         $stmt = $db->prepare($query);
         $db->execute($stmt, $params);
+        $db->commit();
+    } catch (\Exception $e) {
+        $db->rollBack();
+        throw $e;
+    }
 }
phpmyfaq/src/phpMyFAQ/Database/Pgsql.php (2)

30-59: Consider moving PgsqlStatement to its own file.

The PgsqlStatement class is defined inline within Pgsql.php. For PSR-4 autoloading compliance and maintainability, consider extracting it to phpmyfaq/src/phpMyFAQ/Database/PgsqlStatement.php.


361-376: Unused $options parameter in prepare().

The $options parameter is declared but never used. This is intentional for interface consistency with other drivers (PDO uses options), but consider adding a PHPDoc @SuppressWarnings(PHPMD.UnusedFormalParameter) annotation to silence the static analysis warning.

Suppress unused parameter warning
     /**
      * Prepares a statement for execution and returns a statement object.
      *
      * @param string $query   The SQL query
      * @param array  $options The driver options
      * @return PgsqlStatement|false
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
      */
     public function prepare(string $query, array $options = []): PgsqlStatement|false
phpmyfaq/src/phpMyFAQ/Database/Mysqli.php (1)

30-54: Consider moving MysqliStatement to its own file.

Same as PgsqlStatement, this class should ideally be in its own file for PSR-4 compliance.

phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php (1)

25-49: Consider moving Sqlite3Statement to its own file.

Consistent with other drivers, consider extracting to a separate file.

phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php (1)

27-56: Consider moving SqlsrvStatement to its own file.

Consistent with other drivers, consider extracting to a separate file.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c52c3ec and 8aa2adc.

📒 Files selected for processing (5)
  • phpmyfaq/src/phpMyFAQ/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Database/Pgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
🧰 Additional context used
📓 Path-based instructions (1)
**/phpmyfaq/src/**/*.php

📄 CodeRabbit inference engine (AGENTS.md)

Use PER Coding Style 3.0 for PHP code

Files:

  • phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php
  • phpmyfaq/src/phpMyFAQ/Database/Pgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php
🪛 GitHub Actions: phpMyFAQ Build
phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php

[error] 1-1: Mago formatting required. File needs to be formatted. Run 'mago format' to fix code style issues in this file.

phpmyfaq/src/phpMyFAQ/Database/Pgsql.php

[error] 1-1: Mago formatting required. File needs to be formatted. Run 'mago format' to fix code style issues in this file.

phpmyfaq/src/phpMyFAQ/Database/Mysqli.php

[error] 1-1: Mago formatting required. File needs to be formatted. Run 'mago format' to fix code style issues in this file.

phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php

[error] 1-1: Mago formatting required. File needs to be formatted. Run 'mago format' to fix code style issues in this file.

phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php

[error] 1-1: Mago formatting required. File needs to be formatted. Run 'mago format' to fix code style issues in this file.

🪛 PHPMD (2.15.0)
phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php

344-344: Avoid unused parameters such as '$options'. (undefined)

(UnusedFormalParameter)

phpmyfaq/src/phpMyFAQ/Database/Pgsql.php

368-368: Avoid unused parameters such as '$options'. (undefined)

(UnusedFormalParameter)

phpmyfaq/src/phpMyFAQ/Database/Mysqli.php

356-356: Avoid unused parameters such as '$options'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (10)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (2)

55-60: LGTM! Clean dependency injection.

The constructor properly accepts the Configuration dependency using PHP 8 promoted properties, enabling DB-backed plugin management.


153-204: Activation gating logic looks solid.

The conditional registration based on active state is correctly implemented with appropriate guards for optional plugin methods. The dependency checking preserves the original incompatibility handling behavior.

phpmyfaq/src/phpMyFAQ/Database/Pgsql.php (1)

1-1: Pipeline failure: Mago formatting required.

The CI pipeline indicates this file needs formatting. Run mago format to fix code style issues.

phpmyfaq/src/phpMyFAQ/Database/Mysqli.php (2)

378-400: Parameter binding works correctly.

The type inference logic for bind_param() correctly maps PHP types to mysqli type specifiers (i/d/s/b). The implementation handles the common use cases well.

Note: Using 'b' (blob) as fallback for non-scalar types might not work as expected since blob parameters require special handling with send_long_data(). Consider throwing an exception for unsupported types instead.


1-1: Pipeline failure: Mago formatting required.

Run mago format to fix code style issues in this file.

phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php (2)

337-351: Consistent implementation with other drivers.

The prepare() method follows the same pattern as other drivers. The unused $options parameter maintains interface consistency.


1-1: Pipeline failure: Mago formatting required.

Run mago format to fix code style issues in this file.

phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php (3)

399-405: Parameterized queries executed via sqlsrv_query() with direct parameter binding.

When statement->statement is a string (from the ? placeholder path), parameters are passed directly to sqlsrv_query(). This is correct for SQL Server's parameterized query support - the driver handles parameter binding safely. However, this mixing of approaches (prepared statements vs. direct parameterized queries) may cause confusion.


1-1: Pipeline failure: Mago formatting required.

Run mago format to fix code style issues in this file.


365-376: Remove the SQL injection concern; the implementation is secure.

While the prepare() method does behave differently from other drivers (returning a raw SQL string when ? is detected, rather than a prepared statement resource), this pattern is intentional for the sqlsrv extension. The execute() method correctly handles both cases—when it receives a string statement, it uses sqlsrv_query() with the parameters array, which the sqlsrv extension properly binds separately to prevent SQL injection. This is the correct approach for the sqlsrv API. No security risk exists here.

If code consistency across drivers is a concern, note that this class is already marked @deprecated with plans for removal in v5.0 in favor of PDO.

Likely an incorrect or invalid review comment.

Comment on lines +351 to +364
/**
* Prepares a statement for execution and returns a statement object.
*
* @param string $query The SQL query
* @param array $options The driver options
* @return resource|string|false
*/
/**
* Prepares a statement for execution and returns a statement object.
*
* @param string $query The SQL query
* @param array $options The driver options
* @return SqlsrvStatement|false
*/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate PHPDoc block.

There are two consecutive PHPDoc blocks for the prepare() method. Remove the duplicate.

Proposed fix
-    /**
-     * Prepares a statement for execution and returns a statement object.
-     *
-     * @param string $query   The SQL query
-     * @param array  $options The driver options
-     * @return resource|string|false
-     */
     /**
      * Prepares a statement for execution and returns a statement object.
      *
      * @param string $query   The SQL query
      * @param array  $options The driver options
      * @return SqlsrvStatement|false
      */
     public function prepare(string $query, array $options = []): SqlsrvStatement|false
🤖 Prompt for AI Agents
In @phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php around lines 351 - 364, The file
contains two consecutive PHPDoc blocks for the prepare() method; remove the
duplicate block so only one PHPDoc remains immediately above the prepare()
method declaration—prefer keeping the more specific block that returns
"SqlsrvStatement|false" and delete the earlier, redundant block to avoid
duplicate documentation.

Comment on lines +211 to +230
public function activatePlugin(string $pluginName): void
{
$this->updatePluginStatus($pluginName, true);
}

/**
* Deactivates a plugin
*/
public function deactivatePlugin(string $pluginName): void
{
$this->updatePluginStatus($pluginName, false);
}

/**
* Checks if a plugin is active
*/
public function isPluginActive(string $pluginName): bool
{
return in_array($pluginName, $this->loadedPlugins, true);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Runtime state is not synchronized with DB changes.

The activatePlugin and deactivatePlugin methods only update the database but don't modify the runtime state ($this->loadedPlugins). This means isPluginActive() will return stale information until the next request when loadPlugins() runs again.

Consider either:

  1. Updating $this->loadedPlugins immediately (add/remove from array)
  2. Documenting that plugin state changes require a reload/new request
  3. Providing a reloadPlugins() method to refresh state

The current behavior could confuse administrators who toggle plugins and don't see immediate effects.

🔄 Example fix for immediate state update
 public function activatePlugin(string $pluginName): void
 {
     $this->updatePluginStatus($pluginName, true);
+    if (!in_array($pluginName, $this->loadedPlugins, true)) {
+        $this->loadedPlugins[] = $pluginName;
+    }
 }

 public function deactivatePlugin(string $pluginName): void
 {
     $this->updatePluginStatus($pluginName, false);
+    $key = array_search($pluginName, $this->loadedPlugins, true);
+    if ($key !== false) {
+        unset($this->loadedPlugins[$key]);
+        $this->loadedPlugins = array_values($this->loadedPlugins);
+    }
 }

Note: Full activation/deactivation would also require registering/unregistering events and assets, which is more complex.

🤖 Prompt for AI Agents
In @phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php around lines 211 - 230,
activatePlugin and deactivatePlugin only change DB via updatePluginStatus but
don't update runtime $this->loadedPlugins, so isPluginActive can return stale
data; fix by mutating $this->loadedPlugins immediately after updating the DB: in
activatePlugin(string $pluginName) call updatePluginStatus($pluginName, true)
then add $pluginName to $this->loadedPlugins if not present, and in
deactivatePlugin(string $pluginName) call updatePluginStatus($pluginName, false)
then remove $pluginName from $this->loadedPlugins if present; alternatively (or
additionally) provide a public reloadPlugins() method that calls loadPlugins()
and document that admins must reload for changes to take effect—use the existing
updatePluginStatus, activatePlugin, deactivatePlugin, isPluginActive,
loadPlugins and $this->loadedPlugins symbols to locate where to apply the
change.

Comment on lines 287 to 308
private function getPluginsFromDatabase(): array
{
$db = $this->configuration->getDb();
$table = \phpMyFAQ\Database::getTablePrefix() . 'faqplugins';

// Ensure table exists to avoid crashes during update/install if not yet run
try {
$result = $db->query("SELECT name, active, config FROM $table");
} catch (\Exception $e) {
// Table might not exist yet
return [];
}

$plugins = [];
while ($row = $db->fetchObject($result)) {
$plugins[$row->name] = [
'active' => $row->active,
'config' => $row->config
];
}
return $plugins;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

SQL injection risk and overly broad exception handling.

Critical: Line 294 uses direct string interpolation for the table name in the query. While getTablePrefix() is likely safe, this pattern is risky and inconsistent with the prepared statement usage elsewhere in this class. If table prefix ever comes from user input or configuration, this becomes a critical SQL injection vulnerability.

Major: The catch block captures all \Exception instances, which could mask unrelated errors (e.g., connection failures, permission issues). This should catch specific database exceptions.

🔒 Proposed fixes
 private function getPluginsFromDatabase(): array
 {
     $db = $this->configuration->getDb();
     $table = \phpMyFAQ\Database::getTablePrefix() . 'faqplugins';
     
     // Ensure table exists to avoid crashes during update/install if not yet run
     try {
-        $result = $db->query("SELECT name, active, config FROM $table");
+        $query = sprintf('SELECT name, active, config FROM %s', $table);
+        $result = $db->query($query);
-    } catch (\Exception $e) {
+    } catch (\phpMyFAQ\Database\DatabaseException $e) {
         // Table might not exist yet
+        error_log("Plugin table not found or query failed: " . $e->getMessage());
         return [];
     }

Note: Use the specific database exception class that your framework provides instead of the generic \Exception.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php around lines 287 - 308, The
getPluginsFromDatabase method builds a query with direct table-name
interpolation and catches a broad \Exception; change it to build/execute the
SELECT using the database API's safe methods (e.g., prepared statement or the DB
wrapper's safe table quoting) instead of raw string interpolation of
\phpMyFAQ\Database::getTablePrefix(), and replace the catch(\Exception $e) with
the specific database exception type thrown by the DB layer so only
missing-table errors are handled (rethrow or let others bubble). Locate
getPluginsFromDatabase, the $table construction that uses
\phpMyFAQ\Database::getTablePrefix(), and the $db->query / $db->fetchObject
usage to update the query execution and exception type.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
phpmyfaq/src/phpMyFAQ/Database/Mysqli.php (1)

1-1: Remove diff markers from lines 402–404.

The file contains literal + characters at the start of lines 402–404 that are breaking PHP parsing. These diff markers must be removed:

Lines 402–404
        $result = $statement->stmt->get_result();
-+            if ($result !== false) {
-+                $statement->result = $result;
-+            }
+            if ($result !== false) {
+                $statement->result = $result;
+            }
        }
🤖 Fix all issues with AI agents
In @docs/plugins.md:
- Line 33: Replace the extra/odd whitespace in the sentence containing "values
​​in the file" with a single standard ASCII space so the phrase reads "values in
the file"; locate the sentence in the docs/plugins.md text (the line that
currently reads "These predefined values ​​in the file form the basis...") and
normalize the spacing (remove any non-breaking or zero-width space) between
"values" and "in".

In @phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php:
- Around line 34-37: The call to Token::getInstance(...)->verifyToken currently
passes the CSRF token as the first argument and the page id second; swap the
parameters so the page identifier 'admin-plugins' is the first argument and the
token variable $csrfToken is the second when calling verifyToken (keep using
Token::getInstance($this->session) and the same return/JsonResponse logic).
- Around line 81-84: In PluginController (Administration\Api\PluginController)
the call to Token::getInstance(...)->verifyToken currently passes ($csrfToken,
'admin-plugins') with parameters reversed; change the call to pass the token
name first and the token value second (i.e., verifyToken('admin-plugins',
$csrfToken)) so the CSRF check uses the correct arguments; also check and fix
the identical swap in the toggleStatus handler where verifyToken is used.

In @phpmyfaq/src/phpMyFAQ/Database/Mysqli.php:
- Around line 289-290: The array listing table names contains a duplicate entry
"$prefix . 'faqplugins'" on consecutive lines; remove the redundant line so
"$prefix . 'faqplugins'" only appears once in the table names array (ensure any
related usages still rely on a single entry and keep the list unique).
- Around line 169-176: In Mysqli.php remove the duplicate unwrapping check for
MysqliStatement: there are two identical blocks checking "if ($result instanceof
MysqliStatement) { $result = $result->result; }" — delete the second occurrence
so $result is unwrapped only once (keep the first block that performs the unwrap
on MysqliStatement) and ensure no other code relies on the redundant branch.

In @phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php:
- Around line 365-392: The pipeline reports a formatting discrepancy in the
execute method of Sqlite3 (phpMyFAQ/Database/Sqlite3.php) around the bool param
handling; there is no logic bug—run the project's code formatter (mago format as
used by CI, or the configured PHP formatter/CS fixer) on this file to apply the
expected whitespace/indentation changes, then stage the formatted file and
update the PR; target the execute(mixed $statement, array $params = []) method
where the boolean branch sets $type and $param to ensure the file matches the
formatter's output.
- Around line 307-308: The returned table list includes a duplicate entry for
$prefix . 'faqplugins'; locate the array (in the Sqlite3 class method that
builds/returns the table list, e.g., the method that constructs the $prefix .
'faqplugins' entries) and remove the repeated $prefix . 'faqplugins' element so
each table name appears only once, ensuring any downstream code receives a
unique list of tables.
- Around line 142-148: In Sqlite3.php inside the code handling $result (the
block that checks "if ($result instanceof Sqlite3Statement) { $result =
$result->result; }"), remove the duplicated second occurrence so the unwrapping
only happens once; locate the duplicate checks against Sqlite3Statement for the
$result variable and delete the redundant block to avoid dead code and preserve
the single unwrap behavior.

In @phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php:
- Line 146: The CI flagged formatting issues in
phpMyFAQ/Plugin/PluginManager.php around the PluginManager class (specifically
near the blocks at the locations corresponding to lines 146, 194, and 290); run
the project formatter (mago format) on that file, fix any
whitespace/indentation/bracing inconsistencies reported, verify the file formats
cleanly, and commit the formatted file so the pipeline passes.
- Around line 128-138: In PluginManager:: (inside the switch that handles
$typeName) replace the filter_var(..., FILTER_VALIDATE_BOOLEAN) usage with an
explicit boolean cast so boolean config values use (bool) $value; update the
case 'bool' branch to convert truthy/falsy values via (bool) $value (or add
explicit string checks if you need stricter "true"/"false" semantics) to avoid
FILTER_VALIDATE_BOOLEAN returning null for some inputs.
🧹 Nitpick comments (2)
docs/plugins.md (2)

84-102: Clarify the purpose and usage of getImplementation() return value.

The method returns a Twig template string rather than HTML or documentation. Without context, it's unclear how this return value is intended to be used by the plugin system. Does the backend render this as a template, display it as-is, or use it for documentation purposes?

Recommend adding a clarifying comment above this method or expanding the documentation section to explain the contract.


164-164: Good contextual note about Extended Description and Implementation fields.

The note effectively ties the example code (particularly the getAdvDescription() and getImplementation() methods) to their backend UI manifestation. However, consider moving or restating this context earlier—either in the method documentation or immediately after introducing the new getter methods—so readers understand their purpose before encountering the full implementation.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8aa2adc and ebf90bf.

📒 Files selected for processing (5)
  • docs/plugins.md
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php
  • phpmyfaq/src/phpMyFAQ/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php
  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
🧰 Additional context used
📓 Path-based instructions (1)
**/phpmyfaq/src/**/*.php

📄 CodeRabbit inference engine (AGENTS.md)

Use PER Coding Style 3.0 for PHP code

Files:

  • phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php
  • phpmyfaq/src/phpMyFAQ/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php
🧬 Code graph analysis (3)
phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php (4)
phpmyfaq/src/phpMyFAQ/Database/Mysqli.php (4)
  • __construct (35-39)
  • fetchAll (44-53)
  • fetchAll (167-187)
  • execute (377-408)
phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlite3.php (1)
  • __construct (356-359)
phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php (2)
  • fetchAll (77-77)
  • execute (157-157)
phpmyfaq/src/phpMyFAQ/Database/PdoSqlite.php (2)
  • fetchAll (118-125)
  • execute (327-330)
phpmyfaq/src/phpMyFAQ/Database/Mysqli.php (3)
phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php (6)
  • __construct (30-34)
  • fetchAll (39-48)
  • fetchAll (140-160)
  • prepare (349-356)
  • query (207-222)
  • execute (365-392)
phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php (4)
  • fetchAll (77-77)
  • prepare (148-148)
  • query (50-50)
  • execute (157-157)
phpmyfaq/src/phpMyFAQ/Database/PdoMysql.php (4)
  • fetchAll (117-124)
  • prepare (291-294)
  • query (264-283)
  • execute (302-305)
phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php (2)
phpmyfaq/src/phpMyFAQ/Session/Token.php (2)
  • Token (27-219)
  • verifyToken (132-162)
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (5)
  • loadPlugins (87-208)
  • getPlugins (341-344)
  • activatePlugin (213-216)
  • deactivatePlugin (221-224)
  • savePluginConfig (237-258)
🪛 GitHub Actions: phpMyFAQ Build
phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php

[error] 377-377: mago format: formatting changes detected in file.

phpmyfaq/src/phpMyFAQ/Database/Mysqli.php

[error] 1-1: Failed to parse file: Unexpected token 'If'.

phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php

[error] 146-146: mago format: formatting changes detected in file.


[error] 194-194: mago format: formatting changes detected in file.


[error] 290-290: mago format: formatting changes detected in file.

🪛 PHPMD (2.15.0)
phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php

349-349: Avoid unused parameters such as '$options'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (14)
docs/plugins.md (2)

12-12: Good addition of activation/deactivation workflow.

The addition clearly documents that plugins must be activated in the backend and can be toggled without complete uninstallation, which aligns well with the PR objectives for extending the plugin system's configuration and activation capabilities.


15-32: Configuration section is well-structured.

The new section 9.2 clearly explains the PluginConfigurationInterface and provides a concrete example using PHP 8's Constructor Property Promotion. The explanation of how predefined values form the basis for backend configuration options is helpful. Note: Line 33 has a minor spacing issue (flagged separately).

phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php (2)

25-49: LGTM!

The Sqlite3Statement wrapper class follows the same pattern as other database driver statement wrappers (e.g., MysqliStatement, PgsqlStatement). The use of promoted properties is clean, and the deprecation notice properly guides users to the standardized DatabaseDriver::fetchAll() method.


342-356: LGTM!

The prepare() method correctly wraps the native SQLite3 prepare functionality and returns the Sqlite3Statement wrapper. The $options parameter is required by the DatabaseDriver interface contract, so the PHPMD unused parameter warning is a false positive.

phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php (1)

34-34: Verify CSRF token retrieval inconsistency.

The two endpoints handle CSRF tokens differently:

  • toggleStatus (Line 34): retrieves token from header only
  • saveConfig (Line 81): retrieves token from payload with header fallback

Ensure this difference is intentional and aligns with your security requirements.

Also applies to: 81-81

phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (6)

55-60: LGTM!

Clean dependency injection using constructor property promotion. The Configuration dependency is properly injected as a readonly property.


89-91: LGTM!

Good optimization to prevent redundant plugin loading.


171-195: LGTM!

Excellent use of method_exists guards to handle optional plugin methods gracefully. This defensive approach prevents runtime errors when plugins don't implement all optional features.


237-258: LGTM!

The database methods are well-implemented:

  • Proper use of parameterized queries to prevent SQL injection
  • Good error handling in getPluginsFromDatabase for missing table during installation
  • Consistent check-then-upsert pattern in savePluginConfig and updatePluginStatus
  • Correct table prefix usage

Also applies to: 263-284, 289-310


405-422: LGTM!

Excellent path security implementation in both registerPluginStylesheets and registerPluginScripts:

  • Path normalization handles Windows and Unix separators
  • Case-insensitive validation for Windows compatibility
  • Prevents path traversal by ensuring paths remain within plugin directory
  • File existence verification before registration

Also applies to: 455-472


115-119: LGTM!

Good error handling for invalid JSON config. The plugin continues loading without the corrupted config, and the error is logged for debugging.

phpmyfaq/src/phpMyFAQ/Database/Mysqli.php (3)

30-54: LGTM! Consistent implementation of the statement wrapper.

The MysqliStatement class follows the same pattern as other drivers (e.g., Sqlite3Statement) and correctly wraps the prepared statement with an optional result. The deprecation notice on fetchAll() appropriately directs users to DatabaseDriver::fetchAll().


354-368: LGTM! Clean implementation of the prepare method.

The method correctly wraps mysqli::prepare and returns the appropriate MysqliStatement wrapper. The unused $options parameter maintains interface consistency across database drivers.


370-408: LGTM! Robust implementation of the execute method.

The method correctly:

  • Validates the statement type
  • Infers parameter types (int, float, string, blob) for binding
  • Executes the statement and conditionally stores the result

The implementation is consistent with the DatabaseDriver interface and handles edge cases appropriately.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Fix all issues with AI agents
In @phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php:
- Line 82: The if statement in PluginController::... (line with
Token::getInstance($this->session)->verifyToken('admin-plugins', $csrfToken))
has the return statement on the same line causing a formatting/PER style
violation; split the control flow onto multiple lines: place the return new
JsonResponse([...], 403); on the next line, ensure the if uses braces or a
properly formatted single-line block per project style, and fix indentation to
match surrounding code so the Token::getInstance(...)->verifyToken check and the
JsonResponse return are clearly separated.
- Around line 71-83: In saveConfig(), move CSRF validation before full JSON
parsing: first retrieve the CSRF token from the request header (or, if the token
must be supplied in the JSON body, extract/validate only the token field from
the raw request content without json_decoding the entire payload) and call
Token::getInstance($this->session)->verifyToken('admin-plugins', $csrfToken);
only after the token is verified, proceed to call $request->getContent() and
json_decode() and validate the decoded array; mirror the approach used in
toggleStatus() where CSRF is verified prior to parsing to avoid processing
untrusted JSON.

In @phpmyfaq/src/phpMyFAQ/Database/Mysqli.php:
- Around line 378-391: The type-inference must explicitly handle nulls, booleans
and unsupported types before calling $statement->stmt->bind_param: convert
booleans to integers (1/0) and use type 'i'; leave null values as PHP null and
assign them a safe bind type (e.g., 's' or keep as null-aware binding) rather
than 'b'; and reject or serialize arrays/objects (throw an exception or convert
to string) instead of falling back to 'b'. Update the loop that builds $types
and the corresponding $params normalization so nulls and booleans are normalized
and unsupported types are validated prior to invoking bind_param on
$statement->stmt.

In @phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php:
- Around line 351-376: Remove the duplicate/incorrect docblock above the prepare
method so only the correct docblock remains (the one that documents @return
SqlsrvStatement|false); keep the second docblock and delete the first
resource|string|false block, and update the remaining docblock to mention that
the $options parameter is currently unused for interface compatibility (or add a
brief @param note) so it’s clear why $options is present in prepare(string
$query, array $options = []) while the implementation passes options to
sqlsrv_prepare.

In @phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php:
- Around line 201-203: Remove the TODO-style exploratory comments in
PluginManager where incompatible plugins are handled and make the code comment
concise and production-ready: replace the two informal lines around the unset
call with a single clear comment such as "Remove incompatible plugin from the
plugins list" and keep the existing unset($this->plugins[$plugin->getName()]);
call unchanged so the behavior and references to $this->plugins,
$plugin->getName() and unset remain explicit and documented.
- Around line 165-166: Remove the stale inline comment "Apply DB config
overrides here if possible" in the PluginManager class; it is misleading because
DB config overrides are already applied earlier (see the earlier block where the
DB config is merged/applied), so delete that comment near the plugin
initialization/DB logic to avoid confusion for future maintainers.
- Around line 149-152: Replace the informal comment that reads "I will default
to inactive (false) for new plugins found on disk but not in DB." with a more
professional phrasing such as "Default to inactive (false) for new plugins found
on disk but not in DB." near the $isActive assignment in the PluginManager class
so the comment remains clear and formal without changing the logic that sets
$isActive = false.
- Around line 114-147: The code silently proceeds with invalid JSON and uses
heavy reflection for type coercion; update the block handling json_decode so
that if json_last_error() !== JSON_ERROR_NONE you log the error with
json_last_error_msg() and immediately skip applying that config (continue to
next plugin) or explicitly reset to the plugin default config from
$plugin->getConfig(); then prefer an explicit deserialization path by checking
if the config class (get_class($plugin->getConfig())) exposes a static
fromArray(array $data): static and call that to build the config object, and
only if fromArray does not exist fall back to the current
ReflectionProperty-based coercion (ReflectionProperty, getType(),
ReflectionNamedType) as a last-resort fallback.
- Around line 170-172: Remove the unnecessary method_exists() guards around
interface methods in PluginManager: directly call
$plugin->getTranslationsPath(), $plugin->getStylesheets(), and
$plugin->getScripts() (remove the checks that wrap those calls at the locations
using method_exists). These methods are defined on PluginInterface, so eliminate
the defensive conditionals and invoke the methods directly on the $plugin
instance (retaining any existing handling of the returned values).
🧹 Nitpick comments (9)
phpmyfaq/src/phpMyFAQ/Database/Pgsql.php (1)

361-376: Consider documenting the unused $options parameter.

The $options parameter is defined but never used in the implementation. If this is intentional for interface compatibility with DatabaseDriver, consider adding a comment to clarify.

📝 Optional clarification comment
 /**
  * Prepares a statement for execution and returns a statement object.
  *
  * @param string $query   The SQL query
- * @param array  $options The driver options
+ * @param array  $options The driver options (unused for PostgreSQL)
  * @return PgsqlStatement|false
  */
 public function prepare(string $query, array $options = []): PgsqlStatement|false
phpmyfaq/src/phpMyFAQ/Database/Mysqli.php (2)

30-54: Consider removing the deprecated method from the new class.

While the MysqliStatement wrapper class serves its purpose, including a deprecated fetchAll() method in a newly introduced class is unusual. Since the main Mysqli class already handles this via fetchAll(mixed $result) (lines 169-171), consider removing this method to avoid confusion and reduce maintenance burden.

♻️ Proposed refactor
 class MysqliStatement
 {
     public function __construct(
         public \mysqli_stmt $stmt,
         public mixed $result = null,
     ) {
     }
-
-    /**
-     * @deprecated Use DatabaseDriver::fetchAll() instead.
-     */
-    public function fetchAll(): array
-    {
-        $ret = [];
-        if ($this->result) {
-            while ($row = $this->result->fetch_object()) {
-                $ret[] = $row;
-            }
-        }
-        return $ret;
-    }
 }

349-363: Consider error handling consistency.

The $options parameter is unused (though likely required by the DatabaseDriver interface). More importantly, this method returns false on failure without logging errors or throwing exceptions, which differs from the query() method (lines 336-339) that throws mysqli_sql_exception. Consider whether this inconsistency in error handling is intentional.

phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php (2)

59-63: Consider error handling for plugin activation/deactivation.

The calls to activatePlugin() and deactivatePlugin() are not wrapped in a try-catch block. If these methods encounter database errors or other exceptions, they will bubble up unhandled, resulting in a 500 error without a proper JSON response.

🛡️ Proposed error handling
-        if ($active) {
-            $pluginManager->activatePlugin($name);
-        } else {
-            $pluginManager->deactivatePlugin($name);
-        }
-
-        return new JsonResponse(['success' => true]);
+        try {
+            if ($active) {
+                $pluginManager->activatePlugin($name);
+            } else {
+                $pluginManager->deactivatePlugin($name);
+            }
+        } catch (\Exception $e) {
+            return new JsonResponse([
+                'success' => false,
+                'message' => 'Failed to update plugin status: ' . $e->getMessage(),
+            ], 500);
+        }
+
+        return new JsonResponse(['success' => true]);

31-105: Consider extracting common patterns to reduce duplication.

Both toggleStatus() and saveConfig() share similar patterns:

  • PluginManager initialization and loading (lines 39-40, 85-86)
  • JSON payload parsing and validation (lines 42-50, 71-79)
  • Plugin existence validation (lines 52-57, 88-93)

Extracting these into private helper methods would improve maintainability and ensure consistent error handling across endpoints.

Example extraction:

private function parseJsonRequest(Request $request): array|JsonResponse
{
    $content = $request->getContent();
    $data = json_decode($content, true);
    
    if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
        return new JsonResponse([
            'success' => false,
            'message' => 'Invalid JSON payload: ' . json_last_error_msg(),
        ], 400);
    }
    
    return $data;
}

private function validatePluginExists(string $pluginName, $pluginManager): ?JsonResponse
{
    if (!array_key_exists($pluginName, $pluginManager->getPlugins())) {
        return new JsonResponse(['success' => false, 'message' => 'Plugin not found'], 404);
    }
    return null;
}
phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (4)

154-156: Remove redundant comment.

Lines 154-156 state "Allow checking if it IS active. But we only REGISTER events and LOAD scripts if active." This comment is redundant with the immediately following code at line 157 (if ($isActive && ...)). The code is self-documenting.


235-256: Consider transaction handling for database operations.

The savePluginConfig() and updatePluginStatus() methods perform SELECT followed by conditional INSERT/UPDATE without transaction boundaries. If concurrent requests occur, this could lead to race conditions or inconsistent state (e.g., one request checks for existence while another is inserting).

Additionally, the SELECT-then-INSERT/UPDATE pattern could be optimized using INSERT ... ON DUPLICATE KEY UPDATE (MySQL) or equivalent upsert operations, reducing round trips.

🔄 Example with transaction and upsert
public function savePluginConfig(string $pluginName, array $configData): void
{
    $jsonConfig = json_encode($configData);
    $db = $this->configuration->getDb();
    $table = \phpMyFAQ\Database::getTablePrefix() . 'faqplugins';
    
    // Begin transaction
    $db->beginTransaction();
    
    try {
        // Use INSERT ... ON DUPLICATE KEY UPDATE for MySQL
        // or equivalent UPSERT for other databases
        $upsert = sprintf(
            'INSERT INTO %s (name, active, config) VALUES (?, 0, ?) 
             ON DUPLICATE KEY UPDATE config = VALUES(config)',
            $table
        );
        $stmt = $db->prepare($upsert);
        $db->execute($stmt, [$pluginName, $jsonConfig]);
        
        $db->commit();
    } catch (\Exception $e) {
        $db->rollback();
        throw $e;
    }
}

Note: Adjust the upsert syntax based on your database driver (PostgreSQL uses ON CONFLICT, SQLite uses INSERT OR REPLACE).

Also applies to: 261-282


294-294: Mixed query styles for consistency.

Line 294 uses $db->query() for a SELECT statement, while other database operations in this class use $db->prepare() with $db->execute(). While this specific query has no parameters and is safe, maintaining consistent style throughout the class improves readability.

-        $result = $db->query("SELECT name, active, config FROM $table");
+        $select = sprintf('SELECT name, active, config FROM %s', $table);
+        $stmt = $db->prepare($select);
+        $db->execute($stmt);
+        $result = $stmt;

398-470: Consider consolidating duplicate asset registration logic.

The registerPluginStylesheets() and registerPluginScripts() methods have nearly identical implementations. Consider extracting the common path validation logic into a private helper method to reduce duplication and ensure consistent security checks.

♻️ Example consolidation
private function validatePluginAssets(string $pluginName, array $assets, string $type): array
{
    $pluginDir = $this->getPluginDirectory($pluginName);
    $validated = [];
    
    $realPluginDir = str_replace('\\', '/', realpath($pluginDir) ?: $pluginDir);
    $realPluginDir = rtrim($realPluginDir, '/') . '/';
    
    foreach ($assets as $asset) {
        $fullPath = $pluginDir . '/' . $asset;
        $absolutePath = str_replace('\\', '/', realpath($fullPath) ?: $fullPath);
        
        if (str_starts_with(strtolower($absolutePath), strtolower($realPluginDir)) && file_exists($fullPath)) {
            $validated[] = 'content/plugins/' . $pluginName . '/' . $asset;
        }
    }
    
    return $validated;
}

private function registerPluginStylesheets(string $pluginName, array $stylesheets): void
{
    $validated = $this->validatePluginAssets($pluginName, $stylesheets, 'stylesheet');
    if ($validated !== []) {
        $this->pluginStylesheets[$pluginName] = $validated;
    }
}

private function registerPluginScripts(string $pluginName, array $scripts): void
{
    $validated = $this->validatePluginAssets($pluginName, $scripts, 'script');
    if ($validated !== []) {
        $this->pluginScripts[$pluginName] = $validated;
    }
}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebf90bf and d495931.

📒 Files selected for processing (7)
  • docs/plugins.md
  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php
  • phpmyfaq/src/phpMyFAQ/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Database/Pgsql.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
🧰 Additional context used
📓 Path-based instructions (1)
**/phpmyfaq/src/**/*.php

📄 CodeRabbit inference engine (AGENTS.md)

Use PER Coding Style 3.0 for PHP code

Files:

  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php
  • phpmyfaq/src/phpMyFAQ/Database/Mysqli.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php
  • phpmyfaq/src/phpMyFAQ/Database/Pgsql.php
  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
  • phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php
🧠 Learnings (1)
📚 Learning: 2025-12-26T21:59:16.059Z
Learnt from: CR
Repo: thorsten/phpMyFAQ PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-26T21:59:16.059Z
Learning: Applies to **/phpmyfaq/src/**/*.php : Use PER Coding Style 3.0 for PHP code

Applied to files:

  • phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php
  • phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php
🧬 Code graph analysis (3)
phpmyfaq/src/phpMyFAQ/Database/Mysqli.php (1)
phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php (4)
  • fetchAll (77-77)
  • prepare (148-148)
  • query (50-50)
  • execute (157-157)
phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php (3)
phpmyfaq/src/phpMyFAQ/Instance/Database/Pgsql.php (1)
  • __construct (358-361)
phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlsrv.php (1)
  • __construct (356-359)
phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php (3)
  • fetchAll (77-77)
  • prepare (148-148)
  • query (50-50)
phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php (6)
phpmyfaq/src/phpMyFAQ/Database/Mysqli.php (7)
  • __construct (35-39)
  • fetchAll (44-53)
  • fetchAll (167-183)
  • fetchArray (148-151)
  • prepare (356-363)
  • query (328-347)
  • execute (372-403)
phpmyfaq/src/phpMyFAQ/Database/Pgsql.php (7)
  • __construct (35-39)
  • fetchAll (49-58)
  • fetchAll (182-198)
  • fetchArray (255-263)
  • prepare (368-376)
  • query (139-158)
  • execute (385-395)
phpmyfaq/src/phpMyFAQ/Instance/Database/Sqlite3.php (1)
  • __construct (356-359)
phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php (5)
  • fetchAll (77-77)
  • fetchArray (67-67)
  • prepare (148-148)
  • query (50-50)
  • execute (157-157)
phpmyfaq/src/phpMyFAQ/Database/PdoMysql.php (5)
  • fetchAll (117-124)
  • fetchArray (98-101)
  • prepare (291-294)
  • query (264-283)
  • execute (302-305)
phpmyfaq/src/phpMyFAQ/Database/PdoSqlite.php (5)
  • fetchAll (118-125)
  • fetchArray (98-102)
  • prepare (316-319)
  • query (289-308)
  • execute (327-330)
🪛 GitHub Actions: phpMyFAQ Build
phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php

[error] 79-79: mago format --dry-run detected formatting differences. Exit code 1. Run 'mago format' to apply changes to this file.

🪛 PHPMD (2.15.0)
phpmyfaq/src/phpMyFAQ/Database/Mysqli.php

356-356: Avoid unused parameters such as '$options'. (undefined)

(UnusedFormalParameter)

phpmyfaq/src/phpMyFAQ/Database/Pgsql.php

368-368: Avoid unused parameters such as '$options'. (undefined)

(UnusedFormalParameter)

phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php

344-344: Avoid unused parameters such as '$options'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (20)
phpmyfaq/src/phpMyFAQ/Database/Pgsql.php (5)

30-59: LGTM! Clean statement wrapper implementation.

The PgsqlStatement class provides a clean wrapper for prepared statements with appropriate deprecation of the local fetchAll() method in favor of the driver-level method.


86-89: LGTM! Appropriate storage for prepared statements.

The private array for tracking prepared statements is well-placed and supports the new prepared statement functionality.


184-186: LGTM! Consistent unwrapping pattern.

The unwrapping logic correctly extracts the underlying result from PgsqlStatement instances, consistent with the pattern used in other database drivers.


357-357: LGTM! Plugin table added.

The addition of faqplugins to the table enumeration aligns with the PR's plugin system enhancement.


378-395: LGTM! Clean prepared statement execution.

The execute() method correctly:

  • Casts the statement to string via __toString()
  • Executes with pg_execute()
  • Assigns the result back to the statement object
  • Returns a boolean status
phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php (4)

27-56: LGTM! Robust statement wrapper.

The SqlsrvStatement class appropriately handles both resource and string statement types with defensive __toString() logic.


147-149: LGTM! Consistent unwrapping pattern.

The unwrapping logic matches the pattern used in other database drivers.


347-347: LGTM! Plugin table added.

Consistent with the plugin system enhancement across all database drivers.


378-408: LGTM! Dual-mode execution logic is sound.

The execute() method appropriately handles both execution modes:

  • Resource-based prepared statements (lines 391-396)
  • String-based parameterized queries (lines 399-405)

The type guard at lines 387-389 ensures only SqlsrvStatement instances are processed.

phpmyfaq/src/phpMyFAQ/Database/Mysqli.php (2)

169-171: LGTM! Good backward compatibility approach.

The unwrapping of MysqliStatement allows the new prepared statement API to integrate seamlessly with the existing fetchAll() interface.


285-285: LGTM!

The addition of the faqplugins table aligns with the plugin system enhancement.

phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php (5)

25-49: LGTM! Clean statement wrapper implementation.

The Sqlite3Statement class appropriately wraps SQLite3Stmt and provides a clear migration path via the deprecated fetchAll() method.


140-156: Proper handling of statement wrapper.

The updated fetchAll() correctly unwraps Sqlite3Statement instances to access the underlying result.


303-303: LGTM! Plugin table added.

The addition of the faqplugins table aligns with the PR's plugin system enhancement.


337-351: Correct interface implementation; PHPMD warning is a false positive.

The unused $options parameter is required for DatabaseDriver interface compliance. The native SQLite3::prepare() doesn't accept options, so this parameter cannot be used.


353-387: Code implementation is correct for actual usage patterns in the codebase.

The concern about associative array keys is theoretical. All callers in PluginManager.php (lines 244, 250, 254, 270, 281) pass numeric arrays constructed with square bracket syntax. The foreach ($params as $index => $param) pattern with $index + 1 binding correctly handles positional parameter binding, which is the standard approach across all database driver implementations (Sqlite3.php, Pgsql.php, Mysqli.php). No changes needed.

docs/plugins.md (1)

12-12: Documentation enhancements look good!

The documentation updates clearly explain the new plugin activation and configuration features. The example code with the new getter methods (getDescription, getAuthor, getAdvDescription, getImplementation) provides helpful guidance for plugin developers.

Also applies to: 21-21, 33-33, 84-104, 166-166

phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php (3)

55-60: Good dependency injection pattern.

The constructor now properly accepts the Configuration dependency, which enables database operations for plugin state management. The use of readonly ensures immutability.


89-91: LGTM: Guard against multiple invocations.

The early return prevents duplicate plugin loading if loadPlugins() is called multiple times, which is good defensive programming.


398-420: Appreciate the path security measures.

The path normalization and validation logic (lines 403-413 and 453-463) properly guards against path traversal attacks and handles Windows case-insensitivity. The security checks ensure assets remain within plugin directories.

Also applies to: 448-470

Comment on lines +71 to +83
$content = $request->getContent();
$data = json_decode($content, true);

if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
return new JsonResponse([
'success' => false,
'message' => 'Invalid JSON payload: ' . json_last_error_msg(),
], 400);
}

$csrfToken = $data['csrf'] ?? $request->headers->get('X-CSRF-Token');
if (!Token::getInstance($this->session)->verifyToken('admin-plugins', $csrfToken)) { return new JsonResponse(['success' => false, 'message' => 'Invalid CSRF token'], 403);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Security: CSRF validation should occur before JSON parsing.

In saveConfig(), JSON parsing happens at lines 71-79 before CSRF validation at lines 81-83. This differs from toggleStatus(), where CSRF validation occurs first (line 34) before JSON parsing (line 42).

Parsing untrusted JSON before validating CSRF allows attackers to potentially exploit JSON parsing vulnerabilities or trigger denial-of-service through malformed payloads without a valid CSRF token.

🔒 Proposed fix
     public function saveConfig(Request $request): JsonResponse
     {
+        $csrfToken = $request->headers->get('X-CSRF-Token');
+        if (!Token::getInstance($this->session)->verifyToken('admin-plugins', $csrfToken)) {
+            return new JsonResponse(['success' => false, 'message' => 'Invalid CSRF token'], 403);
+        }
+
         $content = $request->getContent();
         $data = json_decode($content, true);

         if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
             return new JsonResponse([
                 'success' => false,
                 'message' => 'Invalid JSON payload: ' . json_last_error_msg(),
             ], 400);
         }

-        $csrfToken = $data['csrf'] ?? $request->headers->get('X-CSRF-Token');
-        if (!Token::getInstance($this->session)->verifyToken('admin-plugins', $csrfToken)) {
-            return new JsonResponse(['success' => false, 'message' => 'Invalid CSRF token'], 403);
-        }
-
         $pluginManager = $this->container->get(id: 'phpmyfaq.plugin.plugin-manager');

Note: If the CSRF token must come from the JSON payload for specific reasons, validate it exists before parsing the entire payload.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$content = $request->getContent();
$data = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
return new JsonResponse([
'success' => false,
'message' => 'Invalid JSON payload: ' . json_last_error_msg(),
], 400);
}
$csrfToken = $data['csrf'] ?? $request->headers->get('X-CSRF-Token');
if (!Token::getInstance($this->session)->verifyToken('admin-plugins', $csrfToken)) { return new JsonResponse(['success' => false, 'message' => 'Invalid CSRF token'], 403);
}
$csrfToken = $request->headers->get('X-CSRF-Token');
if (!Token::getInstance($this->session)->verifyToken('admin-plugins', $csrfToken)) {
return new JsonResponse(['success' => false, 'message' => 'Invalid CSRF token'], 403);
}
$content = $request->getContent();
$data = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
return new JsonResponse([
'success' => false,
'message' => 'Invalid JSON payload: ' . json_last_error_msg(),
], 400);
}
🧰 Tools
🪛 GitHub Actions: phpMyFAQ Build

[error] 79-79: mago format --dry-run detected formatting differences. Exit code 1. Run 'mago format' to apply changes to this file.

🤖 Prompt for AI Agents
In @phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/PluginController.php
around lines 71 - 83, In saveConfig(), move CSRF validation before full JSON
parsing: first retrieve the CSRF token from the request header (or, if the token
must be supplied in the JSON body, extract/validate only the token field from
the raw request content without json_decoding the entire payload) and call
Token::getInstance($this->session)->verifyToken('admin-plugins', $csrfToken);
only after the token is verified, proceed to call $request->getContent() and
json_decode() and validate the decoded array; mirror the approach used in
toggleStatus() where CSRF is verified prior to parsing to avoid processing
untrusted JSON.

Comment on lines +378 to +391
if (!empty($params)) {
$types = '';
foreach ($params as $param) {
if (is_int($param)) {
$types .= 'i';
} elseif (is_float($param)) {
$types .= 'd';
} elseif (is_string($param)) {
$types .= 's';
} else {
$types .= 'b';
}
}
$statement->stmt->bind_param($types, ...$params);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle null and boolean parameter types explicitly.

The type inference logic treats all non-int/float/string values as blob ('b'), which is problematic:

  • null values: MySQLi doesn't have a specific null type for bind_param(). Binding null as blob will not work correctly. Null parameters typically need special handling (e.g., converted to string 'NULL' or handled via nullable type binding).
  • boolean values: Binding booleans as blob could lead to unexpected behavior. Consider converting to integer (1/0) or string.
  • Other types (arrays, objects): Would fail when bound as blob.

This could cause data integrity issues or runtime errors when these types are passed as parameters.

🔧 Proposed fix
         if (!empty($params)) {
             $types = '';
             foreach ($params as $param) {
-                if (is_int($param)) {
+                if (is_null($param)) {
+                    $types .= 's'; // bind null as empty string or handle separately
+                } elseif (is_bool($param)) {
+                    $types .= 'i'; // bind boolean as integer (0 or 1)
+                } elseif (is_int($param)) {
                     $types .= 'i';
                 } elseif (is_float($param)) {
                     $types .= 'd';
                 } elseif (is_string($param)) {
                     $types .= 's';
                 } else {
                     $types .= 'b';
                 }
             }
+            // Convert booleans to integers for binding
+            $params = array_map(fn($p) => is_bool($p) ? (int)$p : $p, $params);
             $statement->stmt->bind_param($types, ...$params);
         }

Note: Null handling may require additional logic depending on your use case. Consider validating parameter types before binding or documenting expected types.

🤖 Prompt for AI Agents
In @phpmyfaq/src/phpMyFAQ/Database/Mysqli.php around lines 378 - 391, The
type-inference must explicitly handle nulls, booleans and unsupported types
before calling $statement->stmt->bind_param: convert booleans to integers (1/0)
and use type 'i'; leave null values as PHP null and assign them a safe bind type
(e.g., 's' or keep as null-aware binding) rather than 'b'; and reject or
serialize arrays/objects (throw an exception or convert to string) instead of
falling back to 'b'. Update the loop that builds $types and the corresponding
$params normalization so nulls and booleans are normalized and unsupported types
are validated prior to invoking bind_param on $statement->stmt.

Comment on lines +351 to +376
/**
* Prepares a statement for execution and returns a statement object.
*
* @param string $query The SQL query
* @param array $options The driver options
* @return resource|string|false
*/
/**
* Prepares a statement for execution and returns a statement object.
*
* @param string $query The SQL query
* @param array $options The driver options
* @return SqlsrvStatement|false
*/
public function prepare(string $query, array $options = []): SqlsrvStatement|false
{
if (str_contains($query, '?')) {
return new SqlsrvStatement($query);
}

$stmt = sqlsrv_prepare($this->conn, $query, [], $options);
if ($stmt) {
return new SqlsrvStatement($stmt);
}
return false;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove duplicate docblock.

Lines 351-357 contain a duplicate and inconsistent docblock. The first docblock incorrectly specifies the return type as @return resource|string|false while the correct one at lines 358-364 specifies @return SqlsrvStatement|false.

🧹 Proposed fix to remove duplicate docblock
-    /**
-     * Prepares a statement for execution and returns a statement object.
-     *
-     * @param string $query   The SQL query
-     * @param array  $options The driver options
-     * @return resource|string|false
-     */
     /**
      * Prepares a statement for execution and returns a statement object.
      *
      * @param string $query   The SQL query
      * @param array  $options The driver options
      * @return SqlsrvStatement|false
      */
     public function prepare(string $query, array $options = []): SqlsrvStatement|false

Additionally, like in Pgsql.php, the $options parameter is unused. If this is for interface compatibility, consider documenting it.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Prepares a statement for execution and returns a statement object.
*
* @param string $query The SQL query
* @param array $options The driver options
* @return resource|string|false
*/
/**
* Prepares a statement for execution and returns a statement object.
*
* @param string $query The SQL query
* @param array $options The driver options
* @return SqlsrvStatement|false
*/
public function prepare(string $query, array $options = []): SqlsrvStatement|false
{
if (str_contains($query, '?')) {
return new SqlsrvStatement($query);
}
$stmt = sqlsrv_prepare($this->conn, $query, [], $options);
if ($stmt) {
return new SqlsrvStatement($stmt);
}
return false;
}
/**
* Prepares a statement for execution and returns a statement object.
*
* @param string $query The SQL query
* @param array $options The driver options
* @return SqlsrvStatement|false
*/
public function prepare(string $query, array $options = []): SqlsrvStatement|false
{
if (str_contains($query, '?')) {
return new SqlsrvStatement($query);
}
$stmt = sqlsrv_prepare($this->conn, $query, [], $options);
if ($stmt) {
return new SqlsrvStatement($stmt);
}
return false;
}
🤖 Prompt for AI Agents
In @phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php around lines 351 - 376, Remove the
duplicate/incorrect docblock above the prepare method so only the correct
docblock remains (the one that documents @return SqlsrvStatement|false); keep
the second docblock and delete the first resource|string|false block, and update
the remaining docblock to mention that the $options parameter is currently
unused for interface compatibility (or add a brief @param note) so it’s clear
why $options is present in prepare(string $query, array $options = []) while the
implementation passes options to sqlsrv_prepare.

Comment on lines +114 to +147
if (!empty($dbPlugins[$pluginName]['config']) && $plugin->getConfig()) {
$configArray = json_decode($dbPlugins[$pluginName]['config'], true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("Failed to decode config for plugin {$pluginName}: " . json_last_error_msg());
continue;
}
if (is_array($configArray)) {
$configObject = $plugin->getConfig();
foreach ($configArray as $key => $value) {
if (property_exists($configObject, $key)) {
try {
$rp = new \ReflectionProperty($configObject, $key);
if ($type = $rp->getType()) {
$typeName = $type instanceof \ReflectionNamedType ? $type->getName() : null;
switch ($typeName) {
case 'int':
$value = (int) $value;
break;
case 'bool':
$value = (bool) $value;
break;
case 'float':
$value = (float) $value;
break;
}
}
} catch (\ReflectionException $e) {
// Fallback to direct assignment if reflection fails
}
$configObject->$key = $value;
}
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Complex configuration merging logic with silent error handling.

The configuration merging logic (lines 114-147) is quite complex, using reflection for type coercion. Two concerns:

  1. Silent failure: Line 117-118 logs JSON decode errors but continues execution with an invalid config. This could lead to unexpected behavior when the plugin expects valid configuration.

  2. Complexity: The reflection-based type coercion adds cognitive load and performance overhead. Consider whether this dynamic approach is necessary, or if a more explicit deserialization pattern would be clearer.

💡 Alternative approach

Consider having plugin configuration classes implement a fromArray(array $data): static method for controlled deserialization:

if (!empty($dbPlugins[$pluginName]['config']) && $plugin->getConfig()) {
    $configArray = json_decode($dbPlugins[$pluginName]['config'], true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        error_log("Failed to decode config for plugin {$pluginName}: " . json_last_error_msg());
        continue; // Skip this plugin or use default config
    }
    
    if (is_array($configArray)) {
        $configClass = get_class($plugin->getConfig());
        if (method_exists($configClass, 'fromArray')) {
            $configObject = $configClass::fromArray($configArray);
            // Use the properly deserialized config
        }
    }
}

This pushes the responsibility of type conversion to the plugin configuration class, making it explicit and testable.

🤖 Prompt for AI Agents
In @phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php around lines 114 - 147, The
code silently proceeds with invalid JSON and uses heavy reflection for type
coercion; update the block handling json_decode so that if json_last_error() !==
JSON_ERROR_NONE you log the error with json_last_error_msg() and immediately
skip applying that config (continue to next plugin) or explicitly reset to the
plugin default config from $plugin->getConfig(); then prefer an explicit
deserialization path by checking if the config class
(get_class($plugin->getConfig())) exposes a static fromArray(array $data):
static and call that to build the config object, and only if fromArray does not
exist fall back to the current ReflectionProperty-based coercion
(ReflectionProperty, getType(), ReflectionNamedType) as a last-resort fallback.

Comment on lines +149 to +152
// I will default to inactive (false) for new plugins found on disk but not in DB.

$isActive = false;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Improve comment professionalism.

Line 149 uses the personal pronoun "I will default to inactive" which is unprofessional in production code. Consider rephrasing to: "Default to inactive (false) for new plugins found on disk but not in DB."

🤖 Prompt for AI Agents
In @phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php around lines 149 - 152,
Replace the informal comment that reads "I will default to inactive (false) for
new plugins found on disk but not in DB." with a more professional phrasing such
as "Default to inactive (false) for new plugins found on disk but not in DB."
near the $isActive assignment in the PluginManager class so the comment remains
clear and formal without changing the logic that sets $isActive = false.

Comment on lines +165 to 166
// Apply DB config overrides here if possible
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove stale comment.

The comment "Apply DB config overrides here if possible" (lines 165-166) is stale. DB config overrides are already applied earlier at lines 114-147. This could confuse future maintainers.

🤖 Prompt for AI Agents
In @phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php around lines 165 - 166,
Remove the stale inline comment "Apply DB config overrides here if possible" in
the PluginManager class; it is misleading because DB config overrides are
already applied earlier (see the earlier block where the DB config is
merged/applied), so delete that comment near the plugin initialization/DB logic
to avoid confusion for future maintainers.

Comment on lines +170 to +172
if (method_exists($plugin, 'getTranslationsPath')) {
$translationsPath = $plugin->getTranslationsPath();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the PluginInterface definition
fd -type f -name "*.php" | xargs rg "interface PluginInterface" -l

Repository: thorsten/phpMyFAQ

Length of output: 297


🏁 Script executed:

# Once we find the interface, examine it
rg "interface PluginInterface" -A 50 --type php

Repository: thorsten/phpMyFAQ

Length of output: 3890


🏁 Script executed:

# Also check the PluginManager file to see the full context
cat -n phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php | head -250 | tail -150

Repository: thorsten/phpMyFAQ

Length of output: 7579


🏁 Script executed:

cat -n phpmyfaq/src/phpMyFAQ/Plugin/PluginInterface.php

Repository: thorsten/phpMyFAQ

Length of output: 2976


Remove unnecessary method_exists() guards for interface methods.

Lines 170-172, 187-189, and 192-194 use method_exists() to guard calls to getTranslationsPath(), getStylesheets(), and getScripts(). All three methods are defined in PluginInterface and are part of the contract that any plugin must implement. These guards are unnecessary and add defensive code clutter. Remove the checks and call the methods directly since the interface guarantees their presence.

🤖 Prompt for AI Agents
In @phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php around lines 170 - 172,
Remove the unnecessary method_exists() guards around interface methods in
PluginManager: directly call $plugin->getTranslationsPath(),
$plugin->getStylesheets(), and $plugin->getScripts() (remove the checks that
wrap those calls at the locations using method_exists). These methods are
defined on PluginInterface, so eliminate the defensive conditionals and invoke
the methods directly on the $plugin instance (retaining any existing handling of
the returned values).

Comment on lines +201 to 203
// Remove plugin from the plugins array since it's incompatible (actually, maybe keep it but mark inactive?)
// Original code removed it. I'll stick to original behavior for incompatible ones.
unset($this->plugins[$plugin->getName()]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clean up TODO-style comments.

Lines 201-202 contain exploratory comments ("actually, maybe keep it but mark inactive?" and "I'll stick to original behavior") that read like internal notes during development. These should be resolved or removed before merging to production.

🤖 Prompt for AI Agents
In @phpmyfaq/src/phpMyFAQ/Plugin/PluginManager.php around lines 201 - 203,
Remove the TODO-style exploratory comments in PluginManager where incompatible
plugins are handled and make the code comment concise and production-ready:
replace the two informal lines around the unset call with a single clear comment
such as "Remove incompatible plugin from the plugins list" and keep the existing
unset($this->plugins[$plugin->getName()]); call unchanged so the behavior and
references to $this->plugins, $plugin->getName() and unset remain explicit and
documented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants