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 +227 -57 3.2.13.3.1 View file →
@@ -7,8 +7,9 @@
7 7
8 8 namespace BitCode\BitForm\Core\Integration\Telegram;
9 9
10 10 use BitCode\BitForm\Core\Util\FileHandler;
11 +use WP_Error;
11 12
12 13 /**
13 14 * Provide functionality for Upload files
14 15 */
@@ -13,8 +14,11 @@
13 14 * Provide functionality for Upload files
14 15 */
15 16 final class FilesApiHelper
16 17 {
18 + /** sendMediaGroup accepts 2-10 items per call. */
19 + private const MEDIA_GROUP_LIMIT = 10;
20 +
17 21 private $_defaultHeader;
18 22 private $_payloadBoundary;
19 23 private $_basepath;
20 24
@@ -24,9 +28,10 @@
24 28 * @param Integer $entryID Current submission ID
25 29 */
26 30 public function __construct($formID, $entryID)
27 31 {
28 - $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);
29 34 $this->_defaultHeader['Content-Type'] = 'multipart/form-data; boundary=' . $this->_payloadBoundary;
30 35 $this->_basepath = FileHandler::getEntriesFileUploadDir($formID, $entryID) . DIRECTORY_SEPARATOR;
31 36 }
32 37
@@ -35,106 +40,271 @@
35 40 *
36 41 * @param string $apiEndPoint Telegram API base URL
37 42 * @param array $data Data to pass to API
38 43 *
39 - * @return array $uploadResponse Telegram API response
44 + * @return string|WP_Error Telegram API response body
40 45 */
41 46 public function uploadFiles($apiEndPoint, $data)
42 47 {
43 - $filename = $this->getFileNameWithExtension($data['photo']) ?? $data['photo'];
44 - $filePath = "{$this->_basepath}{$filename}";
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 + }
56 +
45 57 $mimeType = mime_content_type($filePath);
46 - $fileType = \explode('/', $mimeType);
58 + $mimeType = $mimeType ? $mimeType : 'application/octet-stream';
59 + $param = self::classifyMime($mimeType);
47 60
48 - switch ($fileType[0]) {
49 - case 'image':
61 + switch ($param) {
62 + case 'photo':
50 63 $apiMethod = '/sendPhoto';
51 - $param = 'photo';
52 64 break;
53 65
54 66 case 'audio':
55 67 $apiMethod = '/sendAudio';
56 - $param = 'audio';
57 68 break;
69 +
58 70 case 'video':
59 71 $apiMethod = '/sendVideo';
60 - $param = 'video';
61 72 break;
62 73
63 74 default:
64 75 $apiMethod = '/sendDocument';
65 - $param = 'document';
66 76 break;
67 77 }
68 78 $uploadFileEndpoint = $apiEndPoint . $apiMethod;
69 79
70 - $data[$param] = new \CURLFILE($filePath);
71 - if ('photo' !== $param) {
72 - 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 + ];
73 127 }
74 - $args = [
75 - 'body' => $data,
76 - 'timeout' => 30,
77 - 'headers' => $this->_defaultHeader,
78 - ];
79 - $response = wp_remote_post($uploadFileEndpoint, $args);
80 - return wp_remote_retrieve_body($response);
128 +
129 + $batches = [];
130 + foreach ($buckets as $items) {
131 + foreach (array_chunk($items, self::MEDIA_GROUP_LIMIT) as $chunk) {
132 + $batches[] = $chunk;
133 + }
134 + }
135 +
136 + return $batches;
81 137 }
82 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 + */
83 148 public function uploadMultipleFiles($apiEndPoint, $data)
84 149 {
85 - $param = 'media';
86 150 $uploadMultipleFileEndpoint = $apiEndPoint . '/sendMediaGroup';
87 - $postFields = [
88 - 'chat_id' => $data['chat_id'],
89 - 'caption' => $data['caption']
90 - ];
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 = [];
91 156
92 - foreach ($data['media'] as $key => $value) {
93 - $filename = $this->getFileNameWithExtension($value) ?? $value;
94 - $filePath = "{$this->_basepath}{$filename}";
95 - $mimeType = mime_content_type($filePath);
96 - $fileType = \explode('/', $mimeType);
97 - 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 + ];
98 163
99 - if ('image' === $fileType[0]) {
100 - $type = 'photo';
101 - } elseif ('application' === $fileType[0] || 'text' === $fileType[0]) {
102 - $type = 'document';
103 - } elseif ('application' === $fileType[0]) {
104 - $type = 'document';
105 - } else {
106 - $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;
107 168 }
108 169
109 - $media[] = [
110 - 'type' => $type,
111 - 'media' => "attach://{$key}.path",
112 - 'caption' => $data['caption'],
113 - 'parse_mode' => 'HTML'
170 + $media[] = $mediaItem;
171 + $files[$attachName] = [
172 + 'path' => $item['path'],
173 + 'mime' => $item['mime'],
114 174 ];
115 - $nameK = "{$key}.path";
116 - $postFields[$nameK] = new \CURLFILE($filePath);
117 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 +
118 181 $postFields['media'] = wp_json_encode($media);
119 182
120 - if ('media' !== $param) {
121 - 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';
122 209 }
210 + }
123 211
124 - $args = [
125 - 'body' => $postFields,
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 + }
230 +
231 + $response = wp_remote_post($endpoint, [
232 + 'body' => $payload,
126 233 'timeout' => 30,
127 - 'headers' => [
128 - 'Content-Type' => 'multipart/form-data'
129 - ],
130 - ];
131 - $response = wp_remote_post($uploadMultipleFileEndpoint, $args);
234 + 'headers' => $this->_defaultHeader,
235 + ]);
236 +
237 + if (is_wp_error($response)) {
238 + return $response;
239 + }
240 +
132 241 return wp_remote_retrieve_body($response);
133 242 }
134 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 +
135 305 private function getFileNameWithExtension($url)
136 306 {
137 - $fileName = basename($url);
307 + $fileName = basename(strtok($url, '?'));
138 308 return false !== strpos($fileName, '.') ? $fileName : null;
139 309 }
140 310 }