Overview

Packages

  • Jonah
  • None

Classes

  • Jonah
  • Jonah_Api
  • Jonah_Block_Latest
  • Jonah_Driver
  • Jonah_Driver_Sql
  • Jonah_Exception
  • Jonah_Factory_Driver
  • Jonah_Form_Feed
  • Jonah_Form_Story
  • Jonah_Test
  • Jonah_View_ChannelDelete
  • Jonah_View_ChannelEdit
  • Jonah_View_ChannelList
  • Jonah_View_StoryDelete
  • Jonah_View_StoryEdit
  • Jonah_View_StoryList
  • Jonah_View_StoryPdf
  • Jonah_View_StoryView
  • Jonah_View_TagSearchList
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Jonah_Driver:: is responsible for storing, searching, sorting and filtering
  4:  * locally generated and managed articles.  Aggregation is left to Hippo.
  5:  *
  6:  * Copyright 2002-2012 Horde LLC (http://www.horde.org/)
  7:  *
  8:  * See the enclosed file LICENSE for license information (BSD). If you did not
  9:  * did not receive this file, see http://cvs.horde.org/co.php/jonah/LICENSE.
 10:  *
 11:  * @author  Chuck Hagenbuch <chuck@horde.org>
 12:  * @author  Marko Djukic <marko@oblo.com>
 13:  * @author  Jan Schneider <jan@horde.org>
 14:  * @author  Ben Klang <ben@alkaloid.net>
 15:  * @author  Michael J. Rubinsky <mrubinsk@horde.org>
 16:  * @package Jonah
 17:  */
 18: class Jonah_Driver
 19: {
 20:     /**
 21:      * Hash containing connection parameters.
 22:      *
 23:      * @var array
 24:      */
 25:     protected $_params = array();
 26: 
 27:     /**
 28:      * Constructs a new Driver storage object.
 29:      *
 30:      * @param array $params  A hash containing connection parameters.
 31:      */
 32:     public function __construct($params = array())
 33:     {
 34:         $this->_params = $params;
 35:     }
 36: 
 37:     /**
 38:      * Remove a channel from storage.
 39:      *
 40:      * @param array $info  A channel info array. (@TODO: Look at passing just
 41:      *                     the id?)
 42:      */
 43:     public function deleteChannel($info)
 44:     {
 45:         return $this->_deleteChannel($info['channel_id']);
 46:     }
 47: 
 48:     /**
 49:      * Fetches the requested channel, while actually passing on the request to
 50:      * the backend _getChannel() function to do the real work.
 51:      *
 52:      * @param integer $channel_id  The channel id to fetch.
 53:      *
 54:      * @return array  The channel details as an array
 55:      * @throws InvalidArgumentException
 56:      */
 57:     public function getChannel($channel_id)
 58:     {
 59:         static $channel = array();
 60: 
 61:         /* We need a non empty channel id. */
 62:         if (empty($channel_id)) {
 63:             throw new InvalidArgumentException(_("Missing channel id."));
 64:         }
 65: 
 66:         /* Cache the fetching of channels. */
 67:         if (!isset($channel[$channel_id])) {
 68:             $channel[$channel_id] = $this->_getChannel($channel_id);
 69:             if (empty($channel[$channel_id]['channel_link'])) {
 70:                 $channel[$channel_id]['channel_official'] =
 71:                     Horde::url('delivery/html.php', true, -1)->add('channel_id', $channel_id)->setRaw(false);
 72:             } else {
 73:                 $channel[$channel_id]['channel_official'] = str_replace(array('%25c', '%c'), array('%c', $channel_id), $channel[$channel_id]['channel_link']);
 74:             }
 75: 
 76:         }
 77: 
 78:         return $channel[$channel_id];
 79:     }
 80: 
 81:     /**
 82:      * Returns the most recent or all stories from a channel.
 83:      *
 84:      * @param integer $criteria    An associative array of attributes on which
 85:      *                             the resulting stories should be filtered.
 86:      *                             Examples:
 87:      *                             'channel' => string Channel slug
 88:      *                             'channel_id' => int Channel ID
 89:      *                             'author' => string Story author
 90:      *                             'updated-min' => Horde_Date Only return
 91:      *                                                         stories updated
 92:      *                                                         on or after this
 93:      *                                                         date
 94:      *                             'updated-max' => Horde_Date Only return
 95:      *                                                         stories updated
 96:      *                                                         on or before this
 97:      *                                                         date
 98:      *                             'published-min' => Horde_Date Only return
 99:      *                                                           stories
100:      *                                                           published on or
101:      *                                                           after this date
102:      *                             'published-max' => Horde_Date Only return
103:      *                                                           stories
104:      *                                                           published on or
105:      *                                                           before date
106:      *                             'tags' => array Array of tag names ANY of
107:      *                                             which may match the story to
108:      *                                             be included
109:      *                             'alltags' => array Array of tag names ALL of
110:      *                                                which must be associated
111:      *                                                with the story to be
112:      *                                                included
113:      *                             'keywords' => array Array of strings ALL of
114:      *                                                 which matching must
115:      *                                                 include
116:      *                             'published' => boolean Whether to return only
117:      *                                                    published stories;
118:      *                                                    null will return both
119:      *                                                    published and
120:      *                                                    unpublished
121:      *                             'startnumber' => int Story number to begin
122:      *                             'endnumber' => int Story number to end
123:      *                             'limit' => int Max number of stories
124:      *
125:      * @param integer $order       How to order the results for internal
126:      *                             channels. Possible values are the
127:      *                             Jonah::ORDER_* constants.
128:      *
129:      * @return array  The specified number (or less, if there are fewer) of
130:      *                stories from the given channel.
131:      * @throws InvalidArgumentException
132:      */
133:     public function getStories($criteria, $order = Jonah::ORDER_PUBLISHED)
134:     {
135:         // Convert a channel slug into a channel ID if necessary
136:         if (isset($criteria['channel']) && !isset($criteria['channel_id'])) {
137:             $criteria['channel_id'] = $this->getIdBySlug($criteria['channel']);
138:         }
139: 
140:         // Validate that we have proper Horde_Date objects
141:         if (isset($criteria['updated-min'])) {
142:             if (!is_a($criteria['updated-min'], 'Horde_Date')) {
143:                 throw new InvalidArgumentException("Invalid date object provided for update start date.");
144:             }
145:         }
146:         if (isset($criteria['updated-max'])) {
147:             if (!is_a($criteria['updated-max'], 'Horde_Date')) {
148:                 throw new InvalidArgumentException("Invalid date object provided for update end date.");
149:             }
150:         }
151:         if (isset($criteria['published-min'])) {
152:             if (!is_a($criteria['published-min'], 'Horde_Date')) {
153:                 throw new InvalidArgumentException("Invalid date object provided for published start date.");
154:             }
155:         }
156:         if (isset($criteria['published-max'])) {
157:             if (!is_a($criteria['published-max'], 'Horde_Date')) {
158:                 throw new InvalidArgumentException("Invalid date object provided for published end date.");
159:             }
160:         }
161: 
162:         // Collect the applicable tag IDs
163:         $criteria['tagIDs'] = array();
164:         if (isset($criteria['tags'])) {
165:             $criteria['tagIDs'] = array_merge($criteria['tagIDs'], $this->getTagIds($criteria['tags']));
166:         }
167:         if (isset($criteria['alltags'])) {
168:             $criteria['tagIDs'] = array_merge($criteria['tagIDs'], $this->getTagIds($criteria['alltags']));
169:         }
170: 
171:         return $this->_getStories($criteria, $order);
172:     }
173: 
174:     /**
175:      * Save the provided story to storage.
176:      *
177:      * @param array $info  The story information array. Passed by reference so
178:      *                     we can add/change the id when saved.
179:      */
180:     public function saveStory(&$info)
181:     {
182:         $this->_saveStory($info);
183:     }
184: 
185:     /**
186:      * Retrieve the requested story from storage.
187:      *
188:      * @param integer $channel_id  The channel id to obtain story from.
189:      * @param integer $story_id    The story id to obtain.
190:      * @param boolean $read        Increment the read counter?
191:      *
192:      * @return array  The story information array
193:      */
194:     public function getStory($channel_id, $story_id, $read = false)
195:     {
196:         $channel = $this->getChannel($channel_id);
197:         $story = $this->_getStory($story_id, $read);
198: 
199:         /* Format story link. */
200:         $story['link'] = $this->getStoryLink($channel, $story);
201: 
202:         /* Format dates. */
203:         $date_format = $GLOBALS['prefs']->getValue('date_format');
204:         $story['updated_date'] = strftime($date_format, $story['updated']);
205:         if (!empty($story['published'])) {
206:             $story['published_date'] = strftime($date_format, $story['published']);
207:         }
208: 
209:         return $story;
210:     }
211: 
212:     /**
213:      * Returns the official link to a story.
214:      *
215:      * @param array $channel  A channel hash.
216:      * @param array $story    A story hash.
217:      *
218:      * @return Horde_Url  The story link.
219:      */
220:     public function getStoryLink($channel, $story)
221:     {
222:         if (!empty($story['url']) && empty($story['body'])) {
223:             $url = $story['url'];
224:         } elseif ((empty($story['url']) || !empty($story['body'])) &&
225:             !empty($channel['channel_story_url'])) {
226:             $url = $channel['channel_story_url'];
227:         } else {
228:             $url = Horde::url('stories/view.php', true, -1)->add(array('channel_id' => '%c', 'id' => '%s'))->setRaw(false);
229:         }
230:         return new Horde_Url(str_replace(array('%25c', '%25s', '%c', '%s'),
231:                                          array('%c', '%s', $channel['channel_id'], $story['id']),
232:                                          $url));
233:     }
234: 
235:     /**
236:      */
237:     public function getChecksum($story)
238:     {
239:         return md5($story['title'] . $story['description']);
240:     }
241: 
242:     /**
243:      */
244:     public function getIntervalLabel($seconds = null)
245:     {
246:         $interval = array(1 => _("none"),
247:                           1800 => _("30 mins"),
248:                           3600 => _("1 hour"),
249:                           7200 => _("2 hours"),
250:                           14400 => _("4 hours"),
251:                           28800 => _("8 hours"),
252:                           43200 => _("12 hours"),
253:                           86400 => _("24 hours"));
254: 
255:         if ($seconds === null) {
256:             return $interval;
257:         } else {
258:             return $interval[$seconds];
259:         }
260:     }
261: 
262:     /**
263:      * Returns the stories of a channel rendered with the specified template.
264:      *
265:      * @param integer $channel_id  The news channel to get stories from.
266:      * @param string  $tpl         The name of the template to use.
267:      * @param integer $max         The maximum number of stories to get. If
268:      *                             null, all stories will be returned.
269:      * @param integer $from        The number of the story to start with.
270:      * @param integer $order       How to sort the results for internal channels
271:      *                             Possible values are the Jonah::ORDER_*
272:      *                             constants.
273:      *
274:      * @TODO: This doesn't belong in a storage driver class. Move it to a
275:      * view or possible a static method in Jonah::?
276:      *
277:      * @return string  The rendered story listing.
278:      */
279:     public function renderChannel($channel_id, $tpl, $max = 10, $from = 0, $order = Jonah::ORDER_PUBLISHED)
280:     {
281:         $channel = $this->getChannel($channel_id);
282: 
283:         $templates = Horde::loadConfiguration('templates.php', 'templates', 'jonah');
284:         $escape = !isset($templates[$tpl]['escape']) || !empty($templates[$tpl]['escape']);
285:         $template = new Horde_Template();
286: 
287:         if ($escape) {
288:             $channel['channel_name'] = htmlspecialchars($channel['channel_name']);
289:             $channel['channel_desc'] = htmlspecialchars($channel['channel_desc']);
290:         }
291:         $template->set('channel', $channel, true);
292: 
293:         /* Get one story more than requested to see if there are more stories. */
294:         if ($max !== null) {
295:             $stories = $this->getStories(
296:                     array('channel_id' => $channel_id,
297:                           'published' => true,
298:                           'startnumber' => $from,
299:                           'limit' => $max),
300:                     $order);
301:         } else {
302:             $stories = $this->getStories(array('channel_id' => $channel_id,
303:                                                'published' => true),
304:                                          $order);
305:             $max = count($stories);
306:         }
307: 
308:         if (!$stories) {
309:             $template->set('error', _("No stories are currently available."), true);
310:             $template->set('stories', false, true);
311:             $template->set('image', false, true);
312:             $template->set('form', false, true);
313:         } else {
314:             /* Escape. */
315:             if ($escape) {
316:                 array_walk($stories, array($this, '_escapeStories'));
317:             }
318: 
319:             /* Process story summaries. */
320:             array_walk($stories, array($this, '_escapeStoryDescriptions'));
321: 
322:             $template->set('error', false, true);
323:             $template->set('story_marker', Horde::img('story_marker.png'));
324:             $template->set('image', false, true);
325:             $template->set('form', false, true);
326:             if ($from) {
327:                 $template->set('previous', max(0, $from - $max), true);
328:             } else {
329:                 $template->set('previous', false, true);
330:             }
331:             if ($from && !empty($channel['channel_page_link'])) {
332:                 $template->set('previous_link',
333:                                str_replace(
334:                                    array('%25c', '%25n', '%c', '%n'),
335:                                    array('%c', '%n', $channel['channel_id'], max(0, $from - $max)),
336:                                    $channel['channel_page_link']),
337:                                true);
338:             } else {
339:                 $template->set('previous_link', false, true);
340:             }
341:             $more = count($stories) > $max;
342:             if ($more) {
343:                 $template->set('next', $from + $max, true);
344:                 array_pop($stories);
345:             } else {
346:                 $template->set('next', false, true);
347:             }
348:             if ($more && !empty($channel['channel_page_link'])) {
349:                 $template->set('next_link',
350:                                str_replace(
351:                                    array('%25c', '%25n', '%c', '%n'),
352:                                    array('%c', '%n', $channel['channel_id'], $from + $max),
353:                                    $channel['channel_page_link']),
354:                                true);
355:             } else {
356:                 $template->set('next_link', false, true);
357:             }
358: 
359:             $template->set('stories', $stories, true);
360:         }
361: 
362:         return $template->parse($templates[$tpl]['template']);
363:     }
364: 
365:     /**
366:      * @TODO: Move to a view class or static Jonah:: method?
367:      */
368:     protected function _escapeStories(&$value, $key)
369:     {
370:         $value['title'] = htmlspecialchars($value['title']);
371:         $value['description'] = htmlspecialchars($value['description']);
372:         if (isset($value['link'])) {
373:             $value['link'] = htmlspecialchars($value['link']);
374:         }
375:         if (empty($value['body_type']) || $value['body_type'] != 'richtext') {
376:             $value['body'] = htmlspecialchars($value['body']);
377:         }
378:     }
379: 
380:     /**
381:      * @TODO: Move to a view class or static Jonah:: method?
382:      */
383:     protected function _escapeStoryDescriptions(&$value, $key)
384:     {
385:         $value['description'] = nl2br($value['description']);
386:     }
387: 
388:     /**
389:      * Returns the provided story as a MIME part.
390:      *
391:      * @param array $story  A data array representing a story.
392:      *
393:      * @return MIME_Part  The MIME message part containing the story parts.
394:      * @TODO: Refactor to use new Horde MIME library
395:      */
396:     protected function getStoryAsMessage($story)
397:     {
398:         require_once 'Horde/MIME/Part.php';
399: 
400:         /* Add the story to the message based on the story's body type. */
401:         switch ($story['body_type']) {
402:         case 'richtext':
403:             /* Get a plain text version of a richtext story. */
404:             $body_html = $story['body'];
405:             $body_text = $GLOBALS['injector']->getInstance('Horde_Core_Factory_TextFilter')->filter($body_html, 'html2text');
406: 
407:             /* Add description. */
408:             $body_html = '<p>' . $GLOBALS['injector']->getInstance('Horde_Core_Factory_TextFilter')->filter($story['desc'], 'text2html', array('parselevel' => Horde_Text_Filter_Text2html::MICRO, 'callback' => null)) . "</p>\n" . $body_html;
409:             $body_text = Horde_String::wrap('  ' . $story['description'], 70) . "\n\n" . $body_text;
410: 
411:             /* Add the text version of the story to the base message. */
412:             $message_text = new MIME_Part('text/plain');
413:             $message_text->setCharset('UTF-8');
414:             $message_text->setContents($message_text->replaceEOL($body_text));
415:             $message_text->setDescription(_("Plaintext Version of Story"));
416: 
417:             /* Add an HTML version of the story to the base message. */
418:             $message_html = new MIME_Part('text/html', Horde_String::wrap($body_html),
419:                                           'UTF-8', 'inline');
420:             $message_html->setDescription(_("HTML Version of Story"));
421: 
422:             /* Add the two parts as multipart/alternative. */
423:             $basepart = new MIME_Part('multipart/alternative');
424:             $basepart->addPart($message_text);
425:             $basepart->addPart($message_html);
426: 
427:             return $basepart;
428: 
429:         case 'text':
430:             /* This is just a plain text story. */
431:             $message_text = new MIME_Part('text/plain');
432:             $message_text->setContents($message_text->replaceEOL($story['description'] . "\n\n" . $story['body']));
433:             $message_text->setCharset('UTF-8');
434: 
435:             return $message_text;
436:         }
437:     }
438: 
439:     /**
440:      * Stubs for the tag functions. If supported by the backend, these need
441:      * to be implemented in the concrete Jonah_Driver_* class.
442:      *
443:      * @TODO: These will be moved to a new Tagger class and will interface
444:      * with the Content_Tagger api.
445:      */
446:     function writeTags($resource_id, $channel_id, $tags)
447:     {
448:         return PEAR::raiseError(_("Tag support not enabled in backend."));
449:     }
450: 
451:     function readTags($resource_id)
452:     {
453:         return PEAR::raiseError(_("Tag support not enabled in backend."));
454:     }
455: 
456:     function listTagInfo($tags = array(), $channel_id = null)
457:     {
458:         return PEAR::raiseError(_("Tag support not enabled in backend."));
459:     }
460: 
461:     function searchTagsById($ids, $max = 10, $from = 0, $channel_id = array(),
462:                             $order = Jonah::ORDER_PUBLISHED)
463:     {
464:         return PEAR::raiseError(_("Tag support not enabled in backend."));
465:     }
466: 
467:     function getTagNames($ids)
468:     {
469:         return PEAR::raiseError(_("Tag support not enabled in backend."));
470:     }
471: 
472:     function getIdBySlug($channel)
473:     {
474:         return $this->_getIdBySlug($channel);
475:     }
476: 
477: }
478: 
API documentation generated by ApiGen