fix: guard api_plugin_moveup/movedown against NULL prior/next id (1.2.x backport)#7158
Merged
TheWitness merged 3 commits intoMay 16, 2026
Conversation
Backport from develop. When called at the boundary (first plugin moved up, last moved down), MAX/MIN on an empty set returns NULL. With strict SQL modes stripped on connect, UPDATE SET id = NULL on a NOT NULL column silently stores 0, corrupting the primary key and causing 1062 errors in any subsequent code that copies plugin_config into a temp table. Also preserves id=0 rows in plugins_load_temp_table by setting NO_AUTO_VALUE_ON_ZERO for the single bulk copy INSERT, then restoring the original session sql_mode immediately after. Includes regression tests (tests/Unit/PluginMoveorderBugTest.php). Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Backports a fix to prevent plugin_config.id corruption when moving plugins up/down at ordering boundaries, and adds a defensive sql_mode tweak to preserve literal id=0 rows during temp-table population in the plugin management UI.
Changes:
- Add guards in
api_plugin_moveup()/api_plugin_movedown()to avoid swapping when there is no prior/next row (boundary no-op). - Temporarily enable
NO_AUTO_VALUE_ON_ZEROduring theplugin_config -> tempbulk copy, then restore the original sessionsql_mode. - Add Pest unit tests that assert the presence/order of the new guards and the
sql_modesave/restore around the bulk copy.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
lib/plugins.php |
Adds boundary guards around the 3-step id swap in move-up/move-down to avoid NULL→0 corruption. |
plugins.php |
Adjusts session sql_mode during temp-table copy to preserve literal id=0 rows, then restores it. |
tests/Unit/PluginMoveorderBugTest.php |
Adds regression tests (source-scan invariants) asserting the guards and sql_mode save/restore behavior. |
Comments suppressed due to low confidence (2)
lib/plugins.php:1021
- Same issue here: empty($id)/empty($next_id) will evaluate to true for id=0 or next_id=0, preventing reordering when an id=0 row exists (which this PR otherwise supports by preserving id=0 during temp-table copies). Use explicit comparisons against NULL/false instead of empty().
$id = db_fetch_cell_prepared('SELECT id FROM plugin_config WHERE directory = ?', array($plugin));
if (!empty($id)) {
$next_id = db_fetch_cell_prepared('SELECT MIN(id) FROM plugin_config WHERE id > ?', array($id));
/* MIN() on an empty set returns NULL; same NULL->0 corruption risk as moveup. */
if (!empty($next_id)) {
$temp_id = db_fetch_cell('SELECT MAX(id) FROM plugin_config') + 1;
db_execute_prepared('UPDATE plugin_config SET id = ? WHERE id = ?', array($temp_id, $next_id));
tests/Unit/PluginMoveorderBugTest.php:113
- These movedown tests also lock in empty()-based guards ("if (!empty($id))" / "if (!empty($next_id))"), which will treat id=0 as empty and prevent reordering around a legitimate id=0 row. If the intent is only to guard against NULL (empty set) and false (no row), the assertions should be updated accordingly.
test('api_plugin_movedown has outer !empty($id) guard', function () use ($libPluginsPath) {
$source = file_get_contents($libPluginsPath);
$fn_pos = strpos($source, 'function api_plugin_movedown(');
$id_guard = strpos($source, 'if (!empty($id))', $fn_pos);
expect($fn_pos)->not->toBeFalse();
expect($id_guard)->not->toBeFalse('!empty($id) guard missing from api_plugin_movedown');
});
test('api_plugin_movedown has !empty($next_id) guard around the three-step swap', function () use ($libPluginsPath) {
$source = file_get_contents($libPluginsPath);
$fn_pos = strpos($source, 'function api_plugin_movedown(');
$guard_pos = strpos($source, 'if (!empty($next_id))', $fn_pos);
expect($fn_pos)->not->toBeFalse();
expect($guard_pos)->not->toBeFalse('!empty($next_id) guard missing from api_plugin_movedown');
});
TheWitness
previously approved these changes
May 15, 2026
Copilot review (Cacti#7158) correctly noted that empty(0) returns true, so the prior guards would incorrectly skip a row with id=0 (valid when NO_AUTO_VALUE_ON_ZERO is active). Use $id !== false for db_fetch_cell_prepared() results (no-row case) and $prior_id/$next_id !== null for MAX/MIN on an empty set. Also replace CONCAT_WS sql_mode construction with PHP-side string building to avoid empty-token edge cases on some servers. Update test assertions to match the new guard expressions. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
TheWitness
approved these changes
May 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backport of #7157 to 1.2.x. The same two bugs exist identically in both branches.
Root cause
api_plugin_moveup()/api_plugin_movedown()rotate plugin ids via a three-step swap. At the boundary (first plugin moved up, last moved down),MAX(id) WHERE id < ?/MIN(id) WHERE id > ?returnsNULL. With strict SQL modes stripped on connect,UPDATE plugin_config SET id = NULLon aNOT NULLcolumn silently stores0, corrupting the primary key. Subsequent calls toplugins_load_temp_table()then fail withERROR 1062because the id=0 row gets AUTO_INCREMENT-reassigned to the next sequence value, colliding with the row already at that id.Fixes
api_plugin_moveup: add!empty($prior_id)guard — moving the first plugin is a no-op.api_plugin_movedown: add outer!empty($id)guard (was missing) and!empty($next_id)guard — moving the last plugin is a no-op.plugins_load_temp_table: setNO_AUTO_VALUE_ON_ZEROfor the single bulk copy INSERT, then restore the original session sql_mode, so any existing id=0 row is preserved literally.Includes
tests/Unit/PluginMoveorderBugTest.phpwith 12 regression tests (all pass).