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 +260 -37 2.03.3.1 View file →
@@ -1,5 +1,6 @@
1 1 <?php
2 +
2 3 /**
3 4 * Telegram Files Api
4 5 *
5 6 */
@@ -5,12 +6,19 @@
5 6 */
6 7
7 8 namespace BitCode\BitForm\Core\Integration\Telegram;
8 9
10 +use BitCode\BitForm\Core\Util\FileHandler;
11 +use WP_Error;
12 +
9 13 /**
10 14 * Provide functionality for Upload files
11 15 */
12 -final class FilesApiHelper {
16 +final class FilesApiHelper
17 +{
18 + /** sendMediaGroup accepts 2-10 items per call. */
19 + private const MEDIA_GROUP_LIMIT = 10;
20 +
13 21 private $_defaultHeader;
14 22 private $_payloadBoundary;
15 23 private $_basepath;
16 24
@@ -18,70 +26,285 @@
18 26 *
19 27 * @param Integer $formID ID of the form, for which integration is executing
20 28 * @param Integer $entryID Current submission ID
21 29 */
22 - public function __construct($formID, $entryID) {
23 - $this->_payloadBoundary = wp_generate_password(24);
30 + public function __construct($formID, $entryID)
31 + {
32 + // No special chars: the boundary is echoed in the header and every part delimiter.
33 + $this->_payloadBoundary = 'BitFormBoundary' . wp_generate_password(24, false);
24 34 $this->_defaultHeader['Content-Type'] = 'multipart/form-data; boundary=' . $this->_payloadBoundary;
25 - $this->_basepath = BITFORMS_UPLOAD_DIR . DIRECTORY_SEPARATOR . $formID . DIRECTORY_SEPARATOR . $entryID . DIRECTORY_SEPARATOR;
35 + $this->_basepath = FileHandler::getEntriesFileUploadDir($formID, $entryID) . DIRECTORY_SEPARATOR;
26 36 }
27 37
28 38 /**
29 39 * Helps to execute upload files api
30 40 *
31 - * @param String $apiEndPoint Telegram API base URL
32 - * @param Array $data Data to pass to API
41 + * @param string $apiEndPoint Telegram API base URL
42 + * @param array $data Data to pass to API
33 43 *
34 - * @return Array $uploadResponse Telegram API response
44 + * @return string|WP_Error Telegram API response body
35 45 */
36 - public function uploadFiles($apiEndPoint, $data) {
37 - $mimeType = mime_content_type("{$this->_basepath}{$data['photo']}");
38 - $fileType = \explode('/', $mimeType);
46 + public function uploadFiles($apiEndPoint, $data)
47 + {
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 + }
39 56
40 - switch ($fileType[0]) {
41 - 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':
42 63 $apiMethod = '/sendPhoto';
43 - $param = 'photo';
44 64 break;
45 65
46 66 case 'audio':
47 67 $apiMethod = '/sendAudio';
48 - $param = 'audio';
49 68 break;
69 +
50 70 case 'video':
51 71 $apiMethod = '/sendVideo';
52 - $param = 'video';
53 72 break;
54 73
55 74 default:
56 75 $apiMethod = '/sendDocument';
57 - $param = 'document';
58 76 break;
59 77 }
60 78 $uploadFileEndpoint = $apiEndPoint . $apiMethod;
61 79
62 - $data[$param] = new \CURLFILE("{$this->_basepath}{$data['photo']}");
63 - if ('photo' !== $param) {
64 - 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 + ];
65 127 }
66 - $curl = curl_init();
67 - curl_setopt_array(
68 - $curl,
69 - [
70 - CURLOPT_URL => $uploadFileEndpoint,
71 - CURLOPT_RETURNTRANSFER => true,
72 - CURLOPT_ENCODING => '',
73 - CURLOPT_MAXREDIRS => 10,
74 - CURLOPT_TIMEOUT => 0,
75 - CURLOPT_FOLLOWLOCATION => true,
76 - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
77 - CURLOPT_CUSTOMREQUEST => 'POST',
78 - CURLOPT_POSTFIELDS => $data,
79 - ]
80 - );
81 128
82 - $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 + }
83 135
84 - curl_close($curl);
85 - return $uploadResponse;
136 + return $batches;
137 + }
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 + */
148 + public function uploadMultipleFiles($apiEndPoint, $data)
149 + {
150 + $uploadMultipleFileEndpoint = $apiEndPoint . '/sendMediaGroup';
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 = [];
156 +
157 + foreach ($data['media'] as $key => $item) {
158 + $attachName = "file{$key}";
159 + $mediaItem = [
160 + 'type' => $item['kind'],
161 + 'media' => "attach://{$attachName}",
162 + ];
163 +
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;
168 + }
169 +
170 + $media[] = $mediaItem;
171 + $files[$attachName] = [
172 + 'path' => $item['path'],
173 + 'mime' => $item['mime'],
174 + ];
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 +
181 + $postFields['media'] = wp_json_encode($media);
182 +
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';
209 + }
210 + }
211 +
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,
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;
86 309 }
87 310 }