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