1<?php
2
3use dokuwiki\Extension\Plugin;
4
5/**
6 * DokuWiki Plugin lms (Helper Component)
7 *
8 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
9 * @author  Andreas Gohr <dokuwiki@cosmocode.de>
10 */
11class helper_plugin_lms extends Plugin
12{
13    /**
14     * Return all lessons and info about the user's current completion status
15     *
16     * @param string|null $user Username, null for no user data
17     * @return array A list of lesson infos
18     */
19    public function getLessons($user = null)
20    {
21        $cp = $this->getControlPage();
22        if (!$cp) return [];
23
24        $lessons = array_fill_keys($this->parseControlPage($cp), 0);
25        if ($user !== null) {
26            $lessons = array_merge($lessons, $this->getUserLessons($user));
27        }
28
29        return $lessons;
30    }
31
32    /**
33     * Find the nearest controlpage
34     *
35     * @return false|string
36     */
37    public function getControlPage()
38    {
39        global $ID;
40        global $INFO;
41
42        $cp = $this->getConf('controlpage');
43        $oldid = $ID;
44        $ID = $INFO['id'];
45        $cp = page_findnearest($cp, false);
46        $ID = $oldid;
47        return $cp;
48    }
49
50    /**
51     * @param string $id Page ID of the lesson
52     * @param bool $seen Mark as seen or unseen
53     * @param string $user Username
54     * @return bool
55     */
56    public function markLesson($id, $user, $seen = true)
57    {
58        if ($user === null) return false;
59
60        $file = $this->getUserFile($user);
61        $line = time() . "\t" . $id . "\t" . ($seen ? 1 : 0) . "\n";
62        return io_saveFile($file, $line, true);
63    }
64
65    /**
66     * Get the list of completed lessons for a user
67     *
68     * This skips all lessons that used to be seen but have been marked unseen later
69     *
70     * @param string $user
71     * @return array
72     */
73    public function getUserLessons($user)
74    {
75        $file = $this->getUserFile($user);
76        if (!file_exists($file)) return [];
77
78        $lessons = [];
79        $lines = file($file);
80        foreach ($lines as $line) {
81            [$time, $id, $seen] = explode("\t", trim($line));
82
83            // we use simple log files
84            if ($seen) {
85                $lessons[$id] = $time;
86            } elseif (isset($lessons[$id])) {
87                // an already seen lesson might have been marked unseen later
88                unset($lessons[$id]);
89            }
90        }
91
92        return $lessons;
93    }
94
95    /**
96     * Get Seen-Info of a single lesson
97     *
98     * @param string $id Page ID of the lesson
99     * @return int|false Either the lesson info or fals if given ID is not a lesson
100     */
101    public function getLesson($id, $user)
102    {
103        $all = $this->getLessons($user);
104        return $all[$id] ?? false;
105    }
106
107    /**
108     * Get the next lesson relative to the given one
109     *
110     * @param string $id current lesson
111     * @param null|string $user When user is given, next unseen lesson is returned
112     * @return string
113     */
114    public function getNextLesson($id, $user = null)
115    {
116        $all = $this->getLessons($user);
117
118        if (!isset($all[$id])) return false; // current page is not a lesson
119
120        $keys = array_keys($all);
121        $self = array_search($id, $keys);
122        $len = count($keys);
123
124        for ($i = $self + 1; $i < $len; $i++) {
125            if ($user !== null && $all[$keys[$i]] !== 0) {
126                continue; // next element has already been seen by user
127            }
128            return $keys[$i];
129        }
130
131        // no more lessons
132        return false;
133    }
134
135    /**
136     * Get the previous lesson relative to the given one
137     *
138     * @param string $id current lesson
139     * @param null|string $user When user is given, previous unseen lesson is returned
140     * @return string
141     */
142    public function getPrevLesson($id, $user = null)
143    {
144        $all = $this->getLessons($user);
145
146        if (!isset($all[$id])) return false; // current page is not a lesson
147
148        $keys = array_keys($all);
149        $self = array_search($id, $keys);
150
151        for ($i = $self - 1; $i >= 0; $i--) {
152            if ($user !== null && $all[$keys[$i]] !== 0) {
153                continue; // next element has already been seen by user
154            }
155            return $keys[$i];
156        }
157
158        // no more lessons
159        return false;
160    }
161
162
163    /**
164     * Get the filename used for storing lesson completions
165     *
166     * @param string $user username
167     */
168    protected function getUserFile($user)
169    {
170        global $conf;
171
172        // we're not using cache files but our own meta directory
173        $user = utf8_encodeFN($user); // make sure the user is clean for directories
174        return $conf['metadir'] . '_lms/' . $user . '.lms';
175    }
176
177    public function getKnownUsers()
178    {
179        global $conf;
180
181        $lmsData = $conf['metadir'] . '_lms/';
182
183        $s = scandir($lmsData);
184
185        $users = array_map(function ($file) {
186            if (!in_array($file, ['.', '..'])) {
187                return str_replace('.lms', '', $file);
188            }
189        }, $s);
190
191        return array_filter($users);
192    }
193
194    /**
195     * Get a list of links from the given control page
196     *
197     * @param string $cp The control page
198     * @return array
199     */
200    protected function parseControlPage($cp)
201    {
202        $cpns = getNS($cp);
203        $exists = false; // ignored
204        $pages = [];
205
206        $instructions = p_cached_instructions(wikiFN($cp), false, $cp);
207        if ($instructions === null) return [];
208
209        foreach ($instructions as $instruction) {
210            if ($instruction[0] !== 'internallink') continue;
211            $link = $instruction[1][0];
212            resolve_pageid($cpns, $link, $exists);
213
214            // Only pages below the control page's namespace are considered lessons
215            $ns = getNS($link);
216            $check = $cpns ? ":$cpns" : '';
217            if (!preg_match("/^$check(:|$)/", ":$ns")) {
218                continue;
219            }
220
221            $pages[] = $link;
222        }
223
224        $pages = array_values(array_unique($pages));
225        return $pages;
226    }
227}
228