Skip to content

Technical Playback Access

Daniel Neto edited this page Nov 12, 2026 · 2 revisions

Mandatory playback access

Implementation baseline: Streamer migration 30.3, PayPerView 11.16, VideoHLS 25.10, and the matching separately versioned FlixHouse api/functions.php update. This is an opt-in product rule, independent of the existing OR-based visibility groups and monetization grants. Basic configuration: Playback access rules.

Implementation commits in the local checkouts: Streamer 9d599382f6 and installer follow-up d4c63b3d0b, PayPerView a4a3406, VideoHLS 3428e04, FlixHouse 56c343f. Each plugin is a separate repository. These identify the source changes; they do not imply a Marketplace release, a production deployment or publication of this wiki.

Model and precedence

objects/PlaybackAccess.php owns resolution, validation and the prerequisite decision. updatedb/updateDb.v30.3.sql creates playback_access_rules with primary key (scope, scope_id), where scope is category or video. require stores the permitted integer group IDs as JSON; unrestricted is an explicit stop-inheritance exception. Inherit is represented by no row.

Fresh installations contain the same table in install/database.sql. install/checkConfiguration.php passes schema version 30.3 to installerSeed() in install/installer.php; verification checks every required playback-rule column before seeding. Core database changes must keep both installer entry files and delegated helpers synchronized. Unit consistency tests compare migration and clean-install definitions/version; the disposable-database installer integration test verifies the resulting schema and stored version.

Resolution is: video override, then its category, then each parent category until the nearest explicit row. A category exception applies to descendants unless a closer category/video overrides it. Without a rule, existing behavior remains. Administrators retain their normal bypass. A required rule matches ANY selected effective user group, using UserGroups::getUserGroups() and the existing subscription/PPV dynamic membership providers. The rule does not itself grant payment, password or ad-free benefits.

Rules are cached only within the current request, not in a session or persistent playback-permission cache. Category lookup for a video reads its current categories_id, so a move does not wait for the video catalog cache. Rule reads after saves bypass sqlDAL's per-request read cache. Malformed rules, rule-read failures and category cycles deny access; deleted required groups cannot match. Before the table exists, legacy behavior is preserved. A failed schema query is distinguished from an absent table and fails closed. This compatibility fallback is not a substitute for running the migration before activation.

Integration points

