1<?php 2/** 3 * The Kronolith_Driver_Resource class implements the Kronolith_Driver API for 4 * storing resource calendars in a SQL backend. 5 * 6 * Copyright 1999-2017 Horde LLC (http://www.horde.org/) 7 * 8 * See the enclosed file COPYING for license information (GPL). If you 9 * did not receive this file, see http://www.horde.org/licenses/gpl. 10 * 11 * @author Luc Saillard <luc.saillard@fr.alcove.com> 12 * @author Chuck Hagenbuch <chuck@horde.org> 13 * @author Jan Schneider <jan@horde.org> 14 * @author Michael J Rubinsky <mrubinsk@horde.org> 15 * @package Kronolith 16 */ 17class Kronolith_Driver_Resource_Sql extends Kronolith_Driver 18{ 19 /** 20 * The main event storage driver. 21 * 22 * @var Kronolith_Driver 23 */ 24 protected $_driver; 25 26 /** 27 * Column information as Horde_Db_Adapter_Base_Column objects. 28 * 29 * @var array 30 */ 31 protected $_columns = array(); 32 33 /** 34 * The class name of the event object to instantiate. 35 * 36 * @var string 37 */ 38 protected $_eventClass = 'Kronolith_Event_Resource_Sql'; 39 40 /** 41 * Attempts to open a connection to the SQL server. 42 * 43 * @throws Kronolith_Exception 44 */ 45 public function initialize() 46 { 47 if (empty($this->_params['db'])) { 48 throw new InvalidArgumentException('Missing required Horde_Db_Adapter instance'); 49 } 50 try { 51 $this->_db = $this->_params['db']; 52 } catch (Horde_Exception $e) { 53 throw new Kronolith_Exception($e); 54 } 55 56 $this->_params = array_merge(array( 57 'table' => 'kronolith_resources' 58 ), $this->_params); 59 60 $this->_driver = Kronolith::getDriver(); 61 $this->_columns = $this->_db->columns($this->_params['table']); 62 } 63 64 /** 65 * Selects a calendar as the currently opened calendar. 66 * 67 * @param string $calendar A calendar identifier. 68 */ 69 public function open($calendar) 70 { 71 $this->calendar = $calendar; 72 $this->_driver->open($calendar); 73 } 74 75 /** 76 * Lists all events in the time range, optionally restricting results to 77 * only events with alarms. 78 * 79 * @param Horde_Date $startDate The start of range date. 80 * @param Horde_Date $endDate The end of date range. 81 * @param array $options Additional options: 82 * - show_recurrence: (boolean) Return every instance of a recurring 83 * event? 84 * DEFAULT: false (Only return recurring events once 85 * inside $startDate - $endDate range) 86 * - has_alarm: (boolean) Only return events with alarms. 87 * DEFAULT: false (Return all events) 88 * - json: (boolean) Store the results of the event's toJson() 89 * method? 90 * DEFAULT: false 91 * - cover_dates: (boolean) Add the events to all days that they 92 * cover? 93 * DEFAULT: true 94 * - hide_exceptions: (boolean) Hide events that represent exceptions to 95 * a recurring event. 96 * DEFAULT: false (Do not hide exception events) 97 * - fetch_tags: (boolean) Fetch tags for all events. 98 * DEFAULT: false (Do not fetch event tags) 99 * 100 * @throws Kronolith_Exception 101 */ 102 public function listEvents(Horde_Date $startDate = null, 103 Horde_Date $endDate = null, 104 array $options = array()) 105 { 106 $json = !empty($options['json']); 107 $options['json'] = false; 108 $events = $this->_driver->listEvents($startDate, $endDate, $options); 109 $results = array(); 110 111 foreach ($events as $period_key => $period) { 112 foreach ($period as $event_id => $event) { 113 $resource_event = $this->_buildResourceEvent($event); 114 $results[$period_key][$event_id] = $json 115 ? $resource_event->toJson() 116 : $resource_event; 117 } 118 } 119 120 return $results; 121 } 122 123 protected function _buildResourceEvent($driver_event) 124 { 125 $resource_event = new $this->_eventClass($this); 126 $resource_event->fromDriver($driver_event->toProperties(true)); 127 $resource_event->calendar = $this->calendar; 128 129 return $resource_event; 130 } 131 132 /** 133 * Get an event or events with the given UID value. 134 * 135 * @param string $uid The UID to match 136 * @param array $calendars A restricted array of calendar ids to search 137 * @param boolean $getAll Return all matching events? 138 * 139 * @return Kronolith_Event 140 * @throws Kronolith_Exception 141 * @throws Horde_Exception_NotFound 142 */ 143 public function getByUID($uid, $calendars = null, $getAll = false) 144 { 145 $event = new $this->_eventClass($this); 146 $driver_event = $this->_driver->getByUID($uid, $calendars, $getAll); 147 $event->fromDriver($driver_event->toProperties(true)); 148 $event->calendar = $this->calendar; 149 return $event; 150 } 151 152 /** 153 * @throws Kronolith_Exception 154 * @throws Horde_Exception_NotFound 155 */ 156 public function getEvent($eventId = null) 157 { 158 if (!strlen($eventId)) { 159 $event = new $this->_eventClass($this); 160 $event->calendar = $this->calendar; 161 162 return $event; 163 } 164 165 $driver_event = $this->_driver->getEvent($eventId); 166 $event = $this->_buildResourceEvent($driver_event); 167 168 return $event; 169 } 170 171 /** 172 * Saves an event in the backend. 173 * 174 * If it is a new event, it is added, otherwise the event is updated. 175 * 176 * @param Kronolith_Event $event The event to save. 177 * 178 * @return string The event id. 179 * @throws Horde_Mime_Exception 180 * @throws Kronolith_Exception 181 */ 182 public function saveEvent(Kronolith_Event $event) 183 { 184 return $this->_driver->saveEvent($event); 185 } 186 187 /** 188 * Delete an event. 189 * 190 * Since this is the Kronolith_Resource's version of the event, if we 191 * delete it, we must also make sure to remove it from the event that 192 * it is attached to. Not sure if there is a better way to do this... 193 * 194 * @param string|Kronolith_Event_Resource_Sql $eventId The ID of the event 195 * to delete. 196 * @param boolean $silent Don't send notifications, used when deleting 197 * events in bulk from maintenance tasks. 198 * @param boolean $keep_bound If true, does not remove the resource from 199 * the bound event. @since 4.2.2 200 * 201 * @throws Kronolith_Exception 202 * @throws Horde_Exception_NotFound 203 */ 204 public function deleteEvent($eventId, $silent = false, $keep_bound = false) 205 { 206 if ($eventId instanceof Kronolith_Event_Resource_Sql) { 207 $delete_event = $eventId; 208 $eventId = $delete_event->id; 209 } else { 210 $delete_event = $this->getEvent($eventId); 211 } 212 213 if ($keep_bound) { 214 return; 215 } 216 217 $uid = $delete_event->uid; 218 $events = $this->_driver->getByUID($uid, null, true); 219 foreach ($events as $e) { 220 $resources = $e->getResources(); 221 if (count($resources)) { 222 $r = $this->getResource($this->getResourceIdByCalendar($delete_event->calendar)); 223 $e->removeResource($r); 224 $e->save(); 225 } 226 } 227 $this->_driver->open($this->calendar); 228 $this->_driver->deleteEvent($delete_event, $silent); 229 } 230 231 /** 232 * Save or update a Kronolith_Resource 233 * 234 * @param Kronolith_Resource_Base $resource 235 * 236 * @return Kronolith_Resource object 237 * @throws Kronolith_Exception, Horde_Exception_PermissionDenied 238 */ 239 public function save(Kronolith_Resource_Base $resource) 240 { 241 if (!$GLOBALS['registry']->isAdmin() && 242 !$GLOBALS['injector']->getInstance('Horde_Core_Perms')->hasAppPermission('resource_management')) { 243 throw new Horde_Exception_PermissionDenied(); 244 } 245 if ($resource->getId()) { 246 $query = 'UPDATE ' . $this->_params['table'] . ' SET resource_name = ?, ' 247 . 'resource_calendar = ? , resource_description = ?, ' 248 . 'resource_response_type = ?, resource_type = ?, ' 249 . 'resource_members = ?, resource_email = ? WHERE resource_id = ?'; 250 251 $values = array($this->convertToDriver($resource->get('name')), 252 $resource->get('calendar'), 253 $this->convertToDriver($resource->get('description')), 254 $resource->get('response_type'), 255 $resource->get('type'), 256 serialize($resource->get('members')), 257 $resource->get('email'), 258 $resource->getId()); 259 260 try { 261 $this->_db->update($query, $values); 262 } catch (Horde_Db_Exception $e) { 263 throw new Kronolith_Exception($e); 264 } 265 } else { 266 $query = 'INSERT INTO ' . $this->_params['table'] 267 . ' (resource_name, resource_calendar, ' 268 . 'resource_description, resource_response_type, ' 269 . ' resource_type, resource_members, resource_email)' 270 . ' VALUES (?, ?, ?, ?, ?, ?, ?)'; 271 $values = array($this->convertToDriver($resource->get('name')), 272 $resource->get('calendar'), 273 $this->convertToDriver($resource->get('description')), 274 $resource->get('response_type'), 275 $resource->get('type'), 276 serialize($resource->get('members')), 277 $resource->get('email')); 278 try { 279 $id = $this->_db->insert($query, $values); 280 } catch (Horde_Db_Exception $e) { 281 throw new Kronolith_Exception($e); 282 } 283 $resource->setId($id); 284 } 285 286 return $resource; 287 } 288 289 /** 290 * Removes a resource from storage, along with any events in the resource's 291 * calendar. 292 * 293 * @param Kronolith_Resource_Base $resource The kronolith resource to remove 294 * 295 * @throws Kronolith_Exception, Horde_Exception_PermissionDenied 296 */ 297 public function delete($resource) 298 { 299 if (!$GLOBALS['registry']->isAdmin() && 300 !$GLOBALS['injector']->getInstance('Horde_Core_Perms')->hasAppPermission('resource_management')) { 301 throw new Horde_Exception_PermissionDenied(); 302 } 303 304 if (!$resource->getId()) { 305 throw new Kronolith_Exception(_("Resource not valid.")); 306 } 307 308 // Get group memberships and remove from group. 309 $groups = $this->getGroupMemberships($resource->getId()); 310 foreach ($groups as $id) { 311 $rg = $this->getResource($id); 312 $members = $rg->get('members'); 313 unset($members[array_search($resource->getId(), $members)]); 314 $rg->set('members', $members); 315 $rg->save(); 316 } 317 318 $this->_deleteResourceCalendar($resource->get('calendar')); 319 try { 320 $query = 'DELETE FROM ' . $this->_params['table'] . ' WHERE resource_id = ?'; 321 $this->_db->delete($query, array($resource->getId())); 322 } catch (Horde_Db_Exception $e) { 323 throw new Kronolith_Exception($e); 324 } 325 } 326 327 /** 328 * Obtain a Kronolith_Resource by the resource's id 329 * 330 * @param integer $id The key for the Kronolith_Resource 331 * 332 * @return Kronolith_Resource_Base 333 * @throws Kronolith_Exception 334 */ 335 public function getResource($id) 336 { 337 $query = 'SELECT resource_id, resource_name, resource_calendar, ' 338 . 'resource_description, resource_response_type, resource_type, ' 339 . 'resource_members, resource_email FROM ' . $this->_params['table'] 340 . ' WHERE resource_id = ?'; 341 342 try { 343 $results = $this->_db->selectOne($query, array($id)); 344 } catch (Horde_Db_Exception $e) { 345 throw new Kronolith_Exception($e); 346 } 347 if (!count($results)) { 348 throw new Horde_Exception_NotFound('Resource not found'); 349 } 350 351 $class = 'Kronolith_Resource_' . $results['resource_type']; 352 if (!class_exists($class)) { 353 throw new Kronolith_Exception('Could not load the class definition for ' . $class); 354 } 355 356 return new $class($this->_fromDriver($results)); 357 } 358 359 /** 360 * Obtain the resource id associated with the given calendar uid. 361 * 362 * @param string $calendar The calendar's uid. 363 * 364 * @return integer The Kronolith_Resource id. 365 * @throws Kronolith_Exception 366 */ 367 public function getResourceIdByCalendar($calendar) 368 { 369 $query = 'SELECT resource_id FROM ' . $this->_params['table'] 370 . ' WHERE resource_calendar = ?'; 371 try { 372 $result = $this->_db->selectValue($query, array($calendar)); 373 } catch (Horde_Db_Exception $e) { 374 throw new Kronolith_Exception($e); 375 } 376 if (empty($result)) { 377 throw new Horde_Exception_NotFound('Resource not found'); 378 } 379 380 return $result; 381 } 382 383 /** 384 * Determine if the provided calendar id represents a resource's calendar. 385 * 386 * @param string $calendar The calendar identifier to check. 387 * 388 * @return boolean 389 */ 390 public function isResourceCalendar($calendar) 391 { 392 $query = 'SELECT count(*) FROM ' . $this->_params['table'] 393 . ' WHERE resource_calendar = ?'; 394 try { 395 return $this->_db->selectValue($query, array($calendar)) > 0; 396 } catch (Horde_Db_Exception $e) { 397 throw new Kronolith_Exception($e); 398 } 399 } 400 401 /** 402 * Return a list of Kronolith_Resources 403 * 404 * Right now, all users have Horde_Perms::READ, but only system admins have 405 * Horde_Perms::EDIT | Horde_Perms::DELETE 406 * 407 * @param integer $perms A Horde_Perms::* constant. 408 * @param array $filter A hash of field/values to filter on. 409 * @param string $orderby Field to order results by. Null for no ordering. 410 * 411 * @return an array of Kronolith_Resource objects. 412 * @throws Kronolith_Exception 413 */ 414 public function listResources($perms = Horde_Perms::READ, array $filter = array(), $orderby = null) 415 { 416 if (($perms & (Horde_Perms::EDIT | Horde_Perms::DELETE)) && 417 !$GLOBALS['registry']->isAdmin()) { 418 return array(); 419 } 420 421 $query = 'SELECT resource_id, resource_name, resource_calendar, resource_description,' 422 . ' resource_response_type, resource_type, resource_members, resource_email FROM ' 423 . $this->_params['table']; 424 if (count($filter)) { 425 $clause = ' WHERE '; 426 $i = 0; 427 $c = count($filter); 428 foreach (array_keys($filter) as $field) { 429 $clause .= 'resource_' . $field . ' = ?' . (($i++ < ($c - 1)) ? ' AND ' : ''); 430 } 431 $query .= $clause; 432 } 433 434 if (!empty($orderby)) { 435 $query .= ' ORDER BY resource_' . $orderby; 436 } 437 438 try { 439 $results = $this->_db->selectAll($query, $filter); 440 } catch (Horde_Db_Exception $e) { 441 throw new Kronolith_Exception($e); 442 } 443 $return = array(); 444 foreach ($results as $row) { 445 $class = 'Kronolith_Resource_' . $row['resource_type']; 446 $return[$row['resource_id']] = new $class($this->_fromDriver(array_merge(array('resource_id' => $row['resource_id']), $row))); 447 } 448 449 return $return; 450 } 451 452 /** 453 * Obtain the group id for each group the specified resource is a member of. 454 * 455 * @param integer $resource_id The resource id to check for. 456 * 457 * @return array An array of group ids. 458 * @throws Kronolith_Exception 459 */ 460 public function getGroupMemberships($resource_id) 461 { 462 $groups = $this->listResources(Horde_Perms::READ, array('type' => Kronolith_Resource::TYPE_GROUP)); 463 $in = array(); 464 foreach ($groups as $group) { 465 $members = $group->get('members'); 466 if (array_search($resource_id, $members) !== false) { 467 $in[] = $group->getId(); 468 } 469 } 470 471 return $in; 472 } 473 474 /** 475 * Converts a value from the driver's charset to the default 476 * charset. 477 * 478 * @param mixed $value A value to convert. 479 * 480 * @return mixed The converted value. 481 */ 482 public function convertFromDriver($value) 483 { 484 return Horde_String::convertCharset($value, $this->_params['charset'], 'UTF-8'); 485 } 486 487 /** 488 * Converts a value from the default charset to the driver's 489 * charset. 490 * 491 * @param mixed $value A value to convert. 492 * 493 * @return mixed The converted value. 494 */ 495 public function convertToDriver($value) 496 { 497 return Horde_String::convertCharset($value, 'UTF-8', $this->_params['charset']); 498 } 499 500 /** 501 * Delete the resource calendar 502 * 503 * @param string $calendar The calendar id. 504 */ 505 public function _deleteResourceCalendar($calendar) 506 { 507 $this->open($calendar); 508 $events = $this->listEvents(null, null, array('cover_dates' => false)); 509 foreach ($events as $dayevents) { 510 foreach ($dayevents as $event) { 511 $this->deleteEvent($event, true); 512 } 513 } 514 } 515 516 /** 517 * Convert from driver keys and charset to Kronolith keys and charset. 518 * 519 * @param array $params The key/values to convert. 520 * 521 * @return array An array of converted values. 522 */ 523 protected function _fromDriver(array $params) 524 { 525 $return = array(); 526 527 foreach ($params as $field => $value) { 528 switch ($field) { 529 case 'resource_description': 530 $value = $this->_columns['resource_description'] 531 ->binaryToString($value); 532 // Fall through. 533 case 'resource_name': 534 $value = $this->convertFromDriver($value); 535 break; 536 case 'resource_members': 537 $value = $this->_columns['resource_members'] 538 ->binaryToString($value); 539 $value = @unserialize($value); 540 break; 541 } 542 543 $return[str_replace('resource_', '', $field)] = $value; 544 } 545 546 return $return; 547 } 548 549 /** 550 * Helper function to update an existing event's tags to tagger storage. 551 * 552 * @param Kronolith_Event $event The event to update 553 */ 554 protected function _updateTags(Kronolith_Event $event) 555 { 556 // noop 557 } 558 559 /** 560 * Helper function to add tags from a newly creted event to the tagger. 561 * 562 * @param Kronolith_Event $event The event to save tags to storage for. 563 */ 564 protected function _addTags(Kronolith_Event $event) 565 { 566 // noop 567 } 568 569 protected function _handleNotifications(Kronolith_Event $event, $action) 570 { 571 // noop 572 } 573} 574