|
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.