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