diff --git a/forms-bridge/addons/listmonk/class-listmonk-addon.php b/forms-bridge/addons/listmonk/class-listmonk-addon.php index c595c22e..6ad8a55d 100644 --- a/forms-bridge/addons/listmonk/class-listmonk-addon.php +++ b/forms-bridge/addons/listmonk/class-listmonk-addon.php @@ -159,43 +159,43 @@ public function get_endpoint_schema( $endpoint, $backend, $method = null ) { } } - if ( '/api/subscribers' === $endpoint ) { - return array( - array( - 'name' => 'email', - 'schema' => array( 'type' => 'string' ), - 'required' => true, - ), - array( - 'name' => 'name', - 'schema' => array( 'type' => 'string' ), - ), - array( - 'name' => 'status', - 'schema' => array( 'type' => 'string' ), - ), - array( - 'name' => 'lists', - 'schema' => array( - 'type' => 'array', - 'items' => array( 'type' => 'number' ), - ), - ), - array( - 'name' => 'preconfirm_subscriptions', - 'schema' => array( 'type' => 'boolean' ), - ), - array( - 'name' => 'attribs', - 'schema' => array( - 'type' => 'object', - 'properties' => array(), - ), - ), - ); + if ( '/api/subscribers' !== $endpoint ) { + return array(); } - return array(); + return array( + array( + 'name' => 'email', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + array( + 'name' => 'name', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'status', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'lists', + 'schema' => array( + 'type' => 'array', + 'items' => array( 'type' => 'number' ), + ), + ), + array( + 'name' => 'preconfirm_subscriptions', + 'schema' => array( 'type' => 'boolean' ), + ), + array( + 'name' => 'attribs', + 'schema' => array( + 'type' => 'object', + 'properties' => array(), + ), + ), + ); } } diff --git a/forms-bridge/addons/nextcloud/class-nextcloud-addon.php b/forms-bridge/addons/nextcloud/class-nextcloud-addon.php index 06ec01bb..e30d4588 100644 --- a/forms-bridge/addons/nextcloud/class-nextcloud-addon.php +++ b/forms-bridge/addons/nextcloud/class-nextcloud-addon.php @@ -62,6 +62,7 @@ static function ( $prune, $bridge ) { 2 ); } + /** * Performs a request against the backend to check the connexion status. * diff --git a/forms-bridge/addons/zulip/assets/logo.png b/forms-bridge/addons/zulip/assets/logo.png new file mode 100644 index 00000000..f18e04b8 Binary files /dev/null and b/forms-bridge/addons/zulip/assets/logo.png differ diff --git a/forms-bridge/addons/zulip/class-zulip-addon.php b/forms-bridge/addons/zulip/class-zulip-addon.php new file mode 100644 index 00000000..b9a3434c --- /dev/null +++ b/forms-bridge/addons/zulip/class-zulip-addon.php @@ -0,0 +1,187 @@ + '__zulip-' . time(), + 'endpoint' => '/api/v1/streams', + 'method' => 'GET', + 'backend' => $backend, + ), + 'zulip' + ); + + $response = $bridge->submit(); + + if ( is_wp_error( $response ) ) { + Logger::log( 'Zulip 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 Zulip_Form_Bridge( + array( + 'name' => '__zulip-' . time(), + 'endpoint' => $endpoint, + 'method' => 'GET', + 'backend' => $backend, + ), + 'zulip' + ); + + return $bridge->submit(); + } + + /** + * Performs an introspection of the backend endpoint and returns API fields. + * + * @param string $endpoint API endpoint. + * @param string $backend Backend name. + * @param string|null $method HTTP method. + * + * @return array List of fields and content type of the endpoint. + */ + public function get_endpoint_schema( $endpoint, $backend, $method = null ) { + if ( function_exists( 'yaml_parse' ) ) { + $response = wp_remote_get( self::OAS_URL ); + + if ( ! is_wp_error( $response ) ) { + $data = yaml_parse( $response['body'] ); + + if ( $data ) { + try { + $oa_explorer = new OpenAPI( $data ); + + $method = strtolower( $method ?? 'post' ); + $path = preg_replace( '', '', $endpoint ); + $source = in_array( $method, array( 'post', 'put', 'patch' ), true ) ? 'body' : 'query'; + $params = $oa_explorer->params( $path, $method, $source ); + + return $params ?: array(); + } catch ( Exception ) { + // do nothing. + } + } + } + } + + if ( '/api/v1/messages' !== $endpoint ) { + return array(); + } + + return array( + array( + 'name' => 'type', + 'schema' => array( + 'type' => 'string', + 'enum' => array( 'direct', 'stream' ), + ), + 'required' => true, + ), + array( + 'name' => 'to', + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => array( 'string', 'integer' ), + ), + ), + 'required' => true, + ), + array( + 'name' => 'content', + 'schema' => array( 'type' => 'string' ), + 'required' => true, + ), + array( + 'name' => 'topic', + 'schema' => array( + 'type' => 'string', + 'default' => '(no topic)', + ), + ), + array( + 'name' => 'queue_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'local_id', + 'schema' => array( 'type' => 'string' ), + ), + array( + 'name' => 'read_by_sender', + 'schema' => array( 'type' => 'boolean' ), + ), + ); + } +} + +Zulip_Addon::setup(); diff --git a/forms-bridge/addons/zulip/class-zulip-form-bridge.php b/forms-bridge/addons/zulip/class-zulip-form-bridge.php new file mode 100644 index 00000000..0e0a66b9 --- /dev/null +++ b/forms-bridge/addons/zulip/class-zulip-form-bridge.php @@ -0,0 +1,69 @@ +backend()->clone( + array( + 'headers' => array( + array( + 'name' => 'Content-Type', + 'value' => 'multipart/form-data', + ), + ), + ) + ); + + $annex = "\n\n----\n" . esc_html( __( 'Attachments', 'forms-bridge' ) ) . ":\n"; + + $attachments = Forms_Bridge::attachments( $uploads ); + + foreach ( $attachments as $name => $path ) { + $response = $backend->post( '/api/v1/user_uploads', array(), array(), array( $name => $path ) ); + + if ( is_wp_error( $response ) ) { + return $response; + } elseif ( 'success' !== $response['data']['result'] ) { + return new WP_Error( 'zulip_upload', __( 'Can not upload a file to Zulip', 'forms-bridge' ), $response['data'] ); + } + + unset( $payload[ $name ] ); + unset( $payload[ $name . '_filename' ] ); + + $annex .= "* [{$name}]({$response['data']['url']})\n"; + } + + $payload['content'] .= $annex; + } + + return parent::submit( $payload, array() ); + } +} diff --git a/forms-bridge/addons/zulip/hooks.php b/forms-bridge/addons/zulip/hooks.php new file mode 100644 index 00000000..0c7408ce --- /dev/null +++ b/forms-bridge/addons/zulip/hooks.php @@ -0,0 +1,109 @@ + array( + array( + 'ref' => '#backend', + 'name' => 'name', + 'description' => __( + 'Label of the Zulip API backend connection', + 'forms-bridge' + ), + 'default' => 'Zulip API', + ), + array( + 'ref' => '#backend', + 'name' => 'base_url', + 'description' => __( + 'Base URL of your Zulip', + 'forms-bridge' + ), + 'type' => 'url', + 'default' => 'https://your-organization.zulipchat.com', + ), + array( + 'ref' => '#backend/headers[]', + 'name' => 'Content-Type', + 'value' => 'application/x-www-form-urlencoded', + ), + array( + 'ref' => '#credential', + 'name' => 'name', + 'label' => __( 'Name', 'forms-bridge' ), + 'type' => 'text', + 'required' => true, + ), + array( + 'ref' => '#credential', + 'name' => 'schema', + 'type' => 'text', + 'value' => 'Basic', + ), + array( + 'ref' => '#credential', + 'name' => 'client_id', + 'label' => __( 'User email', 'forms-bridge' ), + 'type' => 'text', + ), + array( + 'ref' => '#credential', + 'name' => 'client_secret', + 'label' => __( 'API key', 'forms-bridge' ), + 'description' => __( + 'You can get it from the "Account & privacy" section of your profile menu', + 'forms-bridge' + ), + 'type' => 'text', + 'required' => true, + ), + array( + 'ref' => '#bridge', + 'name' => 'method', + 'value' => 'POST', + ), + ), + 'bridge' => array( + 'backend' => '', + 'endpoint' => '', + 'method' => 'POST', + ), + 'backend' => array( + 'name' => 'Zulip API', + 'headers' => array( + array( + 'name' => 'Content-Type', + 'value' => 'application/x-www-form-urlencoded', + ), + ), + ), + 'credential' => array( + 'name' => '', + 'schema' => 'Basic', + 'client_id' => '', + 'client_secret' => '', + ), + ), + $defaults, + $schema + ); + }, + 10, + 3 +); diff --git a/forms-bridge/addons/zulip/templates/contact.php b/forms-bridge/addons/zulip/templates/contact.php new file mode 100644 index 00000000..101ee6c6 --- /dev/null +++ b/forms-bridge/addons/zulip/templates/contact.php @@ -0,0 +1,121 @@ + __( 'Contacts Stream', 'forms-bridge' ), + 'description' => __( + 'Contact form template. The resulting bridge will notify form submissions in a Zulip stream', + 'forms-bridge' + ), + 'fields' => array( + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => '/api/v1/messages', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'to[0]', + 'label' => __( 'Stream', 'forms-bridge' ), + 'description' => __( + 'Name of the stream (channel) where notifications will be sent', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + 'endpoint' => '/api/v1/streams', + 'finger' => array( + 'value' => 'streams[].stream_id', + 'label' => 'streams[].name', + ), + ), + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'topic', + 'label' => __( 'Topic', 'forms-bridge' ), + 'description' => __( 'Topic under which the messages will be notified', 'forms-bridge' ), + 'type' => 'text', + 'default' => 'WordPress Contacts', + 'required' => true, + ), + 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/v1/messages', + 'custom_fields' => array( + array( + 'name' => 'type', + 'value' => 'stream', + ), + ), + 'mutations' => array( + array( + array( + 'from' => 'to[]', + 'to' => 'to[]', + 'cast' => 'integer', + ), + array( + 'from' => 'to', + 'to' => 'to', + 'cast' => 'json', + ), + array( + 'from' => 'your-name', + 'to' => 'content.name', + 'cast' => 'string', + ), + array( + 'from' => 'your-email', + 'to' => 'content.email', + 'cast' => 'string', + ), + array( + 'from' => '?comments', + 'to' => 'content.comments', + 'cast' => 'string', + ), + array( + 'from' => 'content', + 'to' => 'content', + 'cast' => 'pretty_json', + ), + ), + ), + ), +); diff --git a/forms-bridge/addons/zulip/templates/direct-message.php b/forms-bridge/addons/zulip/templates/direct-message.php new file mode 100644 index 00000000..1a176283 --- /dev/null +++ b/forms-bridge/addons/zulip/templates/direct-message.php @@ -0,0 +1,108 @@ + __( 'Direct Messages', 'forms-bridge' ), + 'description' => __( + 'Contact form template. The resulting bridge will send form submissions as direct messages on Zulip', + 'forms-bridge' + ), + 'fields' => array( + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => '/api/v1/messages', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'to[0]', + 'label' => __( 'User', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( + 'endpoint' => '/api/v1/users', + 'finger' => array( + 'value' => 'members[].user_id', + 'label' => 'members[].delivery_email', + ), + ), + ), + 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/v1/messages', + 'custom_fields' => array( + array( + 'name' => 'type', + 'value' => 'direct', + ), + ), + 'mutations' => array( + array( + array( + 'from' => 'to[]', + 'to' => 'to[]', + 'cast' => 'integer', + ), + array( + 'from' => 'to', + 'to' => 'to', + 'cast' => 'json', + ), + array( + 'from' => 'your-name', + 'to' => 'content.name', + 'cast' => 'string', + ), + array( + 'from' => 'your-email', + 'to' => 'content.email', + 'cast' => 'string', + ), + array( + 'from' => '?comments', + 'to' => 'content.comments', + 'cast' => 'string', + ), + array( + 'from' => 'content', + 'to' => 'content', + 'cast' => 'pretty_json', + ), + ), + ), + ), +); diff --git a/forms-bridge/addons/zulip/templates/support.php b/forms-bridge/addons/zulip/templates/support.php new file mode 100644 index 00000000..c4f21da1 --- /dev/null +++ b/forms-bridge/addons/zulip/templates/support.php @@ -0,0 +1,128 @@ + __( 'Support Stream', 'forms-bridge' ), + 'description' => __( + 'Support form template. The resulting bridge will notify form submissions in a Zulip stream', + 'forms-bridge' + ), + 'fields' => array( + array( + 'ref' => '#bridge', + 'name' => 'endpoint', + 'value' => '/api/v1/messages', + ), + array( + 'ref' => '#bridge/custom_fields[]', + 'name' => 'to[0]', + 'label' => __( 'Stream', 'forms-bridge' ), + 'description' => __( + 'Name of the stream (channel) where notifications will be sent', + 'forms-bridge' + ), + 'type' => 'select', + 'options' => array( + 'endpoint' => '/api/v1/streams', + 'finger' => array( + 'value' => 'streams[].stream_id', + 'label' => 'streams[].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' => 'topic', + 'label' => __( 'Topic', 'forms-bridge' ), + 'type' => 'select', + 'options' => array( + array( + 'value' => 'A', + 'label' => 'Option 1', + ), + array( + 'value' => 'B', + 'label' => 'Option 2', + ), + ), + 'required' => true, + ), + array( + 'name' => 'comments', + 'label' => __( 'Comments', 'forms-bridge' ), + 'type' => 'textarea', + ), + ), + ), + 'bridge' => array( + 'endpoint' => '/api/v1/messages', + 'custom_fields' => array( + array( + 'name' => 'type', + 'value' => 'stream', + ), + ), + 'mutations' => array( + array( + array( + 'from' => 'to[]', + 'to' => 'to[]', + 'cast' => 'integer', + ), + array( + 'from' => 'to', + 'to' => 'to', + 'cast' => 'json', + ), + array( + 'from' => 'your-name', + 'to' => 'content.name', + 'cast' => 'string', + ), + array( + 'from' => 'your-email', + 'to' => 'content.email', + 'cast' => 'string', + ), + array( + 'from' => '?comments', + 'to' => 'content.comments', + 'cast' => 'string', + ), + array( + 'from' => 'content', + 'to' => 'content', + 'cast' => 'pretty_json', + ), + ), + ), + ), +); diff --git a/forms-bridge/includes/class-form-bridge.php b/forms-bridge/includes/class-form-bridge.php index d410b0cc..4ebf9c95 100644 --- a/forms-bridge/includes/class-form-bridge.php +++ b/forms-bridge/includes/class-form-bridge.php @@ -163,6 +163,7 @@ public static function schema( $addon = null ) { 'or', 'xor', 'json', + 'pretty_json', 'csv', 'concat', 'join', @@ -558,11 +559,17 @@ private function cast( $value, $mapper ) { false ); case 'json': - if ( ! is_array( $value ) ) { - return ''; + if ( is_array( $value ) || is_object( $value ) ) { + return wp_json_encode( (array) $value, JSON_UNESCAPED_UNICODE ); + } + + return $value; + case 'pretty_json': + if ( is_array( $value ) || is_object( $value ) ) { + return wp_json_encode( (array) $value, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT ); } - return wp_json_encode( $value, JSON_UNESCAPED_UNICODE ); + return $value; case 'csv': if ( ! wp_is_numeric_array( $value ) ) { return ''; diff --git a/forms-bridge/includes/class-forms-bridge.php b/forms-bridge/includes/class-forms-bridge.php index b945d3cd..599b312a 100644 --- a/forms-bridge/includes/class-forms-bridge.php +++ b/forms-bridge/includes/class-forms-bridge.php @@ -312,12 +312,11 @@ function ( $field ) { true ) ) { - $attachments = self::stringify_attachments( - $attachments - ); + $attachments = self::stringify_attachments( $attachments ); foreach ( $attachments as $name => $value ) { $submission[ $name ] = $value; } + $attachments = array(); Logger::log( 'Submission after attachments stringify' ); Logger::log( $submission ); @@ -467,7 +466,7 @@ private static function prune_empties( $submission_data ) { * * @return array Map of uploaded files. */ - private static function attachments( $uploads ) { + public static function attachments( $uploads ) { $attachments = array(); foreach ( $uploads as $name => $upload ) { diff --git a/src/components/Mutations/Layers.jsx b/src/components/Mutations/Layers.jsx index ba378612..e0633ded 100644 --- a/src/components/Mutations/Layers.jsx +++ b/src/components/Mutations/Layers.jsx @@ -42,6 +42,10 @@ const castOptions = [ value: "json", label: "JSON", }, + { + value: "pretty_json", + label: "Pretty JSON", + }, { value: "csv", label: "CSV",