xref: /dokuwiki/inc/Subscriptions/BulkSubscriptionSender.php (revision 0a5c6ce45808677de55e4cfcaf91c02ab671041c)
1<?php
2
3namespace dokuwiki\Subscriptions;
4
5use dokuwiki\ChangeLog\PageChangeLog;
6use dokuwiki\Extension\AuthPlugin;
7use dokuwiki\Input\Input;
8use Exception;
9
10class BulkSubscriptionSender extends SubscriptionSender
11{
12    /**
13     * Send digest and list subscriptions
14     *
15     * This sends mails to all subscribers that have a subscription for namespaces above
16     * the given page if the needed $conf['subscribe_time'] has passed already.
17     *
18     * This function is called form lib/exe/indexer.php
19     *
20     * @param string $page
21     * @return int number of sent mails
22     * @throws Exception
23     */
24    public function sendBulk($page)
25    {
26        $subscriberManager = new SubscriberManager();
27        if (!$subscriberManager->isenabled()) {
28            return 0;
29        }
30
31        /** @var AuthPlugin $auth */
32        global $auth;
33        global $conf;
34        global $USERINFO;
35        /** @var Input $INPUT */
36        global $INPUT;
37        $count = 0;
38
39        $subscriptions = $subscriberManager->subscribers($page, null, ['digest', 'list']);
40
41        // remember current user info
42        $olduinfo = $USERINFO;
43        $olduser = $INPUT->server->str('REMOTE_USER');
44
45        foreach ($subscriptions as $target => $users) {
46            if (!$this->lock($target)) {
47                continue;
48            }
49
50            foreach ($users as $user => $info) {
51                [$style, $lastupdate] = $info;
52
53                $lastupdate = (int)$lastupdate;
54                if ($lastupdate + $conf['subscribe_time'] > time()) {
55                    // Less than the configured time period passed since last
56                    // update.
57                    continue;
58                }
59
60                // Work as the user to make sure ACLs apply correctly
61                $USERINFO = $auth->getUserData($user);
62                $INPUT->server->set('REMOTE_USER', $user);
63                if ($USERINFO === false) {
64                    continue;
65                }
66                if (!$USERINFO['mail']) {
67                    continue;
68                }
69
70                if (str_ends_with($target, ':')) {
71                    // subscription target is a namespace, get all changes within
72                    $changes = getRecentsSince($lastupdate, null, getNS($target));
73                } else {
74                    // single page subscription, check ACL ourselves
75                    if (auth_quickaclcheck($target) < AUTH_READ) {
76                        continue;
77                    }
78                    $meta = p_get_metadata($target);
79                    $changes = [$meta['last_change']];
80                }
81
82                // Filter out pages only changed in small and own edits
83                $change_ids = [];
84                foreach ($changes as $rev) {
85                    $n = 0;
86                    $pagelog = new PageChangeLog($rev['id']);
87                    while (
88                        !is_null($rev) && $rev['date'] >= $lastupdate &&
89                        ($INPUT->server->str('REMOTE_USER') === $rev['user'] ||
90                            $rev['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT)
91                    ) {
92                        $revisions = $pagelog->getRevisions($n++, 1);
93                        $rev = ($revisions !== []) ? $pagelog->getRevisionInfo($revisions[0]) : null;
94                    }
95
96                    if (!is_null($rev) && $rev['date'] >= $lastupdate) {
97                        // Some change was not a minor one and not by myself
98                        $change_ids[] = $rev['id'];
99                    }
100                }
101
102                // send it
103                if ($style === 'digest') {
104                    foreach ($change_ids as $change_id) {
105                        $this->sendDigest(
106                            $USERINFO['mail'],
107                            $change_id,
108                            $lastupdate
109                        );
110                        $count++;
111                    }
112                } elseif ($style === 'list') {
113                    $this->sendList($USERINFO['mail'], $change_ids, $target, $lastupdate);
114                    $count++;
115                }
116                // TODO: Handle duplicate subscriptions.
117
118                // Update notification time.
119                $subscriberManager->add($target, $user, $style, time());
120            }
121            $this->unlock($target);
122        }
123
124        // restore current user info
125        $USERINFO = $olduinfo;
126        $INPUT->server->set('REMOTE_USER', $olduser);
127        return $count;
128    }
129
130    /**
131     * Lock subscription info
132     *
133     * We don't use io_lock() her because we do not wait for the lock and use a larger stale time
134     *
135     * @param string $id The target page or namespace, specified by id; Namespaces
136     *                   are identified by appending a colon.
137     *
138     * @return bool true, if you got a succesful lock
139     * @author Adrian Lang <lang@cosmocode.de>
140     */
141    protected function lock($id)
142    {
143        global $conf;
144
145        $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock';
146
147        if (is_dir($lock) && time() - @filemtime($lock) > 60 * 5) {
148            // looks like a stale lock - remove it
149            @rmdir($lock);
150        }
151
152        // try creating the lock directory
153        if (!@mkdir($lock)) {
154            return false;
155        }
156
157        if ($conf['dperm']) {
158            chmod($lock, $conf['dperm']);
159        }
160        return true;
161    }
162
163    /**
164     * Unlock subscription info
165     *
166     * @param string $id The target page or namespace, specified by id; Namespaces
167     *                   are identified by appending a colon.
168     *
169     * @return bool
170     * @author Adrian Lang <lang@cosmocode.de>
171     */
172    protected function unlock($id)
173    {
174        global $conf;
175        $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock';
176        return @rmdir($lock);
177    }
178
179    /**
180     * Return the last revision before lastupdate.
181     *
182     * @param string $id         The target page or namespace, specified by id; Namespaces
183     *                           are identified by appending a colon.
184     * @param int    $lastupdate Time of the last notification
185     *
186     * @return int|null The revision timestamp, or null if no earlier revision exists
187     * @author Michael Stapelberg <stapelberg+dokuwiki@google.com>
188     */
189    protected function lastRevBefore($id, $lastupdate)
190    {
191        $pagelog = new PageChangeLog($id);
192        $n = 0;
193        do {
194            $rev = $pagelog->getRevisions($n++, 1);
195            $rev = ($rev !== []) ? $rev[0] : null;
196        } while (!is_null($rev) && $rev > $lastupdate);
197        return $rev;
198    }
199
200    /**
201     * Send a digest mail
202     *
203     * Sends a digest mail showing a bunch of changes of a single page. Basically the same as sendPageDiff()
204     * but determines the last known revision first
205     *
206     * @param string $subscriber_mail The target mail address
207     * @param string $id              The ID
208     * @param int    $lastupdate      Time of the last notification
209     *
210     * @return bool
211     * @author Adrian Lang <lang@cosmocode.de>
212     *
213     */
214    protected function sendDigest($subscriber_mail, $id, $lastupdate)
215    {
216        $rev = $this->lastRevBefore($id, $lastupdate);
217
218        // TODO I'm not happy with the following line and passing $this->mailer around. Not sure how to solve it better
219        $pageSubSender = new PageSubscriptionSender($this->mailer);
220        return $pageSubSender->sendPageDiff(
221            $subscriber_mail,
222            'subscr_digest',
223            $id,
224            $rev
225        );
226    }
227
228    /**
229     * Send a list mail
230     *
231     * Sends a list mail showing a list of changed pages.
232     *
233     * @param string $subscriber_mail The target mail address
234     * @param array  $ids             Array of ids
235     * @param string $ns_id           The id of the namespace
236     * @param int    $lastupdate      Time of the last notification
237     *
238     * @return bool true if a mail was sent
239     * @author Adrian Lang <lang@cosmocode.de>
240     *
241     */
242    protected function sendList($subscriber_mail, $ids, $ns_id, $lastupdate)
243    {
244        global $lang;
245
246        if ($ids === []) {
247            return false;
248        }
249
250        $tlist = '';
251        $hlist = '<ul>';
252        foreach ($ids as $id) {
253            $last = $this->lastRevBefore($id, $lastupdate);
254            $link = wl($id, [], true);
255            $difflink = $last ? wl($id, ['do' => 'diff', 'rev' => $last], true) : null;
256
257            $tlist .= '* ' . $link;
258            if ($difflink) {
259                $tlist .= ' (' . $lang['diff'] . ': ' . $difflink . ')';
260            }
261            $tlist .= NL;
262
263            $hlist .= '<li>';
264            $hlist .= '<a href="' . $link . '">' . hsc($id) . '</a>';
265            if ($difflink) {
266                $hlist .= ' (<a href="' . $difflink . '">' . $lang['diff'] . '</a>)';
267            }
268            $hlist .= '</li>' . NL;
269        }
270        $hlist .= '</ul>';
271
272        $id = prettyprint_id($ns_id);
273        $trep = [
274            'DIFF' => rtrim($tlist),
275            'PAGE' => $id,
276            'SUBSCRIBE' => wl($id, ['do' => 'subscribe'], true, '&'),
277        ];
278        $hrep = [
279            'DIFF' => $hlist,
280        ];
281
282        return $this->send(
283            $subscriber_mail,
284            'subscribe_list',
285            $ns_id,
286            'subscr_list',
287            $trep,
288            $hrep
289        );
290    }
291}
292