[ZF-6295] Generic:
[zend.git] / library / Zend / Gdata / MediaMimeStream.php
blobdccd65301522a9c42f08ac06d72ca0254ab5a91c
1 <?php
3 /**
4 * Zend Framework
6 * LICENSE
8 * This source file is subject to the new BSD license that is bundled
9 * with this package in the file LICENSE.txt.
10 * It is also available through the world-wide-web at this URL:
11 * http://framework.zend.com/license/new-bsd
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@zend.com so we can send you a copy immediately.
16 * @category Zend
17 * @package Zend_Gdata
18 * @subpackage Gdata
19 * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
20 * @license http://framework.zend.com/license/new-bsd New BSD License
23 /**
24 * A streaming Media MIME class that allows for buffered read operations.
26 * @category Zend
27 * @package Zend_Gdata
28 * @subpackage Gdata
29 * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
30 * @license http://framework.zend.com/license/new-bsd New BSD License
32 class Zend_Gdata_MediaMimeStream
35 /**
36 * The Content-Type section that precedes the XML data in the message.
38 * @var string
40 // TODO (jhartmann) Add support for charset [ZF-5768]
41 const XML_HEADER = "Content-Type: application/atom+xml\r\n\r\n";
43 /**
44 * A constant indicating the xml string part of the message
46 * @var integer
48 const PART_XML_STRING = 0;
50 /**
51 * A constant indicating the file binary part of the message
53 * @var integer
55 const PART_FILE_BINARY = 1;
57 /**
58 * A constant indicating the closing boundary string of the message
60 * @var integer
62 const PART_CLOSING_XML_STRING = 2;
64 /**
65 * The maximum buffer size that can be used.
67 * @var integer
69 const MAX_BUFFER_SIZE = 8192;
71 /**
72 * A valid MIME boundary including a linefeed.
74 * @var string
76 protected $_boundaryLine = null;
78 /**
79 * A valid MIME boundary without a linefeed for use in the header.
81 * @var string
83 protected $_boundaryString = null;
85 /**
86 * A valid MIME closing boundary including a linefeed.
88 * @var string
90 protected $_closingBoundaryLine = null;
92 /**
93 * A handle to the file that is part of the message.
95 * @var resource
97 protected $_fileHandle = null;
99 /**
100 * The headers that preceed the file binary including linefeeds.
102 * @var string
104 protected $_fileHeaders = null;
107 * The internet media type of the enclosed file.
109 * @var string
111 protected $_fileContentType = null;
114 * The file size.
116 * @var integer
118 protected $_fileSize = null;
121 * The total size of the message.
123 * @var integer
125 protected $_totalSize = null;
128 * The XML string that typically represents the entry to be sent.
130 * @var string
132 protected $_xmlString = null;
135 * The number of bytes that have been read so far.
137 * @var integer
139 protected $_bytesRead = 0;
142 * Enumeration indicating the part of the message that is currently being
143 * read. Allowed values are: 0, 1 and 2, corresponding to the constants:
144 * PART_XML_STRING, PART_FILE_BINARY, PART_CLOSING_XML_STRING
146 * @var integer
148 protected $_currentPart = 0;
151 * A nested array containing the message to be sent. Each element contains
152 * an array in the format:
154 * [integer (size of message)][string (message)]
156 * Note that the part corresponding to the file only contains a size.
158 * @var array
160 protected $_parts = null;
163 * A boolean to be set immediately once we have finished reading.
165 * @var boolean
167 protected $_doneReading = false;
170 * Create a new MimeMediaStream object.
172 * @param string $xmlString The string corresponding to the XML section
173 * of the message, typically an atom entry or feed.
174 * @param string $filePath The path to the file that constitutes the binary
175 * part of the message.
176 * @param string $fileContentType The valid internet media type of the file.
177 * @throws Zend_Gdata_App_IOException If the file cannot be read or does
178 * not exist. Also if mbstring.func_overload has been set > 1.
180 public function __construct($xmlString = null, $filePath = null,
181 $fileContentType = null)
183 $this->_xmlString = $xmlString;
184 $this->_filePath = $filePath;
185 $this->_fileContentType = $fileContentType;
187 if (!file_exists($filePath) || !is_readable($filePath)) {
188 require_once 'Zend/Gdata/App/IOException.php';
189 throw new Zend_Gdata_App_IOException('File to be uploaded at ' .
190 $filePath . ' does not exist or is not readable.');
193 $this->_fileHandle = fopen($filePath, 'rb', true);
194 $this->generateBoundaries();
195 $this->calculatePartSizes();
199 * Generate the MIME message boundary strings.
201 * @return void
203 private function generateBoundaries()
205 $this->_boundaryString = '=_' . md5(microtime(1) . rand(1,20));
206 $this->_boundaryLine = "\r\n" . '--' . $this->_boundaryString . "\r\n";
207 $this->_closingBoundaryLine = "\r\n" . '--' . $this->_boundaryString .
208 '--';
212 * Calculate the sizes of the MIME message sections.
214 * @return void
216 private function calculatePartSizes()
218 $this->_fileHeaders = 'Content-Type: ' . $this->_fileContentType .
219 "\r\n" . 'Content-Transfer-Encoding: binary' . "\r\n\r\n";
220 $this->_fileSize = filesize($this->_filePath);
222 $stringSection = $this->_boundaryLine . self::XML_HEADER .
223 $this->_xmlString . "\r\n" . $this->_boundaryLine .
224 $this->_fileHeaders;
225 $stringLen = strlen($stringSection);
226 $closingBoundaryLen = strlen($this->_closingBoundaryLine);
228 $this->_parts = array();
229 $this->_parts[] = array($stringLen, $stringSection);
230 $this->_parts[] = array($this->_fileSize);
231 $this->_parts[] = array($closingBoundaryLen,
232 $this->_closingBoundaryLine);
234 $this->_totalSize = $stringLen + $this->_fileSize + $closingBoundaryLen;
238 * A wrapper around fread() that doesn't error when $length is 0.
240 * @param integer $length Number of bytes to read.
241 * @return string Results of byte operation.
243 private function smartfread($length)
245 if ($length < 1) {
246 return '';
247 } else {
248 return fread($this->_fileHandle, $length);
253 * A non mbstring overloadable strlen-like function.
255 * @param string $string The string whose length we want to get.
256 * @return integer The length of the string.
258 private function strlen2($string)
260 return array_sum(char_count($string));
264 * Read a specific chunk of the the MIME multipart message.
266 * This function works by examining the internal 'parts' array. It
267 * expects that array to consist of basically a string, a file handle
268 * and a closing string.
270 * An abbreviated version of what this function does is as follows:
272 * - throw exception if trying to read bigger than the allocated max buffer
273 * - If bufferSize bigger than the entire message: return it and exit.
275 * - Check how far to read by looking at how much has been read.
276 * - Figure out whether we are crossing sections in this read:
277 * i.e. -> reading past the xml_string and into the file ?
278 * - Determine whether we are crossing two sections in this read:
279 * i.e. xml_string, file and half of the closing string or
280 * possibly file, closing string and next (non-existant) section
281 * and handle each case.
282 * - If we are NOT crossing any sections: read either string and
283 * increment counter, or read file (no counter needed since fread()
284 * stores it's own counter.
285 * - If we are crossing 1 section, figure out how much remains in that
286 * section that we are currently reading and how far to read into
287 * the next section. If the section just read is xml_string, then
288 * immediately unset it from our 'parts' array. If it is the file,
289 * then close the handle.
291 * @param integer $bufferSize The size of the chunk that is to be read,
292 * must be lower than MAX_BUFFER_SIZE.
293 * @throws Zend_Gdata_App_InvalidArgumentException if buffer size too big.
294 * @return string A corresponding piece of the message. This could be
295 * binary or regular text.
297 public function read($bufferSize)
299 if ($bufferSize > self::MAX_BUFFER_SIZE) {
300 require_once 'Zend/Gdata/App/InvalidArgumentException.php';
301 throw new Zend_Gdata_App_InvalidArgumentException('Buffer size ' .
302 'is larger than the supported max of ' . self::MAX_BUFFER_SIZE);
305 // handle edge cases where bytesRead is negative
306 if ($this->_bytesRead < 0) {
307 $this->_bytesRead = 0;
310 $returnString = null;
311 // If entire message is smaller than the buffer, just return everything
312 if ($bufferSize > $this->_totalSize) {
313 $returnString = $this->_parts[self::PART_XML_STRING][1];
314 $returnString .= fread($this->_fileHandle, $bufferSize);
315 $returnString .= $this->_closingBoundaryLine;
316 $this->closeFileHandle();
317 $this->_doneReading = true;
318 return $returnString;
321 // increment internal counters
322 $readTo = $this->_bytesRead + $bufferSize;
323 $sizeOfCurrentPart = $this->_parts[$this->_currentPart][0];
324 $sizeOfNextPart = 0;
326 // if we are in a part past the current part, exit
327 if ($this->_currentPart > self::PART_CLOSING_XML_STRING) {
328 $this->_doneReading = true;
329 return;
332 // if bytes read is bigger than the current part and we are
333 // at the end, return
334 if (($this->_bytesRead > $sizeOfCurrentPart) &&
335 ($this->_currentPart == self::PART_CLOSING_XML_STRING)) {
336 $this->_doneReading = true;
337 return;
340 // check if we have a next part
341 if ($this->_currentPart != self::PART_CLOSING_XML_STRING) {
342 $nextPart = $this->_currentPart + 1;
343 $sizeOfNextPart = $this->_parts[$nextPart][0];
346 $readIntoNextPart = false;
347 $readFromRemainingPart = null;
348 $readFromNextPart = null;
350 // are we crossing into multiple sections of the message in
351 // this read?
352 if ($readTo > ($sizeOfCurrentPart + $sizeOfNextPart)) {
353 if ($this->_currentPart == self::PART_XML_STRING) {
354 // If we are in XML string and have crossed over the file
355 // return that and whatever we can from the closing boundary
356 // string.
357 $returnString = $this->_parts[self::PART_XML_STRING][1];
358 unset($this->_parts[self::PART_XML_STRING]);
359 $returnString .= fread($this->_fileHandle,
360 self::MAX_BUFFER_SIZE);
361 $this->closeFileHandle();
363 $readFromClosingString = $readTo -
364 ($sizeOfCurrentPart + $sizeOfNextPart);
365 $returnString .= substr(
366 $this->_parts[self::PART_CLOSING_XML_STRING][1], 0,
367 $readFromClosingString);
368 $this->_bytesRead = $readFromClosingString;
369 $this->_currentPart = self::PART_CLOSING_XML_STRING;
370 return $returnString;
372 } elseif ($this->_currentPart == self::PART_FILE_BINARY) {
373 // We have read past the entire message, so return it.
374 $returnString .= fread($this->_fileHandle,
375 self::MAX_BUFFER_SIZE);
376 $returnString .= $this->_closingBoundaryLine;
377 $this->closeFileHandle();
378 $this->_doneReading = true;
379 return $returnString;
381 // are we just crossing from one section into another?
382 } elseif ($readTo >= $sizeOfCurrentPart) {
383 $readIntoNextPart = true;
384 $readFromRemainingPart = $sizeOfCurrentPart - $this->_bytesRead;
385 $readFromNextPart = $readTo - $sizeOfCurrentPart;
388 if (!$readIntoNextPart) {
389 // we are not crossing any section so just return something
390 // from the current part
391 switch ($this->_currentPart) {
392 case self::PART_XML_STRING:
393 $returnString = $this->readFromStringPart(
394 $this->_currentPart, $this->_bytesRead, $bufferSize);
395 break;
396 case self::PART_FILE_BINARY:
397 $returnString = fread($this->_fileHandle, $bufferSize);
398 break;
399 case self::PART_CLOSING_XML_STRING:
400 $returnString = $this->readFromStringPart(
401 $this->_currentPart, $this->_bytesRead, $bufferSize);
402 break;
404 } else {
405 // we are crossing from one section to another, so figure out
406 // where we are coming from and going to
407 switch ($this->_currentPart) {
408 case self::PART_XML_STRING:
409 // crossing from string to file
410 $returnString = $this->readFromStringPart(
411 $this->_currentPart, $this->_bytesRead,
412 $readFromRemainingPart);
413 // free up string
414 unset($this->_parts[self::PART_XML_STRING]);
415 $returnString .= $this->smartfread($this->_fileHandle,
416 $readFromNextPart);
417 $this->_bytesRead = $readFromNextPart - 1;
418 break;
419 case self::PART_FILE_BINARY:
420 // skipping past file section
421 $returnString = $this->smartfread($this->_fileHandle,
422 $readFromRemainingPart);
423 $this->closeFileHandle();
424 // read closing boundary string
425 $returnString = $this->readFromStringPart(
426 self::PART_CLOSING_XML_STRING, 0, $readFromNextPart);
427 // have we read past the entire closing boundary string?
428 if ($readFromNextPart >=
429 $this->_parts[self::PART_CLOSING_XML_STRING][0]) {
430 $this->_doneReading = true;
431 return $returnString;
434 // Reset counter appropriately since we are now just
435 // counting how much of the final string is being read.
436 $this->_bytesRead = $readFromNextPart - 1;
437 break;
438 case self::PART_CLOSING_XML_STRING:
439 // reading past the end of the closing boundary
440 if ($readFromRemainingPart > 0) {
441 $returnString = $this->readFromStringPart(
442 $this->_currentPart, $this->_bytesRead,
443 $readFromRemainingPart);
444 $this->_doneReading = true;
446 return $returnString;
448 $this->_currentPart++;
450 $this->_bytesRead += $bufferSize;
451 return $returnString;
455 * Convenience method to shorthand the reading of non-file parts of the
456 * message.
458 * @param integer $part The part from which to read (supports only 0 or 2).
459 * @param integer $start The point at which to read from.
460 * @param integer $length How many characters to read.
461 * @return string A string of characters corresponding to the requested
462 * section.
464 private function readFromStringPart($part, $start, $length)
466 return substr($this->_parts[$part][1], $start, $length);
470 * Return the total size of the mime message.
472 * @return integer Total size of the message to be sent.
474 public function getTotalSize()
476 return $this->_totalSize;
480 * Check whether we have data left to read.
482 * @return boolean True if there is data remaining in the mime message,
483 * false, otherwise.
485 public function hasData()
487 return !($this->_doneReading);
491 * Close the internal file that we are streaming to the socket.
493 * @return void
495 protected function closeFileHandle()
497 if ($this->_fileHandle !== null) {
498 fclose($this->_fileHandle);
503 * Return a Content-type header that includes the current boundary string.
505 * @return string A valid HTTP Content-Type header.
507 public function getContentType()
509 return 'multipart/related; boundary="' .
510 $this->_boundaryString . '"' . "\r\n";