Component Behavior
User::canWatchVideo() / canWatchVideoWithAds() Check the prerequisite before a session cache or plugin can grant access.
view/modeYoutube.php Check the requested ID before fallback selection; a denied viewer gets PlaybackAccess::showUnlockPage() (see below). With free playback with ads enabled, or a restricted title allowed through its purchase, resolve that requested title without the catalog/group filter; otherwise the SQL user-group filter stays. The normal playback/payment/password checks still run.
PlayerSkins::getMediaTag() Do not produce a media tag for a denied video.
playListSource Preserve playlist metadata but blank denied media URLs, including MP4 sources. The existing trusted APISecret export remains privileged.
API::get_api_user_can_watch_video() Preserve response fields while applying the prerequisite to plugin status flags.
API catalogs, programs, favorites and watch later Recheck mandatory rules after caches and filter episodes, related/next videos, original media maps and converted MP3 fields. Cached stdClass rows and sparse object collections are supported. The existing privileged APISecret behavior remains.
Preview, autoplay, gallery and VOD fallback infoFromURL.php retains metadata without denied sources; autoplay does not build denied next sources; CustomizeUser::canDownloadVideosFromVideo() checks the prerequisite; when the newest VOD is denied, Live's latestOrLive.php widget shows the newest one of the same selection the viewer passes (latestOrLiveGetPermittedVOD(), one page of 100, suggested first outside a category) and never a forbidden/login page; with none, it gives the same "video not found" response as an empty selection.
FlixHouse API Blank stm_url and return stm_requires_user_group=true with an actionable restriction message. A plain requested users_id is not authentication for this prerequisite. Existing fields remain.
PayPerView checkout and buyPPV() Refuse a restricted movie for a user lacking the required group before payment, unless the opted-in direct-title rental qualifies. Refused page/buy.php and subscribePlanPerUser.php requests with that videos_id show the unlock page when the Streamer has showUnlockPage(), otherwise the 403. Existing provider transaction/savepoint reconciliation remains responsible for already received card funds.
Subscription showPlans.php For a viewer denied by the mandatory rule, reuse the subscription IDs from PlaybackAccess::getUnlockOffers() so the checkout enforces both group requirements. Other viewers, requests without videos_id and older Streamers keep the normal catalog filter.
VideoHLS token issuer Refuse a denied request before the token cache; encrypted tokens carry the authenticated user ID and video ID.
VideoHLS key handler Reject directory traversal before authorizing a video and reading its key; distinguish SQL lookup failure from an unknown filename. Check before the key cache, whitelists and protection-disabled shortcut. A signed, valid matching-video token carries the issuing user's identity; current group membership is rechecked. A denied media request returns JSON HTTP 403 rather than redirecting to HTML login.
view/videoEmbeded.php Check PlaybackAccess::isAllowed() before printing sources: getEmbed() hooks can return early (SecureVideosDirectory whitelist, free playback with ads).
Core playlist JSON endpoints filterVideoRows() removes original playback fields as well as playable VideoUrl/videosURL entries, retaining images, titles and collection fields. Internal playlist/storage helpers remain unchanged.
feed/index.php (RSS, MRSS, Roku, Vizio) Rows whose effective rule is require (PlaybackAccess::isRestricted(), errors count as restricted) are removed for every viewer after the row cache is read, because feed outputs are cached for all viewers. saveRule() calls ObjectYPT::deleteALLCache(), like a category save, so cached Roku/Vizio output is rebuilt.

Unlock page (2026-11-04)

