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
bit-form / includes / Core / Integration / Telegram / FilesApiHelper.php

FilesApiHelper.php in Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder 3.3.1, at includes/Core/Integration/Telegram/FilesApiHelper.php

311 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Telegram Files Api
5 *
6 */
7
8 namespace BitCode\BitForm\Core\Integration\Telegram;
9
10 use BitCode\BitForm\Core\Util\FileHandler;
11 use WP_Error;
12
13 /**
14 * Provide functionality for Upload files
15 */
16 final class FilesApiHelper
17 {
18 /** sendMediaGroup accepts 2-10 items per call. */
19 private const MEDIA_GROUP_LIMIT = 10;
20
21 private $_defaultHeader;
22 private $_payloadBoundary;
23 private $_basepath;
24
25 /**
26 *
27 * @param Integer $formID ID of the form, for which integration is executing
28 * @param Integer $entryID Current submission ID
29 */
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);
34 $this->_defaultHeader['Content-Type'] = 'multipart/form-data; boundary=' . $this->_payloadBoundary;
35 $this->_basepath = FileHandler::getEntriesFileUploadDir($formID, $entryID) . DIRECTORY_SEPARATOR;
36 }
37
38 /**
39 * Helps to execute upload files api
40 *
41 * @param string $apiEndPoint Telegram API base URL
42 * @param array $data Data to pass to API
43 *
44 * @return string|WP_Error Telegram API response body
45 */
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 }
56
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':
63 $apiMethod = '/sendPhoto';
64 break;
65
66 case 'audio':
67 $apiMethod = '/sendAudio';
68 break;
69
70 case 'video':
71 $apiMethod = '/sendVideo';
72 break;
73
74 default:
75 $apiMethod = '/sendDocument';
76 break;
77 }
78 $uploadFileEndpoint = $apiEndPoint . $apiMethod;
79
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 ];
127 }
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;
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;
309 }
310 }
311