railpage/railpagecore

View on GitHub
lib/Downloads/Download.php

Summary

Maintainability
F
1 wk
Test Coverage
<?php

/** 
 * Downloads 
 * @since Version 3.0
 * @version 3.10.0
 * @package Railpage
 * @author Michael Greenhill
 */

namespace Railpage\Downloads;

use Railpage\Users\User;
use Railpage\Users\Factory as UserFactory;
use Railpage\Url;
use DateTime;
use DateTimeZone;
use Exception;

/**
 * Download item class
 * @since Version 3.2
 * @version 3.8.7
 */

class Download extends Base {
    
    /**
     * Download ID
     * @since Version 3.2
     * @var int $id The ID of the download
     */
    
    public $id; 
    
    /**
     * Download name
     * @since Version 3.2
     * @var string $name The name of the download
     */
    
    public $name;
    
    /**
     * Description
     * @since Version 3.2
     * @var string $desc Text describing the download
     */
    
    public $desc;
    
    /**
     * Download URL
     * @since Version 3.2
     * @var string $url_file The URL of the download, relative to the site root
     */
    
    public $url_file;
    
    /**
     * Filename
     * @since Version 3.2
     * @var string $filename The original filename of the download
     */
    
    public $filename;
    
    /**
     * Filepath
     * Absolute filepath 
     * @since Version 3.2
     * @var string $filepath Path to the download on the server
     */
    
    public $filepath;
    
    /**
     * User ID
     * @since Version 3.2
     * @var int $user_id The ID of the user that added this download
     */
    
    public $user_id;
    
    /**
     * Hits
     * @since Version 3.2
     * @var int $hits Number of times this has been downloaded
     */
     
    public $hits;
    
    /**
     * Filesize
     * @version 3.2
     * @var string $filesize Size of the download
     */
     
    public $filesize;
    
    /**
     * MIME type
     * @since Version 3.2
     * @var string $mime MIME type of the download
     */
    
    public $mime;
    
    /**
     * Object ID
     * @since Version 3.2
     * @var string $object_id Object ID - used to alert staff via PM. Probably not the best way to do it - might look at deprecating this in the future
     */
    
    public $object_id;
    
    /**
     * Extra data
     * @since Version 3.5
     * @var array $extra_data Any extra data about this download
     */
    
    public $extra_data = [];
    
    /**
     * Active ("deleted")
     * @since Version 3.2
     * @var int $active Active/inactive flag
     */
    
    public $active;
    
    /**
     * Approved
     * @since Version 3.2
     * @var int $approved Approved/pending flag
     */
    
    public $approved;
    
    /**
     * Category object
     * @since Version 3.2
     * @var \Railpage\Downloads\Category $Category The category of this download
     */
    
    public $Category;
    
    /**
     * DateTime object
     * @since Version 3.8.7
     * @var \DateTime $DateTime DateTime object representing the date it was added to the database
     */
    
    public $Date;
    
    /**
     * Author
     * @since Version 3.9.1
     * @var \Railpage\Users\User $Author
     */
    
    public $Author;
    
    /**
     * Constructor
     * @since Version 3.2
     */
    
    public function __construct() {
        foreach (func_get_args() as $arg) {
            if (filter_var($arg, FILTER_VALIDATE_INT)) {
                $this->id = $arg;
            }
        }
        
        parent::__construct();
        
        if (empty($this->id)) {
            return;
        }
            
        $this->mckey = sprintf("railpage:downloads.download=%d", $this->id);
        
        // Populate the object vars
        $this->fetch(); 
    }
    
    /**
     * Load the download data from the database and populate this class
     * @since Version 3.2
     * @return boolean
     */
    
