1<?php
2/**
3 * Copyright 2001-2016 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  Notification
12 */
13
14/**
15 * The Horde_Notification package provides a subject-observer pattern for
16 * raising and showing messages of different types and to different listeners.
17 *
18 * @author   Jan Schneider <jan@horde.org>
19 * @category Horde
20 * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
21 * @package  Notification
22 */
23class Horde_Notification_Handler
24{
25    /**
26     * Decorators.
27     *
28     * @var array
29     */
30    protected $_decorators = array();
31
32    /**
33     * Forces immediate attachment of a notification to a listener.
34     *
35     * @var boolean
36     */
37    protected $_forceAttach = false;
38
39    /**
40     * Additional handle definitions.
41     *
42     * @var array
43     */
44    protected $_handles = array(
45        'default' => array(
46            '*' => 'Horde_Notification_Event'
47        )
48    );
49
50    /**
51     * Hash containing all attached listener objects.
52     *
53     * @var array
54     */
55    protected $_listeners = array();
56
57    /**
58     * The storage location where we store the messages.
59     *
60     * @var Horde_Notification_Storage
61     */
62    protected $_storage;
63
64    /**
65     * Initialize the notification system.
66     *
67     * @param Horde_Notification_Storage $storage  The storage object to use.
68     */
69    public function __construct(Horde_Notification_Storage_Interface $storage)
70    {
71        $this->_storage = $storage;
72    }
73
74    /**
75     * Registers a listener with the notification object and includes
76     * the necessary library file dynamically.
77     *
78     * @param string $listener  The name of the listener to attach. These
79     *                          names must be unique; further listeners with
80     *                          the same name will be ignored.
81     * @param array $params     A hash containing any additional configuration
82     *                          or connection parameters a listener driver
83     *                          might need.
84     * @param string $class     The class name from which the driver was
85     *                          instantiated if not the default one. If given
86     *                          you have to include the library file
87     *                          containing this class yourself. This is useful
88     *                          if you want the listener driver to be
89     *                          overriden by an application's implementation
90     *
91     * @return Horde_Notification_Listener  The listener object.
92     * @throws Horde_Exception
93     */
94    public function attach($listener, $params = null, $class = null)
95    {
96        if ($ob = $this->getListener($listener)) {
97            return $ob;
98        }
99
100        if (is_null($class)) {
101            $class = 'Horde_Notification_Listener_' . Horde_String::ucfirst(Horde_String::lower($listener));
102        }
103
104        if (class_exists($class)) {
105            $this->_listeners[$listener] = new $class($params);
106            if (!$this->_storage->exists($listener)) {
107                $this->_storage->set($listener, array());
108            }
109            $this->_addTypes($listener);
110            return $this->_listeners[$listener];
111        }
112
113        throw new Horde_Exception(sprintf('Notification listener %s not found.', $class));
114    }
115
116    /**
117     * Remove a listener from the notification list.
118     *
119     * @param string $listner  The name of the listener to detach.
120     *
121     * @throws Horde_Exception
122     */
123    public function detach($listener)
124    {
125        if ($ob = $this->getListener($listener)) {
126            unset($this->_listeners[$ob->getName()]);
127            $this->_storage->clear($ob->getName());
128        } else {
129            throw new Horde_Exception(sprintf('Notification listener %s not found.', $listener));
130        }
131    }
132
133    /**
134     * Clear any notification events that may exist in a listener.
135     *
136     * @param string $listener  The name of the listener to flush. If null,
137     *                          clears all unattached events.
138     */
139    public function clear($listener = null)
140    {
141        if (is_null($listener)) {
142            $this->_storage->clear('_unattached');
143        } elseif ($ob = $this->getListener($listener)) {
144            $this->_storage->clear($ob->getName());
145        }
146    }
147
148    /**
149     * Returns the current Listener object for a given listener type.
150     *
151     * @param string $type  The listener type.
152     *
153     * @return mixed  A Horde_Notification_Listener object, or null if
154     *                $type listener is not attached.
155     */
156    public function get($type)
157    {
158        foreach ($this->_listeners as $listener) {
159            if ($listener->handles($type)) {
160                return $listener;
161            }
162        }
163
164        return null;
165    }
166
167    /**
168     * Returns a listener object given a listener name.
169     *
170     * @param string $listener  The listener name.
171     *
172     * @return mixed  Either a Horde_Notification_Listener or null.
173     */
174    public function getListener($listener)
175    {
176        $listener = Horde_String::lower(basename($listener));
177        return empty($this->_listeners[$listener])
178            ? null
179            : $this->_listeners[$listener];
180    }
181
182    /**
183     * Adds a type handler to a given Listener.
184     * To change the default listener, use the following:
185     * <pre>
186     *   $ob->addType('default', '*', $classname);
187     * </pre>
188     *
189     * @param string $listener  The listener name.
190     * @param string $type      The listener type.
191     * @param string $class     The Event class to use.
192     */
193    public function addType($listener, $type, $class)
194    {
195        $this->_handles[$listener][$type] = $class;
196
197        if (isset($this->_listeners[$listener])) {
198            $this->_addTypes($listener);
199        }
200    }
201
202    /**
203     * Adds any additional listener types to a given Listener.
204     *
205     * @param string $listener  The listener name.
206     */
207    protected function _addTypes($listener)
208    {
209        if (isset($this->_handles[$listener])) {
210            foreach ($this->_handles[$listener] as $type => $class) {
211                $this->_listeners[$listener]->addType($type, $class);
212            }
213        }
214    }
215
216    /**
217     * Add a decorator.
218     *
219     * @param Horde_Notification_Handler_Decorator_Base $decorator  The
220     *                                                              Decorator
221     *                                                              object.
222     */
223    public function addDecorator(Horde_Notification_Handler_Decorator_Base $decorator)
224    {
225        $this->_decorators[] = $decorator;
226    }
227
228    /**
229     * Add an event to the Horde message stack.
230     *
231     * @param mixed $event    Horde_Notification_Event object or message
232     *                        string.
233     * @param string $type    The type of message.
234     * @param array $flags    Array of optional flags that will be passed to
235     *                        the registered listeners.
236     * @param array $options  Additional options:
237     * <pre>
238     * 'immediate' - (boolean) If true, immediately tries to attach to a
239     *               listener. If no listener exists for this type, the
240     *               message will be dropped.
241     *               DEFAULT: false (message will be attached to available
242     *               handler at the time notify() is called).
243     * </pre>
244     */
245    public function push($event, $type = null, array $flags = array(),
246                         $options = array())
247    {
248        if ($event instanceof Horde_Notification_Event) {
249            $event->flags = $flags;
250            $event->type = $type;
251        } else {
252            $class = (!is_null($type) && ($listener = $this->get($type)))
253                ? $listener->handles($type)
254                : $this->_handles['default']['*'];
255
256            /* Transparently create a Horde_Notification_Event object. */
257            $event = new $class($event, $type, $flags);
258        }
259
260        foreach ($this->_decorators as $decorator) {
261            $decorator->push($event, $options);
262        }
263
264        if (!$this->_forceAttach && empty($options['immediate'])) {
265            $this->_storage->push('_unattached', $event);
266        } else {
267            if ($listener = $this->get($event->type)) {
268                $this->_storage->push($listener->getName(), $event);
269            }
270        }
271    }
272
273    /**
274     * Passes the message stack to all listeners and asks them to
275     * handle their messages.
276     *
277     * @param array $options  An array containing display options for the
278     *                        listeners. Any options not contained in this
279     *                        list will be passed to the listeners.
280     * <pre>
281     * listeners - (array) The list of listeners to notify.
282     * raw - (boolean) If true, does not call the listener's notify()
283     *       function.
284     * </pre>
285     */
286    public function notify(array $options = array())
287    {
288        /* Convert the 'listeners' option into the format expected by the
289         * notification handler. */
290        if (!isset($options['listeners'])) {
291            $listeners = array_keys($this->_listeners);
292        } elseif (!is_array($options['listeners'])) {
293            $listeners = array($options['listeners']);
294        } else {
295            $listeners = $options['listeners'];
296        }
297
298        $events = array();
299        $unattached = $this->_storage->exists('_unattached')
300            ? $this->_storage->get('_unattached')
301            : array();
302
303        /* Pass the message stack to all listeners and asks them to handle
304         * their messages. */
305        foreach ($listeners as $listener) {
306            $listener = Horde_String::lower($listener);
307
308            if (isset($this->_listeners[$listener])) {
309                $instance = $this->_listeners[$listener];
310                $name = $instance->getName();
311
312                foreach (array_keys($unattached) as $val) {
313                    if ($unattached[$val] instanceof Horde_Notification_Event
314                        && $instance->handles($unattached[$val]->type)) {
315                        $this->_storage->push($name, $unattached[$val]);
316                        unset($unattached[$val]);
317                    }
318                }
319
320                foreach ($this->_decorators as $decorator) {
321                    $this->_forceAttach = true;
322                    try {
323                        $decorator->notify($this, $instance);
324                    } catch (Horde_Notification_Exception $e) {
325                        $this->push($e);
326                    }
327                    $this->_forceAttach = false;
328                }
329
330                if (!$this->_storage->exists($name)) {
331                    continue;
332                }
333
334                $tmp = $this->_storage->get($name);
335                if (empty($options['raw'])) {
336                    $instance->notify($tmp, $options);
337                }
338                $this->_storage->clear($name);
339
340                $events = array_merge($events, $tmp);
341            }
342        }
343
344        if (empty($unattached)) {
345            $this->_storage->clear('_unattached');
346        } else {
347            $this->_storage->set('_unattached', $unattached);
348        }
349
350        return $events;
351    }
352
353    /**
354     * Return the number of notification messages in the stack.
355     *
356     * @author David Ulevitch <davidu@everydns.net>
357     *
358     * @param string $my_listener  The name of the listener.
359     *
360     * @return integer  The number of messages in the stack.
361     */
362    public function count($my_listener = null)
363    {
364        $count = 0;
365
366        if (!is_null($my_listener)) {
367            if ($ob = $this->get($my_listener)) {
368                $count = count($this->_storage->get($ob->getName()));
369
370                if ($this->_storage->exists('_unattached')) {
371                    foreach ($this->_storage->get('_unattached') as $val) {
372                        if ($ob->handles($val->type)) {
373                            ++$count;
374                        }
375                    }
376                }
377            }
378        } else {
379            if ($this->_storage->exists('_unattached')) {
380                $count = count($this->_storage->get('_unattached'));
381            }
382
383            foreach ($this->_listeners as $val) {
384                if ($this->_storage->exists($val->getName())) {
385                    $count += count($this->_storage->get($val->getName()));
386                }
387            }
388        }
389
390        return $count;
391    }
392
393}
394