PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.62
Timetics – Appointment Booking Calendar & Scheduling v1.0.62
1.0.62 1.0.63 1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 1.0.22 All 64 releases
timetics / vendor / themewinter / email-notification-sdk / src / Whatsapp / TemplatesAPI.php

TemplatesAPI.php in Timetics – Appointment Booking Calendar & Scheduling 1.0.62, at vendor/themewinter/email-notification-sdk/src/Whatsapp/TemplatesAPI.php

353 lines 12.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Ens\Whatsapp;
3
4 use Ens\Utils\Helpers;
5 use WP_HTTP_Response;
6 use WP_REST_Controller;
7 use WP_REST_Server;
8
9 /**
10 * Class TemplatesAPI
11 *
12 * Fetches WhatsApp message templates from Meta Graph API and returns parsed
13 * metadata (body text, example values, parameter count) so the UI can render
14 * an accurate, template-aware parameter editor instead of asking the user to
15 * guess how many values to provide.
16 *
17 * @package Ens\Whatsapp
18 *
19 * @since 1.0.0
20 */
21 class TemplatesAPI extends WP_REST_Controller {
22
23 const GRAPH_API_VERSION = 'v25.0';
24 const CACHE_TTL = 5 * MINUTE_IN_SECONDS;
25
26 /**
27 * @var string
28 */
29 protected $namespace;
30
31 /**
32 * @var string
33 */
34 protected $rest_base = 'whatsapp/templates';
35
36 /**
37 * @var string
38 */
39 protected $identifier;
40
41 /**
42 * Initialize and register REST routes.
43 *
44 * @since 1.0.0
45 *
46 * @param string $identifier The consumer plugin identifier.
47 *
48 * @return void
49 */
50 public function init( $identifier ) {
51 $this->identifier = $identifier;
52 $plugin_slug = Helpers::get_config_data( $this->identifier, 'plugin_slug' );
53 $this->namespace = $plugin_slug . '/v1';
54
55 add_action( 'rest_api_init', [ $this, 'register_routes' ] );
56 }
57
58 /**
59 * Register the REST routes.
60 *
61 * @since 1.0.0
62 *
63 * @return void
64 */
65 public function register_routes() {
66 register_rest_route(
67 $this->namespace,
68 '/' . $this->rest_base,
69 [
70 [
71 'methods' => WP_REST_Server::READABLE,
72 'callback' => [ $this, 'get_items' ],
73 'permission_callback' => [ $this, 'get_items_permissions_check' ],
74 'args' => [
75 'refresh' => [
76 'description' => __( 'Bypass cache and fetch a fresh list from Meta.', 'wp-cafe' ),
77 'type' => 'boolean',
78 'default' => false,
79 ],
80 ],
81 ],
82 ]
83 );
84 }
85
86 /**
87 * Permission check — only logged-in users with manage capability.
88 *
89 * Consumers can override via the `{prefix}_ens_whatsapp_templates_permission` filter.
90 *
91 * @since 1.0.0
92 *
93 * @param \WP_REST_Request $request Current request.
94 *
95 * @return bool|WP_HTTP_Response
96 */
97 public function get_items_permissions_check( $request ) {
98 $nonce_check = Helpers::ens_verify_nonce( $request->get_header( 'x_wp_nonce' ), $this->identifier );
99 if ( $nonce_check instanceof WP_HTTP_Response ) {
100 return $nonce_check;
101 }
102
103 $can = current_user_can( 'manage_options' );
104
105 return (bool) apply_filters(
106 Helpers::get_hook_name( $this->identifier, 'ens_whatsapp_templates_permission' ),
107 $can,
108 $request
109 );
110 }
111
112 /**
113 * Fetch and return parsed templates.
114 *
115 * @since 1.0.0
116 *
117 * @param \WP_REST_Request $request Current request.
118 *
119 * @return \WP_REST_Response
120 */
121 public function get_items( $request ) {
122 $force_refresh = (bool) $request->get_param( 'refresh' );
123 $creds = $this->get_credentials();
124
125 if ( empty( $creds['access_token'] ) || empty( $creds['business_id'] ) ) {
126 return rest_ensure_response( [
127 'success' => 0,
128 'status_code' => 400,
129 'message' => __( 'WhatsApp credentials missing. Set Access Token and WhatsApp Business Account ID in integration settings.', 'wp-cafe' ),
130 'data' => [
131 'items' => [],
132 'missing_fields' => array_values( array_filter( [
133 empty( $creds['access_token'] ) ? 'access_token' : null,
134 empty( $creds['business_id'] ) ? 'business_id' : null,
135 ] ) ),
136 ],
137 ] );
138 }
139
140 $cache_key = $this->identifier . '_ens_whatsapp_templates_cache';
141 if ( ! $force_refresh ) {
142 $cached = get_transient( $cache_key );
143 if ( is_array( $cached ) ) {
144 return rest_ensure_response( [
145 'success' => 1,
146 'status_code' => 200,
147 'message' => __( 'Templates loaded from cache.', 'wp-cafe' ),
148 'data' => [ 'items' => $cached, 'cached' => true ],
149 ] );
150 }
151 }
152
153 $version = apply_filters( 'ens_whatsapp_api_version', self::GRAPH_API_VERSION, $this->identifier );
154 $endpoint = add_query_arg(
155 [
156 'fields' => 'name,language,status,category,parameter_format,components',
157 'limit' => 200,
158 ],
159 'https://graph.facebook.com/' . rawurlencode( $version ) . '/' . rawurlencode( $creds['business_id'] ) . '/message_templates'
160 );
161
162 $response = wp_remote_get( $endpoint, [
163 'timeout' => 15,
164 'headers' => [
165 'Authorization' => 'Bearer ' . $creds['access_token'],
166 ],
167 ] );
168
169 if ( is_wp_error( $response ) ) {
170 return rest_ensure_response( [
171 'success' => 0,
172 'status_code' => 502,
173 'message' => $response->get_error_message(),
174 'data' => [ 'items' => [] ],
175 ] );
176 }
177
178 $code = (int) wp_remote_retrieve_response_code( $response );
179 $body = wp_remote_retrieve_body( $response );
180 $json = json_decode( $body, true );
181
182 if ( $code >= 400 ) {
183 $meta_error = isset( $json['error']['message'] ) ? $json['error']['message'] : __( 'Unknown error from Meta API.', 'wp-cafe' );
184 return rest_ensure_response( [
185 'success' => 0,
186 'status_code' => $code,
187 'message' => $meta_error,
188 'data' => [ 'items' => [], 'raw' => $json ],
189 ] );
190 }
191
192 $items = isset( $json['data'] ) && is_array( $json['data'] ) ? $json['data'] : [];
193 $parsed = array_values( array_filter( array_map( [ $this, 'parse_template' ], $items ) ) );
194
195 set_transient( $cache_key, $parsed, self::CACHE_TTL );
196
197 return rest_ensure_response( [
198 'success' => 1,
199 'status_code' => 200,
200 'message' => __( 'Templates loaded.', 'wp-cafe' ),
201 'data' => [ 'items' => $parsed, 'cached' => false ],
202 ] );
203 }
204
205 /**
206 * Reduce a raw Meta template object to the shape the UI needs.
207 *
208 * Skips non-APPROVED templates so the flow builder cannot pick a template
209 * that would fail at send time.
210 *
211 * @since 1.0.0
212 *
213 * @param array $tpl Raw template object.
214 *
215 * @return array|null
216 */
217 protected function parse_template( $tpl ) {
218 if ( ! is_array( $tpl ) || empty( $tpl['name'] ) ) {
219 return null;
220 }
221
222 $status = isset( $tpl['status'] ) ? strtoupper( (string) $tpl['status'] ) : '';
223 if ( 'APPROVED' !== $status ) {
224 return null;
225 }
226
227 $components = isset( $tpl['components'] ) && is_array( $tpl['components'] ) ? $tpl['components'] : [];
228 $body = $this->find_component( $components, 'BODY' );
229 $header = $this->find_component( $components, 'HEADER' );
230 $footer = $this->find_component( $components, 'FOOTER' );
231 $body_text = isset( $body['text'] ) ? (string) $body['text'] : '';
232 $parameter_format = isset( $tpl['parameter_format'] ) ? strtoupper( (string) $tpl['parameter_format'] ) : 'POSITIONAL';
233
234 $body_params = $this->extract_body_params( $body_text, $body, $parameter_format );
235
236 return [
237 'name' => (string) $tpl['name'],
238 'language' => isset( $tpl['language'] ) ? (string) $tpl['language'] : 'en_US',
239 'status' => $status,
240 'category' => isset( $tpl['category'] ) ? (string) $tpl['category'] : '',
241 'parameter_format' => $parameter_format,
242 'body_text' => $body_text,
243 'body_params' => $body_params,
244 'param_count' => count( $body_params ),
245 'header_text' => isset( $header['text'] ) ? (string) $header['text'] : '',
246 'footer_text' => isset( $footer['text'] ) ? (string) $footer['text'] : '',
247 ];
248 }
249
250 /**
251 * Find a component by type (BODY, HEADER, FOOTER, BUTTONS).
252 *
253 * @since 1.0.0
254 *
255 * @param array $components List of component arrays.
256 * @param string $type Component type to find.
257 *
258 * @return array|null
259 */
260 protected function find_component( $components, $type ) {
261 foreach ( $components as $component ) {
262 if ( isset( $component['type'] ) && strtoupper( (string) $component['type'] ) === $type ) {
263 return $component;
264 }
265 }
266 return null;
267 }
268
269 /**
270 * Extract the ordered list of body parameters from the body component.
271 *
272 * Returns an array of `[ 'key' => string, 'example' => string ]` entries.
273 * For POSITIONAL templates `key` is the numeric placeholder (1, 2, 3); for
274 * NAMED templates it's the parameter name.
275 *
276 * @since 1.0.0
277 *
278 * @param string $body_text Body text with `{{N}}` or `{{name}}` placeholders.
279 * @param array $body_component Raw body component (for example values).
280 * @param string $parameter_format `POSITIONAL` or `NAMED`.
281 *
282 * @return array
283 */
284 protected function extract_body_params( $body_text, $body_component, $parameter_format ) {
285 if ( '' === $body_text ) {
286 return [];
287 }
288
289 preg_match_all( '/{{\s*([a-zA-Z0-9_]+)\s*}}/', $body_text, $matches );
290 if ( empty( $matches[1] ) ) {
291 return [];
292 }
293
294 $keys = array_values( array_unique( $matches[1] ) );
295 $examples = [];
296
297 if ( 'NAMED' === $parameter_format ) {
298 $named = isset( $body_component['example']['body_text_named_params'] ) ? $body_component['example']['body_text_named_params'] : [];
299 if ( is_array( $named ) ) {
300 foreach ( $named as $pair ) {
301 if ( isset( $pair['param_name'] ) ) {
302 $examples[ (string) $pair['param_name'] ] = isset( $pair['example'] ) ? (string) $pair['example'] : '';
303 }
304 }
305 }
306 } else {
307 $positional = isset( $body_component['example']['body_text'][0] ) && is_array( $body_component['example']['body_text'][0] )
308 ? $body_component['example']['body_text'][0]
309 : [];
310 foreach ( $positional as $index => $value ) {
311 $examples[ (string) ( $index + 1 ) ] = (string) $value;
312 }
313 }
314
315 $result = [];
316 foreach ( $keys as $key ) {
317 $result[] = [
318 'key' => (string) $key,
319 'example' => isset( $examples[ $key ] ) ? (string) $examples[ $key ] : '',
320 ];
321 }
322
323 return $result;
324 }
325
326 /**
327 * Read credentials via the same filter MetaCloudProvider uses so a single
328 * source of truth maps plugin options into the SDK shape.
329 *
330 * @since 1.0.0
331 *
332 * @return array
333 */
334 protected function get_credentials() {
335 $stored = get_option( $this->identifier . '_ens_whatsapp_settings', [] );
336 if ( ! is_array( $stored ) ) {
337 $stored = [];
338 }
339 $defaults = [
340 'access_token' => '',
341 'phone_number_id' => '',
342 'business_id' => '',
343 ];
344 $creds = wp_parse_args( $stored, $defaults );
345
346 return apply_filters(
347 Helpers::get_hook_name( $this->identifier, 'ens_whatsapp_credentials' ),
348 $creds,
349 $this->identifier
350 );
351 }
352 }
353