    public function fetch() {
        if (empty($this->id)) {
            throw new Exception("Cannot fetch download object - no download ID given"); 
        }
        
        $query = "SELECT d.*, UNIX_TIMESTAMP(d.date) AS date_unix FROM download_items AS d WHERE d.id = ?";
        
        $row = $this->db->fetchRow($query, $this->id);
            
        if (!is_array($row) || count($row) === 0) {
            throw new Exception("Requested download not found");
        }
        
        // Populate the vars
        $this->name     = $row['title']; 
        $this->desc     = $row['description']; 
        $this->url_file = $row['url']; 
        $this->filename = empty($row['filename']) ? basename($row['url']) : $row['filename']; 
        $this->Date     = new DateTime($row['date'], new DateTimeZone("Australia/Melbourne"));
        $this->hits     = $row['hits'];
        $this->filesize = isset($row['filesize']) && $row['filesize'] > 0 ? formatBytes($row['filesize']) : "Unknown";
        $this->user_id  = $row['user_id'];
        $this->filepath = $row['filepath'];
        
        $this->object_id    = $row['object_id'];
        $this->approved     = $row['approved'];
        $this->active       = $row['active'];
        $this->extra_data   = $row['extra_data'];
        $this->mime = $row['mime'];
        
        if (empty($this->filepath) && !empty($this->url_file)) {
            $pathinfo = parse_url($this->url_file); 
            $this->filepath = str_replace("/uploads/", "", $pathinfo['path']);
            
            try {
                $this->commit(); 
            } catch (Exception $e) {
                // Do nothing
            }
        }
        
        if (!preg_match("@^(http|https)://@", $this->url_file)) {
            $this->url_file = parent::DOWNLOAD_HOST . parent::DOWNLOAD_DIR . $this->url_file; 
        }
        
        if ($row['date'] == "0000-00-00 00:00:00") {
            $this->Date = new DateTime("now", new DateTimeZone("Australia/Melbourne")); 
            $this->commit();
        }
        
        if (empty($this->user_id) && !empty($row['submitter'])) {
            $this->submitter = $row['submitter'];
        }
        
        /**
         * Load the category this download belongs to
         */
        
        $this->Category = new Category($row['category_id']); 
        
        /**
         * Load the author
         */
        
        $this->Author = UserFactory::CreateUser($this->user_id);
        
        if (empty($this->mime) && file_exists(RP_DOWNLOAD_DIR . $this->filepath)) {
            $finfo = finfo_open(FILEINFO_MIME_TYPE); 
            $this->mime = finfo_file($finfo, RP_DOWNLOAD_DIR . $this->filepath);
            $this->commit(); 
        }
        
        $this->url = Utility\DownloadUtility::buildUrls($this); 
        
    }
    
    /**
     * Validate the file OK before committing it
     * @since Version 3.2
     * @version 3.8.7
     * @return boolean
     * @throws \Exception if the download name is empty
     * @throws \Exception if the download filename is empty
     */
    
    public function validate() {
        if (empty($this->name)) {
            throw new Exception("Verification failed - download must have a name");
        }
        
        if (!$this->Date instanceof DateTime) {
            $this->Date = new DateTime;
        }
        
        if (empty($this->filename)) {
            throw new Exception("Verification failed - download must have a filename");
        }
        
        if (is_null($this->mime)) {
            $this->mime = "";
        }
        
        if (!filter_var($this->active, FILTER_VALIDATE_INT)) {
            $this->active = 1;
        } 
        
        if (!filter_var($this->approved, FILTER_VALIDATE_INT)) {
            $this->approved = 0;
        }
        
        if (!filter_var($this->hits, FILTER_VALIDATE_INT)) {
            $this->hits = 0;
        }
        
        if ($this->Author instanceof User) {
            $this->user_id = $this->Author->id;
        }
        
        if (!filter_var($this->user_id, FILTER_VALIDATE_INT)) {
            throw new Exception("No valid owner of this download has been provided");
        }
        
        return true;
    }
    
    /**
     * Commit a file to the database
     * @since Version 3.2
     * @version 3.8.7
     * @return boolean
     */
    
    public function commit() {
        
        $this->validate();
        
        $data = array(
            "category_id" => $this->Category instanceof Category ? $this->Category->id : 10,
            "title" => $this->name,
            "url" => $this->url_file,
            "filename" => $this->filename,
            "mime" => $this->mime,
            "description" => $this->desc,
            "date" => $this->Date->format("Y-m-d h:i:s"),
            "hits" => $this->hits,
            "user_id" => $this->user_id,
            "filepath" => $this->filepath,
            "object_id" => filter_var($this->object_id, FILTER_VALIDATE_INT) ? $this->object_id : 0,
            "approved" => $this->approved,
            "active" => $this->active,
            "extra_data" => json_encode($this->extra_data),
            "url" => $this->approved ? str_replace(dirname(dirname(__FILE__)), RP_PROTOCOL."://" . RP_HOST, $this->filepath) : ""
        );
        
        /**
         * Commit the changes
         */
        
        if (empty($this->id)) {
            $data['active']         = 1;
            $data['category_id']    = $this->cat_id;
            $data['filesize']       = empty($this->filesize) ? 0 : $this->filesize; 
            
            $this->db->insert("download_items", $data);
            $this->id = $this->db->lastInsertId();
        
            $this->url = Utility\DownloadUtility::buildUrls($this); 
            
            return $this->id;
        }
        
        $where = array(
            "id = ?" => $this->id
        );
        
        $this->db->update("download_items", $data, $where);
        
        $this->url = Utility\DownloadUtility::buildUrls($this); 
        
    }
    
    /**
     * Log to the database when this file has been downloaded
     * @since Version 3.5
     * @param string $ip The client IP address
     * @param int $userId The user that downloaded this file
     * @param string $username The username of the user that downloaded this file
     */
    
