[ Index ] |
PHP Cross Reference of DokuWiki |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Class for handling (email) subscriptions 4 * 5 * @author Adrian Lang <lang@cosmocode.de> 6 * @author Andreas Gohr <andi@splitbrain.org> 7 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 8 */ 9 class Subscription { 10 11 /** 12 * Check if subscription system is enabled 13 * 14 * @return bool 15 */ 16 public function isenabled() { 17 return actionOK('subscribe'); 18 } 19 20 /** 21 * Return the subscription meta file for the given ID 22 * 23 * @author Adrian Lang <lang@cosmocode.de> 24 * 25 * @param string $id The target page or namespace, specified by id; Namespaces 26 * are identified by appending a colon. 27 * @return string 28 */ 29 protected function file($id) { 30 $meta_fname = '.mlist'; 31 if((substr($id, -1, 1) === ':')) { 32 $meta_froot = getNS($id); 33 $meta_fname = '/'.$meta_fname; 34 } else { 35 $meta_froot = $id; 36 } 37 return metaFN((string) $meta_froot, $meta_fname); 38 } 39 40 /** 41 * Lock subscription info 42 * 43 * We don't use io_lock() her because we do not wait for the lock and use a larger stale time 44 * 45 * @author Adrian Lang <lang@cosmocode.de> 46 * @param string $id The target page or namespace, specified by id; Namespaces 47 * are identified by appending a colon. 48 * @return bool true, if you got a succesful lock 49 */ 50 protected function lock($id) { 51 global $conf; 52 53 $lock = $conf['lockdir'].'/_subscr_'.md5($id).'.lock'; 54 55 if(is_dir($lock) && time() - @filemtime($lock) > 60 * 5) { 56 // looks like a stale lock - remove it 57 @rmdir($lock); 58 } 59 60 // try creating the lock directory 61 if(!@mkdir($lock, $conf['dmode'])) { 62 return false; 63 } 64 65 if(!empty($conf['dperm'])) chmod($lock, $conf['dperm']); 66 return true; 67 } 68 69 /** 70 * Unlock subscription info 71 * 72 * @author Adrian Lang <lang@cosmocode.de> 73 * @param string $id The target page or namespace, specified by id; Namespaces 74 * are identified by appending a colon. 75 * @return bool 76 */ 77 protected function unlock($id) { 78 global $conf; 79 $lock = $conf['lockdir'].'/_subscr_'.md5($id).'.lock'; 80 return @rmdir($lock); 81 } 82 83 /** 84 * Construct a regular expression for parsing a subscription definition line 85 * 86 * @author Andreas Gohr <andi@splitbrain.org> 87 * 88 * @param string|array $user 89 * @param string|array $style 90 * @param string|array $data 91 * @return string complete regexp including delimiters 92 * @throws Exception when no data is passed 93 */ 94 protected function buildregex($user = null, $style = null, $data = null) { 95 // always work with arrays 96 $user = (array) $user; 97 $style = (array) $style; 98 $data = (array) $data; 99 100 // clean 101 $user = array_filter(array_map('trim', $user)); 102 $style = array_filter(array_map('trim', $style)); 103 $data = array_filter(array_map('trim', $data)); 104 105 // user names are encoded 106 $user = array_map('auth_nameencode', $user); 107 108 // quote 109 $user = array_map('preg_quote_cb', $user); 110 $style = array_map('preg_quote_cb', $style); 111 $data = array_map('preg_quote_cb', $data); 112 113 // join 114 $user = join('|', $user); 115 $style = join('|', $style); 116 $data = join('|', $data); 117 118 // any data at all? 119 if($user.$style.$data === '') throw new Exception('no data passed'); 120 121 // replace empty values, set which ones are optional 122 $sopt = ''; 123 $dopt = ''; 124 if($user === '') { 125 $user = '\S+'; 126 } 127 if($style === '') { 128 $style = '\S+'; 129 $sopt = '?'; 130 } 131 if($data === '') { 132 $data = '\S+'; 133 $dopt = '?'; 134 } 135 136 // assemble 137 return "/^($user)(?:\\s+($style))$sopt(?:\\s+($data))$dopt$/"; 138 } 139 140 /** 141 * Recursively search for matching subscriptions 142 * 143 * This function searches all relevant subscription files for a page or 144 * namespace. 145 * 146 * @author Adrian Lang <lang@cosmocode.de> 147 * 148 * @param string $page The target object’s (namespace or page) id 149 * @param string|array $user 150 * @param string|array $style 151 * @param string|array $data 152 * @return array 153 */ 154 public function subscribers($page, $user = null, $style = null, $data = null) { 155 if(!$this->isenabled()) return array(); 156 157 // Construct list of files which may contain relevant subscriptions. 158 $files = array(':' => $this->file(':')); 159 do { 160 $files[$page] = $this->file($page); 161 $page = getNS(rtrim($page, ':')).':'; 162 } while($page !== ':'); 163 164 $re = $this->buildregex($user, $style, $data); 165 166 // Handle files. 167 $result = array(); 168 foreach($files as $target => $file) { 169 if(!file_exists($file)) continue; 170 171 $lines = file($file); 172 foreach($lines as $line) { 173 // fix old style subscription files 174 if(strpos($line, ' ') === false) $line = trim($line)." every\n"; 175 176 // check for matching entries 177 if(!preg_match($re, $line, $m)) continue; 178 179 $u = rawurldecode($m[1]); // decode the user name 180 if(!isset($result[$target])) $result[$target] = array(); 181 $result[$target][$u] = array($m[2], $m[3]); // add to result 182 } 183 } 184 return array_reverse($result); 185 } 186 187 /** 188 * Adds a new subscription for the given page or namespace 189 * 190 * This will automatically overwrite any existent subscription for the given user on this 191 * *exact* page or namespace. It will *not* modify any subscription that may exist in higher namespaces. 192 * 193 * @param string $id The target page or namespace, specified by id; Namespaces 194 * are identified by appending a colon. 195 * @param string $user 196 * @param string $style 197 * @param string $data 198 * @throws Exception when user or style is empty 199 * @return bool 200 */ 201 public function add($id, $user, $style, $data = '') { 202 if(!$this->isenabled()) return false; 203 204 // delete any existing subscription 205 $this->remove($id, $user); 206 207 $user = auth_nameencode(trim($user)); 208 $style = trim($style); 209 $data = trim($data); 210 211 if(!$user) throw new Exception('no subscription user given'); 212 if(!$style) throw new Exception('no subscription style given'); 213 if(!$data) $data = time(); //always add current time for new subscriptions 214 215 $line = "$user $style $data\n"; 216 $file = $this->file($id); 217 return io_saveFile($file, $line, true); 218 } 219 220 /** 221 * Removes a subscription for the given page or namespace 222 * 223 * This removes all subscriptions matching the given criteria on the given page or 224 * namespace. It will *not* modify any subscriptions that may exist in higher 225 * namespaces. 226 * 227 * @param string $id The target object’s (namespace or page) id 228 * @param string|array $user 229 * @param string|array $style 230 * @param string|array $data 231 * @return bool 232 */ 233 public function remove($id, $user = null, $style = null, $data = null) { 234 if(!$this->isenabled()) return false; 235 236 $file = $this->file($id); 237 if(!file_exists($file)) return true; 238 239 $re = $this->buildregex($user, $style, $data); 240 return io_deleteFromFile($file, $re, true); 241 } 242 243 /** 244 * Get data for $INFO['subscribed'] 245 * 246 * $INFO['subscribed'] is either false if no subscription for the current page 247 * and user is in effect. Else it contains an array of arrays with the fields 248 * “target”, “style”, and optionally “data”. 249 * 250 * @param string $id Page ID, defaults to global $ID 251 * @param string $user User, defaults to $_SERVER['REMOTE_USER'] 252 * @return array 253 * @author Adrian Lang <lang@cosmocode.de> 254 */ 255 function user_subscription($id = '', $user = '') { 256 if(!$this->isenabled()) return false; 257 258 global $ID; 259 /** @var Input $INPUT */ 260 global $INPUT; 261 if(!$id) $id = $ID; 262 if(!$user) $user = $INPUT->server->str('REMOTE_USER'); 263 264 $subs = $this->subscribers($id, $user); 265 if(!count($subs)) return false; 266 267 $result = array(); 268 foreach($subs as $target => $info) { 269 $result[] = array( 270 'target' => $target, 271 'style' => $info[$user][0], 272 'data' => $info[$user][1] 273 ); 274 } 275 276 return $result; 277 } 278 279 /** 280 * Send digest and list subscriptions 281 * 282 * This sends mails to all subscribers that have a subscription for namespaces above 283 * the given page if the needed $conf['subscribe_time'] has passed already. 284 * 285 * This function is called form lib/exe/indexer.php 286 * 287 * @param string $page 288 * @return int number of sent mails 289 */ 290 public function send_bulk($page) { 291 if(!$this->isenabled()) return 0; 292 293 /** @var DokuWiki_Auth_Plugin $auth */ 294 global $auth; 295 global $conf; 296 global $USERINFO; 297 /** @var Input $INPUT */ 298 global $INPUT; 299 $count = 0; 300 301 $subscriptions = $this->subscribers($page, null, array('digest', 'list')); 302 303 // remember current user info 304 $olduinfo = $USERINFO; 305 $olduser = $INPUT->server->str('REMOTE_USER'); 306 307 foreach($subscriptions as $target => $users) { 308 if(!$this->lock($target)) continue; 309 310 foreach($users as $user => $info) { 311 list($style, $lastupdate) = $info; 312 313 $lastupdate = (int) $lastupdate; 314 if($lastupdate + $conf['subscribe_time'] > time()) { 315 // Less than the configured time period passed since last 316 // update. 317 continue; 318 } 319 320 // Work as the user to make sure ACLs apply correctly 321 $USERINFO = $auth->getUserData($user); 322 $INPUT->server->set('REMOTE_USER',$user); 323 if($USERINFO === false) continue; 324 if(!$USERINFO['mail']) continue; 325 326 if(substr($target, -1, 1) === ':') { 327 // subscription target is a namespace, get all changes within 328 $changes = getRecentsSince($lastupdate, null, getNS($target)); 329 } else { 330 // single page subscription, check ACL ourselves 331 if(auth_quickaclcheck($target) < AUTH_READ) continue; 332 $meta = p_get_metadata($target); 333 $changes = array($meta['last_change']); 334 } 335 336 // Filter out pages only changed in small and own edits 337 $change_ids = array(); 338 foreach($changes as $rev) { 339 $n = 0; 340 while(!is_null($rev) && $rev['date'] >= $lastupdate && 341 ($INPUT->server->str('REMOTE_USER') === $rev['user'] || 342 $rev['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT)) { 343 $pagelog = new PageChangeLog($rev['id']); 344 $rev = $pagelog->getRevisions($n++, 1); 345 $rev = (count($rev) > 0) ? $rev[0] : null; 346 } 347 348 if(!is_null($rev) && $rev['date'] >= $lastupdate) { 349 // Some change was not a minor one and not by myself 350 $change_ids[] = $rev['id']; 351 } 352 } 353 354 // send it 355 if($style === 'digest') { 356 foreach($change_ids as $change_id) { 357 $this->send_digest( 358 $USERINFO['mail'], $change_id, 359 $lastupdate 360 ); 361 $count++; 362 } 363 } elseif($style === 'list') { 364 $this->send_list($USERINFO['mail'], $change_ids, $target); 365 $count++; 366 } 367 // TODO: Handle duplicate subscriptions. 368 369 // Update notification time. 370 $this->add($target, $user, $style, time()); 371 } 372 $this->unlock($target); 373 } 374 375 // restore current user info 376 $USERINFO = $olduinfo; 377 $INPUT->server->set('REMOTE_USER',$olduser); 378 return $count; 379 } 380 381 /** 382 * Send the diff for some page change 383 * 384 * @param string $subscriber_mail The target mail address 385 * @param string $template Mail template ('subscr_digest', 'subscr_single', 'mailtext', ...) 386 * @param string $id Page for which the notification is 387 * @param int|null $rev Old revision if any 388 * @param string $summary Change summary if any 389 * @return bool true if successfully sent 390 */ 391 public function send_diff($subscriber_mail, $template, $id, $rev = null, $summary = '') { 392 global $DIFF_INLINESTYLES; 393 394 // prepare replacements (keys not set in hrep will be taken from trep) 395 $trep = array( 396 'PAGE' => $id, 397 'NEWPAGE' => wl($id, '', true, '&'), 398 'SUMMARY' => $summary, 399 'SUBSCRIBE' => wl($id, array('do' => 'subscribe'), true, '&') 400 ); 401 $hrep = array(); 402 403 if($rev) { 404 $subject = 'changed'; 405 $trep['OLDPAGE'] = wl($id, "rev=$rev", true, '&'); 406 407 $old_content = rawWiki($id, $rev); 408 $new_content = rawWiki($id); 409 410 $df = new Diff(explode("\n", $old_content), 411 explode("\n", $new_content)); 412 $dformat = new UnifiedDiffFormatter(); 413 $tdiff = $dformat->format($df); 414 415 $DIFF_INLINESTYLES = true; 416 $df = new Diff(explode("\n", $old_content), 417 explode("\n", $new_content)); 418 $dformat = new InlineDiffFormatter(); 419 $hdiff = $dformat->format($df); 420 $hdiff = '<table>'.$hdiff.'</table>'; 421 $DIFF_INLINESTYLES = false; 422 } else { 423 $subject = 'newpage'; 424 $trep['OLDPAGE'] = '---'; 425 $tdiff = rawWiki($id); 426 $hdiff = nl2br(hsc($tdiff)); 427 } 428 429 $trep['DIFF'] = $tdiff; 430 $hrep['DIFF'] = $hdiff; 431 432 $headers = array('Message-Id' => $this->getMessageID($id)); 433 if ($rev) { 434 $headers['In-Reply-To'] = $this->getMessageID($id, $rev); 435 } 436 437 return $this->send( 438 $subscriber_mail, $subject, $id, 439 $template, $trep, $hrep, $headers 440 ); 441 } 442 443 /** 444 * Send the diff for some media change 445 * 446 * @fixme this should embed thumbnails of images in HTML version 447 * 448 * @param string $subscriber_mail The target mail address 449 * @param string $template Mail template ('uploadmail', ...) 450 * @param string $id Media file for which the notification is 451 * @param int|bool $rev Old revision if any 452 */ 453 public function send_media_diff($subscriber_mail, $template, $id, $rev = false) { 454 global $conf; 455 456 $file = mediaFN($id); 457 list($mime, /* $ext */) = mimetype($id); 458 459 $trep = array( 460 'MIME' => $mime, 461 'MEDIA' => ml($id,'',true,'&',true), 462 'SIZE' => filesize_h(filesize($file)), 463 ); 464 465 if ($rev && $conf['mediarevisions']) { 466 $trep['OLD'] = ml($id, "rev=$rev", true, '&', true); 467 } else { 468 $trep['OLD'] = '---'; 469 } 470 471 $headers = array('Message-Id' => $this->getMessageID($id, @filemtime($file))); 472 if ($rev) { 473 $headers['In-Reply-To'] = $this->getMessageID($id, $rev); 474 } 475 476 $this->send($subscriber_mail, 'upload', $id, $template, $trep, null, $headers); 477 478 } 479 480 /** 481 * Send a notify mail on new registration 482 * 483 * @author Andreas Gohr <andi@splitbrain.org> 484 * 485 * @param string $login login name of the new user 486 * @param string $fullname full name of the new user 487 * @param string $email email address of the new user 488 * @return bool true if a mail was sent 489 */ 490 public function send_register($login, $fullname, $email) { 491 global $conf; 492 if(empty($conf['registernotify'])) return false; 493 494 $trep = array( 495 'NEWUSER' => $login, 496 'NEWNAME' => $fullname, 497 'NEWEMAIL' => $email, 498 ); 499 500 return $this->send( 501 $conf['registernotify'], 502 'new_user', 503 $login, 504 'registermail', 505 $trep 506 ); 507 } 508 509 /** 510 * Send a digest mail 511 * 512 * Sends a digest mail showing a bunch of changes of a single page. Basically the same as send_diff() 513 * but determines the last known revision first 514 * 515 * @author Adrian Lang <lang@cosmocode.de> 516 * 517 * @param string $subscriber_mail The target mail address 518 * @param string $id The ID 519 * @param int $lastupdate Time of the last notification 520 * @return bool 521 */ 522 protected function send_digest($subscriber_mail, $id, $lastupdate) { 523 $pagelog = new PageChangeLog($id); 524 $n = 0; 525 do { 526 $rev = $pagelog->getRevisions($n++, 1); 527 $rev = (count($rev) > 0) ? $rev[0] : null; 528 } while(!is_null($rev) && $rev > $lastupdate); 529 530 return $this->send_diff( 531 $subscriber_mail, 532 'subscr_digest', 533 $id, $rev 534 ); 535 } 536 537 /** 538 * Send a list mail 539 * 540 * Sends a list mail showing a list of changed pages. 541 * 542 * @author Adrian Lang <lang@cosmocode.de> 543 * 544 * @param string $subscriber_mail The target mail address 545 * @param array $ids Array of ids 546 * @param string $ns_id The id of the namespace 547 * @return bool true if a mail was sent 548 */ 549 protected function send_list($subscriber_mail, $ids, $ns_id) { 550 if(count($ids) === 0) return false; 551 552 $tlist = ''; 553 $hlist = '<ul>'; 554 foreach($ids as $id) { 555 $link = wl($id, array(), true); 556 $tlist .= '* '.$link.NL; 557 $hlist .= '<li><a href="'.$link.'">'.hsc($id).'</a></li>'.NL; 558 } 559 $hlist .= '</ul>'; 560 561 $id = prettyprint_id($ns_id); 562 $trep = array( 563 'DIFF' => rtrim($tlist), 564 'PAGE' => $id, 565 'SUBSCRIBE' => wl($id, array('do' => 'subscribe'), true, '&') 566 ); 567 $hrep = array( 568 'DIFF' => $hlist 569 ); 570 571 return $this->send( 572 $subscriber_mail, 573 'subscribe_list', 574 $ns_id, 575 'subscr_list', $trep, $hrep 576 ); 577 } 578 579 /** 580 * Helper function for sending a mail 581 * 582 * @author Adrian Lang <lang@cosmocode.de> 583 * 584 * @param string $subscriber_mail The target mail address 585 * @param string $subject The lang id of the mail subject (without the 586 * prefix “mail_”) 587 * @param string $context The context of this mail, eg. page or namespace id 588 * @param string $template The name of the mail template 589 * @param array $trep Predefined parameters used to parse the 590 * template (in text format) 591 * @param array $hrep Predefined parameters used to parse the 592 * template (in HTML format), null to default to $trep 593 * @param array $headers Additional mail headers in the form 'name' => 'value' 594 * @return bool 595 */ 596 protected function send($subscriber_mail, $subject, $context, $template, $trep, $hrep = null, $headers = array()) { 597 global $lang; 598 global $conf; 599 600 $text = rawLocale($template); 601 $subject = $lang['mail_'.$subject].' '.$context; 602 $mail = new Mailer(); 603 $mail->bcc($subscriber_mail); 604 $mail->subject($subject); 605 $mail->setBody($text, $trep, $hrep); 606 if(in_array($template, array('subscr_list', 'subscr_digest'))){ 607 $mail->from($conf['mailfromnobody']); 608 } 609 if(isset($trep['SUBSCRIBE'])) { 610 $mail->setHeader('List-Unsubscribe', '<'.$trep['SUBSCRIBE'].'>', false); 611 } 612 613 foreach ($headers as $header => $value) { 614 $mail->setHeader($header, $value); 615 } 616 617 return $mail->send(); 618 } 619 620 /** 621 * Get a valid message id for a certain $id and revision (or the current revision) 622 * 623 * @param string $id The id of the page (or media file) the message id should be for 624 * @param string $rev The revision of the page, set to the current revision of the page $id if not set 625 * @return string 626 */ 627 protected function getMessageID($id, $rev = null) { 628 static $listid = null; 629 if (is_null($listid)) { 630 $server = parse_url(DOKU_URL, PHP_URL_HOST); 631 $listid = join('.', array_reverse(explode('/', DOKU_BASE))).$server; 632 $listid = urlencode($listid); 633 $listid = strtolower(trim($listid, '.')); 634 } 635 636 if (is_null($rev)) { 637 $rev = @filemtime(wikiFN($id)); 638 } 639 640 return "<$id?rev=$rev@$listid>"; 641 } 642 643 /** 644 * Default callback for COMMON_NOTIFY_ADDRESSLIST 645 * 646 * Aggregates all email addresses of user who have subscribed the given page with 'every' style 647 * 648 * @author Steven Danz <steven-danz@kc.rr.com> 649 * @author Adrian Lang <lang@cosmocode.de> 650 * 651 * @todo move the whole functionality into this class, trigger SUBSCRIPTION_NOTIFY_ADDRESSLIST instead, 652 * use an array for the addresses within it 653 * 654 * @param array &$data Containing the entries: 655 * - $id (the page id), 656 * - $self (whether the author should be notified, 657 * - $addresslist (current email address list) 658 * - $replacements (array of additional string substitutions, @KEY@ to be replaced by value) 659 */ 660 public function notifyaddresses(&$data) { 661 if(!$this->isenabled()) return; 662 663 /** @var DokuWiki_Auth_Plugin $auth */ 664 global $auth; 665 global $conf; 666 /** @var Input $INPUT */ 667 global $INPUT; 668 669 $id = $data['id']; 670 $self = $data['self']; 671 $addresslist = $data['addresslist']; 672 673 $subscriptions = $this->subscribers($id, null, 'every'); 674 675 $result = array(); 676 foreach($subscriptions as $target => $users) { 677 foreach($users as $user => $info) { 678 $userinfo = $auth->getUserData($user); 679 if($userinfo === false) continue; 680 if(!$userinfo['mail']) continue; 681 if(!$self && $user == $INPUT->server->str('REMOTE_USER')) continue; //skip our own changes 682 683 $level = auth_aclcheck($id, $user, $userinfo['grps']); 684 if($level >= AUTH_READ) { 685 if(strcasecmp($userinfo['mail'], $conf['notify']) != 0) { //skip user who get notified elsewhere 686 $result[$user] = $userinfo['mail']; 687 } 688 } 689 } 690 } 691 $data['addresslist'] = trim($addresslist.','.implode(',', $result), ','); 692 } 693 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body