Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions assets/images/bird.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions includes/Gateways.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ public function all() {
'vonage' => __NAMESPACE__ . '\Gateways\Vonage',
'clickatell' => __NAMESPACE__ . '\Gateways\Clickatell',
'plivo' => __NAMESPACE__ . '\Gateways\Plivo',
'bird' => __NAMESPACE__ . '\Gateways\Bird',
];

if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
Expand Down
188 changes: 188 additions & 0 deletions includes/Gateways/Bird.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?php

namespace Texty\Gateways;

use WP_Error;

/**
* Bird Class
*
* @see https://docs.bird.com/api/channels-api/send-message
*/
class Bird implements GatewayInterface {
/**
* API Base URL
*/
const API_BASE = 'https://api.bird.com';

/**
* Get the name
*
* @return string
*/
public function name() {
return __( 'Bird', 'texty' );
}

/**
* Get the description
*
* @return string
*/
public function description() {
return sprintf(
// translators: URL to Bird settings and help docs
__(
'Send SMS with Bird. Follow <a href="%1$s" target="_blank">this link</a> to get started with Bird. Follow <a href="%2$s" target="_blank">these instructions</a> to configure the gateway.',
'texty'
),
'https://docs.bird.com/api/quickstarts/send-an-sms-message',
'https://github.com/getdokan/texty/wiki/Bird'
);
Comment on lines +32 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add rel="noopener noreferrer" to external links opened in a new tab.

This prevents reverse‑tabnabbing when using target="_blank".

🔧 Suggested change
-                'Send SMS with Bird. Follow <a href="%1$s" target="_blank">this link</a> to get started with Bird. Follow <a href="%2$s" target="_blank">these instructions</a> to configure the gateway.',
+                'Send SMS with Bird. Follow <a href="%1$s" target="_blank" rel="noopener noreferrer">this link</a> to get started with Bird. Follow <a href="%2$s" target="_blank" rel="noopener noreferrer">these instructions</a> to configure the gateway.',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function description() {
return sprintf(
// translators: URL to Bird settings and help docs
__(
'Send SMS with Bird. Follow <a href="%1$s" target="_blank">this link</a> to get started with Bird. Follow <a href="%2$s" target="_blank">these instructions</a> to configure the gateway.',
'texty'
),
'https://docs.bird.com/api/quickstarts/send-an-sms-message',
'https://github.com/getdokan/texty/wiki/Bird'
);
public function description() {
return sprintf(
// translators: URL to Bird settings and help docs
__(
'Send SMS with Bird. Follow <a href="%1$s" target="_blank" rel="noopener noreferrer">this link</a> to get started with Bird. Follow <a href="%2$s" target="_blank" rel="noopener noreferrer">these instructions</a> to configure the gateway.',
'texty'
),
'https://docs.bird.com/api/quickstarts/send-an-sms-message',
'https://github.com/getdokan/texty/wiki/Bird'
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Gateways/Bird.php` around lines 32 - 41, The description() method
returns HTML links with target="_blank" which are vulnerable to
reverse-tabnabbing; update the sprintf/translation string in Bird::description()
so both anchor tags include rel="noopener noreferrer" alongside target="_blank"
(i.e., change '<a href="%1$s" target="_blank">' and '<a href="%2$s"
target="_blank">' to include rel="noopener noreferrer") to harden external links
opened in a new tab.

}

/**
* Get the logo
*
* @return string
*/
public function logo() {
return TEXTY_URL . '/assets/images/bird.svg';
}

/**
* Get the settings
*
* @return array
*/
public function get_settings() {
$creds = texty()->settings()->get( 'bird' );

return [
'access_key' => [
'name' => __( 'Access Key', 'texty' ),
'type' => 'text',
'value' => isset( $creds['access_key'] ) ? $creds['access_key'] : '',
'help' => '',
],
'workspace_id' => [
'name' => __( 'Workspace ID', 'texty' ),
'type' => 'text',
'value' => isset( $creds['workspace_id'] ) ? $creds['workspace_id'] : '',
'help' => __( 'Found in your Bird dashboard workspace settings', 'texty' ),
],
'channel_id' => [
'name' => __( 'Channel ID', 'texty' ),
'type' => 'text',
'value' => isset( $creds['channel_id'] ) ? $creds['channel_id'] : '',
'help' => __( 'The SMS channel ID from your Bird workspace', 'texty' ),
],
];
}

/**
* Get the API endpoint for sending messages
*
* @param string $workspace_id
* @param string $channel_id
*
* @return string
*/
private function get_endpoint( $workspace_id, $channel_id ) {
return sprintf(
'%s/workspaces/%s/channels/%s/messages',
self::API_BASE,
$workspace_id,
$channel_id
);
}

/**
* Send SMS
*
* @param string $to
* @param string $message
*
* @return WP_Error|true
*/
public function send( $to, $message ) {
$creds = texty()->settings()->get( 'bird' );
$endpoint = $this->get_endpoint( $creds['workspace_id'], $creds['channel_id'] );

$args = [
'headers' => [
'Authorization' => 'AccessKey ' . $creds['access_key'],
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( [
'receiver' => [
'contacts' => [
[
'identifierValue' => '+' . ltrim( $to, '+' ),
],
],
],
'body' => [
'type' => 'text',
'text' => [
'text' => $message,
],
],
] ),
];

$response = wp_remote_post( $endpoint, $args );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );

if ( 202 !== $response_code ) {
$error_message = isset( $body->errors[0]->description ) ? $body->errors[0]->description : __( 'Unknown error', 'texty' );
$error_code = isset( $body->errors[0]->code ) ? $body->errors[0]->code : 'bird_error';

return new WP_Error( $error_code, $error_message, $body );
}

return true;
Comment on lines +108 to +145

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n includes/Gateways/Bird.php | sed -n '100,150p'

Repository: getdokan/texty

Length of output: 2067


🏁 Script executed:

find includes/Gateways -type f -name "*.php" | head -5

Repository: getdokan/texty

Length of output: 211


🏁 Script executed:

# Check how Clickatell handles this
rg -A 30 "public function send" includes/Gateways/Clickatell.php | head -50

Repository: getdokan/texty

Length of output: 1029


🏁 Script executed:

# Check Plivo to confirm the pattern
rg -A 25 "public function send" includes/Gateways/Plivo.php | head -40

Repository: getdokan/texty

Length of output: 944


🏁 Script executed:

# Quick check: does settings()->get('bird') have any default handling?
rg -B5 -A10 "bird" includes/Config.php | head -40

Repository: getdokan/texty

Length of output: 117


🏁 Script executed:

# Check if there are any other send() implementations to understand the pattern better
rg -B2 "is_wp_error" includes/Gateways/ | head -20

Repository: getdokan/texty

Length of output: 652


Add is_wp_error() guard and validate credentials before use.

The code doesn't check if wp_remote_post() returns a WP_Error before calling wp_remote_retrieve_response_code() and wp_remote_retrieve_body(), which will fail when network errors occur. Additionally, direct array access to $creds without validation will trigger PHP notices if any required keys are missing. Other gateway implementations (Clickatell, Twilio, Vonage) all include this guard.

Suggested fix
 public function send( $to, $message ) {
     $creds    = texty()->settings()->get( 'bird' );
+    if ( empty( $creds['access_key'] ) || empty( $creds['workspace_id'] ) || empty( $creds['channel_id'] ) ) {
+        return new WP_Error( 'bird_missing_credentials', __( 'Bird credentials are not configured.', 'texty' ) );
+    }
+
     $endpoint = $this->get_endpoint( $creds['workspace_id'], $creds['channel_id'] );

     $args = [
         'headers' => [
             'Authorization' => 'AccessKey ' . $creds['access_key'],
             'Content-Type'  => 'application/json',
         ],
         'body'    => wp_json_encode( [
             'receiver' => [
                 'contacts' => [
                     [
                         'identifierValue' => '+' . ltrim( $to, '+' ),
                     ],
                 ],
             ],
             'body'     => [
                 'type' => 'text',
                 'text' => [
                     'text' => $message,
                 ],
             ],
         ] ),
     ];

     $response      = wp_remote_post( $endpoint, $args );
+    if ( is_wp_error( $response ) ) {
+        return $response;
+    }
     $response_code = wp_remote_retrieve_response_code( $response );
     $body          = json_decode( wp_remote_retrieve_body( $response ) );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function send( $to, $message ) {
$creds = texty()->settings()->get( 'bird' );
$endpoint = $this->get_endpoint( $creds['workspace_id'], $creds['channel_id'] );
$args = [
'headers' => [
'Authorization' => 'AccessKey ' . $creds['access_key'],
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( [
'receiver' => [
'contacts' => [
[
'identifierValue' => '+' . ltrim( $to, '+' ),
],
],
],
'body' => [
'type' => 'text',
'text' => [
'text' => $message,
],
],
] ),
];
$response = wp_remote_post( $endpoint, $args );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( 202 !== $response_code ) {
$error_message = isset( $body->errors[0]->description ) ? $body->errors[0]->description : __( 'Unknown error', 'texty' );
$error_code = isset( $body->errors[0]->code ) ? $body->errors[0]->code : 'bird_error';
return new WP_Error( $error_code, $error_message, $body );
}
return true;
public function send( $to, $message ) {
$creds = texty()->settings()->get( 'bird' );
if ( empty( $creds['access_key'] ) || empty( $creds['workspace_id'] ) || empty( $creds['channel_id'] ) ) {
return new WP_Error( 'bird_missing_credentials', __( 'Bird credentials are not configured.', 'texty' ) );
}
$endpoint = $this->get_endpoint( $creds['workspace_id'], $creds['channel_id'] );
$args = [
'headers' => [
'Authorization' => 'AccessKey ' . $creds['access_key'],
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( [
'receiver' => [
'contacts' => [
[
'identifierValue' => '+' . ltrim( $to, '+' ),
],
],
],
'body' => [
'type' => 'text',
'text' => [
'text' => $message,
],
],
] ),
];
$response = wp_remote_post( $endpoint, $args );
if ( is_wp_error( $response ) ) {
return $response;
}
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( 202 !== $response_code ) {
$error_message = isset( $body->errors[0]->description ) ? $body->errors[0]->description : __( 'Unknown error', 'texty' );
$error_code = isset( $body->errors[0]->code ) ? $body->errors[0]->code : 'bird_error';
return new WP_Error( $error_code, $error_message, $body );
}
return true;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Gateways/Bird.php` around lines 108 - 145, In Bird::send(), add
validation for the $creds array (ensure 'workspace_id', 'channel_id', and
'access_key' exist) and return a WP_Error if any are missing before calling
get_endpoint or building $args; after calling wp_remote_post($endpoint, $args)
guard the response with is_wp_error() and return that WP_Error (or wrap it)
instead of passing it to
wp_remote_retrieve_response_code()/wp_remote_retrieve_body(); keep the existing
error-parsing logic for non-202 responses but only run it when $response is not
a WP_Error.

}

/**
* Validate a REST API request
*
* @param WP_REST_Request $request
*
* @return WP_Error|array
*/
public function validate( $request ) {
$creds = $request->get_param( 'bird' );

$endpoint = sprintf(
'%s/workspaces/%s',
self::API_BASE,
$creds['workspace_id']
);

$args = [
'headers' => [
'Authorization' => 'AccessKey ' . $creds['access_key'],
'Content-Type' => 'application/json',
],
];

$response = wp_remote_get( $endpoint, $args );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );

if ( 200 !== $response_code ) {
$error_message = isset( $body->errors[0]->description ) ? $body->errors[0]->description : __( 'Invalid credentials', 'texty' );
$error_code = isset( $body->errors[0]->code ) ? $body->errors[0]->code : 'bird_error';

return new WP_Error( $error_code, $error_message, $body );
}

return [
'access_key' => $creds['access_key'],
'workspace_id' => $creds['workspace_id'],
'channel_id' => $creds['channel_id'],
];
Comment on lines +155 to +186

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's view the Bird.php file to see the full context
cat -n includes/Gateways/Bird.php | sed -n '140,200p'

Repository: getdokan/texty

Length of output: 1944


🏁 Script executed:

# Check if there are other gateway implementations to understand patterns
fd -e php includes/Gateways/ | head -10

Repository: getdokan/texty

Length of output: 435


🏁 Script executed:

# Check how other validate methods handle request params
rg -A 10 'function validate\(' includes/Gateways/ -t php

Repository: getdokan/texty

Length of output: 3698


🏁 Script executed:

# Check REST API route registration for the validate endpoint
rg -B 5 -A 15 'validate.*endpoint\|register.*rest' includes/Gateways/ -t php

Repository: getdokan/texty

Length of output: 40


🏁 Script executed:

# Check the main plugin file or router for param validation
fd . includes -type f -name "*.php" | xargs rg -l 'register_rest_route\|add_rest_route' | head -5

Repository: getdokan/texty

Length of output: 228


🏁 Script executed:

# Look at where the validate() method is called
rg -B 3 -A 3 '->validate\(' --type php

Repository: getdokan/texty

Length of output: 462


🏁 Script executed:

# Check if there's any validation wrapper around the validate method
rg -B 10 'function validate' includes/Gateways/GatewayInterface.php -A 5

Repository: getdokan/texty

Length of output: 279


🏁 Script executed:

# Search for REST API route registration with param validation
rg -i 'register_rest_route\|rest_route' includes/ -A 20

Repository: getdokan/texty

Length of output: 40


🏁 Script executed:

# Check full Vonage validate implementation to understand the pattern
cat -n includes/Gateways/Vonage.php | sed -n '130,200p'

Repository: getdokan/texty

Length of output: 1104


🏁 Script executed:

# Check what happens in one of the simpler gateways (Clickatell)
cat -n includes/Gateways/Clickatell.php

Repository: getdokan/texty

Length of output: 4707


🏁 Script executed:

# Search for where validate() is invoked to understand the calling context
rg 'validate\(' includes/ --context 3

Repository: getdokan/texty

Length of output: 2926


🏁 Script executed:

# Check the full context in Settings.php where validate() is called
rg -B 10 -A 10 'validate\( \$request \)' includes/Api/Settings.php

Repository: getdokan/texty

Length of output: 732


🏁 Script executed:

# Check if there's param validation on the REST route before validate() is called
rg -B 20 'validate.*request' includes/Api/Settings.php | head -60

Repository: getdokan/texty

Length of output: 658


🏁 Script executed:

# Check what wp_remote_retrieve_response_code returns when wp_remote_get returns WP_Error
cat -n includes/Gateways/Bird.php | sed -n '1,50p'

Repository: getdokan/texty

Length of output: 1550


Guard against missing credentials and handle wp_remote_get errors.

The validate() method directly accesses credential array keys without checking if they exist, which will trigger PHP notices when parameters are missing. Additionally, wp_remote_get() can return a WP_Error object, but the code calls wp_remote_retrieve_response_code() and wp_remote_retrieve_body() on it without first checking with is_wp_error(). The send() method in Clickatell.php demonstrates the correct error handling pattern.

✅ Suggested fix
 public function validate( $request ) {
-    $creds = $request->get_param( 'bird' );
+    $creds = (array) $request->get_param( 'bird' );
+    if ( empty( $creds['access_key'] ) || empty( $creds['workspace_id'] ) || empty( $creds['channel_id'] ) ) {
+        return new WP_Error( 'bird_missing_credentials', __( 'Bird credentials are not configured.', 'texty' ) );
+    }

     $endpoint = sprintf(
         '%s/workspaces/%s',
         self::API_BASE,
         $creds['workspace_id']
     );

     $args = [
         'headers' => [
             'Authorization' => 'AccessKey ' . $creds['access_key'],
             'Content-Type'  => 'application/json',
         ],
     ];

     $response      = wp_remote_get( $endpoint, $args );
+    if ( is_wp_error( $response ) ) {
+        return $response;
+    }
     $response_code = wp_remote_retrieve_response_code( $response );
     $body          = json_decode( wp_remote_retrieve_body( $response ) );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function validate( $request ) {
$creds = $request->get_param( 'bird' );
$endpoint = sprintf(
'%s/workspaces/%s',
self::API_BASE,
$creds['workspace_id']
);
$args = [
'headers' => [
'Authorization' => 'AccessKey ' . $creds['access_key'],
'Content-Type' => 'application/json',
],
];
$response = wp_remote_get( $endpoint, $args );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( 200 !== $response_code ) {
$error_message = isset( $body->errors[0]->description ) ? $body->errors[0]->description : __( 'Invalid credentials', 'texty' );
$error_code = isset( $body->errors[0]->code ) ? $body->errors[0]->code : 'bird_error';
return new WP_Error( $error_code, $error_message, $body );
}
return [
'access_key' => $creds['access_key'],
'workspace_id' => $creds['workspace_id'],
'channel_id' => $creds['channel_id'],
];
public function validate( $request ) {
$creds = (array) $request->get_param( 'bird' );
if ( empty( $creds['access_key'] ) || empty( $creds['workspace_id'] ) || empty( $creds['channel_id'] ) ) {
return new WP_Error( 'bird_missing_credentials', __( 'Bird credentials are not configured.', 'texty' ) );
}
$endpoint = sprintf(
'%s/workspaces/%s',
self::API_BASE,
$creds['workspace_id']
);
$args = [
'headers' => [
'Authorization' => 'AccessKey ' . $creds['access_key'],
'Content-Type' => 'application/json',
],
];
$response = wp_remote_get( $endpoint, $args );
if ( is_wp_error( $response ) ) {
return $response;
}
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( 200 !== $response_code ) {
$error_message = isset( $body->errors[0]->description ) ? $body->errors[0]->description : __( 'Invalid credentials', 'texty' );
$error_code = isset( $body->errors[0]->code ) ? $body->errors[0]->code : 'bird_error';
return new WP_Error( $error_code, $error_message, $body );
}
return [
'access_key' => $creds['access_key'],
'workspace_id' => $creds['workspace_id'],
'channel_id' => $creds['channel_id'],
];
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Gateways/Bird.php` around lines 155 - 186, The validate() method
currently assumes creds keys exist and assumes wp_remote_get() succeeded; update
validate() to first verify required keys
('access_key','workspace_id','channel_id') exist in $creds and return a WP_Error
if any are missing, then call wp_remote_get() and check is_wp_error($response)
before using wp_remote_retrieve_response_code() / wp_remote_retrieve_body(); if
is_wp_error, return that WP_Error (or wrap it with a descriptive code/message),
and preserve the existing handling of non-200 responses by extracting
$body->errors safely (checking properties exist) and returning a WP_Error with
the error code/message — follow the error-checking pattern used in
Clickatell.php's send() for reference.

}
}