Completed
Push — develop ( dabc0d...b6b0f6 )
by John
03:01
created

Article::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 50
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 50
rs 9.3333
cc 1
eloc 34
nc 1
nop 0
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\Logging\Logger;
11
use Alpha\Util\Http\Session\SessionProviderFactory;
12
use Alpha\Exception\ValidationException;
13
use Alpha\Exception\FileNotFoundException;
14
use Alpha\Exception\AlphaException;
15
use Alpha\Controller\Front\FrontController;
16
17
/**
18
 * An article class for the CMS.
19
 *
20
 * @since 1.0
21
 *
22
 * @author John Collins <[email protected]>
23
 * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
24
 * @copyright Copyright (c) 2017, John Collins (founder of Alpha Framework).
25
 * All rights reserved.
26
 *
27
 * <pre>
28
 * Redistribution and use in source and binary forms, with or
29
 * without modification, are permitted provided that the
30
 * following conditions are met:
31
 *
32
 * * Redistributions of source code must retain the above
33
 *   copyright notice, this list of conditions and the
34
 *   following disclaimer.
35
 * * Redistributions in binary form must reproduce the above
36
 *   copyright notice, this list of conditions and the
37
 *   following disclaimer in the documentation and/or other
38
 *   materials provided with the distribution.
39
 * * Neither the name of the Alpha Framework nor the names
40
 *   of its contributors may be used to endorse or promote
41
 *   products derived from this software without specific
42
 *   prior written permission.
43
 *
44
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
45
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
46
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
47
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
48
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
49
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
50
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
51
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
52
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
53
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
54
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
55
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
56
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
57
 * </pre>
58
 */
