dolibarr 25.0.0-alpha
api_dolresources.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2015 Jean-François Ferry <jfefe@aternatik.fr>
3 * Copyright (C) 2026 Dolicraft <contact@dolicraft.com>
4 * Copyright (C) 2026 Frédéric France <frederic.france@free.fr>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20use Luracast\Restler\RestException;
21
22require_once DOL_DOCUMENT_ROOT.'/resource/class/dolresource.class.php';
23
41{
45 public static $FIELDS = array(
46 'ref'
47 );
48
52 public $resource;
53
57 public function __construct()
58 {
59 global $db;
60
61 $this->db = $db;
62 $this->resource = new Dolresource($this->db);
63 }
64
78 public function get($id)
79 {
80 if (!DolibarrApiAccess::$user->hasRight('resource', 'read')) {
81 throw new RestException(403);
82 }
83
84 $result = $this->resource->fetch($id);
85 if ($result <= 0) {
86 throw new RestException(404, 'Resource not found');
87 }
88
89 if (!DolibarrApi::_checkAccessToResource('resource', $this->resource->id)) {
90 throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
91 }
92
93 return $this->_cleanObjectDatas($this->resource);
94 }
95
115 public function index($sortfield = "t.ref", $sortorder = 'ASC', $limit = 100, $page = 0, $sqlfilters = '', $properties = '')
116 {
117 if (!DolibarrApiAccess::$user->hasRight('resource', 'read')) {
118 throw new RestException(403);
119 }
120
121 $obj_ret = array();
122
123 $sql = "SELECT t.rowid";
124 $sql .= " FROM ".$this->db->prefix()."resource as t";
125 $sql .= " WHERE t.entity IN (".getEntity('resource').")";
126
127 // Add sql filters
128 if ($sqlfilters) {
129 $errormessage = '';
130 $sql .= forgeSQLFromUniversalSearchCriteria($sqlfilters, $errormessage);
131 if ($errormessage) {
132 throw new RestException(400, 'Error when validating parameter sqlfilters -> '.$errormessage);
133 }
134 }
135
136 $sql .= $this->db->order($sortfield, $sortorder);
137 if ($limit) {
138 if ($page < 0) {
139 $page = 0;
140 }
141 $offset = $limit * $page;
142
143 $sql .= $this->db->plimit($limit + 1, $offset);
144 }
145
146 $result = $this->db->query($sql);
147 if (!$result) {
148 throw new RestException(503, 'Error when retrieving resource list: '.$this->db->lasterror());
149 }
150
151 $num = $this->db->num_rows($result);
152 $min = min($num, ($limit <= 0 ? $num : $limit));
153 $i = 0;
154 while ($i < $min) {
155 $obj = $this->db->fetch_object($result);
156 $resource_static = new Dolresource($this->db);
157 if ($resource_static->fetch($obj->rowid) > 0) {
158 $obj_ret[] = $this->_filterObjectProperties($this->_cleanObjectDatas($resource_static), $properties);
159 }
160 $i++;
161 }
162
163 return $obj_ret;
164 }
165
178 public function post($request_data = null)
179 {
180 if (!DolibarrApiAccess::$user->hasRight('resource', 'write')) {
181 throw new RestException(403);
182 }
183
184 // Check mandatory fields
185 $this->_validate($request_data);
186
187 foreach ($request_data as $field => $value) {
188 if ($field === 'caller') {
189 // Add a mention of caller so on trigger called after action, we can filter to avoid a loop if we try to sync back again with the caller
190 $this->resource->context['caller'] = sanitizeVal($request_data['caller'], 'aZ09');
191 continue;
192 }
193
194 $this->resource->$field = $this->_checkValForAPI($field, $value, $this->resource);
195 }
196
197 // Note: create() returns the id of the new resource on success, and a positive
198 // count of errors on failure, so the result must be compared to the id and not to 0.
199 $result = $this->resource->create(DolibarrApiAccess::$user);
200 if ($result <= 0 || $result != $this->resource->id) {
201 throw new RestException(500, "Error creating resource", array_merge(array($this->resource->error), $this->resource->errors));
202 }
203
204 return $this->resource->id;
205 }
206
220 public function put($id, $request_data = null)
221 {
222 if (!DolibarrApiAccess::$user->hasRight('resource', 'write')) {
223 throw new RestException(403);
224 }
225
226 $result = $this->resource->fetch($id);
227 if ($result <= 0) {
228 throw new RestException(404, 'Resource not found');
229 }
230
231 if (!DolibarrApi::_checkAccessToResource('resource', $this->resource->id)) {
232 throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
233 }
234
235 $this->resource->oldcopy = dol_clone($this->resource, 2); // @phan-suppress-current-line PhanTypeMismatchProperty
236
237 foreach ($request_data as $field => $value) {
238 if ($field == 'id') {
239 continue;
240 }
241 if ($field === 'caller') {
242 // Add a mention of caller so on trigger called after action, we can filter to avoid a loop if we try to sync back again with the caller
243 $this->resource->context['caller'] = sanitizeVal($request_data['caller'], 'aZ09');
244 continue;
245 }
246
247 if ($field == 'array_options' && is_array($value)) {
248 foreach ($value as $index => $val) {
249 $this->resource->array_options[$index] = $this->_checkValForAPI($field, $val, $this->resource);
250 }
251 continue;
252 }
253
254 $this->resource->$field = $this->_checkValForAPI($field, $value, $this->resource);
255 }
256
257 if ($this->resource->update(DolibarrApiAccess::$user) <= 0) {
258 throw new RestException(500, $this->resource->errorsToString());
259 }
260
261 return $this->get($id);
262 }
263
276 public function delete($id)
277 {
278 if (!DolibarrApiAccess::$user->hasRight('resource', 'delete')) {
279 throw new RestException(403);
280 }
281
282 $result = $this->resource->fetch($id);
283 if ($result <= 0) {
284 throw new RestException(404, 'Resource not found');
285 }
286
287 if (!DolibarrApi::_checkAccessToResource('resource', $this->resource->id)) {
288 throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
289 }
290
291 if ($this->resource->delete(DolibarrApiAccess::$user) <= 0) {
292 throw new RestException(500, 'Error when deleting resource: '.$this->resource->errorsToString());
293 }
294
295 return array(
296 'success' => array(
297 'code' => 200,
298 'message' => 'Resource deleted'
299 )
300 );
301 }
302
321 public function getElementResources($element_type, $element_id)
322 {
323 if (!DolibarrApiAccess::$user->hasRight('resource', 'read')) {
324 throw new RestException(403);
325 }
326
327 $element = $this->_fetchElement($element_type, $element_id);
328
329 $result = array();
330 foreach ($this->resource->getElementResources($element->element, $element->id) as $link) {
331 $result[] = $this->_formatLink($link);
332 }
333
334 return $result;
335 }
336
357 public function postElementResources($element_type, $element_id, $request_data = null)
358 {
359 if (!DolibarrApiAccess::$user->hasRight('resource', 'write')) {
360 throw new RestException(403);
361 }
362
363 if ($request_data === null) {
364 $request_data = array();
365 }
366 if (!isset($request_data['resource_id'])) {
367 throw new RestException(400, "resource_id field missing");
368 }
369
370 $element = $this->_fetchElement($element_type, $element_id);
371
372 $resource_id = (int) $request_data['resource_id'];
373 if ($this->resource->fetch($resource_id) <= 0) {
374 throw new RestException(404, 'Resource not found');
375 }
376
377 $busy = empty($request_data['busy']) ? 0 : 1;
378 $mandatory = empty($request_data['mandatory']) ? 0 : 1;
379
380 // The unique index of llx_element_resources rejects a duplicate, so the case is detected
381 // here to answer 409 instead of letting the insert fail with a 500.
382 foreach ($this->resource->getElementResources($element->element, $element->id) as $existing) {
383 if ((int) $existing['resource_id'] == $resource_id && $existing['resource_type'] == $this->resource->element) {
384 throw new RestException(409, 'Resource already linked to this element');
385 }
386 }
387
388 // A busy link books the resource, so the same double booking check as the interface is
389 // applied. getBookingConflicts() returns -1 on SQL error, an array otherwise.
390 if ($busy) {
391 $conflicts = $this->_getBookingConflicts($element, $resource_id);
392 if (!empty($conflicts)) {
393 throw new RestException(409, 'Resource already used on this period by: '.$this->_describeConflicts($conflicts));
394 }
395 }
396
397 if ($element->add_element_resource($resource_id, $this->resource->element, $busy, $mandatory) <= 0) {
398 throw new RestException(500, 'Error when linking resource: '.$element->errorsToString());
399 }
400
401 foreach ($this->resource->getElementResources($element->element, $element->id) as $link) {
402 if ((int) $link['resource_id'] == $resource_id && $link['resource_type'] == $this->resource->element) {
403 return $this->_formatLink($link);
404 }
405 }
406
407 throw new RestException(500, 'Link created but not found back');
408 }
409
426 public function deleteElementResources($element_type, $element_id, $id)
427 {
428 if (!DolibarrApiAccess::$user->hasRight('resource', 'delete')) {
429 throw new RestException(403);
430 }
431
432 $element = $this->_fetchElement($element_type, $element_id);
433
434 // delete_resource() deletes by rowid without checking the row belongs to the element, so
435 // the link is looked up on the element first to not allow deleting the link of another one.
436 $found = false;
437 foreach ($this->resource->getElementResources($element->element, $element->id) as $link) {
438 if ((int) $link['rowid'] == (int) $id) {
439 $found = true;
440 break;
441 }
442 }
443 if (!$found) {
444 throw new RestException(404, 'Link not found for this element');
445 }
446
447 if ($element->delete_resource($id, $element->element) <= 0) {
448 throw new RestException(500, 'Error when unlinking resource: '.$element->errorsToString());
449 }
450
451 return array(
452 'success' => array(
453 'code' => 200,
454 'message' => 'Resource unlinked'
455 )
456 );
457 }
458
479 public function getBookingConflicts($id, $date_start, $date_end)
480 {
481 if (!DolibarrApiAccess::$user->hasRight('resource', 'read')) {
482 throw new RestException(403);
483 }
484
485 if ($this->resource->fetch($id) <= 0) {
486 throw new RestException(404, 'Resource not found');
487 }
488
489 $start = $this->_toTimestamp($date_start, 'date_start');
490 $end = $this->_toTimestamp($date_end, 'date_end');
491 if ($end < $start) {
492 throw new RestException(400, 'date_end is before date_start');
493 }
494
495 $conflicts = $this->resource->getBookingConflicts($this->resource->id, $this->resource->element, $start, $end);
496 if (!is_array($conflicts)) {
497 throw new RestException(500, 'Error when searching the conflicts: '.$this->resource->errorsToString());
498 }
499
500 return $conflicts;
501 }
502
503 // phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
510 protected function _cleanObjectDatas($object)
511 {
512 // phpcs:enable
513 $object = parent::_cleanObjectDatas($object);
514
515 unset($object->cache_code_type_resource);
516 unset($object->objelement);
517 unset($object->type_label);
518
519 // Properties of the link between an element and a resource, only filled by
520 // fetchElementResource() and not by fetch()
521 unset($object->resource_id);
522 unset($object->resource_type);
523 unset($object->element_id);
524 unset($object->element_type);
525 unset($object->busy);
526 unset($object->mandatory);
527 unset($object->fulldayevent);
528
529 return $object;
530 }
531
543 private function _fetchElement($element_type, $element_id)
544 {
545 $element_type = (string) $element_type;
546 if ($element_type === '' || !preg_match('/^[a-z0-9_]+$/i', $element_type)) {
547 throw new RestException(400, 'Bad value for parameter element_type');
548 }
549 if ((int) $element_id <= 0) {
550 throw new RestException(400, 'Bad value for parameter element_id');
551 }
552
553 $element = fetchObjectByElement((int) $element_id, $element_type);
554 if (!is_object($element) || empty($element->id)) {
555 throw new RestException(404, 'Element '.$element_type.' not found');
556 }
557
558 // Linking a resource writes on the element, so the access to the element itself is checked
559 // and not only the permissions on the resources.
560 if (!DolibarrApi::_checkAccessToResource($element_type, $element->id)) {
561 throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login);
562 }
563
564 return $element;
565 }
566
573 private function _formatLink($link)
574 {
575 $resource = null;
576 $resource_static = new Dolresource($this->db);
577 if ($resource_static->fetch((int) $link['resource_id']) > 0) {
578 $resource = $this->_cleanObjectDatas($resource_static);
579 }
580
581 return array(
582 'id' => (int) $link['rowid'],
583 'resource_id' => (int) $link['resource_id'],
584 'resource_type' => $link['resource_type'],
585 'busy' => (int) $link['busy'],
586 'mandatory' => (int) $link['mandatory'],
587 'resource' => $resource
588 );
589 }
590
600 private function _getBookingConflicts($element, $resource_id)
601 {
602 $date_start = 0;
603 $date_end = 0;
604
605 // Only the agenda event carries a usable period on the object itself. The other elements
606 // are linked without a booking check, as the interface does.
607 if ($element->element == 'action') {
609 '@phan-var-force ActionComm $element';
610 $date_start = empty($element->datep) ? 0 : $element->datep;
611 $date_end = empty($element->datef) ? $date_start : $element->datef;
612 if ($date_start && !empty($element->fulldayevent)) {
613 $parts = dol_getdate((int) $date_start);
614 $date_start = dol_mktime(0, 0, 0, $parts['mon'], $parts['mday'], $parts['year']);
615 $date_end = dol_mktime(23, 59, 59, $parts['mon'], $parts['mday'], $parts['year']);
616 }
617 }
618
619 if (empty($date_start)) {
620 return array();
621 }
622
623 $conflicts = $this->resource->getBookingConflicts($resource_id, $this->resource->element, $date_start, $date_end, $element->element, $element->id);
624 if (!is_array($conflicts)) {
625 throw new RestException(500, 'Error when searching the conflicts: '.$this->resource->errorsToString());
626 }
627
628 return $conflicts;
629 }
630
637 private function _describeConflicts($conflicts)
638 {
639 $out = array();
640 foreach ($conflicts as $conflict) {
641 $out[] = $conflict['element_type'].' '.$conflict['element_id'].(empty($conflict['ref']) ? '' : ' ('.$conflict['ref'].')');
642 }
643
644 return implode(', ', $out);
645 }
646
656 private function _toTimestamp($value, $name)
657 {
658 if (is_numeric($value)) {
659 return (int) $value;
660 }
661
662 $timestamp = dol_stringtotime((string) $value);
663 if (empty($timestamp)) {
664 throw new RestException(400, 'Bad value for parameter '.$name);
665 }
666
667 return (int) $timestamp;
668 }
669
678 private function _validate($data)
679 {
680 if ($data === null) {
681 $data = array();
682 }
683 $resource = array();
684 foreach (Dolresources::$FIELDS as $field) {
685 if (!isset($data[$field])) {
686 throw new RestException(400, "$field field missing");
687 }
688 $resource[$field] = $data[$field];
689 }
690
691 return $resource;
692 }
693}
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:47
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
Class for API REST v1.
Definition api.class.php:35
_filterObjectProperties($object, $properties)
Filter properties that will be returned on object.
_checkValForAPI($field, $value, $object)
Check and convert a string depending on its type/name.
static _checkAccessToResource($resource, $resource_id=0, $dbtablename='', $feature2='', $dbt_keyfield='fk_soc', $dbt_select='rowid', $parenttableforentity='')
Check access by user to a given resource.
DAO Resource object.
_describeConflicts($conflicts)
Describe conflicting bookings for an error message.
_cleanObjectDatas($object)
Clean sensible object datas.
post($request_data=null)
Create resource object.
getBookingConflicts($id, $date_start, $date_end)
Get the bookings that conflict with a period for a resource.
_toTimestamp($value, $name)
Convert a date given to the API into a timestamp.
__construct()
Constructor.
_fetchElement($element_type, $element_id)
Load the element carrying the resources and check the access to it.
deleteElementResources($element_type, $element_id, $id)
Unlink a resource from an element.
_validate($data)
Validate fields before create or update object.
_formatLink($link)
Format a link of llx_element_resources for the answer.
put($id, $request_data=null)
Update resource.
getElementResources($element_type, $element_id)
Get the resources linked to an element.
index($sortfield="t.ref", $sortorder='ASC', $limit=100, $page=0, $sqlfilters='', $properties='')
List resources.
postElementResources($element_type, $element_id, $request_data=null)
Link a resource to an element.
dol_stringtotime($string, $gm=1, $processnotimeasnoon=0)
Convert a string date into a GM Timestamps date Warning: YYYY-MM-DDTHH:MM:SS+02:00 (RFC3339) is not s...
Definition date.lib.php:442
$date_start
Variables from include:
dol_mktime($hour, $minute, $second, $month, $day, $year, $gm='auto', $check=1)
Return a timestamp date built from detailed information (by default a local PHP server timestamp) Rep...
forgeSQLFromUniversalSearchCriteria($filter, &$errorstr='', $noand=0, $nopar=0, $noerror=0, $forbiddenfields=array())
forgeSQLFromUniversalSearchCriteria
dol_clone($srcobject, $native=2)
Create a clone of instance of object (new instance with same value for each properties) With native =...
sanitizeVal($out='', $check='alphanohtml', $filter=null, $options=null)
Return a sanitized or empty value after checking value against a rule.
dol_getdate($timestamp, $fast=false, $forcetimezone='')
Return an array with locale date info.