diff --git a/includes/collector/class-prv-ai-visibility-collector.php b/includes/collector/class-prv-ai-visibility-collector.php
index d45bfe5..cb4fbe3 100644
--- a/includes/collector/class-prv-ai-visibility-collector.php
+++ b/includes/collector/class-prv-ai-visibility-collector.php
@@ -1,4 +1,5 @@
(string) $row['run_id'],
+ 'run_id' => (string) $row['run_id'],
'captured_at' => (string) $row['captured_at'],
'score' => $this->compute_score(
(int) $row['cited_count'],
diff --git a/includes/collector/interface-prv-data-collector.php b/includes/collector/interface-prv-data-collector.php
index 8a2dd33..c77c647 100644
--- a/includes/collector/interface-prv-data-collector.php
+++ b/includes/collector/interface-prv-data-collector.php
@@ -1,10 +1,11 @@
self::MENU_SLUG,
- 'prv_run_done' => 1,
- 'prv_probed' => (int) $counts['probed'],
- 'prv_skipped' => (int) ( $counts['skipped_budget'] + $counts['skipped_error'] ),
+ 'page' => self::MENU_SLUG,
+ 'prv_run_done' => 1,
+ 'prv_probed' => (int) $counts['probed'],
+ 'prv_skipped' => (int) ( $counts['skipped_budget'] + $counts['skipped_error'] ),
),
admin_url( 'admin.php' )
);
@@ -183,10 +184,10 @@ class PRV_Admin_Page {
if ( empty( $_GET['prv_run_done'] ) ) {
return;
}
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended
- $probed = (int) ( $_GET['prv_probed'] ?? 0 );
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended
- $skipped = (int) ( $_GET['prv_skipped'] ?? 0 );
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
+ $probed = isset( $_GET['prv_probed'] ) ? absint( wp_unslash( $_GET['prv_probed'] ) ) : 0;
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
+ $skipped = isset( $_GET['prv_skipped'] ) ? absint( wp_unslash( $_GET['prv_skipped'] ) ) : 0;
echo '
';
echo esc_html(
diff --git a/includes/core/class-prv-autoloader.php b/includes/core/class-prv-autoloader.php
index b48b1ce..62ea42b 100644
--- a/includes/core/class-prv-autoloader.php
+++ b/includes/core/class-prv-autoloader.php
@@ -1,4 +1,5 @@
'bpc-157', 'label' => 'BPC-157' ),
- array( 'slug' => 'tb-500', 'label' => 'TB-500' ),
- array( 'slug' => 'mk-677', 'label' => 'MK-677' ),
- array( 'slug' => 'cjc-1295', 'label' => 'CJC-1295' ),
- array( 'slug' => 'ghrp-6', 'label' => 'GHRP-6' ),
- array( 'slug' => 'ipamorelin', 'label' => 'Ipamorelin' ),
- array( 'slug' => 'semaglutide', 'label' => 'Semaglutide' ),
- array( 'slug' => 'tirzepatide', 'label' => 'Tirzepatide' ),
- array( 'slug' => 'selank', 'label' => 'Selank' ),
- array( 'slug' => 'semax', 'label' => 'Semax' ),
- array( 'slug' => 'nad-plus', 'label' => 'NAD+' ),
- array( 'slug' => 'aod-9604', 'label' => 'AOD-9604' ),
+ array(
+ 'slug' => 'bpc-157',
+ 'label' => 'BPC-157',
+ ),
+ array(
+ 'slug' => 'tb-500',
+ 'label' => 'TB-500',
+ ),
+ array(
+ 'slug' => 'mk-677',
+ 'label' => 'MK-677',
+ ),
+ array(
+ 'slug' => 'cjc-1295',
+ 'label' => 'CJC-1295',
+ ),
+ array(
+ 'slug' => 'ghrp-6',
+ 'label' => 'GHRP-6',
+ ),
+ array(
+ 'slug' => 'ipamorelin',
+ 'label' => 'Ipamorelin',
+ ),
+ array(
+ 'slug' => 'semaglutide',
+ 'label' => 'Semaglutide',
+ ),
+ array(
+ 'slug' => 'tirzepatide',
+ 'label' => 'Tirzepatide',
+ ),
+ array(
+ 'slug' => 'selank',
+ 'label' => 'Selank',
+ ),
+ array(
+ 'slug' => 'semax',
+ 'label' => 'Semax',
+ ),
+ array(
+ 'slug' => 'nad-plus',
+ 'label' => 'NAD+',
+ ),
+ array(
+ 'slug' => 'aod-9604',
+ 'label' => 'AOD-9604',
+ ),
);
}
diff --git a/includes/core/class-prv-cost-ledger.php b/includes/core/class-prv-cost-ledger.php
index 8165bfa..a1c7fb9 100644
--- a/includes/core/class-prv-cost-ledger.php
+++ b/includes/core/class-prv-cost-ledger.php
@@ -1,4 +1,5 @@
get_month_to_date_usd();
+ $cap = PRV_Config::get_monthly_budget_usd();
+ $spent = $this->get_month_to_date_usd();
return ( $spent + $estimated_cost_usd ) <= $cap;
}
@@ -95,7 +96,7 @@ class PRV_Cost_Ledger {
$table = PRV_Table_Manager::get_table_name();
- // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$result = $wpdb->update(
$table,
array( 'cost_usd' => number_format( $cost_usd, 8, '.', '' ) ),
diff --git a/includes/core/class-prv-cron.php b/includes/core/class-prv-cron.php
index 9816ea5..dcf1f52 100644
--- a/includes/core/class-prv-cron.php
+++ b/includes/core/class-prv-cron.php
@@ -1,4 +1,5 @@
0,
- 'skipped_budget' => 0,
- 'skipped_error' => 0,
+ 'probed' => 0,
+ 'skipped_budget' => 0,
+ 'skipped_error' => 0,
);
foreach ( $peptides as $peptide ) {
@@ -66,22 +71,23 @@ class PRV_Probe_Runner {
$provider = $this->resolve_provider( $model );
if ( null === $provider || ! $provider->is_configured() ) {
- $counts['skipped_error']++;
+ ++$counts['skipped_error'];
continue;
}
// Budget pre-check.
$estimated = $this->get_estimated_cost( $model );
if ( ! $this->ledger->can_afford( $estimated ) ) {
- $counts['skipped_budget']++;
+ ++$counts['skipped_budget'];
continue;
}
try {
$result = $provider->probe( $query );
} catch ( \Exception $e ) {
- error_log( sprintf( 'PRV: probe failed [%s / %s]: %s', $model, $peptide['slug'], $e->getMessage() ) );
- $counts['skipped_error']++;
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ error_log( sprintf( 'PRV: probe failed [%s / %s]: %s', $model, $peptide['slug'], $e->getMessage() ) );
+ ++$counts['skipped_error'];
continue;
}
@@ -90,7 +96,7 @@ class PRV_Probe_Runner {
$this->ledger->update_row_cost( $row_id, $result->get_cost_usd() );
}
- $counts['probed']++;
+ ++$counts['probed'];
}
}
}
@@ -101,11 +107,11 @@ class PRV_Probe_Runner {
/**
* Persist a single probe result to the database.
*
- * @param string $run_id UUID for this run.
- * @param array{slug:string, label:string} $peptide Peptide config.
- * @param string $model Model identifier.
- * @param string $intent_tpl Raw intent template string.
- * @param PRV_Probe_Result $result Probe result.
+ * @param string $run_id UUID for this run.
+ * @param array $peptide Peptide config with slug and label.
+ * @param string $model Model identifier.
+ * @param string $intent_tpl Raw intent template string.
+ * @param PRV_Probe_Result $result Probe result.
*
* @return int Inserted row ID, or 0 on failure.
*/
@@ -118,17 +124,17 @@ class PRV_Probe_Runner {
$wpdb->insert(
$table,
array(
- 'run_id' => $run_id,
- 'captured_at' => current_time( 'mysql', true ),
- 'peptide_slug' => $peptide['slug'],
- 'peptide_label' => $peptide['label'],
- 'model' => $model,
- 'prompt_intent' => $intent_tpl,
- 'cited' => $result->is_cited() ? 1 : 0,
- 'our_position' => $result->get_our_position(),
+ 'run_id' => $run_id,
+ 'captured_at' => current_time( 'mysql', true ),
+ 'peptide_slug' => $peptide['slug'],
+ 'peptide_label' => $peptide['label'],
+ 'model' => $model,
+ 'prompt_intent' => $intent_tpl,
+ 'cited' => $result->is_cited() ? 1 : 0,
+ 'our_position' => $result->get_our_position(),
'source_domains' => wp_json_encode( $result->get_source_domains() ),
- 'raw_excerpt' => $result->get_raw_excerpt(),
- 'cost_usd' => number_format( $result->get_cost_usd(), 8, '.', '' ),
+ 'raw_excerpt' => $result->get_raw_excerpt(),
+ 'cost_usd' => number_format( $result->get_cost_usd(), 8, '.', '' ),
),
array( '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%s', '%s' )
);
diff --git a/includes/core/class-prv-table-manager.php b/includes/core/class-prv-table-manager.php
index 020f52f..81592c4 100644
--- a/includes/core/class-prv-table-manager.php
+++ b/includes/core/class-prv-table-manager.php
@@ -1,4 +1,5 @@
prefix . self::TABLE_BASE;
- // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( "DROP TABLE IF EXISTS {$table}" );
}
}
diff --git a/includes/panel/class-prv-ai-visibility-panel.php b/includes/panel/class-prv-ai-visibility-panel.php
index dce78ec..3ccf5df 100644
--- a/includes/panel/class-prv-ai-visibility-panel.php
+++ b/includes/panel/class-prv-ai-visibility-panel.php
@@ -1,4 +1,5 @@
render_proxy_note();
$this->render_meta_bar( $last_run_at, $mtd_cost, $cap );
@@ -96,7 +97,7 @@ class PRV_Ai_Visibility_Panel implements PRV_Dashboard_Panel {
$labels = array();
$scores = array();
foreach ( $trendline as $point ) {
- $labels[] = esc_js( date( 'M j', strtotime( $point['captured_at'] ) ) );
+ $labels[] = esc_js( gmdate( 'M j', (int) strtotime( $point['captured_at'] ) ) );
$scores[] = (float) $point['score'];
}
@@ -104,8 +105,10 @@ class PRV_Ai_Visibility_Panel implements PRV_Dashboard_Panel {
$scores_json = wp_json_encode( $scores );
echo '';
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '';
echo '
';
diff --git a/includes/panel/interface-prv-dashboard-panel.php b/includes/panel/interface-prv-dashboard-panel.php
index 4c06320..9375c6a 100644
--- a/includes/panel/interface-prv-dashboard-panel.php
+++ b/includes/panel/interface-prv-dashboard-panel.php
@@ -1,4 +1,5 @@
= 400 ) {
+ // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
throw new \RuntimeException(
sprintf( 'PR Vision gateway HTTP %d: %s', $status, substr( $raw_body, 0, 300 ) )
);
@@ -120,6 +122,7 @@ class PRV_Gateway_Client {
remove_action( 'http_api_curl', $curl_filter, 99 );
}
+ // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
throw new \RuntimeException(
sprintf( 'PR Vision: gateway failed after %d attempts. Last: %s', PRV_MAX_RETRIES, $last_error )
);
diff --git a/includes/providers/class-prv-openrouter-provider.php b/includes/providers/class-prv-openrouter-provider.php
index f7fc425..72aebec 100644
--- a/includes/providers/class-prv-openrouter-provider.php
+++ b/includes/providers/class-prv-openrouter-provider.php
@@ -1,4 +1,5 @@
model = $model;
- $this->gateway = $gateway ?? new PRV_Gateway_Client();
+ $this->gateway = $gateway ?? new PRV_Gateway_Client();
$this->detector = $detector ?? new PRV_Citation_Detector();
}
/**
* {@inheritDoc}
*
- * @param string $query
+ * @param string $query The search query to probe.
+ *
* @return PRV_Probe_Result
* @throws \RuntimeException On permanent API failure.
*/
@@ -71,7 +81,10 @@ class PRV_OpenRouter_Provider implements PRV_Probe_Provider {
$body = array(
'model' => $this->model,
'messages' => array(
- array( 'role' => 'user', 'content' => $query ),
+ array(
+ 'role' => 'user',
+ 'content' => $query,
+ ),
),
'max_tokens' => 512,
);
diff --git a/includes/providers/class-prv-perplexity-provider.php b/includes/providers/class-prv-perplexity-provider.php
index 9ac205d..be564bb 100644
--- a/includes/providers/class-prv-perplexity-provider.php
+++ b/includes/providers/class-prv-perplexity-provider.php
@@ -1,4 +1,5 @@
gateway = $gateway ?? new PRV_Gateway_Client();
+ $this->gateway = $gateway ?? new PRV_Gateway_Client();
$this->detector = $detector ?? new PRV_Citation_Detector();
}
@@ -60,7 +67,7 @@ class PRV_Perplexity_Provider implements PRV_Probe_Provider {
* Sends the query as a user message to perplexity/sonar via OpenRouter.
* Parses the citations[] array from the OpenRouter response envelope.
*
- * @param string $query
+ * @param string $query The search query to probe.
*
* @return PRV_Probe_Result
* @throws \RuntimeException On permanent API failure.
@@ -69,9 +76,12 @@ class PRV_Perplexity_Provider implements PRV_Probe_Provider {
$api_key = $this->resolve_api_key();
$body = array(
- 'model' => self::MODEL,
- 'messages' => array(
- array( 'role' => 'user', 'content' => $query ),
+ 'model' => self::MODEL,
+ 'messages' => array(
+ array(
+ 'role' => 'user',
+ 'content' => $query,
+ ),
),
'max_tokens' => 512,
);
@@ -135,11 +145,12 @@ class PRV_Perplexity_Provider implements PRV_Probe_Provider {
/**
* Retrieve the OpenRouter API key from wp-config constants.
*
- * @return string
+ * @return string The API key.
* @throws \RuntimeException When the constant is not defined or empty.
*/
private function resolve_api_key(): string {
if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) || '' === PRV_OPENROUTER_API_KEY ) {
+ // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
throw new \RuntimeException(
__( 'PRV_OPENROUTER_API_KEY constant is not defined. Add it to wp-config.php.', 'pr-vision' )
);
diff --git a/includes/providers/interface-prv-probe-provider.php b/includes/providers/interface-prv-probe-provider.php
index dd6c24d..1598d1e 100644
--- a/includes/providers/interface-prv-probe-provider.php
+++ b/includes/providers/interface-prv-probe-provider.php
@@ -1,4 +1,5 @@
prefix . 'prv_ai_visibility';
-// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( "DROP TABLE IF EXISTS {$table}" );
/* ── 2. Delete all prv_ prefixed options ──────────────────────────── */
-// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
+// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE 'prv\_%'"
);