Skip to content

Fix xPDOQuery::set() string quoting and introduce xPDOExpression for query layer#280

Merged
opengeek merged 1 commit intomodxcms:3.xfrom
opengeek:fix/54-xpdoexpression-query-scope
Apr 7, 2026
Merged

Fix xPDOQuery::set() string quoting and introduce xPDOExpression for query layer#280
opengeek merged 1 commit intomodxcms:3.xfrom
opengeek:fix/54-xpdoexpression-query-scope

Conversation

@opengeek
Copy link
Copy Markdown
Member

@opengeek opengeek commented Apr 6, 2026

What changed and why

Fixes #54: xPDOQuery::set() was calling isConditionalClause() on plain string
values in the SET clause. Any string containing a SQL keyword (IN, LIKE, NOT,
etc.) caused $type to remain null, leaving the value inlined unquoted in the
generated SQL. This is both a data correctness bug and a SQL injection surface for
any application that passes user-influenced data through updateCollection().

The fix removes isConditionalClause() from the set() value-type detection path.
Plain strings now always receive PDO::PARAM_STR and are quoted. The only way to
inline a verbatim SQL fragment in a SET value is to wrap it in the new
xPDOExpression value object.

This also introduces xPDO\Om\xPDOExpression — a final value object for passing
raw SQL fragments through xPDOQuery::set() and xPDOQuery::select() without
quoting or identifier escaping. Supersedes PR #273 (closed); scope is restricted to
the query layer only, per architectural review.

Files and methods changed

  • src/xPDO/Om/xPDOExpression.php — new final class xPDOExpression: wraps a raw SQL string; exposes getExpression(): string
  • src/xPDO/xPDO.phpexpression(string $expr): factory method delegating to xPDOExpression constructor
  • src/xPDO/Om/xPDOQuery.phpset(): removed isConditionalClause() from value-type path; sortby(): added instanceof xPDOExpression branch; groupby(): added instanceof xPDOExpression branch; parseConditions(): unified elseif gate with inner expression check; bare xPDOExpression to where() handled before isConditionalClause()
  • src/xPDO/Om/mysql/xPDOQuery.phpconstruct(): xPDOExpression inline in SET and SELECT
  • src/xPDO/Om/pgsql/xPDOQuery.phpconstruct(): xPDOExpression inline in SET and SELECT
  • src/xPDO/Om/sqlite/xPDOQuery.phpconstruct(): xPDOExpression inline in SET and SELECT
  • src/xPDO/Om/sqlsrv/xPDOQuery.phpconstruct(): xPDOExpression inline in SET and SELECT; parseConditions() override unified

Compatibility impact

All four supported drivers updated: mysql, pgsql, sqlite, sqlsrv. Changes are applied identically in each driver's construct() method.

Test coverage

Six test files added/extended — 458 tests total, 647 assertions, 0 failures. New coverage: xPDOExpressionTest, xPDOQuerySetTest, xPDOQuerySelectTest, xPDOQueryJoinTest, xPDOQueryWhereTest (10 tests), xPDOQuerySortByTest (2 new methods including groupby fix).

Breaking change assessment

The string-quoting fix is a behavior change for any caller passing raw SQL fragments as plain strings to xPDOQuery::set(). That usage was undocumented and exploitable — not a supported API. Callers needing raw SQL in a SET value must now use $xpdo->expression('...'). No public API signature changes. Safe for patch-level consumers using the documented API.

AI Disclosure

This fix was developed using an AI-assisted workflow. AI agents (Claude) wrote tests, code, and performed initial code review under the direction of the project maintainer (@opengeek), who reviewed the final diff, validated the approach, and takes responsibility for the submission.

Contributors

Issue #54 originally reported the xPDOQuery::set() string quoting problem. PR #273 (@opengeek) established the xPDOExpression concept; this PR corrects the scope to the query layer only.

…ry layer

Fixes modxcms#54: xPDOQuery::set() was calling isConditionalClause() on plain string
values, which left any string containing SQL keywords (IN, LIKE, NOT, etc.)
with $type = null, causing them to be inlined unquoted in the SET clause — a
SQL injection surface and a data correctness bug.

- Plain string values in set() now always receive PDO::PARAM_STR and are
  quoted; isConditionalClause() is removed from the set() value-type path.
- New final class xPDO\Om\xPDOExpression wraps a raw SQL fragment and is
  recognised by set() and select() in all four drivers (mysql, pgsql, sqlite,
  sqlsrv), which inline it verbatim without quoting.
- New xPDO::expression(string $expr): xPDOExpression factory method.
- Ten new PHPUnit tests cover the factory, the value object, the quoting fix,
  and xPDOExpression passthrough in both SET and SELECT contexts.

Supersedes PR modxcms#273 (closed); restricted to query layer only per Rollins
architectural review.
@opengeek opengeek marked this pull request as ready for review April 6, 2026 19:31
@opengeek opengeek merged commit f7cf7b8 into modxcms:3.x Apr 7, 2026
15 checks passed
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.

updateCollection fails when 'set' string contains operator

1 participant