[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> subscription.php (source)

   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  }