1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 
<?php
namespace Omeka\File;

use finfo;
use Omeka\File\Store\StoreInterface;
use Zend\Math\Rand;

class TempFile
{
    /**
     * Map of nonstandard-to-standard media types.
     */
    const MEDIA_TYPE_ALIASES = [
        // application/ogg
        'application/x-ogg' => 'application/ogg',
        // application/rtf
        'text/rtf' => 'application/rtf',
        // audio/midi
        'audio/mid' => 'audio/midi',
        'audio/x-midi' => 'audio/midi',
        // audio/mpeg
        'audio/mp3' => 'audio/mpeg',
        'audio/mpeg3' => 'audio/mpeg',
        'audio/x-mp3' => 'audio/mpeg',
        'audio/x-mpeg' => 'audio/mpeg',
        'audio/x-mpeg3' => 'audio/mpeg',
        'audio/x-mpegaudio' => 'audio/mpeg',
        'audio/x-mpg' => 'audio/mpeg',
        // audio/ogg
        'audio/x-ogg' => 'audio/ogg',
        // audio/x-aac
        'audio/aac' => 'audio/x-aac',
        // audio/x-aiff
        'audio/aiff' => 'audio/x-aiff',
        // audio/x-ms-wma
        'audio/x-wma' => 'audio/x-ms-wma',
        'audio/wma' => 'audio/x-ms-wma',
        // audio/mp4
        'audio/x-mp4' => 'audio/mp4',
        'audio/x-m4a' => 'audio/mp4',
        // audio/x-wav
        'audio/wav' => 'audio/x-wav',
        // image/bmp
        'image/x-ms-bmp' => 'image/bmp',
        // image/x-icon
        'image/icon' => 'image/x-icon',
        // video/mp4
        'video/x-m4v' => 'video/mp4',
        // video/x-ms-asf
        'video/asf' => 'video/x-ms-asf',
        // video/x-ms-wmv
        'video/wmv' => 'video/x-ms-wmv',
        // video/x-msvideo
        'video/avi' => 'video/x-msvideo',
        'video/msvideo' => 'video/x-msvideo',
    ];

    /**
     * @var StoreInterface
     */
    protected $store;

    /**
     * @var ThumbnailManager
     */
    protected $thumbnailManager;

    /**
     * @var string Directory where to save this temporary file
     */
    protected $tempDir;

    /**
     * @var string Path to this temprorary file
     */
    protected $tempPath;

    /**
     * @var array Media type map
     */
    protected $mediaTypeMap;

    /**
     * @var string The name of the original source file
     */
    protected $sourceName;

    /**
     * @var string Base name of the stored file (without extension)
     */
    protected $storageId;

    /**
     * @var string Internet media type of the file
     */
    protected $mediaType;

    /**
     * @var string Extension of the file
     */
    protected $extension;

    /**
     * @param string $tempDir
     * @param array $mediaTypeMap
     * @param StoreInterface $store
     * @param ThumbnailManager $thumbnailManager
     */
    public function __construct($tempDir, array $mediaTypeMap,
        StoreInterface $store, ThumbnailManager $thumbnailManager
    ) {
        $this->tempDir = $tempDir;
        $this->mediaTypeMap = $mediaTypeMap;
        $this->store = $store;
        $this->thumbnailManager = $thumbnailManager;

        // Always create a new, uniquely named temporary file.
        $this->setTempPath(tempnam($tempDir, 'omeka'));
    }

    /**
     * Set the path to the temporary file.
     *
     * Typically needed only when the temporary file already exists on the
     * server.
     *
     * @param string $tempPath
     */
    public function setTempPath($tempPath)
    {
        $this->tempPath = $tempPath;
    }

    /**
     * Get the path to the temporary file.
     *
     * @param null|string $tempDir
     * @return string
     */
    public function getTempPath()
    {
        return $this->tempPath;
    }

    /**
     * Get the name/path of the source file.
     *
     * @return string
     */
    public function getSourceName()
    {
        return $this->sourceName;
    }

    /**
     * Set the name/path of the source file.
     *
     * @param string $sourceName
     */
    public function setSourceName($sourceName)
    {
        $this->sourceName = $sourceName;
    }

    /**
     * Get the storage ID.
     *
     * The storage ID is the base name (without extension) of the persistently
     * stored file.
     *
     * @return string
     */
    public function getStorageId()
    {
        if (isset($this->storageId)) {
            return $this->storageId;
        }
        $this->storageId = bin2hex(Rand::getBytes(20));
        return $this->storageId;
    }

