Completed
Push — develop ( f25192...c2ccd8 )
by John
05:07 queued 02:11
created

Article::getAttachmentsURL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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