* WC Admin Note Data_Store class file.
namespace Automattic\WooCommerce\Admin\Notes;
defined( 'ABSPATH' ) || exit;
* WC Admin Note Data Store (Custom Tables)
class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface {
// Extensions should define their own contexts and use them to avoid applying woocommerce_note_where_clauses when not needed.
const WC_ADMIN_NOTE_OPER_GLOBAL = 'global';
* Method to create a new note in the database.
* @param Note $note Admin note.
public function create( &$note ) {
$note->set_date_created( $date_created );
$note_to_be_inserted = array(
'name' => $note->get_name(),
'type' => $note->get_type(),
'locale' => $note->get_locale(),
'title' => $note->get_title(),
'content' => $note->get_content(),
'status' => $note->get_status(),
'source' => $note->get_source(),
'is_snoozable' => (int) $note->get_is_snoozable(),
'layout' => $note->get_layout(),
'image' => $note->get_image(),
'is_deleted' => (int) $note->get_is_deleted(),
'is_read' => (int) $note->get_is_read(),
$note_to_be_inserted['content_data'] = wp_json_encode( $note->get_content_data() );
$note_to_be_inserted['date_created'] = gmdate( 'Y-m-d H:i:s', $date_created );
$note_to_be_inserted['date_reminder'] = null;
$wpdb->insert( $wpdb->prefix . 'wc_admin_notes', $note_to_be_inserted );
$note_id = $wpdb->insert_id;
$note->set_id( $note_id );
$this->save_actions( $note );
* Fires when an admin note is created.
* @param int $note_id Note ID.
do_action( 'woocommerce_note_created', $note_id );
* @param Note $note Admin note.
* @throws \Exception Throws exception when invalid data is found.
public function read( &$note ) {
$note_id = $note->get_id();
if ( 0 !== $note_id || '0' !== $note_id ) {
$note_row = $wpdb->get_row(
"SELECT * FROM {$wpdb->prefix}wc_admin_notes WHERE note_id = %d LIMIT 1",
if ( 0 === $note->get_id() || '0' === $note->get_id() ) {
$this->read_actions( $note );
$note->set_object_read( true );
* Fires when an admin note is loaded.
* @param int $note_id Note ID.
do_action( 'woocommerce_note_loaded', $note );
$note->set_name( $note_row->name );
$note->set_type( $note_row->type );
$note->set_locale( $note_row->locale );
$note->set_title( $note_row->title );
$note->set_content( $note_row->content );
// The default for 'content_value' used to be an array, so there might be rows with invalid data!
$content_data = json_decode( $note_row->content_data );
$content_data = new \stdClass();
} elseif ( is_array( $content_data ) ) {
$content_data = (object) $content_data;
$note->set_content_data( $content_data );
$note->set_status( $note_row->status );
$note->set_source( $note_row->source );
$note->set_date_created( $note_row->date_created );
$note->set_date_reminder( $note_row->date_reminder );
$note->set_is_snoozable( (bool) $note_row->is_snoozable );
$note->set_is_deleted( (bool) $note_row->is_deleted );
isset( $note_row->is_read ) && $note->set_is_read( (bool) $note_row->is_read );
$note->set_layout( $note_row->layout );
$note->set_image( $note_row->image );
$this->read_actions( $note );
$note->set_object_read( true );
* Fires when an admin note is loaded.
* @param int $note_id Note ID.
do_action( 'woocommerce_note_loaded', $note );
throw new \Exception( __( 'Invalid admin note', 'woocommerce' ) );
* Updates a note in the database.
* @param Note $note Admin note.
public function update( &$note ) {
$date_created = $note->get_date_created();
$date_created_timestamp = $date_created->getTimestamp();
$date_created_to_db = gmdate( 'Y-m-d H:i:s', $date_created_timestamp );
$date_reminder = $note->get_date_reminder();
if ( is_null( $date_reminder ) ) {
$date_reminder_to_db = null;
$date_reminder_timestamp = $date_reminder->getTimestamp();
$date_reminder_to_db = gmdate( 'Y-m-d H:i:s', $date_reminder_timestamp );
$wpdb->prefix . 'wc_admin_notes',
'name' => $note->get_name(),
'type' => $note->get_type(),
'locale' => $note->get_locale(),
'title' => $note->get_title(),
'content' => $note->get_content(),
'content_data' => wp_json_encode( $note->get_content_data() ),
'status' => $note->get_status(),
'source' => $note->get_source(),
'date_created' => $date_created_to_db,
'date_reminder' => $date_reminder_to_db,
'is_snoozable' => (int) $note->get_is_snoozable(),
'layout' => $note->get_layout(),
'image' => $note->get_image(),
'is_deleted' => (int) $note->get_is_deleted(),
'is_read' => (int) $note->get_is_read(),
array( 'note_id' => $note->get_id() )
$this->save_actions( $note );
* Fires when an admin note is updated.
* @param int $note_id Note ID.
do_action( 'woocommerce_note_updated', $note->get_id() );
* Deletes a note from the database.
* @param Note $note Admin note.
* @param array $args Array of args to pass to the delete method (not used).
public function delete( &$note, $args = array() ) {
$note_id = $note->get_id();
$wpdb->delete( $wpdb->prefix . 'wc_admin_notes', array( 'note_id' => $note_id ) );
$wpdb->delete( $wpdb->prefix . 'wc_admin_note_actions', array( 'note_id' => $note_id ) );
* Fires when an admin note is deleted.
* @param int $note_id Note ID.
do_action( 'woocommerce_note_deleted', $note_id );
* Read actions from the database.
* @param Note $note Admin note.
private function read_actions( &$note ) {
$db_actions = $wpdb->get_results(
"SELECT action_id, name, label, query, status, actioned_text, nonce_action, nonce_name
FROM {$wpdb->prefix}wc_admin_note_actions
foreach ( $db_actions as $action ) {
$note_actions[] = (object) array(
'id' => (int) $action->action_id,
'label' => $action->label,
'query' => $action->query,
'status' => $action->status,
'actioned_text' => $action->actioned_text,
'nonce_action' => $action->nonce_action,
'nonce_name' => $action->nonce_name,
$note->set_actions( $note_actions );
* Save actions to the database.
* This function clears old actions, then re-inserts new if any changes are found.
* @param Note $note Note object.
private function save_actions( &$note ) {
$changed_props = array_keys( $note->get_changes() );
if ( ! in_array( 'actions', $changed_props, true ) ) {
// Process action removal. Actions are removed from
// the note if they aren't part of the changeset.
// See Note::add_action().
$changed_actions = $note->get_actions( 'edit' );
$actions_to_keep = array();
foreach ( $changed_actions as $action ) {
if ( ! empty( $action->id ) ) {
$actions_to_keep[] = (int) $action->id;
$clear_actions_query = $wpdb->prepare(
"DELETE FROM {$wpdb->prefix}wc_admin_note_actions WHERE note_id = %d",
if ( $actions_to_keep ) {
$clear_actions_query .= sprintf( ' AND action_id NOT IN (%s)', implode( ',', $actions_to_keep ) );
$wpdb->query( $clear_actions_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
// Update/insert the actions in this changeset.
foreach ( $changed_actions as $action ) {
'note_id' => $note->get_id(),
'label' => $action->label,
'query' => $action->query,
'status' => $action->status,
'actioned_text' => $action->actioned_text,
'nonce_action' => $action->nonce_action,
'nonce_name' => $action->nonce_name,
if ( ! empty( $action->id ) ) {
$action_data['action_id'] = $action->id;
$wpdb->prefix . 'wc_admin_note_actions',
// Update actions from DB (to grab new IDs).
$this->read_actions( $note );
* Return an ordered list of notes.
* @param array $args Query arguments.
* @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed.
* @return array An array of objects containing a note id.
public function get_notes( $args = array(), $context = self::WC_ADMIN_NOTE_OPER_GLOBAL ) {
'per_page' => get_option( 'posts_per_page' ),
'orderby' => 'date_created',
$args = wp_parse_args( $args, $defaults );
$offset = $args['per_page'] * ( $args['page'] - 1 );
$where_clauses = $this->get_notes_where_clauses( $args, $context );
// sanitize order and orderby.
$order_by = '`' . str_replace( '`', '', $args['orderby'] ) . '`';
$order_dir = 'asc' === strtolower( $args['order'] ) ? 'ASC' : 'DESC';
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT * FROM {$wpdb->prefix}wc_admin_notes WHERE 1=1{$where_clauses} ORDER BY {$order_by} {$order_dir} LIMIT %d, %d",
return $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
* Return an ordered list of notes, without paging or applying the 'woocommerce_note_where_clauses' filter.
* INTERNAL: This method is not intended to be used by external code, and may change without notice.
* @param array $args Query arguments.
* @return array An array of database records.
public function lookup_notes( $args = array() ) {
'orderby' => 'date_created',
$args = wp_parse_args( $args, $defaults );
$where_clauses = $this->args_to_where_clauses( $args );
// sanitize order and orderby.
$order_by = '`' . str_replace( '`', '', $args['orderby'] ) . '`';
$order_dir = 'asc' === strtolower( $args['order'] ) ? 'ASC' : 'DESC';
$query = "SELECT * FROM {$wpdb->prefix}wc_admin_notes WHERE 1=1{$where_clauses} ORDER BY {$order_by} {$order_dir}";
return $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
* Return a count of notes.
* @param string $type Comma separated list of note types.
* @param string $status Comma separated list of statuses.
* @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed.
* @return string Count of objects with given type, status and context.
public function get_notes_count( $type = array(), $status = array(), $context = self::WC_ADMIN_NOTE_OPER_GLOBAL ) {
$where_clauses = $this->get_notes_where_clauses(
if ( ! empty( $where_clauses ) ) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_admin_notes WHERE 1=1{$where_clauses}" );
return $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wc_admin_notes" );
* Parses the query arguments passed in as arrays and escapes the values.
* @param array $args the query arguments.
* @param string $key the key of the specific argument.
* @param array|null $allowed_types optional allowed_types if only a specific set is allowed.
* @return array the escaped array of argument values.
private function get_escaped_arguments_array_by_key( $args = array(), $key = '', $allowed_types = null ) {
if ( isset( $args[ $key ] ) ) {
foreach ( $args[ $key ] as $args_type ) {
$args_type = trim( $args_type );
$allowed = is_null( $allowed_types ) || in_array( $args_type, $allowed_types, true );
$arg_array[] = sprintf( "'%s'", esc_sql( $args_type ) );
* Return where clauses for getting notes by status and type. For use in both the count and listing queries.
* Applies woocommerce_note_where_clauses filter.
* @uses args_to_where_clauses
* @param array $args Array of args to pass.
* @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed.
* @return string Where clauses for the query.
public function get_notes_where_clauses( $args = array(), $context = self::WC_ADMIN_NOTE_OPER_GLOBAL ) {
$where_clauses = $this->args_to_where_clauses( $args );
* Filter the notes WHERE clause before retrieving the data.
* Allows modification of the notes select criterial.
* @param string $where_clauses The generated WHERE clause.
* @param array $args The original arguments for the request.
* @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed.
return apply_filters( 'woocommerce_note_where_clauses', $where_clauses, $args, $context );
* Return where clauses for notes queries without applying woocommerce_note_where_clauses filter.
* INTERNAL: This method is not intended to be used by external code, and may change without notice.
* @param array $args Array of arguments for query conditionals.
* @return string Where clauses.
protected function args_to_where_clauses( $args = array() ) {
$allowed_types = Note::get_allowed_types();
$where_type_array = $this->get_escaped_arguments_array_by_key( $args, 'type', $allowed_types );
$allowed_statuses = Note::get_allowed_statuses();
$where_status_array = $this->get_escaped_arguments_array_by_key( $args, 'status', $allowed_statuses );
$escaped_is_deleted = '';
if ( isset( $args['is_deleted'] ) ) {
$escaped_is_deleted = esc_sql( $args['is_deleted'] );
$where_name_array = $this->get_escaped_arguments_array_by_key( $args, 'name' );
$where_excluded_name_array = $this->get_escaped_arguments_array_by_key( $args, 'excluded_name' );
$where_source_array = $this->get_escaped_arguments_array_by_key( $args, 'source' );
$escaped_where_types = implode( ',', $where_type_array );
$escaped_where_status = implode( ',', $where_status_array );
$escaped_where_names = implode( ',', $where_name_array );
$escaped_where_excluded_names = implode( ',', $where_excluded_name_array );
$escaped_where_source = implode( ',', $where_source_array );
if ( ! empty( $escaped_where_types ) ) {
$where_clauses .= " AND type IN ($escaped_where_types)";
if ( ! empty( $escaped_where_status ) ) {
$where_clauses .= " AND status IN ($escaped_where_status)";
if ( ! empty( $escaped_where_names ) ) {