Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/api/api.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ int api_stats_top_domains(struct ftl_conn *api);
int api_stats_top_clients(struct ftl_conn *api);
int api_stats_recentblocked(struct ftl_conn *api);
cJSON *get_top_domains(struct ftl_conn *api, const int count,
const bool blocked, const bool domains_only);
const bool blocked, const bool domains_only,
const int adlist_id);
cJSON *get_top_clients(struct ftl_conn *api, const int count,
const bool blocked, const bool clients_only,
const bool names_only, const bool ip_if_no_name);
Expand Down
13 changes: 13 additions & 0 deletions src/api/docs/content/specs/stats.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ components:
parameters:
- $ref: 'stats.yaml#/components/parameters/top_items/blocked'
- $ref: 'stats.yaml#/components/parameters/top_items/count'
- $ref: 'stats.yaml#/components/parameters/top_items/list'
responses:
'200':
description: OK
Expand Down Expand Up @@ -799,3 +800,15 @@ components:
type: integer
required: false
example: 10
list:
in: query
description: >
Filter results to domains that belong to a specific subscription list
(adlist). The value is the integer ID of the adlist as returned by
GET /api/lists. When omitted (or negative), results from all lists
are returned.
name: list
schema:
type: integer
required: false
example: 1
4 changes: 2 additions & 2 deletions src/api/padd.c
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ int api_padd(struct ftl_conn *api)
JSON_ADD_NUMBER_TO_OBJECT(json, "active_clients", active_clients);
JSON_ADD_NUMBER_TO_OBJECT(json, "gravity_size", num_gravity);

cJSON *top_domains = get_top_domains(api, 1, false, true);
cJSON *top_domains = get_top_domains(api, 1, false, true, -1);
if(cJSON_GetArraySize(top_domains) == 0)
{
JSON_ADD_NULL_TO_OBJECT(json, "top_domain");
Expand All @@ -87,7 +87,7 @@ int api_padd(struct ftl_conn *api)
JSON_COPY_STR_TO_OBJECT(json, "top_domain", domain);
}
cJSON_Delete(top_domains);
cJSON *top_blocked = get_top_domains(api, 1, true, true);
cJSON *top_blocked = get_top_domains(api, 1, true, true, -1);
if(cJSON_GetArraySize(top_blocked) == 0)
{
JSON_ADD_NULL_TO_OBJECT(json, "top_blocked");
Expand Down
4 changes: 2 additions & 2 deletions src/api/queries.c
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ int api_queries_suggestions(struct ftl_conn *api)
get_int_var(api->request->query_string, "count", &count);

// Get domains
cJSON *domain = get_top_domains(api, count, false, true);
cJSON *blocked = get_top_domains(api, count, true, true);
cJSON *domain = get_top_domains(api, count, false, true, -1);
cJSON *blocked = get_top_domains(api, count, true, true, -1);
// Add domains from both arrays, avoiding duplicates
cJSON *entry = NULL;
cJSON_ArrayForEach(entry, blocked)
Expand Down
22 changes: 19 additions & 3 deletions src/api/stats.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
#include "overTime.h"
// enum REGEX
#include "regex_r.h"
// gravityDB_domain_in_adlist()
#include "database/gravity-db.h"
// sqrt()
#include <math.h>

Expand Down Expand Up @@ -177,7 +179,8 @@ int api_stats_summary(struct ftl_conn *api)
}

cJSON *get_top_domains(struct ftl_conn *api, const int count,
const bool blocked, const bool domains_only)
const bool blocked, const bool domains_only,
const int adlist_id)
{
// Exit before processing any data if requested via config setting
if(config.misc.privacylevel.v.privacy_level >= PRIVACY_HIDE_DOMAINS)
Expand Down Expand Up @@ -278,6 +281,14 @@ cJSON *get_top_domains(struct ftl_conn *api, const int count,
}
}

// When a specific adlist is requested, skip domains that don't
// belong to it. We check gravity.db here (outside SHM lock would
// be cleaner, but the lock is re-acquired above — safe to call
// gravityDB helpers while holding SHM lock as they use their own
// SQLite handle).
if(adlist_id >= 0 && !gravityDB_domain_in_adlist(domain, adlist_id))
skip_domain = true;

