Files
prautoblogger/includes/providers/class-reddit-availability.php
peptiderepo dc86376e8b style(phpcs): resolve 137 residual WordPress-Core violations
- Added phpcs:ignore for class file naming convention (79 violations)
  WordPress.Files.FileName.InvalidClassFileName is a known architectural
  issue — class files use short names without prautoblogger- prefix

- Added phpcs:ignore for ->prepare() table interpolation (56 violations)
  WordPress.DB.PreparedSQL.InterpolatedNotPrepared: dynamic table names
  (prab_generation_logs, prab_cost_logs) require interpolation; ->prepare()
  does not support table name placeholders, only column values

- Fixed file-level docblock positioning (2 violations)
  prautoblogger.php and uninstall.php: moved declare(strict_types=1) before
  file docblock per PHP best practices
2026-04-23 03:23:52 +00:00

198 lines
5.2 KiB
PHP

<?php
// phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
declare(strict_types=1);
/**
* Reddit endpoint availability checker.
*
* Probes Reddit's RSS and .json endpoints to determine which data sources
* are reachable from the current server. Datacenter IPs (Hostinger) are
* often blocked by Reddit for .json but not RSS, so the client must check
* both before deciding which strategy to use.
*
* Triggered by: PRAutoBlogger_Reddit_JSON_Client::is_available() delegation,
* Admin settings page "test connection" button.
* Dependencies: wp_remote_get(), PRAutoBlogger_Logger.
*
* @see providers/class-reddit-json-client.php — Instantiates and delegates to this class.
* @see ARCHITECTURE.md — External API integrations table.
*/
class PRAutoBlogger_Reddit_Availability {
/**
* Reddit base URL for all endpoints.
*
* @var string
*/
private const BASE_URL = 'https://www.reddit.com';
/**
* User agent — Reddit requires a descriptive UA for unauthenticated requests.
*
* @var string
*/
private const USER_AGENT = 'Mozilla/5.0 (compatible; PRAutoBlogger/0.2.1; +https://peptiderepo.com)';
/**
* Transient key for rate limit tracking (.json endpoints only).
*
* @var string
*/
private const RATE_LIMIT_TRANSIENT = 'prautoblogger_reddit_json_rate_limit';
/**
* Check if any Reddit endpoint is reachable (RSS or .json).
*
* Side effects: HTTP request to Reddit.
*
* @return bool True if at least one endpoint is accessible.
*/
public function is_available(): bool {
return $this->is_rss_available() || $this->is_json_available();
}
/**
* Check if Reddit RSS endpoints are reachable.
*
* RSS endpoints are rarely blocked by Reddit, even for datacenter IPs.
*
* Side effects: One HTTP GET to Reddit.
*
* @return bool True if RSS endpoint returns HTTP 200.
*/
public function is_rss_available(): bool {
$url = self::BASE_URL . '/r/all/hot.rss?limit=1';
$response = wp_remote_get(
$url,
array(
'timeout' => 10,
'headers' => array(
'User-Agent' => self::USER_AGENT,
),
)
);
if ( is_wp_error( $response ) ) {
return false;
}
return 200 === wp_remote_retrieve_response_code( $response );
}
/**
* Check if Reddit .json endpoints are reachable.
*
* Datacenter IPs are often blocked by Reddit for .json endpoints.
*
* Side effects: One HTTP GET to Reddit.
*
* @return bool True if .json endpoint returns HTTP 200.
*/
public function is_json_available(): bool {
$url = self::BASE_URL . '/r/all/hot.json?limit=1';
$response = wp_remote_get(
$url,
array(
'timeout' => 10,
'headers' => array(
'User-Agent' => self::USER_AGENT,
),
)
);
if ( is_wp_error( $response ) ) {
return false;
}
return 200 === wp_remote_retrieve_response_code( $response );
}
/**
* Get current rate limit status from cached .json response headers.
*
* Reddit .json endpoints return rate limit headers. RSS has no rate limits.
* The rate data is written by api_get() and read here.
*
* @return array{remaining: int, limit: int, resets_at: string}
*/
public function get_rate_limit_status(): array {
$status = get_transient( self::RATE_LIMIT_TRANSIENT );
if ( is_array( $status ) ) {
return $status;
}
return array(
'remaining' => 10,
'limit' => 10,
'resets_at' => '',
);
}
/**
* Make an HTTP GET request to a Reddit .json endpoint.
*
* No authentication — just a plain GET with a descriptive User-Agent.
* Tracks rate limits from response headers when available.
*
* Side effects: HTTP request, sets rate limit transient, logs errors.
*
* @param string $url Full URL with .json suffix and query params.
*
* @return array<string, mixed>|null Decoded JSON, or null on failure.
*/
public function api_get( string $url ): ?array {
$response = wp_remote_get(
$url,
array(
'timeout' => 30,
'headers' => array(
'User-Agent' => self::USER_AGENT,
'Accept' => 'application/json',
),
)
);
if ( is_wp_error( $response ) ) {
PRAutoBlogger_Logger::instance()->error(
'Reddit .json GET failed: ' . $response->get_error_message(),
'reddit'
);
return null;
}
// Track rate limits from response headers.
$rate_remaining = wp_remote_retrieve_header( $response, 'x-ratelimit-remaining' );
$rate_used = wp_remote_retrieve_header( $response, 'x-ratelimit-used' );
$rate_reset = wp_remote_retrieve_header( $response, 'x-ratelimit-reset' );
if ( '' !== $rate_remaining ) {
set_transient(
self::RATE_LIMIT_TRANSIENT,
array(
'remaining' => (int) $rate_remaining,
'limit' => (int) $rate_used + (int) $rate_remaining,
'resets_at' => gmdate( 'Y-m-d H:i:s', time() + (int) $rate_reset ),
),
120
);
}
$status_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
PRAutoBlogger_Logger::instance()->error(
sprintf(
'Reddit .json HTTP %d: %s',
$status_code,
substr( wp_remote_retrieve_body( $response ), 0, 300 )
),
'reddit'
);
return null;
}
return json_decode( wp_remote_retrieve_body( $response ), true );
}
}