Completed
Push — master ( cb292b...347229 )
by Robbie
18s queued 10s
created

BlogController::getArchiveDay()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 8.6186
c 0
b 0
f 0
cc 7
nc 8
nop 0
1
<?php
2
3
namespace SilverStripe\Blog\Model;
4
5
use PageController;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Control\HTTPResponse_Exception;
8
use SilverStripe\Control\RSS\RSSFeed;
9
use SilverStripe\ORM\DataList;
10
use SilverStripe\ORM\FieldType\DBDatetime;
11
use SilverStripe\ORM\FieldType\DBHTMLText;
12
use SilverStripe\ORM\PaginatedList;
13
use SilverStripe\ORM\SS_List;
14
use SilverStripe\Security\Member;
15
use SilverStripe\View\Parsers\URLSegmentFilter;
16
17
/**
18
 * @method Blog data()
19
 */
20
class BlogController extends PageController
21
{
22
    /**
23
     * @var array
24
     */
25
    private static $allowed_actions = [
26
        'archive',
27
        'tag',
28
        'category',
29
        'rss',
30
        'profile'
31
    ];
32
33
    /**
34
     * @var array
35
     */
36
    private static $url_handlers = [
37
        'tag/$Tag!/$Rss'             => 'tag',
38
        'category/$Category!/$Rss'   => 'category',
39
        'archive/$Year!/$Month/$Day' => 'archive',
40
        'profile/$Profile!'          => 'profile'
41
    ];
42
43
    /**
44
     * @var array
45
     */
46
    private static $casting = [
47
        'MetaTitle'         => 'Text',
48
        'FilterDescription' => 'Text'
49
    ];
50
51
    /**
52
     * If enabled, blog author profiles will be turned off for this site
53
     *
54
     * @config
55
     * @var bool
56
     */
57
    private static $disable_profiles = false;
58
59
    /**
60
     * The current Blog Post DataList query.
61
     *
62
     * @var DataList
63
     */
64
    protected $blogPosts;
65
66
    /**
67
     * Renders a Blog Member's profile.
68
     *
69
     * @throws HTTPResponse_Exception
70
     * @return $this
71
     */
72
    public function profile()
73
    {
74
        if ($this->config()->get('disable_profiles')) {
75
            $this->httpError(404, 'Not Found');
76
        }
77
78
        // Get profile posts
79
        $posts = $this->getCurrentProfilePosts();
80
        if (!$posts) {
81
            $this->httpError(404, 'Not Found');
82
        }
83
84
        $this->setFilteredPosts($posts);
85
        return $this;
86
    }
87
88
    /**
89
     * Get the Member associated with the current URL segment.
90
     *
91
     * @return null|Member|BlogMemberExtension
92
     */
93
    public function getCurrentProfile()
94
    {
95
        $segment = $this->getCurrentProfileURLSegment();
96
        if (!$segment) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $segment of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
97
            return null;
98
        }
99
100
        /** @var Member $profile */
101
        $profile = Member::get()
102
            ->find('URLSegment', $segment);
103
        return $profile;
104
    }
105
106
    /**
107
     * Get URL Segment of current profile
108
     *
109
     * @return null|string
110
     */
111 View Code Duplication
    public function getCurrentProfileURLSegment()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
112
    {
113
        $segment = isset($this->urlParams['Profile'])
114
            ? $this->urlParams['Profile']
115
            : null;
116
        if (!$segment) {
117
            return null;
118
        }
119
120
        // url encode unless it's multibyte (already pre-encoded in the database)
121
        // see https://github.com/silverstripe/silverstripe-cms/pull/2384
122
        return URLSegmentFilter::singleton()->getAllowMultibyte()
123
            ? $segment
124
            : rawurlencode($segment);
125
    }
126
127
    /**
128
     * Get posts related to the current Member profile.
129
     *
130
     * @return null|DataList|BlogPost[]
131
     */
132
    public function getCurrentProfilePosts()
133
    {
134
        $profile = $this->getCurrentProfile();
135
136
        if ($profile) {
137
            return $profile->BlogPosts()->filter('ParentID', $this->ID);
138
        }
139
140
        return null;
141
    }
142
143
    /**
144
     * Renders an archive for a specified date. This can be by year or year/month.
145
     *
146
     * @return $this
147
     * @throws HTTPResponse_Exception
148
     */
149
    public function archive()
150
    {
151
        $year = $this->getArchiveYear();
152
        $month = $this->getArchiveMonth();
153
        $day = $this->getArchiveDay();
154
155
        // Validate all values
156
        if ($year === false || $month === false || $day === false) {
157
            $this->httpError(404, 'Not Found');
158
        }
159
160
        $posts = $this->data()->getArchivedBlogPosts($year, $month, $day);
161
        $this->setFilteredPosts($posts);
162
        return $this;
163
    }
