1<?php
2
3/* Modern Contact Plugin for Dokuwiki
4 *
5 * Copyright (C) 2008 Bob Baddeley (bobbaddeley.com)
6 * Copyright (C) 2010-2012 Marvin Thomas Rabe (marvinrabe.de)
7 * Copyright (C) 2020 Luffah
8 *
9 * This program is free software; you can redistribute it and/or modify it under the terms
10 * of the GNU General Public License as published by the Free Software Foundation; either
11 * version 3 of the License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
14 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
15 * See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along with this program;
18 * if not, see <http://www.gnu.org/licenses/>. */
19
20/**
21 * Embed a send email form onto any page
22 * @license GNU General Public License 3 <http://www.gnu.org/licenses/>
23 * @author Bob Baddeley <bob@bobbaddeley.com>
24 * @author Marvin Thomas Rabe <mrabe@marvinrabe.de>
25 * @author Luffah <contact@luffah.xyz>
26 */
27
28if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
29if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
30require_once(DOKU_PLUGIN.'syntax.php');
31require_once(DOKU_INC.'inc/auth.php');
32require_once(dirname(__file__).'/recaptchalib.php');
33
34class syntax_plugin_groupmail extends DokuWiki_Syntax_Plugin {
35
36  public static $captcha = false;
37  public static $lastFormIdx = 1;
38
39  private static $recipientFields = array(
40    'toemail', 'touser', 'togroup',
41    'ccemail', 'ccuser', 'ccgroup',
42    'bccemail', 'bccuser', 'bccgroup'
43  );
44  private $formId = '';
45  private $status = 1;
46  private $statusMessage;
47  private $errorFlags = array();
48  private $recipient_groups = array();
49  private $sender_groups = array();
50
51  /**
52   * Syntax type
53   */
54  public function getType(){
55    return 'container';
56  }
57
58  public function getPType(){
59    return 'block';
60  }
61
62  /**
63   * Where to sort in?
64   */
65  public function getSort(){
66    return 300;
67  }
68
69  /**
70   * Connect pattern to lexer.
71   */
72  public function connectTo($mode) {
73    $this->Lexer->addSpecialPattern('\{\{groupmail>[^}]*\}\}',$mode,'plugin_groupmail');
74  }
75
76  /**
77   * Handle the match.
78   */
79  public function handle($match, $state, $pos, Doku_Handler $handler){
80    if (isset($_REQUEST['comment']))
81      return false;
82
83    $match = substr($match,12,-2); //strip markup from start and end
84    $data = array();
85
86    //handle params
87    foreach(explode('|',$match) as $param){
88      $splitparam = explode('=',$param,2);
89      $key = $splitparam[0];
90      $val = count($splitparam)==2 ? $splitparam[1]:Null;
91      //multiple targets/profils possible for the email
92      //add multiple to field in the dokuwiki page code
93      // example : {{groupmail>to=profile1,profile2|subject=Feedback from Site}}
94      if (in_array($key, syntax_plugin_groupmail::$recipientFields)){
95        if (is_null($val)) continue;
96        if (isset($data[$key])){
97          $data[$key] .= ",".$val; //it is a "toemail" param but not the first
98        }else{
99          $data[$key] = $val; // it is the first "toemail" param
100        }
101
102      } else if ($key=='autofrom'){  // autofrom doesn't require value
103        $data[$key] = is_null($val) ? 'true' : $val;
104      } else {
105        $data[$splitparam[0]] = $splitparam[1]; // All other parameters
106      }
107    }
108    return $data;
109  }
110
111  private function check_recipient_access($data, $field){
112    if (isset($data[$field])){
113      $typ=substr($field, -4);
114      $vals = explode(',' , $data[$field]);
115
116      if ($typ == 'roup') {  # group list type
117        foreach ($vals as $group) {
118          if (!in_array($group, $this->recipient_groups)){
119            $this->_set_error("acl_togroup", array($group, $field), 'acl');
120          }
121        }
122      } elseif ($typ == 'user') {  # user list type
123        foreach ($vals as $userId) {
124          $info = $auth->getUserData($userId);
125          if (isset($info)) {
126            if (count(array_intersect($info['grps'], $this->recipient_groups))==0) {
127              $this->_set_error("acl_touser", array($userId, $field), 'acl');
128            }
129          } else {
130            $this->_set_error("acl_touser", array($userId, $field), 'acl');
131          }
132        }
133      }
134    }
135  }
136
137  private function getexplodedvals($data, $fields){
138    $res = array();
139    foreach ($fields as $field){
140      if (isset($data[$field]))
141        $res[$field] = explode(',' , $data[$field]);
142    }
143    return $res;
144  }
145
146  /**
147   * Create output.
148   */
149  public function render($mode, Doku_Renderer $renderer, $data) {
150    global $USERINFO;
151
152    if($mode == 'xhtml'){
153      // Disable cache
154      $renderer->info['cache'] = false;
155
156      /**
157       * Basic access rights based on group
158       */
159      $this->sender_groups = explode(',', $this->getConf('sender_groups'));
160      $this->recipient_groups = explode(',', $this->getConf('recipient_groups'));
161
162      if (count(array_intersect($USERINFO['grps'], $this->sender_groups))==0) {
163        // user have no right to see this
164        return true;
165      }
166
167      if ( !$this->getConf('allow_email') && (
168        isset($data['toemail']) || isset($data['bccemail']) || isset($data['ccemail'])
169      )) {
170        $this->_set_error("external_email_forbidden", Null, 'acl');
171      }
172      $vals = $this->getExplodedVals($data, syntax_plugin_groupmail::$recipientFields);
173      if ( $this->getConf('confidentiality') == 'one' ) {
174        $nb_recipients=0;
175        if (preg_grep('/(cc|group)/', array_keys($vals)))
176          $nb_recipients+=2;
177
178        foreach(array('touser', 'toemail', 'bccuser', 'bccemail') as $field)
179          if (isset($vals[$field])) $nb_recipients+=count($vals[$field]);
180
181        if ($nb_recipients > 1)
182          $this->_set_error("acl_one", Null, 'acl');
183      } elseif ( $this->getConf('confidentiality') == 'bcc' ) {
184        $nb_recipients=0;
185        if (preg_grep('/^(cc|togroup)/', array_keys($vals)))
186          $nb_recipients+=2;
187
188        if (preg_grep('/^bcc/', array_keys($vals)))
189          $nb_recipients+=1;
190
191        foreach(array('touser', 'toemail') as $field)
192          if (isset($vals[$field])) $nb_recipients+=count($vals[$field]);
193
194        if ($nb_recipients > 1)
195          $this->_set_error("acl_bcc", Null, 'acl');
196      }
197      $this->check_recipient_access($vals, 'togroup');
198      $this->check_recipient_access($vals, 'touser');
199      $this->check_recipient_access($vals, 'ccgroup');
200      $this->check_recipient_access($vals, 'ccuser');
201      $this->check_recipient_access($vals, 'bccgroup');
202      $this->check_recipient_access($vals, 'bccuser');
203      if ($this->errorFlags['acl']){
204        $renderer->doc .= $this->_html_status_box();
205        return true;
206      }
207
208      // Define unique form id
209      $this->formId = 'groupmail-form-'.(syntax_plugin_groupmail::$lastFormIdx++);
210
211
212      $renderer->doc .= $this->mailgroup_form($data, $vals);
213      return true;
214    }
215    return false;
216  }
217
218  /*
219   * Build the mail form
220   */
221  private function mailgroup_form ($data, $recipientVals){
222    global $USERINFO;
223
224    // Is there none captcha on the side?
225    $captcha = ($this->getConf('captcha') == 1 && syntax_plugin_groupmail::$captcha == false)?true:false;
226
227    // Setup send log destination
228    if ( isset($data['sendlog']) )
229      $sendlog = $data['sendlog'];
230    elseif ( '' != $this->getConf('sendlog') )
231      $sendlog = $this->getConf('sendlog');
232
233    $ret = '<form id="'.$this->formId.'" action="'.$_SERVER['REQUEST_URI'].'#'.$this->formId.'" method="POST">';
234
235    // Send message and give feedback
236    if (isset($_POST['submit-'.$this->formId]))
237      if($this->_send_groupmail($captcha, $sendlog))
238        $ret .= $this->_html_status_box();
239
240    if (isset($data['subject']))
241      $ret .= '<input type="hidden" name="subject" value="'.$data['subject'].'" />';
242
243    foreach (array_keys($recipientVals) as $field) {
244      $ret .= '<input type="hidden" name="'.$field.'" value="'.$data[$field].'" />';
245    }
246
247    // Build view for form items
248    $ret .= "<fieldset>";
249    if  (isset($data['title'])) {
250      $title = $data['title'];
251    } else {
252      $title = '';
253      $sep = '';
254      if (isset($data['subject'])) { $title .= '"'.$data['subject'].'"'; $sep=' ';  }
255      $and = False;
256      if (isset($data['touser']))  { $title .= $sep.'to '. $data['touser']; $sep=', '; $and=True;}
257      if (isset($data['togroup'])) { $title .= $sep.($and? '': 'to '). $data['togroup']; $sep=', ';}
258      $and = False;
259      if (isset($data['ccuser']))  { $title .= $sep.'cc '.  $data['ccuser']; $sep=', '; $and=True; }
260      if (isset($data['ccgroup'])) { $title .= $sep.($and? '': 'cc ').  $data['ccgroup']; $sep=', ';}
261      $and = False;
262      if (isset($data['bccuser'])) { $title .= 'bcc '.  $data['bccuser']; $sep=', '; $and=True;}
263      if (isset($data['bccgroup'])){ $title .= $sep.($and? '': 'bcc ').  $data['bccgroup']; $sep=', ';}
264    }
265    $ret .= "<legend>".$title."</legend>";
266    if ( !isset($data['autofrom']) || $data['autofrom'] != 'true' ) {
267      $ret .= $this->_form_row($this->getLang("name"), 'name', 'text', $USERINFO['name']);
268      $ret .= $this->_form_row($this->getLang("email"), 'required_email', 'text', $USERINFO['mail']);
269    }
270    if ( !isset($data['subject']) )
271      $ret .= $this->_form_row($this->getLang("subject"), 'subject', 'text');
272
273    $ret .= $this->_form_row($this->getLang("content"), 'content', 'textarea',
274      isset($data['content']) ? $data['content'] : '');
275
276    // Captcha
277    if($captcha) {
278      if($this->errorFlags["captcha"]) {
279        $ret .= '<style>#recaptcha_response_field { border: 1px solid #e18484 !important; }</style>';
280      }
281      $ret .= "<tr><td colspan='2'>"
282        . "<script type='text/javascript'>var RecaptchaOptions = { lang : '".$conf['lang']."', "
283        . "theme : '".$this->getConf('recaptchalayout')."' };</script>"
284        . recaptcha_get_html($this->getConf('recaptchakey'))."</td></tr>";
285      syntax_plugin_groupmail::$captcha = true;
286    }
287
288
289    if (isset($data['autofrom']) && $data['autofrom'] == 'true' ) {
290      $ret .= '<input type="hidden" name="email" value="'.$USERINFO['mail'].'" />';
291      $ret .= '<input type="hidden" name="name" value="'.$USERINFO['name'].'" />';
292    }
293
294    $ret .= '<input type="submit" name="submit-'.$this->formId.'" value="'.$this->getLang('send').'" />';
295    $ret .= "</fieldset>";
296
297    $ret .= "</form>";
298
299    return $ret;
300  }
301
302  private function send_mail ($to, $subject, $content, $from, $cc, $bcc) {
303    // send a mail
304    $mail = new Mailer();
305    $mail->to($to);
306    $mail->cc($cc);
307    $mail->bcc($bcc);
308    $mail->from($from);
309    $mail->subject($subject);
310    $mail->setBody($content);
311    $ok = $mail->send();
312    return $ok;
313  }
314
315  private function _email_list(){ // string, string ...
316    global $auth;
317    $items = array();
318    foreach (func_get_args() as $field) {
319      if (!isset($_REQUEST[$field])) continue;
320      $typ=substr($field, -4);
321      $vals=explode(',' , $_POST[$field]);
322      if ($typ == 'roup') {  # group list type
323        if (!method_exists($auth, "retrieveUsers")) continue;
324        foreach ($vals as $grp) {
325          $userInfoHash = $auth->retrieveUsers(0,-1,array('grps'=>'^'.preg_quote($grp,'/').'$'));
326          foreach ($userInfoHash as $u => $info) { array_push($items, $info['mail']); }
327        }
328      } elseif ($typ == 'user') {  # user list type
329        foreach ($vals as $userId) {
330          $info = $auth->getUserData($userId);
331          if (isset($info)) {
332            array_push($items, $info['mail']);
333          }
334        }
335      } else { # mail list type
336        foreach($email as $vals){  array_push($items, $email); }
337      }
338    }
339    return $items;
340  }
341
342  /**
343   * Check values are somehow valid
344   */
345  private function _validate_value($val, $typ, $as_array=False, $multiline=False){
346    # FIXME improve security if possible
347    if ($as_array) {
348      foreach ($val as $v) { $this->_validate_value($v, $typ, False, $multiline); }
349      return;
350    }
351    if ($typ == 'email' || $typ == 'to' || $typ == 'from' || $typ == 'cc' || $typ == 'bcc') {
352      if(!mail_isvalid($val)) $this->_set_error("valid_".$typ, Null, $typ);
353    }
354    if ((!$multiline  && preg_match("/(\r|\n)/",$val)) || preg_match("/(MIME-Version: )/",$val) || preg_match("/(Content-Type: )/",$val)){
355      $this->_set_error("valid_".$typ, Null, $typ);
356    }
357  }
358
359  /**
360   * Verify and send email content.�
361   */
362  protected function _send_groupmail($captcha=false, $sendlog){
363    global $auth;
364    global $USERINFO;
365    global $ID;
366
367    require_once(DOKU_INC.'inc/mail.php');
368
369    $name  = $_POST['name'];
370    $email = $_POST['email'];
371    $subject = $_POST['subject'];
372    $comment = $_POST['content'];
373
374    // required fields filled
375    if(strlen($_POST['content']) < 10) $this->_set_error('content');
376    if(strlen($name) < 2) $this->_set_error('name');
377
378    // checks recaptcha answer
379    if($this->getConf('captcha') == 1 && $captcha == true) {
380      $resp = recaptcha_check_answer ($this->getConf('recaptchasecret'),
381        $_SERVER["REMOTE_ADDR"],
382        $_POST["recaptcha_challenge_field"],
383        $_POST["recaptcha_response_field"]);
384      if (!$resp->is_valid) $this->_set_error('captcha');
385    }
386
387    // record email in log
388    $lastline = '';
389    if ( isset($sendlog)  &&  $sendlog != '' ) {
390      $targetpage = htmlspecialchars(trim($sendlog));
391      $oldrecord = rawWiki($targetpage);
392      $bytes = bin2hex(random_bytes(8));
393      $newrecord = '====== msg'.$bytes.' ======'."\n";
394      $newrecord .= "**<nowiki>".$subject."</nowiki>** \n";
395      $newrecord .= '//'.$this->getLang("from").' '.$name.($this->getConf('confidentiality') =='all'?' <'.$email.'>':'');
396      $newrecord .= ' '.strftime($this->getLang("datetime"))."//\n";
397      $newrecord .= "\n<code>\n".trim($comment,"\n\t ")."\n</code>\n\n";
398      saveWikiText($targetpage, $newrecord.$oldrecord, "New entry", true);
399      $lastline .= $this->getLang("viewonline").wl($ID,'', true).'?id='.$targetpage."#msg".$bytes."\n\n\n";
400
401      $this->statusMessage = $this->getLang("viewonline").'<a href="'.wl($ID,'', true).'?id='.$targetpage."#msg".$bytes.'">'.$bytes."</a>";
402    }
403
404    $comment .= "\n\n";
405    $comment .= '---------------------------------------------------------------'."\n";
406    $comment .= $this->getLang("sent by")." ".$name.' <'.$email.'>'."\n";
407    $comment .= $this->getLang("via").wl($ID,'',true)."\n";
408    $comment .= $lastline;
409
410    $to = $this->_email_list('toemail', 'touser', 'togroup');
411    if (count($to) == 0) {
412      array_push($to, $this->getConf('default'));
413    }
414
415    $ccs = array_diff($this->_email_list('ccemail', 'ccuser', 'ccgroup'), $to);
416    $bccs = array_diff($this->_email_list('bccemail', 'bccuser', 'bccgroup'), $to, $ccs);
417
418    // A bunch of tests to secure content
419    $this->_validate_value($name, 'name');
420    $this->_validate_value($email, 'email');
421    $this->_validate_value($subject, 'subject');
422    $this->_validate_value($to, 'to', True);
423    $this->_validate_value($css, 'cc', True);
424    $this->_validate_value($bccs, 'bcc', True);
425    $this->_validate_value($comment, 'content', False, True);
426
427    // Status has not changed.
428    if($this->status != 0) {
429      // send only if message is not empty
430      // this should never be the case anyway because the form has
431      // validation to ensure a non-empty comment
432      if (trim($comment, " \t") != ''){
433        if ($this->send_mail($to, $subject, $comment, $email, $ccs, $bccs)){
434          $this->statusMessage = $this->getLang("success")."\n".$this->statusMessage;
435        } else {
436          $this->_set_error('unknown');
437        }
438      }
439    }
440
441    return true;
442  }
443
444  /**
445   * Manage error messages.
446   */
447  protected function _set_error($msgid, $args=Null, $type=Null) {
448    $lang = $this->getLang("error");
449    if (is_null($type)) $type=$msgid;
450    $msgstr = $lang[$msgid];
451    if (!is_null($args)){
452      $msgstr = vprintf($msgstr, $args);
453    }
454    $this->status = 0;
455    $this->statusMessage .= empty($this->statusMessage)?$msgstr:'<br>'.$msgstr;
456    $this->errorFlags[$type] = true;
457  }
458
459  /**
460   * Show up error messages.
461   */
462  protected function _html_status_box() {
463    $res = '<p class="'.(($this->status == 0)?'groupmail_error':'groupmail_success').'">'.$this->statusMessage.'</p>';
464    $this->statusMessage = '';
465    $this->errorFlags = array();
466    return $res;
467  }
468
469  /**
470   * Renders a form row.
471   */
472  protected function _form_row($label, $name, $type, $default='') {
473    $value = (isset($_POST['submit-'.$this->formId]) && $this->status == 0)?$_POST[$name]:$default;
474    $class = ($this->errorFlags[$name])?'class="error_field"':'';
475    $row = '<label for="'.$name.'">'.$label.'</label>';
476    if($type == 'textarea')
477      $row .= '<textarea name="'.$name.'" wrap="on" cols="40" rows="6" '.$class.' required>'.$value.'</textarea>';
478    elseif($type == 'multiple_email')
479      $row .= '<input type="email" name="'.$name.'" '.$class.' multiple>';
480    elseif($type == 'required_email')
481      $row .= '<input type="email" name="'.$name.'" '.$class.' required>';
482    else
483      $row .= '<input type="'.$type.'" value="'.$value.'" name="'.$name.'" '.$class.'>';
484    return $row;
485  }
486
487}
488