    /**
     * Set the storage ID
     *
     * @param string $storageId
     */
    public function setStorageId($storageId)
    {
        $this->storageId = $storageId;
    }

    /**
     * Store a file.
     *
     * @param string $prefix The storage prefix
     * @param null|string $extension The file extension, if different file
     * @param null|string $tempPath The temp path, if different file
     * @return string The path of the stored file
     */
    public function store($prefix, $extension = null, $tempPath = null)
    {
        if (null === $extension) {
            $extension = $this->getExtension(); // could return null
        }
        if (null !== $extension) {
            $extension = ".$extension";
        }
        if (null === $tempPath) {
            $tempPath = $this->getTempPath();
        }
        $storagePath = sprintf('%s/%s%s', $prefix, $this->getStorageId(), $extension);
        $this->store->put($tempPath, $storagePath);
        return $storagePath;
    }

    /**
     * Store this as an "original" file.
     *
     * @return string The path of the stored file
     */
    public function storeOriginal()
    {
        return $this->store('original');
    }

    /**
     * Store this as an "asset" file.
     *
     * @return string The path of the stored file
     */
    public function storeAsset()
    {
        return $this->store('asset');
    }

    /**
     * Create and store thumbnail derivatives of this file.
     *
     * @return bool Whether thumbnails were created and stored
     */
    public function storeThumbnails()
    {
        $thumbnailer = $this->thumbnailManager->buildThumbnailer();

        $tempPaths = [];

        try {
            $thumbnailer->setSource($this);
            $thumbnailer->setOptions($this->thumbnailManager->getThumbnailerOptions());
            foreach ($this->thumbnailManager->getTypeConfig() as $type => $config) {
                $tempPaths[$type] = $thumbnailer->create(
                    $config['strategy'], $config['constraint'], $config['options']
                );
            }
        } catch (Exception\CannotCreateThumbnailException $e) {
            // Delete temporary files created before exception was thrown.
            foreach ($tempPaths as $tempPath) {
                @unlink($tempPath);
            }
            return false;
        }

        // Finally, store the thumbnails.
        foreach ($tempPaths as $type => $tempPath) {
            $this->store($type, 'jpg', $tempPath);
            // Delete the temporary file in case the file store hasn't already.
            @unlink($tempPath);
        }

        return true;
    }

    /**
     * Get the Internet media type of the file.
     *
     * @uses finfo
     * @return string
     */
    public function getMediaType()
    {
        if (isset($this->mediaType)) {
            return $this->mediaType;
        }
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        $mediaType = $finfo->file($this->getTempPath());
        if (array_key_exists($mediaType, self::MEDIA_TYPE_ALIASES)) {
            $mediaType = self::MEDIA_TYPE_ALIASES[$mediaType];
        }
        $this->mediaType = $mediaType;
        return $this->mediaType;
    }

    /**
     * Get the filename extension for the original file.
     *
     * Heuristically determines whether the passed file has an extension. The
     * source name must contain at least one dot, the source name must not end
     * with a dot, and the extension must not be over 12 characters.
     *
     * Returns the extension if found. Returns a "best guess" extension if the
     * media type is known but the original extension is not found. Returns
     * null if the file has no source name or the file has no extension and the
     * media type cannot be mapped to an extension.
     *
     * @return string|null
     */
    public function getExtension()
    {
        if (isset($this->extension)) {
            return $this->extension;
        }
        $extension = null;
        if (!$sourceName = $this->getSourceName()) {
            return $extension;
        }
        $dotPos = strrpos($sourceName, '.');
        if (false !== $dotPos) {
            $sourceNameLen = strlen($sourceName);
            $extensionPos = $dotPos + 1;
            if ($sourceNameLen !== $extensionPos && (12 >= $sourceNameLen - $extensionPos)) {
                $extension = strtolower(substr($sourceName, $extensionPos));
            }
        }
        if (null === $extension) {
            $mediaType = $this->getMediaType();
            if (isset($this->mediaTypeMap[$mediaType][0])) {
                $extension = strtolower($this->mediaTypeMap[$mediaType][0]);
            }
        }
        return $extension;
    }

    /**
     * Get the SHA-256 checksum of the file.
     *
     * @uses hash_file
     * @return string
     */
    public function getSha256()
    {
        return hash_file('sha256', $this->getTempPath());
    }

    /**
     * Delete this temporary file.
     *
     * Always delete a temporary file after all work has been done. Otherwise
     * the file will remain in the temporary directory.
     *
     * @return bool Whether the file was deleted/never created
     */
    public function delete()
    {
        if (isset($this->tempPath)) {
            return unlink($this->tempPath);
        }
        return true;
    }
}