summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoruckelman <uckelman@nomic.net>2010-10-31 20:53:21 +0000
committeruckelman <uckelman@nomic.net>2010-10-31 20:53:21 +0000
commit88d4c55ec38c68c07496ed059616b15622fc83e7 (patch)
treed54d2449e66d359cac3d4bc6171e072c4832b1f8
parent54551bc4cb0dee8549c229d18fca3bf3dc0d5c48 (diff)
Major refactoring to make bridge easier to test.
git-svn-id: https://vassalengine.svn.sourceforge.net/svnroot/vassalengine/site-src/trunk@7431 67b53d14-2c14-4ace-a08f-0dab2b34000c
-rw-r--r--src/Bridge.php159
-rw-r--r--src/BridgeImpl.php180
-rw-r--r--src/MailmanToPhpBB3.php108
-rw-r--r--src/PhpBB3.php340
-rw-r--r--src/PhpBB3Impl.php364
-rw-r--r--src/PhpBB3ToMailman.php175
-rw-r--r--src/Util.php12
-rw-r--r--src/build_email.php140
-rw-r--r--src/forum_post_delete.php7
-rw-r--r--src/forum_post_send.php236
-rw-r--r--src/list_post_receive.php90
-rw-r--r--test/UtilTest.php48
-rw-r--r--test/build_email_test.php150
13 files changed, 1218 insertions, 791 deletions
diff --git a/src/Bridge.php b/src/Bridge.php
index dd0a1f1..a84c304 100644
--- a/src/Bridge.php
+++ b/src/Bridge.php
@@ -20,163 +20,26 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-require_once(__DIR__ . '/BridgeConf.php');
-require_once(__DIR__ . '/Util.php');
+interface Bridge {
+ public function getPostId($messageId);
-class Bridge {
- protected $db;
+ public function getMessageId($postId);
- public function __construct($db = FALSE) {
- $this->db = $db ? $db :
- new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB, DB_USER, DB_PASS);
+ public function setPostId($messageId, $postId);
- $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
- }
+ public function getDefaultForumId($list);
- public function getPostId($messageId) {
- throw_if_null($messageId);
+ public function getLists($forumId);
- $sql = 'SELECT post_id FROM posts ' .
- 'WHERE message_id = ' . $this->db->quote($messageId);
+ public function reserveEditId($postId);
- $row = $this->get_exactly_one_row($sql);
- return $row ? $row['post_id'] : false;
- }
+ public function registerByEditId($editId, $messageId, $inReplyTo);
- public function getMessageId($postId) {
- throw_if_null($postId);
+ public function registerByMessageId($messageId, $inReplyTo);
- $sql = 'SELECT p1.message_id FROM posts AS p1 ' .
- 'LEFT OUTER JOIN posts AS p2 ON (' .
- 'p1.post_id = p2.post_id AND ' .
- 'p1.edit_id < p2.edit_id' .
- ') WHERE p1.post_id = ' . $postId . ' AND ' .
- 'p2.post_id IS NULL';
+ public function unregisterMessage($editId);
- $row = $this->get_exactly_one_row($sql);
- return $row ? $row['message_id'] : false;
- }
-
- public function setPostId($messageId, $postId) {
- throw_if_null($messageId);
- throw_if_null($postId);
-
- $sql = 'UPDATE posts SET ' .
- 'post_id = ' . $postId . ' ' .
- 'WHERE message_id = ' . $this->db->quote($messageId);
-
- $count = $this->db->exec($sql);
-
- if ($count != 1) {
- throw new Exception('Failed to set post id: ' . $messageId);
- }
- }
-
- public function getDefaultForumId($list) {
- throw_if_null($list);
-
- $sql = 'SELECT forum_id FROM forums ' .
- 'WHERE list_name = ' . $this->db->quote($list);
-
- $row = $this->get_exactly_one_row($sql);
- return $row ? $row['forum_id'] : false;
- }
-
- public function getLists($forumId) {
- throw_if_null($forumId);
-
- $sql = 'SELECT list_name FROM lists ' .
- 'WHERE forum_id = ' . $forumId;
-
- $result = $this->db->query($sql);
-
- $rows = $result->fetchAll(PDO::FETCH_COLUMN);
- $result->closeCursor();
- return $rows;
- }
-
- public function reserveEditId($postId) {
- throw_if_null($postId);
-
- $sql = 'INSERT INTO posts (post_id) VALUES (' . $postId . ')';
-
- $count = $this->db->exec($sql);
- if ($count != 1) {
- throw new Exception('Failed to register post id: ' . $postId);
- }
-
- return $this->db->lastInsertId();
- }
-
- public function registerByEditId($editId, $messageId, $inReplyTo) {
- throw_if_null($messageId);
-
- $sql = 'UPDATE posts SET ' .
- 'message_id = ' . $this->db->quote($messageId) . ', ' .
- 'in_reply_to = ' . $this->quote($inReplyTo) . ' ' .
- 'WHERE edit_id = ' . $editId;
-
- $count = $this->db->exec($sql);
- return $count == 1;
- }
-
- public function registerByMessageId($messageId, $inReplyTo) {
- throw_if_null($messageId);
-
- $sql = 'INSERT IGNORE INTO posts ' .
- '(message_id, in_reply_to) ' .
- 'VALUES (' .
- $this->db->quote($messageId) . ', ' .
- $this->quote($inReplyTo) .
- ')';
-
- $count = $this->db->exec($sql);
- return $count == 1 ? $this->db->lastInsertId() : false;
- }
-
- public function unregisterMessage($editId) {
- throw_if_null($editId);
-
- $sql = 'DELETE FROM posts WHERE edit_id = ' . $editId;
-
- $count = $this->db->exec($sql);
-
- if ($count != 1) {
- throw new Exception('Failed to delete edit id: ' . $editId);
- }
- }
-
- public function removePost($postId) {
- throw_if_null($postId);
-
- $sql = 'DELETE FROM posts WHERE post_id = ' . $postId;
-
- $count = $this->db->exec($sql);
-
- return $count > 0;
- }
-
- protected function get_exactly_one_row($sql) {
- $result = $this->db->query($sql);
-
- $rows = $result->fetchAll(PDO::FETCH_ASSOC);
- $result->closeCursor();
-
- switch (count($rows)) {
- case 0:
- return false;
-
- case 1:
- return $rows[0];
-
- default:
- throw new Exception("Too many rows returned: $sql");
- }
- }
-
- protected function quote($arg) {
- return $arg === null ? 'NULL' : $this->db->quote($arg);
- }
+ public function removePost($postId);
}
?>
diff --git a/src/BridgeImpl.php b/src/BridgeImpl.php
new file mode 100644
index 0000000..df21a89
--- /dev/null
+++ b/src/BridgeImpl.php
@@ -0,0 +1,180 @@
+<?php
+
+#
+# $Id$
+#
+# forum-list bridge
+# Copyright (C) 2010 Joel Uckelman
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+require_once(__DIR__ . '/Bridge.php');
+require_once(__DIR__ . '/Util.php');
+
+class BridgeImpl implements Bridge {
+ protected $db;
+
+ public function __construct(PDO $db) {
+ $this->db = $db;
+ $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ }
+
+ public function getPostId($messageId) {
+ throw_if_null($messageId);
+
+ $sql = 'SELECT post_id FROM posts ' .
+ 'WHERE message_id = ' . $this->db->quote($messageId);
+
+ $row = $this->get_exactly_one_row($sql);
+ return $row ? $row['post_id'] : false;
+ }
+
+ public function getMessageId($postId) {
+ throw_if_null($postId);
+
+ $sql = 'SELECT p1.message_id FROM posts AS p1 ' .
+ 'LEFT OUTER JOIN posts AS p2 ON (' .
+ 'p1.post_id = p2.post_id AND ' .
+ 'p1.edit_id < p2.edit_id' .
+ ') WHERE p1.post_id = ' . $postId . ' AND ' .
+ 'p2.post_id IS NULL';
+
+ $row = $this->get_exactly_one_row($sql);
+ return $row ? $row['message_id'] : false;
+ }
+
+ public function setPostId($messageId, $postId) {
+ throw_if_null($messageId);
+ throw_if_null($postId);
+
+ $sql = 'UPDATE posts SET ' .
+ 'post_id = ' . $postId . ' ' .
+ 'WHERE message_id = ' . $this->db->quote($messageId);
+
+ $count = $this->db->exec($sql);
+
+ if ($count != 1) {
+ throw new Exception('Failed to set post id: ' . $messageId);
+ }
+ }
+
+ public function getDefaultForumId($list) {
+ throw_if_null($list);
+
+ $sql = 'SELECT forum_id FROM forums ' .
+ 'WHERE list_name = ' . $this->db->quote($list);
+
+ $row = $this->get_exactly_one_row($sql);
+ return $row ? $row['forum_id'] : false;
+ }
+
+ public function getLists($forumId) {
+ throw_if_null($forumId);
+
+ $sql = 'SELECT list_name FROM lists ' .
+ 'WHERE forum_id = ' . $forumId;
+
+ $result = $this->db->query($sql);
+
+ $rows = $result->fetchAll(PDO::FETCH_COLUMN);
+ $result->closeCursor();
+ return $rows;
+ }
+
+ public function reserveEditId($postId) {
+ throw_if_null($postId);
+
+ $sql = 'INSERT INTO posts (post_id) VALUES (' . $postId . ')';
+
+ $count = $this->db->exec($sql);
+ if ($count != 1) {
+ throw new Exception('Failed to register post id: ' . $postId);
+ }
+
+ return $this->db->lastInsertId();
+ }
+
+ public function registerByEditId($editId, $messageId, $inReplyTo) {
+ throw_if_null($messageId);
+
+ $sql = 'UPDATE posts SET ' .
+ 'message_id = ' . $this->db->quote($messageId) . ', ' .
+ 'in_reply_to = ' . $this->quote($inReplyTo) . ' ' .
+ 'WHERE edit_id = ' . $editId;
+
+ $count = $this->db->exec($sql);
+ return $count == 1;
+ }
+
+ public function registerByMessageId($messageId, $inReplyTo) {
+ throw_if_null($messageId);
+
+ $sql = 'INSERT IGNORE INTO posts ' .
+ '(message_id, in_reply_to) ' .
+ 'VALUES (' .
+ $this->db->quote($messageId) . ', ' .
+ $this->quote($inReplyTo) .
+ ')';
+
+ $count = $this->db->exec($sql);
+ return $count == 1 ? $this->db->lastInsertId() : false;
+ }
+
+ public function unregisterMessage($editId) {
+ throw_if_null($editId);
+
+ $sql = 'DELETE FROM posts WHERE edit_id = ' . $editId;
+
+ $count = $this->db->exec($sql);
+
+ if ($count != 1) {
+ throw new Exception('Failed to delete edit id: ' . $editId);
+ }
+ }
+
+ public function removePost($postId) {
+ throw_if_null($postId);
+
+ $sql = 'DELETE FROM posts WHERE post_id = ' . $postId;
+
+ $count = $this->db->exec($sql);
+
+ return $count > 0;
+ }
+
+ protected function get_exactly_one_row($sql) {
+ $result = $this->db->query($sql);
+
+ $rows = $result->fetchAll(PDO::FETCH_ASSOC);
+ $result->closeCursor();
+
+ switch (count($rows)) {
+ case 0:
+ return false;
+
+ case 1:
+ return $rows[0];
+
+ default:
+ throw new Exception("Too many rows returned: $sql");
+ }
+ }
+
+ protected function quote($arg) {
+ return $arg === null ? 'NULL' : $this->db->quote($arg);
+ }
+}
+
+?>
diff --git a/src/MailmanToPhpBB3.php b/src/MailmanToPhpBB3.php
new file mode 100644
index 0000000..b4ef325
--- /dev/null
+++ b/src/MailmanToPhpBB3.php
@@ -0,0 +1,108 @@
+<?php
+
+#
+# $Id: Bridge.php 7051 2010-07-29 13:23:57Z uckelman $
+#
+# forum-list bridge
+# Copyright (C) 2010 Joel Uckelman
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+require_once('Log.php');
+
+require_once(__DIR__ . '/Bridge.php');
+require_once(__DIR__ . '/Message.php');
+require_once(__DIR__ . '/PhpBB3.php');
+
+class MailmanToPhpBB3 {
+ protected $bridge;
+ protected $phpbb;
+ protected $logger;
+
+ public function __construct(Bridge $bridge, PhpBB3 $phpbb, Log $logger) {
+ $this->bridge = $bridge;
+ $this->phpbb = $phpbb;
+ $this->logger = $logger;
+ }
+
+ public function process(Message $msg) {
+ $messageId = $msg->getMessageId();
+ $inReplyTo = $msg->getInReplyTo();
+ $rererences = $msg->getReferences();
+ $soruce = $msg->getSource();
+
+ $logger->info($messageId . ' received from ' . $source);
+
+ $editId = $this->bridge->registerByMessageId($messageId, $inReplyTo);
+
+ if ($editId === false) {
+ # This message has already been processed, bail out
+ $logger->info($messageId . ' already seen, skipping');
+ exit;
+ }
+
+ try {
+ $forumId = $topicId = null;
+ $postType = null;
+
+ if ($inReplyTo) {
+ # Possibly a reply to an existing topic
+ $parentId = $this->bridge->getPostId($inReplyTo);
+ if ($parentId === false) {
+ throw new Exception('unrecognized Reply-To: ' . $inReplyTo);
+ }
+
+ $ids = $this->phpbb->getTopicAndForumIds($parentId);
+ if ($ids === false) {
+ throw new Exception('unrecognized parent id: ' . $parentId);
+ }
+
+ # Found the parent's forum and topic, post to those
+ $forumId = $ids['forum_id'];
+ $topicId = $ids['topic_id'];
+ $postType = 'reply';
+
+ $logger->info($messageId . ' replies to ' . $parentId);
+ }
+ else {
+ # A message starting a new topic, post to default forum for its source
+ $forumId = $this->bridge->getDefaultForumId($source);
+ if ($forumId === false) {
+ throw new Exception('unrecognized source: ' . $source);
+ }
+
+ $postType = 'post';
+
+ $logger->info($messageId . ' is a new post');
+ }
+
+ $logger->info(
+ $messageId . ' will be posted to ' . $forumId . ':' . $topicId);
+
+ # Post the message to the forum
+ $postId = $this->phpbb->postMessage($postType, $forumId, $topicId, $msg);
+ $this->bridge->setPostId($messageId, $postId);
+
+ $logger->info($messageId . ' posted as ' . $postId);
+ }
+ catch (Exception $e) {
+ # Bridging failed, unregister message.
+ $this->bridge->unregisterMessage($editId);
+ throw $e;
+ }
+ }
+}
+
+?>
diff --git a/src/PhpBB3.php b/src/PhpBB3.php
index 59f1d43..2f485e2 100644
--- a/src/PhpBB3.php
+++ b/src/PhpBB3.php
@@ -20,345 +20,25 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-require_once(__DIR__ . '/Util.php');
+interface PhpBB3 {
+ public function getUserId($from);
-# phpBB setup
-define('IN_PHPBB', true);
-require_once(__DIR__ . '/PhpBB3Conf.php');
-$phpEx = substr(strrchr(__FILE__, '.'), 1);
-require_once($phpbb_root_path . 'common.' . $phpEx);
-require_once($phpbb_root_path . 'includes/functions.' . $phpEx);
-require_once($phpbb_root_path . 'includes/functions_posting.' . $phpEx);
-require_once($phpbb_root_path . 'includes/functions_user.' . $phpEx);
+ public function getUserName($id);
-class PhpBB3 {
- public function __construct() {
- }
+ public function getTopicAndForumIds($post_id);
- public function getUserId($from) {
- throw_if_null($from);
+ public function forumExists($forumId);
- global $db;
+ public function topicStatus($topicId);
- # NB: There might be multiple user accounts associated with one email
- # address. We can only return one user id, so we decide in favor of
- # the account which was most recently used to visit the forum.
- $sql = 'SELECT u1.user_id FROM ' . USERS_TABLE . ' AS u1 ' .
- 'LEFT OUTER JOIN ' . USERS_TABLE . ' AS u2 ON (' .
- 'u1.user_email = u2.user_email AND ' .
- 'u1.user_lastvisit < u2.user_lastvisit' .
- ') WHERE u1.user_email = "' . $db->sql_escape($from) . '" AND ' .
- 'u2.user_email IS NULL';
+ public function getPostTime($postId);
- $row = $this->get_exactly_one_row($sql);
- return $row ? $row['user_id'] : false;
- }
+ public function getAttachmentData($attachId);
- public function getUserName($id) {
- throw_if_null($id);
-
- # NB: user_get_id_name is pass-by-reference; we copy $id to prevent
- # it from being modified, as we might need it for error messages
- $ids = array($id);
- $err = user_get_id_name($ids, $names);
- if ($err) {
- throw new Exception("Could not resolve user id $id: $err");
- }
-
- if (!isset($names[$id])) {
- throw new Exception("Unknown user id: $id");
- }
-
- return $names[$id];
- }
-
- public function getTopicAndForumIds($post_id) {
- throw_if_null($post_id);
-
- global $db;
-
- $sql = 'SELECT topic_id, forum_id FROM ' . POSTS_TABLE . ' ' .
- 'WHERE post_id = ' . $post_id;
-
- $row = $this->get_exactly_one_row($sql);
- return $row;
- }
-
- public function forumExists($forumId) {
- throw_if_null($forumId);
-
- global $db;
-
- $sql = 'SELECT 1 FROM ' . FORUMS_TABLE . ' ' .
- 'WHERE forum_id = ' . $forumId . ' LIMIT 1';
-
- $result = $db->sql_query($sql);
-
- $rows = $db->sql_fetchrowset($result);
- $db->sql_freeresult($result);
-
- switch (count($rows)) {
- case 0:
- return false;
-
- case 1:
- return true;
-
- default:
- # Should be impossible due to LIMIT 1.
- throw new Exception("Too many rows returned: $sql");
- }
- }
-
- public function topicStatus($topicId) {
- throw_if_null($topicId);
-
- global $db;
-
- $sql = 'SELECT topic_status FROM ' . TOPICS_TABLE . ' ' .
- 'WHERE topic_id = ' . $topicId;
-
- $row = $this->get_exactly_one_row($sql);
- return $row ? $row['topic_status'] : false;
- }
-
- public function getPostTime($postId) {
- throw_if_null($postId);
-
- global $db;
-
- $sql = 'SELECT post_time FROM ' . POSTS_TABLE . ' ' .
- 'WHERE post_id = ' . $postId;
-
- $row = $this->get_exactly_one_row($sql);
- return $row ? $row['post_time'] : false;
- }
-
- public function getAttachmentData($attachId) {
- throw_if_null($attachId);
-
- global $db;
-
- $sql = 'SELECT physical_filename, real_filename, ' .
- 'attach_comment, mimetype ' .
- 'FROM ' . ATTACHMENTS_TABLE . ' ' .
- 'WHERE attach_id = ' . $attachId;
-
- $row = $this->get_exactly_one_row($sql);
- return $row;
- }
-
- public function postMessage($postType, $forumId, $topicId, $msg) {
- throw_if_null($msg);
-
- if ($postType == 'post') {
- # do nothing
- }
- else if ($postType == 'reply') {
- # Check that we're not replying to a locked topic.
- $status = $this->topicStatus($topicId);
- if ($status === false) {
- throw new Exception('topic does not exist: ' . $topicId);
- }
-
- switch ($this->topicStatus($topicId)) {
- case ITEM_UNLOCKED:
- # normal, ok
- break;
- case ITEM_LOCKED:
- throw new Exception('post to locked topic: ' . $topicId);
- break;
- case ITEM_MOVED:
- # Should not happen, since the only topics with this status
- # are new shadow topics created after moves.
- throw new Exception('post to moved topic: ' . $topicId);
- break;
- default:
- # Should not happen.
- throw new Exception('bad topic status: ' . $topicId);
- break;
- }
- }
- else {
- # Should not happen.
- throw new Exception('bad post type: ' . $postType);
- }
-
- if (!$this->forumExists($forumId)) {
- throw new Exception('forum does not exist: ' . $forumId);
- }
-
- $userId = $this->getUserId($msg->getFrom());
- if ($userId === false) {
- throw new Exception('unrecognized email address: ' . $msg->getFrom());
- }
-
- $userName = $this->getUserName($userId);
- if ($userName === false) {
- throw new Exception('unrecognized user id: ' . $userId);
- }
-
- $subject = $msg->getSubject();
- list($message, $attachments) = $msg->getFlattenedParts();
-
-# FIXME: extract the footer pattern into a config file?
- # strip the list footer
- $message = preg_replace("/^_______________________________________________\nmessages mailing list\nmessages@vassalengine.org\nhttp:\/\/www.vassalengine.org\/mailman\/listinfo\/messages.*/ms", '', $message);
-
-# TODO: convert > quoting into BBCode
-
- # handle attachments
- $attachment_data = array();
-
- foreach ($attachments as $a) {
- $attachment_data[] = $this->addAttachment(
- $userId, $a['filename'], $a['comment'], $a['mimetype'], $a['data']
- );
- }
-
- # bring in the PhpBB globals
- global $phpEx, $phpbb_root_path, $user, $auth,
- $template, $cache, $db, $config;
-
- # authenticate ourselves
- $user->session_create($userId);
- $auth->acl($user->data);
-
-# FIXME: strip list and forum tag from subject
- $subject = utf8_normalize_nfc($subject);
- $message = utf8_normalize_nfc($message);
-
- $uid = $bitfield = $options = '';
-
- generate_text_for_storage(
- $subject, $uid, $bitfield, $options, false, false, false
- );
-
- generate_text_for_storage(
- $message, $uid, $bitfield, $options, true, true, true
- );
-
- # build the data array for submit_post
- $postId = null;
-
- $data = array(
- 'forum_id' => $forumId,
- 'topic_id' => &$topicId,
- 'post_id' => &$postId,
- 'icon_id' => false,
-
- 'enable_bbcode' => true,
- 'enable_smilies' => true,
- 'enable_urls' => true,
- 'enable_sig' => true,
-
- 'message' => $message,
- 'message_md5' => md5($message),
-
- 'bbcode_bitfield' => $bitfield,
- 'bbcode_uid' => $uid,
-
- 'post_edit_locked' => 0,
- 'topic_title' => $subject,
- 'notify_set' => false,
- 'notify' => false,
- 'post_time' => 0,
- 'forum_name' => '',
- 'enable_indexing' => true
- );
-
- if (!empty($attachment_data)) {
- $data['attachment_data'] = $attachment_data;
- }
-
- $poll = '';
-
- submit_post($postType, $subject, $userName, POST_NORMAL, $poll, $data);
-
- return $postId;
- }
+ public function postMessage($postType, $forumId, $topicId, $msg);
public function addAttachment($userId, $filename, $comment,
- $mimetype, $data) {
- throw_if_null($userId);
- throw_if_null($filename);
- throw_if_null($mimetype);
- throw_if_null($data);
-
- global $db;
-
-# TODO: check that attachment is a permissible type, size
-
- # lifted from include/functions_upload.php: filespec::clean_filename()
- $physicalFilename = $userId . '_' . md5(unique_id());
-
- # get extension
- $dot = strrpos($filename, '.');
- $extension = $dot === false ? '' : substr($filename, $dot + 1);
-
- # put the attachment data into the db
- $sql = 'INSERT INTO ' . ATTACHMENTS_TABLE . ' (' .
- 'poster_id, is_orphan, physical_filename, real_filename, ' .
- 'attach_comment, extension, mimetype, filesize, filetime' .
- ') VALUES (' .
- $userId . ', ' .
- '1, ' .
- '"' . $physicalFilename . '", ' .
- '"' . $db->sql_escape($filename) . '", ' .
- '"' . $db->sql_escape($comment) . '", ' .
- '"' . $db->sql_escape($extension) . '", ' .
- '"' . $db->sql_escape($mimetype) . '", ' .
- strlen($data) . ', ' .
- time() .
- ')';
-
- $db->sql_query($sql);
-
- if ($db->sql_affectedrows() != 1) {
- throw new Exception("Adding attachment failed: $sql");
- }
-
- # post the attachment data to our attachment writer shim
- require_once(__DIR__ . '/HTTP_POST_multipart.php');
-
- $url = 'http://www.test.nomic.net/forum/attachment_writer.php';
- $poster = new HTTP_POST_multipart();
- $poster->addData('password', '5rnudbp7dLkijcwrT@sz');
- $poster->addFile(1, $physicalFilename, $mimetype, null, 'binary', $data);
- $result = $poster->post($url);
-
- if ($result != 1) {
- throw new Exception('Attachment writer failed: ' . $result);
- }
-
- # return the attachment info needed by submit_post
- return array(
- 'attach_id' => $db->sql_nextid(),
- 'is_orphan' => 1,
- 'real_filename' => $realFilename,
- 'attach_comment' => $comment,
- );
- }
-
- protected function get_exactly_one_row($sql) {
- global $db;
-
- $result = $db->sql_query($sql);
-
- $rows = $db->sql_fetchrowset($result);
- $db->sql_freeresult($result);
-
- switch (count($rows)) {
- case 0:
- return false;
-
- case 1:
- return $rows[0];
-
- default:
- throw new Exception("Too many rows returned: $sql");
- }
- }
+ $mimetype, $data);
}
?>
diff --git a/src/PhpBB3Impl.php b/src/PhpBB3Impl.php
new file mode 100644
index 0000000..9986a6a
--- /dev/null
+++ b/src/PhpBB3Impl.php
@@ -0,0 +1,364 @@
+<?php
+
+#
+# $Id$
+#
+# forum-list bridge
+# Copyright (C) 2010 Joel Uckelman
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+require_once(__DIR__ . '/PhpBB3.php');
+require_once(__DIR__ . '/Util.php');
+
+# phpBB setup
+define('IN_PHPBB', true);
+$phpEx = substr(strrchr(__FILE__, '.'), 1);
+require_once($phpbb_root_path . 'common.' . $phpEx);
+require_once($phpbb_root_path . 'includes/functions.' . $phpEx);
+require_once($phpbb_root_path . 'includes/functions_posting.' . $phpEx);
+require_once($phpbb_root_path . 'includes/functions_user.' . $phpEx);
+
+class PhpBB3Impl implements PhpBB3 {
+ public function __construct() {
+ }
+
+ public function getUserId($from) {
+ throw_if_null($from);
+
+ global $db;
+
+ # NB: There might be multiple user accounts associated with one email
+ # address. We can only return one user id, so we decide in favor of
+ # the account which was most recently used to visit the forum.
+ $sql = 'SELECT u1.user_id FROM ' . USERS_TABLE . ' AS u1 ' .
+ 'LEFT OUTER JOIN ' . USERS_TABLE . ' AS u2 ON (' .
+ 'u1.user_email = u2.user_email AND ' .
+ 'u1.user_lastvisit < u2.user_lastvisit' .
+ ') WHERE u1.user_email = "' . $db->sql_escape($from) . '" AND ' .
+ 'u2.user_email IS NULL';
+
+ $row = $this->get_exactly_one_row($sql);
+ return $row ? $row['user_id'] : false;
+ }
+
+ public function getUserName($id) {
+ throw_if_null($id);
+
+ # NB: user_get_id_name is pass-by-reference; we copy $id to prevent
+ # it from being modified, as we might need it for error messages
+ $ids = array($id);
+ $err = user_get_id_name($ids, $names);
+ if ($err) {
+ throw new Exception("Could not resolve user id $id: $err");
+ }
+
+ if (!isset($names[$id])) {
+ throw new Exception("Unknown user id: $id");
+ }
+
+ return $names[$id];
+ }
+
+ public function getTopicAndForumIds($post_id) {
+ throw_if_null($post_id);
+
+ global $db;
+
+ $sql = 'SELECT topic_id, forum_id FROM ' . POSTS_TABLE . ' ' .
+ 'WHERE post_id = ' . $post_id;
+
+ $row = $this->get_exactly_one_row($sql);
+ return $row;
+ }
+
+ public function forumExists($forumId) {
+ throw_if_null($forumId);
+
+ global $db;
+
+ $sql = 'SELECT 1 FROM ' . FORUMS_TABLE . ' ' .
+ 'WHERE forum_id = ' . $forumId . ' LIMIT 1';
+
+ $result = $db->sql_query($sql);
+
+ $rows = $db->sql_fetchrowset($result);
+ $db->sql_freeresult($result);
+
+ switch (count($rows)) {
+ case 0:
+ return false;
+
+ case 1:
+ return true;
+
+ default:
+ # Should be impossible due to LIMIT 1.
+ throw new Exception("Too many rows returned: $sql");
+ }
+ }
+
+ public function topicStatus($topicId) {
+ throw_if_null($topicId);
+
+ global $db;
+
+ $sql = 'SELECT topic_status FROM ' . TOPICS_TABLE . ' ' .
+ 'WHERE topic_id = ' . $topicId;
+
+ $row = $this->get_exactly_one_row($sql);
+ return $row ? $row['topic_status'] : false;
+ }
+
+ public function getPostTime($postId) {
+ throw_if_null($postId);
+
+ global $db;
+
+ $sql = 'SELECT post_time FROM ' . POSTS_TABLE . ' ' .
+ 'WHERE post_id = ' . $postId;
+
+ $row = $this->get_exactly_one_row($sql);
+ return $row ? $row['post_time'] : false;
+ }
+
+ public function getAttachmentData($attachId) {
+ throw_if_null($attachId);
+
+ global $db;
+
+ $sql = 'SELECT physical_filename, real_filename, ' .
+ 'attach_comment, mimetype ' .
+ 'FROM ' . ATTACHMENTS_TABLE . ' ' .
+ 'WHERE attach_id = ' . $attachId;
+
+ $row = $this->get_exactly_one_row($sql);
+ return $row;
+ }
+
+ public function postMessage($postType, $forumId, $topicId, $msg) {
+ throw_if_null($msg);
+
+ if ($postType == 'post') {
+ # do nothing
+ }
+ else if ($postType == 'reply') {
+ # Check that we're not replying to a locked topic.
+ $status = $this->topicStatus($topicId);
+ if ($status === false) {
+ throw new Exception('topic does not exist: ' . $topicId);
+ }
+
+ switch ($this->topicStatus($topicId)) {
+ case ITEM_UNLOCKED:
+ # normal, ok
+ break;
+ case ITEM_LOCKED:
+ throw new Exception('post to locked topic: ' . $topicId);
+ break;
+ case ITEM_MOVED:
+ # Should not happen, since the only topics with this status
+ # are new shadow topics created after moves.
+ throw new Exception('post to moved topic: ' . $topicId);
+ break;
+ default:
+ # Should not happen.
+ throw new Exception('bad topic status: ' . $topicId);
+ break;
+ }
+ }
+ else {
+ # Should not happen.
+ throw new Exception('bad post type: ' . $postType);
+ }
+
+ if (!$this->forumExists($forumId)) {
+ throw new Exception('forum does not exist: ' . $forumId);
+ }
+
+ $userId = $this->getUserId($msg->getFrom());
+ if ($userId === false) {
+ throw new Exception('unrecognized email address: ' . $msg->getFrom());
+ }
+
+ $userName = $this->getUserName($userId);
+ if ($userName === false) {
+ throw new Exception('unrecognized user id: ' . $userId);
+ }
+
+ $subject = $msg->getSubject();
+ list($message, $attachments) = $msg->getFlattenedParts();
+
+# FIXME: extract the footer pattern into a config file?
+ # strip the list footer
+ $message = preg_replace("/^_______________________________________________\nmessages mailing list\nmessages@vassalengine.org\nhttp:\/\/www.vassalengine.org\/mailman\/listinfo\/messages.*/ms", '', $message);
+
+# TODO: convert > quoting into BBCode
+
+ # handle attachments
+ $attachment_data = array();
+
+ foreach ($attachments as $a) {
+ $attachment_data[] = $this->addAttachment(
+ $userId, $a['filename'], $a['comment'], $a['mimetype'], $a['data']
+ );
+ }
+
+ # bring in the PhpBB globals
+ global $phpEx, $phpbb_root_path, $user, $auth,
+ $template, $cache, $db, $config;
+
+ # authenticate ourselves
+ $user->session_create($userId);
+ $auth->acl($user->data);
+
+# FIXME: strip list and forum tag from subject
+ $subject = utf8_normalize_nfc($subject);
+ $message = utf8_normalize_nfc($message);
+
+ $uid = $bitfield = $options = '';
+
+ generate_text_for_storage(
+ $subject, $uid, $bitfield, $options, false, false, false
+ );
+
+ generate_text_for_storage(
+ $message, $uid, $bitfield, $options, true, true, true
+ );
+
+ # build the data array for submit_post
+ $postId = null;
+
+ $data = array(
+ 'forum_id' => $forumId,
+ 'topic_id' => &$topicId,
+ 'post_id' => &$postId,
+ 'icon_id' => false,
+
+ 'enable_bbcode' => true,
+ 'enable_smilies' => true,
+ 'enable_urls' => true,
+ 'enable_sig' => true,
+
+ 'message' => $message,
+ 'message_md5' => md5($message),
+
+ 'bbcode_bitfield' => $bitfield,
+ 'bbcode_uid' => $uid,
+
+ 'post_edit_locked' => 0,
+ 'topic_title' => $subject,
+ 'notify_set' => false,
+ 'notify' => false,
+ 'post_time' => 0,
+ 'forum_name' => '',
+ 'enable_indexing' => true
+ );
+
+ if (!empty($attachment_data)) {
+ $data['attachment_data'] = $attachment_data;
+ }
+
+ $poll = '';
+
+ submit_post($postType, $subject, $userName, POST_NORMAL, $poll, $data);
+
+ return $postId;
+ }
+
+ public function addAttachment($userId, $filename, $comment,
+ $mimetype, $data) {
+ throw_if_null($userId);
+ throw_if_null($filename);
+ throw_if_null($mimetype);
+ throw_if_null($data);
+
+ global $db;
+
+# TODO: check that attachment is a permissible type, size
+
+ # lifted from include/functions_upload.php: filespec::clean_filename()
+ $physicalFilename = $userId . '_' . md5(unique_id());
+
+ # get extension
+ $dot = strrpos($filename, '.');
+ $extension = $dot === false ? '' : substr($filename, $dot + 1);
+
+ # put the attachment data into the db
+ $sql = 'INSERT INTO ' . ATTACHMENTS_TABLE . ' (' .
+ 'poster_id, is_orphan, physical_filename, real_filename, ' .
+ 'attach_comment, extension, mimetype, filesize, filetime' .
+ ') VALUES (' .
+ $userId . ', ' .
+ '1, ' .
+ '"' . $physicalFilename . '", ' .
+ '"' . $db->sql_escape($filename) . '", ' .
+ '"' . $db->sql_escape($comment) . '", ' .
+ '"' . $db->sql_escape($extension) . '", ' .
+ '"' . $db->sql_escape($mimetype) . '", ' .
+ strlen($data) . ', ' .
+ time() .
+ ')';
+
+ $db->sql_query($sql);
+
+ if ($db->sql_affectedrows() != 1) {
+ throw new Exception("Adding attachment failed: $sql");
+ }
+
+ # post the attachment data to our attachment writer shim
+ require_once(__DIR__ . '/HTTP_POST_multipart.php');
+
+ $url = 'http://www.test.nomic.net/forum/attachment_writer.php';
+ $poster = new HTTP_POST_multipart();
+ $poster->addData('password', '5rnudbp7dLkijcwrT@sz');
+ $poster->addFile(1, $physicalFilename, $mimetype, null, 'binary', $data);
+ $result = $poster->post($url);
+
+ if ($result != 1) {
+ throw new Exception('Attachment writer failed: ' . $result);
+ }
+
+ # return the attachment info needed by submit_post
+ return array(
+ 'attach_id' => $db->sql_nextid(),
+ 'is_orphan' => 1,
+ 'real_filename' => $realFilename,
+ 'attach_comment' => $comment,
+ );
+ }
+
+ protected function get_exactly_one_row($sql) {
+ global $db;
+
+ $result = $db->sql_query($sql);
+
+ $rows = $db->sql_fetchrowset($result);
+ $db->sql_freeresult($result);
+
+ switch (count($rows)) {
+ case 0:
+ return false;
+
+ case 1:
+ return $rows[0];
+
+ default:
+ throw new Exception("Too many rows returned: $sql");
+ }
+ }
+}
+
+?>
diff --git a/src/PhpBB3ToMailman.php b/src/PhpBB3ToMailman.php
new file mode 100644
index 0000000..fccd2b4
--- /dev/null
+++ b/src/PhpBB3ToMailman.php
@@ -0,0 +1,175 @@
+<?php
+
+#
+# $Id: Bridge.php 7051 2010-07-29 13:23:57Z uckelman $
+#
+# forum-list bridge
+# Copyright (C) 2010 Joel Uckelman
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+require_once('Log.php');
+require_once('Mail.php');
+
+require_once(__DIR__ . '/BBCodeParser.php');
+require_once(__DIR__ . '/Bridge.php');
+require_once(__DIR__ . '/PhpBB3.php');
+require_once(__DIR__ . '/Util.php');
+require_once(__DIR__ . '/build_email.php');
+
+class PhpBB3ToMailman {
+
+ protected $bridge;
+ protected $phpbb;
+ protected $logger;
+
+ public function __construct(Bridge $bridge, PhpBB3 $phpbb, Log $logger) {
+ $this->bridge = $bridge;
+ $this->phpbb = $phpbb;
+ $this->logger = $logger;
+ }
+
+ public function process($config, $user, $mode, $data, $post_data) {
+ # Sanity check
+ if (!in_array($mode, array('post', 'reply', 'quote', 'edit'))) {
+ throw new Exception('unrecognized mode: ' . $mode);
+ }
+
+ $postId = $data['post_id'];
+ $forumId = $data['forum_id'];
+
+ $this->logger->info($postId . ' received from phpBB forum ' . $forumId);
+
+ $to = $this->bridge->getLists($forumId);
+ if (count($to) == 0) {
+ # No lists to send to, bail out.
+ return;
+ }
+ $to = implode(', ', $to);
+
+ $userName = $user->data['username'];
+ $userEmail = $user->data['user_email'];
+
+ $sender = 'forum-bridge@vassalengine.org';
+
+ $subject = html_entity_decode(
+ '[' . $post_data['forum_name'] . '] ' . $post_data['post_subject'],
+ ENT_QUOTES
+ );
+
+ $time = null;
+ if ($mode == 'edit') {
+ # Post time is NOT updated on edit, so we get the current time
+ $time = time();
+ }
+ else {
+ $time = $this->phpbb->getPostTime($postId);
+ if ($time === false) {
+ throw new Exception('no post time: ' . $postId);
+ }
+ }
+
+ $inReplyTo = null;
+ $references = null;
+
+ if ($mode == 'reply' || $mode == 'quote') {
+ $firstId = $data['topic_first_post_id'];
+ $firstMessageId = $this->bridge->getMessageId($firstId);
+ if ($firstMessageId === false) {
+ $this->logger->info($postId . ' replies to an unknown message');
+ }
+ else {
+ $inReplyTo = $references = $firstMessageId;
+ $this->logger->info($postId . ' replies to ' . $firstMessageId);
+ }
+ }
+ else if ($mode == 'edit') {
+ $inReplyTo = $this->bridge->getMessageId($postId);
+ }
+
+ $forumURL = 'http://' . $_SERVER['SERVER_NAME'] .
+ dirname($_SERVER['SCRIPT_NAME']);
+
+ $editId = $this->bridge->reserveEditId($postId);
+ $messageId = build_message_id($postId, $editId,
+ $time, $_SERVER['SERVER_NAME']);
+
+ # Assemble the message headers
+ $headers = build_headers(
+ $userName,
+ $userEmail,
+ $to,
+ $sender,
+ $subject,
+ $mode == 'edit',
+ $time,
+ $messageId,
+ $forumURL,
+ $inReplyTo,
+ $references
+ );
+
+ # Build the message body
+ $parser = new BBCodeParser();
+ $text = $parser->parse($data['message'], $data['bbcode_uid']);
+ $text = build_text($text, $mode == 'edit');
+
+ # Build the bridge footer
+ $footer = build_footer($postId, $forumURL);
+
+ $attachments = array();
+ foreach ($data['attachment_data'] as $a) {
+ $attachId = $a['attach_id'];
+
+ $adata = $this->phpbb->getAttachmentData($attachId);
+ if ($adata === false) {
+ throw new Exception('unrecognized attachment id: ' . $attachId);
+ }
+
+ $adata['path'] = $phpbb_root_path . $config['upload_path'] . '/' .
+ utf8_basename($adata['physical_filename']);
+
+ $attachments[] = $adata;
+ }
+
+ # Build the message body
+ $body = build_body($headers, $text, $attachments, $footer);
+
+ $mailer = Mail::factory('sendmail');
+
+ # Register the message
+ $seen = !$this->bridge->registerByEditId($editId, $messageId, $inReplyTo);
+ if ($seen) {
+ throw new Exception('message id already seen: ' . $messageId);
+ }
+
+ try {
+ # Send the message
+ $err = $mailer->send($to, $headers, $body);
+ if (PEAR::isError($err)) {
+ throw new Exception('Mail::send error: ' . $err->toString());
+ }
+
+ $this->logger->info($postId . ' sent to ' . $to . ' as ' . $messageId);
+ }
+ catch (Exception $e) {
+ # Bridging failed, unregister message.
+ $this->bridge->unregisterMessage($editId);
+ throw $e;
+ }
+ }
+}
+
+?>
diff --git a/src/Util.php b/src/Util.php
index 2535bc5..0f1499a 100644
--- a/src/Util.php
+++ b/src/Util.php
@@ -24,16 +24,16 @@ function throw_if_null($arg) {
if ($arg === null) throw new Exception('argument is null');
}
-function build_message_id($postId, $editId, $time, $forumHost) {
- return "<$time.$postId.$editId.bridge@$forumHost>";
+function is_ascii($str) {
+ return !preg_match('/[^[:ascii:]]/', $str);
}
-function is_ascii($string) {
- return !preg_match('/[^[:ascii:]]/', $str);
+function utf8_quote($str) {
+ return '=?UTF-8?B?' . base64_encode($str) . '?=';
}
-function utf8_quote($string) {
- return '=?UTF-8?B?' . base64_encode($string) . '?=';
+function utf8_quote_non_ascii($str) {
+ return is_ascii($str) ? $str : utf8_quote($str);
}
?>
diff --git a/src/build_email.php b/src/build_email.php
new file mode 100644
index 0000000..5301106
--- /dev/null
+++ b/src/build_email.php
@@ -0,0 +1,140 @@
+<?php
+
+require_once(__DIR__ . '/Util.php');
+
+function build_text($text, $edit) {
+ if ($edit) {
+ $edit_notice = <<<EOF
+[This message has been edited.]
+
+
+EOF;
+
+ $text = $edit_notice . $text;
+ }
+
+ return $text;
+}
+
+function build_footer($postId, $forumURL) {
+ $postURL = "$forumURL/viewtopic.php?p=$postId#p$postId";
+ $footer = <<<EOF
+
+_______________________________________________
+Read this topic online here:
+$postURL
+EOF;
+
+ return $footer;
+}
+
+function build_body(array &$headers, $text, $attachments, $footer) {
+ $body = null;
+
+ # Handle attachements, if any
+ if (empty($attachments)) {
+ # No attachments, send a plain email
+ $body = $text . "\n" . $footer;
+ $headers['Content-Type'] = 'text/plain; charset=UTF-8; format=flowed';
+ $headers['Content-Transfer-Encoding'] = '8bit';
+ }
+ else {
+ # Attachments, build a MIME email
+ require_once('Mail/mimePart.php');
+
+ $headers['MIME-Version'] = '1.0';
+
+ $params = array('content_type' => 'multipart/mixed');
+ $mime = new Mail_mimePart('', $params);
+
+ # Build the main body
+ build_text_part($mime, $text);
+
+ # Build each attachment
+ foreach ($attachments as $a) {
+ $bytes = file_get_contents($a['path']);
+ if ($bytes === false) {
+ throw new Exception('failed to read file: ' . $a['path']);
+ }
+
+ build_attachment(
+ $mime,
+ $adata['mimetype'],
+ $adata['real_filename'],
+ $adata['attach_comment'],
+ $bytes
+ );
+ }
+
+ # Build footer
+ build_text_part($mime, $footer);
+
+ # Encode the message
+ $msg = $mime->encode();
+ $headers = array_merge($headers, $msg['headers']);
+ $body = $msg['body'];
+ }
+
+ return $body;
+}
+
+function build_headers($userName, $userEmail, $to, $sender, $subject, $edit,
+ $time, $messageId, $forumURL, $inReplyTo, $references) {
+
+ $from = sprintf('%s <%s>', utf8_quote_non_ascii($userName), $userEmail);
+ $subject = utf8_quote_non_ascii($subject);
+ $date = date(DATE_RFC2822, $time);
+
+ if ($edit) {
+ $edit_header = 'Edit: ';
+ $subject = $edit_header . $subject;
+ }
+
+ $headers = array(
+ 'To' => $to,
+ 'From' => $from,
+ 'Sender' => $sender,
+ 'Subject' => $subject,
+ 'Date' => $date,
+ 'Message-ID' => $messageId,
+ 'X-BeenThere' => $forumURL
+ );
+
+ if ($inReplyTo !== null) {
+ $headers['In-Reply-To'] = $inReplyTo;
+ }
+
+ if ($references !== null) {
+ $headers['References'] = $references;
+ }
+
+ return $headers;
+}
+
+function build_text_part(Mail_mimePart $mime, $text) {
+ $params = array(
+ 'content_type' => 'text/plain',
+ 'charset' => 'utf-8',
+ 'encoding' => '8bit',
+ 'disposition' => 'inline'
+ );
+ $mime->addSubPart($text, $params);
+}
+
+function build_attachment(Mail_mimePart $mime, $type,
+ $filename, $descr, $data) {
+ $params = array(
+ 'content_type' => $type,
+ 'encoding' => 'base64',
+ 'disposition' => 'attachment',
+ 'dfilename' => $filename,
+ 'description' => $descr
+ );
+ $mime->addSubPart($data, $params);
+}
+
+function build_message_id($postId, $editId, $time, $forumHost) {
+ return "<$time.$postId.$editId.bridge@$forumHost>";
+}
+
+?>
diff --git a/src/forum_post_delete.php b/src/forum_post_delete.php
index f80b1ad..be3fa0f 100644
--- a/src/forum_post_delete.php
+++ b/src/forum_post_delete.php
@@ -38,9 +38,12 @@ function remove_post($postId) {
require_once('Log.php');
$logger = &Log::singleton('file', '/var/log/listbridge', 'one');
- require_once(__DIR__ . '/Bridge.php');
+ require_once(__DIR__ . '/BridgeConf.php')
+ require_once(__DIR__ . '/BridgeImpl.php');
+
+ $db = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB, DB_USER, DB_PASS);
+ $bridge = new BridgeImpl($db);
- $bridge = new Bridge();
if ($bridge->removePost($postId)) {
$logger->info($postId . ' deleted');
}
diff --git a/src/forum_post_send.php b/src/forum_post_send.php
index ffab780..c2b23bd 100644
--- a/src/forum_post_send.php
+++ b/src/forum_post_send.php
@@ -34,7 +34,6 @@ catch (Exception $e) {
}
function send_post_to_lists($config, $user, $mode, $data, $post_data) {
-
require_once('Log.php');
$logger = &Log::singleton('file', '/var/log/listbridge', 'one');
@@ -45,234 +44,19 @@ function send_post_to_lists($config, $user, $mode, $data, $post_data) {
print '</p>';
*/
- # Sanity check
- if (!in_array($mode, array('post', 'reply', 'quote', 'edit'))) {
- throw new Exception('unrecognized mode: ' . $mode);
- }
-
- require_once('Mail.php');
-
- require_once(__DIR__ . '/BBCodeParser.php');
- require_once(__DIR__ . '/Bridge.php');
- require_once(__DIR__ . '/PhpBB3.php');
- require_once(__DIR__ . '/Util.php');
-
- $postId = $data['post_id'];
- $forumId = $data['forum_id'];
+ require_once(__DIR__ . '/BridgeConf.php');
+ require_once(__DIR__ . '/BridgeImpl.php');
+ require_once(__DIR__ . '/PhpBB3Conf.php');
+ require_once(__DIR__ . '/PhpBB3Impl.php');
+ require_once(__DIR__ . '/PhpBB3ToMailman.php');
- $logger->info($postId . ' received from phpBB forum ' . $forumId);
-
- $bridge = new Bridge();
-
- $to = $bridge->getLists($forumId);
- if (count($to) == 0) {
- # No lists to send to, bail out.
- return;
- }
- $to = implode(', ', $to);
-
- $userName = $user->data['username'];
- $userEmail = $user->data['user_email'];
-
- # NB: Don't use utf8_quote on things which don't need it.
- $from = (is_ascii($userName) ? $userName : utf8_quote($userName)) .
- ' <' . $userEmail . '>';
-
- $sender = 'forum-bridge@vassalengine.org';
-
- $subject = html_entity_decode(
- '[' . $post_data['forum_name'] . '] ' . $post_data['post_subject'],
- ENT_QUOTES
- );
-
- if (!is_ascii($subject)) {
- $subject = utf8_quote($subject);
- }
-
- $phpbb = new PhpBB3();
-
- $time = null;
- if ($mode == 'edit') {
- # Post time is NOT updated on edit, so we get the current time
- $time = time();
- }
- else {
- $time = $phpbb->getPostTime($postId);
- if ($time === false) {
- throw new Exception('no post time: ' . $postId);
- }
- }
-
- $date = date(DATE_RFC2822, $time);
-
- $inReplyTo = null;
- $references = null;
-
- if ($mode == 'reply' || $mode == 'quote') {
- $firstId = $data['topic_first_post_id'];
- $firstMessageId = $bridge->getMessageId($firstId);
- if ($firstMessageId === false) {
- $logger->info($postId . ' replies to an unknown message');
- }
- else {
- $inReplyTo = $references = $firstMessageId;
- $logger->info($postId . ' replies to ' . $firstMessageId);
- }
- }
- else if ($mode == 'edit') {
- $inReplyTo = $bridge->getMessageId($postId);
- }
-
- $forumURL = 'http://' . $_SERVER['SERVER_NAME'] .
- dirname($_SERVER['SCRIPT_NAME']);
-
- $editId = $bridge->reserveEditId($postId);
- $messageId = build_message_id($postId, $editId,
- $time, $_SERVER['SERVER_NAME']);
-
- # Assemble the message headers
- $headers = array(
- 'To' => $to,
- 'From' => $from,
- 'Sender' => $sender,
- 'Subject' => $subject,
- 'Date' => $date,
- 'Message-ID' => $messageId,
- 'X-BeenThere' => $forumURL
- );
-
- if ($inReplyTo !== null) {
- $headers['In-Reply-To'] = $inReplyTo;
- }
-
- if ($references !== null) {
- $headers['References'] = $references;
- }
-
- # Build the message body
- $parser = new BBCodeParser();
- $text = $parser->parse($data['message'], $data['bbcode_uid']);
-
- if ($mode == 'edit') {
- $edit_notice = <<<EOF
-[This message has been edited.]
-
+ $db = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB, DB_USER, DB_PASS);
+ $bridge = new BridgeImpl($db);
-EOF;
-
- $edit_header = 'Edit: ';
-
- $text = $edit_notice . $text;
- $headers['Subject'] = $edit_header . $headers['Subject'];
- }
-
- # Build the bridge footer
- $postURL = "$forumURL/viewtopic.php?p=$postId#p$postId";
- $footer = <<<EOF
-
-_______________________________________________
-Read this topic online here:
-$postURL
-EOF;
-
- $body = null;
-
- # Handle attachements, if any
- if (empty($data['attachment_data'])) {
- # No attachments, send a plain email
- $body = $text . "\n" . $footer;
- $headers['Content-Type'] = 'text/plain; charset=UTF-8; format=flowed';
- $headers['Content-Transfer-Encoding'] = '8bit';
- }
- else {
- # Attachments, build a MIME email
- require_once('Mail/mimePart.php');
-
- $headers['MIME-Version'] = '1.0';
-
- $params = array('content_type' => 'multipart/mixed');
- $mime = new Mail_mimePart('', $params);
-
- # Build the main body
- build_text_part($mime, $text);
-
- # Build each attachment
- foreach ($data['attachment_data'] as $a) {
- $attachId = $a['attach_id'];
- $adata = $phpbb->getAttachmentData($attachId);
- if ($adata === false) {
- throw new Exception('unrecognized attachment id: ' . $attachId);
- }
-
- $afile = $phpbb_root_path . $config['upload_path'] . '/' .
- utf8_basename($adata['physical_filename']);
-
- $bytes = file_get_contents($afile);
- if ($bytes === false) {
- throw new Exception('failed to read file: ' . $afile);
- }
-
- build_attachment(
- $mime,
- $adata['mimetype'],
- $adata['real_filename'],
- $adata['attach_comment'],
- $bytes
- );
- }
-
- # Build footer
- build_text_part($mime, $footer);
-
- # Encode the message
- $msg = $mime->encode();
- $headers = array_merge($headers, $msg['headers']);
- $body = $msg['body'];
- }
-
- $mailer = Mail::factory('sendmail');
-
- # Register the message
- $seen = !$bridge->registerByEditId($editId, $messageId, $inReplyTo);
- if ($seen) {
- throw new Exception('message id already seen: ' . $messageId);
- }
-
- try {
- # Send the message
- $err = $mailer->send($to, $headers, $body);
- if (PEAR::isError($err)) {
- throw new Exception('Mail::send error: ' . $err->toString());
- }
-
- $logger->info($postId . ' sent to ' . $to . ' as ' . $messageId);
- }
- catch (Exception $e) {
- # Bridging failed, unregister message.
- $bridge->unregisterMessage($editId);
- throw $e;
- }
-}
-
-function build_text_part($mime, $text) {
- $params = array(
- 'content_type' => 'text/plain',
- 'charset' => 'utf-8',
- 'encoding' => '8bit',
- 'disposition' => 'inline'
- );
- $mime->addSubPart($text, $params);
-}
+ $phpbb = new PhpBB3Impl();
-function build_attachment($mime, $type, $filename, $descr, $data) {
- $params = array(
- 'content_type' => $type,
- 'encoding' => 'base64',
- 'disposition' => 'attachment',
- 'dfilename' => $filename,
- 'description' => $descr
- );
- $mime->addSubPart($data, $params);
+ $conduit = new PhpBB3ToMailman($bridge, $phpbb, $logger);
+ $conduit->process($config, $user, $mode, $data, $post_data);
}
?>
diff --git a/src/list_post_receive.php b/src/list_post_receive.php
index 2072742..2c5ddc2 100644
--- a/src/list_post_receive.php
+++ b/src/list_post_receive.php
@@ -20,97 +20,29 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-# TODO: logging!
-# TODO: Refactor postMessage().
-
require_once('Log.php');
$logger = &Log::singleton('file', '/var/log/listbridge', 'one');
try {
- require_once('/var/www/bridge/src/Bridge.php');
- require_once('/var/www/bridge/src/MailmanLib.php');
- require_once('/var/www/bridge/src/MailmanMessage.php');
- require_once('/var/www/bridge/src/PhpBB3.php');
-
- # Read the message from STDIN
-# $url = 'php://stdin';
-
-# $input = read_raw_message($url);
-# $msg = new MailmanMessage($input);
-
if (!isset($_POST['message'])) {
throw new Exception('No message in POST');
}
- $msg = new MailmanMessage($_POST['message']);
-
- $messageId = $msg->getMessageId();
- $inReplyTo = $msg->getInReplyTo();
- $rererences = $msg->getReferences();
- $soruce = $msg->getSource();
-
- $logger->info($messageId . ' received from ' . $source);
-
- $bridge = new Bridge();
- $editId = $bridge->registerByMessageId($messageId, $inReplyTo);
-
- if ($editId === false) {
- # This message has already been processed, bail out
- $logger->info($messageId . ' already seen, skipping');
- exit;
- }
-
- try {
- $phpbb = new PhpBB3();
-
- $forumId = $topicId = null;
- $postType = null;
-
- if ($inReplyTo) {
- # Possibly a reply to an existing topic
- $parentId = $bridge->getPostId($inReplyTo);
- if ($parentId === false) {
- throw new Exception('unrecognized Reply-To: ' . $inReplyTo);
- }
+ require_once(__DIR__ . '/BridgeConf.php');
+ require_once(__DIR__ . '/BridgeImpl.php');
+ require_once(__DIR__ . '/PhpBB3Conf.php');
+ require_once(__DIR__ . '/PhpBB3Impl.php');
+ require_once(__DIR__ . '/MailmanToPhpBB3.php');
- $ids = $phpbb->getTopicAndForumIds($parentId);
- if ($ids === false) {
- throw new Exception('unrecognized parent id: ' . $parentId);
- }
-
- # Found the parent's forum and topic, post to those
- $forumId = $ids['forum_id'];
- $topicId = $ids['topic_id'];
- $postType = 'reply';
-
- $logger->info($messageId . ' replies to ' . $parentId);
- }
- else {
- # A message starting a new topic, post to default forum for its source
- $forumId = $bridge->getDefaultForumId($source);
- if ($forumId === false) {
- throw new Exception('unrecognized source: ' . $source);
- }
-
- $postType = 'post';
+ $msg = new MailmanMessage($_POST['message']);
- $logger->info($messageId . ' is a new post');
- }
+ $db = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB, DB_USER, DB_PASS);
+ $bridge = new BridgeImpl($db);
- $logger->info(
- $messageId . ' will be posted to ' . $forumId . ':' . $topicId);
-
- # Post the message to the forum
- $postId = $phpbb->postMessage($postType, $forumId, $topicId, $msg);
- $bridge->setPostId($messageId, $postId);
+ $phpbb = new PhpBB3Impl();
- $logger->info($messageId . ' posted as ' . $postId);
- }
- catch (Exception $e) {
- # Bridging failed, unregister message.
- $bridge->unregisterMessage($editId);
- throw $e;
- }
+ $conduit = new MailmanToPhpBB3($bridge, $phpbb, $logger);
+ $conduit->process($msg);
}
catch (Exception $e) {
$logger->err($e);
diff --git a/test/UtilTest.php b/test/UtilTest.php
new file mode 100644
index 0000000..7135b9f
--- /dev/null
+++ b/test/UtilTest.php
@@ -0,0 +1,48 @@
+<?php
+
+require_once('PHPUnit/Framework.php');
+
+require_once(__DIR__ . '/../src/Util.php');
+
+class UtilTest extends PHPUnit_Framework_TestCase {
+ public function is_ascii_provider() {
+ return array(
+ array('', true),
+ array('foo', true),
+ array('Heizölrückstoßabdämpfung', false)
+ );
+ }
+
+ /** @dataProvider is_ascii_provider */
+ public function test_is_ascii($string, $expected) {
+ $this->assertEquals($expected, is_ascii($string));
+ }
+
+ public function utf8_quote_provider() {
+ return array(
+ array('', '=?UTF-8?B??='),
+ array('foo', '=?UTF-8?B?Zm9v?='),
+ array('Heizölrückstoßabdämpfung', '=?UTF-8?B?SGVpesO2bHLDvGNrc3Rvw59hYmTDpG1wZnVuZw==?=')
+ );
+ }
+
+ /** @dataProvider utf8_quote_provider */
+ public function test_utf8_quote($string, $expected) {
+ $this->assertEquals($expected, utf8_quote($string));
+ }
+
+ public function utf8_quote_non_ascii_provider() {
+ return array(
+ array('', ''),
+ array('foo', 'foo'),
+ array('Heizölrückstoßabdämpfung', '=?UTF-8?B?SGVpesO2bHLDvGNrc3Rvw59hYmTDpG1wZnVuZw==?=')
+ );
+ }
+
+ /** @dataProvider utf8_quote_non_ascii_provider */
+ public function test_utf8_quote_non_ascii($string, $expected) {
+ $this->assertEquals($expected, utf8_quote_non_ascii($string));
+ }
+}
+
+?>
diff --git a/test/build_email_test.php b/test/build_email_test.php
new file mode 100644
index 0000000..c78ab7b
--- /dev/null
+++ b/test/build_email_test.php
@@ -0,0 +1,150 @@
+<?php
+
+require_once('PHPUnit/Framework.php');
+
+require_once(__DIR__ . '/../src/build_email.php');
+
+class build_email_test extends PHPUnit_Framework_TestCase {
+
+ public function test_build_text_edit() {
+ $this->assertEquals(
+ "[This message has been edited.]
+
+foo bar",
+ build_text('foo bar', true)
+ );
+ }
+
+ public function test_build_text_no_edit() {
+ $this->assertEquals(
+ 'foo bar',
+ build_text('foo bar', false)
+ );
+ }
+
+ public function test_build_footer() {
+ $this->assertEquals(
+ "
+_______________________________________________
+Read this topic online here:
+http://www.example.com/viewtopic.php?p=42#p42",
+ build_footer(42, 'http://www.example.com')
+ );
+ }
+
+ protected $default_headers = array(
+ 'To' => 'messages@vassalengine.org',
+ 'From' => 'Joel Uckelman <uckelman@nomic.net>',
+ 'Sender' => 'forum-bridge@vassalengine.org',
+ 'Subject' => 'Test message',
+ 'Date' => 'Sun, 31 Oct 2010 08:46:00 -0700',
+ 'Message-ID' => '<20100302094228.33F0310091@charybdis.ellipsis.cx>',
+ 'X-BeenThere' => 'http://www.example.com',
+ 'In-Reply-To' => '<1267473003.m2f.17543@www.vassalengine.org>',
+ 'References' => '<1267171317.m2f.17507@www.vassalengine.org> <1267473003.m2f.17543@www.vassalengine.org>'
+ );
+
+ protected $default_headers_params = array(
+ 'Joel Uckelman',
+ 'uckelman@nomic.net',
+ 'messages@vassalengine.org',
+ 'forum-bridge@vassalengine.org',
+ 'Test message',
+ false,
+ 1288539960,
+ '<20100302094228.33F0310091@charybdis.ellipsis.cx>',
+ 'http://www.example.com',
+ '<1267473003.m2f.17543@www.vassalengine.org>',
+ '<1267171317.m2f.17507@www.vassalengine.org> <1267473003.m2f.17543@www.vassalengine.org>'
+ );
+
+ protected function call_build_headers(array $headers, array $params) {
+ $this->assertEquals(
+ $headers,
+ build_headers(
+ $params[0],
+ $params[1],
+ $params[2],
+ $params[3],
+ $params[4],
+ $params[5],
+ $params[6],
+ $params[7],
+ $params[8],
+ $params[9],
+ $params[10]
+ )
+ );
+ }
+
+ public function test_build_headers() {
+ $headers = $this->default_headers;
+ $headers_params = $this->default_headers_params;
+ $this->call_build_headers($headers, $headers_params);
+ }
+
+ public function test_build_headers_no_in_reply_to() {
+ $headers = $this->default_headers;
+ $headers_params = $this->default_headers_params;
+
+ unset($headers['In-Reply-To']);
+ $headers_params[9] = null;
+
+ $this->call_build_headers($headers, $headers_params);
+ }
+
+ public function test_build_headers_no_references() {
+ $headers = $this->default_headers;
+ $headers_params = $this->default_headers_params;
+
+ unset($headers['References']);
+ $headers_params[10] = null;
+
+ $this->call_build_headers($headers, $headers_params);
+ }
+
+ public function test_build_headers_utf8_subject() {
+ $headers = $this->default_headers;
+ $headers_params = $this->default_headers_params;
+
+ $headers['Subject'] = '=?UTF-8?B?SGVpesO2bHLDvGNrc3Rvw59hYmTDpG1wZnVuZw==?=';
+ $headers_params[4] = 'Heizölrückstoßabdämpfung';
+
+ $this->call_build_headers($headers, $headers_params);
+ }
+
+ public function test_build_headers_utf8_username() {
+ $headers = $this->default_headers;
+ $headers_params = $this->default_headers_params;
+
+ $headers['From'] = '=?UTF-8?B?SGVpesO2bHLDvGNrc3Rvw59hYmTDpG1wZnVuZw==?= <uckelman@nomic.net>';
+ $headers_params[0] = 'Heizölrückstoßabdämpfung';
+
+ $this->call_build_headers($headers, $headers_params);
+ }
+
+ public function test_build_body_no_attachments() {
+ $headers = array();
+ $text = 'This is some test text.';
+ $footer = "
+_______________________________________________
+Read this topic online here:
+http://www.example.com/viewtopic.php?p=42#p42";
+ $attachments = null;
+
+ $body = build_body($headers, $text, $attachments, $footer);
+
+
+ $this->assertEquals('text/plain; charset=UTF-8; format=flowed', $headers['Content-Type']);
+ $this->assertEquals('8bit', $headers['Content-Transfer-Encoding']);
+ $this->assertEquals("$text\n$footer", $body);
+ }
+
+ public function test_build_body_attachments() {
+ // FIXME: This is kind of a complex test to write, because the result
+ // is a Mail_mimePart object.
+ $this->markTestIncomplete();
+ }
+}
+
+?>