1<?php
2/**
3 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5 *
6 * Licensed under The MIT License
7 * For full copyright and license information, please see the LICENSE.txt
8 * Redistributions of files must retain the above copyright notice.
9 *
10 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11 * @link          https://cakephp.org CakePHP(tm) Project
12 * @since         3.0.0
13 * @license       https://opensource.org/licenses/mit-license.php MIT License
14 */
15namespace Cake\Database;
16
17use Cake\Database\Expression\FieldInterface;
18use Cake\Database\Expression\IdentifierExpression;
19use Cake\Database\Expression\OrderByExpression;
20
21/**
22 * Contains all the logic related to quoting identifiers in a Query object
23 *
24 * @internal
25 */
26class IdentifierQuoter
27{
28    /**
29     * The driver instance used to do the identifier quoting
30     *
31     * @var \Cake\Database\Driver
32     */
33    protected $_driver;
34
35    /**
36     * Constructor
37     *
38     * @param \Cake\Database\Driver $driver The driver instance used to do the identifier quoting
39     */
40    public function __construct(Driver $driver)
41    {
42        $this->_driver = $driver;
43    }
44
45    /**
46     * Iterates over each of the clauses in a query looking for identifiers and
47     * quotes them
48     *
49     * @param \Cake\Database\Query $query The query to have its identifiers quoted
50     * @return \Cake\Database\Query
51     */
52    public function quote(Query $query)
53    {
54        $binder = $query->getValueBinder();
55        $query->setValueBinder(false);
56
57        if ($query->type() === 'insert') {
58            $this->_quoteInsert($query);
59        } elseif ($query->type() === 'update') {
60            $this->_quoteUpdate($query);
61        } else {
62            $this->_quoteParts($query);
63        }
64
65        $query->traverseExpressions([$this, 'quoteExpression']);
66        $query->setValueBinder($binder);
67
68        return $query;
69    }
70
71    /**
72     * Quotes identifiers inside expression objects
73     *
74     * @param \Cake\Database\ExpressionInterface $expression The expression object to walk and quote.
75     * @return void
76     */
77    public function quoteExpression($expression)
78    {
79        if ($expression instanceof FieldInterface) {
80            $this->_quoteComparison($expression);
81
82            return;
83        }
84
85        if ($expression instanceof OrderByExpression) {
86            $this->_quoteOrderBy($expression);
87
88            return;
89        }
90
91        if ($expression instanceof IdentifierExpression) {
92            $this->_quoteIdentifierExpression($expression);
93
94            return;
95        }
96    }
97
98    /**
99     * Quotes all identifiers in each of the clauses of a query
100     *
101     * @param \Cake\Database\Query $query The query to quote.
102     * @return void
103     */
104    protected function _quoteParts($query)
105    {
106        foreach (['distinct', 'select', 'from', 'group'] as $part) {
107            $contents = $query->clause($part);
108
109            if (!is_array($contents)) {
110                continue;
111            }
112
113            $result = $this->_basicQuoter($contents);
114            if (!empty($result)) {
115                $query->{$part}($result, true);
116            }
117        }
118
119        $joins = $query->clause('join');
120        if ($joins) {
121            $joins = $this->_quoteJoins($joins);
122            $query->join($joins, [], true);
123        }
124    }
125
126    /**
127     * A generic identifier quoting function used for various parts of the query
128     *
129     * @param array $part the part of the query to quote
130     * @return array
131     */
132    protected function _basicQuoter($part)
133    {
134        $result = [];
135        foreach ((array)$part as $alias => $value) {
136            $value = !is_string($value) ? $value : $this->_driver->quoteIdentifier($value);
137            $alias = is_numeric($alias) ? $alias : $this->_driver->quoteIdentifier($alias);
138            $result[$alias] = $value;
139        }
140
141        return $result;
142    }
143
144    /**
145     * Quotes both the table and alias for an array of joins as stored in a Query
146     * object
147     *
148     * @param array $joins The joins to quote.
149     * @return array
150     */
151    protected function _quoteJoins($joins)
152    {
153        $result = [];
154        foreach ($joins as $value) {
155            $alias = null;
156            if (!empty($value['alias'])) {
157                $alias = $this->_driver->quoteIdentifier($value['alias']);
158                $value['alias'] = $alias;
159            }
160
161            if (is_string($value['table'])) {
162                $value['table'] = $this->_driver->quoteIdentifier($value['table']);
163            }
164
165            $result[$alias] = $value;
166        }
167
168        return $result;
169    }
170
171    /**
172     * Quotes the table name and columns for an insert query
173     *
174     * @param \Cake\Database\Query $query The insert query to quote.
175     * @return void
176     */
177    protected function _quoteInsert($query)
178    {
179        list($table, $columns) = $query->clause('insert');
180        $table = $this->_driver->quoteIdentifier($table);
181        foreach ($columns as &$column) {
182            if (is_scalar($column)) {
183                $column = $this->_driver->quoteIdentifier($column);
184            }
185        }
186        $query->insert($columns)->into($table);
187    }
188
189    /**
190     * Quotes the table name for an update query
191     *
192     * @param \Cake\Database\Query $query The update query to quote.
193     * @return void
194     */
195    protected function _quoteUpdate($query)
196    {
197        $table = $query->clause('update')[0];
198
199        if (is_string($table)) {
200            $query->update($this->_driver->quoteIdentifier($table));
201        }
202    }
203
204    /**
205     * Quotes identifiers in expression objects implementing the field interface
206     *
207     * @param \Cake\Database\Expression\FieldInterface $expression The expression to quote.
208     * @return void
209     */
210    protected function _quoteComparison(FieldInterface $expression)
211    {
212        $field = $expression->getField();
213        if (is_string($field)) {
214            $expression->setField($this->_driver->quoteIdentifier($field));
215        } elseif (is_array($field)) {
216            $quoted = [];
217            foreach ($field as $f) {
218                $quoted[] = $this->_driver->quoteIdentifier($f);
219            }
220            $expression->setField($quoted);
221        } elseif ($field instanceof ExpressionInterface) {
222            $this->quoteExpression($field);
223        }
224    }
225
226    /**
227     * Quotes identifiers in "order by" expression objects
228     *
229     * Strings with spaces are treated as literal expressions
230     * and will not have identifiers quoted.
231     *
232     * @param \Cake\Database\Expression\OrderByExpression $expression The expression to quote.
233     * @return void
234     */
235    protected function _quoteOrderBy(OrderByExpression $expression)
236    {
237        $expression->iterateParts(function ($part, &$field) {
238            if (is_string($field)) {
239                $field = $this->_driver->quoteIdentifier($field);
240
241                return $part;
242            }
243            if (is_string($part) && strpos($part, ' ') === false) {
244                return $this->_driver->quoteIdentifier($part);
245            }
246
247            return $part;
248        });
249    }
250
251    /**
252     * Quotes identifiers in "order by" expression objects
253     *
254     * @param \Cake\Database\Expression\IdentifierExpression $expression The identifiers to quote.
255     * @return void
256     */
257    protected function _quoteIdentifierExpression(IdentifierExpression $expression)
258    {
259        $expression->setIdentifier(
260            $this->_driver->quoteIdentifier($expression->getIdentifier())
261        );
262    }
263}
264