• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dragomano / Light-Portal / 19949548504

05 Dec 2025 01:26AM UTC coverage: 51.562% (+0.5%) from 51.086%
19949548504

push

github

web-flow
Merge pull request #313 from dragomano/fixes

Update to 3.0 beta 2

15 of 56 new or added lines in 15 files covered. (26.79%)

4 existing lines in 2 files now uncovered.

5660 of 10977 relevant lines covered (51.56%)

4.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

49.36
/src/Sources/LightPortal/Repositories/PageRepository.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * @package Light Portal
5
 * @link https://dragomano.ru/mods/light-portal
6
 * @author Bugo <bugo@dragomano.ru>
7
 * @copyright 2019-2025 Bugo
8
 * @license https://spdx.org/licenses/GPL-3.0-or-later.html GPL-3.0-or-later
9
 *
10
 * @version 3.0
11
 */
12

13
namespace LightPortal\Repositories;
14

15
use Bugo\Compat\Config;
16
use Bugo\Compat\ErrorHandler;
17
use Bugo\Compat\Lang;
18
use Bugo\Compat\Logging;
19
use Bugo\Compat\Msg;
20
use Bugo\Compat\Security;
21
use Bugo\Compat\User;
22
use Bugo\Compat\Utils;
23
use Exception;
24
use Laminas\Db\Sql\Predicate\Expression;
25
use Laminas\Db\Sql\Select;
26
use LightPortal\Database\PortalSqlInterface;
27
use LightPortal\Enums\AlertAction;
28
use LightPortal\Enums\ContentType;
29
use LightPortal\Enums\EntryType;
30
use LightPortal\Enums\NotifyType;
31
use LightPortal\Enums\Permission;
32
use LightPortal\Enums\PortalHook;
33
use LightPortal\Enums\PortalSubAction;
34
use LightPortal\Enums\Status;
35
use LightPortal\Events\EventDispatcherInterface;
36
use LightPortal\Utils\Content;
37
use LightPortal\Utils\DateTime;
38
use LightPortal\Utils\Icon;
39
use LightPortal\Utils\NotifierInterface;
40
use LightPortal\Utils\Setting;
41
use LightPortal\Utils\Str;
42

43
use function LightPortal\app;
44

45
use const LP_PAGE_URL;
46

47
if (! defined('SMF'))
1✔
48
        die('No direct access...');
×
49

