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