Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.59% covered (success)
92.59%
50 / 54
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Expression
92.59% covered (success)
92.59%
50 / 54
71.43% covered (warning)
71.43%
5 / 7
30.37
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
14
 and
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 or
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 andExpr
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 orExpr
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 toSql
86.96% covered (warning)
86.96%
20 / 23
0.00% covered (danger)
0.00%
0 / 1
11.27
 toGeneralizedSql
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Wikimedia\Rdbms;
4
5use InvalidArgumentException;
6use LogicException;
7use Wikimedia\Rdbms\Database\DbQuoter;
8
9/**
10 * A composite leaf representing an expression.
11 *
12 * @since 1.42
13 */
14class Expression implements IExpression {
15    private string $field;
16    private string $op;
17    /** @var ?scalar|RawSQLValue|Blob|LikeValue|non-empty-list<scalar|Blob> */
18    private $value;
19
20    /**
21     * Store an expression
22     *
23     * @param string $field
24     * @param-taint $field exec_sql
25     * @param string $op One of '>', '<', '!=', '=', '>=', '<=', IExpression::LIKE, IExpression::NOT_LIKE
26     * @phan-param '\x3E'|'\x3C'|'!='|'='|'\x3E='|'\x3C='|'LIKE'|'NOT LIKE' $op
27     * @param-taint $op exec_sql
28     * @param ?scalar|RawSQLValue|Blob|LikeValue|non-empty-list<scalar|Blob> $value
29     * @param-taint $value escapes_sql
30     * @internal Outside of rdbms, Use IReadableDatabase::expr() to create an expression object.
31     */
32    public function __construct( string $field, string $op, $value ) {
33        if ( !in_array( $op, IExpression::ACCEPTABLE_OPERATORS ) ) {
34            throw new InvalidArgumentException( "Operator $op is not supported" );
35        }
36        if (
37            ( is_array( $value ) || $value === null ) &&
38            !in_array( $op, [ '!=', '=' ] )
39        ) {
40            throw new InvalidArgumentException( "Operator $op can't take array or null as value" );
41        }
42
43        if ( is_array( $value ) ) {
44            if ( !$value ) {
45                throw new InvalidArgumentException( "The array of values can't be empty" );
46            } elseif ( !array_is_list( $value ) ) {
47                throw new InvalidArgumentException( "The array of values must be a list" );
48            } elseif ( in_array( null, $value, true ) ) {
49                throw new InvalidArgumentException( "NULL can't be in the array of values" );
50            }
51        }
52
53        if ( in_array( $op, [ IExpression::LIKE, IExpression::NOT_LIKE ] ) && !( $value instanceof LikeValue ) ) {
54            throw new InvalidArgumentException( "Value for 'LIKE' expression must be of LikeValue type" );
55        }
56        if ( !in_array( $op, [ IExpression::LIKE, IExpression::NOT_LIKE ] ) && ( $value instanceof LikeValue ) ) {
57            throw new InvalidArgumentException( "LikeValue may only be used with 'LIKE' expression" );
58        }
59
60        $field = trim( $field );
61        if ( !preg_match( '/^[A-Za-z\d\._]+$/', $field ) ) {
62            throw new InvalidArgumentException( "$field might contain SQL injection" );
63        }
64        $this->field = $field;
65        $this->op = $op;
66        $this->value = $value;
67    }
68
69    /**
70     * @param string $field
71     * @param-taint $field exec_sql
72     * @param string $op One of '>', '<', '!=', '=', '>=', '<=', IExpression::LIKE, IExpression::NOT_LIKE
73     * @phan-param '\x3E'|'\x3C'|'!='|'='|'\x3E='|'\x3C='|'LIKE'|'NOT LIKE' $op
74     * @param-taint $op exec_sql
75     * @param ?scalar|RawSQLValue|Blob|LikeValue|non-empty-list<scalar|Blob> $value
76     * @param-taint $value escapes_sql
77     */
78    #[\NoDiscard]
79    public function and( string $field, string $op, $value ): AndExpressionGroup {
80        $exprGroup = new AndExpressionGroup( $this );
81        return $exprGroup->and( $field, $op, $value );
82    }
83
84    /**
85     * @param string $field
86     * @param-taint $field exec_sql
87     * @param string $op One of '>', '<', '!=', '=', '>=', '<=', IExpression::LIKE, IExpression::NOT_LIKE
88     * @phan-param '\x3E'|'\x3C'|'!='|'='|'\x3E='|'\x3C='|'LIKE'|'NOT LIKE' $op
89     * @param-taint $op exec_sql
90     * @param ?scalar|RawSQLValue|Blob|LikeValue|non-empty-list<scalar|Blob> $value
91     * @param-taint $value escapes_sql
92     */
93    #[\NoDiscard]
94    public function or( string $field, string $op, $value ): OrExpressionGroup {
95        $exprGroup = new OrExpressionGroup( $this );
96        return $exprGroup->or( $field, $op, $value );
97    }
98
99    /**
100     * @param IExpression $expr
101     * @return AndExpressionGroup
102     */
103    #[\NoDiscard]
104    public function andExpr( IExpression $expr ): AndExpressionGroup {
105        $exprGroup = new AndExpressionGroup( $this );
106        return $exprGroup->andExpr( $expr );
107    }
108
109    /**
110     * @param IExpression $expr
111     * @return OrExpressionGroup
112     */
113    #[\NoDiscard]
114    public function orExpr( IExpression $expr ): OrExpressionGroup {
115        $exprGroup = new OrExpressionGroup( $this );
116        return $exprGroup->orExpr( $expr );
117    }
118
119    /**
120     * @internal to be used by rdbms library only
121     * @return-taint none
122     */
123    public function toSql( DbQuoter $dbQuoter ): string {
124        if ( is_array( $this->value ) ) {
125            if ( count( $this->value ) === 1 ) {
126                $value = $this->value[0];
127                if ( $this->op === '=' ) {
128                    return $this->field . ' = ' . $dbQuoter->addQuotes( $value );
129                } elseif ( $this->op === '!=' ) {
130                    return $this->field . ' != ' . $dbQuoter->addQuotes( $value );
131                } else {
132                    throw new LogicException( "Operator $this->op can't take array as value" );
133                }
134            }
135            $list = implode( ',', array_map( $dbQuoter->addQuotes( ... ), $this->value ) );
136            if ( $this->op === '=' ) {
137                return $this->field . " IN ($list)";
138            } elseif ( $this->op === '!=' ) {
139                return $this->field . " NOT IN ($list)";
140            } else {
141                throw new LogicException( "Operator $this->op can't take array as value" );
142            }
143        }
144        if ( $this->value === null ) {
145            if ( $this->op === '=' ) {
146                return $this->field . " IS NULL";
147            } elseif ( $this->op === '!=' ) {
148                return $this->field . " IS NOT NULL";
149            } else {
150                throw new LogicException( "Operator $this->op can't take null as value" );
151            }
152        }
153        if ( $this->value instanceof LikeValue ) {
154            // implies that `op` is LIKE or NOT_LIKE, checked in constructor
155            return $this->field . ' ' . $this->op . ' ' . $this->value->toSql( $dbQuoter );
156        }
157        return $this->field . ' ' . $this->op . ' ' . $dbQuoter->addQuotes( $this->value );
158    }
159
160    /**
161     * @internal to be used by rdbms library only
162     */
163    public function toGeneralizedSql(): string {
164        return $this->field . ' ' . $this->op . ' ?';
165    }
166}