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 $subs = $this->subscribers($id, $user); 128 if (!count($subs)) { 129 return false; 130 } 131 132 $result = []; 133 foreach ($subs as $target => $info) { 134 $result[] = [ 135 'target' => $target, 136 'style' => $info[$user][0], 137 'data' => $info[$user][1], 138 ]; 139 } 140 141 return $result; 142 } 143 144 /** 145 * Recursively search for matching subscriptions 146 * 147 * This function searches all relevant subscription files for a page or 148 * namespace. 149 * 150 * @author Adrian Lang <lang@cosmocode.de> 151 * 152 * @param string $page The target object’s (namespace or page) id 153 * @param string|array $user 154 * @param string|array $style 155 * @param string|array $data 156 * 157 * @return array 158 */ 159 public function subscribers($page, $user = null, $style = null, $data = null) 160 { 161 if (!$this->isenabled()) { 162 return []; 163 } 164 165 // Construct list of files which may contain relevant subscriptions. 166 $files = [':' => $this->file(':')]; 167 do { 168 $files[$page] = $this->file($page); 169 $page = getNS(rtrim($page, ':')) . ':'; 170 } while ($page !== ':'); 171 172 $regexBuilder = new SubscriberRegexBuilder(); 173 $re = $regexBuilder->buildRegex($user, $style, $data); 174 175 // Handle files. 176 $result = []; 177 foreach ($files as $target => $file) { 178 if (!file_exists($file)) { 179 continue; 180 } 181 182 $lines = file($file); 183 foreach ($lines as $line) { 184 // fix old style subscription files 185 if (strpos($line, ' ') === false) { 186 $line = trim($line) . " every\n"; 187 } 188 189 // check for matching entries 190 if (!preg_match($re, $line, $m)) { 191 continue; 192 } 193 194 $u = rawurldecode($m[1]); // decode the user name 195 if (!isset($result[$target])) { 196 $result[$target] = []; 197 } 198 $result[$target][$u] = [$m[2], $m[3]]; // add to result 199 } 200 } 201 return array_reverse($result); 202 } 203 204 /** 205 * Default callback for COMMON_NOTIFY_ADDRESSLIST 206 * 207 * Aggregates all email addresses of user who have subscribed the given page with 'every' style 208 * 209 * @author Adrian Lang <lang@cosmocode.de> 210 * @author Steven Danz <steven-danz@kc.rr.com> 211 * 212 * @todo move the whole functionality into this class, trigger SUBSCRIPTION_NOTIFY_ADDRESSLIST instead, 213 * use an array for the addresses within it 214 * 215 * @param array &$data Containing the entries: 216 * - $id (the page id), 217 * - $self (whether the author should be notified, 218 * - $addresslist (current email address list) 219 * - $replacements (array of additional string substitutions, @KEY@ to be replaced by value) 220 */ 221 public function notifyAddresses(&$data) 222 { 223 if (!$this->isenabled()) { 224 return; 225 } 226 227 /** @var DokuWiki_Auth_Plugin $auth */ 228 global $auth; 229 global $conf; 230 /** @var \Input $INPUT */ 231 global $INPUT; 232 233 $id = $data['id']; 234 $self = $data['self']; 235 $addresslist = $data['addresslist']; 236 237 $subscriptions = $this->subscribers($id, null, 'every'); 238 239 $result = []; 240 foreach ($subscriptions as $target => $users) { 241 foreach ($users as $user => $info) { 242 $userinfo = $auth->getUserData($user); 243 if ($userinfo === false) { 244 continue; 245 } 246 if (!$userinfo['mail']) { 247 continue; 248 } 249 if (!$self && $user == $INPUT->server->str('REMOTE_USER')) { 250 continue; 251 } //skip our own changes 252 253 $level = auth_aclcheck($id, $user, $userinfo['grps']); 254 if ($level >= AUTH_READ) { 255 if (strcasecmp($userinfo['mail'], $conf['notify']) != 0) { //skip user who get notified elsewhere 256 $result[$user] = $userinfo['mail']; 257 } 258 } 259 } 260 } 261 $data['addresslist'] = trim($addresslist . ',' . implode(',', $result), ','); 262 } 263 264 /** 265 * Return the subscription meta file for the given ID 266 * 267 * @author Adrian Lang <lang@cosmocode.de> 268 * 269 * @param string $id The target page or namespace, specified by id; Namespaces 270 * are identified by appending a colon. 271 * 272 * @return string 273 */ 274 protected function file($id) 275 { 276 $meta_fname = '.mlist'; 277 if ((substr($id, -1, 1) === ':')) { 278 $meta_froot = getNS($id); 279 $meta_fname = '/' . $meta_fname; 280 } else { 281 $meta_froot = $id; 282 } 283 return metaFN((string)$meta_froot, $meta_fname); 284 } 285} 286