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