diff --git a/classes/class-admin.php b/classes/class-admin.php index 98749ee74..df8cf4461 100644 --- a/classes/class-admin.php +++ b/classes/class-admin.php @@ -162,8 +162,7 @@ public function __construct( $plugin ) { add_action( 'wp_ajax_wp_stream_reset', array( $this, 'wp_ajax_reset' ) ); // Uninstall Streams and Deactivate plugin. - $uninstall = new Uninstall( $this->plugin ); - add_action( 'wp_ajax_wp_stream_uninstall', array( $uninstall, 'uninstall' ) ); + $uninstall = $this->plugin->db->driver->purge_storage( $this->plugin ); // Auto purge setup. add_action( 'wp_loaded', array( $this, 'purge_schedule_setup' ) ); diff --git a/classes/class-db-driver-wpdb.php b/classes/class-db-driver-wpdb.php new file mode 100755 index 000000000..ed5882b96 --- /dev/null +++ b/classes/class-db-driver-wpdb.php @@ -0,0 +1,168 @@ +query = new Query( $this ); + + global $wpdb; + $prefix = apply_filters( 'wp_stream_db_tables_prefix', $wpdb->base_prefix ); + + $this->table = $prefix . 'stream'; + $this->table_meta = $prefix . 'stream_meta'; + + $wpdb->stream = $this->table; + $wpdb->streammeta = $this->table_meta; + + // Hack for get_metadata + $wpdb->recordmeta = $this->table_meta; + } + + /** + * Insert a record. + * + * @param array $data Data to insert. + * + * @return int + */ + public function insert_record( $data ) { + global $wpdb; + + if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) { + return false; + } + + $meta = $data['meta']; + unset( $data['meta'] ); + + $result = $wpdb->insert( $this->table, $data ); + if ( ! $result ) { + return false; + } + + $record_id = $wpdb->insert_id; + + // Insert record meta + foreach ( (array) $meta as $key => $vals ) { + foreach ( (array) $vals as $val ) { + $this->insert_meta( $record_id, $key, $val ); + } + } + + return $record_id; + } + + /** + * Insert record meta + * + * @param int $record_id + * @param string $key + * @param string $val + * + * @return array + */ + public function insert_meta( $record_id, $key, $val ) { + global $wpdb; + + $result = $wpdb->insert( + $this->table_meta, + array( + 'record_id' => $record_id, + 'meta_key' => $key, + 'meta_value' => $val, + ) + ); + + return $result; + } + + /** + * Retrieve records + * + * @param array $args + * + * @return array + */ + public function get_records( $args ) { + return $this->query->query( $args ); + } + + /** + * 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 occurrence of each value in the column, + * increasing the efficiency of the query. + * + * @param string $column + * + * @return array + */ + public function get_column_values( $column ) { + global $wpdb; + return (array) $wpdb->get_results( + "SELECT DISTINCT $column FROM $wpdb->stream", // @codingStandardsIgnoreLine can't prepare column name + 'ARRAY_A' + ); + } + + /** + * Public getter to return table names + * + * @return array + */ + public function get_table_names() { + return array( + $this->table, + $this->table_meta, + ); + } + + /** + * Init storage. + * + * @param \WP_Stream\Plugin $plugin Instance of the plugin. + * @return \WP_Stream\Install + */ + public function setup_storage( $plugin ) { + return new Install( $plugin ); + } + + /** + * Purge storage. + * + * @param \WP_Stream\Plugin $plugin Instance of the plugin. + * @return \WP_Stream\Uninstall + */ + public function purge_storage( $plugin ) { + $uninstall = new Uninstall( $plugin ); + add_action( 'wp_ajax_wp_stream_uninstall', array( $uninstall, 'uninstall' ) ); + + return $uninstall; + } + +} diff --git a/classes/class-db-driver.php b/classes/class-db-driver.php new file mode 100755 index 000000000..82cd37774 --- /dev/null +++ b/classes/class-db-driver.php @@ -0,0 +1,53 @@ +plugin = $plugin; - $this->query = new Query( $this ); - - global $wpdb; - - /** - * Allows devs to alter the tables prefix, default to base_prefix - * - * @param string $prefix - * - * @return string - */ - $prefix = apply_filters( 'wp_stream_db_tables_prefix', $wpdb->base_prefix ); - - $this->table = $prefix . 'stream'; - $this->table_meta = $prefix . 'stream_meta'; - - $wpdb->stream = $this->table; - $wpdb->streammeta = $this->table_meta; - - // Hack for get_metadata - $wpdb->recordmeta = $this->table_meta; - } - - /** - * Public getter to return table names - * - * @return array + * @param DB_Driver $driver Driver we want to use. */ - public function get_table_names() { - return array( - $this->table, - $this->table_meta, - ); + public function __construct( $driver ) { + $this->driver = $driver; } /** @@ -101,37 +56,34 @@ public function insert( $record ) { return false; } - global $wpdb; - $fields = array( 'object_id', 'site_id', 'blog_id', 'user_id', 'user_role', 'created', 'summary', 'ip', 'connector', 'context', 'action' ); $data = array_intersect_key( $record, array_flip( $fields ) ); - $result = $wpdb->insert( $this->table, $data ); + $meta = array(); + foreach ( (array) $record['meta'] as $key => $vals ) { + // If associative array, serialize it, otherwise loop on its members + $vals = ( is_array( $vals ) && 0 !== key( $vals ) ) ? array( $vals ) : $vals; + + foreach ( (array) $vals as $num => $val ) { + $vals[ $num ] = maybe_serialize( $val ); + } + $meta[ $key ] = $vals; + } + + $data['meta'] = $meta; + + $record_id = $this->driver->insert_record( $data ); - if ( 1 !== $result ) { + if ( ! $record_id ) { /** * Fires on a record insertion error * * @param array $record * @param mixed $result */ - do_action( 'wp_stream_record_insert_error', $record, $result ); - - return $result; - } - - $record_id = $wpdb->insert_id; + do_action( 'wp_stream_record_insert_error', $record, false ); - // Insert record meta - foreach ( (array) $record['meta'] as $key => $vals ) { - // If associative array, serialize it, otherwise loop on its members - $vals = ( is_array( $vals ) && 0 !== key( $vals ) ) ? array( $vals ) : $vals; - - foreach ( (array) $vals as $val ) { - $val = maybe_serialize( $val ); - - $this->insert_meta( $record_id, $key, $val ); - } + return false; } /** @@ -145,35 +97,11 @@ public function insert( $record ) { return absint( $record_id ); } - /** - * Insert record meta - * - * @param int $record_id - * @param string $key - * @param string $val - * - * @return array - */ - public function insert_meta( $record_id, $key, $val ) { - global $wpdb; - - $result = $wpdb->insert( - $this->table_meta, - array( - 'record_id' => $record_id, - 'meta_key' => $key, - 'meta_value' => $val, - ) - ); - - return $result; - } - /** * 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, + * GROUP BY allows query to find just the first occurrence of each value in the column, * increasing the efficiency of the query. * * @see assemble_records @@ -183,19 +111,14 @@ public function insert_meta( $record_id, $key, $val ) { * * @return array */ - function existing_records( $column ) { - global $wpdb; - + public function existing_records( $column ) { // Sanitize column $allowed_columns = array( 'ID', 'site_id', 'blog_id', 'object_id', 'user_id', 'user_role', 'created', 'summary', 'connector', 'context', 'action', 'ip' ); if ( ! in_array( $column, $allowed_columns, true ) ) { return array(); } - $rows = $wpdb->get_results( - "SELECT DISTINCT $column FROM $wpdb->stream", // @codingStandardsIgnoreLine can't prepare column name - 'ARRAY_A' - ); + $rows = $this->driver->get_column_values( $column ); if ( is_array( $rows ) && ! empty( $rows ) ) { $output_array = array(); @@ -211,19 +134,114 @@ function existing_records( $column ) { $column = sprintf( 'stream_%s', $column ); - return isset( $this->plugin->connectors->term_labels[ $column ] ) ? $this->plugin->connectors->term_labels[ $column ] : array(); + $term_labels = wp_stream_get_instance()->connectors->term_labels; + return isset( $term_labels[ $column ] ) ? $term_labels[ $column ] : array(); } /** - * Helper function for calling $this->query->query() - * - * @see Query->query() + * Get stream records * * @param array Query args * * @return array Stream Records */ - function query( $args ) { - return $this->query->query( $args ); + public function get_records( $args ) { + $defaults = array( + // Search param + 'search' => null, + 'search_field' => 'summary', + 'record_after' => null, // Deprecated, use date_after instead + // Date-based filters + 'date' => null, // Ex: 2015-07-01 + 'date_from' => null, // Ex: 2015-07-01 + 'date_to' => null, // Ex: 2015-07-01 + 'date_after' => null, // Ex: 2015-07-01T15:19:21+00:00 + 'date_before' => null, // Ex: 2015-07-01T15:19:21+00:00 + // Record ID filters + 'record' => null, + 'record__in' => array(), + 'record__not_in' => array(), + // Pagination params + 'records_per_page' => get_option( 'posts_per_page', 20 ), + 'paged' => 1, + // Order + 'order' => 'desc', + 'orderby' => 'date', + // Fields selection + 'fields' => array(), + ); + + // Additional property fields + $properties = array( + 'user_id' => null, + 'user_role' => null, + 'ip' => null, + 'object_id' => null, + 'site_id' => null, + 'blog_id' => null, + 'connector' => null, + 'context' => null, + 'action' => null, + ); + + /** + * Filter allows additional query properties to be added + * + * @return array Array of query properties + */ + $properties = apply_filters( 'wp_stream_query_properties', $properties ); + + // Add property fields to defaults, including their __in/__not_in variations + foreach ( $properties as $property => $default ) { + if ( ! isset( $defaults[ $property ] ) ) { + $defaults[ $property ] = $default; + } + + $defaults[ "{$property}__in" ] = array(); + $defaults[ "{$property}__not_in" ] = array(); + } + + $args = wp_parse_args( $args, $defaults ); + + /** + * Filter allows additional arguments to query $args + * + * @return array Array of query arguments + */ + $args = apply_filters( 'wp_stream_query_args', $args ); + + $result = (array) $this->driver->get_records( $args ); + $this->found_records_count = isset( $result['count'] ) ? $result['count'] : 0; + + return empty( $result['items'] ) ? array() : $result['items']; + } + + /** + * Helper function, backwards compatibility + * + * @param array $args Query args + * + * @return array Stream Records + */ + public function query( $args ) { + return $this->get_records( $args ); + } + + /** + * Return the number of records found in last request + * + * return int + */ + public function get_found_records_count() { + return $this->found_records_count; + } + + /** + * Public getter to return table names + * + * @return array + */ + public function get_table_names() { + return $this->driver->get_table_names(); } } diff --git a/classes/class-list-table.php b/classes/class-list-table.php index c3ffc9711..b73b689ce 100644 --- a/classes/class-list-table.php +++ b/classes/class-list-table.php @@ -206,8 +206,7 @@ function get_records() { } $args['records_per_page'] = apply_filters( 'stream_records_per_page', $args['records_per_page'] ); - $items = $this->plugin->db->query( $args ); - + $items = $this->plugin->db->get_records( $args ); return $items; } @@ -217,7 +216,7 @@ function get_records() { * @return integer */ public function get_total_found_rows() { - return $this->plugin->db->query->found_records; + return $this->plugin->db->get_found_records_count(); } function column_default( $item, $column_name ) { diff --git a/classes/class-plugin.php b/classes/class-plugin.php index 399dd6847..238152bf2 100755 --- a/classes/class-plugin.php +++ b/classes/class-plugin.php @@ -83,14 +83,24 @@ public function __construct() { require_once $this->locations['inc_dir'] . 'functions.php'; // Load DB helper interface/class - $driver = '\WP_Stream\DB'; - if ( class_exists( $driver ) ) { - $this->db = new DB( $this ); + $driver_class = apply_filters( 'wp_stream_db_driver', '\WP_Stream\DB_Driver_WPDB' ); + $driver = null; + + if ( class_exists( $driver_class ) ) { + $driver = new $driver_class(); + $this->db = new DB( $driver ); } + $error = false; if ( ! $this->db ) { + $error = esc_html__( 'Stream: Could not load chosen DB driver.', 'stream' ); + } elseif ( ! $driver instanceof DB_Driver ) { + $error = esc_html__( 'Stream: DB driver must implement DB Driver interface.', 'stream' ); + } + + if ( $error ) { wp_die( - esc_html__( 'Stream: Could not load chosen DB driver.', 'stream' ), + esc_html( $error ), esc_html__( 'Stream DB Error', 'stream' ) ); } @@ -107,12 +117,15 @@ public function __construct() { // Add frontend indicator add_action( 'wp_head', array( $this, 'frontend_indicator' ) ); + // Change DB driver after plugin loaded if any add-ons want to replace + add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ) ); + // Load admin area classes if ( is_admin() || ( defined( 'WP_STREAM_DEV_DEBUG' ) && WP_STREAM_DEV_DEBUG ) || ( defined( 'WP_CLI' ) && WP_CLI ) ) { $this->admin = new Admin( $this ); - $this->install = new Install( $this ); + $this->install = $driver->setup_storage( $this ); } elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) { - $this->admin = new Admin( $this ); + $this->admin = new Admin( $this, $driver ); } // Load WP-CLI command @@ -221,4 +234,17 @@ private function locate_plugin() { public function get_version() { return self::VERSION; } + + /** + * Change plugin database driver in case driver plugin loaded after stream + */ + public function plugins_loaded() { + // Load DB helper interface/class + $driver_class = apply_filters( 'wp_stream_db_driver', '\WP_Stream\DB_Driver_WPDB' ); + + if ( class_exists( $driver_class ) ) { + $driver = new $driver_class(); + $this->db = new DB( $driver ); + } + } } diff --git a/classes/class-query.php b/classes/class-query.php index 72a19d2f1..2250cb95c 100644 --- a/classes/class-query.php +++ b/classes/class-query.php @@ -2,11 +2,6 @@ namespace WP_Stream; class Query { - /** - * @var DB - */ - public $db; - /** * Hold the number of records found * @@ -14,15 +9,6 @@ class Query { */ public $found_records = 0; - /** - * Class constructor. - * - * @param DB $db The parent database class. - */ - public function __construct( $db ) { - $this->db = $db; - } - /** * Query records * @@ -33,70 +19,6 @@ public function __construct( $db ) { public function query( $args ) { global $wpdb; - $defaults = array( - // Search param - 'search' => null, - 'search_field' => 'summary', - 'record_after' => null, // Deprecated, use date_after instead - // Date-based filters - 'date' => null, // Ex: 2015-07-01 - 'date_from' => null, // Ex: 2015-07-01 - 'date_to' => null, // Ex: 2015-07-01 - 'date_after' => null, // Ex: 2015-07-01T15:19:21+00:00 - 'date_before' => null, // Ex: 2015-07-01T15:19:21+00:00 - // Record ID filters - 'record' => null, - 'record__in' => array(), - 'record__not_in' => array(), - // Pagination params - 'records_per_page' => get_option( 'posts_per_page', 20 ), - 'paged' => 1, - // Order - 'order' => 'desc', - 'orderby' => 'date', - // Fields selection - 'fields' => array(), - ); - - // Additional property fields - $properties = array( - 'user_id' => null, - 'user_role' => null, - 'ip' => null, - 'object_id' => null, - 'site_id' => null, - 'blog_id' => null, - 'connector' => null, - 'context' => null, - 'action' => null, - ); - - /** - * Filter allows additional query properties to be added - * - * @return array Array of query properties - */ - $properties = apply_filters( 'wp_stream_query_properties', $properties ); - - // Add property fields to defaults, including their __in/__not_in variations - foreach ( $properties as $property => $default ) { - if ( ! isset( $defaults[ $property ] ) ) { - $defaults[ $property ] = $default; - } - - $defaults[ "{$property}__in" ] = array(); - $defaults[ "{$property}__not_in" ] = array(); - } - - $args = wp_parse_args( $args, $defaults ); - - /** - * Filter allows additional arguments to query $args - * - * @return array Array of query arguments - */ - $args = apply_filters( 'wp_stream_query_args', $args ); - $join = ''; $where = ''; @@ -305,20 +227,19 @@ public function query( $args ) { */ $query = apply_filters( 'wp_stream_db_query', $query, $args ); + $result = array(); /** * QUERY THE DATABASE FOR RESULTS */ - $results = $wpdb->get_results( $query ); // @codingStandardsIgnoreLine $query already prepared - - // Hold the number of records found - $this->found_records = absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) ); + $result['items'] = $wpdb->get_results( $query ); // @codingStandardsIgnoreLine $query already prepared + $result['count'] = $result['items'] ? absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) ) : 0; // Add meta to the records, when applicable if ( empty( $fields ) || in_array( 'meta', $fields, true ) ) { - $results = $this->add_record_meta( $results ); + $result['items'] = $this->add_record_meta( $result['items'] ); } - return (array) $results; + return $result; } /** diff --git a/tests/tests/test-class-db-driver-wpdb.php b/tests/tests/test-class-db-driver-wpdb.php new file mode 100644 index 000000000..6eefb008c --- /dev/null +++ b/tests/tests/test-class-db-driver-wpdb.php @@ -0,0 +1,122 @@ +driver = new DB_Driver_WPDB(); + } + + public function test_construct() { + $this->assertNotEmpty( $this->driver->table ); + $this->assertNotEmpty( $this->driver->table_meta ); + + global $wpdb; + $this->assertEquals( $this->driver->table, $wpdb->stream ); + $this->assertEquals( $this->driver->table_meta, $wpdb->streammeta ); + $this->assertEquals( $this->driver->table_meta, $wpdb->recordmeta ); + } + + /* + * Also tests the insert_meta method + */ + public function test_insert() { + $dummy_data = $this->dummy_stream_data(); + $dummy_data['meta'] = $this->dummy_meta_data(); + + $stream_id = $this->driver->insert_record( $dummy_data ); + + $this->assertNotFalse( $stream_id ); + $this->assertGreaterThan( 0, $stream_id ); + + $this->assertEquals( 0, did_action( 'wp_stream_record_insert_error' ) ); + $this->assertGreaterThan( 0, did_action( 'wp_stream_record_inserted' ) ); + + global $wpdb; + + // Check that records exist + $stream_result = $wpdb->get_row( "SELECT * FROM {$wpdb->stream} WHERE ID = $stream_id", ARRAY_A ); + $this->assertNotEmpty( $stream_result ); + + foreach ( $this->dummy_stream_data() as $dummy_key => $dummy_value ) { + $this->assertArrayHasKey( $dummy_key, $stream_result ); + if ( 'created' === $dummy_key ) { + // It may take up to two seconds to insert a record, so check the time difference + $dummy_time = strtotime( $dummy_value ); + $result_time = strtotime( $stream_result[ $dummy_key ] ); + $this->assertTrue( $dummy_time > 0 ); + $this->assertTrue( $result_time > 0 ); + $this->assertTrue( $result_time - $dummy_time < 2 ); + $this->assertTrue( $result_time - $dummy_time >= -2 ); + } else { + $this->assertEquals( $dummy_value, $stream_result[ $dummy_key ] ); + } + } + + // Check that meta exists + $meta_result = $wpdb->get_results( "SELECT * FROM {$wpdb->streammeta} WHERE record_id = $stream_id", ARRAY_A ); + $this->assertNotEmpty( $meta_result ); + + $found_all_keys = true; + foreach ( $meta_result as $meta_row ) { + $key = $meta_row['meta_key']; + $value = $meta_row['meta_value']; + if ( ! isset( $dummy_data['meta'][ $key ] ) || $value !== $dummy_data['meta'][ $key ] ) { + $found_all_keys = false; + } + } + + $this->assertTrue( $found_all_keys ); + } + + public function test_get_column_values() { + $summaries = $this->driver->get_column_values( 'summary' ); + $this->assertNotEmpty( $summaries ); + + global $wpdb; + $wpdb->suppress_errors( true ); + + $bad_column = $this->driver->get_column_values( 'daisy' ); + $this->assertEmpty( $bad_column ); + + $wpdb->suppress_errors( false ); + } + + public function test_table_names() { + $table_names = $this->driver->get_table_names(); + + $this->assertNotEmpty( $table_names ); + $this->assertInternalType( 'array', $table_names ); + $this->assertEquals( array( $this->driver->table, $this->driver->table_meta ), $table_names ); + } + + private function dummy_stream_data() { + return array( + 'object_id' => 10, + 'site_id' => '1', + 'blog_id' => get_current_blog_id(), + 'user_id' => '1', + 'user_role' => 'administrator', + 'created' => date( 'Y-m-d h:i:s' ), + 'summary' => '"Hello Dave" plugin activated', + 'ip' => '192.168.0.1', + 'connector' => 'installer', + 'context' => 'plugins', + 'action' => 'activated', + ); + } + + private function dummy_meta_data() { + return array( + 'space_helmet' => 'false', + ); + } +} diff --git a/tests/tests/test-class-db.php b/tests/tests/test-class-db.php index 2c8680264..a476e6c78 100644 --- a/tests/tests/test-class-db.php +++ b/tests/tests/test-class-db.php @@ -16,30 +16,6 @@ public function setUp() { $this->assertNotEmpty( $this->db ); } - public function test_construct() { - $this->assertNotEmpty( $this->db->plugin ); - $this->assertInstanceOf( '\WP_Stream\Plugin', $this->db->plugin ); - - $this->assertNotEmpty( $this->db->query ); - $this->assertInstanceOf( '\WP_Stream\Query', $this->db->query ); - - $this->assertNotEmpty( $this->db->table ); - $this->assertNotEmpty( $this->db->table_meta ); - - global $wpdb; - $this->assertEquals( $this->db->table, $wpdb->stream ); - $this->assertEquals( $this->db->table_meta, $wpdb->streammeta ); - $this->assertEquals( $this->db->table_meta, $wpdb->recordmeta ); - } - - public function test_get_table_names() { - $table_names = $this->db->get_table_names(); - - $this->assertNotEmpty( $table_names ); - $this->assertInternalType( 'array', $table_names ); - $this->assertEquals( array( $this->db->table, $this->db->table_meta ), $table_names ); - } - /* * Also tests the insert_meta method */