xref: /dokuwiki/inc/Subscriptions/SubscriberManager.php (revision bff2c9d24314e25b31ceb53d51de76d678a0a4dc)
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 ($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                // if no last sent is set, use 0
200                if (!isset($m[3])) {
201                    $m[3] = 0;
202                }
203
204                $u = rawurldecode($m[1]); // decode the user name
205                if (!isset($result[$target])) {
206                    $result[$target] = [];
207                }
208                $result[$target][$u] = [$m[2], $m[3]]; // add to result
209            }
210        }
211        return array_reverse($result);
212    }
213
214    /**
215     * Default callback for COMMON_NOTIFY_ADDRESSLIST
216     *
217     * Aggregates all email addresses of user who have subscribed the given page with 'every' style
218     *
219     * @author Adrian Lang <lang@cosmocode.de>
220     * @author Steven Danz <steven-danz@kc.rr.com>
221     *
222     * @todo   move the whole functionality into this class, trigger SUBSCRIPTION_NOTIFY_ADDRESSLIST instead,
223     *         use an array for the addresses within it
224     *
225     * @param array &$data Containing the entries:
226     *                     - $id (the page id),
227     *                     - $self (whether the author should be notified,
228     *                     - $addresslist (current email address list)
229     *                     - $replacements (array of additional string substitutions, @KEY@ to be replaced by value)
230     */
231    public function notifyAddresses(&$data)
232    {
233        if (!$this->isenabled()) {
234            return;
235        }
236
237        /** @var DokuWiki_Auth_Plugin $auth */
238        global $auth;
239        global $conf;
240        /** @var \Input $INPUT */
241        global $INPUT;
242
243        $id = $data['id'];
244        $self = $data['self'];
245        $addresslist = $data['addresslist'];
246
247        $subscriptions = $this->subscribers($id, null, 'every');
248
249        $result = [];
250        foreach ($subscriptions as $users) {
251            foreach ($users as $user => $info) {
252                $userinfo = $auth->getUserData($user);
253                if ($userinfo === false) {
254                    continue;
255                }
256                if (!$userinfo['mail']) {
257                    continue;
258                }
259                if (!$self && $user == $INPUT->server->str('REMOTE_USER')) {
260                    continue;
261                } //skip our own changes
262
263                $level = auth_aclcheck($id, $user, $userinfo['grps']);
264                if ($level >= AUTH_READ) {
265                    if (strcasecmp($userinfo['mail'], $conf['notify']) != 0) { //skip user who get notified elsewhere
266                        $result[$user] = $userinfo['mail'];
267                    }
268                }
269            }
270        }
271        $data['addresslist'] = trim($addresslist . ',' . implode(',', $result), ',');
272    }
273
274    /**
275     * Return the subscription meta file for the given ID
276     *
277     * @author Adrian Lang <lang@cosmocode.de>
278     *
279     * @param string $id The target page or namespace, specified by id; Namespaces
280     *                   are identified by appending a colon.
281     *
282     * @return string
283     */
284    protected function file($id)
285    {
286        $meta_fname = '.mlist';
287        if ((substr($id, -1, 1) === ':')) {
288            $meta_froot = getNS($id);
289            $meta_fname = '/' . $meta_fname;
290        } else {
291            $meta_froot = $id;
292        }
293        return metaFN((string)$meta_froot, $meta_fname);
294    }
295}
296