xref: /dokuwiki/inc/Subscriptions/SubscriberManager.php (revision 51ee2399b9fa7b6a12080e5b0c448f753c553a24)
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