diff --git a/classes/controllers/FrmWelcomeTourController.php b/classes/controllers/FrmWelcomeTourController.php
index 3c9e347063..2a92fb5eba 100644
--- a/classes/controllers/FrmWelcomeTourController.php
+++ b/classes/controllers/FrmWelcomeTourController.php
@@ -435,7 +435,7 @@ private static function more_than_the_default_form_exists() {
*/
private static function check_for_form_embeds() {
global $wpdb;
- $result = $wpdb->get_var( "SELECT 1 FROM {$wpdb->posts} WHERE post_content LIKE '%[formidable %' LIMIT 1" );
+ $result = $wpdb->get_var( $wpdb->prepare( 'SELECT 1 FROM %i WHERE post_content LIKE %s LIMIT 1', $wpdb->posts, '%[formidable %' ) );
return '1' === $result;
}
diff --git a/classes/helpers/FrmEmailSummaryHelper.php b/classes/helpers/FrmEmailSummaryHelper.php
index 40c7147960..ccd756aef5 100644
--- a/classes/helpers/FrmEmailSummaryHelper.php
+++ b/classes/helpers/FrmEmailSummaryHelper.php
@@ -330,10 +330,12 @@ public static function get_top_forms( $from_date, $to_date, $limit = 5 ) {
$result = $wpdb->get_results(
$wpdb->prepare(
- "SELECT fr.id AS form_id, fr.name AS form_name, COUNT(*) as items_count
- FROM {$wpdb->prefix}frm_items AS it INNER JOIN {$wpdb->prefix}frm_forms AS fr ON it.form_id = fr.id
+ 'SELECT fr.id AS form_id, fr.name AS form_name, COUNT(*) as items_count
+ FROM %i AS it INNER JOIN %i AS fr ON it.form_id = fr.id
WHERE it.created_at BETWEEN %s AND %s AND it.is_draft = 0 AND parent_form_id = 0
- GROUP BY form_id ORDER BY items_count DESC LIMIT %d",
+ GROUP BY form_id ORDER BY items_count DESC LIMIT %d',
+ $wpdb->prefix . 'frm_items',
+ $wpdb->prefix . 'frm_forms',
$from_date,
$to_date . ' 23:59:59',
intval( $limit )
diff --git a/classes/helpers/FrmFormsListHelper.php b/classes/helpers/FrmFormsListHelper.php
index 664a65e1b4..8890036613 100644
--- a/classes/helpers/FrmFormsListHelper.php
+++ b/classes/helpers/FrmFormsListHelper.php
@@ -710,8 +710,8 @@ private function query_posts_contain_form( $form ) {
$like_where = implode( ' OR ', $like_where );
$where = "post_type IN ('post', 'page') AND ($like_where)";
- // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
- $posts = $wpdb->get_results( "SELECT ID,post_title,post_name FROM $wpdb->posts WHERE $where" );
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $posts = $wpdb->get_results( $wpdb->prepare( 'SELECT ID,post_title,post_name FROM %i', $wpdb->posts ) . ' WHERE ' . $where );
if ( ! is_array( $posts ) ) {
return array();
diff --git a/classes/models/FrmDb.php b/classes/models/FrmDb.php
index 72bb7a5903..72184c5043 100644
--- a/classes/models/FrmDb.php
+++ b/classes/models/FrmDb.php
@@ -823,8 +823,7 @@ public static function cache_delete_group( $group ) {
public static function db_column_exists( $table, $column ) {
global $wpdb;
- // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
- $result = $wpdb->get_results( $wpdb->prepare( 'SHOW COLUMNS FROM ' . $wpdb->prefix . $table . ' LIKE %s', $column ) );
+ $result = $wpdb->get_results( $wpdb->prepare( 'SHOW COLUMNS FROM %i LIKE %s', $wpdb->prefix . $table, $column ) );
return ! empty( $result );
}
}
diff --git a/classes/models/FrmEntry.php b/classes/models/FrmEntry.php
index e168aea121..41f94e8fce 100644
--- a/classes/models/FrmEntry.php
+++ b/classes/models/FrmEntry.php
@@ -383,8 +383,8 @@ public static function destroy( $id ) {
*/
do_action( 'frm_before_destroy_entry', $id, $entry );
- $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_item_metas WHERE item_id=%d', $id ) );
- $result = $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_items WHERE id=%d', $id ) );
+ $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE item_id=%d', $wpdb->prefix . 'frm_item_metas', $id ) );
+ $result = $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE id=%d', $wpdb->prefix . 'frm_items', $id ) );
self::clear_cache();
diff --git a/classes/models/FrmEntryMeta.php b/classes/models/FrmEntryMeta.php
index d8f1d36058..ed438c957b 100644
--- a/classes/models/FrmEntryMeta.php
+++ b/classes/models/FrmEntryMeta.php
@@ -185,9 +185,10 @@ public static function update_entry_metas( $entry_id, $values ) {
'field_id' => $field_ids_to_remove,
);
FrmDb::get_where_clause_and_values( $where );
+ array_unshift( $where['values'], $wpdb->prefix . 'frm_item_metas' );
// Delete any leftovers
- $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_item_metas ' . $where['where'], $where['values'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, SlevomatCodingStandard.Files.LineLength.LineTooLong
+ $wpdb->query( $wpdb->prepare( 'DELETE FROM %i ' . $where['where'], $where['values'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
self::clear_cache();
}
@@ -226,7 +227,7 @@ public static function delete_entry_meta( $entry_id, $field_id ) {
global $wpdb;
self::clear_cache();
- return $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}frm_item_metas WHERE field_id=%d AND item_id=%d", $field_id, $entry_id ) );
+ return $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE field_id=%d AND item_id=%d', $wpdb->prefix . 'frm_item_metas', $field_id, $entry_id ) );
}
/**
@@ -356,7 +357,7 @@ private static function meta_field_query( $field_id, $order, $limit, $args, arra
if ( is_numeric( $field_id ) ) {
$query[] = $wpdb->prepare( 'WHERE em.field_id=%d', $field_id );
} else {
- $query[] = $wpdb->prepare( 'LEFT JOIN ' . $wpdb->prefix . 'frm_fields fi ON (em.field_id = fi.id) WHERE fi.field_key=%s', $field_id );
+ $query[] = $wpdb->prepare( 'LEFT JOIN %i fi ON (em.field_id = fi.id) WHERE fi.field_key=%s', $wpdb->prefix . 'frm_fields', $field_id );
}
if ( ! $args['is_draft'] ) {
@@ -636,7 +637,7 @@ public static function search_entry_metas( $search, $field_id, $operator ) {
$search = '%' . $search . '%';
}
- $query = $wpdb->prepare( "SELECT DISTINCT item_id FROM {$wpdb->prefix}frm_item_metas WHERE meta_value {$operator} %s and field_id = %d", $search, $field_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, SlevomatCodingStandard.Files.LineLength.LineTooLong
+ $query = $wpdb->prepare( "SELECT DISTINCT item_id FROM %i WHERE meta_value {$operator} %s and field_id = %d", $wpdb->prefix . 'frm_item_metas', $search, $field_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, SlevomatCodingStandard.Files.LineLength.LineTooLong
}//end if
$results = $wpdb->get_col( $query, 0 ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
diff --git a/classes/models/FrmField.php b/classes/models/FrmField.php
index 724c5bf0fe..a833730a66 100644
--- a/classes/models/FrmField.php
+++ b/classes/models/FrmField.php
@@ -836,9 +836,9 @@ public static function destroy( $id ) {
self::delete_form_transient( $field->form_id );
- $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_item_metas WHERE field_id=%d', $id ) );
+ $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE field_id=%d', $wpdb->prefix . 'frm_item_metas', $id ) );
- return $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_fields WHERE id=%d', $id ) );
+ return $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE id=%d', $wpdb->prefix . 'frm_fields', $id ) );
}
/**
@@ -854,7 +854,7 @@ public static function delete_form_transient( $form_id ) {
delete_transient( 'frm_form_fields_' . $form_id . 'excludeexclude' );
global $wpdb;
- $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->options . ' WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s', '_transient_timeout_frm_form_fields_' . $form_id . 'ex%', '_transient_frm_form_fields_' . $form_id . 'ex%', '_transient_timeout_frm_form_fields_' . $form_id . 'in%', '_transient_frm_form_fields_' . $form_id . 'in%' ) ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
+ $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s', $wpdb->options, '_transient_timeout_frm_form_fields_' . $form_id . 'ex%', '_transient_frm_form_fields_' . $form_id . 'ex%', '_transient_timeout_frm_form_fields_' . $form_id . 'in%', '_transient_frm_form_fields_' . $form_id . 'in%' ) ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
FrmDb::cache_delete_group( 'frm_field' );
@@ -892,7 +892,7 @@ public static function getOne( $id, $filter = false ) {
global $wpdb;
$where = is_numeric( $id ) ? 'id=%d' : 'field_key=%s';
- $query = $wpdb->prepare( 'SELECT * FROM ' . $wpdb->prefix . 'frm_fields WHERE ' . $where, $id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query = $wpdb->prepare( 'SELECT * FROM %i WHERE ' . $where, $wpdb->prefix . 'frm_fields', $id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$results = FrmDb::check_cache( $id, 'frm_field', $query, 'get_row', 0 );
diff --git a/classes/models/FrmForm.php b/classes/models/FrmForm.php
index 8c69884741..1df6405de1 100644
--- a/classes/models/FrmForm.php
+++ b/classes/models/FrmForm.php
@@ -636,9 +636,10 @@ public static function set_status( $id, $status ) {
'or' => 1,
);
FrmDb::get_where_clause_and_values( $where );
- array_unshift( $where['values'], $status );
+ array_unshift( $where['values'], $wpdb->prefix . 'frm_forms', $status );
- $query_results = $wpdb->query( $wpdb->prepare( 'UPDATE ' . $wpdb->prefix . 'frm_forms SET status = %s ' . $where['where'], $where['values'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, SlevomatCodingStandard.Files.LineLength.LineTooLong
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $query_results = $wpdb->query( $wpdb->prepare( 'UPDATE %i SET status = %s ' . $where['where'], $where['values'] ) );
} else {
$query_results = $wpdb->update( $wpdb->prefix . 'frm_forms', array( 'status' => $status ), array( 'id' => $id ) );
$wpdb->update( $wpdb->prefix . 'frm_forms', array( 'status' => $status ), array( 'parent_form_id' => $id ) );
@@ -729,9 +730,9 @@ public static function destroy( $id ) {
}
// Disconnect the fields from this form
- $wpdb->query( $wpdb->prepare( 'DELETE fi FROM ' . $wpdb->prefix . 'frm_fields AS fi LEFT JOIN ' . $wpdb->prefix . 'frm_forms fr ON (fi.form_id = fr.id) WHERE fi.form_id=%d OR parent_form_id=%d', $id, $id ) ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
+ $wpdb->query( $wpdb->prepare( 'DELETE fi FROM %i AS fi LEFT JOIN %i fr ON (fi.form_id = fr.id) WHERE fi.form_id=%d OR parent_form_id=%d', $wpdb->prefix . 'frm_fields', $wpdb->prefix . 'frm_forms', $id, $id ) ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
- $query_results = $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_forms WHERE id=%d OR parent_form_id=%d', $id, $id ) );
+ $query_results = $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE id=%d OR parent_form_id=%d', $wpdb->prefix . 'frm_forms', $id, $id ) );
if ( ! $query_results ) {
return $query_results;
diff --git a/classes/models/FrmMigrate.php b/classes/models/FrmMigrate.php
index cee115c6da..f4ca5a5bb1 100644
--- a/classes/models/FrmMigrate.php
+++ b/classes/models/FrmMigrate.php
@@ -302,28 +302,28 @@ private function add_composite_indexes_for_entries() {
$index_name = 'idx_is_draft_created_at';
if ( ! self::index_exists( $table_name, $index_name ) ) {
- $wpdb->query( "CREATE INDEX idx_is_draft_created_at ON `{$wpdb->prefix}frm_items` (is_draft, created_at)" );
+ $wpdb->query( $wpdb->prepare( 'CREATE INDEX idx_is_draft_created_at ON %i (is_draft, created_at)', $table_name ) );
}
$table_name = "{$wpdb->prefix}frm_item_metas";
$index_name = 'idx_field_id_item_id';
if ( ! self::index_exists( $table_name, $index_name ) ) {
- $wpdb->query( "CREATE INDEX idx_field_id_item_id ON `{$wpdb->prefix}frm_item_metas` (field_id, item_id)" );
+ $wpdb->query( $wpdb->prepare( 'CREATE INDEX idx_field_id_item_id ON %i (field_id, item_id)', $table_name ) );
}
$table_name = "{$wpdb->prefix}frm_items";
$index_name = 'idx_form_id_is_draft';
if ( ! self::index_exists( $table_name, $index_name ) ) {
- $wpdb->query( "CREATE INDEX idx_form_id_is_draft ON `{$wpdb->prefix}frm_items` (form_id, is_draft)" );
+ $wpdb->query( $wpdb->prepare( 'CREATE INDEX idx_form_id_is_draft ON %i (form_id, is_draft)', $table_name ) );
}
$table_name = "{$wpdb->prefix}frm_fields";
$index_name = 'idx_form_id_type';
if ( ! self::index_exists( $table_name, $index_name ) ) {
- $wpdb->query( "CREATE INDEX idx_form_id_type ON `{$wpdb->prefix}frm_fields` (form_id, type(30))" );
+ $wpdb->query( $wpdb->prepare( 'CREATE INDEX idx_form_id_type ON %i (form_id, type(30))', $table_name ) );
}
}
@@ -428,11 +428,11 @@ public function uninstall() {
global $wpdb, $wp_roles;
- $wpdb->query( 'DROP TABLE IF EXISTS ' . $this->fields ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
- $wpdb->query( 'DROP TABLE IF EXISTS ' . $this->forms ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
- $wpdb->query( 'DROP TABLE IF EXISTS ' . $this->entries ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
- $wpdb->query( 'DROP TABLE IF EXISTS ' . $this->entry_metas ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
- $wpdb->query( 'DROP TABLE IF EXISTS ' . $this->gated_tokens ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $this->fields ) );
+ $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $this->forms ) );
+ $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $this->entries ) );
+ $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $this->entry_metas ) );
+ $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $this->gated_tokens ) );
delete_option( 'frm_options' );
delete_option( 'frm_db_version' );
@@ -463,7 +463,7 @@ public function uninstall() {
remove_action( 'before_delete_post', 'FrmProDisplaysController::before_delete_post' );
remove_action( 'deleted_post', 'FrmProEntriesController::delete_entry' );
- $post_ids = $wpdb->get_col( $wpdb->prepare( 'SELECT ID FROM ' . $wpdb->posts . ' WHERE post_type in (%s, %s, %s)', FrmFormActionsController::$action_post_type, FrmStylesController::$post_type, 'frm_display' ) ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
+ $post_ids = $wpdb->get_col( $wpdb->prepare( 'SELECT ID FROM %i WHERE post_type in (%s, %s, %s)', $wpdb->posts, FrmFormActionsController::$action_post_type, FrmStylesController::$post_type, 'frm_display' ) ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
foreach ( $post_ids as $post_id ) {
// Delete's each post.
@@ -477,7 +477,7 @@ public function uninstall() {
delete_transient( 'frmpro_options' );
delete_transient( FrmOnboardingWizardController::TRANSIENT_NAME );
- $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->options . ' WHERE option_name LIKE %s OR option_name LIKE %s', '_transient_timeout_frm_form_fields_%', '_transient_frm_form_fields_%' ) ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
+ $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE option_name LIKE %s OR option_name LIKE %s', $wpdb->options, '_transient_timeout_frm_form_fields_%', '_transient_frm_form_fields_%' ) ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
do_action( 'frm_after_uninstall' );
@@ -759,10 +759,10 @@ private function migrate_to_25() {
*/
private function migrate_to_23() {
global $wpdb;
- $exists = $wpdb->get_row( 'SHOW COLUMNS FROM ' . $this->forms . ' LIKE "parent_form_id"' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $exists = $wpdb->get_row( $wpdb->prepare( 'SHOW COLUMNS FROM %i LIKE %s', $this->forms, 'parent_form_id' ) );
if ( ! $exists ) {
- $wpdb->query( 'ALTER TABLE ' . $this->forms . ' ADD parent_form_id int(11) default 0' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ $wpdb->query( $wpdb->prepare( 'ALTER TABLE %i ADD parent_form_id int(11) default 0', $this->forms ) );
}
}
diff --git a/classes/models/FrmStyle.php b/classes/models/FrmStyle.php
index 7cb8e3bb13..1ae46cc204 100644
--- a/classes/models/FrmStyle.php
+++ b/classes/models/FrmStyle.php
@@ -588,7 +588,7 @@ public function get_all( $orderby = 'title', $order = 'ASC', $limit = 99 ) {
if ( ! $temp_styles ) {
global $wpdb;
// Make sure there wasn't a conflict with the query
- $query = $wpdb->prepare( 'SELECT * FROM ' . $wpdb->posts . ' WHERE post_type=%s AND post_status=%s ORDER BY post_title ASC LIMIT 99', FrmStylesController::$post_type, 'publish' ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
+ $query = $wpdb->prepare( 'SELECT * FROM %i WHERE post_type=%s AND post_status=%s ORDER BY post_title ASC LIMIT 99', $wpdb->posts, FrmStylesController::$post_type, 'publish' ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
$temp_styles = FrmDb::check_cache( 'frm_backup_style_check', 'frm_styles', $query, 'get_results' );
if ( ! $temp_styles ) {
diff --git a/phpcs-sniffs/Formidable/Sniffs/Security/PreferIdentifierPlaceholderSniff.php b/phpcs-sniffs/Formidable/Sniffs/Security/PreferIdentifierPlaceholderSniff.php
new file mode 100644
index 0000000000..cdd4096720
--- /dev/null
+++ b/phpcs-sniffs/Formidable/Sniffs/Security/PreferIdentifierPlaceholderSniff.php
@@ -0,0 +1,997 @@
+query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}frm_items WHERE id = %d", $id ) );
+ * $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_items WHERE id = %d', $id ) );
+ * $wpdb->query( 'DROP TABLE IF EXISTS ' . $this->table );
+ *
+ * Good:
+ * $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE id = %d', $wpdb->prefix . 'frm_items', $id ) );
+ * $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $this->table ) );
+ *
+ * CREATE TABLE statements are exempt because they are built for dbDelta(), which cannot use prepare().
+ */
+class PreferIdentifierPlaceholderSniff implements Sniff {
+
+ /**
+ * SQL keywords that are directly followed by a table identifier.
+ *
+ * A bare ON is deliberately excluded because join conditions follow it with column
+ * references. Only the CREATE INDEX ... ON form takes a table there.
+ *
+ * @var string
+ */
+ const TABLE_KEYWORDS = 'FROM|JOIN|INTO|UPDATE|TABLE|EXISTS|INDEX\s+\S+\s+ON';
+
+ /**
+ * Regex fragment matching one interpolated segment inside a double quoted string.
+ *
+ * @var string
+ */
+ const INTERP_SEGMENT = '(?:\{\$[^}]+\}|\$[A-Za-z_]\w*(?:->\w+)*)';
+
+ /**
+ * $wpdb methods that receive raw SQL as their first argument.
+ *
+ * @var array
+ */
+ private $queryMethods = array(
+ 'query',
+ 'get_var',
+ 'get_row',
+ 'get_col',
+ 'get_results',
+ );
+
+ /**
+ * Returns an array of tokens this test wants to listen for.
+ *
+ * @return array
+ */
+ public function register() {
+ return array( T_VARIABLE );
+ }
+
+ /**
+ * Processes this test, when one of its tokens is encountered.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token in the stack passed in $tokens.
+ *
+ * @return void
+ */
+ public function process( File $phpcsFile, $stackPtr ) {
+ $tokens = $phpcsFile->getTokens();
+
+ if ( $tokens[ $stackPtr ]['content'] !== '$wpdb' ) {
+ return;
+ }
+
+ $objectOp = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true );
+
+ if ( false === $objectOp || $tokens[ $objectOp ]['code'] !== T_OBJECT_OPERATOR ) {
+ return;
+ }
+
+ $methodToken = $phpcsFile->findNext( T_WHITESPACE, $objectOp + 1, null, true );
+
+ if ( false === $methodToken || $tokens[ $methodToken ]['code'] !== T_STRING ) {
+ return;
+ }
+
+ $methodName = $tokens[ $methodToken ]['content'];
+ $isPrepare = 'prepare' === $methodName;
+
+ if ( ! $isPrepare && ! in_array( $methodName, $this->queryMethods, true ) ) {
+ return;
+ }
+
+ $openParen = $phpcsFile->findNext( T_WHITESPACE, $methodToken + 1, null, true );
+
+ if ( false === $openParen || $tokens[ $openParen ]['code'] !== T_OPEN_PARENTHESIS || ! isset( $tokens[ $openParen ]['parenthesis_closer'] ) ) {
+ return;
+ }
+
+ $closeParen = $tokens[ $openParen ]['parenthesis_closer'];
+
+ if ( ! $isPrepare && $this->first_arg_is_wpdb_call( $phpcsFile, $openParen ) ) {
+ // The inner $wpdb->prepare() call is processed on its own token.
+ return;
+ }
+
+ $argEnd = $this->find_first_arg_end( $phpcsFile, $openParen, $closeParen );
+ $parts = $this->parse_concat_parts( $phpcsFile, $openParen + 1, $argEnd );
+
+ if ( ! $parts ) {
+ return;
+ }
+
+ if ( $this->is_create_table_statement( $parts ) ) {
+ return;
+ }
+
+ $refs = $this->find_table_refs( $parts );
+
+ if ( ! $refs ) {
+ return;
+ }
+
+ if ( ! $isPrepare ) {
+ $phpcsFile->addError(
+ 'Table name built into SQL passed to $wpdb->%s() without prepare(). Wrap the query in $wpdb->prepare() and use the %%i placeholder for the identifier.',
+ $refs[0]['ptr'],
+ 'TableNotPrepared',
+ array( $methodName )
+ );
+
+ return;
+ }
+
+ $this->handle_prepare_refs( $phpcsFile, $parts, $refs, $openParen, $argEnd, $closeParen );
+ }
+
+ /**
+ * Checks whether the first argument of a call starts with another $wpdb method call.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $openParen The opening parenthesis of the outer call.
+ *
+ * @return bool
+ */
+ private function first_arg_is_wpdb_call( File $phpcsFile, $openParen ) {
+ $tokens = $phpcsFile->getTokens();
+ $firstArg = $phpcsFile->findNext( T_WHITESPACE, $openParen + 1, null, true );
+
+ return false !== $firstArg && $tokens[ $firstArg ]['code'] === T_VARIABLE && $tokens[ $firstArg ]['content'] === '$wpdb';
+ }
+
+ /**
+ * Finds the token position that ends the first argument (the first top level comma, or the closing parenthesis).
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $openParen The opening parenthesis of the call.
+ * @param int $closeParen The closing parenthesis of the call.
+ *
+ * @return int
+ */
+ private function find_first_arg_end( File $phpcsFile, $openParen, $closeParen ) {
+ $tokens = $phpcsFile->getTokens();
+
+ for ( $i = $openParen + 1; $i < $closeParen; $i++ ) {
+ $i = $this->skip_nested( $tokens, $i );
+
+ if ( $i >= $closeParen ) {
+ break;
+ }
+
+ if ( $tokens[ $i ]['code'] === T_COMMA ) {
+ return $i;
+ }
+ }
+
+ return $closeParen;
+ }
+
+ /**
+ * Skips over nested parentheses and bracket structures.
+ *
+ * @param array $tokens The token stack.
+ * @param int $i The current token position.
+ *
+ * @return int The position to continue from (the closer when a nested structure starts at $i).
+ */
+ private function skip_nested( $tokens, $i ) {
+ if ( $tokens[ $i ]['code'] === T_OPEN_PARENTHESIS && isset( $tokens[ $i ]['parenthesis_closer'] ) ) {
+ return $tokens[ $i ]['parenthesis_closer'];
+ }
+
+ if ( isset( $tokens[ $i ]['bracket_closer'] ) && in_array( $tokens[ $i ]['code'], array( T_OPEN_SQUARE_BRACKET, T_OPEN_SHORT_ARRAY, T_OPEN_CURLY_BRACKET ), true ) ) {
+ return $tokens[ $i ]['bracket_closer'];
+ }
+
+ if ( $tokens[ $i ]['code'] === T_ARRAY && isset( $tokens[ $i ]['parenthesis_closer'] ) ) {
+ return $tokens[ $i ]['parenthesis_closer'];
+ }
+
+ return $i;
+ }
+
+ /**
+ * Parses an expression token range into concatenation parts.
+ *
+ * Each part is array( 'type' => 'sq'|'dq'|'expr', 'start' => int, 'end' => int, 'content' => string ).
+ * For string parts, content is the inner string without the surrounding quotes.
+ * For expr parts, content is the raw PHP code.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $start The first token of the expression.
+ * @param int $end The token after the last token of the expression.
+ *
+ * @return array|false
+ */
+ private function parse_concat_parts( File $phpcsFile, $start, $end ) {
+ $tokens = $phpcsFile->getTokens();
+ $parts = array();
+ $run = array();
+
+ for ( $i = $start; $i < $end; $i++ ) {
+ $code = $tokens[ $i ]['code'];
+
+ if ( T_WHITESPACE === $code || T_COMMENT === $code ) {
+ continue;
+ }
+
+ if ( T_STRING_CONCAT === $code ) {
+ $part = $this->close_part( $phpcsFile, $run );
+
+ if ( ! $part ) {
+ return false;
+ }
+
+ $parts[] = $part;
+ $run = array();
+ continue;
+ }
+
+ $next = $this->skip_nested( $tokens, $i );
+
+ for ( $j = $i; $j <= $next; $j++ ) {
+ $run[] = $j;
+ }
+
+ $i = $next;
+ }
+
+ $part = $this->close_part( $phpcsFile, $run );
+
+ if ( ! $part ) {
+ return false;
+ }
+
+ $parts[] = $part;
+
+ return $parts;
+ }
+
+ /**
+ * Converts a token run into a single concatenation part.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param array $run Token positions collected for this part.
+ *
+ * @return array|false
+ */
+ private function close_part( File $phpcsFile, $run ) {
+ if ( ! $run ) {
+ return false;
+ }
+
+ $tokens = $phpcsFile->getTokens();
+ $start = $run[0];
+ $end = $run[ count( $run ) - 1 ];
+
+ // PHPCS splits multiline strings into one token per line, so a string part
+ // is any run made up entirely of tokens of the same string type.
+ $isSingleQuoted = true;
+ $isDoubleQuoted = true;
+ $raw = '';
+
+ foreach ( $run as $ptr ) {
+ if ( $tokens[ $ptr ]['code'] !== T_CONSTANT_ENCAPSED_STRING ) {
+ $isSingleQuoted = false;
+ }
+
+ if ( $tokens[ $ptr ]['code'] !== T_DOUBLE_QUOTED_STRING ) {
+ $isDoubleQuoted = false;
+ }
+
+ $raw .= $tokens[ $ptr ]['content'];
+ }
+
+ if ( $isSingleQuoted || $isDoubleQuoted ) {
+ $type = "'" === $raw[0] ? 'sq' : 'dq';
+
+ return array(
+ 'type' => $type,
+ 'start' => $start,
+ 'end' => $end,
+ 'content' => substr( $raw, 1, -1 ),
+ );
+ }
+
+ return array(
+ 'type' => 'expr',
+ 'start' => $start,
+ 'end' => $end,
+ 'content' => trim( $phpcsFile->getTokensAsString( $start, $end - $start + 1 ) ),
+ );
+ }
+
+ /**
+ * Checks whether the SQL starts with CREATE TABLE, which is exempt (dbDelta schema DDL).
+ *
+ * @param array $parts The concatenation parts.
+ *
+ * @return bool
+ */
+ private function is_create_table_statement( $parts ) {
+ if ( 'expr' === $parts[0]['type'] ) {
+ return false;
+ }
+
+ return (bool) preg_match( '/^\s*CREATE\s+TABLE/i', $parts[0]['content'] );
+ }
+
+ /**
+ * Finds table identifier references in the parsed parts.
+ *
+ * Each ref is array(
+ * 'type' => 'in_string'|'cross_part',
+ * 'ptr' => int (token to report on),
+ * 'part' => int (part index for in_string refs, first part index for cross_part refs),
+ * 'offset' => int (byte offset of the ref inside the string, in_string only),
+ * 'length' => int (byte length including surrounding backticks, in_string only),
+ * 'text' => string (the matched identifier text, in_string only),
+ * 'expr' => string (PHP expression to move into the prepare arguments),
+ * 'parts' => array (part indexes consumed, cross_part only),
+ * 'lead_word' => string (leading identifier characters absorbed from the following literal, cross_part only),
+ * ).
+ *
+ * @param array $parts The concatenation parts.
+ *
+ * @return array
+ */
+ private function find_table_refs( $parts ) {
+ $refs = array();
+ $count = count( $parts );
+
+ for ( $p = 0; $p < $count; $p++ ) {
+ $part = $parts[ $p ];
+
+ if ( 'dq' === $part['type'] ) {
+ $refs = array_merge( $refs, $this->find_in_string_refs( $part, $p ) );
+ }
+
+ if ( 'expr' === $part['type'] || $p + 1 >= $count || 'expr' !== $parts[ $p + 1 ]['type'] ) {
+ continue;
+ }
+
+ if ( ! preg_match( '/\b(?:' . self::TABLE_KEYWORDS . ')\s+`?\s*$/i', $part['content'] ) ) {
+ continue;
+ }
+
+ $ref = $this->collect_cross_part_ref( $parts, $p + 1 );
+
+ if ( $ref ) {
+ $refs[] = $ref;
+ }
+ }
+
+ return $refs;
+ }
+
+ /**
+ * Finds table references fully contained inside a double quoted string part.
+ *
+ * @param array $part The dq part.
+ * @param int $p The part index.
+ *
+ * @return array
+ */
+ private function find_in_string_refs( $part, $p ) {
+ $interp = self::INTERP_SEGMENT;
+ $refRun = '(?:[A-Za-z0-9_]+)?(?:' . $interp . '(?:[A-Za-z0-9_]+)?)+';
+
+ if ( ! preg_match_all( '/\b(?:' . self::TABLE_KEYWORDS . ')\s+(`?)(' . $refRun . ')(`?)/i', $part['content'], $matches, PREG_OFFSET_CAPTURE ) ) {
+ return array();
+ }
+
+ $refs = array();
+
+ foreach ( $matches[2] as $index => $match ) {
+ $text = $match[0];
+ $tickBefore = $matches[1][ $index ][0];
+ $tickAfter = $matches[3][ $index ][0];
+ $startOffset = $matches[1][ $index ][1];
+
+ if ( '' !== $tickBefore && '' === $tickAfter ) {
+ // Unbalanced backtick, the identifier continues in another part. Not safely fixable.
+ $tickAfter = '';
+ }
+
+ $refs[] = array(
+ 'type' => 'in_string',
+ 'ptr' => $part['start'],
+ 'part' => $p,
+ 'offset' => $startOffset,
+ 'length' => strlen( $tickBefore ) + strlen( $text ) + strlen( $tickAfter ),
+ 'text' => $text,
+ 'expr' => $this->segments_to_expression( $text ),
+ );
+ }
+
+ return $refs;
+ }
+
+ /**
+ * Collects a table reference that spans concatenation parts, starting at an expr part.
+ *
+ * @param array $parts The concatenation parts.
+ * @param int $start The index of the first expr part of the reference.
+ *
+ * @return array|false
+ */
+ private function collect_cross_part_ref( $parts, $start ) {
+ $count = count( $parts );
+ $exprBits = array();
+ $consumed = array();
+ $leadWord = '';
+ $p = $start;
+
+ while ( $p < $count ) {
+ $part = $parts[ $p ];
+
+ if ( 'expr' === $part['type'] ) {
+ $exprBits[] = $part['content'];
+ $consumed[] = $p;
+ $p++;
+ continue;
+ }
+
+ if ( 'sq' !== $part['type'] ) {
+ break;
+ }
+
+ if ( ! preg_match( '/^([A-Za-z0-9_]+)/', $part['content'], $match ) ) {
+ break;
+ }
+
+ if ( $match[1] === $part['content'] && $p + 1 < $count && 'expr' === $parts[ $p + 1 ]['type'] ) {
+ // The whole literal is part of the identifier and it continues with another expression.
+ $exprBits[] = "'" . $match[1] . "'";
+ $consumed[] = $p;
+ $p++;
+ continue;
+ }
+
+ $exprBits[] = "'" . $match[1] . "'";
+ $leadWord = $match[1];
+ break;
+ }
+
+ if ( ! $exprBits ) {
+ return false;
+ }
+
+ return array(
+ 'type' => 'cross_part',
+ 'ptr' => $parts[ $start ]['start'],
+ 'part' => $start,
+ 'parts' => $consumed,
+ 'lead_word' => $leadWord,
+ 'expr' => implode( ' . ', $exprBits ),
+ );
+ }
+
+ /**
+ * Converts an interpolated identifier text into an equivalent PHP expression.
+ *
+ * @param string $text The identifier text, e.g. "{$wpdb->prefix}frm_items".
+ *
+ * @return string
+ */
+ private function segments_to_expression( $text ) {
+ preg_match_all( '/\{\$([^}]+)\}|\$[A-Za-z_]\w*(?:->\w+)*|[A-Za-z0-9_]+/', $text, $matches, PREG_SET_ORDER );
+
+ $bits = array();
+
+ foreach ( $matches as $match ) {
+ if ( isset( $match[1] ) && '' !== $match[1] ) {
+ $bits[] = '$' . $match[1];
+ } elseif ( '$' === $match[0][0] ) {
+ $bits[] = $match[0];
+ } else {
+ $bits[] = "'" . $match[0] . "'";
+ }
+ }
+
+ return implode( ' . ', $bits );
+ }
+
+ /**
+ * Reports and, when safe, fixes table references inside a $wpdb->prepare() call.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param array $parts The concatenation parts of the first argument.
+ * @param array $refs The detected table references.
+ * @param int $openParen The opening parenthesis of the prepare() call.
+ * @param int $argEnd The end of the first argument (comma or closing parenthesis).
+ * @param int $closeParen The closing parenthesis of the prepare() call.
+ *
+ * @return void
+ */
+ private function handle_prepare_refs( File $phpcsFile, $parts, $refs, $openParen, $argEnd, $closeParen ) {
+ $fixable = $this->refs_are_fixable( $parts, $refs );
+ $args = $fixable ? $this->parse_args( $phpcsFile, $argEnd, $closeParen ) : false;
+
+ if ( $fixable && false !== $args ) {
+ $placeholderTotal = $this->count_placeholders_in_literals( $parts );
+ $fixable = count( $args ) === $placeholderTotal;
+ }
+
+ if ( ! $fixable || false === $args ) {
+ foreach ( $refs as $ref ) {
+ $phpcsFile->addError(
+ 'Table name built into $wpdb->prepare() SQL. Use the %i placeholder and pass the identifier as a prepare() argument.',
+ $ref['ptr'],
+ 'TableInPrepare'
+ );
+ }
+
+ return;
+ }
+
+ $fix = false;
+
+ foreach ( $refs as $ref ) {
+ $fix = $phpcsFile->addFixableError(
+ 'Table name built into $wpdb->prepare() SQL. Use the %i placeholder and pass the identifier as a prepare() argument.',
+ $ref['ptr'],
+ 'TableInPrepare'
+ ) || $fix;
+ }
+
+ if ( ! $fix ) {
+ return;
+ }
+
+ $this->apply_fix( $phpcsFile, $parts, $refs, $openParen, $argEnd, $closeParen, $args );
+ }
+
+ /**
+ * Determines whether the detected references can be fixed automatically.
+ *
+ * Fixing is only safe when the whole first argument is made of literals plus the
+ * reference expressions themselves, so every placeholder can be counted and every
+ * prepare() argument maps positionally.
+ *
+ * @param array $parts The concatenation parts.
+ * @param array $refs The detected table references.
+ *
+ * @return bool
+ */
+ private function refs_are_fixable( $parts, $refs ) {
+ $refPartIndexes = array();
+
+ foreach ( $refs as $ref ) {
+ if ( 'cross_part' === $ref['type'] ) {
+ foreach ( $ref['parts'] as $p ) {
+ $refPartIndexes[ $p ] = true;
+ }
+ }
+ }
+
+ foreach ( $parts as $p => $part ) {
+ if ( 'expr' === $part['type'] && ! isset( $refPartIndexes[ $p ] ) ) {
+ return false;
+ }
+
+ if ( 'expr' !== $part['type'] && preg_match( '/%\d+\$/', $part['content'] ) ) {
+ // Numbered placeholders change argument mapping. Not safely fixable.
+ return false;
+ }
+
+ if ( 'dq' === $part['type'] && $this->dq_has_unhandled_interpolation( $parts, $refs, $p ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks whether a double quoted part contains interpolation that is not part of a detected reference.
+ *
+ * Leftover interpolation before a reference makes placeholder counting unreliable.
+ * Leftover interpolation after every reference is harmless for argument mapping only
+ * when it cannot contain placeholders, which cannot be known, so any leftover
+ * interpolation before the last reference blocks fixing. Leftover interpolation
+ * after the last reference in the same string is allowed because the placeholder
+ * positions for every reference are already determined by the literal text before them.
+ *
+ * @param array $parts The concatenation parts.
+ * @param array $refs The detected table references.
+ * @param int $p The part index to inspect.
+ *
+ * @return bool
+ */
+ private function dq_has_unhandled_interpolation( $parts, $refs, $p ) {
+ $content = $parts[ $p ]['content'];
+
+ if ( ! preg_match_all( '/' . self::INTERP_SEGMENT . '/', $content, $matches, PREG_OFFSET_CAPTURE ) ) {
+ return false;
+ }
+
+ $lastRefEnd = -1;
+
+ foreach ( $refs as $ref ) {
+ if ( 'in_string' === $ref['type'] && $ref['part'] === $p ) {
+ $lastRefEnd = max( $lastRefEnd, $ref['offset'] + $ref['length'] );
+ }
+ }
+
+ foreach ( $matches[0] as $match ) {
+ $offset = $match[1];
+ $covered = false;
+
+ foreach ( $refs as $ref ) {
+ if ( 'in_string' === $ref['type'] && $ref['part'] === $p && $offset >= $ref['offset'] && $offset < $ref['offset'] + $ref['length'] ) {
+ $covered = true;
+ break;
+ }
+ }
+
+ if ( ! $covered && $offset < $lastRefEnd ) {
+ return true;
+ }
+
+ if ( ! $covered && -1 === $lastRefEnd ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Counts value placeholders in all literal parts.
+ *
+ * @param array $parts The concatenation parts.
+ *
+ * @return int
+ */
+ private function count_placeholders_in_literals( $parts ) {
+ $count = 0;
+
+ foreach ( $parts as $part ) {
+ if ( 'expr' !== $part['type'] ) {
+ $count += $this->count_placeholders( $part['content'] );
+ }
+ }
+
+ return $count;
+ }
+
+ /**
+ * Counts value placeholders in a piece of SQL text.
+ *
+ * @param string $text The SQL text.
+ *
+ * @return int
+ */
+ private function count_placeholders( $text ) {
+ $text = str_replace( '%%', '', $text );
+
+ return preg_match_all( '/%[dfFsi]/', $text );
+ }
+
+ /**
+ * Splits the remaining prepare() arguments into token ranges.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $argEnd The end of the first argument (comma or closing parenthesis).
+ * @param int $closeParen The closing parenthesis of the call.
+ *
+ * @return array|false Array of array( 'start' => int, 'end' => int ), or false when the arguments cannot be parsed.
+ */
+ private function parse_args( File $phpcsFile, $argEnd, $closeParen ) {
+ $tokens = $phpcsFile->getTokens();
+
+ if ( $argEnd >= $closeParen ) {
+ return array();
+ }
+
+ $args = array();
+ $argStart = false;
+ $lastReal = false;
+
+ for ( $i = $argEnd + 1; $i < $closeParen; $i++ ) {
+ $code = $tokens[ $i ]['code'];
+
+ if ( T_WHITESPACE === $code || T_COMMENT === $code ) {
+ continue;
+ }
+
+ $next = $this->skip_nested( $tokens, $i );
+
+ if ( $next !== $i ) {
+ if ( false === $argStart ) {
+ $argStart = $i;
+ }
+
+ $lastReal = $next;
+ $i = $next;
+ continue;
+ }
+
+ if ( T_COMMA === $code ) {
+ if ( false === $argStart ) {
+ return false;
+ }
+
+ $args[] = array(
+ 'start' => $argStart,
+ 'end' => $lastReal,
+ );
+ $argStart = false;
+ $lastReal = false;
+ continue;
+ }
+
+ if ( false === $argStart ) {
+ $argStart = $i;
+ }
+
+ $lastReal = $i;
+ }
+
+ if ( false !== $argStart ) {
+ $args[] = array(
+ 'start' => $argStart,
+ 'end' => $lastReal,
+ );
+ }
+
+ return $args;
+ }
+
+ /**
+ * Applies the %i fix: rewrites the SQL argument and inserts identifier expressions into the argument list.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param array $parts The concatenation parts.
+ * @param array $refs The detected table references.
+ * @param int $openParen The opening parenthesis of the call.
+ * @param int $argEnd The end of the first argument.
+ * @param int $closeParen The closing parenthesis of the call.
+ * @param array $args The existing argument token ranges.
+ *
+ * @return void
+ */
+ private function apply_fix( File $phpcsFile, $parts, $refs, $openParen, $argEnd, $closeParen, $args ) {
+ $newParts = $this->build_new_parts( $parts, $refs );
+ $insertions = $this->calculate_insertions( $parts, $refs );
+ $newSql = $this->render_parts( $newParts );
+
+ $phpcsFile->fixer->beginChangeset();
+
+ $sqlStart = $parts[0]['start'];
+ $sqlEnd = $parts[ count( $parts ) - 1 ]['end'];
+
+ $phpcsFile->fixer->replaceToken( $sqlStart, $newSql );
+
+ for ( $i = $sqlStart + 1; $i <= $sqlEnd; $i++ ) {
+ $phpcsFile->fixer->replaceToken( $i, '' );
+ }
+
+ // Group insertions by argument slot, keeping reference order.
+ $bySlot = array();
+
+ foreach ( $insertions as $insertion ) {
+ $bySlot[ $insertion['slot'] ][] = $insertion['expr'];
+ }
+
+ foreach ( $bySlot as $slot => $exprs ) {
+ $code = implode( ', ', $exprs );
+
+ if ( isset( $args[ $slot ] ) ) {
+ $phpcsFile->fixer->addContentBefore( $args[ $slot ]['start'], $code . ', ' );
+ } elseif ( $args ) {
+ $lastArg = $args[ count( $args ) - 1 ];
+ $phpcsFile->fixer->addContent( $lastArg['end'], ', ' . $code );
+ } else {
+ $phpcsFile->fixer->addContent( $sqlEnd, ', ' . $code );
+ }
+ }
+
+ $phpcsFile->fixer->endChangeset();
+ }
+
+ /**
+ * Builds the substituted part contents with %i in place of each reference.
+ *
+ * @param array $parts The concatenation parts.
+ * @param array $refs The detected table references.
+ *
+ * @return array Array of array( 'type' => 'sq'|'dq', 'content' => string ), expr parts consumed by refs are dropped.
+ */
+ private function build_new_parts( $parts, $refs ) {
+ $contents = array();
+ $types = array();
+ $dropped = array();
+ $leadTrim = array();
+
+ foreach ( $parts as $p => $part ) {
+ $contents[ $p ] = $part['content'];
+ $types[ $p ] = $part['type'];
+ }
+
+ foreach ( $refs as $ref ) {
+ if ( 'in_string' === $ref['type'] ) {
+ continue;
+ }
+
+ $firstPart = $ref['parts'][0];
+ $prevPart = $firstPart - 1;
+
+ // Replace the trailing whitespace/backtick after the keyword with a single space and %i.
+ $hadBacktick = (bool) preg_match( '/`\s*$/', $contents[ $prevPart ] );
+ $contents[ $prevPart ] = rtrim( $contents[ $prevPart ], " \t\n`" ) . ' %i';
+
+ foreach ( $ref['parts'] as $p ) {
+ $dropped[ $p ] = true;
+ }
+
+ // Strip the absorbed leading word (and a possible closing backtick) from the following literal.
+ $afterPart = $ref['parts'][ count( $ref['parts'] ) - 1 ] + 1;
+
+ if ( isset( $contents[ $afterPart ] ) && ( '' !== $ref['lead_word'] || $hadBacktick ) ) {
+ $leadTrim[ $afterPart ] = $ref['lead_word'];
+ }
+ }
+
+ foreach ( $leadTrim as $p => $word ) {
+ if ( '' === $word || 0 === strpos( $contents[ $p ], $word ) ) {
+ $contents[ $p ] = ltrim( substr( $contents[ $p ], strlen( $word ) ), '`' );
+ }
+
+ // The literal was fully absorbed into the identifier expression.
+ if ( '' === $contents[ $p ] ) {
+ $dropped[ $p ] = true;
+ }
+ }
+
+ // In-string substitutions, applied right to left so offsets stay valid.
+ $byPart = array();
+
+ foreach ( $refs as $ref ) {
+ if ( 'in_string' === $ref['type'] ) {
+ $byPart[ $ref['part'] ][] = $ref;
+ }
+ }
+
+ foreach ( $byPart as $p => $partRefs ) {
+ usort(
+ $partRefs,
+ function ( $a, $b ) {
+ return $b['offset'] - $a['offset'];
+ }
+ );
+
+ foreach ( $partRefs as $ref ) {
+ $contents[ $p ] = substr_replace( $contents[ $p ], '%i', $ref['offset'], $ref['length'] );
+ }
+ }
+
+ $newParts = array();
+
+ foreach ( $contents as $p => $content ) {
+ if ( isset( $dropped[ $p ] ) ) {
+ continue;
+ }
+
+ $type = $types[ $p ];
+
+ if ( 'dq' === $type && ! preg_match( '/[\$\\\\\']/', $content ) ) {
+ $type = 'sq';
+ }
+
+ $newParts[] = array(
+ 'type' => $type,
+ 'content' => $content,
+ );
+ }
+
+ return $newParts;
+ }
+
+ /**
+ * Calculates where each reference expression must be inserted in the argument list.
+ *
+ * The slot is the number of original value placeholders that appear before the
+ * reference in the SQL, which equals the index of the original argument the
+ * expression must be inserted before.
+ *
+ * @param array $parts The concatenation parts.
+ * @param array $refs The detected table references.
+ *
+ * @return array Array of array( 'slot' => int, 'expr' => string ) in reference order.
+ */
+ private function calculate_insertions( $parts, $refs ) {
+ $insertions = array();
+
+ foreach ( $refs as $ref ) {
+ $slot = 0;
+
+ if ( 'in_string' === $ref['type'] ) {
+ $limitPart = $ref['part'];
+ $limitOffset = $ref['offset'];
+ } else {
+ $limitPart = $ref['parts'][0];
+ $limitOffset = null;
+ }
+
+ foreach ( $parts as $p => $part ) {
+ if ( 'expr' === $part['type'] ) {
+ continue;
+ }
+
+ if ( $p > $limitPart ) {
+ break;
+ }
+
+ if ( $p === $limitPart ) {
+ if ( null !== $limitOffset ) {
+ $slot += $this->count_placeholders( substr( $part['content'], 0, $limitOffset ) );
+ }
+
+ break;
+ }
+
+ $slot += $this->count_placeholders( $part['content'] );
+ }
+
+ $insertions[] = array(
+ 'slot' => $slot,
+ 'expr' => $ref['expr'],
+ );
+ }
+
+ return $insertions;
+ }
+
+ /**
+ * Renders substituted parts back into a single PHP concatenation expression.
+ *
+ * @param array $newParts The substituted parts.
+ *
+ * @return string
+ */
+ private function render_parts( $newParts ) {
+ $rendered = array();
+
+ foreach ( $newParts as $part ) {
+ if ( 'sq' === $part['type'] ) {
+ $code = "'" . $part['content'] . "'";
+ } else {
+ $code = '"' . $part['content'] . '"';
+ }
+
+ $last = count( $rendered ) - 1;
+
+ if ( $last >= 0 && 'sq' === $part['type'] && "'" === substr( $rendered[ $last ], -1 ) && "'" === $rendered[ $last ][0] ) {
+ $rendered[ $last ] = substr( $rendered[ $last ], 0, -1 ) . $part['content'] . "'";
+ continue;
+ }
+
+ $rendered[] = $code;
+ }
+
+ return implode( ' . ', $rendered );
+ }
+}
diff --git a/phpcs-sniffs/Formidable/ruleset.xml b/phpcs-sniffs/Formidable/ruleset.xml
index 815677df14..1c5b3ed24b 100644
--- a/phpcs-sniffs/Formidable/ruleset.xml
+++ b/phpcs-sniffs/Formidable/ruleset.xml
@@ -95,6 +95,7 @@
+
diff --git a/stripe/controllers/FrmStrpLiteEventsController.php b/stripe/controllers/FrmStrpLiteEventsController.php
index a1d85cd8e9..1efeb29bdd 100644
--- a/stripe/controllers/FrmStrpLiteEventsController.php
+++ b/stripe/controllers/FrmStrpLiteEventsController.php
@@ -176,7 +176,8 @@ private function reset_customer() {
}
$wpdb->query(
$wpdb->prepare(
- "DELETE FROM $wpdb->usermeta WHERE meta_value = %s AND meta_key LIKE %s",
+ 'DELETE FROM %i WHERE meta_value = %s AND meta_key LIKE %s',
+ $wpdb->usermeta,
$customer_id,
'_frmstrp_customer_id%'
)
diff --git a/stripe/controllers/FrmTransLiteCRUDController.php b/stripe/controllers/FrmTransLiteCRUDController.php
index ab066b2276..80a583b2ae 100644
--- a/stripe/controllers/FrmTransLiteCRUDController.php
+++ b/stripe/controllers/FrmTransLiteCRUDController.php
@@ -65,20 +65,18 @@ public static function show( $id = 0 ) {
private static function get_payment_row( $id ) {
global $wpdb;
- $table_name = self::table_name();
-
- // @codingStandardsIgnoreStart
return $wpdb->get_row(
$wpdb->prepare(
- "SELECT
+ 'SELECT
p.*, e.user_id
- FROM `{$wpdb->prefix}frm_{$table_name}` p
- LEFT JOIN `{$wpdb->prefix}frm_items` e ON p.item_id = e.id
- WHERE p.id=%d",
+ FROM %i p
+ LEFT JOIN %i e ON p.item_id = e.id
+ WHERE p.id=%d',
+ $wpdb->prefix . 'frm_' . self::table_name(),
+ $wpdb->prefix . 'frm_items',
$id
)
);
- // @codingStandardsIgnoreEnd
}
/**
diff --git a/stripe/controllers/FrmTransLiteSubscriptionsController.php b/stripe/controllers/FrmTransLiteSubscriptionsController.php
index e67ec7e6ed..be29d20fb1 100755
--- a/stripe/controllers/FrmTransLiteSubscriptionsController.php
+++ b/stripe/controllers/FrmTransLiteSubscriptionsController.php
@@ -55,7 +55,7 @@ public static function show_receipt_link( $subscription ) {
public static function show_cancel_link( $sub, $atts = array() ) {
if ( ! isset( $sub->user_id ) ) {
global $wpdb;
- $sub->user_id = $wpdb->get_var( $wpdb->prepare( 'SELECT user_id FROM ' . $wpdb->prefix . 'frm_items WHERE id=%d', $sub->item_id ) );
+ $sub->user_id = $wpdb->get_var( $wpdb->prepare( 'SELECT user_id FROM %i WHERE id=%d', $wpdb->prefix . 'frm_items', $sub->item_id ) );
}
$link = self::cancel_link( $sub, $atts );
diff --git a/stripe/helpers/FrmTransLiteListHelper.php b/stripe/helpers/FrmTransLiteListHelper.php
index 8220ad6f1e..b35e970232 100755
--- a/stripe/helpers/FrmTransLiteListHelper.php
+++ b/stripe/helpers/FrmTransLiteListHelper.php
@@ -63,7 +63,8 @@ public function prepare_items() {
$query = $this->get_table_query();
$order_query = FrmDb::esc_order( "ORDER BY p.{$orderby} $order" );
- // @codingStandardsIgnoreStart
+ // The table query and order query are prepared and escaped where they are built.
+ // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$this->items = $wpdb->get_results(
$wpdb->prepare(
'SELECT p.* ' . $query . $order_query . ' LIMIT %d, %d',
@@ -72,7 +73,7 @@ public function prepare_items() {
)
);
$total_items = $wpdb->get_var( 'SELECT COUNT(*) ' . $query );
- // @codingStandardsIgnoreEnd
+ // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
$this->set_pagination_args(
array(
@@ -92,17 +93,17 @@ private function get_table_query() {
$form_id = FrmAppHelper::get_param( 'form', 0, 'get', 'absint' );
if ( ! $form_id ) {
- return "FROM `{$wpdb->prefix}{$table_name}` p";
+ return $wpdb->prepare( 'FROM %i p', $wpdb->prefix . $table_name );
}
- // @codingStandardsIgnoreStart
- return $wpdb->prepare(
- "FROM `{$wpdb->prefix}{$table_name}` p
- JOIN `{$wpdb->prefix}frm_items` i ON p.item_id = i.id
- WHERE i.form_id = %d",
- $form_id
- );
- // @codingStandardsIgnoreEnd
+ return $wpdb->prepare(
+ 'FROM %i p
+ JOIN %i i ON p.item_id = i.id
+ WHERE i.form_id = %d',
+ $wpdb->prefix . $table_name,
+ $wpdb->prefix . 'frm_items',
+ $form_id
+ );
}
/**
@@ -334,17 +335,20 @@ private function get_form_ids() {
}
global $wpdb;
- // @codingStandardsIgnoreStart
- $forms = $wpdb->get_results(
- "SELECT
- fo.id as form_id,
- fo.name,
- e.id
- FROM {$wpdb->prefix}frm_items e
- LEFT JOIN {$wpdb->prefix}frm_forms fo ON e.form_id = fo.id
- WHERE e.id in (" . implode( ',', $entry_ids ) . ')'
+
+ $ids_placeholders = implode( ',', array_fill( 0, count( $entry_ids ), '%d' ) );
+ $forms = $wpdb->get_results(
+ $wpdb->prepare(
+ 'SELECT
+ fo.id as form_id,
+ fo.name,
+ e.id
+ FROM %i e
+ LEFT JOIN %i fo ON e.form_id = fo.id
+ WHERE e.id in (' . $ids_placeholders . ')', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+ array_merge( array( $wpdb->prefix . 'frm_items', $wpdb->prefix . 'frm_forms' ), $entry_ids )
+ )
);
- // @codingStandardsIgnoreEnd
unset( $entry_ids );
$form_ids = array();
diff --git a/stripe/models/FrmTransLiteDb.php b/stripe/models/FrmTransLiteDb.php
index f2b6e2a869..a3500d2907 100755
--- a/stripe/models/FrmTransLiteDb.php
+++ b/stripe/models/FrmTransLiteDb.php
@@ -147,9 +147,7 @@ public function destroy( $id ) {
*/
do_action( 'frm_before_destroy_' . $this->singular, $id );
- // @codingStandardsIgnoreStart
- return $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . $this->table_name . ' WHERE id=%d', $id ) );
- // @codingStandardsIgnoreEnd
+ return $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE id=%d', $wpdb->prefix . $this->table_name, $id ) );
}
/**
@@ -160,14 +158,13 @@ public function destroy( $id ) {
public function get_one( $id ) {
global $wpdb;
- // @codingStandardsIgnoreStart
return $wpdb->get_row(
$wpdb->prepare(
- 'SELECT * FROM ' . $wpdb->prefix . $this->table_name . ' WHERE id=%d',
+ 'SELECT * FROM %i WHERE id=%d',
+ $wpdb->prefix . $this->table_name,
$id
)
);
- // @codingStandardsIgnoreEnd
}
/**
@@ -183,17 +180,15 @@ public function get_one_by( $id, $field = 'receipt_id' ) {
}
global $wpdb;
- // Can this be exploited?
- $field = sanitize_text_field( $field );
- // @codingStandardsIgnoreStart
+
return $wpdb->get_row(
$wpdb->prepare(
- 'SELECT * FROM ' . $wpdb->prefix . $this->table_name
- . ' WHERE ' . $field . ' = %s ORDER BY created_at DESC',
+ 'SELECT * FROM %i WHERE %i = %s ORDER BY created_at DESC',
+ $wpdb->prefix . $this->table_name,
+ $field,
$id
)
);
- // @codingStandardsIgnoreEnd
}
/**
@@ -216,15 +211,15 @@ public function get_all_by( $value, $field = 'item_id' ) {
}
global $wpdb;
- // @codingStandardsIgnoreStart
+
return $wpdb->get_results(
$wpdb->prepare(
- 'SELECT * FROM ' . $wpdb->prefix . $this->table_name
- . ' WHERE ' . $field . ' = %s ORDER BY created_at DESC',
+ 'SELECT * FROM %i WHERE %i = %s ORDER BY created_at DESC',
+ $wpdb->prefix . $this->table_name,
+ $field,
$value
)
);
- // @codingStandardsIgnoreEnd
}
/**
@@ -234,21 +229,22 @@ public function get_all_by( $value, $field = 'item_id' ) {
*/
public function get_all_for_user( $user_id ) {
global $wpdb;
- // @codingStandardsIgnoreStart
+
return $wpdb->get_results(
$wpdb->prepare(
'SELECT
*,
e.id as entry_id,
p.id as id
- FROM ' . $wpdb->prefix . $this->table_name . ' p '
- . 'LEFT JOIN ' . $wpdb->prefix . 'frm_items e ON e.id = p.item_id '
- . 'WHERE e.user_id = %d '
- . 'ORDER BY p.created_at DESC',
+ FROM %i p
+ LEFT JOIN %i e ON e.id = p.item_id
+ WHERE e.user_id = %d
+ ORDER BY p.created_at DESC',
+ $wpdb->prefix . $this->table_name,
+ $wpdb->prefix . 'frm_items',
$user_id
)
);
- // @codingStandardsIgnoreEnd
}
/**
@@ -265,11 +261,7 @@ public function get_all_for_entry( $id ) {
*/
public function get_count() {
global $wpdb;
- // @codingStandardsIgnoreStart
- return $wpdb->get_var(
- 'SELECT COUNT(*) FROM ' . $wpdb->prefix . $this->table_name
- );
- // @codingStandardsIgnoreEnd
+ return $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $wpdb->prefix . $this->table_name ) );
}
/**
@@ -326,14 +318,14 @@ private function migrate_data( $old_db_version ) {
*/
private function migrate_to_4() {
global $wpdb;
- $result = $wpdb->get_results( $wpdb->prepare( 'SHOW COLUMNS FROM ' . $wpdb->prefix . 'frm_payments LIKE %s', 'completed' ) );
+ $result = $wpdb->get_results( $wpdb->prepare( 'SHOW COLUMNS FROM %i LIKE %s', $wpdb->prefix . 'frm_payments', 'completed' ) );
if ( ! $result ) {
return;
}
$payments = $wpdb->get_results(
- "SELECT * FROM {$wpdb->prefix}frm_payments WHERE completed is NOT NULL AND status is NULL"
+ $wpdb->prepare( 'SELECT * FROM %i WHERE completed is NOT NULL AND status is NULL', $wpdb->prefix . 'frm_payments' )
);
foreach ( $payments as $payment ) {
diff --git a/stripe/models/FrmTransLiteSubscription.php b/stripe/models/FrmTransLiteSubscription.php
index 88e7b01366..8cd142508d 100755
--- a/stripe/models/FrmTransLiteSubscription.php
+++ b/stripe/models/FrmTransLiteSubscription.php
@@ -83,10 +83,11 @@ public function get_overdue_subscriptions() {
global $wpdb;
return $wpdb->get_results(
$wpdb->prepare(
- "SELECT * FROM `{$wpdb->prefix}frm_subscriptions`
+ "SELECT * FROM %i
WHERE fail_count < %d
AND next_bill_date < %s
AND (status = 'active' OR status = 'future_cancel')",
+ $wpdb->prefix . 'frm_subscriptions',
3,
gmdate( 'Y-m-d' )
)
diff --git a/tests/phpunit/database/test_FrmDb.php b/tests/phpunit/database/test_FrmDb.php
index 52990f207d..9d0c2a8b0d 100644
--- a/tests/phpunit/database/test_FrmDb.php
+++ b/tests/phpunit/database/test_FrmDb.php
@@ -24,4 +24,13 @@ public function test_esc_order() {
$this->assertSame( $expected, $actual );
}
}
+
+ /**
+ * @covers FrmDb::db_column_exists
+ */
+ public function test_db_column_exists() {
+ $this->assertTrue( FrmDb::db_column_exists( 'frm_fields', 'field_key' ) );
+ $this->assertTrue( FrmDb::db_column_exists( 'frm_items', 'is_draft' ) );
+ $this->assertFalse( FrmDb::db_column_exists( 'frm_fields', 'missing_column' ) );
+ }
}
diff --git a/tests/phpunit/emails/test_FrmEmailSummaryHelper.php b/tests/phpunit/emails/test_FrmEmailSummaryHelper.php
index 813d023b4d..1ea85be6a2 100644
--- a/tests/phpunit/emails/test_FrmEmailSummaryHelper.php
+++ b/tests/phpunit/emails/test_FrmEmailSummaryHelper.php
@@ -181,4 +181,28 @@ function ( $pre, $parsed_args, $url ) {
FrmEmailSummaryHelper::maybe_remove_recipients_from_api( $recipients );
$this->assertSame( 'recipient2@example.com', $recipients );
}
+
+ /**
+ * @covers FrmEmailSummaryHelper::get_top_forms
+ */
+ public function test_get_top_forms() {
+ $form_a = $this->factory->form->create_and_get();
+ $form_b = $this->factory->form->create_and_get();
+
+ $this->factory->entry->create( $this->factory->field->generate_entry_array( $form_a ) );
+ $this->factory->entry->create( $this->factory->field->generate_entry_array( $form_a ) );
+ $this->factory->entry->create( $this->factory->field->generate_entry_array( $form_b ) );
+
+ $top_forms = FrmEmailSummaryHelper::get_top_forms( gmdate( 'Y-m-d', strtotime( '-1 day' ) ), gmdate( 'Y-m-d' ) );
+ $by_form = array();
+
+ foreach ( $top_forms as $row ) {
+ $by_form[ (int) $row->form_id ] = (int) $row->items_count;
+ }
+
+ $this->assertArrayHasKey( $form_a->id, $by_form );
+ $this->assertArrayHasKey( $form_b->id, $by_form );
+ $this->assertSame( 2, $by_form[ $form_a->id ] );
+ $this->assertSame( 1, $by_form[ $form_b->id ] );
+ }
}
diff --git a/tests/phpunit/entries/test_FrmEntryMeta.php b/tests/phpunit/entries/test_FrmEntryMeta.php
index 3a4387262f..6faeeadb32 100644
--- a/tests/phpunit/entries/test_FrmEntryMeta.php
+++ b/tests/phpunit/entries/test_FrmEntryMeta.php
@@ -84,4 +84,86 @@ public function test_should_join_fields_table() {
$this->assertFalse( $this->run_private_method( array( 'FrmEntryMeta', 'should_join_fields_table' ), array( &$where ) ) );
$this->assertSame( array( 'e.form_id' => 456 ), $where );
}
+
+ /**
+ * @covers FrmEntryMeta::delete_entry_meta
+ */
+ public function test_delete_entry_meta() {
+ $form = $this->factory->form->create_and_get();
+ $field_id = $this->factory->field->create(
+ array(
+ 'form_id' => $form->id,
+ )
+ );
+
+ $entry_data = $this->factory->field->generate_entry_array( $form );
+
+ $entry_data['item_meta'][ $field_id ] = 'Value to delete';
+
+ $entry_id = $this->factory->entry->create( $entry_data );
+
+ $this->assertSame( 'Value to delete', FrmEntryMeta::get_entry_meta_by_field( $entry_id, $field_id ) );
+
+ FrmEntryMeta::delete_entry_meta( $entry_id, $field_id );
+
+ $this->assertNull( FrmEntryMeta::get_entry_meta_by_field( $entry_id, $field_id ) );
+ }
+
+ /**
+ * @covers FrmEntryMeta::get_entry_metas_for_field
+ */
+ public function test_get_entry_metas_for_field() {
+ $form = $this->factory->form->create_and_get();
+ $field_id = $this->factory->field->create(
+ array(
+ 'form_id' => $form->id,
+ )
+ );
+
+ $entry_data = $this->factory->field->generate_entry_array( $form );
+
+ $entry_data['item_meta'][ $field_id ] = 'Meta value to find';
+
+ $this->factory->entry->create( $entry_data );
+
+ // Look up by field id.
+ $values = FrmEntryMeta::get_entry_metas_for_field( $field_id );
+ $this->assertContains( 'Meta value to find', $values );
+
+ // Look up by field key, which joins the fields table.
+ $field_key = FrmField::get_key_by_id( $field_id );
+ $values = FrmEntryMeta::get_entry_metas_for_field( $field_key );
+ $this->assertContains( 'Meta value to find', $values );
+ }
+
+ /**
+ * @covers FrmEntryMeta::search_entry_metas
+ */
+ public function test_search_entry_metas() {
+ $form = $this->factory->form->create_and_get();
+ $field_id = $this->factory->field->create(
+ array(
+ 'form_id' => $form->id,
+ )
+ );
+
+ $entry_data = $this->factory->field->generate_entry_array( $form );
+
+ $entry_data['item_meta'][ $field_id ] = 'Findable value';
+
+ $entry_id = (int) $this->factory->entry->create( $entry_data );
+ $other_entry_data = $this->factory->field->generate_entry_array( $form );
+
+ $other_entry_data['item_meta'][ $field_id ] = 'Something else';
+
+ $other_entry_id = (int) $this->factory->entry->create( $other_entry_data );
+ $matches = array_map( 'intval', FrmEntryMeta::search_entry_metas( 'Findable', $field_id, 'LIKE' ) );
+
+ $this->assertContains( $entry_id, $matches );
+ $this->assertNotContains( $other_entry_id, $matches );
+
+ $matches = array_map( 'intval', FrmEntryMeta::search_entry_metas( 'Something else', $field_id, '=' ) );
+
+ $this->assertContains( $other_entry_id, $matches );
+ }
}
diff --git a/tests/phpunit/fields/test_FrmField.php b/tests/phpunit/fields/test_FrmField.php
index f99d29a64d..099e92afc2 100644
--- a/tests/phpunit/fields/test_FrmField.php
+++ b/tests/phpunit/fields/test_FrmField.php
@@ -72,4 +72,34 @@ public function test_get_all_for_form() {
$this->assertCount( $args['count'], $fields, 'An incorrect number of fields are retrieved with FrmField::get_all_for_form for ' . $test . '.' );
}
}
+
+ /**
+ * @covers FrmField::destroy
+ */
+ public function test_destroy() {
+ $form = $this->factory->form->create_and_get();
+ $field_id = $this->factory->field->create(
+ array(
+ 'form_id' => $form->id,
+ )
+ );
+
+ $entry_data = $this->factory->field->generate_entry_array( $form );
+
+ $entry_data['item_meta'][ $field_id ] = 'Meta for deleted field';
+
+ $entry_id = $this->factory->entry->create( $entry_data );
+
+ $this->assertSame( 'Meta for deleted field', FrmEntryMeta::get_entry_meta_by_field( $entry_id, $field_id ) );
+
+ FrmField::destroy( $field_id );
+
+ $this->assertNull( FrmField::getOne( $field_id ) );
+
+ global $wpdb;
+ $meta_value = $wpdb->get_var(
+ $wpdb->prepare( 'SELECT meta_value FROM %i WHERE item_id = %d AND field_id = %d', $wpdb->prefix . 'frm_item_metas', $entry_id, $field_id )
+ );
+ $this->assertNull( $meta_value );
+ }
}
diff --git a/tests/phpunit/forms/test_FrmForm.php b/tests/phpunit/forms/test_FrmForm.php
index 0a77f8c17e..8ed55c3945 100644
--- a/tests/phpunit/forms/test_FrmForm.php
+++ b/tests/phpunit/forms/test_FrmForm.php
@@ -69,6 +69,27 @@ public function test_destroy() {
}
}
+ /**
+ * @covers FrmForm::set_status
+ */
+ public function test_set_status() {
+ $form_id_1 = $this->factory->form->create();
+ $form_id_2 = $this->factory->form->create();
+
+ FrmForm::set_status( $form_id_1, 'draft' );
+ FrmForm::set_status( $form_id_2, 'draft' );
+
+ $this->assertEquals( 'draft', FrmForm::getOne( $form_id_1 )->status );
+ $this->assertEquals( 'draft', FrmForm::getOne( $form_id_2 )->status );
+
+ // An array of ids runs a single prepared query.
+ $result = FrmForm::set_status( array( $form_id_1, $form_id_2 ), 'published' );
+
+ $this->assertNotFalse( $result );
+ $this->assertEquals( 'published', FrmForm::getOne( $form_id_1 )->status );
+ $this->assertEquals( 'published', FrmForm::getOne( $form_id_2 )->status );
+ }
+
/**
* @group visibility
*
diff --git a/tests/phpunit/forms/test_FrmFormsListHelper.php b/tests/phpunit/forms/test_FrmFormsListHelper.php
new file mode 100644
index 0000000000..c16c0a4933
--- /dev/null
+++ b/tests/phpunit/forms/test_FrmFormsListHelper.php
@@ -0,0 +1,40 @@
+factory->form->create_and_get();
+
+ $post_with_form = $this->factory->post->create(
+ array(
+ 'post_type' => 'page',
+ 'post_status' => 'publish',
+ 'post_content' => 'Before [formidable id=' . $form->id . '] after',
+ )
+ );
+
+ $post_without_form = $this->factory->post->create(
+ array(
+ 'post_type' => 'page',
+ 'post_status' => 'publish',
+ 'post_content' => 'No form embedded here',
+ )
+ );
+
+ $list_helper = new FrmFormsListHelper(
+ array( 'params' => FrmForm::get_admin_params( $form->id ) )
+ );
+
+ $posts = $this->run_private_method( array( $list_helper, 'query_posts_contain_form' ), array( $form ) );
+ $post_ids = array_map( 'intval', wp_list_pluck( $posts, 'ID' ) );
+
+ $this->assertContains( $post_with_form, $post_ids );
+ $this->assertNotContains( $post_without_form, $post_ids );
+ }
+}
diff --git a/tests/phpunit/misc/fixtures/prefer-identifier-placeholder-ruleset.xml b/tests/phpunit/misc/fixtures/prefer-identifier-placeholder-ruleset.xml
new file mode 100644
index 0000000000..5f12cf7ea5
--- /dev/null
+++ b/tests/phpunit/misc/fixtures/prefer-identifier-placeholder-ruleset.xml
@@ -0,0 +1,5 @@
+
+
+ Runs only the PreferIdentifierPlaceholder sniff for its unit tests.
+
+
diff --git a/tests/phpunit/misc/test_FrmPreferIdentifierPlaceholderSniff.php b/tests/phpunit/misc/test_FrmPreferIdentifierPlaceholderSniff.php
new file mode 100644
index 0000000000..25e17f9f7d
--- /dev/null
+++ b/tests/phpunit/misc/test_FrmPreferIdentifierPlaceholderSniff.php
@@ -0,0 +1,314 @@
+process();
+
+ return $file;
+ }
+
+ /**
+ * Collects error sources and fixable flags from a processed file.
+ *
+ * @param \PHP_CodeSniffer\Files\DummyFile $file The processed file.
+ *
+ * @return array Array of array( 'source' => string, 'fixable' => bool ).
+ */
+ private function get_error_list( $file ) {
+ $list = array();
+ foreach ( $file->getErrors() as $line => $cols ) {
+ foreach ( $cols as $col => $errors ) {
+ foreach ( $errors as $error ) {
+ $list[] = array(
+ 'source' => $error['source'],
+ 'fixable' => $error['fixable'],
+ );
+ }
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * Asserts the sniff fixes $code into $expected.
+ *
+ * @param string $code The offending code.
+ * @param string $expected The expected fixed code.
+ *
+ * @return void
+ */
+ private function assert_fixed( $code, $expected ) {
+ $file = $this->process_code( $code );
+ $errors = $this->get_error_list( $file );
+
+ $this->assertNotEmpty( $errors, 'Expected the sniff to flag: ' . $code );
+
+ foreach ( $errors as $error ) {
+ $this->assertSame( 'Formidable.Security.PreferIdentifierPlaceholder.TableInPrepare', $error['source'] );
+ $this->assertTrue( $error['fixable'], 'Expected a fixable error for: ' . $code );
+ }
+
+ $file->fixer->fixFile();
+ $this->assertSame( "fixer->getContents() );
+ }
+
+ /**
+ * Asserts the sniff flags $code with $source and cannot auto-fix it.
+ *
+ * @param string $code The offending code.
+ * @param string $source The expected error source.
+ *
+ * @return void
+ */
+ private function assert_flagged_only( $code, $source ) {
+ $file = $this->process_code( $code );
+ $errors = $this->get_error_list( $file );
+
+ $this->assertNotEmpty( $errors, 'Expected the sniff to flag: ' . $code );
+
+ foreach ( $errors as $error ) {
+ $this->assertSame( $source, $error['source'] );
+ $this->assertFalse( $error['fixable'], 'Expected a non-fixable error for: ' . $code );
+ }
+ }
+
+ /**
+ * Asserts the sniff stays silent for $code.
+ *
+ * @param string $code The clean code.
+ *
+ * @return void
+ */
+ private function assert_clean( $code ) {
+ $file = $this->process_code( $code );
+ $this->assertSame( 0, $file->getErrorCount(), 'Expected no errors for: ' . $code );
+ }
+
+ public function test_fixes_interpolated_prefix_table() {
+ $this->assert_fixed(
+ '$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}frm_item_metas WHERE field_id=%d AND item_id=%d", $field_id, $entry_id ) );',
+ "\$wpdb->query( \$wpdb->prepare( 'DELETE FROM %i WHERE field_id=%d AND item_id=%d', \$wpdb->prefix . 'frm_item_metas', \$field_id, \$entry_id ) );"
+ );
+ }
+
+ public function test_fixes_concatenated_prefix_table() {
+ $this->assert_fixed(
+ "\$wpdb->query( \$wpdb->prepare( 'DELETE FROM ' . \$wpdb->prefix . 'frm_item_metas WHERE field_id=%d', \$id ) );",
+ "\$wpdb->query( \$wpdb->prepare( 'DELETE FROM %i WHERE field_id=%d', \$wpdb->prefix . 'frm_item_metas', \$id ) );"
+ );
+ }
+
+ public function test_fixes_wpdb_property_table() {
+ $this->assert_fixed(
+ "\$ids = \$wpdb->get_col( \$wpdb->prepare( 'SELECT ID FROM ' . \$wpdb->posts . ' WHERE post_type in (%s, %s)', \$a, \$b ) );",
+ "\$ids = \$wpdb->get_col( \$wpdb->prepare( 'SELECT ID FROM %i WHERE post_type in (%s, %s)', \$wpdb->posts, \$a, \$b ) );"
+ );
+ }
+
+ public function test_fixes_dynamic_table_variable() {
+ $this->assert_fixed(
+ "\$result = \$wpdb->get_results( \$wpdb->prepare( 'SHOW COLUMNS FROM ' . \$wpdb->prefix . \$table . ' LIKE %s', \$column ) );",
+ "\$result = \$wpdb->get_results( \$wpdb->prepare( 'SHOW COLUMNS FROM %i LIKE %s', \$wpdb->prefix . \$table, \$column ) );"
+ );
+ }
+
+ public function test_fixes_two_tables_in_one_query() {
+ $this->assert_fixed(
+ "\$wpdb->query( \$wpdb->prepare( 'DELETE fi FROM ' . \$wpdb->prefix . 'frm_fields AS fi LEFT JOIN ' . \$wpdb->prefix"
+ . " . 'frm_forms fr ON (fi.form_id = fr.id) WHERE fi.form_id=%d OR parent_form_id=%d', \$id, \$id ) );",
+ "\$wpdb->query( \$wpdb->prepare( 'DELETE fi FROM %i AS fi LEFT JOIN %i fr ON (fi.form_id = fr.id) WHERE fi.form_id=%d OR parent_form_id=%d',"
+ . " \$wpdb->prefix . 'frm_fields', \$wpdb->prefix . 'frm_forms', \$id, \$id ) );"
+ );
+ }
+
+ public function test_fixes_table_and_keeps_other_interpolation() {
+ $this->assert_fixed(
+ '$query = $wpdb->prepare( "SELECT DISTINCT item_id FROM {$wpdb->prefix}frm_item_metas WHERE meta_value'
+ . ' {$operator} %s and field_id = %d", $search, $field_id );',
+ '$query = $wpdb->prepare( "SELECT DISTINCT item_id FROM %i WHERE meta_value {$operator} %s and field_id = %d",'
+ . ' $wpdb->prefix . \'frm_item_metas\', $search, $field_id );'
+ );
+ }
+
+ public function test_fixes_backticked_interpolated_table() {
+ $this->assert_fixed(
+ '$sub = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}frm_subscriptions` WHERE id = %d", $id ) );',
+ "\$sub = \$wpdb->get_row( \$wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', \$wpdb->prefix . 'frm_subscriptions', \$id ) );"
+ );
+ }
+
+ public function test_fixes_prepare_without_existing_args() {
+ $this->assert_fixed(
+ '$rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}frm_payments WHERE completed is NOT NULL" ) );',
+ "\$rows = \$wpdb->get_results( \$wpdb->prepare( 'SELECT * FROM %i WHERE completed is NOT NULL', \$wpdb->prefix . 'frm_payments' ) );"
+ );
+ }
+
+ public function test_fixes_double_interpolated_table_name() {
+ $this->assert_fixed(
+ '$results = $wpdb->get_results( $wpdb->prepare( "SELECT p.* FROM `{$wpdb->prefix}frm_{$table_name}` p'
+ . ' LEFT JOIN `{$wpdb->prefix}frm_items` e ON p.item_id = e.id WHERE e.id = %d", $id ) );',
+ "\$results = \$wpdb->get_results( \$wpdb->prepare( 'SELECT p.* FROM %i p LEFT JOIN %i e ON p.item_id = e.id WHERE e.id = %d',"
+ . " \$wpdb->prefix . 'frm_' . \$table_name, \$wpdb->prefix . 'frm_items', \$id ) );"
+ );
+ }
+
+ public function test_fixes_create_index_on_table() {
+ $this->assert_fixed(
+ '$wpdb->query( $wpdb->prepare( "CREATE INDEX idx_is_draft_created_at ON {$wpdb->prefix}frm_items (is_draft, created_at)" ) );',
+ "\$wpdb->query( \$wpdb->prepare( 'CREATE INDEX idx_is_draft_created_at ON %i (is_draft, created_at)', \$wpdb->prefix . 'frm_items' ) );"
+ );
+ }
+
+ public function test_fixes_multiline_interpolated_tables() {
+ $code = implode(
+ "\n",
+ array(
+ '$result = $wpdb->get_results(',
+ "\t\$wpdb->prepare(",
+ "\t\t\"SELECT COUNT(*) as items_count",
+ "\t\t\tFROM {\$wpdb->prefix}frm_items AS it INNER JOIN {\$wpdb->prefix}frm_forms AS fr ON it.form_id = fr.id",
+ "\t\t\tWHERE it.created_at BETWEEN %s AND %s\",",
+ "\t\t\$from_date,",
+ "\t\t\$to_date",
+ "\t)",
+ ');',
+ )
+ );
+
+ $expected = implode(
+ "\n",
+ array(
+ '$result = $wpdb->get_results(',
+ "\t\$wpdb->prepare(",
+ "\t\t'SELECT COUNT(*) as items_count",
+ "\t\t\tFROM %i AS it INNER JOIN %i AS fr ON it.form_id = fr.id",
+ "\t\t\tWHERE it.created_at BETWEEN %s AND %s',",
+ "\t\t\$wpdb->prefix . 'frm_items', \$wpdb->prefix . 'frm_forms', \$from_date,",
+ "\t\t\$to_date",
+ "\t)",
+ ');',
+ )
+ );
+
+ $this->assert_fixed( $code, $expected );
+ }
+
+ public function test_flags_unknown_sql_fragment_without_fixing() {
+ $this->assert_flagged_only(
+ "\$wpdb->query( \$wpdb->prepare( 'UPDATE ' . \$wpdb->prefix . 'frm_forms SET status = %s ' . \$where['where'], \$where['values'] ) );",
+ 'Formidable.Security.PreferIdentifierPlaceholder.TableInPrepare'
+ );
+ }
+
+ public function test_flags_variable_alias_without_fixing() {
+ $this->assert_flagged_only(
+ "\$sql = \$wpdb->prepare( ' LEFT JOIN ' . \$wpdb->prefix . 'frm_item_metas em' . \$o_key . ' ON em' . \$o_key . '.field_id=%d ', \$o_field->id );",
+ 'Formidable.Security.PreferIdentifierPlaceholder.TableInPrepare'
+ );
+ }
+
+ public function test_flags_unprepared_query() {
+ $this->assert_flagged_only(
+ "\$wpdb->query( 'DROP TABLE IF EXISTS ' . \$this->fields );",
+ 'Formidable.Security.PreferIdentifierPlaceholder.TableNotPrepared'
+ );
+ }
+
+ public function test_flags_unprepared_interpolated_query() {
+ $this->assert_flagged_only(
+ '$found = $wpdb->get_var( "SELECT 1 FROM {$wpdb->posts} WHERE post_content LIKE \'%[formidable %\' LIMIT 1" );',
+ 'Formidable.Security.PreferIdentifierPlaceholder.TableNotPrepared'
+ );
+ }
+
+ public function test_ignores_create_table_ddl() {
+ $this->assert_clean( '$wpdb->query( "CREATE TABLE {$wpdb->prefix}frm_payments ( id BIGINT )" );' );
+ }
+
+ public function test_ignores_existing_identifier_placeholder() {
+ $this->assert_clean( "\$wpdb->query( \$wpdb->prepare( 'DELETE FROM %i WHERE id = %d', \$wpdb->prefix . 'frm_items', \$id ) );" );
+ }
+
+ public function test_ignores_literal_table_name() {
+ $this->assert_clean( "\$wpdb->query( \$wpdb->prepare( 'DELETE FROM wp_frm_items WHERE id = %d', \$id ) );" );
+ }
+
+ public function test_ignores_frmdb_helper_table_argument() {
+ $this->assert_clean( "\$count = FrmDb::get_var( 'frm_forms', array( 'id' => \$id ), 'COUNT(*)' );" );
+ }
+
+ public function test_ignores_interpolated_alias_after_on() {
+ $this->assert_clean( '$x = $wpdb->prepare( "SELECT * FROM %i em{$o_key} WHERE em{$o_key}.field_id = %d", $wpdb->prefix . \'frm_item_metas\', $o_field->id );' );
+ }
+
+ public function test_ignores_interpolated_alias_in_join_condition() {
+ $this->assert_clean(
+ '$y = $wpdb->prepare( "SELECT * FROM %i pm{$o_key} INNER JOIN %i it ON pm{$o_key}.post_id = it.post_id'
+ . ' WHERE it.id = %d", $wpdb->postmeta, $wpdb->prefix . \'frm_items\', $id );'
+ );
+ }
+}
diff --git a/tests/phpunit/misc/test_FrmWelcomeTourController.php b/tests/phpunit/misc/test_FrmWelcomeTourController.php
new file mode 100644
index 0000000000..eaa421e086
--- /dev/null
+++ b/tests/phpunit/misc/test_FrmWelcomeTourController.php
@@ -0,0 +1,31 @@
+assertFalse( $this->check_for_form_embeds() );
+
+ $this->factory->post->create(
+ array(
+ 'post_content' => 'Before [formidable id=5] after',
+ )
+ );
+
+ $this->assertTrue( $this->check_for_form_embeds() );
+ }
+
+ /**
+ * Calls the private FrmWelcomeTourController::check_for_form_embeds method.
+ *
+ * @return bool
+ */
+ private function check_for_form_embeds() {
+ return $this->run_private_method( array( 'FrmWelcomeTourController', 'check_for_form_embeds' ), array() );
+ }
+}
diff --git a/tests/phpunit/stripe/test_FrmStrpLiteEventsController.php b/tests/phpunit/stripe/test_FrmStrpLiteEventsController.php
new file mode 100644
index 0000000000..c89a29ee00
--- /dev/null
+++ b/tests/phpunit/stripe/test_FrmStrpLiteEventsController.php
@@ -0,0 +1,27 @@
+factory->user->create( array( 'role' => 'subscriber' ) );
+
+ update_user_meta( $user_id, '_frmstrp_customer_id_test', 'cus_abc123' );
+ update_user_meta( $user_id, 'unrelated_meta', 'cus_abc123' );
+
+ $controller = new FrmStrpLiteEventsController();
+ $this->set_private_property( $controller, 'invoice', (object) array( 'id' => 'cus_abc123' ) );
+ $this->run_private_method( array( $controller, 'reset_customer' ), array() );
+
+ // The customer meta is deleted with a direct query, so drop the cached values.
+ clean_user_cache( $user_id );
+
+ $this->assertSame( '', get_user_meta( $user_id, '_frmstrp_customer_id_test', true ) );
+ $this->assertSame( 'cus_abc123', get_user_meta( $user_id, 'unrelated_meta', true ) );
+ }
+}
diff --git a/tests/phpunit/stripe/test_FrmTransLiteCRUDController.php b/tests/phpunit/stripe/test_FrmTransLiteCRUDController.php
new file mode 100644
index 0000000000..012a45a696
--- /dev/null
+++ b/tests/phpunit/stripe/test_FrmTransLiteCRUDController.php
@@ -0,0 +1,42 @@
+upgrade();
+
+ $user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) );
+ wp_set_current_user( $user_id );
+
+ $form = $this->factory->form->create_and_get();
+ $entry = $this->factory->entry->create_and_get( $this->factory->field->generate_entry_array( $form ) );
+ $payment = new FrmTransLitePayment();
+ $payment_id = $payment->create(
+ array(
+ 'receipt_id' => 'rcpt_123',
+ 'item_id' => $entry->id,
+ 'action_id' => 1,
+ 'amount' => 25.00,
+ 'status' => 'complete',
+ 'paysys' => 'stripe',
+ 'created_at' => gmdate( 'Y-m-d H:i:s' ),
+ 'begin_date' => gmdate( 'Y-m-d' ),
+ 'expire_date' => gmdate( 'Y-m-d' ),
+ )
+ );
+
+ $this->assertGreaterThan( 0, $payment_id );
+
+ $row = $this->run_private_method( array( 'FrmTransLiteCRUDController', 'get_payment_row' ), array( $payment_id ) );
+
+ $this->assertEquals( $payment_id, $row->id );
+ $this->assertSame( 'rcpt_123', $row->receipt_id );
+ $this->assertEquals( $entry->user_id, $row->user_id );
+ }
+}
diff --git a/tests/phpunit/stripe/test_FrmTransLiteListHelper.php b/tests/phpunit/stripe/test_FrmTransLiteListHelper.php
new file mode 100644
index 0000000000..9e23862b1d
--- /dev/null
+++ b/tests/phpunit/stripe/test_FrmTransLiteListHelper.php
@@ -0,0 +1,57 @@
+ array() ) );
+ }
+
+ /**
+ * @covers FrmTransLiteListHelper::get_table_query
+ */
+ public function test_get_table_query() {
+ global $wpdb;
+
+ $list_helper = $this->get_list_helper();
+ $query = $this->run_private_method( array( $list_helper, 'get_table_query' ), array() );
+ $this->assertStringContainsString( 'FROM `' . $wpdb->prefix . 'frm_payments` p', $query );
+
+ $form_id = $this->factory->form->create();
+ $_GET['form'] = $form_id;
+ $query = $this->run_private_method( array( $list_helper, 'get_table_query' ), array() );
+
+ unset( $_GET['form'], $_REQUEST['trans_type'] );
+
+ $this->assertStringContainsString( 'FROM `' . $wpdb->prefix . 'frm_payments` p', $query );
+ $this->assertStringContainsString( 'JOIN `' . $wpdb->prefix . 'frm_items` i ON p.item_id = i.id', $query );
+ $this->assertStringContainsString( 'i.form_id = ' . $form_id, $query );
+ }
+
+ /**
+ * @covers FrmTransLiteListHelper::get_form_ids
+ */
+ public function test_get_form_ids() {
+ $form = $this->factory->form->create_and_get();
+ $entry = $this->factory->entry->create_and_get( $this->factory->field->generate_entry_array( $form ) );
+ $list_helper = $this->get_list_helper();
+ $list_helper->items = array(
+ (object) array( 'item_id' => $entry->id ),
+ );
+
+ $form_ids = $this->run_private_method( array( $list_helper, 'get_form_ids' ), array() );
+
+ unset( $_REQUEST['trans_type'] );
+
+ $this->assertArrayHasKey( $entry->id, $form_ids );
+ $this->assertEquals( $form->id, $form_ids[ $entry->id ]->form_id );
+ }
+}
diff --git a/tests/phpunit/stripe/test_FrmTransLiteSubscription.php b/tests/phpunit/stripe/test_FrmTransLiteSubscription.php
new file mode 100644
index 0000000000..cf629d4746
--- /dev/null
+++ b/tests/phpunit/stripe/test_FrmTransLiteSubscription.php
@@ -0,0 +1,118 @@
+upgrade();
+ }
+
+ /**
+ * Creates a subscription row and returns its id.
+ *
+ * @param array $values Values to override the defaults.
+ *
+ * @return int
+ */
+ private function create_subscription( $values = array() ) {
+ $subscription = new FrmTransLiteSubscription();
+ $defaults = array(
+ 'sub_id' => 'sub_test',
+ 'item_id' => 1,
+ 'action_id' => 1,
+ 'amount' => 10.00,
+ 'first_amount' => 10.00,
+ 'interval_count' => 1,
+ 'time_interval' => 'month',
+ 'fail_count' => 0,
+ 'end_count' => 9999,
+ 'next_bill_date' => gmdate( 'Y-m-d', strtotime( '-2 days' ) ),
+ 'status' => 'active',
+ 'paysys' => 'stripe',
+ 'created_at' => gmdate( 'Y-m-d H:i:s' ),
+ );
+
+ return $subscription->create( array_merge( $defaults, $values ) );
+ }
+
+ /**
+ * @covers FrmTransLiteSubscription::get_overdue_subscriptions
+ */
+ public function test_get_overdue_subscriptions() {
+ $overdue_active_id = $this->create_subscription();
+ $overdue_cancel_id = $this->create_subscription( array( 'status' => 'future_cancel' ) );
+ $failed_id = $this->create_subscription( array( 'fail_count' => 3 ) );
+ $future_id = $this->create_subscription( array( 'next_bill_date' => gmdate( 'Y-m-d', strtotime( '+2 days' ) ) ) );
+ $canceled_id = $this->create_subscription( array( 'status' => 'canceled' ) );
+
+ $this->assertGreaterThan( 0, $overdue_active_id );
+
+ $subscription = new FrmTransLiteSubscription();
+ $overdue_ids = array_map( 'intval', wp_list_pluck( $subscription->get_overdue_subscriptions(), 'id' ) );
+
+ $this->assertContains( $overdue_active_id, $overdue_ids );
+ $this->assertContains( $overdue_cancel_id, $overdue_ids );
+ $this->assertNotContains( $failed_id, $overdue_ids );
+ $this->assertNotContains( $future_id, $overdue_ids );
+ $this->assertNotContains( $canceled_id, $overdue_ids );
+ }
+
+ /**
+ * @covers FrmTransLiteDb::get_one
+ * @covers FrmTransLiteDb::get_one_by
+ * @covers FrmTransLiteDb::get_all_by
+ * @covers FrmTransLiteDb::get_count
+ * @covers FrmTransLiteDb::update
+ * @covers FrmTransLiteDb::destroy
+ */
+ public function test_subscription_crud() {
+ $subscription = new FrmTransLiteSubscription();
+ $id = $this->create_subscription(
+ array(
+ 'sub_id' => 'sub_crud',
+ 'item_id' => 42,
+ )
+ );
+
+ $this->assertGreaterThan( 0, $id );
+
+ $row = $subscription->get_one( $id );
+ $this->assertSame( 'sub_crud', $row->sub_id );
+ $this->assertSame( 'active', $row->status );
+
+ $row = $subscription->get_one_by( 'sub_crud', 'sub_id' );
+ $this->assertEquals( $id, $row->id );
+
+ $rows = $subscription->get_all_by( 42, 'item_id' );
+ $this->assertCount( 1, $rows );
+ $this->assertEquals( $id, $rows[0]->id );
+
+ $this->assertGreaterThanOrEqual( 1, (int) $subscription->get_count() );
+
+ $subscription->update( $id, array( 'status' => 'canceled' ) );
+ $this->assertSame( 'canceled', $subscription->get_one( $id )->status );
+
+ $this->set_user_by_role( 'administrator' );
+ $subscription->destroy( $id );
+ $this->assertNull( $subscription->get_one( $id ) );
+ }
+
+ /**
+ * @covers FrmTransLiteDb::get_all_for_user
+ */
+ public function test_get_all_for_user() {
+ $user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) );
+ wp_set_current_user( $user_id );
+
+ $form = $this->factory->form->create_and_get();
+ $entry = $this->factory->entry->create_and_get( $this->factory->field->generate_entry_array( $form ) );
+ $sub_id = $this->create_subscription( array( 'item_id' => $entry->id ) );
+ $subscription = new FrmTransLiteSubscription();
+ $rows = $subscription->get_all_for_user( $entry->user_id );
+
+ $this->assertContains( $sub_id, array_map( 'intval', wp_list_pluck( $rows, 'id' ) ) );
+ }
+}
diff --git a/tests/phpunit/stripe/test_FrmTransLiteSubscriptionsController.php b/tests/phpunit/stripe/test_FrmTransLiteSubscriptionsController.php
new file mode 100644
index 0000000000..872a350fbc
--- /dev/null
+++ b/tests/phpunit/stripe/test_FrmTransLiteSubscriptionsController.php
@@ -0,0 +1,33 @@
+factory->user->create( array( 'role' => 'subscriber' ) );
+ wp_set_current_user( $user_id );
+
+ $form = $this->factory->form->create_and_get();
+ $entry = $this->factory->entry->create_and_get( $this->factory->field->generate_entry_array( $form ) );
+
+ $sub = (object) array(
+ 'id' => 5,
+ 'item_id' => $entry->id,
+ 'status' => 'active',
+ 'paysys' => 'stripe',
+ );
+
+ ob_start();
+ FrmTransLiteSubscriptionsController::show_cancel_link( $sub );
+ $output = ob_get_clean();
+
+ $this->assertTrue( property_exists( $sub, 'user_id' ) );
+ $this->assertEquals( $entry->user_id, $sub->user_id );
+ $this->assertNotEmpty( $output );
+ }
+}
diff --git a/tests/phpunit/styles/test_FrmStyle.php b/tests/phpunit/styles/test_FrmStyle.php
index 5c8bb689b8..29446f8b06 100644
--- a/tests/phpunit/styles/test_FrmStyle.php
+++ b/tests/phpunit/styles/test_FrmStyle.php
@@ -156,4 +156,38 @@ private function trim_braces( $value ) {
$frm_style = new FrmStyle();
return $this->run_private_method( array( $frm_style, 'trim_braces' ), array( $value ) );
}
+
+ /**
+ * @covers FrmStyle::get_all
+ */
+ public function test_get_all() {
+ $frm_style = new FrmStyle();
+ $styles = $frm_style->get_all();
+
+ $this->assertNotEmpty( $styles );
+
+ foreach ( $styles as $style ) {
+ $this->assertSame( FrmStylesController::$post_type, $style->post_type );
+ }
+
+ // With every style removed, a new default style is created on the fly.
+ $style_posts = get_posts(
+ array(
+ 'post_type' => FrmStylesController::$post_type,
+ 'post_status' => 'any',
+ 'numberposts' => -1,
+ )
+ );
+
+ foreach ( $style_posts as $style_post ) {
+ wp_delete_post( $style_post->ID, true );
+ }
+
+ wp_cache_flush();
+
+ $styles = $frm_style->get_all();
+
+ $this->assertNotEmpty( $styles );
+ $this->assertSame( FrmStylesController::$post_type, reset( $styles )->post_type );
+ }
}