1704a815fSMichael Große<?php 2704a815fSMichael Große 3704a815fSMichael Großenamespace dokuwiki\Subscriptions; 4704a815fSMichael Große 5704a815fSMichael Großeuse dokuwiki\ChangeLog\PageChangeLog; 651ee2399SGerrit Uitslaguse dokuwiki\Extension\AuthPlugin; 7704a815fSMichael Großeuse dokuwiki\Input\Input; 851ee2399SGerrit Uitslaguse Exception; 9704a815fSMichael Große 10704a815fSMichael Großeclass BulkSubscriptionSender extends SubscriptionSender 11704a815fSMichael Große{ 12704a815fSMichael Große /** 13704a815fSMichael Große * Send digest and list subscriptions 14704a815fSMichael Große * 15704a815fSMichael Große * This sends mails to all subscribers that have a subscription for namespaces above 16704a815fSMichael Große * the given page if the needed $conf['subscribe_time'] has passed already. 17704a815fSMichael Große * 18704a815fSMichael Große * This function is called form lib/exe/indexer.php 19704a815fSMichael Große * 20704a815fSMichael Große * @param string $page 21704a815fSMichael Große * @return int number of sent mails 2251ee2399SGerrit Uitslag * @throws Exception 23704a815fSMichael Große */ 24704a815fSMichael Große public function sendBulk($page) 25704a815fSMichael Große { 26704a815fSMichael Große $subscriberManager = new SubscriberManager(); 27704a815fSMichael Große if (!$subscriberManager->isenabled()) { 28704a815fSMichael Große return 0; 29704a815fSMichael Große } 30704a815fSMichael Große 3151ee2399SGerrit Uitslag /** @var AuthPlugin $auth */ 32704a815fSMichael Große global $auth; 33704a815fSMichael Große global $conf; 34704a815fSMichael Große global $USERINFO; 35704a815fSMichael Große /** @var Input $INPUT */ 36704a815fSMichael Große global $INPUT; 37704a815fSMichael Große $count = 0; 38704a815fSMichael Große 39704a815fSMichael Große $subscriptions = $subscriberManager->subscribers($page, null, ['digest', 'list']); 40704a815fSMichael Große 41704a815fSMichael Große // remember current user info 42704a815fSMichael Große $olduinfo = $USERINFO; 43704a815fSMichael Große $olduser = $INPUT->server->str('REMOTE_USER'); 44704a815fSMichael Große 45704a815fSMichael Große foreach ($subscriptions as $target => $users) { 46704a815fSMichael Große if (!$this->lock($target)) { 47704a815fSMichael Große continue; 48704a815fSMichael Große } 49704a815fSMichael Große 50704a815fSMichael Große foreach ($users as $user => $info) { 515983e241SAndreas Gohr [$style, $lastupdate] = $info; 52704a815fSMichael Große 53704a815fSMichael Große $lastupdate = (int)$lastupdate; 54704a815fSMichael Große if ($lastupdate + $conf['subscribe_time'] > time()) { 55704a815fSMichael Große // Less than the configured time period passed since last 56704a815fSMichael Große // update. 57704a815fSMichael Große continue; 58704a815fSMichael Große } 59704a815fSMichael Große 60704a815fSMichael Große // Work as the user to make sure ACLs apply correctly 61704a815fSMichael Große $USERINFO = $auth->getUserData($user); 62704a815fSMichael Große $INPUT->server->set('REMOTE_USER', $user); 63704a815fSMichael Große if ($USERINFO === false) { 64704a815fSMichael Große continue; 65704a815fSMichael Große } 66704a815fSMichael Große if (!$USERINFO['mail']) { 67704a815fSMichael Große continue; 68704a815fSMichael Große } 69704a815fSMichael Große 706c16a3a9Sfiwswe if (str_ends_with($target, ':')) { 71704a815fSMichael Große // subscription target is a namespace, get all changes within 72704a815fSMichael Große $changes = getRecentsSince($lastupdate, null, getNS($target)); 73704a815fSMichael Große } else { 74704a815fSMichael Große // single page subscription, check ACL ourselves 75704a815fSMichael Große if (auth_quickaclcheck($target) < AUTH_READ) { 76704a815fSMichael Große continue; 77704a815fSMichael Große } 78704a815fSMichael Große $meta = p_get_metadata($target); 79704a815fSMichael Große $changes = [$meta['last_change']]; 80704a815fSMichael Große } 81704a815fSMichael Große 82704a815fSMichael Große // Filter out pages only changed in small and own edits 83704a815fSMichael Große $change_ids = []; 84704a815fSMichael Große foreach ($changes as $rev) { 85704a815fSMichael Große $n = 0; 86e2905cd4SAndreas Gohr $pagelog = new PageChangeLog($rev['id']); 877d34963bSAndreas Gohr while ( 887d34963bSAndreas Gohr !is_null($rev) && $rev['date'] >= $lastupdate && 89704a815fSMichael Große ($INPUT->server->str('REMOTE_USER') === $rev['user'] || 90252acce3SSatoshi Sahara $rev['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) 91252acce3SSatoshi Sahara ) { 92e2905cd4SAndreas Gohr $revisions = $pagelog->getRevisions($n++, 1); 935983e241SAndreas Gohr $rev = ($revisions !== []) ? $pagelog->getRevisionInfo($revisions[0]) : null; 94704a815fSMichael Große } 95704a815fSMichael Große 96704a815fSMichael Große if (!is_null($rev) && $rev['date'] >= $lastupdate) { 97704a815fSMichael Große // Some change was not a minor one and not by myself 98704a815fSMichael Große $change_ids[] = $rev['id']; 99704a815fSMichael Große } 100704a815fSMichael Große } 101704a815fSMichael Große 102704a815fSMichael Große // send it 103704a815fSMichael Große if ($style === 'digest') { 104704a815fSMichael Große foreach ($change_ids as $change_id) { 105704a815fSMichael Große $this->sendDigest( 106704a815fSMichael Große $USERINFO['mail'], 107704a815fSMichael Große $change_id, 108704a815fSMichael Große $lastupdate 109704a815fSMichael Große ); 110704a815fSMichael Große $count++; 111704a815fSMichael Große } 1125983e241SAndreas Gohr } elseif ($style === 'list') { 113fcc2e27aSMichael Stapelberg $this->sendList($USERINFO['mail'], $change_ids, $target, $lastupdate); 114704a815fSMichael Große $count++; 115704a815fSMichael Große } 116704a815fSMichael Große // TODO: Handle duplicate subscriptions. 117704a815fSMichael Große 118704a815fSMichael Große // Update notification time. 119704a815fSMichael Große $subscriberManager->add($target, $user, $style, time()); 120704a815fSMichael Große } 121704a815fSMichael Große $this->unlock($target); 122704a815fSMichael Große } 123704a815fSMichael Große 124704a815fSMichael Große // restore current user info 125704a815fSMichael Große $USERINFO = $olduinfo; 126704a815fSMichael Große $INPUT->server->set('REMOTE_USER', $olduser); 127704a815fSMichael Große return $count; 128704a815fSMichael Große } 129704a815fSMichael Große 130704a815fSMichael Große /** 131704a815fSMichael Große * Lock subscription info 132704a815fSMichael Große * 133704a815fSMichael Große * We don't use io_lock() her because we do not wait for the lock and use a larger stale time 134704a815fSMichael Große * 135704a815fSMichael Große * @param string $id The target page or namespace, specified by id; Namespaces 136704a815fSMichael Große * are identified by appending a colon. 137704a815fSMichael Große * 138704a815fSMichael Große * @return bool true, if you got a succesful lock 139704a815fSMichael Große * @author Adrian Lang <lang@cosmocode.de> 140704a815fSMichael Große */ 141704a815fSMichael Große protected function lock($id) 142704a815fSMichael Große { 143704a815fSMichael Große global $conf; 144704a815fSMichael Große 145704a815fSMichael Große $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock'; 146704a815fSMichael Große 147704a815fSMichael Große if (is_dir($lock) && time() - @filemtime($lock) > 60 * 5) { 148704a815fSMichael Große // looks like a stale lock - remove it 149704a815fSMichael Große @rmdir($lock); 150704a815fSMichael Große } 151704a815fSMichael Große 152704a815fSMichael Große // try creating the lock directory 153bd539124SAndreas Gohr if (!@mkdir($lock)) { 154704a815fSMichael Große return false; 155704a815fSMichael Große } 156704a815fSMichael Große 15723420346SDamien Regad if ($conf['dperm']) { 158704a815fSMichael Große chmod($lock, $conf['dperm']); 159704a815fSMichael Große } 160704a815fSMichael Große return true; 161704a815fSMichael Große } 162704a815fSMichael Große 163704a815fSMichael Große /** 164704a815fSMichael Große * Unlock subscription info 165704a815fSMichael Große * 166704a815fSMichael Große * @param string $id The target page or namespace, specified by id; Namespaces 167704a815fSMichael Große * are identified by appending a colon. 168704a815fSMichael Große * 169704a815fSMichael Große * @return bool 170704a815fSMichael Große * @author Adrian Lang <lang@cosmocode.de> 171704a815fSMichael Große */ 172704a815fSMichael Große protected function unlock($id) 173704a815fSMichael Große { 174704a815fSMichael Große global $conf; 175704a815fSMichael Große $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock'; 176704a815fSMichael Große return @rmdir($lock); 177704a815fSMichael Große } 178704a815fSMichael Große 179704a815fSMichael Große /** 180fcc2e27aSMichael Stapelberg * Return the last revision before lastupdate. 181fcc2e27aSMichael Stapelberg * 182fcc2e27aSMichael Stapelberg * @param string $id The target page or namespace, specified by id; Namespaces 183fcc2e27aSMichael Stapelberg * are identified by appending a colon. 184*0a5c6ce4SAndreas Gohr * @param int $lastupdate Time of the last notification 185fcc2e27aSMichael Stapelberg * 186*0a5c6ce4SAndreas Gohr * @return int|null The revision timestamp, or null if no earlier revision exists 187fcc2e27aSMichael Stapelberg * @author Michael Stapelberg <stapelberg+dokuwiki@google.com> 188fcc2e27aSMichael Stapelberg */ 189fcc2e27aSMichael Stapelberg protected function lastRevBefore($id, $lastupdate) 190fcc2e27aSMichael Stapelberg { 191fcc2e27aSMichael Stapelberg $pagelog = new PageChangeLog($id); 192fcc2e27aSMichael Stapelberg $n = 0; 193fcc2e27aSMichael Stapelberg do { 194fcc2e27aSMichael Stapelberg $rev = $pagelog->getRevisions($n++, 1); 195fcc2e27aSMichael Stapelberg $rev = ($rev !== []) ? $rev[0] : null; 196fcc2e27aSMichael Stapelberg } while (!is_null($rev) && $rev > $lastupdate); 197fcc2e27aSMichael Stapelberg return $rev; 198fcc2e27aSMichael Stapelberg } 199fcc2e27aSMichael Stapelberg 200fcc2e27aSMichael Stapelberg /** 201704a815fSMichael Große * Send a digest mail 202704a815fSMichael Große * 203704a815fSMichael Große * Sends a digest mail showing a bunch of changes of a single page. Basically the same as sendPageDiff() 204704a815fSMichael Große * but determines the last known revision first 205704a815fSMichael Große * 206704a815fSMichael Große * @param string $subscriber_mail The target mail address 207704a815fSMichael Große * @param string $id The ID 208704a815fSMichael Große * @param int $lastupdate Time of the last notification 209704a815fSMichael Große * 210704a815fSMichael Große * @return bool 211704a815fSMichael Große * @author Adrian Lang <lang@cosmocode.de> 212704a815fSMichael Große * 213704a815fSMichael Große */ 214704a815fSMichael Große protected function sendDigest($subscriber_mail, $id, $lastupdate) 215704a815fSMichael Große { 216fcc2e27aSMichael Stapelberg $rev = $this->lastRevBefore($id, $lastupdate); 217704a815fSMichael Große 218704a815fSMichael Große // TODO I'm not happy with the following line and passing $this->mailer around. Not sure how to solve it better 219704a815fSMichael Große $pageSubSender = new PageSubscriptionSender($this->mailer); 220704a815fSMichael Große return $pageSubSender->sendPageDiff( 221704a815fSMichael Große $subscriber_mail, 222704a815fSMichael Große 'subscr_digest', 223704a815fSMichael Große $id, 224704a815fSMichael Große $rev 225704a815fSMichael Große ); 226704a815fSMichael Große } 227704a815fSMichael Große 228704a815fSMichael Große /** 229704a815fSMichael Große * Send a list mail 230704a815fSMichael Große * 231704a815fSMichael Große * Sends a list mail showing a list of changed pages. 232704a815fSMichael Große * 233704a815fSMichael Große * @param string $subscriber_mail The target mail address 234704a815fSMichael Große * @param array $ids Array of ids 235704a815fSMichael Große * @param string $ns_id The id of the namespace 236fcc2e27aSMichael Stapelberg * @param int $lastupdate Time of the last notification 237704a815fSMichael Große * 238704a815fSMichael Große * @return bool true if a mail was sent 239704a815fSMichael Große * @author Adrian Lang <lang@cosmocode.de> 240704a815fSMichael Große * 241704a815fSMichael Große */ 242fcc2e27aSMichael Stapelberg protected function sendList($subscriber_mail, $ids, $ns_id, $lastupdate) 243704a815fSMichael Große { 244fcc2e27aSMichael Stapelberg global $lang; 245fcc2e27aSMichael Stapelberg 2465983e241SAndreas Gohr if ($ids === []) { 247704a815fSMichael Große return false; 248704a815fSMichael Große } 249704a815fSMichael Große 250704a815fSMichael Große $tlist = ''; 251704a815fSMichael Große $hlist = '<ul>'; 252704a815fSMichael Große foreach ($ids as $id) { 253fcc2e27aSMichael Stapelberg $last = $this->lastRevBefore($id, $lastupdate); 254704a815fSMichael Große $link = wl($id, [], true); 255*0a5c6ce4SAndreas Gohr $difflink = $last ? wl($id, ['do' => 'diff', 'rev' => $last], true) : null; 256*0a5c6ce4SAndreas Gohr 257*0a5c6ce4SAndreas Gohr $tlist .= '* ' . $link; 258*0a5c6ce4SAndreas Gohr if ($difflink) { 259*0a5c6ce4SAndreas Gohr $tlist .= ' (' . $lang['diff'] . ': ' . $difflink . ')'; 260*0a5c6ce4SAndreas Gohr } 261*0a5c6ce4SAndreas Gohr $tlist .= NL; 262*0a5c6ce4SAndreas Gohr 263fcc2e27aSMichael Stapelberg $hlist .= '<li>'; 264fcc2e27aSMichael Stapelberg $hlist .= '<a href="' . $link . '">' . hsc($id) . '</a>'; 265*0a5c6ce4SAndreas Gohr if ($difflink) { 266fcc2e27aSMichael Stapelberg $hlist .= ' (<a href="' . $difflink . '">' . $lang['diff'] . '</a>)'; 267fcc2e27aSMichael Stapelberg } 268fcc2e27aSMichael Stapelberg $hlist .= '</li>' . NL; 269704a815fSMichael Große } 270704a815fSMichael Große $hlist .= '</ul>'; 271704a815fSMichael Große 272704a815fSMichael Große $id = prettyprint_id($ns_id); 273704a815fSMichael Große $trep = [ 274704a815fSMichael Große 'DIFF' => rtrim($tlist), 275704a815fSMichael Große 'PAGE' => $id, 276704a815fSMichael Große 'SUBSCRIBE' => wl($id, ['do' => 'subscribe'], true, '&'), 277704a815fSMichael Große ]; 278704a815fSMichael Große $hrep = [ 279704a815fSMichael Große 'DIFF' => $hlist, 280704a815fSMichael Große ]; 281704a815fSMichael Große 282704a815fSMichael Große return $this->send( 283704a815fSMichael Große $subscriber_mail, 284704a815fSMichael Große 'subscribe_list', 285704a815fSMichael Große $ns_id, 286704a815fSMichael Große 'subscr_list', 287704a815fSMichael Große $trep, 288704a815fSMichael Große $hrep 289704a815fSMichael Große ); 290704a815fSMichael Große } 291704a815fSMichael Große} 292