3 # Copyright (c) STMicroelectronics, 2005. All Rights Reserved.
5 # Originally written by Jean-Philippe Giola, 2005
7 # This file is a part of codendi.
9 # codendi 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 # codendi 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.
26 define('FORUMML_MESSAGE_ID', 1);
27 define('FORUMML_DATE', 2);
28 define('FORUMML_FROM', 3);
29 define('FORUMML_SUBJECT', 4);
30 define('FORUMML_CONTENT_TYPE', 12);
31 define('FORUMML_CC', 34);
33 require_once(dirname(__FILE__).'/../include/ForumML_Attachment.class.php');
34 require_once(dirname(__FILE__).'/../include/ForumML_MessageDao.class.php');
35 //require_once('common/include/Toggler.class.php');
36 require_once 'Mail/RFC822.php';
37 require_once 'common/mail/Mail.class.php';
38 require_once 'PEAR.php';
42 function getForumMLDao() {
43 return new ForumML_MessageDao(CodendiDataAccess::instance());
46 // Get message headers
47 function plugin_forumml_get_message_headers($id_message) {
49 return getForumMLDao()->getMessageHeaders($id_message)->getRow();
52 // Display search results
53 function plugin_forumml_show_search_results($p,$result,$group_id,$list_id) {
55 echo "<table width='100%'>
69 // Build a table full of search results
70 while ($rows = $result->getRow()) {
73 $class="boxitemalt bgcolor-white";
75 $class="boxitem bgcolor-grey";
78 $res1 = getForumMLDao()->getSpecificMessage($rows['id_message'],$list_id)->getRow();
79 $subject = mb_decode_mimeheader($res1['value']);
80 $res2 = getForumMLDao()->getHeaderValue($rows['id_message'],array(2,3));
82 while ($rows2 =$res2->getRow()) {
83 $header[$k] = $rows2['value'];
86 $from = mb_decode_mimeheader($header[1]);
88 // Replace '<' by '<' and '>' by '>'. Otherwise the email adress won't be displayed
89 // because it will be considered as an xhtml tag.
90 $from = preg_replace('/\</', '<', $from);
91 $from = preg_replace('/\>/', '>', $from);
93 $date = date("Y-m-d H:i",strtotime($header[2]));
94 // purify message subject (CODENDI_PURIFIER_FORUMML level)
95 $hp =& ForumML_HTMLPurifier::instance();
96 $subject = $hp->purify($subject,CODENDI_PURIFIER_FORUMML);
98 // display the resulting threads in rows
99 printf ("<tr class='".$class."'>
101 <img src='".$p->getThemePath()."/images/ic/comment.png'/>
102 <a href='message.php?group_id=".$group_id."&topic=".$rows['id_message']."&list=".$list_id."'><b>".$subject."</b></a>
105 <font class='info'>".$date."</font>
108 <font class='info'>".$from."</font>
117 function plugin_forumml_show_all_threads($p,$list_id,$list_name,$offset) {
120 $request =& HTTPRequest::instance();
123 $result = getForumMLDao()->getAllThreadsFromList($list_id,$offset,$chunks);
124 $nbRowFound = $result->rowCount();
126 // Total number of threads
128 $res = getForumMLDao()->countAllThreadsFromList($list_id);
129 if ($res && !db_error()) {
130 $row = $res->getRow();
131 $nbThreads = $row['nb'];
135 $end = min($start + $chunks - 1, $nbRowFound - 1);
137 // all threads to be displayed
141 if (isset($offset) && $offset != 0) {
142 $begin = "<a href=\"/plugins/forumml/message.php?group_id=".$request->get('group_id')."&list=".$list_id.
143 "\"><img src='".$p->getThemePath()."/images/ic/resultset_first.png' title='begin')'/></a>";
144 $previous = "<a href=\"/plugins/forumml/message.php?group_id=".$request->get('group_id')."&list=".$list_id."&offset=".($offset - $chunks).
145 "\"><img src='".$p->getThemePath()."/images/ic/resultset_previous.png' title='".
146 _('Previous ').$chunks.(' messages')."'/></a>";
148 $begin = "<img src='".$p->getThemePath()."/images/ic/resultset_first_disabled.png' alt='".$p->getThemePath()."/images/ic/resultset_first_disabled.png'/>";
149 $previous = "<img src='".$p->getThemePath()."/images/ic/resultset_previous_disabled.png'
150 title='"._('Previous ').$chunks.(' messages')."'/>";
153 if (($offset + $chunks ) < $nbThreads) {
154 $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>";
155 $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>";
157 $next = "<img src='".$p->getThemePath()."/images/ic/resultset_next_disabled.png' title='".$chunks."'/>";
158 $finish = "<img src='".$p->getThemePath()."/images/ic/resultset_last_disabled.png'/>";
161 // display page-splitting information, at the top of threads table
162 echo "<table width='100%'>
164 <td align='left' width='10%'>".
167 <td align='left' width='15%'>".
170 <td align='center' width='55%'>".
171 _('Threads')." ".($start + 1)." - ".($end + 1)." <b>(".$nbThreads.")</b>
173 <td align='right' width='10%'>
176 <td align='right' width='10%'>
182 if ($nbRowFound > 0) {
184 echo "<table class='border' width='100%' border='0'>
185 <tr class='boxtable'>
186 <th class='forumml' ".$colspan." width='40%'>".$item."</th>
187 <th class='forumml' width='15%'>"._('Last updated')."</th>
188 <th class='forumml' width='15%'>"._('Submitted on')."</th>
189 <th class='forumml' width='25%'>"._('Author')."</th>
194 $hp =& ForumML_HTMLPurifier::instance();
196 while (($msg = $result->getRow())) {
199 $class="boxitemalt bgcolor-white";
200 $headerclass="headerlabelalt";
202 $class="boxitem bgcolor-grey";
203 $headerclass="headerlabel";
206 // Get the number of messages in thread
207 // nb of children + message
208 $count = 1 + plugin_forumml_nb_children(array($msg['id_message']));
212 print "<tr class='".$class."'><a name='".$msg['id_message']."'></a>
213 <td class='subject'>";
215 print "<img src='".$p->getThemePath()."/images/ic/comments.png'/>";
218 print "<img src='".$p->getThemePath()."/images/ic/comment.png'/>";
221 // Remove listname from suject
222 $subject = preg_replace('/^[ ]*\['.$list_name.'\]/i', '', $msg['subject']);
224 print "<a href='message.php?group_id=".$request->get('group_id')."&topic=".$msg['id_message']."&list=".$request->get('list')."'>
225 ".$hp->purify($subject, CODENDI_PURIFIER_CONVERT_HTML)."
226 </a> <b><i>(".$count.")</i></b>
228 "<td class='info'>".strftime("%a, %e %h %G %R",$msg['lastup'])."</td>".
229 "<td class='info'>".strftime("%a, %e %h %G %R",strtotime($msg['date']))."</td>
230 <td class='info'>".$hp->purify($msg['sender'], CODENDI_PURIFIER_CONVERT_HTML)."</td>
235 // display page-splitting information, at the bottom of threads table
236 echo "<table width='100%'>
238 <td align='left' width='10%'>".
241 <td align='left' width='15%'>".
244 <td align='center' width='55%'>".
245 _('Threads')." ".($start + 1)." - ".($end + 1)." <b>(".$nbThreads.")</b>
247 <td align='right' width='10%'>
250 <td align='right' width='10%'>
259 function plugin_forumml_nb_children($parents) {
260 if (count($parents) == 0) {
263 $result = getForumMLDao()->countChildrenFromParents(implode(',',$parents));
264 if ($result && !$result->isError()) {
266 while (($row = $result->getRow())) {
267 $p[] = $row['id_message'];
269 $num = $result->rowCount();
270 return $num + plugin_forumml_nb_children($p);
276 * Extract attachment info from a database result
278 * @see plugin_forumml_build_flattened_thread
280 function plugin_forumml_new_attach($row) {
281 if (isset($row['id_attachment']) && $row['id_attachment']) {
282 return array('id_attachment' => $row['id_attachment'],
283 'file_name' => $row['file_name'],
284 'file_type' => $row['file_type'],
285 'file_size' =>$row['file_size'],
286 'file_path' =>$row['file_path'],
287 'content_id' =>$row['content_id']);
294 * Insert a message in the thread list with a unique date
296 * @see plugin_forumml_build_flattened_thread
298 function plugin_forumml_insert_in_thread(&$thread, $row) {
299 $date = strtotime($row['date']);
300 while (isset($thread[$date])) {
303 $thread[$date] = $row;
308 * Insert all messages returned by a SQL query in the thread list with
311 * @see plugin_forumml_build_flattened_thread
313 function plugin_forumml_insert_msg_attach(&$thread, $result) {
316 while ($row = $result->getRow()) {
317 if ($row['id_message'] != $prev) {
319 $parents[] = $row['id_message'];
320 $curMsg = plugin_forumml_insert_in_thread($thread, $row);
321 $thread[$curMsg]['attachments'] = array();
324 $attch = plugin_forumml_new_attach($row);
326 $thread[$curMsg]['attachments'][] = $attch;
328 $prev = $row['id_message'];
334 * Search all chilrens at a given level of depth
336 * @see plugin_forumml_build_flattened_thread
338 function plugin_forumml_build_flattened_thread_children(&$thread, $parents) {
339 if (count($parents) > 0) {
340 $result = getForumMLDao()->getChildrenFromDepthLevel(implode(',',$parents));
341 if ($result && !$result->isError()){
342 $p = plugin_forumml_insert_msg_attach($thread, $result);
343 plugin_forumml_build_flattened_thread_children($thread, $p);
349 * Entry point to create a flattened view of a message thread.
351 * In order to display the messages in the right order, we fetch the
352 * all the messages with the needed hearders and attachments.
353 * To lower the number of SQL queries, there is 1 query per message
355 * All the messages are stored in an array indexed by the message
356 * date. If dates conflict we add +1s to the message date.
357 * Once all the messages are fetched, we just sort the array based on
359 * The thread array looks like:
361 * 123342334 => array(
362 * 'message_id' => '1234',
363 * 'subject' => 'toto',
365 * 'attachments' => array(
366 * 'id_attachment' => '5678',
374 function plugin_forumml_build_flattened_thread($topic) {
376 $result = getForumMLDao()->getFlattenedThread($topic);
377 if ($result && !$result->isError()) {
378 $p = plugin_forumml_insert_msg_attach($thread, $result);
379 plugin_forumml_build_flattened_thread_children($thread, $p);
381 ksort($thread, SORT_NUMERIC);
385 // List all messages inside a thread
386 function plugin_forumml_show_thread($p, $list_id, $parentId, $purgeCache) {
387 $hp = ForumML_HTMLPurifier::instance();
388 $thread = plugin_forumml_build_flattened_thread($parentId);
389 foreach ($thread as $message) {
390 plugin_forumml_show_message($p, $hp, $message, $parentId, $purgeCache);
396 function plugin_forumml_show_message($p, $hp, $msg, $id_parent, $purgeCache) {
397 $body = $msg['body'];
398 $request = HTTPRequest::instance();
400 // Is "ready to display" body already in cache or not
401 $bodyIsCached = false;
402 if (isset($msg['cached_html']) && !$purgeCache) {
403 $bodyIsCached = true;
406 if (PEAR::isError($from_info = Mail_RFC822::parseAddressList($msg['sender'], forge_get_config('web_host'))) || !isset($from_info[0]) || !$from_info[0]->personal) {
407 $from_info = $hp->purify($msg['sender'], CODENDI_PURIFIER_CONVERT_HTML);
409 $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>';
412 echo '<div class="plugin_forumml_message">';
414 echo '<div class="plugin_forumml_message_header boxitemalt" id="plugin_forumml_message_'. $msg['id_message'] .'">';
415 echo '<div class="plugin_forumml_message_header_subject">'. $hp->purify($msg['subject'], CODENDI_PURIFIER_CONVERT_HTML) .'</div>';
417 echo '<a href="#'. $msg['id_message'] .'" title="message #'. $msg['id_message'] .'">';
418 echo '<img src="'. $p->getThemePath() .'/images/ic/comment.png" id="'. $msg['id_message'] .'" style="vertical-align:middle" alt="#'. $msg['id_message'] .'" />';
421 echo ' <span class="plugin_forumml_message_header_from">'. $from_info .'</span>';
422 echo ' <span class="plugin_forumml_message_header_date">'. _('On ').$msg['date'] .'</span>';
424 echo ' <a href="#" id="plugin_forumml_toogle_msg_'.$msg['id_message'].'" class="plugin_forumml_toggle_font">'._('Toggle font family (typewriter/normal)').'</a>';
427 $cc = trim($msg['cc']);
429 if (PEAR::isError($cc_info = Mail_RFC822::parseAddressList($cc, forge_get_config('web_host')))) {
430 $ccs = $hp->purify($cc, CODENDI_PURIFIER_CONVERT_HTML);
433 foreach($cc_info as $c) {
435 $ccs[] = $hp->purify($c->mailbox .'@'. $c->host, CODENDI_PURIFIER_CONVERT_HTML);
437 $ccs[] = '<abbr title="'. $hp->purify($c->mailbox .'@'. $c->host, CODENDI_PURIFIER_CONVERT_HTML) .'">'. $hp->purify($c->personal, CODENDI_PURIFIER_CONVERT_HTML) .'</abbr>';
440 $ccs = implode(', ', $ccs);
442 print '<div class="plugin_forumml_message_header_cc">'. _('Cc :') .' '. $ccs .'</div>';
446 if (strpos($msg['content_type'], 'multipart/') !== false) {
447 $content_type = $msg['msg_type'];
449 $content_type = $msg['content_type'];
451 $is_html = strpos($content_type, "text/html") !== false;
453 // get attached files
454 if (count($msg['attachments'])) {
455 print '<div class="plugin_forumml_message_header_attachments">';
457 foreach($msg['attachments'] as $attachment) {
458 // Special case, this is an HTML email
459 if (preg_match('/.html$/i',$attachment['file_name'])) {
460 // By default, the first html attachment replaces the default body (text)
462 if (!$bodyIsCached && is_file($attachment['file_path'])) {
463 $body = file_get_contents($attachment['file_path']);
468 $flink = $attachment['file_name'];
471 $flink = $attachment['file_name'];
474 echo ', ';
477 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>";
484 print '<div id="plugin_forumml_message_content_'.$msg['id_message'].'" class="plugin_forumml_message_content_std">';
485 $body = str_replace("\r\n","\n", $body);
487 // If there is no cached html of if user requested to regenerate the cache, do it, otherwise use cached HTML.
488 if (!$bodyIsCached) {
489 // Purify message body, according to the content-type
491 // Update attachment links
492 $body = plugin_forumml_replace_attachment($msg['id_message'], $request->get('group_id'), $request->get('list'), $id_parent, $body);
494 // Use CODENDI_PURIFIER_FULL for html mails
495 $msg['cached_html'] = $hp->purify($body,CODENDI_PURIFIER_FULL,$request->get('group_id'));
497 // CODENDI_PURIFIER_FORUMML level : no basic html markups, no forms, no javascript,
498 // Allowed: url + automagic links + <blockquote>
499 $purified_body = $hp->purify($body,CODENDI_PURIFIER_CONVERT_HTML,$request->get('group_id'));
500 $purified_body = str_replace('>', '>', $purified_body);
504 $search_for_quotes = false;
505 $maxi = strlen($purified_body);
506 for($i = 0 ; $i < $maxi ; ++$i) {
507 if ($search_for_quotes) {
508 if($purified_body{$i} == ">") {
510 if($level < $current_level) {
511 $tab_body .= '<blockquote class="grep">';
515 $search_for_quotes = false;
516 if($level > $current_level) {
517 $tab_body .= '</blockquote>';
520 if($purified_body{$i} == "\n" && $i < $maxi - 1) {
521 $search_for_quotes = true;
524 $tab_body .= $purified_body{$i};
527 if($purified_body{$i} == "\n" && $i < $maxi - 1) {
528 $search_for_quotes = true;
531 $tab_body .= $purified_body{$i};
534 $purified_body = str_replace('>', '>', $purified_body);
535 $msg['cached_html'] = nl2br($tab_body);
537 getForumMLDao()->updateCacheHTML($msg['cached_html'] , $msg['id_message']);
539 echo $msg['cached_html'];
543 echo '<div class="plugin_forumml_message_footer">';
545 // If you click on 'Reply', load reply form
546 $vMess = new Valid_UInt('id_mess');
548 if ($request->valid($vMess) && $request->get('id_mess') == $msg['id_message']) {
549 $vReply = new Valid_WhiteList('reply',array(0,1));
551 if ($request->valid($vReply) && $request->get('reply') == 1) {
553 $body = $hp->purify($body, CODENDI_PURIFIER_STRIP_HTML);
555 $body = $hp->purify($body, CODENDI_PURIFIER_CONVERT_HTML);
557 plugin_forumml_reply($hp,$msg['subject'],$msg['id_message'],$id_parent,$body,$msg['sender']);
561 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']."'>
562 <img src='".$p->getThemePath()."/images/ic/comment_add.png'/>
571 // Display the post form under the current post
572 function plugin_forumml_reply($hp,$subject,$in_reply_to,$id_parent,$body,$author) {
574 $request =& HTTPRequest::instance();
575 $tab_tmp = explode("\n",$body);
576 $tab_tmp = array_pad($tab_tmp,-count($tab_tmp)-1,"$author wrote :");
578 echo '<script type="text/javascript" src="scripts/cc_attach_js.php"></script>';
579 echo ' <div id="reply-'. $in_reply_to .'" class="plugin_forumml_message_reply">'."
580 <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'>
581 <input type='hidden' name='reply_to' value='".$in_reply_to."'/>
582 <input type='hidden' name='subject' value='".$subject."'/>
583 <input type='hidden' name='list' value='".$request->get('list')."'/>
584 <input type='hidden' name='group_id' value='".$request->get('group_id')."'/>";
585 echo '<a href="javascript:;" onclick="addHeader(\'\',\'\',1);">['._('Add cc :').']</a>
586 - <a href="javascript:;" onclick="addHeader(\'\',\'\',2);">['._('Attach :').']</a>
587 <input type="hidden" value="0" id="header_val" />
588 <div id="mail_header"></div>';
589 echo "<p><textarea name='message' rows='15' cols='100'>";
591 foreach($tab_tmp as $k => $line) {
596 $indent = substr($line, 0, 4) == '>' ? '>' : '> ';
597 print($indent . $line."\n");
601 echo "</textarea></p>
603 <input type='submit' name='send_reply' value='"._('Submit')."'/>
604 <input type='reset' value='"._('Erase')."'/>
610 // Search & replace reference to attached content
611 // This happens for images attached to html messages (multipart/related)
612 function plugin_forumml_replace_attachment($id_message, $group_id, $list, $id_parent, $body) {
613 if (preg_match_all('/"cid:([^"]*)"/m', $body, $matches)) {
614 $search_parts = array();
615 $replace_parts = array();
616 foreach ($matches[1] as $match) {
617 $result = getForumMLDao()->getAttachment($id_mesage , $match) ;
618 if ($res && $res->rowCount() == 1) {
619 $row = $res->getRow();
620 $url = "upload.php?group_id=".$group_id."&list=".$list."&id=".$row['id_attachment']."&topic=".$id_parent;
621 $search_parts[] = 'cid:'.$match;
622 $replace_parts[] = $url;
625 if (count($replace_parts) > 0) {
626 $body = str_replace($search_parts, $replace_parts, $body);
632 // Build Mail headers, and send the mail
633 function plugin_forumml_process_mail($plug,$reply=false) {
635 $request = HTTPRequest::instance();
636 $hp = ForumML_HTMLPurifier::instance();
638 // Instantiate a new Mail class
641 // Build mail headers
642 $list = new MailmanList($request->get('group_id') , $request->get('list'));
643 $to = $list->getName()."@".forge_get_config('lists_host');
646 $mail->setFrom(UserManager::instance()->getCurrentUser()->getEmail());
648 $vMsg = new Valid_Text('message');
649 if ($request->valid($vMsg)) {
650 $message = $request->get('message');
653 $subject = $request->get('subject');
654 $mail->setSubject($subject);
657 // set In-Reply-To header
658 $hres = plugin_forumml_get_message_headers($request->get('reply_to'));
659 $reply_to = $hres['value'];
660 $mail->addAdditionalHeader("In-Reply-To",$reply_to);
664 if ($request->validArray(new Valid_Email('ccs')) && $request->exist('ccs')) {
667 foreach ($request->get('ccs') as $cc) {
668 if (trim($cc) != "") {
669 $cc_array[$idx] = $hp->purify($cc,CODENDI_PURIFIER_FULL);
673 // Checks sanity of CC List
676 foreach ($cc_array as $key => $cc) {
677 $umanager = UserManager::instance();
678 $user = $umanager->existEmail($cc);
686 $feedback .=_('Invalid email ').$err;
688 // add list of cc users to mail mime
689 if (count($cc_array) > 0) {
690 $cc_list = implode(',',$cc_array);
691 $mail->setCc($cc_list,true);
697 // Process attachments
699 // Define boundaries as specified in RFC:
700 // http://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
701 $boundary = '----=_NextPart';
702 $boundaryStart = '--'.$boundary;
703 $boundaryEnd = '--'.$boundary.'--';
705 // Attachments headers
706 if (isset($_FILES["files"]) && count($_FILES["files"]['name']) > 0) {
708 $text = "This is a multi-part message in MIME format.\n";
709 $text = "$boundaryStart\n";
710 $text .= "Content-Type: text/plain; charset=\"UTF-8\"\n";
711 $text .= "Content-Transfer-Encoding: 8bit\n\n";
714 foreach($_FILES["files"]['name'] as $i => $fileName) {
715 $attachment .= "$boundaryStart\n";
716 $attachment .= "Content-Type:".$_FILES["files"]["type"][$i]."; name=".$fileName."\n";
717 $attachment .= "Content-Transfer-Encoding: base64\n";
718 $attachment .= "Content-Disposition: attachment; filename=".$fileName."\n\n";
719 $attachment .= chunk_split(base64_encode(file_get_contents($_FILES["files"]["tmp_name"][$i])));
721 $attachment .= "\n$boundaryEnd\n";
722 $body = $text.$attachment;
723 // force MimeType to multipart/mixed as default (when instantiating new Mail object) is text/plain
724 $mail->setMimeType('multipart/mixed; boundary="'.$boundary.'"');
725 $mail->addAdditionalHeader("MIME-Version","1.0");
730 $mail->setBody($body);
733 $feedback .= _('Mail successfully sent ');
735 $feedback .= _('Sending mail failed');