PlaybackAccess::showUnlockPage($videos_id) renders view/playbackAccessUnlock.php (styles in view/css/playbackAccessUnlock.css, dark and theme-independent like PayPerView's ppvHero.css) with HTTP 403 and exits. Bots, JSON requests, missing videos and statuses outside Video::getViewableStatus(true) keep the plain forbiddenPage(). The page prints only metadata (artwork, title, rating, duration, category, plain-text description, trailer) and links; it never builds a source. The trailer button appears only when isValidURL($video['trailer1']) holds, like the other trailer buttons: avideoModalIframe() concatenates the address into src="...", and the setTrailer1() filter (FILTER_VALIDATE_URL) accepts quotes and javascript: addresses. The content is capped at the section width (max-width: 100%, cards minmax(min(250px, 100%), 1fr), overflow-wrap): the docked chat makes the section narrower than Bootstrap's fixed .container, and the section clips overflow.

PlaybackAccess::getUnlockOffers($videos_id) checks the video and owner's active status and lists plans that grant a group of the rule (getRequiredGroups()). The remaining playback permission is satisfied by one of:

  • the viewer's existing AVideoPlugin::userCanWatchVideo() result, evaluated independently of the mandatory prerequisite;
  • an active PPV link to the offered whole plan, obtained through PayPerView::getAllPlansFromVideo(); purchasing that plan permits its linked titles even if visibility groups differ;
  • the viewer's current groups plus the plan's groups reaching a video/category visibility group (UserGroups::getVideosAndCategoriesUserGroups()), matching User::canWatchVideo()'s fallback.

An empty visibility-group list alone does not prove playback is allowed. For a paid-only title without a payment-plugin grant, unrelated memberships are omitted. With title rentals disabled, a viewer who already owns a rental still needs the missing mandatory membership. Existing payment lookup helpers may change listing parameters, so their GET, POST and REQUEST state is restored in finally. No purchase is simulated and no permission/group override is installed in the production request.

Sources: SubscriptionPlansTable::getAllActive() (read with GET, POST and REQUEST listing parameters cleared, then restored exactly, including on failure) and group-based PPV plans (Ppv_plans_has_users_groups::getAllPlansFromUserGroups(), plan status = 'a', all active groups of the plan from getAllFromPPV()). Title rentals are offered only with the opt-in described below. Checkouts: Subscription::getBuyURL($videos_id) + plans_id; PPV subscribePlanPerUser.php?plans_id= + redirectUri, or PayPerView::getBuyURL() when ifNeedToPayRedirectURL is set. The standalone PPV group checkout initializes its access state and uses getRedirectUri(getHomePageURL()) as the payment return link; it does not require a video rental ID. Logged-out visitors see the offers and a Sign In button (previously they were sent straight to login). Plan descriptions are shown as escaped text because PPV plans can be user-written.

The PPV wallet endpoint's standalone-plan branch uses the plan's owner as recipient and the existing split helper. When the plan has no owner it uses YPTWallet::transferBalanceToSiteOwner() and the wallet's configured recipient; a missing recipient remains a failed payment. It no longer reads the owner from a nonexistent live transmission. Inactive or nonpositive-duration standalone plans are refused before debit. PPV offers with a nonpositive price or duration are omitted because the existing checkout cannot sell them. Regression cases in PpvPurchaseRegressionTest.php exercise owned/site-owned plans and refusal of inactive plans; the video-rental and live branches retain their existing recipient logic. Ppv_plans_has_users::save() deletes the buyer's PayPerView::getGroupsFromUser/<users_id> cache (30 s): the unlock page reads it before the purchase, and the wallet path only clears the session cache, so a buyer returning within 30 s was shown the unlock page again.

On local vlu.me, video 614 with only_for_paid=0 was checked by simulating each plan purchase in a rolled-back transaction: exactly the listed plans made User::canWatchVideo() true, with no visibility group, visibility group 5 or visibility group 4. Covered by tests/Security/PlaybackAccessUnlockTest.php; the PPV return link is covered by plugin/PayPerView/tests/PpvUnlockCheckoutTest.php. view/videoEmbeded.php refuses with the plain 403 (iframe), but AVideoPlugin::getEmbed() runs first: with PayPerView's onlyPlayVideosWithPayPerViewActive (as on vlu.me) a denied visitor is redirected to embedTrailer.php, whose Buy button now opens the unlock page.

Headless Chrome review on vlu.me (2026-11-04): widths 320, 360, 390, 414, 768, 992, 1024, 1280, 1440 and 1920, logged in (docked chat) and out, light (default) and dark (netflix) themes, Gallery mode (YouPHPFlix2 not enabled), 0/1/2/4/8 offers, long title/plan names, HTML in a PPV plan name/description. No page overflow, clipped content or nested scroll container after the layout fix; no media URL in the HTML. Every offer opened its checkout; wallet purchases of an ownerless PPV plan (site owner credited) and an owned plan (owner credited, 10% to the site) granted access, returned to the title and played the MP4 (muted, so audio was not checked). Card/PayPal checkouts were not paid.

The paid-only offer mismatch found on 2026-11-04 is corrected by the payment-aware predicate above. tests/playback-access-unlock-mysql-regression.php compares the baseline offer list with real User::canWatchVideo() results after each proposed purchase in 50 isolated executions (10 scenarios, no purchase plus four candidate purchases each). It covers public and paid-only titles, visibility groups, direct PPV grants with different visibility groups, an existing rental, existing visibility membership, inactive links and inactive owners. Every fixture transaction is rolled back. Never call ObjectYPT::deleteALLCache() inside such a transaction: cache-table truncation may implicitly commit it.

On local vlu.me, a Googlebot request returns HTTP 200 with only Bot Found [...] before playback routing, due to the configured stopBotsList in the unchanged objects/include_config.php. This predates the unlock page and is distinct from the unit-tested showUnlockPage() bot branch that delegates to forbiddenPage(). Do not infer a playback-source leak or an unlock-page regression from that bootstrap response.

Known limitations:

  • Native HLS clients. The VideoHLS token reaches a key URI only through downloadProtection.js in the browser, and getToken.json.php requires a Referer. Native players without the session cookie never send it: apps, Chromecast, AirPlay and Roku. canAccessMandatoryRule() does not use VideoHLS::ignore(), so their key requests for a rule-protected encrypted video return 403 even for members.
  • PPV group plans. buyPPV(), Wallet/requestPayment.json.php and page/buy.php refuse a restricted video before resolveUserGroupPlanForVideo(). The unlock page therefore links a group plan to subscribePlanPerUser.php?plans_id= without videos_id: it sells the plan (its groups), not a rental of the title.
  • Server-side HLS conversion (not verified at runtime). m3u8ToMP4() (objects/functionsFFMPEG.php, used by HLS-to-MP4 downloads) makes FFmpeg read the playlist over HTTP with a globalToken, but view/hls.php adds that token only to sub-playlist URIs, not to key URIs. FFmpeg's key request has no session or VideoHLS token, so for an encrypted video under a rule it should be refused even when a member or administrator started the download.
  • Key handler cost. downloadProtection.php now loads the full configuration and opens the database before the key cache on every Streamer with this helper, even without rules.
  • Video moves. A video moved into a restricted category disappears from RSS/MRSS rows immediately. A cached Roku/Vizio output can keep it until that cache expires (12 hours) or a cache clear.
  • Orphan rules. Deleting a video or category leaves its rule row.

Title rentals and per-plan ad scope (2026-11-12)

PayPerView option allowRentalOfGroupRestrictedVideos (default on) lets a rental satisfy a mandatory rule for that title only. getEmptyDataObject() supplies true when the saved configuration has no value for this option; the existing PluginAbstract::getDataObject() merge preserves an explicit saved false. Updating does not rewrite stored plugin JSON. Turn the option off to keep the group-only behavior:

  • PlaybackAccess::isAllowed() falls back, after the group check, to PayPerView::purchaseSatisfiesPlaybackGroupRule() = option on and PayPerView::userPurchasedVideo(): an active rental of the title or a whole-plan purchase (Ppv_plans_has_users::getFromDbFromUserAndPlan()) of an active plan that lists it (PayPerView::getAllPlansFromVideo()). The catalog plan status is checked separately because joined rows expose the link's status. A title rental is read fresh through PPV_Plans_Users_Videos::getFromDbFromUserAndVideoPlan(), must have status = a, and expires against getDatabaseTime(); group-link IDs are never treated as video-link IDs. Admin/owner shortcuts of PPV_Plans_Users_Videos::userCanWatchVideo() are deliberately not used. Older PayPerView releases (method missing) keep the group-only rule.
  • Rentable = PayPerView::canRentGroupRestrictedVideo($videos_id, $ppv_plans_id = null): option on and an active direct link (ppv_plans_has_videos) to an active plan with a positive price and duration. Payment paths pass the plan being bought (buyPPV(), Wallet/requestPayment.json.php, subscribePlanPerUser.php); 0 is never rentable. page/buy.php checks any rental plan.
  • getUnlockOffers() adds type = rental offers (subscribePlanPerUser.php?plans_id=&videos_id=, or PayPerView::getBuyURL() with ifNeedToPayRedirectURL), skipping a plan already offered whole (type = ppv). PlaybackAccess::canRentTitle() exposes rentability to integrations.
  • FlixHouse API: a refused but rentable title reports stm_requires_ppv = true and the subscription-or-PPV message (stm_url stays empty). Group-only categories ($flixGroupOnlyCategories, including After Dark) remain hidden from non-members even when rentals are enabled; direct-title eligibility and category discovery are separate decisions.
  • The legacy direct-card endpoint Stripe/requestPayment.json.php uses the same pre-payment rule as the wallet endpoint, before mapping creation or a provider call. getPaymentDetails($plan, $video, $walletFunding, $users_id) refuses an ineligible restricted selection for shared Stripe/PayPal/BTC checkout preparation. The optional fourth argument defaults to the current user; completeWalletCheckout() passes the actual purchaser explicitly for server-to-server fulfillment.

Subscription option plansThatRemoveAdsOnlyFromTheirVideos (plan IDs, default empty): Subscription::isPaidUser() ignores those plans, so AVideoPlugin::isPaidUser() (GoogleAds_IMA isToShowAds(), ADs, FlixHouse Aniview SSAI) no longer makes their subscribers ad-free site-wide. Subscription::limitedPlanRemovesAdsOnVideo() (valid subscription groups from getGroupsFromUser() ∩ listed plans' groups ∩ the video's visibility/category groups) makes Subscription::showAds() return false on the plan's own videos. FlixHouse shares the opt-in decisions between its VMAP hook and SSAI: scoped members skip ads on covered videos; with the scoped setting present, IMA's paid-user setting still controls site-wide ad-free subscribers. With PPV rentals enabled, a valid title purchase skips ads on that title when PayPerView doNotShowAdsForPaidUsers is on. Legacy users_id requests affect ad selection only, never mandatory playback authorization. Both options off retain the previous FlixHouse ad decisions.

With either new option enabled, GoogleAds_IMA VMAP template keys include the viewer's current ad decision (_ads0/_ads1). Membership/rental eligibility is checked before a cache hit, so a renter cannot inherit a visitor's ads and a visitor cannot inherit an ad-free template. Defaults retain the legacy template key. JSON VMAP and authenticated user/pass requests use the same hooks. GoogleAds_IMA has its own repository and must be updated alongside FlixHouse, Subscription, PayPerView and the Streamer before enabling these options. Covered by tests/Security/VMAPScopedAdsCacheTest.php (skipped when GoogleAds_IMA is absent) and FlixHouse tests/ScopedAdsTest.php; real ad-provider delivery still requires provider/device testing.

Measured on FlixHouse production before the change (2026-11-12): the After Dark rule was [8] (All-Access group 11 refused although its plan grants 11); GoogleAds_IMA doNotShowAdsForPaidUsers hid web ads for any subscriber, including After Dark-only; the app VMAP hid ads only where groups matched. Covered by tests/Security/PlaybackAccessUnlockTest.php, plugin/PayPerView/tests/PpvPurchaseRegressionTest.php, plugin/Subscription/tests/AdFreePlansTest.php and five rental scenarios in tests/playback-access-unlock-mysql-regression.php (the whole rental plan is not compared there because the page sells the title's rental instead).

VideoHLS and PayPerView guard the optional helper with class_exists()/file availability for older Streamer releases. Install the coordinated plugin versions before enabling category rules. Do not broaden this opt-in feature into a rewrite of unconfigured media delivery, Encoder authentication, live RTMP callbacks or the existing external API contracts.

The Encoder is unchanged and may still serve multiple Streamers. Test real source retrieval for any Encoder/native-player/CDN configuration involved in rollout. Files served statically, remote unsigned MP4 URLs, existing downloads and unrelated live-key handlers are not converted into per-request ACL enforcement by this change. Protected HLS requires the actual .key route to use the updated VideoHLS handler.

Administration

The category editor and video editor expose Playback access rules, opening view/playbackAccess.php. Only User::isAdmin() may read/manage this screen or call objects/playbackAccess.save.json.php. Saves require POST and the existing globalToken CSRF mechanism. Validate the scope, target, mode and every selected group; an empty required selection is an error. The screen includes an Intro.js tour, effective rule/source summary and a saved-prerequisite test by user ID. User 0 means visitor even when the tester is an administrator.

Do not automatically convert old visibility groups: their OR semantics and interaction with payment plugins may be intentional. For the FlixHouse case, configure the After Dark category with only its legitimate membership groups, not the general rental group. Review any existing PPV offers/paid entitlements for that category before changing the commercial offering.

Validation

From the Streamer checkout:

php vendor/phpunit/phpunit/phpunit tests/Security/PlaybackAccessTest.php --no-coverage
php vendor/phpunit/phpunit/phpunit tests/Security/PlaybackAccessUnlockTest.php --no-coverage
docker exec -u www-data -e AVIDEO_PLAYBACK_UNLOCK_MYSQL_TESTS=1 -w /var/www/html/AVideo avideo-avideo-1 php tests/playback-access-unlock-mysql-regression.php
php vendor/phpunit/phpunit/phpunit tests/Security --no-coverage
php vendor/phpunit/phpunit/phpunit plugin/PayPerView/tests --no-coverage
docker exec -u www-data -e AVIDEO_PLAYBACK_MYSQL_TESTS=1 -w /var/www/html/AVideo avideo-avideo-1 php tests/playback-access-mysql-regression.php
docker exec -u www-data -e AVIDEO_PLAYBACK_HTTP_TESTS=1 -w /var/www/html/AVideo avideo-avideo-1 php tests/playback-access-http-regression.php

The MySQL test is explicitly restricted to local vlu.me, requires the migration and rolls back its fixture rows. It verifies real PPV rental state, inherited denial, authorized groups, a hand-built signed HLS identity (not a real native-player request), feed restriction, and the real playlistsVideos.json.php response for a visitor (borrowing an existing local video with a playable file). The isolated PHPUnit tests execute the production policy/User/HLS/cache filtering methods with controlled collaborators; they do not bootstrap the installation's real configuration.

Also test real HTTP flows: admin save/reload, empty group validation, non-admin/CSRF refusal, visitor/renter/member/expired member, short and titled video routes, API source filtering, HLS token issuance, a native key request without cookies, and group revocation after warming the key cache. Verify normal PPV advertising behavior separately. Browser/UI tests should cover a light and dark theme and a narrow viewport. Automated tests alone do not prove all Encoder, payment-provider, mobile and CDN integrations.

Local validation on 2026-11-03: the PHP 8.2 full suite completed with 1,552 tests, 76,216 assertions and 75 environment-dependent skips; PayPerView completed 36 tests/182 assertions. PHP 8.5 passed the new playback tests (17/41) and disposable-MySQL installer integration tests (9/237). The full PHP 8.5 suite remains affected by existing test-harness deprecations, including TestCase::assertErrorResponse() and reflection helpers. The local MySQL playback regression and HTTP/browser checks covered source denial and membership revocation; disposable records were removed. A real native-player key request was not covered.

Independent review on 2026-11-03:

  • The PHP 8.2 tests/Security suite passed 742 tests; tests/Unit passed with POSIX-only skips.
  • The local MySQL regression passed with the playlist and feed checks.
  • A temporary rule on local vlu.me removed the title from the HTTP MRSS feed, and the title returned after cleanup.
  • The embed check was not reachable over HTTP there, because the local PayPerView settings redirect visitors to the trailer first.

These results do not validate production deployment, real payment gateways or every native/CDN integration.

Further review of the follow-up changes (2026-11-03)

The additional corrections are uncommitted; inspect both staged and unstaged changes with git diff HEAD, plus the untracked HTTP regression file. No production access, deployment, push or commit was performed during this review.

Confirmed defects corrected:

  • Playlist responses retained videos/linked source fields after only VideoUrl had been filtered. API series, favorites/program/watch-later responses had the same missing check; cached converted MP3 fields needed removal too. A stronger real-MySQL endpoint assertion failed before correction. PlaybackAccess::filterVideoRows() centralizes response filtering without altering internal storage/Encoder helpers.
  • Cached API rows can be stdClass rather than arrays, including sparse collections decoded as objects. The previous filter raised a fatal error; an executing regression reproduced it. Both representations are supported while retaining catalog metadata.
  • Preview and autoplay sources, gallery download permission and Live's latest-VOD fallback missed the prerequisite. Tests execute real preview/autoplay/download code with controlled collaborators; HTTP additionally covers previews with a real local MP4 source.
  • Schema-query failure could be mistaken for an absent migration. HLS video-ID lookup also needed to distinguish a failed query from a filename without a database row. Both now fail closed on lookup errors.
  • HLS resolved the saved rule before consulting the central administrator bypass, blocking an administrator on a malformed rule. The key prerequisite now uses the central decision first; ordinary viewers still fail closed.
  • A request with an unconfigured video's filename/token and key=../<restricted-video>/review returned another video's 16-byte test key over HTTP with status 200. The fix refuses dot-segment paths before lookup/cache/file access; the same test returns 403. Normal nested paths remain valid. Encoder-generated relative key URIs resolve to the expected video directory.

Second independent review (2026-11-04), uncommitted:

  • filterVideoRows() turned a denied row's videos media map into a JSON list ([]) on array rows (playlist, program, favorites, watch later, series episodes), while get_api_video() and the original filter send {}. It now always returns an empty object; sources stays a list.
  • The HTTP regression left the key handler's <client>_<key>_v1.tmp file in the container temp directory. It now uses a run-specific key name and removes and checks those files.
  • latestOrLive.php sent visitors to the login page (HTTP 302 from forbiddenPage()) when the newest VOD was restricted; the widget must always show something playable. It now falls back to the newest permitted VOD; the HTTP regression covers it with a newer restricted title in a disposable category.

tests/playback-access-http-regression.php uses disposable, committed local records because separate HTTP connections cannot read uncommitted fixtures. It copies an existing local MP4 smaller than 10 MiB, uses a random temporary member/password, and removes its account, memberships, playlists, category, rules and media files in finally. It verifies normal key delivery, member token issuance through HTTP, explicitly supplied token delivery without cookies, warm-cache revocation, denied metadata responses across playlist/API routes, and the decoded API catalog cache. This does not test automatic token handoff in real native apps or audiovisual playback.

Performance: a fresh prerequisite evaluation of 20 unconfigured videos in one new category performed 43 prepared query executions in the local database (about 10 ms in that run). This is approximately two queries per distinct video, plus rule/parent reads per distinct category and one schema lookup; repeated checks in the same request reuse request caches. This excludes normal catalog queries and full bootstrap costs and is not a production throughput estimate. saveRule() clears global application caches like a category save; repeated/bulk saves can cause expensive cache warming. Batch lookup and narrower output-cache invalidation are separate design options.

Options for the deliberately deferred decisions above: native clients need an authenticated token handoff (scoped manifest/key URLs or a playback-session broker), not a weaker key check; whole-plan PPV needs a separate plan/Subscription entry point or explicit prospective-grant validation; HLS bootstrap cost needs separate measurement/design; Roku/Vizio needs serialized-item rechecks or a suitable rule/category version in output cache keys; orphan rules need deletion/import lifecycle handling.

Additional conditional route: custom plugin/oncologytube/feed/index.php and index_old.php have their own on-disk XML caches and media generation outside feed/index.php. Source inspection shows the core feed filter does not cover them; their deployed configuration and full HTTP flow were not verified. Review their cache/filter design before enabling mandatory rules where this custom feed is used. This is an ignored, unversioned local plugin checkout here, not one of the three reviewed plugin repositories.

Final checks for this review: PHP lint passed for all 34 changed PHP files; PHP 8.2 Security passed 761 tests / 2,760 assertions; Unit passed 756 tests / 73,535 assertions, with 17 POSIX/environment skips; PayPerView passed 36 tests / 182 assertions. The stronger MySQL regression and the HTTP regression both passed. HTTP exercised real source responses and key bytes; the test member received its token through authenticated HTTP, and a subsequent request explicitly supplied it without cookies. Fixture database records and media directories were checked after cleanup. Billing-provider reconciliation, real subscription renewal/expiry, automatic native-client token delivery, audiovisual playback and external storage/CDN/Encoder integrations remain unverified end to end. The expiry unit fixture represents absence of effective group membership, not a real billing renewal event.

Clone this wiki locally