50
final class PageRepository extends AbstractRepository implements PageRepositoryInterface
51
{
52
        protected string $entity = 'page';
53

54
        public function __construct(
55
                protected PortalSqlInterface $sql,
56
                protected EventDispatcherInterface $dispatcher,
57
                protected NotifierInterface $notifier
58
        )
59
        {
60
                parent::__construct($sql, $dispatcher);
37✔
61
        }
62

63
        public function getAll(
64
                int $start,
65
                int $limit,
66
                string $sort,
67
                string $filter = '',
68
                array $whereConditions = []
69
        ): array
70
        {
71
                $params = $this->getLangQueryParams();
3✔
72

73
                $select = $this->sql->select()
3✔
74
                        ->from(['p' => 'lp_pages'])
3✔
75
                        ->columns([
3✔
76
                                Select::SQL_STAR,
3✔
77
                                'date' => new Expression('GREATEST(p.created_at, p.updated_at)'),
3✔
78
                        ])
3✔
79
                        ->join(
3✔
80
                                ['mem' => 'members'],
3✔
81
                                'p.author_id = mem.id_member',
3✔
82
                                ['author_name' => new Expression('COALESCE(mem.real_name, ?)', [$params['guest']])],
3✔
83
                                Select::JOIN_LEFT
3✔
84
                        )
3✔
85
                        ->order($sort)
3✔
86
                        ->limit($limit)
3✔
87
                        ->offset($start);
3✔
88

89
                $this->addParamJoins($select, ['params' => ['allow_comments' => ['alias' => 'par']]]);
3✔
90

91
                $this->addTranslationJoins($select);
3✔
92

93
                if ($filter === 'list') {
3✔
94
                        $select->where([
1✔
95
                                'p.status = ?'      => Status::ACTIVE->value,
1✔
96
                                'p.entry_type = ?'  => EntryType::DEFAULT->name(),
1✔
97
                                'p.deleted_at = ?'  => 0,
1✔
98
                                'p.created_at <= ?' => time(),
1✔
99
                        ]);
1✔
100

101
                        $select->where(['p.permissions' => Permission::all()]);
1✔
102
                        $select->where($this->getTranslationFilter());
1✔
103
                }
104

105
                if ($whereConditions) {
3✔
106
                        $select->where($whereConditions);
×
107
                }
108

109
                $result = $this->sql->execute($select);
3✔
110

111
                $items = [];
3✔
112
                foreach ($result as $row) {
3✔
113
                        Lang::censorText($row['title']);
2✔
114

115
                        $items[$row['page_id']] = [
2✔
116
                                'id'           => $row['page_id'],
2✔
117
                                'category_id'  => $row['category_id'],
2✔
118
                                'slug'         => $row['slug'],
2✔
119
                                'type'         => $row['type'],
2✔
120
                                'entry_type'   => $row['entry_type'],
2✔
121
                                'status'       => $row['status'],
2✔
122
                                'num_views'    => $row['num_views'],
2✔
123
                                'num_comments' => $row['num_comments'],
2✔
124
                                'author_id'    => $row['author_id'],
2✔
125
                                'author_name'  => $row['author_name'],
2✔
126
                                'date'         => DateTime::relative($row['date']),
2✔
127
                                'created_at'   => $row['created_at'],
2✔
128
                                'updated_at'   => $row['updated_at'],
2✔
129
                                'is_front'     => Setting::isFrontpage($row['slug']),
2✔
130
                                'title'        => Str::decodeHtmlEntities($row['title']),
2✔
131
                        ];
2✔
132
                }
133

134
                return $items;
3✔
135
        }
136

137
        public function getTotalCount(string $filter = '', array $whereConditions = []): int
138
        {
139
                $select = $this->sql->select()
1✔
140
                        ->from(['p' => 'lp_pages'])
1✔
141
                        ->columns(['page_id']);
1✔
142

143
                $this->addTranslationJoins($select);
1✔
144

145
                if ($whereConditions) {
1✔
146
                        $select->where($whereConditions);
×
147
                }
148

149
                $countSelect = $this->sql->select()
1✔
150
                        ->from(['sub' => $select])
1✔
151
                        ->columns(['count' => new Expression('COUNT(*)')]);
1✔
152

153
                $result = $this->sql->execute($countSelect)->current();
1✔
154

155
                return (int) $result['count'];
1✔
156
        }
157

158
        public function getData(int|string $item): array
159
        {
160
                if (empty($item)) {
4✔
161
                        return [];
1✔
162
                }
163

164
                $params = $this->getLangQueryParams();
3✔
165

166
                $select = $this->sql->select()
3✔
167
                        ->from(['p' => 'lp_pages'])
3✔
168
                        ->join(
3✔
169
                                ['cat' => 'lp_categories'],
3✔
170
                                'cat.category_id = p.category_id',
3✔
171
                                ['cat_icon' => 'icon'],
3✔
172
                                Select::JOIN_LEFT
3✔
173
                        )
3✔
174
                        ->join(
3✔
175
                                ['mem' => 'members'],
3✔
176
                                'p.author_id = mem.id_member',
3✔
177
                                ['author_name' => new Expression('COALESCE(mem.real_name, ?)', [$params['guest']])],
3✔
178
                                Select::JOIN_LEFT
3✔
179
                        )
3✔
180
                        ->join(
3✔
181
                                ['com' => 'lp_comments'],
3✔
182
                                'com.id = p.last_comment_id',
3✔
183
                                ['comment_date' => 'created_at'],
3✔
184
                                Select::JOIN_LEFT
3✔
185
                        )
3✔
186
                        ->where(['p.' . (is_int($item) ? 'page_id = ?' : 'slug = ?') => $item]);
3✔
187

188
                $this->addParamJoins($select);
3✔
189
                $this->addParamJoins($select, ['params' => ['allow_comments' => ['alias' => 'pac']]]);
3✔
190

191
                $this->addTranslationJoins($select, ['fields' => ['title', 'content', 'description']]);
3✔
192

193
                $this->addTranslationJoins($select, [
3✔
194
                        'primary' => 'cat.category_id',
3✔
195
                        'entity'  => 'category',
3✔
196
                        'fields'  => ['cat_title' => 'title'],
3✔
197
                        'alias'   => 'cat_t',
3✔
198
                ]);
3✔
199

200
                $result = $this->sql->execute($select);
3✔
201

202
                foreach ($result as $row) {
3✔
203
                        if ($row['type'] === ContentType::BBC->name()) {
3✔
204
                                $row['content'] = Msg::un_preparsecode($row['content'] ?? '');
3✔
205
                        }
206

207
                        $data ??= [
3✔
208
                                'id'              => $row['page_id'],
3✔
209
                                'category_id'     => $row['category_id'],
3✔
210
                                'author_id'       => $row['author_id'],
3✔
211
                                'author'          => $row['author_name'],
3✔
212
                                'slug'            => $row['slug'],
3✔
213
                                'type'            => $row['type'],
3✔
214
                                'entry_type'      => $row['entry_type'],
3✔
215
                                'permissions'     => $row['permissions'],
3✔
216
                                'status'          => $row['status'],
3✔
217
                                'num_views'       => $row['num_views'],
3✔
218
                                'num_comments'    => $row['num_comments'],
3✔
219
                                'created_at'      => $row['created_at'],
3✔
220
                                'updated_at'      => $row['updated_at'],
3✔
221
                                'last_comment_id' => $row['last_comment_id'],
3✔
222
                                'sort_value'      => $row['comment_date'],
3✔
223
                                'image'           => $this->getImageFromContent($row['content'], $row['type']),
3✔
224
                                'title'           => $row['title'],
3✔
225
                                'content'         => $row['content'],
3✔
226
                                'description'     => $row['description'],
3✔
227
                                'cat_title'       => $row['cat_title'],
3✔
228
                                'cat_icon'        => Icon::parse($row['cat_icon']),
3✔
229
                                'tags'            => $this->getTags($row['page_id']),
3✔
230
                        ];
3✔
231

232
                        $data['options'][$row['name']] = $row['value'];
3✔
233
                }
234

235
                return $data ?? [];
3✔
236
        }
237

238
        public function setData(int $item = 0): void
239
        {
240
                if (isset(Utils::$context['post_errors']) || $this->request()->hasNot(['save', 'save_exit'])) {
×
241
                        return;
×
242
                }
243

244
                Security::checkSubmitOnce('check');
×
245

246
                $this->prepareBbcContent(Utils::$context['lp_page']);
×
247

248
                empty($item)
×
249
                        ? $item = $this->addData(Utils::$context['lp_page'])
×
250
                        : $this->updateData($item, Utils::$context['lp_page']);
×
251

252
                $this->cache()->flush();
×
253

254
                $this->session()->free('lp');
×
255

256
                if ($this->request()->has('save_exit')) {
×
257
                        $this->response()->redirect('action=admin;area=lp_pages;sa=main');
×
258
                }
259

260
                if ($this->request()->has('save')) {
×
261
                        $this->response()->redirect('action=admin;area=lp_pages;sa=edit;id=' . $item);
×
262
                }
263
        }
264

265
        public function remove(mixed $items): void
266
        {
267
                $items = (array) $items;
×
268

269
                if ($items === [])
×
270
                        return;
×
271

272
                $update = $this->sql->update('lp_pages');
×
273
                $update->set(['deleted_at' => time()]);
×
274
                $update->where->in('page_id', $items);
×
275
                $this->sql->execute($update);
×
276

277
                $this->session()->free('lp');
×
278
        }
279

280
        public function restore(mixed $items): void
281
        {
282
                $items = (array) $items;
×
283

284
                if ($items === [])
×
285
                        return;
×
286

287
                $update = $this->sql->update('lp_pages');
×
288
                $update->set(['deleted_at' => 0]);
×
289
                $update->where->in('page_id', $items);
×
290
                $this->sql->execute($update);
×
291

292
                $this->session()->free('lp');
×
293
        }
294

295
        public function removePermanently(mixed $items): void
296
        {
297
                $items = (array) $items;
×
298

299
                if ($items === [])
×
300
                        return;
×
301

302
                $this->dispatcher->dispatch(PortalHook::onPageRemoving, ['items' => $items]);
×
303

304
                try {
NEW
305
                        $this->transaction->begin();
×
306

NEW
307
                        $deletePages = $this->sql->delete('lp_pages');
×
NEW
308
                        $deletePages->where->in('page_id', $items);
×
NEW
309
                        $this->sql->execute($deletePages);
×
310

NEW
311
                        $deleteTranslations = $this->sql->delete('lp_translations');
×
NEW
312
                        $deleteTranslations->where->in('item_id', $items);
×
NEW
313
                        $deleteTranslations->where->equalTo('type', $this->entity);
×
NEW
314
                        $this->sql->execute($deleteTranslations);
×
315

NEW
316
                        $deleteParams = $this->sql->delete('lp_params');
×
NEW
317
                        $deleteParams->where->in('item_id', $items);
×
NEW
318
                        $deleteParams->where->equalTo('type', $this->entity);
×
NEW
319
                        $this->sql->execute($deleteParams);
×
320

NEW
321
                        $deletePageTag = $this->sql->delete('lp_page_tag');
×
NEW
322
                        $deletePageTag->where->in('page_id', $items);
×
NEW
323
                        $this->sql->execute($deletePageTag);
×
324

NEW
325
                        $this->transaction->commit();
×
NEW
326
                } catch (Exception $e) {
×
NEW
327
                        $this->transaction->rollback();
×
328

NEW
329
                        ErrorHandler::fatal($e->getMessage(), false);
×
330
                }
331

332
                $commentsToRemove = $this->sql->select('lp_comments')->columns(['id']);
×
333
                $commentsToRemove->where->in('page_id', $items);
×
334
                $result = $this->sql->execute($commentsToRemove);
×
335

336
                $commentIds = [];
×
337
                foreach ($result as $row) {
×
338
                        $commentIds[] = $row['id'];
×
339
                }
340

341
                app(CommentRepositoryInterface::class)->remove($commentIds);
×
342

343
                $this->session()->free('lp');
×
344
        }
345

346
        public function getPrevNextLinks(array $page, bool $withinCategory = false): array
347
        {
348
                $params = $this->getLangQueryParams();
28✔
349

350
                $sortOptions = [
28✔
351
                        'created;desc'      => ['field' => 'p.created_at', 'direction' => 'desc'],
28✔
352
                        'created'           => ['field' => 'p.created_at', 'direction' => 'asc'],
28✔
353
                        'updated;desc'      => ['field' => 'GREATEST(p.created_at, p.updated_at)', 'direction' => 'desc'],
28✔
354
                        'updated'           => ['field' => 'GREATEST(p.created_at, p.updated_at)', 'direction' => 'asc'],
28✔
355
                        'last_comment;desc' => ['field' => 'COALESCE(com.created_at, p.created_at)', 'direction' => 'desc'],
28✔
356
                        'last_comment'      => ['field' => 'COALESCE(com.created_at, p.created_at)', 'direction' => 'asc'],
28✔
357
                        'title;desc'        => ['field' => 'LOWER(COALESCE(t.title, tf.title))', 'direction' => 'desc'],
28✔
358
                        'title'             => ['field' => 'LOWER(COALESCE(t.title, tf.title))', 'direction' => 'asc'],
28✔
359
                        'author_name;desc'  => ['field' => 'LOWER(COALESCE(mem.real_name, ?))', 'direction' => 'desc', 'bind' => $params['guest']],
28✔
360
                        'author_name'       => ['field' => 'LOWER(COALESCE(mem.real_name, ?))', 'direction' => 'asc', 'bind' => $params['guest']],
28✔
361
                        'num_views;desc'    => ['field' => 'p.num_views', 'direction' => 'desc'],
28✔
362
                        'num_views'         => ['field' => 'p.num_views', 'direction' => 'asc'],
28✔
363
                        'num_replies;desc'  => ['field' => 'p.num_comments', 'direction' => 'desc'],
28✔
364
                        'num_replies'       => ['field' => 'p.num_comments', 'direction' => 'asc'],
28✔
365
                ];
28✔
366

367
                $sorting = Utils::$context['lp_current_sorting'] ?? 'created;desc';
28✔
368
                $sortOption = $sortOptions[$sorting] ?? $sortOptions['created;desc'];
28✔
369
                $sortField = $sortOption['field'];
28✔
370
                $sortDirection = strtoupper($sortOption['direction']);
28✔
371
                $sortBind = $sortOption['bind'] ?? null;
28✔
372

373
                $currentSortValue = match (true) {
28✔
374
                        str_contains($sorting, 'updated')      => max($page['created_at'], $page['updated_at']),
28✔
375
                        str_contains($sorting, 'last_comment') => $page['sort_value'] ?? $page['created_at'],
24✔
376
                        str_contains($sorting, 'title')        => Utils::$smcFunc['strtolower']($page['title']),
20✔
377
                        str_contains($sorting, 'author_name')  => Utils::$smcFunc['strtolower']($page['author']),
16✔
378
                        str_contains($sorting, 'num_views')    => $page['num_views'],
12✔
379
                        str_contains($sorting, 'num_replies')  => $page['num_comments'],
8✔
380
                        default => $page['created_at'],
4✔
381
                };
28✔
382

383
                $categories = Setting::get('lp_frontpage_categories', 'array', []);
28✔
384

385
                $baseWhere = [
28✔
386
                        'p.page_id != ?'    => $page['id'],
28✔
387
                        'p.created_at <= ?' => time(),
28✔
388
                        'p.entry_type = ?'  => EntryType::DEFAULT->name(),
28✔
389
                        'p.status = ?'      => Status::ACTIVE->value,
28✔
390
                        'p.deleted_at = ?'  => 0,
28✔
391
                        'p.permissions'     => Permission::all(),
28✔
392
                ];
28✔
393

394
                if (! empty($categories)) {
28✔
395
                        $baseWhere['p.category_id'] = $categories;
28✔
396
                }
397

398
                if ($withinCategory) {
28✔
399
                        $baseWhere['p.category_id = ?'] = $page['category_id'];
14✔
400
                }
401

402
                $secondaryField = 'p.created_at';
28✔
403
                $currentSecondaryValue = $page['created_at'];
28✔
404
                $listAsc = $sortDirection === 'ASC';
28✔
405

406
                $nextPrimaryOp = $listAsc ? '>' : '<';
28✔
407
                $nextSecondaryOp = $listAsc ? '>' : '<';
28✔
408

409
                $prevPrimaryOp = $listAsc ? '<' : '>';
28✔
410
                $prevSecondaryOp = $listAsc ? '<' : '>';
28✔
411

412
                if ($sortBind !== null) {
28✔
413
                        $nextWhereParams = [$sortBind, $currentSortValue, $sortBind, $currentSortValue, $currentSecondaryValue];
4✔
414
                        $prevWhereParams = [$sortBind, $currentSortValue, $sortBind, $currentSortValue, $currentSecondaryValue];
4✔
415
                } else {
416
                        $nextWhereParams = [$currentSortValue, $currentSortValue, $currentSecondaryValue];
24✔
417
                        $prevWhereParams = [$currentSortValue, $currentSortValue, $currentSecondaryValue];
24✔
418
                }
419

420
                $nextWhereSql = sprintf(
28✔
421
                        '(%s %s ? OR (%s = ? AND %s %s ?))',
28✔
422
                        $sortField, $nextPrimaryOp, $sortField, $secondaryField, $nextSecondaryOp
28✔
423
                );
28✔
424
                $prevWhereSql = sprintf(
28✔
425
                        '(%s %s ? OR (%s = ? AND %s %s ?))',
28✔
426
                        $sortField, $prevPrimaryOp, $sortField, $secondaryField, $prevSecondaryOp
28✔
427
                );
28✔
428

429
                $nextWhere = new Expression($nextWhereSql, $nextWhereParams);
28✔
430
                $prevWhere = new Expression($prevWhereSql, $prevWhereParams);
28✔
431

432
                $languages = array_unique([$params['lang'], $params['fallback_lang']]);
28✔
433

434
                $base = $this->sql->select()
28✔
435
                        ->from(['p' => 'lp_pages'])
28✔
436
                        ->columns(['page_id', 'slug'])
28✔
437
                        ->where(['(t.lang IN (?) OR tf.lang IN (?))' => [$languages, $languages]])
28✔
438
                        ->where($baseWhere)
28✔
439
                        ->where(["COALESCE(t.title, tf.title, '') != ?" => ['']]);
28✔
440

441
                $this->addTranslationJoins($base);
28✔
442

443
                $where = "item_id = p.page_id AND type = ? AND lang IN (?) AND (title != ? OR content != ?)";
28✔
444
                $sql = "EXISTS (SELECT 1 FROM {$this->sql->getPrefix()}lp_translations WHERE $where)";
28✔
445
                $base->where(new Expression(
28✔
446
                        $sql, [$this->entity, array_unique([User::$me->language, Config::$language]), '', '']
28✔
447
                ));
28✔
448

449
                if (str_contains($sorting, 'last_comment')) {
28✔
450
                        $base->join(
4✔
451
                                ['com' => 'lp_comments'],
4✔
452
                                new Expression('com.id = p.last_comment_id'),
4✔
453
                                ['created_at'],
4✔
454
                                Select::JOIN_LEFT
4✔
455
                        );
4✔
456
                }
457

458
                if (str_contains($sorting, 'author_name')) {
28✔
459
                        $base->join(
4✔
460
                                ['mem' => 'members'],
4✔
461
                                new Expression('mem.id_member = p.author_id'),
4✔
462
                                ['real_name'],
4✔
463
                                Select::JOIN_LEFT
4✔
464
                        );
4✔
465
                }
466

467
                $prevOrder = [
28✔
468
                        new Expression(
28✔
469
                                $sortField . ' ' . ($listAsc ? 'DESC' : 'ASC'),
28✔
470
                                $sortBind !== null ? [$sortBind] : []
28✔
471
                        ),
28✔
472
                        new Expression($secondaryField . ' ' . ($listAsc ? 'DESC' : 'ASC')),
28✔
473
                        new Expression('CASE WHEN t.title IS NOT NULL THEN 1 WHEN tf.title IS NOT NULL THEN 2 END'),
28✔
474
                        new Expression('p.page_id ' . ($listAsc ? 'DESC' : 'ASC'))
28✔
475
                ];
28✔
476

477
                $nextOrder = [
28✔
478
                        new Expression(
28✔
479
                                $sortField . ' ' . $sortDirection,
28✔
480
                                $sortBind !== null ? [$sortBind] : []
28✔
481
                        ),
28✔
482
                        new Expression($secondaryField . ' ' . $sortDirection),
28✔
483
                        new Expression('CASE WHEN t.title IS NOT NULL THEN 1 WHEN tf.title IS NOT NULL THEN 2 END'),
28✔
484
                        new Expression('p.page_id ' . ($listAsc ? 'ASC' : 'DESC'))
28✔
485
                ];
28✔
486

487
                $prev = clone $base;
28✔
488
                $next = clone $base;
28✔
489

490
                $prev->where($prevWhere)->order($prevOrder)->limit(1);
28✔
491
                $next->where($nextWhere)->order($nextOrder)->limit(1);
28✔
492

493
                $result = [
28✔
494
                        'prev' => $this->sql->execute($prev)->current() ?: [],
28✔
495
                        'next' => $this->sql->execute($next)->current() ?: [],
28✔
496
                ];
28✔
497

498
                return [
28✔
499
                        $result['prev']['title'] ?? '',
28✔
500
                        $result['prev']['slug'] ?? '',
28✔
501
                        $result['next']['title'] ?? '',
28✔
502
                        $result['next']['slug'] ?? '',
28✔
503
                ];
28✔
504
        }
505

506
        public function getRelatedPages(array $page): array
507
        {
508
                $titleWords = explode(' ', $page['title']);
×
509
                $slugWords  = explode('-', (string) $page['slug']);
×
510
                $titleCount = count($titleWords);
×
511
                $slugCount  = count($slugWords);
×
512

513
                $searchConditions = [];
×
514

515
                foreach ($titleWords as $key => $word) {
×
516
                        $searchConditions[] = sprintf(
×
517
                                "CASE WHEN LOWER(t.title) LIKE LOWER('%%%s%%') THEN %d ELSE 0 END",
×
518
                                $word,
×
519
                                ($titleCount - $key) * 2
×
520
                        );
×
521
                }
522

523
                foreach ($slugWords as $key => $word) {
×
524
                        $searchConditions[] = sprintf(
×
525
                                "CASE WHEN LOWER(p.slug) LIKE LOWER('%%%s%%') THEN %d ELSE 0 END",
×
526
                                $word,
×
527
                                $slugCount - $key
×
528
                        );
×
529
                }
530

531
                $searchFormula = implode(' + ', $searchConditions);
×
532

533
                $select = $this->sql->select()
×
534
                        ->from(['p' => 'lp_pages'])
×
535
                        ->columns([
×
536
                                'page_id',
×
537
                                'slug',
×
538
                                'type',
×
539
                                'related' => new Expression($searchFormula),
×
540
                        ])
×
541
                        ->where([
×
542
                                'p.status'          => $page['status'],
×
543
                                'p.entry_type'      => $page['entry_type'],
×
544
                                'p.created_at <= ?' => time(),
×
545
                                'p.page_id != ?'    => $page['id'],
×
546
                        ]);
×
547

548
                $this->addTranslationJoins($select, ['fields' => ['title', 'content']]);
×
549

550
                $select->where->in('p.permissions', Permission::all());
×
551
                $select
×
552
                        ->where(new Expression($searchFormula . ' > 0'))
×
553
                        ->where($this->getTranslationFilter())
×
554
                        ->order('related DESC')
×
555
                        ->limit(4);
×
556

557
                $result = $this->sql->execute($select);
×
558

559
                $items = [];
×
560
                foreach ($result as $row) {
×
561
                        if (Setting::isFrontpage($row['slug']))
×
562
                                continue;
×
563

564
                        $row['content'] = Content::parse($row['content'], $row['type']);
×
565

566
                        $items[$row['page_id']] = [
×
567
                                'id'    => $row['page_id'],
×
568
                                'slug'  => $row['slug'],
×
569
                                'link'  => LP_PAGE_URL . $row['slug'],
×
570
                                'image' => Str::getImageFromText($row['content']),
×
571
                                'title' => $row['title'],
×
572
                        ];
×
573
                }
574

575
                return $items;
×
576
        }
577

578
        public function updateNumViews(int $item): void
579
        {
580
                $update = $this->sql->update('lp_pages');
×
581
                $update->set(['num_views' => new Expression('num_views + 1')]);
×
582
                $update->where(['page_id = ?' => $item]);
×
583
                $update->where->notIn('status', [Status::INACTIVE->value, Status::UNAPPROVED->value]);
×
584

585
                $this->sql->execute($update);
×
586
        }
587

588
        public function getMenuItems(): array
589
        {
590
                return $this->langCache('menu_pages')
×
591
                        ->setFallback(function () {
×
592
                                $params = $this->getLangQueryParams();
×
593

594
                                $subSelect = $this->sql->select()
×
595
                                        ->from('lp_translations')
×
596
                                        ->columns(['title'])
×
597
                                        ->where(new Expression('item_id = p.page_id'));
×
598
                                $subSelect->where->equalTo('type', $this->entity);
×
599
                                $subSelect->where->in('lang', [$params['lang'], $params['fallback_lang']]);
×
600
                                $subSelect
×
601
                                        ->order(new Expression('lang = ? DESC', [$params['lang']]))
×
602
                                        ->limit(1);
×
603

604
                                $select = $this->sql->select()
×
605
                                        ->from(['p' => 'lp_pages'])
×
606
                                        ->columns([
×
607
                                                'page_id',
×
608
                                                'slug',
×
609
                                                'permissions',
×
610
                                                'icon' => new Expression('pp2.value'),
×
611
                                                'page_title' => $subSelect,
×
612
                                        ])
×
613
                                        ->where([
×
614
                                                'p.status'          => Status::ACTIVE->value,
×
615
                                                'p.deleted_at'      => 0,
×
616
                                                'p.created_at <= ?' => time(),
×
617
                                                'pp.value = ?'      => '1',
×
618
                                        ]);
×
619

620
                                $this->addParamJoins($select, ['params' => ['show_in_menu', 'page_icon']]);
×
621

622
                                $select->where->in('p.entry_type', EntryType::withoutDrafts());
×
623
                                $select->where($this->getTranslationFilter('p', 'page_id', ['title']));
×
624

625
                                $result = $this->sql->execute($select);
×
626

627
                                $pages = [];
×
628
                                foreach ($result as $row) {
×
629
                                        Lang::censorText($row['page_title']);
×
630

631
                                        $pages[$row['page_id']] = [
×
632
                                                'id'          => $row['page_id'],
×
633
                                                'slug'        => $row['slug'],
×
634
                                                'permissions' => $row['permissions'],
×
635
                                                'icon'        => $row['icon'],
×
636
                                                'title'       => $row['page_title'],
×
637
                                        ];
×
638
                                }
639

640
                                return $pages;
×
641
                        });
×
642
        }
643

644
        public function prepareData(?array &$data): void
645
        {
646
                if (empty($data))
×
647
                        return;
×
648

649
                $isAuthor = $data['author_id'] && $data['author_id'] == User::$me->id;
×
650

651
                $data['created']  = DateTime::relative($data['created_at']);
×
652
                $data['updated']  = DateTime::relative($data['updated_at']);
×
653
                $data['can_view'] = Permission::canViewItem($data['permissions']) || User::$me->is_admin || $isAuthor;
×
654
                $data['can_edit'] = User::$me->is_admin
×
655
                        || User::$me->allowedTo('light_portal_manage_pages_any')
×
656
                        || (User::$me->allowedTo('light_portal_manage_pages_own') && $isAuthor);
×
657

658
                if ($data['type'] === ContentType::BBC->name()) {
×
659
                        $data['content'] = Msg::un_preparsecode($data['content']);
×
660
                }
661

662
                $this->dispatcher->dispatch(PortalHook::preparePageData, ['data' => &$data, 'isAuthor' => $isAuthor]);
×
663
        }
664

665
        public function fetchTags(array $pageIds): iterable
666
        {
667
                if ($pageIds === []) {
3✔
668
                        return;
×
669
                }
670

671
                $select = $this->sql->select()
3✔
672
                        ->from(['tag' => 'lp_tags'])
3✔
673
                        ->join(
3✔
674
                                ['pt' => 'lp_page_tag'],
3✔
675
                                'tag.tag_id = pt.tag_id',
3✔
676
                                ['page_id']
3✔
677
                        )
3✔
678
                        ->where([
3✔
679
                                'tag.status' => Status::ACTIVE->value,
3✔
680
                                'pt.page_id' => $pageIds,
3✔
681
                        ])
3✔
682
                        ->where($this->getTranslationFilter('tag', 'tag_id', ['title'], 'tag'))
3✔
683
                        ->order('title');
3✔
684

685
                $this->addTranslationJoins($select, ['primary' => 'tag.tag_id', 'entity' => 'tag']);
3✔
686

687
                $result = $this->sql->execute($select);
3✔
688

689
                foreach ($result as $row) {
3✔
690
                        Lang::censorText($row['title']);
×
691

692
                        yield $row['page_id'] => [
×
693
                                'tag_id' => $row['tag_id'],
×
694
                                'slug'   => $row['slug'],
×
695
                                'icon'   => Icon::parse($row['icon'] ?: 'fas fa-tag'),
×
696
                                'href'   => PortalSubAction::TAGS->url() . ';id=' . $row['tag_id'],
×
697
                                'name'   => $row['title'],
×
698
                        ];
×
699
                }
700
        }
701

702
        private function addData(array $data): int
703
        {
704
                try {
705
                        $this->transaction->begin();
×
706

707
                        $insert = $this->sql->insert('lp_pages', 'page_id')
×
708
                                ->values([
×
709
                                        'category_id' => $data['category_id'],
×
710
                                        'author_id'   => $data['author_id'],
×
711
                                        'slug'        => $data['slug'],
×
712
                                        'type'        => $data['type'],
×
713
                                        'entry_type'  => $data['entry_type'],
×
714
                                        'permissions' => $data['permissions'],
×
715
                                        'status'      => $data['status'],
×
716
                                        'created_at'  => $this->getPublishTime($data),
×
717
                                ]);
×
718

719
                        $result = $this->sql->execute($insert);
×
720

721
                        $item = (int) $result->getGeneratedValue('page_id');
×
722

723
                        if (empty($item)) {
×
724
                                $this->transaction->rollback();
×
725

726
                                return 0;
×
727
                        }
728

729
                        $this->dispatcher->dispatch(PortalHook::onPageSaving, ['item' => $item]);
×
730

731
                        $data['id'] = $item;
×
732

733
                        $this->saveTranslations($data);
×
734
                        $this->saveOptions($data);
×
735
                        $this->saveTags($data);
×
736

737
                        $this->transaction->commit();
×
738

739
                        // Notify page moderators about new page
740
                        $options = [
×
741
                                'item'      => $item,
×
742
                                'time'      => $this->getPublishTime($data),
×
743
                                'author_id' => $data['author_id'],
×
744
                                'title'     => $data['title'],
×
745
                                'url'       => LP_PAGE_URL . $data['slug']
×
746
                        ];
×
747

748
                        if (! User::$me->allowedTo('light_portal_manage_pages_any')) {
×
749
                                $this->notifier->notify(NotifyType::NEW_PAGE->name(), AlertAction::PAGE_UNAPPROVED->name(), $options);
×
750
                        }
751

752
                        return $item;
×
753
                } catch (Exception $e) {
×
754
                        $this->transaction->rollback();
×
755

756
                        ErrorHandler::fatal($e->getMessage(), false);
×
757

758
                        return 0;
×
759
                }
760
        }
761

762
        private function updateData(int $item, array $data): void
763
        {
764
                try {
765
                        $this->transaction->begin();
×
766

767
                        $update = $this->sql->update('lp_pages')
×
768
                                ->set([
×
769
                                        'category_id' => $data['category_id'],
×
770
                                        'author_id'   => $data['author_id'],
×
771
                                        'slug'        => $data['slug'],
×
772
                                        'type'        => $data['type'],
×
773
                                        'entry_type'  => $data['entry_type'],
×
774
                                        'permissions' => $data['permissions'],
×
775
                                        'status'      => $data['status'],
×
776
                                        'updated_at'  => time(),
×
777
                                ])
×
778
                                ->where(['page_id = ?' => $item]);
×
779

780
                        $this->sql->execute($update);
×
781

782
                        $this->dispatcher->dispatch(PortalHook::onPageSaving, ['item' => $item]);
×
783

784
                        $this->saveTranslations($data, true);
×
785
                        $this->saveTags($data, true);
×
786
                        $this->saveOptions($data, true);
×
787

788
                        if ($data['author_id'] !== User::$me->id) {
×
789
                                $title = $data['title'];
×
790

791
                                Logging::logAction('update_lp_page', [
×
792
                                        $this->entity => Str::html('a', $title)->href(LP_PAGE_URL . $data['slug'])
×
793
                                ]);
×
794
                        }
795

796
                        $this->transaction->commit();
×
797
                } catch (Exception $e) {
×
798
                        $this->transaction->rollback();
×
799

800
                        ErrorHandler::fatal($e->getMessage(), false);
×
801
                }
802
        }
803

804
        private function getTags(int $item): array
805
        {
806
                $tags = [];
3✔
807

808
                foreach ($this->fetchTags([$item]) as $tag) {
3✔
809
                        $tags[$tag['tag_id']] = $tag;
×
810
                }
811

812
                return $tags;
3✔
813
        }
814

815
        private function saveTags(array $data, bool $replace = false): void
816
        {
817
                $rows = [];
×
818
                foreach ($data['tags'] as $tag) {
×
819
                        $rows[] = [
×
820
                                'page_id' => $data['id'],
×
821
                                'tag_id'  => $tag,
×
822
                        ];
×
823
                }
824

825
                if ($rows === [])
×
826
                        return;
×
827

828
                $sqlObject = $replace
×
829
                        ? $this->sql->replace('lp_page_tag')->setConflictKeys(['page_id', 'tag_id'])->batch($rows)
×
830
                        : $this->sql->insert('lp_page_tag')->batch($rows);
×
831

832
                $this->sql->execute($sqlObject);
×
833
        }
834

835
        private function getPublishTime(array $data): int
836
        {
837
                $publishTime = time();
×
838

839
                if ($data['date']) {
×
840
                        $publishTime = strtotime((string) $data['date']);
×
841
                }
842

843
                if ($data['time']) {
×
844
                        $publishTime = strtotime(
×
845
                                date('Y-m-d', $publishTime) . ' ' . $data['time']
×
846
                        );
×
847
                }
848

849
                return $publishTime;
×
850
        }
851

852
        private function getImageFromContent(string $content, string $type): string
853
        {
854
                if (empty(Config::$modSettings['lp_page_og_image']))
3✔
855
                        return '';
3✔
856

857
                $content = Content::parse($content, $type);
×
858
                $imageIsFound = preg_match_all('/<img(.*)src(.*)=(.*)"(.*)"/U', $content, $values);
×
859

860
                if ($imageIsFound && is_array($values)) {
×
861
                        $allImages = array_pop($values);
×
862
                        $image = Config::$modSettings['lp_page_og_image'] == 1
×
863
                                ? array_shift($allImages)
×
864
                                : array_pop($allImages);
×
865

866
                        return Utils::htmlspecialchars($image);
×
867
                }
868

869
                return '';
×
870
        }
871
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc