Completed
Push — develop ( fbb26c...f0282f )
by John
04:22 queued 01:09
created

Article   B

Complexity

Total Complexity 35

Size/Duplication

Total Lines 548
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
wmc 35
c 0
b 0
f 0
lcom 1
cbo 15
dl 0
loc 548
rs 8.25

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 50 1
B after_save_callback() 0 23 6
A after_loadByAttribute_callback() 0 4 1
A after_load_callback() 0 10 1
A loadRecentWithLimit() 0 19 3
A getAttachmentsLocation() 0 6 1
A getAttachmentsURL() 0 6 1
A getAttachmentSecureURL() 0 4 1
A createAttachmentsFolder() 0 16 3
A getArticleScore() 0 18 3
A getArticleVotes() 0 6 1
B checkUserVoted() 0 30 4
A getArticleComments() 0 6 1
A loadContentFromFile() 0 9 2
A isLoadedFromFile() 0 4 2
A getContentFileDate() 0 12 3
A setupRels() 0 21 1
1
<?php
2
3
namespace Alpha\Model;
4
5
use Alpha\Model\Type\SmallText;
6
use Alpha\Model\Type\DEnum;
7
use Alpha\Model\Type\Text;
8
use Alpha\Model\Type\Boolean;
9
use Alpha\Model\Type\Relation;
10
use Alpha\Util\Config\Configprovider;
11
use Alpha\Util\Logging\Logger;
12
use Alpha\Util\Http\Session\SessionProviderFactory;
13
use Alpha\Exception\ValidationException;
14
use Alpha\Exception\FileNotFoundException;
15
use Alpha\Exception\AlphaException;
16
use Alpha\Controller\Front\FrontController;
17
18
/**
19
 * An article class for the CMS.
20
 *
21
 * @since 1.0
22
 *
23
 * @author John Collins <[email protected]>
24
 * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
25
 * @copyright Copyright (c) 2017, John Collins (founder of Alpha Framework).
26
 * All rights reserved.
27
 *
28
 * <pre>
29
 * Redistribution and use in source and binary forms, with or
30
 * without modification, are permitted provided that the
31
 * following conditions are met:
32
 *
33
 * * Redistributions of source code must retain the above
34
 *   copyright notice, this list of conditions and the
35
 *   following disclaimer.
36
 * * Redistributions in binary form must reproduce the above
37
 *   copyright notice, this list of conditions and the
38
 *   following disclaimer in the documentation and/or other
39
 *   materials provided with the distribution.
40
 * * Neither the name of the Alpha Framework nor the names
41
 *   of its contributors may be used to endorse or promote
42
 *   products derived from this software without specific
43
 *   prior written permission.
44
 *
45
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
46
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
47
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
48
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
49
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
50
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
51
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
52
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
53
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
54
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
55
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
56
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
57
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
58
 * </pre>
59
 */
