1<?php 2 3namespace dokuwiki\Subscriptions; 4 5use dokuwiki\Input\Input; 6use DokuWiki_Auth_Plugin; 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 */ 81 public function remove($id, $user = null, $style = null, $data = null) 82 { 83 if (!$this->isenabled()) { 84 return false; 85 } 86 87 $file = $this->file($id); 88 if (!file_exists($file)) { 89 return true; 90 } 91 92 $regexBuilder = new SubscriberRegexBuilder(); 93 $re = $regexBuilder->buildRegex($user, $style, $data); 94 return io_deleteFromFile($file, $re, true); 95 } 96 97 /** 98 * Get data for $INFO['subscribed'] 99 * 100 * $INFO['subscribed'] is either false if no subscription for the current page 101 * and user is in effect. Else it contains an array of arrays with the fields 102 * “target”, “style”, and optionally “data”. 103 * 104 * @author Adrian Lang <lang@cosmocode.de> 105 * 106 * @param string $id Page ID, defaults to global $ID 107 * @param string $user User, defaults to $_SERVER['REMOTE_USER'] 108 * 109 * @return array|false 110 */ 111 public function userSubscription($id = '', $user = '') 112 { 113 if (!$this->isenabled()) { 114 return false; 115 } 116 117 global $ID; 118 /** @var Input $INPUT */ 119 global $INPUT; 120 if (!$id) { 121 $id = $ID; 122 } 123 if (!$user) { 124 $user = $INPUT->server->str('REMOTE_USER'); 125 } 126 127 if (empty($user)) { 128 // not logged in 129 return false; 130 } 131 132 $subs = $this->subscribers($id, $user); 133 if (!count($subs)) { 134 return false; 135 } 136 137 $result = []; 138 foreach ($subs as $target => $info) { 139 $result[] = [ 140 'target' => $target, 141 'style' => $info[$user][0], 142 'data' => $info[$user][1], 143 ]; 144 } 145 146 return $result; 147 } 148 149 /** 150 * Recursively search for matching subscriptions 151 * 152 * This function searches all relevant subscription files for a page or 153 * namespace. 154 * 155 * @author Adrian Lang <lang@cosmocode.de> 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 */ 164 public function subscribers($page, $user = null, $style = null, $data = null) 165 { 166 if (!$this->isenabled()) { 167 return []; 168 } 169 170 // Construct list of files which may contain relevant subscriptions. 171 $files = [':' => $this->file(':')]; 172 do { 173 $files[$page] = $this->file($page); 174 $page = getNS(rtrim($page, ':')) . ':'; 175 } while ($page !== ':'); 176 177 $regexBuilder = new SubscriberRegexBuilder(); 178 $re = $regexBuilder->buildRegex($user, $style, $data); 179 180 // Handle files. 181 $result = []; 182 foreach ($files as $target => $file) { 183 if (!file_exists($file)) { 184 continue; 185 } 186 187 $lines = file($file); 188 foreach ($lines as $line) { 189 // fix old style subscription files 190 if (strpos($line, ' ') === false) { 191 $line = trim($line) . " every\n"; 192 } 193 194 // check for matching entries 195 if (!preg_match($re, $line, $m)) { 196 continue; 197 } 198 199 $u = rawurldecode($m[1]); // decode the user name 200 if (!isset($result[$target])) { 201 $result[$target] = []; 202 } 203 $result[$target][$u] = [$m[2], $m[3]]; // add to result 204 } 205 } 206 return array_reverse($result); 207 } 208 209 /** 210 * Default callback for COMMON_NOTIFY_ADDRESSLIST 211 * 212 * Aggregates all email addresses of user who have subscribed the given page with 'every' style 213 * 214 * @author Adrian Lang <lang@cosmocode.de> 215 * @author Steven Danz <steven-danz@kc.rr.com> 216 * 217 * @todo move the whole functionality into this class, trigger SUBSCRIPTION_NOTIFY_ADDRESSLIST instead, 218 * use an array for the addresses within it 219 * 220 * @param array &$data Containing the entries: 221 * - $id (the page id), 222 * - $self (whether the author should be notified, 223 * - $addresslist (current email address list) 224 * - $replacements (array of additional string substitutions, @KEY@ to be replaced by value) 225 */ 226 public function notifyAddresses(&$data) 227 { 228 if (!$this->isenabled()) { 229 return; 230 } 231 232 /** @var DokuWiki_Auth_Plugin $auth */ 233 global $auth; 234 global $conf; 235 /** @var \Input $INPUT */ 236 global $INPUT; 237 238 $id = $data['id']; 239 $self = $data['self']; 240 $addresslist = $data['addresslist']; 241 242 $subscriptions = $this->subscribers($id, null, 'every'); 243 244 $result = []; 245 foreach ($subscriptions as $target => $users) { 246 foreach ($users as $user => $info) { 247 $userinfo = $auth->getUserData($user); 248 if ($userinfo === false) { 249 continue; 250 } 251 if (!$userinfo['mail']) { 252 continue; 253 } 254 if (!$self && $user == $INPUT->server->str('REMOTE_USER')) { 255 continue; 256 } //skip our own changes 257 258 $level = auth_aclcheck($id, $user, $userinfo['grps']); 259 if ($level >= AUTH_READ) { 260 if (strcasecmp($userinfo['mail'], $conf['notify']) != 0) { //skip user who get notified elsewhere 261 $result[$user] = $userinfo['mail']; 262 } 263 } 264 } 265 } 266 $data['addresslist'] = trim($addresslist . ',' . implode(',', $result), ','); 267 } 268 269 /** 270 * Return the subscription meta file for the given ID 271 * 272 * @author Adrian Lang <lang@cosmocode.de> 273 * 274 * @param string $id The target page or namespace, specified by id; Namespaces 275 * are identified by appending a colon. 276 * 277 * @return string 278 */ 279 protected function file($id) 280 { 281 $meta_fname = '.mlist'; 282 if ((substr($id, -1, 1) === ':')) { 283 $meta_froot = getNS($id); 284 $meta_fname = '/' . $meta_fname; 285 } else { 286 $meta_froot = $id; 287 } 288 return metaFN((string)$meta_froot, $meta_fname); 289 } 290} 291