    public function log($ip = null, $userId = null, $username = null) {
        
        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
            return false;
        }
        
        $data = array(
            "download_id" => $this->id,
            "date" => "NOW()",
            "ip" => $ip
        );
        
        if ($userId != null && $username != null) {
            $data['user_id'] = $userId;
            $data['username'] = $username;
        }
        
        $this->db->insert("log_downloads", $data);
        return true;
    }
    
    /**
     * Delete this download
     * @since Version 3.8.7
     * @return boolean
     */
    
    public function delete() {
        if (is_file($this->filepath)) {
            unlink($this->filepath);
        }
        
        $where = array(
            "id = ?" => $this->id
        );
        
        return $this->db->delete("download_items", $where);
    }
    
    /**
     * Generate a thumbnail of this file
     * @since Version 3.9.1
     * @return string
     */
    
    public function getThumbnail() {
        
        $mime = explode("/", $this->mime); 
        $thumbnail = sprintf("%s.thumbnail.jpg", $this->filepath);
        
        if (file_exists(RP_DOWNLOAD_DIR . $thumbnail)) {
            $info = getimagesize(RP_DOWNLOAD_DIR . $thumbnail);
            
            return array(
                "file" => $thumbnail,
                "width" => $info[0],
                "height" => $info[1],
                "size" => filesize(RP_DOWNLOAD_DIR . $thumbnail)
            );
        }
        
        switch ($mime[0]) {
            case "video" : 
                $avlib = self::getAVLib();
                
                exec(sprintf("%s -i %s%s -vsync 1 -r 1 -an -y -vframes 1 '%s%s'", $avlib, RP_DOWNLOAD_DIR, $this->filepath, RP_DOWNLOAD_DIR, $thumbnail), $return);
                
                $info = getimagesize(RP_DOWNLOAD_DIR . $thumbnail);
            
                return array(
                    "file" => $thumbnail,
                    "width" => $info[0],
                    "height" => $info[1],
                    "size" => filesize(RP_DOWNLOAD_DIR . $thumbnail)
                );
            
            case "image" :
                $info = getimagesize(RP_DOWNLOAD_DIR . $this->filepath);
                
                return array(
                    "file" => $this->filepath,
                    "width" => $info[0],
                    "height" => $info[1],
                    "size" => filesize(RP_DOWNLOAD_DIR . $this->filepath)
                );
        }
        
        switch ($this->mime) {
            
            case "application/pdf" :
            case "application/x-pdf" : 
                exec(
                    sprintf(
                        "convert -thumbnail 800 -background white -alpha remove '%s%s'[0] %s%s", 
                        RP_DOWNLOAD_DIR, 
                        $this->filepath, 
                        RP_DOWNLOAD_DIR, 
                        $thumbnail
                    ), 
                    $return
                ); 
                
                $info = getimagesize(RP_DOWNLOAD_DIR . $thumbnail);
            
                return array(
                    "file" => $thumbnail,
                    "width" => $info[0],
                    "height" => $info[1],
                    "size" => filesize(RP_DOWNLOAD_DIR . $thumbnail)
                );
            
        }
        
        return false;
    }
    
    /**
     * Get detailed information about this file
     * @since Version 3.9.1
     * @return array
     */
    
    public function getDetails() {
        
        $mckey = sprintf("%s;details", $this->mckey); 
        
        if (!$data = $this->Memcached->fetch($mckey)) {
            if (empty($this->mime) && file_exists(RP_DOWNLOAD_DIR . $this->filepath)) {
                $finfo = finfo_open(FILEINFO_MIME_TYPE); 
                $this->mime = finfo_file($finfo, RP_DOWNLOAD_DIR . $this->filepath);
            }
            
            $mime = explode("/", $this->mime); 
            
            $data = array(
                "type" => ucwords($mime[0]),
                "size" => $this->filesize,
                "downloads" => $this->hits,
                "added" => array(
                    "absolute" => $this->Date->format("Y-m-d g:i:s a"),
                    "relative" => time2str($this->Date->getTimestamp())
                ),
                "thumbnail" => $this->getThumbnail(),
                "video" => $this->getHTML5Video()
            );
            
            $this->Memcached->save($mckey, $data, strtotime("+12 hours"));
        }
        
        return $data;
    }
    
    /**
     * Get the AV library available on this system
     * @since Version 3.9.1
     * @return string
     * @throws \Exception if no suitable AV library is available
     */
    
    public static function getAVLib() {
        $avlib = false;
        
        if (file_exists("/usr/bin/avconv")) {
            $avlib = "/usr/bin/avconv";
        }
        
        if (!$avlib) {
            throw new Exception("No video library (libav or ffmpeg) was found");
        }
        
        return $avlib;
    }
    
    /**
     * Convert this file to a HTML5-compatible video format
     * @since Version 3.9.1
     * @return array
     */
    
    public function getHTML5Video() {
        if (empty($this->mime) && file_exists(RP_DOWNLOAD_DIR . $this->filepath)) {
            $this->getThumbnail(); 
        }
        
        $mime = explode("/", $this->mime); 
        
        if ($mime[0] != "video") {
            return false;
        }
        
        $avlib = self::getAVLib();
        
        $videos = array();
        
        set_time_limit(1200);
        
        /**
         * Mp4
         */
        
        $dstfile = sprintf("%s/%s.v3.mp4", pathinfo($this->filepath, PATHINFO_DIRNAME), pathinfo($this->filepath, PATHINFO_FILENAME));
        
        if (!file_exists(RP_DOWNLOAD_DIR . $dstfile)) {
            exec(sprintf("%s -i %s%s -vcodec h264 -acodec aac -strict -2 -ar 64k %s%s", $avlib, RP_DOWNLOAD_DIR, $this->filepath, RP_DOWNLOAD_DIR, $dstfile), $return);
        }
        
        $finfo = finfo_open(FILEINFO_MIME_TYPE); 
        $mime = finfo_file($finfo, RP_DOWNLOAD_DIR . $dstfile);
        
        $videos['mp4'] = array(
            "absolute" => RP_DOWNLOAD_DIR . $dstfile,
            "file" => $dstfile,
            "size" => filesize(RP_DOWNLOAD_DIR . $dstfile),
            "mime" => $mime
        );
        
        /**
         * WebM
         */
        
        $dstfile = sprintf("%s/%s.v3.webm", pathinfo($this->filepath, PATHINFO_DIRNAME), pathinfo($this->filepath, PATHINFO_FILENAME));
        
        if (!file_exists(RP_DOWNLOAD_DIR . $dstfile)) {
            exec(sprintf("%s -i %s%s -acodec libvorbis -ac 2 -ab 96k -ar 44100 %s%s", $avlib, RP_DOWNLOAD_DIR, $this->filepath, RP_DOWNLOAD_DIR, $dstfile), $return);
        }
        
        $finfo = finfo_open(FILEINFO_MIME_TYPE); 
        $mime = finfo_file($finfo, RP_DOWNLOAD_DIR . $dstfile);
        
        $videos['webm'] = array(
            "absolute" => RP_DOWNLOAD_DIR . $dstfile,
            "file" => $dstfile,
            "size" => filesize(RP_DOWNLOAD_DIR . $dstfile),
            "mime" => $mime
        );
        
        return $videos;
    }
    
    /**
     * Get icon representing this file type
     * @since Version 3.10.0
     * @return array
     */
    
    public function getIcon() {
        
        $mime = explode("/", $this->mime); 
        
        switch ($mime[0]) {
            case "video" : 
                return [
                    "label" => "Video",
                    "icon" => '<i class="fa fa-file-video-o"></i>'
                ];
                
                break;
                
            case "image" : 
                return [
                    "label" => "Image",
                    "icon" => '<i class="fa fa-file-image-o"></i>'
                ];
                
                break;
                
        }
        
        switch ($this->mime) {
            case "application/msword":
            case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
                return [ 
                    "label" => "Microsoft Word",
                    "icon" => '<i class="fa fa-file-word-o"></i>'
                ];
                
                break;
            
            case "application/vnd.ms-excel":
            case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
                return [ 
                    "label" => "Microsoft Excel",
                    "icon" => '<i class="fa fa-file-excel-o"></i>'
                ];
                
                break;
            
            case "application/pdf":
            case "application/x-pdf":
                return [ 
                    "label" => "PDF",
                    "icon" => '<i class="fa fa-file-pdf-o"></i>'
                ];
                
                break;
                
        }
        
        return [
            "label" => "Unknown",
            "icon" => '<i class="fa fa-file-o"></i>'
        ];
        
    }
    
    /**
     * Get an associative array of this object
     * @since Version 3.10.0
     * @return array
     */
    
    public function getArray() {
        
        $data = [
            "id" => $this->id,
            "name" => $this->name,
            "description" => $this->desc,
            "url_file" => $this->url_file,
            "filename" => $this->filename,
            "filepath" => $this->filepath,
            "filesize" => $this->filesize,
            "mime" => $this->mime,
            "active" => $this->active,
            "approved" => $this->approved,
            "meta" => $this->extra_data,
            "date" => $this->Date,
            "author" => $this->Author->getArray(),
            "url" => $this->url->getURLs(),
            "thumbnail" => $this->getThumbnail(),
            "icon" => $this->getIcon(),
        ];
        
        return $data;
        
    }
}