PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.3.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.3.1
3.3.1 V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 2.10.1 All 138 releases
← All changes | includes/Core/Integration/Telegram/FilesApiHelper.php +235 -79 2.10.03.3.1 View file →
@@ -6,13 +6,19 @@
6 6 */
7 7
8 8 namespace BitCode\BitForm\Core\Integration\Telegram;
9 9
10 +use BitCode\BitForm\Core\Util\FileHandler;
11 +use WP_Error;
12 +
10 13 /**
11 14 * Provide functionality for Upload files
12 15 */
13 16 final class FilesApiHelper
14 17 {
18 + /** sendMediaGroup accepts 2-10 items per call. */
19 + private const MEDIA_GROUP_LIMIT = 10;
20 +
15 21 private $_defaultHeader;
16 22 private $_payloadBoundary;
17 23 private $_basepath;
18 24
@@ -22,11 +28,12 @@
22 28 * @param Integer $entryID Current submission ID
23 29 */
24 30 public function __construct($formID, $entryID)
25 31 {
26 - $this->_payloadBoundary = wp_generate_password(24);
32 + // No special chars: the boundary is echoed in the header and every part delimiter.
33 + $this->_payloadBoundary = 'BitFormBoundary' . wp_generate_password(24, false);
27 34 $this->_defaultHeader['Content-Type'] = 'multipart/form-data; boundary=' . $this->_payloadBoundary;
28 - $this->_basepath = BITFORMS_UPLOAD_DIR . DIRECTORY_SEPARATOR . $formID . DIRECTORY_SEPARATOR . $entryID . DIRECTORY_SEPARATOR;
35 + $this->_basepath = FileHandler::getEntriesFileUploadDir($formID, $entryID) . DIRECTORY_SEPARATOR;
29 36 }
30 37
31 38 /**
32 39 * Helps to execute upload files api
@@ -33,122 +40,271 @@
33 40 *
34 41 * @param string $apiEndPoint Telegram API base URL
35 42 * @param array $data Data to pass to API
36 43 *
37 - * @return array $uploadResponse Telegram API response
44 + * @return string|WP_Error Telegram API response body
38 45 */
39 46 public function uploadFiles($apiEndPoint, $data)
40 47 {
41 - $mimeType = mime_content_type("{$this->_basepath}{$data['photo']}");
42 - $fileType = \explode('/', $mimeType);
48 + $filePath = $this->resolveFilePath($data['photo']);
49 + if (is_null($filePath)) {
50 + return new WP_Error(
51 + 'TELEGRAM_FILE_NOT_FOUND',
52 + /* translators: %s: uploaded file reference */
53 + sprintf(__('Telegram attachment could not be read: %s', 'bit-form'), (string) $data['photo'])
54 + );
55 + }
43 56
44 - switch ($fileType[0]) {
45 - case 'image':
57 + $mimeType = mime_content_type($filePath);
58 + $mimeType = $mimeType ? $mimeType : 'application/octet-stream';
59 + $param = self::classifyMime($mimeType);
60 +
61 + switch ($param) {
62 + case 'photo':
46 63 $apiMethod = '/sendPhoto';
47 - $param = 'photo';
48 64 break;
49 65
50 66 case 'audio':
51 67 $apiMethod = '/sendAudio';
52 - $param = 'audio';
53 68 break;
69 +
54 70 case 'video':
55 71 $apiMethod = '/sendVideo';
56 - $param = 'video';
57 72 break;
58 73
59 74 default:
60 75 $apiMethod = '/sendDocument';
61 - $param = 'document';
62 76 break;
63 77 }
64 78 $uploadFileEndpoint = $apiEndPoint . $apiMethod;
65 79
66 - $data[$param] = new \CURLFILE("{$this->_basepath}{$data['photo']}");
67 - if ('photo' !== $param) {
68 - unset($data['photo']);
80 + unset($data['photo']);
81 +
82 + $files = [
83 + $param => [
84 + 'path' => $filePath,
85 + 'mime' => $mimeType,
86 + ],
87 + ];
88 +
89 + return $this->post($uploadFileEndpoint, $data, $files);
90 + }
91 +
92 + /**
93 + * Split attachment URLs into batches Telegram will accept.
94 + *
95 + * sendMediaGroup takes at most ten items and the group must be type-compatible:
96 + * photo and video may share one, documents may not, audio may not. Bucket order
97 + * follows first appearance, so a single-type upload behaves as before.
98 + *
99 + * @param array $urls Stored attachment URLs
100 + *
101 + * @return array List of batches; each batch is a list of
102 + * ['url' => string, 'path' => string, 'mime' => string, 'kind' => string].
103 + * Unreadable files are dropped.
104 + */
105 + public function buildMediaBatches($urls)
106 + {
107 + $buckets = [];
108 +
109 + foreach ($urls as $url) {
110 + $filePath = $this->resolveFilePath($url);
111 + if (is_null($filePath)) {
112 + continue;
113 + }
114 +
115 + $mimeType = mime_content_type($filePath);
116 + $mimeType = $mimeType ? $mimeType : 'application/octet-stream';
117 + $kind = self::classifyMime($mimeType);
118 + // photo and video are the only pair Telegram lets share a media group
119 + $bucket = ('photo' === $kind || 'video' === $kind) ? 'visual' : $kind;
120 +
121 + $buckets[$bucket][] = [
122 + 'url' => $url,
123 + 'path' => $filePath,
124 + 'mime' => $mimeType,
125 + 'kind' => $kind,
126 + ];
69 127 }
70 - $curl = curl_init();
71 - curl_setopt_array(
72 - $curl,
73 - [
74 - CURLOPT_URL => $uploadFileEndpoint,
75 - CURLOPT_RETURNTRANSFER => true,
76 - CURLOPT_ENCODING => '',
77 - CURLOPT_MAXREDIRS => 10,
78 - CURLOPT_TIMEOUT => 0,
79 - CURLOPT_FOLLOWLOCATION => true,
80 - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
81 - CURLOPT_CUSTOMREQUEST => 'POST',
82 - CURLOPT_POSTFIELDS => $data,
83 - ]
84 - );
85 128
86 - $uploadResponse = curl_exec($curl);
129 + $batches = [];
130 + foreach ($buckets as $items) {
131 + foreach (array_chunk($items, self::MEDIA_GROUP_LIMIT) as $chunk) {
132 + $batches[] = $chunk;
133 + }
134 + }
87 135
88 - curl_close($curl);
89 - return $uploadResponse;
136 + return $batches;
90 137 }
91 138
139 + /**
140 + * Send one type-compatible batch of at most ten files as a media group.
141 + *
142 + * @param string $apiEndPoint Telegram API base URL
143 + * @param array $data chat_id, parse_mode, caption, and `media`: one
144 + * batch from buildMediaBatches()
145 + *
146 + * @return string|WP_Error Telegram API response body
147 + */
92 148 public function uploadMultipleFiles($apiEndPoint, $data)
93 149 {
94 - $param = 'media';
95 150 $uploadMultipleFileEndpoint = $apiEndPoint . '/sendMediaGroup';
96 - $postFields = [
97 - 'chat_id' => $data['chat_id'],
98 - 'caption' => $data['caption']
99 - ];
151 + $postFields = ['chat_id' => $data['chat_id']];
152 + $parseMode = empty($data['parse_mode']) ? 'HTML' : $data['parse_mode'];
153 + $caption = isset($data['caption']) ? $data['caption'] : '';
154 + $media = [];
155 + $files = [];
100 156
101 - foreach ($data['media'] as $key => $value) {
102 - $mimeType = mime_content_type("{$this->_basepath}{$value}");
103 - $fileType = \explode('/', $mimeType);
104 - unset($data['media'][$key]);
157 + foreach ($data['media'] as $key => $item) {
158 + $attachName = "file{$key}";
159 + $mediaItem = [
160 + 'type' => $item['kind'],
161 + 'media' => "attach://{$attachName}",
162 + ];
105 163
106 - if ('image' === $fileType[0]) {
107 - $type = 'photo';
108 - } elseif ('application' === $fileType[0] || 'text' === $fileType[0]) {
109 - $type = 'document';
110 - } elseif ('application' === $fileType[0]) {
111 - $type = 'document';
112 - } else {
113 - $type = $fileType[0];
164 + // Telegram shows the album caption from the first item only.
165 + if (empty($media) && '' !== $caption) {
166 + $mediaItem['caption'] = $caption;
167 + $mediaItem['parse_mode'] = $parseMode;
114 168 }
115 169
116 - $media[] = [
117 - 'type' => $type,
118 - 'media' => "attach://{$key}.path",
119 - 'caption' => $data['caption'],
120 - 'parse_mode' => 'HTML'
170 + $media[] = $mediaItem;
171 + $files[$attachName] = [
172 + 'path' => $item['path'],
173 + 'mime' => $item['mime'],
121 174 ];
122 - $nameK = "{$key}.path";
123 - $postFields[$nameK] = new \CURLFILE("{$this->_basepath}{$value}");
124 175 }
176 +
177 + if (empty($media)) {
178 + return new WP_Error('TELEGRAM_FILE_NOT_FOUND', __('None of the Telegram attachments could be read.', 'bit-form'));
179 + }
180 +
125 181 $postFields['media'] = wp_json_encode($media);
126 182
127 - if ('media' !== $param) {
128 - unset($data['media']);
183 + return $this->post($uploadMultipleFileEndpoint, $postFields, $files);
184 + }
185 +
186 + /**
187 + * Telegram's media type for a MIME type; also the sendX endpoint suffix.
188 + *
189 + * @param string $mimeType
190 + *
191 + * @return string photo|audio|video|document
192 + */
193 + private static function classifyMime($mimeType)
194 + {
195 + $group = strtok($mimeType, '/');
196 +
197 + switch ($group) {
198 + case 'image':
199 + return 'photo';
200 +
201 + case 'audio':
202 + return 'audio';
203 +
204 + case 'video':
205 + return 'video';
206 +
207 + default:
208 + return 'document';
129 209 }
210 + }
130 211
131 - $curl = curl_init();
132 - curl_setopt_array(
133 - $curl,
134 - [
135 - CURLOPT_URL => $uploadMultipleFileEndpoint,
136 - CURLOPT_RETURNTRANSFER => true,
137 - CURLOPT_ENCODING => '',
138 - CURLOPT_MAXREDIRS => 10,
139 - CURLOPT_TIMEOUT => 0,
140 - CURLOPT_FOLLOWLOCATION => true,
141 - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
142 - CURLOPT_CUSTOMREQUEST => 'POST',
143 - CURLOPT_POSTFIELDS => $postFields,
144 - CURLOPT_HTTPHEADER => [
145 - 'Content-Type: multipart/form-data'
146 - ],
147 - ]
148 - );
212 + /**
213 + * Post a hand-built multipart/form-data payload.
214 + *
215 + * wp_remote_post() runs array bodies through http_build_query(), which flattens a
216 + * \CURLFile into params and never uploads it, so the body is encoded here.
217 + *
218 + * @param string $endpoint
219 + * @param array $fields Scalar form fields
220 + * @param array $files [name => ['path' => ..., 'mime' => ...]]
221 + *
222 + * @return string|WP_Error
223 + */
224 + private function post($endpoint, $fields, $files)
225 + {
226 + $payload = $this->buildMultipartBody($fields, $files);
227 + if (is_wp_error($payload)) {
228 + return $payload;
229 + }
149 230
150 - $uploadResponse = curl_exec($curl);
151 - curl_close($curl);
152 - return $uploadResponse;
231 + $response = wp_remote_post($endpoint, [
232 + 'body' => $payload,
233 + 'timeout' => 30,
234 + 'headers' => $this->_defaultHeader,
235 + ]);
236 +
237 + if (is_wp_error($response)) {
238 + return $response;
239 + }
240 +
241 + return wp_remote_retrieve_body($response);
242 + }
243 +
244 + /**
245 + * @return string|WP_Error
246 + */
247 + private function buildMultipartBody($fields, $files)
248 + {
249 + $boundary = $this->_payloadBoundary;
250 + $payload = '';
251 +
252 + foreach ($fields as $name => $value) {
253 + if (is_null($value) || '' === $value || is_array($value) || is_object($value)) {
254 + continue;
255 + }
256 + $payload .= "--{$boundary}\r\n";
257 + $payload .= "Content-Disposition: form-data; name=\"{$name}\"\r\n\r\n";
258 + $payload .= $value . "\r\n";
259 + }
260 +
261 + foreach ($files as $name => $file) {
262 + $contents = file_get_contents($file['path']);
263 + if (false === $contents) {
264 + return new WP_Error(
265 + 'TELEGRAM_FILE_NOT_FOUND',
266 + /* translators: %s: attachment file path */
267 + sprintf(__('Telegram attachment could not be read: %s', 'bit-form'), $file['path'])
268 + );
269 + }
270 + $filename = basename($file['path']);
271 + $payload .= "--{$boundary}\r\n";
272 + $payload .= "Content-Disposition: form-data; name=\"{$name}\"; filename=\"{$filename}\"\r\n";
273 + $payload .= "Content-Type: {$file['mime']}\r\n\r\n";
274 + $payload .= $contents . "\r\n";
275 + }
276 +
277 + $payload .= "--{$boundary}--\r\n";
278 +
279 + return $payload;
280 + }
281 +
282 + /**
283 + * Map a stored attachment URL back to its file inside this entry's upload dir.
284 + *
285 + * @param mixed $url
286 + *
287 + * @return string|null Absolute readable path, or null when it can't be resolved
288 + */
289 + private function resolveFilePath($url)
290 + {
291 + if (!is_string($url) || '' === $url) {
292 + return null;
293 + }
294 +
295 + $filename = $this->getFileNameWithExtension(rawurldecode($url));
296 + if (is_null($filename) || !FileHandler::isSafeFileName($filename)) {
297 + return null;
298 + }
299 +
300 + $filePath = $this->_basepath . $filename;
301 +
302 + return is_readable($filePath) && is_file($filePath) ? $filePath : null;
303 + }
304 +
305 + private function getFileNameWithExtension($url)
306 + {
307 + $fileName = basename(strtok($url, '?'));
308 + return false !== strpos($fileName, '.') ? $fileName : null;
153 309 }
154 310 }