fix: convert all PHP files from space indentation to tabs (PHPCS)
WordPress coding standards require tab indentation. All 12 PHP files in includes/, admin/, and public/ were using 4-space indentation. Converted to tabs to pass the Generic.WhiteSpace.DisallowSpaceIndent PHPCS rule. Whitespace-only change — no logic modifications. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$raw_start = isset( $_GET['start_date'] ) ? sanitize_text_field( wp_unslash( $_GET['start_date'] ) ) : '';
|
$raw_start = isset( $_GET['start_date'] ) ? sanitize_text_field( wp_unslash( $_GET['start_date'] ) ) : '';
|
||||||
@@ -27,126 +27,126 @@ $total_articles = count( $top_articles );
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="wrap peptide-news-dashboard">
|
<div class="wrap peptide-news-dashboard">
|
||||||
<h1><?php esc_html_e( 'Peptide News Analytics', 'peptide-news' ); ?></h1>
|
<h1><?php esc_html_e( 'Peptide News Analytics', 'peptide-news' ); ?></h1>
|
||||||
|
|
||||||
<!-- Date range filter -->
|
<!-- Date range filter -->
|
||||||
<form method="get" class="pn-date-filter">
|
<form method="get" class="pn-date-filter">
|
||||||
<input type="hidden" name="page" value="peptide-news-dashboard" />
|
<input type="hidden" name="page" value="peptide-news-dashboard" />
|
||||||
<label for="start_date"><?php esc_html_e( 'From:', 'peptide-news' ); ?></label>
|
<label for="start_date"><?php esc_html_e( 'From:', 'peptide-news' ); ?></label>
|
||||||
<input type="date" id="start_date" name="start_date" value="<?php echo esc_attr( $start_date ); ?>" />
|
<input type="date" id="start_date" name="start_date" value="<?php echo esc_attr( $start_date ); ?>" />
|
||||||
<label for="end_date"><?php esc_html_e( 'To:', 'peptide-news' ); ?></label>
|
<label for="end_date"><?php esc_html_e( 'To:', 'peptide-news' ); ?></label>
|
||||||
<input type="date" id="end_date" name="end_date" value="<?php echo esc_attr( $end_date ); ?>" />
|
<input type="date" id="end_date" name="end_date" value="<?php echo esc_attr( $end_date ); ?>" />
|
||||||
<button type="submit" class="button button-primary"><?php esc_html_e( 'Filter', 'peptide-news' ); ?></button>
|
<button type="submit" class="button button-primary"><?php esc_html_e( 'Filter', 'peptide-news' ); ?></button>
|
||||||
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=peptide-news-dashboard&start_date=' . urlencode( $start_date ) . '&end_date=' . urlencode( $end_date ) . '&export=csv' ), 'peptide_news_export_csv' ) ); ?>" class="button"><?php esc_html_e( 'Export CSV', 'peptide-news' ); ?></a>
|
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=peptide-news-dashboard&start_date=' . urlencode( $start_date ) . '&end_date=' . urlencode( $end_date ) . '&export=csv' ), 'peptide_news_export_csv' ) ); ?>" class="button"><?php esc_html_e( 'Export CSV', 'peptide-news' ); ?></a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Summary cards -->
|
<!-- Summary cards -->
|
||||||
<div class="pn-summary-cards">
|
<div class="pn-summary-cards">
|
||||||
<div class="pn-card">
|
<div class="pn-card">
|
||||||
<h3><?php echo esc_html( number_format( $total_clicks ) ); ?></h3>
|
<h3><?php echo esc_html( number_format( $total_clicks ) ); ?></h3>
|
||||||
<p><?php esc_html_e( 'Total Clicks', 'peptide-news' ); ?></p>
|
<p><?php esc_html_e( 'Total Clicks', 'peptide-news' ); ?></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pn-card">
|
<div class="pn-card">
|
||||||
<h3><?php echo esc_html( number_format( $total_unique ) ); ?></h3>
|
<h3><?php echo esc_html( number_format( $total_unique ) ); ?></h3>
|
||||||
<p><?php esc_html_e( 'Unique Visitors', 'peptide-news' ); ?></p>
|
<p><?php esc_html_e( 'Unique Visitors', 'peptide-news' ); ?></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pn-card">
|
<div class="pn-card">
|
||||||
<h3><?php echo esc_html( $total_articles ); ?></h3>
|
<h3><?php echo esc_html( $total_articles ); ?></h3>
|
||||||
<p><?php esc_html_e( 'Articles Clicked', 'peptide-news' ); ?></p>
|
<p><?php esc_html_e( 'Articles Clicked', 'peptide-news' ); ?></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pn-card">
|
<div class="pn-card">
|
||||||
<h3><?php echo $total_articles > 0 ? esc_html( round( $total_clicks / $total_articles, 1 ) ) : '0'; ?></h3>
|
<h3><?php echo $total_articles > 0 ? esc_html( round( $total_clicks / $total_articles, 1 ) ) : '0'; ?></h3>
|
||||||
<p><?php esc_html_e( 'Avg Clicks / Article', 'peptide-news' ); ?></p>
|
<p><?php esc_html_e( 'Avg Clicks / Article', 'peptide-news' ); ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Charts row -->
|
<!-- Charts row -->
|
||||||
<div class="pn-charts-row">
|
<div class="pn-charts-row">
|
||||||
<div class="pn-chart-box pn-chart-wide">
|
<div class="pn-chart-box pn-chart-wide">
|
||||||
<h2><?php esc_html_e( 'Click Trends', 'peptide-news' ); ?></h2>
|
<h2><?php esc_html_e( 'Click Trends', 'peptide-news' ); ?></h2>
|
||||||
<canvas id="pn-trends-chart" height="300"></canvas>
|
<canvas id="pn-trends-chart" height="300"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pn-charts-row">
|
<div class="pn-charts-row">
|
||||||
<div class="pn-chart-box">
|
<div class="pn-chart-box">
|
||||||
<h2><?php esc_html_e( 'Device Breakdown', 'peptide-news' ); ?></h2>
|
<h2><?php esc_html_e( 'Device Breakdown', 'peptide-news' ); ?></h2>
|
||||||
<canvas id="pn-devices-chart" height="250"></canvas>
|
<canvas id="pn-devices-chart" height="250"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="pn-chart-box">
|
<div class="pn-chart-box">
|
||||||
<h2><?php esc_html_e( 'Source Performance', 'peptide-news' ); ?></h2>
|
<h2><?php esc_html_e( 'Source Performance', 'peptide-news' ); ?></h2>
|
||||||
<canvas id="pn-sources-chart" height="250"></canvas>
|
<canvas id="pn-sources-chart" height="250"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Top articles table -->
|
<!-- Top articles table -->
|
||||||
<div class="pn-table-section">
|
<div class="pn-table-section">
|
||||||
<h2><?php esc_html_e( 'Top Articles', 'peptide-news' ); ?></h2>
|
<h2><?php esc_html_e( 'Top Articles', 'peptide-news' ); ?></h2>
|
||||||
<table class="wp-list-table widefat fixed striped">
|
<table class="wp-list-table widefat fixed striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:5%">#</th>
|
<th style="width:5%">#</th>
|
||||||
<th style="width:45%"><?php esc_html_e( 'Article', 'peptide-news' ); ?></th>
|
<th style="width:45%"><?php esc_html_e( 'Article', 'peptide-news' ); ?></th>
|
||||||
<th style="width:15%"><?php esc_html_e( 'Source', 'peptide-news' ); ?></th>
|
<th style="width:15%"><?php esc_html_e( 'Source', 'peptide-news' ); ?></th>
|
||||||
<th style="width:15%"><?php esc_html_e( 'Categories', 'peptide-news' ); ?></th>
|
<th style="width:15%"><?php esc_html_e( 'Categories', 'peptide-news' ); ?></th>
|
||||||
<th style="width:10%"><?php esc_html_e( 'Clicks', 'peptide-news' ); ?></th>
|
<th style="width:10%"><?php esc_html_e( 'Clicks', 'peptide-news' ); ?></th>
|
||||||
<th style="width:10%"><?php esc_html_e( 'Unique', 'peptide-news' ); ?></th>
|
<th style="width:10%"><?php esc_html_e( 'Unique', 'peptide-news' ); ?></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php if ( empty( $top_articles ) ) : ?>
|
<?php if ( empty( $top_articles ) ) : ?>
|
||||||
<tr><td colspan="6"><?php esc_html_e( 'No click data for this period.', 'peptide-news' ); ?></td></tr>
|
<tr><td colspan="6"><?php esc_html_e( 'No click data for this period.', 'peptide-news' ); ?></td></tr>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<?php foreach( $top_articles as $i => $article ) : ?>
|
<?php foreach( $top_articles as $i => $article ) : ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?php echo esc_html( $i + 1 ); ?></td>
|
<td><?php echo esc_html( $i + 1 ); ?></td>
|
||||||
<td><a href="<?php echo esc_url( $article->source_url ); ?>" target="_blank" rel="noopener"><?php echo esc_html( $article->title ); ?></a></td>
|
<td><a href="<?php echo esc_url( $article->source_url ); ?>" target="_blank" rel="noopener"><?php echo esc_html( $article->title ); ?></a></td>
|
||||||
<td><?php echo esc_html( $article->source ); ?></td>
|
<td><?php echo esc_html( $article->source ); ?></td>
|
||||||
<td><?php echo esc_html( $article->categories ); ?></td>
|
<td><?php echo esc_html( $article->categories ); ?></td>
|
||||||
<td><strong><?php echo esc_html( number_format( $article->total_clicks ) ); ?></strong></td>
|
<td><strong><?php echo esc_html( number_format( $article->total_clicks ) ); ?></strong></td>
|
||||||
<td><?php echo esc_html( number_format( $article->total_unique ) ); ?></td>
|
<td><?php echo esc_html( number_format( $article->total_unique ) ); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Popular topics table -->
|
<!-- Popular topics table -->
|
||||||
<div class="pn-table-section">
|
<div class="pn-table-section">
|
||||||
<h2><?php esc_html_e( 'Popular Topics', 'peptide-news' ); ?></h2>
|
<h2><?php esc_html_e( 'Popular Topics', 'peptide-news' ); ?></h2>
|
||||||
<table class="wp-list-table widefat fixed striped">
|
<table class="wp-list-table widefat fixed striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:5%">#</th>
|
<th style="width:5%">#</th>
|
||||||
<th style="width:45%"><?php esc_html_e( 'Topic', 'peptide-news' ); ?></th>
|
<th style="width:45%"><?php esc_html_e( 'Topic', 'peptide-news' ); ?></th>
|
||||||
<th style="width:25%"><?php esc_html_e( 'Total Clicks', 'peptide-news' ); ?></th>
|
<th style="width:25%"><?php esc_html_e( 'Total Clicks', 'peptide-news' ); ?></th>
|
||||||
<th style="width:25%"><?php esc_html_e( 'Articles', 'peptide-news' ); ?></th>
|
<th style="width:25%"><?php esc_html_e( 'Articles', 'peptide-news' ); ?></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php if ( empty( $topics ) ) : ?>
|
<?php if ( empty( $topics ) ) : ?>
|
||||||
<tr><td colspan="4"><?php esc_html_e( 'No topic data for this period.', 'peptide-news' ); ?></td></tr>
|
<tr><td colspan="4"><?php esc_html_e( 'No topic data for this period.', 'peptide-news' ); ?></td></tr>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<?php foreach( $topics as $i => $topic ) : ?>
|
<?php foreach( $topics as $i => $topic ) : ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?php echo esc_html( $i + 1 ); ?></td>
|
<td><?php echo esc_html( $i + 1 ); ?></td>
|
||||||
<td><?php echo esc_html( $topic['topic'] ); ?></td>
|
<td><?php echo esc_html( $topic['topic'] ); ?></td>
|
||||||
<td><?php echo esc_html( number_format( $topic['total_clicks'] ) ); ?></td>
|
<td><?php echo esc_html( number_format( $topic['total_clicks'] ) ); ?></td>
|
||||||
<td><?php echo esc_html( $topic['article_count'] ); ?></td>
|
<td><?php echo esc_html( $topic['article_count'] ); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Pass PHP data to JS for Chart.js rendering.
|
// Pass PHP data to JS for Chart.js rendering.
|
||||||
var peptideNewsDashboardData = {
|
var peptideNewsDashboardData = {
|
||||||
trends: <?php echo wp_json_encode( $trends ); ?>,
|
trends: <?php echo wp_json_encode( $trends ); ?>,
|
||||||
devices: <?php echo wp_json_encode( $devices ); ?>,
|
devices: <?php echo wp_json_encode( $devices ); ?>,
|
||||||
sources: <?php echo wp_json_encode( $sources ); ?>
|
sources: <?php echo wp_json_encode( $sources ); ?>
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,180 +8,180 @@
|
|||||||
*/
|
*/
|
||||||
class Peptide_News_Activator {
|
class Peptide_News_Activator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create custom tables and schedule cron.
|
* Create custom tables and schedule cron.
|
||||||
*/
|
*/
|
||||||
public static function activate() {
|
public static function activate() {
|
||||||
self::create_tables();
|
self::create_tables();
|
||||||
self::set_default_options();
|
self::set_default_options();
|
||||||
self::schedule_cron();
|
self::schedule_cron();
|
||||||
|
|
||||||
// Create the log table (idempotent via dbDelta).
|
// Create the log table (idempotent via dbDelta).
|
||||||
if ( class_exists( 'Peptide_News_Logger' ) ) {
|
if ( class_exists( 'Peptide_News_Logger' ) ) {
|
||||||
Peptide_News_Logger::create_table();
|
Peptide_News_Logger::create_table();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the articles and analytics database tables.
|
* Create the articles and analytics database tables.
|
||||||
*/
|
*/
|
||||||
private static function create_tables() {
|
private static function create_tables() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$charset_collate = $wpdb->get_charset_collate();
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
|
|
||||||
// Articles table — stores fetched news data for analysis.
|
// Articles table — stores fetched news data for analysis.
|
||||||
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||||
$sql_articles = "CREATE TABLE {$articles_table} (
|
$sql_articles = "CREATE TABLE {$articles_table} (
|
||||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
source VARCHAR(100) NOT NULL DEFAULT '',
|
source VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
source_url VARCHAR(2048) NOT NULL DEFAULT '',
|
source_url VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
excerpt TEXT NOT NULL,
|
excerpt TEXT NOT NULL,
|
||||||
content LONGTEXT NOT NULL,
|
content LONGTEXT NOT NULL,
|
||||||
author VARCHAR(255) NOT NULL DEFAULT '',
|
author VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
thumbnail_url VARCHAR(2048) NOT NULL DEFAULT '',
|
thumbnail_url VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
thumbnail_local VARCHAR(512) NOT NULL DEFAULT '',
|
thumbnail_local VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
published_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
published_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
fetched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
fetched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
categories VARCHAR(500) NOT NULL DEFAULT '',
|
categories VARCHAR(500) NOT NULL DEFAULT '',
|
||||||
tags VARCHAR(500) NOT NULL DEFAULT '',
|
tags VARCHAR(500) NOT NULL DEFAULT '',
|
||||||
language VARCHAR(10) NOT NULL DEFAULT 'en',
|
language VARCHAR(10) NOT NULL DEFAULT 'en',
|
||||||
sentiment_score FLOAT DEFAULT NULL,
|
sentiment_score FLOAT DEFAULT NULL,
|
||||||
ai_summary TEXT NOT NULL,
|
ai_summary TEXT NOT NULL,
|
||||||
hash VARCHAR(64) NOT NULL DEFAULT '',
|
hash VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY hash (hash),
|
UNIQUE KEY hash (hash),
|
||||||
KEY idx_published (published_at),
|
KEY idx_published (published_at),
|
||||||
KEY idx_source (source),
|
KEY idx_source (source),
|
||||||
KEY idx_active_published (is_active, published_at)
|
KEY idx_active_published (is_active, published_at)
|
||||||
) {$charset_collate};";
|
) {$charset_collate};";
|
||||||
|
|
||||||
// Click analytics table — one row per click event.
|
// Click analytics table — one row per click event.
|
||||||
$clicks_table = $wpdb->prefix . 'peptide_news_clicks';
|
$clicks_table = $wpdb->prefix . 'peptide_news_clicks';
|
||||||
$sql_clicks = "CREATE TABLE {$clicks_table} (
|
$sql_clicks = "CREATE TABLE {$clicks_table} (
|
||||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
article_id BIGINT(20) UNSIGNED NOT NULL,
|
article_id BIGINT(20) UNSIGNED NOT NULL,
|
||||||
clicked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
clicked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
user_ip VARCHAR(45) NOT NULL DEFAULT '',
|
user_ip VARCHAR(45) NOT NULL DEFAULT '',
|
||||||
user_agent TEXT NOT NULL,
|
user_agent TEXT NOT NULL,
|
||||||
referrer_url VARCHAR(2048) NOT NULL DEFAULT '',
|
referrer_url VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
page_url VARCHAR(2048) NOT NULL DEFAULT '',
|
page_url VARCHAR(2048) NOT NULL DEFAULT '',
|
||||||
session_id VARCHAR(64) NOT NULL DEFAULT '',
|
session_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
user_id BIGINT(20) UNSIGNED DEFAULT NULL,
|
user_id BIGINT(20) UNSIGNED DEFAULT NULL,
|
||||||
country VARCHAR(2) NOT NULL DEFAULT '',
|
country VARCHAR(2) NOT NULL DEFAULT '',
|
||||||
device_type VARCHAR(20) NOT NULL DEFAULT '',
|
device_type VARCHAR(20) NOT NULL DEFAULT '',
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
KEY idx_article (article_id),
|
KEY idx_article (article_id),
|
||||||
KEY idx_clicked_at (clicked_at),
|
KEY idx_clicked_at (clicked_at),
|
||||||
KEY idx_session (session_id),
|
KEY idx_session (session_id),
|
||||||
KEY idx_article_date (article_id, clicked_at)
|
KEY idx_article_date (article_id, clicked_at)
|
||||||
) {$charset_collate};";
|
) {$charset_collate};";
|
||||||
|
|
||||||
// Daily aggregates table — for fast trend queries.
|
// Daily aggregates table — for fast trend queries.
|
||||||
$daily_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
$daily_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||||
$sql_daily = "CREATE TABLE {$daily_table} (
|
$sql_daily = "CREATE TABLE {$daily_table} (
|
||||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
article_id BIGINT(20) UNSIGNED NOT NULL,
|
article_id BIGINT(20) UNSIGNED NOT NULL,
|
||||||
stat_date DATE NOT NULL,
|
stat_date DATE NOT NULL,
|
||||||
click_count INT UNSIGNED NOT NULL DEFAULT 0,
|
click_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
unique_visitors INT UNSIGNED NOT NULL DEFAULT 0,
|
unique_visitors INT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY idx_article_date (article_id, stat_date),
|
UNIQUE KEY idx_article_date (article_id, stat_date),
|
||||||
KEY idx_date (stat_date)
|
KEY idx_date (stat_date)
|
||||||
) {$charset_collate};";
|
) {$charset_collate};";
|
||||||
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
|
||||||
dbDelta( $sql_articles );
|
dbDelta( $sql_articles );
|
||||||
dbDelta( $sql_clicks );
|
dbDelta( $sql_clicks );
|
||||||
dbDelta( $sql_daily );
|
dbDelta( $sql_daily );
|
||||||
|
|
||||||
// Add foreign key constraints if they don't exist.
|
// Add foreign key constraints if they don't exist.
|
||||||
self::add_foreign_key_constraints( $articles_table, $clicks_table, $daily_table );
|
self::add_foreign_key_constraints( $articles_table, $clicks_table, $daily_table );
|
||||||
|
|
||||||
update_option( 'peptide_news_db_version', PEPTIDE_NEWS_VERSION );
|
update_option( 'peptide_news_db_version', PEPTIDE_NEWS_VERSION );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set sensible default option values.
|
* Set sensible default option values.
|
||||||
*/
|
*/
|
||||||
private static function set_default_options() {
|
private static function set_default_options() {
|
||||||
$defaults = array(
|
$defaults = array(
|
||||||
'fetch_interval' => 'twicedaily',
|
'fetch_interval' => 'twicedaily',
|
||||||
'articles_count' => 10,
|
'articles_count' => 10,
|
||||||
'newsapi_key' => '',
|
'newsapi_key' => '',
|
||||||
'newsapi_enabled' => 0,
|
'newsapi_enabled' => 0,
|
||||||
'rss_feeds' => implode( "\n", array(
|
'rss_feeds' => implode( "\n", array(
|
||||||
'https://news.google.com/rss/search?q=peptide+research&hl=en-US&gl=US&ceid=US:en',
|
'https://news.google.com/rss/search?q=peptide+research&hl=en-US&gl=US&ceid=US:en',
|
||||||
'https://pubmed.ncbi.nlm.nih.gov/rss/search/1sUlVGjEXm1YN9QiGPblv_-bwB51JCtyLwVX85CVGDKCHFFR9Q/?limit=20&utm_campaign=pubmed-2&fc=20220101000000',
|
'https://pubmed.ncbi.nlm.nih.gov/rss/search/1sUlVGjEXm1YN9QiGPblv_-bwB51JCtyLwVX85CVGDKCHFFR9Q/?limit=20&utm_campaign=pubmed-2&fc=20220101000000',
|
||||||
) ),
|
) ),
|
||||||
'rss_enabled' => 1,
|
'rss_enabled' => 1,
|
||||||
'search_keywords' => 'peptide, peptides, peptide therapy, peptide research, BPC-157, Thymosin, GHK-Cu',
|
'search_keywords' => 'peptide, peptides, peptide therapy, peptide research, BPC-157, Thymosin, GHK-Cu',
|
||||||
'thumbnail_fallback' => PEPTIDE_NEWS_PLUGIN_URL . 'public/images/default-thumb.png',
|
'thumbnail_fallback' => PEPTIDE_NEWS_PLUGIN_URL . 'public/images/default-thumb.png',
|
||||||
'article_retention' => 90,
|
'article_retention' => 90,
|
||||||
'analytics_retention' => 365,
|
'analytics_retention' => 365,
|
||||||
'anonymize_ip' => 1,
|
'anonymize_ip' => 1,
|
||||||
'llm_enabled' => 0,
|
'llm_enabled' => 0,
|
||||||
'openrouter_api_key' => '',
|
'openrouter_api_key' => '',
|
||||||
'llm_keywords_model' => 'google/gemini-2.0-flash-001',
|
'llm_keywords_model' => 'google/gemini-2.0-flash-001',
|
||||||
'llm_summary_model' => 'google/gemini-2.0-flash-001',
|
'llm_summary_model' => 'google/gemini-2.0-flash-001',
|
||||||
'llm_max_articles' => 10,
|
'llm_max_articles' => 10,
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ( $defaults as $key => $value ) {
|
foreach ( $defaults as $key => $value ) {
|
||||||
if ( false === get_option( "peptide_news_{$key}" ) ) {
|
if ( false === get_option( "peptide_news_{$key}" ) ) {
|
||||||
add_option( "peptide_news_{$key}", $value );
|
add_option( "peptide_news_{$key}", $value );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule the cron event based on the configured interval.
|
* Schedule the cron event based on the configured interval.
|
||||||
*/
|
*/
|
||||||
private static function schedule_cron() {
|
private static function schedule_cron() {
|
||||||
$interval = get_option( 'peptide_news_fetch_interval', 'twicedaily' );
|
$interval = get_option( 'peptide_news_fetch_interval', 'twicedaily' );
|
||||||
|
|
||||||
if ( ! wp_next_scheduled( 'peptide_news_cron_fetch' ) ) {
|
if ( ! wp_next_scheduled( 'peptide_news_cron_fetch' ) ) {
|
||||||
wp_schedule_event( time(), $interval, 'peptide_news_cron_fetch' );
|
wp_schedule_event( time(), $interval, 'peptide_news_cron_fetch' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add foreign key constraints to tables.
|
* Add foreign key constraints to tables.
|
||||||
*/
|
*/
|
||||||
private static function add_foreign_key_constraints( $articles_table, $clicks_table, $daily_table ) {
|
private static function add_foreign_key_constraints( $articles_table, $clicks_table, $daily_table ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||||
$existing_clicks_fk = $wpdb->get_var(
|
$existing_clicks_fk = $wpdb->get_var(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
"SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = 'article_id' AND REFERENCED_TABLE_NAME IS NOT NULL LIMIT 1",
|
"SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = 'article_id' AND REFERENCED_TABLE_NAME IS NOT NULL LIMIT 1",
|
||||||
$clicks_table
|
$clicks_table
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( empty( $existing_clicks_fk ) ) {
|
if ( empty( $existing_clicks_fk ) ) {
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||||
$wpdb->query(
|
$wpdb->query(
|
||||||
"ALTER TABLE `{$clicks_table}` ADD CONSTRAINT `fk_clicks_article_id` FOREIGN KEY (`article_id`) REFERENCES `{$articles_table}`(`id`) ON DELETE CASCADE"
|
"ALTER TABLE `{$clicks_table}` ADD CONSTRAINT `fk_clicks_article_id` FOREIGN KEY (`article_id`) REFERENCES `{$articles_table}`(`id`) ON DELETE CASCADE"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||||
$existing_stats_fk = $wpdb->get_var(
|
$existing_stats_fk = $wpdb->get_var(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
"SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = 'article_id' AND REFERENCED_TABLE_NAME IS NOT NULL LIMIT 1",
|
"SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = 'article_id' AND REFERENCED_TABLE_NAME IS NOT NULL LIMIT 1",
|
||||||
$daily_table
|
$daily_table
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( empty( $existing_stats_fk ) ) {
|
if ( empty( $existing_stats_fk ) ) {
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||||
$wpdb->query(
|
$wpdb->query(
|
||||||
"ALTER TABLE `{$daily_table}` ADD CONSTRAINT `fk_stats_article_id` FOREIGN KEY (`article_id`) REFERENCES `{$articles_table}`(`id`) ON DELETE CASCADE"
|
"ALTER TABLE `{$daily_table}` ADD CONSTRAINT `fk_stats_article_id` FOREIGN KEY (`article_id`) REFERENCES `{$articles_table}`(`id`) ON DELETE CASCADE"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,342 +9,342 @@
|
|||||||
*/
|
*/
|
||||||
class Peptide_News_Analytics {
|
class Peptide_News_Analytics {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record a click event for an article.
|
* Record a click event for an article.
|
||||||
*
|
*
|
||||||
* @param int $article_id
|
* @param int $article_id
|
||||||
* @param array $meta Additional context (IP, UA, referrer, etc.).
|
* @param array $meta Additional context (IP, UA, referrer, etc.).
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function record_click( $article_id, $meta = array() ) {
|
public static function record_click( $article_id, $meta = array() ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = $wpdb->prefix . 'peptide_news_clicks';
|
$table = $wpdb->prefix . 'peptide_news_clicks';
|
||||||
|
|
||||||
$ip = isset( $meta['ip'] ) ? $meta['ip'] : self::get_client_ip();
|
$ip = isset( $meta['ip'] ) ? $meta['ip'] : self::get_client_ip();
|
||||||
|
|
||||||
// Skip recording if no valid IP is available.
|
// Skip recording if no valid IP is available.
|
||||||
if ( null === $ip ) {
|
if ( null === $ip ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally anonymize IP (GDPR-friendly).
|
// Optionally anonymize IP (GDPR-friendly).
|
||||||
if ( get_option( 'peptide_news_anonymize_ip', 1 ) ) {
|
if ( get_option( 'peptide_news_anonymize_ip', 1 ) ) {
|
||||||
$ip = self::anonymize_ip( $ip );
|
$ip = self::anonymize_ip( $ip );
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = array(
|
$data = array(
|
||||||
'article_id' => absint( $article_id ),
|
'article_id' => absint( $article_id ),
|
||||||
'clicked_at' => current_time( 'mysql' ),
|
'clicked_at' => current_time( 'mysql' ),
|
||||||
'user_ip' => sanitize_text_field( $ip ),
|
'user_ip' => sanitize_text_field( $ip ),
|
||||||
'user_agent' => sanitize_text_field( isset( $meta['user_agent'] ) ? $meta['user_agent'] : '' ),
|
'user_agent' => sanitize_text_field( isset( $meta['user_agent'] ) ? $meta['user_agent'] : '' ),
|
||||||
'referrer_url' => esc_url_raw( isset( $meta['referrer'] ) ? $meta['referrer'] : '' ),
|
'referrer_url' => esc_url_raw( isset( $meta['referrer'] ) ? $meta['referrer'] : '' ),
|
||||||
'page_url' => esc_url_raw( isset( $meta['page_url'] ) ? $meta['page_url'] : '' ),
|
'page_url' => esc_url_raw( isset( $meta['page_url'] ) ? $meta['page_url'] : '' ),
|
||||||
'session_id' => sanitize_text_field( isset( $meta['session_id'] ) ? $meta['session_id'] : '' ),
|
'session_id' => sanitize_text_field( isset( $meta['session_id'] ) ? $meta['session_id'] : '' ),
|
||||||
'user_id' => is_user_logged_in() ? get_current_user_id() : null,
|
'user_id' => is_user_logged_in() ? get_current_user_id() : null,
|
||||||
'device_type' => sanitize_text_field( self::detect_device( isset( $meta['user_agent'] ) ? $meta['user_agent'] : '' ) ),
|
'device_type' => sanitize_text_field( self::detect_device( isset( $meta['user_agent'] ) ? $meta['user_agent'] : '' ) ),
|
||||||
);
|
);
|
||||||
|
|
||||||
$result = $wpdb->insert( $table, $data );
|
$result = $wpdb->insert( $table, $data );
|
||||||
|
|
||||||
// Update daily aggregate.
|
// Update daily aggregate.
|
||||||
if ( false !== $result ) {
|
if ( false !== $result ) {
|
||||||
self::update_daily_stats( $article_id, $ip );
|
self::update_daily_stats( $article_id, $ip );
|
||||||
}
|
}
|
||||||
|
|
||||||
return false !== $result;
|
return false !== $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update daily aggregated stats.
|
* Update daily aggregated stats.
|
||||||
*
|
*
|
||||||
* @param int $article_id
|
* @param int $article_id
|
||||||
* @param string $ip Anonymized IP for unique visitor counting.
|
* @param string $ip Anonymized IP for unique visitor counting.
|
||||||
*/
|
*/
|
||||||
private static function update_daily_stats( $article_id, $ip ) {
|
private static function update_daily_stats( $article_id, $ip ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = $wpdb->prefix . 'peptide_news_daily_stats';
|
$table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||||
$today = current_time( 'Y-m-d' );
|
$today = current_time( 'Y-m-d' );
|
||||||
$clicks_tbl = $wpdb->prefix . 'peptide_news_clicks';
|
$clicks_tbl = $wpdb->prefix . 'peptide_news_clicks';
|
||||||
|
|
||||||
// Upsert daily click count.
|
// Upsert daily click count.
|
||||||
$existing = $wpdb->get_row( $wpdb->prepare(
|
$existing = $wpdb->get_row( $wpdb->prepare(
|
||||||
"SELECT id, click_count FROM {$table} WHERE article_id = %d AND stat_date = %s",
|
"SELECT id, click_count FROM {$table} WHERE article_id = %d AND stat_date = %s",
|
||||||
$article_id,
|
$article_id,
|
||||||
$today
|
$today
|
||||||
) );
|
) );
|
||||||
|
|
||||||
// Count unique IPs for today.
|
// Count unique IPs for today.
|
||||||
$unique = $wpdb->get_var( $wpdb->prepare(
|
$unique = $wpdb->get_var( $wpdb->prepare(
|
||||||
"SELECT COUNT(DISTINCT user_ip) FROM {$clicks_tbl} WHERE article_id = %d AND DATE(clicked_at) = %s",
|
"SELECT COUNT(DISTINCT user_ip) FROM {$clicks_tbl} WHERE article_id = %d AND DATE(clicked_at) = %s",
|
||||||
$article_id,
|
$article_id,
|
||||||
$today
|
$today
|
||||||
) );
|
) );
|
||||||
|
|
||||||
if ( $existing ) {
|
if ( $existing ) {
|
||||||
$wpdb->update(
|
$wpdb->update(
|
||||||
$table,
|
$table,
|
||||||
array(
|
array(
|
||||||
'click_count' => $existing->click_count + 1,
|
'click_count' => $existing->click_count + 1,
|
||||||
'unique_visitors' => (int) $unique,
|
'unique_visitors' => (int) $unique,
|
||||||
),
|
),
|
||||||
array( 'id' => $existing->id ),
|
array( 'id' => $existing->id ),
|
||||||
array( '%d', '%d' ),
|
array( '%d', '%d' ),
|
||||||
array( '%d' )
|
array( '%d' )
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$wpdb->insert(
|
$wpdb->insert(
|
||||||
$table,
|
$table,
|
||||||
array(
|
array(
|
||||||
'article_id' => $article_id,
|
'article_id' => $article_id,
|
||||||
'stat_date' => $today,
|
'stat_date' => $today,
|
||||||
'click_count' => 1,
|
'click_count' => 1,
|
||||||
'unique_visitors' => 1,
|
'unique_visitors' => 1,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get top articles by click count within a date range.
|
* Get top articles by click count within a date range.
|
||||||
*
|
*
|
||||||
* @param string $start_date Y-m-d
|
* @param string $start_date Y-m-d
|
||||||
* @param string $end_date Y-m-d
|
* @param string $end_date Y-m-d
|
||||||
* @param int $limit
|
* @param int $limit
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function get_top_articles( $start_date, $end_date, $limit = 20 ) {
|
public static function get_top_articles( $start_date, $end_date, $limit = 20 ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$stats_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
$stats_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||||
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||||
|
|
||||||
return $wpdb->get_results( $wpdb->prepare(
|
return $wpdb->get_results( $wpdb->prepare(
|
||||||
"SELECT a.id, a.title, a.source, a.source_url, a.thumbnail_url, a.categories, a.published_at,
|
"SELECT a.id, a.title, a.source, a.source_url, a.thumbnail_url, a.categories, a.published_at,
|
||||||
SUM(s.click_count) AS total_clicks,
|
SUM(s.click_count) AS total_clicks,
|
||||||
SUM(s.unique_visitors) AS total_unique
|
SUM(s.unique_visitors) AS total_unique
|
||||||
FROM {$stats_table} s
|
FROM {$stats_table} s
|
||||||
INNER JOIN {$articles_table} a ON a.id = s.article_id
|
INNER JOIN {$articles_table} a ON a.id = s.article_id
|
||||||
WHERE s.stat_date BETWEEN %s AND %s
|
WHERE s.stat_date BETWEEN %s AND %s
|
||||||
GROUP BY a.id
|
GROUP BY a.id
|
||||||
ORDER BY total_clicks DESC
|
ORDER BY total_clicks DESC
|
||||||
LIMIT %d",
|
LIMIT %d",
|
||||||
$start_date,
|
$start_date,
|
||||||
$end_date,
|
$end_date,
|
||||||
$limit
|
$limit
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get click trends over time (daily totals).
|
* Get click trends over time (daily totals).
|
||||||
*
|
*
|
||||||
* @param string $start_date
|
* @param string $start_date
|
||||||
* @param string $end_date
|
* @param string $end_date
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function get_click_trends( $start_date, $end_date ) {
|
public static function get_click_trends( $start_date, $end_date ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = $wpdb->prefix . 'peptide_news_daily_stats';
|
$table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||||
|
|
||||||
return $wpdb->get_results( $wpdb->prepare(
|
return $wpdb->get_results( $wpdb->prepare(
|
||||||
"SELECT stat_date, SUM(click_count) AS total_clicks, SUM(unique_visitors) AS total_unique
|
"SELECT stat_date, SUM(click_count) AS total_clicks, SUM(unique_visitors) AS total_unique
|
||||||
FROM {$table}
|
FROM {$table}
|
||||||
WHERE stat_date BETWEEN %s AND %s
|
WHERE stat_date BETWEEN %s AND %s
|
||||||
GROUP BY stat_date
|
GROUP BY stat_date
|
||||||
ORDER BY stat_date ASC",
|
ORDER BY stat_date ASC",
|
||||||
$start_date,
|
$start_date,
|
||||||
$end_date
|
$end_date
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get popular topics/categories by click volume.
|
* Get popular topics/categories by click volume.
|
||||||
*
|
*
|
||||||
* @param string $start_date
|
* @param string $start_date
|
||||||
* @param string $end_date
|
* @param string $end_date
|
||||||
* @param int $limit
|
* @param int $limit
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function get_popular_topics( $start_date, $end_date, $limit = 20 ) {
|
public static function get_popular_topics( $start_date, $end_date, $limit = 20 ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$stats_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
$stats_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||||
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||||
|
|
||||||
// Get articles with their click counts.
|
// Get articles with their click counts.
|
||||||
$rows = $wpdb->get_results( $wpdb->prepare(
|
$rows = $wpdb->get_results( $wpdb->prepare(
|
||||||
"SELECT a.categories, a.title, SUM(s.click_count) AS clicks
|
"SELECT a.categories, a.title, SUM(s.click_count) AS clicks
|
||||||
FROM {$stats_table} s
|
FROM {$stats_table} s
|
||||||
INNER JOIN {$articles_table} a ON a.id = s.article_id
|
INNER JOIN {$articles_table} a ON a.id = s.article_id
|
||||||
WHERE s.stat_date BETWEEN %s AND %s AND a.categories != ''
|
WHERE s.stat_date BETWEEN %s AND %s AND a.categories != ''
|
||||||
GROUP BY a.id
|
GROUP BY a.id
|
||||||
ORDER BY clicks DESC",
|
ORDER BY clicks DESC",
|
||||||
$start_date,
|
$start_date,
|
||||||
$end_date
|
$end_date
|
||||||
) );
|
) );
|
||||||
|
|
||||||
// Aggregate by category.
|
// Aggregate by category.
|
||||||
$topics = array();
|
$topics = array();
|
||||||
foreach ( $rows as $row ) {
|
foreach ( $rows as $row ) {
|
||||||
$cats = array_map( 'trim', explode( ',', $row->categories ) );
|
$cats = array_map( 'trim', explode( ',', $row->categories ) );
|
||||||
foreach ( $cats as $cat ) {
|
foreach ( $cats as $cat ) {
|
||||||
if ( empty( $cat ) ) {
|
if ( empty( $cat ) ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$cat_lower = strtolower( $cat );
|
$cat_lower = strtolower( $cat );
|
||||||
if ( ! isset( $topics[ $cat_lower ] ) ) {
|
if ( ! isset( $topics[ $cat_lower ] ) ) {
|
||||||
$topics[ $cat_lower ] = array(
|
$topics[ $cat_lower ] = array(
|
||||||
'topic' => $cat,
|
'topic' => $cat,
|
||||||
'total_clicks' => 0,
|
'total_clicks' => 0,
|
||||||
'article_count' => 0,
|
'article_count' => 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
$topics[ $cat_lower ]['total_clicks'] += (int) $row->clicks;
|
$topics[ $cat_lower ]['total_clicks'] += (int) $row->clicks;
|
||||||
$topics[ $cat_lower ]['article_count'] += 1;
|
$topics[ $cat_lower ]['article_count'] += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by clicks.
|
// Sort by clicks.
|
||||||
usort( $topics, function ( $a, $b ) {
|
usort( $topics, function ( $a, $b ) {
|
||||||
return $b['total_clicks'] - $a['total_clicks'];
|
return $b['total_clicks'] - $a['total_clicks'];
|
||||||
} );
|
} );
|
||||||
|
|
||||||
return array_slice( $topics, 0, $limit );
|
return array_slice( $topics, 0, $limit );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get device breakdown stats.
|
* Get device breakdown stats.
|
||||||
*
|
*
|
||||||
* @param string $start_date
|
* @param string $start_date
|
||||||
* @param string $end_date
|
* @param string $end_date
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function get_device_breakdown( $start_date, $end_date ) {
|
public static function get_device_breakdown( $start_date, $end_date ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = $wpdb->prefix . 'peptide_news_clicks';
|
$table = $wpdb->prefix . 'peptide_news_clicks';
|
||||||
|
|
||||||
return $wpdb->get_results( $wpdb->prepare(
|
return $wpdb->get_results( $wpdb->prepare(
|
||||||
"SELECT device_type, COUNT(*) AS click_count
|
"SELECT device_type, COUNT(*) AS click_count
|
||||||
FROM {$table}
|
FROM {$table}
|
||||||
WHERE DATE(clicked_at) BETWEEN %s AND %s
|
WHERE DATE(clicked_at) BETWEEN %s AND %s
|
||||||
GROUP BY device_type
|
GROUP BY device_type
|
||||||
ORDER BY click_count DESC",
|
ORDER BY click_count DESC",
|
||||||
$start_date,
|
$start_date,
|
||||||
$end_date
|
$end_date
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get source performance stats.
|
* Get source performance stats.
|
||||||
*
|
*
|
||||||
* @param string $start_date
|
* @param string $start_date
|
||||||
* @param string $end_date
|
* @param string $end_date
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function get_source_performance( $start_date, $end_date ) {
|
public static function get_source_performance( $start_date, $end_date ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$stats_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
$stats_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||||
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||||
|
|
||||||
return $wpdb->get_results( $wpdb->prepare(
|
return $wpdb->get_results( $wpdb->prepare(
|
||||||
"SELECT a.source,
|
"SELECT a.source,
|
||||||
COUNT(DISTINCT a.id) AS article_count,
|
COUNT(DISTINCT a.id) AS article_count,
|
||||||
SUM(s.click_count) AS total_clicks,
|
SUM(s.click_count) AS total_clicks,
|
||||||
ROUND(IFNULL(SUM(s.click_count) / NULLIF(COUNT(DISTINCT a.id), 0), 0), 1) AS avg_clicks_per_article
|
ROUND(IFNULL(SUM(s.click_count) / NULLIF(COUNT(DISTINCT a.id), 0), 0), 1) AS avg_clicks_per_article
|
||||||
FROM {$stats_table} s
|
FROM {$stats_table} s
|
||||||
INNER JOIN {$articles_table} a ON a.id = s.article_id
|
INNER JOIN {$articles_table} a ON a.id = s.article_id
|
||||||
WHERE s.stat_date BETWEEN %s AND %s
|
WHERE s.stat_date BETWEEN %s AND %s
|
||||||
GROUP BY a.source
|
GROUP BY a.source
|
||||||
ORDER BY total_clicks DESC",
|
ORDER BY total_clicks DESC",
|
||||||
$start_date,
|
$start_date,
|
||||||
$end_date
|
$end_date
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export click data as CSV-ready array.
|
* Export click data as CSV-ready array.
|
||||||
*
|
*
|
||||||
* @param string $start_date
|
* @param string $start_date
|
||||||
* @param string $end_date
|
* @param string $end_date
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function export_clicks_csv( $start_date, $end_date ) {
|
public static function export_clicks_csv( $start_date, $end_date ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$clicks_table = $wpdb->prefix . 'peptide_news_clicks';
|
$clicks_table = $wpdb->prefix . 'peptide_news_clicks';
|
||||||
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||||
return $wpdb->get_results( $wpdb->prepare(
|
return $wpdb->get_results( $wpdb->prepare(
|
||||||
"SELECT c.clicked_at, a.title, a.source, a.source_url, a.categories,
|
"SELECT c.clicked_at, a.title, a.source, a.source_url, a.categories,
|
||||||
c.referrer_url, c.page_url, c.device_type, c.session_id
|
c.referrer_url, c.page_url, c.device_type, c.session_id
|
||||||
FROM {$clicks_table} c
|
FROM {$clicks_table} c
|
||||||
INNER JOIN {$articles_table} a ON a.id = c.article_id
|
INNER JOIN {$articles_table} a ON a.id = c.article_id
|
||||||
WHERE DATE(c.clicked_at) BETWEEN %s AND %s
|
WHERE DATE(c.clicked_at) BETWEEN %s AND %s
|
||||||
ORDER BY c.clicked_at DESC",
|
ORDER BY c.clicked_at DESC",
|
||||||
$start_date,
|
$start_date,
|
||||||
$end_date
|
$end_date
|
||||||
), ARRAY_A );
|
), ARRAY_A );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the client IP address.
|
* Get the client IP address.
|
||||||
*
|
*
|
||||||
* @return string|null
|
* @return string|null
|
||||||
*/
|
*/
|
||||||
private static function get_client_ip() {
|
private static function get_client_ip() {
|
||||||
$ip_keys = array( 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR' );
|
$ip_keys = array( 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR' );
|
||||||
|
|
||||||
foreach ( $ip_keys as $key ) {
|
foreach ( $ip_keys as $key ) {
|
||||||
if ( ! empty( $_SERVER[ $key ] ) ) {
|
if ( ! empty( $_SERVER[ $key ] ) ) {
|
||||||
$ip = explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ) );
|
$ip = explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ) );
|
||||||
$ip = trim( $ip[0] );
|
$ip = trim( $ip[0] );
|
||||||
if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
|
if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
|
||||||
return $ip;
|
return $ip;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anonymize an IP address (zero out last octet for IPv4, last 80 bits for IPv6).
|
* Anonymize an IP address (zero out last octet for IPv4, last 80 bits for IPv6).
|
||||||
*
|
*
|
||||||
* @param string $ip
|
* @param string $ip
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
private static function anonymize_ip( $ip ) {
|
private static function anonymize_ip( $ip ) {
|
||||||
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
|
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
|
||||||
return preg_replace( '/\.\d+$/', '.0', $ip );
|
return preg_replace( '/\.\d+$/', '.0', $ip );
|
||||||
}
|
}
|
||||||
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
|
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
|
||||||
return substr( $ip, 0, strrpos( $ip, ':' ) ) . ':0000';
|
return substr( $ip, 0, strrpos( $ip, ':' ) ) . ':0000';
|
||||||
}
|
}
|
||||||
return '0.0.0.0';
|
return '0.0.0.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple device detection from user agent.
|
* Simple device detection from user agent.
|
||||||
*
|
*
|
||||||
* @param string $ua
|
* @param string $ua
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
private static function detect_device( $ua ) {
|
private static function detect_device( $ua ) {
|
||||||
$ua = strtolower( $ua );
|
$ua = strtolower( $ua );
|
||||||
|
|
||||||
if ( preg_match( '/(tablet|ipad|playbook|silk)/', $ua ) ) {
|
if ( preg_match( '/(tablet|ipad|playbook|silk)/', $ua ) ) {
|
||||||
return 'tablet';
|
return 'tablet';
|
||||||
}
|
}
|
||||||
if ( preg_match( '/(mobile|android|iphone|ipod|phone|blackberry|opera mini|iemobile)/', $ua ) ) {
|
if ( preg_match( '/(mobile|android|iphone|ipod|phone|blackberry|opera mini|iemobile)/', $ua ) ) {
|
||||||
return 'mobile';
|
return 'mobile';
|
||||||
}
|
}
|
||||||
if ( preg_match( '/(bot|crawl|spider|slurp|wget|curl)/', $ua ) ) {
|
if ( preg_match( '/(bot|crawl|spider|slurp|wget|curl)/', $ua ) ) {
|
||||||
return 'bot';
|
return 'bot';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'desktop';
|
return 'desktop';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
class Peptide_News_Deactivator {
|
class Peptide_News_Deactivator {
|
||||||
|
|
||||||
public static function deactivate() {
|
public static function deactivate() {
|
||||||
wp_clear_scheduled_hook( 'peptide_news_cron_fetch' );
|
wp_clear_scheduled_hook( 'peptide_news_cron_fetch' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -12,109 +12,109 @@
|
|||||||
|
|
||||||
class Peptide_News_Loader {
|
class Peptide_News_Loader {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The array of actions registered with WordPress.
|
* The array of actions registered with WordPress.
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
* @access protected
|
* @access protected
|
||||||
* @var array $actions The actions registered with WordPress to fire when the plugin loads.
|
* @var array $actions The actions registered with WordPress to fire when the plugin loads.
|
||||||
*/
|
*/
|
||||||
protected $actions;
|
protected $actions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The array of filters registered with WordPress.
|
* The array of filters registered with WordPress.
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
* @access protected
|
* @access protected
|
||||||
* @var array $filters The filters registered with WordPress.
|
* @var array $filters The filters registered with WordPress.
|
||||||
*/
|
*/
|
||||||
protected $filters;
|
protected $filters;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the collections used to maintain the actions and filters.
|
* Initialize the collections used to maintain the actions and filters.
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->actions = array();
|
$this->actions = array();
|
||||||
$this->filters = array();
|
$this->filters = array();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new action to the collection to be registered with WordPress.
|
* Add a new action to the collection to be registered with WordPress.
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
* @param string $hook The name of the WordPress action that is being registered.
|
* @param string $hook The name of the WordPress action that is being registered.
|
||||||
* @param object $component A reference to the instance of the object on which the action is defined.
|
* @param object $component A reference to the instance of the object on which the action is defined.
|
||||||
* @param string $callback The name of the function definition on the $component.
|
* @param string $callback The name of the function definition on the $component.
|
||||||
* @param int $priority Optional. The priority at which the function should be fired. Default is 10.
|
* @param int $priority Optional. The priority at which the function should be fired. Default is 10.
|
||||||
* @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1.
|
* @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1.
|
||||||
*/
|
*/
|
||||||
public function add_action( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
|
public function add_action( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
|
||||||
$this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args );
|
$this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new filter to the collection to be registered with WordPress.
|
* Add a new filter to the collection to be registered with WordPress.
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
* @param string $hook The name of the WordPress filter that is being registered.
|
* @param string $hook The name of the WordPress filter that is being registered.
|
||||||
* @param object $component A reference to the instance of the object on which the filter is defined.
|
* @param object $component A reference to the instance of the object on which the filter is defined.
|
||||||
* @param string $callback The name of the function definition on the $component.
|
* @param string $callback The name of the function definition on the $component.
|
||||||
* @param int $priority Optional. The priority at which the function should be fired. Default is 10.
|
* @param int $priority Optional. The priority at which the function should be fired. Default is 10.
|
||||||
* @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1.
|
* @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1.
|
||||||
*/
|
*/
|
||||||
public function add_filter( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
|
public function add_filter( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
|
||||||
$this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args );
|
$this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility function that is used to register the actions and hooks into a single collection.
|
* A utility function that is used to register the actions and hooks into a single collection.
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
* @access private
|
* @access private
|
||||||
* @param array $hooks The collection of hooks that is being registered (actions or filters).
|
* @param array $hooks The collection of hooks that is being registered (actions or filters).
|
||||||
* @param string $hook The name of the WordPress filter that is being registered.
|
* @param string $hook The name of the WordPress filter that is being registered.
|
||||||
* @param object $component A reference to the instance of the object on which the filter is defined.
|
* @param object $component A reference to the instance of the object on which the filter is defined.
|
||||||
* @param string $callback The name of the function definition on the $component.
|
* @param string $callback The name of the function definition on the $component.
|
||||||
* @param int $priority The priority at which the function should be fired.
|
* @param int $priority The priority at which the function should be fired.
|
||||||
* @param int $accepted_args The number of arguments that should be passed to the $callback.
|
* @param int $accepted_args The number of arguments that should be passed to the $callback.
|
||||||
* @return array The collection of actions and filters registered with WordPress.
|
* @return array The collection of actions and filters registered with WordPress.
|
||||||
*/
|
*/
|
||||||
private function add( $hooks, $hook, $component, $callback, $priority, $accepted_args ) {
|
private function add( $hooks, $hook, $component, $callback, $priority, $accepted_args ) {
|
||||||
$hooks[] = array(
|
$hooks[] = array(
|
||||||
'hook' => $hook,
|
'hook' => $hook,
|
||||||
'component' => $component,
|
'component' => $component,
|
||||||
'callback' => $callback,
|
'callback' => $callback,
|
||||||
'priority' => $priority,
|
'priority' => $priority,
|
||||||
'accepted_args' => $accepted_args,
|
'accepted_args' => $accepted_args,
|
||||||
);
|
);
|
||||||
|
|
||||||
return $hooks;
|
return $hooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the filters and actions with WordPress.
|
* Register the filters and actions with WordPress.
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
public function run() {
|
public function run() {
|
||||||
foreach ( $this->filters as $hook ) {
|
foreach ( $this->filters as $hook ) {
|
||||||
add_filter(
|
add_filter(
|
||||||
$hook['hook'],
|
$hook['hook'],
|
||||||
array( $hook['component'], $hook['callback'] ),
|
array( $hook['component'], $hook['callback'] ),
|
||||||
$hook['priority'],
|
$hook['priority'],
|
||||||
$hook['accepted_args']
|
$hook['accepted_args']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ( $this->actions as $hook ) {
|
foreach ( $this->actions as $hook ) {
|
||||||
add_action(
|
add_action(
|
||||||
$hook['hook'],
|
$hook['hook'],
|
||||||
array( $hook['component'], $hook['callback'] ),
|
array( $hook['component'], $hook['callback'] ),
|
||||||
$hook['priority'],
|
$hook['priority'],
|
||||||
$hook['accepted_args']
|
$hook['accepted_args']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,246 +9,246 @@
|
|||||||
*/
|
*/
|
||||||
class Peptide_News_Logger {
|
class Peptide_News_Logger {
|
||||||
|
|
||||||
/** Log level constants. */
|
/** Log level constants. */
|
||||||
const LEVEL_INFO = 'info';
|
const LEVEL_INFO = 'info';
|
||||||
const LEVEL_WARNING = 'warning';
|
const LEVEL_WARNING = 'warning';
|
||||||
const LEVEL_ERROR = 'error';
|
const LEVEL_ERROR = 'error';
|
||||||
const LEVEL_DEBUG = 'debug';
|
const LEVEL_DEBUG = 'debug';
|
||||||
|
|
||||||
/** Maximum rows kept in the log table (auto-pruned on write). */
|
/** Maximum rows kept in the log table (auto-pruned on write). */
|
||||||
const MAX_ROWS = 2000;
|
const MAX_ROWS = 2000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the log table name.
|
* Return the log table name.
|
||||||
*/
|
*/
|
||||||
public static function table() {
|
public static function table() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
return $wpdb->prefix . 'peptide_news_log';
|
return $wpdb->prefix . 'peptide_news_log';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* Writing
|
* Writing
|
||||||
* ----------------------------------------------------------------*/
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write a log entry.
|
* Write a log entry.
|
||||||
*
|
*
|
||||||
* @param string $level One of the LEVEL_* constants.
|
* @param string $level One of the LEVEL_* constants.
|
||||||
* @param string $message Human-readable message.
|
* @param string $message Human-readable message.
|
||||||
* @param string $context Category tag (fetch, llm, admin, cron, general).
|
* @param string $context Category tag (fetch, llm, admin, cron, general).
|
||||||
*/
|
*/
|
||||||
public static function log( $level, $message, $context = 'general' ) {
|
public static function log( $level, $message, $context = 'general' ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$wpdb->insert(
|
$wpdb->insert(
|
||||||
self::table(),
|
self::table(),
|
||||||
array(
|
array(
|
||||||
'level' => $level,
|
'level' => $level,
|
||||||
'context' => $context,
|
'context' => $context,
|
||||||
'message' => $message,
|
'message' => $message,
|
||||||
'created_at' => current_time( 'mysql' ),
|
'created_at' => current_time( 'mysql' ),
|
||||||
),
|
),
|
||||||
array( '%s', '%s', '%s', '%s' )
|
array( '%s', '%s', '%s', '%s' )
|
||||||
);
|
);
|
||||||
|
|
||||||
// Auto-prune old entries.
|
// Auto-prune old entries.
|
||||||
self::maybe_prune();
|
self::maybe_prune();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convenience: info level. */
|
/** Convenience: info level. */
|
||||||
public static function info( $message, $context = 'general' ) {
|
public static function info( $message, $context = 'general' ) {
|
||||||
self::log( self::LEVEL_INFO, $message, $context );
|
self::log( self::LEVEL_INFO, $message, $context );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convenience: warning level. */
|
/** Convenience: warning level. */
|
||||||
public static function warning( $message, $context = 'general' ) {
|
public static function warning( $message, $context = 'general' ) {
|
||||||
self::log( self::LEVEL_WARNING, $message, $context );
|
self::log( self::LEVEL_WARNING, $message, $context );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convenience: error level. */
|
/** Convenience: error level. */
|
||||||
public static function error( $message, $context = 'general' ) {
|
public static function error( $message, $context = 'general' ) {
|
||||||
self::log( self::LEVEL_ERROR, $message, $context );
|
self::log( self::LEVEL_ERROR, $message, $context );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convenience: debug level (only writes when WP_DEBUG is on). */
|
/** Convenience: debug level (only writes when WP_DEBUG is on). */
|
||||||
public static function debug( $message, $context = 'general' ) {
|
public static function debug( $message, $context = 'general' ) {
|
||||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
self::log( self::LEVEL_DEBUG, $message, $context );
|
self::log( self::LEVEL_DEBUG, $message, $context );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* Reading
|
* Reading
|
||||||
* ----------------------------------------------------------------*/
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve log entries.
|
* Retrieve log entries.
|
||||||
*
|
*
|
||||||
* @param array $args {
|
* @param array $args {
|
||||||
* Optional. Query arguments.
|
* Optional. Query arguments.
|
||||||
*
|
*
|
||||||
* @type string $level Filter by level.
|
* @type string $level Filter by level.
|
||||||
* @type string $context Filter by context.
|
* @type string $context Filter by context.
|
||||||
* @type int $limit Number of rows.
|
* @type int $limit Number of rows.
|
||||||
* @type int $offset Pagination offset.
|
* @type int $offset Pagination offset.
|
||||||
* }
|
* }
|
||||||
* @return array { 'rows' => array, 'total' => int }
|
* @return array { 'rows' => array, 'total' => int }
|
||||||
*/
|
*/
|
||||||
public static function get_logs( $args = array() ) {
|
public static function get_logs( $args = array() ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$defaults = array(
|
$defaults = array(
|
||||||
'level' => '',
|
'level' => '',
|
||||||
'context' => '',
|
'context' => '',
|
||||||
'limit' => 50,
|
'limit' => 50,
|
||||||
'offset' => 0,
|
'offset' => 0,
|
||||||
);
|
);
|
||||||
$args = wp_parse_args( $args, $defaults );
|
$args = wp_parse_args( $args, $defaults );
|
||||||
|
|
||||||
$table = self::table();
|
$table = self::table();
|
||||||
$where = array( '1=1' );
|
$where = array( '1=1' );
|
||||||
$vals = array();
|
$vals = array();
|
||||||
|
|
||||||
if ( ! empty( $args['level'] ) ) {
|
if ( ! empty( $args['level'] ) ) {
|
||||||
$where[] = 'level = %s';
|
$where[] = 'level = %s';
|
||||||
$vals[] = $args['level'];
|
$vals[] = $args['level'];
|
||||||
}
|
}
|
||||||
if ( ! empty( $args['context'] ) ) {
|
if ( ! empty( $args['context'] ) ) {
|
||||||
$where[] = 'context = %s';
|
$where[] = 'context = %s';
|
||||||
$vals[] = $args['context'];
|
$vals[] = $args['context'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$where_sql = implode( ' AND ', $where );
|
$where_sql = implode( ' AND ', $where );
|
||||||
|
|
||||||
// Total count.
|
// Total count.
|
||||||
$count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
|
$count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
|
||||||
$total = empty( $vals )
|
$total = empty( $vals )
|
||||||
? (int) $wpdb->get_var( $count_sql )
|
? (int) $wpdb->get_var( $count_sql )
|
||||||
: (int) $wpdb->get_var( $wpdb->prepare( $count_sql, $vals ) );
|
: (int) $wpdb->get_var( $wpdb->prepare( $count_sql, $vals ) );
|
||||||
|
|
||||||
// Rows.
|
// Rows.
|
||||||
$order_sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY id DESC LIMIT %d OFFSET %d";
|
$order_sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY id DESC LIMIT %d OFFSET %d";
|
||||||
$vals[] = absint( $args['limit'] );
|
$vals[] = absint( $args['limit'] );
|
||||||
$vals[] = absint( $args['offset'] );
|
$vals[] = absint( $args['offset'] );
|
||||||
$rows = $wpdb->get_results( $wpdb->prepare( $order_sql, $vals ) );
|
$rows = $wpdb->get_results( $wpdb->prepare( $order_sql, $vals ) );
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'rows' => $rows ? $rows : array(),
|
'rows' => $rows ? $rows : array(),
|
||||||
'total' => $total,
|
'total' => $total,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* Maintenance
|
* Maintenance
|
||||||
* ----------------------------------------------------------------*/
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove rows beyond the MAX_ROWS cap.
|
* Remove rows beyond the MAX_ROWS cap.
|
||||||
*/
|
*/
|
||||||
private static function maybe_prune() {
|
private static function maybe_prune() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table = self::table();
|
$table = self::table();
|
||||||
|
|
||||||
// Only prune every ~50 writes (probabilistic).
|
// Only prune every ~50 writes (probabilistic).
|
||||||
if ( wp_rand( 1, 50 ) !== 1 ) {
|
if ( wp_rand( 1, 50 ) !== 1 ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
|
$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
|
||||||
|
|
||||||
if ( $count > self::MAX_ROWS ) {
|
if ( $count > self::MAX_ROWS ) {
|
||||||
$delete_count = $count - self::MAX_ROWS;
|
$delete_count = $count - self::MAX_ROWS;
|
||||||
$wpdb->query( $wpdb->prepare(
|
$wpdb->query( $wpdb->prepare(
|
||||||
"DELETE FROM {$table} ORDER BY id ASC LIMIT %d",
|
"DELETE FROM {$table} ORDER BY id ASC LIMIT %d",
|
||||||
$delete_count
|
$delete_count
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all log entries.
|
* Clear all log entries.
|
||||||
*/
|
*/
|
||||||
public static function clear() {
|
public static function clear() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$wpdb->query( "TRUNCATE TABLE " . self::table() );
|
$wpdb->query( "TRUNCATE TABLE " . self::table() );
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* AJAX handlers
|
* AJAX handlers
|
||||||
* ----------------------------------------------------------------*/
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX: fetch paginated log entries.
|
* AJAX: fetch paginated log entries.
|
||||||
*/
|
*/
|
||||||
public static function ajax_get_logs() {
|
public static function ajax_get_logs() {
|
||||||
check_ajax_referer( 'peptide_news_admin', 'nonce' );
|
check_ajax_referer( 'peptide_news_admin', 'nonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( 'Unauthorized', 403 );
|
wp_send_json_error( 'Unauthorized', 403 );
|
||||||
}
|
}
|
||||||
|
|
||||||
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
|
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
|
||||||
$level = isset( $_POST['level'] ) ? sanitize_text_field( $_POST['level'] ) : '';
|
$level = isset( $_POST['level'] ) ? sanitize_text_field( $_POST['level'] ) : '';
|
||||||
$context = isset( $_POST['context'] ) ? sanitize_text_field( $_POST['context'] ) : '';
|
$context = isset( $_POST['context'] ) ? sanitize_text_field( $_POST['context'] ) : '';
|
||||||
$per_page = 50;
|
$per_page = 50;
|
||||||
|
|
||||||
$result = self::get_logs( array(
|
$result = self::get_logs( array(
|
||||||
'level' => $level,
|
'level' => $level,
|
||||||
'context' => $context,
|
'context' => $context,
|
||||||
'limit' => $per_page,
|
'limit' => $per_page,
|
||||||
'offset' => ( $page - 1 ) * $per_page,
|
'offset' => ( $page - 1 ) * $per_page,
|
||||||
) );
|
) );
|
||||||
|
|
||||||
wp_send_json_success( array(
|
wp_send_json_success( array(
|
||||||
'rows' => $result['rows'],
|
'rows' => $result['rows'],
|
||||||
'total' => $result['total'],
|
'total' => $result['total'],
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'total_pages' => ceil( $result['total'] / $per_page ),
|
'total_pages' => ceil( $result['total'] / $per_page ),
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX: clear all log entries.
|
* AJAX: clear all log entries.
|
||||||
*/
|
*/
|
||||||
public static function ajax_clear_logs() {
|
public static function ajax_clear_logs() {
|
||||||
check_ajax_referer( 'peptide_news_admin', 'nonce' );
|
check_ajax_referer( 'peptide_news_admin', 'nonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( 'Unauthorized', 403 );
|
wp_send_json_error( 'Unauthorized', 403 );
|
||||||
}
|
}
|
||||||
|
|
||||||
self::clear();
|
self::clear();
|
||||||
self::info( 'Log cleared by admin.', 'admin' );
|
self::info( 'Log cleared by admin.', 'admin' );
|
||||||
|
|
||||||
wp_send_json_success( array( 'message' => 'Log cleared.' ) );
|
wp_send_json_success( array( 'message' => 'Log cleared.' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* Schema
|
* Schema
|
||||||
* ----------------------------------------------------------------*/
|
* ----------------------------------------------------------------*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the log table (called from activator / upgrade check).
|
* Create the log table (called from activator / upgrade check).
|
||||||
*/
|
*/
|
||||||
public static function create_table() {
|
public static function create_table() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = self::table();
|
$table = self::table();
|
||||||
$charset_collate = $wpdb->get_charset_collate();
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
|
|
||||||
$sql = "CREATE TABLE {$table} (
|
$sql = "CREATE TABLE {$table} (
|
||||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
level VARCHAR(10) NOT NULL DEFAULT 'info',
|
level VARCHAR(10) NOT NULL DEFAULT 'info',
|
||||||
context VARCHAR(30) NOT NULL DEFAULT 'general',
|
context VARCHAR(30) NOT NULL DEFAULT 'general',
|
||||||
message TEXT NOT NULL,
|
message TEXT NOT NULL,
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
KEY idx_level (level),
|
KEY idx_level (level),
|
||||||
KEY idx_context (context),
|
KEY idx_context (context),
|
||||||
KEY idx_created (created_at)
|
KEY idx_created (created_at)
|
||||||
) {$charset_collate};";
|
) {$charset_collate};";
|
||||||
|
|
||||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
dbDelta( $sql );
|
dbDelta( $sql );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,418 +17,418 @@
|
|||||||
*/
|
*/
|
||||||
class Peptide_News_Rest_API {
|
class Peptide_News_Rest_API {
|
||||||
|
|
||||||
const API_NAMESPACE = 'peptide-news/v1';
|
const API_NAMESPACE = 'peptide-news/v1';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register REST routes.
|
* Register REST routes.
|
||||||
*/
|
*/
|
||||||
public function register_routes() {
|
public function register_routes() {
|
||||||
|
|
||||||
// Public: list articles.
|
// Public: list articles.
|
||||||
register_rest_route( self::API_NAMESPACE, '/articles', array(
|
register_rest_route( self::API_NAMESPACE, '/articles', array(
|
||||||
'methods' => 'GET',
|
'methods' => 'GET',
|
||||||
'callback' => array( $this, 'get_articles' ),
|
'callback' => array( $this, 'get_articles' ),
|
||||||
'permission_callback' => '__return_true',
|
'permission_callback' => '__return_true',
|
||||||
'args' => array(
|
'args' => array(
|
||||||
'count' => array(
|
'count' => array(
|
||||||
'default' => 10,
|
'default' => 10,
|
||||||
'sanitize_callback' => 'absint',
|
'sanitize_callback' => 'absint',
|
||||||
),
|
),
|
||||||
'page' => array(
|
'page' => array(
|
||||||
'default' => 1,
|
'default' => 1,
|
||||||
'sanitize_callback' => 'absint',
|
'sanitize_callback' => 'absint',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
) );
|
) );
|
||||||
|
|
||||||
// Admin-only analytics endpoints.
|
// Admin-only analytics endpoints.
|
||||||
$analytics_routes = array(
|
$analytics_routes = array(
|
||||||
'/analytics/top' => 'get_top_articles',
|
'/analytics/top' => 'get_top_articles',
|
||||||
'/analytics/trends' => 'get_trends',
|
'/analytics/trends' => 'get_trends',
|
||||||
'/analytics/topics' => 'get_topics',
|
'/analytics/topics' => 'get_topics',
|
||||||
'/analytics/devices' => 'get_devices',
|
'/analytics/devices' => 'get_devices',
|
||||||
'/analytics/sources' => 'get_sources',
|
'/analytics/sources' => 'get_sources',
|
||||||
'/analytics/export' => 'get_export',
|
'/analytics/export' => 'get_export',
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ( $analytics_routes as $route => $callback ) {
|
foreach ( $analytics_routes as $route => $callback ) {
|
||||||
register_rest_route( self::API_NAMESPACE, $route, array(
|
register_rest_route( self::API_NAMESPACE, $route, array(
|
||||||
'methods' => 'GET',
|
'methods' => 'GET',
|
||||||
'callback' => array( $this, $callback ),
|
'callback' => array( $this, $callback ),
|
||||||
'permission_callback' => array( $this, 'check_admin_permissions' ),
|
'permission_callback' => array( $this, 'check_admin_permissions' ),
|
||||||
'args' => $this->get_date_range_args(),
|
'args' => $this->get_date_range_args(),
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify the request comes from an admin user.
|
* Verify the request comes from an admin user.
|
||||||
*
|
*
|
||||||
* @param WP_REST_Request $request
|
* @param WP_REST_Request $request
|
||||||
* @return bool|WP_Error
|
* @return bool|WP_Error
|
||||||
*/
|
*/
|
||||||
public function check_admin_permissions( $request ) {
|
public function check_admin_permissions( $request ) {
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
return new WP_Error(
|
return new WP_Error(
|
||||||
'rest_forbidden',
|
'rest_forbidden',
|
||||||
__( 'You do not have permission to access analytics data.', 'peptide-news' ),
|
__( 'You do not have permission to access analytics data.', 'peptide-news' ),
|
||||||
array( 'status' => 403 )
|
array( 'status' => 403 )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common date-range argument definitions.
|
* Common date-range argument definitions.
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
private function get_date_range_args() {
|
private function get_date_range_args() {
|
||||||
return array(
|
return array(
|
||||||
'start_date' => array(
|
'start_date' => array(
|
||||||
'default' => gmdate( 'Y-m-d', strtotime( '-30 days' ) ),
|
'default' => gmdate( 'Y-m-d', strtotime( '-30 days' ) ),
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
'validate_callback' => array( $this, 'validate_date_format' ),
|
'validate_callback' => array( $this, 'validate_date_format' ),
|
||||||
),
|
),
|
||||||
'end_date' => array(
|
'end_date' => array(
|
||||||
'default' => gmdate( 'Y-m-d' ),
|
'default' => gmdate( 'Y-m-d' ),
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
'validate_callback' => array( $this, 'validate_date_format' ),
|
'validate_callback' => array( $this, 'validate_date_format' ),
|
||||||
),
|
),
|
||||||
'limit' => array(
|
'limit' => array(
|
||||||
'default' => 20,
|
'default' => 20,
|
||||||
'sanitize_callback' => 'absint',
|
'sanitize_callback' => 'absint',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that a parameter is a Y-m-d date string.
|
* Validate that a parameter is a Y-m-d date string.
|
||||||
*
|
*
|
||||||
* @param string $value
|
* @param string $value
|
||||||
* @param WP_REST_Request $request
|
* @param WP_REST_Request $request
|
||||||
* @param string $param
|
* @param string $param
|
||||||
* @return true|WP_Error
|
* @return true|WP_Error
|
||||||
*/
|
*/
|
||||||
public function validate_date_format( $value, $request, $param ) {
|
public function validate_date_format( $value, $request, $param ) {
|
||||||
if ( ! preg_match( '/^\\d{4}-\\d{2}-\\d{2}$/', $value ) ) {
|
if ( ! preg_match( '/^\\d{4}-\\d{2}-\\d{2}$/', $value ) ) {
|
||||||
return new WP_Error(
|
return new WP_Error(
|
||||||
'rest_invalid_date',
|
'rest_invalid_date',
|
||||||
sprintf( __( 'Invalid date format for %s. Expected Y-m-d.', 'peptide-news' ), $param ),
|
sprintf( __( 'Invalid date format for %s. Expected Y-m-d.', 'peptide-news' ), $param ),
|
||||||
array( 'status' => 400 )
|
array( 'status' => 400 )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /articles
|
* GET /articles
|
||||||
*
|
*
|
||||||
* Returns active articles with server-side transient caching.
|
* Returns active articles with server-side transient caching.
|
||||||
* Cache is invalidated whenever new articles are stored.
|
* Cache is invalidated whenever new articles are stored.
|
||||||
*/
|
*/
|
||||||
public function get_articles( $request ) {
|
public function get_articles( $request ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$count = absint( min( $request->get_param( 'count' ), 100 ) );
|
$count = absint( min( $request->get_param( 'count' ), 100 ) );
|
||||||
$page = absint( max( $request->get_param( 'page' ), 1 ) );
|
$page = absint( max( $request->get_param( 'page' ), 1 ) );
|
||||||
$table = $wpdb->prefix . 'peptide_news_articles';
|
$table = $wpdb->prefix . 'peptide_news_articles';
|
||||||
|
|
||||||
// Build cache key from page and count params.
|
// Build cache key from page and count params.
|
||||||
$cache_key = 'peptide_news_articles_' . $page . '_' . $count;
|
$cache_key = 'peptide_news_articles_' . $page . '_' . $count;
|
||||||
$cached_response = get_transient( $cache_key );
|
$cached_response = get_transient( $cache_key );
|
||||||
if ( false !== $cached_response ) {
|
if ( false !== $cached_response ) {
|
||||||
return rest_ensure_response( $cached_response );
|
return rest_ensure_response( $cached_response );
|
||||||
}
|
}
|
||||||
|
|
||||||
$total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE is_active = %d", 1 ) );
|
$total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE is_active = %d", 1 ) );
|
||||||
|
|
||||||
// Ensure page doesn't exceed max available pages.
|
// Ensure page doesn't exceed max available pages.
|
||||||
$max_page = max( 1, (int) ceil( $total / $count ) );
|
$max_page = max( 1, (int) ceil( $total / $count ) );
|
||||||
$page = min( $page, $max_page );
|
$page = min( $page, $max_page );
|
||||||
$offset = ( $page - 1 ) * $count;
|
$offset = ( $page - 1 ) * $count;
|
||||||
|
|
||||||
$articles = $wpdb->get_results( $wpdb->prepare(
|
$articles = $wpdb->get_results( $wpdb->prepare(
|
||||||
"SELECT id, source, source_url, title, excerpt, ai_summary, author,
|
"SELECT id, source, source_url, title, excerpt, ai_summary, author,
|
||||||
thumbnail_url, thumbnail_local, published_at, categories, tags
|
thumbnail_url, thumbnail_local, published_at, categories, tags
|
||||||
FROM {$table}
|
FROM {$table}
|
||||||
WHERE is_active = 1
|
WHERE is_active = 1
|
||||||
ORDER BY published_at DESC
|
ORDER BY published_at DESC
|
||||||
LIMIT %d OFFSET %d",
|
LIMIT %d OFFSET %d",
|
||||||
$count,
|
$count,
|
||||||
$offset
|
$offset
|
||||||
) );
|
) );
|
||||||
|
|
||||||
// Strip source name from titles, excerpts, and summaries.
|
// Strip source name from titles, excerpts, and summaries.
|
||||||
$nbsp = "\xC2\xA0"; // UTF-8 non-breaking space.
|
$nbsp = "\xC2\xA0"; // UTF-8 non-breaking space.
|
||||||
foreach ( $articles as $article ) {
|
foreach ( $articles as $article ) {
|
||||||
$fields = array( 'title', 'excerpt', 'ai_summary' );
|
$fields = array( 'title', 'excerpt', 'ai_summary' );
|
||||||
foreach ( $fields as $field ) {
|
foreach ( $fields as $field ) {
|
||||||
if ( empty( $article->$field ) ) {
|
if ( empty( $article->$field ) ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// First pass: use strip_source_suffix for known patterns.
|
// First pass: use strip_source_suffix for known patterns.
|
||||||
$article->$field = $this->strip_source_suffix( $article->$field, $article->source, $article->source_url );
|
$article->$field = $this->strip_source_suffix( $article->$field, $article->source, $article->source_url );
|
||||||
|
|
||||||
// Second pass: catch any remaining nbsp-separated publisher names
|
// Second pass: catch any remaining nbsp-separated publisher names
|
||||||
// (e.g., "...text\xC2\xA0\xC2\xA0Publisher Name" from Google News RSS).
|
// (e.g., "...text\xC2\xA0\xC2\xA0Publisher Name" from Google News RSS).
|
||||||
$text = $article->$field;
|
$text = $article->$field;
|
||||||
$pos = strrpos( $text, $nbsp . $nbsp );
|
$pos = strrpos( $text, $nbsp . $nbsp );
|
||||||
if ( false === $pos ) {
|
if ( false === $pos ) {
|
||||||
$pos = strrpos( $text, $nbsp );
|
$pos = strrpos( $text, $nbsp );
|
||||||
}
|
}
|
||||||
if ( false !== $pos && $pos > 10 ) {
|
if ( false !== $pos && $pos > 10 ) {
|
||||||
$candidate = rtrim( substr( $text, 0, $pos ) );
|
$candidate = rtrim( substr( $text, 0, $pos ) );
|
||||||
if ( strlen( $candidate ) > strlen( $text ) * 0.3 ) {
|
if ( strlen( $candidate ) > strlen( $text ) * 0.3 ) {
|
||||||
$article->$field = $candidate;
|
$article->$field = $candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = array(
|
$data = array(
|
||||||
'articles' => $articles,
|
'articles' => $articles,
|
||||||
'total' => (int) $total,
|
'total' => (int) $total,
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'per_page' => $count,
|
'per_page' => $count,
|
||||||
'total_pages' => ceil( $total / $count ),
|
'total_pages' => ceil( $total / $count ),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cache the response for 1 hour.
|
// Cache the response for 1 hour.
|
||||||
set_transient( $cache_key, $data, HOUR_IN_SECONDS );
|
set_transient( $cache_key, $data, HOUR_IN_SECONDS );
|
||||||
|
|
||||||
$response = rest_ensure_response( $data );
|
$response = rest_ensure_response( $data );
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strip the source/publisher name from the end of a string.
|
* Strip the source/publisher name from the end of a string.
|
||||||
*
|
*
|
||||||
* Handles common separator patterns like " - Source", " | Source",
|
* Handles common separator patterns like " - Source", " | Source",
|
||||||
* " — Source", and " – Source". Uses case-insensitive matching against
|
* " — Source", and " – Source". Uses case-insensitive matching against
|
||||||
* the source field. Also extracts publisher names from source URLs
|
* the source field. Also extracts publisher names from source URLs
|
||||||
* and has a regex fallback for common RSS title patterns.
|
* and has a regex fallback for common RSS title patterns.
|
||||||
*
|
*
|
||||||
* @param string $text The text to clean.
|
* @param string $text The text to clean.
|
||||||
* @param string $source The source name to strip.
|
* @param string $source The source name to strip.
|
||||||
* @param string $source_url The source URL (used to extract domain-based publisher names).
|
* @param string $source_url The source URL (used to extract domain-based publisher names).
|
||||||
* @return string Cleaned text.
|
* @return string Cleaned text.
|
||||||
*/
|
*/
|
||||||
private function strip_source_suffix( $text, $source, $source_url = '' ) {
|
private function strip_source_suffix( $text, $source, $source_url = '' ) {
|
||||||
if ( empty( $text ) ) {
|
if ( empty( $text ) ) {
|
||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode HTML entities first so becomes actual characters.
|
// Decode HTML entities first so becomes actual characters.
|
||||||
$text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
|
$text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
|
||||||
|
|
||||||
// Common separators between title/excerpt and source name.
|
// Common separators between title/excerpt and source name.
|
||||||
// Includes regular spaces, non-breaking spaces (\xC2\xA0), and typographic dashes.
|
// Includes regular spaces, non-breaking spaces (\xC2\xA0), and typographic dashes.
|
||||||
$nbsp = "\xC2\xA0"; // UTF-8 non-breaking space
|
$nbsp = "\xC2\xA0"; // UTF-8 non-breaking space
|
||||||
$separators = array(
|
$separators = array(
|
||||||
' - ', ' | ', ' — ', ' – ', ' // ',
|
' - ', ' | ', ' — ', ' – ', ' // ',
|
||||||
$nbsp . $nbsp, // double non-breaking space (Google News RSS excerpts)
|
$nbsp . $nbsp, // double non-breaking space (Google News RSS excerpts)
|
||||||
' ', // double regular space
|
' ', // double regular space
|
||||||
$nbsp, // single non-breaking space
|
$nbsp, // single non-breaking space
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build list of source names to try matching against.
|
// Build list of source names to try matching against.
|
||||||
$source_names = array();
|
$source_names = array();
|
||||||
if ( ! empty( $source ) ) {
|
if ( ! empty( $source ) ) {
|
||||||
$source_names[] = $source;
|
$source_names[] = $source;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get cached source names list to avoid database queries on every API call.
|
// Get cached source names list to avoid database queries on every API call.
|
||||||
$cached_sources = get_transient( 'peptide_news_source_names' );
|
$cached_sources = get_transient( 'peptide_news_source_names' );
|
||||||
if ( false === $cached_sources ) {
|
if ( false === $cached_sources ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table = $wpdb->prefix . 'peptide_news_articles';
|
$table = $wpdb->prefix . 'peptide_news_articles';
|
||||||
$cached_sources = $wpdb->get_col( "SELECT DISTINCT source FROM {$table} WHERE source != '' ORDER BY source" );
|
$cached_sources = $wpdb->get_col( "SELECT DISTINCT source FROM {$table} WHERE source != '' ORDER BY source" );
|
||||||
if ( ! is_array( $cached_sources ) ) {
|
if ( ! is_array( $cached_sources ) ) {
|
||||||
$cached_sources = array();
|
$cached_sources = array();
|
||||||
}
|
}
|
||||||
set_transient( 'peptide_news_source_names', $cached_sources, HOUR_IN_SECONDS );
|
set_transient( 'peptide_news_source_names', $cached_sources, HOUR_IN_SECONDS );
|
||||||
}
|
}
|
||||||
$source_names = array_merge( $source_names, $cached_sources );
|
$source_names = array_merge( $source_names, $cached_sources );
|
||||||
|
|
||||||
// Extract domain-based publisher name (e.g., "News-Medical" from "news-medical.net").
|
// Extract domain-based publisher name (e.g., "News-Medical" from "news-medical.net").
|
||||||
if ( ! empty( $source_url ) ) {
|
if ( ! empty( $source_url ) ) {
|
||||||
$host = wp_parse_url( $source_url, PHP_URL_HOST );
|
$host = wp_parse_url( $source_url, PHP_URL_HOST );
|
||||||
if ( $host ) {
|
if ( $host ) {
|
||||||
$host = preg_replace( '/^www\./', '', $host );
|
$host = preg_replace( '/^www\./', '', $host );
|
||||||
$parts = explode( '.', $host );
|
$parts = explode( '.', $host );
|
||||||
if ( count( $parts ) >= 2 ) {
|
if ( count( $parts ) >= 2 ) {
|
||||||
$domain_name = $parts[ count( $parts ) - 2 ];
|
$domain_name = $parts[ count( $parts ) - 2 ];
|
||||||
$source_names[] = str_replace( '-', ' ', $domain_name );
|
$source_names[] = str_replace( '-', ' ', $domain_name );
|
||||||
$source_names[] = $domain_name;
|
$source_names[] = $domain_name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try exact (case-insensitive) match against known source names.
|
// Try exact (case-insensitive) match against known source names.
|
||||||
foreach ( $source_names as $name ) {
|
foreach ( $source_names as $name ) {
|
||||||
foreach ( $separators as $sep ) {
|
foreach ( $separators as $sep ) {
|
||||||
$suffix = $sep . $name;
|
$suffix = $sep . $name;
|
||||||
$suffix_len = strlen( $suffix );
|
$suffix_len = strlen( $suffix );
|
||||||
if ( strlen( $text ) > $suffix_len && strcasecmp( substr( $text, -$suffix_len ), $suffix ) === 0 ) {
|
if ( strlen( $text ) > $suffix_len && strcasecmp( substr( $text, -$suffix_len ), $suffix ) === 0 ) {
|
||||||
return rtrim( substr( $text, 0, -$suffix_len ) );
|
return rtrim( substr( $text, 0, -$suffix_len ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback 1: strip trailing " - Short Publisher Name" patterns (1-5 words).
|
// Fallback 1: strip trailing " - Short Publisher Name" patterns (1-5 words).
|
||||||
// Only strips if the part after the separator looks like a publisher name
|
// Only strips if the part after the separator looks like a publisher name
|
||||||
// (starts with uppercase, no sentence-ending punctuation).
|
// (starts with uppercase, no sentence-ending punctuation).
|
||||||
$pattern = '/\s+[-\x{2013}\x{2014}|]\s+[A-Z][A-Za-z0-9\s.\'\-]{0,50}$/u';
|
$pattern = '/\s+[-\x{2013}\x{2014}|]\s+[A-Z][A-Za-z0-9\s.\'\-]{0,50}$/u';
|
||||||
$candidate = preg_replace( $pattern, '', $text );
|
$candidate = preg_replace( $pattern, '', $text );
|
||||||
if ( $candidate !== $text && strlen( $candidate ) > strlen( $text ) * 0.4 ) {
|
if ( $candidate !== $text && strlen( $candidate ) > strlen( $text ) * 0.4 ) {
|
||||||
return rtrim( $candidate );
|
return rtrim( $candidate );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback 2: strip trailing nbsp-separated publisher names.
|
// Fallback 2: strip trailing nbsp-separated publisher names.
|
||||||
// Google News RSS excerpts use double non-breaking space + publisher name
|
// Google News RSS excerpts use double non-breaking space + publisher name
|
||||||
// when the source field doesn't match (e.g., source = "news.google.com").
|
// when the source field doesn't match (e.g., source = "news.google.com").
|
||||||
// Uses mb_ereg for reliable multibyte matching.
|
// Uses mb_ereg for reliable multibyte matching.
|
||||||
if ( preg_match( '/^(.{10,}?)\x{00A0}{1,2}([A-Z][^.!?\x{00A0}]{0,55})$/u', $text, $m ) ) {
|
if ( preg_match( '/^(.{10,}?)\x{00A0}{1,2}([A-Z][^.!?\x{00A0}]{0,55})$/u', $text, $m ) ) {
|
||||||
if ( mb_strlen( $m[1], 'UTF-8' ) > mb_strlen( $text, 'UTF-8' ) * 0.4 ) {
|
if ( mb_strlen( $m[1], 'UTF-8' ) > mb_strlen( $text, 'UTF-8' ) * 0.4 ) {
|
||||||
return rtrim( $m[1] );
|
return rtrim( $m[1] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /analytics/top
|
* GET /analytics/top
|
||||||
*/
|
*/
|
||||||
public function get_top_articles( $request ) {
|
public function get_top_articles( $request ) {
|
||||||
$data = Peptide_News_Analytics::get_top_articles(
|
$data = Peptide_News_Analytics::get_top_articles(
|
||||||
$request->get_param( 'start_date' ),
|
$request->get_param( 'start_date' ),
|
||||||
$request->get_param( 'end_date' ),
|
$request->get_param( 'end_date' ),
|
||||||
$request->get_param( 'limit' )
|
$request->get_param( 'limit' )
|
||||||
);
|
);
|
||||||
return rest_ensure_response( $data );
|
return rest_ensure_response( $data );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /analytics/trends
|
* GET /analytics/trends
|
||||||
*/
|
*/
|
||||||
public function get_trends( $request ) {
|
public function get_trends( $request ) {
|
||||||
$data = Peptide_News_Analytics::get_click_trends(
|
$data = Peptide_News_Analytics::get_click_trends(
|
||||||
$request->get_param( 'start_date' ),
|
$request->get_param( 'start_date' ),
|
||||||
$request->get_param( 'end_date' )
|
$request->get_param( 'end_date' )
|
||||||
);
|
);
|
||||||
return rest_ensure_response( $data );
|
return rest_ensure_response( $data );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /analytics/topics
|
* GET /analytics/topics
|
||||||
*/
|
*/
|
||||||
public function get_topics( $request ) {
|
public function get_topics( $request ) {
|
||||||
$data = Peptide_News_Analytics::get_popular_topics(
|
$data = Peptide_News_Analytics::get_popular_topics(
|
||||||
$request->get_param( 'start_date' ),
|
$request->get_param( 'start_date' ),
|
||||||
$request->get_param( 'end_date' ),
|
$request->get_param( 'end_date' ),
|
||||||
$request->get_param( 'limit' )
|
$request->get_param( 'limit' )
|
||||||
);
|
);
|
||||||
return rest_ensure_response( $data );
|
return rest_ensure_response( $data );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /analytics/devices
|
* GET /analytics/devices
|
||||||
*/
|
*/
|
||||||
public function get_devices( $request ) {
|
public function get_devices( $request ) {
|
||||||
$data = Peptide_News_Analytics::get_device_breakdown(
|
$data = Peptide_News_Analytics::get_device_breakdown(
|
||||||
$request->get_param( 'start_date' ),
|
$request->get_param( 'start_date' ),
|
||||||
$request->get_param( 'end_date' )
|
$request->get_param( 'end_date' )
|
||||||
);
|
);
|
||||||
return rest_ensure_response( $data );
|
return rest_ensure_response( $data );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /analytics/sources
|
* GET /analytics/sources
|
||||||
*/
|
*/
|
||||||
public function get_sources( $request ) {
|
public function get_sources( $request ) {
|
||||||
$data = Peptide_News_Analytics::get_source_performance(
|
$data = Peptide_News_Analytics::get_source_performance(
|
||||||
$request->get_param( 'start_date' ),
|
$request->get_param( 'start_date' ),
|
||||||
$request->get_param( 'end_date' )
|
$request->get_param( 'end_date' )
|
||||||
);
|
);
|
||||||
return rest_ensure_response( $data );
|
return rest_ensure_response( $data );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /analytics/export — returns CSV download.
|
* GET /analytics/export — returns CSV download.
|
||||||
*/
|
*/
|
||||||
public function get_export( $request ) {
|
public function get_export( $request ) {
|
||||||
$data = Peptide_News_Analytics::export_clicks_csv(
|
$data = Peptide_News_Analytics::export_clicks_csv(
|
||||||
$request->get_param( 'start_date' ),
|
$request->get_param( 'start_date' ),
|
||||||
$request->get_param( 'end_date' )
|
$request->get_param( 'end_date' )
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( empty( $data ) ) {
|
if ( empty( $data ) ) {
|
||||||
return rest_ensure_response( array( 'message' => 'No data for the selected period.' ) );
|
return rest_ensure_response( array( 'message' => 'No data for the selected period.' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return as JSON (client can convert to CSV).
|
// Return as JSON (client can convert to CSV).
|
||||||
return rest_ensure_response( array(
|
return rest_ensure_response( array(
|
||||||
'headers' => array_keys( $data[0] ),
|
'headers' => array_keys( $data[0] ),
|
||||||
'rows' => $data,
|
'rows' => $data,
|
||||||
'count' => count( $data ),
|
'count' => count( $data ),
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler: delete one or more articles by ID.
|
* AJAX handler: delete one or more articles by ID.
|
||||||
*
|
*
|
||||||
* Expects POST with 'ids' (comma-separated article IDs) and 'nonce'.
|
* Expects POST with 'ids' (comma-separated article IDs) and 'nonce'.
|
||||||
* Cascading foreign keys on the clicks and daily_stats tables handle
|
* Cascading foreign keys on the clicks and daily_stats tables handle
|
||||||
* related analytics cleanup automatically.
|
* related analytics cleanup automatically.
|
||||||
*
|
*
|
||||||
* @since 2.1.0
|
* @since 2.1.0
|
||||||
*/
|
*/
|
||||||
public static function ajax_delete_articles() {
|
public static function ajax_delete_articles() {
|
||||||
check_ajax_referer( 'peptide_news_admin', 'nonce' );
|
check_ajax_referer( 'peptide_news_admin', 'nonce' );
|
||||||
|
|
||||||
if ( ! current_user_can( 'manage_options' ) ) {
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
wp_send_json_error( 'Unauthorized.', 403 );
|
wp_send_json_error( 'Unauthorized.', 403 );
|
||||||
}
|
}
|
||||||
|
|
||||||
$raw_ids = isset( $_POST['ids'] ) ? sanitize_text_field( wp_unslash( $_POST['ids'] ) ) : '';
|
$raw_ids = isset( $_POST['ids'] ) ? sanitize_text_field( wp_unslash( $_POST['ids'] ) ) : '';
|
||||||
if ( empty( $raw_ids ) ) {
|
if ( empty( $raw_ids ) ) {
|
||||||
wp_send_json_error( 'No article IDs provided.' );
|
wp_send_json_error( 'No article IDs provided.' );
|
||||||
}
|
}
|
||||||
|
|
||||||
$ids = array_filter( array_map( 'absint', explode( ',', $raw_ids ) ) );
|
$ids = array_filter( array_map( 'absint', explode( ',', $raw_ids ) ) );
|
||||||
if ( empty( $ids ) ) {
|
if ( empty( $ids ) ) {
|
||||||
wp_send_json_error( 'Invalid article IDs.' );
|
wp_send_json_error( 'Invalid article IDs.' );
|
||||||
}
|
}
|
||||||
|
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table = $wpdb->prefix . 'peptide_news_articles';
|
$table = $wpdb->prefix . 'peptide_news_articles';
|
||||||
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
|
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||||
$deleted = $wpdb->query(
|
$deleted = $wpdb->query(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||||
"DELETE FROM {$table} WHERE id IN ({$placeholders})",
|
"DELETE FROM {$table} WHERE id IN ({$placeholders})",
|
||||||
$ids
|
$ids
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( false === $deleted ) {
|
if ( false === $deleted ) {
|
||||||
wp_send_json_error( 'Database error.' );
|
wp_send_json_error( 'Database error.' );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear article cache so the frontend reflects changes.
|
// Clear article cache so the frontend reflects changes.
|
||||||
delete_transient( 'peptide_news_articles_1_10' );
|
delete_transient( 'peptide_news_articles_1_10' );
|
||||||
|
|
||||||
Peptide_News_Logger::info( 'Deleted ' . $deleted . ' article(s): IDs ' . implode( ', ', $ids ), 'admin' );
|
Peptide_News_Logger::info( 'Deleted ' . $deleted . ' article(s): IDs ' . implode( ', ', $ids ), 'admin' );
|
||||||
|
|
||||||
wp_send_json_success( array(
|
wp_send_json_success( array(
|
||||||
'deleted' => $deleted,
|
'deleted' => $deleted,
|
||||||
'ids' => $ids,
|
'ids' => $ids,
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,111 +8,111 @@
|
|||||||
*/
|
*/
|
||||||
class Peptide_News {
|
class Peptide_News {
|
||||||
|
|
||||||
/** @var Peptide_News_Loader */
|
/** @var Peptide_News_Loader */
|
||||||
protected $loader;
|
protected $loader;
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
protected $plugin_name;
|
protected $plugin_name;
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
protected $version;
|
protected $version;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->version = PEPTIDE_NEWS_VERSION;
|
$this->version = PEPTIDE_NEWS_VERSION;
|
||||||
$this->plugin_name = 'peptide-news';
|
$this->plugin_name = 'peptide-news';
|
||||||
|
|
||||||
$this->load_dependencies();
|
$this->load_dependencies();
|
||||||
$this->define_admin_hooks();
|
$this->define_admin_hooks();
|
||||||
$this->define_public_hooks();
|
$this->define_public_hooks();
|
||||||
$this->define_cron_hooks();
|
$this->define_cron_hooks();
|
||||||
$this->define_rest_hooks();
|
$this->define_rest_hooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all required files.
|
* Load all required files.
|
||||||
*/
|
*/
|
||||||
private function load_dependencies() {
|
private function load_dependencies() {
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-loader.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-loader.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-activator.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-activator.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-deactivator.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-deactivator.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-fetcher.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-fetcher.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-analytics.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-analytics.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-rest-api.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-rest-api.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-llm.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-llm.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-logger.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-logger.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-content-filter.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-content-filter.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'admin/class-peptide-news-admin.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'admin/class-peptide-news-admin.php';
|
||||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'public/class-peptide-news-public.php';
|
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'public/class-peptide-news-public.php';
|
||||||
|
|
||||||
$this->loader = new Peptide_News_Loader();
|
$this->loader = new Peptide_News_Loader();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register admin-side hooks.
|
* Register admin-side hooks.
|
||||||
*/
|
*/
|
||||||
private function define_admin_hooks() {
|
private function define_admin_hooks() {
|
||||||
$admin = new Peptide_News_Admin( $this->plugin_name, $this->version );
|
$admin = new Peptide_News_Admin( $this->plugin_name, $this->version );
|
||||||
|
|
||||||
$this->loader->add_action( 'admin_enqueue_scripts', $admin, 'enqueue_styles' );
|
$this->loader->add_action( 'admin_enqueue_scripts', $admin, 'enqueue_styles' );
|
||||||
$this->loader->add_action( 'admin_enqueue_scripts', $admin, 'enqueue_scripts' );
|
$this->loader->add_action( 'admin_enqueue_scripts', $admin, 'enqueue_scripts' );
|
||||||
$this->loader->add_action( 'admin_menu', $admin, 'add_admin_menu' );
|
$this->loader->add_action( 'admin_menu', $admin, 'add_admin_menu' );
|
||||||
$this->loader->add_action( 'admin_init', $admin, 'register_settings' );
|
$this->loader->add_action( 'admin_init', $admin, 'register_settings' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register public-facing hooks.
|
* Register public-facing hooks.
|
||||||
*/
|
*/
|
||||||
private function define_public_hooks() {
|
private function define_public_hooks() {
|
||||||
$public = new Peptide_News_Public( $this->plugin_name, $this->version );
|
$public = new Peptide_News_Public( $this->plugin_name, $this->version );
|
||||||
|
|
||||||
$this->loader->add_action( 'wp_enqueue_scripts', $public, 'enqueue_styles' );
|
$this->loader->add_action( 'wp_enqueue_scripts', $public, 'enqueue_styles' );
|
||||||
$this->loader->add_action( 'wp_enqueue_scripts', $public, 'enqueue_scripts' );
|
$this->loader->add_action( 'wp_enqueue_scripts', $public, 'enqueue_scripts' );
|
||||||
$this->loader->add_action( 'init', $public, 'register_shortcode' );
|
$this->loader->add_action( 'init', $public, 'register_shortcode' );
|
||||||
$this->loader->add_action( 'widgets_init', $public, 'register_widget' );
|
$this->loader->add_action( 'widgets_init', $public, 'register_widget' );
|
||||||
$this->loader->add_action( 'wp_ajax_peptide_news_track_click', $public, 'handle_click_tracking' );
|
$this->loader->add_action( 'wp_ajax_peptide_news_track_click', $public, 'handle_click_tracking' );
|
||||||
$this->loader->add_action( 'wp_ajax_nopriv_peptide_news_track_click', $public, 'handle_click_tracking' );
|
$this->loader->add_action( 'wp_ajax_nopriv_peptide_news_track_click', $public, 'handle_click_tracking' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register WP-Cron hooks for scheduled fetching.
|
* Register WP-Cron hooks for scheduled fetching.
|
||||||
*/
|
*/
|
||||||
private function define_cron_hooks() {
|
private function define_cron_hooks() {
|
||||||
$fetcher = new Peptide_News_Fetcher();
|
$fetcher = new Peptide_News_Fetcher();
|
||||||
|
|
||||||
$this->loader->add_action( 'peptide_news_cron_fetch', $fetcher, 'fetch_all_sources' );
|
$this->loader->add_action( 'peptide_news_cron_fetch', $fetcher, 'fetch_all_sources' );
|
||||||
$this->loader->add_filter( 'cron_schedules', $fetcher, 'add_custom_cron_schedules' );
|
$this->loader->add_filter( 'cron_schedules', $fetcher, 'add_custom_cron_schedules' );
|
||||||
$this->loader->add_action( 'wp_ajax_peptide_news_backfill_sources', $fetcher, 'ajax_backfill_sources' );
|
$this->loader->add_action( 'wp_ajax_peptide_news_backfill_sources', $fetcher, 'ajax_backfill_sources' );
|
||||||
$this->loader->add_action( 'wp_ajax_peptide_news_generate_summaries', 'Peptide_News_LLM', 'ajax_generate_summaries' );
|
$this->loader->add_action( 'wp_ajax_peptide_news_generate_summaries', 'Peptide_News_LLM', 'ajax_generate_summaries' );
|
||||||
$this->loader->add_action( 'wp_ajax_peptide_news_get_logs', 'Peptide_News_Logger', 'ajax_get_logs' );
|
$this->loader->add_action( 'wp_ajax_peptide_news_get_logs', 'Peptide_News_Logger', 'ajax_get_logs' );
|
||||||
$this->loader->add_action( 'wp_ajax_peptide_news_clear_logs', 'Peptide_News_Logger', 'ajax_clear_logs' );
|
$this->loader->add_action( 'wp_ajax_peptide_news_clear_logs', 'Peptide_News_Logger', 'ajax_clear_logs' );
|
||||||
$this->loader->add_action( 'wp_ajax_peptide_news_delete_articles', 'Peptide_News_Rest_API', 'ajax_delete_articles' );
|
$this->loader->add_action( 'wp_ajax_peptide_news_delete_articles', 'Peptide_News_Rest_API', 'ajax_delete_articles' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register REST API hooks.
|
* Register REST API hooks.
|
||||||
*/
|
*/
|
||||||
private function define_rest_hooks() {
|
private function define_rest_hooks() {
|
||||||
$rest = new Peptide_News_Rest_API();
|
$rest = new Peptide_News_Rest_API();
|
||||||
|
|
||||||
$this->loader->add_action( 'rest_api_init', $rest, 'register_routes' );
|
$this->loader->add_action( 'rest_api_init', $rest, 'register_routes' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the loader to execute all registered hooks.
|
* Run the loader to execute all registered hooks.
|
||||||
*/
|
*/
|
||||||
public function run() {
|
public function run() {
|
||||||
$this->loader->run();
|
$this->loader->run();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_plugin_name() {
|
public function get_plugin_name() {
|
||||||
return $this->plugin_name;
|
return $this->plugin_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_version() {
|
public function get_version() {
|
||||||
return $this->version;
|
return $this->version;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_loader() {
|
public function get_loader() {
|
||||||
return $this->loader;
|
return $this->loader;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,161 +10,161 @@
|
|||||||
*/
|
*/
|
||||||
class Peptide_News_Public {
|
class Peptide_News_Public {
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $plugin_name;
|
private $plugin_name;
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $version;
|
private $version;
|
||||||
|
|
||||||
public function __construct( $plugin_name, $version ) {
|
public function __construct( $plugin_name, $version ) {
|
||||||
$this->plugin_name = $plugin_name;
|
$this->plugin_name = $plugin_name;
|
||||||
$this->version = $version;
|
$this->version = $version;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register front-end CSS (enqueued on demand by the shortcode/widget).
|
* Register front-end CSS (enqueued on demand by the shortcode/widget).
|
||||||
*/
|
*/
|
||||||
public function enqueue_styles() {
|
public function enqueue_styles() {
|
||||||
wp_register_style(
|
wp_register_style(
|
||||||
$this->plugin_name,
|
$this->plugin_name,
|
||||||
PEPTIDE_NEWS_PLUGIN_URL . 'public/css/public-style.css',
|
PEPTIDE_NEWS_PLUGIN_URL . 'public/css/public-style.css',
|
||||||
array(),
|
array(),
|
||||||
$this->version,
|
$this->version,
|
||||||
'all'
|
'all'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register front-end JS (enqueued on demand by the shortcode/widget).
|
* Register front-end JS (enqueued on demand by the shortcode/widget).
|
||||||
*
|
*
|
||||||
* Loads React (from WordPress core), the feed component, and
|
* Loads React (from WordPress core), the feed component, and
|
||||||
* passes configuration via wp_localize_script.
|
* passes configuration via wp_localize_script.
|
||||||
*/
|
*/
|
||||||
public function enqueue_scripts() {
|
public function enqueue_scripts() {
|
||||||
// React feed component (depends on WP-bundled React).
|
// React feed component (depends on WP-bundled React).
|
||||||
wp_register_script(
|
wp_register_script(
|
||||||
$this->plugin_name . '-feed',
|
$this->plugin_name . '-feed',
|
||||||
PEPTIDE_NEWS_PLUGIN_URL . 'public/js/peptide-news-feed.js',
|
PEPTIDE_NEWS_PLUGIN_URL . 'public/js/peptide-news-feed.js',
|
||||||
array( 'react', 'react-dom' ),
|
array( 'react', 'react-dom' ),
|
||||||
$this->version,
|
$this->version,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the shortcode.
|
* Register the shortcode.
|
||||||
*/
|
*/
|
||||||
public function register_shortcode() {
|
public function register_shortcode() {
|
||||||
add_shortcode( 'peptide_news', array( $this, 'render_shortcode' ) );
|
add_shortcode( 'peptide_news', array( $this, 'render_shortcode' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the widget.
|
* Register the widget.
|
||||||
*/
|
*/
|
||||||
public function register_widget() {
|
public function register_widget() {
|
||||||
register_widget( 'Peptide_News_Widget' );
|
register_widget( 'Peptide_News_Widget' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the [peptide_news] shortcode.
|
* Render the [peptide_news] shortcode.
|
||||||
*
|
*
|
||||||
* Outputs a mount-point div for the React feed component and
|
* Outputs a mount-point div for the React feed component and
|
||||||
* enqueues all required assets. Configuration is passed to JS
|
* enqueues all required assets. Configuration is passed to JS
|
||||||
* via a localized script object.
|
* via a localized script object.
|
||||||
*
|
*
|
||||||
* @param array $atts Shortcode attributes.
|
* @param array $atts Shortcode attributes.
|
||||||
* @return string HTML output.
|
* @return string HTML output.
|
||||||
*/
|
*/
|
||||||
public function render_shortcode( $atts ) {
|
public function render_shortcode( $atts ) {
|
||||||
$atts = shortcode_atts( array(
|
$atts = shortcode_atts( array(
|
||||||
'count' => get_option( 'peptide_news_articles_count', 10 ),
|
'count' => get_option( 'peptide_news_articles_count', 10 ),
|
||||||
), $atts, 'peptide_news' );
|
), $atts, 'peptide_news' );
|
||||||
|
|
||||||
$count = absint( $atts['count'] );
|
$count = absint( $atts['count'] );
|
||||||
|
|
||||||
// Enqueue assets only when the shortcode is actually rendered.
|
// Enqueue assets only when the shortcode is actually rendered.
|
||||||
wp_enqueue_style( $this->plugin_name );
|
wp_enqueue_style( $this->plugin_name );
|
||||||
wp_enqueue_script( $this->plugin_name . '-feed' );
|
wp_enqueue_script( $this->plugin_name . '-feed' );
|
||||||
|
|
||||||
// Pass configuration to the React component.
|
// Pass configuration to the React component.
|
||||||
wp_localize_script( $this->plugin_name . '-feed', 'peptideNewsFeed', array(
|
wp_localize_script( $this->plugin_name . '-feed', 'peptideNewsFeed', array(
|
||||||
'restUrl' => esc_url_raw( rest_url( 'peptide-news/v1/' ) ),
|
'restUrl' => esc_url_raw( rest_url( 'peptide-news/v1/' ) ),
|
||||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||||
'nonce' => wp_create_nonce( 'peptide_news_track' ),
|
'nonce' => wp_create_nonce( 'peptide_news_track' ),
|
||||||
'sessionId' => $this->get_or_create_session_id(),
|
'sessionId' => $this->get_or_create_session_id(),
|
||||||
'count' => $count,
|
'count' => $count,
|
||||||
) );
|
) );
|
||||||
|
|
||||||
// React mount point — the component renders into this div.
|
// React mount point — the component renders into this div.
|
||||||
return '<div data-peptide-news-feed></div>';
|
return '<div data-peptide-news-feed></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle AJAX click tracking with basic rate limiting.
|
* Handle AJAX click tracking with basic rate limiting.
|
||||||
*/
|
*/
|
||||||
public function handle_click_tracking() {
|
public function handle_click_tracking() {
|
||||||
check_ajax_referer( 'peptide_news_track', 'nonce' );
|
check_ajax_referer( 'peptide_news_track', 'nonce' );
|
||||||
|
|
||||||
$article_id = isset( $_POST['article_id'] ) ? absint( $_POST['article_id'] ) : 0;
|
$article_id = isset( $_POST['article_id'] ) ? absint( $_POST['article_id'] ) : 0;
|
||||||
|
|
||||||
if ( ! $article_id ) {
|
if ( ! $article_id ) {
|
||||||
wp_send_json_error( 'Invalid article ID.' );
|
wp_send_json_error( 'Invalid article ID.' );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limit: max 1 click per article per session per 5 seconds.
|
// Rate limit: max 1 click per article per session per 5 seconds.
|
||||||
$session_id = isset( $_POST['session_id'] ) ? sanitize_text_field( wp_unslash( $_POST['session_id'] ) ) : '';
|
$session_id = isset( $_POST['session_id'] ) ? sanitize_text_field( wp_unslash( $_POST['session_id'] ) ) : '';
|
||||||
$throttle_key = 'pn_click_' . md5( $session_id . '_' . $article_id );
|
$throttle_key = 'pn_click_' . md5( $session_id . '_' . $article_id );
|
||||||
|
|
||||||
if ( get_transient( $throttle_key ) ) {
|
if ( get_transient( $throttle_key ) ) {
|
||||||
wp_send_json_success( array( 'throttled' => true ) );
|
wp_send_json_success( array( 'throttled' => true ) );
|
||||||
}
|
}
|
||||||
set_transient( $throttle_key, 1, 5 );
|
set_transient( $throttle_key, 1, 5 );
|
||||||
|
|
||||||
$meta = array(
|
$meta = array(
|
||||||
'ip' => '',
|
'ip' => '',
|
||||||
'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
|
'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
|
||||||
'referrer' => isset( $_POST['referrer'] ) ? esc_url_raw( wp_unslash( $_POST['referrer'] ) ) : '',
|
'referrer' => isset( $_POST['referrer'] ) ? esc_url_raw( wp_unslash( $_POST['referrer'] ) ) : '',
|
||||||
'page_url' => isset( $_POST['page_url'] ) ? esc_url_raw( wp_unslash( $_POST['page_url'] ) ) : '',
|
'page_url' => isset( $_POST['page_url'] ) ? esc_url_raw( wp_unslash( $_POST['page_url'] ) ) : '',
|
||||||
'session_id' => $session_id,
|
'session_id' => $session_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
$result = Peptide_News_Analytics::record_click( $article_id, $meta );
|
$result = Peptide_News_Analytics::record_click( $article_id, $meta );
|
||||||
|
|
||||||
if ( $result ) {
|
if ( $result ) {
|
||||||
wp_send_json_success();
|
wp_send_json_success();
|
||||||
} else {
|
} else {
|
||||||
wp_send_json_error( 'Failed to record click.' );
|
wp_send_json_error( 'Failed to record click.' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate or retrieve a session ID for visitor tracking.
|
* Generate or retrieve a session ID for visitor tracking.
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
private function get_or_create_session_id() {
|
private function get_or_create_session_id() {
|
||||||
if ( isset( $_COOKIE['pn_session_id'] ) ) {
|
if ( isset( $_COOKIE['pn_session_id'] ) ) {
|
||||||
return sanitize_text_field( wp_unslash( $_COOKIE['pn_session_id'] ) );
|
return sanitize_text_field( wp_unslash( $_COOKIE['pn_session_id'] ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
$session_id = wp_generate_uuid4();
|
$session_id = wp_generate_uuid4();
|
||||||
|
|
||||||
if ( ! headers_sent() ) {
|
if ( ! headers_sent() ) {
|
||||||
setcookie(
|
setcookie(
|
||||||
'pn_session_id',
|
'pn_session_id',
|
||||||
$session_id,
|
$session_id,
|
||||||
array(
|
array(
|
||||||
'expires' => time() + 1800,
|
'expires' => time() + 1800,
|
||||||
'path' => '/',
|
'path' => '/',
|
||||||
'secure' => is_ssl(),
|
'secure' => is_ssl(),
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Lax',
|
'samesite' => 'Lax',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $session_id;
|
return $session_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,63 +174,63 @@ class Peptide_News_Public {
|
|||||||
*/
|
*/
|
||||||
class Peptide_News_Widget extends WP_Widget {
|
class Peptide_News_Widget extends WP_Widget {
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
parent::__construct(
|
parent::__construct(
|
||||||
'peptide_news_widget',
|
'peptide_news_widget',
|
||||||
__( 'Peptide News', 'peptide-news' ),
|
__( 'Peptide News', 'peptide-news' ),
|
||||||
array(
|
array(
|
||||||
'description' => __( 'Displays the latest peptide research news.', 'peptide-news' ),
|
'description' => __( 'Displays the latest peptide research news.', 'peptide-news' ),
|
||||||
'classname' => 'peptide-news-widget',
|
'classname' => 'peptide-news-widget',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function widget( $args, $instance ) {
|
public function widget( $args, $instance ) {
|
||||||
$title = apply_filters( 'widget_title', $instance['title'] ?? __( 'Peptide News', 'peptide-news' ) );
|
$title = apply_filters( 'widget_title', $instance['title'] ?? __( 'Peptide News', 'peptide-news' ) );
|
||||||
$count = isset( $instance['count'] ) ? absint( $instance['count'] ) : 5;
|
$count = isset( $instance['count'] ) ? absint( $instance['count'] ) : 5;
|
||||||
|
|
||||||
echo $args['before_widget'];
|
echo $args['before_widget'];
|
||||||
|
|
||||||
if ( $title ) {
|
if ( $title ) {
|
||||||
echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
|
echo $args['before_title'] . esc_html( $title ) . $args['after_title'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$public = new Peptide_News_Public( 'peptide-news', PEPTIDE_NEWS_VERSION );
|
$public = new Peptide_News_Public( 'peptide-news', PEPTIDE_NEWS_VERSION );
|
||||||
echo $public->render_shortcode( array( 'count' => $count ) );
|
echo $public->render_shortcode( array( 'count' => $count ) );
|
||||||
|
|
||||||
echo $args['after_widget'];
|
echo $args['after_widget'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function form( $instance ) {
|
public function form( $instance ) {
|
||||||
if ( ! is_array( $instance ) ) {
|
if ( ! is_array( $instance ) ) {
|
||||||
$instance = array();
|
$instance = array();
|
||||||
}
|
}
|
||||||
$title = isset( $instance['title'] ) ? $instance['title'] : __( 'Peptide News', 'peptide-news' );
|
$title = isset( $instance['title'] ) ? $instance['title'] : __( 'Peptide News', 'peptide-news' );
|
||||||
$count = isset( $instance['count'] ) ? absint( $instance['count'] ) : 5;
|
$count = isset( $instance['count'] ) ? absint( $instance['count'] ) : 5;
|
||||||
?>
|
?>
|
||||||
<p>
|
<p>
|
||||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
|
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
|
||||||
<?php esc_html_e( 'Title:', 'peptide-news' ); ?>
|
<?php esc_html_e( 'Title:', 'peptide-news' ); ?>
|
||||||
</label>
|
</label>
|
||||||
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
|
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
|
||||||
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
|
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
|
||||||
type="text" value="<?php echo esc_attr( $title ); ?>" />
|
type="text" value="<?php echo esc_attr( $title ); ?>" />
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>">
|
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>">
|
||||||
<?php esc_html_e( 'Number of articles:', 'peptide-news' ); ?>
|
<?php esc_html_e( 'Number of articles:', 'peptide-news' ); ?>
|
||||||
</label>
|
</label>
|
||||||
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"
|
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"
|
||||||
name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>"
|
name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>"
|
||||||
type="number" min="1" max="50" value="<?php echo esc_attr( $count ); ?>" />
|
type="number" min="1" max="50" value="<?php echo esc_attr( $count ); ?>" />
|
||||||
</p>
|
</p>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update( $new_instance, $old_instance ) {
|
public function update( $new_instance, $old_instance ) {
|
||||||
$instance = array();
|
$instance = array();
|
||||||
$instance['title'] = sanitize_text_field( $new_instance['title'] ?? '' );
|
$instance['title'] = sanitize_text_field( $new_instance['title'] ?? '' );
|
||||||
$instance['count'] = absint( $new_instance['count'] ?? 5 );
|
$instance['count'] = absint( $new_instance['count'] ?? 5 );
|
||||||
return $instance;
|
return $instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user