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

AJenbo / agcms / 20972446633

13 Jan 2026 09:01PM UTC coverage: 53.72% (+0.2%) from 53.541%
20972446633

Pull #74

github

web-flow
Merge 8dba43e0f into 498ff829e
Pull Request #74: Add PHP versions 8.4 to 8.5 to CI matrix

248 of 341 new or added lines in 40 files covered. (72.73%)

6 existing lines in 5 files now uncovered.

2780 of 5175 relevant lines covered (53.72%)

13.07 hits per line

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

0.0
/application/inc/Http/Controllers/Admin/MaintenanceController.php
1
<?php
2

3
namespace App\Http\Controllers\Admin;
4

5
use AJenbo\Imap;
6
use App\Exceptions\Exception;
7
use App\Http\Request;
8
use App\Models\Category;
9
use App\Models\Contact;
10
use App\Models\CustomPage;
11
use App\Models\Email;
12
use App\Models\File;
13
use App\Models\Page;
14
use App\Services\ConfigService;
15
use App\Services\DbService;
16
use App\Services\EmailService;
17
use App\Services\OrmService;
18
use App\Services\RenderService;
19
use Symfony\Component\HttpFoundation\JsonResponse;
20
use Symfony\Component\HttpFoundation\Response;
21

22
/**
23
 * @todo test for missing alt="" in <img>
24
 */
