Pages::getSummaryColumns()   A
last analyzed

Complexity

Conditions 6
Paths 16

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 16
nop 0
dl 0
loc 24
rs 9.2222
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Model;
6
7
use App\Repository\PagesRepository;
8
use DateTime;
9
10
/**
11
 * A Pages provides statistics about the pages created by a given User.
12
 */
13
class Pages extends Model
14
{
15
    private const RESULTS_LIMIT_SINGLE_NAMESPACE = 1000;
16
    private const RESULTS_LIMIT_ALL_NAMESPACES = 50;
17
18
    public const REDIR_NONE = 'noredirects';
19
    public const REDIR_ONLY = 'onlyredirects';
20
    public const REDIR_ALL = 'all';
21
    public const DEL_NONE = 'live';
22
    public const DEL_ONLY = 'deleted';
23
    public const DEL_ALL = 'all';
24
25
    /** @var string One of the self::REDIR_ constants of this class. */
26
    protected string $redirects;
27
28
    /** @var string One of the self::DEL_ constants of this class. */
29
    protected string $deleted;
30
31
    /** @var array The list of pages including various statistics, keyed by namespace. */
32
    protected array $pages;
33
34
    /** @var array Number of redirects/pages that were created/deleted, broken down by namespace. */
35
    protected array $countsByNamespace;
36
37
    /**
38
     * Pages constructor.
39
     * @param PagesRepository $repository
40
     * @param Project $project
41
     * @param User $user
42
     * @param string|int $namespace Namespace ID or 'all'.
43
     * @param string $redirects One of the Pages::REDIR_ constants.
44
     * @param string $deleted One of the Pages::DEL_ constants.
45
     * @param int|false $start Start date as Unix timestamp.
46
     * @param int|false $end End date as Unix timestamp.
47
     * @param int|false $offset Unix timestamp. Used for pagination.
48
     */
49
    public function __construct(
50
        PagesRepository $repository,
51
        Project $project,
52
        User $user,
53
        $namespace = 0,
54
        string $redirects = self::REDIR_NONE,
55
        string $deleted = self::DEL_ALL,
56
        $start = false,
57
        $end = false,
58
        $offset = false
59
    ) {
60
        $this->repository = $repository;
61
        $this->project = $project;
62
        $this->user = $user;
63
        $this->namespace = 'all' === $namespace ? 'all' : (int)$namespace;
64
        $this->start = $start;
65
        $this->end = $end;
66
        $this->redirects = $redirects ?: self::REDIR_NONE;
67
        $this->deleted = $deleted ?: self::DEL_ALL;
68
        $this->offset = $offset;
69
    }
70
71
    /**
72
     * The redirects option associated with this Pages instance.
73
     * @return string
74
     */
75
    public function getRedirects(): string
76
    {
77
        return $this->redirects;
78
    }
79
80
    /**
81
     * The deleted pages option associated with this Page instance.
82
     * @return string
83
     */
84
    public function getDeleted(): string
85
    {
86
        return $this->deleted;
87
    }
88
89
    /**
90
     * Fetch and prepare the pages created by the user.
91
     * @param bool $all Whether to get *all* results. This should only be used for
92
     *     export options. HTTP and JSON should paginate.
93
     * @return array
94
     * @codeCoverageIgnore
95
     */
96
    public function prepareData(bool $all = false): array
97
    {
98
        $this->pages = [];
99
100
        foreach ($this->getNamespaces() as $ns) {
101
            $data = $this->fetchPagesCreated($ns, $all);
102
            $this->pages[$ns] = count($data) > 0
103
                ? $this->formatPages($data)[$ns]
104
                : [];
105
        }
106
107
        return $this->pages;
108
    }
109
110
    /**
111
     * The public function to get the list of all pages created by the user,
112
     * up to self::resultsPerPage(), across all namespaces.
113
     * @param bool $all Whether to get *all* results. This should only be used for
114
     *     export options. HTTP and JSON should paginate.
115
     * @return array
116
     */
117
    public function getResults(bool $all = false): array
118
    {
119
        if (!isset($this->pages)) {
120
            $this->prepareData($all);
121
        }
122
        return $this->pages;
123
    }
124
125
    /**
126
     * Return a ISO 8601 timestamp of the last result. This is used for pagination purposes.
127
     * @return string|null
128
     */
129
    public function getLastTimestamp(): ?string
130
    {
131
        if ($this->isMultiNamespace()) {
132
            // No pagination in multi-namespace view.
133
            return null;
134
        }
135
136
        $numResults = count($this->getResults()[$this->getNamespace()]);
137
        $timestamp = new DateTime($this->getResults()[$this->getNamespace()][$numResults - 1]['timestamp']);
138
        return $timestamp->format('Y-m-d\TH:i:s\Z');
139
    }
140
141
    /**
142
     * Get the total number of pages the user has created.
143
     * @return int
144
     */
145
    public function getNumPages(): int
146
    {
147
        $total = 0;
148
        foreach (array_values($this->getCounts()) as $values) {
149
            $total += $values['count'];
150
        }
151
        return $total;
152
    }
153
154
    /**
155
     * Get the total number of pages we're showing data for.
156
     * @return int
157
     */
158
    public function getNumResults(): int
159
    {
160
        $total = 0;
161
        foreach (array_values($this->getResults()) as $pages) {
162
            $total += count($pages);
163
        }
164
        return $total;
165
    }
166
167
    /**
168
     * Get the total number of pages that are currently deleted.
169
     * @return int
170
     */
171
    public function getNumDeleted(): int
172
    {
173
        $total = 0;
174
        foreach (array_values($this->getCounts()) as $values) {
175
            $total += $values['deleted'];
176
        }
177
        return $total;
178
    }
179
180
    /**
181
     * Get the total number of pages that are currently redirects.
182
     * @return int
183
     */
184
    public function getNumRedirects(): int
185
    {
186
        $total = 0;
187
        foreach (array_values($this->getCounts()) as $values) {
188
            $total += $values['redirects'];
189
        }
190
        return $total;
191
    }
192
193
    /**
194
     * Get the namespaces in which this user has created pages.
195
     * @return int[] The IDs.
196
     */
197
    public function getNamespaces(): array
198
    {
199
        return array_keys($this->getCounts());
200
    }
201
202
    /**
203
     * Number of namespaces being reported.
204
     * @return int
205
     */
206
    public function getNumNamespaces(): int
207
    {
208
        return count(array_keys($this->getCounts()));
209
    }
210
211
    /**
212
     * Are there more than one namespace in the results?
213
     * @return bool
214
     */
215
    public function isMultiNamespace(): bool
216
    {
217
        return $this->getNumNamespaces() > 1 || ('all' === $this->getNamespace() && 1 === $this->getNumNamespaces());
218
    }
219
220
    /**
221
     * Get the sum of all page sizes, across all specified namespaces.
222
     * @return int
223
     */
224
    public function getTotalPageSize(): int
225
    {
226
        return array_sum(array_column($this->getCounts(), 'total_length'));
227
    }
228
229
    /**
230
     * Get average size across all pages.
231
     * @return float
232
     */
233
    public function averagePageSize(): float
234
    {
235
        return $this->getTotalPageSize() / $this->getNumPages();
236
    }
237
238
    /**
239
     * Number of redirects/pages that were created/deleted, broken down by namespace.
240
     * @return array Namespace IDs as the keys, with values 'count', 'deleted' and 'redirects'.
241
     */
242
    public function getCounts(): array
243
    {
244
        if (isset($this->countsByNamespace)) {
245
            return $this->countsByNamespace;
246
        }
247
248
        $counts = [];
249
250
        foreach ($this->countPagesCreated() as $row) {
251
            $ns = (int)$row['namespace'];
252
            $count = (int)$row['count'];
253
            $totalLength = (int)$row['total_length'];
254
            $counts[$ns] = [
255
                'count' => $count,
256
                'total_length' => $totalLength,
257
                'avg_length' => round($count > 0 ? $totalLength / $count : 0, 1),
258
            ];
259
            if (self::DEL_NONE !== $this->deleted) {
260
                $counts[$ns]['deleted'] = (int)$row['deleted'];
261
            }
262
            if (self::REDIR_NONE !== $this->redirects) {
263
                $counts[$ns]['redirects'] = (int)$row['redirects'];
264
            }
265
        }
266
267
        $this->countsByNamespace = $counts;
268
        return $this->countsByNamespace;
269
    }
270
271
    /**
272
     * Get the number of pages the user created by assessment.
273
     * @return array Keys are the assessment class, values are the counts.
274
     */
275
    public function getAssessmentCounts(): array
276
    {
277
        if ($this->getNumPages() > $this->resultsPerPage()) {
278
            $counts = $this->repository->getAssessmentCounts(
0 ignored issues
show
Bug introduced by
The method getAssessmentCounts() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PagesRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

278
            /** @scrutinizer ignore-call */ 
279
            $counts = $this->repository->getAssessmentCounts(
Loading history...
279
                $this->project,
280
                $this->user,
281
                $this->namespace,
282
                $this->redirects
283
            );
284
        } else {
285
            $counts = [];
286
            foreach ($this->pages as $nsPages) {
287
                foreach ($nsPages as $page) {
288
                    if (!isset($counts[$page['assessment']['class'] ?? 'Unknown'])) {
289
                        $counts[$page['assessment']['class'] ?? 'Unknown'] = 1;
290
                    } else {
291
                        $counts[$page['assessment']['class'] ?? 'Unknown']++;
292
                    }
293
                }
294
            }
295
        }
296
297
        arsort($counts);
298
299
        return $counts;
300
    }
301
302
    /**
303
     * Number of results to show, depending on the namespace.
304
     * @param bool $all Whether to get *all* results. This should only be used for
305
     *     export options. HTTP and JSON should paginate.
306
     * @return int|false
307
     */
308
    public function resultsPerPage(bool $all = false)
309
    {
310
        if (true === $all) {
311
            return false;
312
        }
313
        if ('all' === $this->namespace) {
314
            return self::RESULTS_LIMIT_ALL_NAMESPACES;
315
        }
316
        return self::RESULTS_LIMIT_SINGLE_NAMESPACE;
317
    }
318
319
    /**
320
     * What columns to show in namespace totals table.
321
     * @return string[]
322
     */
323
    public function getSummaryColumns(): array
324
    {
325
        $order = ['namespace', 'pages', 'redirects', 'deleted', 'live', 'total-page-size', 'average-page-size'];
326
327
        $summaryColumns = ['namespace'];
328
        if (in_array($this->getDeleted(), [self::DEL_ALL, self::DEL_ONLY])) {
329
            $summaryColumns[] = 'deleted';
330
        }
331
        if (self::DEL_ALL === $this->getDeleted()) {
332
            $summaryColumns[] = 'live';
333
        }
334
        if (in_array($this->getRedirects(), [self::REDIR_ALL, self::REDIR_ONLY])) {
335
            $summaryColumns[] = 'redirects';
336
        }
337
        if (self::DEL_ONLY !== $this->getDeleted() && self::REDIR_ONLY !== $this->getRedirects()) {
338
            $summaryColumns[] = 'pages';
339
        }
340
341
        $summaryColumns[] = 'total-page-size';
342
        $summaryColumns[] = 'average-page-size';
343
344
        // Re-sort based on $order
345
        return array_values(array_filter($order, static function ($column) use ($summaryColumns) {
346
            return in_array($column, $summaryColumns);
347
        }));
348
    }
349
350
    /**
351
     * Get the deletion summary to be shown when hovering over the "Deleted" text in the UI.
352
     * @param int $namespace
353
     * @param string $pageTitle
354
     * @param string $offset
355
     * @return string|null null if no deletion summary is available.
356
     */
357
    public function getDeletionSummary(int $namespace, string $pageTitle, string $offset): ?string
358
    {
359
        $ret = $this->repository->getDeletionSummary($this->project, $namespace, $pageTitle, $offset);
0 ignored issues
show
Bug introduced by
The method getDeletionSummary() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PagesRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

359
        /** @scrutinizer ignore-call */ 
360
        $ret = $this->repository->getDeletionSummary($this->project, $namespace, $pageTitle, $offset);
Loading history...
360
        if (!$ret) {
361
            return null;
362
        }
363
        $timestampStr = (new DateTime($ret['log_timestamp']))->format('Y-m-d H:i');
364
        $summary = Edit::wikifyString($ret['comment_text'], $this->project, $this->page, true);
365
        $userpageUrl = $this->project->getUrlForPage("User:{$ret['actor_name']}");
366
        return "$timestampStr (<a target='_blank' href=\"$userpageUrl\">{$ret['actor_name']}</a>): <i>$summary</i>";
367
    }
368
369
    /**
370
     * Run the query to get pages created by the user with options.
371
     * This is ran independently for each namespace if $this->namespace is 'all'.
372
     * @param int $namespace Namespace ID.
373
     * @param bool $all Whether to get *all* results. This should only be used for
374
     *     export options. HTTP and JSON should paginate.
375
     * @return array
376
     */
377
    private function fetchPagesCreated(int $namespace, bool $all = false): array
378
    {
379
        return $this->repository->getPagesCreated(
0 ignored issues
show
Bug introduced by
The method getPagesCreated() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PagesRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

379
        return $this->repository->/** @scrutinizer ignore-call */ getPagesCreated(
Loading history...
380
            $this->project,
381
            $this->user,
382
            $namespace,
383
            $this->redirects,
384
            $this->deleted,
385
            $this->start,
386
            $this->end,
387
            $this->resultsPerPage($all),
388
            $this->offset
389
        );
390
    }
391
392
    /**
393
     * Run the query to get the number of pages created by the user with given options.
394
     * @return array
395
     */
396
    private function countPagesCreated(): array
397
    {
398
        return $this->repository->countPagesCreated(
0 ignored issues
show
Bug introduced by
The method countPagesCreated() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PagesRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

398
        return $this->repository->/** @scrutinizer ignore-call */ countPagesCreated(
Loading history...
399
            $this->project,
400
            $this->user,
401
            $this->namespace,
402
            $this->redirects,
403
            $this->deleted,
404
            $this->start,
405
            $this->end
406
        );
407
    }
408
409
    /**
410
     * Format the data, adding page titles, assessment badges,
411
     * and sorting by namespace and then timestamp.
412
     * @param array $pages As returned by self::fetchPagesCreated()
413
     * @return array
414
     */
415
    private function formatPages(array $pages): array
416
    {
417
        $results = [];
418
419
        foreach ($pages as $row) {
420
            $fullPageTitle = $row['namespace'] > 0
421
                ? $this->project->getNamespaces()[$row['namespace']].':'.$row['page_title']
422
                : $row['page_title'];
423
            $pageData = [
424
                'deleted' => 'arc' === $row['type'],
425
                'namespace' => $row['namespace'],
426
                'page_title' => $row['page_title'],
427
                'full_page_title' => $fullPageTitle,
428
                'redirect' => (bool)$row['redirect'] || (bool)$row['was_redirect'],
429
                'timestamp' => $row['timestamp'],
430
                'rev_id' => $row['rev_id'],
431
                'rev_length' => $row['rev_length'],
432
                'length' => $row['length'],
433
            ];
434
435
            if ($row['recreated']) {
436
                $pageData['recreated'] = (bool)$row['recreated'];
437
            } else {
438
                // This is always NULL for live pages, in which case 'recreated' doesn't apply.
439
                unset($pageData['recreated']);
440
            }
441
442
            if ($this->project->hasPageAssessments()) {
443
                $attrs = $this->project
444
                    ->getPageAssessments()
445
                    ->getClassAttrs($row['pa_class'] ?: 'Unknown');
446
                $pageData['assessment'] = [
447
                    'class' => $row['pa_class'] ?: 'Unknown',
448
                    'badge' => $this->project
449
                        ->getPageAssessments()
450
                        ->getBadgeURL($row['pa_class'] ?: 'Unknown'),
451
                    'color' => $attrs['color'],
452
                    'category' => $attrs['category'],
453
                ];
454
            }
455
456
            $results[$row['namespace']][] = $pageData;
457
        }
458
459
        return $results;
460
    }
461
}
462