diff --git a/forms-bridge/addons/gsheets/hooks.php b/forms-bridge/addons/gsheets/hooks.php index 58f0b689..1bc263d5 100644 --- a/forms-bridge/addons/gsheets/hooks.php +++ b/forms-bridge/addons/gsheets/hooks.php @@ -87,8 +87,7 @@ function ( $defaults, $addon, $schema ) { 'name' => 'scope', 'label' => __( 'Scope', 'forms-bridge' ), 'type' => 'text', - 'value' => - 'https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/spreadsheets', + 'value' => 'https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/spreadsheets', 'required' => true, ), array( @@ -147,8 +146,7 @@ function ( $defaults, $addon, $schema ) { 'name' => '', 'schema' => 'Bearer', 'oauth_url' => 'https://accounts.google.com/o/oauth2/v2', - 'scope' => - 'https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/spreadsheets', + 'scope' => 'https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/spreadsheets', 'client_id' => '', 'client_secret' => '', 'access_token' => '', @@ -173,8 +171,7 @@ function ( $data, $template_id ) { return $data; } - $data['bridge']['endpoint'] = - '/v4/spreadsheets/' . $data['bridge']['endpoint']; + $data['bridge']['endpoint'] = '/v4/spreadsheets/' . $data['bridge']['endpoint']; return $data; }, 10, @@ -184,7 +181,7 @@ function ( $data, $template_id ) { add_filter( 'http_bridge_oauth_url', function ( $url, $verb ) { - if ( strpos( $url, 'accounts.google.com' ) === false ) { + if ( false === strpos( $url, 'accounts.google.com' ) ) { return $url; } diff --git a/forms-bridge/addons/slack/assets/logo.png b/forms-bridge/addons/slack/assets/logo.png new file mode 100644 index 00000000..03fb1f13 Binary files /dev/null and b/forms-bridge/addons/slack/assets/logo.png differ diff --git a/forms-bridge/addons/slack/class-slack-addon.php b/forms-bridge/addons/slack/class-slack-addon.php new file mode 100644 index 00000000..2eae6177 --- /dev/null +++ b/forms-bridge/addons/slack/class-slack-addon.php @@ -0,0 +1,93 @@ + '__slack-' . time(), + 'endpoint' => '/api/conversations.list', + 'method' => 'GET', + 'backend' => $backend, + ) + ); + + $response = $bridge->submit(); + + if ( is_wp_error( $response ) ) { + Logger::log( 'Slack backend ping error response', Logger::ERROR ); + Logger::log( $response, Logger::ERROR ); + return false; + } + + return true; + } + + /** + * Performs a GET request against the backend endpoint and retrive the response data. + * + * @param string $endpoint API endpoint. + * @param string $backend Backend name. + * + * @return array|WP_Error + */ + public function fetch( $endpoint, $backend ) { + $bridge = new Slack_Form_Bridge( + array( + 'name' => '__slack-' . time(), + 'endpoint' => $endpoint, + 'method' => 'GET', + 'backend' => $backend, + ), + 'zulip' + ); + + return $bridge->submit(); + } +} + +Slack_Addon::setup(); diff --git a/forms-bridge/addons/slack/class-slack-form-bridge.php b/forms-bridge/addons/slack/class-slack-form-bridge.php new file mode 100644 index 00000000..dc9fb18d --- /dev/null +++ b/forms-bridge/addons/slack/class-slack-form-bridge.php @@ -0,0 +1,153 @@ +backend(); + + foreach ( $attachments as $name => $path ) { + $info = pathinfo( $path ); + $filename = $info['basename']; + + $response = $backend->post( + '/api/files.getUploadURLExternal', + array( + 'length' => filesize( $path ), + 'filename' => $filename, + ), + array( + 'Content-Type' => 'application/x-www-form-urlencoded', + ) + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( isset( $response['data']['error'] ) ) { + return new WP_Error( 'slack_upload', __( 'Can not upload a file to Slack', 'forms-bridge' ), $response['data'] ); + } + + $file_id = $response['data']['file_id']; + $upload_url = $response['data']['upload_url']; + + $response = http_bridge_post( + $upload_url, + file_get_contents( $path ), + array( + 'Content-Type' => 'application/octet-stream', + ), + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $attachments[ $name ] = $file_id; + } + + $files = array(); + foreach ( $attachments as $name => $file_id ) { + $files[] = array( + 'id' => $file_id, + 'title' => $name, + ); + } + + $response = $backend->post( + '/api/files.completeUploadExternal', + array( + 'files' => wp_json_encode( $files ), + 'channel_id' => $payload['channel'] ?? $payload['channel_id'] ?? null, + ), + array( + 'Content-Type' => 'application/x-www-form-urlencoded', + ), + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( isset( $response['data']['error'] ) ) { + return new WP_Error( 'slack_upload', __( 'Can not upload a file to Slack', 'forms-bridge' ), $response['data'] ); + } + + $annex = "\n\n----\n" . esc_html( __( 'Attachments', 'forms-bridge' ) ) . ":\n"; + + foreach ( $response['data']['files'] as $upload ) { + $annex .= "* [{$upload['name']}]({$upload['permalink']})\n"; + } + + if ( isset( $payload['markdown_text'] ) ) { + $payload['markdown_text'] .= $annex; + } else { + $payload['text'] .= $annex; + } + } + + add_filter( + 'http_bridge_request', + static function ( $request ) { + self::$request = $request; + return $request; + }, + 10, + 1 + ); + + $response = parent::submit( $payload, array() ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( isset( $response['data']['error'] ) ) { + return new WP_Error( + 'slack_rpc_error', + 'Slack bridge response error', + array( + 'request' => self::$request, + 'response' => $response, + ) + ); + } + + return $response; + } +} diff --git a/forms-bridge/addons/slack/hooks.php b/forms-bridge/addons/slack/hooks.php new file mode 100644 index 00000000..0cc8834f --- /dev/null +++ b/forms-bridge/addons/slack/hooks.php @@ -0,0 +1,148 @@ + array( + array( + 'ref' => '#credential', + 'name' => 'name', + 'label' => __( 'Name', 'forms-bridge' ), + 'type' => 'text', + 'required' => true, + ), + array( + 'ref' => '#credential', + 'name' => 'schema', + 'type' => 'text', + 'value' => 'Bearer', + ), + array( + 'ref' => '#credential', + 'name' => 'oauth_url', + 'label' => __( 'Authorization URL', 'forms-bridge' ), + 'type' => 'text', + 'value' => 'https://slack.com/oauth/v2', + ), + array( + 'ref' => '#credential', + 'name' => 'client_id', + 'label' => __( 'Client ID', 'forms-bridge' ), + 'type' => 'text', + 'required' => true, + ), + array( + 'ref' => '#credential', + 'name' => 'client_secret', + 'label' => __( 'Client secret', 'forms-bridge' ), + 'type' => 'text', + 'required' => true, + ), + array( + 'ref' => '#credential', + 'name' => 'scope', + 'label' => __( 'Scope', 'forms-bridge' ), + 'type' => 'text', + 'value' => 'chat:write,channels:read,users:read,files:write', + 'required' => true, + ), + array( + 'ref' => '#backend', + 'name' => 'name', + 'description' => __( + 'Label of the Slack API backend connection', + 'forms-bridge' + ), + 'default' => 'Slack API', + ), + array( + 'ref' => '#backend', + 'name' => 'base_url', + 'type' => 'url', + 'value' => 'https://slack.com', + ), + array( + 'ref' => '#bridge', + 'name' => 'method', + 'value' => 'POST', + ), + ), + 'credential' => array( + 'name' => '', + 'schema' => 'Bearer', + 'oauth_url' => 'https://slack.com/oauth/v2', + 'scope' => 'chat:write,channels:read,users:read', + 'client_id' => '', + 'client_secret' => '', + 'access_token' => '', + 'expires_at' => 0, + 'refresh_token' => '', + ), + 'backend' => array( + 'base_url' => 'https://slack.com', + 'headers' => array( + array( + 'name' => 'Accept', + 'value' => 'application/json', + ), + ), + ), + ), + $defaults, + $schema, + ); + }, + 10, + 3 +); + +add_filter( + 'http_bridge_oauth_update_tokens', + function ( $tokens, $credential ) { + if ( false !== strstr( $credential->oauth_url, 'slack.com' ) ) { + $tokens['expires_at'] = time() + 60 * 60 * 24 * 365 * 10; + $tokens['refresh_token'] = $tokens['access_token']; + $tokens['refresh_token_expires_at'] = time() + 60 * 60 * 24 * 365 * 10; + } + + return $tokens; + }, + 10, + 2 +); + +add_filter( + 'http_bridge_oauth_url', + function ( $url, $verb ) { + if ( false === strstr( $url, 'slack.com' ) ) { + return $url; + } + + if ( 'auth' === $verb ) { + return $url .= 'orize'; + } + + if ( 'token/revoke' === $verb ) { + return 'https://slack.com/api/auth.revoke'; + } + + return 'https://slack.com/api/oauth.v2.access'; + }, + 10, + 2 +); diff --git a/forms-bridge/addons/slack/templates/contact.php b/forms-bridge/addons/slack/templates/contact.php new file mode 100644 index 00000000..594e18f3 --- /dev/null +++ b/forms-bridge/addons/slack/templates/contact.php @@ -0,0 +1,96 @@ + __( 'Contacts Channel', 'forms-bridge' ), + 'description' => __( + 'Contact form template. The resulting bridge will notify form submissions in a Slack channel', + 'forms-bridge' + ), + 'fields' => array( + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => '/api/chat.postMessage', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'channel', + 'label' => __( 'Channel', 'forms-bridge' ), + 'description' => __( + 'Name of the channel where notifications will be sent', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + 'endpoint' => '/api/conversations.list', + 'finger' => array( + 'value' => 'channels[].id', + 'label' => 'channels[].name', + ), + ), + ), + array( + 'ref' => '#form', + 'name' => 'title', + 'default' => __( 'Contacts', 'forms-bridge' ), + ), + ), + 'form' => array( + 'title' => __( 'Contacts', 'forms-bridge' ), + 'fields' => array( + array( + 'name' => 'your-name', + 'label' => __( 'Your name', 'forms-bridge' ), + 'type' => 'text', + 'required' => true, + ), + array( + 'name' => 'your-email', + 'label' => __( 'Your email', 'forms-bridge' ), + 'type' => 'email', + 'required' => true, + ), + array( + 'name' => 'comments', + 'label' => __( 'Comments', 'forms-bridge' ), + 'type' => 'textarea', + ), + ), + ), + 'bridge' => array( + 'endpoint' => '/api/chat.postMessage', + 'mutations' => array( + array( + array( + 'from' => 'your-name', + 'to' => 'text.name', + 'cast' => 'string', + ), + array( + 'from' => 'your-email', + 'to' => 'text.email', + 'cast' => 'string', + ), + array( + 'from' => '?comments', + 'to' => 'text.comments', + 'cast' => 'string', + ), + array( + 'from' => 'text', + 'to' => 'text', + 'cast' => 'pretty_json', + ), + ), + ), + ), +); diff --git a/forms-bridge/addons/slack/templates/direct-message.php b/forms-bridge/addons/slack/templates/direct-message.php new file mode 100644 index 00000000..b3727c07 --- /dev/null +++ b/forms-bridge/addons/slack/templates/direct-message.php @@ -0,0 +1,92 @@ + __( 'Direct Messages', 'forms-bridge' ), + 'description' => __( + 'Contact form template. The resulting bridge will send form submissions as direct messages on Slack', + 'forms-bridge' + ), + 'fields' => array( + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => '/api/chat.postMessage', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'channel', + 'label' => __( 'User', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( + 'endpoint' => '/api/users.list', + 'finger' => array( + 'value' => 'members[].id', + 'label' => 'members[].name', + ), + ), + ), + array( + 'ref' => '#form', + 'name' => 'title', + 'default' => __( 'Direct Messages', 'forms-bridge' ), + ), + ), + 'form' => array( + 'title' => __( 'Direct Messages', 'forms-bridge' ), + 'fields' => array( + array( + 'name' => 'your-name', + 'label' => __( 'Your name', 'forms-bridge' ), + 'type' => 'text', + 'required' => true, + ), + array( + 'name' => 'your-email', + 'label' => __( 'Your email', 'forms-bridge' ), + 'type' => 'email', + 'required' => true, + ), + array( + 'name' => 'comments', + 'label' => __( 'Comments', 'forms-bridge' ), + 'type' => 'textarea', + ), + ), + ), + 'bridge' => array( + 'endpoint' => '/api/chat.postMessage', + 'mutations' => array( + array( + array( + 'from' => 'your-name', + 'to' => 'text.name', + 'cast' => 'string', + ), + array( + 'from' => 'your-email', + 'to' => 'text.email', + 'cast' => 'string', + ), + array( + 'from' => '?comments', + 'to' => 'text.comments', + 'cast' => 'string', + ), + array( + 'from' => 'text', + 'to' => 'text', + 'cast' => 'pretty_json', + ), + ), + ), + ), +); diff --git a/forms-bridge/addons/slack/templates/support.php b/forms-bridge/addons/slack/templates/support.php new file mode 100644 index 00000000..d48d7db6 --- /dev/null +++ b/forms-bridge/addons/slack/templates/support.php @@ -0,0 +1,96 @@ + __( 'Support Channel', 'forms-bridge' ), + 'description' => __( + 'Support form template. The resulting bridge will notify form submissions in a Slack channel', + 'forms-bridge' + ), + 'fields' => array( + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => '/api/chat.postMessage', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'channel', + 'label' => __( 'Channel', 'forms-bridge' ), + 'description' => __( + 'Name of the channel where notifications will be sent', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + 'endpoint' => '/api/conversations.list', + 'finger' => array( + 'value' => 'channels[].id', + 'label' => 'channels[].name', + ), + ), + ), + array( + 'ref' => '#form', + 'name' => 'title', + 'default' => __( 'Support', 'forms-bridge' ), + ), + ), + 'form' => array( + 'title' => __( 'Support', 'forms-bridge' ), + 'fields' => array( + array( + 'name' => 'your-name', + 'label' => __( 'Your name', 'forms-bridge' ), + 'type' => 'text', + 'required' => true, + ), + array( + 'name' => 'your-email', + 'label' => __( 'Your email', 'forms-bridge' ), + 'type' => 'email', + 'required' => true, + ), + array( + 'name' => 'comments', + 'label' => __( 'Comments', 'forms-bridge' ), + 'type' => 'textarea', + ), + ), + ), + 'bridge' => array( + 'endpoint' => '/api/chat.postMessage', + 'mutations' => array( + array( + array( + 'from' => 'your-name', + 'to' => 'text.name', + 'cast' => 'string', + ), + array( + 'from' => 'your-email', + 'to' => 'text.email', + 'cast' => 'string', + ), + array( + 'from' => '?comments', + 'to' => 'text.comments', + 'cast' => 'string', + ), + array( + 'from' => 'text', + 'to' => 'text', + 'cast' => 'pretty_json', + ), + ), + ), + ), +); diff --git a/forms-bridge/addons/zulip/class-zulip-addon.php b/forms-bridge/addons/zulip/class-zulip-addon.php index b9a3434c..92048ad9 100644 --- a/forms-bridge/addons/zulip/class-zulip-addon.php +++ b/forms-bridge/addons/zulip/class-zulip-addon.php @@ -20,7 +20,6 @@ * Zulip addon class */ class Zulip_Addon extends Addon { - /** * Holds the addon's title. * diff --git a/forms-bridge/addons/zulip/class-zulip-form-bridge.php b/forms-bridge/addons/zulip/class-zulip-form-bridge.php index 0e0a66b9..c437ec51 100644 --- a/forms-bridge/addons/zulip/class-zulip-form-bridge.php +++ b/forms-bridge/addons/zulip/class-zulip-form-bridge.php @@ -18,7 +18,6 @@ * Form bridge implamentation for the Zulip API. */ class Zulip_Form_Bridge extends Form_Bridge { - /** * Submits payload and attachments to the bridge's backend. * diff --git a/forms-bridge/deps/http b/forms-bridge/deps/http index 418b5932..803b7a4d 160000 --- a/forms-bridge/deps/http +++ b/forms-bridge/deps/http @@ -1 +1 @@ -Subproject commit 418b593220c9babdeeed18c7f78a57d9fadd3bfd +Subproject commit 803b7a4de09d1a382792a5c1b857a4a3974186bb diff --git a/src/components/Credential/AuthorizeButton.jsx b/src/components/Credential/AuthorizeButton.jsx index d949cb2c..554dcddf 100644 --- a/src/components/Credential/AuthorizeButton.jsx +++ b/src/components/Credential/AuthorizeButton.jsx @@ -47,11 +47,11 @@ export default function AuthorizeButton({ addon, data }) { method: "POST", data: { credential: data }, }) - .then(({ success }) => { + .then(({ success, redirect_url }) => { if (!success) throw "error"; const form = document.createElement("form"); - form.action = data.oauth_url + "/auth"; + form.action = redirect_url; form.method = "GET"; form.target = "_blank"; diff --git a/src/components/Templates/Wizard/useAuthorizedCredential.js b/src/components/Templates/Wizard/useAuthorizedCredential.js index d288b043..541690cc 100644 --- a/src/components/Templates/Wizard/useAuthorizedCredential.js +++ b/src/components/Templates/Wizard/useAuthorizedCredential.js @@ -107,12 +107,12 @@ export default function useAuthorizedCredential({ data = {}, fields = [] }) { method: "POST", data: { credential }, }) - .then(({ success }) => { + .then(({ success, redirect_url }) => { if (!success) throw "error"; const form = document.createElement("form"); form.method = "GET"; - form.action = credential.oauth_url + "/auth"; + form.action = redirect_url; form.target = "_blank"; let innerHTML = `