diff --git a/composer.json b/composer.json index 3d719e9..1065ccc 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,8 @@ "wp-phpunit/wp-phpunit": "^6.7", "yoast/phpunit-polyfills": "^3.0", "wpackagist-plugin/query-monitor": "^3.17", - "wpackagist-plugin/wp-mail-debugger": "^1.1" + "wpackagist-plugin/wp-mail-debugger": "^1.1", + "wpackagist-plugin/tablepress": "^3.0" }, "scripts": { "lint": [ diff --git a/composer.lock b/composer.lock index fe95434..918377f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "678bea4f820866aa14095865fbe38ee2", + "content-hash": "ed1f674a81d2e797aa28a993ff6a6f85", "packages": [], "packages-dev": [ { @@ -2552,6 +2552,24 @@ "type": "wordpress-plugin", "homepage": "https://wordpress.org/plugins/query-monitor/" }, + { + "name": "wpackagist-plugin/tablepress", + "version": "3.0.1", + "source": { + "type": "svn", + "url": "https://plugins.svn.wordpress.org/tablepress/", + "reference": "tags/3.0.1" + }, + "dist": { + "type": "zip", + "url": "https://downloads.wordpress.org/plugin/tablepress.3.0.1.zip" + }, + "require": { + "composer/installers": "^1.0 || ^2.0" + }, + "type": "wordpress-plugin", + "homepage": "https://wordpress.org/plugins/tablepress/" + }, { "name": "wpackagist-plugin/wp-mail-debugger", "version": "1.1", diff --git a/plugin.php b/plugin.php index 52f7a75..c38589e 100644 --- a/plugin.php +++ b/plugin.php @@ -10,8 +10,12 @@ * Text Domain: contact-form-7-extras */ +require_once __DIR__ . '/src/class-cf7-extras-form-settings.php'; +require_once __DIR__ . '/src/class-cf7-extras-integration.php'; // Before all integrations. +require_once __DIR__ . '/src/class-cf7-extras-integration-tablepress.php'; require_once __DIR__ . '/src/class-cf7-extras.php'; $plugin = Cf7_Extras::instance(); $plugin->set_plugin_dir( __DIR__ ); -$plugin->init(); + +add_action( 'plugins_loaded', array( $plugin, 'init' ) ); diff --git a/readme.txt.md b/readme.txt.md index b1cafe1..decf219 100644 --- a/readme.txt.md +++ b/readme.txt.md @@ -7,7 +7,7 @@ Tested up to: 6.7 Stable tag: STABLETAG License: GPLv2 or later -Simple controls, analytics, tracking and redirects for Contact Form 7. +Analytics, tracking, redirects and storage for Contact Form 7. ## Description @@ -23,6 +23,7 @@ This is an addon for the [Contact Form 7](https://wordpress.org/plugins/contact- - Disable automatic paragraph formatting - Disable HTML5 input field types or enable the HTML5 input type fallback - Specify the Google reCAPTCHA language +- Store form submissions in [Storage for Contact Form 7](https://preseto.com/go/cf7-storage?utm_source=wporg) or [TablePress](https://wordpress.org/plugins/tablepress/). Please note that some settings work on the per-page level and will apply to all forms on the same page. For example, disabling AJAX form submissions for one form will disable AJAX submissions on all forms on the same page. @@ -34,9 +35,15 @@ Please note that some settings work on the per-page level and will apply to all The plugin adds a new "Controls" tab for each Contact Form 7 form in the WordPress administration area. +### Form Submission Storage + +*Setup the official companion plugin [Storage for Contact Form 7 plugin](https://preseto.com/go/cf7-storage?utm_source=wporg) for capturing the form submissions safely in the WordPres database.* + +Alternatively, there is also a basic integration with the [TablePress plugin](https://wordpress.org/plugins/tablepress/). Select the TablePress table where to store the form submissions. The plugin will add any missing columns for each form field, and append the form entries as rows to the table. Additionally, fields `cf7_time` (submission time as [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)) and `cf7_url` (URL where the form was submitted) are stored along with the form data. + ### Analytics Tracking -The plugin _automatically_ triggers analytics events for the following services: +The plugin *automatically* triggers analytics events for the following services: - [Google Analytics (GA4)](https://support.google.com/analytics/topic/14088998) using [Google Tag Manager](https://support.google.com/tagmanager/answer/9442095) and [Global Tag (gtag.js)](https://developers.google.com/tag-platform/gtagjs) with `ga()`, `_gaq.push()` and `dataLayer.push()` implementations, - [Matomo](https://matomo.org/) (formerly Piwik), @@ -80,7 +87,7 @@ Add it as [a Composer dependency](https://packagist.org/packages/kasparsd/contac ### How to save Contact Form 7 submissions in the WordPress database? -The "[Storage for Contact Form 7](https://codecanyon.net/item/storage-for-contact-form-7-/7806229)" plugin stores all contact form submissions (including attachments) securely in the WordPress database. It also provides a CSV export of the form entries. +The "[Storage for Contact Form 7](https://preseto.com/go/cf7-storage?utm_source=wporg)" plugin stores all contact form submissions (including attachments) securely in the WordPress database. It also provides a CSV export of the form entries. ## Screenshots diff --git a/src/class-cf7-extras-form-settings.php b/src/class-cf7-extras-form-settings.php new file mode 100644 index 0000000..8319693 --- /dev/null +++ b/src/class-cf7-extras-form-settings.php @@ -0,0 +1,100 @@ + false, + 'disable-ajax' => false, + 'html5-disable' => false, + 'html5-fallback' => false, + 'disable-autop' => false, + 'redirect-success' => false, + 'track-ga-success' => false, + 'track-ga-submit' => false, + 'track-ga' => false, + 'google-recaptcha-lang' => null, + ); + + /** + * Form instance. + * + * @var WPCF7_ContactForm + */ + protected $form; + + /** + * Setup settings for a form. + * + * @param WPCF7_ContactForm $form Form instance. + */ + public function __construct( $form ) { + $this->form = $form; + } + + /** + * Get settings for a form by ID. + * + * @param int $form_id Form ID. + * + * @return self + */ + public static function from_form_id( $form_id ) { + return new self( wpcf7_contact_form( $form_id ) ); + } + + /** + * Get the form ID. + * + * @return int + */ + public function form_id() { + return $this->form->id(); + } + + /** + * Get a setting value. + * + * @param string $key Setting key. + * + * @return mixed|null Return null if the setting is not found. + */ + public function get( $key ) { + $settings = $this->all(); + + if ( isset( $settings[ $key ] ) ) { + return $settings[ $key ]; + } + + return null; + } + + /** + * Get all settings. + * + * @return array + */ + public function all() { + $settings = get_post_meta( $this->form->id(), 'extras', true ); + + if ( ! is_array( $settings ) ) { + $settings = array(); + } + + $settings = array_merge( $this->defaults, $settings ); + + // Convert legacy settings into one. + if ( ! empty( $settings['track-ga-success'] ) || ! empty( $settings['track-ga-submit'] ) ) { + $settings['track-ga'] = true; + } + + return $settings; + } +} diff --git a/src/class-cf7-extras-integration-tablepress.php b/src/class-cf7-extras-integration-tablepress.php new file mode 100644 index 0000000..b55c45b --- /dev/null +++ b/src/class-cf7-extras-integration-tablepress.php @@ -0,0 +1,218 @@ +load_all( false ); + + foreach ( $table_ids as $table_id ) { + $table = TablePress::$model_table->load( $table_id, false, false ); + + if ( ! is_wp_error( $table ) ) { + $tables[ $table_id ] = $table['name']; // Attention: The table name is not unique! + } + } + + return $tables; + } + + /** + * Add the TablePress storage field to the form settings. + * + * @param array $fields Fields. + * @param array $settings Settings array for the form ID. + * + * @return array + */ + public function controls_fields( $fields, $settings ) { + $tablepress_tables = $this->get_tablepress_tables(); + $tablepress_options = array(); + + $tablepress_id_selected = null; + if ( ! empty( $settings[ self::FIELD_TABLEPRESS_ID ] ) ) { + $tablepress_id_selected = (int) $settings[ self::FIELD_TABLEPRESS_ID ]; + } + + if ( is_array( $tablepress_tables ) ) { + if ( empty( $tablepress_tables ) ) { + $tablepress_options[] = sprintf( + '', + esc_html__( 'No tables found', 'contact-form-7-extras' ) + ); + } else { + $tablepress_options[] = sprintf( + '', + esc_html__( 'Select a TablePress table', 'contact-form-7-extras' ) + ); + } + + foreach ( $tablepress_tables as $table_id => $table_name ) { + $tablepress_options[] = sprintf( + '', + $table_id, + selected( $tablepress_id_selected, $table_id, false ), + esc_html( $table_name ) + ); + } + } else { + $tablepress_options[] = sprintf( + '', + esc_html__( 'Activate TablePress to enable this feature', 'contact-form-7-extras' ) + ); + } + + $entries_links = array(); + + if ( ! empty( $settings['tablepress-id'] ) ) { + $entries_url = add_query_arg( + array( + 'page' => 'tablepress', + 'action' => 'edit', + 'table_id' => (int) $settings['tablepress-id'], + ), + admin_url( 'admin.php' ) + ); + + $entries_links[] = sprintf( + '%s', + esc_url( $entries_url ), + esc_html__( 'View Entries', 'contact-form-7-extras' ) + ); + } + + $tablepress_fields = array( + 'tablepress-id' => array( + 'label' => __( 'Store in TablePress', 'contact-form-7-extras' ), + 'docs_url' => 'https://formcontrols.com/docs/contact-form-7-to-tablepress', + 'field' => sprintf( + ' + %s +
%s
', + esc_attr( $this->get_field_name( self::FIELD_TABLEPRESS_ID ) ), + implode( '', $tablepress_options ), + implode( ' | ', $entries_links ), + esc_html__( 'Store form submissions in a TablePress table. Entries are appended to any existing rows and columns along with a dedicated header column matching the field names.', 'contact-form-7-extras' ) + ), + ), + ); + + return array_merge( $tablepress_fields, $fields ); // Merge to prepend the storage fields. + } + + /** + * Store the form submission in TablePress. + * + * @param WPCF7_ContactForm $contact_form Instance of the form being processed. + * + * @return void + */ + public function store_submission_in_tablepress( $contact_form ) { + $settings = $this->get_settings( $contact_form->id() ); + $table_id = (int) $settings->get( 'tablepress-id' ); + + if ( ! empty( $table_id ) && class_exists( 'TablePress' ) ) { + $table = TablePress::$model_table->load( $table_id ); + + if ( ! empty( $table ) && is_array( $table['data'] ) ) { + $form_submission = WPCF7_Submission::get_instance(); + $form_data = $form_submission->get_posted_data(); + + $extra_data = array( + 'cf7_time' => gmdate( 'c', $form_submission->get_meta( 'timestamp' ) ), + 'cf7_url' => $form_submission->get_meta( 'url' ), + ); + + $form_data = array_merge( $extra_data, $form_data ); + + $table = $this->get_data_for_table( $table, $form_data ); + + if ( is_array( $table ) ) { + TablePress::$model_table->save( $table ); + } + } + } + } + + /** + * Merge form data with table data. + * + * @param array $table TablePress table data. + * @param array $form_data Form data. + * + * @return array Updated TablePress table data. + */ + public function get_data_for_table( $table, $form_data ) { + $header_row = array_map( 'trim', current( $table['data'] ) ); + + // Get the column index for each header value. + foreach ( array_keys( $form_data ) as $form_field_name ) { + if ( ! in_array( $form_field_name, $header_row, true ) ) { + $header_row[] = $form_field_name; + } + } + + // Map by values for quick lookup for the new data. + $col_index = array_flip( $header_row ); // This works only if there are no duplicate column names. + + // Prefill the row with empty values. + $table_row = array_fill( 0, count( $col_index ), '' ); + + foreach ( $form_data as $key => $value ) { + if ( is_array( $value ) ) { + $col_value = implode( ', ', $value ); + } elseif ( is_scalar( $value ) ) { + $col_value = (string) $value; + } + + $table_row[ $col_index[ $key ] ] = $col_value; + } + + $table['data'][0] = $header_row; // Update the header row to include our headers. + $table['data'][] = $table_row; // Append our row. + + $max_cols = max( array_map( 'count', $table['data'] ) ); + $cols_fill = array_fill( 0, $max_cols, '' ); + + // Ensure all rows have the same number of columns. + $table['data'] = array_map( + function ( $row ) use ( $cols_fill ) { + return array_replace( $cols_fill, $row ); + }, + $table['data'] + ); + + return $table; + } +} diff --git a/src/class-cf7-extras-integration.php b/src/class-cf7-extras-integration.php new file mode 100644 index 0000000..b950f94 --- /dev/null +++ b/src/class-cf7-extras-integration.php @@ -0,0 +1,40 @@ +init(); + } + } + return true; } @@ -337,6 +354,13 @@ public function wpcf7_metabox( $cf7 ) { $fields ); + /** + * Let plugins add items to the settings. + * + * @param array $fields List of fields to display. + */ + $fields = apply_filters( 'cf7_extras__controls_fields', $fields, $settings ); + $rows = array(); foreach ( $fields as $field_id => $field ) { @@ -479,53 +503,23 @@ public function add_form( $form ) { * * @param WPCF7_ContactForm $form Form object. * @param string $field Setting field id. - * @param boolean $fresh Fetch a fresh value from the DB instead of cache. + * @param boolean $fresh Not used. * * @return mixed */ public function get_form_settings( $form, $field = null, $fresh = false ) { - static $form_settings = array(); - - if ( isset( $form_settings[ $form->id() ] ) && ! $fresh ) { - $settings = $form_settings[ $form->id() ]; - } else { - $settings = get_post_meta( $form->id(), 'extras', true ); + if ( ! isset( $this->form_settings[ $form->id() ] ) ) { + $this->form_settings[ $form->id() ] = new Cf7_Extras_Form_Settings( $form ); // Cache it for re-use. } - $settings = wp_parse_args( - $settings, - array( - 'disable-css' => false, - 'disable-ajax' => false, - 'html5-disable' => false, - 'html5-fallback' => false, - 'disable-autop' => false, - 'redirect-success' => false, - 'track-ga-success' => false, - 'track-ga-submit' => false, - 'track-ga' => false, - 'google-recaptcha-lang' => null, - ) - ); - - // Cache it for re-use. - $form_settings[ $form->id() ] = $settings; - - // Convert individual legacy settings into one. - if ( ! empty( $settings['track-ga-success'] ) || ! empty( $settings['track-ga-submit'] ) ) { - $settings['track-ga'] = true; - } + $settings = $this->form_settings[ $form->id() ]; // Return a specific field value. if ( isset( $field ) ) { - if ( isset( $settings[ $field ] ) ) { - return $settings[ $field ]; - } else { - return null; - } + return $settings->get( $field ); } - return $settings; + return $settings->all(); } diff --git a/tests/phpunit/class-integration-tablepress-test.php b/tests/phpunit/class-integration-tablepress-test.php new file mode 100644 index 0000000..cccadf9 --- /dev/null +++ b/tests/phpunit/class-integration-tablepress-test.php @@ -0,0 +1,45 @@ + array( + array( + '', + 'header cell', + ), + array( + 'first_name', + '', + ), + ), + ); + + $form_data = array( + 'first_name' => 'John ', + 'age' => '25', + 'food_preferences' => array( + 'pizza', + 'pasta', + ), + ); + + $this->assertEquals( + array( + array( '', 'header cell', 'first_name', 'age', 'food_preferences' ), + array( 'first_name', '', '', '', '' ), + array( '', '', 'John ', '25', 'pizza, pasta' ), + ), + $integration->get_data_for_table( $table, $form_data )['data'] + ); + } +}