1: <?php
2: /**
3: * PHP OpenCloud library.
4: *
5: * @copyright Copyright 2013 Rackspace US, Inc. See COPYING for licensing information.
6: * @license https://www.apache.org/licenses/LICENSE-2.0 Apache 2.0
7: * @version 1.6.0
8: * @author Glen Campbell <glen.campbell@rackspace.com>
9: * @author Jamie Hannaford <jamie.hannaford@rackspace.com>
10: */
11:
12: namespace OpenCloud\ObjectStore\Resource;
13:
14: use finfo as FileInfo;
15: use OpenCloud\Common\Lang;
16: use OpenCloud\Common\Exceptions;
17: use OpenCloud\ObjectStore\AbstractService;
18: use OpenCloud\Common\Request\Response\Http;
19:
20: /**
21: * Objects are the basic storage entities in Cloud Files. They represent the
22: * files and their optional metadata you upload to the system. When you upload
23: * objects to Cloud Files, the data is stored as-is (without compression or
24: * encryption) and consists of a location (container), the object's name, and
25: * any metadata you assign consisting of key/value pairs.
26: */
27: class DataObject extends AbstractStorageObject
28: {
29: /**
30: * Object name. The only restriction on object names is that they must be
31: * less than 1024 bytes in length after URL encoding.
32: *
33: * @var string
34: */
35: public $name;
36:
37: /**
38: * Hash value of the object.
39: *
40: * @var string
41: */
42: public $hash;
43:
44: /**
45: * Size of object in bytes.
46: *
47: * @var string
48: */
49: public $bytes;
50:
51: /**
52: * Date of last modification.
53: *
54: * @var string
55: */
56: public $last_modified;
57:
58: /**
59: * Object's content type.
60: *
61: * @var string
62: */
63: public $content_type;
64:
65: /**
66: * Object's content length.
67: *
68: * @var string
69: */
70: public $content_length;
71:
72: /**
73: * Other headers set for this object (e.g. Access-Control-Allow-Origin)
74: *
75: * @var array
76: */
77: public $extra_headers = array();
78:
79: /**
80: * Whether or not to calculate and send an ETag on create.
81: *
82: * @var bool
83: */
84: public $send_etag = true;
85:
86: /**
87: * The data contained by the object.
88: *
89: * @var string
90: */
91: private $data;
92:
93: /**
94: * The ETag value.
95: *
96: * @var string
97: */
98: private $etag;
99:
100: /**
101: * The parent container of this object.
102: *
103: * @var CDNContainer
104: */
105: private $container;
106:
107: /**
108: * Is this data object a pseudo directory?
109: *
110: * @var bool
111: */
112: private $directory = false;
113:
114: /**
115: * Used to translate header values (returned by requests) into properties.
116: *
117: * @var array
118: */
119: private $headerTranslate = array(
120: 'Etag' => 'hash',
121: 'ETag' => 'hash',
122: 'Last-Modified' => 'last_modified',
123: 'Content-Length' => array('bytes', 'content_length'),
124: );
125:
126: /**
127: * These properties can be freely set by the user for CRUD operations.
128: *
129: * @var array
130: */
131: private $allowedProperties = array(
132: 'name',
133: 'content_type',
134: 'extra_headers',
135: 'send_etag'
136: );
137:
138: /**
139: * Option for clearing the status cache when objects are uploaded to API.
140: * By default, it is set to FALSE for performance; but if you have files
141: * that are rapidly and very often updated, you might want to clear the status
142: * cache so PHP reads the files directly, instead of relying on the cache.
143: *
144: * @link http://php.net/manual/en/function.clearstatcache.php
145: * @var bool
146: */
147: public $clearStatusCache = false;
148:
149: /**
150: * A DataObject is related to a container and has a name
151: *
152: * If `$name` is specified, then it attempts to retrieve the object from the
153: * object store.
154: *
155: * @param Container $container the container holding this object
156: * @param mixed $cdata if an object or array, it is treated as values
157: * with which to populate the object. If it is a string, it is
158: * treated as a name and the object's info is retrieved from
159: * the service.
160: * @return void
161: */
162: public function __construct($container, $cdata = null)
163: {
164: parent::__construct();
165:
166: $this->container = $container;
167:
168: // For pseudo-directories, we need to ensure the name is set
169: if (!empty($cdata->subdir)) {
170: $this->name = $cdata->subdir;
171: $this->directory = true;
172: } else {
173: $this->populate($cdata);
174: }
175: }
176:
177: /**
178: * Is this data object a pseudo-directory?
179: *
180: * @return bool
181: */
182: public function isDirectory()
183: {
184: return $this->directory;
185: }
186:
187: /**
188: * Allow other objects to know what the primary key is.
189: *
190: * @return string
191: */
192: public function primaryKeyField()
193: {
194: return 'name';
195: }
196:
197: /**
198: * Is this a real file?
199: *
200: * @param string $filename
201: * @return bool
202: */
203: private function isRealFile($filename)
204: {
205: return $filename != '/dev/null' && $filename != 'NUL';
206: }
207:
208: /**
209: * Set this file's content type.
210: *
211: * @param string $contentType
212: */
213: public function setContentType($contentType)
214: {
215: $this->content_type = $contentType;
216: }
217:
218: /**
219: * Return the content type.
220: *
221: * @return string
222: */
223: public function getContentType()
224: {
225: return $this->content_type;
226: }
227:
228: /**
229: * Returns the URL of the data object
230: *
231: * If the object is new and doesn't have a name, then an exception is
232: * thrown.
233: *
234: * @param string $subresource Not used
235: * @return string
236: * @throws NoNameError
237: */
238: public function url($subresource = '')
239: {
240: if (!$this->name) {
241: throw new Exceptions\NoNameError(Lang::translate('Object has no name'));
242: }
243:
244: return Lang::noslash(
245: $this->container->url()) . '/' . str_replace('%2F', '/', rawurlencode($this->name)
246: );
247: }
248:
249: /**
250: * Creates (or updates; both the same) an instance of the object
251: *
252: * @api
253: * @param array $params an optional associative array that can contain the
254: * 'name' and 'content_type' of the object
255: * @param string $filename if provided, then the object is loaded from the
256: * specified file
257: * @return boolean
258: * @throws CreateUpdateError
259: */
260: public function create($params = array(), $filename = null, $extractArchive = null)
261: {
262: // Set and validate params
263: $this->setParams($params);
264:
265: // assume no file upload
266: $fp = false;
267:
268: // if the filename is provided, process it
269: if ($filename) {
270:
271: if (!$fp = @fopen($filename, 'r')) {
272: throw new Exceptions\IOError(sprintf(
273: Lang::translate('Could not open file [%s] for reading'),
274: $filename
275: ));
276: }
277:
278: // @todo Maybe, for performance, we could set the "clear status cache"
279: // feature to false by default - but allow users to set to true if required
280: clearstatcache($this->clearStatusCache === true, $filename);
281:
282: // Cast filesize as a floating point
283: $filesize = (float) filesize($filename);
284:
285: // Check it's below a reasonable size, and set
286: // @codeCoverageIgnoreStart
287: if ($filesize > AbstractService::MAX_OBJECT_SIZE) {
288: throw new Exceptions\ObjectError("File size exceeds maximum object size.");
289: }
290: // @codeCoverageIgnoreEnd
291: $this->content_length = $filesize;
292:
293: // Guess the content type if necessary
294: if (!$this->getContentType() && $this->isRealFile($filename)) {
295: $this->setContentType($this->inferContentType($filename));
296: }
297:
298: // Send ETag checksum if necessary
299: if ($this->send_etag) {
300: $this->etag = md5_file($filename);
301: }
302:
303: // Announce to the world
304: $this->getLogger()->info('Uploading {size} bytes from {name}', array(
305: 'size' => $filesize,
306: 'name' => $filename
307: ));
308:
309: } else {
310: // compute the length
311: $this->content_length = strlen($this->data);
312:
313: if ($this->send_etag) {
314: $this->etag = md5($this->data);
315: }
316: }
317:
318: // Only allow supported archive types
319: // http://docs.rackspace.com/files/api/v1/cf-devguide/content/Extract_Archive-d1e2338.html
320: $extractArchiveUrlArg = '';
321:
322: if ($extractArchive) {
323: if ($extractArchive !== "tar.gz" && $extractArchive !== "tar.bz2") {
324: throw new Exceptions\ObjectError(
325: "Extract Archive only supports tar.gz and tar.bz2"
326: );
327: } else {
328: $extractArchiveUrlArg = "?extract-archive=" . $extractArchive;
329: $this->etag = null;
330: $this->setContentType('');
331: }
332: }
333:
334: // Set headers
335: $headers = $this->metadataHeaders();
336:
337: if (!empty($this->etag)) {
338: $headers['ETag'] = $this->etag;
339: }
340:
341: // Content-Type is no longer required; if not specified, it will
342: // attempt to guess based on the file extension.
343: if (!$this->getContentType()) {
344: $headers['Content-Type'] = $this->getContentType();
345: }
346:
347: $headers['Content-Length'] = $this->content_length;
348:
349: // Merge in extra headers
350: if (!empty($this->extra_headers)) {
351: $headers = $this->extra_headers + $headers;
352: }
353:
354: // perform the request
355: $response = $this->getService()->request(
356: $this->url() . $extractArchiveUrlArg,
357: 'PUT',
358: $headers,
359: $fp ? $fp : $this->data
360: );
361:
362: // check the status
363: // @codeCoverageIgnoreStart
364: if (($status = $response->httpStatus()) >= 300) {
365: throw new Exceptions\CreateUpdateError(sprintf(
366: Lang::translate('Problem saving/updating object [%s] HTTP status [%s] response [%s]'),
367: $this->url() . $extractArchiveUrlArg,
368: $status,
369: $response->httpBody()
370: ));
371: }
372: // @codeCoverageIgnoreEnd
373:
374: // set values from response
375: $this->saveResponseHeaders($response);
376:
377: // close the file handle
378: if ($fp) {
379: fclose($fp);
380: }
381:
382: return $response;
383: }
384:
385: /**
386: * Update() is provided as an alias for the Create() method
387: *
388: * Since update and create both use a PUT request, the different functions
389: * may allow the developer to distinguish between the semantics in his or
390: * her application.
391: *
392: * @api
393: * @param array $params an optional associative array that can contain the
394: * 'name' and 'type' of the object
395: * @param string $filename if provided, the object is loaded from the file
396: * @return boolean
397: */
398: public function update($params = array(), $filename = '')
399: {
400: return $this->create($params, $filename);
401: }
402:
403: /**
404: * UpdateMetadata() - updates headers
405: *
406: * Updates metadata headers
407: *
408: * @api
409: * @param array $params an optional associative array that can contain the
410: * 'name' and 'type' of the object
411: * @return boolean
412: */
413: public function updateMetadata($params = array())
414: {
415: $this->setParams($params);
416:
417: // set the headers
418: $headers = $this->metadataHeaders();
419: $headers['Content-Type'] = $this->getContentType();
420:
421: $response = $this->getService()->request(
422: $this->url(),
423: 'POST',
424: $headers
425: );
426:
427: // check the status
428: // @codeCoverageIgnoreStart
429: if (($stat = $response->httpStatus()) >= 204) {
430: throw new Exceptions\UpdateError(sprintf(
431: Lang::translate('Problem updating object [%s] HTTP status [%s] response [%s]'),
432: $this->url(),
433: $stat,
434: $response->httpBody()
435: ));
436: }
437: // @codeCoverageIgnoreEnd
438:
439: return $response;
440: }
441:
442: /**
443: * Deletes an object from the Object Store
444: *
445: * Note that we can delete without retrieving by specifying the name in the
446: * parameter array.
447: *
448: * @api
449: * @param array $params an array of parameters
450: * @return HttpResponse if successful; FALSE if not
451: * @throws DeleteError
452: */
453: public function delete($params = array())
454: {
455: $this->setParams($params);
456:
457: $response = $this->getService()->request($this->url(), 'DELETE');
458:
459: // check the status
460: // @codeCoverageIgnoreStart
461: if (($stat = $response->httpStatus()) >= 300) {
462: throw new Exceptions\DeleteError(sprintf(
463: Lang::translate('Problem deleting object [%s] HTTP status [%s] response [%s]'),
464: $this->url(),
465: $stat,
466: $response->httpBody()
467: ));
468: }
469: // @codeCoverageIgnoreEnd
470:
471: return $response;
472: }
473:
474: /**
475: * Copies the object to another container/object
476: *
477: * Note that this function, because it operates within the Object Store
478: * itself, is much faster than downloading the object and re-uploading it
479: * to a new object.
480: *
481: * @param DataObject $target the target of the COPY command
482: */
483: public function copy(DataObject $target)
484: {
485: $uri = sprintf('/%s/%s', $target->container()->name(), $target->name());
486:
487: $this->getLogger()->info('Copying object to [{uri}]', array('uri' => $uri));
488:
489: $response = $this->getService()->request(
490: $this->url(),
491: 'COPY',
492: array('Destination' => $uri)
493: );
494:
495: // check response code
496: // @codeCoverageIgnoreStart
497: if ($response->httpStatus() > 202) {
498: throw new Exceptions\ObjectCopyError(sprintf(
499: Lang::translate('Error copying object [%s], status [%d] response [%s]'),
500: $this->url(),
501: $response->httpStatus(),
502: $response->httpBody()
503: ));
504: }
505: // @codeCoverageIgnoreEnd
506:
507: return $response;
508: }
509:
510: /**
511: * Returns the container of the object
512: *
513: * @return Container
514: */
515: public function container()
516: {
517: return $this->container;
518: }
519:
520: /**
521: * returns the TEMP_URL for the object
522: *
523: * Some notes:
524: * * The `$secret` value is arbitrary; it must match the value set for
525: * the `X-Account-Meta-Temp-URL-Key` on the account level. This can be
526: * set by calling `$service->SetTempUrlSecret($secret)`.
527: * * The `$expires` value is the number of seconds you want the temporary
528: * URL to be valid for. For example, use `60` to make it valid for a
529: * minute
530: * * The `$method` must be either GET or PUT. No other methods are
531: * supported.
532: *
533: * @param string $secret the shared secret
534: * @param integer $expires the expiration time (in seconds)
535: * @param string $method either GET or PUT
536: * @return string the temporary URL
537: */
538: public function tempUrl($secret, $expires, $method)
539: {
540: $method = strtoupper($method);
541: $expiry_time = time() + $expires;
542:
543: // check for proper method
544: if ($method != 'GET' && $method != 'PUT') {
545: throw new Exceptions\TempUrlMethodError(sprintf(
546: Lang::translate(
547: 'Bad method [%s] for TempUrl; only GET or PUT supported'),
548: $method
549: ));
550: }
551:
552: // construct the URL
553: $url = $this->url();
554: $path = urldecode(parse_url($url, PHP_URL_PATH));
555:
556: $hmac_body = "$method\n$expiry_time\n$path";
557: $hash = hash_hmac('sha1', $hmac_body, $secret);
558:
559: $this->getLogger()->info('URL [{url}]; SIG [{sig}]; HASH [{hash}]', array(
560: 'url' => $url,
561: 'sig' => $hmac_body,
562: 'hash' => $hash
563: ));
564:
565: $temp_url = sprintf('%s?temp_url_sig=%s&temp_url_expires=%d', $url, $hash, $expiry_time);
566:
567: // debug that stuff
568: $this->getLogger()->info('TempUrl generated [{url}]', array(
569: 'url' => $temp_url
570: ));
571:
572: return $temp_url;
573: }
574:
575: /**
576: * Sets object data from string
577: *
578: * This is a convenience function to permit the use of other technologies
579: * for setting an object's content.
580: *
581: * @param string $data
582: * @return void
583: */
584: public function setData($data)
585: {
586: $this->data = (string) $data;
587: }
588:
589: /**
590: * Return object's data as a string
591: *
592: * @return string the entire object
593: */
594: public function saveToString()
595: {
596: return $this->getService()->request($this->url())->httpBody();
597: }
598:
599: /**
600: * Saves the object's data to local filename
601: *
602: * Given a local filename, the Object's data will be written to the newly
603: * created file.
604: *
605: * Example:
606: * <code>
607: * # ... authentication/connection/container code excluded
608: * # ... see previous examples
609: *
610: * # Whoops! I deleted my local README, let me download/save it
611: * #
612: * $my_docs = $conn->get_container("documents");
613: * $doc = $my_docs->get_object("README");
614: *
615: * $doc->SaveToFilename("/home/ej/cloudfiles/readme.restored");
616: * </code>
617: *
618: * @param string $filename name of local file to write data to
619: * @return boolean <kbd>TRUE</kbd> if successful
620: * @throws IOException error opening file
621: * @throws InvalidResponseException unexpected response
622: */
623: public function saveToFilename($filename)
624: {
625: if (!$fp = @fopen($filename, "wb")) {
626: throw new Exceptions\IOError(sprintf(
627: Lang::translate('Could not open file [%s] for writing'),
628: $filename
629: ));
630: }
631:
632: $result = $this->getService()->request($this->url(), 'GET', array(), $fp);
633:
634: fclose($fp);
635:
636: return $result;
637: }
638:
639: /**
640: * Saves the object's to a stream filename
641: *
642: * Given a local filename, the Object's data will be written to the stream
643: *
644: * Example:
645: * <code>
646: * # ... authentication/connection/container code excluded
647: * # ... see previous examples
648: *
649: * # If I want to write the README to a temporary memory string I
650: * # do :
651: * #
652: * $my_docs = $conn->get_container("documents");
653: * $doc = $my_docs->DataObject(array("name"=>"README"));
654: *
655: * $fp = fopen('php://temp', 'r+');
656: * $doc->SaveToStream($fp);
657: * fclose($fp);
658: * </code>
659: *
660: * @param string $filename name of local file to write data to
661: * @return boolean <kbd>TRUE</kbd> if successful
662: * @throws IOException error opening file
663: * @throws InvalidResponseException unexpected response
664: */
665: public function saveToStream($resource)
666: {
667: if (!is_resource($resource)) {
668: throw new Exceptions\ObjectError(
669: Lang::translate("Resource argument not a valid PHP resource."
670: ));
671: }
672:
673: return $this->getService()->request($this->url(), 'GET', array(), $resource);
674: }
675:
676:
677: /**
678: * Returns the object's MD5 checksum
679: *
680: * Accessor method for reading Object's private ETag attribute.
681: *
682: * @api
683: * @return string MD5 checksum hexidecimal string
684: */
685: public function getETag()
686: {
687: return $this->etag;
688: }
689:
690: /**
691: * Purges the object from the CDN
692: *
693: * Note that the object will still be served up to the time of its
694: * TTL value.
695: *
696: * @api
697: * @param string $email An email address that will be notified when
698: * the object is purged.
699: * @return void
700: * @throws CdnError if the container is not CDN-enabled
701: * @throws CdnHttpError if there is an HTTP error in the transaction
702: */
703: public function purgeCDN($email)
704: {
705: // @codeCoverageIgnoreStart
706: if (!$cdn = $this->Container()->CDNURL()) {
707: throw new Exceptions\CdnError(Lang::translate('Container is not CDN-enabled'));
708: }
709: // @codeCoverageIgnoreEnd
710:
711: $url = $cdn . '/' . $this->name;
712: $headers['X-Purge-Email'] = $email;
713: $response = $this->getService()->request($url, 'DELETE', $headers);
714:
715: // check the status
716: // @codeCoverageIgnoreStart
717: if ($response->httpStatus() > 204) {
718: throw new Exceptions\CdnHttpError(sprintf(
719: Lang::translate('Error purging object, status [%d] response [%s]'),
720: $response->httpStatus(),
721: $response->httpBody()
722: ));
723: }
724: // @codeCoverageIgnoreEnd
725:
726: return true;
727: }
728:
729: /**
730: * Returns the CDN URL (for managing the object)
731: *
732: * Note that the DataObject::PublicURL() method is used to return the
733: * publicly-available URL of the object, while the CDNURL() is used
734: * to manage the object.
735: *
736: * @return string
737: */
738: public function CDNURL()
739: {
740: return $this->container()->CDNURL() . '/' . $this->name;
741: }
742:
743: /**
744: * Returns the object's Public CDN URL, if available
745: *
746: * @api
747: * @param string $type can be 'streaming', 'ssl', 'ios-streaming',
748: * or anything else for the
749: * default URL. For example, `$object->PublicURL('ios-streaming')`
750: * @return string
751: */
752: public function publicURL($type = null)
753: {
754: if (!$prefix = $this->container()->CDNURI()) {
755: return null;
756: }
757:
758: switch(strtoupper($type)) {
759: case 'SSL':
760: $url = $this->container()->SSLURI().'/'.$this->name;
761: break;
762: case 'STREAMING':
763: $url = $this->container()->streamingURI().'/'.$this->name;
764: break;
765: case 'IOS':
766: case 'IOS-STREAMING':
767: $url = $this->container()->iosStreamingURI().'/'.$this->name;
768: break;
769: default:
770: $url = $prefix.'/'.$this->name;
771: break;
772: }
773:
774: return $url;
775: }
776:
777: /**
778: * Sets parameters from an array and validates them.
779: *
780: * @param array $params Associative array of parameters
781: * @return void
782: */
783: private function setParams(array $params = array())
784: {
785: // Inspect the user's array for any unapproved keys, and unset if necessary
786: foreach (array_diff(array_keys($params), $this->allowedProperties) as $key) {
787: $this->getLogger()->warning('You cannot use the {keyName} key when creating an object', array(
788: 'keyName' => $key
789: ));
790: unset($params[$key]);
791: }
792:
793: $this->populate($params);
794: }
795:
796: /**
797: * Retrieves a single object, parses headers
798: *
799: * @return void
800: * @throws NoNameError, ObjFetchError
801: */
802: private function fetch()
803: {
804: if (!$this->name) {
805: throw new Exceptions\NoNameError(Lang::translate('Cannot retrieve an unnamed object'));
806: }
807:
808: $response = $this->getService()->request($this->url(), 'HEAD', array('Accept' => '*/*'));
809:
810: // check for errors
811: // @codeCoverageIgnoreStart
812: if ($response->httpStatus() >= 300) {
813: throw new Exceptions\ObjFetchError(sprintf(
814: Lang::translate('Problem retrieving object [%s]'),
815: $this->url()
816: ));
817: }
818: // @codeCoverageIgnoreEnd
819:
820: // set headers as metadata?
821: $this->saveResponseHeaders($response);
822:
823: // parse the metadata
824: $this->getMetadata($response);
825: }
826:
827: /**
828: * Extracts the headers from the response, and saves them as object
829: * attributes. Additional name conversions are done where necessary.
830: *
831: * @param Http $response
832: */
833: private function saveResponseHeaders(Http $response, $fillExtraIfNotFound = true)
834: {
835: foreach ($response->headers() as $header => $value) {
836: if (isset($this->headerTranslate[$header])) {
837: // This header needs to be translated
838: $property = $this->headerTranslate[$header];
839: // Are there multiple properties that need to be set?
840: if (is_array($property)) {
841: foreach ($property as $subProperty) {
842: $this->$subProperty = $value;
843: }
844: } else {
845: $this->$property = $value;
846: }
847: } elseif ($fillExtraIfNotFound === true) {
848: // Otherwise, stock extra headers
849: $this->extra_headers[$header] = $value;
850: }
851: }
852: }
853:
854: /**
855: * Compatability.
856: */
857: public function refresh()
858: {
859: return $this->fetch();
860: }
861:
862: /**
863: * Returns the service associated with this object
864: *
865: * It's actually the object's container's service, so this method will
866: * simplify things a bit.
867: */
868: private function getService()
869: {
870: return $this->container->getService();
871: }
872:
873: /**
874: * Performs an internal check to get the proper MIME type for an object
875: *
876: * This function would go over the available PHP methods to get
877: * the MIME type.
878: *
879: * By default it will try to use the PHP fileinfo library which is
880: * available from PHP 5.3 or as an PECL extension
881: * (http://pecl.php.net/package/Fileinfo).
882: *
883: * It will get the magic file by default from the system wide file
884: * which is usually available in /usr/share/magic on Unix or try
885: * to use the file specified in the source directory of the API
886: * (share directory).
887: *
888: * if fileinfo is not available it will try to use the internal
889: * mime_content_type function.
890: *
891: * @param string $handle name of file or buffer to guess the type from
892: * @return boolean <kbd>TRUE</kbd> if successful
893: * @throws BadContentTypeException
894: * @codeCoverageIgnore
895: */
896: private function inferContentType($handle)
897: {
898: if ($contentType = $this->getContentType()) {
899: return $contentType;
900: }
901:
902: $contentType = false;
903:
904: $filePath = (is_string($handle)) ? $handle : (string) $handle;
905:
906: if (function_exists("finfo_open")) {
907:
908: $magicPath = dirname(__FILE__) . "/share/magic";
909: $finfo = new FileInfo(FILEINFO_MIME, file_exists($magicPath) ? $magicPath : null);
910:
911: if ($finfo) {
912:
913: $contentType = is_file($filePath)
914: ? $finfo->file($handle)
915: : $finfo->buffer($handle);
916:
917: /**
918: * PHP 5.3 fileinfo display extra information like charset so we
919: * remove everything after the ; since we are not into that stuff
920: */
921: if (null !== ($extraInfo = strpos($contentType, "; "))) {
922: $contentType = substr($contentType, 0, $extraInfo);
923: }
924: }
925:
926: //unset($finfo);
927: }
928:
929: if (!$contentType) {
930: // Try different native function instead
931: if (is_file((string) $handle) && function_exists("mime_content_type")) {
932: $contentType = mime_content_type($handle);
933: } else {
934: $this->getLogger()->error('Content-Type cannot be found');
935: }
936: }
937:
938: return $contentType;
939: }
940:
941: }
942: