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