25
class MaintenanceController extends AbstractAdminController
26
{
27
    /**
28
     * Create or edit category.
29
     *
30
     * @throws Exception
31
     */
32
    public function index(Request $request): Response
33
    {
34
        $db = app(DbService::class);
×
35

36
        $db->addLoadedTable('emails');
×
37
        $emailStatus = $db->fetchArray("SHOW TABLE STATUS LIKE 'emails'");
×
38
        /** @var (int|string)[] */
39
        $emailStatus = reset($emailStatus);
×
40

41
        $page = app(OrmService::class)->getOne(CustomPage::class, 0);
×
42
        if (!$page) {
×
43
            throw new Exception(_('Cron status missing'));
×
44
        }
45

46
        $db->addLoadedTable('emails');
×
47
        $data = [
×
48
            'dbSize'             => $this->byteToHuman($this->getDbSize()),
×
49
            'wwwSize'            => $this->byteToHuman($this->getSizeOfFiles()),
×
50
            'pendingEmails'      => $db->fetchOne("SELECT count(*) as 'count' FROM `emails`")['count'],
×
51
            'totalDelayedEmails' => (int)$emailStatus['Auto_increment'] - 1,
×
52
            'lastrun'            => $page->getTimeStamp(),
×
53
        ] + $this->basicPageData($request);
×
54

55
        return $this->render('admin/get_db_error', $data);
×
56
    }
57

58
    /**
59
     * Format bytes in a hum frindly maner.
60
     */
61
    private function byteToHuman(int $size): string
62
    {
63
        $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB', 'BiB'];
×
64
        foreach ($units as $unit) {
×
65
            if ($size < 1024 || 'BiB' === $unit) {
×
66
                break;
×
67
            }
68

69
            $size /= 1024;
×
70
        }
71

NEW
72
        return number_format($size, 1, valstring(localeconv()['mon_decimal_point']), '') . $unit;
×
73
    }
74

75
    /**
76
     * Remove newletter submissions that are missing vital information.
77
     */
78
    public function removeBadContacts(): JsonResponse
79
    {
80
        $contacts = app(OrmService::class)->getByQuery(
×
81
            Contact::class,
82
            "SELECT * FROM `email` WHERE `email` = '' AND `adresse` = '' AND `tlf1` = '' AND `tlf2` = ''"
83
        );
84
        foreach ($contacts as $contact) {
×
85
            $contact->delete();
×
86
        }
87

88
        return new JsonResponse(['count' => count($contacts)]);
×
89
    }
90

91
    /**
92
     * Get a list of pages with no bindings.
93
     */
94
    public function orphanPages(): JsonResponse
95
    {
96
        $pages = app(OrmService::class)->getByQuery(
×
97
            Page::class,
98
            'SELECT * FROM `sider` WHERE `id` NOT IN(SELECT `side` FROM `bind`)'
99
        );
100

101
        $html = '';
×
102
        if ($pages) {
×
103
            $html = '<b>' . _('The following pages have no binding') . '</b><br />';
×
104
            foreach ($pages as $page) {
×
105
                $html .= '<a href="/admin/page/' . $page->getId() . '/">' . $page->getId()
×
106
                    . ': ' . $page->getTitle() . '</a><br />';
×
107
            }
108
        }
109

110
        return new JsonResponse(['html' => $html]);
×
111
    }
112

113
    /**
114
     * Get list of pages with bindings to both active and inactive sections of the site.
115
     *
116
     * @throws Exception
117
     */
118
    public function mismatchedBindings(): JsonResponse
119
    {
120
        $html = '';
×
121

122
        // Map out active / inactive
123
        $categoryActiveMaps = [];
×
124

125
        $orm = app(OrmService::class);
×
126

127
        $categories = $orm->getByQuery(Category::class, 'SELECT * FROM `kat`');
×
128
        foreach ($categories as $category) {
×
129
            $categoryActiveMaps[(int)$category->isInactive()][] = $category->getId();
×
130
        }
131

132
        $pages = $orm->getByQuery(
×
133
            Page::class,
134
            '
135
            SELECT * FROM `sider`
136
            WHERE EXISTS (
137
                SELECT * FROM bind
138
                WHERE side = sider.id
139
                AND kat IN (' . implode(',', $categoryActiveMaps[0]) . ')
×
140
            )
141
            AND EXISTS (
142
                SELECT * FROM bind
143
                WHERE side = sider.id
144
                AND kat IN (' . implode(',', $categoryActiveMaps[1]) . ')
×
145
            )
146
            ORDER BY id
147
            '
148
        );
149
        if ($pages) {
×
150
            $html .= '<b>' . _('The following pages are both active and inactive') . '</b><br />';
×
151
            foreach ($pages as $page) {
×
152
                $html .= '<a href="/admin/page/' . $page->getId() . '/">' . $page->getId() . ': '
×
153
                    . $page->getTitle() . '</a><br />';
×
154
            }
155
        }
156

157
        $db = app(DbService::class);
×
158

159
        //Add active pages that has a list that links to this page
160
        $db->addLoadedTable('list_rows', 'lists', 'sider', 'bind');
×
161
        $pages = $db->fetchArray(
×
162
            '
163
            SELECT `sider`.*, `lists`.`page_id`
164
            FROM `list_rows`
165
            JOIN `lists` ON `list_rows`.`list_id` = `lists`.`id`
166
            JOIN `sider` ON `list_rows`.`link` = `sider`.id
167
            WHERE EXISTS (
168
                SELECT * FROM bind
169
                WHERE side = `lists`.`page_id`
170
                AND kat IN (' . implode(',', $categoryActiveMaps[0]) . ')
×
171
            )
172
            AND EXISTS (
173
                SELECT * FROM bind
174
                WHERE side = sider.id
175
                AND kat IN (' . implode(',', $categoryActiveMaps[1]) . ')
×
176
            )
177
            ORDER BY `lists`.`page_id`
178
            '
179
        );
180
        if ($pages) {
×
181
            $html .= '<b>' . _('The following inactive pages appear in a list on an active page:') . '</b><br />';
×
182
            foreach ($pages as $page) {
×
183
                $listPage = $orm->getOne(Page::class, (int)$page['page_id']);
×
184
                if (!$listPage) {
×
185
                    throw new Exception(_('Page disappeared during processing'));
×
186
                }
187

188
                unset($page['page_id']);
×
189
                $page = new Page(Page::mapFromDB($page));
×
190
                $html .= '<a href="/admin/page/' . $listPage->getId() . '/">' . $listPage->getId() . ': '
×
191
                    . $listPage->getTitle() . '</a> -&gt; <a href="/admin/page/' . $page->getId() . '/">'
×
192
                    . $page->getId() . ': ' . $page->getTitle() . '</a><br />';
×
193
            }
194
        }
195

196
        return new JsonResponse(['html' => $html]);
×
197
    }
198

199
    /**
200
     * List categories that have been circularly linked.
201
     */
202
    public function circularLinks(): JsonResponse
203
    {
204
        $html = '';
×
205

206
        $categories = app(OrmService::class)->getByQuery(Category::class, 'SELECT * FROM `kat` WHERE bind != 0 AND bind != -1');
×
207
        foreach ($categories as $category) {
×
208
            $branchIds = [$category->getId() => true];
×
209
            while ($category = $category->getParent()) {
×
210
                if (isset($branchIds[$category->getId()])) {
×
211
                    $html .= '<a href="/admin/categories/' . $category->getId() . '/">' . $category->getId()
×
212
                        . ': ' . $category->getTitle() . '</a><br />';
×
213
                    break;
×
214
                }
215
                $branchIds[$category->getId()] = true;
×
216
            }
217
        }
218
        if ($html) {
×
219
            $html = '<b>' . _('The following categories have circular references:') . '</b><br />' . $html;
×
220
        }
221

222
        return new JsonResponse(['html' => $html]);
×
223
    }
224

225
    /**
226
     * Remove enteries for files that do no longer exist.
227
     */
228
    public function removeNoneExistingFiles(): JsonResponse
229
    {
230
        $files = app(OrmService::class)->getByQuery(File::class, 'SELECT * FROM `files`');
×
231

232
        $app = app();
×
233

234
        $deleted = 0;
×
235
        $missingFiles = [];
×
236
        foreach ($files as $file) {
×
237
            if (!is_file($app->basePath($file->getPath()))) {
×
238
                if (!$file->isInUse()) {
×
239
                    $file->delete();
×
240
                    $deleted++;
×
241

242
                    continue;
×
243
                }
244

245
                $missingFiles[] = $file->getPath();
×
246
            }
247
        }
248

249
        return new JsonResponse(['missingFiles' => $missingFiles, 'deleted' => $deleted]);
×
250
    }
251

252
    /**
253
     * Get list of files with problematic names.
254
     */
255
    public function badFileNames(): JsonResponse
256
    {
257
        $files = app(OrmService::class)->getByQuery(
×
258
            File::class,
259
            '
260
            SELECT * FROM `files`
261
            WHERE `path` COLLATE UTF8_bin REGEXP \'[A-Z|_"\\\'`:%=#&+?*<>{}\\]+[^/]+$\'
262
            ORDER BY `path` ASC
263
            '
264
        );
265
        $html = '';
×
266
        if ($files) {
×
267
            $msg = ngettext(
×
268
                'The following %d file must be renamed:',
269
                'The following %d files must be renamed:',
270
                count($files)
×
271
            );
272
            $html = '<br /><b>' . sprintf($msg, count($files))
×
273
                . '</b><br /><br /><br /><a onclick="explorer(\'\',\'\')">';
274
            foreach ($files as $file) {
×
275
                $html .= $file->getPath() . '<br />';
×
276
            }
277
            $html .= '</a>';
×
278
        }
279

280
        return new JsonResponse(['html' => $html]);
×
281
    }
282

283
    /**
284
     * Get list of bad folder names.
285
     *
286
     * @todo only repport one error per folder
287
     */
288
    public function badFolderNames(): JsonResponse
289
    {
290
        $db = app(DbService::class);
×
291

292
        $db->addLoadedTable('files');
×
293
        $html = '';
×
294
        $errors = $db->fetchArray(
×
295
            '
296
            SELECT path FROM `files`
297
            WHERE `path` COLLATE UTF8_bin REGEXP \'[A-Z|_"\\\'`:%=#&+?*<>{}\\]+.*[/]+\'
298
            ORDER BY `path` ASC
299
            '
300
        );
301
        if ($errors) {
×
302
            $msg = ngettext(
×
303
                'The following %d file is in a folder that needs to be renamed:',
304
                'The following %d files are in a folder that needs to be renamed:',
305
                count($errors)
×
306
            );
307
            $html .= '<br /><b>' . sprintf($msg, count($errors)) . '</b><br /><a onclick="explorer(\'\',\'\')">';
×
308
            foreach ($errors as $value) {
×
309
                $html .= $value['path'] . '<br />';
×
310
            }
311
            $html .= '</a>';
×
312
        }
313
        if ($html) {
×
314
            $html = '<b>' . _('The following folders must be renamed:') . '</b><br />' . $html;
×
315
        }
316

317
        return new JsonResponse(['html' => $html]);
×
318
    }
319

320
    /**
321
     * Endpoint for getting system usage.
322
     */
323
    public function usage(Request $request): JsonResponse
324
    {
325
        return new JsonResponse([
×
326
            'www' => $this->getSizeOfFiles(),
×
327
            'db'  => $this->getDbSize(),
×
328
        ]);
329
    }
330

331
    /**
332
     * Resend any email that failed ealier.
333
     *
334
     * @throws Exception
335
     */
336
    public function sendDelayedEmail(): JsonResponse
337
    {
338
        $orm = app(OrmService::class);
×
339

340
        $cronStatus = $orm->getOne(CustomPage::class, 0);
×
341
        if (!$cronStatus) {
×
342
            throw new Exception(_('Cron status missing'));
×
343
        }
344

345
        $html = '';
×
346

347
        $emails = $orm->getByQuery(Email::class, 'SELECT * FROM `emails`');
×
348
        if ($emails) {
×
349
            $emailsSendt = 0;
×
350
            $emailService = app(EmailService::class);
×
351
            foreach ($emails as $email) {
×
352
                $emailService->send($email);
×
353
                $email->delete();
×
354
                $emailsSendt++;
×
355
            }
356

357
            $cronStatus->save();
×
358

359
            $msg = ngettext(
×
360
                '%d of %d email was sent.',
361
                '%d of %d emails were sent.',
362
                $emailsSendt
363
            );
364
            $html = sprintf($msg, $emailsSendt, count($emails));
×
365
        }
366

367
        return new JsonResponse(['html' => $html]);
×
368
    }
369

370
    /**
371
     * Get list of contacts with invalid emails.
372
     */
373
    public function contactsWithInvalidEmails(): JsonResponse
374
    {
375
        $contacts = app(OrmService::class)->getByQuery(Contact::class, "SELECT * FROM `email` WHERE `email` != ''");
×
376
        foreach ($contacts as $key => $contact) {
×
377
            if ($contact->isEmailValide()) {
×
378
                unset($contacts[$key]);
×
379
            }
380
        }
381

382
        $html = app(RenderService::class)->render('admin/partial-subscriptions_with_bad_emails', ['contacts' => $contacts]);
×
383

384
        return new JsonResponse(['html' => $html]);
×
385
    }
386

387
    /**
388
     * Get combined email usage.
389
     */
390
    public function mailUsage(): JsonResponse
391
    {
392
        $size = 0;
×
393

394
        foreach (ConfigService::getEmailConfigs() as $email) {
×
395
            $imap = new Imap(
×
396
                $email->address,
×
397
                $email->password,
×
398
                $email->imapHost,
×
399
                $email->imapPort
×
400
            );
401

402
            foreach ($imap->listMailboxes() as $mailbox) {
×
NEW
403
                $mailboxStatus = $imap->select(valstring($mailbox['name']), true);
×
404
                if (!$mailboxStatus['exists']) {
×
405
                    continue;
×
406
                }
407

408
                $mails = $imap->fetch('1:*', 'RFC822.SIZE');
×
409
                preg_match_all('/RFC822.SIZE\s([0-9]+)/', $mails['data'], $mailSizes);
×
410
                $size += array_sum($mailSizes[1]);
×
411
            }
412
        }
413

414
        return new JsonResponse(['size' => $size]);
×
415
    }
416

417
    /**
418
     * Get size of database.
419
     */
420
    private function getDbSize(): int
421
    {
422
        $tabels = app(DbService::class)->fetchArray('SHOW TABLE STATUS');
×
423
        $dbsize = 0;
×
424
        foreach ($tabels as $tabel) {
×
NEW
425
            $dbsize += valint($tabel['Data_length']);
×
NEW
426
            $dbsize += valint($tabel['Index_length']);
×
427
        }
428

429
        return (int)$dbsize;
×
430
    }
431

432
    /**
433
     * Get total size of files.
434
     */
435
    private function getSizeOfFiles(): int
436
    {
437
        $db = app(DbService::class);
×
438

439
        $db->addLoadedTable('files');
×
440
        $files = $db->fetchOne('SELECT sum(`size`) AS `filesize` FROM `files`');
×
441

442
        return (int)($files['filesize'] ?? 0);
×
443
    }
444
}
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