164
165
    /**
166
     * Fetches the archive year from the url.
167
     *
168
     * Returns int if valid, current year if not provided, false if invalid value
169
     *
170
     * @return int|false
171
     */
172
    public function getArchiveYear()
173
    {
174
        if (isset($this->urlParams['Year'])
175
            && preg_match('/^[0-9]{4}$/', $this->urlParams['Year'])
176
        ) {
177
            return (int)$this->urlParams['Year'];
178
        }
179
180
        if (empty($this->urlParams['Year']) &&
181
            $this->urlParams['Action'] === 'archive'
182
        ) {
183
            return DBDatetime::now()->Year();
184
        }
185
186
        return false;
187
    }
188
189
    /**
190
     * Fetches the archive money from the url.
191
     *
192
     * Returns int if valid, null if not provided, false if invalid value
193
     *
194
     * @return null|int|false
195
     */
196
    public function getArchiveMonth()
197
    {
198
        $month = isset($this->urlParams['Month'])
199
            ? $this->urlParams['Month']
200
            : null;
201
202
        if (!$month) {
203
            return null;
204
        }
205
206
        if (preg_match('/^[0-9]{1,2}$/', $month)
207
            && $month > 0
208
            && $month < 13
209
        ) {
210
            return (int)$month;
211
        }
212
213
        return false;
214
    }
215
216
    /**
217
     * Fetches the archive day from the url.
218
     *
219
     * Returns int if valid, null if not provided, false if invalid value
220
     *
221
     * @return null|int|false
222
     */
223
    public function getArchiveDay()
224
    {
225
        $day = isset($this->urlParams['Day'])
226
            ? $this->urlParams['Day']
227
            : null;
228
229
        if (!$day) {
230
            return null;
231
        }
232
233
        // Cannot calculate day without month and year
234
        $month = $this->getArchiveMonth();
235
        $year = $this->getArchiveYear();
236
        if (!$month || !$year) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $year of type integer|false is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
237
            return false;
238
        }
239
240
        if (preg_match('/^[0-9]{1,2}$/', $day) && checkdate($month, $day, $year)) {
241
            return (int)$day;
242
        }
243
244
        return false;
245
    }
246
247
    /**
248
     * Renders the blog posts for a given tag.
249
     *
250
     * @return DBHTMLText|$this
251
     * @throws HTTPResponse_Exception
252
     */
253 View Code Duplication
    public function tag()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
254
    {
255
        // Ensure tag exists
256
        $tag = $this->getCurrentTag();
257
        if (!$tag) {
258
            $this->httpError(404, 'Not Found');
259
        }
260
261
        // Get posts with this tag
262
        $posts = $this
263
            ->data()
264
            ->getBlogPosts()
265
            ->filter(['Tags.URLSegment' => $tag->URLSegment]); // Soft duplicate handling
266
267
        $this->setFilteredPosts($posts);
268
269
        // Render as RSS if provided
270
        if ($this->isRSS()) {
271
            return $this->rssFeed($posts, $tag->getLink());
272
        }
273
274
        return $this;
275
    }
276
277
    /**
278
     * Get BlogTag assigned to current filter
279
     *
280
     * @return null|BlogTag
281
     */
282
    public function getCurrentTag()
283
    {
284
        $segment = $this->getCurrentTagURLSegment();
285
        if (!$segment) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $segment of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
286
            return null;
287
        }
288
289
        /** @var BlogTag $tag */
290
        $tag = $this
291
            ->data()
292
            ->Tags(false)// Show "no results" instead of "404"
293
            ->find('URLSegment', $segment);
294
        return $tag;
295
    }
296
297
    /**
298
     * Get URLSegment of selected category (not: URLEncoded based on multibyte)
299
     *
300
     * @return string|null
301
     */
302 View Code Duplication
    public function getCurrentTagURLSegment()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
303
    {
304
        $segment = isset($this->urlParams['Tag'])
305
            ? $this->urlParams['Tag']
306
            : null;
307
308
        // url encode unless it's multibyte (already pre-encoded in the database)
309
        // see https://github.com/silverstripe/silverstripe-cms/pull/2384
310
        return URLSegmentFilter::singleton()->getAllowMultibyte()
311
            ? $segment
312
            : rawurlencode($segment);
313
    }
314
315
    /**
316
     * Renders the blog posts for a given category.
317
     *
318
     * @return DBHTMLText|$this
319
     * @throws HTTPResponse_Exception
320
     */
321 View Code Duplication
    public function category()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
