1<?php
2/**
3 * Copyright 1999-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @category  Horde
9 * @copyright 1999-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
11 * @package   Mime
12 */
13
14/**
15 * Utilities to determine MIME content-types of unknown content.
16 *
17 * @author    Anil Madhavapeddy <anil@recoil.org>
18 * @author    Michael Slusarz <slusarz@horde.org>
19 * @category  Horde
20 * @copyright 1999-2017 Horde LLC
21 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
22 * @package   Mime
23 */
24class Horde_Mime_Magic
25{
26    /**
27     * The MIME extension map.
28     *
29     * @var array
30     */
31    protected static $_map = null;
32
33    /**
34     * Returns a copy of the MIME extension map.
35     *
36     * @return array  The MIME extension map.
37     */
38    protected static function _getMimeExtensionMap()
39    {
40        if (is_null(self::$_map)) {
41            require __DIR__ . '/mime.mapping.php';
42            self::$_map = $mime_extension_map;
43        }
44
45        return self::$_map;
46    }
47
48    /**
49     * Attempt to convert a file extension to a MIME type, based
50     * on the global Horde and application specific config files.
51     *
52     * If we cannot map the file extension to a specific type, then
53     * we fall back to a custom MIME handler 'x-extension/$ext', which
54     * can be used as a normal MIME type internally throughout Horde.
55     *
56     * @param string $ext  The file extension to be mapped to a MIME type.
57     *
58     * @return string  The MIME type of the file extension.
59     */
60    public static function extToMime($ext)
61    {
62        if (empty($ext)) {
63            return 'application/octet-stream';
64        }
65
66        $ext = Horde_String::lower($ext);
67        $map = self::_getMimeExtensionMap();
68        $pos = 0;
69
70        while (!isset($map[$ext])) {
71            if (($pos = strpos($ext, '.')) === false) {
72                break;
73            }
74            $ext = substr($ext, $pos + 1);
75        }
76
77        return isset($map[$ext])
78            ? $map[$ext]
79            : 'x-extension/' . $ext;
80    }
81
82    /**
83     * Attempt to convert a filename to a MIME type, based on the global Horde
84     * and application specific config files.
85     *
86     * @param string $filename  The filename to be mapped to a MIME type.
87     * @param boolean $unknown  How should unknown extensions be handled? If
88     *                          true, will return 'x-extension/*' types.  If
89     *                          false, will return 'application/octet-stream'.
90     *
91     * @return string  The MIME type of the filename.
92     */
93    public static function filenameToMime($filename, $unknown = true)
94    {
95        $pos = strlen($filename) + 1;
96        $type = '';
97
98        $map = self::_getMimeExtensionMap();
99        for ($i = 0; $i <= $map['__MAXPERIOD__']; ++$i) {
100            $npos = strrpos(substr($filename, 0, $pos - 1), '.');
101            if ($npos === false) {
102                break;
103            }
104            $pos = $npos + 1;
105        }
106
107        $type = ($pos === false) ? '' : self::extToMime(substr($filename, $pos));
108
109        return (empty($type) || (!$unknown && (strpos($type, 'x-extension') !== false)))
110            ? 'application/octet-stream'
111            : $type;
112    }
113
114    /**
115     * Attempt to convert a MIME type to a file extension, based
116     * on the global Horde and application specific config files.
117     *
118     * If we cannot map the type to a file extension, we return false.
119     *
120     * @param string $type  The MIME type to be mapped to a file extension.
121     *
122     * @return string  The file extension of the MIME type.
123     */
124    public static function mimeToExt($type)
125    {
126        if (empty($type)) {
127            return false;
128        }
129
130        if (($key = array_search($type, self::_getMimeExtensionMap())) === false) {
131            list($major, $minor) = explode('/', $type);
132            if ($major == 'x-extension') {
133                return $minor;
134            }
135            if (strpos($minor, 'x-') === 0) {
136                return substr($minor, 2);
137            }
138            return false;
139        }
140
141        return $key;
142    }
143
144    /**
145     * Attempt to determine the MIME type of an unknown file.
146     *
147     * @param string $path      The path to the file to analyze.
148     * @param string $magic_db  Path to the mime magic database.
149     * @param array $opts       Additional options:
150     *   - nostrip: (boolean) Don't strip parameter information from MIME
151     *              type string.
152     *              DEFAULT: false
153     *
154     * @return mixed  The MIME type of the file. Returns false if the file
155     *                type can not be determined.
156     */
157    public static function analyzeFile($path, $magic_db = null,
158                                       $opts = array())
159    {
160        if (Horde_Util::extensionExists('fileinfo')) {
161            $res = empty($magic_db)
162                ? finfo_open(FILEINFO_MIME)
163                : finfo_open(FILEINFO_MIME, $magic_db);
164
165            if ($res) {
166                $type = trim(finfo_file($res, $path));
167                finfo_close($res);
168
169                /* Remove any additional information. */
170                if (empty($opts['nostrip'])) {
171                    foreach (array(';', ',', '\\0') as $separator) {
172                        if (($pos = strpos($type, $separator)) !== false) {
173                            $type = rtrim(substr($type, 0, $pos));
174                        }
175                    }
176
177                    if (preg_match('|^[a-z0-9]+/[.-a-z0-9]+$|i', $type)) {
178                        return $type;
179                    }
180                } else {
181                    return $type;
182                }
183            }
184        }
185
186        return false;
187    }
188
189    /**
190     * Attempt to determine the MIME type of an unknown byte stream.
191     *
192     * @param string $data      The file data to analyze.
193     * @param string $magic_db  Path to the mime magic database.
194     * @param array $opts       Additional options:
195     *   - nostrip: (boolean) Don't strip parameter information from MIME
196     *              type string.
197     *              DEFAULT: false
198     *
199     * @return mixed  The MIME type of the file. Returns false if the file
200     *                type can not be determined.
201     */
202    public static function analyzeData($data, $magic_db = null,
203                                       $opts = array())
204    {
205        /* If the PHP Mimetype extension is available, use that. */
206        if (Horde_Util::extensionExists('fileinfo')) {
207            $res = empty($magic_db)
208                ? @finfo_open(FILEINFO_MIME)
209                : @finfo_open(FILEINFO_MIME, $magic_db);
210
211            if (!$res) {
212                return false;
213            }
214
215            $type = trim(finfo_buffer($res, $data));
216            finfo_close($res);
217
218            /* Remove any additional information. */
219            if (empty($opts['nostrip'])) {
220                if (($pos = strpos($type, ';')) !== false) {
221                    $type = rtrim(substr($type, 0, $pos));
222                }
223
224                if (($pos = strpos($type, ',')) !== false) {
225                    $type = rtrim(substr($type, 0, $pos));
226                }
227            }
228
229            return $type;
230        }
231
232        return false;
233    }
234
235}
236