Commit 604d557d authored by KR Moorhouse's avatar KR Moorhouse Committed by GitHub

Merge pull request #3591 from wpninjas/anonymize-data

Anonymize data
parents ee2d6021 76ec27f7
/**
* When we init a save action, listen for form changes
*
* @package Ninja Forms builder
* @subpackage Main App
* @copyright (c) 2017 WP Ninjas
* @since 3.1.7
*/
define( [], function( settingCollection ) {
var controller = Marionette.Object.extend( {
initialize: function() {
this.listenTo( nfRadio.channel( 'actions-save' ), 'init:actionModel', this.initSave );
},
/**
* Set listeners up to listen for add/delete fields for Save action
*/
initSave: function( actionModel ) {
this.model = actionModel;
/*
* When we init a save action model, register a listener for new
* fields
*/
this.listenTo( Backbone.Radio.channel( 'fields' ), 'add:field',
this.checkFieldAdded );
/*
* When we init a save action model, register a listener for deleted
* fields
*/
this.listenTo( Backbone.Radio.channel( 'fields' ), 'delete:field',
this.checkFieldDeleted );
},
/**
* When a save action is init'd, check to see if a new field added
* is an email and decide if it needs to be the 'submitter_email'
* for privacy regulation functionality
*
* @param {backbone.model} actionModel
* @return {void}
*/
checkFieldAdded: function( newFieldModel ) {
if( 'email' == newFieldModel.get( 'type' ) ) {
var submitter_email = this.model.get('submitter_email');
if( '' === submitter_email ) {
this.model.set( 'submitter_email', newFieldModel.get( 'key' ) );
}
}
},
/**
* When a save action is init'd, check to see if a field that has been
* deleted is an email and rearrance the submitter email setting
* for privacy regulation functionality
*
* @param {backbone.model} actionModel
* @return {void}
*/
checkFieldDeleted: function( fieldModel ) {
var submitter_email = this.model.get( 'submitter_email' );
if( submitter_email == fieldModel.get( 'key' ) ) {
this.model.set( 'submitter_email', '' );
}
},
});
return controller;
} );
\ No newline at end of file
......@@ -101,6 +101,7 @@ define(
'controllers/actions/collectPaymentCalculations',
'controllers/actions/collectPaymentFixed',
'controllers/actions/collectPayment',
'controllers/actions/save',
/*
* TODO: Settings domain controllers
......@@ -205,6 +206,7 @@ define(
ActionCollectPaymentCalculations,
ActionCollectPaymentFixed,
ActionCollectPayment,
ActionSave,
/*
* TODO: Settings domain controllers
......@@ -297,7 +299,8 @@ define(
new ActionNewsletterList();
new ActionDeleteFieldListener();
new ActionCollectPaymentCalculations();
new ActionCollectPayment();
new ActionCollectPayment();
new ActionSave();
new ActionTypes();
new ActionData();
new ActionSettings();
......
......@@ -302,6 +302,59 @@ define( ['views/app/drawer/mergeTagsContent', 'views/app/drawer/settingError'],
return helpTextWrapper.innerHTML;
},
/*
* Render a select element with only the email fields on the
* form
*/
renderEmailFieldOptions: function() {
var fields = nfRadio.channel( 'fields' ).request( 'get:collection' );
initialOption = document.createElement( 'option' );
initialOption.value = '';
initialOption.label = '--';
initialOption.innerHTML = '--';
var select_value = '';
var select = document.createElement( 'select' );
select.classList.add( 'setting' );
select.setAttribute( 'data-id', 'my_seledt' );
select.appendChild( initialOption );
var index = 0;
var that = this;
fields.each( function( field ) {
// Check for the field type in our lookup array and...
if( 'email' != field.get( 'type' ) ) {
// Return if the type is in our lookup array.
return '';
}
var option = document.createElement( 'option' );
option.value = field.get( 'key' );
option.innerHTML = field.get( 'label' );
option.label = field.get( 'label' );
if( that.value === field.get( 'key' ) ) {
option.setAttribute( 'selected', 'selected' );
}
select.appendChild( option );
index = index + 1;
});
label = document.createElement( 'label' );
label.classList.add( 'nf-select' );
label.appendChild( select );
// Select Lists need an empty '<div></div>' for styling purposes.
emptyContainer = document.createElement( 'div' );
label.appendChild( emptyContainer );
// The template requires a string.
return label.innerHTML;
},
renderMergeTags: function() {
if ( this.use_merge_tags && ! this.hide_merge_tags ) {
return '<span class="dashicons dashicons-list-view merge-tags"></span>';
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
File mode changed from 100755 to 100644
File mode changed from 100644 to 100755
......@@ -29,6 +29,7 @@ abstract class NF_Abstracts_UserInfo extends NF_Fields_Textbox
'help',
'description',
'custom_name_attribute',
'personally_identifiable',
'disable_browser_autocomplete' )
);
......
......@@ -81,6 +81,12 @@ final class NF_Actions_DeleteDataRequest extends NF_Abstracts_Action
*/
if( ! $request_id instanceof WP_Error ) {
// send the request if it's not an error.
// to anonymize or not to anonymize, that is the question
add_post_meta( $request_id,
'nf_anonymize_data',
$action_settings[ 'anonymize' ] );
wp_send_user_request( $request_id );
}
......
......@@ -2,6 +2,33 @@
class NF_Admin_UserDataRequests {
/**
* @var array
*/
protected $ignored_field_types = array (
'html',
'submit',
'hr',
'recaptcha',
'spam',
'creditcard',
'creditcardcvc',
'creditcardexpiration',
'creditcardfullname',
'creditcardnumber',
'creditcardzip'
);
/**
* @var WP_User
*/
protected $user;
/**
* @var string
*/
protected $request_email;
/** Class constructor */
public function __construct() {
add_filter( 'wp_privacy_personal_data_exporters', array(
......@@ -54,69 +81,67 @@ class NF_Admin_UserDataRequests {
$export_items = array();
// get the user
$user = get_user_by( 'email', $email_address );
$this->user = get_user_by( 'email', $email_address );
$this->request_email = $email_address;
if ( $user && $user->ID ) {
$item_id = "ninja-forms-" . $user->ID;
if( $this->user && $this->user->ID ) {
$item_id = "ninja-forms-" . $this->user->ID;
} else {
$item_id = "ninja-forms";
}
$group_id = 'ninja-forms';
$group_id = 'ninja-forms';
$group_label = __( 'Ninja Forms Submission Data', 'ninja-forms' );
$group_label = __( 'Ninja Forms Submission Data', 'ninja-forms' );
// we get the submissions the old-fashioned way
$subs = get_posts(
array(
'author' => $user->ID,
'post_type' => 'nf_sub',
'posts_per_page' => -1
)
);
$subs = $this->get_related_subs( $email_address );
foreach($subs as $sub) {
$data = array();
// get the field values from postmeta
$sub_meta = get_post_meta( $sub->ID );
// make sure we have a form submission
if ( isset( $sub_meta[ '_form_id' ] ) ) {
$form = Ninja_Forms()->form( $sub_meta[ '_form_id' ][ 0 ] )
->get();
$fields = Ninja_Forms()->form( $sub_meta[ '_form_id' ][ 0 ] )
->get_fields();
foreach ( $fields as $field_id => $field ) {
// we don't care about submit fields
if ( 'submit' != $field->get_setting( 'type' ) ) {
// make sure there is a value
if ( isset( $sub_meta[ '_field_' . $field_id ] ) ) {
//listcheckbox fields may need to be unserialized
if( 'listcheckbox' == $field->get_setting( 'type' ) ) {
//implode the unserialized array
$value = implode( ',', maybe_unserialize(
$sub_meta[ '_field_' . $field_id ][ 0 ] ));
} else {
$value = $sub_meta[ '_field_' . $field_id ][ 0 ];
}
// Add label/value pairs to data array
$data[] = array(
'name' => $field->get_setting( 'label' ),
'value' => $value
);
foreach($subs as $sub) {
$data = array();
// get the field values from postmeta
$sub_meta = get_post_meta( $sub->ID );
// make sure we have a form submission
if ( isset( $sub_meta[ '_form_id' ] ) ) {
$form = Ninja_Forms()->form( $sub_meta[ '_form_id' ][ 0 ] )
->get();
$fields = Ninja_Forms()->form( $sub_meta[ '_form_id' ][ 0 ] )
->get_fields();
foreach ( $fields as $field_id => $field ) {
// we don't care about submit, hr, divider, html fields
if ( ! in_array( $field->get_setting( 'type' ),
$this->ignored_field_types ) ) {
// make sure there is a value
if ( isset( $sub_meta[ '_field_' . $field_id ] ) ) {
//multi-value fields may need to be unserialized
if( in_array( $field->get_setting( 'type' ),
array( 'listcheckbox', 'listmultiselect' ) ) ){
//implode the unserialized array
$value = implode( ',', maybe_unserialize(
$sub_meta[ '_field_' . $field_id ][ 0 ] ) );
} else {
$value = $sub_meta[ '_field_' . $field_id ][ 0 ];
}
// Add label/value pairs to data array
$data[] = array(
'name' => $field->get_setting( 'label' ),
'value' => $value
);
}
}
// Add this group of items to the exporters data array.
$export_items[] = array(
'group_id' => $group_id . '-' . $sub->ID,
'group_label' => $group_label . '-' .
$form->get_setting( 'title' ),
'item_id' => $item_id . '-' . $sub->ID,
'data' => $data,
);
}
// Add this group of items to the exporters data array.
$export_items[] = array(
'group_id' => $group_id . '-' . $sub->ID,
'group_label' => $group_label . '-' .
$form->get_setting( 'title' ),
'item_id' => $item_id . '-' . $sub->ID,
'data' => $data,
);
}
}
// Returns an array of exported items for this pass, but also a boolean whether this exporter is finished.
......@@ -137,6 +162,7 @@ class NF_Admin_UserDataRequests {
* @return array
*/
function plugin_user_data_eraser( $email_address, $page = 1 ) {
if ( empty( $email_address ) ) {
return array(
'items_removed' => false,
......@@ -147,38 +173,271 @@ class NF_Admin_UserDataRequests {
}
// get the user
$user = get_user_by( 'email', $email_address );
$this->user = get_user_by( 'email', $email_address );
$this->request_email = $email_address;
$request_id = $_REQUEST[ 'id' ];
$make_anonymous = get_post_meta( $request_id, 'nf_anonymize_data',
true);
$messages = array();
$items_removed = false;
$items_retained = false;
if ( $user && $user->ID ) {
$subs = $this->get_related_subs( $email_address );
if( 0 < sizeof( $subs ) ) {
$items_removed = true;
}
if( '1' != $make_anonymous ) {
$this->delete_submissions( $subs );
$items_removed = true;
} else {
$this->anonymize_submissions( $subs, $email_address );
}
// get submissions the old-fashioned way
$subs = get_posts(
/**
* Returns an array of exported items for this pass, but also a boolean
* whether this exporter is finished.
* If not it will be called again with $page increased by 1.
* */
return array(
'items_removed' => $items_removed,
'items_retained' => $items_retained,
'messages' => $messages,
'done' => true,
);
}
/**
* Retrieve all submissions related(by author id or email address) to the
* given email address
*
* @param $email_address
*
* @return array
*/
private function get_related_subs( $email_address ) {
// array if subs where user is author
$logged_in_subs = array();
if ( $this->user && $this->user->ID ) {
// get submission ids the old-fashioned way if user is author
$logged_in_subs = get_posts(
array(
'author' => $user->ID,
'post_type' => 'nf_sub',
'posts_per_page' => -1
'author' => $this->user->ID,
'post_type' => 'nf_sub',
'posts_per_page' => - 1,
'fields' => 'ids'
)
);
if( 0 < sizeof( $subs ) ) {
$items_removed = true;
}
// get submission ids where email address is a field value
$anon_sub_ids = $this->get_subs_by_email( $email_address );
// merge anonymous and author submissions ids and get unique
$sub_ids = array_unique( array_merge( $logged_in_subs, $anon_sub_ids ) );
// get post objects related to the email address
return get_posts(
array(
'include' => implode(',', $sub_ids),
'post_type' => 'nf_sub',
'posts_per_page' => -1,
)
);
}
/**
* Get submission ids where the submission has the give email address as
* data
*
* @param $email_address
*
* @return array
*/
private function get_subs_by_email( $email_address ) {
global $wpdb;
// query to find any submission with our requester's email as value
$anon_subs_query = "SELECT DISTINCT(m.post_id) FROM `" . $wpdb->prefix
. "postmeta` m
JOIN `" . $wpdb->prefix . "posts` p ON p.id = m.post_id
WHERE m.meta_value = '" . $email_address . "'
AND p.post_type = 'nf_sub'";
$anon_subs = $wpdb->get_results( $anon_subs_query );
$sub_id_array = array();
// let's get the integer value of those submission ids
if( 0 < sizeof( $anon_subs ) ) {
foreach( $anon_subs as $sub ) {
$sub_id_array[] = intval( $sub->post_id );
}
}
return $sub_id_array;
}
/**
* Delete Submissions
*
* @param $subs
*/
private function delete_submissions( $subs ) {
if( 0 < sizeof( $subs ) ) {
// iterate and delete the submissions
foreach($subs as $sub) {
wp_delete_post( $sub->ID, true );
}
}
}
// Returns an array of exported items for this pass, but also a boolean whether this exporter is finished.
//If not it will be called again with $page increased by 1.
return array(
'items_removed' => $items_removed,
'items_retained' => $items_retained,
'messages' => $messages,
'done' => true,
);
/**
* This will (redact) personal data and anonymize submissions
*
* @param $subs
*/
private function anonymize_submissions( $subs ) {
$form_id_array = array();
$submitter_field = '';
if( 0 < sizeof( $subs ) ) {
$anonymize_data = false;
foreach( $subs as $sub ) {
// get the form id
$form_id = get_post_meta( $sub->ID, '_form_id', true );
$form = Ninja_Forms()->form( $form_id );
/*
* Do we have a use, if so does the post(submission) author
* match the user. If so, then anonymize
*/
if( $this->user && $this->user->ID
&& $sub->post_author == $this->user->ID ) {
$anonymize_data = true;
} else {
/*
* Otherwise, does the submitter email for the submission
* equal the email for the request
*/
$form_submitter_email = '';
if( in_array( $form_id, array_keys( $form_id_array ) ) ) {
/*
* if we already have the submitter field key, no
* need to iterate over the actions again
*/
$submitter_field = $form_id_array[ $form_id ];
} else {
$actions = $form->get_actions();
if ( 0 < sizeof( $actions ) ) {
foreach ( $actions as $action ) {
// we only care about the save action
if ( 'save' == $action->get_setting( 'type' )
&& null != $action->get_setting( 'submitter_email' )
&& '' != $action->get_setting( 'submitter_email' ) ) {
// get the submitter field
$submitter_field = $action->get_setting( 'submitter_email' );
/*
* Add the form id and submitter field to
* this array so we don't have to load
* the form again if we have multiple
* submissions for the same form
*/
$form_id_array[ $form_id ] = $submitter_field;
break;
}
}
}
}
/*
* If the submitter field is not empty, then let's
* get the value given in the form submission for
* that field
*/
if ( '' != $submitter_field ) {
$fields = $form->get_fields();
foreach ( $fields as $field ) {
$key = $field->get_setting( 'key' );
// we only care about email fields
if ( 'email' == $field->get_setting( 'type' )
&& $submitter_field == $key ) {
// if we have a match, get the value
$form_submitter_email = get_post_meta(
$sub->ID,
'_field_' . $field->get_id(),
true );
break;
}
}
}
// if form submitter email matches requester's email
if( $form_submitter_email === $this->request_email ) {
$anonymize_data = true;
}
}
if( $anonymize_data ) {
// anonymize the actual submitted for values
$this->anonymize_fields($sub, $form->get_fields() );
}
}
}
}
/**
* This will anonymize personally identifiable fields and anonymize
* submissions submitted by the user with the provided email address
*
* @param $sub
* @param $fields
*/
private function anonymize_fields( $sub, $fields ) {
foreach( $fields as $field ) {
$type = $field->get_setting( 'type' );
// ignore fields that aren't saved
if( ! in_array( $type, $this->ignored_field_types ) ) {
$is_personal = $field->get_setting( 'personally_identifiable' );
/**
* If this is personally identifiable, redact it
*/
if( null != $is_personal && '1' == $is_personal ) {
$field_id = $field->get_id();
// make sure we have that field saved.
$field_value = get_post_meta(
$sub->ID,
'_field_' . $field_id,
true
);
if( '' != $field_value ) {
update_post_meta(
$sub->ID,
'_field_' . $field_id,
'(redacted)'
);
}
}
}
}
// Remove the author id if the the email address belongs to the author
if( $this->user && $this->user->ID &&
$this->user->ID == $sub->post_author ) {
wp_update_post(
array(
'ID' => $sub->ID,
'post_author' => 0
)
);
}
}
}
\ No newline at end of file
......@@ -21,4 +21,11 @@ return apply_filters( 'ninja_forms_action_deletedatarequest_settings', array(
'width' => 'one-half',
'use_merge_tags' => true,
),
'anonymize' => array(
'name' => 'anonymize',
'type' => 'toggle',
'group' => 'advanced',
'label' => __( 'Anonymize Data', 'ninja-forms' ),
'width' => 'full',
),
) );
\ No newline at end of file
......@@ -5,6 +5,16 @@ return apply_filters( 'ninja_forms_action_email_settings', array(
/*
* To
*/
'submitter_email' => array(
'name' => 'submitter_email',
'type' => 'email-select',
'options' => array(),
'group' => 'primary',
'label' => __( 'Designated Submitter\'s Email Address', 'ninja-forms' ),
'value' => '',
'help' => __( 'The email address used in this field will be allowed to '
. 'make data export and delete requests on behalf of their form submission.', 'ninja-forms' ),
),
'fields_save_toggle' => array(
'name' => 'fields-save-toggle',
......
......@@ -653,6 +653,16 @@ return apply_filters( 'ninja_forms_field_settings', array(
'help' => __( 'This column in the submissions table will sort by number.', 'ninja-forms' ),
),
'personally_identifiable' => array(
'name' => 'personally_identifiable',
'type' => 'toggle',
'group' => 'advanced',
'label' => __( 'This Field Is Personally Identifiable Data', 'ninja-forms' ),
'width' => 'full',
'value' => '',
'help' => __( 'This option helps with privacy regulation compliance', 'ninja-forms' ),
),
/*
|--------------------------------------------------------------------------
| Display Settings
......
......@@ -25,5 +25,6 @@ class NF_Fields_Address extends NF_Fields_Textbox
$this->_nicename = __( 'Address', 'ninja-forms' );
$this->_settings[ 'custom_name_attribute' ][ 'value' ] = 'address';
$this->_settings[ 'personally_identifiable' ][ 'value' ] = '1';
}
}
......@@ -23,5 +23,6 @@ class NF_Fields_Address2 extends NF_Fields_Textbox
$this->_nicename = __( 'Address 2', 'ninja-forms' );
$this->_settings[ 'custom_name_attribute' ][ 'value' ] = 'address';
$this->_settings[ 'personally_identifiable' ][ 'value' ] = '1';
}
}
......@@ -19,7 +19,7 @@ class NF_Fields_Email extends NF_Abstracts_UserInfo
protected $_test_value = 'foo@bar.dev';
protected $_settings_all_fields = array( 'custom_name_attribute' );
protected $_settings_all_fields = array( 'custom_name_attribute', 'personally_identifiable' );
public function __construct()
{
......@@ -28,6 +28,7 @@ class NF_Fields_Email extends NF_Abstracts_UserInfo
$this->_nicename = __( 'Email', 'ninja-forms' );
$this->_settings[ 'custom_name_attribute' ][ 'value' ] = 'email';
$this->_settings[ 'personally_identifiable' ][ 'value' ] = '1';