1<?php 2 3namespace dokuwiki\Subscriptions; 4 5use dokuwiki\Extension\AuthPlugin; 6use dokuwiki\Input\Input; 7use Exception; 8 9class SubscriberManager 10{ 11 12 /** 13 * Check if subscription system is enabled 14 * 15 * @return bool 16 */ 17 public function isenabled() 18 { 19 return actionOK('subscribe'); 20 } 21 22 /** 23 * Adds a new subscription for the given page or namespace 24 * 25 * This will automatically overwrite any existent subscription for the given user on this 26 * *exact* page or namespace. It will *not* modify any subscription that may exist in higher namespaces. 27 * 28 * @throws Exception when user or style is empty 29 * 30 * @param string $id The target page or namespace, specified by id; Namespaces 31 * are identified by appending a colon. 32 * @param string $user 33 * @param string $style 34 * @param string $data 35 * 36 * @return bool 37 */ 38 public function add($id, $user, $style, $data = '') 39 { 40 if (!$this->isenabled()) { 41 return false; 42 } 43 44 // delete any existing subscription 45 $this->remove($id, $user); 46 47 $user = auth_nameencode(trim($user)); 48 $style = trim($style); 49 $data = trim($data); 50 51 if (!$user) { 52 throw new Exception('no subscription user given'); 53 } 54 if (!$style) { 55 throw new Exception('no subscription style given'); 56 } 57 if (!$data) { 58 $data = time(); 59 } //always add current time for new subscriptions 60 61 $line = "$user $style $data\n"; 62 $file = $this->file($id); 63 return io_saveFile($file, $line, true); 64 } 65 66 67 /** 68 * Removes a subscription for the given page or namespace 69 * 70 * This removes all subscriptions matching the given criteria on the given page or 71 * namespace. It will *not* modify any subscriptions that may exist in higher 72 * namespaces. 73 * 74 * @param string $id The target object’s (namespace or page) id 75 * @param string|array $user 76 * @param string|array $style 77 * @param string|array $data 78 * 79 * @return bool 80 * @throws Exception 81 */ 82 public function remove($id, $user = null, $style = null, $data = null) 83 { 84 if (!$this->isenabled()) { 85 return false; 86 } 87 88 $file = $this->file($id); 89 if (!file_exists($file)) { 90 return true; 91 } 92 93 $regexBuilder = new SubscriberRegexBuilder(); 94 $re = $regexBuilder->buildRegex($user, $style, $data); 95 return io_deleteFromFile($file, $re, true); 96 } 97 98 /** 99 * Get data for $INFO['subscribed'] 100 * 101 * $INFO['subscribed'] is either false if no subscription for the current page 102 * and user is in effect. Else it contains an array of arrays with the fields 103 * “target”, “style”, and optionally “data”. 104 * 105 * @param string $id Page ID, defaults to global $ID 106 * @param string $user User, defaults to $_SERVER['REMOTE_USER'] 107 * 108 * @return array|false 109 * @throws Exception 110 * 111 * @author Adrian Lang <lang@cosmocode.de> 112 */ 113 public function userSubscription($id = '', $user = '') 114 { 115 if (!$this->isenabled()) { 116 return false; 117 } 118 119 global $ID; 120 /** @var Input $INPUT */ 121 global $INPUT; 122 if (!$id) { 123 $id = $ID; 124 } 125 if (!$user) { 126 $user = $INPUT->server->str('REMOTE_USER'); 127 } 128 129 if (empty($user)) { 130 // not logged in 131 return false; 132 } 133 134 $subs = $this->subscribers($id, $user); 135 if ($subs === []) { 136 return false; 137 } 138 139 $result = []; 140 foreach ($subs as $target => $info) { 141 $result[] = [ 142 'target' => $target, 143 'style' => $info[$user][0], 144 'data' => $info[$user][1], 145 ]; 146 } 147 148 return $result; 149 } 150 151 /** 152 * Recursively search for matching subscriptions 153 * 154 * This function searches all relevant subscription files for a page or 155 * namespace. 156 * 157 * @param string $page The target object’s (namespace or page) id 158 * @param string|array $user 159 * @param string|array $style 160 * @param string|array $data 161 * 162 * @return array 163 * @throws Exception 164 * 165 * @author Adrian Lang <lang@cosmocode.de> 166 * 167 */ 168 public function subscribers($page, $user = null, $style = null, $data = null) 169 { 170 if (!$this->isenabled()) { 171 return []; 172 } 173 174 // Construct list of files which may contain relevant subscriptions. 175 $files = [':' => $this->file(':')]; 176 do { 177 $files[$page] = $this->file($page); 178 $page = getNS(rtrim($page, ':')) . ':'; 179 } while ($page !== ':'); 180 181 $regexBuilder = new SubscriberRegexBuilder(); 182 $re = $regexBuilder->buildRegex($user, $style, $data); 183 184 // Handle files. 185 $result = []; 186 foreach ($files as $target => $file) { 187 if (!file_exists($file)) { 188 continue; 189 } 190 191 $lines = file($file); 192 foreach ($lines as $line) { 193 // fix old style subscription files 194 if (strpos($line, ' ') === false) { 195 $line = trim($line) . " every\n"; 196 } 197 198 // check for matching entries 199 if (!preg_match($re, $line, $m)) { 200 continue; 201 } 202 203 // if no last sent is set, use 0 204 if (!isset($m[3])) { 205 $m[3] = 0; 206 } 207 208 $u = rawurldecode($m[1]); // decode the user name 209 if (!isset($result[$target])) { 210 $result[$target] = []; 211 } 212 $result[$target][$u] = [$m[2], $m[3]]; // add to result 213 } 214 } 215 return array_reverse($result); 216 } 217 218 /** 219 * Default callback for COMMON_NOTIFY_ADDRESSLIST 220 * 221 * Aggregates all email addresses of user who have subscribed the given page with 'every' style 222 * 223 * @param array &$data Containing the entries: 224 * - $id (the page id), 225 * - $self (whether the author should be notified, 226 * - $addresslist (current email address list) 227 * - $replacements (array of additional string substitutions, @KEY@ to be replaced by value) 228 * @throws Exception 229 * 230 * @author Adrian Lang <lang@cosmocode.de> 231 * @author Steven Danz <steven-danz@kc.rr.com> 232 * 233 * @todo move the whole functionality into this class, trigger SUBSCRIPTION_NOTIFY_ADDRESSLIST instead, 234 * use an array for the addresses within it 235 */ 236 public function notifyAddresses(&$data) 237 { 238 if (!$this->isenabled()) { 239 return; 240 } 241 242 /** @var AuthPlugin $auth */ 243 global $auth; 244 global $conf; 245 /** @var \Input $INPUT */ 246 global $INPUT; 247 248 $id = $data['id']; 249 $self = $data['self']; 250 $addresslist = $data['addresslist']; 251 252 $subscriptions = $this->subscribers($id, null, 'every'); 253 254 $result = []; 255 foreach ($subscriptions as $users) { 256 foreach ($users as $user => $info) { 257 $userinfo = $auth->getUserData($user); 258 if ($userinfo === false) { 259 continue; 260 } 261 if (!$userinfo['mail']) { 262 continue; 263 } 264 if (!$self && $user == $INPUT->server->str('REMOTE_USER')) { 265 continue; 266 } //skip our own changes 267 268 $level = auth_aclcheck($id, $user, $userinfo['grps']); 269 if ($level >= AUTH_READ) { 270 if (strcasecmp($userinfo['mail'], $conf['notify']) != 0) { //skip user who get notified elsewhere 271 $result[$user] = $userinfo['mail']; 272 } 273 } 274 } 275 } 276 $data['addresslist'] = trim($addresslist . ',' . implode(',', $result), ','); 277 } 278 279 /** 280 * Return the subscription meta file for the given ID 281 * 282 * @author Adrian Lang <lang@cosmocode.de> 283 * 284 * @param string $id The target page or namespace, specified by id; Namespaces 285 * are identified by appending a colon. 286 * 287 * @return string 288 */ 289 protected function file($id) 290 { 291 $meta_fname = '.mlist'; 292 if ((substr($id, -1, 1) === ':')) { 293 $meta_froot = getNS($id); 294 $meta_fname = '/' . $meta_fname; 295 } else { 296 $meta_froot = $id; 297 } 298 return metaFN((string)$meta_froot, $meta_fname); 299 } 300} 301