diff --git a/change_log.txt b/change_log.txt new file mode 100644 index 0000000..9100dff --- /dev/null +++ b/change_log.txt @@ -0,0 +1,30 @@ +## 2.0 | 2021-07-07 +- Updated authentication method for enhanced security. Important: Manual re-authentication in the add-on suggested. +- Fixed an issue where the Add-on icon is missing on the Form Settings page for Gravity Forms 2.5. +- Deprecated old authentication method. Authentication will continue to work until removed. + + +### 1.4 | 2020-09-09 +- Added support for Gravity Forms 2.5. + + +### 1.3 | 2020-05-05 +- Added security enhancements. +- Added translations for Hebrew, Hindi, Japanese, and Turkish. +- Updated Javascript files, stylesheets to use minified versions. +- Updated labels to be stored by ID instead of by color. +- Fixed error message for when a file cannot be attached to a card. +- Fixed issue of wrong variable being used in upgrade function. + + +### 1.2 +- Fixed fatal error which could occur if no boards are returned when initializing the API. + + +### 1.1 +- Added support for delaying feed processing until payment by PayPal Standard is successfully completed. +- Fixed an issue where a due date of Jan 1, 1970 was set on the card if the mapped date field was empty. + + +### 1.0 +- It's all new! diff --git a/class-gf-trello.php b/class-gf-trello.php new file mode 100644 index 0000000..2b507bf --- /dev/null +++ b/class-gf-trello.php @@ -0,0 +1,1694 @@ +add_delayed_payment_support( + array( + 'option_label' => esc_html__( 'Create Trello card only when payment is received.', 'gravityformstrello' ) + ) + ); + + } + + /** + * Initialize admin hooks. + * + * @since 2.0 + * + * @return void + */ + public function init_admin() { + parent::init_admin(); + add_action( 'admin_init', array( $this, 'maybe_update_auth_tokens' ) ); + add_action( 'admin_notices', array( $this, 'maybe_display_authentication_notice' ) ); + } + + /** + * Add AJAX callbacks. + * + * @since 2.0 + */ + public function init_ajax() { + parent::init_ajax(); + add_action( 'wp_ajax_gf_trello_deauthorize', array( $this, 'ajax_deauthorize' ) ); + } + + /** + * Revoke token and remove it from settings. + * + * @since 2.0 + */ + public function ajax_deauthorize() { + check_ajax_referer( 'gf_trello_deauth', 'nonce' ); + // If user is not authorized, exit. + if ( ! GFCommon::current_user_can_any( $this->_capabilities_settings_page ) ) { + wp_send_json_error( array( 'message' => esc_html__( 'Access denied.', 'gravityformstrello' ) ) ); + } + + add_filter( 'https_ssl_verify', '__return_false' ); + + $settings = $this->get_plugin_settings(); + $token = rgar( $settings, 'authToken' ); + if ( rgblank( $token ) ) { + wp_send_json_success(); + } + + $response = wp_remote_request( + 'https://api.trello.com/1/tokens/' . $token . '/?key=' . $this->get_trello_app_key() . '&token=' . $token, + array( + 'method' => 'DELETE', + 'headers' => array( + 'Accept' => 'application/json', + ), + ) + ); + + if ( is_wp_error( $response ) ) { + wp_send_json_error( $response->get_error_message() ); + } + + $response_body = json_decode( wp_remote_retrieve_body( $response ), 'ARRAY_A' ); + + if ( is_array( $response_body ) && array_key_exists( '_value', $response_body ) && $response_body['_value'] == null ) { + wp_send_json_success(); + } else { + wp_send_json_error( array( 'message' => esc_html__( 'Unable to revoke token at Trello.', 'gravityformstrello' ) ) ); + } + + wp_send_json_success(); + } + + /** + * Update auth tokens if required. + * + * @since 2.0 + */ + public function maybe_update_auth_tokens() { + + if ( rgget( 'subview' ) !== $this->get_slug() ) { + return; + } + + $payload = $this->get_oauth_payload(); + if ( ! $payload ) { + return; + } + $this->log_debug( __METHOD__ . '(): Payload received.' . var_export( $payload, true ) ); + + $auth_payload = $this->get_decoded_auth_payload( $payload ); + + // If state does not match, do not save. + if ( rgpost( 'state' ) && ! wp_verify_nonce( rgar( $payload, 'state' ), 'gf_trello_auth' ) ) { + GFCommon::add_error_message( esc_html__( 'Unable to connect to Trello due to mismatched state.', 'gravityformstrello' ) ); + return; + } + + $token = rgar( $auth_payload, 'access_token' ); + + if ( false === $this->save_access_token( $token ) || rgpost( 'auth_error' ) ) { + GFCommon::add_error_message( esc_html__( 'Unable to connect your Trello account.', 'gravityformstrello' ) ); + } + } + + /** + * Decodes the auth_payload returned form Gravity API. + * + * @since 2.0 + * + * @param array $payload + * + * @return array + */ + private function get_decoded_auth_payload( $payload ) { + $auth_payload_string = rgar( $payload, 'auth_payload' ); + return empty( $auth_payload_string ) ? array() : json_decode( $auth_payload_string, true ); + } + + /** + * Saves the access token to the plugin settings. + * + * @since 2.0 + * + * @param string $token The Access Token. + * + * @return bool + */ + private function save_access_token( $token ) { + if ( empty( $token ) || $this->is_save_postback() ) { + return false; + } + + $settings = $this->get_plugin_settings(); + $settings['authToken'] = $token; + $settings['reauth_version'] = self::LAST_REAUTHENTICATION_VERSION; // Set the API authentication version. + $this->update_plugin_settings( $settings ); + + return true; + } + + /** + * Get the authorization payload data. + * + * Returns the auth POST request if it's present, otherwise attempts to return a recent transient cache. + * + * @since 2.0 + * + * @return array + */ + private function get_oauth_payload() { + $payload = array_filter( + array( + 'auth_payload' => rgpost( 'auth_payload' ), + 'auth_error' => rgpost( 'auth_error' ), + 'state' => rgpost( 'state' ), + ) + ); + + if ( count( $payload ) === 2 || isset( $payload['auth_error'] ) ) { + return $payload; + } + + $payload = get_transient( "gravityapi_response_{$this->_slug}" ); + + if ( rgar( $payload, 'state' ) !== get_transient( "gravityapi_request_{$this->_slug}" ) ) { + return array(); + } + + delete_transient( "gravityapi_response_{$this->_slug}" ); + + return is_array( $payload ) ? $payload : array(); + } + + /** + * Enqueue admin scripts. + * + * @since 1.0 + * @access public + * + * @return array + */ + public function scripts() { + + $min = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG || isset( $_GET['gform_debug'] ) ? '' : '.min'; + + $scripts = array( + array( + 'handle' => 'trello_client', + 'deps' => array( 'jquery' ), + 'src' => '//api.trello.com/1/client.js?key=' . $this->get_trello_app_key(), + ), + array( + 'handle' => 'gform_trello_admin', + 'deps' => array( 'jquery', 'trello_client' ), + 'src' => $this->get_base_url() . "/js/admin{$min}.js", + 'version' => $this->_version, + 'enqueue' => array( + array( + 'admin_page' => array( 'plugin_settings' ) + ), + ), + 'strings' => array( + 'deauth_nonce' => wp_create_nonce( 'gf_trello_deauth' ), + 'ajaxurl' => admin_url( 'admin-ajax.php' ), + 'requires_reauth' => $this->requires_api_reauthentication(), + ), + ), + ); + + return array_merge( parent::scripts(), $scripts ); + + } + + /** + * Return the plugin's icon for the plugin/form settings menu. + * + * @since 1.3 + * + * @return string + */ + public function get_menu_icon() { + + return $this->is_gravityforms_supported( '2.5-beta-4' ) ? 'gform-icon--trello' : 'dashicons-admin-generic'; + + } + + + + + + // # PLUGIN SETTINGS ----------------------------------------------------------------------------------------------- + + /** + * Setup plugin settings fields. + * + * @since 1.0 + * @since 2.0 Added reauth_version field. + * + * @return array + */ + public function plugin_settings_fields() { + + return array( + array( + 'fields' => array( + array( + 'name' => 'reauth_version', + 'type' => 'hidden', + ), + array( + 'name' => 'authToken', + 'type' => 'hidden', + ), + array( + 'name' => '', + 'label' => esc_html__( 'Authorize with Trello', 'gravityformstrello' ), + 'type' => 'auth_token_button', + ), + ) + ), + ); + + } + + /** + * Hide submit button on plugin settings page. + * + * @since 1.3 + * + * @param string $html + * + * @return string + */ + public function filter_gform_settings_header_buttons( $html = '' ) { + + // If this is not the plugin settings page, return. + if ( ! $this->is_plugin_settings( $this->get_slug() ) ) { + return $html; + } + + // Hide button. + $html = str_replace( 'initialize_api() ) { + + $html = sprintf( + '%2$s', + $this->get_auth_url(), + esc_html__( 'Click here to authenticate your Trello account.', 'gravityformstrello' ) + ); + + } else { + + $html = esc_html__( 'Trello has been authenticated with your account.', 'gravityformstrello' ); + $html .= "  

