      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 1577 2012-10-24 22:21:51Z alphadevx $
     11:  * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
     12:  * @copyright Copyright (c) 2012, 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->content->setHelper('Please provide some content for the article.');
    218:         $this->content->setRule("/\w+/");
    219:         $this->headerContent = new Text();
    220:         $this->author = new String();
    221:         $this->author->setHelper('Please state the name of the author of this article');
    222:         $this->author->setSize(70);
    223:         $this->author->setRule("/\w+/");
    224:         $this->published = new Boolean(0);
    225:
    226:         $this->comments = new Relation();
    227:         $this->markTransient('comments');
    228:         $this->comments->setValue($this->OID);
    229:         $this->comments->setRelatedClass('ArticleCommentObject');
    230:         $this->comments->setRelatedClassField('articleOID');
    231:         $this->comments->setRelatedClassDisplayField('content');
    232:         $this->comments->setRelationType('ONE-TO-MANY');
    233:
    234:         $this->votes = new Relation();
    235:         $this->markTransient('votes');
    236:         $this->votes->setValue($this->OID);
    237:         $this->votes->setRelatedClass('ArticleVoteObject');
    238:         $this->votes->setRelatedClassField('articleOID');
    239:         $this->votes->setRelatedClassDisplayField('score');
    240:         $this->votes->setRelationType('ONE-TO-MANY');
    241:
    242:         $this->tags = new Relation();
    243:         $this->markTransient('tags');
    244:         $this->tags->setRelatedClass('TagObject');
    245:         $this->tags->setRelatedClassField('taggedOID');
    246:         $this->tags->setRelatedClassDisplayField('content');
    247:         $this->tags->setRelationType('ONE-TO-MANY');
    248:         $this->tags->setTaggedClass(get_class($this));
    249:         $this->tags->setValue($this->OID);
    250:
    251:         $this->URL = '';
    252:         $this->printURL = '';
    253:         // mark the URL attributes as transient
    254:         $this->markTransient('URL');
    255:         $this->markTransient('printURL');
    256:
    257:         // mark title as unique
    258:         $this->markUnique('title');
    259:
    260:         $this->markTransient('filePath');
    261:         $this->markTransient('taggedAttributes');
    262:     }
    263:
    264:     /**
    265:      * After creating a new ArticleObject, tokenize the description field to form a set
    266:      * of automated tags and save them.
    267:      *
    268:      * @since 1.0
    269:      */
    270:     protected function after_save_callback() {
    271:         if($this->getVersion() == 1 && $this->tags instanceof Relation) {
    272:             // update the empty tags values to reference this OID
    273:             $this->tags->setValue($this->OID);
    274:
    275:             foreach($this->taggedAttributes as $tagged) {
    276:                 $tags = TagObject::tokenize($this->get($tagged), get_class($this), $this->getOID());
    277:                 foreach($tags as $tag) {
    278:                     try {
    279:                         $tag->save();
    280:                     }catch(ValidationException $e){
    281:                         /*
    282:                          * The unique key has most-likely been violated because this BO is already tagged with this
    283:                          * value, so we can ignore in this case.
    284:                          */
    285:                     }
    286:                 }
    287:             }
    288:         }
    289:     }
    290:
    291:     /**
    292:      * Set up the transient URL attributes for the artcile after it has loaded
    293:      *
    294:      * @since 1.0
    295:      */
    296:     protected function after_loadByAttribute_callback() {
    297:         $this->after_load_callback();
    298:     }
    299:
    300:     /**
    301:      * Set up the transient URL attributes for the article after it has loaded
    302:      *
    303:      * @since 1.0
    304:      */
    305:     protected function after_load_callback() {
    306:         global $config;
    307:         global $front;
    308:
    309:         // check the config to see if we are using mod_rewrite
    310:         if($config->get('app.use.mod.rewrite')) {
    311:             // check to see if an alias is registered for the view_article_title controller, otherwise use the long URL version
    312:             if(isset($front) && $front->hasAlias('ViewArticleTitle')) {
    313:                 $alias = $front->getControllerAlias('ViewArticleTitle');
    314:
    315:                 $this->URL = $config->get('app.url').$alias.'/'.str_replace(' ', '-', $this->title->getValue());
    316:             }else{
    317:                 $this->URL = $config->get('app.url').'ViewArticleTitle/title/'.str_replace(' ', $config->get('cms.url.title.separator'), $this->title->getValue());
    318:             }
    319:         }else{
    320:             $this->URL = $config->get('app.url').'alpha/controller/ViewArticle.php?oid='.$this->getID();
    321:         }
    322:
    323:         // now set up the print version URL
    324:         if($config->get('app.use.mod.rewrite')) {
    325:             // check to see if an alias is registered for the view_article_title controller, otherwise use the long URL version
    326:             if(isset($front) && $front->hasAlias('ViewArticlePrint')) {
    327:                 $alias = $front->getControllerAlias('ViewArticlePrint');
    328:
    329:                 $this->printURL = $config->get('app.url').$alias.'/'.str_replace(' ', '-', $this->title->getValue());
    330:             }else{
    331:                 $this->printURL = $config->get('app.url').'ViewArticlePrint/title/'.str_replace(' ', $config->get('cms.url.title.separator'), $this->title->getValue());
    332:             }
    333:         }else{
    334:             $this->printURL = $config->get('app.url').'alpha/controller/ViewArticlePrint.php?title='.$this->title->getValue();
    335:         }
    336:
    337:         $this->comments->setValue($this->OID);
    338:         $this->votes->setValue($this->OID);
    339:     }
    340:
    341:     /**
    342:      * Gets an array of the OIDs of the most recent articles added to the system (by date), from the newest
    343:      * article to the amount specified by the $limit
    344:      *
    345:      * @param integer $limit
    346:      * @param string $excludeID
    347:      * @return array
    348:      * @since 1.0
    349:      * @throws AlphaException
    350:      */
    351:     public function loadRecentWithLimit($limit, $excludeID = ''){
    352:
    353:         if($excludeID != '') {
    354:             $denum = new DEnum('ArticleObject::section');
    355:             $excludeID = $denum->getOptionID($excludeID);
    356:         }
    357:
    358:         $sqlQuery = "SELECT OID FROM ".$this->getTableName()." WHERE published='1' AND section!='$excludeID' ORDER BY created_ts DESC LIMIT 0, $limit;";
    359:
    360:         $result = $this->query($sqlQuery);
    361:
    362:         $OIDs = array();
    363:
    364:         foreach($result as $row)
    365:             array_push($OIDs, $row['OID']);
    366:
    367:         return $OIDs;
    368:     }
    369:
    370:     /**
    371:      * Generates the location of the attachments folder for this article
    372:      *
    373:      * @return string
    374:      * @since 1.0
    375:      */
    376:     public function getAttachmentsLocation() {
    377:         global $config;
    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:      * @since 1.0
    387:      */
    388:     public function getAttachmentsURL() {
    389:         global $config;
    390:
    391:         return $config->get('app.url').'attachments/article_'.$this->getID();
    392:     }
    393:
    394:     /**
    395:      * Generates a secure URL for downloading an attachment file via the ViewAttachment controller
    396:      *
    397:      * @param string $filename
    398:      * @since 1.0
    399:      */
    400:     public function getAttachmentSecureURL($filename) {
    401:         global $config;
    402:
    403:         return FrontController::generateSecureURL('act=ViewAttachment&dir='.$this->getAttachmentsLocation().'&filename='.$filename);
    404:     }
    405:
    406:     /**
    407:      * Creates the attachment folder for the article on the server.
    408:      *
    409:      * @since 1.0
    410:      * @throws AlphaException
    411:      */
    412:     public function createAttachmentsFolder() {
    413:         // create the attachment directory for the article
    414:         try{
    415:             mkdir($this->getAttachmentsLocation());
    416:         }catch (Exception $e) {
    417:             throw new AlphaException('Unable to create the folder ['.$this->getAttachmentsLocation().'] for the article.');
    418:         }
    419:
    420:         // ...and set write permissions on the folder
    421:         try{
    422:             chmod($this->getAttachmentsLocation(), 0777);
    423:         }catch (Exception $e) {
    424:             throw new AlphaException('Unable to set write permissions on the folder ['.$this->getAttachmentsLocation().'].');
    425:         }
    426:     }
    427:
    428:     /**
    429:      * Method for returning the calculated score for this article
    430:      *
    431:      * @return double
    432:      * @since 1.0
    433:      */
    434:     public function getArticleScore() {
    435:         $votes = $this->getArticleVotes();
    436:
    437:         $score = 0;
    438:         $total_score = 0;
    439:         $vote_count = count($votes);
    440:
    441:         for($i = 0; $i < $vote_count; $i++){
    442:             $total_score += $votes[$i]->get('score');
    443:         }
    444:
    445:         if($vote_count > 0)
    446:         $score = $total_score/$vote_count;
    447:
    448:         return sprintf("%01.2f", $score);
    449:     }
    450:
    451:     /**
    452:      * Method for fetching all of the votes for this article
    453:      *
    454:      * @return array An array of ArticleVoteObject objects
    455:      * @since 1.0
    456:      */
    457:     public function getArticleVotes() {
    458:         $votes = $this->votes->getRelatedObjects();
    459:
    460:         return $votes;
    461:     }
    462:
    463:     /**
    464:      * Method to determine if the logged-in user has already voted for this article
    465:      *
    466:      * @return boolean True if they have voted already, false otherwise
    467:      * @since 1.0
    468:      * @throws AlphaException
    469:      */
    470:     public function checkUserVoted() {
    471:         // just going to return true if nobody is logged in
    472:         if (!isset($_SESSION['currentUser']))
    473:             return true;
    474:
    475:         $userID = $_SESSION['currentUser']->getID();
    476:
    477:         $vote = new ArticleVoteObject();
    478:
    479:         $sqlQuery = "SELECT COUNT(*) AS usersVote FROM ".$vote->getTableName()." WHERE articleOID='".$this->OID."' AND personOID='".$userID."';";
    480:
    481:         $result = $this->query($sqlQuery);
    482:
    483:         if(!isset($result[0])) {
    484:             throw new AlphaException('Failed to check if the current user voted for the article ['.$this->OID.'], query ['.$sqlQuery.']');
    485:             return false;
    486:         }
    487:
    488:         $row = $result[0];
    489:
    490:         if($row['usersVote'] == "0")
    491:             return false;
    492:         else
    493:             return true;
    494:     }
    495:
    496:     /**
    497:      * Method for fetching all of the comments for this article
    498:      *
    499:      * @return array An array of ArticleCommentObject objects
    500:      * @since 1.0
    501:      */
    502:     public function getArticleComments() {
    503:         $comments = $this->comments->getRelatedObjects();
    504:
    505:         return $comments;
    506:     }
    507:
    508:     /**
    509:      * Loads the content of the ArticleObject from the specified file path
    510:      *
    511:      * @param $filePath
    512:      * @since 1.0
    513:      * @throws FileNotFoundException
    514:      */
    515:     public function loadContentFromFile($filePath) {
    516:         try{
    517:             $this->content->setValue(file_get_contents($filePath));
    518:             $this->filePath = $filePath;
    519:         }catch (Exception $e) {
    520:             throw new FileNotFoundException($e->getMessage());
    521:         }
    522:     }
    523:
    524:     /**
    525:      * Returns true if the article content was loaded from a .text file, false otherwise.
    526:      *
    527:      * @return boolean
    528:      * @since 1.0
    529:      */
    530:     public function isLoadedFromFile() {
    531:         return ($this->filePath == '' ? false: true);
    532:     }
    533:
    534:     /**
    535:      * Returns the timestamp of when the content .text file for this article was last
    536:      * modified.
    537:      *
    538:      * @return string
    539:      * @since 1.0
    540:      * @throws FileNotFoundException
    541:      */
    542:     public function getContentFileDate() {
    543:         if($this->filePath != '') {
    544:             try{
    545:                 return date("Y-m-d H:i:s", filemtime($this->filePath));
    546:             }catch (Exception $e) {
    547:                 throw new FileNotFoundException($e->getMessage());
    548:             }
    549:         }else{
    550:             throw new FileNotFoundException('Error trying to access an article content file when none is set!');
    551:         }
    552:     }
    553: }
    554:
    555: ?>