322
    {
323
        $category = $this->getCurrentCategory();
324
325
        if (!$category) {
326
            $this->httpError(404, 'Not Found');
327
        }
328
329
        // Get posts with this category
330
        $posts = $this
331
            ->data()
332
            ->getBlogPosts()
333
            ->filter(['Categories.URLSegment' => $category->URLSegment]); // Soft duplicate handling
334
        $this->setFilteredPosts($posts);
335
336
        if ($this->isRSS()) {
337
            return $this->rssFeed($posts, $category->getLink());
338
        }
339
        return $this;
340
    }
341
342
    /**
343
     * Category Getter for use in templates.
344
     *
345
     * @return null|BlogCategory
346
     */
347
    public function getCurrentCategory()
348
    {
349
        $segment = $this->getCurrentCategoryURLSegment();
350
        if (!$segment) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $segment of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
351
            return null;
352
        }
353
354
        /** @var BlogCategory $category */
355
        $category = $this
356
            ->data()
357
            ->Categories(false)// Show "no results" instead of "404"
358
            ->find('URLSegment', $segment);
359
        return $category;
360
    }
361
362
    /**
363
     * Get URLSegment of selected category
364
     *
365
     * @return string|null
366
     */
367 View Code Duplication
    public function getCurrentCategoryURLSegment()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
368
    {
369
        $segment = isset($this->urlParams['Category'])
370
            ? $this->urlParams['Category']
371
            : null;
372
373
        // url encode unless it's multibyte (already pre-encoded in the database)
374
        // see https://github.com/silverstripe/silverstripe-cms/pull/2384
375
        return URLSegmentFilter::singleton()->getAllowMultibyte()
376
            ? $segment
377
            : rawurlencode($segment);
378
    }
379
380
    /**
381
     * Get the meta title for the current action.
382
     *
383
     * @return string
384
     */
385
    public function getMetaTitle()
386
    {
387
        $title = $this->data()->getTitle();
388
        $filter = $this->getFilterDescription();
389
390
        if ($filter) {
391
            $title = sprintf('%s - %s', $title, $filter);
392
        }
393
394
        $this->extend('updateMetaTitle', $title);
395
396
        return $title;
397
    }
398
399
    /**
400
     * Returns a description of the current filter.
401
     *
402
     * @return string
403
     */
404
    public function getFilterDescription()
405
    {
406
        $items = [];
407
408
        $list = $this->PaginatedList();
409
        $currentPage = $list->CurrentPage();
410
411
        if ($currentPage > 1) {
412
            $items[] = _t(
413
                'SilverStripe\\Blog\\Model\\Blog.FILTERDESCRIPTION_PAGE',
414
                'Page {page}',
415
                null,
416
                [
417
                    'page' => $currentPage
418
                ]
419
            );
420
        }
421
422
        if ($author = $this->getCurrentProfile()) {
423
            $items[] = _t(
424
                'SilverStripe\\Blog\\Model\\Blog.FILTERDESCRIPTION_AUTHOR',
425
                'By {author}',
426
                null,
427
                [
428
                    'author' => $author->Title
429
                ]
430
            );
431
        }
432
433
        if ($tag = $this->getCurrentTag()) {
434
            $items[] = _t(
435
                'SilverStripe\\Blog\\Model\\Blog.FILTERDESCRIPTION_TAG',
436
                'Tagged with {tag}',
437
                null,
438
                [
439
                    'tag' => $tag->Title
440
                ]
441
            );
442
        }
443
444
        if ($category = $this->getCurrentCategory()) {
445
            $items[] = _t(
446
                'SilverStripe\\Blog\\Model\\Blog.FILTERDESCRIPTION_CATEGORY',
447
                'In category {category}',
448
                null,
449
                [
450
                    'category' => $category->Title
451
                ]
452
            );
453
        }
454
455
        if ($this->getArchiveYear()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getArchiveYear() of type integer|false is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
456
            if ($this->getArchiveDay()) {
457
                $date = $this->getArchiveDate()->Nice();
458
            } elseif ($this->getArchiveMonth()) {
459
                $date = $this->getArchiveDate()->format('MMMM, y');
460
            } else {
461
                $date = $this->getArchiveDate()->format('y');
462
            }
463
464
            $items[] = _t(
465
                'SilverStripe\\Blog\\Model\\Blog.FILTERDESCRIPTION_DATE',
466
                'In {date}',
467
                null,
468
                [
469
                    'date' => $date,
470
                ]
471
            );
472
        }
473
474
        $result = '';
475
476
        if ($items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $items of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
477
            $result = implode(', ', $items);
478
        }
479
480
        $this->extend('updateFilterDescription', $result);
481
482
        return $result;
483
    }
484
485
    /**
486
     * Get filtered blog posts
487
     *
488
     * @return DataList|BlogPost[]
489
     */
490
    public function getFilteredPosts()
491
    {
492
        return $this->blogPosts ?: $this->data()->getBlogPosts();
493
    }
494
495
    /**
496
     * Set filtered posts
497
     *
498
     * @param SS_List|BlogPost[] $posts
499
     * @return $this
500
     */
