1<?php 2/** 3 * Copyright 2007-2017 Horde LLC (http://www.horde.org/) 4 * 5 * See the enclosed file COPYING for license information (LGPL). If you 6 * did not receive this file, see http://www.horde.org/licenses/lgpl21. 7 * 8 * @author Jan Schneider <jan@horde.org> 9 * @category Horde 10 * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 11 * @package Alarm 12 */ 13 14/** 15 * The Horde_Alarm class provides an interface to deal with reminders, alarms 16 * and notifications through a standardized API. 17 * 18 * @author Jan Schneider <jan@horde.org> 19 * @category Horde 20 * @copyright 2007-2017 Horde LLC 21 * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1 22 * @package Alarm 23 */ 24abstract class Horde_Alarm 25{ 26 /** 27 * Logger. 28 * 29 * @var Horde_Log_Logger 30 */ 31 protected $_logger; 32 33 /** 34 * Alarm loader callback. 35 * 36 * @var mixed 37 */ 38 protected $_loader; 39 40 /** 41 * Hash containing connection parameters. 42 * 43 * @var array 44 */ 45 protected $_params = array( 46 'ttl' => 300 47 ); 48 49 /** 50 * All registered notification handlers. 51 * 52 * @var array 53 */ 54 protected $_handlers = array(); 55 56 /** 57 * Whether handler classes have been dynamically loaded already. 58 * 59 * @var boolean 60 */ 61 protected $_handlersLoaded = false; 62 63 /** 64 * A list of errors, exceptions etc. that occured during notify() calls. 65 * 66 * @var array 67 */ 68 protected $_errors = array(); 69 70 /** 71 * Constructor. 72 * 73 * @param array $params Configuration parameters: 74 * <pre> 75 * 'logger' - (Horde_Log_Logger) A logger instance. 76 * 'ttl' - (integer) Time to live value, in seconds. 77 * </pre> 78 */ 79 public function __construct(array $params = array()) 80 { 81 if (isset($params['logger'])) { 82 $this->_logger = $params['logger']; 83 unset($params['logger']); 84 } 85 if (isset($params['loader'])) { 86 $this->_loader = $params['loader']; 87 unset($params['loader']); 88 } 89 $this->_params = array_merge($this->_params, $params); 90 } 91 92 /** 93 * Returns a list of alarms from the backend. 94 * 95 * @param string $user Return alarms for this user, all users if 96 * null, or global alarms if empty. 97 * @param Horde_Date $time The time when the alarms should be active. 98 * Defaults to now. 99 * @param boolean $load Update active alarms from all applications? 100 * @param boolean $preload Preload alarms that go off within the next 101 * ttl time span? 102 * 103 * @return array A list of alarm hashes. 104 * @throws Horde_Alarm_Exception 105 */ 106 public function listAlarms($user = null, Horde_Date $time = null, 107 $load = false, $preload = true) 108 { 109 if (empty($time)) { 110 $time = new Horde_Date(time()); 111 } 112 if ($load && is_callable($this->_loader)) { 113 call_user_func($this->_loader, $user, $preload); 114 } 115 116 $alarms = $this->_list($user, $time); 117 118 foreach (array_keys($alarms) as $alarm) { 119 if (isset($alarms[$alarm]['mail']['body'])) { 120 $alarms[$alarm]['mail']['body'] = $this->_fromDriver($alarms[$alarm]['mail']['body']); 121 } 122 } 123 return $alarms; 124 } 125 126 /** 127 * Returns a list of alarms from the backend. 128 * 129 * @param Horde_Date $time The time when the alarms should be active. 130 * @param string $user Return alarms for this user, all users if 131 * null, or global alarms if empty. 132 * 133 * @return array A list of alarm hashes. 134 * @throws Horde_Alarm_Exception 135 */ 136 abstract protected function _list($user, Horde_Date $time); 137 138 /** 139 * Returns a list of all global alarms from the backend. 140 * 141 * @return array A list of alarm hashes. 142 * @throws Horde_Alarm_Exception 143 */ 144 public function globalAlarms() 145 { 146 $alarms = $this->_global(); 147 foreach (array_keys($alarms) as $alarm) { 148 if (isset($alarms[$alarm]['mail']['body'])) { 149 $alarms[$alarm]['mail']['body'] = $this->_fromDriver($alarms[$alarm]['mail']['body']); 150 } 151 } 152 return $alarms; 153 } 154 155 /** 156 * Returns a list of all global alarms from the backend. 157 * 158 * @return array A list of alarm hashes. 159 */ 160 abstract protected function _global(); 161 162 /** 163 * Returns an alarm hash from the backend. 164 * 165 * @param string $id The alarm's unique id. 166 * @param string $user The alarm's user 167 * 168 * @return array An alarm hash. Contains the following: 169 * <pre> 170 * id: Unique alarm id. 171 * user: The alarm's user. Empty if a global alarm. 172 * start: The alarm start as a Horde_Date. 173 * end: The alarm end as a Horde_Date. 174 * methods: The notification methods for this alarm. 175 * params: The paramters for the notification methods. 176 * title: The alarm title. 177 * text: An optional alarm description. 178 * snooze: The snooze time (next time) of the alarm as a Horde_Date. 179 * internal: Holds internally used data. 180 * instanceid: Holds an instance identifier for recurring alarms. 181 * (@since 2.2.0) 182 * </pre> 183 * @throws Horde_Alarm_Exception 184 */ 185 public function get($id, $user) 186 { 187 $alarm = $this->_get($id, $user); 188 189 if (isset($alarm['mail']['body'])) { 190 $alarm['mail']['body'] = $this->_fromDriver($alarm['mail']['body']); 191 } 192 193 return $alarm; 194 } 195 196 /** 197 * Returns an alarm hash from the backend. 198 * 199 * @param string $id The alarm's unique id. 200 * @param string $user The alarm's user 201 * 202 * @return array An alarm hash. 203 * @throws Horde_Alarm_Exception 204 */ 205 abstract protected function _get($id, $user); 206 207 /** 208 * Stores an alarm hash in the backend. 209 * 210 * The alarm will be added if it doesn't exist, and updated otherwise. 211 * 212 * @param array $alarm An alarm hash. See self::get() for format. 213 * @param boolean $keep Whether to keep the snooze value and notification 214 * status unchanged. If true, the alarm will get 215 * "un-snoozed", and notifications (like mails) are 216 * sent again. 217 * 218 * @throws Horde_Alarm_Exception 219 */ 220 public function set(array $alarm, $keep = false) 221 { 222 if (isset($alarm['mail']['body'])) { 223 $alarm['mail']['body'] = $this->_toDriver($alarm['mail']['body']); 224 } 225 226 // If this is a recurring alarm and we have a new instanceid, 227 // remove the previous entry regardless of the value of $keep. 228 // Otherwise, the alarm will never be reset. @since 2.2.0 229 if (!empty($alarm['instanceid']) && 230 !$this->exists($alarm['id'], isset($alarm['user']) ? $alarm['user'] : '', !empty($alarm['instanceid']) ? $alarm['instanceid'] : null)) { 231 $this->delete($alarm['id'], isset($alarm['user']) ? $alarm['user'] : ''); 232 } 233 234 if ($this->exists($alarm['id'], isset($alarm['user']) ? $alarm['user'] : '')) { 235 $this->_update($alarm, $keep); 236 if (!$keep) { 237 foreach ($this->_handlers as &$handler) { 238 $handler->reset($alarm); 239 } 240 } 241 } else { 242 $this->_add($alarm); 243 } 244 } 245 246 /** 247 * Adds an alarm hash to the backend. 248 * 249 * @param array $alarm An alarm hash. 250 * 251 * @throws Horde_Alarm_Exception 252 */ 253 abstract protected function _add(array $alarm); 254 255 /** 256 * Updates an alarm hash in the backend. 257 * 258 * @param array $alarm An alarm hash. 259 * @param boolean $keepsnooze Whether to keep the snooze value unchanged. 260 * 261 * @throws Horde_Alarm_Exception 262 */ 263 abstract protected function _update(array $alarm, $keepsnooze = false); 264 265 /** 266 * Updates internal alarm properties, i.e. properties not determined by 267 * the application setting the alarm. 268 * 269 * @param string $id The alarm's unique id. 270 * @param string $user The alarm's user 271 * @param array $internal A hash with the internal data. 272 * 273 * @throws Horde_Alarm_Exception 274 */ 275 abstract public function internal($id, $user, array $internal); 276 277 /** 278 * Returns whether an alarm with the given id exists already. 279 * 280 * @param string $id The alarm's unique id. 281 * @param string $user The alarm's user 282 * @param string $instanceid An optional instanceid to check for. 283 * @since 2.2.0 284 * 285 * @return boolean True if the specified alarm exists. 286 */ 287 public function exists($id, $user, $instanceid = null) 288 { 289 try { 290 return $this->_exists($id, $user, $instanceid); 291 } catch (Horde_Alarm_Exception $e) { 292 return false; 293 } 294 } 295 296 /** 297 * Returns whether an alarm with the given id exists already. 298 * 299 * @param string $id The alarm's unique id. 300 * @param string $user The alarm's user 301 * @param string $instanceid An optional instanceid to match. 302 * 303 * @return boolean True if the specified alarm exists. 304 * @throws Horde_Alarm_Exception 305 */ 306 abstract protected function _exists($id, $user, $instanceid = null); 307 308 /** 309 * Delays (snoozes) an alarm for a certain period. 310 * 311 * @param string $id The alarm's unique id. 312 * @param string $user The notified user. 313 * @param integer $minutes The delay in minutes. A negative value 314 * dismisses the alarm completely. 315 * 316 * @throws Horde_Alarm_Exception 317 */ 318 public function snooze($id, $user, $minutes) 319 { 320 if (empty($user)) { 321 throw new Horde_Alarm_Exception('This alarm cannot be snoozed.'); 322 } 323 324 $alarm = $this->get($id, $user); 325 326 if ($alarm) { 327 if ($minutes > 0) { 328 $alarm['snooze'] = new Horde_Date(time()); 329 $alarm['snooze']->min += $minutes; 330 $this->_snooze($id, $user, $alarm['snooze']); 331 return; 332 } 333 334 $this->_dismiss($id, $user); 335 } 336 } 337 338 /** 339 * Delays (snoozes) an alarm for a certain period. 340 * 341 * @param string $id The alarm's unique id. 342 * @param string $user The alarm's user 343 * @param Horde_Date $snooze The snooze time. 344 * 345 * @throws Horde_Alarm_Exception 346 */ 347 abstract protected function _snooze($id, $user, Horde_Date $snooze); 348 349 /** 350 * Returns whether an alarm is snoozed. 351 * 352 * @param string $id The alarm's unique id. 353 * @param string $user The alarm's user 354 * @param Horde_Date $time The time when the alarm may be snoozed. 355 * Defaults to now. 356 * 357 * @return boolean True if the alarm is snoozed. 358 * 359 * @throws Horde_Alarm_Exception 360 */ 361 public function isSnoozed($id, $user, Horde_Date $time = null) 362 { 363 if (is_null($time)) { 364 $time = new Horde_Date(time()); 365 } 366 return (bool)$this->_isSnoozed($id, $user, $time); 367 } 368 369 /** 370 * Returns whether an alarm is snoozed. 371 * 372 * @param string $id The alarm's unique id. 373 * @param string $user The alarm's user 374 * @param Horde_Date $time The time when the alarm may be snoozed. 375 * 376 * @return boolean True if the alarm is snoozed. 377 * @throws Horde_Alarm_Exception 378 */ 379 abstract protected function _isSnoozed($id, $user, Horde_Date $time); 380 381 /** 382 * Dismisses an alarm. 383 * 384 * @param string $id The alarm's unique id. 385 * @param string $user The alarm's user 386 * 387 * @throws Horde_Alarm_Exception 388 */ 389 abstract protected function _dismiss($id, $user); 390 391 /** 392 * Deletes an alarm from the backend. 393 * 394 * @param string $id The alarm's unique id. 395 * @param string $user The alarm's user. All users' alarms if null. 396 * 397 * @throws Horde_Alarm_Exception 398 */ 399 function delete($id, $user = null) 400 { 401 $this->_delete($id, $user); 402 } 403 404 /** 405 * Deletes an alarm from the backend. 406 * 407 * @param string $id The alarm's unique id. 408 * @param string $user The alarm's user. All users' alarms if null. 409 * 410 * @throws Horde_Alarm_Exception 411 */ 412 abstract protected function _delete($id, $user = null); 413 414 /** 415 * Notifies the user about any active alarms. 416 * 417 * @param string $user Notify this user, all users if null, or guest 418 * users if empty. 419 * @param boolean $load Update active alarms from all applications? 420 * @param boolean $preload Preload alarms that go off within the next 421 * ttl time span? 422 * @param array $exclude Don't notify with these methods. 423 * 424 * @throws Horde_Alarm_Exception if loading of alarms fails, but not if 425 * notifying of individual alarms fails. 426 */ 427 public function notify($user = null, $load = true, $preload = true, 428 array $exclude = array()) 429 { 430 try { 431 $alarms = $this->listAlarms($user, null, $load, $preload); 432 } catch (Horde_Alarm_Exception $e) { 433 if ($this->_logger) { 434 $this->_logger->log($e, 'ERR'); 435 } 436 throw $e; 437 } 438 439 if (empty($alarms)) { 440 return; 441 } 442 443 $handlers = $this->handlers(); 444 foreach ($alarms as $alarm) { 445 foreach ($alarm['methods'] as $key => $alarm_method) { 446 if (isset($handlers[$alarm_method]) && 447 !in_array($alarm_method, $exclude)) { 448 try { 449 $handlers[$alarm_method]->notify($alarm); 450 } catch (Horde_Alarm_Exception $e) { 451 $this->_errors[$alarm['id'] . "\0" . $key] = $e; 452 } 453 } 454 } 455 } 456 } 457 458 /** 459 * Registers a notification handler. 460 * 461 * @param string $name A handler name. 462 * @param Horde_Alarm_Handler $handler A notification handler. 463 */ 464 public function addHandler($name, Horde_Alarm_Handler $handler) 465 { 466 $this->_handlers[$name] = $handler; 467 $handler->alarm = $this; 468 } 469 470 /** 471 * Returns a list of available notification handlers and parameters. 472 * 473 * The returned list is a hash with method names as the keys and 474 * optionally associated parameters as values. The parameters are hashes 475 * again with parameter names as keys and parameter information as 476 * values. The parameter information is hash with the following keys: 477 * 'desc' contains a parameter description; 'required' specifies whether 478 * this parameter is required. 479 * 480 * @return array List of methods and parameters. 481 */ 482 public function handlers() 483 { 484 if (!$this->_handlersLoaded) { 485 foreach (new DirectoryIterator(__DIR__ . '/Alarm/Handler') as $file) { 486 if (!$file->isFile() || substr($file->getFilename(), -4) != '.php') { 487 continue; 488 } 489 $handler = Horde_String::lower($file->getBasename('.php')); 490 if (isset($this->_handlers[$handler])) { 491 continue; 492 } 493 require_once $file->getPathname(); 494 $class = 'Horde_Alarm_Handler_' . $file->getBasename('.php'); 495 if (class_exists($class, false)) { 496 $this->addHandler($handler, new $class()); 497 } 498 } 499 $this->_handlerLoaded = true; 500 } 501 502 return $this->_handlers; 503 } 504 505 /** 506 * Returns a list of errors, exceptions etc. that occured during notify() 507 * calls. 508 * 509 * @since Horde_Alarm 2.1.0 510 * @since Horde_Alarm 2.2.9 the keys consist of the alarm id concatenated 511 * with a NUL character and an alarm method key. 512 * 513 * @return array Error list. 514 */ 515 public function getErrors() 516 { 517 return $this->_errors; 518 } 519 520 /** 521 * Garbage collects old alarms in the backend. 522 * 523 * @param boolean $force Force garbace collection? If false, GC happens 524 * with a 1% chance. 525 * 526 * @throws Horde_Alarm_Exception 527 */ 528 public function gc($force = false) 529 { 530 /* A 1% chance we will run garbage collection during a call. */ 531 if ($force || rand(0, 99) == 0) { 532 $this->_gc(); 533 } 534 } 535 536 /** 537 * Garbage collects old alarms in the backend. 538 * 539 * @throws Horde_Alarm_Exception 540 */ 541 abstract protected function _gc(); 542 543 /** 544 * Attempts to initialize the backend. 545 * 546 * @throws Horde_Alarm_Exception 547 */ 548 abstract public function initialize(); 549 550 /** 551 * Converts a value from the driver's charset. 552 * 553 * @param mixed $value Value to convert. 554 * 555 * @return mixed Converted value. 556 */ 557 abstract protected function _fromDriver($value); 558 559 /** 560 * Converts a value to the driver's charset. 561 * 562 * @param mixed $value Value to convert. 563 * 564 * @return mixed Converted value. 565 */ 566 abstract protected function _toDriver($value); 567 568} 569