Overview

Packages

  • Agora
  • None

Classes

  • Agora
  • Agora_Api
  • Agora_Driver
  • Agora_Driver_SplitSql
  • Agora_Driver_Sql
  • Agora_Exception
  • Agora_Factory_Driver
  • Agora_Form_Forum
  • Agora_Form_Message
  • Agora_Form_Search
  • Agora_View
  • Horde_Form_Renderer_MessageForm
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /**
   3:  * Agora_Driver:: provides the functions to access both threads and
   4:  * individual messages.
   5:  *
   6:  * Copyright 2003-2012 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  Marko Djukic <marko@oblo.com>
  12:  * @author  Jan Schneider <jan@horde.org>
  13:  * @author  Duck <duck@obala.net>
  14:  * @package Agora
  15:  */
  16: class Agora_Driver {
  17: 
  18:     /**
  19:      * Charset
  20:      *
  21:      * @var string
  22:      */
  23:     protected $_charset;
  24: 
  25:     /**
  26:      * The database connection object.
  27:      *
  28:      * @var Horde_Db_Adapter
  29:      */
  30:     protected $_db;
  31: 
  32:     /**
  33:      * The forums scope.
  34:      *
  35:      * @var string
  36:      */
  37:     protected $_scope;
  38: 
  39:     /**
  40:      * Current forum data
  41:      *
  42:      * @var array
  43:      */
  44:     public $_forum;
  45: 
  46:     /**
  47:      * Current forum ID
  48:      *
  49:      * @var string
  50:      */
  51:     public $_forum_id;
  52: 
  53:     /**
  54:      * Scope theads table name
  55:      *
  56:      * @var string
  57:      */
  58:     protected $_threads_table = 'agora_messages';
  59: 
  60:     /**
  61:      * Scope theads table name
  62:      *
  63:      * @var string
  64:      */
  65:     protected $_forums_table = 'agora_forums';
  66: 
  67:     /**
  68:      * Cache object
  69:      *
  70:      * @var Horde_Cache
  71:      */
  72:     protected $_cache;
  73: 
  74:     /**
  75:      * Constructor
  76:      */
  77:     public function __construct($scope, $params)
  78:     {
  79:         if (empty($params['db'])) {
  80:             throw new InvalidArgumentException('Missing required connection parameter(s).');
  81:         }
  82: 
  83:         /* Set parameters. */
  84:         $this->_scope = $scope;
  85:         $this->_db = $params['db'];
  86:         $this->_charset = $params['charset'];
  87: 
  88:         /* Initialize the Cache object. */
  89:         $this->_cache = $GLOBALS['injector']->getInstance('Horde_Cache');
  90:     }
  91: 
  92:     /**
  93:      * Checks if attachments are allowed in messages for the current forum.
  94:      *
  95:      * @return boolean  Whether attachments allowed or not.
  96:      */
  97:     public function allowAttachments()
  98:     {
  99:         return ($GLOBALS['conf']['forums']['enable_attachments'] == '1' ||
 100:                 ($GLOBALS['conf']['forums']['enable_attachments'] == '0' &&
 101:                  $this->_forum['forum_attachments']));
 102:     }
 103: 
 104:     /**
 105:      * Saves the message.
 106:      *
 107:      * @param array $info  Array containing all the message data to save.
 108:      *
 109:      * @return mixed  Message ID on success or PEAR_Error on failure.
 110:      * @throws Agora_Exception
 111:      */
 112:     public function saveMessage($info)
 113:     {
 114:         /* Check if the thread is locked before changing anything. */
 115:         if ($info['message_parent_id'] &&
 116:             $this->isThreadLocked($info['message_parent_id'])) {
 117:             return PEAR::raiseError(_("This thread has been locked."));
 118:         }
 119: 
 120:         /* Check post permissions. */
 121:         if (!$this->hasPermission(Horde_Perms::EDIT)) {
 122:             return PEAR::raiseError(sprintf(_("You don't have permission to post messages in forum %s."), $this->_forum_id));
 123:         }
 124: 
 125:         if (empty($info['message_id'])) {
 126:             /* Get thread parents */
 127:             // TODO message_thread is always parent root, probably can use it here.
 128:             if ($info['message_parent_id'] > 0) {
 129:                 $parents = $this->_db->selectValue('SELECT parents FROM ' . $this->_threads_table . ' WHERE message_id = ?',
 130:                                                     array($info['message_parent_id']));
 131:                 $info['parents'] = $parents . ':' . $info['message_parent_id'];
 132:                 $info['message_thread'] = $this->getThreadRoot($info['message_parent_id']);
 133:             } else {
 134:                 $info['parents'] = '';
 135:                 $info['message_thread'] = 0;
 136:             }
 137: 
 138:             /* Create new message */
 139:             $sql = 'INSERT INTO ' . $this->_threads_table
 140:                 . ' (forum_id, message_thread, parents, '
 141:                 . 'message_author, message_subject, body, attachments, '
 142:                 . 'message_timestamp, message_modifystamp, ip) '
 143:                 . ' VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)';
 144: 
 145:             $author = $GLOBALS['registry']->getAuth() ? $GLOBALS['registry']->getAuth() : $info['posted_by'];
 146:             $values = array($this->_forum_id,
 147:                             $info['message_thread'],
 148:                             $info['parents'],
 149:                             $author,
 150:                             $this->convertToDriver($info['message_subject']),
 151:                             $this->convertToDriver($info['message_body']),
 152:                             $_SERVER['REQUEST_TIME'],
 153:                             $_SERVER['REQUEST_TIME'],
 154:                             $_SERVER['REMOTE_ADDR']);
 155: 
 156:             try {
 157:                 $info['message_id'] = $this->_db->insert($sql, $values);
 158:             } catch (Horde_Db_Exception $e) {
 159:                 throw new Agora_Exception($e->getMessage());
 160:             }
 161: 
 162:             /* Update last message in forum, but only if it is not moderated */
 163:             if (!$this->_forum['forum_moderated']) {
 164:                 // Send the new post to the distribution address
 165:                 if ($this->_forum['forum_distribution_address']) {
 166:                     Agora::distribute($info['message_id']);
 167:                 }
 168:                 /* Update cached message/thread counts and last poster */
 169:                 $this->_lastInForum($this->_forum_id, $info['message_id'], $author, $_SERVER['REQUEST_TIME']);
 170:                 $this->_forumSequence($this->_forum_id, 'message', '+');
 171:                 if ($info['message_thread']) {
 172:                     $this->_sequence($info['message_thread'], '+');
 173:                     $this->_lastInThread($info['message_thread'], $info['message_id'], $author, $_SERVER['REQUEST_TIME']);
 174:                 } else {
 175:                     $this->_forumSequence($this->_forum_id, 'thread', '+');
 176:                 }
 177:             }
 178: 
 179:         } else {
 180:             // TODO clearing cache for editing doesn't work
 181:             /* Update message data */
 182:             $sql = 'UPDATE ' . $this->_threads_table . ' SET ' .
 183:                    'message_subject = ?, body = ?, message_modifystamp = ? WHERE message_id = ?';
 184:             $values = array($this->convertToDriver($info['message_subject']),
 185:                             $this->convertToDriver($info['message_body']),
 186:                             $_SERVER['REQUEST_TIME'],
 187:                             $info['message_id']);
 188: 
 189:             try {
 190:                 $this->_db->execute($sql, $values);
 191:             } catch (Horde_Db_Exception $e) {
 192:                 throw new Agora_Exception($e->getMessage());
 193:             }
 194: 
 195:             /* Get message thread for cache expiration */
 196:             $info['message_thread'] = $this->getThreadRoot($info['message_id']);
 197:         }
 198: 
 199:         /* Handle attachment saves or deletions. */
 200:         if (!empty($info['message_attachment']) ||
 201:             !empty($info['attachment_delete'])) {
 202:             $vfs = Agora::getVFS();
 203:             if ($vfs instanceof PEAR_Error) {
 204:                 return $vfs;
 205:             }
 206:             $vfs_dir = Agora::VFS_PATH . $this->_forum_id . '/' . $info['message_id'];
 207: 
 208:             /* Check if delete requested or new attachment loaded, and delete
 209:              * any existing one. */
 210:             if (!empty($info['attachment_delete'])) {
 211:                 $sql = 'SELECT file_id FROM agore_files WHERE message_id = ?';
 212:                 foreach ($this->_db->selectValues($sql, array($info['message_id'])) as $file_id) {
 213:                     if ($vfs->exists($vfs_dir, $file_id)) {
 214:                         $delete = $vfs->deleteFile($vfs_dir, $file_id);
 215:                         if ($delete instanceof PEAR_Error) {
 216:                             return $delete;
 217:                         }
 218:                     }
 219:                 }
 220:                 try {
 221:                     $this->_db->execute('DELETE FROM agore_files WHERE message_id = ?', array($info['message_id']));
 222:                 } catch (Horde_Db_Exception $e) {
 223:                     throw new Agora_Exception($e->getMessage());
 224:                 }
 225:                 $attachments = 0;
 226:             }
 227: 
 228:             /* Save new attachment information. */
 229:             if (!empty($info['message_attachment'])) {
 230:                 $file_sql = 'INSERT INTO agora_files (file_name, file_type, file_size, message_id) VALUES (?, ?, ?, ?)';
 231:                 $file_data = array($info['message_attachment']['name'],
 232:                                    $info['message_attachment']['type'],
 233:                                    $info['message_attachment']['size'],
 234:                                    $info['message_id']);
 235: 
 236:                 try {
 237:                     $file_id = $this->_db->insert($file_sql, $file_data);
 238:                 } catch (Horde_Db_Exception $e) {
 239:                     throw new Agora_Exception($e->getMessage());
 240:                 }
 241: 
 242:                 $result = $vfs->write($vfs_dir, $file_id, $info['message_attachment']['file'], true);
 243:                 if ($result instanceof PEAR_Error) {
 244:                     return $result;
 245:                 }
 246:                 $attachments = 1;
 247:             }
 248: 
 249:             $sql = 'UPDATE ' . $this->_threads_table . ' SET attachments = ? WHERE message_id = ?';
 250:             try {
 251:                 $this->_db->execute($sql, array($attachments, $info['message_id']));
 252:             } catch (Horde_Db_Exception $e) {
 253:                 throw new Agora_Exception($e->getMessage());
 254:             }
 255:         }
 256: 
 257:         /* Update cache */
 258:         $this->_updateCacheState($info['message_thread']);
 259: 
 260:         return $info['message_id'];
 261:     }
 262: 
 263:     /**
 264:      * Moves a thread to another forum.
 265:      *
 266:      * @todo Update the number of messages in the old/new forum
 267:      *
 268:      * @param integer $thread_id  The ID of the thread to move.
 269:      * @param integer $forum_id   The ID of the destination forum.
 270:      *
 271:      * @throws Agora_Exception
 272:      */
 273:     public function moveThread($thread_id, $forum_id)
 274:     {
 275:         $sql = 'SELECT forum_id FROM ' . $this->_threads_table . ' WHERE message_id = ?';
 276:         try {
 277:             $old_forum = $this->_db->selectValue($sql, array($thread_id));
 278:         } catch (Horde_Db_Exception $e) {
 279:             throw new Agora_Exception($e->getMessage());
 280:         }
 281: 
 282:         $sql = 'UPDATE ' . $this->_threads_table . ' SET forum_id = ? WHERE message_thread = ? OR message_id = ?';
 283:         try {
 284:             $this->_db->execute($sql, array($forum_id, $thread_id, $thread_id));
 285:         } catch (Horde_Db_Exception $e) {
 286:             throw new Agora_Exception($e->getMessage());
 287:         }
 288: 
 289:         $this->_forumSequence($old_forum, 'thread', '-');
 290:         $this->_forumSequence($forum_id, 'thread', '+');
 291: 
 292:         /* Update last message */
 293:         $this->_lastInForum($old_forum);
 294:         $this->_lastInForum($forum_id);
 295: 
 296:         /* Update cache */
 297:         $this->_updateCacheState($thread_id);
 298: 
 299:         return true;
 300:     }
 301: 
 302:     /**
 303:      * Splits a thread on message id.
 304:      *
 305:      * @param integer $message_id  The ID of the message to split at.
 306:      *
 307:      * @throws Agora_Exception
 308:      */
 309:     public function splitThread($message_id)
 310:     {
 311:         $sql = 'SELECT message_thread FROM ' . $this->_threads_table . ' WHERE message_id = ?';
 312:         try {
 313:             $thread_id = $this->_db->selectValue($sql, array($message_id));
 314:         } catch (Horde_Db_Exception $e) {
 315:             throw new Agora_Exception($e->getMessage());
 316:         }
 317: 
 318:         $sql = 'UPDATE ' . $this->_threads_table . ' SET message_thread = ?, parents = ? WHERE message_id = ?';
 319:         try {
 320:             $this->_db->execute($sql, array(0, '', $message_id));
 321:         } catch (Horde_Db_Exception $e) {
 322:             throw new Agora_Exception($e->getMessage());
 323:         }
 324: 
 325:         $sql = 'SELECT message_thread, parents, message_id FROM ' . $this->_threads_table . ' WHERE parents LIKE ?';
 326:         try {
 327:             $children = $this->_db->selectAll($sql, array(":$thread_id:%$message_id%"));
 328:         } catch (Horde_Db_Exception $e) {
 329:             throw new Agora_Exception($e->getMessage());
 330:         }
 331: 
 332:         if (!empty($children)) {
 333:             $pos = strpos($children[0]['parents'], ':' . $message_id);
 334:             foreach ($children as $i => $message) {
 335:                 $children[$i]['message_thread'] = (int)$message_id;
 336:                 $children[$i]['parents'] = substr($message['parents'], $pos);
 337:             }
 338: 
 339:             $sql = 'UPDATE ' . $this->_threads_table . ' SET message_thread = ?, parents = ? WHERE message_id = ?';
 340:             try {
 341:                 $this->_db->execute($sql, $children);
 342:             } catch (Horde_Db_Exception $e) {
 343:                 throw new Agora_Exception($e->getMessage());
 344:             }
 345:         }
 346: 
 347:         /* Update count on old thread */
 348:         $count = $this->countThreads($thread_id);
 349:         $sql = 'UPDATE ' . $this->_threads_table . ' SET message_seq = ? WHERE message_id = ?';
 350:         try {
 351:             $this->_db->execute($sql, array($count, $thread_id));
 352:         } catch (Horde_Db_Exception $e) {
 353:             throw new Agora_Exception($e->getMessage());
 354:         }
 355: 
 356:         /* Update count on new thread */
 357:         $count = $this->countThreads($message_id);
 358:         $sql = 'UPDATE ' . $this->_threads_table . ' SET message_seq = ? WHERE message_id = ?';
 359:         try {
 360:             $this->_db->execute($sql, array($count, $message_id));
 361:         } catch (Horde_Db_Exception $e) {
 362:             throw new Agora_Exception($e->getMessage());
 363:         }
 364: 
 365:         /* Update last message */
 366:         $this->_lastInForum($this->_forum_id);
 367:         $this->_lastInThread($thread_id);
 368:         $this->_lastInThread($message_id);
 369: 
 370:         $this->_forumSequence($this->_forum_id, 'thread', '+');
 371: 
 372:         /* Update cache */
 373:         $this->_updateCacheState($thread_id);
 374:     }
 375: 
 376:     /**
 377:      * Merges two threads.
 378:      *
 379:      * @param integer $thread_id   The ID of the thread to merge.
 380:      * @param integer $message_id  The ID of the message to merge to.
 381:      *
 382:      * @throws Agora_Exception
 383:      */
 384:     public function mergeThread($thread_from, $message_id)
 385:     {
 386:         $sql = 'SELECT message_thread, parents FROM ' . $this->_threads_table . ' WHERE message_id = ?';
 387:         try {
 388:             $destination = $this->_db->selectOne($sql, array($message_id));
 389:         } catch (Horde_Db_Exception $e) {
 390:             throw new Agora_Exception($e->getMessage());
 391:         }
 392: 
 393:         /* Merge to the top level */
 394:         if ($destination['message_thread'] == 0) {
 395:             $destination['message_thread'] = $message_id;
 396:         }
 397: 
 398:         $sql = 'SELECT message_thread, parents, message_id FROM ' . $this->_threads_table . ' WHERE message_id = ? OR message_thread = ?';
 399:         try {
 400:             $children = $this->_db->selectAll($sql, array($thread_from, $thread_from));
 401:         } catch (Horde_Db_Exception $e) {
 402:             throw new Agora_Exception($e->getMessage());
 403:         }
 404: 
 405:         /* TODO: merging more than one message breaks parent/child relations,
 406:          * also merging to deeper level than thread root doesn't work. */
 407:         if (!empty($children)) {
 408:             $sql = 'UPDATE ' . $this->_threads_table . ' SET message_thread = ?, parents = ? WHERE message_id = ?';
 409: 
 410:             foreach ($children as $i => $message) {
 411:                 $children[$i]['message_thread'] = $destination['message_thread'];
 412:                 if (!empty($destination['parents'])) {
 413:                     $children[$i]['parents'] = $destination['parents'] . $message['parents'];
 414:                 } else {
 415:                     $children[$i]['parents'] = ':' . $message_id;
 416:                 }
 417: 
 418:                 try {
 419:                     $this->_db->execute($sql, $children[$i]);
 420:                 } catch (Horde_Db_Exception $e) {
 421:                     throw new Agora_Exception($e->getMessage());
 422:                 }
 423:             }
 424:         }
 425: 
 426:         $count = $this->countThreads($destination['message_thread']);
 427:         $sql = 'UPDATE ' . $this->_threads_table . ' SET message_seq = ? WHERE message_id = ?';
 428:         try {
 429:             $this->_db->execute($sql, array($count, $destination['message_thread']));
 430:         } catch (Horde_Db_Exception $e) {
 431:             throw new Agora_Exception($e->getMessage());
 432:         }
 433: 
 434:         /* Update last message */
 435:         $this->_lastInForum($this->_forum_id);
 436:         $this->_lastInThread($destination['message_thread']);
 437: 
 438:         $this->_forumSequence($this->_forum_id, 'thread', '-');
 439: 
 440:         /* Update cache */
 441:         $this->_updateCacheState($destination['message_thread']);
 442:     }
 443: 
 444:     /**
 445:      * Fetches a message.
 446:      *
 447:      * @param integer $message_id  The ID of the message to fetch.
 448:      *
 449:      * @throws Horde_Exception_NotFound
 450:      * @throws Agora_Exception
 451:      */
 452:     public function getMessage($message_id)
 453:     {
 454:         $message = $this->_cache->get('agora_msg' . $message_id, $GLOBALS['conf']['cache']['default_lifetime']);
 455:         if ($message) {
 456:             return unserialize($message);
 457:         }
 458: 
 459:         $sql = 'SELECT message_id, forum_id, message_thread, parents, '
 460:             . 'message_author, message_subject, body, message_seq, '
 461:             . 'message_timestamp, view_count, locked, attachments FROM '
 462:             . $this->_threads_table . ' WHERE message_id = ?';
 463:         try {
 464:             $message = $this->_db->selectOne($sql, array($message_id));
 465:         } catch (Horde_Db_Exception $e) {
 466:             throw new Agora_Exception($e->getMessage());
 467:         }
 468: 
 469:         if (empty($message)) {
 470:             throw new Horde_Exception_NotFound(sprintf(_("Message ID \"%d\" not found"), $message_id));
 471:         }
 472: 
 473:         $message['message_subject'] = $this->convertFromDriver($message['message_subject']);
 474:         $message['body'] = $this->convertFromDriver($message['body']);
 475:         if ($message['message_thread'] == 0) {
 476:             $message['message_thread'] = $message_id;
 477:         }
 478: 
 479:         /* Is author a moderator? */
 480:         if (isset($this->_forum['moderators']) &&
 481:             in_array($message['message_author'], $this->_forum['moderators'])) {
 482:             $message['message_author_moderator'] = 1;
 483:         }
 484: 
 485:         $this->_cache->set('agora_msg' . $message_id, serialize($message));
 486: 
 487:         return $message;
 488:     }
 489: 
 490:     /**
 491:      * Returns a hash with all information necessary to reply to a message.
 492:      *
 493:      * @param mixed $message  The ID of the parent message to reply to, or arry of its data.
 494:      *
 495:      * @return array  A hash with all relevant information.
 496:      * @throws Horde_Exception_NotFound
 497:      * @throws Agora_Exception
 498:      */
 499:     public function replyMessage($message)
 500:     {
 501:         if (!is_array($message)) {
 502:             $message = $this->getMessage($message);
 503:         }
 504: 
 505:         /* Set up the form subject with the parent subject. */
 506:         if (Horde_String::lower(Horde_String::substr($message['message_subject'], 0, 3)) != 're:') {
 507:             $message['message_subject'] = 'Re: ' . $message['message_subject'];
 508:         } else {
 509:             $message['message_subject'] = $message['message_subject'];
 510:         }
 511: 
 512:         /* Prepare the message quite body . */
 513:         $message['body'] = sprintf(_("Posted by %s on %s"),
 514:                                    htmlspecialchars($message['message_author']),
 515:                                    strftime($GLOBALS['prefs']->getValue('date_format'), $message['message_timestamp']))
 516:             . "\n-------------------------------------------------------\n"
 517:             . $message['body'];
 518:         $message['body'] = "\n> " . Horde_String::wrap($message['body'], 60, "\n> ");
 519: 
 520:         return $message;
 521:     }
 522: 
 523:     /**
 524:      * Deletes a message and all replies.
 525:      *
 526:      * @todo Detele all related attachments from VFS.
 527:      *
 528:      * @param integer $message_id  The ID of the message to delete.
 529:      *
 530:      * @return string  Thread ID on success.
 531:      * @throws Agora_Exception
 532:      */
 533:     public function deleteMessage($message_id)
 534:     {
 535:         /* Check delete permissions. */
 536:         if (!$this->hasPermission(Horde_Perms::DELETE)) {
 537:             return PEAR::raiseError(sprintf(_("You don't have permission to delete messages in forum %s."), $this->_forum_id));
 538:         }
 539: 
 540:         $sql = 'SELECT message_thread FROM ' . $this->_threads_table . ' WHERE message_id = ?';
 541:         try {
 542:             $thread_id = $this->_db->selectValue($sql, array($message_id));
 543:         } catch (Horde_Db_Exception $e) {
 544:             throw new Agora_Exception($e->getMessage());
 545:         }
 546: 
 547:         $sql = 'DELETE FROM ' . $this->_threads_table . ' WHERE message_id = ' . (int)$message_id;
 548:         if ($thread_id == 0) {
 549:             $sql .= ' OR message_thread = ' . (int)$message_id;
 550:         }
 551: 
 552:         try {
 553:             $this->_db->execute($sql);
 554:         } catch (Horde_Db_Exception $e) {
 555:             throw new Agora_Exception($e->getMessage());
 556:         }
 557: 
 558:         /* Update counts */
 559:         // TODO message count is not correctly decreased after deleting more than one message.
 560:         $this->_forumSequence($this->_forum_id, 'message', '-');
 561:         if ($thread_id) {
 562:             $this->_sequence($thread_id, '-');
 563:         } else {
 564:             $this->_forumSequence($this->_forum_id, 'thread', '-');
 565:         }
 566: 
 567:         $this->_lastInForum($this->_forum_id);
 568:         $this->_lastInThread($thread_id);
 569: 
 570:         /* Update cache */
 571:         $this->_updateCacheState($thread_id);
 572: 
 573:         return $thread_id;
 574:     }
 575: 
 576:     /**
 577:      * Update lastMessage in a Forum
 578:      *
 579:      * @param integer $forum_id          Forum to update
 580:      * @param integer $message_id        Last message id
 581:      * @param string  $message_author    Last message author
 582:      * @param integer $message_timestamp Last message timestamp
 583:      *
 584:      * @throws Agora_Exception
 585:      */
 586:     private function _lastInForum($forum_id, $message_id = 0, $message_author = '', $message_timestamp = 0)
 587:     {
 588:         /* Get the last message in form or thread - when managing threads */
 589:         if ($message_id == 0) {
 590:             $sql = $this->_db->addLimitOffset('SELECT message_id, message_author, message_timestamp FROM ' . $this->_threads_table
 591:                 . ' WHERE forum_id = ' . (int)$forum_id . ' ORDER BY message_id DESC', array('limit' => 1));
 592:             try {
 593:                 $last = $this->_db->selectOne($sql);
 594:             } catch (Horde_Db_Execution $e) {
 595:                 throw new Agora_Exception($e->getMessage());
 596:             }
 597:             if (!empty($last)) {
 598:                 extract($last);
 599:             }
 600:         }
 601: 
 602:         $sql = 'UPDATE ' . $this->_forums_table
 603:             . ' SET last_message_id = ?, last_message_author = ?, last_message_timestamp = ? WHERE forum_id = ?';
 604:         $values = array($message_id, $message_author, $message_timestamp, $forum_id);
 605: 
 606:         try {
 607:             $this->_db->execute($sql, $values);
 608:         } catch (Horde_Db_Execution $e) {
 609:             throw new Agora_Exception($e->getMessage());
 610:         }
 611: 
 612:         $this->_cache->expire('agora_forum_' . $forum_id, $GLOBALS['conf']['cache']['default_lifetime']);
 613:     }
 614: 
 615:     /**
 616:      * Update lastMessage in Thread
 617:      *
 618:      * @param integer $thread_id         Thread to update
 619:      * @param integer $message_id        Last message id
 620:      * @param string  $message_author    Last message author
 621:      * @param integer $message_timestamp Last message timestamp
 622:      *
 623:      * @throws Agora_Exception
 624:      */
 625:     private function _lastInThread($thread_id, $message_id = 0, $message_author = '', $message_timestamp = 0)
 626:     {
 627:         /* Get the last message in form or thread - when managing threads */
 628:         if ($message_id == 0) {
 629:             $sql = $this->_db->addLimitOffset('SELECT message_id, message_author, message_timestamp FROM ' . $this->_threads_table
 630:                 . ' WHERE message_thread = ' . (int)$thread_id . ' ORDER BY message_id DESC', array('limit' => 1));
 631:             try {
 632:                 $last = $this->_db->selectOne($sql);
 633:             } catch (Horde_Db_Execution $e) {
 634:                 throw new Agora_Exception($e->getMessage());
 635:             }
 636:             if (!empty($last)) {
 637:                 extract($last);
 638:             }
 639:         }
 640: 
 641:         $sql = 'UPDATE ' . $this->_threads_table
 642:             . ' SET last_message_id = ?, last_message_author = ?, message_modifystamp = ? WHERE message_id = ?';
 643:         $values = array($message_id, $message_author, $message_timestamp, $thread_id);
 644: 
 645:         try {
 646:             $this->_db->execute($sql, $values);
 647:         } catch (Horde_Db_Execution $e) {
 648:             throw new Agora_Exception($e->getMessage());
 649:         }
 650:     }
 651: 
 652:     /**
 653:      * Increments or decrements a forum's message count.
 654:      *
 655:      * @param integer $forum_id     Forum to update
 656:      * @param string  $type         What to increment message, thread or view.
 657:      * @param integer|string $diff  Incremental or decremental step, either a
 658:      *                              positive or negative integer, or a plus or
 659:      *                              minus sign.
 660:      */
 661:     public function _forumSequence($forum_id, $type = 'message', $diff = '+')
 662:     {
 663:         $t = $type . '_count';
 664:         $sql = 'UPDATE ' . $this->_forums_table . ' SET ' . $t . ' = ';
 665: 
 666:         switch ($diff) {
 667:         case '+':
 668:         case '-':
 669:             $sql .= $t . ' ' . $diff . ' 1';
 670:             break;
 671: 
 672:         default:
 673:             $sql .= (int)$diff;
 674:             break;
 675:         }
 676: 
 677:         $sql .= ' WHERE forum_id = ' . (int)$forum_id;
 678: 
 679:         // TODO do we really need this return?
 680:         return $this->_db->execute($sql);
 681:     }
 682: 
 683:     /**
 684:      * Increments or decrements a thread's message count.
 685:      *
 686:      * @param integer $thread_id    Thread to update.
 687:      * @param integer|string $diff  Incremental or decremental step, either a
 688:      *                              positive or negative integer, or a plus or
 689:      *                              minus sign.
 690:      */
 691:     private function _sequence($thread_id, $diff = '+')
 692:     {
 693:         $sql = 'UPDATE ' . $this->_threads_table . ' SET message_seq = ';
 694: 
 695:         switch ($diff) {
 696:         case '+':
 697:         case '-':
 698:             $sql .= 'message_seq ' . $diff . ' 1';
 699:             break;
 700: 
 701:         default:
 702:             $sql .= (int)$diff;
 703:             break;
 704:         }
 705: 
 706:         $sql .= ', message_modifystamp = ' . $_SERVER['REQUEST_TIME'] . '  WHERE message_id = ' . (int)$thread_id;
 707:         // TODO do we really need this return?
 708:         return $this->_db->execute($sql);
 709:     }
 710: 
 711:     /**
 712:      * Deletes an entire message thread.
 713:      *
 714:      * @param integer $thread_id  The ID of the thread to delete. If not
 715:      *                            specified will delete all the threads for the
 716:      *                            current forum.
 717:      *
 718:      * @throws Agora_Exception
 719:      */
 720:     public function deleteThread($thread_id = 0)
 721:     {
 722:         /* Check delete permissions. */
 723:         if (!$this->hasPermission(Horde_Perms::DELETE)) {
 724:             return PEAR::raiseError(sprintf(_("You don't have permission to delete messages in forum %s."), $this->_forum_id));
 725:         }
 726: 
 727:         if ($thread_id > 0) {
 728:             $sql = 'DELETE FROM ' . $this->_threads_table . ' WHERE message_thread = ' . (int)$thread_id;
 729:             try {
 730:                 $this->_db->execute($sql);
 731:             } catch (Horde_Db_Exception $e) {
 732:                 throw new Agora_Exception($e->getMessage());
 733:             }
 734: 
 735:             $sql = 'SELECT COUNT(*) FROM ' . $this->_threads_table . ' WHERE forum_id = ' . (int)$this->_forum_id;
 736:             $messages = $this->_db->selectValue($sql);
 737: 
 738:             $this->_forumSequence($this->_forum_id, 'thread', '-');
 739:             $this->_forumSequence($this->_forum_id, 'message', $messages);
 740: 
 741:             /* Update cache */
 742:             $this->_updateCacheState($thread_id);
 743: 
 744:         } else {
 745:             $sql = 'DELETE FROM ' . $this->_threads_table . ' WHERE forum_id = ' . (int)$this->_forum_id;
 746:             try {
 747:                 $this->_db->execute($sql);
 748:             } catch (Horde_Db_Exception $e) {
 749:                 throw new Agora_Exception($e->getMessage());
 750:             }
 751: 
 752:             $this->_forumSequence($this->_forum_id, 'thread', '0');
 753:             $this->_forumSequence($this->_forum_id, 'message', '0');
 754:         }
 755: 
 756:         /* Update last message */
 757:         $this->_lastInForum($this->_forum_id);
 758: 
 759:         return true;
 760:     }
 761: 
 762:     /**
 763:      * Returns a list of threads.
 764:      *
 765:      * @param integer $thread_root   Message at which to start the thread.
 766:      *                               If null get all forum threads
 767:      * @param boolean $all_levels    Show all child levels or just one level.
 768:      * @param string  $sort_by       The column by which to sort.
 769:      * @param integer $sort_dir      The direction by which to sort:
 770:      *                                   0 - ascending
 771:      *                                   1 - descending
 772:      * @param boolean $message_view
 773:      * @param string  $link_back     A url to pass to the reply script which
 774:      *                               will be returned to after an insertion of
 775:      *                               a post. Useful in cases when this thread
 776:      *                               view is used in blocks to return to the
 777:      *                               original page rather than to Agora.
 778:      * @param string  $base_url      An alternative URL where edit/delete links
 779:      *                               point to. Mainly for api usage. Takes "%p"
 780:      *                               as a placeholder for the parent message ID.
 781:      * @param string  $from          The thread to start listing at.
 782:      * @param string  $count         The number of threads to return.
 783:      * @param boolean $nofollow      Whether to set the 'rel="nofollow"'
 784:      *                               attribute on linked URLs in the messages.
 785:      */
 786:     public function getThreads($thread_root = 0,
 787:                         $all_levels = false,
 788:                         $sort_by = 'message_timestamp',
 789:                         $sort_dir = 0,
 790:                         $message_view = false,
 791:                         $link_back = '',
 792:                         $base_url = null,
 793:                         $from = null,
 794:                         $count = null,
 795:                         $nofollow = false)
 796:     {
 797:         /* Check read permissions */
 798:         if (!$this->hasPermission(Horde_Perms::SHOW)) {
 799:             return PEAR::raiseError(sprintf(_("You don't have permission to read messages in forum %s."), $this->_forum_id));
 800:         }
 801: 
 802:         /* Get messages data */
 803:         $messages = $this->_getThreads($thread_root, $all_levels, $sort_by, $sort_dir, $message_view, $from, $count);
 804:         if ($messages instanceof PEAR_Error || empty($messages)) {
 805:             return $messages;
 806:         }
 807: 
 808:         /* Moderators */
 809:         if (isset($this->_forum['moderators'])) {
 810:             $moderators = array_flip($this->_forum['moderators']);
 811:         }
 812: 
 813:         /* Set up the base urls for actions. */
 814:         $view_url = Horde::url('messages/index.php');
 815:         if ($base_url) {
 816:             $edit_url = $base_url;
 817:             $del_url = Horde_Util::addParameter($base_url, 'delete', 'true');
 818:         } else {
 819:             $edit_url = Horde::url('messages/edit.php');
 820:             $del_url = Horde::url('messages/delete.php');
 821:         }
 822: 
 823:         // Get needed prefs
 824:         $per_page = $GLOBALS['prefs']->getValue('thread_per_page');
 825:         $view_bodies = $GLOBALS['prefs']->getValue('thread_view_bodies');
 826:         $abuse_url = Horde::url('messages/abuse.php');
 827:         $hot_img = Horde::img('hot.png', _("Hot thread"), array('title' => _("Hot thread")));
 828:         $new_img = Horde::img('required.png', _("New posts"), array('title' => _("New posts")));
 829:         $is_moderator = $this->hasPermission(Horde_Perms::DELETE);
 830: 
 831:         /* Loop through the threads and set up the array. */
 832:         foreach ($messages as &$message) {
 833: 
 834:             /* Add attachment link */
 835:             if ($message['attachments']) {
 836:                 $message['message_attachment'] = $this->getAttachmentLink($message['message_id']);
 837:             }
 838: 
 839:             /* Get last message link */
 840:             if ($thread_root == 0 && $message['last_message_id'] > 0) {
 841:                 $url = Agora::setAgoraId($message['forum_id'], $message['last_message_id'], $view_url, $this->_scope);
 842:                 $message['message_url'] = Horde::link($url);
 843:                 $last_timestamp = $message['last_message_timestamp'];
 844:             } else {
 845:                 $last_timestamp = $message['message_timestamp'];
 846:             }
 847: 
 848:             /* Check if thread is hot */
 849:             if ($this->isHot($message['view_count'], $last_timestamp)) {
 850:                 $message['hot'] = $hot_img;
 851:             }
 852: 
 853:             /* Check if has new posts since user last visit */
 854:             if ($thread_root == 0 && $this->isNew($message['message_id'], $last_timestamp)) {
 855:                 $message['new'] = $new_img;
 856:             }
 857: 
 858:             /* Mark moderators */
 859:             if (isset($this->_forum['moderators']) && array_key_exists($message['message_author'], $moderators)) {
 860:                 $message['message_author_moderator'] = 1;
 861:             }
 862: 
 863:             /* Link to view the message. */
 864:             $url = Agora::setAgoraId($message['forum_id'], $message['message_id'], $view_url, $this->_scope);
 865:             $message['link'] = Horde::link($url, $message['message_subject'], '', '', '', $message['message_subject']);
 866: 
 867:             /* Set up indenting for threads. */
 868:             if ($sort_by != 'message_thread') {
 869:                 unset($message['indent'], $message['parent']);
 870: 
 871:                 /* Links to pages */
 872:                 if ($thread_root == 0 && $message['message_seq'] > $per_page && $view_bodies == 2) {
 873:                     $sub_pages = $message['message_seq'] / $per_page;
 874:                     for ($i = 0; $i < $sub_pages; $i++) {
 875:                         $page_title = sprintf(_("Page %d"), $i+1);
 876:                         $message['pages'][] = Horde::link(Horde_Util::addParameter($url, 'thread_page', $i), $page_title, '', '', '', $page_title) . ($i+1) . '</a>';
 877:                     }
 878:                 }
 879:             }
 880: 
 881:             /* Button to post a reply to the message. */
 882:             if (!$message['locked']) {
 883:                 if ($base_url) {
 884:                     $url = $base_url;
 885:                     if (strpos($url, '%p') !== false) {
 886:                         $url = str_replace('%p', $message['message_id'], $url);
 887:                     } else {
 888:                         $url = Horde_Util::addParameter($url, 'message_parent_id', $message['message_id']);
 889:                     }
 890:                     if (!empty($link_back)) {
 891:                         $url = Horde_Util::addParameter($url, 'url', $link_back);
 892:                     }
 893:                 } else {
 894:                     $url = Agora::setAgoraId($message['forum_id'], $message['message_id'], $view_url, $this->_scope);
 895:                 }
 896:                 $url = Horde_Util::addParameter($url, 'reply_focus', 1) . '#messageform';
 897:                 $message['reply'] = Horde::link($url, _("Reply to message"), '', '', '', _("Reply to message")) . _("Reply") . '</a>';
 898:             }
 899: 
 900:             /* Link to edit the message. */
 901:             if ($thread_root > 0 && isset($this->_forum['moderators'])) {
 902:                 $url = Agora::setAgoraId($message['forum_id'], $message['message_id'], $abuse_url);
 903:                 $message['actions'][] = Horde::link($url, _("Report as abuse")) . _("Report as abuse") . '</a>';
 904:             }
 905: 
 906:             if ($is_moderator) {
 907:                 /* Link to edit the message. */
 908:                 $url = Agora::setAgoraId($message['forum_id'], $message['message_id'], $edit_url, $this->_scope);
 909:                 $message['actions'][] = Horde::link($url, _("Edit"), '', '', '', _("Edit message")) . _("Edit") . '</a>';
 910: 
 911:                 /* Link to delete the message. */
 912:                 $url = Agora::setAgoraId($message['forum_id'], $message['message_id'], $del_url, $this->_scope);
 913:                 $message['actions'][] = Horde::link($url, _("Delete"), '', '', '', _("Delete message")) . _("Delete") . '</a>';
 914: 
 915:                 /* Link to lock/unlock the message. */
 916:                 $url = Agora::setAgoraId($this->_forum_id, $message['message_id'], Horde::url('messages/lock.php'), $this->_scope);
 917:                 $label = ($message['locked']) ? _("Unlock") : _("Lock");
 918:                 $message['actions'][] = Horde::link($url, $label, '', '', '', $label) . $label . '</a>';
 919: 
 920:                 /* Link to move thread to another forum. */
 921:                 if ($this->_scope == 'agora') {
 922:                     if ($message['message_thread'] == $message['message_id']) {
 923:                         $url = Agora::setAgoraId($this->_forum_id, $message['message_id'], Horde::url('messages/move.php'), $this->_scope);
 924:                         $message['actions'][] = Horde::link($url, _("Move"), '', '', '', _("Move")) . _("Move") . '</a>';
 925: 
 926:                         /* Link to merge a message thred with anoter thread. */
 927:                         $url = Agora::setAgoraId($this->_forum_id, $message['message_id'], Horde::url('messages/merge.php'), $this->_scope);
 928:                         $message['actions'][] = Horde::link($url, _("Merge"), '', '', '', _("Merge")) . _("Merge") . '</a>';
 929:                     } elseif ($message['message_thread'] != 0) {
 930: 
 931:                         /* Link to split thread to two threads, from this message after. */
 932:                         $url = Agora::setAgoraId($this->_forum_id, $message['message_id'], Horde::url('messages/split.php'), $this->_scope);
 933:                         $message['actions'][] = Horde::link($url, _("Split"), '', '', '', _("Split")) . _("Split") . '</a>';
 934:                     }
 935:                 }
 936:             }
 937:         }
 938: 
 939:         return $messages;
 940:     }
 941: 
 942:     /**
 943:      * Formats a message body.
 944:      *
 945:      * @param string $messages  Messages to format
 946:      * @param string $sort_by   List format order
 947:      * @param boolean $format     Format messages body
 948:      * @param integer $thread_root      Thread root
 949:      */
 950:     protected function _formatThreads($messages, $sort_by = 'message_modifystamp',
 951:                             $format = false, $thread_root = 0)
 952:     {
 953:         /* Loop through the threads and set up the array. */
 954:         foreach ($messages as &$message) {
 955:             $message['message_author'] = htmlspecialchars($message['message_author']);
 956:             $message['message_subject'] = htmlspecialchars($this->convertFromDriver($message['message_subject']));
 957:             $message['message_date'] = $this->dateFormat($message['message_timestamp']);
 958:             if ($format) {
 959:                 $message['body'] = $this->formatBody($this->convertFromDriver($message['body']));
 960:             }
 961: 
 962:             /* If we are on the top, thread id is message itself. */
 963:             if ($message['message_thread'] == 0) {
 964:                 $message['message_thread'] = $message['message_id'];
 965:             }
 966: 
 967:             /* Get last message. */
 968:             if ($thread_root == 0 && $message['last_message_id'] > 0) {
 969:                 $message['last_message_date'] = $this->dateFormat($message['last_message_timestamp']);
 970:             }
 971: 
 972:             /* Set up indenting for threads. */
 973:             if ($sort_by == 'message_thread') {
 974:                 $indent = explode(':', $message['parents']);
 975:                 $message['indent'] = count($indent) - 1;
 976:                 $last = array_pop($indent);
 977: 
 978:                 /* TODO: this won't work because array_search doesn't search in
 979:                  * multi-dimensional arrays.
 980:                  *
 981:                  * From what I see this is only needed because there is a bug in message
 982:                  * deletion anyway. Parents should always be in array, because when
 983:                  * deleting we should delete all sub-messages. We even state this in GUI,
 984:                  * but we actually don't do it so.
 985:                  *
 986:                 /*if (array_search($last, $messages) != 'message_id') {
 987:                     $message['indent'] = 1;
 988:                     $last = null;
 989:                 }*/
 990:                 $message['parent'] = $last ? $last : null;
 991:             }
 992:         }
 993: 
 994:         return $messages;
 995:     }
 996: 
 997:     /**
 998:      * Formats a message body.
 999:      *
1000:      * @param string $body           Text to format.
1001:      */
1002:     public function formatBody($body)
1003:     {
1004:         static $filters, $filters_params;
1005: 
1006:         if ($filters == null) {
1007:             $filters = array('text2html', 'bbcode', 'highlightquotes', 'emoticons');
1008:             $filters_params = array(array('parselevel' => Horde_Text_Filter_Text2html::MICRO),
1009:                                     array(),
1010:                                     array('citeblock' => true),
1011:                                     array('entities' => true));
1012: 
1013:             // TODO: file doesn't exist anymore
1014:             $config_dir = $GLOBALS['registry']->get('fileroot', 'agora') . '/config/';
1015:             $config_file = 'words.php';
1016:             if (file_exists($config_dir . $config_file)) {
1017:                 if (!empty($GLOBALS['conf']['vhosts'])) {
1018:                     $v_file = substr($config_file, 0, -4) . '-' . $GLOBALS['conf']['server']['name'] . '.php';
1019:                     if (file_exists($config_dir . $config_file)) {
1020:                         $config_file = $v_file;
1021:                     }
1022:                 }
1023: 
1024:                 $filters[] = 'words';
1025:                 $filters_params[] = array('words_file' => $config_dir . $config_file,
1026:                                         'replacement' => false);
1027:             }
1028:         }
1029: 
1030:         if (($hasBBcode = strpos($body, '[')) !== false &&
1031:                 strpos($body, '[/', $hasBBcode) !== false) {
1032:             $filters_params[0]['parselevel'] = Horde_Text_Filter_Text2html::NOHTML;
1033:         }
1034: 
1035:         return $GLOBALS['injector']->getInstance('Horde_Core_Factory_TextFilter')->filter($body, $filters, $filters_params);
1036:     }
1037: 
1038:     /**
1039:      * Returns true if the message is hot.
1040:      */
1041:     public function isHot($views, $last_post)
1042:     {
1043:         if (!$GLOBALS['conf']['threads']['track_views']) {
1044:             return false;
1045:         }
1046: 
1047:         return ($views > $GLOBALS['prefs']->getValue('threads_hot')) && $last_post > ($_SERVER['REQUEST_TIME'] - 86400);
1048:     }
1049: 
1050:     /**
1051:      * Returns true, has new posts since user last visit
1052:      */
1053:     public function isNew($thread_id, $last_post)
1054:     {
1055:         if (!isset($_COOKIE['agora_viewed_threads']) ||
1056:             ($pos1 = strpos($_COOKIE['agora_viewed_threads'], ':' . $thread_id . '|')) === false ||
1057:             ($pos2 = strpos($_COOKIE['agora_viewed_threads'], '|', $pos1)) === false ||
1058:              substr($_COOKIE['agora_viewed_threads'], $pos2+1, 10) > $last_post
1059:             ) {
1060:             return false;
1061:         }
1062: 
1063:         return true;
1064:     }
1065: 
1066:     /**
1067:      * Fetches a list of messages awaiting moderation. Selects all messages,
1068:      * irrespective of the thread root, which have the 'moderate' flag set in
1069:      * the attributes.
1070:      *
1071:      * @param string  $sort_by   The column by which to sort.
1072:      * @param integer $sort_dir  The direction by which to sort:
1073:      *                           0 - ascending
1074:      *                           1 - descending
1075:      *
1076:      * @throws Agora_Exception
1077:      */
1078:     public function getModerateList($sort_by, $sort_dir)
1079:     {
1080:         $sql = 'SELECT forum_id, forum_name FROM ' . $this->_forums_table . ' WHERE forum_moderated = ?';
1081:         $values = array(1);
1082: 
1083:         /* Check permissions */
1084:         if ($GLOBALS['registry']->isAdmin(array('permission' => 'agora:admin')) ||
1085:             ($GLOBALS['injector']->getInstance('Horde_Perms')->exists('agora:forums:' . $this->_scope) &&
1086:              $GLOBALS['injector']->getInstance('Horde_Perms')->hasPermission('agora:forums:' . $this->_scope, $GLOBALS['registry']->getAuth(), Horde_Perms::DELETE))) {
1087:                 $sql .= ' AND scope = ? ';
1088:                 $values[] = $this->_scope;
1089:         } else {
1090:             // Get only author forums
1091:             $sql .= ' AND scope = ? AND author = ?';
1092:             $values[] = $this->_scope;
1093:             $values[] = $GLOBALS['registry']->getAuth();
1094:         }
1095: 
1096:         /* Get moderate forums and their names */
1097:         try {
1098:             $forums_list = $this->_db->selectAssoc($sql, $values);
1099:         } catch (Horde_Db_Exception $e) {
1100:             throw new Agora_Exception($e->getMessage());
1101:         }
1102:         if (empty($forums_list)) {
1103:             return $forums_list;
1104:         }
1105: 
1106:         /* Get message waiting for approval */
1107:         $sql = 'SELECT message_id, forum_id, message_subject, message_author, '
1108:             . 'body, message_timestamp, attachments FROM ' . $this->_threads_table . ' WHERE forum_id IN ('
1109:             . implode(',', array_keys($forums_list)) . ')'
1110:             . ' AND approved = ? ORDER BY ' . $sort_by . ' '
1111:             . ($sort_dir ? 'DESC' : 'ASC');
1112: 
1113:         try {
1114:             $messages = $this->_db->selectAll($sql, array(0));
1115:         } catch (Horde_Db_Exception $e) {
1116:             throw new Agora_Exception($e->getMessage());
1117:         }
1118: 
1119:         /* Loop through the messages and set up the array. */
1120:         $approve_url = Horde_Util::addParameter(Horde::url('moderate.php'), 'approve', true);
1121:         $del_url  = Horde::url('messages/delete.php');
1122:         foreach ($messages as &$message) {
1123:             $message['forum_name'] = $this->convertFromDriver($forums_list[$message['forum_id']]);
1124:             $message['message_author'] = htmlspecialchars($message['message_author']);
1125:             $message['message_subject'] = htmlspecialchars($this->convertFromDriver($message['message_subject']));
1126:             $message['message_body'] = $GLOBALS['injector']
1127:                 ->getInstance('Horde_Core_Factory_TextFilter')
1128:                 ->filter($this->convertFromDriver($message['body']), 'highlightquotes');
1129:             if ($message['attachments']) {
1130:                 $message['message_attachment'] = $this->getAttachmentLink($message['message_id']);
1131:             }
1132:             $message['message_date'] = $this->dateFormat($message['message_timestamp']);
1133:         }
1134: 
1135:         return $messages;
1136:     }
1137: 
1138:     /**
1139:      * Get banned users from the current forum
1140:      */
1141:     public function getBanned()
1142:     {
1143:         $perm_name = 'agora:forums:' . $this->_scope . ':' . $this->_forum_id;
1144:         if (!$GLOBALS['injector']->getInstance('Horde_Perms')->exists($perm_name)) {
1145:             return array();
1146:         }
1147: 
1148:         $forum_perm = $GLOBALS['injector']->getInstance('Horde_Perms')->getPermission($perm_name);
1149:         if (!($forum_perm instanceof Horde_Perms_Permission)) {
1150:             return $forum_perm;
1151:         }
1152: 
1153:         $permissions = $forum_perm->getUserPermissions();
1154:         if (empty($permissions)) {
1155:             return $permissions;
1156:         }
1157: 
1158:         /* Filter users moderators */
1159:         $filter = Horde_Perms::EDIT | Horde_Perms::DELETE;
1160:         foreach ($permissions as $user => $level) {
1161:             if ($level & $filter) {
1162:                 unset($permissions[$user]);
1163:             }
1164:         }
1165: 
1166:         return $permissions;
1167:     }
1168: 
1169:     /**
1170:      * Ban user on a specific forum.
1171:      *
1172:      * @param string  $user      Moderator username.
1173:      * @param integer $forum_id  Forum to add moderator to.
1174:      * @param string  $action    Action to peform ('add' or 'delete').
1175:      */
1176:     public function updateBan($user, $forum_id = null, $action = 'add')
1177:     {
1178:         if ($forum_id == null) {
1179:             $forum_id = $this->_forum_id;
1180:         }
1181: 
1182:         $perms = $GLOBALS['injector']->getInstance('Horde_Perms');
1183:         $perm_name = 'agora:forums:' . $this->_scope . ':' . $forum_id;
1184:         if (!$perms->exists($perm_name)) {
1185:             $forum_perm = $GLOBALS['injector']
1186:                 ->getInstance('Horde_Core_Perms')
1187:                 ->newPermission($perm_name);
1188:             $perms->addPermission($forum_perm);
1189:         } else {
1190:             $forum_perm = $perms->getPermission($perm_name);
1191:             if ($forum_perm instanceof PEAR_Error) {
1192:                 return $forum_perm;
1193:             }
1194:         }
1195: 
1196:         if ($action == 'add') {
1197:             // Allow to only read posts
1198:             $forum_perm->removeUserPermission($user, Horde_Perms::ALL, true);
1199:             $forum_perm->addUserPermission($user, Horde_Perms::READ, true);
1200:         } else {
1201:             // Remove all acces to user
1202:             $forum_perm->removeUserPermission($user, Horde_Perms::ALL, true);
1203:         }
1204: 
1205:         return true;
1206:     }
1207: 
1208:     /**
1209:      * Updates forum moderators.
1210:      *
1211:      * @param string  $moderator  Moderator username.
1212:      * @param integer $forum_id   Forum to add moderator to.
1213:      * @param string  $action     Action to peform ('add' or 'delete').
1214:      *
1215:      * @throws Agora_Exception
1216:      */
1217:     public function updateModerator($moderator, $forum_id = null, $action = 'add')
1218:     {
1219:         if ($forum_id == null) {
1220:             $forum_id = $this->_forum_id;
1221:         }
1222: 
1223:         switch ($action) {
1224:         case 'add':
1225:             $sql = 'INSERT INTO agora_moderators (forum_id, horde_uid) VALUES (?, ?)';
1226:             break;
1227: 
1228:         case 'delete':
1229:             $sql = 'DELETE FROM agora_moderators WHERE forum_id = ? AND horde_uid = ?';
1230:             break;
1231:         }
1232: 
1233:         try {
1234:             $this->_db->execute($sql, array($forum_id, $moderator));
1235:         } catch (Horde_Db_Exception $e) {
1236:             throw new Agora_Exception($e->getMessage());
1237:         }
1238: 
1239:         /* Update permissions */
1240:         $perm_name = 'agora:forums:' . $this->_scope . ':' . $forum_id;
1241:         $perms = $GLOBALS['injector']->getInstance('Horde_Perms');
1242:         if (!$perms->exists($perm_name)) {
1243:             $forum_perm = $GLOBALS['injector']
1244:                 ->getInstance('Horde_Core_Perms')
1245:                 ->newPermission($perm_name);
1246:             $perms->addPermission($forum_perm);
1247:         } else {
1248:             $forum_perm = $perms->getPermission($perm_name);
1249:             if ($forum_perm instanceof PEAR_Error) {
1250:                 return $forum_perm;
1251:             }
1252:         }
1253: 
1254:         switch ($action) {
1255:         case 'add':
1256:             $forum_perm->addUserPermission($moderator, Horde_Perms::DELETE, true);
1257:             break;
1258: 
1259:         case 'delete':
1260:             $forum_perm->removeUserPermission($moderator, Horde_Perms::DELETE, true);
1261:             break;
1262:         }
1263: 
1264:         $this->_cache->expire('agora_forum_' . $forum_id, $GLOBALS['conf']['cache']['default_lifetime']);
1265:     }
1266: 
1267:     /**
1268:      * Approves one or more ids.
1269:      *
1270:      * @param string $action  Whether to 'approve' or 'delete' messages.
1271:      * @param array $ids      Array of message IDs.
1272:      *
1273:      * @throws Agora_Exception
1274:      */
1275:     public function moderate($action, $ids)
1276:     {
1277:         switch ($action) {
1278:         case 'approve':
1279: 
1280:             /* Get message thread to expire cache */
1281:             $sql = 'SELECT message_thread FROM ' . $this->_threads_table
1282:                     . ' WHERE message_id IN (' . implode(',', $ids) . ')';
1283:             try {
1284:                 $threads = $this->_db->selectValues($sql);
1285:             } catch (Horde_Db_Exception $e) {
1286:                 throw new Agora_Exception($e->getMessage());
1287:             }
1288:             $this->_updateCacheState($threads);
1289: 
1290:             $sql = 'UPDATE ' . $this->_threads_table . ' SET approved = 1'
1291:                  . ' WHERE message_id IN (' . implode(',', $ids) . ')';
1292:             try {
1293:                 $this->_db->execute($sql);
1294:             } catch (Horde_Db_Exception $e) {
1295:                 throw new Agora_Exception($e->getMessage());
1296:             }
1297: 
1298:             /* Save original forum_id for later resetting */
1299:             $orig_forum_id = $this->_forum_id;
1300:             foreach ($ids as $message_id) {
1301:                 /* Update cached message and thread counts */
1302:                 $message = $this->getMessage($message_id);
1303:                 $this->_forum_id = $message['forum_id'];
1304: 
1305:                 /* Update cached last poster */
1306:                 $this->_lastInForum($this->_forum_id);
1307:                 $this->_forumSequence($this->_forum_id, 'message', '+');
1308:                 if (!empty($message['parents'])) {
1309:                     $this->_sequence($message['message_thread'], '+');
1310:                     $this->_lastInThread($message['message_thread'], $message_id, $message['message_author'], $_SERVER['REQUEST_TIME']);
1311:                 } else {
1312:                     $this->_forumSequence($this->_forum_id, 'thread', '+');
1313:                 }
1314: 
1315:                 /* Send the new post to the distribution address */
1316:                 Agora::distribute($message_id);
1317:             }
1318: 
1319:             /* Restore original forum ID */
1320:             $this->_forum_id = $orig_forum_id;
1321:             break;
1322: 
1323:         case 'delete':
1324:             foreach ($ids as $id) {
1325:                 $this->deleteMessage($id);
1326:             }
1327:             break;
1328:         }
1329:     }
1330: 
1331:     /**
1332:      * Returns the number of replies on a thread, or threads in a forum
1333:      *
1334:      * @param integer $thread_root  Thread to count.
1335:      *
1336:      * @return integer  The number of messages in thread or PEAR_Error on
1337:      *                  failure.
1338:      */
1339:     public function countThreads($thread_root = 0)
1340:     {
1341:         $sql = 'SELECT COUNT(*) FROM ' . $this->_threads_table . ' WHERE message_thread = ?';
1342:         if ($thread_root) {
1343:             return $this->_db->selectValue($sql, array($thread_root));
1344:         } else {
1345:             return $this->_db->selectValue($sql . ' AND forum_id = ?', array(0, $this->_forum_id));
1346:         }
1347:     }
1348: 
1349:     /**
1350:      * Returns the number of all messages (threads and replies) in a forum
1351:      *
1352:      * @return integer  The number of messages in forum or PEAR_Error on
1353:      *                  failure.
1354:      */
1355:     public function countMessages()
1356:     {
1357:         $sql = 'SELECT COUNT(*) FROM ' . $this->_threads_table . ' WHERE forum_id = ?';
1358:         return $this->_db->selectValue($sql, array($this->_forum_id));
1359:     }
1360: 
1361:     /**
1362:      * Returns a table showing the specified message list.
1363:      *
1364:      * @param array $threads         A hash with the thread messages as
1365:      *                               returned by {@link
1366:      *                               Agora_Driver::getThreads}.
1367:      * @param array $col_headers     A hash with the column headers.
1368:      * @param boolean $bodies        Display the message bodies?
1369:      * @param string $template_file  Template to use.
1370:      *
1371:      * @return string  The rendered message table.
1372:      */
1373:     public function getThreadsUi($threads, $col_headers, $bodies = false,
1374:                                  $template_file = false)
1375:     {
1376:         if (!count($threads)) {
1377:             return '';
1378:         }
1379: 
1380:         /* Render threaded lists with Horde_Tree. */
1381:         if (!$template_file && isset($threads[0]['indent'])) {
1382:             $tree = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Tree')->create('threads', 'Html', array(
1383:                 'multiline' => $bodies,
1384:                 'lines' => !$bodies
1385:             ));
1386: 
1387:             $tree->setHeader(array(
1388:                 array(
1389:                     'class' => $col_headers['message_thread_class_plain'],
1390:                     'html' => '<strong>' . $col_headers['message_thread'] . '</strong>'
1391:                 ),
1392:                 array(
1393:                     'class' => $col_headers['message_author_class_plain'],
1394:                     'html' => '<strong>' . $col_headers['message_author'] . '</strong>'
1395:                 ),
1396:                 array(
1397:                     'class' => $col_headers['message_timestamp_class_plain'],
1398:                     'html' => '<strong>' . $col_headers['message_timestamp'] . '</strong>'
1399:                 )
1400:             ));
1401: 
1402:             foreach ($threads as &$thread) {
1403:                 if ($bodies) {
1404:                     $text = '<strong>' . $thread['message_subject'] . '</strong><small>[';
1405:                     if (isset($thread['reply'])) {
1406:                         $text .= ' ' . $thread['reply'];
1407:                     }
1408:                     if (!empty($thread['actions'])) {
1409:                         $text .= ', ' . implode(', ', $thread['actions']);
1410:                     }
1411:                     $text .= ']</small><br />' .
1412:                         str_replace(array("\r", "\n"), '', $thread['body'] . ((isset($thread['message_attachment'])) ? $thread['message_attachment'] : ''));
1413:                 } else {
1414:                     $text = '<strong>' . $thread['link'] . $thread['message_subject'] . '</a></strong> ';
1415:                     if (isset($thread['actions'])) {
1416:                         $text .= '<small>[' . implode(', ', $thread['actions']) . ']</small>';
1417:                     }
1418:                 }
1419: 
1420:                 $tree->addNode(
1421:                     $thread['message_id'],
1422:                     $thread['parent'],
1423:                     $text,
1424:                     $thread['indent'],
1425:                     true,
1426:                     array(
1427:                         'class' => 'linedRow',
1428:                         'icon' => false
1429:                     ),
1430:                     array(
1431:                         $thread['message_author'],
1432:                         $thread['message_date']
1433:                     )
1434:                 );
1435:             }
1436: 
1437:             return $tree->getTree(true);
1438:         }
1439: 
1440:         /* Set up the thread template tags. */
1441:         $view = new Agora_View();
1442:         $view->threads_list = $threads;
1443:         $view->col_headers = $col_headers;
1444:         $view->thread_view_bodies = $bodies;
1445: 
1446:         /* Render template. */
1447:         if (!$template_file) {
1448:             $template_file = 'messages/threads';
1449:         }
1450: 
1451:         return $view->render($template_file);
1452:     }
1453: 
1454:     /**
1455:      * @throws Agora_Exception
1456:      */
1457:     public function getThreadRoot($message_id)
1458:     {
1459:         $sql = 'SELECT message_thread FROM ' . $this->_threads_table . ' WHERE message_id = ?';
1460:         try {
1461:             $thread_id = $this->_db->selectValue($sql, array($message_id));
1462:         } catch (Horde_Db_Exception $e) {
1463:             throw new Agora_Exception($e->getMessage());
1464:         }
1465:         return $thread_id ? $thread_id : $message_id;
1466:     }
1467: 
1468:     /**
1469:      */
1470:     public function setThreadLock($message_id, $lock)
1471:     {
1472:         $sql = 'UPDATE ' . $this->_threads_table . ' SET locked = ? WHERE message_id = ? OR message_thread = ?';
1473:         $values = array($lock, $message_id, $message_id);
1474:         return $this->_db->execute($sql, $values);
1475:     }
1476: 
1477:     /**
1478:      * @return boolean
1479:      */
1480:     public function isThreadLocked($message_id)
1481:     {
1482:         $sql = 'SELECT message_thread FROM ' . $this->_threads_table . ' WHERE message_id = ?';
1483:         $thread = $this->_db->selectValue($sql, array($message_id));
1484: 
1485:         return $this->_db->selectValue('SELECT locked FROM ' . $this->_threads_table . ' WHERE message_id = ?', array($thread));
1486:     }
1487: 
1488:     /**
1489:      */
1490:     public function getThreadActions()
1491:     {
1492:         /* Actions. */
1493:         $actions = array();
1494: 
1495:         $url = Agora::setAgoraId($this->_forum_id, null, Horde::url('messages/edit.php'));
1496:         if ($this->hasPermission(Horde_Perms::EDIT)) {
1497:             $actions[] = array('url' => $url, 'label' => _("Post message"));
1498:         }
1499: 
1500:         if ($this->hasPermission(Horde_Perms::DELETE)) {
1501:             if ($this->_scope == 'agora') {
1502:                 $url = Agora::setAgoraId($this->_forum_id, null, Horde::url('editforum.php'));
1503:                 $actions[] = array('url' => $url, 'label' => _("Edit Forum"));
1504:             }
1505:             $url = Agora::setAgoraId($this->_forum_id, null, Horde::url('deleteforum.php'), $this->_scope);
1506:             $actions[] = array('url' => $url, 'label' => _("Delete Forum"));
1507:             $url = Agora::setAgoraId($this->_forum_id, null, Horde::url('ban.php'), $this->_scope);
1508:             $actions[] = array('url' => $url, 'label' => _("Ban"));
1509:         }
1510: 
1511:         return $actions;
1512:     }
1513: 
1514:     /**
1515:      */
1516:     public function getForm($vars, $title, $editing = false, $new_forum = false)
1517:     {
1518:         global $conf;
1519: 
1520:         $form = new Agora_Form_Message($vars, $title);
1521:         $form->setButtons($editing ? _("Save") : _("Post"));
1522:         $form->addHidden('', 'url', 'text', false);
1523: 
1524:         /* Figure out what to do with forum IDs. */
1525:         if ($new_forum) {
1526:             /* This is a new forum to be created, create the var to hold the
1527:              * full path for the new forum. */
1528:             $form->addHidden('', 'new_forum', 'text', false);
1529:         } else {
1530:             /* This is an existing forum so create the forum ID variable. */
1531:             $form->addHidden('', 'forum_id', 'int', false);
1532:         }
1533: 
1534:         $form->addHidden('', 'scope', 'text', false);
1535:         $form->addHidden('', 'message_id', 'int', false);
1536:         $form->addHidden('', 'message_parent_id', 'int', false);
1537: 
1538:         if (!$GLOBALS['registry']->getAuth()) {
1539:             $form->addVariable(_("From"), 'posted_by', 'text', true);
1540:         }
1541: 
1542:         /* We are replaying, so display the quote button */
1543:         if ($vars->get('message_parent_id')) {
1544:             $desc = '<input type="button" value="' . _("Quote") . '" class="button" '
1545:                   . 'onClick="this.form.message_body.value=this.form.message_body.value + this.form.message_body_old.value; this.form.message_body_old.value = \'\';" />';
1546:             $form->addVariable(_("Subject"), 'message_subject', 'text', true, false, $desc);
1547:             $form->addHidden('', 'message_body_old', 'longtext', false);
1548:         } else {
1549:             $form->addVariable(_("Subject"), 'message_subject', 'text', true);
1550:         }
1551: 
1552:         $form->addVariable(_("Message"), 'message_body', 'longtext', true);
1553: 
1554:         /* Check if an attachment is available and set variables for deleting
1555:          * and previewing. */
1556:         if ($vars->get('attachment_preview')) {
1557:             $form->addVariable(_("Delete the existing attachment?"), 'attachment_delete', 'boolean', false);
1558:             $form->addVariable(_("Current attachment"), 'attachment_preview', 'html', false);
1559:         }
1560: 
1561:         if ($this->allowAttachments()) {
1562:             $form->addVariable(_("Attachment"), 'message_attachment', 'file', false);
1563:         }
1564: 
1565:         if (!empty($conf['forums']['captcha']) && !$GLOBALS['registry']->getAuth()) {
1566:             $form->addVariable(_("Spam protection"), 'captcha', 'figlet', true, null, null, array(Agora::getCAPTCHA(!$form->isSubmitted()), $conf['forums']['figlet_font']));
1567:         }
1568: 
1569:         return $form;
1570:     }
1571: 
1572:     /**
1573:      * Formats time according to user preferences.
1574:      *
1575:      * @param int $timestamp  Message timestamp.
1576:      *
1577:      * @return string  Formatted date.
1578:      */
1579:     public function dateFormat($timestamp)
1580:     {
1581:         return strftime($GLOBALS['prefs']->getValue('date_format'), $timestamp)
1582:             . ' '
1583:             . (date($GLOBALS['prefs']->getValue('twentyFour') ? 'G:i' : 'g:ia', $timestamp));
1584:     }
1585: 
1586:     /**
1587:      * Logs a message view.
1588:      *
1589:      * @return boolean True, if the view was logged, false if the message was aleredy seen
1590:      * @throws Agora_Exception
1591:      */
1592:     public function logView($thread_id)
1593:     {
1594:         if (!$GLOBALS['conf']['threads']['track_views']) {
1595:             return false;
1596:         }
1597: 
1598:         if ($GLOBALS['browser']->isRobot()) {
1599:             return false;
1600:         }
1601: 
1602:         /* We already read this thread? */
1603:         if (isset($_COOKIE['agora_viewed_threads']) &&
1604:             strpos($_COOKIE['agora_viewed_threads'], ':' . $thread_id . '|') !== false) {
1605:             return false;
1606:         }
1607: 
1608:         /* Rembember when we see a thread */
1609:         if (!isset($_COOKIE['agora_viewed_threads'])) {
1610:             $_COOKIE['agora_viewed_threads'] = ':';
1611:         }
1612:         $_COOKIE['agora_viewed_threads'] .= $thread_id . '|' . $_SERVER['REQUEST_TIME'] . ':';
1613: 
1614:         setcookie('agora_viewed_threads', $_COOKIE['agora_viewed_threads'], $_SERVER['REQUEST_TIME']+22896000,
1615:                     $GLOBALS['conf']['cookie']['path'], $GLOBALS['conf']['cookie']['domain'],
1616:                     $GLOBALS['conf']['use_ssl'] == 1 ? 1 : 0);
1617: 
1618:         /* Update the count */
1619:         $sql = 'UPDATE ' . $this->_threads_table . ' SET view_count = view_count + 1 WHERE message_id = ?';
1620:         try {
1621:             $this->_db->execute($sql, array($thread_id));
1622:         } catch (Horde_Db_Exception $e) {
1623:             throw new Agora_Exception($e->getMessage());
1624:         }
1625: 
1626:         return true;
1627:     }
1628: 
1629:     /**
1630:      * Constructs message attachments link.
1631:      *
1632:      * @throws Agora_Exception
1633:      */
1634:     public function getAttachmentLink($message_id)
1635:     {
1636:         if (!$this->allowAttachments()) {
1637:             return '';
1638:         }
1639: 
1640:         $sql = 'SELECT file_id, file_name, file_size, file_type FROM agora_files WHERE message_id = ?';
1641:         try {
1642:             $files = $this->_db->selectAll($sql, array($message_id));
1643:         } catch (Horde_Db_Exception $e) {
1644:             throw new Agora_Exception($e->getMessage());
1645:         }
1646:         if (empty($files)) {
1647:             return $files;
1648:         }
1649: 
1650:         /* Constuct the link with a tooltip for further info on the download. */
1651:         $html = '<br />';
1652:         $view_url = Horde::url('view.php');
1653:         foreach ($files as $file) {
1654:             $mime_icon = $GLOBALS['injector']->getInstance('Horde_Core_Factory_MimeViewer')->getIcon($file['file_type']);
1655:             $title = _("download") . ': ' . $file['file_name'];
1656:             $tooltip = $title . "\n" . sprintf(_("size: %s"), $this->formatSize($file['file_size'])) . "\n" . sprintf(_("type: %s"), $file['file_type']);
1657:             $url = Horde_Util::addParameter($view_url, array('forum_id' => $this->_forum_id,
1658:                                                        'message_id' => $message_id,
1659:                                                        'file_id' => $file['file_id'],
1660:                                                        'file_name' => $file['file_name'],
1661:                                                        'file_type' => $file['file_type']));
1662:             $html .= Horde::linkTooltip($url, $title, '', '', '', $tooltip) .
1663:                      Horde::img($mime_icon, $title, 'align="middle"', '') . '&nbsp;' . $file['file_name'] . '</a>&nbsp;&nbsp;<br />';
1664:         }
1665: 
1666:         return $html;
1667:     }
1668: 
1669:     /**
1670:      * Formats file size.
1671:      *
1672:      * @param int $filesize
1673:      *
1674:      * @return string  Formatted filesize.
1675:      */
1676:     public function formatSize($filesize)
1677:     {
1678:         $units = array('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
1679:         $pass = 0; // set zero, for Bytes
1680:         while($filesize >= 1024) {
1681:             $filesize /= 1024;
1682:             $pass++;
1683:         }
1684: 
1685:         return round($filesize, 2) . ' ' . $units[$pass];
1686:     }
1687: 
1688: 
1689:     /**
1690:      * Fetches a forum data.
1691:      *
1692:      * @param integer $forum_id  The ID of the forum to fetch.
1693:      *
1694:      * @return array  The forum hash or a PEAR_Error on failure.
1695:      * @throws Horde_Exception_NotFound
1696:      * @throws Agora_Exception
1697:      */
1698:     public function getForum($forum_id = 0)
1699:     {
1700:         if (!$forum_id) {
1701:             $forum_id = $this->_forum_id;
1702:         } elseif ($forum_id instanceof PEAR_Error) {
1703:             return $forum_id;
1704:         }
1705: 
1706:         // Make the requested forum the current forum
1707:         $this->_forum_id = $forum_id;
1708: 
1709:         /* Check if we can read messages in this forum */
1710:         if (!$this->hasPermission(Horde_Perms::SHOW, $forum_id)) {
1711:             return PEAR::raiseError(sprintf(_("You don't have permission to access messages in forum %s."), $forum_id));
1712:         }
1713: 
1714:         $forum = $this->_cache->get('agora_forum_' . $forum_id, $GLOBALS['conf']['cache']['default_lifetime']);
1715:         if ($forum) {
1716:             return unserialize($forum);
1717:         }
1718: 
1719:         $sql = 'SELECT forum_id, forum_name, scope, active, forum_description, '
1720:             . 'forum_parent_id, forum_moderated, forum_attachments, '
1721:             . 'forum_distribution_address, author, message_count, thread_count '
1722:             . 'FROM ' . $this->_forums_table . ' WHERE forum_id = ?';
1723:         try {
1724:             $forum = $this->_db->selectOne($sql, array($forum_id));
1725:         } catch (Horde_Db_Exception $e) {
1726:             throw new Agora_Exception($e->getMessage());
1727:         }
1728:         if (empty($forum)) {
1729:             throw new Horde_Exception_NotFound(sprintf(_("Forum %s does not exist."), $forum_id));
1730:         }
1731: 
1732:         $forum['forum_name'] = $this->convertFromDriver($forum['forum_name']);
1733:         $forum['forum_description'] = $this->convertFromDriver($forum['forum_description']);
1734:         $forum['forum_distribution_address'] = $this->convertFromDriver($forum['forum_distribution_address']);
1735: 
1736:         /* Get moderators */
1737:         $sql = 'SELECT horde_uid FROM agora_moderators WHERE forum_id = ?';
1738:         try {
1739:             $moderators = $this->_db->selectValues($sql, array($forum_id));
1740:         } catch (Horde_Db_Exception $e) {
1741:             throw new Agora_Exception($e->getMessage());
1742:         }
1743:         if (!empty($moderators)) {
1744:             $forum['moderators'] = $moderators;
1745:         }
1746: 
1747:         $this->_cache->set('agora_forum_' . $forum_id, serialize($forum));
1748: 
1749:         return $forum;
1750:     }
1751: 
1752:     /**
1753:      * Returns the number of forums.
1754:      */
1755:     public function countForums()
1756:     {
1757:         $sql = 'SELECT COUNT(*) FROM ' . $this->_forums_table . ' WHERE active = ? AND scope = ?';
1758:         return $this->_db->selectValue($sql, array(1, $this->_scope));
1759:     }
1760: 
1761:     /**
1762:      * Fetches a list of forums.
1763:      *
1764:      * @todo This function needs refactoring, as it doesn't return consistent
1765:      * results. For example when running with $formatted = false it will return
1766:      * an indexed array, but when running with $formatted = true the result is
1767:      * associative array.
1768:      *
1769:      * @param integer $root_forum  The first level forum.
1770:      * @param boolean $formatted   Whether to return the list formatted or raw.
1771:      * @param string  $sort_by     The column to sort by.
1772:      * @param integer $sort_dir    Sort direction, 0 = ascending,
1773:      *                             1 = descending.
1774:      * @param boolean $add_scope   Add parent forum if forum for another
1775:      *                             scopelication.
1776:      * @param string  $from        The forum to start listing at.
1777:      * @param string  $count       The number of forums to return.
1778:      *
1779:      * @return mixed  An array of forums or PEAR_Error on failure.
1780:      * @throws Agora_Exception
1781:      */
1782:     public function getForums($root_forum = 0, $formatted = true,
1783:                        $sort_by = 'forum_name', $sort_dir = 0,
1784:                        $add_scope = false, $from = 0, $count = 0)
1785:     {
1786:         /* Get messages data */
1787:         $forums = $this->_getForums($root_forum, $formatted, $sort_by,
1788:                                     $sort_dir, $add_scope, $from, $count);
1789:         if ($forums instanceof PEAR_Error || empty($forums) || !$formatted) {
1790:             return $forums;
1791:         }
1792: 
1793:         $user = $GLOBALS['registry']->getAuth();
1794:         $edit_url =  Horde::url('messages/edit.php');
1795:         $editforum_url =  Horde::url('editforum.php');
1796:         $delete_url = Horde::url('deleteforum.php');
1797: 
1798:         foreach ($forums as $key => &$forum) {
1799:             if (!$this->hasPermission(Horde_Perms::SHOW, $forum['forum_id'], $forum['scope'])) {
1800:                 unset($forums[$key]);
1801:                 continue;
1802:             }
1803: 
1804:             $forum['indentn'] =  0;
1805:             $forum['indent'] = '';
1806:             if (!$this->hasPermission(Horde_Perms::READ, $forum['forum_id'], $forum['scope'])) {
1807:                 continue;
1808:             }
1809: 
1810:             $forum['url'] = Agora::setAgoraId($forum['forum_id'], null, Horde::url('threads.php'), $forum['scope'], true);
1811:             $forum['message_count'] = number_format($forum['message_count']);
1812:             $forum['thread_count'] = number_format($forum['thread_count']);
1813: 
1814:             if ($forum['last_message_id']) {
1815:                 $forum['last_message_date'] = $this->dateFormat($forum['last_message_timestamp']);
1816:                 $forum['last_message_url'] = Agora::setAgoraId($forum['forum_id'], $forum['last_message_id'], Horde::url('messages/index.php'), $forum['scope'], true);
1817:             }
1818: 
1819:             $forum['actions'] = array();
1820: 
1821:             /* Post message button. */
1822:             if ($this->hasPermission(Horde_Perms::EDIT, $forum['forum_id'], $forum['scope'])) {
1823:                 /* New Post forum button. */
1824:                 $url = Agora::setAgoraId($forum['forum_id'], null, $edit_url, $forum['scope'], true);
1825:                 $forum['actions'][] = Horde::link($url, _("Post message")) . _("New Post") . '</a>';
1826: 
1827:                 if ($GLOBALS['registry']->isAdmin(array('permission' => 'agora:admin'))) {
1828:                     /* Edit forum button. */
1829:                     $url = Agora::setAgoraId($forum['forum_id'], null, $editforum_url, $forum['scope'], true);
1830:                     $forum['actions'][] = Horde::link($url, _("Edit forum")) . _("Edit") . '</a>';
1831:                 }
1832:             }
1833: 
1834:             if ($GLOBALS['registry']->isAdmin(array('permission' => 'agora:admin'))) {
1835:                 /* Delete forum button. */
1836:                 $url = Agora::setAgoraId($forum['forum_id'], null, $delete_url, $forum['scope'], true);
1837:                 $forum['actions'][] = Horde::link($url, _("Delete forum")) . _("Delete") . '</a>';
1838:             }
1839: 
1840:             /* User is a moderator */
1841:             if (isset($forum['moderators']) && in_array($user, $forum['moderators'])) {
1842:                 $sql = 'SELECT COUNT(forum_id) FROM ' . $this->_threads_table
1843:                     . ' WHERE forum_id = ? AND approved = ?'
1844:                     . ' GROUP BY forum_id';
1845:                 try {
1846:                     $unapproved = $this->_db->selectValue($sql, array($forum['forum_id'], 0));
1847:                 } catch (Horde_Db_Exception $e) {
1848:                     throw new Agora_Exception($e->getMessage());
1849:                 }
1850: 
1851:                 $url = Horde::link(Horde::url('moderate.php', true), _("Moderate")) . _("Moderate") . '</a>';
1852:                 $forum['actions'][] = $url . ' (' . $unapproved . ')' ;
1853:             }
1854:         }
1855: 
1856:         return $forums;
1857:     }
1858: 
1859:     /**
1860:      * Fetches a list of forums.
1861:      *
1862:      * @param integer $root_forum  The first level forum.
1863:      * @param boolean $formatted   Whether to return the list formatted or raw.
1864:      * @param string  $sort_by     The column to sort by.
1865:      * @param integer $sort_dir    Sort direction, 0 = ascending,
1866:      *                             1 = descending.
1867:      * @param boolean $add_scope   Add parent forum if forum for another
1868:      *                             scopelication.
1869:      * @param string  $from        The forum to start listing at.
1870:      * @param string  $count       The number of forums to return.
1871:      *
1872:      * @return mixed  An array of forums or PEAR_Error on failure.
1873:      */
1874:     protected function _getForums($root_forum = 0, $formatted = true,
1875:                         $sort_by = 'forum_name', $sort_dir = 0,
1876:                         $add_scope = false,  $from = 0, $count = 0)
1877:     {
1878:         return array();
1879:     }
1880: 
1881:     /**
1882:      * Fetches a list of forums.
1883:      *
1884:      * @param integer $forums      Forums to format
1885:      *
1886:      * @return array  An array of forums.
1887:      * @throws Agora_Exception
1888:      */
1889:     protected function _formatForums($forums)
1890:     {
1891:         /* Get moderators */
1892:         foreach ($forums as $forum) {
1893:             $forums_list[] = $forum['forum_id'];
1894:         }
1895:         $sql = 'SELECT forum_id, horde_uid'
1896:             . ' FROM agora_moderators WHERE forum_id IN (' . implode(',', array_values($forums_list)) . ')';
1897:         try {
1898:             $moderators = $this->_db->selectAll($sql);
1899:         } catch (Horde_Db_Exception $e) {
1900:             throw new Agora_Exception($e->getMessage());
1901:         }
1902: 
1903:         foreach ($forums as $key => $forum) {
1904:             $forums[$key]['forum_name'] = $this->convertFromDriver($forums[$key]['forum_name']);
1905:             $forums[$key]['forum_description'] = $this->convertFromDriver($forums[$key]['forum_description']);
1906:             foreach ($moderators as $moderator) {
1907:                 if ($moderator['forum_id'] == $forum['forum_id']) {
1908:                     $forums[$key]['moderators'][] = $moderator['horde_uid'];
1909:                 }
1910:             }
1911:         }
1912: 
1913:         return $forums;
1914:     }
1915: 
1916:     /**
1917:      * Get forums ids and titles
1918:      *
1919:      * @return array  An array of forums and form names.
1920:      */
1921:     public function getBareForums()
1922:     {
1923:         return array();
1924:     }
1925: 
1926:     /**
1927:      * Creates a new forum.
1928:      *
1929:      * @param string $forum_name  Forum name.
1930:      * @param string $forum_owner Forum owner.
1931:      *
1932:      * @return integer ID of the new generated forum.
1933:      * @throws Agora_Exception
1934:      */
1935:     public function newForum($forum_name, $owner)
1936:     {
1937:         if (empty($forum_name)) {
1938:             throw new Agora_Exception(_("Cannot create a forum with an empty name."));
1939:         }
1940: 
1941:         $sql = 'INSERT INTO ' . $this->_forums_table . ' (scope, forum_name, active, author) VALUES (?, ?, ?, ?)';
1942:         $values = array($this->_scope, $this->convertToDriver($forum_name), 1, $owner);
1943:         try {
1944:             $forum_id = $this->_db->insert($sql, $values);
1945:         } catch (Horde_Db_Exception $e) {
1946:             throw new Agora_Exception($e->getMessage());
1947:         }
1948: 
1949:         return $forum_id;
1950:     }
1951: 
1952:     /**
1953:      * Saves a forum, either creating one if no forum ID is given or updating
1954:      * an existing one.
1955:      *
1956:      * @param array $info  The forum information to save consisting of:
1957:      *                       forum_id
1958:      *                       forum_author
1959:      *                       forum_parent_id
1960:      *                       forum_name
1961:      *                       forum_moderated
1962:      *                       forum_description
1963:      *                       forum_attachments
1964:      *
1965:      * @return integer  The forum ID on success.
1966:      * @throws Agora_Exception
1967:      */
1968:     public function saveForum($info)
1969:     {
1970:         if (empty($info['forum_id'])) {
1971:             if (empty($info['author'])) {
1972:                 $info['author'] = $GLOBALS['registry']->getAuth();
1973:             }
1974:             $info['forum_id'] = $this->newForum($info['forum_name'], $info['author']);
1975:         }
1976: 
1977:         $sql = 'UPDATE ' . $this->_forums_table . ' SET forum_name = ?, forum_parent_id = ?, '
1978:              . 'forum_description = ?, forum_moderated = ?, '
1979:              . 'forum_attachments = ?, forum_distribution_address = ? '
1980:              . 'WHERE forum_id = ?';
1981: 
1982:         $values = array($this->convertToDriver($info['forum_name']),
1983:                         (int)$info['forum_parent_id'],
1984:                         $this->convertToDriver($info['forum_description']),
1985:                         (int)$info['forum_moderated'],
1986:                         isset($info['forum_attachments']) ? (int)$info['forum_attachments'] : abs($GLOBALS['conf']['forums']['enable_attachments']),
1987:                         isset($info['forum_distribution_address']) ? $info['forum_distribution_address'] : '',
1988:                         $info['forum_id']);
1989: 
1990:         try {
1991:             $this->_db->execute($sql, $values);
1992:         } catch (Horde_Db_Exception $e) {
1993:             throw new Agora_Exception($e->getMessage());
1994:         }
1995: 
1996:         $this->_updateCacheState(0);
1997:         $this->_cache->expire('agora_forum_' . $info['forum_id'], $GLOBALS['conf']['cache']['default_lifetime']);
1998: 
1999:         return $info['forum_id'];
2000:     }
2001: 
2002:     /**
2003:      * Deletes a forum, any subforums that are present and all messages
2004:      * contained in the forum and subforums.
2005:      *
2006:      * @param integer $forum_id  The ID of the forum to delete.
2007:      *
2008:      * @return boolean  True on success.
2009:      * @throws Agora_Exception
2010:      */
2011:     public function deleteForum($forum_id)
2012:     {
2013:         $this->deleteThread();
2014: 
2015:         /* Delete the forum itself. */
2016:         try {
2017:             $this->_db->delete('DELETE FROM ' . $this->_forums_table . ' WHERE forum_id = ' . (int)$forum_id);
2018:         } catch (Horde_Db_Exception $e) {
2019:             throw new Agora_Exception($e->getMessage());
2020:         }
2021: 
2022:         return true;
2023:     }
2024: 
2025:     /**
2026:      * Searches forums for matching threads or posts.
2027:      *
2028:      * @param array $filter  Hash of filter criteria:
2029:      *          'forums'         => Array of forum IDs to search.  If not
2030:      *                              present, searches all forums.
2031:      *          'keywords'       => Array of keywords to search for.  If not
2032:      *                              present, finds all posts/threads.
2033:      *          'allkeywords'    => Boolean specifying whether to find all
2034:      *                              keywords; otherwise, wants any keyword.
2035:      *                              False if not supplied.
2036:      *          'message_author' => Name of author to find posts by.  If not
2037:      *                              present, any author.
2038:      *          'searchsubjects' => Boolean specifying whether to search
2039:      *                              subjects.  True if not supplied.
2040:      *          'searchcontents' => Boolean specifying whether to search
2041:      *                              post contents.  False if not supplied.
2042:      * @param string  $sort_by       The column by which to sort.
2043:      * @param integer $sort_dir      The direction by which to sort:
2044:      *                                   0 - ascending
2045:      *                                   1 - descending
2046:      * @param string  $from          The thread to start listing at.
2047:      * @param string  $count         The number of threads to return.
2048:      *
2049:      * @return array  A search result hash where:
2050:      *          'results'        => Array of messages.
2051:      *          'total           => Total message number.
2052:      * @throws Agora_Exception
2053:      */
2054:     public function search($filter, $sort_by = 'message_subject', $sort_dir = 0,
2055:                     $from = 0, $count = 0)
2056:     {
2057:         if (!isset($filter['allkeywords'])) {
2058:             $filter['allkeywords'] = false;
2059:         }
2060:         if (!isset($filter['searchsubjects'])) {
2061:             $filter['searchsubjects'] = true;
2062:         }
2063:         if (!isset($filter['searchcontents'])) {
2064:             $filter['searchcontents'] = false;
2065:         }
2066: 
2067:         /* Select forums ids to search in */
2068:         $sql = 'SELECT forum_id, forum_name FROM ' . $this->_forums_table . ' WHERE ';
2069:         if (empty($filter['forums'])) {
2070:             $sql .= ' active = ? AND scope = ?';
2071:             $values = array(1, $this->_scope);
2072:         } else {
2073:             $sql .= ' forum_id IN (' . implode(',', $filter['forums']) . ')';
2074:             $values = array();
2075:         }
2076:         try {
2077:             $forums = $this->_db->selectAssoc($sql, $values);
2078:         } catch (Horde_Db_Exception $e) {
2079:             throw new Agora_Exception($e->getMessage());
2080:         }
2081: 
2082:         /* Build query  */
2083:         $sql = ' FROM ' . $this->_threads_table . ' WHERE forum_id IN (' . implode(',', array_keys($forums)) . ')';
2084: 
2085:         if (!empty($filter['keywords'])) {
2086:             $sql .= ' AND (';
2087:             if ($filter['searchsubjects']) {
2088:                 $keywords = '';
2089:                 foreach ($filter['keywords'] as $keyword) {
2090:                     if (!empty($keywords)) {
2091:                         $keywords .= $filter['allkeywords'] ? ' AND ' : ' OR ';
2092:                     }
2093:                     $keywords .= 'message_subject LIKE ' . $this->_db->quote('%' . $keyword . '%');
2094:                 }
2095:                 $sql .= '(' . $keywords . ')';
2096:             }
2097:             if ($filter['searchcontents']) {
2098:                 if ($filter['searchsubjects']) {
2099:                     $sql .= ' OR ';
2100:                 }
2101:                 $keywords = '';
2102:                 foreach ($filter['keywords'] as $keyword) {
2103:                     if (!empty($keywords)) {
2104:                         $keywords .= $filter['allkeywords'] ? ' AND ' : ' OR ';
2105:                     }
2106:                     $keywords .= 'body LIKE ' . $this->_db->quote('%' . $keyword . '%');
2107:                 }
2108:                 $sql .= '(' . $keywords . ')';
2109:             }
2110:             $sql .= ')';
2111:         }
2112: 
2113:         if (!empty($filter['author'])) {
2114:             $sql .= ' AND message_author = ' . $this->_db->quote(Horde_String::lower($filter['author']));
2115:         }
2116: 
2117:         /* Sort by result column. */
2118:         $sql .= ' ORDER BY ' . $sort_by . ' ' . ($sort_dir ? 'DESC' : 'ASC');
2119: 
2120:         /* Slice directly in DB. */
2121:         if ($count) {
2122:             $total = $this->_db->selectValue('SELECT COUNT(*) '  . $sql);
2123:             $sql = $this->_db->addLimitOffset($sql, array('limit' => $count, 'offset' => $from));
2124:         }
2125: 
2126:         $sql = 'SELECT message_id, forum_id, message_subject, message_author, message_timestamp '  . $sql;
2127:         try {
2128:             $messages = $this->_db->select($sql);
2129:         } catch (Horde_Db_Exception $e) {
2130:             throw new Agora_Exception($e->getMessage());
2131:         }
2132:         if (empty($messages)) {
2133:             return array('results' => array(), 'total' => 0);
2134:         }
2135: 
2136:         $results = array();
2137:         $msg_url = Horde::url('messages/index.php');
2138:         $forum_url = Horde::url('threads.php');
2139:         while ($message = $messages->fetch()) {
2140:             if (!isset($results[$message['forum_id']])) {
2141:                 $index = array('agora' => $message['forum_id'], 'scope' => $this->_scope);
2142:                 $results[$message['forum_id']] = array('forum_id'   => $message['forum_id'],
2143:                                                        'forum_url'  => Horde_Util::addParameter($forum_url, $index),
2144:                                                        'forum_name' => $this->convertFromDriver($forums[$message['forum_id']]),
2145:                                                        'messages'   => array());
2146:             }
2147:             $index = array('agora' => $message['forum_id']. '.' . $message['message_id'], 'scope' => $this->_scope);
2148:             $results[$message['forum_id']]['messages'][] = array(
2149:                 'message_id' => $message['message_id'],
2150:                 'message_subject' => htmlspecialchars($this->convertFromDriver($message['message_subject'])),
2151:                 'message_author' => $message['message_author'],
2152:                 'message_date' => $this->dateFormat($message['message_timestamp']),
2153:                 'message_url' => Horde_Util::addParameter($msg_url, $index));
2154:         }
2155: 
2156:         return array('results' => $results, 'total' => $total);
2157:     }
2158: 
2159:     /**
2160:      * Finds out if the user has the specified rights to the messages forum.
2161:      *
2162:      * @param integer $perm      The permission level needed for access.
2163:      * @param integer $forum_id  Forum to check permissions for.
2164:      * @param string $scope      Application scope to use.
2165:      *
2166:      * @return boolean  True if the user has the specified permissions.
2167:      */
2168:     public function hasPermission($perm = Horde_Perms::READ, $forum_id = null, $scope = null)
2169:     {
2170:         // Allow all admins
2171:         if (($forum_id === null && isset($this->_forum['author']) && $this->_forum['author'] == $GLOBALS['registry']->getAuth()) ||
2172:             $GLOBALS['registry']->isAdmin(array('permission' => 'agora:admin'))) {
2173:             return true;
2174:         }
2175: 
2176:         // Allow forum author
2177:         if ($forum_id === null) {
2178:             $forum_id = $this->_forum_id;
2179:         }
2180: 
2181:         if ($scope === null) {
2182:             $scope = $this->_scope;
2183:         }
2184: 
2185:         $perms = $GLOBALS['injector']->getInstance('Horde_Perms');
2186:         if (!$perms->exists('agora:forums:' . $scope) &&
2187:             !$perms->exists('agora:forums:' . $scope . ':' . $forum_id)) {
2188:             return ($perm & Horde_Perms::DELETE) ? false : true;
2189:         }
2190: 
2191:         return $perms->hasPermission('agora:forums:' . $scope, $GLOBALS['registry']->getAuth(), $perm) ||
2192:             $perms->hasPermission('agora:forums:' . $scope . ':' . $forum_id, $GLOBALS['registry']->getAuth(), $perm);
2193:     }
2194: 
2195:     /**
2196:      * Converts a value from the driver's charset to the default charset.
2197:      *
2198:      * @param mixed $value  A value to convert.
2199:      *
2200:      * @return mixed  The converted value.
2201:      */
2202:     public function convertFromDriver($value)
2203:     {
2204:         return Horde_String::convertCharset($value, $this->_charset, 'UTF-8');
2205:     }
2206: 
2207:     /**
2208:      * Converts a value from the default charset to the driver's charset.
2209:      *
2210:      * @param mixed $value  A value to convert.
2211:      *
2212:      * @return mixed  The converted value.
2213:      */
2214:     public function convertToDriver($value)
2215:     {
2216:         return Horde_String::convertCharset($value, 'UTF-8', $this->_charset);
2217:     }
2218: 
2219:     /**
2220:      * Increment namespace
2221:      */
2222:     private function _updateCacheState($thread)
2223:     {
2224:         if (is_array($thread)) {
2225:             foreach ($thread as $id) {
2226:                 $key = 'prefix_' . $this->_forum_id . '_' . $id;
2227:                 $prefix = $this->_cache->get($key, $GLOBALS['conf']['cache']['default_lifetime']);
2228:                 if ($prefix) {
2229:                     $this->_cache->set($key, $prefix + 1);
2230:                 }
2231:             }
2232:         } else {
2233:             $key = 'prefix_' . $this->_forum_id . '_' . $thread;
2234:             $prefix = $this->_cache->get($key, $GLOBALS['conf']['cache']['default_lifetime']);
2235:             if ($prefix) {
2236:                 $this->_cache->set($key, $prefix + 1);
2237:             } else {
2238:                 $this->_cache->set($key, 2);
2239:             }
2240:         }
2241:     }
2242: 
2243:     /**
2244:      * Append namespace to cache key
2245:      */
2246:     private function _getCacheKey($key, $thread = 0)
2247:     {
2248:         static $prefix;
2249: 
2250:         if ($prefix == null) {
2251:             $prefix = $this->_cache->get('prefix_' . $this->_forum_id . '_' . $thread,
2252:                                         $GLOBALS['conf']['cache']['default_lifetime']);
2253:             if (!$prefix) {
2254:                 $prefix = '1';
2255:             }
2256:         }
2257: 
2258:         return 's_' . $prefix . '_' . $thread . '_' . $key;
2259:     }
2260: 
2261:     /**
2262:      * Get cache value
2263:      */
2264:     protected function _getCache($key, $thread = 0)
2265:     {
2266:         $key = $this->_getCacheKey($key, $thread);
2267: 
2268:         return $this->_cache->get($key, $GLOBALS['conf']['cache']['default_lifetime']);
2269:     }
2270: 
2271:     /**
2272:      * Set cache value
2273:      */
2274:     protected function _setCache($key, $value, $thread = 0)
2275:     {
2276:         $key = $this->_getCacheKey($key, $thread);
2277: 
2278:         return $this->_cache->set($key, $value);
2279:     }
2280: }
2281: 
API documentation generated by ApiGen