1<?php 2 3namespace dokuwiki\Subscriptions; 4 5use dokuwiki\ChangeLog\PageChangeLog; 6use dokuwiki\Extension\AuthPlugin; 7use dokuwiki\Input\Input; 8use Exception; 9 10class BulkSubscriptionSender extends SubscriptionSender 11{ 12 /** 13 * Send digest and list subscriptions 14 * 15 * This sends mails to all subscribers that have a subscription for namespaces above 16 * the given page if the needed $conf['subscribe_time'] has passed already. 17 * 18 * This function is called form lib/exe/indexer.php 19 * 20 * @param string $page 21 * @return int number of sent mails 22 * @throws Exception 23 */ 24 public function sendBulk($page) 25 { 26 $subscriberManager = new SubscriberManager(); 27 if (!$subscriberManager->isenabled()) { 28 return 0; 29 } 30 31 /** @var AuthPlugin $auth */ 32 global $auth; 33 global $conf; 34 global $USERINFO; 35 /** @var Input $INPUT */ 36 global $INPUT; 37 $count = 0; 38 39 $subscriptions = $subscriberManager->subscribers($page, null, ['digest', 'list']); 40 41 // remember current user info 42 $olduinfo = $USERINFO; 43 $olduser = $INPUT->server->str('REMOTE_USER'); 44 45 foreach ($subscriptions as $target => $users) { 46 if (!$this->lock($target)) { 47 continue; 48 } 49 50 foreach ($users as $user => $info) { 51 [$style, $lastupdate] = $info; 52 53 $lastupdate = (int)$lastupdate; 54 if ($lastupdate + $conf['subscribe_time'] > time()) { 55 // Less than the configured time period passed since last 56 // update. 57 continue; 58 } 59 60 // Work as the user to make sure ACLs apply correctly 61 $USERINFO = $auth->getUserData($user); 62 $INPUT->server->set('REMOTE_USER', $user); 63 if ($USERINFO === false) { 64 continue; 65 } 66 if (!$USERINFO['mail']) { 67 continue; 68 } 69 70 if (str_ends_with($target, ':')) { 71 // subscription target is a namespace, get all changes within 72 $changes = getRecentsSince($lastupdate, null, getNS($target)); 73 } else { 74 // single page subscription, check ACL ourselves 75 if (auth_quickaclcheck($target) < AUTH_READ) { 76 continue; 77 } 78 $meta = p_get_metadata($target); 79 $changes = [$meta['last_change']]; 80 } 81 82 // Filter out pages only changed in small and own edits 83 $change_ids = []; 84 foreach ($changes as $rev) { 85 $n = 0; 86 $pagelog = new PageChangeLog($rev['id']); 87 while ( 88 !is_null($rev) && $rev['date'] >= $lastupdate && 89 ($INPUT->server->str('REMOTE_USER') === $rev['user'] || 90 $rev['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) 91 ) { 92 $revisions = $pagelog->getRevisions($n++, 1); 93 $rev = ($revisions !== []) ? $pagelog->getRevisionInfo($revisions[0]) : null; 94 } 95 96 if (!is_null($rev) && $rev['date'] >= $lastupdate) { 97 // Some change was not a minor one and not by myself 98 $change_ids[] = $rev['id']; 99 } 100 } 101 102 // send it 103 if ($style === 'digest') { 104 foreach ($change_ids as $change_id) { 105 $this->sendDigest( 106 $USERINFO['mail'], 107 $change_id, 108 $lastupdate 109 ); 110 $count++; 111 } 112 } elseif ($style === 'list') { 113 $this->sendList($USERINFO['mail'], $change_ids, $target, $lastupdate); 114 $count++; 115 } 116 // TODO: Handle duplicate subscriptions. 117 118 // Update notification time. 119 $subscriberManager->add($target, $user, $style, time()); 120 } 121 $this->unlock($target); 122 } 123 124 // restore current user info 125 $USERINFO = $olduinfo; 126 $INPUT->server->set('REMOTE_USER', $olduser); 127 return $count; 128 } 129 130 /** 131 * Lock subscription info 132 * 133 * We don't use io_lock() her because we do not wait for the lock and use a larger stale time 134 * 135 * @param string $id The target page or namespace, specified by id; Namespaces 136 * are identified by appending a colon. 137 * 138 * @return bool true, if you got a succesful lock 139 * @author Adrian Lang <lang@cosmocode.de> 140 */ 141 protected function lock($id) 142 { 143 global $conf; 144 145 $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock'; 146 147 if (is_dir($lock) && time() - @filemtime($lock) > 60 * 5) { 148 // looks like a stale lock - remove it 149 @rmdir($lock); 150 } 151 152 // try creating the lock directory 153 if (!@mkdir($lock)) { 154 return false; 155 } 156 157 if ($conf['dperm']) { 158 chmod($lock, $conf['dperm']); 159 } 160 return true; 161 } 162 163 /** 164 * Unlock subscription info 165 * 166 * @param string $id The target page or namespace, specified by id; Namespaces 167 * are identified by appending a colon. 168 * 169 * @return bool 170 * @author Adrian Lang <lang@cosmocode.de> 171 */ 172 protected function unlock($id) 173 { 174 global $conf; 175 $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock'; 176 return @rmdir($lock); 177 } 178 179 /** 180 * Return the last revision before lastupdate. 181 * 182 * @param string $id The target page or namespace, specified by id; Namespaces 183 * are identified by appending a colon. 184 * @param int $lastupdate Time of the last notification 185 * 186 * @return int|null The revision timestamp, or null if no earlier revision exists 187 * @author Michael Stapelberg <stapelberg+dokuwiki@google.com> 188 */ 189 protected function lastRevBefore($id, $lastupdate) 190 { 191 $pagelog = new PageChangeLog($id); 192 $n = 0; 193 do { 194 $rev = $pagelog->getRevisions($n++, 1); 195 $rev = ($rev !== []) ? $rev[0] : null; 196 } while (!is_null($rev) && $rev > $lastupdate); 197 return $rev; 198 } 199 200 /** 201 * Send a digest mail 202 * 203 * Sends a digest mail showing a bunch of changes of a single page. Basically the same as sendPageDiff() 204 * but determines the last known revision first 205 * 206 * @param string $subscriber_mail The target mail address 207 * @param string $id The ID 208 * @param int $lastupdate Time of the last notification 209 * 210 * @return bool 211 * @author Adrian Lang <lang@cosmocode.de> 212 * 213 */ 214 protected function sendDigest($subscriber_mail, $id, $lastupdate) 215 { 216 $rev = $this->lastRevBefore($id, $lastupdate); 217 218 // TODO I'm not happy with the following line and passing $this->mailer around. Not sure how to solve it better 219 $pageSubSender = new PageSubscriptionSender($this->mailer); 220 return $pageSubSender->sendPageDiff( 221 $subscriber_mail, 222 'subscr_digest', 223 $id, 224 $rev 225 ); 226 } 227 228 /** 229 * Send a list mail 230 * 231 * Sends a list mail showing a list of changed pages. 232 * 233 * @param string $subscriber_mail The target mail address 234 * @param array $ids Array of ids 235 * @param string $ns_id The id of the namespace 236 * @param int $lastupdate Time of the last notification 237 * 238 * @return bool true if a mail was sent 239 * @author Adrian Lang <lang@cosmocode.de> 240 * 241 */ 242 protected function sendList($subscriber_mail, $ids, $ns_id, $lastupdate) 243 { 244 global $lang; 245 246 if ($ids === []) { 247 return false; 248 } 249 250 $tlist = ''; 251 $hlist = '<ul>'; 252 foreach ($ids as $id) { 253 $last = $this->lastRevBefore($id, $lastupdate); 254 $link = wl($id, [], true); 255 $difflink = $last ? wl($id, ['do' => 'diff', 'rev' => $last], true) : null; 256 257 $tlist .= '* ' . $link; 258 if ($difflink) { 259 $tlist .= ' (' . $lang['diff'] . ': ' . $difflink . ')'; 260 } 261 $tlist .= NL; 262 263 $hlist .= '<li>'; 264 $hlist .= '<a href="' . $link . '">' . hsc($id) . '</a>'; 265 if ($difflink) { 266 $hlist .= ' (<a href="' . $difflink . '">' . $lang['diff'] . '</a>)'; 267 } 268 $hlist .= '</li>' . NL; 269 } 270 $hlist .= '</ul>'; 271 272 $id = prettyprint_id($ns_id); 273 $trep = [ 274 'DIFF' => rtrim($tlist), 275 'PAGE' => $id, 276 'SUBSCRIBE' => wl($id, ['do' => 'subscribe'], true, '&'), 277 ]; 278 $hrep = [ 279 'DIFF' => $hlist, 280 ]; 281 282 return $this->send( 283 $subscriber_mail, 284 'subscribe_list', 285 $ns_id, 286 'subscr_list', 287 $trep, 288 $hrep 289 ); 290 } 291} 292