Support Castos campaigns via passthrough URLs#899
Conversation
Enable passthrough URLs for podcasts with active campaigns, not just ads.
Castos can push status changes via PUT /ssp/v1/podcasts/{series_id} for
immediate activation; hourly cron sync serves as backup.
Co-Authored-By: Claude via AIContext
📝 WalkthroughWalkthroughReplaces Ads_Controller with a new Passthrough_Controller to manage Castos dynamic passthrough content (ads and campaigns), adds campaigns flags to entities and REST payloads, exposes a REST PUT to update per-series passthrough settings and flush related transients, removes legacy ads cron scheduling, and adds unit tests for passthrough behavior. ChangesPassthrough and Campaigns Integration
Sequence DiagramsequenceDiagram
actor Client
participant REST as "REST API"
participant PassthruCtrl as "Passthrough_Controller"
participant WordPress as "WordPress"
participant CastosAPI as "Castos API"
Client->>REST: PUT /podcasts/{series_id} (ads_enabled, campaigns_enabled)
REST->>PassthruCtrl: update_podcast(request)
PassthruCtrl->>WordPress: ssp_update_option(enable_ads/enable_campaigns)
REST->>REST: flush_episode_file_data_transients(series_id)
REST-->>Client: 200 OK {series_id}
WordPress->>PassthruCtrl: maybe_use_dynamic_content(enclosure_url, episode_id)
PassthruCtrl->>PassthruCtrl: ssp_series_passthrough_required(series_id)
alt passthrough required
PassthruCtrl->>PassthruCtrl: get_episode_file_data(episode_id) (check transient)
alt cache miss
PassthruCtrl->>CastosAPI: fetch episode file data
CastosAPI-->>PassthruCtrl: episode file data (dynamic_url, flags)
PassthruCtrl->>WordPress: set transient(episode_file_data)
end
PassthruCtrl-->>WordPress: return dynamic_url
else passthrough not required
PassthruCtrl-->>WordPress: return original_url
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@php/classes/controllers/class-cron-controller.php`:
- Around line 113-116: The unschedule logic for the legacy cron
('ssp_check_ads') currently runs on admin_init and therefore misses auto-upgrade
installs; move this logic into the plugin upgrade/migration path so it runs
during upgrades (e.g., inside your upgrade handler/migration method that runs
when plugin version changes or during activation update flow). Specifically,
remove the wp_next_scheduled/wp_unschedule_event block from admin_init and
invoke the same checks (call wp_next_scheduled('ssp_check_ads') and
wp_unschedule_event($timestamp, 'ssp_check_ads') when $timestamp is truthy) from
the upgrade routine (the function/method responsible for version migrations),
ensuring it runs even on sites without admin page visits.
In `@php/classes/controllers/class-passthrough-controller.php`:
- Around line 61-63: The hourly cron path currently toggles series flags via
sync_series_option (called with self::ENABLE_ADS_OPTION and
self::ENABLE_CAMPAIGNS_OPTION) but does not clear the stale
ssp_episode_file_data_* transients, so add logic immediately after those
sync_series_option calls to purge related transients for that series; implement
a private helper on the PassthroughController class (e.g.
clear_episode_file_data_transients($series_id)) that removes all matching
ssp_episode_file_data_* transients for the given series (use the appropriate
WordPress transient/option deletion approach or a $wpdb DELETE LIKE on
_transient/_transient_timeout) and call it from the cron sync path so the cron
fallback matches the PUT webhook behavior.
In `@php/classes/rest/class-episodes-rest-controller.php`:
- Around line 136-143: The route uses update_item_permissions_check() which
delegates to validate_castos_authentication() and currently returns distinct 401
bodies; change the auth error handling so any authentication failure from
validate_castos_authentication() (and any path inside
update_item_permissions_check()) is normalized to a single generic WP_Error with
status 401 and a generic message like "Request signature invalid." — locate
validate_castos_authentication() and/or update_item_permissions_check() in
class-episodes-rest-controller.php and replace or wrap all branches that return
WP_Error for missing/invalid headers or keys so they instead return the same
generic WP_Error object (keep the unique function names:
validate_castos_authentication, update_item_permissions_check, update_podcast)
to ensure the PUT /podcasts/(?P<series_id>[\d]+) route always emits the
identical 401 response body.
- Around line 324-345: The transient flush misses episodes that have no series
term (the "default" series) and non-published posts because get_posts() defaults
to published posts; in flush_episode_file_data_transients($series_id) change the
query args to include 'post_status' => 'any' and, when $series_id is
empty/represents the default series, replace the tax_query that selects by
'terms' with a tax_query that uses ssp_series_taxonomy() and 'operator' => 'NOT
EXISTS' so you fetch posts without that taxonomy term; otherwise keep the
existing tax_query for a specific $series_id. Ensure you still iterate over
$episode_ids and delete_transient('ssp_episode_file_data_' . $episode_id) as
before.
In `@php/classes/rest/class-rest-api-controller.php`:
- Line 81: The REST series response is using the global option key
('enable_campaigns') while the series field plumbing expects per-series meta
('ss_podcasting_data_campaigns_enabled_{series_id}'), so align the read/write
keys: change the $podcast['campaigns_enabled'] assignment in
class-rest-api-controller.php to obtain the value via the same per-series
accessor used by create_api_series_fields()/series_get_field_value(), and update
the PUT/update handler that currently calls
ssp_update_option('enable_campaigns') to persist to the per-series meta key (the
ss_podcasting_data_campaigns_enabled_{series_id} key or the helper that writes
series meta) so reads and writes use the same identifier.
In `@tests/WPUnit/SSPEpisodeFunctionsTest.php`:
- Around line 365-372: The test mutates shared option state with
ssp_update_option and currently only resets it after assertions, risking
leftover state when assertions fail; update tests like
testSeriesPassthroughRequiredWithAdsEnabled (and the analogous campaigns-enabled
and episode-level tests) to wrap the mutation and assertion in a try/finally
block where you call ssp_update_option to enable the flag, run the
ssp_series_passthrough_required / related assertion inside try, and in finally
reset both passthrough-related options (both series and campaign/episode
passthrough flags) via ssp_update_option to their original values so cleanup
always runs even on failure.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dcb943a3-dee4-4012-9cd8-5aec3b316b49
📒 Files selected for processing (10)
php/classes/controllers/class-ads-controller.phpphp/classes/controllers/class-app-controller.phpphp/classes/controllers/class-cron-controller.phpphp/classes/controllers/class-passthrough-controller.phpphp/classes/entities/class-api-podcast.phpphp/classes/entities/class-episode-file-data.phpphp/classes/rest/class-episodes-rest-controller.phpphp/classes/rest/class-rest-api-controller.phpphp/includes/ssp-functions.phptests/WPUnit/SSPEpisodeFunctionsTest.php
💤 Files with no reviewable changes (1)
- php/classes/controllers/class-ads-controller.php
Co-Authored-By: Claude via AIContext
There was a problem hiding this comment.
🧹 Nitpick comments (2)
php/classes/rest/class-episodes-rest-controller.php (2)
324-347: 💤 Low valueDefault-series flush is overly broad but safe.
When
$series_idis0(default series), the method flushes transients for all episodes across all series rather than just episodes without a series term. This is more conservative (no stale data) but less efficient.If precision is preferred, filter to episodes lacking the series taxonomy:
♻️ Targeted flush for default series
if ( $series_id ) { $args['tax_query'] = array( array( 'taxonomy' => ssp_series_taxonomy(), 'field' => 'term_id', 'terms' => $series_id, ), ); + } else { + // Default series: episodes with no series term assigned + $args['tax_query'] = array( + array( + 'taxonomy' => ssp_series_taxonomy(), + 'operator' => 'NOT EXISTS', + ), + ); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@php/classes/rest/class-episodes-rest-controller.php` around lines 324 - 347, The flush_episode_file_data_transients function currently clears transients for all episodes when $series_id is 0; instead, modify the conditional so when $series_id is truthy you keep the existing tax_query filtering by term_id, and when $series_id is 0 you add a tax_query for the series taxonomy using operator 'NOT EXISTS' (or equivalent) to target only episodes that lack a series term; update the block referencing ssp_series_taxonomy(), get_posts(), and delete_transient('ssp_episode_file_data_' . $episode_id) accordingly so only episodes without the series term are flushed in the default-series case.
296-316: 💤 Low valueConsider validating that
series_idcorresponds to an existing series term.The endpoint accepts any integer
series_idwithout verifying the term exists. This could lead to orphan options being created for non-existent series. While not critical (the data is harmless), validation would improve API correctness.♻️ Suggested validation
public function update_podcast( $request ) { $series_id = (int) $request->get_param( 'series_id' ); + + // Validate series exists (0 represents default series) + if ( $series_id && ! term_exists( $series_id, ssp_series_taxonomy() ) ) { + return new \WP_Error( 'invalid_series', 'Series not found.', array( 'status' => 404 ) ); + } + $ads_enabled = $request->get_param( 'ads_enabled' );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@php/classes/rest/class-episodes-rest-controller.php` around lines 296 - 316, The update_podcast handler currently accepts any integer series_id; before calling ssp_update_option or flush_episode_file_data_transients, validate the series term exists (use term_exists($series_id, 'series') or get_term($series_id, 'series')) and if it does not, return a REST error (e.g. new WP_Error('rest_series_not_found', 'Series not found', array('status' => 404)) or equivalent rest_ensure_response) instead of proceeding; place this check at the start of update_podcast to prevent creating options for non-existent series.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@php/classes/rest/class-episodes-rest-controller.php`:
- Around line 324-347: The flush_episode_file_data_transients function currently
clears transients for all episodes when $series_id is 0; instead, modify the
conditional so when $series_id is truthy you keep the existing tax_query
filtering by term_id, and when $series_id is 0 you add a tax_query for the
series taxonomy using operator 'NOT EXISTS' (or equivalent) to target only
episodes that lack a series term; update the block referencing
ssp_series_taxonomy(), get_posts(), and
delete_transient('ssp_episode_file_data_' . $episode_id) accordingly so only
episodes without the series term are flushed in the default-series case.
- Around line 296-316: The update_podcast handler currently accepts any integer
series_id; before calling ssp_update_option or
flush_episode_file_data_transients, validate the series term exists (use
term_exists($series_id, 'series') or get_term($series_id, 'series')) and if it
does not, return a REST error (e.g. new WP_Error('rest_series_not_found',
'Series not found', array('status' => 404)) or equivalent rest_ensure_response)
instead of proceeding; place this check at the start of update_podcast to
prevent creating options for non-existent series.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: fbd4ad78-4e5f-4754-be30-0ace3cd9b106
📒 Files selected for processing (1)
php/classes/rest/class-episodes-rest-controller.php
Summary
Ads_ControllertoPassthrough_Controller— activates passthrough when either ads or campaigns are enabledPUT /ssp/v1/podcasts/{series_id}endpoint for Castos to push ads/campaigns status changes immediately (HMAC-authenticated)ssp_check_adscron hook on upgradeCastos-side changes required
campaigns_enabledfield toGET /api/v2/podcastsresponsecampaigns_enabledfield toGET /api/v2/ssp/episodes/{id}/fileresponse (in thepodcastobject)PUT {site_url}/wp-json/ssp/v1/podcasts/{series_id}with HMAC auth when ads or campaigns settings changeTest plan
enable_campaignsoption is set toonPUT /ssp/v1/podcasts/{series_id}updates local options and flushes transientsssp_check_adscron hook gets unscheduled after upgradeSSPEpisodeFunctionsTest— all 27 tests should passSummary by CodeRabbit
New Features
Chores
Tests