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