Total Complexity | 1002 |
Total Lines | 6348 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like WikiManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use WikiManager, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
17 | final class WikiManager |
||
18 | { |
||
19 | /** Legacy compat: set from index.php */ |
||
20 | private readonly CWikiRepository $wikiRepo; |
||
21 | public string $page = 'index'; |
||
22 | public string $action = 'showpage'; |
||
23 | public ?string $charset = null; |
||
24 | protected ?string $baseUrl = null; |
||
25 | public ?string $url = null; |
||
26 | |||
27 | /** Optional in-memory preload for view */ |
||
28 | private array $wikiData = []; |
||
29 | public ?string $tbl_wiki = null; |
||
30 | public ?string $tbl_wiki_mailcue = null; |
||
31 | |||
32 | public function __construct(?CWikiRepository $wikiRepo = null) |
||
33 | { |
||
34 | if ($wikiRepo instanceof CWikiRepository) { |
||
35 | $this->wikiRepo = $wikiRepo; |
||
36 | } else { |
||
37 | $em = \Database::getManager(); |
||
38 | /** @var CWikiRepository $repo */ |
||
39 | $repo = $em->getRepository(CWiki::class); |
||
40 | $this->wikiRepo = $repo; |
||
41 | } |
||
42 | $this->baseUrl = $this->computeBaseUrl(); |
||
43 | } |
||
44 | |||
45 | /** DBAL connection (plain SQL with Doctrine). */ |
||
46 | private function conn(): Connection |
||
47 | { |
||
48 | return Container::getEntityManager()->getConnection(); |
||
49 | } |
||
50 | |||
51 | /** Table names (shortcuts) */ |
||
52 | private function tblWikiMailcue(): string |
||
53 | { |
||
54 | return 'c_wiki_mailcue'; |
||
55 | } |
||
56 | private function tblWiki(): string |
||
57 | { |
||
58 | return 'c_wiki'; |
||
59 | } |
||
60 | |||
61 | /** |
||
62 | * Set the base URL to be used by the wiki for links. |
||
63 | * Keeps backward compatibility by also setting $this->url. |
||
64 | */ |
||
65 | public function setBaseUrl(string $url): void |
||
66 | { |
||
67 | $this->baseUrl = $url; |
||
68 | // compat: some sites use $this->url as base string |
||
69 | $this->url = $url; |
||
70 | } |
||
71 | |||
72 | /** |
||
73 | * Get the base URL. If not previously set, compute a safe default. |
||
74 | */ |
||
75 | public function getBaseUrl(): string |
||
76 | { |
||
77 | if (!empty($this->baseUrl)) { |
||
78 | return $this->baseUrl; |
||
79 | } |
||
80 | $computed = api_get_self().'?'.api_get_cidreq(); |
||
81 | $this->setBaseUrl($computed); |
||
82 | |||
83 | return $this->baseUrl; |
||
84 | } |
||
85 | |||
86 | /** |
||
87 | * Helper to build URLs with additional parameters based on the current one. |
||
88 | */ |
||
89 | public function buildUrl(array $params = []): string |
||
90 | { |
||
91 | $base = $this->getBaseUrl(); |
||
92 | return $params ? $base.'&'.http_build_query($params) : $base; |
||
93 | } |
||
94 | |||
95 | /** |
||
96 | * Detects at runtime which column links mailcue to the page: |
||
97 | * - 'publication_id' (some installations) |
||
98 | * - 'id' (legacy) |
||
99 | * - null if none exists (disables the feature) |
||
100 | */ |
||
101 | private function mailcueLinkColumn(): ?string |
||
102 | { |
||
103 | static $col = null; |
||
104 | if ($col !== null) { |
||
105 | return $col; |
||
106 | } |
||
107 | |||
108 | $sm = $this->conn()->createSchemaManager(); |
||
109 | $cols = array_map(fn($c) => $c->getName(), $sm->listTableColumns($this->tblWikiMailcue())); |
||
110 | $col = in_array('publication_id', $cols, true) ? 'publication_id' |
||
111 | : (in_array('id', $cols, true) ? 'id' : null); |
||
112 | |||
113 | return $col; |
||
114 | } |
||
115 | |||
116 | /** |
||
117 | * Returns the ID (c_wiki's `id` column) of the **first version** of the page |
||
118 | * in the current context. This is the anchor used by the discussion/notify. |
||
119 | */ |
||
120 | private function firstVersionIdByReflink(string $reflink): ?int |
||
121 | { |
||
122 | $ctx = self::ctx(); |
||
123 | $sql = 'SELECT MIN(id) AS id |
||
124 | FROM '.$this->tblWiki().' |
||
125 | WHERE c_id = :cid |
||
126 | AND reflink = :ref |
||
127 | AND COALESCE(group_id,0) = :gid |
||
128 | AND COALESCE(session_id,0) = :sid'; |
||
129 | |||
130 | $id = $this->conn()->fetchOne($sql, [ |
||
131 | 'cid' => (int)$ctx['courseId'], |
||
132 | 'ref' => html_entity_decode($reflink), |
||
133 | 'gid' => (int)$ctx['groupId'], |
||
134 | 'sid' => (int)$ctx['sessionId'], |
||
135 | ]); |
||
136 | |||
137 | return $id ? (int)$id : null; |
||
138 | } |
||
139 | |||
140 | /** |
||
141 | * Load wiki data (iid or reflink) into $this->wikiData for view compatibility. |
||
142 | * @param int|string|bool $wikiId iid of CWiki row or a reflink. Falsy => [] |
||
143 | */ |
||
144 | public function setWikiData($wikiId): void |
||
145 | { |
||
146 | $this->wikiData = self::getWikiDataFromDb($wikiId); |
||
147 | } |
||
148 | |||
149 | /** Query DB and return a flat array with latest-version fields in context. */ |
||
150 | private static function getWikiDataFromDb($wikiId): array |
||
151 | { |
||
152 | $ctx = self::ctx(); |
||
153 | $em = Container::getEntityManager(); |
||
154 | $repo = self::repo(); |
||
155 | |||
156 | $last = null; |
||
157 | $pageId = 0; |
||
158 | |||
159 | if (is_numeric($wikiId)) { |
||
160 | /** @var CWiki|null $row */ |
||
161 | $row = $em->find(CWiki::class, (int)$wikiId); |
||
162 | if (!$row) { return []; } |
||
163 | $pageId = (int)($row->getPageId() ?: $row->getIid()); |
||
164 | } elseif (is_string($wikiId) && $wikiId !== '') { |
||
165 | /** @var CWiki|null $first */ |
||
166 | $first = $repo->createQueryBuilder('w') |
||
167 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
168 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($wikiId)) |
||
169 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
170 | ->orderBy('w.version', 'ASC') |
||
171 | ->setMaxResults(1) |
||
172 | ->getQuery()->getOneOrNullResult(); |
||
173 | if (!$first) { return []; } |
||
174 | $pageId = (int)$first->getPageId(); |
||
175 | } else { |
||
176 | return []; |
||
177 | } |
||
178 | |||
179 | if ($pageId > 0) { |
||
180 | $qb = $repo->createQueryBuilder('w') |
||
181 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
182 | ->andWhere('w.pageId = :pid')->setParameter('pid', $pageId) |
||
183 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
184 | ->orderBy('w.version', 'DESC') |
||
185 | ->setMaxResults(1); |
||
186 | |||
187 | if ($ctx['sessionId'] > 0) { |
||
188 | $qb->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
189 | } else { |
||
190 | $qb->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
191 | } |
||
192 | |||
193 | /** @var CWiki|null $last */ |
||
194 | $last = $qb->getQuery()->getOneOrNullResult(); |
||
195 | } |
||
196 | |||
197 | if (!$last) { |
||
198 | return []; |
||
199 | } |
||
200 | |||
201 | return [ |
||
202 | 'iid' => (int)$last->getIid(), |
||
203 | 'page_id' => (int)$last->getPageId(), |
||
204 | 'title' => (string)$last->getTitle(), |
||
205 | 'reflink' => (string)$last->getReflink(), |
||
206 | 'content' => (string)$last->getContent(), |
||
207 | 'user_id' => (int)$last->getUserId(), |
||
208 | 'dtime' => $last->getDtime(), |
||
209 | 'version' => (int)$last->getVersion(), |
||
210 | 'visibility' => (int)$last->getVisibility(), |
||
211 | 'visibility_disc' => (int)$last->getVisibilityDisc(), |
||
212 | 'assignment' => (int)$last->getAssignment(), |
||
213 | 'progress' => (string)$last->getProgress(), |
||
214 | 'score' => (int)$last->getScore(), |
||
215 | ]; |
||
216 | } |
||
217 | |||
218 | /** Build request context (course/session/group + URLs). */ |
||
219 | private static function ctx(?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): array |
||
220 | { |
||
221 | $courseId = $courseId ?? api_get_course_int_id(); |
||
222 | $sessionId = $sessionId ?? api_get_session_id(); |
||
223 | $groupId = $groupId ?? api_get_group_id(); |
||
224 | |||
225 | return [ |
||
226 | 'courseId' => $courseId, |
||
227 | 'course' => api_get_course_entity($courseId), |
||
228 | 'courseInfo' => api_get_course_info($courseId), |
||
229 | 'courseCode' => api_get_course_id(), |
||
230 | |||
231 | 'sessionId' => $sessionId, |
||
232 | 'session' => api_get_session_entity($sessionId), |
||
233 | |||
234 | 'groupId' => $groupId, |
||
235 | |||
236 | 'baseUrl' => api_get_path(WEB_CODE_PATH).'wiki/index.php?'.api_get_cidreq(), |
||
237 | ]; |
||
238 | } |
||
239 | |||
240 | /** @return CWikiRepository */ |
||
241 | private static function repo(): CWikiRepository |
||
242 | { |
||
243 | return Container::getEntityManager()->getRepository(CWiki::class); |
||
244 | } |
||
245 | |||
246 | /** @return ObjectRepository */ |
||
247 | private static function confRepo(): ObjectRepository |
||
248 | { |
||
249 | return Container::getEntityManager()->getRepository(CWikiConf::class); |
||
250 | } |
||
251 | |||
252 | /** Feature switch for categories. */ |
||
253 | private static function categoriesEnabled(): bool |
||
254 | { |
||
255 | return api_get_configuration_value('wiki_categories_enabled') === true |
||
256 | || api_get_setting('wiki.wiki_categories_enabled') === 'true'; |
||
257 | } |
||
258 | |||
259 | /** True if a reflink is available in current context (course/session/group). */ |
||
260 | public static function checktitle( |
||
261 | string $title, |
||
262 | ?int $courseId = null, |
||
263 | ?int $sessionId = null, |
||
264 | ?int $groupId = null |
||
265 | ): bool { |
||
266 | // Use same criterion as the whole module |
||
267 | return self::existsByReflink($title, $courseId, $sessionId, $groupId); |
||
268 | } |
||
269 | |||
270 | public function editPage(): void |
||
271 | { |
||
272 | $ctx = self::ctx(); |
||
273 | $em = Container::getEntityManager(); |
||
274 | $repo = self::repo(); |
||
275 | $userId = (int) api_get_user_id(); |
||
276 | |||
277 | // Sessions: only users allowed to edit inside the session |
||
278 | if ($ctx['sessionId'] !== 0 && api_is_allowed_to_session_edit(false, true) === false) { |
||
279 | api_not_allowed(); |
||
280 | return; |
||
281 | } |
||
282 | |||
283 | $page = self::normalizeReflink($this->page); |
||
284 | $row = []; |
||
285 | $canEdit = false; |
||
286 | $iconAssignment = ''; |
||
287 | $conf = null; |
||
288 | |||
289 | self::dbg('enter editPage title='.$this->page.' normalized='.$page); |
||
290 | self::dbg('ctx cid='.$ctx['courseId'].' gid='.$ctx['groupId'].' sid='.$ctx['sessionId'].' user='.$userId); |
||
291 | |||
292 | // Historic rule: outside groups, home (index) is editable only by teacher/admin |
||
293 | if (self::isMain($page) && (int)$ctx['groupId'] === 0 |
||
294 | && !api_is_allowed_to_edit(false, true) && !api_is_platform_admin() |
||
295 | ) { |
||
296 | Display::addFlash(Display::return_message('Only course managers can edit the home page (index) outside groups.', 'error')); |
||
297 | self::dbg('block: home edit not allowed (student)'); |
||
298 | return; |
||
299 | } |
||
300 | |||
301 | // ---- FIRST (oldest row for this reflink in context) ---- |
||
302 | $qbFirst = $repo->createQueryBuilder('w') |
||
303 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
304 | ->andWhere('w.reflink = :r')->setParameter('r', $page) |
||
305 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
306 | ->orderBy('w.version', 'ASC') |
||
307 | ->setMaxResults(1); |
||
308 | |||
309 | if ($ctx['sessionId'] > 0) { |
||
310 | $qbFirst->andWhere('(COALESCE(w.sessionId,0) = 0 OR w.sessionId = :sid)') |
||
311 | ->setParameter('sid', (int)$ctx['sessionId']); |
||
312 | } else { |
||
313 | $qbFirst->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
314 | } |
||
315 | |||
316 | /** @var CWiki|null $first */ |
||
317 | $first = $qbFirst->getQuery()->getOneOrNullResult(); |
||
318 | self::dbg('$first '.($first ? 'HIT pid='.$first->getPageId() : 'MISS')); |
||
319 | |||
320 | // ---- LAST (latest version in same context) ---- |
||
321 | $last = null; |
||
322 | if ($first && $first->getPageId()) { |
||
323 | $qbLast = $repo->createQueryBuilder('w') |
||
324 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
325 | ->andWhere('w.pageId = :pid')->setParameter('pid', (int)$first->getPageId()) |
||
326 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
327 | ->orderBy('w.version', 'DESC') |
||
328 | ->setMaxResults(1); |
||
329 | |||
330 | if ($ctx['sessionId'] > 0) { |
||
331 | $qbLast->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
332 | } else { |
||
333 | $qbLast->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
334 | } |
||
335 | |||
336 | /** @var CWiki|null $last */ |
||
337 | $last = $qbLast->getQuery()->getOneOrNullResult(); |
||
338 | } |
||
339 | self::dbg('$last '.($last ? 'HIT iid='.$last->getIid().' ver='.$last->getVersion() : 'MISS')); |
||
340 | |||
341 | // ---- Defaults (when page does not exist yet) ---- |
||
342 | $content = '<div class="wiki-placeholder">'.sprintf(get_lang('DefaultContent'), api_get_path(WEB_IMG_PATH)).'</div>'; |
||
343 | $title = self::displayTitleFor($page, null); |
||
344 | $pageId = 0; |
||
345 | |||
346 | // ---- Base permissions ---- |
||
347 | if (!empty($ctx['groupId'])) { |
||
348 | $groupInfo = GroupManager::get_group_properties((int)$ctx['groupId']); |
||
349 | $canEdit = api_is_allowed_to_edit(false, true) |
||
350 | || api_is_platform_admin() |
||
351 | || GroupManager::is_user_in_group($userId, $groupInfo); |
||
352 | if (!$canEdit) { |
||
353 | Display::addFlash(Display::return_message('Only group members can edit this page.', 'warning')); |
||
354 | self::dbg('block: not group member'); |
||
355 | return; |
||
356 | } |
||
357 | } else { |
||
358 | // Outside groups: if not home, let users reach editor; hard locks/config will gate below. |
||
359 | $canEdit = true; |
||
360 | } |
||
361 | |||
362 | if ($last) { |
||
363 | if ($last->getContent() === '' && $last->getTitle() === '' && $page === '') { |
||
364 | Display::addFlash(Display::return_message('You must select a page.', 'error')); |
||
365 | self::dbg('block: empty page selection'); |
||
366 | return; |
||
367 | } |
||
368 | |||
369 | $content = api_html_entity_decode($last->getContent()); |
||
370 | $title = api_html_entity_decode($last->getTitle()); |
||
371 | $pageId = (int)$last->getPageId(); |
||
372 | |||
373 | // Assignment rules |
||
374 | if ((int)$last->getAssignment() === 1) { |
||
375 | Display::addFlash(Display::return_message('This is an assignment page. Be careful when editing.')); |
||
376 | $iconAssignment = Display::getMdiIcon(ActionIcon::WIKI_ASSIGNMENT, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, 'Assignment'); |
||
377 | } elseif ((int)$last->getAssignment() === 2) { |
||
378 | $iconAssignment = Display::getMdiIcon(ActionIcon::WIKI_WORK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, 'Work'); |
||
379 | if ($userId !== (int)$last->getUserId() |
||
380 | && !api_is_allowed_to_edit(false, true) && !api_is_platform_admin() |
||
381 | ) { |
||
382 | Display::addFlash(Display::return_message('This page is locked by the teacher for other users.', 'warning')); |
||
383 | self::dbg('block: assignment=2 and not owner'); |
||
384 | return; |
||
385 | } |
||
386 | } |
||
387 | |||
388 | // Hard lock by teacher |
||
389 | if ((int)$last->getEditlock() === 1 |
||
390 | && !api_is_allowed_to_edit(false, true) && !api_is_platform_admin() |
||
391 | ) { |
||
392 | Display::addFlash(Display::return_message('This page is locked by the teacher.', 'warning')); |
||
393 | self::dbg('block: teacher hard lock'); |
||
394 | return; |
||
395 | } |
||
396 | |||
397 | // Conf row (limits/dates) |
||
398 | $conf = self::confRepo()->findOneBy(['cId' => $ctx['courseId'], 'pageId' => (int)$last->getPageId()]); |
||
399 | } |
||
400 | |||
401 | // ---- Config constraints (no redirects; just show and stop) ---- |
||
402 | if ($conf) { |
||
403 | if ($conf->getStartdateAssig() && time() < api_strtotime($conf->getStartdateAssig())) { |
||
404 | $msg = 'The task does not begin until: '.api_get_local_time($conf->getStartdateAssig()); |
||
405 | Display::addFlash(Display::return_message($msg, 'warning')); |
||
406 | self::dbg('block: before start date'); |
||
407 | return; |
||
408 | } |
||
409 | |||
410 | if ($conf->getEnddateAssig() && time() > strtotime($conf->getEnddateAssig()) && (int)$conf->getDelayedsubmit() === 0) { |
||
411 | $msg = 'The deadline has passed: '.api_get_local_time($conf->getEnddateAssig()); |
||
412 | Display::addFlash(Display::return_message($msg, 'warning')); |
||
413 | self::dbg('block: after end date (no delayed submit)'); |
||
414 | return; |
||
415 | } |
||
416 | |||
417 | if ((int)$conf->getMaxVersion() > 0 && $last && (int)$last->getVersion() >= (int)$conf->getMaxVersion()) { |
||
418 | Display::addFlash(Display::return_message('You have reached the maximum number of versions.', 'warning')); |
||
419 | self::dbg('block: max versions reached'); |
||
420 | return; |
||
421 | } |
||
422 | |||
423 | if ((int)$conf->getMaxText() > 0 && $last && $conf->getMaxText() <= self::word_count($last->getContent())) { |
||
424 | Display::addFlash(Display::return_message('You have reached the maximum number of words.', 'warning')); |
||
425 | self::dbg('block: max words reached'); |
||
426 | return; |
||
427 | } |
||
428 | |||
429 | // Informative task block (non-blocking) |
||
430 | if ($conf->getTask()) { |
||
431 | $msgTask = '<b>'.get_lang('DescriptionOfTheTask').'</b><p>'.$conf->getTask().'</p><hr>'; |
||
432 | $msgTask .= '<p>'.get_lang('StartDate').': '.($conf->getStartdateAssig() ? api_get_local_time($conf->getStartdateAssig()) : get_lang('No')).'</p>'; |
||
433 | $msgTask .= '<p>'.get_lang('EndDate').': '.($conf->getEnddateAssig() ? api_get_local_time($conf->getEnddateAssig()) : get_lang('No')); |
||
434 | $msgTask .= ' ('.get_lang('AllowLaterSends').') '.(((int)$conf->getDelayedsubmit() === 0) ? get_lang('No') : get_lang('Yes')).'</p>'; |
||
435 | $msgTask .= '<p>'.get_lang('OtherSettings').': '.get_lang('NMaxVersion').': '.((int)$conf->getMaxVersion() ?: get_lang('No')); |
||
436 | $msgTask .= ' '.get_lang('NMaxWords').': '.((int)$conf->getMaxText() ?: get_lang('No')).'</p>'; |
||
437 | Display::addFlash(Display::return_message($msgTask)); |
||
438 | } |
||
439 | } |
||
440 | |||
441 | // ---- Concurrency / editing lock (quiet admin override; show only on expiry) ---- |
||
442 | if ($last) { |
||
443 | $lockBy = (int) $last->getIsEditing(); |
||
444 | $timeoutSec = 1200; // 20 minutes |
||
445 | $ts = $last->getTimeEdit() ? self::toTimestamp($last->getTimeEdit()) : 0; |
||
446 | $elapsed = time() - $ts; |
||
447 | $expired = ($ts === 0) || ($elapsed >= $timeoutSec); |
||
448 | $canOverride = api_is_allowed_to_edit(false, true) || api_is_platform_admin(); |
||
449 | |||
450 | self::dbg('lock check: lockBy='.$lockBy.' ts=' . ($ts ? date('c',$ts) : 'NULL') . |
||
451 | ' elapsed='.$elapsed.' expired=' . ($expired?'1':'0') . |
||
452 | ' canOverride=' . ($canOverride?'1':'0')); |
||
453 | |||
454 | if ($lockBy !== 0 && $lockBy !== $userId) { |
||
455 | if ($expired || $canOverride) { |
||
456 | // Take over the lock |
||
457 | $last->setIsEditing($userId); |
||
458 | $last->setTimeEdit(new \DateTime('now', new \DateTimeZone('UTC'))); |
||
459 | $em->flush(); |
||
460 | |||
461 | // Only notify if the previous lock actually expired; silent on teacher/admin override |
||
462 | if ($expired) { |
||
463 | Display::addFlash( |
||
464 | Display::return_message('The previous editing lock expired. You now have the lock.', 'normal', false) |
||
465 | ); |
||
466 | } |
||
467 | |||
468 | self::dbg('lock takeover by user='.$userId.' (expired=' . ($expired?'1':'0') . ')'); |
||
469 | } else { |
||
470 | // Active lock and cannot override → inform and stop (no redirect) |
||
471 | $rest = max(0, $timeoutSec - $elapsed); |
||
472 | $info = api_get_user_info($lockBy); |
||
473 | if ($info) { |
||
474 | $msg = get_lang('ThisPageisBeginEditedBy').PHP_EOL |
||
475 | .UserManager::getUserProfileLink($info).PHP_EOL |
||
476 | .get_lang('ThisPageisBeginEditedTryLater').PHP_EOL |
||
477 | .date('i', $rest).PHP_EOL |
||
478 | .get_lang('MinMinutes'); |
||
479 | Display::addFlash(Display::return_message($msg, 'normal', false)); |
||
480 | } else { |
||
481 | Display::addFlash(Display::return_message('This page is currently being edited by another user.', 'normal', false)); |
||
482 | } |
||
483 | self::dbg('stop: lock active and not override-able'); |
||
484 | return; |
||
485 | } |
||
486 | } |
||
487 | |||
488 | // If no lock, set it now (best-effort) |
||
489 | if ($lockBy === 0) { |
||
490 | Display::addFlash(Display::return_message(get_lang('WarningMaxEditingTime'))); |
||
491 | $last->setIsEditing($userId); |
||
492 | $last->setTimeEdit(new \DateTime('now', new \DateTimeZone('UTC'))); |
||
493 | $em->flush(); |
||
494 | self::dbg('lock set by user='.$userId); |
||
495 | } |
||
496 | } |
||
497 | |||
498 | // ------- FORM ------- |
||
499 | $url = $ctx['baseUrl'].'&'.http_build_query(['action' => 'edit', 'title' => $page]); |
||
500 | $form = new FormValidator('wiki', 'post', $url); |
||
501 | $form->addElement('header', $iconAssignment.str_repeat(' ', 3).api_htmlentities($title)); |
||
502 | |||
503 | // Default values |
||
504 | $row = [ |
||
505 | 'id' => (int)($last?->getIid() ?? 0), |
||
506 | 'page_id' => (int)($last?->getPageId() ?? $pageId), |
||
507 | 'reflink' => $page, |
||
508 | 'title' => $title, |
||
509 | 'content' => $content, |
||
510 | 'version' => (int)($last?->getVersion() ?? 0), |
||
511 | 'progress' => (string)($last?->getProgress() ?? ''), |
||
512 | 'comment' => '', |
||
513 | 'assignment' => (int)($last?->getAssignment() ?? 0), |
||
514 | ]; |
||
515 | |||
516 | // Preselect categories |
||
517 | if ($last && true === api_get_configuration_value('wiki_categories_enabled')) { |
||
518 | /** @var CWiki $wikiRow */ |
||
519 | $wikiRow = $em->find(CWiki::class, (int)$last->getIid()); |
||
520 | foreach ($wikiRow->getCategories() as $category) { |
||
521 | $row['category'][] = $category->getId(); |
||
522 | } |
||
523 | } |
||
524 | |||
525 | // Version guard in session |
||
526 | Session::write('_version', (int)($row['version'] ?? 0)); |
||
527 | |||
528 | self::dbg('rendering edit form for page='.$page); |
||
529 | self::setForm($form, $row); |
||
530 | $form->addElement('hidden', 'title'); |
||
531 | $form->addButtonSave(get_lang('Save'), 'SaveWikiChange'); |
||
532 | |||
533 | $form->setDefaults($row); |
||
534 | $form->display(); |
||
535 | |||
536 | // -------- SAVE ---------- |
||
537 | if ($form->validate()) { |
||
538 | $values = $form->exportValues(); |
||
539 | |||
540 | if (empty($values['title'])) { |
||
541 | Display::addFlash(Display::return_message(get_lang('NoWikiPageTitle'), 'error')); |
||
542 | } elseif (!self::double_post($values['wpost_id'])) { |
||
543 | // ignore duplicate post |
||
544 | } elseif (!empty($values['version']) |
||
545 | && (int)Session::read('_version') !== 0 |
||
546 | && (int)$values['version'] !== (int)Session::read('_version') |
||
547 | ) { |
||
548 | Display::addFlash(Display::return_message(get_lang('EditedByAnotherUser'), 'error')); |
||
549 | } else { |
||
550 | $returnMessage = self::saveWiki($values); |
||
551 | Display::addFlash(Display::return_message($returnMessage, 'confirmation')); |
||
552 | |||
553 | // Best-effort: clear lock after save |
||
554 | if ($last) { |
||
555 | $last->setIsEditing(0); |
||
556 | $last->setTimeEdit(null); |
||
557 | $em->flush(); |
||
558 | } |
||
559 | } |
||
560 | |||
561 | $wikiData = $this->getWikiData(); |
||
562 | $redirectUrl = $ctx['baseUrl'].'&action=showpage&title='.urlencode(self::normalizeReflink($wikiData['reflink'] ?? $page)); |
||
563 | header('Location: '.$redirectUrl); |
||
564 | exit; |
||
565 | } |
||
566 | } |
||
567 | |||
568 | /** Public getter for the “view preload”. */ |
||
569 | public function getWikiData(): array |
||
570 | { |
||
571 | return $this->wikiData ?? []; |
||
572 | } |
||
573 | |||
574 | /** Very simple anti double-post using session. */ |
||
575 | public static function double_post($wpost_id): bool |
||
576 | { |
||
577 | $key = '_wiki_wpost_seen'; |
||
578 | $seen = (array) (Session::read($key) ?? []); |
||
579 | if (in_array($wpost_id, $seen, true)) { |
||
580 | return false; |
||
581 | } |
||
582 | $seen[] = $wpost_id; |
||
583 | Session::write($key, $seen); |
||
584 | return true; |
||
585 | } |
||
586 | |||
587 | /** Redirect helper to the main page. */ |
||
588 | private function redirectHome(): void |
||
589 | { |
||
590 | $ctx = self::ctx(); |
||
591 | $target = $ctx['baseUrl'].'&action=showpage&title='.urlencode($this->page ?: 'index'); |
||
592 | header('Location: '.$target); |
||
593 | exit; |
||
594 | } |
||
595 | |||
596 | public static function setForm(FormValidator $form, array $row = []): void |
||
597 | { |
||
598 | // Toolbar by permissions |
||
599 | $toolBar = api_is_allowed_to_edit(null, true) |
||
600 | ? ['ToolbarSet' => 'Wiki', 'Width' => '100%', 'Height' => '400'] |
||
601 | : ['ToolbarSet' => 'WikiStudent', 'Width' => '100%', 'Height' => '400', 'UserStatus' => 'student']; |
||
602 | |||
603 | // Content + comment |
||
604 | $form->addHtmlEditor('content', get_lang('Content'), false, false, $toolBar); |
||
605 | $form->addElement('text', 'comment', get_lang('Comments')); |
||
606 | |||
607 | // Progress select (values 0..100 step 10) |
||
608 | $progressValues = ['' => '']; |
||
609 | for ($i = 10; $i <= 100; $i += 10) { $progressValues[(string)$i] = (string)$i; } |
||
610 | // 5th parameter: attributes as array |
||
611 | $form->addElement('select', 'progress', get_lang('Progress'), $progressValues, []); |
||
612 | |||
613 | // Categories |
||
614 | $catsEnabled = api_get_configuration_value('wiki_categories_enabled') === true |
||
615 | || api_get_setting('wiki.wiki_categories_enabled') === 'true'; |
||
616 | |||
617 | if ($catsEnabled) { |
||
618 | $em = Container::getEntityManager(); |
||
619 | $categories = $em->getRepository(CWikiCategory::class)->findByCourse(api_get_course_entity()); |
||
620 | |||
621 | $form->addSelectFromCollection( |
||
622 | 'category', |
||
623 | get_lang('Categories'), |
||
624 | $categories, |
||
625 | ['multiple' => 'multiple'], |
||
626 | false, |
||
627 | 'getNodeName' |
||
628 | ); |
||
629 | } |
||
630 | |||
631 | // Advanced params (only for teachers/admin and not on index) |
||
632 | if ((api_is_allowed_to_edit(false, true) || api_is_platform_admin()) |
||
633 | && isset($row['reflink']) && $row['reflink'] !== 'index' |
||
634 | ) { |
||
635 | $form->addElement('advanced_settings', 'advanced_params', get_lang('AdvancedParameters')); |
||
636 | $form->addElement('html', '<div id="advanced_params_options" style="display:none">'); |
||
637 | |||
638 | // Task description |
||
639 | $form->addHtmlEditor( |
||
640 | 'task', |
||
641 | get_lang('DescriptionOfTheTask'), |
||
642 | false, |
||
643 | false, |
||
644 | ['ToolbarSet' => 'wiki_task', 'Width' => '100%', 'Height' => '200'] |
||
645 | ); |
||
646 | |||
647 | // Feedbacks + progress goals |
||
648 | $form->addElement('label', null, get_lang('AddFeedback')); |
||
649 | |||
650 | $form->addElement('textarea', 'feedback1', get_lang('Feedback1')); |
||
651 | $form->addElement('select', 'fprogress1', get_lang('FProgress'), $progressValues, []); |
||
652 | |||
653 | $form->addElement('textarea', 'feedback2', get_lang('Feedback2')); |
||
654 | $form->addElement('select', 'fprogress2', get_lang('FProgress'), $progressValues, []); |
||
655 | |||
656 | $form->addElement('textarea', 'feedback3', get_lang('Feedback3')); |
||
657 | $form->addElement('select', 'fprogress3', get_lang('FProgress'), $progressValues, []); |
||
658 | |||
659 | // Dates (toggles) |
||
660 | $form->addElement('checkbox', 'initstartdate', null, get_lang('StartDate'), ['id' => 'start_date_toggle']); |
||
661 | $row['initstartdate'] = empty($row['startdate_assig']) ? null : 1; |
||
662 | $style = empty($row['startdate_assig']) ? 'display:none' : 'display:block'; |
||
663 | $form->addElement('html', '<div id="start_date" style="'.$style.'">'); |
||
664 | $form->addDatePicker('startdate_assig', ''); |
||
665 | $form->addElement('html', '</div>'); |
||
666 | |||
667 | $form->addElement('checkbox', 'initenddate', null, get_lang('EndDate'), ['id' => 'end_date_toggle']); |
||
668 | $row['initenddate'] = empty($row['enddate_assig']) ? null : 1; |
||
669 | $style = empty($row['enddate_assig']) ? 'display:none' : 'display:block'; |
||
670 | $form->addElement('html', '<div id="end_date" style="'.$style.'">'); |
||
671 | $form->addDatePicker('enddate_assig', ''); |
||
672 | $form->addElement('html', '</div>'); |
||
673 | |||
674 | // Limits & flags |
||
675 | $form->addElement('checkbox', 'delayedsubmit', null, get_lang('AllowLaterSends')); |
||
676 | $form->addElement('text', 'max_text', get_lang('NMaxWords')); |
||
677 | $form->addElement('text', 'max_version', get_lang('NMaxVersion')); |
||
678 | $form->addElement('checkbox', 'assignment', null, get_lang('CreateAssignmentPage')); |
||
679 | |||
680 | $form->addElement('html', '</div>'); |
||
681 | } |
||
682 | |||
683 | // Hidden fields |
||
684 | $form->addElement('hidden', 'page_id'); |
||
685 | $form->addElement('hidden', 'reflink'); |
||
686 | $form->addElement('hidden', 'version'); |
||
687 | $form->addElement('hidden', 'wpost_id', api_get_unique_id()); |
||
688 | } |
||
689 | |||
690 | |||
691 | /** Return all rows being edited (is_editing != 0) respecting session condition. */ |
||
692 | public static function getAllWiki(?int $courseId = null, ?int $sessionId = null): array |
||
693 | { |
||
694 | $ctx = self::ctx($courseId, $sessionId, null); |
||
695 | $repo = self::repo(); |
||
696 | |||
697 | $qb = $repo->createQueryBuilder('w') |
||
698 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
699 | ->andWhere('COALESCE(w.isEditing, 0) <> 0') |
||
700 | ->orderBy('w.timeEdit', 'DESC'); |
||
701 | |||
702 | if ($ctx['sessionId'] > 0) { |
||
703 | $qb->andWhere('(COALESCE(w.sessionId,0) = 0 OR w.sessionId = :sid)') |
||
704 | ->setParameter('sid', $ctx['sessionId']); |
||
705 | } else { |
||
706 | $qb->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
707 | } |
||
708 | |||
709 | return $qb->getQuery()->getArrayResult(); |
||
710 | } |
||
711 | |||
712 | /** If "view" is an old version, show a notice against latest. */ |
||
713 | public function checkLastVersion($viewId): void |
||
714 | { |
||
715 | if (empty($viewId)) { |
||
716 | return; |
||
717 | } |
||
718 | $ctx = self::ctx(); |
||
719 | $em = Container::getEntityManager(); |
||
720 | |||
721 | /** @var CWiki|null $row */ |
||
722 | $row = $em->getRepository(CWiki::class)->find((int)$viewId); |
||
723 | if (!$row) { |
||
724 | return; |
||
725 | } |
||
726 | |||
727 | $qb = $em->getRepository(CWiki::class)->createQueryBuilder('w') |
||
728 | ->select('w.iid') |
||
729 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
730 | ->andWhere('w.pageId = :pid')->setParameter('pid', (int)$row->getPageId()) |
||
731 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
732 | ->orderBy('w.version', 'DESC') |
||
733 | ->setMaxResults(1); |
||
734 | |||
735 | if ($ctx['sessionId'] > 0) { |
||
736 | $qb->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
737 | } else { |
||
738 | $qb->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
739 | } |
||
740 | |||
741 | $latest = $qb->getQuery()->getOneOrNullResult(); |
||
742 | if ($latest && (int)($latest['iid'] ?? 0) !== (int)$viewId) { |
||
743 | Display::addFlash( |
||
744 | Display::return_message(get_lang('You are not viewing the most recent version'), 'warning', false) |
||
745 | ); |
||
746 | } |
||
747 | } |
||
748 | |||
749 | /** Top action bar (classic look). */ |
||
750 | public function showActionBar(): void |
||
751 | { |
||
752 | $ctx = self::ctx(); |
||
753 | $page = (string) $this->page; |
||
754 | $left = ''; |
||
755 | |||
756 | $left .= Display::url( |
||
757 | Display::getMdiIcon(ActionIcon::HOME, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Home')), |
||
758 | $ctx['baseUrl'].'&'.http_build_query(['action' => 'showpage', 'title' => 'index']) |
||
759 | ); |
||
760 | |||
761 | if (api_is_allowed_to_session_edit(false, true) && api_is_allowed_to_edit()) { |
||
762 | $left .= Display::url( |
||
763 | Display::getMdiIcon(ActionIcon::ADD, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('AddNew')), |
||
764 | $ctx['baseUrl'].'&action=addnew' |
||
765 | ); |
||
766 | } |
||
767 | |||
768 | if (self::categoriesEnabled() && (api_is_allowed_to_edit(false, true) || api_is_platform_admin())) { |
||
769 | $left .= Display::url( |
||
770 | Display::getMdiIcon(ActionIcon::CREATE_CATEGORY, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Categories')), |
||
771 | $ctx['baseUrl'].'&action=category' |
||
772 | ); |
||
773 | |||
774 | $addNewStatus = (int) self::check_addnewpagelock(); |
||
775 | if ($addNewStatus === 0) { |
||
776 | $left .= Display::url( |
||
777 | Display::getMdiIcon(ActionIcon::LOCK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('AddOptionProtected')), |
||
778 | $ctx['baseUrl'].'&'.http_build_query([ |
||
779 | 'action' => 'showpage', |
||
780 | 'title' => api_htmlentities('index'), |
||
781 | 'actionpage' => 'unlockaddnew', |
||
782 | ]) |
||
783 | ); |
||
784 | } else { |
||
785 | $left .= Display::url( |
||
786 | Display::getMdiIcon(ActionIcon::UNLOCK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('AddOptionUnprotected')), |
||
787 | $ctx['baseUrl'].'&'.http_build_query([ |
||
788 | 'action' => 'showpage', |
||
789 | 'title' => api_htmlentities('index'), |
||
790 | 'actionpage' => 'lockaddnew', |
||
791 | ]) |
||
792 | ); |
||
793 | } |
||
794 | } |
||
795 | |||
796 | $left .= Display::url( |
||
797 | Display::getMdiIcon(ActionIcon::SEARCH, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Search')), |
||
798 | $ctx['baseUrl'].'&action=searchpages' |
||
799 | ); |
||
800 | |||
801 | $left .= Display::url( |
||
802 | Display::getMdiIcon(ActionIcon::INFORMATION, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Statistics')), |
||
803 | $ctx['baseUrl'].'&'.http_build_query(['action' => 'more', 'title' => api_htmlentities(urlencode($page))]) |
||
804 | ); |
||
805 | |||
806 | $left .= Display::url( |
||
807 | Display::getMdiIcon(ActionIcon::LIST, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('All pages')), |
||
808 | $ctx['baseUrl'].'&action=allpages' |
||
809 | ); |
||
810 | |||
811 | $left .= Display::url( |
||
812 | Display::getMdiIcon(ActionIcon::HISTORY, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Recent changes')), |
||
813 | $ctx['baseUrl'].'&action=recentchanges' |
||
814 | ); |
||
815 | |||
816 | $frm = new FormValidator('wiki_search', 'get', $ctx['baseUrl'], '', [], FormValidator::LAYOUT_INLINE); |
||
817 | $frm->addText('search_term', get_lang('SearchTerm'), false); |
||
818 | $frm->addHidden('cid', $ctx['courseId']); |
||
819 | $frm->addHidden('sid', $ctx['sessionId']); |
||
820 | $frm->addHidden('gid', $ctx['groupId']); |
||
821 | $frm->addHidden('gradebook', '0'); |
||
822 | $frm->addHidden('origin', ''); |
||
823 | $frm->addHidden('action', 'searchpages'); |
||
824 | $frm->addButtonSearch(get_lang('Search')); |
||
825 | $right = $frm->returnForm(); |
||
826 | |||
827 | echo self::twToolbarHtml($left, $right); |
||
828 | } |
||
829 | |||
830 | /** Concurrency guard: mark/unmark is_editing for current page. */ |
||
831 | public function blockConcurrentEditions(int $userId, string $action): void |
||
832 | { |
||
833 | try { |
||
834 | $ctx = self::ctx(); |
||
835 | $em = Container::getEntityManager(); |
||
836 | |||
837 | if ($action === 'edit' && !empty($this->page)) { |
||
838 | $em->createQuery('UPDATE Chamilo\CourseBundle\Entity\CWiki w |
||
839 | SET w.isEditing = 1, w.timeEdit = :now |
||
840 | WHERE w.cId = :cid AND w.reflink = :r AND COALESCE(w.groupId,0) = :gid') |
||
841 | ->setParameter('now', api_get_utc_datetime(null, false, true)) |
||
842 | ->setParameter('cid', $ctx['courseId']) |
||
843 | ->setParameter('r', html_entity_decode($this->page)) |
||
844 | ->setParameter('gid', (int)$ctx['groupId']) |
||
845 | ->execute(); |
||
846 | } else { |
||
847 | $em->createQuery('UPDATE Chamilo\CourseBundle\Entity\CWiki w |
||
848 | SET w.isEditing = 0 |
||
849 | WHERE w.cId = :cid AND COALESCE(w.groupId,0) = :gid AND COALESCE(w.sessionId,0) = :sid') |
||
850 | ->setParameter('cid', $ctx['courseId']) |
||
851 | ->setParameter('gid', (int)$ctx['groupId']) |
||
852 | ->setParameter('sid', (int)$ctx['sessionId']) |
||
853 | ->execute(); |
||
854 | } |
||
855 | } catch (\Throwable $e) { |
||
856 | // silent best-effort |
||
857 | } |
||
858 | } |
||
859 | |||
860 | public static function delete_wiki(?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): string |
||
861 | { |
||
862 | |||
863 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
864 | $em = Container::getEntityManager(); |
||
865 | $conn = $em->getConnection(); |
||
866 | |||
867 | $cid = (int) $ctx['courseId']; |
||
868 | $gid = (int) $ctx['groupId']; |
||
869 | $sid = (int) $ctx['sessionId']; |
||
870 | |||
871 | $predGroup = $gid === 0 ? '(group_id IS NULL OR group_id = 0)' : 'group_id = :gid'; |
||
872 | $predSession = $sid === 0 ? '(session_id IS NULL OR session_id = 0)' : 'session_id = :sid'; |
||
873 | |||
874 | $pre = (int) $conn->fetchOne( |
||
875 | "SELECT COUNT(*) FROM c_wiki WHERE c_id = :cid AND $predGroup AND $predSession", |
||
876 | array_filter(['cid'=>$cid,'gid'=>$gid,'sid'=>$sid], static fn($v)=>true), |
||
877 | ); |
||
878 | |||
879 | if ($pre === 0) { |
||
880 | return get_lang('WikiDeleted').' (0 rows in this context)'; |
||
881 | } |
||
882 | |||
883 | $conn->beginTransaction(); |
||
884 | try { |
||
885 | $deletedDiscuss = $conn->executeStatement( |
||
886 | "DELETE d FROM c_wiki_discuss d |
||
887 | WHERE d.c_id = :cid |
||
888 | AND d.publication_id IN ( |
||
889 | SELECT DISTINCT w.page_id |
||
890 | FROM c_wiki w |
||
891 | WHERE w.c_id = :cid |
||
892 | AND $predGroup |
||
893 | AND $predSession |
||
894 | )", |
||
895 | array_filter(['cid'=>$cid,'gid'=>$gid,'sid'=>$sid], static fn($v)=>true), |
||
896 | ); |
||
897 | |||
898 | $deletedConf = $conn->executeStatement( |
||
899 | "DELETE c FROM c_wiki_conf c |
||
900 | WHERE c.c_id = :cid |
||
901 | AND c.page_id IN ( |
||
902 | SELECT DISTINCT w.page_id |
||
903 | FROM c_wiki w |
||
904 | WHERE w.c_id = :cid |
||
905 | AND $predGroup |
||
906 | AND $predSession |
||
907 | )", |
||
908 | array_filter(['cid'=>$cid,'gid'=>$gid,'sid'=>$sid], static fn($v)=>true), |
||
909 | ); |
||
910 | |||
911 | $deletedRelCat = $conn->executeStatement( |
||
912 | "DELETE rc FROM c_wiki_rel_category rc |
||
913 | WHERE rc.wiki_id IN ( |
||
914 | SELECT w.iid |
||
915 | FROM c_wiki w |
||
916 | WHERE w.c_id = :cid |
||
917 | AND $predGroup |
||
918 | AND $predSession |
||
919 | )", |
||
920 | array_filter(['cid'=>$cid,'gid'=>$gid,'sid'=>$sid], static fn($v)=>true), |
||
921 | ); |
||
922 | |||
923 | $deletedMailcue = $conn->executeStatement( |
||
924 | "DELETE m FROM c_wiki_mailcue m |
||
925 | WHERE m.c_id = :cid |
||
926 | AND ".($gid === 0 ? '(m.group_id IS NULL OR m.group_id = 0)' : 'm.group_id = :gid')." |
||
927 | AND ".($sid === 0 ? '(m.session_id IS NULL OR m.session_id = 0)' : 'm.session_id = :sid'), |
||
928 | array_filter(['cid'=>$cid,'gid'=>$gid,'sid'=>$sid], static fn($v)=>true), |
||
929 | ); |
||
930 | |||
931 | $deletedWiki = $conn->executeStatement( |
||
932 | "DELETE w FROM c_wiki w |
||
933 | WHERE w.c_id = :cid |
||
934 | AND $predGroup |
||
935 | AND $predSession", |
||
936 | array_filter(['cid'=>$cid,'gid'=>$gid,'sid'=>$sid], static fn($v)=>true), |
||
937 | ); |
||
938 | |||
939 | $conn->commit(); |
||
940 | |||
941 | return get_lang('WikiDeleted')." (versions=$deletedWiki, comments=$deletedDiscuss, conf=$deletedConf, catRel=$deletedRelCat, watchers=$deletedMailcue)"; |
||
942 | } catch (\Throwable $e) { |
||
943 | $conn->rollBack(); |
||
944 | // Short and clear message |
||
945 | return get_lang('Delete failed'); |
||
946 | } |
||
947 | } |
||
948 | |||
949 | /** Returns true if there is at least one version of a page (reflink) in the given context */ |
||
950 | private static function existsByReflink(string $reflink, ?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): bool |
||
951 | { |
||
952 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
953 | $qb = self::repo()->createQueryBuilder('w') |
||
954 | ->select('COUNT(w.iid)') |
||
955 | ->andWhere('w.cId = :cid')->setParameter('cid', (int)$ctx['courseId']) |
||
956 | ->andWhere('w.reflink = :r')->setParameter('r', $reflink) |
||
957 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']); |
||
958 | |||
959 | if ((int)$ctx['sessionId'] > 0) { |
||
960 | $qb->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
961 | } else { |
||
962 | $qb->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
963 | } |
||
964 | |||
965 | return ((int)$qb->getQuery()->getSingleScalarResult()) > 0; |
||
966 | } |
||
967 | |||
968 | /** |
||
969 | * Core save (new page or new version). Single source of truth. |
||
970 | */ |
||
971 | public static function saveWiki(array $values, ?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): string |
||
972 | { |
||
973 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
974 | $em = Container::getEntityManager(); |
||
975 | $repo = self::repo(); |
||
976 | $conn = $em->getConnection(); |
||
977 | |||
978 | $userId = api_get_user_id(); |
||
979 | $now = new \DateTime('now', new \DateTimeZone('UTC')); |
||
980 | |||
981 | // --- sanitize + normalize --- |
||
982 | $rawTitle = trim((string)($values['title'] ?? '')); |
||
983 | if ($rawTitle === '') { |
||
984 | return get_lang('NoWikiPageTitle'); |
||
985 | } |
||
986 | |||
987 | // Prepare safe strings (emoji-safe) |
||
988 | $values['title'] = self::utf8mb4_safe_entities((string) ($values['title'] ?? '')); |
||
989 | $values['content'] = self::utf8mb4_safe_entities((string) ($values['content'] ?? '')); |
||
990 | $values['comment'] = self::utf8mb4_safe_entities((string) ($values['comment'] ?? '')); |
||
991 | |||
992 | $content = $values['content'] ?? ''; |
||
993 | if ($content === '') { |
||
994 | $content = '<p> </p>'; // minimal content |
||
995 | } |
||
996 | if (api_get_setting('htmlpurifier_wiki') === 'true') { |
||
997 | $content = Security::remove_XSS($content); |
||
998 | } |
||
999 | |||
1000 | // Extract link tokens ([[...]]) |
||
1001 | $linkTo = self::links_to($content); |
||
1002 | |||
1003 | // Create vs update |
||
1004 | $incomingPageId = (int)($values['page_id'] ?? 0); |
||
1005 | $isNewPage = ($incomingPageId === 0); |
||
1006 | |||
1007 | // ---------- Determine reflink (KEY FIX) ---------- |
||
1008 | // Prefer an explicit 'reflink' if provided; else derive from the typed title. |
||
1009 | $explicitRef = trim((string)($values['reflink'] ?? '')); |
||
1010 | $candidate = $explicitRef !== '' ? $explicitRef : $rawTitle; |
||
1011 | |||
1012 | if ($isNewPage) { |
||
1013 | // For NEW pages, build the reflink from what the user typed, NOT from any outer GET param. |
||
1014 | // Normalize but only collapse to 'index' if the user explicitly typed an alias of Home. |
||
1015 | $reflink = self::normalizeToken($candidate); |
||
1016 | |||
1017 | $homeAliases = array_filter([ |
||
1018 | 'index', |
||
1019 | self::normalizeToken((string) (get_lang('Home') ?: 'Home')), |
||
1020 | ]); |
||
1021 | |||
1022 | if (in_array($reflink, $homeAliases, true)) { |
||
1023 | $reflink = 'index'; |
||
1024 | } |
||
1025 | } else { |
||
1026 | // For existing pages, keep behavior consistent with previous code |
||
1027 | $reflink = self::normalizeReflink($candidate); |
||
1028 | } |
||
1029 | |||
1030 | if (method_exists(__CLASS__, 'dbg')) { |
||
1031 | self::dbg('[SAVE] isNewPage=' . ($isNewPage ? '1' : '0') |
||
1032 | . ' | rawTitle=' . $rawTitle |
||
1033 | . ' | explicitRef=' . ($explicitRef === '' ? '(empty)' : $explicitRef) |
||
1034 | . ' | computedReflink=' . $reflink |
||
1035 | . ' | cid='.(int)$ctx['courseId'].' gid='.(int)$ctx['groupId'].' sid='.(int)$ctx['sessionId']); |
||
1036 | } |
||
1037 | |||
1038 | // --- If NEW page: abort if reflink already exists in this context --- |
||
1039 | if ($isNewPage) { |
||
1040 | $qbExists = $repo->createQueryBuilder('w') |
||
1041 | ->select('w.iid') |
||
1042 | ->andWhere('w.cId = :cid')->setParameter('cid', (int)$ctx['courseId']) |
||
1043 | ->andWhere('w.reflink = :r')->setParameter('r', $reflink) |
||
1044 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']); |
||
1045 | |||
1046 | if ((int)$ctx['sessionId'] > 0) { |
||
1047 | $qbExists->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
1048 | } else { |
||
1049 | $qbExists->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
1050 | } |
||
1051 | |||
1052 | $qbExists->orderBy('w.version', 'DESC')->setMaxResults(1); |
||
1053 | |||
1054 | if (method_exists(__CLASS__, 'dbg')) { |
||
1055 | $dql = $qbExists->getDQL(); |
||
1056 | $sql = $qbExists->getQuery()->getSQL(); |
||
1057 | $params = $qbExists->getQuery()->getParameters(); |
||
1058 | $types = []; |
||
1059 | foreach ($params as $p) { $types[$p->getName()] = $p->getType(); } |
||
1060 | self::dbg('[EXISTS DQL] '.$dql); |
||
1061 | self::dbg('[EXISTS SQL] '.$sql); |
||
1062 | self::dbg('[EXISTS PARAMS] '.json_encode(array_reduce(iterator_to_array($params), function($a,$p){$a[$p->getName()]=$p->getValue();return $a;}, []))); |
||
1063 | self::dbg('[EXISTS TYPES] '.json_encode($types)); |
||
1064 | } |
||
1065 | |||
1066 | $exists = (bool) $qbExists->getQuery()->getOneOrNullResult(); |
||
1067 | if ($exists) { |
||
1068 | return get_lang('ThePageAlreadyExists'); |
||
1069 | } |
||
1070 | } |
||
1071 | |||
1072 | // --- Find latest version if NOT new (by page_id) --- |
||
1073 | $last = null; |
||
1074 | if (!$isNewPage) { |
||
1075 | $qb = $repo->createQueryBuilder('w') |
||
1076 | ->andWhere('w.cId = :cid')->setParameter('cid', (int)$ctx['courseId']) |
||
1077 | ->andWhere('w.pageId = :pid')->setParameter('pid', $incomingPageId) |
||
1078 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
1079 | ->orderBy('w.version', 'DESC') |
||
1080 | ->setMaxResults(1); |
||
1081 | |||
1082 | if ((int)$ctx['sessionId'] > 0) { |
||
1083 | $qb->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
1084 | } else { |
||
1085 | $qb->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
1086 | } |
||
1087 | |||
1088 | /** @var CWiki|null $last */ |
||
1089 | $last = $qb->getQuery()->getOneOrNullResult(); |
||
1090 | } |
||
1091 | |||
1092 | // base version and pageId |
||
1093 | $version = $last ? ((int) $last->getVersion() + 1) : 1; |
||
1094 | $pageId = (int) $last?->getPageId(); |
||
1095 | |||
1096 | $w = new CWiki(); |
||
1097 | $w->setCId((int) $ctx['courseId']); |
||
1098 | $w->setPageId($pageId); |
||
1099 | $w->setReflink($reflink); |
||
1100 | $w->setTitle($values['title']); |
||
1101 | $w->setContent($content); |
||
1102 | $w->setUserId($userId); |
||
1103 | |||
1104 | // group/session as ints (0 = none) |
||
1105 | $w->setGroupId((int) $ctx['groupId']); |
||
1106 | $w->setSessionId((int) $ctx['sessionId']); |
||
1107 | |||
1108 | $w->setDtime($now); |
||
1109 | |||
1110 | // inherit flags or defaults |
||
1111 | $w->setAddlock( $last ? $last->getAddlock() : 1); |
||
1112 | $w->setEditlock( $last ? (int) $last->getEditlock() : 0); |
||
1113 | $w->setVisibility( $last ? $last->getVisibility() : 1); |
||
1114 | $w->setAddlockDisc( $last ? $last->getAddlockDisc() : 1); |
||
1115 | $w->setVisibilityDisc($last ? $last->getVisibilityDisc() : 1); |
||
1116 | $w->setRatinglockDisc($last ? $last->getRatinglockDisc() : 1); |
||
1117 | |||
1118 | $w->setAssignment((int) ($values['assignment'] ?? ($last ? (int) $last->getAssignment() : 0))); |
||
1119 | $w->setComment((string) ($values['comment'] ?? '')); |
||
1120 | $w->setProgress((string) ($values['progress'] ?? '')); |
||
1121 | $w->setScore($last ? ((int) $last->getScore() ?: 0) : 0); |
||
1122 | |||
1123 | $w->setVersion($version); |
||
1124 | $w->setIsEditing(0); |
||
1125 | $w->setTimeEdit(null); |
||
1126 | $w->setHits($last ? ((int) $last->getHits() ?: 0) : 0); |
||
1127 | |||
1128 | $w->setLinksto($linkTo); |
||
1129 | $w->setTag(''); |
||
1130 | $w->setUserIp(api_get_real_ip()); |
||
1131 | |||
1132 | $w->setParent($ctx['course']); |
||
1133 | $w->setCreator(api_get_user_entity()); |
||
1134 | $groupEntity = $ctx['groupId'] ? api_get_group_entity((int)$ctx['groupId']) : null; |
||
1135 | $w->addCourseLink($ctx['course'], $ctx['session'], $groupEntity); |
||
1136 | |||
1137 | // Categories |
||
1138 | if (true === api_get_configuration_value('wiki_categories_enabled')) { |
||
1139 | $catIds = (array)($values['category'] ?? []); |
||
1140 | if (!empty($catIds)) { |
||
1141 | $catRepo = $em->getRepository(CWikiCategory::class); |
||
1142 | foreach ($catIds as $catId) { |
||
1143 | $cat = $catRepo->find((int) $catId); |
||
1144 | if ($cat) { $w->addCategory($cat); } |
||
1145 | } |
||
1146 | } |
||
1147 | } |
||
1148 | |||
1149 | $em->persist($w); |
||
1150 | $em->flush(); |
||
1151 | |||
1152 | if (method_exists(__CLASS__, 'dbg')) { |
||
1153 | self::dbg('[SAVE] after first flush iid='.(int)$w->getIid().' pageId='.(int)$w->getPageId().' reflink='.$reflink); |
||
1154 | } |
||
1155 | |||
1156 | // If FIRST version of a new page, set page_id = iid |
||
1157 | if ($isNewPage) { |
||
1158 | $w->setPageId((int) $w->getIid()); |
||
1159 | $em->flush(); |
||
1160 | if (method_exists(__CLASS__, 'dbg')) { |
||
1161 | self::dbg('[SAVE] after setPageId flush iid='.(int)$w->getIid().' pageId='.(int)$w->getPageId()); |
||
1162 | } |
||
1163 | $pageId = (int) $w->getPageId(); |
||
1164 | } else { |
||
1165 | $pageId = (int)$incomingPageId; |
||
1166 | } |
||
1167 | |||
1168 | // DB sanity check |
||
1169 | $check = (int) $conn->fetchOne( |
||
1170 | 'SELECT COUNT(*) FROM c_wiki |
||
1171 | WHERE c_id = :cid |
||
1172 | AND reflink = :r |
||
1173 | AND COALESCE(group_id,0) = :gid |
||
1174 | AND '.((int)$ctx['sessionId'] > 0 ? '(COALESCE(session_id,0) IN (0,:sid))' : 'COALESCE(session_id,0) = 0'), |
||
1175 | [ |
||
1176 | 'cid' => (int)$ctx['courseId'], |
||
1177 | 'r' => $reflink, |
||
1178 | 'gid' => (int)$ctx['groupId'], |
||
1179 | 'sid' => (int)$ctx['sessionId'], |
||
1180 | ] |
||
1181 | ); |
||
1182 | |||
1183 | if (method_exists(__CLASS__, 'dbg')) { |
||
1184 | self::dbg('[SAVE] db count after save='.$check.' (reflink='.$reflink.')'); |
||
1185 | } |
||
1186 | |||
1187 | if ($check === 0) { |
||
1188 | throw new \RuntimeException('Wiki save failed: no row inserted (cid='.$ctx['courseId'].', reflink='.$reflink.', gid='.$ctx['groupId'].', sid='.$ctx['sessionId'].')'); |
||
1189 | } |
||
1190 | |||
1191 | // ---- CWikiConf ---- |
||
1192 | $hasConfFields = isset($values['task']) || isset($values['feedback1']) || isset($values['feedback2']) |
||
1193 | || isset($values['feedback3']) || isset($values['fprogress1']) || isset($values['fprogress2']) |
||
1194 | || isset($values['fprogress3']) || isset($values['max_text']) || isset($values['max_version']) |
||
1195 | || array_key_exists('startdate_assig', $values) || array_key_exists('enddate_assig', $values) |
||
1196 | || isset($values['delayedsubmit']); |
||
1197 | |||
1198 | if ($version === 1 && $hasConfFields) { |
||
1199 | $conf = new CWikiConf(); |
||
1200 | $conf->setCId((int) $ctx['courseId']); |
||
1201 | $conf->setPageId($pageId); |
||
1202 | $conf->setTask((string) ($values['task'] ?? '')); |
||
1203 | $conf->setFeedback1((string) ($values['feedback1'] ?? '')); |
||
1204 | $conf->setFeedback2((string) ($values['feedback2'] ?? '')); |
||
1205 | $conf->setFeedback3((string) ($values['feedback3'] ?? '')); |
||
1206 | $conf->setFprogress1((string) ($values['fprogress1'] ?? '')); |
||
1207 | $conf->setFprogress2((string) ($values['fprogress2'] ?? '')); |
||
1208 | $conf->setFprogress3((string) ($values['fprogress3'] ?? '')); |
||
1209 | $conf->setMaxText((int) ($values['max_text'] ?? 0)); |
||
1210 | $conf->setMaxVersion((int) ($values['max_version'] ?? 0)); |
||
1211 | $conf->setStartdateAssig(self::toDateTime($values['startdate_assig'] ?? null)); |
||
1212 | $conf->setEnddateAssig(self::toDateTime($values['enddate_assig'] ?? null)); |
||
1213 | $conf->setDelayedsubmit((int) ($values['delayedsubmit'] ?? 0)); |
||
1214 | $em->persist($conf); |
||
1215 | $em->flush(); |
||
1216 | } elseif ($hasConfFields) { |
||
1217 | /** @var CWikiConf|null $conf */ |
||
1218 | $conf = self::confRepo()->findOneBy(['cId' => (int) $ctx['courseId'], 'pageId' => $pageId]); |
||
1219 | if ($conf) { |
||
1220 | $conf->setTask((string) ($values['task'] ?? $conf->getTask())); |
||
1221 | $conf->setFeedback1((string) ($values['feedback1'] ?? $conf->getFeedback1())); |
||
1222 | $conf->setFeedback2((string) ($values['feedback2'] ?? $conf->getFeedback2())); |
||
1223 | $conf->setFeedback3((string) ($values['feedback3'] ?? $conf->getFeedback3())); |
||
1224 | $conf->setFprogress1((string) ($values['fprogress1'] ?? $conf->getFprogress1())); |
||
1225 | $conf->setFprogress2((string) ($values['fprogress2'] ?? $conf->getFprogress2())); |
||
1226 | $conf->setFprogress3((string) ($values['fprogress3'] ?? $conf->getFprogress3())); |
||
1227 | if (isset($values['max_text'])) { $conf->setMaxText((int) $values['max_text']); } |
||
1228 | if (isset($values['max_version'])) { $conf->setMaxVersion((int) $values['max_version']); } |
||
1229 | if (array_key_exists('startdate_assig', $values)) { $conf->setStartdateAssig(self::toDateTime($values['startdate_assig'])); } |
||
1230 | if (array_key_exists('enddate_assig', $values)) { $conf->setEnddateAssig(self::toDateTime($values['enddate_assig'])); } |
||
1231 | if (isset($values['delayedsubmit'])) { $conf->setDelayedsubmit((int) $values['delayedsubmit']); } |
||
1232 | $em->flush(); |
||
1233 | } |
||
1234 | } |
||
1235 | |||
1236 | // Notify watchers (legacy: 'P' = page change) |
||
1237 | self::check_emailcue($reflink, 'P', $now, $userId); |
||
1238 | |||
1239 | return $isNewPage ? get_lang('TheNewPageHasBeenCreated') : get_lang('Saved'); |
||
1240 | } |
||
1241 | |||
1242 | |||
1243 | /** |
||
1244 | * Compat wrappers (to avoid breaking old calls). |
||
1245 | */ |
||
1246 | public static function save_wiki(array $values, ?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): string |
||
1247 | { |
||
1248 | return self::saveWiki($values, $courseId, $sessionId, $groupId); |
||
1249 | } |
||
1250 | |||
1251 | public static function save_new_wiki(array $values, ?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): string|false |
||
1252 | { |
||
1253 | $msg = self::saveWiki($values, $courseId, $sessionId, $groupId); |
||
1254 | |||
1255 | return $msg === get_lang('NoWikiPageTitle') ? false : $msg; |
||
1256 | } |
||
1257 | |||
1258 | /** |
||
1259 | * Send email notifications to watchers. |
||
1260 | * @param int|string $id_or_ref 'P' => reflink | 'D' => iid of CWiki row | 'A'/'E' => 0 |
||
1261 | */ |
||
1262 | public static function check_emailcue($id_or_ref, string $type, $lastime = '', $lastuser = ''): void |
||
1263 | { |
||
1264 | $ctx = self::ctx(api_get_course_int_id(), api_get_session_id(), api_get_group_id()); |
||
1265 | $em = Container::getEntityManager(); |
||
1266 | |||
1267 | $allowSend = false; |
||
1268 | $emailAssignment = null; |
||
1269 | $emailPageName = ''; |
||
1270 | $emailDateChanges = ''; |
||
1271 | $emailText = ''; |
||
1272 | $watchKey = null; |
||
1273 | $pageReflink = null; |
||
1274 | |||
1275 | // When timestamp provided |
||
1276 | if ($lastime instanceof \DateTimeInterface) { |
||
1277 | $emailDateChanges = $lastime->format('Y-m-d H:i:s'); |
||
1278 | } elseif (is_string($lastime) && $lastime !== '') { |
||
1279 | $emailDateChanges = $lastime; |
||
1280 | } |
||
1281 | |||
1282 | // Author line |
||
1283 | $emailUserAuthor = ''; |
||
1284 | if ($lastuser) { |
||
1285 | $ui = api_get_user_info((int) $lastuser); |
||
1286 | $emailUserAuthor = ($type === 'P' || $type === 'D') |
||
1287 | ? get_lang('EditedBy').': '.($ui['complete_name'] ?? '') |
||
1288 | : get_lang('AddedBy').': '.($ui['complete_name'] ?? ''); |
||
1289 | } else { |
||
1290 | $ui = api_get_user_info(api_get_user_id()); |
||
1291 | $emailUserAuthor = ($type === 'E') |
||
1292 | ? get_lang('DeletedBy').': '.($ui['complete_name'] ?? '') |
||
1293 | : get_lang('EditedBy').': '.($ui['complete_name'] ?? ''); |
||
1294 | } |
||
1295 | |||
1296 | $repoWiki = $em->getRepository(CWiki::class); |
||
1297 | $repoCue = $em->getRepository(CWikiMailcue::class); |
||
1298 | |||
1299 | // --- Resolve page + message according to event type --- |
||
1300 | if ($type === 'P') { |
||
1301 | // Page modified -> $id_or_ref is a reflink |
||
1302 | /** @var CWiki|null $first */ |
||
1303 | $first = $repoWiki->createQueryBuilder('w') |
||
1304 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
1305 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode((string)$id_or_ref)) |
||
1306 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
1307 | ->orderBy('w.version', 'ASC') |
||
1308 | ->setMaxResults(1) |
||
1309 | ->getQuery()->getOneOrNullResult(); |
||
1310 | |||
1311 | if ($first) { |
||
1312 | $emailPageName = (string) $first->getTitle(); |
||
1313 | $pageReflink = (string) $first->getReflink(); |
||
1314 | if ((int) $first->getVisibility() === 1) { |
||
1315 | $allowSend = true; |
||
1316 | $emailText = get_lang('EmailWikipageModified').' <strong>'.$emailPageName.'</strong> '.get_lang('Wiki'); |
||
1317 | $watchKey = 'watch:'.$pageReflink; |
||
1318 | } |
||
1319 | } |
||
1320 | } elseif ($type === 'D') { |
||
1321 | // New discussion comment -> $id_or_ref is publication_id (page_id) |
||
1322 | /** @var CWiki|null $row */ |
||
1323 | $row = $repoWiki->createQueryBuilder('w') |
||
1324 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
1325 | ->andWhere('w.pageId = :pid')->setParameter('pid', (int)$id_or_ref) |
||
1326 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
1327 | ->orderBy('w.version', 'DESC') |
||
1328 | ->setMaxResults(1) |
||
1329 | ->getQuery()->getOneOrNullResult(); |
||
1330 | |||
1331 | if ($row) { |
||
1332 | $emailPageName = (string) $row->getTitle(); |
||
1333 | $pageReflink = (string) $row->getReflink(); |
||
1334 | if ((int) $row->getVisibilityDisc() === 1) { |
||
1335 | $allowSend = true; |
||
1336 | $emailText = get_lang('EmailWikiPageDiscAdded').' <strong>'.$emailPageName.'</strong> '.get_lang('Wiki'); |
||
1337 | $watchKey = 'watchdisc:'.$pageReflink; |
||
1338 | } |
||
1339 | } |
||
1340 | } elseif ($type === 'A') { |
||
1341 | // New page added (find latest row in this context) |
||
1342 | /** @var CWiki|null $row */ |
||
1343 | $row = $repoWiki->createQueryBuilder('w') |
||
1344 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
1345 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
1346 | ->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']) |
||
1347 | ->orderBy('w.iid', 'DESC') |
||
1348 | ->setMaxResults(1) |
||
1349 | ->getQuery()->getOneOrNullResult(); |
||
1350 | |||
1351 | if ($row) { |
||
1352 | $emailPageName = (string) $row->getTitle(); |
||
1353 | $pageReflink = (string) $row->getReflink(); |
||
1354 | $emailDateChanges = $row->getDtime() ? $row->getDtime()->format('Y-m-d H:i:s') : $emailDateChanges; |
||
1355 | |||
1356 | if ((int) $row->getAssignment() === 0) { |
||
1357 | $allowSend = true; |
||
1358 | } elseif ((int) $row->getAssignment() === 1) { |
||
1359 | $emailAssignment = get_lang('AssignmentDescExtra').' ('.get_lang('AssignmentMode').')'; |
||
1360 | $allowSend = true; |
||
1361 | } elseif ((int) $row->getAssignment() === 2) { |
||
1362 | $allowSend = false; // teacher-locked work page |
||
1363 | } |
||
1364 | |||
1365 | $emailText = get_lang('EmailWikiPageAdded').' <strong>'.$emailPageName.'</strong> '.get_lang('In').' '.get_lang('Wiki'); |
||
1366 | // If someone subscribed after creation, use the same key as page watchers |
||
1367 | $watchKey = 'watch:'.$pageReflink; |
||
1368 | } |
||
1369 | } elseif ($type === 'E') { |
||
1370 | // Page deleted (generic) |
||
1371 | $allowSend = true; |
||
1372 | $emailText = get_lang('EmailWikipageDedeleted'); |
||
1373 | if ($emailDateChanges === '') { |
||
1374 | $emailDateChanges = date('Y-m-d H:i:s'); |
||
1375 | } |
||
1376 | } |
||
1377 | |||
1378 | if (!$allowSend) { |
||
1379 | return; |
||
1380 | } |
||
1381 | |||
1382 | $courseInfo = $ctx['courseInfo'] ?: (api_get_course_info_by_id((int)$ctx['courseId']) ?: []); |
||
1383 | $courseTitle = $courseInfo['title'] ?? ($courseInfo['name'] ?? ''); |
||
1384 | $courseName = $courseInfo['name'] ?? $courseTitle; |
||
1385 | |||
1386 | // Group/session labels |
||
1387 | $grpName = ''; |
||
1388 | if ((int)$ctx['groupId'] > 0) { |
||
1389 | $g = GroupManager::get_group_properties((int)$ctx['groupId']); |
||
1390 | $grpName = $g['name'] ?? ''; |
||
1391 | } |
||
1392 | $sessionName = ((int)$ctx['sessionId'] > 0) ? api_get_session_name((int)$ctx['sessionId']) : ''; |
||
1393 | |||
1394 | // --- Fetch watchers filtered by type (when available) --- |
||
1395 | $qb = $repoCue->createQueryBuilder('m') |
||
1396 | ->andWhere('m.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
1397 | ->andWhere('COALESCE(m.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
1398 | ->andWhere('COALESCE(m.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
1399 | |||
1400 | // Only mail the relevant subscribers |
||
1401 | if (!empty($watchKey)) { |
||
1402 | $qb->andWhere('m.type = :t')->setParameter('t', $watchKey); |
||
1403 | } |
||
1404 | |||
1405 | $watchers = $qb->getQuery()->getArrayResult(); |
||
1406 | if (empty($watchers)) { |
||
1407 | return; |
||
1408 | } |
||
1409 | |||
1410 | // Optional logo |
||
1411 | $extraParams = []; |
||
1412 | if (api_get_configuration_value('mail_header_from_custom_course_logo') === true) { |
||
1413 | $extraParams = ['logo' => CourseManager::getCourseEmailPicture($courseInfo)]; |
||
1414 | } |
||
1415 | |||
1416 | foreach ($watchers as $w) { |
||
1417 | $uid = (int) ($w['userId'] ?? 0); |
||
1418 | if ($uid === 0) { |
||
1419 | continue; |
||
1420 | } |
||
1421 | // Do not email the actor themself |
||
1422 | if ($lastuser && (int)$lastuser === $uid) { |
||
1423 | continue; |
||
1424 | } |
||
1425 | |||
1426 | $uInfo = api_get_user_info($uid); |
||
1427 | if (!$uInfo || empty($uInfo['email'])) { |
||
1428 | continue; |
||
1429 | } |
||
1430 | |||
1431 | $nameTo = $uInfo['complete_name']; |
||
1432 | $emailTo = $uInfo['email']; |
||
1433 | $from = (string) api_get_setting('emailAdministrator'); |
||
1434 | |||
1435 | $subject = get_lang('Email wiki changes').' - '.$courseTitle; |
||
1436 | |||
1437 | $body = get_lang('DearUser').' '.api_get_person_name($uInfo['firstname'] ?? '', $uInfo['lastname'] ?? '').',<br /><br />'; |
||
1438 | if ((int)$ctx['sessionId'] === 0) { |
||
1439 | $body .= $emailText.' <strong>'.$courseName.($grpName ? ' - '.$grpName : '').'</strong><br /><br /><br />'; |
||
1440 | } else { |
||
1441 | $body .= $emailText.' <strong>'.$courseName.' ('.$sessionName.')'.($grpName ? ' - '.$grpName : '').'</strong><br /><br /><br />'; |
||
1442 | } |
||
1443 | if ($emailUserAuthor) { |
||
1444 | $body .= $emailUserAuthor.($emailDateChanges ? ' ('.$emailDateChanges.')' : '').'<br /><br /><br />'; |
||
1445 | } |
||
1446 | if ($emailAssignment) { |
||
1447 | $body .= $emailAssignment.'<br /><br /><br />'; |
||
1448 | } |
||
1449 | $body .= '<span style="font-size:70%;">'.get_lang('EmailWikiChangesExt_1').': <strong>'.get_lang('NotifyChanges').'</strong><br />'; |
||
1450 | $body .= get_lang('EmailWikiChangesExt_2').': <strong>'.get_lang('NotNotifyChanges').'</strong></span><br />'; |
||
1451 | |||
1452 | @api_mail_html( |
||
1453 | $nameTo, |
||
1454 | $emailTo, |
||
1455 | $subject, |
||
1456 | $body, |
||
1457 | $from, |
||
1458 | $from, |
||
1459 | [], |
||
1460 | [], |
||
1461 | false, |
||
1462 | $extraParams, |
||
1463 | '' |
||
1464 | ); |
||
1465 | } |
||
1466 | } |
||
1467 | |||
1468 | /** Full view (classic structure + modern toolbar wrapper) */ |
||
1469 | public static function display_wiki_entry( |
||
1470 | string $newtitle, |
||
1471 | ?string $page = null, |
||
1472 | ?int $courseId = null, |
||
1473 | ?int $sessionId = null, |
||
1474 | ?int $groupId = null |
||
1475 | ): ?string { |
||
1476 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
1477 | $em = Container::getEntityManager(); |
||
1478 | $repo = self::repo(); |
||
1479 | |||
1480 | // Resolve the page key we will work with |
||
1481 | $pageKey = self::normalizeReflink($newtitle !== '' ? $newtitle : ($page ?? null)); |
||
1482 | |||
1483 | // --- ONE toggle block (lock/visible/notify) with PRG redirect --- |
||
1484 | $actionPage = $_GET['actionpage'] ?? null; |
||
1485 | if ($actionPage !== null) { |
||
1486 | $allowed = ['lock','unlock','visible','invisible','locknotify','unlocknotify']; |
||
1487 | |||
1488 | if (in_array($actionPage, $allowed, true)) { |
||
1489 | $conn = $em->getConnection(); |
||
1490 | $cid = (int)$ctx['courseId']; |
||
1491 | $gid = (int)$ctx['groupId']; |
||
1492 | $sid = (int)$ctx['sessionId']; |
||
1493 | $uid = (int)api_get_user_id(); |
||
1494 | |||
1495 | $predG = 'COALESCE(group_id,0) = :gid'; |
||
1496 | $predS = 'COALESCE(session_id,0) = :sid'; |
||
1497 | |||
1498 | switch ($actionPage) { |
||
1499 | case 'lock': |
||
1500 | case 'unlock': |
||
1501 | // Only teachers/admins can toggle lock |
||
1502 | if (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) { |
||
1503 | Display::addFlash(Display::return_message('Not allowed to lock/unlock this page.', 'error', false)); |
||
1504 | break; |
||
1505 | } |
||
1506 | $newVal = ($actionPage === 'lock') ? 1 : 0; |
||
1507 | $conn->executeStatement( |
||
1508 | "UPDATE c_wiki SET editlock = :v |
||
1509 | WHERE c_id = :cid AND reflink = :r AND $predG AND $predS", |
||
1510 | ['v'=>$newVal, 'cid'=>$cid, 'r'=>$pageKey, 'gid'=>$gid, 'sid'=>$sid] |
||
1511 | ); |
||
1512 | break; |
||
1513 | |||
1514 | case 'visible': |
||
1515 | case 'invisible': |
||
1516 | // Only teachers/admins can toggle visibility |
||
1517 | if (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) { |
||
1518 | Display::addFlash(Display::return_message('Not allowed to change visibility.', 'error', false)); |
||
1519 | break; |
||
1520 | } |
||
1521 | $newVal = ($actionPage === 'visible') ? 1 : 0; |
||
1522 | $conn->executeStatement( |
||
1523 | "UPDATE c_wiki SET visibility = :v |
||
1524 | WHERE c_id = :cid AND reflink = :r AND $predG AND $predS", |
||
1525 | ['v'=>$newVal, 'cid'=>$cid, 'r'=>$pageKey, 'gid'=>$gid, 'sid'=>$sid] |
||
1526 | ); |
||
1527 | break; |
||
1528 | |||
1529 | case 'locknotify': |
||
1530 | case 'unlocknotify': |
||
1531 | // Session editors can subscribe/unsubscribe |
||
1532 | if (!api_is_allowed_to_session_edit()) { |
||
1533 | Display::addFlash(Display::return_message('Not allowed to (un)subscribe notifications.', 'error', false)); |
||
1534 | break; |
||
1535 | } |
||
1536 | $watchKey = 'watch:'.$pageKey; |
||
1537 | |||
1538 | if ($actionPage === 'locknotify') { |
||
1539 | // Insert if not exists |
||
1540 | $conn->executeStatement( |
||
1541 | "INSERT INTO c_wiki_mailcue (c_id, group_id, session_id, user_id, type) |
||
1542 | SELECT :cid, :gid, :sid, :uid, :t |
||
1543 | FROM DUAL |
||
1544 | WHERE NOT EXISTS ( |
||
1545 | SELECT 1 FROM c_wiki_mailcue |
||
1546 | WHERE c_id = :cid AND $predG AND $predS |
||
1547 | AND user_id = :uid AND type = :t |
||
1548 | )", |
||
1549 | ['cid'=>$cid, 'gid'=>$gid, 'sid'=>$sid, 'uid'=>$uid, 't'=>$watchKey] |
||
1550 | ); |
||
1551 | } else { // unlocknotify |
||
1552 | $conn->executeStatement( |
||
1553 | "DELETE FROM c_wiki_mailcue |
||
1554 | WHERE c_id = :cid AND $predG AND $predS |
||
1555 | AND user_id = :uid AND type = :t", |
||
1556 | ['cid'=>$cid, 'gid'=>$gid, 'sid'=>$sid, 'uid'=>$uid, 't'=>$watchKey] |
||
1557 | ); |
||
1558 | } |
||
1559 | break; |
||
1560 | } |
||
1561 | |||
1562 | // PRG redirect so icons reflect the change immediately |
||
1563 | header('Location: '.$ctx['baseUrl'].'&action=showpage&title='.urlencode($pageKey)); |
||
1564 | exit; |
||
1565 | } |
||
1566 | } |
||
1567 | |||
1568 | /** @var CWiki|null $first */ |
||
1569 | $first = $repo->createQueryBuilder('w') |
||
1570 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
1571 | ->andWhere('w.reflink = :reflink')->setParameter('reflink', $pageKey) |
||
1572 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
1573 | ->orderBy('w.version', 'ASC') |
||
1574 | ->setMaxResults(1) |
||
1575 | ->getQuery()->getOneOrNullResult(); |
||
1576 | |||
1577 | $keyVisibility = $first?->getVisibility(); |
||
1578 | $pageId = $first?->getPageId() ?? 0; |
||
1579 | |||
1580 | $last = null; |
||
1581 | if ($pageId) { |
||
1582 | $qb = $repo->createQueryBuilder('w') |
||
1583 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
1584 | ->andWhere('w.pageId = :pid')->setParameter('pid', $pageId) |
||
1585 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
1586 | ->orderBy('w.version', 'DESC') |
||
1587 | ->setMaxResults(1); |
||
1588 | |||
1589 | if ($ctx['sessionId'] > 0) { |
||
1590 | $qb->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
1591 | } else { |
||
1592 | $qb->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
1593 | } |
||
1594 | |||
1595 | $last = $qb->getQuery()->getOneOrNullResult(); |
||
1596 | } |
||
1597 | |||
1598 | if ($last && $last->getPageId()) { |
||
1599 | Event::addEvent(LOG_WIKI_ACCESS, LOG_WIKI_PAGE_ID, (int)$last->getPageId()); |
||
1600 | $last->setHits(((int)$last->getHits()) + 1); |
||
1601 | $em->flush(); |
||
1602 | } |
||
1603 | |||
1604 | $content = ''; |
||
1605 | $title = ''; |
||
1606 | |||
1607 | if (!$last || ($last->getContent() === '' && $last->getTitle() === '' && $pageKey === 'index')) { |
||
1608 | $groupInfo = GroupManager::get_group_properties((int)$ctx['groupId']); |
||
1609 | if (api_is_allowed_to_edit(false, true) |
||
1610 | || api_is_platform_admin() |
||
1611 | || GroupManager::is_user_in_group(api_get_user_id(), $groupInfo) |
||
1612 | || api_is_allowed_in_course() |
||
1613 | ) { |
||
1614 | $content = '<div class="text-center">' |
||
1615 | .sprintf(get_lang('Default content'), api_get_path(WEB_IMG_PATH)) |
||
1616 | .'</div>'; |
||
1617 | $title = get_lang('Home'); |
||
1618 | } else { |
||
1619 | Display::addFlash(Display::return_message(get_lang('Wiki stand by'), 'normal', false)); |
||
1620 | return null; |
||
1621 | } |
||
1622 | } else { |
||
1623 | if (true === api_get_configuration_value('wiki_html_strict_filtering')) { |
||
1624 | $content = Security::remove_XSS($last->getContent(), COURSEMANAGERLOWSECURITY); |
||
1625 | } else { |
||
1626 | $content = Security::remove_XSS($last->getContent()); |
||
1627 | } |
||
1628 | $title = htmlspecialchars_decode(Security::remove_XSS($last->getTitle())); |
||
1629 | } |
||
1630 | |||
1631 | // Badges next to title |
||
1632 | $pageTitleText = self::displayTitleFor($pageKey, $last ? $last->getTitle() : null); |
||
1633 | $pageTitle = api_htmlentities($pageTitleText); |
||
1634 | if ($last) { |
||
1635 | $badges = ''; |
||
1636 | $assign = (int) $last->getAssignment(); |
||
1637 | |||
1638 | if ($assign === 1) { |
||
1639 | $badges .= Display::getMdiIcon( |
||
1640 | ActionIcon::WIKI_ASSIGNMENT, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Assignment desc extra') |
||
1641 | ); |
||
1642 | } elseif ($assign === 2) { |
||
1643 | $badges .= Display::getMdiIcon( |
||
1644 | ActionIcon::WIKI_WORK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Assignment work') |
||
1645 | ); |
||
1646 | } |
||
1647 | |||
1648 | // Task badge (if any) |
||
1649 | $hasTask = self::confRepo()->findOneBy([ |
||
1650 | 'cId' => $ctx['courseId'], |
||
1651 | 'pageId' => (int) $last->getPageId(), |
||
1652 | ]); |
||
1653 | if ($hasTask && $hasTask->getTask()) { |
||
1654 | $badges .= Display::getMdiIcon( |
||
1655 | ActionIcon::WIKI_TASK, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Standard task') |
||
1656 | ); |
||
1657 | } |
||
1658 | |||
1659 | if ($badges !== '') { |
||
1660 | $pageTitle = $badges.' '.$pageTitle; |
||
1661 | } |
||
1662 | } |
||
1663 | |||
1664 | // Visibility gate |
||
1665 | if ($keyVisibility != "1" |
||
1666 | && !api_is_allowed_to_edit(false, true) |
||
1667 | && !api_is_platform_admin() |
||
1668 | && ($last?->getAssignment() != 2 || $keyVisibility != "0" || api_get_user_id() != $last?->getUserId()) |
||
1669 | && !api_is_allowed_in_course() |
||
1670 | ) { |
||
1671 | return null; |
||
1672 | } |
||
1673 | |||
1674 | // Actions (left/right) |
||
1675 | $actionsLeft = ''; |
||
1676 | $actionsRight = ''; |
||
1677 | |||
1678 | // Edit |
||
1679 | $editLink = '<a href="'.$ctx['baseUrl'].'&action=edit&title='.api_htmlentities(urlencode($pageKey)).'"' |
||
1680 | .self::is_active_navigation_tab('edit').'>' |
||
1681 | .Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Edit')).'</a>'; |
||
1682 | |||
1683 | $groupInfo = GroupManager::get_group_properties((int)$ctx['groupId']); |
||
1684 | if (api_is_allowed_to_edit(false, true) |
||
1685 | || api_is_allowed_in_course() |
||
1686 | || GroupManager::is_user_in_group(api_get_user_id(), $groupInfo) |
||
1687 | ) { |
||
1688 | $actionsLeft .= $editLink; |
||
1689 | } |
||
1690 | |||
1691 | $pageProgress = (int)$last?->getProgress() * 10; |
||
1692 | $pageScore = (int)$last?->getScore(); |
||
1693 | |||
1694 | if ($last) { |
||
1695 | // Lock / Unlock |
||
1696 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
1697 | $isLocked = (self::check_protect_page($pageKey, $ctx['courseId'], $ctx['sessionId'], $ctx['groupId']) == 1); |
||
1698 | $lockAction = $isLocked ? 'unlock' : 'lock'; |
||
1699 | $lockIcon = $isLocked ? ActionIcon::LOCK : ActionIcon::UNLOCK; |
||
1700 | $actionsRight .= '<a href="'.$ctx['baseUrl'].'&action=showpage&actionpage='.$lockAction |
||
1701 | .'&title='.api_htmlentities(urlencode($pageKey)).'">' |
||
1702 | .Display::getMdiIcon($lockIcon, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, $isLocked ? get_lang('Locked') : get_lang('Unlocked')) |
||
1703 | .'</a>'; |
||
1704 | } |
||
1705 | |||
1706 | // Visibility |
||
1707 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
1708 | $isVisible = (self::check_visibility_page($pageKey, $ctx['courseId'], $ctx['sessionId'], $ctx['groupId']) == 1); |
||
1709 | $visAction = $isVisible ? 'invisible' : 'visible'; |
||
1710 | $visIcon = $isVisible ? ActionIcon::VISIBLE : ActionIcon::INVISIBLE; |
||
1711 | $actionsRight .= '<a href="'.$ctx['baseUrl'].'&action=showpage&actionpage='.$visAction |
||
1712 | .'&title='.api_htmlentities(urlencode($pageKey)).'">' |
||
1713 | .Display::getMdiIcon($visIcon, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, $isVisible ? get_lang('Hide') : get_lang('Show')) |
||
1714 | .'</a>'; |
||
1715 | } |
||
1716 | |||
1717 | // Notify |
||
1718 | if (api_is_allowed_to_session_edit()) { |
||
1719 | $isWatching = (self::check_notify_page($pageKey) == 1); |
||
1720 | $notifyAction = $isWatching ? 'unlocknotify' : 'locknotify'; |
||
1721 | $notifyIcon = $isWatching ? ActionIcon::SEND_SINGLE_EMAIL : ActionIcon::NOTIFY_OFF; |
||
1722 | $notifyTitle = $isWatching ? get_lang('CancelNotifyMe') : get_lang('NotifyMe'); |
||
1723 | $actionsRight .= '<a href="'.$ctx['baseUrl'].'&action=showpage&actionpage='.$notifyAction |
||
1724 | .'&title='.api_htmlentities(urlencode($pageKey)).'">' |
||
1725 | .Display::getMdiIcon($notifyIcon, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, $notifyTitle) |
||
1726 | .'</a>'; |
||
1727 | } |
||
1728 | |||
1729 | // Discuss |
||
1730 | if ((api_is_allowed_to_session_edit(false, true) && api_is_allowed_to_edit()) |
||
1731 | || GroupManager::is_user_in_group(api_get_user_id(), $groupInfo) |
||
1732 | ) { |
||
1733 | $actionsRight .= '<a href="'.$ctx['baseUrl'].'&action=discuss&title=' |
||
1734 | .api_htmlentities(urlencode($pageKey)).'" '.self::is_active_navigation_tab('discuss').'>' |
||
1735 | .Display::getMdiIcon(ActionIcon::COMMENT, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Discuss this page')) |
||
1736 | .'</a>'; |
||
1737 | } |
||
1738 | |||
1739 | // History |
||
1740 | $actionsRight .= '<a href="'.$ctx['baseUrl'].'&action=history&title=' |
||
1741 | .api_htmlentities(urlencode($pageKey)).'" '.self::is_active_navigation_tab('history').'>' |
||
1742 | .Display::getMdiIcon(ActionIcon::HISTORY, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Show page history')) |
||
1743 | .'</a>'; |
||
1744 | |||
1745 | // Links |
||
1746 | $actionsRight .= '<a href="'.$ctx['baseUrl'].'&action=links&title=' |
||
1747 | .api_htmlentities(urlencode($pageKey)).'" '.self::is_active_navigation_tab('links').'>' |
||
1748 | .Display::getMdiIcon(ActionIcon::LINKS, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Links pages')) |
||
1749 | .'</a>'; |
||
1750 | |||
1751 | // Delete |
||
1752 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
1753 | $actionsRight .= '<a href="'.$ctx['baseUrl'].'&action=delete&title=' |
||
1754 | .api_htmlentities(urlencode($pageKey)).'"'.self::is_active_navigation_tab('delete').'>' |
||
1755 | .Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Delete')) |
||
1756 | .'</a>'; |
||
1757 | } |
||
1758 | |||
1759 | // Export |
||
1760 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
1761 | $actionsRight .= '<a href="'.$ctx['baseUrl'].'&action=export2doc&wiki_id='.$last->getIid().'">' |
||
1762 | .Display::getMdiIcon(ActionIcon::EXPORT_DOC, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Export')) |
||
1763 | .'</a>'; |
||
1764 | } |
||
1765 | $actionsRight .= '<a href="'.$ctx['baseUrl'].'&action=export_to_pdf&wiki_id='.$last->getIid().'">' |
||
1766 | .Display::getMdiIcon(ActionIcon::EXPORT_PDF, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Export')) |
||
1767 | .'</a>'; |
||
1768 | if (api_get_configuration_value('unoconv.binaries')) { |
||
1769 | $actionsRight .= '<a href="'.$ctx['baseUrl'].'&'.http_build_query(['action' => 'export_to_doc_file', 'id' => $last->getIid()]).'">' |
||
1770 | .Display::getMdiIcon(ActionIcon::EXPORT_DOC, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Export')) |
||
1771 | .'</a>'; |
||
1772 | } |
||
1773 | |||
1774 | |||
1775 | $actionsRight .= '<a href="#" onclick="javascript:(function(){var a=window.open(\'\',\'\',\'width=800,height=600\');a.document.open(\'text/html\');a.document.write($(\'#wikititle\').prop(\'outerHTML\'));a.document.write($(\'#wikicontent\').prop(\'outerHTML\'));a.document.close();a.print();})(); return false;">' |
||
1776 | .Display::getMdiIcon(ActionIcon::PRINT, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Print')) |
||
1777 | .'</a>'; |
||
1778 | } |
||
1779 | |||
1780 | // Classic top bar |
||
1781 | $contentHtml = self::v1ToolbarHtml($actionsLeft, $actionsRight); |
||
1782 | |||
1783 | // Link post-processing |
||
1784 | $pageWiki = self::detect_news_link($content); |
||
1785 | $pageWiki = self::detect_irc_link($pageWiki); |
||
1786 | $pageWiki = self::detect_ftp_link($pageWiki); |
||
1787 | $pageWiki = self::detect_mail_link($pageWiki); |
||
1788 | $pageWiki = self::detect_anchor_link($pageWiki); |
||
1789 | $pageWiki = self::detect_external_link($pageWiki); |
||
1790 | $pageWiki = self::make_wiki_link_clickable($pageWiki, $ctx['baseUrl']); |
||
1791 | |||
1792 | // Footer meta + categories |
||
1793 | $footerMeta = |
||
1794 | '<span>'.get_lang('Progress').': '.$pageProgress.'%</span> '. |
||
1795 | '<span>'.get_lang('Rating').': '.$pageScore.'</span> '. |
||
1796 | '<span>'.get_lang('Words').': '.self::word_count($content).'</span>'; |
||
1797 | |||
1798 | $categories = self::returnCategoriesBlock( |
||
1799 | (int)($last?->getIid() ?? 0), |
||
1800 | '<div class="wiki-catwrap">', |
||
1801 | '</div>' |
||
1802 | ); |
||
1803 | |||
1804 | // Classic shell + new helper classes |
||
1805 | $contentHtml .= |
||
1806 | '<div id="tool-wiki" class="wiki-root">'. |
||
1807 | '<div id="mainwiki" class="wiki-wrap">'. |
||
1808 | ' <div id="wikititle" class="wiki-card wiki-title"><h1>'.$pageTitle.'</h1></div>'. |
||
1809 | ' <div id="wikicontent" class="wiki-card wiki-prose">'.$pageWiki.'</div>'. |
||
1810 | ' <div id="wikifooter" class="wiki-card wiki-footer">'. |
||
1811 | ' <div class="meta">'.$footerMeta.'</div>'.$categories. |
||
1812 | ' </div>'. |
||
1813 | '</div>'. |
||
1814 | '</div>'; |
||
1815 | |||
1816 | return $contentHtml; |
||
1817 | } |
||
1818 | |||
1819 | private static function v1ToolbarHtml(string $left, string $right): string |
||
1820 | { |
||
1821 | if ($left === '' && $right === '') { |
||
1822 | return ''; |
||
1823 | } |
||
1824 | |||
1825 | return |
||
1826 | '<div class="wiki-actions" style="display:flex;align-items:center;gap:6px;padding:6px 8px;border:1px solid #ddd;border-radius:4px;background:#fff">'. |
||
1827 | ' <div class="wiki-actions-left" style="display:inline-flex;gap:6px">'.$left.'</div>'. |
||
1828 | ' <div class="wiki-actions-right" style="display:inline-flex;gap:6px;margin-left:auto">'.$right.'</div>'. |
||
1829 | '</div>'; |
||
1830 | } |
||
1831 | |||
1832 | /** Render category links of a page as search filters. */ |
||
1833 | private static function returnCategoriesBlock(int $wikiId, string $tagStart = '<div>', string $tagEnd = '</div>'): string |
||
1834 | { |
||
1835 | if (!self::categoriesEnabled() || $wikiId <= 0) { |
||
1836 | return ''; |
||
1837 | } |
||
1838 | |||
1839 | try { |
||
1840 | $em = Container::getEntityManager(); |
||
1841 | /** @var CWiki|null $wiki */ |
||
1842 | $wiki = $em->find(CWiki::class, $wikiId); |
||
1843 | if (!$wiki) { return ''; } |
||
1844 | } catch (\Throwable $e) { |
||
1845 | return ''; |
||
1846 | } |
||
1847 | |||
1848 | $baseUrl = self::ctx()['baseUrl']; |
||
1849 | |||
1850 | $links = []; |
||
1851 | foreach ($wiki->getCategories()->getValues() as $category) { |
||
1852 | /** @var CWikiCategory $category */ |
||
1853 | $urlParams = [ |
||
1854 | 'search_term' => isset($_GET['search_term']) ? Security::remove_XSS($_GET['search_term']) : '', |
||
1855 | 'SubmitWikiSearch' => '', |
||
1856 | '_qf__wiki_search' => '', |
||
1857 | 'action' => 'searchpages', |
||
1858 | 'categories' => ['' => $category->getId()], |
||
1859 | ]; |
||
1860 | $href = $baseUrl.'&'.http_build_query($urlParams); |
||
1861 | $label = api_htmlentities($category->getName()); |
||
1862 | $links[] = self::twCategoryPill($href, $label); |
||
1863 | } |
||
1864 | |||
1865 | if (empty($links)) { |
||
1866 | return ''; |
||
1867 | } |
||
1868 | |||
1869 | return $tagStart.implode('', $links).$tagEnd; |
||
1870 | } |
||
1871 | |||
1872 | /** Active class helper for toolbar tabs. */ |
||
1873 | public static function is_active_navigation_tab($paramwk) |
||
1874 | { |
||
1875 | if (isset($_GET['action']) && $_GET['action'] == $paramwk) { |
||
1876 | return ' class="active"'; |
||
1877 | } |
||
1878 | return ''; |
||
1879 | } |
||
1880 | |||
1881 | /** Return 1 if current user is subscribed to page notifications, else 0 (also processes toggles). */ |
||
1882 | public static function check_notify_page(string $reflink, ?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): int |
||
1883 | { |
||
1884 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
1885 | $conn = Container::getEntityManager()->getConnection(); |
||
1886 | |||
1887 | $cid = (int)$ctx['courseId']; |
||
1888 | $gid = (int)$ctx['groupId']; |
||
1889 | $sid = (int)$ctx['sessionId']; |
||
1890 | $uid = (int)api_get_user_id(); |
||
1891 | |||
1892 | $watchKey = 'watch:'.self::normalizeReflink($reflink); |
||
1893 | |||
1894 | $count = (int)$conn->fetchOne( |
||
1895 | 'SELECT COUNT(*) |
||
1896 | FROM c_wiki_mailcue |
||
1897 | WHERE c_id = :cid |
||
1898 | AND COALESCE(group_id,0) = :gid |
||
1899 | AND '.($sid > 0 ? 'COALESCE(session_id,0) = :sid' : 'COALESCE(session_id,0) = 0').' |
||
1900 | AND user_id = :uid |
||
1901 | AND type = :t', |
||
1902 | ['cid'=>$cid,'gid'=>$gid,'sid'=>$sid,'uid'=>$uid,'t'=>$watchKey] |
||
1903 | ); |
||
1904 | |||
1905 | return $count > 0 ? 1 : 0; |
||
1906 | } |
||
1907 | |||
1908 | /** Word count from HTML (UTF-8 safe). */ |
||
1909 | private static function word_count(string $html): int |
||
1910 | { |
||
1911 | $text = html_entity_decode(strip_tags($html), ENT_QUOTES, 'UTF-8'); |
||
1912 | $text = preg_replace('/\s+/u', ' ', trim($text)); |
||
1913 | if ($text === '') { |
||
1914 | return 0; |
||
1915 | } |
||
1916 | $tokens = preg_split('/\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY); |
||
1917 | return is_array($tokens) ? count($tokens) : 0; |
||
1918 | } |
||
1919 | |||
1920 | /** True if any row with this title exists in the context. */ |
||
1921 | public static function wiki_exist( |
||
1922 | string $reflink, |
||
1923 | ?int $courseId = null, |
||
1924 | ?int $sessionId = null, |
||
1925 | ?int $groupId = null |
||
1926 | ): bool { |
||
1927 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
1928 | $repo = self::repo(); |
||
1929 | |||
1930 | // Ensure canonicalization (Home/Main_Page → index, lowercase, etc.) |
||
1931 | $reflink = self::normalizeReflink($reflink); |
||
1932 | |||
1933 | $qb = $repo->createQueryBuilder('w') |
||
1934 | ->select('w.iid') |
||
1935 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
1936 | ->andWhere('w.reflink = :r')->setParameter('r', $reflink) |
||
1937 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
1938 | ->setMaxResults(1); |
||
1939 | |||
1940 | if ($ctx['sessionId'] > 0) { |
||
1941 | // In a session: it may exist in 0 (global) or in the current session |
||
1942 | $qb->andWhere('(COALESCE(w.sessionId,0) = 0 OR w.sessionId = :sid)') |
||
1943 | ->setParameter('sid', (int)$ctx['sessionId']); |
||
1944 | } else { |
||
1945 | $qb->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
1946 | } |
||
1947 | |||
1948 | return !empty($qb->getQuery()->getArrayResult()); |
||
1949 | } |
||
1950 | |||
1951 | /** Read/toggle global addlock; returns current value or null. */ |
||
1952 | public static function check_addnewpagelock(?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): ?int |
||
1953 | { |
||
1954 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
1955 | $em = Container::getEntityManager(); |
||
1956 | $repo = self::repo(); |
||
1957 | |||
1958 | /** @var CWiki|null $row */ |
||
1959 | $row = $repo->createQueryBuilder('w') |
||
1960 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
1961 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
1962 | ->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']) |
||
1963 | ->orderBy('w.version', 'ASC') |
||
1964 | ->setMaxResults(1) |
||
1965 | ->getQuery()->getOneOrNullResult(); |
||
1966 | |||
1967 | $status = $row ? (int)$row->getAddlock() : null; |
||
1968 | |||
1969 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
1970 | if (isset($_GET['actionpage'])) { |
||
1971 | if ($_GET['actionpage'] === 'lockaddnew' && $status === 1) { |
||
1972 | $status = 0; |
||
1973 | } elseif ($_GET['actionpage'] === 'unlockaddnew' && $status === 0) { |
||
1974 | $status = 1; |
||
1975 | } |
||
1976 | |||
1977 | $em->createQuery('UPDATE Chamilo\CourseBundle\Entity\CWiki w SET w.addlock = :v WHERE w.cId = :cid AND COALESCE(w.groupId,0) = :gid AND COALESCE(w.sessionId,0) = :sid') |
||
1978 | ->setParameter('v', $status) |
||
1979 | ->setParameter('cid', $ctx['courseId']) |
||
1980 | ->setParameter('gid', (int)$ctx['groupId']) |
||
1981 | ->setParameter('sid', (int)$ctx['sessionId']) |
||
1982 | ->execute(); |
||
1983 | |||
1984 | $row = $repo->createQueryBuilder('w') |
||
1985 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
1986 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
1987 | ->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']) |
||
1988 | ->orderBy('w.version', 'ASC') |
||
1989 | ->setMaxResults(1) |
||
1990 | ->getQuery()->getOneOrNullResult(); |
||
1991 | |||
1992 | return $row ? (int)$row->getAddlock() : null; |
||
1993 | } |
||
1994 | } |
||
1995 | |||
1996 | return $status; |
||
1997 | } |
||
1998 | |||
1999 | /** Read/toggle editlock by page (reflink); returns current status (0/1). */ |
||
2000 | public static function check_protect_page(string $page, ?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): int |
||
2001 | { |
||
2002 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
2003 | $em = Container::getEntityManager(); |
||
2004 | $repo = self::repo(); |
||
2005 | |||
2006 | /** @var CWiki|null $row */ |
||
2007 | $row = $repo->createQueryBuilder('w') |
||
2008 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
2009 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($page)) |
||
2010 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
2011 | ->orderBy('w.version', 'ASC') |
||
2012 | ->setMaxResults(1) |
||
2013 | ->getQuery()->getOneOrNullResult(); |
||
2014 | |||
2015 | if (!$row) { |
||
2016 | return 0; |
||
2017 | } |
||
2018 | |||
2019 | $status = (int)$row->getEditlock(); |
||
2020 | $pid = (int)$row->getPageId(); |
||
2021 | |||
2022 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
2023 | if (!empty($_GET['actionpage'])) { |
||
2024 | if ($_GET['actionpage'] === 'lock' && $status === 0) { |
||
2025 | $status = 1; |
||
2026 | } elseif ($_GET['actionpage'] === 'unlock' && $status === 1) { |
||
2027 | $status = 0; |
||
2028 | } |
||
2029 | |||
2030 | $em->createQuery('UPDATE Chamilo\CourseBundle\Entity\CWiki w SET w.editlock = :v WHERE w.cId = :cid AND w.pageId = :pid') |
||
2031 | ->setParameter('v', $status) |
||
2032 | ->setParameter('cid', $ctx['courseId']) |
||
2033 | ->setParameter('pid', $pid) |
||
2034 | ->execute(); |
||
2035 | |||
2036 | $row = $repo->createQueryBuilder('w') |
||
2037 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
2038 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($page)) |
||
2039 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
2040 | ->orderBy('w.version', 'ASC') |
||
2041 | ->setMaxResults(1) |
||
2042 | ->getQuery()->getOneOrNullResult(); |
||
2043 | } |
||
2044 | } |
||
2045 | |||
2046 | return (int)($row?->getEditlock() ?? 0); |
||
2047 | } |
||
2048 | |||
2049 | /** Read/toggle visibility by page (reflink); returns current status (0/1). */ |
||
2050 | public static function check_visibility_page(string $page, ?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): int |
||
2051 | { |
||
2052 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
2053 | $em = Container::getEntityManager(); |
||
2054 | $repo = self::repo(); |
||
2055 | |||
2056 | /** @var CWiki|null $row */ |
||
2057 | $row = $repo->createQueryBuilder('w') |
||
2058 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
2059 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($page)) |
||
2060 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
2061 | ->orderBy('w.version', 'ASC') |
||
2062 | ->setMaxResults(1) |
||
2063 | ->getQuery()->getOneOrNullResult(); |
||
2064 | |||
2065 | if (!$row) { |
||
2066 | return 0; |
||
2067 | } |
||
2068 | |||
2069 | $status = (int)$row->getVisibility(); |
||
2070 | |||
2071 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
2072 | if (!empty($_GET['actionpage'])) { |
||
2073 | if ($_GET['actionpage'] === 'visible' && $status === 0) { |
||
2074 | $status = 1; |
||
2075 | } elseif ($_GET['actionpage'] === 'invisible' && $status === 1) { |
||
2076 | $status = 0; |
||
2077 | } |
||
2078 | |||
2079 | $em->createQuery('UPDATE Chamilo\CourseBundle\Entity\CWiki w SET w.visibility = :v WHERE w.cId = :cid AND w.reflink = :r AND COALESCE(w.groupId,0) = :gid') |
||
2080 | ->setParameter('v', $status) |
||
2081 | ->setParameter('cid', $ctx['courseId']) |
||
2082 | ->setParameter('r', html_entity_decode($page)) |
||
2083 | ->setParameter('gid', (int)$ctx['groupId']) |
||
2084 | ->execute(); |
||
2085 | |||
2086 | $row = $repo->createQueryBuilder('w') |
||
2087 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
2088 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($page)) |
||
2089 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
2090 | ->orderBy('w.version', 'ASC') |
||
2091 | ->setMaxResults(1) |
||
2092 | ->getQuery()->getOneOrNullResult(); |
||
2093 | } |
||
2094 | } |
||
2095 | |||
2096 | return (int)($row?->getVisibility() ?? 1); |
||
2097 | } |
||
2098 | |||
2099 | private static function toDateTime(null|string|\DateTime $v): ?\DateTime |
||
2100 | { |
||
2101 | if ($v instanceof \DateTime) { |
||
2102 | return $v; |
||
2103 | } |
||
2104 | if (is_string($v) && $v !== '') { |
||
2105 | try { return new \DateTime($v); } catch (\Throwable) {} |
||
2106 | } |
||
2107 | return null; |
||
2108 | } |
||
2109 | |||
2110 | /** Extract [[wikilinks]] → space-separated normalized reflinks. */ |
||
2111 | private static function links_to(string $input): string |
||
2112 | { |
||
2113 | $parts = preg_split("/(\[\[|]])/", $input, -1, PREG_SPLIT_DELIM_CAPTURE); |
||
2114 | $out = []; |
||
2115 | foreach ($parts as $k => $v) { |
||
2116 | if (($parts[$k-1] ?? null) === '[[' && ($parts[$k+1] ?? null) === ']]') { |
||
2117 | if (api_strpos($v, '|') !== false) { |
||
2118 | [$link] = explode('|', $v, 2); |
||
2119 | $link = trim($link); |
||
2120 | } else { |
||
2121 | $link = trim($v); |
||
2122 | } |
||
2123 | $out[] = Database::escape_string(str_replace(' ', '_', $link)).' '; |
||
2124 | } |
||
2125 | } |
||
2126 | return implode($out); |
||
2127 | } |
||
2128 | |||
2129 | private static function detect_external_link(string $input): string |
||
2130 | { |
||
2131 | return str_replace('href=', 'class="wiki_link_ext" href=', $input); |
||
2132 | } |
||
2133 | private static function detect_anchor_link(string $input): string |
||
2134 | { |
||
2135 | return str_replace('href="#', 'class="wiki_anchor_link" href="#', $input); |
||
2136 | } |
||
2137 | private static function detect_mail_link(string $input): string |
||
2138 | { |
||
2139 | return str_replace('href="mailto', 'class="wiki_mail_link" href="mailto', $input); |
||
2140 | } |
||
2141 | private static function detect_ftp_link(string $input): string |
||
2142 | { |
||
2143 | return str_replace('href="ftp', 'class="wiki_ftp_link" href="ftp', $input); |
||
2144 | } |
||
2145 | private static function detect_news_link(string $input): string |
||
2146 | { |
||
2147 | return str_replace('href="news', 'class="wiki_news_link" href="news', $input); |
||
2148 | } |
||
2149 | private static function detect_irc_link(string $input): string |
||
2150 | { |
||
2151 | return str_replace('href="irc', 'class="wiki_irc_link" href="irc', $input); |
||
2152 | } |
||
2153 | |||
2154 | /** Convert [[Page|Title]] to <a> depending on existence. */ |
||
2155 | private static function make_wiki_link_clickable(string $input, string $baseUrl): string |
||
2156 | { |
||
2157 | $parts = preg_split("/(\[\[|]])/", $input, -1, PREG_SPLIT_DELIM_CAPTURE); |
||
2158 | |||
2159 | foreach ($parts as $k => $v) { |
||
2160 | if (($parts[$k-1] ?? null) === '[[' && ($parts[$k+1] ?? null) === ']]') { |
||
2161 | if (api_strpos($v, '|') !== false) { |
||
2162 | [$rawLink, $title] = explode('|', $v, 2); |
||
2163 | $rawLink = trim(strip_tags($rawLink)); |
||
2164 | $title = trim($title); |
||
2165 | } else { |
||
2166 | $rawLink = trim(strip_tags($v)); |
||
2167 | $title = trim($v); |
||
2168 | } |
||
2169 | |||
2170 | $reflink = self::normalizeReflink($rawLink); |
||
2171 | if (self::isMain($reflink)) { |
||
2172 | $title = self::displayTitleFor('index'); |
||
2173 | } |
||
2174 | |||
2175 | if (self::checktitle($reflink)) { |
||
2176 | $href = $baseUrl.'&action=showpage&title='.urlencode($reflink); |
||
2177 | $parts[$k] = '<a href="'.$href.'" class="wiki_link">'.$title.'</a>'; |
||
2178 | } else { |
||
2179 | $href = $baseUrl.'&action=addnew&title='.Security::remove_XSS(urlencode($reflink)); |
||
2180 | $parts[$k] = '<a href="'.$href.'" class="new_wiki_link">'.$title.'</a>'; |
||
2181 | } |
||
2182 | |||
2183 | unset($parts[$k-1], $parts[$k+1]); |
||
2184 | } |
||
2185 | } |
||
2186 | return implode('', $parts); |
||
2187 | } |
||
2188 | |||
2189 | private static function assignCategoriesToWiki(CWiki $wiki, array $categoriesIdList): void |
||
2190 | { |
||
2191 | if (!self::categoriesEnabled()) { |
||
2192 | return; |
||
2193 | } |
||
2194 | |||
2195 | $em = Container::getEntityManager(); |
||
2196 | |||
2197 | foreach ($categoriesIdList as $categoryId) { |
||
2198 | if (!$categoryId) { |
||
2199 | continue; |
||
2200 | } |
||
2201 | /** @var CWikiCategory|null $category */ |
||
2202 | $category = $em->find(CWikiCategory::class, (int)$categoryId); |
||
2203 | if ($category) { |
||
2204 | if (method_exists($wiki, 'getCategories') && !$wiki->getCategories()->contains($category)) { |
||
2205 | $wiki->addCategory($category); |
||
2206 | } else { |
||
2207 | $wiki->addCategory($category); |
||
2208 | } |
||
2209 | } |
||
2210 | } |
||
2211 | |||
2212 | $em->flush(); |
||
2213 | } |
||
2214 | |||
2215 | private static function twToolbarHtml(string $leftHtml, string $rightHtml = ''): string |
||
2216 | { |
||
2217 | $wrap = 'flex items-center gap-2 [&_a]:inline-flex [&_a]:items-center [&_a]:gap-2 [&_a]:rounded-lg [&_a]:border [&_a]:border-slate-200 [&_a]:bg-white [&_a]:px-3 [&_a]:py-1.5 [&_a]:text-sm [&_a]:font-medium [&_a]:text-slate-700 [&_a]:shadow-sm hover:[&_a]:shadow'; |
||
2218 | return '<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">'. |
||
2219 | '<div class="'.$wrap.'">'.$leftHtml.'</div>'. |
||
2220 | '<div class="'.$wrap.'">'.$rightHtml.'</div>'. |
||
2221 | '</div>'; |
||
2222 | } |
||
2223 | |||
2224 | private static function twPanel(string $body, string $title = '', string $footer = ''): string |
||
2225 | { |
||
2226 | $html = '<div class="rounded-2xl border border-slate-200 bg-white shadow-sm">'; |
||
2227 | if ($title !== '') { |
||
2228 | $html .= '<div class="border-b border-slate-200 px-5 py-4 text-lg font-semibold text-slate-800">'.$title.'</div>'; |
||
2229 | } |
||
2230 | $html .= '<div class="px-5 py-6 leading-relaxed text-slate-700">'.$body.'</div>'; |
||
2231 | if ($footer !== '') { |
||
2232 | $html .= '<div class="border-t border-slate-200 px-5 py-3 text-sm text-slate-600">'.$footer.'</div>'; |
||
2233 | } |
||
2234 | $html .= '</div>'; |
||
2235 | return $html; |
||
2236 | } |
||
2237 | |||
2238 | /** Category pill link */ |
||
2239 | private static function twCategoryPill(string $href, string $label): string |
||
2240 | { |
||
2241 | return '<a href="'.$href.'" class="inline-flex items-center rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-700 hover:bg-slate-200">'.$label.'</a>'; |
||
2242 | } |
||
2243 | |||
2244 | /** Convert DateTime|string|int|null to timestamp. */ |
||
2245 | private static function toTimestamp($v): int |
||
2246 | { |
||
2247 | if ($v instanceof \DateTimeInterface) { |
||
2248 | return $v->getTimestamp(); |
||
2249 | } |
||
2250 | if (is_int($v)) { |
||
2251 | return $v; |
||
2252 | } |
||
2253 | if (is_string($v) && $v !== '') { |
||
2254 | $t = strtotime($v); |
||
2255 | if ($t !== false) { |
||
2256 | return $t; |
||
2257 | } |
||
2258 | } |
||
2259 | return time(); |
||
2260 | } |
||
2261 | |||
2262 | public function display_new_wiki_form(): void |
||
2263 | { |
||
2264 | $ctx = self::ctx(); |
||
2265 | $url = $ctx['baseUrl'].'&'.http_build_query(['action' => 'addnew']); |
||
2266 | $form = new FormValidator('wiki_new', 'post', $url); |
||
2267 | |||
2268 | // Required title |
||
2269 | $form->addElement('text', 'title', get_lang('Title')); |
||
2270 | $form->addRule('title', get_lang('ThisFieldIsRequired'), 'required'); |
||
2271 | |||
2272 | // Editor and advanced fields (adds a hidden wpost_id inside if your setForm doesn’t) |
||
2273 | self::setForm($form); |
||
2274 | |||
2275 | // Ensure there is a wpost_id for double_post() |
||
2276 | if (!$form->elementExists('wpost_id')) { |
||
2277 | $form->addElement('hidden', 'wpost_id', api_get_unique_id()); |
||
2278 | } |
||
2279 | |||
2280 | // Prefill if ?title= is present |
||
2281 | $titleFromGet = isset($_GET['title']) ? htmlspecialchars_decode(Security::remove_XSS((string) $_GET['title'])) : ''; |
||
2282 | $form->setDefaults(['title' => $titleFromGet]); |
||
2283 | |||
2284 | // --- Process first (don’t output yet) --- |
||
2285 | if ($form->validate()) { |
||
2286 | $values = $form->exportValues(); |
||
2287 | |||
2288 | // Consistent dates (if provided) |
||
2289 | $toTs = static function ($v): ?int { |
||
2290 | if ($v instanceof \DateTimeInterface) { return $v->getTimestamp(); } |
||
2291 | if (is_string($v) && $v !== '') { return strtotime($v); } |
||
2292 | return null; |
||
2293 | }; |
||
2294 | $startTs = isset($values['startdate_assig']) ? $toTs($values['startdate_assig']) : null; |
||
2295 | $endTs = isset($values['enddate_assig']) ? $toTs($values['enddate_assig']) : null; |
||
2296 | |||
2297 | if ($startTs && $endTs && $startTs > $endTs) { |
||
2298 | Display::addFlash(Display::return_message(get_lang('EndDateCannotBeBeforeStartDate'), 'error', false)); |
||
2299 | // show the form again |
||
2300 | $form->display(); |
||
2301 | return; |
||
2302 | } |
||
2303 | |||
2304 | // Anti double-post (if wpost is missing, don’t block) |
||
2305 | if (isset($values['wpost_id']) && !self::double_post($values['wpost_id'])) { |
||
2306 | // Duplicate: go back without saving |
||
2307 | Display::addFlash(Display::return_message(get_lang('DuplicateSubmissionIgnored'), 'warning', false)); |
||
2308 | $form->display(); |
||
2309 | return; |
||
2310 | } |
||
2311 | |||
2312 | // If “assignment for all” => generate per user (if needed) |
||
2313 | if (!empty($values['assignment']) && (int)$values['assignment'] === 1) { |
||
2314 | // If your implementation needs it, keep it; otherwise omit for now. |
||
2315 | // self::auto_add_page_users($values); |
||
2316 | } |
||
2317 | |||
2318 | // Save: use our robust helper |
||
2319 | $msg = self::save_new_wiki($values); |
||
2320 | if ($msg === false) { |
||
2321 | Display::addFlash(Display::return_message(get_lang('NoWikiPageTitle'), 'error', false)); |
||
2322 | $form->display(); |
||
2323 | return; |
||
2324 | } |
||
2325 | |||
2326 | Display::addFlash(Display::return_message($msg, 'confirmation', false)); |
||
2327 | |||
2328 | // Redirect to the created page (no output beforehand) |
||
2329 | $wikiData = self::getWikiData(); |
||
2330 | $redirRef = self::normalizeReflink($wikiData['reflink'] ?? self::normalizeReflink($values['title'] ?? 'index')); |
||
2331 | $redirectUrl = $ctx['baseUrl'].'&'.http_build_query(['action' => 'showpage', 'title' => $redirRef]); |
||
2332 | header('Location: '.$redirectUrl); |
||
2333 | exit; |
||
2334 | } |
||
2335 | |||
2336 | // --- Show form (GET or invalid POST) --- |
||
2337 | $form->addButtonSave(get_lang('Save'), 'SaveWikiNew'); |
||
2338 | $form->display(); |
||
2339 | } |
||
2340 | |||
2341 | public function getHistory(): void |
||
2342 | { |
||
2343 | $page = (string) $this->page; |
||
2344 | |||
2345 | if (empty($_GET['title'])) { |
||
2346 | Display::addFlash(Display::return_message(get_lang('Must select a page'), 'error', false)); |
||
2347 | return; |
||
2348 | } |
||
2349 | |||
2350 | $ctx = self::ctx(); |
||
2351 | $repo = self::repo(); |
||
2352 | |||
2353 | // Latest version (for visibility/ownership) |
||
2354 | $qbLast = $repo->createQueryBuilder('w') |
||
2355 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
2356 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($page)) |
||
2357 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
2358 | ->orderBy('w.version', 'DESC')->setMaxResults(1); |
||
2359 | |||
2360 | if ($ctx['sessionId'] > 0) { |
||
2361 | $qbLast->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
2362 | } else { |
||
2363 | $qbLast->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
2364 | } |
||
2365 | |||
2366 | /** @var CWiki|null $last */ |
||
2367 | $last = $qbLast->getQuery()->getOneOrNullResult(); |
||
2368 | |||
2369 | $keyVisibility = $last?->getVisibility(); |
||
2370 | $keyAssignment = $last?->getAssignment(); |
||
2371 | $keyTitle = $last?->getTitle(); |
||
2372 | $keyUserId = $last?->getUserId(); |
||
2373 | |||
2374 | // Permissions |
||
2375 | $userId = api_get_user_id(); |
||
2376 | $canSee = |
||
2377 | $keyVisibility == 1 || |
||
2378 | api_is_allowed_to_edit(false, true) || |
||
2379 | api_is_platform_admin() || |
||
2380 | ($keyAssignment == 2 && $keyVisibility == 0 && $userId == $keyUserId); |
||
2381 | |||
2382 | if (!$canSee) { |
||
2383 | Display::addFlash(Display::return_message(get_lang('Not allowed'), 'error', false)); |
||
2384 | return; |
||
2385 | } |
||
2386 | |||
2387 | // All versions (DESC) |
||
2388 | $qbAll = $repo->createQueryBuilder('w') |
||
2389 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
2390 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($page)) |
||
2391 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
2392 | ->orderBy('w.version', 'DESC'); |
||
2393 | |||
2394 | if ($ctx['sessionId'] > 0) { |
||
2395 | $qbAll->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
2396 | } else { |
||
2397 | $qbAll->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
2398 | } |
||
2399 | |||
2400 | /** @var CWiki[] $versions */ |
||
2401 | $versions = $qbAll->getQuery()->getResult(); |
||
2402 | |||
2403 | // Assignment icon |
||
2404 | $icon = null; |
||
2405 | if ((int)$keyAssignment === 1) { |
||
2406 | $icon = Display::return_icon('wiki_assignment.png', get_lang('Assignment desc extra'), '', ICON_SIZE_SMALL); |
||
2407 | } elseif ((int)$keyAssignment === 2) { |
||
2408 | $icon = Display::return_icon('wiki_work.png', get_lang('Assignment work extra'), '', ICON_SIZE_SMALL); |
||
2409 | } |
||
2410 | |||
2411 | // View 1: pick two versions |
||
2412 | if (!isset($_POST['HistoryDifferences']) && !isset($_POST['HistoryDifferences2'])) { |
||
2413 | $title = (string) $_GET['title']; |
||
2414 | |||
2415 | echo '<div id="wikititle">'.($icon ? $icon.' ' : '').api_htmlentities($keyTitle ?? '').'</div>'; |
||
2416 | |||
2417 | $actionUrl = self::ctx()['baseUrl'].'&'.http_build_query(['action' => 'history', 'title' => api_htmlentities($title)]); |
||
2418 | echo '<form id="differences" method="POST" action="'.$actionUrl.'">'; |
||
2419 | echo '<ul style="list-style-type:none">'; |
||
2420 | echo '<br />'; |
||
2421 | echo '<button class="search" type="submit" name="HistoryDifferences" value="HistoryDifferences">'.get_lang('Show differences').' '.get_lang('Lines diff').'</button> '; |
||
2422 | echo '<button class="search" type="submit" name="HistoryDifferences2" value="HistoryDifferences2">'.get_lang('Show differences').' '.get_lang('Words diff').'</button>'; |
||
2423 | echo '<br /><br />'; |
||
2424 | |||
2425 | $total = count($versions); |
||
2426 | foreach ($versions as $i => $w) { |
||
2427 | $ui = api_get_user_info((int)$w->getUserId()); |
||
2428 | $username = $ui ? api_htmlentities(sprintf(get_lang('LoginX'), $ui['username']), ENT_QUOTES) : get_lang('Anonymous'); |
||
2429 | |||
2430 | $oldStyle = ($i === 0) ? 'style="visibility:hidden;"' : ''; |
||
2431 | $newChecked = ($i === 0) ? ' checked' : ''; |
||
2432 | $newStyle = ($i === $total -1) ? 'style="visibility:hidden;"' : ''; |
||
2433 | $oldChecked = ($i === 1) ? ' checked' : ''; |
||
2434 | |||
2435 | $dtime = $w->getDtime() ? $w->getDtime()->format('Y-m-d H:i:s') : ''; |
||
2436 | $comment = (string) $w->getComment(); |
||
2437 | $commentShort = $comment !== '' ? api_htmlentities(api_substr($comment, 0, 100)) : '---'; |
||
2438 | $needsDots = (api_strlen($comment) > 100) ? '...' : ''; |
||
2439 | |||
2440 | echo '<li style="margin-bottom:5px">'; |
||
2441 | echo '<input name="old" value="'.$w->getIid().'" type="radio" '.$oldStyle.' '.$oldChecked.'/> '; |
||
2442 | echo '<input name="new" value="'.$w->getIid().'" type="radio" '.$newStyle.' '.$newChecked.'/> '; |
||
2443 | echo '<a href="'.self::ctx()['baseUrl'].'&action=showpage&title='.api_htmlentities(urlencode($page)).'&view='.$w->getIid().'">'.$dtime.'</a> '; |
||
2444 | echo '('.get_lang('Version').' '.(int)$w->getVersion().') '; |
||
2445 | echo get_lang('By').' '; |
||
2446 | if ($ui !== false) { |
||
2447 | echo UserManager::getUserProfileLink($ui); |
||
2448 | } else { |
||
2449 | echo $username.' ('.api_htmlentities((string)$w->getUserIp()).')'; |
||
2450 | } |
||
2451 | echo ' ( '.get_lang('Progress').': '.api_htmlentities((string)$w->getProgress()).'%, '; |
||
2452 | echo get_lang('Comments').': '.$commentShort.$needsDots.' )'; |
||
2453 | echo '</li>'; |
||
2454 | } |
||
2455 | |||
2456 | echo '<br />'; |
||
2457 | echo '<button class="search" type="submit" name="HistoryDifferences" value="HistoryDifferences">'.get_lang('Show differences').' '.get_lang('Lines diff').'</button> '; |
||
2458 | echo '<button class="search" type="submit" name="HistoryDifferences2" value="HistoryDifferences2">'.get_lang('Show differences').' '.get_lang('Words diff').'</button>'; |
||
2459 | echo '</ul></form>'; |
||
2460 | |||
2461 | return; |
||
2462 | } |
||
2463 | |||
2464 | // View 2: differences between two versions |
||
2465 | $versionOld = null; |
||
2466 | if (!empty($_POST['old'])) { |
||
2467 | $versionOld = $repo->find((int) $_POST['old']); |
||
2468 | } |
||
2469 | $versionNew = $repo->find((int) $_POST['new']); |
||
2470 | |||
2471 | $oldTime = $versionOld?->getDtime()?->format('Y-m-d H:i:s'); |
||
2472 | $oldContent = $versionOld?->getContent(); |
||
2473 | |||
2474 | if (isset($_POST['HistoryDifferences'])) { |
||
2475 | include 'diff.inc.php'; |
||
2476 | |||
2477 | echo '<div id="wikititle">'.api_htmlentities((string)$versionNew->getTitle()).' |
||
2478 | <font size="-2"><i>('.get_lang('Differences new').'</i> |
||
2479 | <font style="background-color:#aaaaaa">'.$versionNew->getDtime()?->format('Y-m-d H:i:s').'</font> |
||
2480 | <i>'.get_lang('Differences old').'</i> |
||
2481 | <font style="background-color:#aaaaaa">'.$oldTime.'</font>) |
||
2482 | '.get_lang('Legend').': |
||
2483 | <span class="diffAdded">'.get_lang('Wiki diff added line').'</span> |
||
2484 | <span class="diffDeleted">'.get_lang('Wiki diff deleted line').'</span> |
||
2485 | <span class="diffMoved">'.get_lang('Wiki diff moved line').'</span></font> |
||
2486 | </div>'; |
||
2487 | |||
2488 | echo '<table>'.diff((string)$oldContent, (string)$versionNew->getContent(), true, 'format_table_line').'</table>'; |
||
2489 | echo '<br /><strong>'.get_lang('Legend').'</strong><div class="diff">'; |
||
2490 | echo '<table><tr><td></td><td>'; |
||
2491 | echo '<span class="diffEqual">'.get_lang('Wiki diff unchanged line').'</span><br />'; |
||
2492 | echo '<span class="diffAdded">'.get_lang('Wiki diff added line').'</span><br />'; |
||
2493 | echo '<span class="diffDeleted">'.get_lang('Wiki diff deleted line').'</span><br />'; |
||
2494 | echo '<span class="diffMoved">'.get_lang('Wiki diff moved line').'</span><br />'; |
||
2495 | echo '</td></tr></table>'; |
||
2496 | } |
||
2497 | |||
2498 | if (isset($_POST['HistoryDifferences2'])) { |
||
2499 | $lines1 = [strip_tags((string)$oldContent)]; |
||
2500 | $lines2 = [strip_tags((string)$versionNew->getContent())]; |
||
2501 | $diff = new Text_Diff($lines1, $lines2); |
||
2502 | $renderer = new Text_Diff_Renderer_inline(); |
||
2503 | |||
2504 | echo '<style>del{background:#fcc}ins{background:#cfc}</style>'.$renderer->render($diff); |
||
2505 | echo '<br /><strong>'.get_lang('Legend').'</strong><div class="diff">'; |
||
2506 | echo '<table><tr><td></td><td>'; |
||
2507 | echo '<span class="diffAddedTex">'.get_lang('Wiki diff added tex').'</span><br />'; |
||
2508 | echo '<span class="diffDeletedTex">'.get_lang('Wiki diff deleted tex').'</span><br />'; |
||
2509 | echo '</td></tr></table>'; |
||
2510 | } |
||
2511 | } |
||
2512 | |||
2513 | public function getLastWikiData($refLink): array |
||
2514 | { |
||
2515 | $ctx = self::ctx(); |
||
2516 | $em = Container::getEntityManager(); |
||
2517 | $repo = $em->getRepository(CWiki::class); |
||
2518 | |||
2519 | $qb = $repo->createQueryBuilder('w') |
||
2520 | ->andWhere('w.cId = :cid')->setParameter('cid', (int)$ctx['courseId']) |
||
2521 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode((string)$refLink)) |
||
2522 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
2523 | ->orderBy('w.version', 'DESC') |
||
2524 | ->setMaxResults(1); |
||
2525 | |||
2526 | if ((int)$ctx['sessionId'] > 0) { |
||
2527 | $qb->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
2528 | } else { |
||
2529 | $qb->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
2530 | } |
||
2531 | |||
2532 | /** @var CWiki|null $w */ |
||
2533 | $w = $qb->getQuery()->getOneOrNullResult(); |
||
2534 | if (!$w) { |
||
2535 | return []; |
||
2536 | } |
||
2537 | |||
2538 | // Map to legacy-like keys |
||
2539 | return [ |
||
2540 | 'iid' => $w->getIid(), |
||
2541 | 'page_id' => $w->getPageId(), |
||
2542 | 'reflink' => $w->getReflink(), |
||
2543 | 'title' => $w->getTitle(), |
||
2544 | 'content' => $w->getContent(), |
||
2545 | 'user_id' => $w->getUserId(), |
||
2546 | 'group_id' => $w->getGroupId(), |
||
2547 | 'dtime' => $w->getDtime(), |
||
2548 | 'addlock' => $w->getAddlock(), |
||
2549 | 'editlock' => $w->getEditlock(), |
||
2550 | 'visibility' => $w->getVisibility(), |
||
2551 | 'assignment' => $w->getAssignment(), |
||
2552 | 'comment' => $w->getComment(), |
||
2553 | 'progress' => $w->getProgress(), |
||
2554 | 'score' => $w->getScore(), |
||
2555 | 'version' => $w->getVersion(), |
||
2556 | 'is_editing' => $w->getIsEditing(), |
||
2557 | 'time_edit' => $w->getTimeEdit(), |
||
2558 | 'hits' => $w->getHits(), |
||
2559 | 'linksto' => $w->getLinksto(), |
||
2560 | 'tag' => $w->getTag(), |
||
2561 | 'user_ip' => $w->getUserIp(), |
||
2562 | 'session_id' => $w->getSessionId(), |
||
2563 | ]; |
||
2564 | } |
||
2565 | |||
2566 | public function auto_add_page_users($values): void |
||
2684 | } |
||
2685 | } |
||
2686 | } |
||
2687 | |||
2688 | public function restore_wikipage( |
||
2689 | $r_page_id, |
||
2690 | $r_reflink, |
||
2691 | $r_title, |
||
2692 | $r_content, |
||
2693 | $r_group_id, |
||
2694 | $r_assignment, |
||
2695 | $r_progress, |
||
2696 | $c_version, |
||
2697 | $r_version, |
||
2698 | $r_linksto |
||
2699 | ) { |
||
2700 | $ctx = self::ctx(); |
||
2701 | $_course = $ctx['courseInfo']; |
||
2702 | $r_user_id = api_get_user_id(); |
||
2703 | $r_dtime = api_get_utc_datetime(); // string for mail |
||
2704 | $dTime = api_get_utc_datetime(null, false, true); // DateTime (entity) |
||
2705 | |||
2706 | $r_version = ((int)$r_version) + 1; |
||
2707 | $r_comment = get_lang('Restored from version').': '.$c_version; |
||
2708 | $groupInfo = GroupManager::get_group_properties((int)$r_group_id); |
||
2709 | |||
2710 | $em = Container::getEntityManager(); |
||
2711 | |||
2712 | $newWiki = (new CWiki()) |
||
2713 | ->setCId((int)$ctx['courseId']) |
||
2714 | ->setPageId((int)$r_page_id) |
||
2715 | ->setReflink((string)$r_reflink) |
||
2716 | ->setTitle((string)$r_title) |
||
2717 | ->setContent((string)$r_content) |
||
2718 | ->setUserId((int)$r_user_id) |
||
2719 | ->setGroupId((int)$r_group_id) |
||
2720 | ->setDtime($dTime) |
||
2721 | ->setAssignment((int)$r_assignment) |
||
2722 | ->setComment((string)$r_comment) |
||
2723 | ->setProgress((int)$r_progress) |
||
2724 | ->setVersion((int)$r_version) |
||
2725 | ->setLinksto((string)$r_linksto) |
||
2726 | ->setUserIp(api_get_real_ip()) |
||
2727 | ->setSessionId((int)$ctx['sessionId']) |
||
2728 | ->setAddlock(0)->setEditlock(0)->setVisibility(0) |
||
2729 | ->setAddlockDisc(0)->setVisibilityDisc(0)->setRatinglockDisc(0) |
||
2730 | ->setIsEditing(0)->setTag(''); |
||
2731 | |||
2732 | $newWiki->setParent($ctx['course']); |
||
2733 | $newWiki->setCreator(api_get_user_entity()); |
||
2734 | $groupEntity = $ctx['groupId'] ? api_get_group_entity($ctx['groupId']) : null; |
||
2735 | $newWiki->addCourseLink($ctx['course'], $ctx['session'], $groupEntity); |
||
2736 | |||
2737 | $em->persist($newWiki); |
||
2738 | $em->flush(); |
||
2739 | |||
2740 | api_item_property_update($_course, 'wiki', $newWiki->getIid(), 'WikiAdded', api_get_user_id(), $groupInfo); |
||
2741 | self::check_emailcue((string)$r_reflink, 'P', $r_dtime, (int)$r_user_id); |
||
2742 | |||
2743 | return get_lang('Page restored'); |
||
2744 | } |
||
2745 | |||
2746 | public function restorePage() |
||
2747 | { |
||
2748 | $ctx = self::ctx(); |
||
2749 | $userId = api_get_user_id(); |
||
2750 | $current_row = $this->getWikiData(); |
||
2751 | $last_row = $this->getLastWikiData($this->page); |
||
2752 | |||
2753 | if (empty($last_row)) { |
||
2754 | return false; |
||
2755 | } |
||
2756 | |||
2757 | $PassEdit = false; |
||
2758 | |||
2759 | // Only teacher/admin can edit index or assignment-teacher pages |
||
2760 | if ( |
||
2761 | (($current_row['reflink'] ?? '') === 'index' || |
||
2762 | ($current_row['reflink'] ?? '') === '' || |
||
2763 | ((int)$current_row['assignment'] === 1)) && |
||
2764 | (!api_is_allowed_to_edit(false, true) && (int)$ctx['groupId'] === 0) |
||
2765 | ) { |
||
2766 | Display::addFlash(Display::return_message(get_lang('Only edit pages course manager'), 'normal', false)); |
||
2767 | return false; |
||
2768 | } |
||
2769 | |||
2770 | // Group wiki |
||
2771 | if ((int)($current_row['group_id'] ?? 0) !== 0) { |
||
2772 | $groupInfo = GroupManager::get_group_properties((int)$ctx['groupId']); |
||
2773 | if (api_is_allowed_to_edit(false, true) || |
||
2774 | api_is_platform_admin() || |
||
2775 | GroupManager::is_user_in_group($userId, $groupInfo) || |
||
2776 | api_is_allowed_in_course() |
||
2777 | ) { |
||
2778 | $PassEdit = true; |
||
2779 | } else { |
||
2780 | Display::addFlash(Display::return_message(get_lang('Only edit pages group members'), 'normal', false)); |
||
2781 | $PassEdit = false; |
||
2782 | } |
||
2783 | } else { |
||
2784 | $PassEdit = true; |
||
2785 | } |
||
2786 | |||
2787 | // Assignment rules |
||
2788 | if ((int)$current_row['assignment'] === 1) { |
||
2789 | Display::addFlash(Display::return_message(get_lang('Edit assignment warning'), 'normal', false)); |
||
2790 | } elseif ((int)$current_row['assignment'] === 2) { |
||
2791 | if ((int)$userId !== (int)($current_row['user_id'] ?? 0)) { |
||
2792 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
2793 | $PassEdit = true; |
||
2794 | } else { |
||
2795 | Display::addFlash(Display::return_message(get_lang('Lock by teacher'), 'normal', false)); |
||
2796 | $PassEdit = false; |
||
2797 | } |
||
2798 | } |
||
2799 | } |
||
2800 | |||
2801 | if (!$PassEdit) { |
||
2802 | return false; |
||
2803 | } |
||
2804 | |||
2805 | // Edit lock |
||
2806 | if ((int)($current_row['editlock'] ?? 0) === 1 && |
||
2807 | (!api_is_allowed_to_edit(false, true) || !api_is_platform_admin()) |
||
2808 | ) { |
||
2809 | Display::addFlash(Display::return_message(get_lang('Locked'), 'normal', false)); |
||
2810 | return false; |
||
2811 | } |
||
2812 | |||
2813 | // Concurrency |
||
2814 | $isEditing = (int)($last_row['is_editing'] ?? 0); |
||
2815 | if ($isEditing !== 0 && $isEditing !== (int)$userId) { |
||
2816 | $timeVal = $last_row['time_edit'] ?? null; |
||
2817 | $ts = $timeVal instanceof \DateTimeInterface ? $timeVal->getTimestamp() : (is_string($timeVal) ? strtotime($timeVal) : time()); |
||
2818 | $elapsed = time() - $ts; |
||
2819 | $rest = max(0, 1200 - $elapsed); // 20 min |
||
2820 | |||
2821 | $userinfo = api_get_user_info($isEditing); |
||
2822 | $msg = get_lang('This page is begin edited by').' <a href='.$userinfo['profile_url'].'>'. |
||
2823 | Display::tag('span', $userinfo['complete_name_with_username']).'</a> '. |
||
2824 | get_lang('This page is begin edited try later').' '.date("i", $rest).' '.get_lang('Min minutes'); |
||
2825 | |||
2826 | Display::addFlash(Display::return_message($msg, 'normal', false)); |
||
2827 | return false; |
||
2828 | } |
||
2829 | |||
2830 | // Restore (create new version with previous content) |
||
2831 | Display::addFlash( |
||
2832 | Display::return_message( |
||
2833 | self::restore_wikipage( |
||
2834 | (int)$current_row['page_id'], |
||
2835 | (string)$current_row['reflink'], |
||
2836 | (string)$current_row['title'], |
||
2837 | (string)$current_row['content'], |
||
2838 | (int)$current_row['group_id'], |
||
2839 | (int)$current_row['assignment'], |
||
2840 | (int)$current_row['progress'], |
||
2841 | (int)$current_row['version'], |
||
2842 | (int)$last_row['version'], |
||
2843 | (string)$current_row['linksto'] |
||
2844 | ).': '.Display::url( |
||
2845 | api_htmlentities((string)$last_row['title']), |
||
2846 | $ctx['baseUrl'].'&'.http_build_query(['action' => 'showpage', 'title' => api_htmlentities((string)$last_row['reflink'])]) |
||
2847 | ), |
||
2848 | 'confirmation', |
||
2849 | false |
||
2850 | ) |
||
2851 | ); |
||
2852 | |||
2853 | return true; |
||
2854 | } |
||
2855 | |||
2856 | public function handleAction(string $action): void |
||
2857 | { |
||
2858 | $page = $this->page; |
||
2859 | $ctx = self::ctx(); |
||
2860 | $url = $ctx['baseUrl']; |
||
2861 | |||
2862 | // Local renderer for breadcrumb + stylish pills (uniform look) |
||
2863 | $renderStatsHeader = function (string $activeKey) use ($url) { |
||
2864 | static $wikiHdrCssInjected = false; |
||
2865 | |||
2866 | // Labels (use existing lang keys) |
||
2867 | $items = [ |
||
2868 | 'mactiveusers' => get_lang('Most active users'), |
||
2869 | 'mvisited' => get_lang('Most visited pages'), |
||
2870 | 'mostchanged' => get_lang('Most changed pages'), |
||
2871 | 'orphaned' => get_lang('Orphaned pages'), |
||
2872 | 'wanted' => get_lang('Wanted pages'), |
||
2873 | 'mostlinked' => get_lang('Most linked pages'), |
||
2874 | 'statistics' => get_lang('Statistics'), |
||
2875 | ]; |
||
2876 | |||
2877 | // Simple icon map |
||
2878 | $icons = [ |
||
2879 | 'mactiveusers' => \Chamilo\CoreBundle\Enums\ActionIcon::STAR, |
||
2880 | 'mvisited' => \Chamilo\CoreBundle\Enums\ActionIcon::HISTORY, |
||
2881 | 'mostchanged' => \Chamilo\CoreBundle\Enums\ActionIcon::REFRESH, |
||
2882 | 'orphaned' => \Chamilo\CoreBundle\Enums\ActionIcon::LINKS, |
||
2883 | 'wanted' => \Chamilo\CoreBundle\Enums\ActionIcon::SEARCH, |
||
2884 | 'mostlinked' => \Chamilo\CoreBundle\Enums\ActionIcon::LINKS, |
||
2885 | 'statistics' => \Chamilo\CoreBundle\Enums\ActionIcon::INFORMATION, |
||
2886 | ]; |
||
2887 | |||
2888 | if (!$wikiHdrCssInjected) { |
||
2889 | $wikiHdrCssInjected = true; |
||
2890 | } |
||
2891 | |||
2892 | $activeLabel = api_htmlentities($items[$activeKey] ?? ''); |
||
2893 | |||
2894 | echo '<div class="wiki-pills">'; |
||
2895 | foreach ($items as $key => $label) { |
||
2896 | $isActive = ($key === $activeKey); |
||
2897 | $href = $url.'&action='.$key; |
||
2898 | $icon = Display::getMdiIcon($icons[$key] ?? \Chamilo\CoreBundle\Enums\ActionIcon::VIEW_DETAILS, |
||
2899 | 'mdi-inline', null, ICON_SIZE_SMALL, $label); |
||
2900 | echo '<a class="pill'.($isActive ? ' active' : '').'" href="'.$href.'"'. |
||
2901 | ($isActive ? ' aria-current="page"' : '').'>'.$icon.'<span>'.api_htmlentities($label).'</span></a>'; |
||
2902 | } |
||
2903 | echo '</div>'; |
||
2904 | }; |
||
2905 | |||
2906 | switch ($action) { |
||
2907 | case 'export_to_pdf': |
||
2908 | if (isset($_GET['wiki_id'])) { |
||
2909 | self::export_to_pdf($_GET['wiki_id'], api_get_course_id()); |
||
2910 | break; |
||
2911 | } |
||
2912 | break; |
||
2913 | |||
2914 | case 'export2doc': |
||
2915 | if (isset($_GET['wiki_id'])) { |
||
2916 | $export2doc = self::export2doc($_GET['wiki_id']); |
||
2917 | if ($export2doc) { |
||
2918 | Display::addFlash( |
||
2919 | Display::return_message( |
||
2920 | get_lang('ThePageHasBeenExportedToDocArea'), |
||
2921 | 'confirmation', |
||
2922 | false |
||
2923 | ) |
||
2924 | ); |
||
2925 | } |
||
2926 | } |
||
2927 | break; |
||
2928 | |||
2929 | case 'restorepage': |
||
2930 | self::restorePage(); |
||
2931 | break; |
||
2932 | |||
2933 | case 'more': |
||
2934 | self::getStatsTable(); |
||
2935 | break; |
||
2936 | |||
2937 | case 'statistics': |
||
2938 | $renderStatsHeader('statistics'); |
||
2939 | self::getStats(); |
||
2940 | break; |
||
2941 | |||
2942 | case 'mactiveusers': |
||
2943 | $renderStatsHeader('mactiveusers'); |
||
2944 | self::getActiveUsers($action); |
||
2945 | break; |
||
2946 | |||
2947 | case 'usercontrib': |
||
2948 | self::getUserContributions((int)($_GET['user_id'] ?? 0), $action); |
||
2949 | break; |
||
2950 | |||
2951 | case 'mostchanged': |
||
2952 | $renderStatsHeader('mostchanged'); |
||
2953 | $this->getMostChangedPages($action); |
||
2954 | break; |
||
2955 | |||
2956 | case 'mvisited': |
||
2957 | $renderStatsHeader('mvisited'); |
||
2958 | self::getMostVisited(); |
||
2959 | break; |
||
2960 | |||
2961 | case 'wanted': |
||
2962 | $renderStatsHeader('wanted'); |
||
2963 | $this->getWantedPages(); |
||
2964 | break; |
||
2965 | |||
2966 | case 'orphaned': |
||
2967 | $renderStatsHeader('orphaned'); |
||
2968 | self::getOrphaned(); |
||
2969 | break; |
||
2970 | |||
2971 | case 'mostlinked': |
||
2972 | $renderStatsHeader('mostlinked'); |
||
2973 | self::getMostLinked(); |
||
2974 | break; |
||
2975 | |||
2976 | case 'delete': |
||
2977 | $this->deletePageWarning(); |
||
2978 | break; |
||
2979 | |||
2980 | case 'deletewiki': |
||
2981 | echo '<nav aria-label="breadcrumb" class="wiki-breadcrumb"> |
||
2982 | <ol class="breadcrumb"> |
||
2983 | <li class="breadcrumb-item"><a href="'. |
||
2984 | $this->url(['action' => 'showpage', 'title' => 'index']).'">'.get_lang('Wiki').'</a></li> |
||
2985 | <li class="breadcrumb-item active" aria-current="page">'.get_lang('Delete').'</li> |
||
2986 | </ol> |
||
2987 | </nav>'; |
||
2988 | |||
2989 | echo '<div class="actions">'.get_lang('Delete wiki').'</div>'; |
||
2990 | |||
2991 | $canDelete = api_is_allowed_to_edit(false, true) || api_is_platform_admin(); |
||
2992 | $confirmedPost = isset($_POST['confirm_delete']) && $_POST['confirm_delete'] === '1'; |
||
2993 | |||
2994 | if (!$canDelete) { |
||
2995 | echo Display::return_message(get_lang('Only admin can delete the wiki'), 'error', false); |
||
2996 | break; |
||
2997 | } |
||
2998 | |||
2999 | if (!$confirmedPost) { |
||
3000 | $actionUrl = $this->url(['action' => 'deletewiki']); |
||
3001 | $msg = '<p>'.get_lang('ConfirmDeleteWiki').'</p>'; |
||
3002 | $msg .= '<form method="post" action="'.Security::remove_XSS($actionUrl).'" style="display:inline-block;margin-right:1rem;">'; |
||
3003 | $msg .= '<input type="hidden" name="confirm_delete" value="1">'; |
||
3004 | $msg .= '<button type="submit" class="btn btn-danger">'.get_lang('Yes').'</button>'; |
||
3005 | $msg .= ' <a class="btn btn-default" href="'.$this->url().'">'.get_lang('No').'</a>'; |
||
3006 | $msg .= '</form>'; |
||
3007 | |||
3008 | echo Display::return_message($msg, 'warning', false); |
||
3009 | break; |
||
3010 | } |
||
3011 | |||
3012 | $summary = self::delete_wiki(); |
||
3013 | |||
3014 | Display::addFlash(Display::return_message($summary, 'confirmation', false)); |
||
3015 | header('Location: '.$this->url()); |
||
3016 | exit; |
||
3017 | |||
3018 | case 'searchpages': |
||
3019 | self::getSearchPages($action); |
||
3020 | break; |
||
3021 | |||
3022 | case 'links': |
||
3023 | self::getLinks($page); |
||
3024 | break; |
||
3025 | |||
3026 | case 'addnew': |
||
3027 | if (0 != api_get_session_id() && api_is_allowed_to_session_edit(false, true) == false) { |
||
3028 | api_not_allowed(); |
||
3029 | } |
||
3030 | |||
3031 | echo '<div class="actions">'.get_lang('Add new page').'</div>'; |
||
3032 | echo '<br/>'; |
||
3033 | |||
3034 | // Show the tip ONLY if "index" is missing or has no real content |
||
3035 | try { |
||
3036 | $ctx = self::ctx(); |
||
3037 | $repo = self::repo(); |
||
3038 | |||
3039 | $qb = $repo->createQueryBuilder('w') |
||
3040 | ->andWhere('w.cId = :cid')->setParameter('cid', (int)$ctx['courseId']) |
||
3041 | ->andWhere('w.reflink = :r')->setParameter('r', 'index') |
||
3042 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
3043 | ->orderBy('w.version', 'DESC') |
||
3044 | ->setMaxResults(1); |
||
3045 | |||
3046 | if ((int)$ctx['sessionId'] > 0) { |
||
3047 | $qb->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
3048 | } else { |
||
3049 | $qb->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
3050 | } |
||
3051 | |||
3052 | /** @var CWiki|null $indexRow */ |
||
3053 | $indexRow = $qb->getQuery()->getOneOrNullResult(); |
||
3054 | |||
3055 | $indexIsEmpty = true; |
||
3056 | if ($indexRow) { |
||
3057 | // Decode entities, strip HTML, normalize NBSP and whitespace |
||
3058 | $raw = (string)$indexRow->getContent(); |
||
3059 | $text = api_html_entity_decode($raw, ENT_QUOTES, api_get_system_encoding()); |
||
3060 | $text = strip_tags($text); |
||
3061 | $text = preg_replace('/\xC2\xA0/u', ' ', $text); // NBSP |
||
3062 | $text = trim(preg_replace('/\s+/u', ' ', $text)); |
||
3063 | |||
3064 | // Consider empty if no letters/digits (handles <p> </p>, placeholders, etc.) |
||
3065 | $indexIsEmpty = ($text === '' || !preg_match('/[\p{L}\p{N}]/u', $text)); |
||
3066 | } |
||
3067 | |||
3068 | if ($indexIsEmpty && (api_is_allowed_to_edit(false, true) || api_is_platform_admin() || api_is_allowed_in_course())) { |
||
3069 | Display::addFlash( |
||
3070 | Display::return_message(get_lang('Go and edit main page'), 'normal', false) |
||
3071 | ); |
||
3072 | } |
||
3073 | } catch (\Throwable $e) { |
||
3074 | // If something goes wrong checking content, fail-safe to *not* nag the user. |
||
3075 | } |
||
3076 | |||
3077 | // Lock for creating new pages (only affects NON-editors) |
||
3078 | if (self::check_addnewpagelock() == 0 |
||
3079 | && (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) |
||
3080 | ) { |
||
3081 | Display::addFlash( |
||
3082 | Display::return_message(get_lang('Add pages locked'), 'error', false) |
||
3083 | ); |
||
3084 | break; |
||
3085 | } |
||
3086 | |||
3087 | self::display_new_wiki_form(); |
||
3088 | break; |
||
3089 | |||
3090 | case 'show': |
||
3091 | case 'showpage': |
||
3092 | $requested = self::normalizeReflink($_GET['title'] ?? null); |
||
3093 | echo self::display_wiki_entry($requested, $requested); |
||
3094 | break; |
||
3095 | |||
3096 | case 'edit': |
||
3097 | self::editPage(); |
||
3098 | break; |
||
3099 | |||
3100 | case 'history': |
||
3101 | self::getHistory(); |
||
3102 | break; |
||
3103 | |||
3104 | case 'recentchanges': |
||
3105 | self::recentChanges($page, $action); |
||
3106 | break; |
||
3107 | |||
3108 | case 'allpages': |
||
3109 | self::allPages($action); |
||
3110 | break; |
||
3111 | |||
3112 | case 'discuss': |
||
3113 | self::getDiscuss($page); |
||
3114 | break; |
||
3115 | |||
3116 | case 'export_to_doc_file': |
||
3117 | self::exportTo($_GET['id'], 'odt'); |
||
3118 | exit; |
||
|
|||
3119 | break; |
||
3120 | |||
3121 | case 'category': |
||
3122 | $this->addCategory(); |
||
3123 | break; |
||
3124 | |||
3125 | case 'delete_category': |
||
3126 | $this->deleteCategory(); |
||
3127 | break; |
||
3128 | } |
||
3129 | } |
||
3130 | |||
3131 | public function showLinks(string $page): void |
||
3132 | { |
||
3133 | $ctx = self::ctx(); |
||
3134 | $repo = self::repo(); |
||
3135 | |||
3136 | // Basic guard: this action expects a title in the request (legacy behavior) |
||
3137 | if (empty($_GET['title'])) { |
||
3138 | Display::addFlash(Display::return_message(get_lang('Must select a page'), 'error', false)); |
||
3139 | return; |
||
3140 | } |
||
3141 | |||
3142 | // Canonical reflink for the requested page (logic uses 'index' as main page) |
||
3143 | $reflink = self::normalizeReflink($page); |
||
3144 | |||
3145 | // Token used inside "linksto" (UI-friendly, localized with underscores when index) |
||
3146 | $needleToken = self::displayTokenFor($reflink); |
||
3147 | |||
3148 | // --- Header block: title + assignment icon (use first version as anchor) --- |
||
3149 | /** @var CWiki|null $first */ |
||
3150 | $first = $repo->createQueryBuilder('w') |
||
3151 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
3152 | ->andWhere('w.reflink = :r')->setParameter('r', $reflink) |
||
3153 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
3154 | ->orderBy('w.version', 'ASC') |
||
3155 | ->setMaxResults(1) |
||
3156 | ->getQuery()->getOneOrNullResult(); |
||
3157 | |||
3158 | if (!$first) { |
||
3159 | Display::addFlash(Display::return_message(get_lang('Must select a page'), 'error', false)); |
||
3160 | return; |
||
3161 | } |
||
3162 | |||
3163 | $assignIcon = ''; |
||
3164 | if ((int)$first->getAssignment() === 1) { |
||
3165 | $assignIcon = Display::getMdiIcon(ActionIcon::WIKI_ASSIGNMENT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Assignment desc')); |
||
3166 | } elseif ((int)$first->getAssignment() === 2) { |
||
3167 | $assignIcon = Display::getMdiIcon(ActionIcon::WIKI_WORK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Assignment work')); |
||
3168 | } |
||
3169 | |||
3170 | echo '<div id="wikititle">'.get_lang('Links pages from').": $assignIcon ". |
||
3171 | Display::url( |
||
3172 | api_htmlentities($first->getTitle()), |
||
3173 | $ctx['baseUrl'].'&'.http_build_query(['action' => 'showpage', 'title' => api_htmlentities($reflink)]) |
||
3174 | ).'</div>'; |
||
3175 | |||
3176 | // --- Query: latest version per page that *may* link to $needleToken --- |
||
3177 | $qb = $repo->createQueryBuilder('w') |
||
3178 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
3179 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
3180 | ->andWhere($ctx['sessionId'] > 0 ? '(COALESCE(w.sessionId,0) IN (0, :sid))' : 'COALESCE(w.sessionId,0) = 0') |
||
3181 | ->setParameter('sid', (int)$ctx['sessionId']) |
||
3182 | ->andWhere('w.linksto LIKE :needle')->setParameter('needle', '%'.$needleToken.'%') |
||
3183 | ->andWhere('w.version = ( |
||
3184 | SELECT MAX(w2.version) FROM '.CWiki::class.' w2 |
||
3185 | WHERE w2.cId = w.cId |
||
3186 | AND w2.pageId = w.pageId |
||
3187 | AND COALESCE(w2.groupId,0) = :gid2 |
||
3188 | AND '.($ctx['sessionId'] > 0 |
||
3189 | ? '(COALESCE(w2.sessionId,0) IN (0, :sid2))' |
||
3190 | : 'COALESCE(w2.sessionId,0) = 0').' |
||
3191 | )') |
||
3192 | ->setParameter('gid2', (int)$ctx['groupId']) |
||
3193 | ->setParameter('sid2', (int)$ctx['sessionId']); |
||
3194 | |||
3195 | // Visibility gate for students |
||
3196 | if (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) { |
||
3197 | $qb->andWhere('w.visibility = 1'); |
||
3198 | } |
||
3199 | |||
3200 | /** @var CWiki[] $candidates */ |
||
3201 | $candidates = $qb->getQuery()->getResult(); |
||
3202 | |||
3203 | // --- Precise token filter: ensure space-delimited match in "linksto" --- |
||
3204 | $items = []; |
||
3205 | foreach ($candidates as $obj) { |
||
3206 | $tokens = preg_split('/\s+/', trim((string)$obj->getLinksto())) ?: []; |
||
3207 | if (in_array($needleToken, $tokens, true)) { |
||
3208 | $items[] = $obj; |
||
3209 | } |
||
3210 | } |
||
3211 | |||
3212 | if (!$items) { |
||
3213 | echo self::twPanel('<em>'.get_lang('No results').'</em>', get_lang('Links pages')); |
||
3214 | return; |
||
3215 | } |
||
3216 | |||
3217 | // --- Render simple table --- |
||
3218 | $rowsHtml = ''; |
||
3219 | foreach ($items as $obj) { |
||
3220 | $ui = api_get_user_info((int)$obj->getUserId()); |
||
3221 | $authorCell = $ui |
||
3222 | ? UserManager::getUserProfileLink($ui) |
||
3223 | : get_lang('Anonymous').' ('.$obj->getUserIp().')'; |
||
3224 | |||
3225 | $icon = ''; |
||
3226 | if ((int)$obj->getAssignment() === 1) { |
||
3227 | $icon = Display::getMdiIcon(ActionIcon::WIKI_ASSIGNMENT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Assignment desc')); |
||
3228 | } elseif ((int)$obj->getAssignment() === 2) { |
||
3229 | $icon = Display::getMdiIcon(ActionIcon::WIKI_WORK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Assignment work')); |
||
3230 | } |
||
3231 | |||
3232 | $when = $obj->getDtime() ? api_get_local_time($obj->getDtime()) : ''; |
||
3233 | $rowsHtml .= '<tr>'. |
||
3234 | '<td style="width:30px">'.$icon.'</td>'. |
||
3235 | '<td>'.Display::url( |
||
3236 | api_htmlentities($obj->getTitle()), |
||
3237 | $ctx['baseUrl'].'&'.http_build_query(['action' => 'showpage', 'title' => api_htmlentities($obj->getReflink())]) |
||
3238 | ).'</td>'. |
||
3239 | '<td>'.$authorCell.'</td>'. |
||
3240 | '<td>'.$when.'</td>'. |
||
3241 | '</tr>'; |
||
3242 | } |
||
3243 | |||
3244 | $table = |
||
3245 | '<table class="table table-striped">'. |
||
3246 | '<thead><tr>'. |
||
3247 | '<th>'.get_lang('Type').'</th>'. |
||
3248 | '<th>'.get_lang('Title').'</th>'. |
||
3249 | '<th>'.get_lang('Author').'</th>'. |
||
3250 | '<th>'.get_lang('Date').'</th>'. |
||
3251 | '</tr></thead>'. |
||
3252 | '<tbody>'.$rowsHtml.'</tbody>'. |
||
3253 | '</table>'; |
||
3254 | |||
3255 | echo self::twPanel($table, get_lang('LinksPages')); |
||
3256 | } |
||
3257 | |||
3258 | public function showDiscuss(string $page): void |
||
3259 | { |
||
3260 | $ctx = self::ctx(); |
||
3261 | $em = Container::getEntityManager(); |
||
3262 | $repo = self::repo(); |
||
3263 | |||
3264 | if ($ctx['sessionId'] !== 0 && api_is_allowed_to_session_edit(false, true) === false) { |
||
3265 | api_not_allowed(); |
||
3266 | } |
||
3267 | |||
3268 | if (empty($_GET['title'])) { |
||
3269 | Display::addFlash(Display::return_message(get_lang('Must select a page'), 'error', false)); |
||
3270 | return; |
||
3271 | } |
||
3272 | |||
3273 | // FIRST and LAST version (to get properties and page_id) |
||
3274 | /** @var CWiki|null $first */ |
||
3275 | $first = $repo->createQueryBuilder('w') |
||
3276 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
3277 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($page)) |
||
3278 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
3279 | ->orderBy('w.version', 'ASC') |
||
3280 | ->setMaxResults(1) |
||
3281 | ->getQuery()->getOneOrNullResult(); |
||
3282 | |||
3283 | if (!$first) { |
||
3284 | Display::addFlash(Display::return_message(get_lang('Discuss not available'), 'normal', false)); |
||
3285 | return; |
||
3286 | } |
||
3287 | |||
3288 | $qbLast = $repo->createQueryBuilder('w') |
||
3289 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
3290 | ->andWhere('w.pageId = :pid')->setParameter('pid', (int)$first->getPageId()) |
||
3291 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
3292 | ->orderBy('w.version', 'DESC') |
||
3293 | ->setMaxResults(1); |
||
3294 | |||
3295 | if ($ctx['sessionId'] > 0) { |
||
3296 | $qbLast->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
3297 | } else { |
||
3298 | $qbLast->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
3299 | } |
||
3300 | |||
3301 | /** @var CWiki|null $last */ |
||
3302 | $last = $qbLast->getQuery()->getOneOrNullResult(); |
||
3303 | if (!$last) { |
||
3304 | Display::addFlash(Display::return_message(get_lang('Discuss not available'), 'normal', false)); |
||
3305 | return; |
||
3306 | } |
||
3307 | |||
3308 | // Visibility gate for discussions (like legacy) |
||
3309 | $canSeeDiscuss = |
||
3310 | ((int)$last->getVisibilityDisc() === 1) || |
||
3311 | api_is_allowed_to_edit(false, true) || |
||
3312 | api_is_platform_admin() || |
||
3313 | ((int)$last->getAssignment() === 2 && (int)$last->getVisibilityDisc() === 0 && api_get_user_id() === (int)$last->getUserId()); |
||
3314 | |||
3315 | if (!$canSeeDiscuss) { |
||
3316 | Display::addFlash(Display::return_message(get_lang('LockByTeacher'), 'warning', false)); |
||
3317 | return; |
||
3318 | } |
||
3319 | |||
3320 | // Process toggles (lock/unlock/visibility/rating/notify) |
||
3321 | $lockLabel = ''; |
||
3322 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
3323 | $discLocked = (self::check_addlock_discuss($page) === 1); |
||
3324 | $lockLabel = $discLocked |
||
3325 | ? Display::getMdiIcon(ActionIcon::UNLOCK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Unlock')) |
||
3326 | : Display::getMdiIcon(ActionIcon::LOCK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Lock')); |
||
3327 | |||
3328 | $visIcon = (self::check_visibility_discuss($page) === 1) |
||
3329 | ? Display::getMdiIcon(ActionIcon::VISIBLE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Hide')) |
||
3330 | : Display::getMdiIcon(ActionIcon::INVISIBLE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Show')); |
||
3331 | |||
3332 | $rateIcon = (self::check_ratinglock_discuss($page) === 1) |
||
3333 | ? Display::getMdiIcon(ActionIcon::STAR, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Unlock')) |
||
3334 | : Display::getMdiIcon(ActionIcon::STAR_OUTLINE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Lock')); |
||
3335 | |||
3336 | echo '<div class="flex gap-2 justify-end">'. |
||
3337 | '<a href="'.$ctx['baseUrl'].'&action=discuss&actionpage='.( $discLocked ? 'unlockdisc' : 'lockdisc' ).'&title='.api_htmlentities(urlencode($page)).'">'.$lockLabel.'</a>'. |
||
3338 | '<a href="'.$ctx['baseUrl'].'&action=discuss&actionpage='.( self::check_visibility_discuss($page) ? 'hidedisc' : 'showdisc' ).'&title='.api_htmlentities(urlencode($page)).'">'.$visIcon.'</a>'. |
||
3339 | '<a href="'.$ctx['baseUrl'].'&action=discuss&actionpage='.( self::check_ratinglock_discuss($page) ? 'unlockrating' : 'lockrating' ).'&title='.api_htmlentities(urlencode($page)).'">'.$rateIcon.'</a>'. |
||
3340 | '</div>'; |
||
3341 | } |
||
3342 | |||
3343 | // Notify toggle (course-scope watchers; reuses page-level method) |
||
3344 | $isWatching = (self::check_notify_page($page) === 1); |
||
3345 | $notifyIcon = $isWatching |
||
3346 | ? Display::getMdiIcon(ActionIcon::SEND_SINGLE_EMAIL, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Cancel')) |
||
3347 | : Display::getMdiIcon(ActionIcon::NOTIFY_OFF, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Notify me')); |
||
3348 | echo '<div class="flex gap-2 justify-end">'. |
||
3349 | '<a href="'.$ctx['baseUrl'].'&action=discuss&actionpage='.( $isWatching ? 'unlocknotify' : 'locknotify' ).'&title='.api_htmlentities(urlencode($page)).'">'.$notifyIcon.'</a>'. |
||
3350 | '</div>'; |
||
3351 | |||
3352 | // Header (title + last editor/time) |
||
3353 | $lastInfo = $last->getUserId() ? api_get_user_info((int)$last->getUserId()) : false; |
||
3354 | $metaRight = ''; |
||
3355 | if ($lastInfo !== false) { |
||
3356 | $metaRight = ' ('.get_lang('The latest version was edited by').' '.UserManager::getUserProfileLink($lastInfo).' '.api_get_local_time($last->getDtime()?->format('Y-m-d H:i:s')).')'; |
||
3357 | } |
||
3358 | |||
3359 | $assignIcon = ''; |
||
3360 | if ((int)$last->getAssignment() === 1) { |
||
3361 | $assignIcon = Display::getMdiIcon(ActionIcon::WIKI_ASSIGNMENT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Assignment desc')); |
||
3362 | } elseif ((int)$last->getAssignment() === 2) { |
||
3363 | $assignIcon = Display::getMdiIcon(ActionIcon::WIKI_WORK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Assignment work')); |
||
3364 | } |
||
3365 | |||
3366 | echo '<div id="wikititle">'.$assignIcon.' '.api_htmlentities($last->getTitle()).$metaRight.'</div>'; |
||
3367 | |||
3368 | // Comment form (only if not locked or user is teacher/admin) |
||
3369 | $discLocked = ((int)$last->getAddlockDisc() === 1); |
||
3370 | $canPost = !$discLocked || api_is_allowed_to_edit(false, true) || api_is_platform_admin(); |
||
3371 | |||
3372 | if ($canPost) { |
||
3373 | $ratingAllowed = ((int)$last->getRatinglockDisc() === 1) || api_is_allowed_to_edit(false, true) || api_is_platform_admin(); |
||
3374 | $ratingSelect = $ratingAllowed |
||
3375 | ? '<select name="rating" id="rating" class="form-control">'. |
||
3376 | '<option value="-" selected>-</option>'. |
||
3377 | implode('', array_map(static fn($n) => '<option value="'.$n.'">'.$n.'</option>', range(0,10))). |
||
3378 | '</select>' |
||
3379 | : '<input type="hidden" name="rating" value="-">'; |
||
3380 | |||
3381 | $actionUrl = $ctx['baseUrl'].'&action=discuss&title='.api_htmlentities(urlencode($page)); |
||
3382 | echo '<div class="panel panel-default"><div class="panel-body">'. |
||
3383 | '<form method="post" action="'.$actionUrl.'" class="form-horizontal">'. |
||
3384 | '<input type="hidden" name="wpost_id" value="'.api_get_unique_id().'">'. |
||
3385 | '<div class="form-group">'. |
||
3386 | '<label class="col-sm-2 control-label">'.get_lang('Comments').':</label>'. |
||
3387 | '<div class="col-sm-10"><textarea class="form-control" name="comment" cols="80" rows="5" id="comment"></textarea></div>'. |
||
3388 | '</div>'. |
||
3389 | '<div class="form-group">'. |
||
3390 | '<label class="col-sm-2 control-label">'.get_lang('Rating').':</label>'. |
||
3391 | '<div class="col-sm-10">'.$ratingSelect.'</div>'. |
||
3392 | '</div>'. |
||
3393 | '<div class="form-group">'. |
||
3394 | '<div class="col-sm-offset-2 col-sm-10">'. |
||
3395 | '<button class="btn btn--primary" type="submit" name="Submit">'.get_lang('Send').'</button>'. |
||
3396 | '</div>'. |
||
3397 | '</div>'. |
||
3398 | '</form>'. |
||
3399 | '</div></div>'; |
||
3400 | } |
||
3401 | |||
3402 | // Handle POST (add comment) |
||
3403 | if (isset($_POST['Submit']) && self::double_post($_POST['wpost_id'] ?? '')) { |
||
3404 | $comment = (string)($_POST['comment'] ?? ''); |
||
3405 | $scoreIn = (string)($_POST['rating'] ?? '-'); |
||
3406 | $score = $scoreIn !== '-' ? max(0, min(10, (int)$scoreIn)) : null; |
||
3407 | |||
3408 | $disc = new CWikiDiscuss(); |
||
3409 | $disc |
||
3410 | ->setCId($ctx['courseId']) |
||
3411 | ->setPublicationId((int)$last->getPageId()) |
||
3412 | ->setUsercId(api_get_user_id()) |
||
3413 | ->setComment($comment) |
||
3414 | ->setPScore($scoreIn !== '-' ? $score : null) |
||
3415 | ->setDtime(api_get_utc_datetime(null, false, true)); |
||
3416 | |||
3417 | $em->persist($disc); |
||
3418 | $em->flush(); |
||
3419 | |||
3420 | self::check_emailcue((int)$last->getIid(), 'D', api_get_utc_datetime(), api_get_user_id()); |
||
3421 | |||
3422 | header('Location: '.$ctx['baseUrl'].'&action=discuss&title='.api_htmlentities(urlencode($page))); |
||
3423 | exit; |
||
3424 | } |
||
3425 | |||
3426 | echo '<hr noshade size="1">'; |
||
3427 | |||
3428 | // Load comments |
||
3429 | $discRepo = $em->getRepository(CWikiDiscuss::class); |
||
3430 | $reviews = $discRepo->createQueryBuilder('d') |
||
3431 | ->andWhere('d.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
3432 | ->andWhere('d.publicationId = :pid')->setParameter('pid', (int)$last->getPageId()) |
||
3433 | ->orderBy('d.iid', 'DESC') |
||
3434 | ->getQuery()->getResult(); |
||
3435 | |||
3436 | $countAll = count($reviews); |
||
3437 | $scored = array_values(array_filter($reviews, static fn($r) => $r->getPScore() !== null)); |
||
3438 | $countScore = count($scored); |
||
3439 | $avg = $countScore > 0 ? round(array_sum(array_map(static fn($r) => (int)$r->getPScore(), $scored)) / $countScore, 2) : 0.0; |
||
3440 | |||
3441 | echo get_lang('Num comments').': '.$countAll.' - '.get_lang('Num comments score').': '.$countScore.' - '.get_lang('Rating media').': '.$avg; |
||
3442 | |||
3443 | // Persist average into wiki.score (fits integer nullable; we save rounded int) |
||
3444 | $last->setScore((int)round($avg)); |
||
3445 | $em->flush(); |
||
3446 | |||
3447 | echo '<hr noshade size="1">'; |
||
3448 | |||
3449 | foreach ($reviews as $r) { |
||
3450 | $ui = api_get_user_info((int)$r->getUsercId()); |
||
3451 | $role = ($ui && (string)$ui['status'] === '5') ? get_lang('Student') : get_lang('Teacher'); |
||
3452 | $name = $ui ? $ui['complete_name'] : get_lang('Anonymous'); |
||
3453 | $avatar = $ui && !empty($ui['avatar']) ? $ui['avatar'] : UserManager::getUserPicture((int)$r->getUsercId()); |
||
3454 | $profile = $ui ? UserManager::getUserProfileLink($ui) : api_htmlentities($name); |
||
3455 | |||
3456 | $score = $r->getPScore(); |
||
3457 | $scoreText = ($score === null) ? '-' : (string)$score; |
||
3458 | |||
3459 | echo '<p><table>'. |
||
3460 | '<tr>'. |
||
3461 | '<td rowspan="2"><img src="'.api_htmlentities($avatar).'" alt="'.api_htmlentities($name).'" width="40" height="50" /></td>'. |
||
3462 | '<td style="color:#999">'.$profile.' ('.$role.') '.api_get_local_time($r->getDtime()?->format('Y-m-d H:i:s')).' - '.get_lang('Rating').': '.$scoreText.'</td>'. |
||
3463 | '</tr>'. |
||
3464 | '<tr>'. |
||
3465 | '<td>'.api_htmlentities((string)$r->getComment()).'</td>'. |
||
3466 | '</tr>'. |
||
3467 | '</table></p>'; |
||
3468 | } |
||
3469 | } |
||
3470 | |||
3471 | public static function check_addlock_discuss(string $page, ?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): int |
||
3472 | { |
||
3473 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
3474 | $em = Container::getEntityManager(); |
||
3475 | $repo = self::repo(); |
||
3476 | |||
3477 | /** @var CWiki|null $row */ |
||
3478 | $row = $repo->createQueryBuilder('w') |
||
3479 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
3480 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($page)) |
||
3481 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
3482 | ->orderBy('w.version', 'ASC') |
||
3483 | ->setMaxResults(1) |
||
3484 | ->getQuery()->getOneOrNullResult(); |
||
3485 | |||
3486 | if (!$row) { return 0; } |
||
3487 | |||
3488 | $status = (int)$row->getAddlockDisc(); |
||
3489 | $pid = (int)$row->getPageId(); |
||
3490 | |||
3491 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
3492 | $act = (string)($_GET['actionpage'] ?? ''); |
||
3493 | if ($act === 'lockdisc' && $status === 0) { $status = 1; } |
||
3494 | if ($act === 'unlockdisc' && $status === 1) { $status = 0; } |
||
3495 | |||
3496 | $em->createQuery('UPDATE '.CWiki::class.' w SET w.addlockDisc = :v WHERE w.cId = :cid AND w.pageId = :pid') |
||
3497 | ->setParameter('v', $status) |
||
3498 | ->setParameter('cid', $ctx['courseId']) |
||
3499 | ->setParameter('pid', $pid) |
||
3500 | ->execute(); |
||
3501 | |||
3502 | $row = $repo->createQueryBuilder('w') |
||
3503 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
3504 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($page)) |
||
3505 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
3506 | ->orderBy('w.version', 'ASC') |
||
3507 | ->setMaxResults(1) |
||
3508 | ->getQuery()->getOneOrNullResult(); |
||
3509 | } |
||
3510 | |||
3511 | return (int)($row?->getAddlockDisc() ?? 0); |
||
3512 | } |
||
3513 | |||
3514 | public static function check_visibility_discuss(string $page, ?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): int |
||
3515 | { |
||
3516 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
3517 | $em = Container::getEntityManager(); |
||
3518 | $repo = self::repo(); |
||
3519 | |||
3520 | /** @var CWiki|null $row */ |
||
3521 | $row = $repo->createQueryBuilder('w') |
||
3522 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
3523 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($page)) |
||
3524 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
3525 | ->orderBy('w.version', 'ASC') |
||
3526 | ->setMaxResults(1) |
||
3527 | ->getQuery()->getOneOrNullResult(); |
||
3528 | |||
3529 | if (!$row) { return 0; } |
||
3530 | |||
3531 | $status = (int)$row->getVisibilityDisc(); |
||
3532 | $pid = (int)$row->getPageId(); |
||
3533 | |||
3534 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
3535 | $act = (string)($_GET['actionpage'] ?? ''); |
||
3536 | if ($act === 'showdisc' && $status === 0) { $status = 1; } |
||
3537 | if ($act === 'hidedisc' && $status === 1) { $status = 0; } |
||
3538 | |||
3539 | $em->createQuery('UPDATE '.CWiki::class.' w SET w.visibilityDisc = :v WHERE w.cId = :cid AND w.pageId = :pid') |
||
3540 | ->setParameter('v', $status) |
||
3541 | ->setParameter('cid', $ctx['courseId']) |
||
3542 | ->setParameter('pid', $pid) |
||
3543 | ->execute(); |
||
3544 | |||
3545 | $row = $repo->createQueryBuilder('w') |
||
3546 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
3547 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($page)) |
||
3548 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
3549 | ->orderBy('w.version', 'ASC') |
||
3550 | ->setMaxResults(1) |
||
3551 | ->getQuery()->getOneOrNullResult(); |
||
3552 | } |
||
3553 | |||
3554 | return (int)($row?->getVisibilityDisc() ?? 1); |
||
3555 | } |
||
3556 | |||
3557 | public static function check_ratinglock_discuss(string $page, ?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): int |
||
3558 | { |
||
3559 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
3560 | $em = Container::getEntityManager(); |
||
3561 | $repo = self::repo(); |
||
3562 | |||
3563 | /** @var CWiki|null $row */ |
||
3564 | $row = $repo->createQueryBuilder('w') |
||
3565 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
3566 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($page)) |
||
3567 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
3568 | ->orderBy('w.version', 'ASC') |
||
3569 | ->setMaxResults(1) |
||
3570 | ->getQuery()->getOneOrNullResult(); |
||
3571 | |||
3572 | if (!$row) { return 0; } |
||
3573 | |||
3574 | $status = (int)$row->getRatinglockDisc(); |
||
3575 | $pid = (int)$row->getPageId(); |
||
3576 | |||
3577 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
3578 | $act = (string)($_GET['actionpage'] ?? ''); |
||
3579 | if ($act === 'lockrating' && $status === 1) { $status = 0; } |
||
3580 | if ($act === 'unlockrating' && $status === 0) { $status = 1; } |
||
3581 | |||
3582 | $em->createQuery('UPDATE '.CWiki::class.' w SET w.ratinglockDisc = :v WHERE w.cId = :cid AND w.pageId = :pid') |
||
3583 | ->setParameter('v', $status) |
||
3584 | ->setParameter('cid', $ctx['courseId']) |
||
3585 | ->setParameter('pid', $pid) |
||
3586 | ->execute(); |
||
3587 | |||
3588 | $row = $repo->createQueryBuilder('w') |
||
3589 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
3590 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($page)) |
||
3591 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
3592 | ->orderBy('w.version', 'ASC') |
||
3593 | ->setMaxResults(1) |
||
3594 | ->getQuery()->getOneOrNullResult(); |
||
3595 | } |
||
3596 | |||
3597 | return (int)($row?->getRatinglockDisc() ?? 1); |
||
3598 | } |
||
3599 | |||
3600 | public function deletePageWarning(): void |
||
3601 | { |
||
3602 | $ctx = self::ctx(); |
||
3603 | $repo = self::repo(); |
||
3604 | $em = Container::getEntityManager(); |
||
3605 | |||
3606 | // Permissions |
||
3607 | if (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) { |
||
3608 | Display::addFlash(Display::return_message(get_lang('OnlyAdminDeletePageWiki'), 'normal', false)); |
||
3609 | return; |
||
3610 | } |
||
3611 | |||
3612 | // Page to delete |
||
3613 | $pageRaw = $_GET['title'] ?? ''; |
||
3614 | $page = self::normalizeReflink($pageRaw); |
||
3615 | if ($page === '') { |
||
3616 | Display::addFlash(Display::return_message(get_lang('MustSelectPage'), 'error', false)); |
||
3617 | header('Location: '.$ctx['baseUrl'].'&action=allpages'); |
||
3618 | exit; |
||
3619 | } |
||
3620 | |||
3621 | // Resolve first version (to get page_id) within this context |
||
3622 | $qbFirst = $repo->createQueryBuilder('w') |
||
3623 | ->andWhere('w.cId = :cid')->setParameter('cid', (int)$ctx['courseId']) |
||
3624 | ->andWhere('w.reflink = :r')->setParameter('r', $page) |
||
3625 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
3626 | ->orderBy('w.version', 'ASC') |
||
3627 | ->setMaxResults(1); |
||
3628 | |||
3629 | if ((int)$ctx['sessionId'] > 0) { |
||
3630 | $qbFirst->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
3631 | } else { |
||
3632 | $qbFirst->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
3633 | } |
||
3634 | |||
3635 | /** @var CWiki|null $first */ |
||
3636 | $first = $qbFirst->getQuery()->getOneOrNullResult(); |
||
3637 | if (!$first || !(int)$first->getPageId()) { |
||
3638 | Display::addFlash(Display::return_message(get_lang('WikiPageNotFound'), 'error', false)); |
||
3639 | header('Location: '.$ctx['baseUrl'].'&action=allpages'); |
||
3640 | exit; |
||
3641 | } |
||
3642 | |||
3643 | $niceName = self::displayTitleFor($page, $first->getTitle()); |
||
3644 | |||
3645 | // Warn if deleting the main (index) page |
||
3646 | if ($page === 'index') { |
||
3647 | Display::addFlash(Display::return_message(get_lang('WarningDeleteMainPage'), 'warning', false)); |
||
3648 | } |
||
3649 | |||
3650 | // Confirmation? |
||
3651 | $confirmed = |
||
3652 | (isset($_POST['confirm_delete']) && $_POST['confirm_delete'] === '1') || |
||
3653 | (isset($_GET['delete']) && ($_GET['delete'] === 'yes' || $_GET['delete'] === '1')); |
||
3654 | |||
3655 | if ($confirmed) { |
||
3656 | // Delete by reflink inside current context |
||
3657 | $ok = $this->deletePageByReflink($page, (int)$ctx['courseId'], (int)$ctx['sessionId'], (int)$ctx['groupId']); |
||
3658 | |||
3659 | if ($ok) { |
||
3660 | Display::addFlash( |
||
3661 | Display::return_message(get_lang('WikiPageDeleted'), 'confirmation', false) |
||
3662 | ); |
||
3663 | } else { |
||
3664 | Display::addFlash( |
||
3665 | Display::return_message(get_lang('DeleteFailed'), 'error', false) |
||
3666 | ); |
||
3667 | } |
||
3668 | |||
3669 | header('Location: '.$ctx['baseUrl'].'&action=allpages'); |
||
3670 | exit; |
||
3671 | } |
||
3672 | |||
3673 | $postUrl = $this->url(['action' => 'delete', 'title' => $page]); |
||
3674 | |||
3675 | $msg = '<p>'.sprintf(get_lang('Are you sure you want to delete this page and its history?'), '<b>'.api_htmlentities($niceName).'</b>').'</p>'; |
||
3676 | $msg .= '<form method="post" action="'.Security::remove_XSS($postUrl).'" style="display:inline-block;margin-right:1rem;">'; |
||
3677 | $msg .= '<input type="hidden" name="confirm_delete" value="1">'; |
||
3678 | $msg .= '<button type="submit" class="btn btn-danger">'.get_lang('Yes').'</button>'; |
||
3679 | $msg .= ' <a class="btn btn-default" href="'.$ctx['baseUrl'].'&action=allpages">'.get_lang('No').'</a>'; |
||
3680 | $msg .= '</form>'; |
||
3681 | |||
3682 | echo Display::return_message($msg, 'warning', false); |
||
3683 | } |
||
3684 | |||
3685 | private function deletePageByReflink( |
||
3686 | string $reflink, |
||
3687 | ?int $courseId = null, |
||
3688 | ?int $sessionId = null, |
||
3689 | ?int $groupId = null |
||
3690 | ): bool { |
||
3691 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
3692 | $em = Container::getEntityManager(); |
||
3693 | $repo = self::repo(); |
||
3694 | |||
3695 | /** @var CWiki|null $first */ |
||
3696 | $first = $repo->createQueryBuilder('w') |
||
3697 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
3698 | ->andWhere('w.reflink = :r')->setParameter('r', html_entity_decode($reflink)) |
||
3699 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
3700 | ->orderBy('w.version', 'ASC') |
||
3701 | ->setMaxResults(1) |
||
3702 | ->getQuery()->getOneOrNullResult(); |
||
3703 | |||
3704 | if (!$first) { |
||
3705 | return false; |
||
3706 | } |
||
3707 | |||
3708 | $pageId = (int)$first->getPageId(); |
||
3709 | |||
3710 | // Delete Conf for this pageId |
||
3711 | $em->createQuery('DELETE FROM '.CWikiConf::class.' c WHERE c.cId = :cid AND c.pageId = :pid') |
||
3712 | ->setParameter('cid', $ctx['courseId']) |
||
3713 | ->setParameter('pid', $pageId) |
||
3714 | ->execute(); |
||
3715 | |||
3716 | // Delete Discuss for this pageId |
||
3717 | $em->createQuery('DELETE FROM '.CWikiDiscuss::class.' d WHERE d.cId = :cid AND d.publicationId = :pid') |
||
3718 | ->setParameter('cid', $ctx['courseId']) |
||
3719 | ->setParameter('pid', $pageId) |
||
3720 | ->execute(); |
||
3721 | |||
3722 | // Delete all versions (respect group/session) |
||
3723 | $qb = $em->createQuery('DELETE FROM '.CWiki::class.' w WHERE w.cId = :cid AND w.pageId = :pid AND COALESCE(w.groupId,0) = :gid AND '.( |
||
3724 | $ctx['sessionId'] > 0 ? '(COALESCE(w.sessionId,0) = 0 OR w.sessionId = :sid)' : 'COALESCE(w.sessionId,0) = 0' |
||
3725 | )); |
||
3726 | $qb->setParameter('cid', $ctx['courseId']) |
||
3727 | ->setParameter('pid', $pageId) |
||
3728 | ->setParameter('gid', (int)$ctx['groupId']); |
||
3729 | if ($ctx['sessionId'] > 0) { $qb->setParameter('sid', (int)$ctx['sessionId']); } |
||
3730 | $qb->execute(); |
||
3731 | |||
3732 | self::check_emailcue(0, 'E'); |
||
3733 | |||
3734 | return true; |
||
3735 | } |
||
3736 | |||
3737 | public function allPages(string $action): void |
||
3738 | { |
||
3739 | $ctx = self::ctx(); // ['courseId','groupId','sessionId','baseUrl','courseCode'] |
||
3740 | $em = Container::getEntityManager(); |
||
3741 | |||
3742 | // Header + "Delete whole wiki" (only teachers/admin) |
||
3743 | echo '<div class="actions">'.get_lang('All pages'); |
||
3744 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
3745 | echo ' <a href="'.$ctx['baseUrl'].'&action=deletewiki">'. |
||
3746 | Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Delete wiki')). |
||
3747 | '</a>'; |
||
3748 | } |
||
3749 | echo '</div>'; |
||
3750 | |||
3751 | // Latest version per page (by reflink) in current context |
||
3752 | $qb = $em->createQueryBuilder() |
||
3753 | ->select('w') |
||
3754 | ->from(CWiki::class, 'w') |
||
3755 | ->andWhere('w.cId = :cid')->setParameter('cid', (int)$ctx['courseId']) |
||
3756 | ->andWhere('COALESCE(w.groupId, 0) = :gid')->setParameter('gid', (int)$ctx['groupId']); |
||
3757 | |||
3758 | if ((int)$ctx['sessionId'] > 0) { |
||
3759 | $qb->andWhere('COALESCE(w.sessionId, 0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
3760 | } else { |
||
3761 | $qb->andWhere('COALESCE(w.sessionId, 0) = 0'); |
||
3762 | } |
||
3763 | |||
3764 | if (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) { |
||
3765 | $qb->andWhere('w.visibility = 1'); |
||
3766 | } |
||
3767 | |||
3768 | // Subquery: max version for each reflink |
||
3769 | $sub = $em->createQueryBuilder() |
||
3770 | ->select('MAX(w2.version)') |
||
3771 | ->from(CWiki::class, 'w2') |
||
3772 | ->andWhere('w2.cId = :cid') |
||
3773 | ->andWhere('w2.reflink = w.reflink') |
||
3774 | ->andWhere('COALESCE(w2.groupId, 0) = :gid'); |
||
3775 | |||
3776 | if ((int)$ctx['sessionId'] > 0) { |
||
3777 | $sub->andWhere('COALESCE(w2.sessionId, 0) = :sid'); |
||
3778 | } else { |
||
3779 | $sub->andWhere('COALESCE(w2.sessionId, 0) = 0'); |
||
3780 | } |
||
3781 | |||
3782 | $qb->andWhere('w.version = ('.$sub->getDQL().')') |
||
3783 | ->orderBy('w.title', 'ASC'); |
||
3784 | |||
3785 | /** @var CWiki[] $pages */ |
||
3786 | $pages = $qb->getQuery()->getResult(); |
||
3787 | |||
3788 | // Prefetch Conf->task (avoid N+1) |
||
3789 | $pageIds = array_values(array_unique(array_filter( |
||
3790 | array_map(static fn(CWiki $w) => $w->getPageId(), $pages), |
||
3791 | static fn($v) => $v !== null |
||
3792 | ))); |
||
3793 | $taskByPageId = []; |
||
3794 | if ($pageIds) { |
||
3795 | $confQb = self::confRepo()->createQueryBuilder('c'); |
||
3796 | $confs = $confQb |
||
3797 | ->select('c.pageId, c.task') |
||
3798 | ->andWhere('c.cId = :cid')->setParameter('cid', (int)$ctx['courseId']) |
||
3799 | ->andWhere($confQb->expr()->in('c.pageId', ':pids'))->setParameter('pids', $pageIds) |
||
3800 | ->getQuery()->getArrayResult(); |
||
3801 | |||
3802 | foreach ($confs as $c) { |
||
3803 | if (!empty($c['task'])) { |
||
3804 | $taskByPageId[(int)$c['pageId']] = true; |
||
3805 | } |
||
3806 | } |
||
3807 | } |
||
3808 | |||
3809 | // Build rows: ALWAYS strings so TableSort can safely run strip_tags() |
||
3810 | $rows = []; |
||
3811 | foreach ($pages as $w) { |
||
3812 | $hasTask = !empty($taskByPageId[(int)$w->getPageId()]); |
||
3813 | $titlePack = json_encode([ |
||
3814 | 'title' => (string) $w->getTitle(), |
||
3815 | 'reflink' => (string) $w->getReflink(), |
||
3816 | 'iid' => (int) $w->getIid(), |
||
3817 | 'hasTask' => (bool) $hasTask, |
||
3818 | ], JSON_UNESCAPED_UNICODE); |
||
3819 | |||
3820 | $authorPack = json_encode([ |
||
3821 | 'userId' => (int) $w->getUserId(), |
||
3822 | 'ip' => (string) $w->getUserIp(), |
||
3823 | ], JSON_UNESCAPED_UNICODE); |
||
3824 | |||
3825 | $rows[] = [ |
||
3826 | (string) $w->getAssignment(), // 0: type (iconified) |
||
3827 | $titlePack, // 1: title data (JSON string) |
||
3828 | $authorPack, // 2: author data (JSON string) |
||
3829 | $w->getDtime() ? $w->getDtime()->format('Y-m-d H:i:s') : '', // 3: date string |
||
3830 | (string) $w->getReflink(), // 4: actions (needs reflink) |
||
3831 | ]; |
||
3832 | } |
||
3833 | |||
3834 | $table = new SortableTableFromArrayConfig( |
||
3835 | $rows, |
||
3836 | 1, |
||
3837 | 25, |
||
3838 | 'AllPages_table', |
||
3839 | '', |
||
3840 | '', |
||
3841 | 'ASC' |
||
3842 | ); |
||
3843 | |||
3844 | $table->set_additional_parameters([ |
||
3845 | 'cid' => $ctx['courseId'], |
||
3846 | 'gid' => $ctx['groupId'], |
||
3847 | 'sid' => $ctx['sessionId'], |
||
3848 | 'action' => Security::remove_XSS($action), |
||
3849 | ]); |
||
3850 | |||
3851 | $table->set_header(0, get_lang('Type'), true, ['style' => 'width:48px;']); |
||
3852 | $table->set_header(1, get_lang('Title'), true); |
||
3853 | $table->set_header(2, get_lang('Author').' <small>'.get_lang('Last version').'</small>'); |
||
3854 | $table->set_header(3, get_lang('Date').' <small>'.get_lang('Last version').'</small>'); |
||
3855 | |||
3856 | if (api_is_allowed_to_session_edit(false, true)) { |
||
3857 | $table->set_header(4, get_lang('Actions'), false, ['style' => 'width: 280px;']); |
||
3858 | } |
||
3859 | |||
3860 | // Column 0: icons (type + task badge) |
||
3861 | $table->set_column_filter(0, function ($value, string $urlParams, array $row) { |
||
3862 | $icons = self::assignmentIcon((int)$value); |
||
3863 | $packed = json_decode((string)$row[1], true) ?: []; |
||
3864 | if (!empty($packed['hasTask'])) { |
||
3865 | $icons .= Display::getMdiIcon( |
||
3866 | ActionIcon::WIKI_TASK, |
||
3867 | 'ch-tool-icon', |
||
3868 | null, |
||
3869 | ICON_SIZE_SMALL, |
||
3870 | get_lang('Standard task') |
||
3871 | ); |
||
3872 | } |
||
3873 | return $icons; |
||
3874 | }); |
||
3875 | |||
3876 | // Column 1: title link + categories |
||
3877 | $table->set_column_filter(1, function ($value) use ($ctx) { |
||
3878 | $data = json_decode((string)$value, true) ?: []; |
||
3879 | $ref = (string)($data['reflink'] ?? ''); |
||
3880 | $rawTitle = (string)($data['title'] ?? ''); |
||
3881 | $iid = (int)($data['iid'] ?? 0); |
||
3882 | |||
3883 | // Show "Home" for index if DB title is empty |
||
3884 | $display = self::displayTitleFor($ref, $rawTitle); |
||
3885 | |||
3886 | $href = $ctx['baseUrl'].'&'.http_build_query([ |
||
3887 | 'action' => 'showpage', |
||
3888 | 'title' => api_htmlentities($ref), |
||
3889 | ]); |
||
3890 | |||
3891 | return Display::url(api_htmlentities($display), $href) |
||
3892 | . self::returnCategoriesBlock($iid, '<div><small>', '</small></div>'); |
||
3893 | }); |
||
3894 | |||
3895 | // Column 2: author |
||
3896 | $table->set_column_filter(2, function ($value) { |
||
3897 | $data = json_decode((string)$value, true) ?: []; |
||
3898 | $uid = (int)($data['userId'] ?? 0); |
||
3899 | $ip = (string)($data['ip'] ?? ''); |
||
3900 | return self::authorLink($uid, $ip); |
||
3901 | }); |
||
3902 | |||
3903 | // Column 3: local time |
||
3904 | $table->set_column_filter(3, function ($value) { |
||
3905 | return !empty($value) ? api_get_local_time($value) : ''; |
||
3906 | }); |
||
3907 | |||
3908 | // Column 4: actions |
||
3909 | $table->set_column_filter(4, function ($value) use ($ctx) { |
||
3910 | if (!api_is_allowed_to_session_edit(false, true)) { |
||
3911 | return ''; |
||
3912 | } |
||
3913 | $ref = (string)$value; |
||
3914 | |||
3915 | $actions = Display::url( |
||
3916 | Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('EditPage')), |
||
3917 | $ctx['baseUrl'].'&'.http_build_query(['action' => 'edit', 'title' => api_htmlentities($ref)]) |
||
3918 | ); |
||
3919 | $actions .= Display::url( |
||
3920 | Display::getMdiIcon(ActionIcon::COMMENT, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Discuss')), |
||
3921 | $ctx['baseUrl'].'&'.http_build_query(['action' => 'discuss', 'title' => api_htmlentities($ref)]) |
||
3922 | ); |
||
3923 | $actions .= Display::url( |
||
3924 | Display::getMdiIcon(ActionIcon::HISTORY, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('History')), |
||
3925 | $ctx['baseUrl'].'&'.http_build_query(['action' => 'history', 'title' => api_htmlentities($ref)]) |
||
3926 | ); |
||
3927 | $actions .= Display::url( |
||
3928 | Display::getMdiIcon(ActionIcon::LINKS, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('LinksPages')), |
||
3929 | $ctx['baseUrl'].'&'.http_build_query(['action' => 'links', 'title' => api_htmlentities($ref)]) |
||
3930 | ); |
||
3931 | |||
3932 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
3933 | $actions .= Display::url( |
||
3934 | Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Delete')), |
||
3935 | $ctx['baseUrl'].'&'.http_build_query(['action' => 'delete', 'title' => api_htmlentities($ref)]) |
||
3936 | ); |
||
3937 | } |
||
3938 | return $actions; |
||
3939 | }); |
||
3940 | |||
3941 | $table->display(); |
||
3942 | } |
||
3943 | |||
3944 | public function getSearchPages(string $action): void |
||
3945 | { |
||
3946 | $ctx = self::ctx(); |
||
3947 | $url = $ctx['baseUrl'].'&'.http_build_query(['action' => api_htmlentities($action), 'mode_table' => 'yes1']); |
||
3948 | |||
3949 | echo '<div class="actions">'.get_lang('Search').'</div>'; |
||
3950 | |||
3951 | if (isset($_GET['mode_table'])) { |
||
3952 | if (!isset($_GET['SearchPages_table_page_nr'])) { |
||
3953 | $_GET['search_term'] = $_POST['search_term'] ?? ''; |
||
3954 | $_GET['search_content'] = $_POST['search_content'] ?? ''; |
||
3955 | $_GET['all_vers'] = $_POST['all_vers'] ?? ''; |
||
3956 | $_GET['categories'] = $_POST['categories'] ?? []; |
||
3957 | $_GET['match_all_categories']= !empty($_POST['match_all_categories']); |
||
3958 | } |
||
3959 | $this->display_wiki_search_results( |
||
3960 | (string) $_GET['search_term'], |
||
3961 | (int) $_GET['search_content'], |
||
3962 | (int) $_GET['all_vers'], |
||
3963 | (array) $_GET['categories'], |
||
3964 | (bool) $_GET['match_all_categories'] |
||
3965 | ); |
||
3966 | return; |
||
3967 | } |
||
3968 | |||
3969 | // Build form |
||
3970 | $form = new FormValidator('wiki_search', 'get', $url); |
||
3971 | $form->addHidden('cid', $ctx['courseId']); |
||
3972 | $form->addHidden('sid', $ctx['sessionId']); |
||
3973 | $form->addHidden('gid', $ctx['groupId']); |
||
3974 | $form->addHidden('gradebook', '0'); |
||
3975 | $form->addHidden('origin', ''); |
||
3976 | $form->addHidden('action', 'searchpages'); |
||
3977 | |||
3978 | $form->addText('search_term', get_lang('Search term'), false, ['autofocus' => 'autofocus']); |
||
3979 | $form->addCheckBox('search_content', '', get_lang('Search also in content')); |
||
3980 | $form->addCheckbox('all_vers', '', get_lang('Also search in older versions of each page')); |
||
3981 | |||
3982 | if (self::categoriesEnabled()) { |
||
3983 | $categories = Container::getEntityManager() |
||
3984 | ->getRepository(CWikiCategory::class) |
||
3985 | ->findByCourse(api_get_course_entity()); |
||
3986 | $form->addSelectFromCollection( |
||
3987 | 'categories', |
||
3988 | get_lang('Categories'), |
||
3989 | $categories, |
||
3990 | ['multiple' => 'multiple'], |
||
3991 | false, |
||
3992 | 'getNodeName' |
||
3993 | ); |
||
3994 | $form->addCheckBox('match_all_categories', '', get_lang('Must be in ALL the selected categories')); |
||
3995 | } |
||
3996 | |||
3997 | $form->addButtonSearch(get_lang('Search'), 'SubmitWikiSearch'); |
||
3998 | $form->addRule('search_term', get_lang('Too short'), 'minlength', 3); |
||
3999 | |||
4000 | if ($form->validate()) { |
||
4001 | $form->display(); |
||
4002 | $values = $form->exportValues(); |
||
4003 | $this->display_wiki_search_results( |
||
4004 | (string)$values['search_term'], |
||
4005 | (int)($values['search_content'] ?? 0), |
||
4006 | (int)($values['all_vers'] ?? 0), |
||
4007 | (array)($values['categories'] ?? []), |
||
4008 | !empty($values['match_all_categories']) |
||
4009 | ); |
||
4010 | } else { |
||
4011 | $form->display(); |
||
4012 | } |
||
4013 | } |
||
4014 | |||
4015 | public function display_wiki_search_results( |
||
4016 | string $searchTerm, |
||
4017 | int $searchContent = 0, |
||
4018 | int $allVersions = 0, |
||
4019 | array $categoryIdList = [], |
||
4020 | bool $matchAllCategories = false |
||
4021 | ): void { |
||
4022 | $ctx = self::ctx(); |
||
4023 | $em = Container::getEntityManager(); |
||
4024 | $repo = self::repo(); |
||
4025 | $url = $ctx['baseUrl']; |
||
4026 | |||
4027 | $categoryIdList = array_map('intval', $categoryIdList); |
||
4028 | |||
4029 | echo '<legend>'.get_lang('Wiki search results').': '.Security::remove_XSS($searchTerm).'</legend>'; |
||
4030 | |||
4031 | $qb = $repo->createQueryBuilder('wp'); |
||
4032 | $qb->andWhere('wp.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
4033 | ->andWhere('COALESCE(wp.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']); |
||
4034 | |||
4035 | if ($ctx['sessionId'] > 0) { |
||
4036 | $qb->andWhere('COALESCE(wp.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
4037 | } else { |
||
4038 | $qb->andWhere('COALESCE(wp.sessionId,0) = 0'); |
||
4039 | } |
||
4040 | |||
4041 | // Visibility for students |
||
4042 | if (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) { |
||
4043 | $qb->andWhere('wp.visibility = 1'); |
||
4044 | } |
||
4045 | |||
4046 | // Search by title (+content if requested) |
||
4047 | $likeTerm = '%'.$searchTerm.'%'; |
||
4048 | $or = $qb->expr()->orX( |
||
4049 | $qb->expr()->like('wp.title', ':term') |
||
4050 | ); |
||
4051 | if ($searchContent === 1) { |
||
4052 | $or->add($qb->expr()->like('wp.content', ':term')); |
||
4053 | } |
||
4054 | $qb->andWhere($or)->setParameter('term', $likeTerm); |
||
4055 | |||
4056 | // Categories filter |
||
4057 | if (!empty($categoryIdList)) { |
||
4058 | if ($matchAllCategories) { |
||
4059 | $i = 0; |
||
4060 | foreach ($categoryIdList as $catId) { |
||
4061 | ++$i; |
||
4062 | $aliasC = 'wc'.$i; |
||
4063 | $qb->innerJoin('wp.categories', $aliasC, 'WITH', $qb->expr()->eq($aliasC.'.id', ':cid'.$i)) |
||
4064 | ->setParameter('cid'.$i, $catId); |
||
4065 | } |
||
4066 | } else { |
||
4067 | $qb->innerJoin('wp.categories', 'wc') |
||
4068 | ->andWhere('wc.id IN (:cids)') |
||
4069 | ->setParameter('cids', $categoryIdList); |
||
4070 | } |
||
4071 | } |
||
4072 | |||
4073 | // Only latest per page unless allVersions=1 |
||
4074 | if ($allVersions !== 1) { |
||
4075 | $sub = $em->createQueryBuilder() |
||
4076 | ->select('MAX(s2.version)') |
||
4077 | ->from(\Chamilo\CourseBundle\Entity\CWiki::class, 's2') |
||
4078 | ->andWhere('s2.cId = :cid') |
||
4079 | ->andWhere('s2.reflink = wp.reflink') |
||
4080 | ->andWhere('COALESCE(s2.groupId,0) = :gid'); |
||
4081 | |||
4082 | if ($ctx['sessionId'] > 0) { |
||
4083 | $sub->andWhere('COALESCE(s2.sessionId,0) = :sid'); |
||
4084 | } else { |
||
4085 | $sub->andWhere('COALESCE(s2.sessionId,0) = 0'); |
||
4086 | } |
||
4087 | $qb->andWhere($qb->expr()->eq('wp.version', '(' . $sub->getDQL() . ')')); |
||
4088 | } |
||
4089 | |||
4090 | $qb->orderBy('wp.dtime', 'DESC'); |
||
4091 | |||
4092 | /** @var \Chamilo\CourseBundle\Entity\CWiki[] $rows */ |
||
4093 | $rows = $qb->getQuery()->getResult(); |
||
4094 | |||
4095 | if (!$rows) { |
||
4096 | echo get_lang('NoSearchResults'); |
||
4097 | return; |
||
4098 | } |
||
4099 | |||
4100 | // Icons |
||
4101 | $iconEdit = Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('EditPage')); |
||
4102 | $iconDiscuss = Display::getMdiIcon(ActionIcon::COMMENT, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Discuss')); |
||
4103 | $iconHistory = Display::getMdiIcon(ActionIcon::HISTORY, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('History')); |
||
4104 | $iconLinks = Display::getMdiIcon(ActionIcon::LINKS, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('LinksPages')); |
||
4105 | $iconDelete = Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Delete')); |
||
4106 | |||
4107 | $data = []; |
||
4108 | foreach ($rows as $w) { |
||
4109 | $assignIcon = self::assignmentIcon((int)$w->getAssignment()); |
||
4110 | |||
4111 | $wikiLinkParams = ['action' => 'showpage', 'title' => $w->getReflink()]; |
||
4112 | if ($allVersions === 1) { |
||
4113 | $wikiLinkParams['view'] = $w->getIid(); |
||
4114 | } |
||
4115 | |||
4116 | $titleLink = Display::url( |
||
4117 | api_htmlentities($w->getTitle()), |
||
4118 | $url.'&'.http_build_query($wikiLinkParams) |
||
4119 | ).self::returnCategoriesBlock((int)$w->getIid(), '<div><small>', '</small></div>'); |
||
4120 | |||
4121 | $author = self::authorLink((int)$w->getUserId(), (string)$w->getUserIp()); |
||
4122 | $date = api_convert_and_format_date($w->getDtime()); |
||
4123 | |||
4124 | if ($allVersions === 1) { |
||
4125 | $data[] = [$assignIcon, $titleLink, $author, $date, (int)$w->getVersion()]; |
||
4126 | } else { |
||
4127 | $actions = ''; |
||
4128 | $actions .= Display::url($iconEdit, $url.'&'.http_build_query(['action' => 'edit', 'title' => $w->getReflink()])); |
||
4129 | $actions .= Display::url($iconDiscuss, $url.'&'.http_build_query(['action' => 'discuss', 'title' => $w->getReflink()])); |
||
4130 | $actions .= Display::url($iconHistory, $url.'&'.http_build_query(['action' => 'history', 'title' => $w->getReflink()])); |
||
4131 | $actions .= Display::url($iconLinks, $url.'&'.http_build_query(['action' => 'links', 'title' => $w->getReflink()])); |
||
4132 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
4133 | $actions .= Display::url($iconDelete, $url.'&'.http_build_query(['action' => 'delete', 'title' => $w->getReflink()])); |
||
4134 | } |
||
4135 | |||
4136 | $data[] = [$assignIcon, $titleLink, $author, $date, $actions]; |
||
4137 | } |
||
4138 | } |
||
4139 | |||
4140 | $table = new SortableTableFromArrayConfig( |
||
4141 | $data, |
||
4142 | 1, // default sort by title |
||
4143 | 10, |
||
4144 | 'SearchPages_table', |
||
4145 | '', |
||
4146 | '', |
||
4147 | 'ASC' |
||
4148 | ); |
||
4149 | |||
4150 | $extra = [ |
||
4151 | 'cid' => (int)$ctx['courseId'], |
||
4152 | 'gid' => (int)$ctx['groupId'], |
||
4153 | 'sid' => (int)$ctx['sessionId'], |
||
4154 | 'action' => $_GET['action'] ?? 'searchpages', |
||
4155 | 'mode_table' => 'yes2', |
||
4156 | 'search_term' => (string)$searchTerm, |
||
4157 | 'search_content' => (int)$searchContent, |
||
4158 | 'all_vers' => (int)$allVersions, |
||
4159 | 'match_all_categories' => $matchAllCategories ? 1 : 0, |
||
4160 | ]; |
||
4161 | |||
4162 | foreach ($categoryIdList as $i => $cidVal) { |
||
4163 | $extra['categories['.$i.']'] = (int)$cidVal; |
||
4164 | } |
||
4165 | $table->set_additional_parameters($extra); |
||
4166 | |||
4167 | $table->set_header(0, get_lang('Type'), true, ['style' => 'width:48px;']); |
||
4168 | $table->set_header(1, get_lang('Title')); |
||
4169 | if ($allVersions === 1) { |
||
4170 | $table->set_header(2, get_lang('Author')); |
||
4171 | $table->set_header(3, get_lang('Date')); |
||
4172 | $table->set_header(4, get_lang('Version')); |
||
4173 | } else { |
||
4174 | $table->set_header(2, get_lang('Author').' <small>'.get_lang('LastVersion').'</small>'); |
||
4175 | $table->set_header(3, get_lang('Date').' <small>'.get_lang('LastVersion').'</small>'); |
||
4176 | $table->set_header(4, get_lang('Actions'), false, ['style' => 'width:280px;']); |
||
4177 | } |
||
4178 | $table->display(); |
||
4179 | } |
||
4180 | |||
4181 | public function recentChanges(string $page, string $action): void |
||
4182 | { |
||
4183 | $ctx = self::ctx(); |
||
4184 | $url = $ctx['baseUrl']; |
||
4185 | |||
4186 | // Top bar: notify-all toggle (only if user can session-edit) |
||
4187 | $notifyBlock = ''; |
||
4188 | if (api_is_allowed_to_session_edit(false, true)) { |
||
4189 | if (self::check_notify_all() === 1) { |
||
4190 | $notifyBlock = Display::getMdiIcon(ActionIcon::INFORMATION, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('CancelNotifyByEmail')) |
||
4191 | .' '.get_lang('Not notify changes'); |
||
4192 | $act = 'unlocknotifyall'; |
||
4193 | } else { |
||
4194 | $notifyBlock = Display::getMdiIcon(ActionIcon::SEND_SINGLE_EMAIL, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('NotifyByEmail')) |
||
4195 | .' '.get_lang('Notify changes'); |
||
4196 | $act = 'locknotifyall'; |
||
4197 | } |
||
4198 | |||
4199 | echo '<div class="actions"><span style="float:right;">'. |
||
4200 | '<a href="'.$url.'&action=recentchanges&actionpage='.$act.'&title='.api_htmlentities(urlencode($page)).'">'.$notifyBlock.'</a>'. |
||
4201 | '</span>'.get_lang('Recent changes').'</div>'; |
||
4202 | } else { |
||
4203 | echo '<div class="actions">'.get_lang('Recent changes').'</div>'; |
||
4204 | } |
||
4205 | |||
4206 | $repo = self::repo(); |
||
4207 | $em = Container::getEntityManager(); |
||
4208 | |||
4209 | $qb = $repo->createQueryBuilder('w') |
||
4210 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
4211 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']); |
||
4212 | |||
4213 | if ($ctx['sessionId'] > 0) { |
||
4214 | $qb->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
4215 | } else { |
||
4216 | $qb->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
4217 | } |
||
4218 | |||
4219 | // Students only see visible pages |
||
4220 | if (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) { |
||
4221 | $qb->andWhere('w.visibility = 1'); |
||
4222 | } |
||
4223 | |||
4224 | $qb->orderBy('w.dtime', 'DESC'); |
||
4225 | |||
4226 | /** @var CWiki[] $list */ |
||
4227 | $list = $qb->getQuery()->getResult(); |
||
4228 | |||
4229 | if (empty($list)) { |
||
4230 | return; |
||
4231 | } |
||
4232 | |||
4233 | $rows = []; |
||
4234 | foreach ($list as $w) { |
||
4235 | $assignIcon = self::assignmentIcon((int)$w->getAssignment()); |
||
4236 | |||
4237 | // Task icon? |
||
4238 | $iconTask = ''; |
||
4239 | $conf = self::confRepo()->findOneBy(['cId' => $ctx['courseId'], 'pageId' => (int)$w->getPageId()]); |
||
4240 | if ($conf && $conf->getTask()) { |
||
4241 | $iconTask = Display::getMdiIcon(ActionIcon::WIKI_TASK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('StandardTask')); |
||
4242 | } |
||
4243 | |||
4244 | $titleLink = Display::url( |
||
4245 | api_htmlentities($w->getTitle()), |
||
4246 | $url.'&'.http_build_query([ |
||
4247 | 'action' => 'showpage', |
||
4248 | 'title' => api_htmlentities($w->getReflink()), |
||
4249 | 'view' => (int)$w->getIid(), // jump to that version |
||
4250 | ]) |
||
4251 | ); |
||
4252 | |||
4253 | $actionText = ((int)$w->getVersion() > 1) ? get_lang('EditedBy') : get_lang('AddedBy'); |
||
4254 | $authorLink = self::authorLink((int)$w->getUserId(), (string)$w->getUserIp()); |
||
4255 | |||
4256 | $rows[] = [ |
||
4257 | api_get_local_time($w->getDtime()), |
||
4258 | $assignIcon.$iconTask, |
||
4259 | $titleLink, |
||
4260 | $actionText, |
||
4261 | $authorLink, |
||
4262 | ]; |
||
4263 | } |
||
4264 | |||
4265 | $table = new SortableTableFromArrayConfig( |
||
4266 | $rows, |
||
4267 | 0, |
||
4268 | 10, |
||
4269 | 'RecentPages_table', |
||
4270 | '', |
||
4271 | '', |
||
4272 | 'DESC' |
||
4273 | ); |
||
4274 | $table->set_additional_parameters([ |
||
4275 | 'cid' => $ctx['courseId'], |
||
4276 | 'gid' => $ctx['groupId'], |
||
4277 | 'sid' => $ctx['sessionId'], |
||
4278 | 'action' => Security::remove_XSS($action), |
||
4279 | ]); |
||
4280 | |||
4281 | $table->set_header(0, get_lang('Date'), true, ['style' => 'width:200px;']); |
||
4282 | $table->set_header(1, get_lang('Type'), true, ['style' => 'width:48px;']); |
||
4283 | $table->set_header(2, get_lang('Title'), true); |
||
4284 | $table->set_header(3, get_lang('Actions'), true, ['style' => 'width:120px;']); |
||
4285 | $table->set_header(4, get_lang('Author'), true); |
||
4286 | $table->display(); |
||
4287 | } |
||
4288 | |||
4289 | private static function assignmentIcon(int $assignment): string |
||
4290 | { |
||
4291 | return match ($assignment) { |
||
4292 | 1 => Display::getMdiIcon(ActionIcon::WIKI_ASSIGNMENT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('AssignmentDesc')), |
||
4293 | 2 => Display::getMdiIcon(ActionIcon::WIKI_WORK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('AssignmentWork')), |
||
4294 | default => '', |
||
4295 | }; |
||
4296 | } |
||
4297 | |||
4298 | private static function authorLink(int $userId, string $userIp): string |
||
4299 | { |
||
4300 | $ui = $userId ? api_get_user_info($userId) : false; |
||
4301 | if ($ui !== false) { |
||
4302 | return UserManager::getUserProfileLink($ui); |
||
4303 | } |
||
4304 | return get_lang('Anonymous').' ('.api_htmlentities($userIp).')'; |
||
4305 | } |
||
4306 | |||
4307 | /** Course-wide watchers toggle for "Recent Changes". Returns 1 if subscribed, else 0 (and processes GET toggles). */ |
||
4308 | public static function check_notify_all(?int $courseId = null, ?int $sessionId = null, ?int $groupId = null): int |
||
4309 | { |
||
4310 | $ctx = self::ctx($courseId, $sessionId, $groupId); |
||
4311 | $em = Container::getEntityManager(); |
||
4312 | |||
4313 | $userId = api_get_user_id(); |
||
4314 | $repoMail = $em->getRepository(CWikiMailcue::class); |
||
4315 | |||
4316 | /** @var CWikiMailcue|null $existing */ |
||
4317 | $existing = $repoMail->findOneBy([ |
||
4318 | 'cId' => $ctx['courseId'], |
||
4319 | 'groupId' => (int)$ctx['groupId'], |
||
4320 | 'sessionId' => (int)$ctx['sessionId'], |
||
4321 | 'userId' => $userId, |
||
4322 | ]); |
||
4323 | |||
4324 | if (api_is_allowed_to_session_edit() && !empty($_GET['actionpage'])) { |
||
4325 | $act = (string) $_GET['actionpage']; |
||
4326 | |||
4327 | if ('locknotifyall' === $act && !$existing) { |
||
4328 | $cue = new CWikiMailcue(); |
||
4329 | $cue->setCId($ctx['courseId']) |
||
4330 | ->setUserId($userId) |
||
4331 | ->setGroupId((int)$ctx['groupId']) |
||
4332 | ->setSessionId((int)$ctx['sessionId']) |
||
4333 | ->setType('wiki'); |
||
4334 | $em->persist($cue); |
||
4335 | $em->flush(); |
||
4336 | $existing = $cue; |
||
4337 | } |
||
4338 | |||
4339 | if ('unlocknotifyall' === $act && $existing) { |
||
4340 | $em->remove($existing); |
||
4341 | $em->flush(); |
||
4342 | $existing = null; |
||
4343 | } |
||
4344 | } |
||
4345 | |||
4346 | return $existing ? 1 : 0; |
||
4347 | } |
||
4348 | |||
4349 | public function getUserContributions(int $userId, string $action): void |
||
4350 | { |
||
4351 | $ctx = self::ctx(); |
||
4352 | $url = $ctx['baseUrl']; |
||
4353 | $userId = (int) $userId; |
||
4354 | |||
4355 | $userinfo = api_get_user_info($userId); |
||
4356 | if ($userinfo !== false) { |
||
4357 | echo '<div class="actions">'. |
||
4358 | Display::url( |
||
4359 | get_lang('User contributions').': '.$userinfo['complete_name_with_username'], |
||
4360 | $url.'&'.http_build_query(['action' => 'usercontrib', 'user_id' => $userId]) |
||
4361 | ). |
||
4362 | '</div>'; |
||
4363 | } |
||
4364 | |||
4365 | $qb = self::repo()->createQueryBuilder('w') |
||
4366 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
4367 | ->andWhere('w.userId = :uid')->setParameter('uid', $userId) |
||
4368 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']); |
||
4369 | |||
4370 | if ($ctx['sessionId'] > 0) { |
||
4371 | $qb->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
4372 | } else { |
||
4373 | $qb->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
4374 | } |
||
4375 | |||
4376 | if (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) { |
||
4377 | $qb->andWhere('w.visibility = 1'); |
||
4378 | } |
||
4379 | |||
4380 | $qb->orderBy('w.dtime', 'DESC'); |
||
4381 | |||
4382 | /** @var CWiki[] $list */ |
||
4383 | $list = $qb->getQuery()->getResult(); |
||
4384 | |||
4385 | if (empty($list)) { |
||
4386 | return; |
||
4387 | } |
||
4388 | |||
4389 | $rows = []; |
||
4390 | foreach ($list as $w) { |
||
4391 | $rows[] = [ |
||
4392 | api_get_local_time($w->getDtime()), |
||
4393 | self::assignmentIcon((int)$w->getAssignment()), |
||
4394 | Display::url( |
||
4395 | api_htmlentities($w->getTitle()), |
||
4396 | $url.'&'.http_build_query([ |
||
4397 | 'action' => 'showpage', |
||
4398 | 'title' => api_htmlentities($w->getReflink()), |
||
4399 | 'view' => (int)$w->getIid(), |
||
4400 | ]) |
||
4401 | ), |
||
4402 | Security::remove_XSS((string)$w->getVersion()), |
||
4403 | Security::remove_XSS((string)$w->getComment()), |
||
4404 | Security::remove_XSS((string)$w->getProgress()).' %', |
||
4405 | Security::remove_XSS((string)$w->getScore()), |
||
4406 | ]; |
||
4407 | } |
||
4408 | |||
4409 | $table = new SortableTableFromArrayConfig($rows, 2, 10, 'UsersContributions_table', '', '', 'ASC'); |
||
4410 | $table->set_additional_parameters([ |
||
4411 | 'cid' => $ctx['courseId'], |
||
4412 | 'gid' => $ctx['groupId'], |
||
4413 | 'sid' => $ctx['sessionId'], |
||
4414 | 'action' => Security::remove_XSS($action), |
||
4415 | 'user_id' => (int)$userId, |
||
4416 | ]); |
||
4417 | $table->set_header(0, get_lang('Date'), true, ['style' => 'width:200px;']); |
||
4418 | $table->set_header(1, get_lang('Type'), true, ['style' => 'width:48px;']); |
||
4419 | $table->set_header(2, get_lang('Title'), true, ['style' => 'width:200px;']); |
||
4420 | $table->set_header(3, get_lang('Version'), true, ['style' => 'width:60px;']); |
||
4421 | $table->set_header(4, get_lang('Comment'), true, ['style' => 'width:200px;']); |
||
4422 | $table->set_header(5, get_lang('Progress'),true, ['style' => 'width:80px;']); |
||
4423 | $table->set_header(6, get_lang('Rating'), true, ['style' => 'width:80px;']); |
||
4424 | $table->display(); |
||
4425 | } |
||
4426 | |||
4427 | public function getMostChangedPages(string $action): void |
||
4428 | { |
||
4429 | $ctx = self::ctx(); |
||
4430 | $url = $ctx['baseUrl']; |
||
4431 | |||
4432 | echo '<div class="actions">'.get_lang('Most changed pages').'</div>'; |
||
4433 | |||
4434 | // Aggregate: max(version) per reflink with context gates |
||
4435 | $qb = self::repo()->createQueryBuilder('w') |
||
4436 | ->select('w.reflink AS reflink, MAX(w.version) AS changes') |
||
4437 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
4438 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']); |
||
4439 | |||
4440 | if ($ctx['sessionId'] > 0) { |
||
4441 | $qb->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
4442 | } else { |
||
4443 | $qb->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
4444 | } |
||
4445 | if (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) { |
||
4446 | $qb->andWhere('w.visibility = 1'); |
||
4447 | } |
||
4448 | $qb->groupBy('w.reflink'); |
||
4449 | |||
4450 | $raw = $qb->getQuery()->getArrayResult(); |
||
4451 | if (empty($raw)) { |
||
4452 | return; |
||
4453 | } |
||
4454 | |||
4455 | $rows = []; |
||
4456 | foreach ($raw as $r) { |
||
4457 | $reflink = (string)$r['reflink']; |
||
4458 | // Fetch latest page for title + assignment |
||
4459 | $latest = self::repo()->findOneBy( |
||
4460 | ['cId' => $ctx['courseId'], 'reflink' => $reflink, 'groupId' => (int)$ctx['groupId'], 'sessionId' => (int)$ctx['sessionId']], |
||
4461 | ['version' => 'DESC', 'dtime' => 'DESC'] |
||
4462 | ) ?? self::repo()->findOneBy(['cId' => $ctx['courseId'], 'reflink' => $reflink], ['version' => 'DESC']); |
||
4463 | |||
4464 | if (!$latest) { |
||
4465 | continue; |
||
4466 | } |
||
4467 | |||
4468 | $rows[] = [ |
||
4469 | self::assignmentIcon((int)$latest->getAssignment()), |
||
4470 | Display::url( |
||
4471 | api_htmlentities($latest->getTitle()), |
||
4472 | $url.'&'.http_build_query(['action' => 'showpage', 'title' => api_htmlentities($reflink)]) |
||
4473 | ), |
||
4474 | (int)$r['changes'], |
||
4475 | ]; |
||
4476 | } |
||
4477 | |||
4478 | $table = new SortableTableFromArrayConfig($rows, 2, 10, 'MostChangedPages_table', '', '', 'DESC'); |
||
4479 | $table->set_additional_parameters([ |
||
4480 | 'cid' => $ctx['courseId'], |
||
4481 | 'gid' => $ctx['groupId'], |
||
4482 | 'sid' => $ctx['sessionId'], |
||
4483 | 'action' => Security::remove_XSS($action), |
||
4484 | ]); |
||
4485 | $table->set_header(0, get_lang('Type'), true, ['style' => 'width:48px;']); |
||
4486 | $table->set_header(1, get_lang('Title'), true); |
||
4487 | $table->set_header(2, get_lang('Changes'),true, ['style' => 'width:100px;']); |
||
4488 | $table->display(); |
||
4489 | } |
||
4490 | |||
4491 | public function getMostVisited(): void |
||
4492 | { |
||
4493 | $ctx = self::ctx(); |
||
4494 | $url = $ctx['baseUrl']; |
||
4495 | |||
4496 | echo '<div class="actions">'.get_lang('Most visited pages').'</div>'; |
||
4497 | |||
4498 | // Aggregate: sum(hits) per reflink |
||
4499 | $qb = self::repo()->createQueryBuilder('w') |
||
4500 | ->select('w.reflink AS reflink, SUM(COALESCE(w.hits,0)) AS totalHits') |
||
4501 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
4502 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']); |
||
4503 | |||
4504 | if ($ctx['sessionId'] > 0) { |
||
4505 | $qb->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
4506 | } else { |
||
4507 | $qb->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
4508 | } |
||
4509 | if (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) { |
||
4510 | $qb->andWhere('w.visibility = 1'); |
||
4511 | } |
||
4512 | $qb->groupBy('w.reflink'); |
||
4513 | |||
4514 | $raw = $qb->getQuery()->getArrayResult(); |
||
4515 | if (empty($raw)) { |
||
4516 | return; |
||
4517 | } |
||
4518 | |||
4519 | $rows = []; |
||
4520 | foreach ($raw as $r) { |
||
4521 | $reflink = (string)$r['reflink']; |
||
4522 | $latest = self::repo()->findOneBy( |
||
4523 | ['cId' => $ctx['courseId'], 'reflink' => $reflink, 'groupId' => (int)$ctx['groupId'], 'sessionId' => (int)$ctx['sessionId']], |
||
4524 | ['version' => 'DESC', 'dtime' => 'DESC'] |
||
4525 | ) ?? self::repo()->findOneBy(['cId' => $ctx['courseId'], 'reflink' => $reflink], ['version' => 'DESC']); |
||
4526 | |||
4527 | if (!$latest) { |
||
4528 | continue; |
||
4529 | } |
||
4530 | |||
4531 | $rows[] = [ |
||
4532 | self::assignmentIcon((int)$latest->getAssignment()), |
||
4533 | Display::url( |
||
4534 | api_htmlentities($latest->getTitle()), |
||
4535 | $url.'&'.http_build_query(['action' => 'showpage', 'title' => api_htmlentities($reflink)]) |
||
4536 | ), |
||
4537 | (int)$r['totalHits'], |
||
4538 | ]; |
||
4539 | } |
||
4540 | |||
4541 | $table = new SortableTableFromArrayConfig($rows, 2, 10, 'MostVisitedPages_table', '', '', 'DESC'); |
||
4542 | $table->set_additional_parameters([ |
||
4543 | 'cid' => $ctx['courseId'], |
||
4544 | 'gid' => $ctx['groupId'], |
||
4545 | 'sid' => $ctx['sessionId'], |
||
4546 | 'action' => Security::remove_XSS($this->action ?? 'mvisited'), |
||
4547 | ]); |
||
4548 | $table->set_header(0, get_lang('Type'), true, ['style' => 'width:48px;']); |
||
4549 | $table->set_header(1, get_lang('Title'), true); |
||
4550 | $table->set_header(2, get_lang('Visits'), true, ['style' => 'width:100px;']); |
||
4551 | $table->display(); |
||
4552 | } |
||
4553 | |||
4554 | public function getMostLinked(): void |
||
4555 | { |
||
4556 | $ctx = self::ctx(); |
||
4557 | $url = $ctx['baseUrl']; |
||
4558 | |||
4559 | echo '<div class="actions">'.get_lang('Most linked pages').'</div>'; |
||
4560 | |||
4561 | // All existing page reflinks in context |
||
4562 | $qbPages = self::repo()->createQueryBuilder('w') |
||
4563 | ->select('DISTINCT w.reflink AS reflink') |
||
4564 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
4565 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']); |
||
4566 | if ($ctx['sessionId'] > 0) { |
||
4567 | $qbPages->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
4568 | } else { |
||
4569 | $qbPages->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
4570 | } |
||
4571 | $pages = array_map(fn($r) => (string)$r['reflink'], $qbPages->getQuery()->getArrayResult()); |
||
4572 | |||
4573 | // Latest version of every page in context |
||
4574 | $latestList = $this->getLatestPagesForContext(); |
||
4575 | |||
4576 | // Collect "linksto" tokens pointing to existing pages (excluding self) |
||
4577 | $linked = []; |
||
4578 | foreach ($latestList as $w) { |
||
4579 | $selfRef = $w->getReflink(); |
||
4580 | $tokens = preg_split('/\s+/', trim((string)$w->getLinksto())) ?: []; |
||
4581 | foreach ($tokens as $t) { |
||
4582 | $t = trim($t); |
||
4583 | if ($t === '' || $t === $selfRef) { |
||
4584 | continue; |
||
4585 | } |
||
4586 | if (in_array($t, $pages, true)) { |
||
4587 | $linked[] = $t; |
||
4588 | } |
||
4589 | } |
||
4590 | } |
||
4591 | |||
4592 | $linked = array_values(array_unique($linked)); |
||
4593 | $rows = []; |
||
4594 | foreach ($linked as $ref) { |
||
4595 | $rows[] = [ |
||
4596 | Display::url( |
||
4597 | str_replace('_', ' ', $ref), |
||
4598 | $url.'&'.http_build_query(['action' => 'showpage', 'title' => str_replace('_', ' ', $ref)]) |
||
4599 | ), |
||
4600 | ]; |
||
4601 | } |
||
4602 | |||
4603 | $table = new SortableTableFromArrayConfig($rows, 0, 10, 'LinkedPages_table', '', '', 'ASC'); |
||
4604 | $table->set_additional_parameters([ |
||
4605 | 'cid' => $ctx['courseId'], |
||
4606 | 'gid' => $ctx['groupId'], |
||
4607 | 'sid' => $ctx['sessionId'], |
||
4608 | 'action' => Security::remove_XSS($this->action ?? 'mostlinked'), |
||
4609 | ]); |
||
4610 | $table->set_header(0, get_lang('Title'), true); |
||
4611 | $table->display(); |
||
4612 | } |
||
4613 | |||
4614 | public function getOrphaned(): void |
||
4615 | { |
||
4616 | $ctx = self::ctx(); |
||
4617 | $url = $ctx['baseUrl']; |
||
4618 | |||
4619 | echo '<div class="actions">'.get_lang('Orphaned pages').'</div>'; |
||
4620 | |||
4621 | // All page reflinks in context |
||
4622 | $qbPages = self::repo()->createQueryBuilder('w') |
||
4623 | ->select('DISTINCT w.reflink AS reflink') |
||
4624 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
4625 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']); |
||
4626 | if ($ctx['sessionId'] > 0) { |
||
4627 | $qbPages->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
4628 | } else { |
||
4629 | $qbPages->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
4630 | } |
||
4631 | $pages = array_map(fn($r) => (string)$r['reflink'], $qbPages->getQuery()->getArrayResult()); |
||
4632 | |||
4633 | // Latest version per reflink |
||
4634 | $latestList = $this->getLatestPagesForContext(); |
||
4635 | |||
4636 | // Gather all linksto tokens across latest versions |
||
4637 | $linkedTokens = []; |
||
4638 | foreach ($latestList as $w) { |
||
4639 | $self = $w->getReflink(); |
||
4640 | $tokens = preg_split('/\s+/', trim((string)$w->getLinksto())) ?: []; |
||
4641 | foreach ($tokens as $t) { |
||
4642 | $t = trim($t); |
||
4643 | if ($t === '' || $t === $self) { |
||
4644 | continue; |
||
4645 | } |
||
4646 | $linkedTokens[] = $t; |
||
4647 | } |
||
4648 | } |
||
4649 | $linkedTokens = array_values(array_unique($linkedTokens)); |
||
4650 | |||
4651 | // Orphaned = pages not referenced by any token |
||
4652 | $orphaned = array_values(array_diff($pages, $linkedTokens)); |
||
4653 | |||
4654 | $rows = []; |
||
4655 | foreach ($orphaned as $ref) { |
||
4656 | // Fetch one latest entity to check visibility/assignment/title |
||
4657 | $latest = self::repo()->findOneBy( |
||
4658 | ['cId' => $ctx['courseId'], 'reflink' => $ref, 'groupId' => (int)$ctx['groupId'], 'sessionId' => (int)$ctx['sessionId']], |
||
4659 | ['version' => 'DESC', 'dtime' => 'DESC'] |
||
4660 | ) ?? self::repo()->findOneBy(['cId' => $ctx['courseId'], 'reflink' => $ref], ['version' => 'DESC']); |
||
4661 | |||
4662 | if (!$latest) { |
||
4663 | continue; |
||
4664 | } |
||
4665 | if ((!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) && (int)$latest->getVisibility() === 0) { |
||
4666 | continue; |
||
4667 | } |
||
4668 | |||
4669 | $rows[] = [ |
||
4670 | self::assignmentIcon((int)$latest->getAssignment()), |
||
4671 | Display::url( |
||
4672 | api_htmlentities($latest->getTitle()), |
||
4673 | $url.'&'.http_build_query(['action' => 'showpage', 'title' => api_htmlentities($ref)]) |
||
4674 | ), |
||
4675 | ]; |
||
4676 | } |
||
4677 | |||
4678 | $table = new SortableTableFromArrayConfig($rows, 1, 10, 'OrphanedPages_table', '', '', 'ASC'); |
||
4679 | $table->set_additional_parameters([ |
||
4680 | 'cid' => $ctx['courseId'], |
||
4681 | 'gid' => $ctx['groupId'], |
||
4682 | 'sid' => $ctx['sessionId'], |
||
4683 | 'action' => Security::remove_XSS($this->action ?? 'orphaned'), |
||
4684 | ]); |
||
4685 | $table->set_header(0, get_lang('Type'), true, ['style' => 'width:48px;']); |
||
4686 | $table->set_header(1, get_lang('Title'), true); |
||
4687 | $table->display(); |
||
4688 | } |
||
4689 | |||
4690 | public function getWantedPages(): void |
||
4691 | { |
||
4692 | $ctx = self::ctx(); |
||
4693 | $url = $ctx['baseUrl']; |
||
4694 | |||
4695 | echo '<div class="actions">'.get_lang('Wanted pages').'</div>'; |
||
4696 | |||
4697 | // Existing page names in context |
||
4698 | $qbPages = self::repo()->createQueryBuilder('w') |
||
4699 | ->select('DISTINCT w.reflink AS reflink') |
||
4700 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
4701 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']); |
||
4702 | if ($ctx['sessionId'] > 0) { |
||
4703 | $qbPages->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
4704 | } else { |
||
4705 | $qbPages->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
4706 | } |
||
4707 | $pages = array_map(fn($r) => (string)$r['reflink'], $qbPages->getQuery()->getArrayResult()); |
||
4708 | |||
4709 | // Latest pages |
||
4710 | $latestList = $this->getLatestPagesForContext(); |
||
4711 | |||
4712 | // Any token in linksto that is not an existing page -> wanted |
||
4713 | $wanted = []; |
||
4714 | foreach ($latestList as $w) { |
||
4715 | $tokens = preg_split('/\s+/', trim((string)$w->getLinksto())) ?: []; |
||
4716 | foreach ($tokens as $t) { |
||
4717 | $t = trim($t); |
||
4718 | if ($t === '') { |
||
4719 | continue; |
||
4720 | } |
||
4721 | if (!in_array($t, $pages, true)) { |
||
4722 | $wanted[] = $t; |
||
4723 | } |
||
4724 | } |
||
4725 | } |
||
4726 | $wanted = array_values(array_unique($wanted)); |
||
4727 | |||
4728 | $rows = []; |
||
4729 | foreach ($wanted as $token) { |
||
4730 | $token = Security::remove_XSS($token); |
||
4731 | $rows[] = [ |
||
4732 | Display::url( |
||
4733 | str_replace('_', ' ', $token), |
||
4734 | $url.'&'.http_build_query(['action' => 'addnew', 'title' => str_replace('_', ' ', $token)]), |
||
4735 | ['class' => 'new_wiki_link'] |
||
4736 | ), |
||
4737 | ]; |
||
4738 | } |
||
4739 | |||
4740 | $table = new SortableTableFromArrayConfig($rows, 0, 10, 'WantedPages_table', '', '', 'ASC'); |
||
4741 | $table->set_additional_parameters([ |
||
4742 | 'cid' => $ctx['courseId'], |
||
4743 | 'gid' => $ctx['groupId'], |
||
4744 | 'sid' => $ctx['sessionId'], |
||
4745 | 'action' => Security::remove_XSS($this->action ?? 'wanted'), |
||
4746 | ]); |
||
4747 | $table->set_header(0, get_lang('Title'), true); |
||
4748 | $table->display(); |
||
4749 | } |
||
4750 | |||
4751 | public function getStats(): bool |
||
4752 | { |
||
4753 | if (!api_is_allowed_to_edit(false, true)) { |
||
4754 | return false; |
||
4755 | } |
||
4756 | |||
4757 | $ctx = self::ctx(); |
||
4758 | echo '<div class="actions">'.get_lang('Statistics').'</div>'; |
||
4759 | |||
4760 | // Pull ALL versions in context (group/session/course) – no visibility filter (teachers) |
||
4761 | $qbAll = self::repo()->createQueryBuilder('w') |
||
4762 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
4763 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']); |
||
4764 | if ($ctx['sessionId'] > 0) { |
||
4765 | $qbAll->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
4766 | } else { |
||
4767 | $qbAll->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
4768 | } |
||
4769 | /** @var CWiki[] $allVersions */ |
||
4770 | $allVersions = $qbAll->getQuery()->getResult(); |
||
4771 | |||
4772 | // Latest version per reflink |
||
4773 | $latestList = $this->getLatestPagesForContext(); |
||
4774 | |||
4775 | // ---- Aggregates across all versions ---- |
||
4776 | $total_versions = count($allVersions); |
||
4777 | $total_visits = 0; |
||
4778 | $total_words = 0; |
||
4779 | $total_links = 0; |
||
4780 | $total_links_anchors = 0; |
||
4781 | $total_links_mail = 0; |
||
4782 | $total_links_ftp = 0; |
||
4783 | $total_links_irc = 0; |
||
4784 | $total_links_news = 0; |
||
4785 | $total_wlinks = 0; |
||
4786 | $total_images = 0; |
||
4787 | $total_flash = 0; |
||
4788 | $total_mp3 = 0; |
||
4789 | $total_flv = 0; |
||
4790 | $total_youtube = 0; |
||
4791 | $total_multimedia = 0; |
||
4792 | $total_tables = 0; |
||
4793 | $total_empty_content = 0; |
||
4794 | $total_comment_version = 0; |
||
4795 | |||
4796 | foreach ($allVersions as $w) { |
||
4797 | $content = (string)$w->getContent(); |
||
4798 | $total_visits += (int)($w->getHits() ?? 0); |
||
4799 | $total_words += (int) self::word_count($content); |
||
4800 | $total_links += substr_count($content, 'href='); |
||
4801 | $total_links_anchors += substr_count($content, 'href="#'); |
||
4802 | $total_links_mail += substr_count($content, 'href="mailto'); |
||
4803 | $total_links_ftp += substr_count($content, 'href="ftp'); |
||
4804 | $total_links_irc += substr_count($content, 'href="irc'); |
||
4805 | $total_links_news += substr_count($content, 'href="news'); |
||
4806 | $total_wlinks += substr_count($content, '[['); |
||
4807 | $total_images += substr_count($content, '<img'); |
||
4808 | $clean_total_flash = preg_replace('/player\.swf/', ' ', $content); |
||
4809 | $total_flash += substr_count((string)$clean_total_flash, '.swf"'); |
||
4810 | $total_mp3 += substr_count($content, '.mp3'); |
||
4811 | $total_flv += (int) (substr_count($content, '.flv') / 5); |
||
4812 | $total_youtube += substr_count($content, 'http://www.youtube.com'); |
||
4813 | $total_multimedia += substr_count($content, 'video/x-msvideo'); |
||
4814 | $total_tables += substr_count($content, '<table'); |
||
4815 | if ($content === '') { |
||
4816 | $total_empty_content++; |
||
4817 | } |
||
4818 | if ((string)$w->getComment() !== '') { |
||
4819 | $total_comment_version++; |
||
4820 | } |
||
4821 | } |
||
4822 | |||
4823 | // ---- Aggregates across latest version per page ---- |
||
4824 | $total_pages = count($latestList); |
||
4825 | $total_visits_lv = 0; |
||
4826 | $total_words_lv = 0; |
||
4827 | $total_links_lv = 0; |
||
4828 | $total_links_anchors_lv = 0; |
||
4829 | $total_links_mail_lv = 0; |
||
4830 | $total_links_ftp_lv = 0; |
||
4831 | $total_links_irc_lv = 0; |
||
4832 | $total_links_news_lv = 0; |
||
4833 | $total_wlinks_lv = 0; |
||
4834 | $total_images_lv = 0; |
||
4835 | $total_flash_lv = 0; |
||
4836 | $total_mp3_lv = 0; |
||
4837 | $total_flv_lv = 0; |
||
4838 | $total_youtube_lv = 0; |
||
4839 | $total_multimedia_lv = 0; |
||
4840 | $total_tables_lv = 0; |
||
4841 | $total_empty_content_lv = 0; |
||
4842 | |||
4843 | $total_editing_now = 0; |
||
4844 | $total_hidden = 0; |
||
4845 | $total_protected = 0; |
||
4846 | $total_lock_disc = 0; |
||
4847 | $total_hidden_disc = 0; |
||
4848 | $total_only_teachers_rating = 0; |
||
4849 | $total_task = 0; |
||
4850 | $total_teacher_assignment = 0; |
||
4851 | $total_student_assignment = 0; |
||
4852 | |||
4853 | $score_sum = 0; |
||
4854 | $progress_sum = 0; |
||
4855 | |||
4856 | foreach ($latestList as $w) { |
||
4857 | $content = (string)$w->getContent(); |
||
4858 | |||
4859 | $total_visits_lv += (int)($w->getHits() ?? 0); |
||
4860 | |||
4861 | $total_words_lv += (int) self::word_count($content); |
||
4862 | $total_links_lv += substr_count($content, 'href='); |
||
4863 | $total_links_anchors_lv += substr_count($content, 'href="#'); |
||
4864 | $total_links_mail_lv += substr_count($content, 'href="mailto'); |
||
4865 | $total_links_ftp_lv += substr_count($content, 'href="ftp'); |
||
4866 | $total_links_irc_lv += substr_count($content, 'href="irc'); |
||
4867 | $total_links_news_lv += substr_count($content, 'href="news'); |
||
4868 | $total_wlinks_lv += substr_count($content, '[['); |
||
4869 | $total_images_lv += substr_count($content, '<img'); |
||
4870 | $clean_total_flash = preg_replace('/player\.swf/', ' ', $content); |
||
4871 | $total_flash_lv += substr_count((string)$clean_total_flash, '.swf"'); |
||
4872 | $total_mp3_lv += substr_count($content, '.mp3'); |
||
4873 | $total_flv_lv += (int) (substr_count($content, '.flv') / 5); |
||
4874 | $total_youtube_lv += substr_count($content, 'http://www.youtube.com'); |
||
4875 | $total_multimedia_lv += substr_count($content, 'video/x-msvideo'); |
||
4876 | $total_tables_lv += substr_count($content, '<table'); |
||
4877 | if ($content === '') { |
||
4878 | $total_empty_content_lv++; |
||
4879 | } |
||
4880 | |||
4881 | // flags/counters from entity fields (latest only) |
||
4882 | if ((int)$w->getIsEditing() !== 0) { |
||
4883 | $total_editing_now++; |
||
4884 | } |
||
4885 | if ((int)$w->getVisibility() === 0) { |
||
4886 | $total_hidden++; |
||
4887 | } |
||
4888 | if ((int)$w->getEditlock() === 1) { |
||
4889 | $total_protected++; |
||
4890 | } |
||
4891 | if ((int)$w->getAddlockDisc() === 0) { |
||
4892 | $total_lock_disc++; |
||
4893 | } |
||
4894 | if ((int)$w->getVisibilityDisc() === 0) { |
||
4895 | $total_hidden_disc++; |
||
4896 | } |
||
4897 | if ((int)$w->getRatinglockDisc() === 0) { |
||
4898 | $total_only_teachers_rating++; |
||
4899 | } |
||
4900 | if ((int)$w->getAssignment() === 1) { |
||
4901 | $total_teacher_assignment++; |
||
4902 | } |
||
4903 | if ((int)$w->getAssignment() === 2) { |
||
4904 | $total_student_assignment++; |
||
4905 | } |
||
4906 | |||
4907 | $conf = self::confRepo()->findOneBy(['cId' => $ctx['courseId'], 'pageId' => (int)$w->getPageId()]); |
||
4908 | if ($conf && (string)$conf->getTask() !== '') { |
||
4909 | $total_task++; |
||
4910 | } |
||
4911 | |||
4912 | $score_sum += (int)($w->getScore() ?? 0); |
||
4913 | $progress_sum += (int)($w->getProgress() ?? 0); |
||
4914 | } |
||
4915 | |||
4916 | $media_score = $total_pages > 0 ? ($score_sum / $total_pages) : 0; |
||
4917 | $media_progress = $total_pages > 0 ? ($progress_sum / $total_pages) : 0; |
||
4918 | |||
4919 | // Student add new pages status (from any latest – addlock is uniform) |
||
4920 | $wiki_add_lock = 0; |
||
4921 | if (!empty($latestList)) { |
||
4922 | $wiki_add_lock = (int)$latestList[0]->getAddlock(); |
||
4923 | } |
||
4924 | $status_add_new_pag = $wiki_add_lock === 1 ? get_lang('Yes') : get_lang('No'); |
||
4925 | |||
4926 | // First and last wiki dates |
||
4927 | $first_wiki_date = ''; |
||
4928 | $last_wiki_date = ''; |
||
4929 | if (!empty($allVersions)) { |
||
4930 | usort($allVersions, fn($a,$b) => $a->getDtime() <=> $b->getDtime()); |
||
4931 | $first_wiki_date = api_get_local_time($allVersions[0]->getDtime()); |
||
4932 | $last_wiki_date = api_get_local_time($allVersions[count($allVersions)-1]->getDtime()); |
||
4933 | } |
||
4934 | |||
4935 | // Total users / total IPs (across all versions) |
||
4936 | $usersSet = []; |
||
4937 | $ipSet = []; |
||
4938 | foreach ($allVersions as $w) { |
||
4939 | $usersSet[(int)$w->getUserId()] = true; |
||
4940 | $ipSet[(string)$w->getUserIp()] = true; |
||
4941 | } |
||
4942 | $total_users = count($usersSet); |
||
4943 | $total_ip = count($ipSet); |
||
4944 | |||
4945 | // ---- Render tables ---- |
||
4946 | |||
4947 | echo '<table class="table table-hover table-striped data_table">'; |
||
4948 | echo '<thead><tr><th colspan="2">'.get_lang('General').'</th></tr></thead>'; |
||
4949 | echo '<tr><td>'.get_lang('StudentAddNewPages').'</td><td>'.$status_add_new_pag.'</td></tr>'; |
||
4950 | echo '<tr><td>'.get_lang('DateCreateOldestWikiPage').'</td><td>'.$first_wiki_date.'</td></tr>'; |
||
4951 | echo '<tr><td>'.get_lang('DateEditLatestWikiVersion').'</td><td>'.$last_wiki_date.'</td></tr>'; |
||
4952 | echo '<tr><td>'.get_lang('AverageScoreAllPages').'</td><td>'.$media_score.' %</td></tr>'; |
||
4953 | echo '<tr><td>'.get_lang('AverageMediaUserProgress').'</td><td>'.$media_progress.' %</td></tr>'; |
||
4954 | echo '<tr><td>'.get_lang('TotalWikiUsers').'</td><td>'.$total_users.'</td></tr>'; |
||
4955 | echo '<tr><td>'.get_lang('TotalIpAdress').'</td><td>'.$total_ip.'</td></tr>'; |
||
4956 | echo '</table><br/>'; |
||
4957 | |||
4958 | echo '<table class="table table-hover table-striped data_table">'; |
||
4959 | echo '<thead><tr><th colspan="2">'.get_lang('Pages').' '.get_lang('And').' '.get_lang('Versions').'</th></tr></thead>'; |
||
4960 | echo '<tr><td>'.get_lang('Pages').' - '.get_lang('NumContributions').'</td><td>'.$total_pages.' ('.get_lang('Versions').': '.$total_versions.')</td></tr>'; |
||
4961 | echo '<tr><td>'.get_lang('EmptyPages').'</td><td>'.$total_empty_content_lv.' ('.get_lang('Versions').': '.$total_empty_content.')</td></tr>'; |
||
4962 | echo '<tr><td>'.get_lang('NumAccess').'</td><td>'.$total_visits_lv.' ('.get_lang('Versions').': '.$total_visits.')</td></tr>'; |
||
4963 | echo '<tr><td>'.get_lang('TotalPagesEditedAtThisTime').'</td><td>'.$total_editing_now.'</td></tr>'; |
||
4964 | echo '<tr><td>'.get_lang('TotalHiddenPages').'</td><td>'.$total_hidden.'</td></tr>'; |
||
4965 | echo '<tr><td>'.get_lang('NumProtectedPages').'</td><td>'.$total_protected.'</td></tr>'; |
||
4966 | echo '<tr><td>'.get_lang('LockedDiscussPages').'</td><td>'.$total_lock_disc.'</td></tr>'; |
||
4967 | echo '<tr><td>'.get_lang('HiddenDiscussPages').'</td><td>'.$total_hidden_disc.'</td></tr>'; |
||
4968 | echo '<tr><td>'.get_lang('TotalComments').'</td><td>'.$total_comment_version.'</td></tr>'; |
||
4969 | echo '<tr><td>'.get_lang('TotalOnlyRatingByTeacher').'</td><td>'.$total_only_teachers_rating.'</td></tr>'; |
||
4970 | echo '<tr><td>'.get_lang('TotalRatingPeers').'</td><td>'.max(0, $total_pages - $total_only_teachers_rating).'</td></tr>'; |
||
4971 | echo '<tr><td>'.get_lang('TotalTeacherAssignments').' - '.get_lang('PortfolioMode').'</td><td>'.$total_teacher_assignment.'</td></tr>'; |
||
4972 | echo '<tr><td>'.get_lang('TotalStudentAssignments').' - '.get_lang('PortfolioMode').'</td><td>'.$total_student_assignment.'</td></tr>'; |
||
4973 | echo '<tr><td>'.get_lang('TotalTask').' - '.get_lang('StandardMode').'</td><td>'.$total_task.'</td></tr>'; |
||
4974 | echo '</table><br/>'; |
||
4975 | |||
4976 | echo '<table class="table table-hover table-striped data_table">'; |
||
4977 | echo '<thead>'; |
||
4978 | echo '<tr><th colspan="3">'.get_lang('ContentPagesInfo').'</th></tr>'; |
||
4979 | echo '<tr><td></td><td>'.get_lang('InTheLastVersion').'</td><td>'.get_lang('InAllVersions').'</td></tr>'; |
||
4980 | echo '</thead>'; |
||
4981 | echo '<tr><td>'.get_lang('NumWords').'</td><td>'.$total_words_lv.'</td><td>'.$total_words.'</td></tr>'; |
||
4982 | echo '<tr><td>'.get_lang('NumlinksHtmlImagMedia').'</td>'. |
||
4983 | '<td>'.$total_links_lv.' ('.get_lang('Anchors').':'.$total_links_anchors_lv.', Mail:'.$total_links_mail_lv.', FTP:'.$total_links_ftp_lv.' IRC:'.$total_links_irc_lv.', News:'.$total_links_news_lv.')</td>'. |
||
4984 | '<td>'.$total_links.' ('.get_lang('Anchors').':'.$total_links_anchors.', Mail:'.$total_links_mail.', FTP:'.$total_links_ftp.' IRC:'.$total_links_irc.', News:'.$total_links_news.')</td></tr>'; |
||
4985 | echo '<tr><td>'.get_lang('NumWikilinks').'</td><td>'.$total_wlinks_lv.'</td><td>'.$total_wlinks.'</td></tr>'; |
||
4986 | echo '<tr><td>'.get_lang('NumImages').'</td><td>'.$total_images_lv.'</td><td>'.$total_images.'</td></tr>'; |
||
4987 | echo '<tr><td>'.get_lang('NumFlash').'</td><td>'.$total_flash_lv.'</td><td>'.$total_flash.'</td></tr>'; |
||
4988 | echo '<tr><td>'.get_lang('NumMp3').'</td><td>'.$total_mp3_lv.'</td><td>'.$total_mp3.'</td></tr>'; |
||
4989 | echo '<tr><td>'.get_lang('NumFlvVideo').'</td><td>'.$total_flv_lv.'</td><td>'.$total_flv.'</td></tr>'; |
||
4990 | echo '<tr><td>'.get_lang('NumYoutubeVideo').'</td><td>'.$total_youtube_lv.'</td><td>'.$total_youtube.'</td></tr>'; |
||
4991 | echo '<tr><td>'.get_lang('NumOtherAudioVideo').'</td><td>'.$total_multimedia_lv.'</td><td>'.$total_multimedia.'</td></tr>'; |
||
4992 | echo '<tr><td>'.get_lang('NumTables').'</td><td>'.$total_tables_lv.'</td><td>'.$total_tables.'</td></tr>'; |
||
4993 | echo '</table>'; |
||
4994 | |||
4995 | return true; |
||
4996 | } |
||
4997 | |||
4998 | public function getStatsTable(): void |
||
4999 | { |
||
5000 | $ctx = self::ctx(); |
||
5001 | $url = $ctx['baseUrl']; |
||
5002 | |||
5003 | // Breadcrumb |
||
5004 | echo '<div class="wiki-bc-wrap"> |
||
5005 | <nav aria-label="breadcrumb"> |
||
5006 | <ol class="breadcrumb breadcrumb--wiki"> |
||
5007 | <li class="breadcrumb-item"> |
||
5008 | <a href="'. |
||
5009 | $this->url(['action'=>'showpage','title'=>'index']).'">'. |
||
5010 | Display::getMdiIcon(\Chamilo\CoreBundle\Enums\ActionIcon::HOME, 'mdi-inline', null, ICON_SIZE_SMALL, get_lang('Home')). |
||
5011 | '<span>'.get_lang('Wiki').'</span> |
||
5012 | </a> |
||
5013 | </li> |
||
5014 | <li class="breadcrumb-item active" aria-current="page">'. |
||
5015 | Display::getMdiIcon(\Chamilo\CoreBundle\Enums\ActionIcon::VIEW_MORE, 'mdi-inline', null, ICON_SIZE_SMALL, get_lang('More')). |
||
5016 | '<span>'.get_lang('More').'</span> |
||
5017 | </li> |
||
5018 | |||
5019 | <div class="breadcrumb-actions"> |
||
5020 | <a class="btn btn-default btn-xs" href="'.$this->url().'">'. |
||
5021 | Display::getMdiIcon(\Chamilo\CoreBundle\Enums\ActionIcon::BACK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Back')). |
||
5022 | ' '.get_lang('Back').' |
||
5023 | </a> |
||
5024 | </div> |
||
5025 | </ol> |
||
5026 | </nav> |
||
5027 | </div>'; |
||
5028 | |||
5029 | echo '<div class="row wiki-stats-grid">'; |
||
5030 | |||
5031 | // Column: “More” |
||
5032 | echo '<div class="col-sm-6 col-md-4"> |
||
5033 | <div class="panel panel-default"> |
||
5034 | <div class="panel-heading"><strong>'.get_lang('More').'</strong></div> |
||
5035 | <div class="panel-body">'. |
||
5036 | |||
5037 | Display::url( |
||
5038 | Display::getMdiIcon(ActionIcon::ADD_USER, 'ch-tool-icon', null, ICON_SIZE_MEDIUM) |
||
5039 | .' '.get_lang('Most active users'), |
||
5040 | $url.'&action=mactiveusers' |
||
5041 | ). |
||
5042 | Display::url( |
||
5043 | Display::getMdiIcon(ActionIcon::HISTORY, 'ch-tool-icon', null, ICON_SIZE_MEDIUM) |
||
5044 | .' '.get_lang('Most visited pages'), |
||
5045 | $url.'&action=mvisited' |
||
5046 | ). |
||
5047 | Display::url( |
||
5048 | Display::getMdiIcon(ActionIcon::REFRESH, 'ch-tool-icon', null, ICON_SIZE_MEDIUM) |
||
5049 | .' '.get_lang('Most changed pages'), |
||
5050 | $url.'&action=mostchanged' |
||
5051 | ). |
||
5052 | |||
5053 | '</div> |
||
5054 | </div> |
||
5055 | </div>'; |
||
5056 | |||
5057 | // Column: “Pages” |
||
5058 | echo '<div class="col-sm-6 col-md-4"> |
||
5059 | <div class="panel panel-default"> |
||
5060 | <div class="panel-heading"><strong>'.get_lang('Pages').'</strong></div> |
||
5061 | <div class="panel-body">'. |
||
5062 | |||
5063 | Display::url( |
||
5064 | Display::getMdiIcon(ActionIcon::CLOSE, 'ch-tool-icon', null, ICON_SIZE_MEDIUM) |
||
5065 | .' '.get_lang('Orphaned pages'), |
||
5066 | $url.'&action=orphaned' |
||
5067 | ). |
||
5068 | Display::url( |
||
5069 | Display::getMdiIcon(ActionIcon::STAR_OUTLINE, 'ch-tool-icon', null, ICON_SIZE_MEDIUM) |
||
5070 | .' '.get_lang('Wanted pages'), |
||
5071 | $url.'&action=wanted' |
||
5072 | ). |
||
5073 | Display::url( |
||
5074 | Display::getMdiIcon(ActionIcon::LINKS, 'ch-tool-icon', null, ICON_SIZE_MEDIUM) |
||
5075 | .' '.get_lang('Most linked pages'), |
||
5076 | $url.'&action=mostlinked' |
||
5077 | ). |
||
5078 | |||
5079 | '</div> |
||
5080 | </div> |
||
5081 | </div>'; |
||
5082 | |||
5083 | // Column: “Statistics” (admins/teachers) |
||
5084 | echo '<div class="col-sm-12 col-md-4"> |
||
5085 | <div class="panel panel-default"> |
||
5086 | <div class="panel-heading"><strong>'.get_lang('Statistics').'</strong></div> |
||
5087 | <div class="panel-body">'; |
||
5088 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
5089 | echo Display::url( |
||
5090 | Display::getMdiIcon(ActionIcon::INFORMATION, 'ch-tool-icon', null, ICON_SIZE_MEDIUM) |
||
5091 | .' '.get_lang('Statistics'), |
||
5092 | $url.'&action=statistics' |
||
5093 | ); |
||
5094 | } else { |
||
5095 | echo '<span class="text-muted">'.get_lang('No data available').'</span>'; |
||
5096 | } |
||
5097 | echo '</div> |
||
5098 | </div> |
||
5099 | </div>'; |
||
5100 | |||
5101 | echo '</div>'; // row |
||
5102 | } |
||
5103 | |||
5104 | public function getLinks(string $page): void |
||
5105 | { |
||
5106 | $ctx = self::ctx(); |
||
5107 | $url = $ctx['baseUrl']; |
||
5108 | $titleInGet = $_GET['title'] ?? null; |
||
5109 | |||
5110 | if (!$titleInGet) { |
||
5111 | Display::addFlash(Display::return_message(get_lang('MustSelectPage'), 'error', false)); |
||
5112 | return; |
||
5113 | } |
||
5114 | |||
5115 | // Normalize incoming page key (handle "Main page" ↔ index ↔ underscored) |
||
5116 | $raw = html_entity_decode($page, ENT_QUOTES); |
||
5117 | $reflink = WikiManager::normalizeReflink((string) $page); |
||
5118 | $displayTitleLink = WikiManager::displayTokenFor($reflink); |
||
5119 | if ($reflink === 'index') { |
||
5120 | $displayTitleLink = str_replace(' ', '_', get_lang('Home')); |
||
5121 | } |
||
5122 | |||
5123 | // Load the target page (latest) to show its title and assignment icon |
||
5124 | $target = self::repo()->findOneBy( |
||
5125 | ['cId' => $ctx['courseId'], 'reflink' => $reflink, 'groupId' => (int)$ctx['groupId'], 'sessionId' => (int)$ctx['sessionId']], |
||
5126 | ['version' => 'DESC', 'dtime' => 'DESC'] |
||
5127 | ) ?? self::repo()->findOneBy(['cId' => $ctx['courseId'], 'reflink' => $reflink], ['version' => 'DESC']); |
||
5128 | if (!$target) { |
||
5129 | Display::addFlash(Display::return_message(get_lang('Must select a page'), 'error', false)); |
||
5130 | return; |
||
5131 | } |
||
5132 | |||
5133 | $assignmentIcon = self::assignmentIcon((int)$target->getAssignment()); |
||
5134 | |||
5135 | echo '<div id="wikititle">' |
||
5136 | .get_lang('LinksPagesFrom').": {$assignmentIcon} " |
||
5137 | .Display::url( |
||
5138 | api_htmlentities($target->getTitle()), |
||
5139 | $url.'&'.http_build_query(['action' => 'showpage', 'title' => api_htmlentities($reflink)]) |
||
5140 | ) |
||
5141 | .'</div>'; |
||
5142 | |||
5143 | // Build list of latest pages in context, then filter those whose linksto contains the token |
||
5144 | $latestList = $this->getLatestPagesForContext(); |
||
5145 | $token = (string)$displayTitleLink; // tokens in linksto are space-separated |
||
5146 | |||
5147 | $rows = []; |
||
5148 | foreach ($latestList as $w) { |
||
5149 | // Visibility gate for students |
||
5150 | if (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) { |
||
5151 | if ((int)$w->getVisibility() !== 1) { |
||
5152 | continue; |
||
5153 | } |
||
5154 | } |
||
5155 | |||
5156 | // match token in linksto (space-separated) |
||
5157 | $tokens = preg_split('/\s+/', trim((string)$w->getLinksto())) ?: []; |
||
5158 | if (!in_array($token, $tokens, true)) { |
||
5159 | continue; |
||
5160 | } |
||
5161 | |||
5162 | // Row build |
||
5163 | $userinfo = api_get_user_info($w->getUserId()); |
||
5164 | $author = $userinfo !== false |
||
5165 | ? UserManager::getUserProfileLink($userinfo) |
||
5166 | : get_lang('Anonymous').' ('.api_htmlentities((string)$w->getUserIp()).')'; |
||
5167 | |||
5168 | $rows[] = [ |
||
5169 | self::assignmentIcon((int)$w->getAssignment()), |
||
5170 | Display::url( |
||
5171 | api_htmlentities($w->getTitle()), |
||
5172 | $url.'&'.http_build_query(['action' => 'showpage', 'title' => api_htmlentities($w->getReflink())]) |
||
5173 | ), |
||
5174 | $author, |
||
5175 | api_get_local_time($w->getDtime()), |
||
5176 | ]; |
||
5177 | } |
||
5178 | |||
5179 | if (empty($rows)) { |
||
5180 | return; |
||
5181 | } |
||
5182 | |||
5183 | $table = new SortableTableFromArrayConfig($rows, 1, 10, 'AllPages_table', '', '', 'ASC'); |
||
5184 | $table->set_additional_parameters([ |
||
5185 | 'cid' => $ctx['courseId'], |
||
5186 | 'gid' => $ctx['groupId'], |
||
5187 | 'sid' => $ctx['sessionId'], |
||
5188 | 'action' => Security::remove_XSS($this->action ?? 'links'), |
||
5189 | ]); |
||
5190 | $table->set_header(0, get_lang('Type'), true, ['style' => 'width:30px;']); |
||
5191 | $table->set_header(1, get_lang('Title'), true); |
||
5192 | $table->set_header(2, get_lang('Author'), true); |
||
5193 | $table->set_header(3, get_lang('Date'), true); |
||
5194 | $table->display(); |
||
5195 | } |
||
5196 | |||
5197 | public function exportTo(int $id, string $format = 'doc'): bool |
||
5198 | { |
||
5199 | $page = self::repo()->findOneBy(['iid' => $id]); |
||
5200 | if ($page instanceof CWiki) { |
||
5201 | $content = (string)$page->getContent(); |
||
5202 | $name = (string)$page->getReflink(); |
||
5203 | if ($content !== '') { |
||
5204 | Export::htmlToOdt($content, $name, $format); |
||
5205 | return true; |
||
5206 | } |
||
5207 | return false; |
||
5208 | } |
||
5209 | |||
5210 | if (method_exists($this, 'getWikiDataFromDb')) { |
||
5211 | $data = self::getWikiDataFromDb($id); |
||
5212 | if (!empty($data['content'])) { |
||
5213 | Export::htmlToOdt($data['content'], (string)$data['reflink'], $format); |
||
5214 | return true; |
||
5215 | } |
||
5216 | } |
||
5217 | |||
5218 | return false; |
||
5219 | } |
||
5220 | |||
5221 | public function export_to_pdf(int $id, string $course_code): bool |
||
5222 | { |
||
5223 | if (!api_is_platform_admin() && api_get_setting('students_export2pdf') !== 'true') { |
||
5224 | Display::addFlash( |
||
5225 | Display::return_message(get_lang('PDFDownloadNotAllowedForStudents'), 'error', false) |
||
5226 | ); |
||
5227 | return false; |
||
5228 | } |
||
5229 | |||
5230 | $page = self::repo()->findOneBy(['iid' => $id]); |
||
5231 | $titleRaw = ''; |
||
5232 | $contentRaw = ''; |
||
5233 | |||
5234 | if ($page instanceof CWiki) { |
||
5235 | $titleRaw = (string) $page->getTitle(); |
||
5236 | $contentRaw = (string) $page->getContent(); |
||
5237 | } elseif (method_exists($this, 'getWikiDataFromDb')) { |
||
5238 | $data = (array) self::getWikiDataFromDb($id); |
||
5239 | $titleRaw = (string) ($data['title'] ?? ''); |
||
5240 | $contentRaw = (string) ($data['content'] ?? ''); |
||
5241 | } |
||
5242 | |||
5243 | if ($titleRaw === '' && $contentRaw === '') { |
||
5244 | Display::addFlash(Display::return_message(get_lang('NoSearchResults'), 'error', false)); |
||
5245 | return false; |
||
5246 | } |
||
5247 | |||
5248 | $this->renderPdfFromHtmlDirect($titleRaw, $contentRaw, $course_code); |
||
5249 | return true; |
||
5250 | } |
||
5251 | |||
5252 | /** |
||
5253 | * Render PDF directly using mPDF (preferred) or Dompdf (fallback). |
||
5254 | * If neither is installed, fall back to direct HTML download. |
||
5255 | */ |
||
5256 | private function renderPdfFromHtmlDirect(string $title, string $content, string $courseCode): void |
||
5257 | { |
||
5258 | // Minimal safe print CSS (UTF-8, supports DejaVu Sans for wide Unicode) |
||
5259 | $css = ' |
||
5260 | body{font-family:"DejaVu Sans",Arial,Helvetica,sans-serif;font-size:12pt;line-height:1.45;color:#222;margin:16px;} |
||
5261 | h1,h2,h3{margin:0 0 10px;} |
||
5262 | h1{font-size:20pt} h2{font-size:16pt} h3{font-size:14pt} |
||
5263 | p{margin:0 0 8px;} img{max-width:100%;height:auto;} |
||
5264 | .wiki-title{font-weight:bold;margin-bottom:12px;border-bottom:1px solid #ddd;padding-bottom:6px;} |
||
5265 | .wiki-content{margin-top:8px;} |
||
5266 | table{border-collapse:collapse} td,th{border:1px solid #ddd;padding:4px} |
||
5267 | pre,code{font-family:Menlo,Consolas,monospace;font-size:10pt;white-space:pre-wrap;word-wrap:break-word} |
||
5268 | '; |
||
5269 | |||
5270 | // Fix relative course media inside content to absolute URLs |
||
5271 | if (defined('REL_COURSE_PATH') && defined('WEB_COURSE_PATH')) { |
||
5272 | if (api_strpos($content, '../..'.api_get_path(REL_COURSE_PATH)) !== false) { |
||
5273 | $content = str_replace('../..'.api_get_path(REL_COURSE_PATH), api_get_path(WEB_COURSE_PATH), $content); |
||
5274 | } |
||
5275 | } |
||
5276 | |||
5277 | // Sanitize title for document/file names |
||
5278 | $safeTitle = trim($title) !== '' ? $title : 'wiki_page'; |
||
5279 | $downloadName = preg_replace('/\s+/', '_', (string) api_replace_dangerous_char($safeTitle)).'.pdf'; |
||
5280 | |||
5281 | // Wrap content (keep structure simple for HTML→PDF engines) |
||
5282 | $html = '<!DOCTYPE html><html lang="'.htmlspecialchars(api_get_language_isocode()).'"><head>' |
||
5283 | .'<meta charset="'.htmlspecialchars(api_get_system_encoding()).'">' |
||
5284 | .'<title>'.htmlspecialchars($safeTitle).'</title>' |
||
5285 | .'<style>'.$css.'</style>' |
||
5286 | .'</head><body>' |
||
5287 | .'<div class="wiki-title"><h1>'.htmlspecialchars($safeTitle).'</h1></div>' |
||
5288 | .'<div class="wiki-content">'.$content.'</div>' |
||
5289 | .'</body></html>'; |
||
5290 | |||
5291 | // --- Try mPDF first --- |
||
5292 | if (class_exists('\\Mpdf\\Mpdf')) { |
||
5293 | // Use mPDF directly |
||
5294 | try { |
||
5295 | $mpdf = new \Mpdf\Mpdf([ |
||
5296 | 'tempDir' => sys_get_temp_dir(), |
||
5297 | 'mode' => 'utf-8', |
||
5298 | 'format' => 'A4', |
||
5299 | 'margin_left' => 12, |
||
5300 | 'margin_right' => 12, |
||
5301 | 'margin_top' => 12, |
||
5302 | 'margin_bottom' => 12, |
||
5303 | ]); |
||
5304 | $mpdf->SetTitle($safeTitle); |
||
5305 | $mpdf->WriteHTML($html); |
||
5306 | // Force download |
||
5307 | $mpdf->Output($downloadName, 'D'); |
||
5308 | exit; |
||
5309 | } catch (\Throwable $e) { |
||
5310 | // Continue to next engine |
||
5311 | } |
||
5312 | } |
||
5313 | |||
5314 | // --- Try Dompdf fallback --- |
||
5315 | if (class_exists('\\Dompdf\\Dompdf')) { |
||
5316 | try { |
||
5317 | $dompdf = new \Dompdf\Dompdf([ |
||
5318 | 'chroot' => realpath(__DIR__.'/../../..'), |
||
5319 | 'isRemoteEnabled' => true, |
||
5320 | ]); |
||
5321 | $dompdf->loadHtml($html, 'UTF-8'); |
||
5322 | $dompdf->setPaper('A4', 'portrait'); |
||
5323 | $dompdf->render(); |
||
5324 | $dompdf->stream($downloadName, ['Attachment' => true]); |
||
5325 | exit; |
||
5326 | } catch (\Throwable $e) { |
||
5327 | // Continue to final fallback |
||
5328 | } |
||
5329 | } |
||
5330 | |||
5331 | // --- Final fallback: deliver HTML as download (not PDF) --- |
||
5332 | // Clean buffers to avoid header issues |
||
5333 | if (function_exists('ob_get_level')) { |
||
5334 | while (ob_get_level() > 0) { @ob_end_clean(); } |
||
5335 | } |
||
5336 | $htmlName = preg_replace('/\.pdf$/i', '.html', $downloadName); |
||
5337 | header('Content-Type: text/html; charset='.api_get_system_encoding()); |
||
5338 | header('Content-Disposition: attachment; filename="'.$htmlName.'"'); |
||
5339 | header('X-Content-Type-Options: nosniff'); |
||
5340 | header('Cache-Control: no-store, no-cache, must-revalidate'); |
||
5341 | header('Pragma: no-cache'); |
||
5342 | echo $html; |
||
5343 | exit; |
||
5344 | } |
||
5345 | |||
5346 | public function getActiveUsers(string $action): void |
||
5347 | { |
||
5348 | echo '<div class="actions">'.get_lang('Most active users').'</div>'; |
||
5349 | |||
5350 | $courseId = $this->currentCourseId(); |
||
5351 | $groupId = $this->currentGroupId(); |
||
5352 | $sessionId = $this->currentSessionId(); |
||
5353 | |||
5354 | $data = $this->wikiRepo->countEditsByUser($courseId, $groupId, $sessionId); |
||
5355 | |||
5356 | if (!$data) { |
||
5357 | return; |
||
5358 | } |
||
5359 | |||
5360 | $rows = []; |
||
5361 | foreach ($data as $row) { |
||
5362 | $userId = (int) $row['userId']; |
||
5363 | $userIp = (string) ($row['userIp'] ?? ''); |
||
5364 | $numEdits = (int) $row['numEdits']; |
||
5365 | |||
5366 | $userInfo = $userId > 0 ? api_get_user_info($userId) : false; |
||
5367 | |||
5368 | $authorCell = ($userId !== 0 && $userInfo !== false) |
||
5369 | ? Display::url( |
||
5370 | $userInfo['complete_name_with_username'], |
||
5371 | $this->wikiUrl(['action' => 'usercontrib', 'user_id' => $userId]) |
||
5372 | ) |
||
5373 | : get_lang('Anonymous').' ('.api_htmlentities($userIp).')'; |
||
5374 | |||
5375 | $rows[] = [ |
||
5376 | $authorCell, |
||
5377 | Display::url( |
||
5378 | (string) $numEdits, |
||
5379 | $this->wikiUrl(['action' => 'usercontrib', 'user_id' => $userId]) |
||
5380 | ), |
||
5381 | ]; |
||
5382 | } |
||
5383 | |||
5384 | $table = new SortableTableFromArrayConfig( |
||
5385 | $rows, |
||
5386 | 1, |
||
5387 | 10, |
||
5388 | 'MostActiveUsersA_table', |
||
5389 | '', |
||
5390 | '', |
||
5391 | 'DESC' |
||
5392 | ); |
||
5393 | $table->set_additional_parameters([ |
||
5394 | 'cid' => $_GET['cid'] ?? null, |
||
5395 | 'gid' => $_GET['gid'] ?? null, |
||
5396 | 'sid' => $_GET['sid'] ?? null, |
||
5397 | 'action' => Security::remove_XSS($action), |
||
5398 | ]); |
||
5399 | $table->set_header(0, get_lang('Author'), true); |
||
5400 | $table->set_header(1, get_lang('Contributions'), true, ['style' => 'width:30px;']); |
||
5401 | $table->display(); |
||
5402 | } |
||
5403 | |||
5404 | /** |
||
5405 | * Check & toggle “notify me by email” for a discussion. |
||
5406 | * Returns current status: 0 (off) / 1 (on). |
||
5407 | */ |
||
5408 | public function checkNotifyDiscuss(string $reflink): int |
||
5409 | { |
||
5410 | $ctx = self::ctx(); |
||
5411 | $conn = $this->conn(); |
||
5412 | $tblMailcue = $this->tblWikiMailcue(); |
||
5413 | $linkCol = $this->mailcueLinkColumn(); |
||
5414 | $userId = (int) api_get_user_id(); |
||
5415 | $versionId = $this->firstVersionIdByReflink($reflink); |
||
5416 | |||
5417 | if (!$versionId) { |
||
5418 | // If the page has no versions yet, there is nothing to toggle |
||
5419 | return 0; |
||
5420 | } |
||
5421 | |||
5422 | // Read current status |
||
5423 | $count = (int) $conn->fetchOne( |
||
5424 | 'SELECT COUNT(*) FROM '.$tblMailcue.' |
||
5425 | WHERE c_id = :cid |
||
5426 | AND '.$linkCol.' = :vid |
||
5427 | AND user_id = :uid |
||
5428 | AND type = :type |
||
5429 | AND COALESCE(group_id,0) = :gid |
||
5430 | AND COALESCE(session_id,0) = :sid', |
||
5431 | [ |
||
5432 | 'cid' => (int)$ctx['courseId'], |
||
5433 | 'vid' => $versionId, |
||
5434 | 'uid' => $userId, |
||
5435 | 'type' => 'D', |
||
5436 | 'gid' => (int)$ctx['groupId'], |
||
5437 | 'sid' => (int)$ctx['sessionId'], |
||
5438 | ] |
||
5439 | ); |
||
5440 | |||
5441 | $status = $count > 0 ? 1 : 0; |
||
5442 | |||
5443 | // Toggle based on actionpage |
||
5444 | $actionPage = $_GET['actionpage'] ?? null; |
||
5445 | |||
5446 | if ($actionPage === 'locknotifydisc' && $status === 0) { |
||
5447 | // Turn ON |
||
5448 | $conn->insert($tblMailcue, [ |
||
5449 | 'c_id' => (int)$ctx['courseId'], |
||
5450 | $linkCol => $versionId, |
||
5451 | 'user_id' => $userId, |
||
5452 | 'type' => 'D', |
||
5453 | 'group_id' => (int)$ctx['groupId'], |
||
5454 | 'session_id' => (int)$ctx['sessionId'], |
||
5455 | ]); |
||
5456 | $status = 1; |
||
5457 | } elseif ($actionPage === 'unlocknotifydisc' && $status === 1) { |
||
5458 | // Turn OFF |
||
5459 | $conn->executeStatement( |
||
5460 | 'DELETE FROM '.$tblMailcue.' |
||
5461 | WHERE c_id = :cid |
||
5462 | AND '.$linkCol.' = :vid |
||
5463 | AND user_id = :uid |
||
5464 | AND type = :type |
||
5465 | AND COALESCE(group_id,0) = :gid |
||
5466 | AND COALESCE(session_id,0) = :sid', |
||
5467 | [ |
||
5468 | 'cid' => (int)$ctx['courseId'], |
||
5469 | 'vid' => $versionId, |
||
5470 | 'uid' => $userId, |
||
5471 | 'type' => 'D', |
||
5472 | 'gid' => (int)$ctx['groupId'], |
||
5473 | 'sid' => (int)$ctx['sessionId'], |
||
5474 | ] |
||
5475 | ); |
||
5476 | $status = 0; |
||
5477 | } |
||
5478 | |||
5479 | return $status; |
||
5480 | } |
||
5481 | |||
5482 | /** |
||
5483 | * Build the Category create/edit form (Doctrine, Chamilo FormValidator). |
||
5484 | */ |
||
5485 | private function createCategoryForm(?CWikiCategory $category = null): FormValidator |
||
5486 | { |
||
5487 | $em = Container::getEntityManager(); |
||
5488 | $categoryRepo = $em->getRepository(CWikiCategory::class); |
||
5489 | |||
5490 | $course = api_get_course_entity(); |
||
5491 | $session = api_get_session_entity(); |
||
5492 | |||
5493 | // List of categories available in this course/session |
||
5494 | $categories = $categoryRepo->findByCourse($course, $session); |
||
5495 | |||
5496 | // Action URL using our url() helper (adds cidreq safely) |
||
5497 | $form = new FormValidator( |
||
5498 | 'category', |
||
5499 | 'post', |
||
5500 | $this->url(['action' => 'category', 'id' => $category ? $category->getId() : null]) |
||
5501 | ); |
||
5502 | |||
5503 | $form->addHeader(get_lang('AddCategory')); |
||
5504 | // attributes array MUST be provided (empty array ok) |
||
5505 | $form->addSelectFromCollection('parent', get_lang('Parent'), $categories, [], true, 'getNodeName'); |
||
5506 | $form->addText('name', get_lang('Name')); |
||
5507 | |||
5508 | if ($category) { |
||
5509 | $form->addButtonUpdate(get_lang('Update')); |
||
5510 | } else { |
||
5511 | $form->addButtonSave(get_lang('Save')); |
||
5512 | } |
||
5513 | |||
5514 | if ($form->validate()) { |
||
5515 | $values = $form->exportValues(); |
||
5516 | $parent = !empty($values['parent']) ? $categoryRepo->find((int)$values['parent']) : null; |
||
5517 | |||
5518 | if (!$category) { |
||
5519 | $category = (new CWikiCategory()) |
||
5520 | ->setCourse($course) |
||
5521 | ->setSession($session); |
||
5522 | $em->persist($category); |
||
5523 | |||
5524 | Display::addFlash(Display::return_message(get_lang('CategoryAdded'), 'success')); |
||
5525 | } else { |
||
5526 | Display::addFlash(Display::return_message(get_lang('CategoryEdited'), 'success')); |
||
5527 | } |
||
5528 | |||
5529 | $category |
||
5530 | ->setName((string)$values['name']) |
||
5531 | ->setParent($parent); |
||
5532 | |||
5533 | $em->flush(); |
||
5534 | |||
5535 | header('Location: '.$this->url(['action' => 'category'])); |
||
5536 | exit; |
||
5537 | } |
||
5538 | |||
5539 | if ($category) { |
||
5540 | $form->setDefaults([ |
||
5541 | 'parent' => $category->getParent() ? $category->getParent()->getId() : 0, |
||
5542 | 'name' => $category->getName(), |
||
5543 | ]); |
||
5544 | } |
||
5545 | |||
5546 | return $form; |
||
5547 | } |
||
5548 | |||
5549 | /** |
||
5550 | * Discussion screen for a wiki page (Doctrine/DBAL). |
||
5551 | */ |
||
5552 | public function getDiscuss(string $page): void |
||
5553 | { |
||
5554 | $ctx = self::ctx(api_get_course_int_id(), api_get_session_id(), api_get_group_id()); |
||
5555 | $em = Container::getEntityManager(); |
||
5556 | $conn = $em->getConnection(); |
||
5557 | |||
5558 | // Session restriction |
||
5559 | if ($ctx['sessionId'] !== 0 && api_is_allowed_to_session_edit(false, true) === false) { |
||
5560 | api_not_allowed(); |
||
5561 | return; |
||
5562 | } |
||
5563 | |||
5564 | if (empty($_GET['title'])) { |
||
5565 | Display::addFlash(Display::return_message(get_lang('MustSelectPage'), 'error', false)); |
||
5566 | return; |
||
5567 | } |
||
5568 | |||
5569 | $pageKey = self::normalizeReflink($page); |
||
5570 | $actionPage = $_GET['actionpage'] ?? null; |
||
5571 | |||
5572 | // --- Inline toggles (PRG) --- |
||
5573 | if ($actionPage) { |
||
5574 | $cid = (int)$ctx['courseId']; |
||
5575 | $gid = (int)$ctx['groupId']; |
||
5576 | $sid = (int)$ctx['sessionId']; |
||
5577 | $uid = (int)api_get_user_id(); |
||
5578 | |||
5579 | $predG = 'COALESCE(group_id,0) = :gid'; |
||
5580 | $predS = 'COALESCE(session_id,0) = :sid'; |
||
5581 | |||
5582 | switch ($actionPage) { |
||
5583 | case 'lockdisc': |
||
5584 | $conn->executeStatement( |
||
5585 | "UPDATE c_wiki SET addlock_disc = 0 |
||
5586 | WHERE c_id = :cid AND reflink = :r AND $predG AND $predS", |
||
5587 | ['cid'=>$cid,'r'=>$pageKey,'gid'=>$gid,'sid'=>$sid] |
||
5588 | ); |
||
5589 | break; |
||
5590 | case 'unlockdisc': |
||
5591 | $conn->executeStatement( |
||
5592 | "UPDATE c_wiki SET addlock_disc = 1 |
||
5593 | WHERE c_id = :cid AND reflink = :r AND $predG AND $predS", |
||
5594 | ['cid'=>$cid,'r'=>$pageKey,'gid'=>$gid,'sid'=>$sid] |
||
5595 | ); |
||
5596 | break; |
||
5597 | case 'hidedisc': |
||
5598 | $conn->executeStatement( |
||
5599 | "UPDATE c_wiki SET visibility_disc = 0 |
||
5600 | WHERE c_id = :cid AND reflink = :r AND $predG AND $predS", |
||
5601 | ['cid'=>$cid,'r'=>$pageKey,'gid'=>$gid,'sid'=>$sid] |
||
5602 | ); |
||
5603 | break; |
||
5604 | case 'showdisc': |
||
5605 | $conn->executeStatement( |
||
5606 | "UPDATE c_wiki SET visibility_disc = 1 |
||
5607 | WHERE c_id = :cid AND reflink = :r AND $predG AND $predS", |
||
5608 | ['cid'=>$cid,'r'=>$pageKey,'gid'=>$gid,'sid'=>$sid] |
||
5609 | ); |
||
5610 | break; |
||
5611 | case 'lockrating': |
||
5612 | $conn->executeStatement( |
||
5613 | "UPDATE c_wiki SET ratinglock_disc = 0 |
||
5614 | WHERE c_id = :cid AND reflink = :r AND $predG AND $predS", |
||
5615 | ['cid'=>$cid,'r'=>$pageKey,'gid'=>$gid,'sid'=>$sid] |
||
5616 | ); |
||
5617 | break; |
||
5618 | case 'unlockrating': |
||
5619 | $conn->executeStatement( |
||
5620 | "UPDATE c_wiki SET ratinglock_disc = 1 |
||
5621 | WHERE c_id = :cid AND reflink = :r AND $predG AND $predS", |
||
5622 | ['cid'=>$cid,'r'=>$pageKey,'gid'=>$gid,'sid'=>$sid] |
||
5623 | ); |
||
5624 | break; |
||
5625 | case 'locknotifydisc': |
||
5626 | if (api_is_allowed_to_session_edit()) { |
||
5627 | $t = 'watchdisc:'.$pageKey; |
||
5628 | $conn->executeStatement( |
||
5629 | "INSERT INTO c_wiki_mailcue (c_id, group_id, session_id, user_id, type) |
||
5630 | SELECT :cid, :gid, :sid, :uid, :t FROM DUAL |
||
5631 | WHERE NOT EXISTS ( |
||
5632 | SELECT 1 FROM c_wiki_mailcue |
||
5633 | WHERE c_id=:cid AND $predG AND $predS AND user_id=:uid AND type=:t |
||
5634 | )", |
||
5635 | ['cid'=>$cid,'gid'=>$gid,'sid'=>$sid,'uid'=>$uid,'t'=>$t] |
||
5636 | ); |
||
5637 | } |
||
5638 | break; |
||
5639 | case 'unlocknotifydisc': |
||
5640 | if (api_is_allowed_to_session_edit()) { |
||
5641 | $t = 'watchdisc:'.$pageKey; |
||
5642 | $conn->executeStatement( |
||
5643 | "DELETE FROM c_wiki_mailcue |
||
5644 | WHERE c_id=:cid AND $predG AND $predS AND user_id=:uid AND type=:t", |
||
5645 | ['cid'=>$cid,'gid'=>$gid,'sid'=>$sid,'uid'=>$uid,'t'=>$t] |
||
5646 | ); |
||
5647 | } |
||
5648 | break; |
||
5649 | } |
||
5650 | |||
5651 | header('Location: '.$this->url(['action' => 'discuss', 'title' => $pageKey])); |
||
5652 | exit; |
||
5653 | } |
||
5654 | |||
5655 | /** @var CWiki|null $last */ |
||
5656 | $last = self::repo()->createQueryBuilder('w') |
||
5657 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
5658 | ->andWhere('w.reflink = :reflink')->setParameter('reflink', $pageKey) |
||
5659 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
5660 | ->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']) |
||
5661 | ->orderBy('w.version', 'DESC')->setMaxResults(1) |
||
5662 | ->getQuery()->getOneOrNullResult(); |
||
5663 | |||
5664 | /** @var CWiki|null $first */ |
||
5665 | $first = self::repo()->createQueryBuilder('w') |
||
5666 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
5667 | ->andWhere('w.reflink = :reflink')->setParameter('reflink', $pageKey) |
||
5668 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']) |
||
5669 | ->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']) |
||
5670 | ->orderBy('w.version', 'ASC')->setMaxResults(1) |
||
5671 | ->getQuery()->getOneOrNullResult(); |
||
5672 | |||
5673 | if (!$last || !$first) { |
||
5674 | Display::addFlash(Display::return_message(get_lang('DiscussNotAvailable'), 'normal', false)); |
||
5675 | return; |
||
5676 | } |
||
5677 | |||
5678 | $publicationId = $first->getPageId() ?: (int)$first->getIid(); |
||
5679 | $lastVersionDate = api_get_local_time($last->getDtime()); |
||
5680 | $lastUserInfo = api_get_user_info($last->getUserId()); |
||
5681 | |||
5682 | // New comment (PRG) |
||
5683 | if (isset($_POST['Submit']) && self::double_post($_POST['wpost_id'] ?? '')) { |
||
5684 | $nowUtc = api_get_utc_datetime(); |
||
5685 | $authorId = (int) api_get_user_id(); |
||
5686 | |||
5687 | $conn->insert('c_wiki_discuss', [ |
||
5688 | 'c_id' => $ctx['courseId'], |
||
5689 | 'publication_id' => $publicationId, |
||
5690 | 'userc_id' => $authorId, |
||
5691 | 'comment' => (string)($_POST['comment'] ?? ''), |
||
5692 | 'p_score' => (string)($_POST['rating'] ?? '-'), |
||
5693 | 'dtime' => $nowUtc, |
||
5694 | ]); |
||
5695 | |||
5696 | self::check_emailcue($publicationId, 'D', $nowUtc, $authorId); |
||
5697 | |||
5698 | header('Location: '.$this->url(['action' => 'discuss', 'title' => $pageKey])); |
||
5699 | exit; |
||
5700 | } |
||
5701 | |||
5702 | // Assignment badge |
||
5703 | $iconAssignment = null; |
||
5704 | if ($last->getAssignment() === 1) { |
||
5705 | $iconAssignment = Display::getMdiIcon( |
||
5706 | ActionIcon::WIKI_ASSIGNMENT, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('AssignmentDescExtra') |
||
5707 | ); |
||
5708 | } elseif ($last->getAssignment() === 2) { |
||
5709 | $iconAssignment = Display::getMdiIcon( |
||
5710 | ActionIcon::WIKI_WORK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('AssignmentWorkExtra') |
||
5711 | ); |
||
5712 | } |
||
5713 | |||
5714 | echo '<div class="wiki-discuss"><div class="wd-wrap">'; |
||
5715 | |||
5716 | // Header |
||
5717 | echo '<div class="wd-header">'; |
||
5718 | echo '<div class="wd-titlebox">'; |
||
5719 | echo '<h3 class="wd-title">'.$iconAssignment.' '.api_htmlentities($last->getTitle()).'</h3>'; |
||
5720 | if ($lastUserInfo !== false) { |
||
5721 | echo '<div class="wd-meta">'.get_lang('The latest version was edited by').' ' |
||
5722 | .UserManager::getUserProfileLink($lastUserInfo).' • '.$lastVersionDate.'</div>'; |
||
5723 | } |
||
5724 | echo '</div>'; |
||
5725 | |||
5726 | // Toolbar |
||
5727 | echo '<div class="wd-toolbar">'; |
||
5728 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
5729 | $addOpen = (self::check_addlock_discuss($pageKey) === 1); |
||
5730 | $lockIcon = $addOpen |
||
5731 | ? Display::getMdiIcon(ActionIcon::LOCK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('LockDiscussExtra')) |
||
5732 | : Display::getMdiIcon(ActionIcon::UNLOCK, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('UnlockDiscussExtra')); |
||
5733 | $lockAction = $addOpen ? 'lockdisc' : 'unlockdisc'; |
||
5734 | echo Display::url($lockIcon, $this->url(['action'=>'discuss','actionpage'=>$lockAction,'title'=>$pageKey])); |
||
5735 | } |
||
5736 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
5737 | $isVisible = (self::check_visibility_discuss($pageKey) === 1); |
||
5738 | $visIcon = $isVisible |
||
5739 | ? Display::getMdiIcon(ActionIcon::VISIBLE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Hide')) |
||
5740 | : Display::getMdiIcon(ActionIcon::INVISIBLE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Show')); |
||
5741 | $visAction = $isVisible ? 'hidedisc' : 'showdisc'; |
||
5742 | echo Display::url($visIcon, $this->url(['action'=>'discuss','actionpage'=>$visAction,'title'=>$pageKey])); |
||
5743 | } |
||
5744 | if (api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
5745 | $ratingOn = (self::check_ratinglock_discuss($pageKey) === 1); |
||
5746 | $starIcon = $ratingOn |
||
5747 | ? Display::getMdiIcon(ActionIcon::STAR, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('LockRatingDiscussExtra')) |
||
5748 | : Display::getMdiIcon(ActionIcon::STAR_OUTLINE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('UnlockRatingDiscussExtra')); |
||
5749 | $rateAction = $ratingOn ? 'lockrating' : 'unlockrating'; |
||
5750 | echo Display::url($starIcon, $this->url(['action'=>'discuss','actionpage'=>$rateAction,'title'=>$pageKey])); |
||
5751 | } |
||
5752 | if ($this->mailcueLinkColumn() !== null) { |
||
5753 | $notifyOn = ($this->checkNotifyDiscuss($pageKey) === 1); |
||
5754 | $notifyIcon = $notifyOn |
||
5755 | ? Display::getMdiIcon(ActionIcon::EMAIL_ON, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('CancelNotifyMe')) |
||
5756 | : Display::getMdiIcon(ActionIcon::EMAIL_OFF, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('NotifyMe')); |
||
5757 | $notifyAction = $notifyOn ? 'unlocknotifydisc' : 'locknotifydisc'; |
||
5758 | echo Display::url($notifyIcon, $this->url(['action'=>'discuss','actionpage'=>$notifyAction,'title'=>$pageKey])); |
||
5759 | } |
||
5760 | echo '</div>'; // wd-toolbar |
||
5761 | echo '</div>'; // wd-header |
||
5762 | |||
5763 | // Form |
||
5764 | if ((int)$last->getAddlockDisc() === 1 || api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
5765 | echo '<div class="panel panel-default wd-card"><div class="panel-body">'; |
||
5766 | echo '<form method="post" action="" class="form-horizontal wd-form">'; |
||
5767 | echo '<input type="hidden" name="wpost_id" value="'.api_get_unique_id().'">'; |
||
5768 | |||
5769 | echo '<div class="form-group">'; |
||
5770 | echo '<label class="col-sm-2 control-label">'.get_lang('Comments').'</label>'; |
||
5771 | echo '<div class="col-sm-10"><textarea class="form-control" name="comment" rows="4" placeholder="'.api_htmlentities(get_lang('Comments')).'"></textarea></div>'; |
||
5772 | echo '</div>'; |
||
5773 | |||
5774 | echo '<div class="form-group">'; |
||
5775 | if ((int)$last->getRatinglockDisc() === 1 || api_is_allowed_to_edit(false, true) || api_is_platform_admin()) { |
||
5776 | echo '<label class="col-sm-2 control-label">'.get_lang('Rating').'</label>'; |
||
5777 | echo '<div class="col-sm-10"><select name="rating" class="form-control wd-rating">'; |
||
5778 | echo '<option value="-" selected>-</option>'; |
||
5779 | for ($i=0; $i<=10; $i++) { echo '<option value="'.$i.'">'.$i.'</option>'; } |
||
5780 | echo '</select></div>'; |
||
5781 | } else { |
||
5782 | echo '<input type="hidden" name="rating" value="-">'; |
||
5783 | // Select disabled para mantener alineación |
||
5784 | echo '<label class="col-sm-2 control-label">'.get_lang('Rating').'</label>'; |
||
5785 | echo '<div class="col-sm-10"><select class="form-control wd-rating" disabled><option>-</option></select></div>'; |
||
5786 | } |
||
5787 | echo '</div>'; |
||
5788 | |||
5789 | echo '<div class="form-group"><div class="col-sm-offset-2 col-sm-10">'; |
||
5790 | echo '<button class="btn btn--primary" type="submit" name="Submit">'.get_lang('Send').'</button>'; |
||
5791 | echo '</div></div>'; |
||
5792 | |||
5793 | echo '</form>'; |
||
5794 | echo '</div></div>'; |
||
5795 | } |
||
5796 | |||
5797 | // Stats |
||
5798 | $comments = $conn->executeQuery( |
||
5799 | "SELECT d.* FROM c_wiki_discuss d |
||
5800 | WHERE d.c_id = :cid AND d.publication_id = :pid |
||
5801 | ORDER BY d.iid DESC", |
||
5802 | ['cid'=>$ctx['courseId'], 'pid'=>$publicationId] |
||
5803 | )->fetchAllAssociative(); |
||
5804 | |||
5805 | $countAll = count($comments); |
||
5806 | $scoredRows = (int)$conn->fetchOne( |
||
5807 | "SELECT COUNT(*) FROM c_wiki_discuss |
||
5808 | WHERE c_id = :cid AND publication_id = :pid AND p_score <> '-'", |
||
5809 | ['cid'=>$ctx['courseId'], 'pid'=>$publicationId] |
||
5810 | ); |
||
5811 | $sumRow = $conn->fetchAssociative( |
||
5812 | "SELECT SUM(CASE WHEN p_score <> '-' THEN p_score END) AS sumWPost |
||
5813 | FROM c_wiki_discuss WHERE c_id = :cid AND publication_id = :pid", |
||
5814 | ['cid'=>$ctx['courseId'], 'pid'=>$publicationId] |
||
5815 | ); |
||
5816 | $avgNumeric = ($scoredRows > 0) ? (float)$sumRow['sumWPost'] / $scoredRows : 0.0; |
||
5817 | |||
5818 | echo '<div class="wd-stats">'; |
||
5819 | echo '<span class="label label-default">'.get_lang('Comments on this page').': '.$countAll.'</span>'; |
||
5820 | echo '<span class="label label-default">'.get_lang('Number of comments scored').': '.$scoredRows.'</span>'; |
||
5821 | echo '<span class="label label-default">'.get_lang('The average rating for the page is').': '.number_format($avgNumeric, 2).' / 10</span>'; |
||
5822 | echo '</div>'; |
||
5823 | |||
5824 | // Persist score on wiki rows |
||
5825 | $conn->executeStatement( |
||
5826 | "UPDATE c_wiki SET score = :score |
||
5827 | WHERE c_id = :cid AND reflink = :reflink |
||
5828 | AND COALESCE(group_id,0) = :gid |
||
5829 | AND COALESCE(session_id,0) = :sid", |
||
5830 | [ |
||
5831 | 'score' => $avgNumeric, |
||
5832 | 'cid' => $ctx['courseId'], |
||
5833 | 'reflink' => $pageKey, |
||
5834 | 'gid' => (int)$ctx['groupId'], |
||
5835 | 'sid' => (int)$ctx['sessionId'], |
||
5836 | ] |
||
5837 | ); |
||
5838 | |||
5839 | // Comments list |
||
5840 | if ($countAll === 0) { |
||
5841 | echo '<div class="well wd-empty">'.get_lang('NoSearchResults').'</div>'; |
||
5842 | } else { |
||
5843 | foreach ($comments as $c) { |
||
5844 | $uInfo = api_get_user_info((int)$c['userc_id']); |
||
5845 | $name = $uInfo ? $uInfo['complete_name'] : get_lang('Anonymous'); |
||
5846 | $status = ($uInfo && (string)$uInfo['status'] === '5') ? get_lang('Student') : get_lang('Teacher'); |
||
5847 | |||
5848 | $photo = ($uInfo && !empty($uInfo['avatar'])) |
||
5849 | ? '<img class="wd-avatar" src="'.$uInfo['avatar'].'" alt="'.api_htmlentities($name).'">' |
||
5850 | : '<div class="wd-avatar wd-avatar--ph"></div>'; |
||
5851 | |||
5852 | $score = (string)$c['p_score']; |
||
5853 | $stars = ''; |
||
5854 | if ($score !== '-' && ctype_digit($score)) { |
||
5855 | $map = [ |
||
5856 | 0=>'rating/stars_0.gif', 1=>'rating/stars_5.gif', 2=>'rating/stars_10.gif', |
||
5857 | 3=>'rating/stars_15.gif',4=>'rating/stars_20.gif',5=>'rating/stars_25.gif', |
||
5858 | 6=>'rating/stars_30.gif',7=>'rating/stars_35.gif',8=>'rating/stars_40.gif', |
||
5859 | 9=>'rating/stars_45.gif',10=>'rating/stars_50.gif', |
||
5860 | ]; |
||
5861 | $stars = Display::return_icon($map[(int)$score]); |
||
5862 | } |
||
5863 | |||
5864 | echo '<div class="wd-comment">'; |
||
5865 | echo $photo; |
||
5866 | echo '<div class="wd-comment-body">'; |
||
5867 | $profileLink = $uInfo ? UserManager::getUserProfileLink($uInfo) : api_htmlentities($name); |
||
5868 | echo '<div class="wd-comment-meta">'.$profileLink.' <span class="wd-dot">•</span> '.$status.' <span class="wd-dot">•</span> ' |
||
5869 | . api_get_local_time($c['dtime']).' <span class="wd-dot">•</span> ' |
||
5870 | . get_lang('Rating').': '.$score.' '.$stars.'</div>'; |
||
5871 | echo '<div class="wd-comment-text">'.api_htmlentities((string)$c['comment']).'</div>'; |
||
5872 | echo '</div>'; |
||
5873 | echo '</div>'; |
||
5874 | } |
||
5875 | } |
||
5876 | |||
5877 | echo '</div></div>'; |
||
5878 | } |
||
5879 | |||
5880 | public function export2doc(int $docId) |
||
5881 | { |
||
5882 | // Course & group context |
||
5883 | $_course = api_get_course_info(); |
||
5884 | $groupInfo = GroupManager::get_group_properties(api_get_group_id()); |
||
5885 | |||
5886 | // Try to get the wiki page |
||
5887 | $page = self::repo()->findOneBy(['iid' => $docId]); |
||
5888 | $data = []; |
||
5889 | if ($page instanceof CWiki) { |
||
5890 | $data = [ |
||
5891 | 'title' => (string) $page->getTitle(), |
||
5892 | 'content' => (string) $page->getContent(), |
||
5893 | ]; |
||
5894 | } elseif (method_exists($this, 'getWikiDataFromDb')) { |
||
5895 | // Backward-compat accessor |
||
5896 | $data = (array) self::getWikiDataFromDb($docId); |
||
5897 | } |
||
5898 | |||
5899 | if (empty($data) || trim((string)($data['title'] ?? '')) === '') { |
||
5900 | // Nothing to export |
||
5901 | return false; |
||
5902 | } |
||
5903 | |||
5904 | $wikiTitle = (string) $data['title']; |
||
5905 | $wikiContents = (string) $data['content']; |
||
5906 | |||
5907 | // XHTML wrapper (kept for old styles and Math support) |
||
5908 | $template = <<<'HTML' |
||
5909 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
||
5910 | <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="{LANGUAGE}" lang="{LANGUAGE}"> |
||
5911 | <head> |
||
5912 | <title>{TITLE}</title> |
||
5913 | <meta http-equiv="Content-Type" content="text/html; charset={ENCODING}" /> |
||
5914 | <style type="text/css" media="screen, projection"> |
||
5915 | /*<![CDATA[*/ |
||
5916 | {CSS} |
||
5917 | /*]]>*/ |
||
5918 | </style> |
||
5919 | {ASCIIMATHML_SCRIPT} |
||
5920 | </head> |
||
5921 | <body dir="{TEXT_DIRECTION}"> |
||
5922 | {CONTENT} |
||
5923 | </body> |
||
5924 | </html> |
||
5925 | HTML; |
||
5926 | |||
5927 | // Resolve visual theme (avoid api_get_setting('stylesheets')) |
||
5928 | $theme = 'chamilo'; |
||
5929 | if (function_exists('api_get_visual_theme')) { |
||
5930 | $t = (string) api_get_visual_theme(); |
||
5931 | if ($t !== '') { |
||
5932 | $theme = $t; |
||
5933 | } |
||
5934 | } |
||
5935 | |||
5936 | // Load theme CSS (best-effort) |
||
5937 | $cssFile = api_get_path(SYS_CSS_PATH).'themes/'.$theme.'/default.css'; |
||
5938 | $css = file_exists($cssFile) ? (string) @file_get_contents($cssFile) : ''; |
||
5939 | if ($css === '') { |
||
5940 | // Minimal fallback CSS to avoid a blank export |
||
5941 | $css = 'body{font:14px/1.5 Arial,Helvetica,sans-serif;color:#222;padding:16px;} |
||
5942 | #wikititle h1{font-size:22px;margin:0 0 10px;} |
||
5943 | #wikicontent{margin-top:8px} |
||
5944 | img{max-width:100%;height:auto;}'; |
||
5945 | } |
||
5946 | |||
5947 | // Fix paths in CSS so exported HTML works out of LMS |
||
5948 | $rootRel = api_get_path(REL_PATH); |
||
5949 | $css = str_replace('behavior:url("/main/css/csshover3.htc");', '', $css); |
||
5950 | $css = str_replace('main/', $rootRel.'main/', $css); |
||
5951 | $css = str_replace('images/', $rootRel.'main/css/themes/'.$theme.'/images/', $css); |
||
5952 | $css = str_replace('../../img/', $rootRel.'main/img/', $css); |
||
5953 | |||
5954 | // Math support if present in content |
||
5955 | $asciiScript = (api_contains_asciimathml($wikiContents) || api_contains_asciisvg($wikiContents)) |
||
5956 | ? '<script src="'.api_get_path(WEB_CODE_PATH).'inc/lib/javascript/asciimath/ASCIIMathML.js" type="text/javascript"></script>'."\n" |
||
5957 | : ''; |
||
5958 | |||
5959 | // Clean wiki links [[...]] → visible text only |
||
5960 | $wikiContents = trim((string) preg_replace('/\[[\[]?([^\]|]*)[|]?([^|\]]*)\][\]]?/', '$1', $wikiContents)); |
||
5961 | |||
5962 | // Build final HTML |
||
5963 | $html = str_replace( |
||
5964 | ['{LANGUAGE}','{ENCODING}','{TEXT_DIRECTION}','{TITLE}','{CSS}','{ASCIIMATHML_SCRIPT}','{CONTENT}'], |
||
5965 | [ |
||
5966 | api_get_language_isocode(), |
||
5967 | api_get_system_encoding(), |
||
5968 | api_get_text_direction(), |
||
5969 | $wikiTitle, |
||
5970 | $css, |
||
5971 | $asciiScript, |
||
5972 | $wikiContents |
||
5973 | ], |
||
5974 | $template |
||
5975 | ); |
||
5976 | |||
5977 | // Replace relative course paths with absolute URLs (guard in case constant differs) |
||
5978 | if (defined('REL_COURSE_PATH') && defined('WEB_COURSE_PATH')) { |
||
5979 | if (api_strpos($html, '../..'.api_get_path(REL_COURSE_PATH)) !== false) { |
||
5980 | $html = str_replace('../..'.api_get_path(REL_COURSE_PATH), api_get_path(WEB_COURSE_PATH), $html); |
||
5981 | } |
||
5982 | } |
||
5983 | |||
5984 | // Compute a safe filename |
||
5985 | $baseName = preg_replace('/\s+/', '_', (string) api_replace_dangerous_char($wikiTitle)); |
||
5986 | $downloadName = $baseName !== '' ? $baseName : 'wiki_page'; |
||
5987 | $downloadName .= '.html'; |
||
5988 | |||
5989 | // --- MODE A: Register in Document tool when SYS_COURSE_PATH exists --- |
||
5990 | if (defined('SYS_COURSE_PATH')) { |
||
5991 | $exportDir = rtrim( |
||
5992 | api_get_path(SYS_COURSE_PATH).api_get_course_path().'/document'.($groupInfo['directory'] ?? ''), |
||
5993 | '/' |
||
5994 | ); |
||
5995 | |||
5996 | if (!is_dir($exportDir)) { |
||
5997 | @mkdir($exportDir, 0775, true); |
||
5998 | } |
||
5999 | |||
6000 | // Ensure unique filename on disk |
||
6001 | $i = 1; |
||
6002 | do { |
||
6003 | $fileName = $baseName.'_'. $i .'.html'; |
||
6004 | $exportPath = $exportDir .'/'. $fileName; |
||
6005 | $i++; |
||
6006 | } while (file_exists($exportPath)); |
||
6007 | |||
6008 | file_put_contents($exportPath, $html); |
||
6009 | |||
6010 | // Register in Document tool |
||
6011 | $relativeDocPath = ($groupInfo['directory'] ?? '').'/'.$fileName; |
||
6012 | $docId = add_document( |
||
6013 | $_course, |
||
6014 | $relativeDocPath, |
||
6015 | 'file', |
||
6016 | (int) filesize($exportPath), |
||
6017 | $wikiTitle |
||
6018 | ); |
||
6019 | |||
6020 | api_item_property_update( |
||
6021 | $_course, |
||
6022 | TOOL_DOCUMENT, |
||
6023 | $docId, |
||
6024 | 'DocumentAdded', |
||
6025 | api_get_user_id(), |
||
6026 | $groupInfo |
||
6027 | ); |
||
6028 | |||
6029 | // Return doc id so caller can flash a confirmation |
||
6030 | return $docId; |
||
6031 | } |
||
6032 | |||
6033 | // --- MODE B (fallback): Direct download (no Document registration) --- |
||
6034 | // Clean existing buffers to avoid header issues |
||
6035 | if (function_exists('ob_get_level')) { |
||
6036 | while (ob_get_level() > 0) { |
||
6037 | @ob_end_clean(); |
||
6038 | } |
||
6039 | } |
||
6040 | |||
6041 | header('Content-Type: text/html; charset='.api_get_system_encoding()); |
||
6042 | header('Content-Disposition: attachment; filename="'.$downloadName.'"'); |
||
6043 | header('X-Content-Type-Options: nosniff'); |
||
6044 | header('Cache-Control: no-store, no-cache, must-revalidate'); |
||
6045 | header('Pragma: no-cache'); |
||
6046 | |||
6047 | |||
6048 | echo $html; |
||
6049 | exit; |
||
6050 | } |
||
6051 | |||
6052 | /** |
||
6053 | * Internal helper to render wiki HTML to PDF with headers/footers and wiki-link cleanup. |
||
6054 | */ |
||
6055 | private function renderPdfFromHtml(string $titleRaw, string $contentRaw, string $courseCode): void |
||
6056 | { |
||
6057 | // Decode entities using platform encoding |
||
6058 | $contentPdf = api_html_entity_decode($contentRaw, ENT_QUOTES, api_get_system_encoding()); |
||
6059 | |||
6060 | // Clean wiki links [[...]] -> visible text only (keep first capture) |
||
6061 | $contentPdf = trim(preg_replace('/\[[\[]?([^\]|]*)[|]?([^|\]]*)\][\]]?/', '$1', $contentPdf)); |
||
6062 | |||
6063 | $titlePdf = api_html_entity_decode($titleRaw, ENT_QUOTES, api_get_system_encoding()); |
||
6064 | |||
6065 | // Ensure UTF-8 for mPDF pipeline |
||
6066 | $titlePdf = api_utf8_encode($titlePdf, api_get_system_encoding()); |
||
6067 | $contentPdf = api_utf8_encode($contentPdf, api_get_system_encoding()); |
||
6068 | |||
6069 | $html = ' |
||
6070 | <!-- defines the headers/footers - this must occur before the headers/footers are set --> |
||
6071 | <!--mpdf |
||
6072 | <pageheader name="odds" content-left="'.htmlspecialchars($titlePdf, ENT_QUOTES).'" header-style-left="color: #880000; font-style: italic;" line="1" /> |
||
6073 | <pagefooter name="odds" content-right="{PAGENO}/{nb}" line="1" /> |
||
6074 | <setpageheader name="odds" page="odd" value="on" show-this-page="1" /> |
||
6075 | <setpagefooter name="odds" page="O" value="on" /> |
||
6076 | mpdf-->'.$contentPdf; |
||
6077 | |||
6078 | $css = api_get_print_css(); |
||
6079 | |||
6080 | $pdf = new PDF(); |
||
6081 | $pdf->content_to_pdf($html, $css, $titlePdf, $courseCode); |
||
6082 | exit; |
||
6083 | } |
||
6084 | |||
6085 | /** |
||
6086 | * Helper: latest version of each page (respecting course/group/session; no visibility gate). |
||
6087 | * @return CWiki[] |
||
6088 | */ |
||
6089 | private function getLatestPagesForContext(): array |
||
6090 | { |
||
6091 | $ctx = self::ctx(); |
||
6092 | $em = Container::getEntityManager(); |
||
6093 | $repo = self::repo(); |
||
6094 | |||
6095 | // Fetch distinct reflinks in context |
||
6096 | $qbRef = $repo->createQueryBuilder('w') |
||
6097 | ->select('DISTINCT w.reflink AS reflink') |
||
6098 | ->andWhere('w.cId = :cid')->setParameter('cid', $ctx['courseId']) |
||
6099 | ->andWhere('COALESCE(w.groupId,0) = :gid')->setParameter('gid', (int)$ctx['groupId']); |
||
6100 | if ($ctx['sessionId'] > 0) { |
||
6101 | $qbRef->andWhere('COALESCE(w.sessionId,0) = :sid')->setParameter('sid', (int)$ctx['sessionId']); |
||
6102 | } else { |
||
6103 | $qbRef->andWhere('COALESCE(w.sessionId,0) = 0'); |
||
6104 | } |
||
6105 | $reflinks = array_map(fn($r) => (string)$r['reflink'], $qbRef->getQuery()->getArrayResult()); |
||
6106 | |||
6107 | $latest = []; |
||
6108 | foreach ($reflinks as $ref) { |
||
6109 | $page = $repo->findOneBy( |
||
6110 | ['cId' => $ctx['courseId'], 'reflink' => $ref, 'groupId' => (int)$ctx['groupId'], 'sessionId' => (int)$ctx['sessionId']], |
||
6111 | ['version' => 'DESC', 'dtime' => 'DESC'] |
||
6112 | ) ?? $repo->findOneBy(['cId' => $ctx['courseId'], 'reflink' => $ref], ['version' => 'DESC', 'dtime' => 'DESC']); |
||
6113 | if ($page) { |
||
6114 | $latest[] = $page; |
||
6115 | } |
||
6116 | } |
||
6117 | return $latest; |
||
6118 | } |
||
6119 | |||
6120 | private function currentCourseId(): int |
||
6121 | { |
||
6122 | return (int) ( $_GET['cid'] ?? api_get_course_int_id() ); |
||
6123 | } |
||
6124 | |||
6125 | private function currentGroupId(): ?int |
||
6126 | { |
||
6127 | $gid = $_GET['gid'] ?? api_get_group_id(); |
||
6128 | return $gid === null ? null : (int) $gid; |
||
6129 | } |
||
6130 | |||
6131 | private function currentSessionId(): ?int |
||
6132 | { |
||
6133 | $sid = $_GET['sid'] ?? api_get_session_id(); |
||
6134 | return $sid === null ? null : (int) $sid; |
||
6135 | } |
||
6136 | |||
6137 | private function wikiUrl(array $extra = []): string |
||
6138 | { |
||
6139 | $base = api_get_self(); |
||
6140 | $params = array_merge([ |
||
6141 | 'cid' => $_GET['cid'] ?? null, |
||
6142 | 'gid' => $_GET['gid'] ?? null, |
||
6143 | 'sid' => $_GET['sid'] ?? null, |
||
6144 | ], $extra); |
||
6145 | |||
6146 | $params = array_filter($params, static fn($v) => $v !== null && $v !== ''); |
||
6147 | |||
6148 | return $base.'?'.http_build_query($params); |
||
6149 | } |
||
6150 | |||
6151 | /** Build base URL with current context (cid, gid, sid) */ |
||
6152 | private function computeBaseUrl(): string |
||
6162 | } |
||
6163 | |||
6164 | /** Helper to create Wiki tool URLs */ |
||
6165 | private function url(array $params = []): string |
||
6166 | { |
||
6167 | return $this->baseUrl.($params ? '&'.http_build_query($params) : ''); |
||
6168 | } |
||
6169 | |||
6170 | private function addCategory(): void |
||
6242 | } |
||
6243 | |||
6244 | private function deleteCategory(): void |
||
6245 | { |
||
6246 | // --- Permissions & feature flag --- |
||
6247 | if (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) { |
||
6248 | api_not_allowed(true); |
||
6249 | } |
||
6250 | if ('true' !== api_get_setting('wiki.wiki_categories_enabled')) { |
||
6251 | api_not_allowed(true); |
||
6252 | } |
||
6253 | |||
6254 | $em = Container::getEntityManager(); |
||
6255 | |||
6256 | if (!isset($_GET['id'])) { |
||
6257 | // English dev msg: Missing category id |
||
6258 | api_not_allowed(true); |
||
6259 | } |
||
6260 | |||
6261 | /** @var CWikiCategory|null $category */ |
||
6262 | $category = $em->find(CWikiCategory::class, (int) $_GET['id']); |
||
6263 | if (!$category) { |
||
6264 | // English dev msg: Category not found |
||
6265 | api_not_allowed(true); |
||
6266 | } |
||
6267 | |||
6268 | // --- Security: only allow removing categories in the current course/session --- |
||
6269 | $course = api_get_course_entity(); |
||
6270 | $session = api_get_session_entity(); |
||
6271 | if ($course !== $category->getCourse() || $session !== $category->getSession()) { |
||
6272 | // English dev msg: Cross-course/session deletion is not allowed |
||
6273 | api_not_allowed(true); |
||
6274 | } |
||
6275 | |||
6276 | // --- Delete and flush --- |
||
6277 | $em->remove($category); |
||
6278 | $em->flush(); |
||
6279 | |||
6280 | // --- UX feedback + redirect --- |
||
6281 | Display::addFlash( |
||
6282 | Display::return_message(get_lang('CategoryDeleted'), 'success') |
||
6283 | ); |
||
6284 | |||
6285 | header('Location: '.$this->url(['action' => 'category'])); |
||
6286 | exit; |
||
6287 | } |
||
6288 | |||
6289 | /** Normalize a reflink into a stable key. Only 'index' is the main page. */ |
||
6290 | public static function normalizeReflink(?string $raw): string |
||
6291 | { |
||
6292 | if ($raw === null || $raw === '') { |
||
6293 | return 'index'; |
||
6294 | } |
||
6295 | $s = self::normalizeToken($raw); |
||
6296 | |||
6297 | // Build aliases for the main page (both keys; fallback-safe) |
||
6298 | $tHome = (string) (get_lang('Home') ?: ''); |
||
6299 | $tDefaultTitle = (string) (get_lang('Home') ?: ''); |
||
6300 | |||
6301 | $aliases = array_filter([ |
||
6302 | 'index', |
||
6303 | self::normalizeToken($tHome), |
||
6304 | self::normalizeToken($tDefaultTitle), |
||
6305 | ]); |
||
6306 | |||
6307 | if (in_array($s, $aliases, true)) { |
||
6308 | return 'index'; |
||
6309 | } |
||
6310 | return $s; |
||
6311 | } |
||
6312 | |||
6313 | /** Internal: apply the same normalization that we use for comparisons. */ |
||
6314 | private static function normalizeToken(string $t): string |
||
6322 | } |
||
6323 | |||
6324 | /** True if the reflink is the main page. */ |
||
6325 | private static function isMain(string $reflink): bool |
||
6326 | { |
||
6327 | return $reflink === 'index'; |
||
6328 | } |
||
6329 | |||
6330 | public static function displayTitleFor(string $reflink, ?string $dbTitle = null): string |
||
6331 | { |
||
6332 | if (self::isMain($reflink)) { |
||
6333 | return get_lang('Home'); |
||
6334 | } |
||
6335 | return $dbTitle !== null && $dbTitle !== '' ? $dbTitle : str_replace('_', ' ', $reflink); |
||
6336 | } |
||
6337 | |||
6338 | public static function displayTokenFor(string $reflink): string |
||
6339 | { |
||
6340 | if (self::isMain($reflink)) { |
||
6341 | $label = get_lang('Home'); |
||
6342 | return str_replace(' ', '_', $label); |
||
6343 | } |
||
6344 | return $reflink; |
||
6345 | } |
||
6346 | |||
6347 | private static function dbg(string $msg): void |
||
6352 | } |
||
6353 | } |
||
6354 | |||
6355 | private static function utf8mb4_safe_entities(string $s): string { |
||
6356 | return preg_replace_callback('/[\x{10000}-\x{10FFFF}]/u', static function($m) { |
||
6357 | $cp = self::uniord($m[0]); |
||
6358 | return sprintf('&#%d;', $cp); |
||
6359 | }, $s); |
||
6360 | } |
||
6361 | private static function uniord(string $c): int { |
||
6365 | } |
||
6366 | } |
||
6367 | |||
6368 | /** Backwards-compat shim so index.php can still `new Wiki()` */ |
||
6369 | if (!class_exists('Wiki')) { |
||
6370 | class_alias(WikiManager::class, 'Wiki'); |
||
6371 | } |
||
6372 |
In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.