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