3 * Copyright (c) STMicroelectronics, 2005. All Rights Reserved.
5 * Originally written by Jean-Philippe Giola, 2005
7 * This file is a part of Fusionforge.
9 * Fusionforge is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
14 * Fusionforge is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
19 * You should have received a copy of the GNU General Public License along
20 * with this program; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 define('FORUMML_MESSAGE_ID', 1);
25 define('FORUMML_DATE', 2);
26 define('FORUMML_FROM', 3);
27 define('FORUMML_SUBJECT', 4);
28 define('FORUMML_CONTENT_TYPE', 12);
29 define('FORUMML_CC', 34);
31 require_once(dirname(__FILE__).'/../include/ForumML_Attachment.class.php');
32 require_once(dirname(__FILE__).'/../include/ForumML_MessageDao.class.php');
33 //require_once('common/include/Toggler.class.php');
34 require_once 'Mail/RFC822.php';
35 require_once 'common/mail/Mail.class.php';
36 require_once 'PEAR.php';
39 function getForumMLDao() {
40 return new ForumML_MessageDao(CodendiDataAccess::instance());
43 // Get message headers
44 function plugin_forumml_get_message_headers($id_message) {
46 return getForumMLDao()->getMessageHeaders($id_message)->getRow();
49 // Display search results
50 function plugin_forumml_show_search_results($p,$result,$group_id,$list_id) {
52 echo "<table width='100%'>
66 // Build a table full of search results
67 while ($rows = $result->getRow()) {
70 $class="boxitemalt bgcolor-white";
72 $class="boxitem bgcolor-grey";
75 $res1 = getForumMLDao()->getSpecificMessage($rows['id_message'],$list_id)->getRow();
76 $subject = mb_decode_mimeheader($res1['value']);
77 $res2 = getForumMLDao()->getHeaderValue($rows['id_message'],array(2,3));
79 while ($rows2 =$res2->getRow()) {
80 $header[$k] = $rows2['value'];
83 $from = mb_decode_mimeheader($header[1]);
85 // Replace '<' by '<' and '>' by '>'. Otherwise the email adress won't be displayed
86 // because it will be considered as an xhtml tag.
87 $from = preg_replace('/\</', '<', $from);
88 $from = preg_replace('/\>/', '>', $from);
90 $date = date("Y-m-d H:i",strtotime($header[2]));
91 // purify message subject (CODENDI_PURIFIER_FORUMML level)
92 $hp =& ForumML_HTMLPurifier::instance();
93 $subject = $hp->purify($subject,CODENDI_PURIFIER_FORUMML);
95 // display the resulting threads in rows
96 printf ("<tr class='".$class."'>
98 <img src='".$p->getThemePath()."/images/ic/comment.png'/>
99 <a href='message.php?group_id=".$group_id."&topic=".$rows['id_message']."&list=".$list_id."'><b>".$subject."</b></a>
102 <font class='info'>".$date."</font>
105 <font class='info'>".$from."</font>
114 function plugin_forumml_show_all_threads($p,$list_id,$list_name,$offset) {
117 $request =& HTTPRequest::instance();
120 $result = getForumMLDao()->getAllThreadsFromList($list_id,$offset,$chunks);
121 $nbRowFound = $result->rowCount();
123 // Total number of threads
125 $res = getForumMLDao()->countAllThreadsFromList($list_id);
126 if ($res && !db_error()) {
127 $row = $res->getRow();
128 $nbThreads = $row['nb'];
132 $end = min($start + $chunks - 1, $nbRowFound - 1);
134 // all threads to be displayed
138 if (isset($offset) && $offset != 0) {
139 $begin = "<a href=\"/plugins/forumml/message.php?group_id=".$request->get('group_id')."&list=".$list_id.
140 "\"><img src='".$p->getThemePath()."/images/ic/resultset_first.png' title='begin')'/></a>";
141 $previous = "<a href=\"/plugins/forumml/message.php?group_id=".$request->get('group_id')."&list=".$list_id."&offset=".($offset - $chunks).
142 "\"><img src='".$p->getThemePath()."/images/ic/resultset_previous.png' title='".
143 _('Previous ').$chunks.(' messages')."'/></a>";
145 $begin = "<img src='".$p->getThemePath()."/images/ic/resultset_first_disabled.png' alt='".$p->getThemePath()."/images/ic/resultset_first_disabled.png'/>";
146 $previous = "<img src='".$p->getThemePath()."/images/ic/resultset_previous_disabled.png'
147 title='"._('Previous ').$chunks.(' messages')."'/>";
150 if (($offset + $chunks ) < $nbThreads) {
151 $next = "<a href=\"/plugins/forumml/message.php?group_id=".$request->get('group_id')."&list=".$list_id."&offset=".($offset + $chunks)."\"><img src='".$p->getThemePath()."/images/ic/resultset_next.png' title='"._('Next ').$chunks.(' messages')."'/></a>";
152 $finish = "<a href=\"/plugins/forumml/message.php?group_id=".$request->get('group_id')."&list=".$list_id."&offset=".($chunks * (int) (($nbThreads - 1) / $chunks))."\"><img src='".$p->getThemePath()."/images/ic/resultset_last.png' title='".$_('Last messages')."'/></a>";
154 $next = "<img src='".$p->getThemePath()."/images/ic/resultset_next_disabled.png' title='".$chunks."'/>";
155 $finish = "<img src='".$p->getThemePath()."/images/ic/resultset_last_disabled.png'/>";
158 // display page-splitting information, at the top of threads table
159 echo "<table width='100%'>
161 <td align='left' width='10%'>".
164 <td align='left' width='15%'>".
167 <td align='center' width='55%'>".
168 _('Threads')." ".($start + 1)." - ".($end + 1)." <b>(".$nbThreads.")</b>
170 <td align='right' width='10%'>
173 <td align='right' width='10%'>
179 if ($nbRowFound > 0) {
181 echo "<table class='border' width='100%' border='0'>
182 <tr class='boxtable'>
183 <th class='forumml' ".$colspan." width='40%'>".$item."</th>
184 <th class='forumml' width='15%'>"._('Last updated')."</th>
185 <th class='forumml' width='15%'>"._('Submitted on')."</th>
186 <th class='forumml' width='25%'>"._('Author')."</th>
189 $hp =& ForumML_HTMLPurifier::instance();
191 while (($msg = $result->getRow())) {
194 $class="boxitemalt bgcolor-white";
195 $headerclass="headerlabelalt";
197 $class="boxitem bgcolor-grey";
198 $headerclass="headerlabel";
201 // Get the number of messages in thread
202 // nb of children + message
203 $count = 1 + plugin_forumml_nb_children(array($msg['id_message']));
206 print "<tr class='".$class."'><a name='".$msg['id_message']."'></a>
207 <td class='subject'>";
209 print "<img src='".$p->getThemePath()."/images/ic/comments.png'/>";
212 print "<img src='".$p->getThemePath()."/images/ic/comment.png'/>";
215 // Remove listname from suject
216 $subject = preg_replace('/^[ ]*\['.$list_name.'\]/i', '', $msg['subject']);
218 print "<a href='message.php?group_id=".$request->get('group_id')."&topic=".$msg['id_message']."&list=".$request->get('list')."'>
219 ".$hp->purify($subject, CODENDI_PURIFIER_CONVERT_HTML)."
220 </a> <b><i>(".$count.")</i></b>
222 "<td class='info'>".strftime("%a, %e %h %G %R",$msg['lastup'])."</td>".
223 "<td class='info'>".strftime("%a, %e %h %G %R",strtotime($msg['date']))."</td>
224 <td class='info'>".$hp->purify($msg['sender'], CODENDI_PURIFIER_CONVERT_HTML)."</td>
229 // display page-splitting information, at the bottom of threads table
230 echo "<table width='100%'>
232 <td align='left' width='10%'>".
235 <td align='left' width='15%'>".
238 <td align='center' width='55%'>".
239 _('Threads')." ".($start + 1)." - ".($end + 1)." <b>(".$nbThreads.")</b>
241 <td align='right' width='10%'>
244 <td align='right' width='10%'>
253 function plugin_forumml_nb_children($parents) {
254 if (count($parents) == 0) {
257 $result = getForumMLDao()->countChildrenFromParents(implode(',',$parents));
258 if ($result && !$result->isError()) {
260 while (($row = $result->getRow())) {
261 $p[] = $row['id_message'];
263 $num = $result->rowCount();
264 return $num + plugin_forumml_nb_children($p);
270 * Extract attachment info from a database result
272 * @see plugin_forumml_build_flattened_thread
274 function plugin_forumml_new_attach($row) {
275 if (isset($row['id_attachment']) && $row['id_attachment']) {
276 return array('id_attachment' => $row['id_attachment'],
277 'file_name' => $row['file_name'],
278 'file_type' => $row['file_type'],
279 'file_size' =>$row['file_size'],
280 'file_path' =>$row['file_path'],
281 'content_id' =>$row['content_id']);
288 * Insert a message in the thread list with a unique date
290 * @see plugin_forumml_build_flattened_thread
292 function plugin_forumml_insert_in_thread(&$thread, $row) {
293 $date = strtotime($row['date']);
294 while (isset($thread[$date])) {
297 $thread[$date] = $row;
302 * Insert all messages returned by a SQL query in the thread list with
305 * @see plugin_forumml_build_flattened_thread
307 function plugin_forumml_insert_msg_attach(&$thread, $result) {
310 while ($row = $result->getRow()) {
311 if ($row['id_message'] != $prev) {
313 $parents[] = $row['id_message'];
314 $curMsg = plugin_forumml_insert_in_thread($thread, $row);
315 $thread[$curMsg]['attachments'] = array();
318 $attch = plugin_forumml_new_attach($row);
320 $thread[$curMsg]['attachments'][] = $attch;
322 $prev = $row['id_message'];
328 * Search all chilrens at a given level of depth
330 * @see plugin_forumml_build_flattened_thread
332 function plugin_forumml_build_flattened_thread_children(&$thread, $parents) {
333 if (count($parents) > 0) {
334 $result = getForumMLDao()->getChildrenFromDepthLevel(implode(',',$parents));
335 if ($result && !$result->isError()){
336 $p = plugin_forumml_insert_msg_attach($thread, $result);
337 plugin_forumml_build_flattened_thread_children($thread, $p);
343 * Entry point to create a flattened view of a message thread.
345 * In order to display the messages in the right order, we fetch the
346 * all the messages with the needed hearders and attachments.
347 * To lower the number of SQL queries, there is 1 query per message
349 * All the messages are stored in an array indexed by the message
350 * date. If dates conflict we add +1s to the message date.
351 * Once all the messages are fetched, we just sort the array based on
353 * The thread array looks like:
355 * 123342334 => array(
356 * 'message_id' => '1234',
357 * 'subject' => 'toto',
359 * 'attachments' => array(
360 * 'id_attachment' => '5678',
368 function plugin_forumml_build_flattened_thread($topic) {
370 $result = getForumMLDao()->getFlattenedThread($topic);
371 if ($result && !$result->isError()) {
372 $p = plugin_forumml_insert_msg_attach($thread, $result);
373 plugin_forumml_build_flattened_thread_children($thread, $p);
375 ksort($thread, SORT_NUMERIC);
379 // List all messages inside a thread
380 function plugin_forumml_show_thread($p, $list_id, $parentId, $purgeCache) {
381 $hp = ForumML_HTMLPurifier::instance();
382 $thread = plugin_forumml_build_flattened_thread($parentId);
383 foreach ($thread as $message) {
384 plugin_forumml_show_message($p, $hp, $message, $parentId, $purgeCache);
389 function plugin_forumml_show_message($p, $hp, $msg, $id_parent, $purgeCache) {
390 $body = $msg['body'];
391 $request = HTTPRequest::instance();
393 // Is "ready to display" body already in cache or not
394 $bodyIsCached = false;
395 if (isset($msg['cached_html']) && !$purgeCache) {
396 $bodyIsCached = true;
399 if (PEAR::isError($from_info = Mail_RFC822::parseAddressList($msg['sender'], forge_get_config('web_host'))) || !isset($from_info[0]) || !$from_info[0]->personal) {
400 $from_info = $hp->purify($msg['sender'], CODENDI_PURIFIER_CONVERT_HTML);
402 $from_info = '<abbr title="'. $hp->purify($from_info[0]->mailbox .'@'. $from_info[0]->host, CODENDI_PURIFIER_CONVERT_HTML) .'">'. $hp->purify($from_info[0]->personal, CODENDI_PURIFIER_CONVERT_HTML) .'</abbr>';
405 echo '<div class="plugin_forumml_message">';
407 echo '<div class="plugin_forumml_message_header boxitemalt" id="plugin_forumml_message_'. $msg['id_message'] .'">';
408 echo '<div class="plugin_forumml_message_header_subject">'. $hp->purify($msg['subject'], CODENDI_PURIFIER_CONVERT_HTML) .'</div>';
410 echo '<a href="#'. $msg['id_message'] .'" title="message #'. $msg['id_message'] .'">';
411 echo '<img src="'. $p->getThemePath() .'/images/ic/comment.png" id="'. $msg['id_message'] .'" style="vertical-align:middle" alt="#'. $msg['id_message'] .'" />';
414 echo ' <span class="plugin_forumml_message_header_from">'. $from_info .'</span>';
415 echo ' <span class="plugin_forumml_message_header_date">'. _('On ').$msg['date'] .'</span>';
417 echo ' <a href="#" id="plugin_forumml_toogle_msg_'.$msg['id_message'].'" class="plugin_forumml_toggle_font">'._('Toggle font family (typewriter/normal)').'</a>';
420 $cc = trim($msg['cc']);
422 if (PEAR::isError($cc_info = Mail_RFC822::parseAddressList($cc, forge_get_config('web_host')))) {
423 $ccs = $hp->purify($cc, CODENDI_PURIFIER_CONVERT_HTML);
426 foreach($cc_info as $c) {
428 $ccs[] = $hp->purify($c->mailbox .'@'. $c->host, CODENDI_PURIFIER_CONVERT_HTML);
430 $ccs[] = '<abbr title="'. $hp->purify($c->mailbox .'@'. $c->host, CODENDI_PURIFIER_CONVERT_HTML) .'">'. $hp->purify($c->personal, CODENDI_PURIFIER_CONVERT_HTML) .'</abbr>';
433 $ccs = implode(', ', $ccs);
435 print '<div class="plugin_forumml_message_header_cc">'. _('Cc:') .' '. $ccs .'</div>';
439 if (strpos($msg['content_type'], 'multipart/') !== false) {
440 $content_type = $msg['msg_type'];
442 $content_type = $msg['content_type'];
444 $is_html = strpos($content_type, "text/html") !== false;
446 // get attached files
447 if (count($msg['attachments'])) {
448 print '<div class="plugin_forumml_message_header_attachments">';
450 foreach($msg['attachments'] as $attachment) {
451 // Special case, this is an HTML email
452 if (preg_match('/.html$/i',$attachment['file_name'])) {
453 // By default, the first html attachment replaces the default body (text)
455 if (!$bodyIsCached && is_file($attachment['file_path'])) {
456 $body = file_get_contents($attachment['file_path']);
461 $flink = $attachment['file_name'];
464 $flink = $attachment['file_name'];
467 echo ', ';
470 echo "<img src='".$p->getThemePath()."/images/ic/attach.png'/> <a href='upload.php?group_id=".$request->get('group_id')."&list=".$request->get('list')."&id=".$attachment['id_attachment']."&topic=".$id_parent."'>".$flink."</a>";
477 print '<div id="plugin_forumml_message_content_'.$msg['id_message'].'" class="plugin_forumml_message_content_std">';
478 $body = str_replace("\r\n","\n", $body);
480 // If there is no cached html of if user requested to regenerate the cache, do it, otherwise use cached HTML.
481 if (!$bodyIsCached) {
482 // Purify message body, according to the content-type
484 // Update attachment links
485 $body = plugin_forumml_replace_attachment($msg['id_message'], $request->get('group_id'), $request->get('list'), $id_parent, $body);
487 // Use CODENDI_PURIFIER_FULL for html mails
488 $msg['cached_html'] = $hp->purify($body,CODENDI_PURIFIER_FULL,$request->get('group_id'));
490 // CODENDI_PURIFIER_FORUMML level : no basic html markups, no forms, no javascript,
491 // Allowed: url + automagic links + <blockquote>
492 $purified_body = $hp->purify($body,CODENDI_PURIFIER_CONVERT_HTML,$request->get('group_id'));
493 $purified_body = str_replace('>', '>', $purified_body);
497 $search_for_quotes = false;
498 $maxi = strlen($purified_body);
499 for($i = 0 ; $i < $maxi ; ++$i) {
500 if ($search_for_quotes) {
501 if($purified_body{$i} == ">") {
503 if($level < $current_level) {
504 $tab_body .= '<blockquote class="grep">';
508 $search_for_quotes = false;
509 if($level > $current_level) {
510 $tab_body .= '</blockquote>';
513 if($purified_body{$i} == "\n" && $i < $maxi - 1) {
514 $search_for_quotes = true;
517 $tab_body .= $purified_body{$i};
520 if($purified_body{$i} == "\n" && $i < $maxi - 1) {
521 $search_for_quotes = true;
524 $tab_body .= $purified_body{$i};
527 $purified_body = str_replace('>', '>', $purified_body);
528 $msg['cached_html'] = nl2br($tab_body);
530 getForumMLDao()->updateCacheHTML($msg['cached_html'] , $msg['id_message']);
532 echo $msg['cached_html'];
536 echo '<div class="plugin_forumml_message_footer">';
538 // If you click on 'Reply', load reply form
539 $vMess = new Valid_UInt('id_mess');
541 if ($request->valid($vMess) && $request->get('id_mess') == $msg['id_message']) {
542 $vReply = new Valid_WhiteList('reply',array(0,1));
544 if ($request->valid($vReply) && $request->get('reply') == 1) {
546 $body = $hp->purify($body, CODENDI_PURIFIER_STRIP_HTML);
548 $body = $hp->purify($body, CODENDI_PURIFIER_CONVERT_HTML);
550 plugin_forumml_reply($hp,$msg['subject'],$msg['id_message'],$id_parent,$body,$msg['sender']);
554 print "<a href='message.php?group_id=".$request->get('group_id')."&topic=".$id_parent."&id_mess=".$msg['id_message']."&reply=1&list=".$request->get('list')."#reply-".$msg['id_message']."'>
555 <img src='".$p->getThemePath()."/images/ic/comment_add.png'/>
564 // Display the post form under the current post
565 function plugin_forumml_reply($hp,$subject,$in_reply_to,$id_parent,$body,$author) {
567 $request =& HTTPRequest::instance();
568 $tab_tmp = explode("\n",$body);
569 $tab_tmp = array_pad($tab_tmp,-count($tab_tmp)-1,"$author wrote :");
571 echo '<script type="text/javascript" src="scripts/cc_attach_js.php"></script>';
572 echo ' <div id="reply-'. $in_reply_to .'" class="plugin_forumml_message_reply">'."
573 <form id='".$in_reply_to."' action='?group_id=".$request->get('group_id')."&list=".$request->get('list')."&topic=".$id_parent."' name='replyform' method='post' enctype='multipart/form-data'>
574 <input type='hidden' name='reply_to' value='".$in_reply_to."'/>
575 <input type='hidden' name='subject' value='".$subject."'/>
576 <input type='hidden' name='list' value='".$request->get('list')."'/>
577 <input type='hidden' name='group_id' value='".$request->get('group_id')."'/>";
578 echo '<a href="javascript:;" onclick="addHeader(\'\',\'\',1);">['._('Add cc:').']</a>
579 - <a href="javascript:;" onclick="addHeader(\'\',\'\',2);">['._('Attach:').']</a>
580 <input type="hidden" value="0" id="header_val" />
581 <div id="mail_header"></div>';
582 echo "<p><textarea name='message' rows='15' cols='100'>";
584 foreach($tab_tmp as $k => $line) {
589 $indent = substr($line, 0, 4) == '>' ? '>' : '> ';
590 print($indent . $line."\n");
594 echo "</textarea></p>
596 <input type='submit' name='send_reply' value='"._('Submit')."'/>
597 <input type='reset' value='"._('Erase')."'/>
603 // Search & replace reference to attached content
604 // This happens for images attached to html messages (multipart/related)
605 function plugin_forumml_replace_attachment($id_message, $group_id, $list, $id_parent, $body) {
606 if (preg_match_all('/"cid:([^"]*)"/m', $body, $matches)) {
607 $search_parts = array();
608 $replace_parts = array();
609 foreach ($matches[1] as $match) {
610 $result = getForumMLDao()->getAttachment($id_mesage , $match) ;
611 if ($res && $res->rowCount() == 1) {
612 $row = $res->getRow();
613 $url = "upload.php?group_id=".$group_id."&list=".$list."&id=".$row['id_attachment']."&topic=".$id_parent;
614 $search_parts[] = 'cid:'.$match;
615 $replace_parts[] = $url;
618 if (count($replace_parts) > 0) {
619 $body = str_replace($search_parts, $replace_parts, $body);
625 // Build Mail headers, and send the mail
626 function plugin_forumml_process_mail($plug,$reply=false) {
628 $request = HTTPRequest::instance();
629 $hp = ForumML_HTMLPurifier::instance();
631 // Instantiate a new Mail class
634 // Build mail headers
635 $list = new MailmanList($request->get('group_id') , $request->get('list'));
636 $to = $list->getName()."@".forge_get_config('lists_host');
639 $mail->setFrom(UserManager::instance()->getCurrentUser()->getEmail());
641 $vMsg = new Valid_Text('message');
642 if ($request->valid($vMsg)) {
643 $message = $request->get('message');
646 $subject = $request->get('subject');
647 $mail->setSubject($subject);
650 // set In-Reply-To header
651 $hres = plugin_forumml_get_message_headers($request->get('reply_to'));
652 $reply_to = $hres['value'];
653 $mail->addAdditionalHeader("In-Reply-To",$reply_to);
657 if ($request->validArray(new Valid_Email('ccs')) && $request->exist('ccs')) {
660 foreach ($request->get('ccs') as $cc) {
661 if (trim($cc) != "") {
662 $cc_array[$idx] = $hp->purify($cc,CODENDI_PURIFIER_FULL);
666 // Checks sanity of CC List
669 foreach ($cc_array as $key => $cc) {
670 $umanager = UserManager::instance();
671 $user = $umanager->existEmail($cc);
679 $feedback .=_('Invalid Email Address')._(': ').$err;
681 // add list of cc users to mail mime
682 if (count($cc_array) > 0) {
683 $cc_list = implode(',',$cc_array);
684 $mail->setCc($cc_list,true);
690 // Process attachments
692 // Define boundaries as specified in RFC:
693 // http://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
694 $boundary = '----=_NextPart';
695 $boundaryStart = '--'.$boundary;
696 $boundaryEnd = '--'.$boundary.'--';
698 // Attachments headers
699 if (isset($_FILES["files"]) && count($_FILES["files"]['name']) > 0) {
701 $text = "This is a multi-part message in MIME format.\n";
702 $text = "$boundaryStart\n";
703 $text .= "Content-Type: text/plain; charset=\"UTF-8\"\n";
704 $text .= "Content-Transfer-Encoding: 8bit\n\n";
707 foreach($_FILES["files"]['name'] as $i => $fileName) {
708 $attachment .= "$boundaryStart\n";
709 $attachment .= "Content-Type:".$_FILES["files"]["type"][$i]."; name=".$fileName."\n";
710 $attachment .= "Content-Transfer-Encoding: base64\n";
711 $attachment .= "Content-Disposition: attachment; filename=".$fileName."\n\n";
712 $attachment .= chunk_split(base64_encode(file_get_contents($_FILES["files"]["tmp_name"][$i])));
714 $attachment .= "\n$boundaryEnd\n";
715 $body = $text.$attachment;
716 // force MimeType to multipart/mixed as default (when instantiating new Mail object) is text/plain
717 $mail->setMimeType('multipart/mixed; boundary="'.$boundary.'"');
718 $mail->addAdditionalHeader("MIME-Version","1.0");
723 $mail->setBody($body);
726 $feedback .= _('Mail successfully sent ');
728 $feedback .= _('Sending mail failed');