if(skip_domain || top_domains[i].count < 1)
continue;

Expand Down Expand Up @@ -333,7 +344,9 @@ int api_stats_top_domains(struct ftl_conn *api)
{
bool blocked = false; // Can be overwritten by query string
int count = 10;
// /api/stats/top_domains?blocked=true
// adlist_id < 0 means "all lists" (default / no filter)
int adlist_id = -1;
// /api/stats/top_domains?blocked=true&count=N&list=<adlist_id>
if(api->request->query_string != NULL)
{
// Should blocked domains be shown?
Expand All @@ -342,9 +355,12 @@ int api_stats_top_domains(struct ftl_conn *api)
// Does the user request a non-default number of replies?
// Note: We do not accept zero query requests here
get_int_var(api->request->query_string, "count", &count);

// Optional filter: restrict results to a specific subscription list
get_int_var(api->request->query_string, "list", &adlist_id);
}

cJSON *json = get_top_domains(api, count, blocked, false);
cJSON *json = get_top_domains(api, count, blocked, false, adlist_id);
JSON_SEND_OBJECT(json);
}

Expand Down
56 changes: 56 additions & 0 deletions src/database/gravity-db.c
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,62 @@ static enum db_result domain_in_list(const char *domain, sqlite3_stmt *stmt, con
return (rc == SQLITE_ROW) ? FOUND : NOT_FOUND;
}

// Check whether a given domain appears in the gravity table for a specific
// adlist. Returns true when the domain is found, false otherwise (including
// when the gravity database is not available).
bool gravityDB_domain_in_adlist(const char *domain, const int adlist_id)
{
// Open gravity database if not already open
if(!gravityDB_opened && !gravityDB_open())
{
log_warn("gravityDB_domain_in_adlist(\"%s\", %d): Gravity database not available",
domain, adlist_id);
return false;
}

sqlite3_stmt *stmt = NULL;
const char *querystr = "SELECT EXISTS(SELECT 1 FROM gravity WHERE domain = ?1 AND adlist_id = ?2);";
int rc = sqlite3_prepare_v2(gravity_db, querystr, -1, &stmt, NULL);
if(rc != SQLITE_OK)
{
log_err("gravityDB_domain_in_adlist(\"%s\", %d): Failed to prepare statement: %s",
domain, adlist_id, sqlite3_errstr(rc));
return false;
}

// Bind domain
if((rc = sqlite3_bind_text(stmt, 1, domain, -1, SQLITE_STATIC)) != SQLITE_OK)
{
log_err("gravityDB_domain_in_adlist(\"%s\", %d): Failed to bind domain: %s",
domain, adlist_id, sqlite3_errstr(rc));
sqlite3_finalize(stmt);
return false;
}

// Bind adlist_id
if((rc = sqlite3_bind_int(stmt, 2, adlist_id)) != SQLITE_OK)
{
log_err("gravityDB_domain_in_adlist(\"%s\", %d): Failed to bind adlist_id: %s",
domain, adlist_id, sqlite3_errstr(rc));
sqlite3_finalize(stmt);
return false;
}

// Execute
rc = sqlite3_step(stmt);
bool found = false;
if(rc == SQLITE_ROW)
found = sqlite3_column_int(stmt, 0) != 0;
else if(rc != SQLITE_DONE)
log_err("gravityDB_domain_in_adlist(\"%s\", %d): Step failed: %s",
domain, adlist_id, sqlite3_errstr(rc));

sqlite3_finalize(stmt);
log_debug(DEBUG_DATABASE, "gravityDB_domain_in_adlist(\"%s\", %d): %s",
domain, adlist_id, found ? "found" : "not found");
return found;
}

void gravityDB_reload_groups(clientsData *client)
{
// Rebuild client table statements (possibly from a different group set)
Expand Down
2 changes: 2 additions & 0 deletions src/database/gravity-db.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,6 @@ bool gravityDB_edit_groups(const enum gravity_list_type listtype, cJSON *groups,

time_t gravity_last_updated(void) __attribute__((pure));

bool gravityDB_domain_in_adlist(const char *domain, const int adlist_id);

#endif //GRAVITY_H