Merge pull request #22928 from artfulrobot/artfulrobot-title-double-html-encoding
[civicrm-core.git] / Civi / Api4 / Query / SqlFunction.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 namespace Civi\Api4\Query;
13
14 /**
15 * Base class for all Sql functions.
16 *
17 * @package Civi\Api4\Query
18 */
19 abstract class SqlFunction extends SqlExpression {
20
21 /**
22 * @var array[]
23 */
24 protected $args = [];
25
26 /**
27 * Used for categorizing functions in the UI
28 *
29 * @var string
30 */
31 protected static $category;
32
33 const CATEGORY_AGGREGATE = 'aggregate',
34 CATEGORY_COMPARISON = 'comparison',
35 CATEGORY_DATE = 'date',
36 CATEGORY_MATH = 'math',
37 CATEGORY_STRING = 'string';
38
39 /**
40 * Parse the argument string into an array of function arguments
41 */
42 protected function initialize() {
43 $arg = trim(substr($this->expr, strpos($this->expr, '(') + 1, -1));
44 foreach ($this->getParams() as $idx => $param) {
45 $prefix = NULL;
46 $name = $param['name'] ?: ($idx + 1);
47 // If this isn't the first param it needs to start with something;
48 // either the name (e.g. "ORDER BY") if it has one, or a comma separating it from the previous param.
49 $start = $param['name'] ?: ($idx ? ',' : NULL);
50 if ($start) {
51 $prefix = $this->captureKeyword([$start], $arg);
52 // Supply api_default
53 if (!$prefix && isset($param['api_default'])) {
54 $this->args[$idx] = [
55 'prefix' => [$start],
56 'expr' => array_map([parent::class, 'convert'], $param['api_default']['expr']),
57 'suffix' => [],
58 ];
59 continue;
60 }
61 if (!$prefix && !$param['optional']) {
62 throw new \API_Exception("Missing param $name for SQL function " . static::getName());
63 }
64 }
65 elseif ($param['flag_before']) {
66 $prefix = $this->captureKeyword(array_keys($param['flag_before']), $arg);
67 }
68 $this->args[$idx] = [
69 'prefix' => (array) $prefix,
70 'expr' => [],
71 'suffix' => [],
72 ];
73 if ($param['max_expr'] && (!$param['name'] || $param['name'] === $prefix)) {
74 $exprs = $this->captureExpressions($arg, $param['must_be'], $param['max_expr']);
75 if (
76 count($exprs) < $param['min_expr'] &&
77 !(!$exprs && $param['optional'])
78 ) {
79 throw new \API_Exception("Too few arguments to param $name for SQL function " . static::getName());
80 }
81 $this->args[$idx]['expr'] = $exprs;
82
83 $this->args[$idx]['suffix'] = (array) $this->captureKeyword(array_keys($param['flag_after']), $arg);
84 }
85 }
86 if (trim($arg)) {
87 throw new \API_Exception("Too many arguments given for SQL function " . static::getName());
88 }
89 }
90
91 /**
92 * Change $dataType according to output of function
93 *
94 * @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues
95 * @param string $value
96 * @param string $dataType
97 * @return string
98 */
99 public function formatOutputValue($value, &$dataType) {
100 if (static::$dataType) {
101 $dataType = static::$dataType;
102 }
103 return $value;
104 }
105
106 /**
107 * Render the expression for insertion into the sql query
108 *
109 * @param array $fieldList
110 * @return string
111 */
112 public function render(array $fieldList): string {
113 $output = '';
114 foreach ($this->args as $arg) {
115 $rendered = $this->renderArg($arg, $fieldList);
116 if (strlen($rendered)) {
117 $output .= (strlen($output) ? ' ' : '') . $rendered;
118 }
119 }
120 return $this->getName() . '(' . $output . ')';
121 }
122
123 /**
124 * @param array $arg
125 * @param array $fieldList
126 * @return string
127 */
128 private function renderArg($arg, $fieldList): string {
129 $rendered = implode(' ', $arg['prefix']);
130 foreach ($arg['expr'] ?? [] as $idx => $expr) {
131 if (strlen($rendered) || $idx) {
132 $rendered .= $idx ? ', ' : ' ';
133 }
134 $rendered .= $expr->render($fieldList);
135 }
136 if ($arg['suffix']) {
137 $rendered .= (strlen($rendered) ? ' ' : '') . implode(' ', $arg['suffix']);
138 }
139 return $rendered;
140 }
141
142 /**
143 * @inheritDoc
144 */
145 public function getAlias(): string {
146 return $this->alias ?? $this->getName() . ':' . implode('_', $this->fields);
147 }
148
149 /**
150 * Get the name of this sql function.
151 * @return string
152 */
153 public static function getName(): string {
154 $className = static::class;
155 return substr($className, strrpos($className, 'SqlFunction') + 11);
156 }
157
158 /**
159 * Get the param metadata for this sql function.
160 * @return array
161 */
162 final public static function getParams(): array {
163 $params = [];
164 foreach (static::params() as $param) {
165 // Merge in defaults to ensure each param has these properties
166 $params[] = $param + [
167 'name' => NULL,
168 'label' => ts('Select'),
169 'min_expr' => 1,
170 'max_expr' => 1,
171 'flag_before' => [],
172 'flag_after' => [],
173 'optional' => FALSE,
174 'must_be' => ['SqlField', 'SqlFunction', 'SqlString', 'SqlNumber', 'SqlNull'],
175 'api_default' => NULL,
176 ];
177 }
178 return $params;
179 }
180
181 abstract protected static function params(): array;
182
183 /**
184 * Get the arguments passed to this sql function instance.
185 * @return array{prefix: array, suffix: array, expr: SqlExpression}[]
186 */
187 public function getArgs(): array {
188 return $this->args;
189 }
190
191 /**
192 * @return string
193 */
194 public static function getCategory(): string {
195 return static::$category;
196 }
197
198 /**
199 * All functions return 'SqlFunction' as their type.
200 *
201 * To get the function name @see SqlFunction::getName()
202 * @return string
203 */
204 public function getType(): string {
205 return 'SqlFunction';
206 }
207
208 /**
209 * @return string
210 */
211 abstract public static function getDescription(): string;
212
213 }