60
class Article extends ActiveRecord
61
{
62
    /**
63
     * The article title.
64
     *
65
     * @var \Alpha\Model\Type\SmallText
66
     *
67
     * @since 1.0
68
     */
69
    protected $title;
70
71
    /**
72
     * The article site section.
73
     *
74
     * @var \Alpha\Model\Type\DEnum
75
     *
76
     * @since 1.0
77
     */
78
    protected $section;
79
80
    /**
81
     * The description of the article.
82
     *
83
     * @var \Alpha\Model\Type\SmallText
84
     *
85
     * @since 1.0
86
     */
87
    protected $description;
88
89
    /**
90
     * Optional custom body onload Javascript.
91
     *
92
     * @var \Alpha\Model\Type\SmallText
93
     *
94
     * @since 1.0
95
     */
96
    protected $bodyOnload;
97
98
    /**
99
     * Any custom HTML header content (e.g. Javascript) for the article.
100
     *
101
     * @var \Alpha\Model\Type\Text
102
     *
103
     * @since 1.0
104
     */
105
    protected $headerContent;
106
107
    /**
108
     * The article content.
109
     *
110
     * @var \Alpha\Model\Type\Text
111
     *
112
     * @since 1.0
113
     */
114
    protected $content;
115
116
    /**
117
     * The author of the article.
118
     *
119
     * @var \Alpha\Model\Type\SmallText
120
     *
121
     * @since 1.0
122
     */
123
    protected $author;
124
125
    /**
126
     * A boolean to control whether the artcile is publically accessible or not.
127
     *
128
     * @var \Alpha\Model\Type\Boolean
129
     *
130
     * @since 1.0
131
     */
132
    protected $published;
133
134
    /**
135
     * A Relation containing all of the comments on this article.
136
     *
137
     * @var \Alpha\Model\Type\Relation
138
     *
139
     * @since 1.0
140
     */
141
    protected $comments;
142
143
    /**
144
     * A Relation containing all of the votes on this article.
145
     *
146
     * @var \Alpha\Model\Type\Relation
147
     *
148
     * @since 1.0
149
     */
150
    protected $votes;
151
152
    /**
153
     * A Relation containing all of the tags on this article.
154
     *
155
     * @var \Alpha\Model\Type\Relation
156
     *
157
     * @since 1.0
158
     */
159
    protected $tags;
160
161
    /**
162
     * An array of all of the attributes on this Record which are tagged.
163
     *
164
     * @var array
165
     *
166
     * @since 1.0
167
     */
168
    protected $taggedAttributes = array('title', 'description', 'content');
169
170
    /**
171
     * Path to a .text file where the content of this article is stored (optional).
172
     *
173
     * @var string
174
     *
175
     * @since 1.0
176
     */
177
    private $filePath;
178
179
    /**
180
     * An array of data display labels for the class properties.
181
     *
182
     * @var array
183
     *
184
     * @since 1.0
185
     */
186
    protected $dataLabels = array('ID' => 'Article ID#', 'title' => 'Title', 'section' => 'Site Section', 'description' => 'Description', 'bodyOnload' => 'Body onload Javascript', 'content' => 'Content', 'headerContent' => 'HTML Header Content', 'author' => 'Author', 'created_ts' => 'Date Added', 'updated_ts' => 'Date of last Update', 'published' => 'Published', 'URL' => 'URL', 'printURL' => 'Printer version URL', 'comments' => 'Comments', 'votes' => 'Votes', 'tags' => 'Tags');
187
188
    /**
189
     * The name of the database table for the class.
190
     *
191
     * @var string
192
     *
193
     * @since 1.0
194
     */
195
    const TABLE_NAME = 'Article';
196
197
    /**
198
     * The URL for this article (transient).
199
     *
200
     * @var string
201
     *
202
     * @since 1.0
203
     */
204
    protected $URL;
205
206
    /**
207
     * The print URL for this article (transient).
208
     *
209
     * @var string
210
     *
211
     * @since 1.0
212
     */
213
    protected $printURL;
214
215
    /**
216
     * Trace logger.
217
     *
218
     * @var \Alpha\Util\Logging\Logger
219
     *
220
     * @since 1.0
221
     */
222
    private static $logger = null;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
223
224
    /**
225
     * The constructor which sets up some housekeeping attributes.
226
     *
227
     * @since 1.0
228
     */
229
    public function __construct()
230
    {
231
        self::$logger = new Logger('Article');
232
233
        // ensure to call the parent constructor
234
        parent::__construct();
235
236
        $this->title = new SmallText();
237
        $this->title->setHelper('Please provide a title for the article.');
238
        $this->title->setSize(100);
239
        $this->title->setRule("/\w+/");
240
241
        $this->section = new DEnum('Alpha\Model\Article::section');
242
243
        $this->description = new SmallText();
244
        $this->description->setHelper('Please provide a brief description of the article.');
245
        $this->description->setSize(200);
246
        $this->description->setRule("/\w+/");
247
        $this->bodyOnload = new SmallText();
248
        $this->content = new Text();
249
        $this->headerContent = new Text();
250
        $this->author = new SmallText();
251
        $this->author->setHelper('Please state the name of the author of this article');
252
        $this->author->setSize(70);
253
        $this->author->setRule("/\w+/");
254
        $this->published = new Boolean(0);
0 ignored issues
show
Documentation introduced by
0 is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
255
256
        $this->comments = new Relation();
257
        $this->markTransient('comments');
258
259
        $this->votes = new Relation();
260
        $this->markTransient('votes');
261
262
        $this->tags = new Relation();
263
        $this->markTransient('tags');
264
265
        $this->URL = '';
266
        $this->printURL = '';
267
        // mark the URL attributes as transient
268
        $this->markTransient('URL');
269
        $this->markTransient('printURL');
270
271
        // mark title as unique
272
        $this->markUnique('title');
273
274
        $this->markTransient('filePath');
275
        $this->markTransient('taggedAttributes');
276
277
        $this->setupRels();
278
    }
279
280
    /**
281
     * After creating a new Article, tokenize the description field to form a set
282
     * of automated tags and save them.
283
     *
284
     * @since 1.0
285
     */
286
    protected function after_save_callback()
287
    {
288
        if ($this->getVersion() == 1 && $this->tags instanceof \Alpha\Model\Type\Relation) {
289
            // update the empty tags values to reference this ID
290
            $this->tags->setValue($this->ID);
291
292
            foreach ($this->taggedAttributes as $tagged) {
293
                $tags = Tag::tokenize($this->get($tagged), 'Alpha\Model\Article', $this->getID());
294
                foreach ($tags as $tag) {
295
                    try {
296
                        $tag->save();
297
                    } catch (ValidationException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
298
                        /*
299
                         * The unique key has most-likely been violated because this Record is already tagged with this
300
                         * value, so we can ignore in this case.
301
                         */
302
                    }
303
                }
304
            }
305
        }
306
307
        $this->setupRels();
308
    }
309
310
    /**
311
     * Set up the transient URL attributes for the artcile after it has loaded.
312
     *
313
     * @since 1.0
314
     */
315
    protected function after_loadByAttribute_callback()
316
    {
317
        $this->{'after_load_callback'}();
318
    }
319
320
    /**
321
     * Set up the transient URL attributes for the article after it has loaded.
322
     *
323
     * @since 1.0
324
     */
325
    protected function after_load_callback()
326
    {
327
        $config = ConfigProvider::getInstance();
328
329
        $this->URL = $config->get('app.url').'/a/'.str_replace(' ', $config->get('cms.url.title.separator'), $this->title->getValue());
330
331
        $this->printURL = $config->get('app.url').'/a/'.str_replace(' ', $config->get('cms.url.title.separator'), $this->title->getValue()).'/print';
332
333
        $this->setupRels();
334
    }
335
336
    /**
337
     * Gets an array of the IDs of the most recent articles added to the system (by date), from the newest
338
     * article to the amount specified by the $limit.
339
     *
340
     * @param int    $limit
341
     * @param string $excludeID
342
     *
343
     * @return array
344
     *
345
     * @since 1.0
346
     *
347
     * @throws \Alpha\Exception\AlphaException
348
     */
349
    public function loadRecentWithLimit($limit, $excludeID = '')
350
    {
351
        if ($excludeID != '') {
352
            $denum = new DEnum('Alpha\Model\Article::section');
353
            $excludeID = $denum->getOptionID($excludeID);
354
        }
355
356
        $sqlQuery = 'SELECT ID FROM '.$this->getTableName()." WHERE published='1' AND section!='$excludeID' ORDER BY created_ts DESC LIMIT 0, $limit;";
357
358
        $result = $this->query($sqlQuery);
359
360
        $IDs = array();
361
362
        foreach ($result as $row) {
363
            array_push($IDs, $row['ID']);
364
        }
365
366
        return $IDs;
367
    }
368
369
    /**
370
     * Generates the location of the attachments folder for this article.
371
     *
372
     * @return string
373
     *
374
     * @since 1.0
375
     */
376
    public function getAttachmentsLocation()
377
    {
378
        $config = ConfigProvider::getInstance();
379
380
        return $config->get('app.file.store.dir').'attachments/article_'.$this->getID();
381
    }
382
383
    /**
384
     * Generates the URL of the attachments folder for this article.
385
     *
386
     * @return string
387
     *
388
     * @since 1.0
389
     */
390
    public function getAttachmentsURL()
391
    {
392
        $config = ConfigProvider::getInstance();
393
394
        return $config->get('app.url').'/attachments/article_'.$this->getID();
395
    }
396
397
    /**
398
     * Generates a secure URL for downloading an attachment file via the ViewAttachment controller.
399
     *
400
     * @param string $filename
401
     *
402
     * @since 1.0
403
     */
404
    public function getAttachmentSecureURL($filename)
405
    {
406
        return FrontController::generateSecureURL('act=Alpha\\Controller\\AttachmentController&articleID='.$this->getID().'&filename='.$filename);
407
    }
408
409
    /**
410
     * Creates the attachment folder for the article on the server.
411
     *
412
     * @since 1.0
413
     *
414
     * @throws \Alpha\Exception\AlphaException
415
     */
416
    public function createAttachmentsFolder()
417
    {
418
        // create the attachment directory for the article
419
        try {
420
            mkdir($this->getAttachmentsLocation());
421
        } catch (\Exception $e) {
422
            throw new AlphaException('Unable to create the folder ['.$this->getAttachmentsLocation().'] for the article.');
423
        }
424
425
        // ...and set write permissions on the folder
426
        try {
427
            chmod($this->getAttachmentsLocation(), 0777);
428
        } catch (\Exception $e) {
429
            throw new AlphaException('Unable to set write permissions on the folder ['.$this->getAttachmentsLocation().'].');
430
        }
431
    }
432
433
    /**
434
     * Method for returning the calculated score for this article.
435
     *
436
     * @return string
437
     *
438
     * @since 1.0
439
     */
440
    public function getArticleScore()
441
    {
442
        $votes = $this->getArticleVotes();
443
444
        $score = 0;
445
        $total_score = 0;
446
        $vote_count = count($votes);
447
448
        for ($i = 0; $i < $vote_count; ++$i) {
449
            $total_score += $votes[$i]->get('score');
450
        }
451
452
        if ($vote_count > 0) {
453
            $score = $total_score/$vote_count;
454
        }
455
456
        return sprintf('%01.2f', $score);
457
    }
458
459
    /**
460
     * Method for fetching all of the votes for this article.
461
     *
462
     * @return array An array of ArticleVote objects
463
     *
464
     * @since 1.0
465
     */
466
    public function getArticleVotes()
467
    {
468
        $votes = $this->votes->getRelatedObjects();
469
470
        return $votes;
471
    }
472
473
    /**
474
     * Method to determine if the logged-in user has already voted for this article.
475
     *
476
     * @return bool True if they have voted already, false otherwise
477
     *
478
     * @since 1.0
479
     *
480
     * @throws \Alpha\Exception\AlphaException
481
     */
482
    public function checkUserVoted()
483
    {
484
        $config = ConfigProvider::getInstance();
485
        $sessionProvider = $config->get('session.provider.name');
486
        $session = SessionProviderFactory::getInstance($sessionProvider);
487
        // just going to return true if nobody is logged in
488
        if ($session->get('currentUser') == null) {
489
            return true;
490
        }
491
492
        $userID = $session->get('currentUser')->getID();
493
494
        $vote = new ArticleVote();
495
496
        $sqlQuery = 'SELECT COUNT(*) AS usersVote FROM '.$vote->getTableName()." WHERE articleID='".$this->ID."' AND personID='".$userID."';";
497
498
        $result = $this->query($sqlQuery);
499
500
        if (!isset($result[0])) {
501
            throw new AlphaException('Failed to check if the current user voted for the article ['.$this->ID.'], query ['.$sqlQuery.']');
502
        }
503
504
        $row = $result[0];
505
506
        if ($row['usersVote'] == '0') {
507
            return false;
508
        } else {
509
            return true;
510
        }
511
    }
512
513
    /**
514
     * Method for fetching all of the comments for this article.
515
     *
516
     * @return array An array of ArticleComment objects
517
     *
518
     * @since 1.0
519
     */
520
    public function getArticleComments()
521
    {
522
        $comments = $this->comments->getRelatedObjects();
523
524
        return $comments;
525
    }
526
527
    /**
528
     * Loads the content of the ArticleObject from the specified file path.
529
     *
530
     * @param string $filePath
531
     *
532
     * @since 1.0
533
     *
534
     * @throws \Alpha\Exception\FileNotFoundException
535
     */
536
    public function loadContentFromFile($filePath)
537
    {
538
        try {
539
            $this->content->setValue(file_get_contents($filePath));
540
            $this->filePath = $filePath;
541
        } catch (\Exception $e) {
542
            throw new FileNotFoundException($e->getMessage());
543
        }
544
    }
545
546
    /**
547
     * Returns true if the article content was loaded from a .text file, false otherwise.
548
     *
549
     * @return bool
550
     *
551
     * @since 1.0
552
     */
553
    public function isLoadedFromFile()
554
    {
555
        return $this->filePath == '' ? false : true;
556
    }
557
558
    /**
559
     * Returns the timestamp of when the content .text file for this article was last
560
     * modified.
561
     *
562
     * @return string
563
     *
564
     * @since 1.0
565
     *
566
     * @throws \Alpha\Exception\FileNotFoundException
567
     */
568
    public function getContentFileDate()
569
    {
570
        if ($this->filePath != '') {
571
            try {
572
                return date('Y-m-d H:i:s', filemtime($this->filePath));
573
            } catch (\Exception $e) {
574
                throw new FileNotFoundException($e->getMessage());
575
            }
576
        } else {
577
            throw new FileNotFoundException('Error trying to access an article content file when none is set!');
578
        }
579
    }
580
581
    /**
582
     * Sets up the Relation definitions on this record object.
583
     *
584
     * @since 2.0
585
     */
586
    protected function setupRels()
587
    {
588
        $this->comments->setValue($this->ID);
589
        $this->comments->setRelatedClass('Alpha\Model\ArticleComment');
590
        $this->comments->setRelatedClassField('articleID');
591
        $this->comments->setRelatedClassDisplayField('content');
592
        $this->comments->setRelationType('ONE-TO-MANY');
593
594
        $this->votes->setValue($this->ID);
595
        $this->votes->setRelatedClass('Alpha\Model\ArticleVote');
596
        $this->votes->setRelatedClassField('articleID');
597
        $this->votes->setRelatedClassDisplayField('score');
598
        $this->votes->setRelationType('ONE-TO-MANY');
599
600
        $this->tags->setRelatedClass('Alpha\Model\Tag');
601
        $this->tags->setRelatedClassField('taggedID');
602
        $this->tags->setRelatedClassDisplayField('content');
603
        $this->tags->setRelationType('ONE-TO-MANY');
604
        $this->tags->setTaggedClass(get_class($this));
605
        $this->tags->setValue($this->ID);
606
    }
607
}
608