diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 4c1bea58934..63fa6290848 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -21,7 +21,7 @@ class WC_Admin_Api_Init { // Hook in data stores. add_filter( 'woocommerce_data_stores', array( 'WC_Admin_Api_Init', 'add_data_stores' ) ); // Add wc-admin report tables to list of WooCommerce tables. - add_filter( 'woocommerce_install_get_tables', array( 'WC_Admin_Api_Init', 'add_report_tables' ) ); + add_filter( 'woocommerce_install_get_tables', array( 'WC_Admin_Api_Init', 'add_tables' ) ); // REST API extensions init. add_action( 'rest_api_init', array( $this, 'rest_api_init' ) ); add_filter( 'rest_endpoints', array( 'WC_Admin_Api_Init', 'filter_rest_endpoints' ), 10, 1 ); @@ -49,7 +49,7 @@ class WC_Admin_Api_Init { require_once dirname( __FILE__ ) . '/class-wc-admin-reports-products-stats-query.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-categories-query.php'; - // Reports data stores. + // Data stores. require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-data-store.php'; require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-orders-data-store.php'; require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-products-data-store.php'; @@ -58,6 +58,11 @@ class WC_Admin_Api_Init { // Data triggers. require_once dirname( __FILE__ ) . '/wc-admin-order-functions.php'; + require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-notes-data-store.php'; + + // CRUD classes. + require_once dirname( __FILE__ ) . '/class-wc-admin-note.php'; + require_once dirname( __FILE__ ) . '/class-wc-admin-notes.php'; } /** @@ -248,17 +253,18 @@ class WC_Admin_Api_Init { 'report-products' => 'WC_Admin_Reports_Products_Data_Store', 'report-products-stats' => 'WC_Admin_Reports_Products_Stats_Data_Store', 'report-categories' => 'WC_Admin_Reports_Categories_Data_Store', + 'admin-note' => 'WC_Admin_Notes_Data_Store', ) ); } /** - * Adds report tables. + * Adds new tables. * * @param array $wc_tables List of WooCommerce tables. * @return array */ - public static function add_report_tables( $wc_tables ) { + public static function add_tables( $wc_tables ) { global $wpdb; return array_merge( @@ -269,6 +275,8 @@ class WC_Admin_Api_Init { "{$wpdb->prefix}wc_order_product_lookup", "{$wpdb->prefix}wc_order_tax_lookup", "{$wpdb->prefix}wc_order_coupon_lookup", + "{$wpdb->prefix}woocommerce_admin_notes", + "{$wpdb->prefix}woocommerce_admin_note_actions", ) ); } @@ -332,7 +340,32 @@ class WC_Admin_Api_Init { KEY order_id (order_id), KEY coupon_id (coupon_id), KEY date_created (date_created) - ) $collate;"; + ) $collate; + CREATE TABLE {$wpdb->prefix}woocommerce_admin_notes ( + note_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + name varchar(255) NOT NULL, + type varchar(20) NOT NULL, + locale varchar(20) NOT NULL, + title longtext NOT NULL, + content longtext NOT NULL, + icon varchar(200) NOT NULL, + content_data longtext NULL default null, + status varchar(200) NOT NULL, + source varchar(200) NOT NULL, + date_created datetime NOT NULL default '0000-00-00 00:00:00', + date_reminder datetime NULL default null, + PRIMARY KEY (note_id) + ) $collate; + CREATE TABLE {$wpdb->prefix}woocommerce_admin_note_actions ( + action_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + note_id BIGINT UNSIGNED NOT NULL, + name varchar(255) NOT NULL, + label varchar(255) NOT NULL, + query longtext NOT NULL, + PRIMARY KEY (action_id), + KEY note_id (note_id) + ) $collate; + "; return $tables; } diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-note.php b/plugins/woocommerce-admin/includes/class-wc-admin-note.php new file mode 100644 index 00000000000..77f5b709bb6 --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-note.php @@ -0,0 +1,474 @@ + '-', + 'type' => self::E_WC_ADMIN_NOTE_INFORMATIONAL, + 'locale' => 'en_US', + 'title' => '-', + 'content' => '-', + 'icon' => 'info', + 'content_data' => array(), + 'status' => self::E_WC_ADMIN_NOTE_UNACTIONED, + 'source' => 'woocommerce', + 'date_created' => '0000-00-00 00:00:00', + 'date_reminder' => '', + 'actions' => array(), + ); + + /** + * Cache group. + * + * @var string + */ + protected $cache_group = 'admin-note'; + + /** + * Note constructor. Loads note data. + * + * @param mixed $data Note data, object, or ID. + */ + public function __construct( $data = '' ) { + parent::__construct( $data ); + + if ( $data instanceof WC_Admin_Note ) { + $this->set_id( absint( $data->get_id() ) ); + } elseif ( is_numeric( $data ) && 'admin-note' === get_post_type( $data ) ) { + $this->set_id( $data ); + } elseif ( is_object( $data ) && ! empty( $data->note_id ) ) { + $this->set_id( $data->note_id ); + $this->set_props( (array) $data ); + $this->set_object_read( true ); + } else { + $this->set_object_read( true ); + } + + $this->data_store = WC_Data_Store::load( 'admin-note' ); + if ( $this->get_id() > 0 ) { + $this->data_store->read( $this ); + } + } + + /* + |-------------------------------------------------------------------------- + | Helpers + |-------------------------------------------------------------------------- + | + | Methods for getting allowed types, statuses. + | + */ + + /** + * Get allowed types. + * + * @return array + */ + public static function get_allowed_types() { + $allowed_types = array( + self::E_WC_ADMIN_NOTE_ERROR, + self::E_WC_ADMIN_NOTE_WARNING, + self::E_WC_ADMIN_NOTE_UPDATE, + self::E_WC_ADMIN_NOTE_INFORMATIONAL, + ); + + return apply_filters( 'woocommerce_admin_note_types', $allowed_types ); + } + + /** + * Get allowed statuses. + * + * @return array + */ + public static function get_allowed_statuses() { + $allowed_statuses = array( + self::E_WC_ADMIN_NOTE_ACTIONED, + self::E_WC_ADMIN_NOTE_UNACTIONED, + ); + + return apply_filters( 'woocommerce_admin_note_statuses', $allowed_statuses ); + } + + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + | + | Methods for getting data from the note object. + | + */ + + /** + * Get note name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_name( $context = 'view' ) { + return $this->get_prop( 'name', $context ); + } + + /** + * Get note type. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_type( $context = 'view' ) { + return $this->get_prop( 'type', $context ); + } + + /** + * Get note locale. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_locale( $context = 'view' ) { + return $this->get_prop( 'locale', $context ); + } + + /** + * Get note title. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_title( $context = 'view' ) { + return $this->get_prop( 'title', $context ); + } + + /** + * Get note content. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_content( $context = 'view' ) { + return $this->get_prop( 'content', $context ); + } + + /** + * Get note icon (Gridicon). + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_icon( $context = 'view' ) { + return $this->get_prop( 'icon', $context ); + } + + /** + * Get note content data (i.e. values that would be needed for re-localization) + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_content_data( $context = 'view' ) { + return $this->get_prop( 'content_data', $context ); + } + + /** + * Get note status. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_status( $context = 'view' ) { + return $this->get_prop( 'status', $context ); + } + + /** + * Get note source. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_source( $context = 'view' ) { + return $this->get_prop( 'source', $context ); + } + + /** + * Get date note was created. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_created( $context = 'view' ) { + return $this->get_prop( 'date_created', $context ); + } + + /** + * Get date on which user should be reminded of the note (if any). + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_reminder( $context = 'view' ) { + return $this->get_prop( 'date_reminder', $context ); + } + + /** + * Get actions on the note (if any). + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_actions( $context = 'view' ) { + return $this->get_prop( 'actions', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + | + | Methods for setting note data. These should not update anything in the + | database itself and should only change what is stored in the class + | object. + | + */ + + /** + * Set note name. + * + * @param string $name Note name. + */ + public function set_name( $name ) { + // Don't allow empty names. + if ( empty( $name ) ) { + $this->error( 'admin_note_invalid_data', __( 'The admin note name prop cannot be empty.', 'wc-admin' ) ); + } + + $this->set_prop( 'name', $name ); + } + + /** + * Set note type. + * + * @param string $type Note type. + */ + public function set_type( $type ) { + if ( empty( $type ) ) { + $this->error( 'admin_note_invalid_data', __( 'The admin note type prop cannot be empty.', 'wc-admin' ) ); + } + + if ( ! in_array( $type, self::get_allowed_types() ) ) { + $this->error( + 'admin_note_invalid_data', + sprintf( + /* translators: %s: admin note type. */ + __( 'The admin note type prop (%s) is not one of the supported types.', 'wc-admin' ), + $type + ) + ); + } + + $this->set_prop( 'type', $type ); + } + + /** + * Set note locale. + * + * @param string $locale Note locale. + */ + public function set_locale( $locale ) { + if ( empty( $locale ) ) { + $this->error( 'admin_note_invalid_data', __( 'The admin note locale prop cannot be empty.', 'wc-admin' ) ); + } + + $this->set_prop( 'locale', $locale ); + } + + /** + * Set note title. + * + * @param string $title Note title. + */ + public function set_title( $title ) { + if ( empty( $title ) ) { + $this->error( 'admin_note_invalid_data', __( 'The admin note title prop cannot be empty.', 'wc-admin' ) ); + } + + $this->set_prop( 'title', $title ); + } + + /** + * Set note content. + * + * @param string $content Note content. + */ + public function set_content( $content ) { + $allowed_html = array( + 'br' => array(), + 'em' => array(), + 'strong' => array(), + ); + + $content = wp_kses( $content, $allowed_html ); + + if ( empty( $content ) ) { + $this->error( 'admin_note_invalid_data', __( 'The admin note content prop cannot be empty.', 'wc-admin' ) ); + } + + $this->set_prop( 'content', $content ); + } + + /** + * Set note icon (Gridicon). + * + * @param string $icon Note icon. + */ + public function set_icon( $icon ) { + if ( empty( $icon ) ) { + $this->error( 'admin_note_invalid_data', __( 'The admin note icon prop cannot be empty.', 'wc-admin' ) ); + } + + $this->set_prop( 'icon', $icon ); + } + + /** + * Set note data for potential re-localization. + * + * @param object $content_data Note data. + */ + public function set_content_data( $content_data ) { + $allowed_type = false; + + // Make sure $content_data is stdClass Object or an array. + if ( ! ( $content_data instanceof stdClass ) ) { + $this->error( 'admin_note_invalid_data', __( 'The admin note content_data prop must be an instance of stdClass.', 'wc-admin' ) ); + } + + $this->set_prop( 'content_data', $content_data ); + } + + /** + * Set note status. + * + * @param string $status Note status. + */ + public function set_status( $status ) { + if ( empty( $status ) ) { + $this->error( 'admin_note_invalid_data', __( 'The admin note status prop cannot be empty.', 'wc-admin' ) ); + } + + if ( ! in_array( $status, self::get_allowed_statuses() ) ) { + $this->error( + 'admin_note_invalid_data', + sprintf( + /* translators: %s: admin note status property. */ + __( 'The admin note status prop (%s) is not one of the supported statuses.', 'wc-admin' ), + $status + ) + ); + } + + $this->set_prop( 'status', $status ); + } + + /** + * Set note source. + * + * @param string $source Note source. + */ + public function set_source( $source ) { + if ( empty( $source ) ) { + $this->error( 'admin_note_invalid_data', __( 'The admin note source prop cannot be empty.', 'wc-admin' ) ); + } + + $this->set_prop( 'source', $source ); + } + + /** + * Set date note was created. NULL is not allowed + * + * @param string|integer $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. + */ + public function set_date_created( $date ) { + if ( empty( $date ) ) { + $this->error( 'admin_note_invalid_data', __( 'The admin note date prop cannot be empty.', 'wc-admin' ) ); + } + + $this->set_date_prop( 'date_created', $date ); + } + + /** + * Set date admin should be reminded of note. NULL IS allowed + * + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. + */ + public function set_date_reminder( $date ) { + $this->set_date_prop( 'date_reminder', $date ); + } + + /** + * Add an action to the note + * + * @param string $name Label name (not presented to user). + * @param string $label Note label (e.g. presented as button label). + * @param string $query Note query (for redirect). + */ + public function add_action( $name, $label, $query ) { + $name = wc_clean( $name ); + $label = wc_clean( $label ); + $query = wc_clean( $query ); + + if ( empty( $name ) ) { + $this->error( 'admin_note_invalid_data', __( 'The admin note action name prop cannot be empty.', 'wc-admin' ) ); + } + + if ( empty( $label ) ) { + $this->error( 'admin_note_invalid_data', __( 'The admin note action label prop cannot be empty.', 'wc-admin' ) ); + } + + if ( empty( $query ) ) { + $this->error( 'admin_note_invalid_data', __( 'The admin note action query prop cannot be empty.', 'wc-admin' ) ); + } + + $action = array( + 'name' => $name, + 'label' => $label, + 'query' => $query, + ); + + $note_actions = $this->get_prop( 'actions', 'edit' ); + $note_actions[] = (object) $action; + $this->set_prop( 'actions', $note_actions ); + } +} diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-notes.php b/plugins/woocommerce-admin/includes/class-wc-admin-notes.php new file mode 100644 index 00000000000..550a448eec0 --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-notes.php @@ -0,0 +1,61 @@ +get_notes(); + $notes = array(); + foreach ( (array) $raw_notes as $raw_note ) { + $note = new WC_Admin_Note( $raw_note ); + $note_id = $note->get_id(); + $notes[ $note_id ] = $note->get_data(); + $notes[ $note_id ]['name'] = $note->get_name( $context ); + $notes[ $note_id ]['type'] = $note->get_type( $context ); + $notes[ $note_id ]['locale'] = $note->get_locale( $context ); + $notes[ $note_id ]['title'] = $note->get_title( $context ); + $notes[ $note_id ]['content'] = $note->get_content( $context ); + $notes[ $note_id ]['icon'] = $note->get_icon( $context ); + $notes[ $note_id ]['content_data'] = $note->get_content_data( $context ); + $notes[ $note_id ]['status'] = $note->get_status( $context ); + $notes[ $note_id ]['source'] = $note->get_source( $context ); + $notes[ $note_id ]['date_created'] = $note->get_date_created( $context ); + $notes[ $note_id ]['date_reminder'] = $note->get_date_reminder( $context ); + $notes[ $note_id ]['actions'] = $note->get_actions( $context ); + } + return $notes; + } + + /** + * Get admin note using it's ID + * + * @param int $note_id Note ID. + * @return WC_Admin_Note|bool + */ + public static function get_note( $note_id ) { + if ( false !== $note_id ) { + try { + return new WC_Admin_Note( $note_id ); + } catch ( Exception $e ) { + return false; + } + } + return false; + } +} diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-notes-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-notes-data-store.php new file mode 100644 index 00000000000..000efcd9df3 --- /dev/null +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-notes-data-store.php @@ -0,0 +1,208 @@ +set_date_created( $date_created ); + + global $wpdb; + + $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(), + 'icon' => $note->get_icon(), + 'status' => $note->get_status(), + 'source' => $note->get_source(), + ); + + $encoding_options = defined( 'JSON_FORCE_OBJECT' ) ? JSON_FORCE_OBJECT : 0; + + $note_to_be_inserted['content_data'] = wp_json_encode( $note->get_content_data(), $encoding_options ); + $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 . 'woocommerce_admin_notes', $note_to_be_inserted ); + $note_id = $wpdb->insert_id; + $note->set_id( $note_id ); + $note->save_meta_data(); + $this->save_actions( $note ); + $note->apply_changes(); + + do_action( 'woocommerce_new_note', $note_id ); + } + + /** + * Method to read a note. + * + * @param WC_Admin_Note $note Admin note. + * @throws Exception Throws exception when invalid data is found. + */ + public function read( &$note ) { + global $wpdb; + + $note->set_defaults(); + $note_row = false; + + $note_id = $note->get_id(); + if ( 0 !== $note_id || '0' !== $note_id ) { + $note_row = $wpdb->get_row( + $wpdb->prepare( + "SELECT name, type, locale, title, content, icon, content_data, status, source, date_created, date_reminder FROM {$wpdb->prefix}woocommerce_admin_notes WHERE note_id = %d LIMIT 1", + $note->get_id() + ) + ); + } + + if ( 0 === $note->get_id() || '0' === $note->get_id() ) { + $this->read_actions( $note ); + $note->read_meta_data(); + $note->set_object_read( true ); + do_action( 'woocommerce_admin_note_loaded', $note ); + } elseif ( $note_row ) { + $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 ); + $note->set_icon( $note_row->icon ); + $note->set_content_data( json_decode( $note_row->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 ); + $this->read_actions( $note ); + $note->read_meta_data(); + $note->set_object_read( true ); + do_action( 'woocommerce_admin_note_loaded', $note ); + } else { + throw new Exception( __( 'Invalid data store for admin note.', 'wc-admin' ) ); + } + } + + /** + * Updates a note in the database. + * + * @param WC_Admin_Note $note Admin note. + */ + public function update( &$note ) { + global $wpdb; + $encoding_options = defined( 'JSON_FORCE_OBJECT' ) ? JSON_FORCE_OBJECT : 0; + + if ( $note->get_id() ) { + $wpdb->update( + $wpdb->prefix . 'woocommerce_admin_notes', array( + 'name' => $note->get_name(), + 'type' => $note->get_type(), + 'locale' => $note->get_locale(), + 'title' => $note->get_title(), + 'content' => $note->get_content(), + 'icon' => $note->get_icon(), + 'content_data' => wp_json_encode( $note->get_content_data(), $encoding_options ), + 'status' => $note->get_status(), + 'source' => $note->get_source(), + 'date_created' => $note->get_date_created(), + 'date_reminder' => $note->get_date_reminder(), + ), array( 'note_id' => $note->get_id() ) + ); + } + + $note->save_meta_data(); + $this->save_actions( $note ); + $note->apply_changes(); + do_action( 'woocommerce_update_note', $note->get_id() ); + } + + /** + * Deletes a note from the database. + * + * @param WC_Admin_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(); + if ( $note->get_id() ) { + global $wpdb; + $wpdb->delete( $wpdb->prefix . 'woocommerce_admin_notes', array( 'note_id' => $note_id ) ); + $wpdb->delete( $wpdb->prefix . 'woocommerce_admin_note_actions', array( 'note_id' => $note_id ) ); + $note->set_id( null ); + } + do_action( 'woocommerce_trash_note', $id ); + } + + /** + * Read actions from the database. + * + * @param WC_Admin_Note $note Admin note. + */ + private function read_actions( &$note ) { + global $wpdb; + + $actions = $wpdb->get_results( + $wpdb->prepare( + "SELECT name, label, query FROM {$wpdb->prefix}woocommerce_admin_note_actions WHERE note_id = %d", + $note->get_id() + ) + ); + if ( $actions ) { + foreach ( $actions as $action ) { + $note->add_action( $action->name, $action->label, $action->query ); + } + } + } + + /** + * Save actions to the database. + * This function clears old actions, then re-inserts new if any changes are found. + * + * @param WC_Admin_Note $note Note object. + * + * @return bool|void + */ + private function save_actions( &$note ) { + $changed_props = array_keys( $note->get_changes() ); + if ( ! in_array( 'actions', $changed_props, true ) ) { + return false; + } + + global $wpdb; + $wpdb->delete( $wpdb->prefix . 'woocommerce_admin_note_actions', array( 'note_id' => $note->get_id() ) ); + foreach ( $note->get_actions( 'edit' ) as $action ) { + $wpdb->insert( + $wpdb->prefix . 'woocommerce_admin_note_actions', array( + 'note_id' => $note->get_id(), + 'name' => $action->name, + 'label' => $action->label, + 'query' => $action->query, + ) + ); + } + } + + /** + * Return an ordered list of notes. + * + * @return array An array of objects containing a note id. + */ + public function get_notes() { + global $wpdb; + return $wpdb->get_results( "SELECT note_id, title, content FROM {$wpdb->prefix}woocommerce_admin_notes order by note_id ASC;" ); + } +}