59
class Article extends ActiveRecord
60
{
61
    /**
62
     * The article title.
63
     *
64
     * @var \Alpha\Model\Type\SmallText
65
     *
66
     * @since 1.0
67
     */
68
    protected $title;
69
70
    /**
71
     * The article site section.
72
     *
73
     * @var \Alpha\Model\Type\DEnum
74
     *
75
     * @since 1.0
76
     */
77
    protected $section;
78
79
    /**
80
     * The description of the article.
81
     *
82
     * @var \Alpha\Model\Type\SmallText
83
     *
84
     * @since 1.0
85
     */
86
    protected $description;
87
88
    /**
89
     * Optional custom body onload Javascript.
90
     *
91
     * @var \Alpha\Model\Type\SmallText
92
     *
93
     * @since 1.0
94
     */
95
    protected $bodyOnload;
96
97
    /**
98
     * Any custom HTML header content (e.g. Javascript) for the article.
99
     *
100
     * @var \Alpha\Model\Type\Text
101
     *
102
     * @since 1.0
103
     */
104
    protected $headerContent;
105
106
    /**
107
     * The article content.
108
     *
109
     * @var \Alpha\Model\Type\Text
110
     *
111
     * @since 1.0
112
     */
113
    protected $content;
114
115
    /**
116
     * The author of the article.
117
     *
118
     * @var \Alpha\Model\Type\SmallText
119
     *
120
     * @since 1.0
121
     */
122
    protected $author;
123
124
    /**
125
     * A boolean to control whether the artcile is publically accessible or not.
126
     *
127
     * @var \Alpha\Model\Type\Boolean
128
     *
129
     * @since 1.0
130
     */
131
    protected $published;
132
133
    /**
134
     * A Relation containing all of the comments on this article.
135
     *
136
     * @var \Alpha\Model\Type\Relation
137
     *
138
     * @since 1.0
139
     */
140
    protected $comments;
141
142
    /**
143
     * A Relation containing all of the votes on this article.
144
     *
145
     * @var \Alpha\Model\Type\Relation
146
     *
147
     * @since 1.0
148
     */
149
    protected $votes;
150
151
    /**
152
     * A Relation containing all of the tags on this article.
153
     *
154
     * @var \Alpha\Model\Type\Relation
155
     *
156
     * @since 1.0
157
     */
158
    protected $tags;
159
160
    /**
161
     * An array of all of the attributes on this Record which are tagged.
162
     *
163
     * @var array
164
     *
165
     * @since 1.0
166
     */
167
    protected $taggedAttributes = array('title', 'description', 'content');
168
169
    /**
170
     * Path to a .text file where the content of this article is stored (optional).
171
     *
172
     * @var string
173
     *
174
     * @since 1.0
175
     */
176
    private $filePath;
177
178
    /**
179
     * An array of data display labels for the class properties.
180
     *
181
     * @var array
182
     *
183
     * @since 1.0
184
     */
185
    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');
186
187
    /**
188
     * The name of the database table for the class.
189
     *
190
     * @var string
191
     *
192
     * @since 1.0
193
     */
194
    const TABLE_NAME = 'Article';
195
196
    /**
197
     * The URL for this article (transient).
198
     *
199
     * @var string
200
     *
201
     * @since 1.0
202
     */
203
    protected $URL;
204
205
    /**
206
     * The print URL for this article (transient).
207
     *
208
     * @var string
209
     *
210
     * @since 1.0
211
     */
212
    protected $printURL;
213
214
    /**
215
     * Trace logger.
216
     *
217
     * @var \Alpha\Util\Logging\Logger
218
     *
219
     * @since 1.0
220
     */
221
    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...
222
223
    /**
224
     * The constructor which sets up some housekeeping attributes.
225
     *
226
     * @since 1.0
227
     */
228
    public function __construct()
229
    {
230
        self::$logger = new Logger('Article');
231
232
        // ensure to call the parent constructor
233
        parent::__construct();
234
235
        $this->title = new SmallText();
236
        $this->title->setHelper('Please provide a title for the article.');
237
        $this->title->setSize(100);
238
        $this->title->setRule("/\w+/");
239
240
        $this->section = new DEnum('Alpha\Model\Article::section');
241
242
        $this->description = new SmallText();
243
        $this->description->setHelper('Please provide a brief description of the article.');
244
        $this->description->setSize(200);
245
        $this->description->setRule("/\w+/");
246
        $this->bodyOnload = new SmallText();
247
        $this->content = new Text();
248
        $this->headerContent = new Text();
249
        $this->author = new SmallText();
250
        $this->author->setHelper('Please state the name of the author of this article');
251
        $this->author->setSize(70);
252
        $this->author->setRule("/\w+/");
253
        $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...
254
255
        $this->comments = new Relation();
256
        $this->markTransient('comments');
257
258
        $this->votes = new Relation();
259
        $this->markTransient('votes');
260
261
        $this->tags = new Relation();
262
        $this->markTransient('tags');
263
264
        $this->URL = '';
265
        $this->printURL = '';
266
        // mark the URL attributes as transient
267
        $this->markTransient('URL');
268
        $this->markTransient('printURL');
269
270
        // mark title as unique
271
        $this->markUnique('title');
272
273
        $this->markTransient('filePath');
274
        $this->markTransient('taggedAttributes');
275
276
        $this->setupRels();
277
    }
278
279
    /**
280
     * After creating a new Article, tokenize the description field to form a set
281
     * of automated tags and save them.
282
     *
283
     * @since 1.0
284
     */
285
    protected function after_save_callback()
286
    {
287
        if ($this->getVersion() == 1 && $this->tags instanceof \Alpha\Model\Type\Relation) {
288
            // update the empty tags values to reference this ID
289
            $this->tags->setValue($this->ID);
290
291
            foreach ($this->taggedAttributes as $tagged) {
292
                $tags = Tag::tokenize($this->get($tagged), 'Alpha\Model\Article', $this->getID());
293
                foreach ($tags as $tag) {
294
                    try {
295
                        $tag->save();
296
                    } catch (ValidationException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
297
                        /*
298
                         * The unique key has most-likely been violated because this Record is already tagged with this
299
                         * value, so we can ignore in this case.
300
                         */
301
                    }
302
                }
303
            }
304
        }
305
306
        $this->setupRels();
307
    }
308
309
    /**
310
     * Set up the transient URL attributes for the artcile after it has loaded.
311
     *
312
     * @since 1.0
313
     */
314
    protected function after_loadByAttribute_callback()
315
    {
316
        $this->{'after_load_callback'}();
317
    }
318
319
    /**
320
     * Set up the transient URL attributes for the article after it has loaded.
321
     *
322
     * @since 1.0
323
     */
324
    protected function after_load_callback()
325
    {
326
        $config = ConfigProvider::getInstance();
327
328
        $this->URL = $config->get('app.url').'/a/'.str_replace(' ', $config->get('cms.url.title.separator'), $this->title->getValue());
329
330
        $this->printURL = $config->get('app.url').'/a/'.str_replace(' ', $config->get('cms.url.title.separator'), $this->title->getValue()).'/print';
331
332
        $this->setupRels();
333
    }
334
335
    /**
336
     * Gets an array of the IDs of the most recent articles added to the system (by date), from the newest
337
     * article to the amount specified by the $limit.
338
     *
339
     * @param int    $limit
340
     * @param string $excludeID
341
     *
342
     * @return array
343
     *
344
     * @since 1.0
345
     *
346
     * @throws \Alpha\Exception\AlphaException
347
     */
348
    public function loadRecentWithLimit($limit, $excludeID = '')
349
    {
350
        if ($excludeID != '') {
351
            $denum = new DEnum('Alpha\Model\Article::section');
352
            $excludeID = $denum->getOptionID($excludeID);
353
        }
354
355
        $sqlQuery = 'SELECT ID FROM '.$this->getTableName()." WHERE published='1' AND section!='$excludeID' ORDER BY created_ts DESC LIMIT 0, $limit;";
356
357
        $result = $this->query($sqlQuery);
358
359
        $IDs = array();
360
361
        foreach ($result as $row) {
362
            array_push($IDs, $row['ID']);
363
        }
364
365
        return $IDs;
366
    }
367
368
    /**
369
     * Generates the location of the attachments folder for this article.
370
     *
371
     * @return string
372
     *
373
     * @since 1.0
374
     */
375
    public function getAttachmentsLocation()
376
    {
377
        $config = ConfigProvider::getInstance();
378
379
        return $config->get('app.file.store.dir').'attachments/article_'.$this->getID();
380
    }
381
382
    /**
383
     * Generates the URL of the attachments folder for this article.
384
     *
385
     * @return string
386
     *
387
     * @since 1.0
388
     */
389
    public function getAttachmentsURL()
390
    {
391
        $config = ConfigProvider::getInstance();
392
393
        return $config->get('app.url').'/attachments/article_'.$this->getID();
394
    }
395
396
    /**
397
     * Generates a secure URL for downloading an attachment file via the ViewAttachment controller.
398
     *
399
     * @param string $filename
400
     *
401
     * @since 1.0
402
     */
403
    public function getAttachmentSecureURL($filename)
404
    {
405
        return FrontController::generateSecureURL('act=Alpha\\Controller\\AttachmentController&articleID='.$this->getID().'&filename='.$filename);
406
    }
407
408
    /**
409
     * Creates the attachment folder for the article on the server.
410
     *
411
     * @since 1.0
412
     *
413
     * @throws \Alpha\Exception\AlphaException
414
     */
415
    public function createAttachmentsFolder()
416
    {
417
        // create the attachment directory for the article
418
        try {
419
            mkdir($this->getAttachmentsLocation());
420
        } catch (\Exception $e) {
421
            throw new AlphaException('Unable to create the folder ['.$this->getAttachmentsLocation().'] for the article.');
422
        }
423
424
        // ...and set write permissions on the folder
425
        try {
426
            chmod($this->getAttachmentsLocation(), 0777);
427
        } catch (\Exception $e) {
428
            throw new AlphaException('Unable to set write permissions on the folder ['.$this->getAttachmentsLocation().'].');
429
        }
430
    }
431
432
    /**
433
     * Method for returning the calculated score for this article.
434
     *
435
     * @return string
436
     *
437
     * @since 1.0
438
     */
439
    public function getArticleScore()
440
    {
441
        $votes = $this->getArticleVotes();
442
443
        $score = 0;
444
        $total_score = 0;
445
        $vote_count = count($votes);
446
447
        for ($i = 0; $i < $vote_count; ++$i) {
448
            $total_score += $votes[$i]->get('score');
449
        }
450
451
        if ($vote_count > 0) {
452
            $score = $total_score/$vote_count;
453
        }
454
455
        return sprintf('%01.2f', $score);
456
    }
457
458
    /**
459
     * Method for fetching all of the votes for this article.
460
     *
461
     * @return array An array of ArticleVote objects
462
     *
463
     * @since 1.0
464
     */
465
    public function getArticleVotes()
466
    {
467
        $votes = $this->votes->getRelatedObjects();
468
469
        return $votes;
470
    }
471
472
    /**
473
     * Method to determine if the logged-in user has already voted for this article.
474
     *
475
     * @return bool True if they have voted already, false otherwise
476
     *
477
     * @since 1.0
478
     *
479
     * @throws \Alpha\Exception\AlphaException
480
     */
481
    public function checkUserVoted()
482
    {
483
        $config = ConfigProvider::getInstance();
484
        $sessionProvider = $config->get('session.provider.name');
485
        $session = SessionProviderFactory::getInstance($sessionProvider);
486
        // just going to return true if nobody is logged in
487
        if ($session->get('currentUser') == null) {
488
            return true;
489
        }
490
491
        $userID = $session->get('currentUser')->getID();
492
493
        $vote = new ArticleVote();
494
495
        $sqlQuery = 'SELECT COUNT(*) AS usersVote FROM '.$vote->getTableName()." WHERE articleID='".$this->ID."' AND personID='".$userID."';";
496
497
        $result = $this->query($sqlQuery);
498
499
        if (!isset($result[0])) {
500
            throw new AlphaException('Failed to check if the current user voted for the article ['.$this->ID.'], query ['.$sqlQuery.']');
501
        }
502
503
        $row = $result[0];
504
505
        if ($row['usersVote'] == '0') {
506
            return false;
507
        } else {
508
            return true;
509
        }
510
    }
511
512
    /**
513
     * Method for fetching all of the comments for this article.
514
     *
515
     * @return array An array of ArticleComment objects
516
     *
517
     * @since 1.0
518
     */
519
    public function getArticleComments()
520
    {
521
        $comments = $this->comments->getRelatedObjects();
522
523
        return $comments;
524
    }
525
526
    /**
527
     * Loads the content of the ArticleObject from the specified file path.
528
     *
529
     * @param string $filePath
530
     *
531
     * @since 1.0
532
     *
533
     * @throws \Alpha\Exception\FileNotFoundException
534
     */
535
    public function loadContentFromFile($filePath)
536
    {
537
        try {
538
            $this->content->setValue(file_get_contents($filePath));
539
            $this->filePath = $filePath;
540
        } catch (\Exception $e) {
541
            throw new FileNotFoundException($e->getMessage());
542
        }
543
    }
544
545
    /**
546
     * Returns true if the article content was loaded from a .text file, false otherwise.
547
     *
548
     * @return bool
549
     *
550
     * @since 1.0
551
     */
552
    public function isLoadedFromFile()
553
    {
554
        return $this->filePath == '' ? false : true;
555
    }
556
557
    /**
558
     * Returns the timestamp of when the content .text file for this article was last
559
     * modified.
560
     *
561
     * @return string
562
     *
563
     * @since 1.0
564
     *
565
     * @throws \Alpha\Exception\FileNotFoundException
566
     */
567
    public function getContentFileDate()
568
    {
569
        if ($this->filePath != '') {
570
            try {
571
                return date('Y-m-d H:i:s', filemtime($this->filePath));
572
            } catch (\Exception $e) {
573
                throw new FileNotFoundException($e->getMessage());
574
            }
575
        } else {
576
            throw new FileNotFoundException('Error trying to access an article content file when none is set!');
577
        }
578
    }
579
580
    /**
581
     * Sets up the Relation definitions on this record object.
582
     *
583
     * @since 2.0
584
     */
585
    protected function setupRels()
586
    {
587
        $this->comments->setValue($this->ID);
588
        $this->comments->setRelatedClass('Alpha\Model\ArticleComment');
589
        $this->comments->setRelatedClassField('articleID');
590
        $this->comments->setRelatedClassDisplayField('content');
591
        $this->comments->setRelationType('ONE-TO-MANY');
592
593
        $this->votes->setValue($this->ID);
594
        $this->votes->setRelatedClass('Alpha\Model\ArticleVote');
595
        $this->votes->setRelatedClassField('articleID');
596
        $this->votes->setRelatedClassDisplayField('score');
597
        $this->votes->setRelationType('ONE-TO-MANY');
598
599
        $this->tags->setRelatedClass('Alpha\Model\Tag');
600
        $this->tags->setRelatedClassField('taggedID');
601
        $this->tags->setRelatedClassDisplayField('content');
602
        $this->tags->setRelationType('ONE-TO-MANY');
603
        $this->tags->setTaggedClass(get_class($this));
604
        $this->tags->setValue($this->ID);
605
    }
606
}
607