501
    public function setFilteredPosts($posts)
502
    {
503
        $this->blogPosts = $posts;
0 ignored issues
show
Documentation Bug introduced by
It seems like $posts of type object<SilverStripe\ORM\SS_List> or array<integer,object<Sil...e\Blog\Model\BlogPost>> is incompatible with the declared type object<SilverStripe\ORM\DataList> of property $blogPosts.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
504
        return $this;
505
    }
506
507
    /**
508
     * Returns a list of paginated blog posts based on the BlogPost dataList.
509
     *
510
     * @return PaginatedList
511
     */
512
    public function PaginatedList()
513
    {
514
        $allPosts = $this->getFilteredPosts();
515
        $posts = PaginatedList::create($allPosts);
516
517
        // Set appropriate page size
518
        if ($this->data()->PostsPerPage > 0) {
519
            $pageSize = $this->data()->PostsPerPage;
520
        } elseif ($count = $allPosts->count()) {
521
            $pageSize = $count;
522
        } else {
523
            $pageSize = 99999;
524
        }
525
        $posts->setPageLength($pageSize);
526
527
        // Set current page
528
        $start = (int)$this->request->getVar($posts->getPaginationGetVar());
529
        $posts->setPageStart($start);
530
531
        return $posts;
532
    }
533
534
535
    /**
536
     * Returns the absolute link to the next page for use in the page meta tags. This helps search engines
537
     * find the pagination and index all pages properly.
538
     *
539
     * @example "<% if $PaginationAbsoluteNextLink %><link rel="next" href="$PaginationAbsoluteNextLink"><% end_if %>"
540
     *
541
     * @return string|null
542
     */
543
    public function PaginationAbsoluteNextLink()
544
    {
545
        $posts = $this->PaginatedList();
546
        if ($posts->NotLastPage()) {
547
            return Director::absoluteURL($posts->NextLink());
548
        }
549
550
        return null;
551
    }
552
553
    /**
554
     * Returns the absolute link to the previous page for use in the page meta tags. This helps search engines
555
     * find the pagination and index all pages properly.
556
     *
557
     * @example "<% if $PaginationAbsolutePrevLink %><link rel="prev" href="$PaginationAbsolutePrevLink"><% end_if %>"
558
     *
559
     * @return string|null
560
     */
561
    public function PaginationAbsolutePrevLink()
562
    {
563
        $posts = $this->PaginatedList();
564
        if ($posts->NotFirstPage()) {
565
            return Director::absoluteURL($posts->PrevLink());
566
        }
567
568
        return null;
569
    }
570
571
    /**
572
     * Displays an RSS feed of blog posts.
573
     *
574
     * @return string
575
     */
576
    public function rss()
577
    {
578
        return $this->rssFeed($this->getFilteredPosts(), $this->Link());
579
    }
580
581
    /**
582
     * Returns the current archive date.
583
     *
584
     * @return null|DBDatetime
585
     */
586
    public function getArchiveDate()
587
    {
588
        $year = $this->getArchiveYear();
589
        $month = $this->getArchiveMonth();
590
        $day = $this->getArchiveDay();
591
592
        if ($year) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $year of type integer|false is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
593
            if ($month) {
594
                $date = sprintf('%s-%s-01', $year, $month);
595
596
                if ($day) {
597
                    $date = sprintf('%s-%s-%s', $year, $month, $day);
598
                }
599
            } else {
600
                $date = sprintf('%s-01-01', $year);
601
            }
602
603
            $obj = DBDatetime::create('date');
604
            $obj->setValue($date);
605
            return $obj;
606
        }
607
608
        return null;
609
    }
610
611
    /**
612
     * Returns a link to the RSS feed.
613
     *
614
     * @return string
615
     */
616
    public function getRSSLink()
617
    {
618
        return $this->Link('rss');
619
    }
620
621
    /**
622
     * Displays an RSS feed of the given blog posts.
623
     *
624
     * @param DataList $blogPosts
625
     * @param string   $link
626
     *
627
     * @return DBHTMLText
628
     */
629
    protected function rssFeed($blogPosts, $link)
630
    {
631
        $rss = RSSFeed::create(
632
            $blogPosts,
633
            $link,
634
            $this->getMetaTitle(),
635
            $this->data()->MetaDescription
636
        );
637
638
        $this->extend('updateRss', $rss);
639
640
        return $rss->outputToBrowser();
641
    }
642
643
    /**
644
     * Returns true if the $Rss sub-action for categories/tags has been set to "rss"
645
     *
646
     * @return bool
647
     */
648
    protected function isRSS()
649
    {
650
        return isset($this->urlParams['RSS']) && strcasecmp($this->urlParams['RSS'], 'rss') == 0;
651
    }
652
}
653