Merge pull request #24022 from colemanw/afformFrontend
[civicrm-core.git] / CRM / Utils / Weight.php
CommitLineData
6a488035
TO
1<?php
2/*
bc77d7c0
TO
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 +--------------------------------------------------------------------+
e70a7fc0 10 */
5bc392e6
EM
11
12/**
13 * Class CRM_Utils_Weight
14 */
6a488035
TO
15class CRM_Utils_Weight {
16 /**
ee3db087 17 * List of GET fields which must be validated
6a488035
TO
18 *
19 * To reduce the size of this patch, we only sign the exploitable fields
20 * which make up "$baseURL" in addOrder() (eg 'filter' or 'dao').
21 * Less-exploitable fields (eg 'dir') are left unsigned.
6714d8d2 22 * 'id','src','dst','dir'
ee3db087 23 * @var array
6a488035 24 */
6714d8d2 25 public static $SIGNABLE_FIELDS = ['reset', 'dao', 'idName', 'url', 'filter'];
6a488035
TO
26
27 /**
100fef9d 28 * Correct duplicate weight entries by putting them (duplicate weights) in sequence.
6a488035 29 *
77855840
TO
30 * @param string $daoName
31 * Full name of the DAO.
32 * @param array $fieldValues
33 * Field => value to be used in the WHERE.
34 * @param string $weightField
50bfb460 35 * Field which contains the weight value.
16b10e64 36 * defaults to 'weight'
6a488035
TO
37 *
38 * @return bool
39 */
00be9182 40 public static function correctDuplicateWeights($daoName, $fieldValues = NULL, $weightField = 'weight') {
6a488035 41 $selectField = "MIN(id) AS dupeId, count(id) as dupeCount, $weightField as dupeWeight";
e5cceea5 42 $groupBy = "$weightField having count(id)>1";
6a488035
TO
43
44 $minDupeID = CRM_Utils_Weight::query('SELECT', $daoName, $fieldValues, $selectField, NULL, NULL, $groupBy);
45
46 // return early if query returned empty
47 // CRM-8043
48 if (!$minDupeID->fetch()) {
49 return TRUE;
50 }
51
52 if ($minDupeID->dupeId) {
53 $additionalWhere = "id !=" . $minDupeID->dupeId . " AND $weightField >= " . $minDupeID->dupeWeight;
353ffa53
TO
54 $update = "$weightField = $weightField + 1";
55 $status = CRM_Utils_Weight::query('UPDATE', $daoName, $fieldValues, $update, $additionalWhere);
6a488035
TO
56 }
57
58 if ($minDupeID->dupeId && $status) {
50bfb460 59 // recursive call to correct all duplicate weight entries.
6a488035
TO
60 return CRM_Utils_Weight::correctDuplicateWeights($daoName, $fieldValues, $weightField);
61 }
62 elseif (!$minDupeID->dupeId) {
63 // case when no duplicate records are found.
64 return TRUE;
65 }
66 elseif (!$status) {
67 // case when duplicate records are found but update status is false.
68 return FALSE;
69 }
70 }
71
72 /**
73 * Remove a row from the specified weight, and shift all rows below it up
74 *
77855840
TO
75 * @param string $daoName
76 * Full name of the DAO.
6a488035 77 * $param integer $weight the weight to be removed
100fef9d 78 * @param int $fieldID
77855840
TO
79 * @param array $fieldValues
80 * Field => value to be used in the WHERE.
81 * @param string $weightField
50bfb460 82 * Field which contains the weight value.
16b10e64 83 * defaults to 'weight'
6a488035
TO
84 *
85 * @return bool
86 */
00be9182 87 public static function delWeight($daoName, $fieldID, $fieldValues = NULL, $weightField = 'weight') {
fffdfeba 88 $object = new $daoName();
6a488035
TO
89 $object->id = $fieldID;
90 if (!$object->find(TRUE)) {
91 return FALSE;
92 }
93
e7292422 94 $weight = (int) $object->weight;
6a488035
TO
95 if ($weight < 1) {
96 return FALSE;
97 }
98
99 // fill the gap
100 $additionalWhere = "$weightField > $weight";
353ffa53
TO
101 $update = "$weightField = $weightField - 1";
102 $status = CRM_Utils_Weight::query('UPDATE', $daoName, $fieldValues, $update, $additionalWhere);
6a488035
TO
103
104 return $status;
105 }
106
107 /**
44cc86bd 108 * Updates the weight fields of other rows according to the new and old weight passed in.
6a488035
TO
109 * And returns the new weight be used. If old-weight not present, Creates a gap for a new row to be inserted
110 * at the specified new weight
111 *
77855840
TO
112 * @param string $daoName
113 * Full name of the DAO.
114 * @param int $oldWeight
115 * @param int $newWeight
116 * @param array $fieldValues
117 * Field => value to be used in the WHERE.
118 * @param string $weightField
119 * Field which contains the weight value,.
16b10e64 120 * defaults to 'weight'
6a488035
TO
121 *
122 * @return int
123 */
00be9182 124 public static function updateOtherWeights($daoName, $oldWeight, $newWeight, $fieldValues = NULL, $weightField = 'weight') {
076fe09a
CW
125 $oldWeight = (int) $oldWeight;
126 $newWeight = (int) $newWeight;
6a488035
TO
127
128 // max weight is the highest current weight
076fe09a 129 $maxWeight = self::getMax($daoName, $fieldValues, $weightField) ?: 1;
6a488035
TO
130
131 if ($newWeight > $maxWeight) {
50bfb460 132 // calculate new weight, CRM-4133
6a488035
TO
133 $calNewWeight = CRM_Utils_Weight::getNewWeight($daoName, $fieldValues, $weightField);
134
50bfb460 135 // no need to update weight for other fields.
6a488035
TO
136 if ($calNewWeight > $maxWeight) {
137 return $calNewWeight;
138 }
139 $newWeight = $maxWeight;
140
141 if (!$oldWeight) {
142 return $newWeight + 1;
143 }
144 }
145 elseif ($newWeight < 1) {
146 $newWeight = 1;
147 }
148
149 // if they're the same, nothing to do
150 if ($oldWeight == $newWeight) {
151 return $newWeight;
152 }
153
076fe09a
CW
154 // Check for an existing record with this weight
155 $existing = self::query('SELECT', $daoName, $fieldValues, "id", "$weightField = $newWeight");
156 // Nothing to do if no existing record has this weight
157 if (empty($existing->N)) {
158 return $newWeight;
159 }
160
6a488035
TO
161 // if oldWeight not present, indicates new weight is to be added. So create a gap for a new row to be inserted.
162 if (!$oldWeight) {
163 $additionalWhere = "$weightField >= $newWeight";
164 $update = "$weightField = ($weightField + 1)";
165 CRM_Utils_Weight::query('UPDATE', $daoName, $fieldValues, $update, $additionalWhere);
6a488035
TO
166 }
167 else {
168 if ($newWeight > $oldWeight) {
169 $additionalWhere = "$weightField > $oldWeight AND $weightField <= $newWeight";
170 $update = "$weightField = ($weightField - 1)";
171 }
172 elseif ($newWeight < $oldWeight) {
173 $additionalWhere = "$weightField >= $newWeight AND $weightField < $oldWeight";
174 $update = "$weightField = ($weightField + 1)";
175 }
176 CRM_Utils_Weight::query('UPDATE', $daoName, $fieldValues, $update, $additionalWhere);
6a488035 177 }
076fe09a 178 return $newWeight;
6a488035
TO
179 }
180
181 /**
100fef9d 182 * Returns the new calculated weight.
6a488035 183 *
77855840
TO
184 * @param string $daoName
185 * Full name of the DAO.
186 * @param array $fieldValues
187 * Field => value to be used in the WHERE.
188 * @param string $weightField
189 * Field which used to get the wt, default to 'weight'.
6a488035 190 *
df8d3074 191 * @return int
6a488035 192 */
00be9182 193 public static function getNewWeight($daoName, $fieldValues = NULL, $weightField = 'weight') {
353ffa53
TO
194 $selectField = "id AS fieldID, $weightField AS weight";
195 $field = CRM_Utils_Weight::query('SELECT', $daoName, $fieldValues, $selectField);
6a488035 196 $sameWeightCount = 0;
be2fb01f 197 $weights = [];
6a488035
TO
198 while ($field->fetch()) {
199 if (in_array($field->weight, $weights)) {
200 $sameWeightCount++;
201 }
202 $weights[$field->fieldID] = $field->weight;
203 }
204
205 $newWeight = 1;
206 if ($sameWeightCount) {
207 $newWeight = max($weights) + 1;
208
50bfb460 209 // check for max wt, should not greater than cal max wt.
6a488035
TO
210 $calMaxWt = min($weights) + count($weights) - 1;
211 if ($newWeight > $calMaxWt) {
212 $newWeight = $calMaxWt;
213 }
214 }
215 elseif (!empty($weights)) {
216 $newWeight = max($weights);
217 }
218
219 return $newWeight;
220 }
221
222 /**
100fef9d 223 * Returns the highest weight.
6a488035 224 *
77855840
TO
225 * @param string $daoName
226 * Full name of the DAO.
227 * @param array $fieldValues
228 * Field => value to be used in the WHERE.
229 * @param string $weightField
50bfb460 230 * Field which contains the weight value.
16b10e64 231 * defaults to 'weight'
6a488035 232 *
df8d3074 233 * @return int
6a488035 234 */
00be9182 235 public static function getMax($daoName, $fieldValues = NULL, $weightField = 'weight') {
22082f91
AB
236 if (empty($weightField)) {
237 Civi::log()->warning('Missing weight field name for ' . $daoName);
238 return 0;
239 }
240
6a488035
TO
241 $selectField = "MAX(ROUND($weightField)) AS max_weight";
242 $weightDAO = CRM_Utils_Weight::query('SELECT', $daoName, $fieldValues, $selectField);
243 $weightDAO->fetch();
244 if ($weightDAO->max_weight) {
245 return $weightDAO->max_weight;
246 }
247 return 0;
248 }
249
250 /**
100fef9d 251 * Returns the default weight ( highest weight + 1 ) to be used.
6a488035 252 *
77855840
TO
253 * @param string $daoName
254 * Full name of the DAO.
255 * @param array $fieldValues
256 * Field => value to be used in the WHERE.
257 * @param string $weightField
50bfb460 258 * Field which contains the weight value.
16b10e64 259 * defaults to 'weight'
6a488035 260 *
df8d3074 261 * @return int
6a488035 262 */
00be9182 263 public static function getDefaultWeight($daoName, $fieldValues = NULL, $weightField = 'weight') {
6a488035
TO
264 $maxWeight = CRM_Utils_Weight::getMax($daoName, $fieldValues, $weightField);
265 return $maxWeight + 1;
266 }
267
268 /**
269 * Execute a weight-related query
270 *
77855840
TO
271 * @param string $queryType
272 * SELECT, UPDATE, DELETE.
076fe09a 273 * @param CRM_Core_DAO|string $daoName
77855840
TO
274 * Full name of the DAO.
275 * @param array $fieldValues
276 * Field => value to be used in the WHERE.
277 * @param string $queryData
278 * Data to be used, dependent on the query type.
3fd42bb5
BT
279 * @param string|null $additionalWhere
280 * Optional WHERE field.
281 * @param string|null $orderBy
77855840 282 * Optional ORDER BY field.
3fd42bb5
BT
283 * @param string|null $groupBy
284 * Optional GROU{} BY field.
f4aaa82a 285 *
16b10e64
CW
286 * @return CRM_Core_DAO
287 * objet that holds the results of the query
6a488035 288 */
79d7553f 289 public static function &query(
a3e55d9c 290 $queryType,
353ffa53 291 $daoName,
e9cde327 292 $fieldValues,
353ffa53
TO
293 $queryData,
294 $additionalWhere = NULL,
295 $orderBy = NULL,
296 $groupBy = NULL
297 ) {
076fe09a
CW
298 $table = $daoName::getTablename();
299 $fields = $daoName::getSupportedFields();
6a488035
TO
300 $fieldlist = array_keys($fields);
301
be2fb01f 302 $whereConditions = [];
6a488035
TO
303 if ($additionalWhere) {
304 $whereConditions[] = $additionalWhere;
305 }
be2fb01f 306 $params = [];
6a488035
TO
307 $fieldNum = 0;
308 if (is_array($fieldValues)) {
309 foreach ($fieldValues as $fieldName => $value) {
310 if (!in_array($fieldName, $fieldlist)) {
311 // invalid field specified. abort.
076fe09a 312 throw new CRM_Core_Exception("Invalid field '$fieldName' for $daoName");
6a488035 313 }
9df894bb
CW
314 if (CRM_Utils_System::isNull($value)) {
315 $whereConditions[] = "$fieldName IS NULL";
316 }
317 else {
318 $fieldNum++;
319 $whereConditions[] = "$fieldName = %$fieldNum";
320 $fieldType = $fields[$fieldName]['type'];
321 $params[$fieldNum] = [$value, CRM_Utils_Type::typeToString($fieldType)];
322 }
6a488035
TO
323 }
324 }
325 $where = implode(' AND ', $whereConditions);
326
327 switch ($queryType) {
328 case 'SELECT':
329 $query = "SELECT $queryData FROM $table";
330 if ($where) {
331 $query .= " WHERE $where";
332 }
333 if ($groupBy) {
334 $query .= " GROUP BY $groupBy";
335 }
336 if ($orderBy) {
337 $query .= " ORDER BY $orderBy";
338 }
339 break;
340
341 case 'UPDATE':
342 $query = "UPDATE $table SET $queryData";
343 if ($where) {
344 $query .= " WHERE $where";
345 }
346 break;
347
348 case 'DELETE':
349 $query = "DELETE FROM $table WHERE $where AND $queryData";
350 break;
351
352 default:
076fe09a 353 throw new CRM_Core_Exception("Invalid query operation for $daoName");
6a488035
TO
354 }
355
356 $resultDAO = CRM_Core_DAO::executeQuery($query, $params);
357 return $resultDAO;
358 }
359
5bc392e6 360 /**
3fd42bb5 361 * @param array $rows
c490a46a 362 * @param string $daoName
100fef9d 363 * @param string $idName
3fd42bb5
BT
364 * @param string $returnURL
365 * @param string|null $filter
5bc392e6 366 */
00be9182 367 public static function addOrder(&$rows, $daoName, $idName, $returnURL, $filter = NULL) {
6a488035
TO
368 if (empty($rows)) {
369 return;
370 }
371
372 $ids = array_keys($rows);
373 $numIDs = count($ids);
374 array_unshift($ids, 0);
353ffa53 375 $ids[] = 0;
6a488035 376 $firstID = $ids[1];
353ffa53 377 $lastID = $ids[$numIDs];
6a488035
TO
378 if ($firstID == $lastID) {
379 $rows[$firstID]['order'] = NULL;
380 return;
381 }
353ffa53
TO
382 $config = CRM_Core_Config::singleton();
383 $imageURL = $config->userFrameworkResourceURL . 'i/arrow';
6a488035 384
be2fb01f 385 $queryParams = [
6a488035
TO
386 'reset' => 1,
387 'dao' => $daoName,
388 'idName' => $idName,
389 'url' => $returnURL,
390 'filter' => $filter,
be2fb01f 391 ];
450f494d 392
6a488035
TO
393 $signer = new CRM_Utils_Signer(CRM_Core_Key::privateKey(), self::$SIGNABLE_FIELDS);
394 $queryParams['_sgn'] = $signer->sign($queryParams);
395 $baseURL = CRM_Utils_System::url('civicrm/admin/weight', $queryParams);
396
397 for ($i = 1; $i <= $numIDs; $i++) {
353ffa53 398 $id = $ids[$i];
6a488035
TO
399 $prevID = $ids[$i - 1];
400 $nextID = $ids[$i + 1];
401
be2fb01f 402 $links = [];
7326b302 403 $url = "{$baseURL}&amp;src=$id";
6a488035
TO
404
405 if ($prevID != 0) {
406 $alt = ts('Move to top');
7326b302 407 $links[] = "<a class=\"crm-weight-arrow\" href=\"{$url}&amp;dst={$firstID}&amp;dir=first\"><img src=\"{$imageURL}/first.gif\" title=\"$alt\" alt=\"$alt\" class=\"order-icon\"></a>";
6a488035
TO
408
409 $alt = ts('Move up one row');
7326b302 410 $links[] = "<a class=\"crm-weight-arrow\" href=\"{$url}&amp;dst={$prevID}&amp;dir=swap\"><img src=\"{$imageURL}/up.gif\" title=\"$alt\" alt=\"$alt\" class=\"order-icon\"></a>";
6a488035
TO
411 }
412 else {
841e66cd
BT
413 $links[] = "<span class=\"order-icon\"></span>";
414 $links[] = "<span class=\"order-icon\"></span>";
6a488035
TO
415 }
416
417 if ($nextID != 0) {
418 $alt = ts('Move down one row');
7326b302 419 $links[] = "<a class=\"crm-weight-arrow\" href=\"{$url}&amp;dst={$nextID}&amp;dir=swap\"><img src=\"{$imageURL}/down.gif\" title=\"$alt\" alt=\"$alt\" class=\"order-icon\"></a>";
6a488035
TO
420
421 $alt = ts('Move to bottom');
7326b302 422 $links[] = "<a class=\"crm-weight-arrow\" href=\"{$url}&amp;dst={$lastID}&amp;dir=last\"><img src=\"{$imageURL}/last.gif\" title=\"$alt\" alt=\"$alt\" class=\"order-icon\"></a>";
6a488035
TO
423 }
424 else {
841e66cd
BT
425 $links[] = "<span class=\"order-icon\"></span>";
426 $links[] = "<span class=\"order-icon\"></span>";
6a488035
TO
427 }
428 $rows[$id]['weight'] = implode('&nbsp;', $links);
429 }
430 }
431
ee3db087
SL
432 /**
433 *
434 * @throws CRM_Core_Exception
435 */
00be9182 436 public static function fixOrder() {
a3d827a7 437 $signature = CRM_Utils_Request::retrieve('_sgn', 'String');
6a488035
TO
438 $signer = new CRM_Utils_Signer(CRM_Core_Key::privateKey(), self::$SIGNABLE_FIELDS);
439
440 // Validate $_GET values b/c subsequent code reads $_GET (via CRM_Utils_Request::retrieve)
353ffa53 441 if (!$signer->validate($signature, $_GET)) {
ee3db087 442 throw new CRM_Core_Exception('Request signature is invalid');
6a488035
TO
443 }
444
445 // Note: Ensure this list matches self::$SIGNABLE_FIELDS
a3d827a7
CW
446 $daoName = CRM_Utils_Request::retrieve('dao', 'String');
447 $id = CRM_Utils_Request::retrieve('id', 'Integer');
448 $idName = CRM_Utils_Request::retrieve('idName', 'String');
449 $url = CRM_Utils_Request::retrieve('url', 'String');
450 $filter = CRM_Utils_Request::retrieve('filter', 'String');
451 $src = CRM_Utils_Request::retrieve('src', 'Integer');
452 $dst = CRM_Utils_Request::retrieve('dst', 'Integer');
453 $dir = CRM_Utils_Request::retrieve('dir', 'String');
353ffa53 454 $object = new $daoName();
6a488035
TO
455 $srcWeight = CRM_Core_DAO::getFieldValue($daoName, $src, 'weight', $idName);
456 $dstWeight = CRM_Core_DAO::getFieldValue($daoName, $dst, 'weight', $idName);
457 if ($srcWeight == $dstWeight) {
4a140040 458 self::fixOrderOutput($url);
6a488035
TO
459 }
460
461 $tableName = $object->tableName();
462
463 $query = "UPDATE $tableName SET weight = %1 WHERE $idName = %2";
be2fb01f
CW
464 $params = [
465 1 => [$dstWeight, 'Integer'],
466 2 => [$src, 'Integer'],
467 ];
6a488035
TO
468 CRM_Core_DAO::executeQuery($query, $params);
469
470 if ($dir == 'swap') {
be2fb01f
CW
471 $params = [
472 1 => [$srcWeight, 'Integer'],
473 2 => [$dst, 'Integer'],
474 ];
6a488035
TO
475 CRM_Core_DAO::executeQuery($query, $params);
476 }
477 elseif ($dir == 'first') {
478 // increment the rest by one
479 $query = "UPDATE $tableName SET weight = weight + 1 WHERE $idName != %1 AND weight < %2";
480 if ($filter) {
481 $query .= " AND $filter";
482 }
be2fb01f
CW
483 $params = [
484 1 => [$src, 'Integer'],
485 2 => [$srcWeight, 'Integer'],
486 ];
6a488035
TO
487 CRM_Core_DAO::executeQuery($query, $params);
488 }
489 elseif ($dir == 'last') {
490 // increment the rest by one
491 $query = "UPDATE $tableName SET weight = weight - 1 WHERE $idName != %1 AND weight > %2";
492 if ($filter) {
493 $query .= " AND $filter";
494 }
be2fb01f
CW
495 $params = [
496 1 => [$src, 'Integer'],
497 2 => [$srcWeight, 'Integer'],
498 ];
6a488035
TO
499 CRM_Core_DAO::executeQuery($query, $params);
500 }
501
4a140040
CW
502 self::fixOrderOutput($url);
503 }
f4aaa82a 504
5bc392e6 505 /**
3fd42bb5 506 * @param string $url
5bc392e6 507 */
00be9182 508 public static function fixOrderOutput($url) {
4a140040
CW
509 if (empty($_GET['snippet']) || $_GET['snippet'] !== 'json') {
510 CRM_Utils_System::redirect($url);
511 }
512
be2fb01f 513 CRM_Core_Page_AJAX::returnJsonResponse([
4a140040 514 'userContext' => $url,
be2fb01f 515 ]);
6a488035 516 }
96025800 517
6a488035 518}