1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* For licensing terms, see /license.txt */ |
4
|
|
|
|
5
|
|
|
use Chamilo\CoreBundle\Enums\ActionIcon; |
6
|
|
|
use Chamilo\CoreBundle\Framework\Container; |
7
|
|
|
use Chamilo\CourseBundle\Entity\CWiki; |
8
|
|
|
use Chamilo\CourseBundle\Entity\CWikiCategory; |
9
|
|
|
use Chamilo\CourseBundle\Entity\CWikiConf; |
10
|
|
|
use Chamilo\CourseBundle\Entity\CWikiDiscuss; |
11
|
|
|
use Chamilo\CourseBundle\Entity\CWikiMailcue; |
12
|
|
|
use Chamilo\CourseBundle\Repository\CWikiRepository; |
13
|
|
|
use Doctrine\DBAL\Connection; |
14
|
|
|
use Doctrine\Persistence\ObjectRepository; |
15
|
|
|
use ChamiloSession as Session; |
16
|
|
|
|
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
|
|
|
// Print |
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 |
2567
|
|
|
{ |
2568
|
|
|
$ctx = self::ctx(); |
2569
|
|
|
$assignmentType = (int)($values['assignment'] ?? 0); |
2570
|
|
|
|
2571
|
|
|
$courseInfo = $ctx['courseInfo'] ?: api_get_course_info_by_id((int)$ctx['courseId']); |
2572
|
|
|
$courseCode = $courseInfo['code'] ?? ''; |
2573
|
|
|
|
2574
|
|
|
$groupId = (int)$ctx['groupId']; |
2575
|
|
|
$groupInfo = $groupId ? GroupManager::get_group_properties($groupId) : null; |
2576
|
|
|
|
2577
|
|
|
// Target users: course vs group |
2578
|
|
|
if ($groupId === 0) { |
2579
|
|
|
$users = (int)$ctx['sessionId'] > 0 |
2580
|
|
|
? CourseManager::get_user_list_from_course_code($courseCode, (int)$ctx['sessionId']) |
2581
|
|
|
: CourseManager::get_user_list_from_course_code($courseCode); |
2582
|
|
|
} else { |
2583
|
|
|
$subs = GroupManager::get_subscribed_users($groupInfo) ?: []; |
2584
|
|
|
$tutors = GroupManager::get_subscribed_tutors($groupInfo) ?: []; |
2585
|
|
|
$byId = []; |
2586
|
|
|
foreach (array_merge($subs, $tutors) as $u) { |
2587
|
|
|
if (!isset($u['user_id'])) { continue; } |
2588
|
|
|
$byId[(int)$u['user_id']] = $u; |
2589
|
|
|
} |
2590
|
|
|
$users = array_values($byId); |
2591
|
|
|
} |
2592
|
|
|
|
2593
|
|
|
// Teacher data |
2594
|
|
|
$teacherId = api_get_user_id(); |
2595
|
|
|
$tInfo = api_get_user_info($teacherId); |
2596
|
|
|
$tLogin = api_htmlentities(sprintf(get_lang('LoginX'), $tInfo['username']), ENT_QUOTES); |
2597
|
|
|
$tName = $tInfo['complete_name'].' - '.$tLogin; |
2598
|
|
|
$tPhotoUrl = $tInfo['avatar'] ?? UserManager::getUserPicture($teacherId); |
2599
|
|
|
$tPhoto = '<img src="'.$tPhotoUrl.'" alt="'.$tName.'" width="40" height="50" align="top" title="'.$tName.'" />'; |
2600
|
|
|
|
2601
|
|
|
$titleOrig = (string)($values['title'] ?? ''); |
2602
|
|
|
$link2teacher = $titleOrig.'_uass'.$teacherId; |
2603
|
|
|
|
2604
|
|
|
$contentA = |
2605
|
|
|
'<div align="center" style="background-color:#F5F8FB;border:solid;border-color:#E6E6E6">'. |
2606
|
|
|
'<table border="0">'. |
2607
|
|
|
'<tr><td style="font-size:24px">'.get_lang('Assignment desc').'</td></tr>'. |
2608
|
|
|
'<tr><td>'.$tPhoto.'<br />'.Display::tag( |
2609
|
|
|
'span', |
2610
|
|
|
api_get_person_name($tInfo['firstname'], $tInfo['lastname']), |
2611
|
|
|
['title' => $tLogin] |
2612
|
|
|
).'</td></tr>'. |
2613
|
|
|
'</table></div>'; |
2614
|
|
|
|
2615
|
|
|
$postedContent = isset($_POST['content']) ? Security::remove_XSS((string)$_POST['content']) : ''; |
2616
|
|
|
$contentB = '<br/><div align="center" style="font-size:24px">'. |
2617
|
|
|
get_lang('Assignment description').': '.$titleOrig.'</div><br/>'. |
2618
|
|
|
$postedContent; |
2619
|
|
|
|
2620
|
|
|
$allStudentsItems = []; |
2621
|
|
|
$postedTitleSafe = isset($_POST['title']) ? Security::remove_XSS((string)$_POST['title']) : $titleOrig; |
2622
|
|
|
|
2623
|
|
|
// Create student pages (assignment = 2) |
2624
|
|
|
foreach ($users as $u) { |
2625
|
|
|
$uid = (int)($u['user_id'] ?? 0); |
2626
|
|
|
if ($uid === 0 || $uid === $teacherId) { continue; } |
2627
|
|
|
|
2628
|
|
|
$uPic = UserManager::getUserPicture($uid); |
2629
|
|
|
$uLogin = api_htmlentities(sprintf(get_lang('LoginX'), (string)$u['username']), ENT_QUOTES); |
2630
|
|
|
$uName = api_get_person_name((string)$u['firstname'], (string)$u['lastname']).' . '.$uLogin; |
2631
|
|
|
$uPhoto = '<img src="'.$uPic.'" alt="'.$uName.'" width="40" height="50" align="bottom" title="'.$uName.'" />'; |
2632
|
|
|
|
2633
|
|
|
$isTutor = $groupInfo && GroupManager::is_tutor_of_group($uid, $groupInfo); |
2634
|
|
|
$isMember = $groupInfo && GroupManager::is_subscribed($uid, $groupInfo); |
2635
|
|
|
$status = ($isTutor && $isMember) ? get_lang('Group tutor and member') |
2636
|
|
|
: ($isTutor ? get_lang('GroupTutor') : ' '); |
2637
|
|
|
|
2638
|
|
|
if ($assignmentType === 1) { |
2639
|
|
|
$studentValues = $values; |
2640
|
|
|
$studentValues['title'] = $titleOrig; |
2641
|
|
|
$studentValues['assignment'] = 2; |
2642
|
|
|
$studentValues['content'] = |
2643
|
|
|
'<div align="center" style="background-color:#F5F8FB;border:solid;border-color:#E6E6E6">'. |
2644
|
|
|
'<table border="0">'. |
2645
|
|
|
'<tr><td style="font-size:24px">'.get_lang('Assignment work').'</td></tr>'. |
2646
|
|
|
'<tr><td>'.$uPhoto.'<br />'.$uName.'</td></tr>'. |
2647
|
|
|
'</table></div>'. |
2648
|
|
|
'[[ '.$link2teacher.' | '.get_lang('Assignment link to teacher page').' ]] '; |
2649
|
|
|
|
2650
|
|
|
$allStudentsItems[] = |
2651
|
|
|
'<li>'. |
2652
|
|
|
Display::tag('span', strtoupper((string)$u['lastname']).', '.(string)$u['firstname'], ['title' => $uLogin]). |
2653
|
|
|
' [[ '.$postedTitleSafe.'_uass'.$uid.' | '.$uPhoto.' ]] '. |
2654
|
|
|
$status. |
2655
|
|
|
'</li>'; |
2656
|
|
|
|
2657
|
|
|
// Pass the student uid to save_new_wiki so the author is the student |
2658
|
|
|
$this->save_new_wiki($studentValues, $uid); |
2659
|
|
|
} |
2660
|
|
|
} |
2661
|
|
|
|
2662
|
|
|
// Teacher page (assignment = 1) listing student works |
2663
|
|
|
foreach ($users as $u) { |
2664
|
|
|
if ((int)($u['user_id'] ?? 0) !== $teacherId) { continue; } |
2665
|
|
|
|
2666
|
|
|
if ($assignmentType === 1) { |
2667
|
|
|
$teacherValues = $values; |
2668
|
|
|
$teacherValues['title'] = $titleOrig; |
2669
|
|
|
$teacherValues['comment'] = get_lang('Assignment desc'); |
2670
|
|
|
sort($allStudentsItems); |
2671
|
|
|
|
2672
|
|
|
$teacherValues['content'] = |
2673
|
|
|
$contentA.$contentB.'<br/>'. |
2674
|
|
|
'<div align="center" style="font-size:18px;background-color:#F5F8FB;border:solid;border-color:#E6E6E6">'. |
2675
|
|
|
get_lang('AssignmentLinkstoStudentsPage').'</div><br/>'. |
2676
|
|
|
'<div style="background-color:#F5F8FB;border:solid;border-color:#E6E6E6">'. |
2677
|
|
|
'<ol>'.implode('', $allStudentsItems).'</ol>'. |
2678
|
|
|
'</div><br/>'; |
2679
|
|
|
|
2680
|
|
|
$teacherValues['assignment'] = 1; |
2681
|
|
|
|
2682
|
|
|
// Pass the teacher id so the reflink ends with _uass<teacherId> |
2683
|
|
|
$this->save_new_wiki($teacherValues, $teacherId); |
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 |
6153
|
|
|
{ |
6154
|
|
|
$base = api_get_self(); |
6155
|
|
|
$params = [ |
6156
|
|
|
'cid' => api_get_course_id(), |
6157
|
|
|
'gid' => api_get_group_id(), |
6158
|
|
|
'sid' => api_get_session_id(), |
6159
|
|
|
]; |
6160
|
|
|
|
6161
|
|
|
return $base.'?'.http_build_query($params); |
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 |
6171
|
|
|
{ |
6172
|
|
|
// --- Permissions & feature flag --- |
6173
|
|
|
if (!api_is_allowed_to_edit(false, true) && !api_is_platform_admin()) { |
6174
|
|
|
api_not_allowed(true); |
6175
|
|
|
} |
6176
|
|
|
if ('true' !== api_get_setting('wiki.wiki_categories_enabled')) { |
6177
|
|
|
api_not_allowed(true); |
6178
|
|
|
} |
6179
|
|
|
|
6180
|
|
|
// --- Repositories / context --- |
6181
|
|
|
$em = Container::getEntityManager(); |
6182
|
|
|
$categoryRepo = $em->getRepository(CWikiCategory::class); |
6183
|
|
|
|
6184
|
|
|
$course = api_get_course_entity(); |
6185
|
|
|
$session = api_get_session_entity(); |
6186
|
|
|
|
6187
|
|
|
// --- If editing, make sure the category exists and belongs to the current course/session --- |
6188
|
|
|
$categoryToEdit = null; |
6189
|
|
|
if (isset($_GET['id'])) { |
6190
|
|
|
$categoryToEdit = $categoryRepo->find((int) $_GET['id']); |
6191
|
|
|
if (!$categoryToEdit) { |
6192
|
|
|
// English dev msg: Category not found in current repository |
6193
|
|
|
api_not_allowed(true); |
6194
|
|
|
} |
6195
|
|
|
if ($course !== $categoryToEdit->getCourse() || $session !== $categoryToEdit->getSession()) { |
6196
|
|
|
// English dev msg: Cross-course/session edition is not allowed |
6197
|
|
|
api_not_allowed(true); |
6198
|
|
|
} |
6199
|
|
|
} |
6200
|
|
|
|
6201
|
|
|
// --- Fetch categories for list --- |
6202
|
|
|
$categories = $categoryRepo->findByCourse($course, $session); |
6203
|
|
|
|
6204
|
|
|
// --- Action icons (MDI) --- |
6205
|
|
|
$iconEdit = Display::getMdiIcon(ActionIcon::EDIT, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Edit')); |
6206
|
|
|
$iconDelete = Display::getMdiIcon(ActionIcon::DELETE, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Delete')); |
6207
|
|
|
|
6208
|
|
|
// --- Build rows for the table --- |
6209
|
|
|
$rows = array_map(function (CWikiCategory $category) use ($iconEdit, $iconDelete) { |
6210
|
|
|
$actions = []; |
6211
|
|
|
$actions[] = Display::url( |
6212
|
|
|
$iconEdit, |
6213
|
|
|
$this->url(['action' => 'category', 'id' => $category->getId()]) |
6214
|
|
|
); |
6215
|
|
|
$actions[] = Display::url( |
6216
|
|
|
$iconDelete, |
6217
|
|
|
$this->url(['action' => 'delete_category', 'id' => $category->getId()]) |
6218
|
|
|
); |
6219
|
|
|
|
6220
|
|
|
return [ |
6221
|
|
|
$category->getNodeName(), |
6222
|
|
|
implode(PHP_EOL, $actions), |
6223
|
|
|
]; |
6224
|
|
|
}, $categories); |
6225
|
|
|
|
6226
|
|
|
// --- Render form (create or edit) --- |
6227
|
|
|
$form = $this->createCategoryForm($categoryToEdit); |
6228
|
|
|
$form->display(); |
6229
|
|
|
|
6230
|
|
|
echo '<hr/>'; |
6231
|
|
|
|
6232
|
|
|
// --- Render table (name + actions) --- |
6233
|
|
|
$table = new SortableTableFromArrayConfig( |
6234
|
|
|
$rows, |
6235
|
|
|
0, |
6236
|
|
|
25, |
6237
|
|
|
'WikiCategories_table' |
6238
|
|
|
); |
6239
|
|
|
$table->set_header(0, get_lang('Name'), false); |
6240
|
|
|
$table->set_header(1, get_lang('Actions'), false, ['class' => 'text-right'], ['class' => 'text-right']); |
6241
|
|
|
$table->display(); |
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 |
6315
|
|
|
{ |
6316
|
|
|
$t = html_entity_decode($t, ENT_QUOTES); |
6317
|
|
|
$t = strip_tags(trim($t)); |
6318
|
|
|
$t = mb_strtolower($t); |
6319
|
|
|
$t = strtr($t, [' ' => '_', '-' => '_']); |
6320
|
|
|
$t = preg_replace('/_+/', '_', $t); |
6321
|
|
|
return $t; |
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 |
6348
|
|
|
{ |
6349
|
|
|
if (1) { |
6350
|
|
|
echo '<!-- WIKI DEBUG: '.htmlspecialchars($msg, ENT_QUOTES).' -->' . PHP_EOL; |
6351
|
|
|
error_log('[WIKI DEBUG] '.$msg); |
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 { |
6362
|
|
|
$u = mb_convert_encoding($c, 'UCS-4BE', 'UTF-8'); |
6363
|
|
|
$u = unpack('N', $u); |
6364
|
|
|
return $u[1]; |
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.