diff --git a/docs/hooks.txt b/docs/hooks.txt index 3b0ded9da93..1a93cbe4110 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -3529,6 +3529,23 @@ $title: Title object being checked against $user: Current user object &$whitelisted: Boolean value of whether this title is whitelisted +'HtmlCacheUpdaterAppendUrls': Declare extra URLs to purge from HTTP caches. +Use $mode to decide whether to gather all related URLs or only those affected by +a re-render of the same content. For example, after a direct revision to the +content the history page will need to be purged. However when re-rendering after +a cascading change from a template, only URLs that render content need purging. +The $mode will be either HtmlCacheUpdater::PURGE_URLS_LINKSUPDATE_ONLY or 0. +$title: Title object for the page being purged. +$mode: Integer. +&$append: Append URLs relating to $title. + +'HtmlCacheUpdaterVaryUrls': Add variants of URLs to purge from HTTP caches. +Extensions that provide site-wide variants of all URLs, such as by serving from +an alternate domain or path, can use this hook to append alternative URLs for +each url in $canonical. +$urls: Canonical URLs. +&$append: Append alternative URLs for $urls. + 'TitleSquidURLs': Called to determine which URLs to purge from HTTP caches. $title: Title object to purge &$urls: An array of URLs to purge from the caches, to be manipulated. diff --git a/includes/Title.php b/includes/Title.php index 74caae940a8..54c77efc5d5 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -3551,28 +3551,14 @@ class Title implements LinkTarget, IDBAccessObject { } /** - * Get a list of URLs to purge from the CDN cache when this - * page changes + * Get a list of URLs to purge from the CDN cache when this page changes. * + * @deprecated 1.35 Use HtmlCacheUpdater * @return string[] Array of String the URLs */ public function getCdnUrls() { - $urls = [ - $this->getInternalURL(), - $this->getInternalURL( 'action=history' ) - ]; - - // If we are looking at a css/js user subpage, purge the action=raw. - if ( $this->isUserJsConfigPage() ) { - $urls[] = $this->getInternalURL( 'action=raw&ctype=text/javascript' ); - } elseif ( $this->isUserJsonConfigPage() ) { - $urls[] = $this->getInternalURL( 'action=raw&ctype=application/json' ); - } elseif ( $this->isUserCssConfigPage() ) { - $urls[] = $this->getInternalURL( 'action=raw&ctype=text/css' ); - } - - Hooks::run( 'TitleSquidURLs', [ $this, &$urls ] ); - return $urls; + $htmlCache = MediaWikiServices::getInstance()->getHtmlCacheUpdater(); + return $htmlCache->getUrls( $this ); } /** @@ -3580,8 +3566,8 @@ class Title implements LinkTarget, IDBAccessObject { * @deprecated 1.35 Use HtmlCacheUpdater */ public function purgeSquid() { - $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater(); - $hcu->purgeTitleUrls( $this, $hcu::PURGE_INTENT_TXROUND_REFLECTED ); + $htmlCache = MediaWikiServices::getInstance()->getHtmlCacheUpdater(); + $htmlCache->purgeTitleUrls( $this, $htmlCache::PURGE_INTENT_TXROUND_REFLECTED ); } /** diff --git a/includes/cache/HtmlCacheUpdater.php b/includes/cache/HtmlCacheUpdater.php index 5d1fc8c14ea..8c0a31472d0 100644 --- a/includes/cache/HtmlCacheUpdater.php +++ b/includes/cache/HtmlCacheUpdater.php @@ -54,6 +54,16 @@ class HtmlCacheUpdater { */ public const PURGE_INTENT_TXROUND_REFLECTED = self::PURGE_PRESEND | self::PURGE_REBOUND; + /** + * Reduce set of URLs to be purged to only those that may be affected by + * change propagation from LinksUpdate (e.g. after a used template was edited). + * + * Specifically, this means URLs only affected by direct revision edits, + * will not be purged. + * @var int + */ + public const PURGE_URLS_LINKSUPDATE_ONLY = 4; + /** * @param int $reboundDelay $wgCdnReboundPurgeDelay * @param bool $useFileCache $wgUseFileCache @@ -64,6 +74,15 @@ class HtmlCacheUpdater { $this->useFileCache = $useFileCache; } + /** + * @param int $flags Bit field + * @param int $flag Constant to check for + * @return bool If $flags contains $flag + */ + private function fieldHasFlag( $flags, $flag ) { + return ( ( $flags & $flag ) === $flag ); + } + /** * Purge the CDN for a URL or list of URLs * @@ -74,12 +93,12 @@ class HtmlCacheUpdater { public function purgeUrls( $urls, $flags = self::PURGE_PRESEND ) { $urls = is_string( $urls ) ? [ $urls ] : $urls; - $reboundDelay = ( ( $flags & self::PURGE_REBOUND ) == self::PURGE_REBOUND ) + $reboundDelay = $this->fieldHasFlag( $flags, self::PURGE_REBOUND ) ? $this->reboundDelay : 0; // no second purge $update = new CdnCacheUpdate( $urls, [ 'reboundDelay' => $reboundDelay ] ); - if ( ( $flags & self::PURGE_PRESEND ) == self::PURGE_PRESEND ) { + if ( $this->fieldHasFlag( $flags, self::PURGE_PRESEND ) ) { DeferredUpdates::addUpdate( $update, DeferredUpdates::PRESEND ); } else { $update->doUpdate(); @@ -101,7 +120,7 @@ class HtmlCacheUpdater { if ( $this->useFileCache ) { $update = HtmlFileCacheUpdate::newFromTitles( $titles ); - if ( ( $flags & self::PURGE_PRESEND ) == self::PURGE_PRESEND ) { + if ( $this->fieldHasFlag( $flags, self::PURGE_PRESEND ) ) { DeferredUpdates::addUpdate( $update, DeferredUpdates::PRESEND ); } else { $update->doUpdate(); @@ -115,4 +134,53 @@ class HtmlCacheUpdater { } $this->purgeUrls( $urls, $flags ); } + + /** + * Get a list of URLs to purge from the CDN cache when this page changes. + * + * @param Title $title + * @param int $flags Bit field of `PURGE_URLS_*` class constants (optional). + * @return string[] URLs + */ + public function getUrls( Title $title, int $flags = 0 ) : array { + // These urls are affected both by direct revisions as well, + // as re-rendering of the same content during a LinksUpdate. + $urls = [ + $title->getInternalURL() + ]; + // Language variant page views are currently not cached + // and thus not purged (T250511). + + // These urls are only affected by direct revisions, and do not require + // purging when a LinksUpdate merely rerenders the same content. + // This exists to avoid large amounts of redundant PURGE traffic (T250261). + if ( !$this->fieldHasFlag( $flags, self::PURGE_URLS_LINKSUPDATE_ONLY ) ) { + $urls[] = $title->getInternalURL( 'action=history' ); + + // Canonical action=raw URLs for user config pages + if ( $title->isUserJsConfigPage() ) { + $urls[] = $title->getInternalURL( 'action=raw&ctype=text/javascript' ); + } elseif ( $title->isUserJsonConfigPage() ) { + $urls[] = $title->getInternalURL( 'action=raw&ctype=application/json' ); + } elseif ( $title->isUserCssConfigPage() ) { + $urls[] = $title->getInternalURL( 'action=raw&ctype=text/css' ); + } + } + + // Extensions may add novel ways to access this content + $append = []; + $mode = $flags & self::PURGE_URLS_LINKSUPDATE_ONLY; + Hooks::run( 'HtmlCacheUpdaterAppendUrls', [ $title, $mode, &$append ] ); + $urls = array_merge( $urls, $append ); + + // Extensions may add novel ways to access the site overall + $append = []; + Hooks::run( 'HtmlCacheUpdaterVaryUrls', [ $urls, &$append ] ); + $urls = array_merge( $urls, $append ); + + // Legacy. TODO: Deprecate this + Hooks::run( 'TitleSquidURLs', [ $title, &$urls ] ); + + return $urls; + } } diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php index f49ecf9e748..913112af842 100644 --- a/tests/phpunit/includes/TitleTest.php +++ b/tests/phpunit/includes/TitleTest.php @@ -1723,15 +1723,5 @@ class TitleTest extends MediaWikiTestCase { Title::makeTitle( NS_MAIN, 'Example' )->getCdnUrls(), 'article' ); - - $this->assertEquals( - [ - 'https://example.org/wiki/User:Example/foo.js', - 'https://example.org/w/index.php?title=User:Example/foo.js&action=history', - 'https://example.org/w/index.php?title=User:Example/foo.js&action=raw&ctype=text/javascript', - ], - Title::makeTitle( NS_USER, 'Example/foo.js' )->getCdnUrls(), - 'user config page' - ); } } diff --git a/tests/phpunit/unit/includes/cache/HtmlCacheUpdaterTest.php b/tests/phpunit/unit/includes/cache/HtmlCacheUpdaterTest.php new file mode 100644 index 00000000000..40b1927d05b --- /dev/null +++ b/tests/phpunit/unit/includes/cache/HtmlCacheUpdaterTest.php @@ -0,0 +1,47 @@ +createMock( Title::class ); + $title->method( 'getInternalURL' )->will( $this->returnCallback( function ( $query = '' ) { + return 'https://test/?title=Example' . ( $query !== '' ? "&$query" : '' ); + } ) ); + + $this->assertEquals( + [ + 'https://test/?title=Example', + 'https://test/?title=Example&action=history', + ], + $htmlCache->getUrls( $title ), + 'all urls for an article' + ); + $this->assertEquals( + [ + 'https://test/?title=Example', + ], + $htmlCache->getUrls( $title, $htmlCache::PURGE_URLS_LINKSUPDATE_ONLY ), + 'linkupdate urls for an article' + ); + + $title = $this->createMock( Title::class ); + $title->method( 'getInternalURL' )->will( $this->returnCallback( function ( $query = '' ) { + return 'https://test/?title=User:Example/foo.js' . ( $query !== '' ? "&$query" : '' ); + } ) ); + $title->method( 'isUserJsConfigPage' )->willReturn( true ); + $this->assertEquals( + [ + 'https://test/?title=User:Example/foo.js', + 'https://test/?title=User:Example/foo.js&action=history', + 'https://test/?title=User:Example/foo.js&action=raw&ctype=text/javascript', + ], + $htmlCache->getUrls( $title ), + 'all urls for a user js page' + ); + } +}