From 3c276cb56f9ac2a9f7e67d60363eda50bd795dbd Mon Sep 17 00:00:00 2001 From: Shady Sharaf Date: Wed, 30 Apr 2014 09:59:09 +0200 Subject: [PATCH] Add new WPDB Storage Adapter Add new WPDB Storage Adapter, instruct Stream to use it Provide a filtering mechanism to alter storage engine used --- includes/db/base.php | 101 +++++++++ includes/db/wpdb.php | 528 +++++++++++++++++++++++++++++++++++++++++++ includes/record.php | 56 +++++ stream.php | 17 +- 4 files changed, 697 insertions(+), 5 deletions(-) create mode 100644 includes/db/base.php create mode 100644 includes/db/wpdb.php create mode 100644 includes/record.php diff --git a/includes/db/base.php b/includes/db/base.php new file mode 100644 index 000000000..e57832f1c --- /dev/null +++ b/includes/db/base.php @@ -0,0 +1,101 @@ + get_option( 'posts_per_page' ), + 'paged' => 1, + // Search param + 'search' => null, + 'search_field' => null, + // Stream core fields filtering + 'type' => 'stream', + 'object_id' => null, + 'ip' => null, + 'site_id' => is_multisite() ? get_current_site()->id : 1, + 'blog_id' => is_network_admin() ? null : get_current_blog_id(), + // Author params + 'author' => null, + 'author_role' => null, + // Date-based filters + 'date' => null, + 'date_from' => null, + 'date_to' => null, + // Visibility filters + 'visibility' => null, + // __in params + 'record_greater_than' => null, + 'record__in' => array(), + 'record__not_in' => array(), + 'record_parent' => '', + 'record_parent__in' => array(), + 'record_parent__not_in' => array(), + 'author__in' => array(), + 'author__not_in' => array(), + 'author_role__in' => array(), + 'author_role__not_in' => array(), + 'ip__in' => array(), + 'ip__not_in' => array(), + // Order + 'order' => 'desc', + 'orderby' => 'ID', + // Meta/Taxonomy sub queries + 'meta_query' => array(), + 'context_query' => array(), + // Fields selection + 'fields' => '', + 'distinct' => false, + 'ignore_context' => null, + // Hide records that match the exclude rules + 'hide_excluded' => ! empty( WP_Stream_Settings::$options['exclude_hide_previous_records'] ), + ); + + $args = wp_parse_args( $args, $defaults ); + + /** + * Filter allows additional arguments to query $args + * + * @param array Array of query arguments + * @return array Updated array of query arguments + */ + $args = apply_filters( 'wp_stream_query_args', $args ); + + if ( true === $args['hide_excluded'] ) { + // Remove record of excluded connector + $args['connector__not_in'] = WP_Stream_Settings::get_excluded_by_key( 'connectors' ); + + // Remove record of excluded context + $args['context__not_in'] = WP_Stream_Settings::get_excluded_by_key( 'contexts' ); + + // Remove record of excluded actions + $args['action__not_in'] = WP_Stream_Settings::get_excluded_by_key( 'actions' ); + + // Remove record of excluded author + $args['author__not_in'] = WP_Stream_Settings::get_excluded_by_key( 'authors_and_roles' ); + + // Remove record of excluded ip + $args['ip__not_in'] = WP_Stream_Settings::get_excluded_by_key( 'ip_addresses' ); + } + + return $args; + } + + function query( $args ) { + throw new Exception( 'Database drivers should all implement `query` method.' ); + } + + function store( $data ) { + throw new Exception( 'Database drivers should all implement `store` method.' ); + } + + function delete( $args ) { + throw new Exception( 'Database drivers should all implement `delete` method.' ); + } + + function reset() { + throw new Exception( 'Database drivers should all implement `reset` method.' ); + } + +} diff --git a/includes/db/wpdb.php b/includes/db/wpdb.php new file mode 100644 index 000000000..b49d20f13 --- /dev/null +++ b/includes/db/wpdb.php @@ -0,0 +1,528 @@ +base_prefix ); + + self::$table = $prefix . 'stream'; + self::$table_meta = $prefix . 'stream_meta'; + self::$table_context = $prefix . 'stream_context'; + + $wpdb->stream = self::$table; + $wpdb->streammeta = self::$table_meta; + $wpdb->streamcontext = self::$table_context; + + // Hack for get_metadata + $wpdb->recordmeta = self::$table_meta; + } + + /** + * Public getter to return table names; + * + * @return array + */ + public function get_table_names() { + return array( + self::$table, + self::$table_meta, + self::$table_context, + ); + } + + /** + * Store a record data + * + * TODO: Make/Use an update method, we currently only have an insert method + * + * @param array $recordarr Record instance + * @return integer ID of record if successful + */ + public function store( $recordarr ) { + return $this->insert( $recordarr ); + } + + public function insert( $recordarr ) { + global $wpdb; + + /** + * Filter allows modification of record information + * + * @param array array of record information + * @return array udpated array of record information + */ + $recordarr = apply_filters( 'wp_stream_record_array', $recordarr ); + + // Allow extensions to handle the saving process + if ( empty( $recordarr ) ) { + return; + } + + $fields = array( 'object_id', 'site_id', 'blog_id', 'author', 'author_role', 'created', 'summary', 'parent', 'visibility', 'ip' ); + $data = array_intersect_key( $recordarr, array_flip( $fields ) ); + $data = array_filter( $data ); + + // TODO: Check/Validate *required* fields + + $result = $wpdb->insert( + self::$table, + $data + ); + + if ( 1 === $result ) { + $record_id = $wpdb->insert_id; + } else { + /** + * Action Hook that fires on an error during post insertion + * + * @param int $record_id Record being inserted + */ + do_action( 'wp_stream_post_insert_error', $result ); + return $result; + } + + $this->prev_record = $record_id; + + $connector = $recordarr['connector']; + + foreach ( (array) $recordarr['contexts'] as $context => $action ) { + $this->insert_context( $record_id, $connector, $context, $action ); + } + + foreach ( $recordarr['meta'] as $key => $vals ) { + foreach ( (array) $vals as $val ) { + $val = maybe_serialize( $val ); + $this->insert_meta( $record_id, $key, $val ); + } + } + + /** + * Fires when A Post is inserted + * + * @param int $record_id Inserted record ID + * @param array $recordarr Array of information on this record + */ + do_action( 'wp_stream_post_inserted', $record_id, $recordarr ); + + return $record_id; + } + + private function insert_context( $record_id, $connector, $context, $action ) { + global $wpdb; + + $result = $wpdb->insert( + self::$table_context, + array( + 'record_id' => $record_id, + 'connector' => $connector, + 'context' => $context, + 'action' => $action, + ) + ); + + return $result; + } + + private function insert_meta( $record_id, $key, $val ) { + global $wpdb; + + $result = $wpdb->insert( + self::$table_meta, + array( + 'record_id' => $record_id, + 'meta_key' => $key, + 'meta_value' => $val, + ) + ); + + return $result; + } + + public function query( $args ) { + global $wpdb; + + $args = $this->parse( $args ); + + $join = ''; + $where = ''; + + // Only join with context table for correct types of records + if ( ! $args['ignore_context'] ) { + $join = sprintf( + ' INNER JOIN %1$s ON ( %1$s.record_id = %2$s.ID )', + $wpdb->streamcontext, + $wpdb->stream + ); + } + + /** + * PARSE CORE FILTERS + */ + if ( $args['object_id'] ) { + $where .= $wpdb->prepare( " AND $wpdb->stream.object_id = %d", $args['object_id'] ); + } + + if ( $args['type'] ) { + $where .= $wpdb->prepare( " AND $wpdb->stream.type = %s", $args['type'] ); + } + + if ( $args['ip'] ) { + $where .= $wpdb->prepare( " AND $wpdb->stream.ip = %s", wp_stream_filter_var( $args['ip'], FILTER_VALIDATE_IP ) ); + } + + if ( is_numeric( $args['site_id'] ) ) { + $where .= $wpdb->prepare( " AND $wpdb->stream.site_id = %d", $args['site_id'] ); + } + + if ( is_numeric( $args['blog_id'] ) ) { + $where .= $wpdb->prepare( " AND $wpdb->stream.blog_id = %d", $args['blog_id'] ); + } + + if ( $args['search'] ) { + $search_field = $args['search_field']; + $where .= $wpdb->prepare( + " AND $wpdb->stream.{$search_field} LIKE %s", + ( false === strpos( $args['search'], '%' ) ) ? "%{$args['search']}%" : $args['search'] + ); + } + + if ( $args['author'] || '0' === $args['author'] ) { + $where .= $wpdb->prepare( " AND $wpdb->stream.author = %d", (int) $args['author'] ); + } + + if ( $args['author_role'] ) { + $where .= $wpdb->prepare( " AND $wpdb->stream.author_role = %s", $args['author_role'] ); + } + + if ( $args['visibility'] ) { + $where .= $wpdb->prepare( " AND $wpdb->stream.visibility = %s", $args['visibility'] ); + } + + /** + * PARSE DATE FILTERS + */ + if ( $args['date'] ) { + $where .= $wpdb->prepare( " AND DATE($wpdb->stream.created) = %s", $args['date'] ); + } else { + if ( $args['date_from'] ) { + $where .= $wpdb->prepare( " AND DATE($wpdb->stream.created) >= %s", $args['date_from'] ); + } + if ( $args['date_to'] ) { + $where .= $wpdb->prepare( " AND DATE($wpdb->stream.created) <= %s", $args['date_to'] ); + } + } + + /** + * PARSE __IN PARAM FAMILY + */ + if ( $args['record_greater_than'] ) { + $where .= $wpdb->prepare( " AND $wpdb->stream.ID > %d", (int) $args['record_greater_than'] ); + } + + if ( $args['record__in'] ) { + $record__in = array_filter( (array) $args['record__in'], 'is_numeric' ); + if ( ! empty( $record__in ) ) { + $record__in_format = '(' . join( ',', array_fill( 0, count( $record__in ), '%d' ) ) . ')'; + $where .= $wpdb->prepare( " AND $wpdb->stream.ID IN {$record__in_format}", $record__in ); + } + } + + if ( $args['record__not_in'] ) { + $record__not_in = array_filter( (array) $args['record__not_in'], 'is_numeric' ); + if ( ! empty( $record__not_in ) ) { + $record__not_in_format = '(' . join( ',', array_fill( 0, count( $record__not_in ), '%d' ) ) . ')'; + $where .= $wpdb->prepare( " AND $wpdb->stream.ID NOT IN {$record__not_in_format}", $record__not_in ); + } + } + + if ( $args['record_parent'] ) { + $where .= $wpdb->prepare( " AND $wpdb->stream.parent = %d", (int) $args['record_parent'] ); + } + + if ( $args['record_parent__in'] ) { + $record_parent__in = array_filter( (array) $args['record_parent__in'], 'is_numeric' ); + if ( ! empty( $record_parent__in ) ) { + $record_parent__in_format = '(' . join( ',', array_fill( 0, count( $record_parent__in ), '%d' ) ) . ')'; + $where .= $wpdb->prepare( " AND $wpdb->stream.parent IN {$record_parent__in_format}", $record_parent__in ); + } + } + + if ( $args['record_parent__not_in'] ) { + $record_parent__not_in = array_filter( (array) $args['record_parent__not_in'], 'is_numeric' ); + if ( ! empty( $record_parent__not_in ) ) { + $record_parent__not_in_format = '(' . join( ',', array_fill( 0, count( $record_parent__not_in ), '%d' ) ) . ')'; + $where .= $wpdb->prepare( " AND $wpdb->stream.parent NOT IN {$record_parent__not_in_format}", $record_parent__not_in ); + } + } + + if ( $args['author__in'] ) { + $author__in = array_filter( (array) $args['author__in'], 'is_numeric' ); + if ( ! empty( $author__in ) ) { + $author__in_format = '(' . join( ',', array_fill( 0, count( $author__in ), '%d' ) ) . ')'; + $where .= $wpdb->prepare( " AND $wpdb->stream.author IN {$author__in_format}", $author__in ); + } + } + + if ( $args['author__not_in'] ) { + $author__not_in = array_filter( (array) $args['author__not_in'], 'is_numeric' ); + if ( ! empty( $author__not_in ) ) { + $author__not_in_format = '(' . join( ',', array_fill( 0, count( $author__not_in ), '%d' ) ) . ')'; + $where .= $wpdb->prepare( " AND $wpdb->stream.author NOT IN {$author__not_in_format}", $author__not_in ); + } + } + + if ( $args['author_role__in'] ) { + if ( ! empty( $args['author_role__in'] ) ) { + $author_role__in = '(' . join( ',', array_fill( 0, count( $args['author_role__in'] ), '%s' ) ) . ')'; + $where .= $wpdb->prepare( " AND $wpdb->stream.author_role IN {$author_role__in}", $args['author_role__in'] ); + } + } + + if ( $args['author_role__not_in'] ) { + if ( ! empty( $args['author_role__not_in'] ) ) { + $author_role__not_in = '(' . join( ',', array_fill( 0, count( $args['author_role__not_in'] ), '%s' ) ) . ')'; + $where .= $wpdb->prepare( " AND $wpdb->stream.author_role NOT IN {$author_role__not_in}", $args['author_role__not_in'] ); + } + } + + if ( $args['ip__in'] ) { + if ( ! empty( $args['ip__in'] ) ) { + $ip__in = '(' . join( ',', array_fill( 0, count( $args['ip__in'] ), '%s' ) ) . ')'; + $where .= $wpdb->prepare( " AND $wpdb->stream.ip IN {$ip__in}", $args['ip__in'] ); + } + } + + if ( $args['ip__not_in'] ) { + if ( ! empty( $args['ip__not_in'] ) ) { + $ip__not_in = '(' . join( ',', array_fill( 0, count( $args['ip__not_in'] ), '%s' ) ) . ')'; + $where .= $wpdb->prepare( " AND $wpdb->stream.ip NOT IN {$ip__not_in}", $args['ip__not_in'] ); + } + } + + /** + * PARSE META QUERY PARAMS + */ + $meta_query = new WP_Meta_Query; + $meta_query->parse_query_vars( $args ); + + if ( ! empty( $meta_query->queries ) ) { + $mclauses = $meta_query->get_sql( 'stream', $wpdb->stream, 'ID' ); + $join .= str_replace( 'stream_id', 'record_id', $mclauses['join'] ); + $where .= str_replace( 'stream_id', 'record_id', $mclauses['where'] ); + } + + /** + * PARSE CONTEXT PARAMS + */ + if ( ! $args['ignore_context'] ) { + $context_query = new WP_Stream_Context_Query( $args ); + $cclauses = $context_query->get_sql(); + $join .= $cclauses['join']; + $where .= $cclauses['where']; + } + + /** + * PARSE PAGINATION PARAMS + */ + $page = intval( $args['paged'] ); + $perpage = intval( $args['records_per_page'] ); + + if ( $perpage >= 0 ) { + $offset = ($page - 1) * $perpage; + $limits = "LIMIT $offset, {$perpage}"; + } else { + $limits = ''; + } + + /** + * PARSE ORDER PARAMS + */ + $order = esc_sql( $args['order'] ); + $orderby = esc_sql( $args['orderby'] ); + $orderable = array( 'ID', 'site_id', 'blog_id', 'object_id', 'author', 'author_role', 'summary', 'visibility', 'parent', 'type', 'created' ); + + if ( in_array( $orderby, $orderable ) ) { + $orderby = $wpdb->stream . '.' . $orderby; + } elseif ( in_array( $orderby, array( 'connector', 'context', 'action' ) ) ) { + $orderby = $wpdb->streamcontext . '.' . $orderby; + } elseif ( 'meta_value_num' === $orderby && ! empty( $args['meta_key'] ) ) { + $orderby = "CAST($wpdb->streammeta.meta_value AS SIGNED)"; + } elseif ( 'meta_value' === $orderby && ! empty( $args['meta_key'] ) ) { + $orderby = "$wpdb->streammeta.meta_value"; + } else { + $orderby = "$wpdb->stream.ID"; + } + $orderby = 'ORDER BY ' . $orderby . ' ' . $order; + + /** + * PARSE FIELDS PARAMETER + */ + $fields = array_filter( explode( ',', $args['fields'] ) ); + $fields = array_intersect( $fields, array_keys( get_class_vars( 'WP_Stream_Record' ) ) ); + $select = "$wpdb->stream.*"; + + if ( ! $args['ignore_context'] ) { + $select .= ", $wpdb->streamcontext.context, $wpdb->streamcontext.action, $wpdb->streamcontext.connector"; + } + + if ( ! empty( $fields ) ) { + $select = array(); + foreach ( $fields as $field ) { + // Escape 'meta' and 'contexts' fields + if ( in_array( $field, array( 'meta', 'contexts' ) ) ) { + continue; + } + $select[] = "{$wpdb->stream}.{$field}"; + } + $select = implode( ', ', $select ); + } + + if ( 1 === count( $fields ) && $args['distinct'] ) { + $select = 'DISTINCT ' . $select; + } elseif ( ! empty( $fields ) ) { + $select .= ", $wpdb->stream.ID"; + } + + /** + * BUILD UP THE FINAL QUERY + */ + $sql = "SELECT SQL_CALC_FOUND_ROWS $select + FROM $wpdb->stream + $join + WHERE 1=1 $where + $orderby + $limits"; + + /** + * Allows developers to change final SQL of Stream Query + * + * @param string $sql SQL statement + * @param array $args Arguments passed to query + * @return string + */ + $sql = apply_filters( 'wp_stream_query', $sql, $args ); + + $results = $wpdb->get_results( $sql ); + $this->found_rows = $wpdb->get_var( 'SELECT FOUND_ROWS()' ); + + if ( in_array( 'meta', $fields ) && is_array( $results ) && $results ) { + $ids = array_map( 'absint', wp_list_pluck( $results, 'ID' ) ); + $sql_meta = sprintf( + "SELECT * FROM $wpdb->streammeta WHERE record_id IN ( %s )", + implode( ',', $ids ) + ); + + $meta = $wpdb->get_results( $sql_meta ); + $ids_f = array_flip( $ids ); + + foreach ( $meta as $meta_record ) { + $results[ $ids_f[ $meta_record->record_id ] ]->meta[ $meta_record->meta_key ][] = $meta_record->meta_value; + } + } + + return $results; + } + + public function delete( $args ) { + global $wpdb; + + if ( $args ) { + // Only get IDs + $args = array_merge( + $args, + array( + 'fields' => 'ID', + 'records_per_page' => -1, + ) + ); + $records = $this->query( $args ); + $ids = wp_list_pluck( $records, 'ID' ); + + if ( ! $ids ) { + return false; + } + $where = sprintf( 'ID IN ( %s )', implode( ',', $ids ) ); + } else { + $where = '1=1'; + } + + // Remove records, and all of their meta/context data + $wpdb->query( "DELETE FROM $wpdb->stream WHERE $where" ); + $where = str_replace( 'ID', 'record_id', $where ); + $wpdb->query( "DELETE FROM $wpdb->streammeta WHERE $where" ); + $wpdb->query( "DELETE FROM $wpdb->streamcontext WHERE $where" ); + } + + public function reset() { + // Delete all tables + foreach ( $this->get_table_names() as $table ) { + $wpdb->query( "DROP TABLE $table" ); + } + } + + /** + * Returns array of existing values for requested column. + * Used to fill search filters with only used items, instead of all items. + * + * GROUP BY allows query to find just the first occurance of each value in the column, + * increasing the efficiency of the query. + * + * @todo increase security against injections + * + * @see assemble_records + * @since 1.0.4 + * @param string Requested Column (i.e., 'context') + * @param string Requested Table + * @return array Array of items to be output to select dropdowns + */ + public function get_existing_records( $column, $table = '' ) { + global $wpdb; + + switch ( $table ) { + case 'stream' : + $values = $wpdb->get_col( "SELECT DISTINCT {$column} FROM {$wpdb->stream}" ); + break; + case 'meta' : + $values = $wpdb->get_col( "SELECT DISTINCT {$column} FROM {$wpdb->streammeta}" ); + break; + default: + $values = $wpdb->get_col( "SELECT DISTINCT {$column} FROM {$wpdb->streamcontext}" ); + break; + } + + if ( is_array( $values ) && ! empty( $values ) ) { + return array_combine( $values, $values ); + } else { + $column = sprintf( 'stream_%s', $column ); + return isset( WP_Stream_Connectors::$term_labels[ $column ] ) ? WP_Stream_Connectors::$term_labels[ $column ] : array(); + } + } + + /** + * Get total count of the last query using query() method + * + * @return integer Total item count + */ + public function get_found_rows() { + return $this->found_rows; + } + +} + +WP_Stream::$db = new WP_Stream_DB_WPDB(); diff --git a/includes/record.php b/includes/record.php new file mode 100644 index 000000000..64f833dfe --- /dev/null +++ b/includes/record.php @@ -0,0 +1,56 @@ +load( $id ); + } + } + + public function load( $id ) { + $records = WP_Stream::get_instance()->db->query( array( 'id' => $id ) ); + + if ( $record ) { + $this->populate( $record ); + } + } + + public function save() { + if ( ! $this->validate() ) { + return new WP_Error( 'validation-error', __( 'Could not validate record data.', 'stream' ) ); + } + + return WP_Stream::get_instance()->db->store( (array) $this ); + } + + public function populate( array $data ) { + $keys = get_class_vars( self ); + $data = array_intersect_key( $raw, $valid ); + foreach ( $data as $key => $val ) { + $this->{$key} = $val; + } + } + + public function validate() { + return true; + } + +} diff --git a/stream.php b/stream.php index d34a3f612..6181327ad 100755 --- a/stream.php +++ b/stream.php @@ -48,7 +48,7 @@ class WP_Stream { /** * @var WP_Stream_DB */ - public $db = null; + public static $db = null; /** * @var WP_Stream_Network @@ -67,9 +67,16 @@ private function __construct() { // Load filters polyfill require_once WP_STREAM_INC_DIR . 'filter-input.php'; - // Load DB helper class - require_once WP_STREAM_INC_DIR . 'db.php'; - $this->db = new WP_Stream_DB; + // Load DB helper interface/class + require_once WP_STREAM_INC_DIR . 'record.php'; + require_once WP_STREAM_INC_DIR . 'db/base.php'; + $driver = apply_filters( 'wp_stream_db_adapter', 'wpdb' ); + if ( file_exists( WP_STREAM_INC_DIR . "db/$driver.php" ) ) { + require_once WP_STREAM_INC_DIR . "db/$driver.php"; + } + if ( ! self::$db ) { + throw new Exception( __( 'Could not load chosen DB driver.', 'stream' ) ); + } // Check DB and add message if not present add_action( 'init', array( $this, 'verify_database_present' ) ); @@ -197,7 +204,7 @@ public function verify_database_present() { $uninstall_message = ''; // Check if all needed DB is present - foreach ( $this->db->get_table_names() as $table_name ) { + foreach ( self::$db->get_table_names() as $table_name ) { if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_name'" ) !== $table_name ) { $database_message .= sprintf( '%s %s', __( 'The following table is not present in the WordPress database:', 'stream' ), $table_name ); }