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