<?PHP
#
#   FILE:  File.php
#
#   Part of the Collection Workflow Integration System (CWIS)
#   Copyright 2010-2013 Edward Almasy and Internet Scout Research Group
#   http://scout.wisc.edu/cwis/
#

/**
* Class representing a stored (usually uploaded) file.
*/
class File
{

    # ---- PUBLIC INTERFACE --------------------------------------------------

    # status codes (set by constructor and returned by File::Status())
    const FILESTAT_OK =             0;
    const FILESTAT_COPYERROR =      1;
    const FILESTAT_PARAMERROR =     2;
    const FILESTAT_ZEROLENGTH =     3;
    const FILESTAT_DOESNOTEXIST =   4;
    const FILESTAT_UNREADABLE =     5;

    /**
    * Constructs a File object using either an existing file or a new file.
    * @param mixed $IdOrFileName The ID of the file if it is already in the
    *       database, or the file name if it is to be added to the database.
    * @param int $ResourceId The ID of the corresponding resource if the
    *       file is being added.  (OPTIONAL)
    * @param int $FieldId The ID of the corresponding field if the file is
    *       being added.  (OPTIONAL)
    * @param string $DesiredFileName The desired name of the file if it is being
    *       added.  (OPTIONAL)
    * @param bool $CheckFileLength Whether to check the length of the file if it
    *       is being added.  (OPTIONAL)
    */
    public function __construct($IdOrFileName, $ResourceId = NULL, $FieldId = NULL,
            $DesiredFileName = NULL, $CheckFileLength = TRUE)
    {
        # assume constructor will succeed
        $this->Status = self::FILESTAT_OK;

        # get our own database handle
        $DB = new Database();
        $this->DB = $DB;

        # if ID supplied
        if (is_numeric($IdOrFileName))
        {
            # set file ID from supplied value
            $this->Id = intval($IdOrFileName);

            # load file info from database
            $DB->Query("SELECT * FROM Files WHERE FileId = ".$this->Id);
            $this->DBFields = $DB->FetchRow();

            # if the image wasn't found in the database
            if (!$DB->NumRowsSelected())
            {
                $this->Status = self::FILESTAT_DOESNOTEXIST;
            }
        }
        # else if file name and resource ID and field ID supplied
        elseif (strlen($IdOrFileName) && ($ResourceId != NULL) && ($FieldId != NULL))
        {
            # if file does not exist
            $TempFileName = $IdOrFileName;
            if (!file_exists($TempFileName) || !is_readable($TempFileName))
            {
                # set status indicating appropriate error
                $this->Status = file_exists($TempFileName)
                        ? self::FILESTAT_DOESNOTEXIST : self::FILESTAT_UNREADABLE;
            }
            else
            {
                # if we were asked to check file length and file was zero length
                $FileLength = filesize($TempFileName);
                if ($CheckFileLength && !$FileLength)
                {
                    # set status indicating zero length file
                    $this->Status = self::FILESTAT_ZEROLENGTH;
                }
                else
                {
                    # generate secret string (used to protect from unauthorized download)
                    srand((double)microtime() * 1000000);
                    $SecretString = sprintf("%04X", rand(1, 30000));

                    # attempt to get file type
                    $FileType = "";
                    if (function_exists("finfo_open"))
                    {
                        $FInfoHandle = finfo_open(FILEINFO_MIME);

                        if ($FInfoHandle)
                        {
                            $FInfoMime = finfo_file($FInfoHandle, $TempFileName);
                            finfo_close($FInfoHandle);

                            if ($FInfoMime)
                            {
                                $FileType = $FInfoMime;
                            }
                        }
                    }
                    else if (function_exists("mime_content_type"))
                    {
                        # mime_content_type has been deprecated, but it may be
                        # the only way to get the mimetype for PHP < 5.3
                        $MimeType = mime_content_type($TempFileName);

                        if ($MimeType)
                        {
                            $FileType = $MimeType;
                        }
                    }

                    # handle Office XML formats
                    # These are recognized by PHP as zip files (because they are), but
                    # IE (and maybe other things?) need a special-snowflake MIME type to
                    # handle them properly.
                    # For a list of the required types, see
                    # https://technet.microsoft.com/en-us/library/ee309278(office.12).aspx
                    if ($FileType == "application/zip; charset=binary")
                    {
                        $MstfPrefix = "application/vnd.openxmlformats-officedocument";

                        $FileNameForExt =  is_null($DesiredFileName) ?
                            $TempFileName : $DesiredFileName ;

                        $FileExt = strtolower(pathinfo(
                           $FileNameForExt, PATHINFO_EXTENSION));

                        switch ($FileExt)
                        {
                            case "docx":
                                $FileType = $MstfPrefix.".wordprocessingml.document";
                                break;

                            case "xlsx":
                                $FileType = $MsftPrefix.".spreadsheetml.sheet";
                                break;

                            case "pptx":
                                $FileType = $MsftPrefix.".presentationml.slideshow";
                                break;

                            default:
                                # do nothing
                        }
                    }

                    # add file info to database
                    $BaseFileName = $DesiredFileName
                            ? basename($DesiredFileName) : basename($TempFileName);
                    $DB->Query("INSERT INTO Files"
                            ." (ResourceId, FieldId, FileName, FileLength, FileType,"
                                ." SecretString)"
                            ." VALUES ("
                                .intval($ResourceId).", "
                                .intval($FieldId).", "
                                ."'".addslashes($BaseFileName)."', "
                                .$FileLength.", "
                                ."'".addslashes($FileType)."', "
                                ."'".$SecretString."')");

                    # retrieve ID of new file
                    $this->Id = $DB->LastInsertId();

                    # load file info back in from database
                    $DB->Query("SELECT * FROM Files WHERE FileId = ".$this->Id);
                    $this->DBFields = $DB->FetchRow();

                    # copy file to storage
                    $CopySucceeded = copy($IdOrFileName, $this->GetNameOfStoredFile());

                    # if copy failed
                    if (!$CopySucceeded)
                    {
                        # remove file info from database
                        $DB->Query("DELETE FROM Files WHERE FileId = ".$this->Id);

                        # set status indicating constructor failed
                        $this->Status = self::FILESTAT_COPYERROR;
                    }
                }
            }
        }
        else
        {
            # set status indicating constructor failed
            $this->Status = self::FILESTAT_PARAMERROR;
        }
    }

    /**
    * Gets the object's status.
    * @return The status code of the object.
    */
    public function Status()
    {
        return $this->Status;
    }

    /**
    * Gets the object's ID.
    * @return The ID of the object.
    */
    public function Id()
    {
        return $this->Id;
    }

    /**
    * Gets the name of the object.
    * @return The name of the file.
    */
    public function Name()
    {
        return $this->DBFields["FileName"];
    }

    /**
    * Gets the length of the file.
    * @return The length of the file.
    */
    public function GetLength()
    {
        return $this->DBFields["FileLength"];
    }

    /**
    * Gets the file's type.
    * @return The file's type.
    */
    public function GetType()
    {
        return $this->DBFields["FileType"];
    }

    /**
    * Gets or sets the comment on the file.
    * @param string $NewValue The new comment on the file.  (OPTIONAL)
    * @return The comment on the file.
    */
    public function Comment($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("FileComment", $NewValue);
    }

    /**
    * Gets or sets the field ID of the File.
    * @param int $NewValue The new field ID of the File.  (OPTIONAL)
    * @return The field ID of the File.
    */
    public function FieldId($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("FieldId", $NewValue);
    }

    /**
    * Gets or sets the resource ID of the File.
    * @param int $NewValue The new resource ID of the File.  (OPTIONAL)
    * @return The resource ID of the File.
    */
    public function ResourceId($NewValue = DB_NOVALUE)
    {
        return $this->UpdateValue("ResourceId", $NewValue);
    }

    /**
    * Gets the MIME type of the file.
    * @return The MIME type of the file.
    */
    public function GetMimeType()
    {
        return strlen($this->GetType())
                ? $this->GetType() : "application/octet-stream";
    }

    /**
    * Returns the relative download link to download the file. If .htaccess
    * files are supported, the redirect that includes the file name is used.
    * @return The relative link to download the file.
    */
    public function GetLink()
    {
        # if CleanURLs are enabled, use the redirect that includes
        # the file name so that browsers don't use index.php as the name
        # for the downloaded file
        if ($GLOBALS["G_PluginManager"]->PluginEnabled("CleanURLs"))
        {
            return "downloads/".$this->Id."/".rawurlencode($this->Name());
        }

        # otherwise use the download portal
        else
        {
            return "index.php?P=DownloadFile&Id=".$this->Id;
        }
    }

    /**
    * Deletes the file and removes its entry from the database. Other methods
    * are invalid after calling this.
    */
    public function Delete()
    {
        # remove file entry from DB
        $this->DB->Query("DELETE FROM Files WHERE FileId = ".$this->Id);

        # delete file
        $FileName = $this->GetNameOfStoredFile();
        if (file_exists($FileName))
        {
            unlink($FileName);
        }
    }

    /**
    * Returns the relative link to the stored file.
    * @return The relative link to the stored file
    */
    public function GetNameOfStoredFile()
    {
        # for each possible storage location
        foreach (self::$StorageLocations as $Dir)
        {
            # build file name for that location
            $FileName = sprintf($Dir."/%06d-%s-%s",
                    $this->Id, $this->DBFields["SecretString"], $this->Name());

            # if file can be found in that location
            if (file_exists($FileName))
            {
                # return file name to caller
                return $FileName;
            }
        }

        # build file name for default (most preferred) location
        $FileName = sprintf(self::GetStorageDirectory()."/%06d-%s-%s",
                $this->Id, $this->DBFields["SecretString"], $this->Name());

        # return file name to caller
        return $FileName;
    }

    /**
    * Get file storage directory.
    * @return string Relative directory path (with no trailing slash).
    */
    public static function GetStorageDirectory()
    {
        # for each possible storage location
        foreach (self::$StorageLocations as $Dir)
        {
            # if location exists and is writeable
            if (is_dir($Dir) && is_writeable($Dir))
            {
                # return location to caller
                return $Dir;
            }
        }

        # return default (most preferred) location to caller
        return self::$StorageLocations[0];
    }


    # ---- PRIVATE INTERFACE -------------------------------------------------

    private $DB;
    private $Status;
    private $Id;
    private $DBFields;

    /** File storage directories, in decreasing order of preference. */
    static private $StorageLocations = array(
            "local/data/files",
            "FileStorage",
            );

    /**
    * Convenience function that supplies parameters to Database->UpdateValue().
    * @param string $FieldName Name of database field.
    * @param mixed $NewValue New value to set.
    * @return The requested value.
    */
    private function UpdateValue($FieldName, $NewValue)
    {
        return $this->DB->UpdateValue("Files", $FieldName, $NewValue,
                               "FileId = ".intval($this->Id),
                               $this->DBFields, TRUE);
    }
}