"; + $html .= sprintf( + ' %1$s', + esc_html__( 'De-Authorize Trello', 'gravityformstrello' ) + ); + + } + + if ( $echo ) { + echo $html; + } + + return $html; + + } + + /** + * Get Gravity API URL. + * + * @since 2.0 + * + * @param string $path Path. + * + * @return string + */ + private function get_gravity_api_url( $path = '' ) { + return ( defined( 'GRAVITY_API_URL' ) ? GRAVITY_API_URL : 'https://gravityapi.com/wp-json/gravityapi/v1' ) . $path; + } + + /** + * Generates the URL of the first step of the OAuth flow. + * + * @since 2.0 + * + * @return string + */ + private function get_auth_url() { + + $nonce = wp_create_nonce( 'gf_trello_auth' ); + if ( get_transient( 'gravityapi_request_' . $this->get_slug() ) ) { + delete_transient( 'gravityapi_request_' . $this->get_slug() ); + } + + set_transient( 'gravityapi_request_' . $this->get_slug(), $nonce, 10 * MINUTE_IN_SECONDS ); + + return add_query_arg( + array( + 'redirect_to' => urlencode( admin_url( 'admin.php?page=gf_settings&subview=' . $this->_slug ) ), + 'state' => $nonce, + ), + $this->get_gravity_api_url( '/auth/trello' ) + ); + } + + + + + // # FEED SETTINGS ------------------------------------------------------------------------------------------------- + + /** + * Setup fields for feed settings. + * + * @since 1.0 + * @access public + * + * @uses GFAddOn::add_field_after() + * @uses GFFeedAddOn::get_default_feed_name() + * @uses GFTrello::get_boards_for_feed_setting() + * @uses GFTrello::get_date_fields_for_feed_setting() + * @uses GFTrello::get_labels_for_feed_setting() + * @uses GFTrello::get_lists_for_feed_setting() + * @uses GFTrello::get_members_for_feed_setting() + * @uses GFTrello::get_upload_fields_for_feed_setting() + * + * @return array + */ + public function feed_settings_fields() { + + // Build feed settings sections. + $sections = array( + array( + 'fields' => array( + array( + 'name' => 'feedName', + 'label' => esc_html__( 'Feed Name', 'gravityformstrello' ), + 'type' => 'text', + 'required' => true, + 'class' => 'medium', + 'default_value' => $this->get_default_feed_name(), + 'tooltip' => sprintf( + '
%s
%s', + esc_html__( 'Name', 'gravityformstrello' ), + esc_html__( 'Enter a feed name to uniquely identify this setup.', 'gravityformstrello' ) + ), + ), + array( + 'name' => 'board', + 'label' => esc_html__( 'Trello Board', 'gravityformstrello' ), + 'type' => 'select', + 'required' => true, + 'choices' => $this->get_boards_for_feed_setting(), + 'onchange' => "jQuery( 'select[name=\"_gaddon_setting_list\"]' ).val( '' ); jQuery( this ).parents( 'form' ).submit();", + 'no_choices' => esc_html__( 'You must have at least one Trello board in your account.', 'gravityformstrello' ), + ), + array( + 'name' => 'list', + 'label' => esc_html__( 'Trello List', 'gravityformstrello' ), + 'type' => 'select', + 'required' => true, + 'choices' => $this->get_lists_for_feed_setting(), + 'dependency' => 'board', + 'onchange' => "jQuery( this ).parents( 'form' ).submit();", + 'no_choices' => esc_html__( 'You must select a Trello board containing lists.', 'gravityformstrello' ), + ), + ), + ), + array( + 'title' => esc_html__( 'Card Settings', 'gravityformstrello' ), + 'dependency' => 'list', + 'fields' => array( + array( + 'name' => 'cardName', + 'type' => 'text', + 'required' => true, + 'class' => 'medium merge-tag-support mt-position-right mt-hide_all_fields', + 'label' => esc_html__( 'Name', 'gravityformstrello' ), + 'default_value' => 'New submission from {form_title}', + ), + array( + 'name' => 'cardDescription', + 'type' => 'textarea', + 'required' => false, + 'class' => 'medium merge-tag-support mt-position-right mt-hide_all_fields', + 'label' => esc_html__( 'Description', 'gravityformstrello' ), + ), + array( + 'name' => 'cardDueDate', + 'type' => 'select_custom', + 'label' => esc_html__( 'Due Date', 'gravityformstrello' ), + 'after_input' => ' ' . esc_html__( 'days after today', 'gravityformstrello' ), + 'choices' => $this->get_date_fields_for_feed_setting(), + 'input_type' => 'number' + ), + array( + 'name' => 'cardLabels', + 'type' => 'checkbox', + 'label' => __( 'Labels', 'gravityformstrello' ), + 'choices' => $this->get_labels_for_feed_setting() + ), + array( + 'name' => 'cardMembers', + 'type' => 'checkbox', + 'label' => esc_html__( 'Members', 'gravityformstrello' ), + 'choices' => $this->get_members_for_feed_setting() + ), + ), + ), + array( + 'title' => esc_html__( 'Feed Conditional Logic', 'gravityformstrello' ), + 'dependency' => 'list', + 'fields' => array( + array( + 'name' => 'feedCondition', + 'type' => 'feed_condition', + 'label' => esc_html__( 'Conditional Logic', 'gravityformstrello' ), + 'checkbox_label' => esc_html__( 'Enable', 'gravityformstrello' ), + 'instructions' => esc_html__( 'Export to Trello if', 'gravityformstrello' ), + 'tooltip' => sprintf( + '
%s
%s', + esc_html__( 'Conditional Logic', 'gravityformstrello' ), + esc_html__( 'When conditional logic is enabled, form submissions will only be exported to Trello when the condition is met. When disabled, all form submissions will be posted.', 'gravityformstrello' ) + ), + ), + ), + ), + ); + + // Get upload fields. + $upload_fields = $this->get_upload_fields_for_feed_setting(); + + // If upload fields were found, add settings field. + if ( ! empty( $upload_fields ) ) { + + // Prepare settings field. + $attachment_field = array( + 'name' => 'cardAttachments', + 'type' => 'checkbox', + 'label' => esc_html__( 'Attachments', 'gravityformstrello' ), + 'choices' => $upload_fields + ); + + // Add field. + $sections = $this->add_field_after( 'cardMembers', $attachment_field, $sections ); + + } + + return $sections; + + } + + /** + * Prepare Trello boards for feed setting. + * + * @since 1.0 + * @access public + * + * @uses GFAddOn::get_setting() + * @uses GFAddOn::log_debug() + * @uses GFAddOn::log_error() + * @uses GFTrello::initialize_error() + * + * @return array $choices + */ + public function get_boards_for_feed_setting() { + + // Initialize choices array. + $choices = array(); + + // If we are unable to initialize the API, return the choices array. + if ( ! $this->initialize_api() ) { + $this->log_error( __METHOD__ . '(): Unable to get boards because API is not initialized.' ); + return $choices; + } + + try { + + // Get the Trello boards. + $boards = $this->api->members->get( 'my/boards' ); + + // Log returned boards. + $this->log_debug( __METHOD__ . '(): Boards: ' . print_r( $boards, true ) ); + + } catch ( \Exception $e ) { + + // Log that we could not retreive the boards. + $this->log_error( __METHOD__ . '(): Unable to retrieve boards; ' . $e->getMessage() ); + + return $choices; + + } + + // If no boards were found, return. + if ( empty( $boards ) ) { + return $choices; + } + + // Add initial choice. + $choices[] = array( + 'label' => esc_html__( 'Select a Board', 'gravityformstrello' ), + 'value' => '', + ); + + // Loop through boards. + foreach ( $boards as $board ) { + + // Add board as choice. + $choices[] = array( + 'label' => esc_html( $board->name ), + 'value' => esc_attr( $board->id ), + ); + + } + + return $choices; + + } + + /** + * Prepare Trello lists for feed setting. + * + * @since 1.0 + * @access public + * + * @uses GFAddOn::get_setting() + * @uses GFAddOn::log_debug() + * @uses GFAddOn::log_error() + * @uses GFTrello::initialize_error() + * + * @return array $choices + */ + public function get_lists_for_feed_setting() { + + // Initialize choices array. + $choices = array(); + + // If we are unable to initialize the API, return the choices array. + if ( ! $this->initialize_api() ) { + $this->log_error( __METHOD__ . '(): Unable to get lists because API is not initialized.' ); + return $choices; + } + + // Get current board. + $board = $this->get_setting( 'board' ); + + try { + + // Get the Trello lists. + $lists = $this->api->boards->get( $board . '/lists' ); + + // Log returned lists. + $this->log_debug( __METHOD__ . '(): Lists for board #' . $board . ': ' . print_r( $lists, true ) ); + + } catch ( \Exception $e ) { + + // Log that we could not retreive the lists. + $this->log_error( __METHOD__ . '(): Unable to retrieve lists; ' . $e->getMessage() ); + + return $choices; + + } + + // If no lists were found, return. + if ( empty( $lists ) ) { + return $choices; + } + + // Add initial choice. + $choices[] = array( + 'label' => esc_html__( 'Select a List', 'gravityformstrello' ), + 'value' => '', + ); + + // Loop through lists. + foreach ( $lists as $list ) { + + // Add list as choice. + $choices[] = array( + 'label' => esc_html( $list->name ), + 'value' => esc_attr( $list->id ), + ); + + } + + return $choices; + + } + + /** + * Prepare Trello labels for feed setting. + * + * @since 1.0 + * @access public + * + * @uses GFAddOn::get_setting() + * @uses GFAddOn::log_debug() + * @uses GFAddOn::log_error() + * @uses GFTrello::initialize_error() + * + * @return array $choices + */ + public function get_labels_for_feed_setting() { + + // Initialize choices array. + $choices = array(); + + // If we are unable to initialize the API, return the choices array. + if ( ! $this->initialize_api() ) { + $this->log_error( __METHOD__ . '(): Unable to get labels because API is not initialized.' ); + return $choices; + } + + // Get current board. + $board = $this->get_setting( 'board' ); + + try { + + // Get the Trello labels. + $labels = $this->api->boards->get( $board . '/labels' ); + + // Log returned labels. + $this->log_debug( __METHOD__ . '(): Labels for board #' . $board . ': ' . print_r( $labels, true ) ); + + } catch ( \Exception $e ) { + + // Log that we could not retreive the labels. + $this->log_error( __METHOD__ . '(): Unable to retrieve labels; ' . $e->getMessage() ); + + return $choices; + + } + + // If no labels were found, return. + if ( empty( $labels ) ) { + return $choices; + } + + // Loop through labels. + foreach ( $labels as $label ) { + + // Add label as choice. + $choices[] = array( + 'label' => rgblank( $label->name ) ? ucwords( $label->color ) : $label->name, + 'name' => 'cardLabels[' . $label->id . ']', + ); + + } + + return $choices; + + } + + /** + * Prepare Trello members for feed setting. + * + * @since 1.0 + * @access public + * + * @uses GFAddOn::get_setting() + * @uses GFAddOn::log_debug() + * @uses GFAddOn::log_error() + * @uses GFTrello::initialize_error() + * + * @return array $choices + */ + public function get_members_for_feed_setting() { + + // Initialize choices array. + $choices = array(); + + // If we are unable to initialize the API, return the choices array. + if ( ! $this->initialize_api() ) { + $this->log_error( __METHOD__ . '(): Unable to get members because API is not initialized.' ); + return $choices; + } + + // Get current board. + $board = $this->get_setting( 'board' ); + + try { + + // Get the Trello members. + $members = $this->api->boards->get( $board . '/members' ); + + // Log returned members. + $this->log_debug( __METHOD__ . '(): Members for board #' . $board . ': ' . print_r( $members, true ) ); + + } catch ( \Exception $e ) { + + // Log that we could not retreive the members. + $this->log_error( __METHOD__ . '(): Unable to retrieve members; ' . $e->getMessage() ); + + return $choices; + + } + + // If no members were found, return. + if ( empty( $members ) ) { + return $choices; + } + + // Loop through members. + foreach ( $members as $member ) { + + // Add member as choice. + $choices[] = array( + 'label' => esc_html( $member->fullName ), + 'name' => 'cardMembers[' . $member->id . ']', + ); + + } + + return $choices; + + } + + /** + * Prepare date fields for feed setting. + * + * @since 1.0 + * @access public + * + * @uses GFAddOn::get_current_form() + * @uses GFCommon::get_fields_by_type() + * + * @return array $choices + */ + public function get_date_fields_for_feed_setting() { + + // Initialize choices array. + $choices = array(); + + // Get form. + $form = $this->get_current_form(); + + // Get date fields for form. + $date_fields = GFCommon::get_fields_by_type( $form, array( 'date' ) ); + + // If no date fields were found, return. + if ( empty( $date_fields ) ) { + return $choices; + } + + // Loop through date fields. + foreach ( $date_fields as $field ) { + + // Add field as choice. + $choices[] = array( + 'label' => $field->label, + 'value' => $field->id, + ); + + } + + return $choices; + + } + + /** + * Prepare file upload fields for feed setting. + * + * @since 1.0 + * @access public + * + * @uses GFAddOn::get_current_form() + * @uses GFCommon::get_fields_by_type() + * + * @return array $choices + */ + public function get_upload_fields_for_feed_setting() { + + // Initialize choices array. + $choices = array(); + + // Get form. + $form = $this->get_current_form(); + + // Get file fields for form. + $file_fields = GFCommon::get_fields_by_type( $form, array( 'fileupload', 'dropbox' ) ); + + // If no file fields were found, return. + if ( empty( $file_fields ) ) { + return $choices; + } + + // Loop through file fields. + foreach ( $file_fields as $field ) { + + // Add field as choice. + $choices[] = array( + 'name' => 'cardAttachments[' . $field->id . ']', + 'label' => $field->label, + 'default_value' => 0, + ); + + } + + return $choices; + + } + + /** + * Set feed creation control. + * + * @since 1.0 + * @access public + * + * @uses GFTrello::initialize_api() + * + * @return bool + */ + public function can_create_feed() { + + return $this->initialize_api(); + + } + + /** + * Enable feed duplication. + * + * @since 1.0 + * @access public + * + * @param int $feed_id Feed ID requesting duplication ability. + * + * @return bool + */ + public function can_duplicate_feed( $feed_id ) { + + return true; + + } + + + + + + // # FEED LIST ----------------------------------------------------------------------------------------------------- + + /** + * Setup columns for feed list table. + * + * @since 1.0 + * @access public + * + * @return array + */ + public function feed_list_columns() { + + return array( + 'feedName' => esc_html__( 'Name', 'gravityformstrello' ), + 'board' => esc_html__( 'Trello Board', 'gravityformstrello' ), + 'list' => esc_html__( 'Trello List', 'gravityformstrello' ) + ); + + } + + /** + * Get Trello board name for feed list table. + * + * @since 1.0 + * @access public + * + * @param array $feed Feed object. + * + * @return string + */ + public function get_column_value_board( $feed ) { + + // If API is not initialized, return the board ID. + if ( ! $this->initialize_api() ) { + return esc_html( $feed['meta']['board'] ); + } + + try { + + // Get the Trello board. + $board = $this->api->boards->get( $feed['meta']['board'] ); + + return isset( $board->name ) ? esc_html( $board->name ) : esc_html( $feed['meta']['board'] ); + + } catch ( Exception $e ) { + + // Log that board could not be retrieved. + $this->log_error( __METHOD__ . '(): Unable to get Trello board; ' . $e->getMessage() ); + + return esc_html( $feed['meta']['board'] ); + + } + + } + + /** + * Get Trello list name for feed list table. + * + * @since 1.0 + * @access public + * + * @param array $feed Feed object. + * + * @return string + */ + public function get_column_value_list( $feed ) { + + // If API is not initialized, return the board ID. + if ( ! $this->initialize_api() ) { + return esc_html( $feed['meta']['list'] ); + } + + try { + + // Get the Trello list. + $list = $this->api->lists->get( $feed['meta']['list'] ); + + return isset( $list->name ) ? esc_html( $list->name ) : esc_html( $feed['meta']['list'] ); + + } catch ( Exception $e ) { + + $this->log_error( __METHOD__ . '(): Unable to get Trello list; ' . $e->getMessage() ); + + // Log that list could not be retrieved. + return esc_html( $feed['meta']['list'] ); + + } + + } + + + + + + // # FEED PROCESSING ----------------------------------------------------------------------------------------------- + + /** + * Process feed. + * + * @since 1.0 + * @access public + * + * @param array $feed The feed object to be processed. + * @param array $entry The entry object currently being processed. + * @param array $form The form object currently being processed. + * + * @uses GFAddOn::get_field_value() + * @uses GFCommon::replace_variables() + * @uses GFFeedAddOn::add_feed_error() + * @uses GFTrello::get_timezone_for_due_date() + * @uses GFTrello::initialize_api() + */ + public function process_feed( $feed, $entry, $form ) { + + // If API instance is not initialized, exit. + if ( ! $this->initialize_api() ) { + $this->add_feed_error( esc_html__( 'Card was not created because API could not be initialized.', 'gravityformstrello' ), $feed, $entry, $form ); + return $entry; + } + + // Prepare card object. + $card = array( + 'name' => GFCommon::replace_variables( $feed['meta']['cardName'], $form, $entry, false, true, false, 'text' ), + 'desc' => GFCommon::replace_variables( $feed['meta']['cardDescription'], $form, $entry, false, true, false, 'text' ), + ); + + // Add members to card. + if ( rgars( $feed, 'meta/cardMembers' ) ) { + + // Loop through card members. + foreach ( $feed['meta']['cardMembers'] as $member_id => $enabled ) { + + // If card member is not enabled, skip it. + if ( '1' !== $enabled ) { + continue; + } + + // Add member to card. + $card['idMembers'][] = $member_id; + + } + + // Convert members to string. + if ( rgar( $card, 'idMembers' ) ) { + $card['idMembers'] = implode( ',', $card['idMembers'] ); + } + + } + + // Add date to card. + if ( rgars( $feed, 'meta/cardDueDate' ) ) { + + // If a custom date string is set, use it. + if ( 'gf_custom' === $feed['meta']['cardDueDate'] && rgars( $feed, 'meta/cardDueDate_custom' ) ) { + + $card['due'] = date( 'Y-m-d\TH:i:s', strtotime( 'midnight +' . $feed['meta']['cardDueDate_custom'] . ' days' ) ) . $this->get_timezone_for_due_date(); + + } else if ( 'gf_custom' !== $feed['meta']['cardDueDate'] ) { + + // Get date field value. + $date = $this->get_field_value( $form, $entry, $feed['meta']['cardDueDate'] ); + + // If date field value was found, add it. + if ( $date ) { + $card['due'] = date( 'Y-m-d\TH:i:s', strtotime( $date ) ) . $this->get_timezone_for_due_date(); + } + + } + + } + + /** + * Change the card properties before sending the data to Trello. + * + * @param array $card The card properties. + * @param array $feed The feed currently being processed. + * @param array $entry The entry currently being processed. + * @param array $form The form currently being processed. + */ + $card = gf_apply_filters( array( 'gform_trello_card', $form['id'] ), $card, $feed, $entry, $form ); + + // Log the card to be created. + $this->log_debug( __METHOD__ . '(): Card to be created => ' . print_r( $card, 1 ) ); + + // If no card name is set, exit. + if ( rgblank( $card['name'] ) ) { + $this->add_feed_error( esc_html__( 'Card could not be created because no name was provided.', 'gravityformstrello' ), $feed, $entry, $form ); + return $entry; + } + + try { + + // Create card. + $card = $this->api->lists->post( $feed['meta']['list'] . '/cards', $card ); + + // If card was successfully created, log card ID. + if ( is_object( $card ) ) { + + $this->log_debug( __METHOD__ . '(): Card #' . $card->id . ' created.' ); + + } else { + + // Log that card could not be created. + $this->add_feed_error( esc_html__( 'Card could not be created.', 'gravityformstrello' ), $feed, $entry, $form ); + + return $entry; + + } + + } catch ( \Exception $e ) { + + // Log that card could not be created. + $this->add_feed_error( esc_html__( 'Card could not be created.', 'gravityformstrello' ) . ' ' . $e->getMessage() , $feed, $entry, $form ); + + return $entry; + + } + + // Add labels to card. + if ( rgars( $feed, 'meta/cardLabels' ) ) { + + // Loop through card labels. + foreach ( $feed['meta']['cardLabels'] as $label_id => $enabled ) { + + // If card label is not enabled, skip it. + if ( '1' !== $enabled ) { + continue; + } + + try { + + // Add label to card. + $this->api->cards->post( $card->id . '/idLabels', array( 'value' => $label_id ) ); + + } catch ( \Exception $e ) { + + // Log that label could not be added to card. + $this->add_feed_error( esc_html__( 'Label could not be added to card.', 'gravityformstrello' ) . ' ' . $e->getMessage() , $feed, $entry, $form ); + + } + + } + + } + + // If no attachment fields are selected, exit. + if ( ! rgars( $feed, 'meta/cardAttachments' ) ) { + return $entry; + } + + // Loop through attachement fields. + foreach ( $feed['meta']['cardAttachments'] as $field_id => $enabled ) { + + // If attachement field is not enabled, skip it. + if ( '1' !== $enabled ) { + continue; + } + + // Get uploaded files for field. + $files = $this->get_field_value( $form, $entry, $field_id ); + $files = array_map( 'trim', explode( ',', $files ) ); + $files = array_filter( $files ); + + // If no files were uploaded for this field, skip it. + if ( empty( $files ) ) { + continue; + } + + // Loop through files. + foreach ( $files as $file ) { + + try { + + // Add file to card. + $this->api->cards->post( $card->id . '/attachments', array( 'url' => $file ) ); + + } catch ( \Exception $e ) { + + // Log that file could not be attached to card. + $this->add_feed_error( sprintf( esc_html__( 'File "%s" could not be attached to card. %s', 'gravityformstrello' ), basename( $file ), $e->getMessage() ), $feed, $entry, $form ); + + } + + } + + } + + } + + + + + + // # HELPER METHODS ------------------------------------------------------------------------------------------------ + + /** + * Maybe display an admin notice when the site needs to be re-authenticated. + * + * @since 2.0 + */ + public function maybe_display_authentication_notice() { + if ( ! $this->requires_api_reauthentication() ) { + return; + } + + $message = sprintf( + /* translators: 1: open tag, 2: close tag */ + esc_html__( + 'Gravity Forms Trello requires re-authentication; %1$sPlease disconnect and re-connect%2$s to continue using this add-on.', + 'gravityformstrello' + ), + '', + '' + ) + ?> + +
+

array( 'href' => true ) ) ); ?>

+
+ get_plugin_settings(); + return ! empty( $settings ) && version_compare( rgar( $settings, 'reauth_version' ), self::LAST_REAUTHENTICATION_VERSION, '<' ); + } + + /** + * Returns the API key. + * + * Old versions of Trello used a different API key, users who were authenticated using this old key should use it until they re authenticate. + * + * @return string + */ + private function get_trello_app_key() { + // Use old API key if the user hasn't re authenticated yet. + if ( $this->requires_api_reauthentication() ) { + return 'dfab0c4a0f18ceda69247a94b8dfa48f'; + } + + return defined( 'TRELLO_APP_ID' ) ? TRELLO_APP_ID : '3d1091b9412c6c6d229c1b5030095d9f'; + } + + /** + * Initializes Trello API if credentials are valid. + * + * @since 1.0 + * @access public + * + * @return bool|null + */ + public function initialize_api() { + + // If API is already initialized, return. + if ( ! is_null( $this->api ) ) { + return true; + } + + // Load the Trello API library. + if ( ! class_exists( '\Trello\Trello' ) ) { + require_once 'includes/Trello/Trello.php'; + } + + // Get the plugin settings + $settings = $this->get_plugin_settings(); + + // If the authentication token empty, return null. + if ( ! rgar( $settings, 'authToken' ) ) { + return null; + } + + // Log that we are going to validate API credentials. + $this->log_debug( __METHOD__ . "(): Validating API info." ); + + try { + + // Initialize a new Trello API object. + $trello = new \Trello\Trello( $this->get_trello_app_key(), null, $settings['authToken'] ); + + // Run API test. + $boards = $trello->members->get( 'my/boards' ); + + } catch ( Exception $e ) { + + // Log that test failed. + $this->log_error( __METHOD__ . '(): API credentials are invalid; ' . $e->getMessage() ); + + return false; + + } + + // If no boards were returned, log that test failed. + if ( $boards === false ) { + + // Log that test failed. + $this->log_error( __METHOD__ . '(): API credentials are invalid.' ); + + return false; + + } + + // Log that test passed. + $this->log_debug( __METHOD__ . '(): API credentials are valid.' ); + + // Assign Trello object to the class. + $this->api = $trello; + + return true; + + } + + /** + * Get timezone offset for card due date. + * + * @since 1.0 + * @access public + * + * @return string Timezone offset in ISO 8601 format. + */ + public function get_timezone_for_due_date() { + + // Get GMT offset. + $gmt_offset = get_option( 'gmt_offset', 0 ); + + // Split offset by half hour. + $gmt_offset = explode( '.', $gmt_offset ); + + // Modify minute offset. + if ( isset( $gmt_offset[1] ) ) { + + switch ( $gmt_offset[1] ) { + case '25': + $gmt_offset[1] = '15'; + break; + case '5': + $gmt_offset[1] = '30'; + break; + case '75': + $gmt_offset[1] = '45'; + break; + } + + } else { + + $gmt_offset[1] = '00'; + + } + + // Get positive/negative offset. + if ( ! is_numeric( substr( $gmt_offset[0], 0, 1 ) ) ) { + + $offset = substr( $gmt_offset[0], 0, 1 ); + $gmt_offset[0] = substr( $gmt_offset[0], -1 ); + + } else { + + $offset = '+'; + + } + + // Add leading zero to hour offset. + $gmt_offset[0] = sprintf( '%02d', $gmt_offset[0] ); + + // Put it all together. + $gmt_offset = $offset . implode( ':', $gmt_offset ); + + return $gmt_offset; + + } + + + + + + // # UPGRADES ------------------------------------------------------------------------------------------------------ + + /** + * Run required routines when upgrading from previous versions of Add-On. + * + * @since 1.2.1 + * @access public + * + * @param string $previous_version Previous version number. + * + * @uses GFAddOn::get_plugin_settings() + * @uses GFFeedAddOn::get_feeds() + * @uses GFFeedAddOn::update_feed_meta() + * @uses GFTrello::initialize_api() + */ + public function upgrade( $previous_version ) { + + // Determine if previous version is before label change. + $previous_is_pre_label = ! empty( $previous_version ) && version_compare( $previous_version, '1.2.1', '<' ); + + // If previous version is before label change, update existing feeds. + if ( $previous_is_pre_label ) { + + // If API is not initialized, return. + if ( ! $this->initialize_api() ) { + return; + } + + // Get feeds. + $feeds = $this->get_feeds(); + + // If no feeds are found, exit. + if ( empty( $feeds ) ) { + return; + } + + // Loop through feeds. + foreach ( $feeds as $feed ) { + + // Get existing labels. + $existing_labels = rgars( $feed, 'meta/cardLabels' ); + + // If no labels are assigned, skip. + if ( empty( $existing_labels ) ) { + continue; + } + + try { + + // Get the Trello labels. + $labels = $this->api->boards->get( $feed['meta']['board'] . '/labels' ); + + // Log returned labels. + $this->log_debug( __METHOD__ . '(): Labels for board #' . $board . ': ' . print_r( $labels, true ) ); + + } catch ( \Exception $e ) { + + // Log that we could not retrieve the labels. + $this->log_error( __METHOD__ . '(): Unable to retrieve labels; ' . $e->getMessage() ); + + continue; + + } + + // Initialize new labels array. + $new_labels = array(); + + // Loop through existing labels array. + foreach ( $existing_labels as $label_color => $enabled ) { + + // If this label is not enabled, skip it. + if ( '1' !== $enabled ) { + continue; + } + + // Loop through labels. + foreach ( $labels as $label ) { + + // If the label colors don't match, skip. + if ( $label->color !== $label_color ) { + continue; + } + + // Add to new labels array. + $new_labels[ $label->id ] = $enabled; + + break; + + } + } + + // Add new labels to feed meta. + $feed['meta']['cardLabels'] = $new_labels; + + // Update feed. + $this->update_feed_meta( $feed['id'], $feed['meta'] ); + + } + } + } + + + +} diff --git a/includes/Trello/OAuthSimple.php b/includes/Trello/OAuthSimple.php new file mode 100644 index 0000000..42a4446 --- /dev/null +++ b/includes/Trello/OAuthSimple.php @@ -0,0 +1,558 @@ + + * @copyright unitedHeroes.net 2011 + * @version 1.3 + * + */ +class OAuthSimple { + private $_secrets; + private $_default_signature_method; + private $_action; + private $_nonce_chars; + + /** + * Constructor + * + * @access public + * @param api_key (String) The API Key (sometimes referred to as the consumer key) This value is usually supplied by the site you wish to use. + * @param shared_secret (String) The shared secret. This value is also usually provided by the site you wish to use. + * @return OAuthSimple (Object) + */ + function __construct ($APIKey = "", $sharedSecret=""){ + + if (!empty($APIKey)) + { + $this->_secrets['consumer_key'] = $APIKey; + } + + if (!empty($sharedSecret)) + { + $this->_secrets['shared_secret'] = $sharedSecret; + } + + $this->_default_signature_method = "HMAC-SHA1"; + $this->_action = "GET"; + $this->_nonce_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + return $this; + } + + /** + * Reset the parameters and URL + * + * @access public + * @return OAuthSimple (Object) + */ + public function reset() { + $this->_parameters = Array(); + $this->path = NULL; + $this->sbs = NULL; + + return $this; + } + + /** + * Set the parameters either from a hash or a string + * + * @access public + * @param(string, object) List of parameters for the call, this can either be a URI string (e.g. "foo=bar&gorp=banana" or an object/hash) + * @return OAuthSimple (Object) + */ + public function setParameters ($parameters=Array()) { + + if (is_string($parameters)) + { + $parameters = $this->_parseParameterString($parameters); + } + if (empty($this->_parameters)) + { + $this->_parameters = $parameters; + } + else if (!empty($parameters)) + { + $this->_parameters = array_merge($this->_parameters,$parameters); + } + if (empty($this->_parameters['oauth_nonce'])) + { + $this->_getNonce(); + } + if (empty($this->_parameters['oauth_timestamp'])) + { + $this->_getTimeStamp(); + } + if (empty($this->_parameters['oauth_consumer_key'])) + { + $this->_getApiKey(); + } + if (empty($this->_parameters['oauth_token'])) + { + $this->_getAccessToken(); + } + if (empty($this->_parameters['oauth_signature_method'])) + { + $this->setSignatureMethod(); + } + if (empty($this->_parameters['oauth_version'])) + { + $this->_parameters['oauth_version']="1.0"; + } + + return $this; + } + + /** + * Convenience method for setParameters + * + * @access public + * @see setParameters + */ + public function setQueryString ($parameters) + { + return $this->setParameters($parameters); + } + + /** + * Set the target URL (does not include the parameters) + * + * @param path (String) the fully qualified URI (excluding query arguments) (e.g "http://example.org/foo") + * @return OAuthSimple (Object) + */ + public function setURL ($path) + { + if (empty($path)) + { + throw new OAuthSimpleException('No path specified for OAuthSimple.setURL'); + } + $this->_path=$path; + + return $this; + } + + /** + * Convenience method for setURL + * + * @param path (String) + * @see setURL + */ + public function setPath ($path) + { + return $this->_path=$path; + } + + /** + * Set the "action" for the url, (e.g. GET,POST, DELETE, etc.) + * + * @param action (String) HTTP Action word. + * @return OAuthSimple (Object) + */ + public function setAction ($action) + { + if (empty($action)) + { + $action = 'GET'; + } + $action = strtoupper($action); + if (preg_match('/[^A-Z]/',$action)) + { + throw new OAuthSimpleException('Invalid action specified for OAuthSimple.setAction'); + } + $this->_action = $action; + + return $this; + } + + /** + * Set the signatures (as well as validate the ones you have) + * + * @param signatures (object) object/hash of the token/signature pairs {api_key:, shared_secret:, oauth_token: oauth_secret:} + * @return OAuthSimple (Object) + */ + public function signatures ($signatures) + { + if (!empty($signatures) && !is_array($signatures)) + { + throw new OAuthSimpleException('Must pass dictionary array to OAuthSimple.signatures'); + } + if (!empty($signatures)) + { + if (empty($this->_secrets)) + { + $this->_secrets=Array(); + } + $this->_secrets=array_merge($this->_secrets,$signatures); + } + if (isset($this->_secrets['api_key'])) + { + $this->_secrets['consumer_key'] = $this->_secrets['api_key']; + } + if (isset($this->_secrets['access_token'])) + { + $this->_secrets['oauth_token'] = $this->_secrets['access_token']; + } + if (isset($this->_secrets['access_secret'])) + { + $this->_secrets['oauth_secret'] = $this->_secrets['access_secret']; + } + if (isset($this->_secrets['access_token_secret'])) + { + $this->_secrets['oauth_secret'] = $this->_secrets['access_token_secret']; + } + if (empty($this->_secrets['consumer_key'])) + { + throw new OAuthSimpleException('Missing required consumer_key in OAuthSimple.signatures'); + } + if (empty($this->_secrets['shared_secret'])) + { + throw new OAuthSimpleException('Missing requires shared_secret in OAuthSimple.signatures'); + } + if (!empty($this->_secrets['oauth_token']) && empty($this->_secrets['oauth_secret'])) + { + throw new OAuthSimpleException('Missing oauth_secret for supplied oauth_token in OAuthSimple.signatures'); + } + + return $this; + } + + public function setTokensAndSecrets($signatures) + { + return $this->signatures($signatures); + } + + /** + * Set the signature method (currently only Plaintext or SHA-MAC1) + * + * @param method (String) Method of signing the transaction (only PLAINTEXT and SHA-MAC1 allowed for now) + * @return OAuthSimple (Object) + */ + public function setSignatureMethod ($method="") + { + if (empty($method)) + { + $method = $this->_default_signature_method; + } + $method = strtoupper($method); + switch($method) + { + case 'PLAINTEXT': + case 'HMAC-SHA1': + $this->_parameters['oauth_signature_method']=$method; + break; + default: + throw new OAuthSimpleException ("Unknown signing method $method specified for OAuthSimple.setSignatureMethod"); + break; + } + + return $this; + } + + /** sign the request + * + * note: all arguments are optional, provided you've set them using the + * other helper functions. + * + * @param args (Array) hash of arguments for the call {action, path, parameters (array), method, signatures (array)} all arguments are optional. + * @return (Array) signed values + */ + public function sign($args=array()) + { + if (!empty($args['action'])) + { + $this->setAction($args['action']); + } + if (!empty($args['path'])) + { + $this->setPath($args['path']); + } + if (!empty($args['method'])) + { + $this->setSignatureMethod($args['method']); + } + if (!empty($args['signatures'])) + { + $this->signatures($args['signatures']); + } + if (empty($args['parameters'])) + { + $args['parameters']=array(); + } + $this->setParameters($args['parameters']); + $normParams = $this->_normalizedParameters(); + $this->_parameters['oauth_signature'] = $this->_generateSignature($normParams); + + return Array ( + 'parameters' => $this->_parameters, + 'signature' => self::_oauthEscape($this->_parameters['oauth_signature']), + 'signed_url' => $this->_path . '?' . $this->_normalizedParameters(), + 'header' => $this->getHeaderString(), + 'sbs'=> $this->sbs + ); + } + + /** + * Return a formatted "header" string + * + * NOTE: This doesn't set the "Authorization: " prefix, which is required. + * It's not set because various set header functions prefer different + * ways to do that. + * + * @param args (Array) + * @return $result (String) + */ + public function getHeaderString ($args=array()) + { + if (empty($this->_parameters['oauth_signature'])) + { + $this->sign($args); + } + $result = 'OAuth '; + + foreach ($this->_parameters as $pName => $pValue) + { + if (strpos($pName,'oauth_') !== 0) + { + continue; + } + if (is_array($pValue)) + { + foreach ($pValue as $val) + { + $result .= $pName .'="' . self::_oauthEscape($val) . '", '; + } + } + else + { + $result .= $pName . '="' . self::_oauthEscape($pValue) . '", '; + } + } + + return preg_replace('/, $/','',$result); + } + + private function _parseParameterString ($paramString) + { + $elements = explode('&',$paramString); + $result = array(); + foreach ($elements as $element) + { + list ($key,$token) = explode('=',$element); + if ($token) + { + $token = urldecode($token); + } + if (!empty($result[$key])) + { + if (!is_array($result[$key])) + { + $result[$key] = array($result[$key],$token); + } + else + { + array_push($result[$key],$token); + } + } + else + $result[$key]=$token; + } + return $result; + } + + + private static function _oauthEscape($string) + { + if ($string === 0) { return 0; } + if ($string == '0') { return '0'; } + if (strlen($string) == 0) { return ''; } + if (is_array($string)) { + throw new OAuthSimpleException('Array passed to _oauthEscape'); + } + $string = rawurlencode($string); + + //FIX: rawurlencode of ~ + $string = str_replace('%7E','~', $string); + $string = str_replace('+','%20',$string); + $string = str_replace('!','%21',$string); + $string = str_replace('*','%2A',$string); + $string = str_replace('\'','%27',$string); + $string = str_replace('(','%28',$string); + $string = str_replace(')','%29',$string); + + return $string; + } + + private function _getNonce($length=5) + { + $result = ''; + $cLength = strlen($this->_nonce_chars); + for ($i=0; $i < $length; $i++) + { + $rnum = rand(0,$cLength); + $result .= substr($this->_nonce_chars,$rnum,1); + } + $this->_parameters['oauth_nonce'] = $result; + + return $result; + } + + private function _getApiKey() + { + if (empty($this->_secrets['consumer_key'])) + { + throw new OAuthSimpleException('No consumer_key set for OAuthSimple'); + } + $this->_parameters['oauth_consumer_key']=$this->_secrets['consumer_key']; + + return $this->_parameters['oauth_consumer_key']; + } + + private function _getAccessToken() + { + if (!isset($this->_secrets['oauth_secret'])) + { + return ''; + } + if (!isset($this->_secrets['oauth_token'])) + { + throw new OAuthSimpleException('No access token (oauth_token) set for OAuthSimple.'); + } + $this->_parameters['oauth_token'] = $this->_secrets['oauth_token']; + + return $this->_parameters['oauth_token']; + } + + private function _getTimeStamp() + { + return $this->_parameters['oauth_timestamp'] = time(); + } + + private function _normalizedParameters() + { + $normalized_keys = array(); + $return_array = array(); + + foreach ( $this->_parameters as $paramName=>$paramValue) { + if (!preg_match('/\w+_secret/',$paramName) OR (strpos($paramValue, '@') !== 0 && !file_exists(substr($paramValue, 1))) ) + { + if (is_array($paramValue)) + { + $normalized_keys[self::_oauthEscape($paramName)] = array(); + foreach($paramValue as $item) + { + array_push($normalized_keys[self::_oauthEscape($paramName)], self::_oauthEscape($item)); + } + } + else + { + $normalized_keys[self::_oauthEscape($paramName)] = self::_oauthEscape($paramValue); + } + } + } + + ksort($normalized_keys); + + foreach($normalized_keys as $key=>$val) + { + if (is_array($val)) + { + sort($val); + foreach($val as $element) + { + array_push($return_array, $key . "=" . $element); + } + } + else + { + array_push($return_array, $key .'='. $val); + } + + } + + return join("&", $return_array); + } + + + private function _generateSignature () + { + $secretKey = ''; + if(isset($this->_secrets['shared_secret'])) + { + $secretKey = self::_oauthEscape($this->_secrets['shared_secret']); + } + + $secretKey .= '&'; + if(isset($this->_secrets['oauth_secret'])) + { + $secretKey .= self::_oauthEscape($this->_secrets['oauth_secret']); + } + switch($this->_parameters['oauth_signature_method']) + { + case 'PLAINTEXT': + return urlencode($secretKey);; + case 'HMAC-SHA1': + $this->sbs = self::_oauthEscape($this->_action).'&'.self::_oauthEscape($this->_path).'&'.self::_oauthEscape($this->_normalizedParameters()); + + return base64_encode(hash_hmac('sha1',$this->sbs,$secretKey,TRUE)); + default: + throw new OAuthSimpleException('Unknown signature method for OAuthSimple'); + break; + } + } +} + +class OAuthSimpleException extends \Exception { + + public function __construct($err, $isDebug = FALSE) + { + self::log_error($err); + if ($isDebug) + { + self::display_error($err, TRUE); + } + } + + public static function log_error($err) + { + error_log($err, 0); + } + + public static function display_error($err, $kill = FALSE) + { + print_r($err); + if ($kill === FALSE) + { + die(); + } + } +} \ No newline at end of file diff --git a/includes/Trello/Trello.php b/includes/Trello/Trello.php new file mode 100644 index 0000000..4e3ab3b --- /dev/null +++ b/includes/Trello/Trello.php @@ -0,0 +1,582 @@ +post() or Trello->boards->get()). See + * https://trello.com/docs/gettingstarted/clientjs.html for detailed information. + * + * Some differences - you cannot specify callbacks for success or error. If they're requested + * I may add them in, but it's not really my style to pass callbacks like that around PHP when + * I can simply return the data instead. + * + * Trello::authorize here does OAuth authentication, so you must pass your Secret Key to the + * constructor or set it after instantiation before calling the authorize method. Some parameters + * are the same as client.js (name, scope, expiration) and there is one extra (redirect_uri) for + * the OAuth callback. + * + * Go to https://trello.com/1/appKey/generate to get your API and OAuth keys + * + * @author Matt Zuba + * @copyright 2013 Matt Zuba + * @version 1.0 + * @package php-trello + */ +class Trello +{ + /** + * php-trello version + */ + private $version = '1.1.1'; + + /** + * Trello API Version + */ + protected $apiVersion = 1; + + /** + * Trello API Endpoint + */ + protected $apiEndpoint = 'https://api.trello.com'; + + /** + * Trello Auth endpoint + */ + protected $authEndpoint = 'https://trello.com'; + + /** + * Populated on instantiation, combo of apiEndpoint and apiVersion + */ + protected $baseUrl; + + /** + * Consumer key from Trello API + */ + protected $consumer_key; + + /** + * OAuth Secret Key + */ + protected $shared_secret; + + /** + * Non-OAuth or OAuth token + */ + protected $token; + + /** + * OAuth Secret token + */ + protected $oauth_secret; + + /** + * Last error encountered by REST api + */ + protected $lastError; + + /** + * __construct + * + * @param string $consumer_key + * @param string $token [optional] + * @param string $shared_secret [optional] + * @param string $oauth_secret [optional] + * @throws \Exception + */ + public function __construct($consumer_key, $shared_secret = null, $token = null, $oauth_secret = null) + { + + // CURL is required in order for this extension to work + if (!function_exists('curl_init')) { + throw new \Exception('CURL is required for php-trello'); + } + + // Sessions are used to for OAuth + if (session_id() == '' && !headers_sent()) { + session_start(); + } + + $this->baseUrl = "$this->apiEndpoint/$this->apiVersion/"; + $this->consumer_key = $consumer_key; + $this->shared_secret = $shared_secret; + $this->token = $token; + $this->oauth_secret = $oauth_secret; + } + + /** + * version + * + * @return int Trello API version + */ + public function version() + { + return $this->apiVersion; + } + + /** + * key + * + * @return string + */ + public function key() + { + return $this->consumer_key; + } + + /** + * setKey + * + * @param string $consumer_key + */ + public function setKey($consumer_key) + { + $this->consumer_key = $consumer_key; + } + + /** + * token + * + * @return string + */ + public function token() + { + return $this->token; + } + + /** + * setToken + * + * @param string $token + */ + public function setToken($token) + { + $this->token = $token; + } + + /** + * oauthSecret + * + * @return string + */ + public function oauthSecret() { + return $this->oauth_secret; + } + + /** + * setOauthSecret + * + * @param string $secret + */ + public function setOauthSecret($secret) { + $this->oauth_secret = $secret; + } + + /** + * authorized + * + * @return boolean + */ + public function authorized() + { + return $this->token != null; + } + + /** + * error + * + * @return string + */ + public function error() + { + return $this->lastError; + } + + /** + * authorize + * Performs an OAuth authorization to Trello. Possible options include: + * name - Application name as the user see's it + * redirect_uri - where should the OAuth request direct it's response + * expiration - how long will the token be good for + * scope - what you will need access to + * + * @param array $userOptions [optional] + * @return void + */ + public function authorize($userOptions = array(), $return = false) + { + if ($this->authorized()) { + return true; + } + + if (!$this->shared_secret) { + return false; + } + + $oauth = new OAuthSimple($this->consumer_key, $this->shared_secret); + + // We're back from an authorization request, process it + if (isset($_GET['oauth_verifier'], $_SESSION['oauth_token_secret'])) { + + // $_SESSION[oauth_token_secret] was stored before the Authorization redirect + $signatures = array( + 'oauth_secret' => $_SESSION['oauth_token_secret'], + 'oauth_token' => $_GET['oauth_token'], + ); + + $request = $oauth->sign(array( + 'path' => "$this->authEndpoint/$this->apiVersion/OAuthGetAccessToken", + 'parameters' => array( + 'oauth_verifier' => $_GET['oauth_verifier'], + 'oauth_token' => $_GET['oauth_token'], + ), + 'signatures' => $signatures, + )); + + // Initiate our request to get a permanent access token + $ch = curl_init($request['signed_url']); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $result = curl_exec($ch); + + // Parse our tokens and store them + parse_str($result, $returned_items); + $this->token = $returned_items['oauth_token']; + $this->oauth_secret = $returned_items['oauth_token_secret']; + + // To prevent a refresh of the page from working to re-do this step, clear out the temp + // access token. + unset($_SESSION['oauth_token_secret']); + + return true; + } + + $options = array_merge(array( + 'name' => null, + 'redirect_uri' => $this->callbackUri(), + 'expiration' => '30days', + 'scope' => array( + 'read' => true, + 'write' => false, + 'account' => false, + ), + ), $userOptions); + + $scope = implode(',', array_keys(array_filter($options['scope']))); + + // Get a request token from Trello + $request = $oauth->sign(array( + 'path' => "$this->authEndpoint/$this->apiVersion/OAuthGetRequestToken", + 'parameters' => array( + 'oauth_callback' => $options['redirect_uri'], + ) + )); + + $ch = curl_init($request['signed_url']); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $result = curl_exec($ch); + + // We store the token_secret for later because it's needed to get a permanent one + parse_str($result, $returned_items); + $request_token = $returned_items['oauth_token']; + $_SESSION['oauth_token_secret'] = $returned_items['oauth_token_secret']; + + // Create and process a request with all of our options for Authorization + $request = $oauth->sign(array( + 'path' => "$this->authEndpoint/$this->apiVersion/OAuthAuthorizeToken", + 'parameters' => array( + 'oauth_token' => $request_token, + 'name' => $options['name'], + 'expiration' => $options['expiration'], + 'scope' => $scope, + ) + )); + + if ($return) { + return $request['signed_url']; + } + + header("Location: $request[signed_url]"); + exit; + } + + /** + * __call + * We use PHP's magic __call method for dynamic calling of the REST types. + * + * @param string $method + * @param array $arguments + * @return mixed array of stdClass objects or false on failure + * @throws \Exception + */ + public function __call($method, $arguments) + { + if (in_array($method, array('get', 'post', 'put', 'delete', 'del'))) { + array_unshift($arguments, strtoupper($method)); + return call_user_func_array(array($this, 'rest'), $arguments); + } + + throw new \Exception("Method $method does not exist."); + } + + /** + * __get + * This is used as a shortcut for the types of collections. + * + * @param string $collection + * @return \Trello\Collection + * @throws \Exception + */ + public function __get($collection) + { + return new Collection($collection, $this); + } + + /** + * rest + * This method actually performs the calls back to the Trello REST service + * + * @param string $method + * @return mixed array of stdClass objects or false on failure + * @throws \Exception + */ + public function rest($method) + { + $args = array_slice(func_get_args(), 1); + extract($this->parseRestArgs($args)); /* path, params */ + + $restData = array(); + if ($this->consumer_key && !$this->shared_secret) { + $restData['key'] = $this->consumer_key; + } + if ($this->token && !$this->shared_secret) { + $restData['token'] = $this->token; + } + + if (is_array($params)) { + $restData = array_merge($restData, $params); + } + + // Perform the CURL query + $ch = curl_init(); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_USERAGENT, "php-trello/$this->version"); + curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + + switch ($method) { + case 'GET': + break; + case 'POST': + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($restData, '', '&')); + $restData = array(); + break; + case 'PUT': + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($restData, '', '&')); + $restData = array(); + break; + case 'DELETE': + case 'DEL': + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + break; + default: + throw new \Exception('Invalid method specified'); + break; + } + + $url = $this->buildRequestUrl($method, $path, $restData); + + curl_setopt($ch, CURLOPT_URL, $url); + + // Grab the response from Trello + $responseBody = curl_exec($ch); + if (!$responseBody) { + + // If there was a CURL error of some sort, log it and return false + $this->lastError = curl_error($ch); + return false; + } + + $responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $responseBody = trim($responseBody); + if (substr($responseCode, 0, 1) != '2') { + + // If we didn't get a 2xx HTTP response from Trello, log the responsebody as an error + $this->lastError = $responseBody; + return false; + } + + $this->lastError = ''; + return json_decode($responseBody); + } + + /** + * buildRequestUrl + * Parse arguments sent to the rest function. Might be extended in future for callbacks. + * + * @param string $method + * @param string $path + * @param array $data + * @return string + */ + public function buildRequestUrl($method, $path, $data) + { + $url = "{$this->baseUrl}{$path}"; + + // If we're using oauth, account for it + if ($this->canOauth()) { + $oauth = new OAuthSimple($this->consumer_key, $this->shared_secret); + $oauth->setTokensAndSecrets(array('access_token' => $this->token,'access_secret' => $this->oauth_secret,)) + ->setParameters($data); + $request = $oauth->sign(array('path' => $url)); + return $request['signed_url']; + } else { + // These methods require the data appended to the URL + if (in_array($method, array('GET', 'DELETE', 'DEL')) && !empty($data)) { + $url .= '?' . http_build_query($data, '', '&'); + } + + return $url; + } + } + + /** + * parseRestArgs + * Parse arguments sent to the rest function. Might be extended in future for callbacks. + * + * @param array $args + * @return array + */ + protected function parseRestArgs($args) + { + $opts = array( + 'path' => '', + 'params' => array(), + ); + + if (!empty($args[0])) { + $opts['path'] = $args[0]; + } + if (!empty($args[1])) { + $opts['params'] = $args[1]; + } + + return $opts; + } + + /** + * canOauth + * Determines if we can use OAuth for our REST request + * + * @return boolean + */ + protected function canOauth() + { + return $this->consumer_key && $this->token && $this->shared_secret && $this->oauth_secret; + } + + /** + * callbackUri + * Returns the currently loaded PHP page to be used as the callback_url if one isn't supplied + * + * @return string + */ + protected function callbackUri() { + if (empty($_SERVER['REQUEST_URI'])) { + return ''; + } + $port = $_SERVER['SERVER_PORT'] == '80' || $_SERVER['SERVER_PORT'] == '443' ? '' : ":$_SERVER[SERVER_PORT]"; + $protocol = 'http' . (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off' ? 's://' : '://'); + return "{$protocol}{$_SERVER['HTTP_HOST']}{$port}{$_SERVER['REQUEST_URI']}"; + } +} + +/** + * Collection + * This is a helper class for calling 'get' on collections (Trello->boards->get()) + * It is not necessary to create objects of this type yourself. + * + * @author Matt Zuba + * @copyright 2013 Matt Zuba + * @version 1.0 + * @package php-trello + */ +class Collection +{ + /** + * Different types of collections (boards, members, etc) + * @var string + */ + protected $collection; + + /** + * Trello object so we can call REST api + * @var \Trello\Trello + */ + protected $trello; + + /** + * Supported collections + */ + protected $collections = array( + 'actions', + 'boards', + 'cards', + 'checklists', + 'lists', + 'members', + 'notifications', + 'organizations', + 'search', + 'tokens', + 'types', + 'webhooks', + ); + + /** + * __construct + * + * @param string $collection + * @param \Trello\Trello $trello + */ + public function __construct($collection, $trello) + { + if (!in_array($collection, $this->collections)) { + throw new \Exception("Unsupported collection: {$collection}."); + } + + $this->collection = $collection; + $this->trello = $trello; + } + + /** + * __call + * Allows for more use of the collection class (issue #5) + * + * @param string $method + * @param array $arguments + * @return mixed array of stdClass objects or false on failure + */ + public function __call($method, $arguments) + { + if (empty($arguments)) { + throw new \Exception('Missing path from method call.'); + } + $path = array_shift($arguments); + array_unshift($arguments, "$this->collection/$path"); + return call_user_func_array(array($this->trello, $method), $arguments); + } +} diff --git a/js/admin.js b/js/admin.js new file mode 100644 index 0000000..bfafdb6 --- /dev/null +++ b/js/admin.js @@ -0,0 +1,53 @@ +/* global gform_trello_admin_strings */ +( function( $ ) { + + $( document ).ready( function() { + + /* Hide save plugin settings button. */ + $( '#tab_gravityformstrello #gform-settings-save' ).hide(); + + /* De-Authorize Trello. */ + $( '#gform_trello_deauth_button' ).on( 'click', function( e ) { + e.preventDefault(); + var deauthButton = $( this ); + deauthButton.attr( 'disabled', 'disabled' ); + + // If using an old token, just delete it. + if ( gform_trello_admin_strings.requires_reauth ) { + $( 'input#authToken' ).val( '' ); + $( '#gform-settings-save' ).trigger( 'click' ); + return; + } + + // De-Authorize. + $.ajax( + { + async: true, + url: ajaxurl, + dataType: 'json', + method: 'POST', + data: { + action: 'gf_trello_deauthorize', + nonce: gform_trello_admin_strings.deauth_nonce, + }, + success: function( response ) { + if ( response.success && response.success == true ) { + location.reload(); + } else { + window.alert( response.data.message ); + } + }, + } + ).fail( + function( jqXHR, textStatus, error ) { + window.alert( error ); + } + ).always( + function () { + deauthButton.removeAttr( 'disabled' ); + } + ); + } ); + } ); + +} )( jQuery ); diff --git a/js/admin.min.js b/js/admin.min.js new file mode 100644 index 0000000..c30d829 --- /dev/null +++ b/js/admin.min.js @@ -0,0 +1 @@ +!function(a){a(document).ready(function(){a("#tab_gravityformstrello #gform-settings-save").hide(),a("#gform_trello_deauth_button").on("click",function(t){t.preventDefault();var e=a(this);if(e.attr("disabled","disabled"),gform_trello_admin_strings.requires_reauth)return a("input#authToken").val(""),void a("#gform-settings-save").trigger("click");a.ajax({async:!0,url:ajaxurl,dataType:"json",method:"POST",data:{action:"gf_trello_deauthorize",nonce:gform_trello_admin_strings.deauth_nonce},success:function(t){t.success&&1==t.success?location.reload():window.alert(t.data.message)}}).fail(function(t,e,a){window.alert(a)}).always(function(){e.removeAttr("disabled")})})})}(jQuery); \ No newline at end of file diff --git a/languages/gravityformstrello-ar.mo b/languages/gravityformstrello-ar.mo new file mode 100644 index 0000000..8cdd11e Binary files /dev/null and b/languages/gravityformstrello-ar.mo differ diff --git a/languages/gravityformstrello-ca.mo b/languages/gravityformstrello-ca.mo new file mode 100644 index 0000000..f9f644d Binary files /dev/null and b/languages/gravityformstrello-ca.mo differ diff --git a/languages/gravityformstrello-da_DK.mo b/languages/gravityformstrello-da_DK.mo new file mode 100644 index 0000000..7ec094f Binary files /dev/null and b/languages/gravityformstrello-da_DK.mo differ diff --git a/languages/gravityformstrello-de_DE.mo b/languages/gravityformstrello-de_DE.mo new file mode 100644 index 0000000..183003f Binary files /dev/null and b/languages/gravityformstrello-de_DE.mo differ diff --git a/languages/gravityformstrello-de_DE_formal.mo b/languages/gravityformstrello-de_DE_formal.mo new file mode 100644 index 0000000..809f83e Binary files /dev/null and b/languages/gravityformstrello-de_DE_formal.mo differ diff --git a/languages/gravityformstrello-en_AU.mo b/languages/gravityformstrello-en_AU.mo new file mode 100644 index 0000000..350bf6d Binary files /dev/null and b/languages/gravityformstrello-en_AU.mo differ diff --git a/languages/gravityformstrello-en_GB.mo b/languages/gravityformstrello-en_GB.mo new file mode 100644 index 0000000..063727c Binary files /dev/null and b/languages/gravityformstrello-en_GB.mo differ diff --git a/languages/gravityformstrello-es_ES.mo b/languages/gravityformstrello-es_ES.mo new file mode 100644 index 0000000..04b834a Binary files /dev/null and b/languages/gravityformstrello-es_ES.mo differ diff --git a/languages/gravityformstrello-fi.mo b/languages/gravityformstrello-fi.mo new file mode 100644 index 0000000..ca585c7 Binary files /dev/null and b/languages/gravityformstrello-fi.mo differ diff --git a/languages/gravityformstrello-fr_CA.mo b/languages/gravityformstrello-fr_CA.mo new file mode 100644 index 0000000..1c9b40e Binary files /dev/null and b/languages/gravityformstrello-fr_CA.mo differ diff --git a/languages/gravityformstrello-fr_FR.mo b/languages/gravityformstrello-fr_FR.mo new file mode 100644 index 0000000..e33e1f4 Binary files /dev/null and b/languages/gravityformstrello-fr_FR.mo differ diff --git a/languages/gravityformstrello-he_IL.mo b/languages/gravityformstrello-he_IL.mo new file mode 100644 index 0000000..87a5075 Binary files /dev/null and b/languages/gravityformstrello-he_IL.mo differ diff --git a/languages/gravityformstrello-hi_IN.mo b/languages/gravityformstrello-hi_IN.mo new file mode 100644 index 0000000..201091d Binary files /dev/null and b/languages/gravityformstrello-hi_IN.mo differ diff --git a/languages/gravityformstrello-hu_HU.mo b/languages/gravityformstrello-hu_HU.mo new file mode 100644 index 0000000..a36a896 Binary files /dev/null and b/languages/gravityformstrello-hu_HU.mo differ diff --git a/languages/gravityformstrello-it_IT.mo b/languages/gravityformstrello-it_IT.mo new file mode 100644 index 0000000..5f2360e Binary files /dev/null and b/languages/gravityformstrello-it_IT.mo differ diff --git a/languages/gravityformstrello-ja.mo b/languages/gravityformstrello-ja.mo new file mode 100644 index 0000000..0665731 Binary files /dev/null and b/languages/gravityformstrello-ja.mo differ diff --git a/languages/gravityformstrello-nb_NO.mo b/languages/gravityformstrello-nb_NO.mo new file mode 100644 index 0000000..91618a5 Binary files /dev/null and b/languages/gravityformstrello-nb_NO.mo differ diff --git a/languages/gravityformstrello-nl_BE.mo b/languages/gravityformstrello-nl_BE.mo new file mode 100644 index 0000000..64b4f5d Binary files /dev/null and b/languages/gravityformstrello-nl_BE.mo differ diff --git a/languages/gravityformstrello-nl_NL.mo b/languages/gravityformstrello-nl_NL.mo new file mode 100644 index 0000000..ab48484 Binary files /dev/null and b/languages/gravityformstrello-nl_NL.mo differ diff --git a/languages/gravityformstrello-pt_BR.mo b/languages/gravityformstrello-pt_BR.mo new file mode 100644 index 0000000..de4b1b9 Binary files /dev/null and b/languages/gravityformstrello-pt_BR.mo differ diff --git a/languages/gravityformstrello-pt_PT.mo b/languages/gravityformstrello-pt_PT.mo new file mode 100644 index 0000000..b699ce3 Binary files /dev/null and b/languages/gravityformstrello-pt_PT.mo differ diff --git a/languages/gravityformstrello-ru_RU.mo b/languages/gravityformstrello-ru_RU.mo new file mode 100644 index 0000000..9a1b5f0 Binary files /dev/null and b/languages/gravityformstrello-ru_RU.mo differ diff --git a/languages/gravityformstrello-sv_SE.mo b/languages/gravityformstrello-sv_SE.mo new file mode 100644 index 0000000..354813d Binary files /dev/null and b/languages/gravityformstrello-sv_SE.mo differ diff --git a/languages/gravityformstrello-tr_TR.mo b/languages/gravityformstrello-tr_TR.mo new file mode 100644 index 0000000..f57d96e Binary files /dev/null and b/languages/gravityformstrello-tr_TR.mo differ diff --git a/languages/gravityformstrello-zh_CN.mo b/languages/gravityformstrello-zh_CN.mo new file mode 100644 index 0000000..a143298 Binary files /dev/null and b/languages/gravityformstrello-zh_CN.mo differ diff --git a/languages/gravityformstrello.pot b/languages/gravityformstrello.pot new file mode 100644 index 0000000..a94e11f --- /dev/null +++ b/languages/gravityformstrello.pot @@ -0,0 +1,183 @@ +# Copyright (C) 2021 Gravity Forms +# This file is distributed under the GPL-2.0+. +msgid "" +msgstr "" +"Project-Id-Version: Gravity Forms Trello Add-On 2.0\n" +"Report-Msgid-Bugs-To: https://gravityforms.com/support\n" +"Last-Translator: Gravity Forms \n" +"Language-Team: Gravity Forms \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2021-07-07T14:33:00+00:00\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"X-Generator: WP-CLI 2.4.0\n" +"X-Domain: gravityformstrello\n" + +#. Plugin Name of the plugin +msgid "Gravity Forms Trello Add-On" +msgstr "" + +#. Plugin URI of the plugin +#. Author URI of the plugin +msgid "https://gravityforms.com" +msgstr "" + +#. Description of the plugin +msgid "Integrates Gravity Forms with Trello" +msgstr "" + +#. Author of the plugin +msgid "Gravity Forms" +msgstr "" + +#: class-gf-trello.php:209 +msgid "Create Trello card only when payment is received." +msgstr "" + +#: class-gf-trello.php:247 +msgid "Access denied." +msgstr "" + +#: class-gf-trello.php:277 +msgid "Unable to revoke token at Trello." +msgstr "" + +#: class-gf-trello.php:304 +msgid "Unable to connect to Trello due to mismatched state." +msgstr "" + +#: class-gf-trello.php:311 +msgid "Unable to connect your Trello account." +msgstr "" + +#: class-gf-trello.php:466 +msgid "Authorize with Trello" +msgstr "" + +#: class-gf-trello.php:518 +msgid "Click here to authenticate your Trello account." +msgstr "" + +#: class-gf-trello.php:523 +msgid "Trello has been authenticated with your account." +msgstr "" + +#: class-gf-trello.php:527 +msgid "De-Authorize Trello" +msgstr "" + +#: class-gf-trello.php:608 +msgid "Feed Name" +msgstr "" + +#: class-gf-trello.php:615 +#: class-gf-trello.php:649 +#: class-gf-trello.php:1123 +msgid "Name" +msgstr "" + +#: class-gf-trello.php:616 +msgid "Enter a feed name to uniquely identify this setup." +msgstr "" + +#: class-gf-trello.php:621 +#: class-gf-trello.php:1124 +msgid "Trello Board" +msgstr "" + +#: class-gf-trello.php:626 +msgid "You must have at least one Trello board in your account." +msgstr "" + +#: class-gf-trello.php:630 +#: class-gf-trello.php:1125 +msgid "Trello List" +msgstr "" + +#: class-gf-trello.php:636 +msgid "You must select a Trello board containing lists." +msgstr "" + +#: class-gf-trello.php:641 +msgid "Card Settings" +msgstr "" + +#: class-gf-trello.php:657 +msgid "Description" +msgstr "" + +#: class-gf-trello.php:662 +msgid "Due Date" +msgstr "" + +#: class-gf-trello.php:663 +msgid "days after today" +msgstr "" + +#: class-gf-trello.php:670 +msgid "Labels" +msgstr "" + +#: class-gf-trello.php:676 +msgid "Members" +msgstr "" + +#: class-gf-trello.php:682 +msgid "Feed Conditional Logic" +msgstr "" + +#: class-gf-trello.php:688 +#: class-gf-trello.php:693 +msgid "Conditional Logic" +msgstr "" + +#: class-gf-trello.php:689 +msgid "Enable" +msgstr "" + +#: class-gf-trello.php:690 +msgid "Export to Trello if" +msgstr "" + +#: class-gf-trello.php:694 +msgid "When conditional logic is enabled, form submissions will only be exported to Trello when the condition is met. When disabled, all form submissions will be posted." +msgstr "" + +#: class-gf-trello.php:711 +msgid "Attachments" +msgstr "" + +#: class-gf-trello.php:772 +msgid "Select a Board" +msgstr "" + +#: class-gf-trello.php:842 +msgid "Select a List" +msgstr "" + +#: class-gf-trello.php:1226 +msgid "Card was not created because API could not be initialized." +msgstr "" + +#: class-gf-trello.php:1296 +msgid "Card could not be created because no name was provided." +msgstr "" + +#: class-gf-trello.php:1313 +#: class-gf-trello.php:1322 +msgid "Card could not be created." +msgstr "" + +#: class-gf-trello.php:1347 +msgid "Label could not be added to card." +msgstr "" + +#: class-gf-trello.php:1389 +msgid "File \"%s\" could not be attached to card. %s" +msgstr "" + +#. translators: 1: open tag, 2: close tag +#: class-gf-trello.php:1417 +msgid "Gravity Forms Trello requires re-authentication; %1$sPlease disconnect and re-connect%2$s to continue using this add-on." +msgstr "" diff --git a/trello.php b/trello.php new file mode 100644 index 0000000..4bb17f0 --- /dev/null +++ b/trello.php @@ -0,0 +1,78 @@ +