1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * Redistributions of files must retain the above copyright notice.
8: *
9: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
10: * @link https://cakephp.org CakePHP(tm) Project
11: * @since 3.0.0
12: * @license https://opensource.org/licenses/mit-license.php MIT License
13: */
14: namespace Cake\Http\Client;
15:
16: use Countable;
17: use finfo;
18:
19: /**
20: * Provides an interface for building
21: * multipart/form-encoded message bodies.
22: *
23: * Used by Http\Client to upload POST/PUT data
24: * and files.
25: */
26: class FormData implements Countable
27: {
28: /**
29: * Boundary marker.
30: *
31: * @var string
32: */
33: protected $_boundary;
34:
35: /**
36: * Whether or not this formdata object has attached files.
37: *
38: * @var bool
39: */
40: protected $_hasFile = false;
41:
42: /**
43: * Whether or not this formdata object has a complex part.
44: *
45: * @var bool
46: */
47: protected $_hasComplexPart = false;
48:
49: /**
50: * The parts in the form data.
51: *
52: * @var \Cake\Http\Client\FormDataPart[]
53: */
54: protected $_parts = [];
55:
56: /**
57: * Get the boundary marker
58: *
59: * @return string
60: */
61: public function boundary()
62: {
63: if ($this->_boundary) {
64: return $this->_boundary;
65: }
66: $this->_boundary = md5(uniqid(time()));
67:
68: return $this->_boundary;
69: }
70:
71: /**
72: * Method for creating new instances of Part
73: *
74: * @param string $name The name of the part.
75: * @param string $value The value to add.
76: * @return \Cake\Http\Client\FormDataPart
77: */
78: public function newPart($name, $value)
79: {
80: return new FormDataPart($name, $value);
81: }
82:
83: /**
84: * Add a new part to the data.
85: *
86: * The value for a part can be a string, array, int,
87: * float, filehandle, or object implementing __toString()
88: *
89: * If the $value is an array, multiple parts will be added.
90: * Files will be read from their current position and saved in memory.
91: *
92: * @param string|\Cake\Http\Client\FormData $name The name of the part to add,
93: * or the part data object.
94: * @param mixed $value The value for the part.
95: * @return $this
96: */
97: public function add($name, $value = null)
98: {
99: if (is_array($value)) {
100: $this->addRecursive($name, $value);
101: } elseif (is_resource($value)) {
102: $this->addFile($name, $value);
103: } elseif ($name instanceof FormDataPart && $value === null) {
104: $this->_hasComplexPart = true;
105: $this->_parts[] = $name;
106: } else {
107: $this->_parts[] = $this->newPart($name, $value);
108: }
109:
110: return $this;
111: }
112:
113: /**
114: * Add multiple parts at once.
115: *
116: * Iterates the parameter and adds all the key/values.
117: *
118: * @param array $data Array of data to add.
119: * @return $this
120: */
121: public function addMany(array $data)
122: {
123: foreach ($data as $name => $value) {
124: $this->add($name, $value);
125: }
126:
127: return $this;
128: }
129:
130: /**
131: * Add either a file reference (string starting with @)
132: * or a file handle.
133: *
134: * @param string $name The name to use.
135: * @param mixed $value Either a string filename, or a filehandle.
136: * @return \Cake\Http\Client\FormDataPart
137: */
138: public function addFile($name, $value)
139: {
140: $this->_hasFile = true;
141:
142: $filename = false;
143: $contentType = 'application/octet-stream';
144: if (is_resource($value)) {
145: $content = stream_get_contents($value);
146: if (stream_is_local($value)) {
147: $finfo = new finfo(FILEINFO_MIME);
148: $metadata = stream_get_meta_data($value);
149: $contentType = $finfo->file($metadata['uri']);
150: $filename = basename($metadata['uri']);
151: }
152: } else {
153: $finfo = new finfo(FILEINFO_MIME);
154: $value = substr($value, 1);
155: $filename = basename($value);
156: $content = file_get_contents($value);
157: $contentType = $finfo->file($value);
158: }
159: $part = $this->newPart($name, $content);
160: $part->type($contentType);
161: if ($filename) {
162: $part->filename($filename);
163: }
164: $this->add($part);
165:
166: return $part;
167: }
168:
169: /**
170: * Recursively add data.
171: *
172: * @param string $name The name to use.
173: * @param mixed $value The value to add.
174: * @return void
175: */
176: public function addRecursive($name, $value)
177: {
178: foreach ($value as $key => $value) {
179: $key = $name . '[' . $key . ']';
180: $this->add($key, $value);
181: }
182: }
183:
184: /**
185: * Returns the count of parts inside this object.
186: *
187: * @return int
188: */
189: public function count()
190: {
191: return count($this->_parts);
192: }
193:
194: /**
195: * Check whether or not the current payload
196: * has any files.
197: *
198: * @return bool Whether or not there is a file in this payload.
199: */
200: public function hasFile()
201: {
202: return $this->_hasFile;
203: }
204:
205: /**
206: * Check whether or not the current payload
207: * is multipart.
208: *
209: * A payload will become multipart when you add files
210: * or use add() with a Part instance.
211: *
212: * @return bool Whether or not the payload is multipart.
213: */
214: public function isMultipart()
215: {
216: return $this->hasFile() || $this->_hasComplexPart;
217: }
218:
219: /**
220: * Get the content type for this payload.
221: *
222: * If this object contains files, `multipart/form-data` will be used,
223: * otherwise `application/x-www-form-urlencoded` will be used.
224: *
225: * @return string
226: */
227: public function contentType()
228: {
229: if (!$this->isMultipart()) {
230: return 'application/x-www-form-urlencoded';
231: }
232:
233: return 'multipart/form-data; boundary=' . $this->boundary();
234: }
235:
236: /**
237: * Converts the FormData and its parts into a string suitable
238: * for use in an HTTP request.
239: *
240: * @return string
241: */
242: public function __toString()
243: {
244: if ($this->isMultipart()) {
245: $boundary = $this->boundary();
246: $out = '';
247: foreach ($this->_parts as $part) {
248: $out .= "--$boundary\r\n";
249: $out .= (string)$part;
250: $out .= "\r\n";
251: }
252: $out .= "--$boundary--\r\n";
253:
254: return $out;
255: }
256: $data = [];
257: foreach ($this->_parts as $part) {
258: $data[$part->name()] = $part->value();
259: }
260:
261: return http_build_query($data);
262: }
263: }
264:
265: // @deprecated 3.4.0 Add backwards compat alias.
266: class_alias('Cake\Http\Client\FormData', 'Cake\Network\Http\FormData');
267: