| @@ -152,9 +152,9 @@ | ||
| 152 | 152 | $params = $request->get_params(); |
| 153 | 153 | $status = !empty($params['status']) ? $params['status'] : "all"; |
| 154 | 154 | $page = !empty($params['page']) ? intval( $params['page'] ) : 1; |
| 155 | 155 | $per_page = !empty($params['per_page']) ? intval( $params['per_page'] ) : 20; |
| 156 | - $search_keyword = !empty($params['s']) ? $params['s'] : ''; | |
| 156 | + $search_keyword = !empty($params['s']) ? sanitize_text_field($params['s']) : ''; | |
| 157 | 157 | $start_from = ($page - 1) * $per_page; |
| 158 | 158 | $query = Database::get_instance()->query() |
| 159 | 159 | ->from('nx_posts a') |
| 160 | 160 | ->join('nx_stats b', 'b.nx_id', '=', 'a.nx_id') |
| @@ -164,11 +164,16 @@ | ||
| 164 | 164 | if ($status !== 'all') { |
| 165 | 165 | $query->where('enabled', $status == 'enabled' ? true : false); |
| 166 | 166 | } |
| 167 | 167 | if( $search_keyword ) { |
| 168 | - $query->where(function($query) use ($search_keyword) { | |
| 169 | - $query->where('title', 'LIKE', '%' . $search_keyword . '%') | |
| 170 | - ->orWhere( 'a.nx_id', 'LIKE', '%'. $search_keyword . '%' ); | |
| 168 | + global $wpdb; | |
| 169 | + // esc_like() so `%` and `_` typed into the search box match themselves | |
| 170 | + // instead of acting as wildcards; the pattern is bound as a value by the | |
| 171 | + // query builder. | |
| 172 | + $like = '%' . $wpdb->esc_like( $search_keyword ) . '%'; | |
| 173 | + $query->where(function($query) use ($like) { | |
| 174 | + $query->where('title', 'LIKE', $like) | |
| 175 | + ->orWhere( 'a.nx_id', 'LIKE', $like ); | |
| 171 | 176 | }); |
| 172 | 177 | } |
| 173 | 178 | |
| 174 | 179 | $query->offset($start_from) |
| @@ -178,9 +183,11 @@ | ||
| 178 | 183 | |
| 179 | 184 | // Add entries count for popup notifications |
| 180 | 185 | global $wpdb; |
| 181 | 186 | $entries_table = $wpdb->prefix . 'nx_entries'; |
| 187 | + // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- False positive: the query is prepared via $this->wpdb->prepare(), which this sniff does not recognise, and only $wpdb->prefix table names are interpolated. Audited 2026-07-16. | |
| 182 | 188 | $entries_counts = $wpdb->get_results( |
| 189 | + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- False positive: the query is prepared via $this->wpdb->prepare(), which this sniff does not recognise, and only $wpdb->prefix table names are interpolated. Audited 2026-07-16. | |
| 183 | 190 | "SELECT nx_id, COUNT(*) as entries_count |
| 184 | 191 | FROM {$entries_table} |
| 185 | 192 | WHERE source IN ('popup_notification', 'exit_intent_custom') |
| 186 | 193 | GROUP BY nx_id", |
| @@ -185,8 +192,9 @@ | ||
| 185 | 192 | WHERE source IN ('popup_notification', 'exit_intent_custom') |
| 186 | 193 | GROUP BY nx_id", |
| 187 | 194 | ARRAY_A |
| 188 | 195 | ); |
| 196 | + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 189 | 197 | |
| 190 | 198 | // Create a lookup array for entries counts |
| 191 | 199 | $entries_lookup = []; |
| 192 | 200 | foreach ($entries_counts as $entry) { |