Merge pull request #309 from virtualtam/refactor/PageCache

CachedPage: move to a proper file, add tests
This commit is contained in:
VirtualTam 2015-08-13 23:54:26 +02:00
commit a3b1b4ae70
8 changed files with 342 additions and 88 deletions

38
application/Cache.php Normal file
View file

@ -0,0 +1,38 @@
<?php
/**
* Cache utilities
*/
/**
* Purges all cached pages
*
* @param string $pageCacheDir page cache directory
*
* @return mixed an error string if the directory is missing
*/
function purgeCachedPages($pageCacheDir)
{
if (! is_dir($pageCacheDir)) {
$error = 'Cannot purge '.$pageCacheDir.': no directory';
error_log($error);
return $error;
}
array_map('unlink', glob($pageCacheDir.'/*.cache'));
}
/**
* Invalidates caches when the database is changed or the user logs out.
*
* @param string $pageCacheDir page cache directory
*/
function invalidateCaches($pageCacheDir)
{
// Purge cache attached to session.
if (isset($_SESSION['tags'])) {
unset($_SESSION['tags']);
}
// Purge page cache shared by sessions.
purgeCachedPages($pageCacheDir);
}

View file

@ -0,0 +1,63 @@
<?php
/**
* Simple cache system, mainly for the RSS/ATOM feeds
*/
class CachedPage
{
// Directory containing page caches
private $cacheDir;
// Full URL of the page to cache -typically the value returned by pageUrl()
private $url;
// Should this URL be cached (boolean)?
private $shouldBeCached;
// Name of the cache file for this URL
private $filename;
/**
* Creates a new CachedPage
*
* @param string $cacheDir page cache directory
* @param string $url page URL
* @param bool $shouldBeCached whether this page needs to be cached
*/
public function __construct($cacheDir, $url, $shouldBeCached)
{
// TODO: check write access to the cache directory
$this->cacheDir = $cacheDir;
$this->url = $url;
$this->filename = $this->cacheDir.'/'.sha1($url).'.cache';
$this->shouldBeCached = $shouldBeCached;
}
/**
* Returns the cached version of a page, if it exists and should be cached
*
* @return a cached version of the page if it exists, null otherwise
*/
public function cachedVersion()
{
if (!$this->shouldBeCached) {
return null;
}
if (is_file($this->filename)) {
return file_get_contents($this->filename);
}
return null;
}
/**
* Puts a page in the cache
*
* @param string $pageContent XML content to cache
*/
public function cache($pageContent)
{
if (!$this->shouldBeCached) {
return;
}
file_put_contents($this->filename, $pageContent);
}
}

View file

@ -269,8 +269,10 @@ private function _readDB()
/** /**
* Saves the database from memory to disk * Saves the database from memory to disk
*
* @param string $pageCacheDir page cache directory
*/ */
public function savedb() public function savedb($pageCacheDir)
{ {
if (!$this->_loggedIn) { if (!$this->_loggedIn) {
// TODO: raise an Exception instead // TODO: raise an Exception instead
@ -280,7 +282,7 @@ public function savedb()
$this->_datastore, $this->_datastore,
self::$phpPrefix.base64_encode(gzdeflate(serialize($this->_links))).self::$phpSuffix self::$phpPrefix.base64_encode(gzdeflate(serialize($this->_links))).self::$phpSuffix
); );
invalidateCaches(); invalidateCaches($pageCacheDir);
} }
/** /**
@ -439,4 +441,3 @@ public function days()
return $linkDays; return $linkDays;
} }
} }
?>

113
index.php
View file

@ -70,6 +70,8 @@
} }
// Shaarli library // Shaarli library
require_once 'application/Cache.php';
require_once 'application/CachedPage.php';
require_once 'application/LinkDB.php'; require_once 'application/LinkDB.php';
require_once 'application/TimeZone.php'; require_once 'application/TimeZone.php';
require_once 'application/Utils.php'; require_once 'application/Utils.php';
@ -202,63 +204,6 @@ function checkUpdate()
} }
// -----------------------------------------------------------------------------------------------
// Simple cache system (mainly for the RSS/ATOM feeds).
class pageCache
{
private $url; // Full URL of the page to cache (typically the value returned by pageUrl())
private $shouldBeCached; // boolean: Should this url be cached?
private $filename; // Name of the cache file for this url.
/*
$url = URL (typically the value returned by pageUrl())
$shouldBeCached = boolean. If false, the cache will be disabled.
*/
public function __construct($url,$shouldBeCached)
{
$this->url = $url;
$this->filename = $GLOBALS['config']['PAGECACHE'].'/'.sha1($url).'.cache';
$this->shouldBeCached = $shouldBeCached;
}
// If the page should be cached and a cached version exists,
// returns the cached version (otherwise, return null).
public function cachedVersion()
{
if (!$this->shouldBeCached) return null;
if (is_file($this->filename)) { return file_get_contents($this->filename); exit; }
return null;
}
// Put a page in the cache.
public function cache($page)
{
if (!$this->shouldBeCached) return;
file_put_contents($this->filename,$page);
}
// Purge the whole cache.
// (call with pageCache::purgeCache())
public static function purgeCache()
{
if (is_dir($GLOBALS['config']['PAGECACHE']))
{
$handler = opendir($GLOBALS['config']['PAGECACHE']);
if ($handler!==false)
{
while (($filename = readdir($handler))!==false)
{
if (endsWith($filename,'.cache')) { unlink($GLOBALS['config']['PAGECACHE'].'/'.$filename); }
}
closedir($handler);
}
}
}
}
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------
// Log to text file // Log to text file
function logm($message) function logm($message)
@ -718,8 +663,16 @@ function showRSS()
// Cache system // Cache system
$query = $_SERVER["QUERY_STRING"]; $query = $_SERVER["QUERY_STRING"];
$cache = new pageCache(pageUrl(),startsWith($query,'do=rss') && !isLoggedIn()); $cache = new CachedPage(
$cached = $cache->cachedVersion(); if (!empty($cached)) { echo $cached; exit; } $GLOBALS['config']['PAGECACHE'],
pageUrl(),
startsWith($query,'do=rss') && !isLoggedIn()
);
$cached = $cache->cachedVersion();
if (! empty($cached)) {
echo $cached;
exit;
}
// If cached was not found (or not usable), then read the database and build the response: // If cached was not found (or not usable), then read the database and build the response:
$LINKSDB = new LinkDB( $LINKSDB = new LinkDB(
@ -798,11 +751,19 @@ function showATOM()
// Cache system // Cache system
$query = $_SERVER["QUERY_STRING"]; $query = $_SERVER["QUERY_STRING"];
$cache = new pageCache(pageUrl(),startsWith($query,'do=atom') && !isLoggedIn()); $cache = new CachedPage(
$cached = $cache->cachedVersion(); if (!empty($cached)) { echo $cached; exit; } $GLOBALS['config']['PAGECACHE'],
// If cached was not found (or not usable), then read the database and build the response: pageUrl(),
startsWith($query,'do=atom') && !isLoggedIn()
);
$cached = $cache->cachedVersion();
if (!empty($cached)) {
echo $cached;
exit;
}
// Read links from database (and filter private links if used it not logged in). // If cached was not found (or not usable), then read the database and build the response:
// Read links from database (and filter private links if used it not logged in).
$LINKSDB = new LinkDB( $LINKSDB = new LinkDB(
$GLOBALS['config']['DATASTORE'], $GLOBALS['config']['DATASTORE'],
isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI'], isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI'],
@ -884,7 +845,11 @@ function showATOM()
function showDailyRSS() { function showDailyRSS() {
// Cache system // Cache system
$query = $_SERVER["QUERY_STRING"]; $query = $_SERVER["QUERY_STRING"];
$cache = new pageCache(pageUrl(), startsWith($query, 'do=dailyrss') && !isLoggedIn()); $cache = new CachedPage(
$GLOBALS['config']['PAGECACHE'],
pageUrl(),
startsWith($query,'do=dailyrss') && !isLoggedIn()
);
$cached = $cache->cachedVersion(); $cached = $cache->cachedVersion();
if (!empty($cached)) { if (!empty($cached)) {
echo $cached; echo $cached;
@ -1076,7 +1041,7 @@ function renderPage()
// -------- User wants to logout. // -------- User wants to logout.
if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=logout')) if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=logout'))
{ {
invalidateCaches(); invalidateCaches($GLOBALS['config']['PAGECACHE']);
logout(); logout();
header('Location: ?'); header('Location: ?');
exit; exit;
@ -1383,7 +1348,7 @@ function renderPage()
$value['tags']=trim(implode(' ',$tags)); $value['tags']=trim(implode(' ',$tags));
$LINKSDB[$key]=$value; $LINKSDB[$key]=$value;
} }
$LINKSDB->savedb(); // Save to disk. $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']); // Save to disk.
echo '<script>alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?\';</script>'; echo '<script>alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?\';</script>';
exit; exit;
} }
@ -1400,7 +1365,7 @@ function renderPage()
$value['tags']=trim(implode(' ',$tags)); $value['tags']=trim(implode(' ',$tags));
$LINKSDB[$key]=$value; $LINKSDB[$key]=$value;
} }
$LINKSDB->savedb(); // Save to disk. $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']); // Save to disk.
echo '<script>alert("Tag was renamed in '.count($linksToAlter).' links.");document.location=\'?searchtags='.urlencode($_POST['totag']).'\';</script>'; echo '<script>alert("Tag was renamed in '.count($linksToAlter).' links.");document.location=\'?searchtags='.urlencode($_POST['totag']).'\';</script>';
exit; exit;
} }
@ -1429,7 +1394,7 @@ function renderPage()
'linkdate'=>$linkdate,'tags'=>str_replace(',',' ',$tags)); 'linkdate'=>$linkdate,'tags'=>str_replace(',',' ',$tags));
if ($link['title']=='') $link['title']=$link['url']; // If title is empty, use the URL as title. if ($link['title']=='') $link['title']=$link['url']; // If title is empty, use the URL as title.
$LINKSDB[$linkdate] = $link; $LINKSDB[$linkdate] = $link;
$LINKSDB->savedb(); // Save to disk. $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']); // Save to disk.
pubsubhub(); pubsubhub();
// If we are called from the bookmarklet, we must close the popup: // If we are called from the bookmarklet, we must close the popup:
@ -1462,7 +1427,7 @@ function renderPage()
// - we are protected from XSRF by the token. // - we are protected from XSRF by the token.
$linkdate=$_POST['lf_linkdate']; $linkdate=$_POST['lf_linkdate'];
unset($LINKSDB[$linkdate]); unset($LINKSDB[$linkdate]);
$LINKSDB->savedb(); // save to disk $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']); // save to disk
// If we are called from the bookmarklet, we must close the popup: // If we are called from the bookmarklet, we must close the popup:
if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; } if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; }
@ -1751,7 +1716,7 @@ function importFile()
} }
} }
} }
$LINKSDB->savedb(); $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']);
echo '<script>alert("File '.json_encode($filename).' ('.$filesize.' bytes) was successfully processed: '.$import_count.' links imported.");document.location=\'?\';</script>'; echo '<script>alert("File '.json_encode($filename).' ('.$filesize.' bytes) was successfully processed: '.$import_count.' links imported.");document.location=\'?\';</script>';
} }
@ -2386,14 +2351,6 @@ function resizeImage($filepath)
return true; return true;
} }
// Invalidate caches when the database is changed or the user logs out.
// (e.g. tags cache).
function invalidateCaches()
{
unset($_SESSION['tags']); // Purge cache attached to session.
pageCache::purgeCache(); // Purge page cache shared by sessions.
}
try { try {
mergeDeprecatedConfig($GLOBALS, isLoggedIn()); mergeDeprecatedConfig($GLOBALS, isLoggedIn());
} catch(Exception $e) { } catch(Exception $e) {

79
tests/CacheTest.php Normal file
View file

@ -0,0 +1,79 @@
<?php
/**
* Cache tests
*/
// required to access $_SESSION array
session_start();
require_once 'application/Cache.php';
/**
* Unitary tests for cached pages
*/
class CachedTest extends PHPUnit_Framework_TestCase
{
// test cache directory
protected static $testCacheDir = 'tests/dummycache';
// dummy cached file names / content
protected static $pages = array('a', 'toto', 'd7b59c');
/**
* Populate the cache with dummy files
*/
public function setUp()
{
if (! is_dir(self::$testCacheDir)) {
mkdir(self::$testCacheDir);
} else {
array_map('unlink', glob(self::$testCacheDir.'/*'));
}
foreach (self::$pages as $page) {
file_put_contents(self::$testCacheDir.'/'.$page.'.cache', $page);
}
file_put_contents(self::$testCacheDir.'/intru.der', 'ShouldNotBeThere');
}
/**
* Purge cached pages
*/
public function testPurgeCachedPages()
{
purgeCachedPages(self::$testCacheDir);
foreach (self::$pages as $page) {
$this->assertFileNotExists(self::$testCacheDir.'/'.$page.'.cache');
}
$this->assertFileExists(self::$testCacheDir.'/intru.der');
}
/**
* Purge cached pages - missing directory
*/
public function testPurgeCachedPagesMissingDir()
{
$this->assertEquals(
'Cannot purge tests/dummycache_missing: no directory',
purgeCachedPages(self::$testCacheDir.'_missing')
);
}
/**
* Purge cached pages and session cache
*/
public function testInvalidateCaches()
{
$this->assertArrayNotHasKey('tags', $_SESSION);
$_SESSION['tags'] = array('goodbye', 'cruel', 'world');
invalidateCaches(self::$testCacheDir);
foreach (self::$pages as $page) {
$this->assertFileNotExists(self::$testCacheDir.'/'.$page.'.cache');
}
$this->assertArrayNotHasKey('tags', $_SESSION);
}
}

121
tests/CachedPageTest.php Normal file
View file

@ -0,0 +1,121 @@
<?php
/**
* PageCache tests
*/
require_once 'application/CachedPage.php';
/**
* Unitary tests for cached pages
*/
class CachedPageTest extends PHPUnit_Framework_TestCase
{
// test cache directory
protected static $testCacheDir = 'tests/pagecache';
protected static $url = 'http://shaar.li/?do=atom';
protected static $filename;
/**
* Create the cache directory if needed
*/
public static function setUpBeforeClass()
{
if (! is_dir(self::$testCacheDir)) {
mkdir(self::$testCacheDir);
}
self::$filename = self::$testCacheDir.'/'.sha1(self::$url).'.cache';
}
/**
* Reset the page cache
*/
public function setUp()
{
if (file_exists(self::$filename)) {
unlink(self::$filename);
}
}
/**
* Create a new cached page
*/
public function testConstruct()
{
new CachedPage(self::$testCacheDir, '', true);
new CachedPage(self::$testCacheDir, '', false);
new CachedPage(self::$testCacheDir, 'http://shaar.li/?do=rss', true);
new CachedPage(self::$testCacheDir, 'http://shaar.li/?do=atom', false);
}
/**
* Cache a page's content
*/
public function testCache()
{
$page = new CachedPage(self::$testCacheDir, self::$url, true);
$this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>');
$this->assertFileExists(self::$filename);
$this->assertEquals(
'<p>Some content</p>',
file_get_contents(self::$filename)
);
}
/**
* "Cache" a page's content - the page is not to be cached
*/
public function testShouldNotCache()
{
$page = new CachedPage(self::$testCacheDir, self::$url, false);
$this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>');
$this->assertFileNotExists(self::$filename);
}
/**
* Return a page's cached content
*/
public function testCachedVersion()
{
$page = new CachedPage(self::$testCacheDir, self::$url, true);
$this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>');
$this->assertFileExists(self::$filename);
$this->assertEquals(
'<p>Some content</p>',
$page->cachedVersion()
);
}
/**
* Return a page's cached content - the file does not exist
*/
public function testCachedVersionNoFile()
{
$page = new CachedPage(self::$testCacheDir, self::$url, true);
$this->assertFileNotExists(self::$filename);
$this->assertEquals(
null,
$page->cachedVersion()
);
}
/**
* Return a page's cached content - the page is not to be cached
*/
public function testNoCachedVersion()
{
$page = new CachedPage(self::$testCacheDir, self::$url, false);
$this->assertFileNotExists(self::$filename);
$this->assertEquals(
null,
$page->cachedVersion()
);
}
}

View file

@ -3,6 +3,7 @@
* Link datastore tests * Link datastore tests
*/ */
require_once 'application/Cache.php';
require_once 'application/LinkDB.php'; require_once 'application/LinkDB.php';
require_once 'application/Utils.php'; require_once 'application/Utils.php';
require_once 'tests/utils/ReferenceLinkDB.php'; require_once 'tests/utils/ReferenceLinkDB.php';
@ -180,11 +181,7 @@ public function testSaveDB()
'tags'=>'unit test' 'tags'=>'unit test'
); );
$testDB[$link['linkdate']] = $link; $testDB[$link['linkdate']] = $link;
$testDB->savedb('tests');
// TODO: move PageCache to a proper class/file
function invalidateCaches() {}
$testDB->savedb();
$testDB = new LinkDB(self::$testDatastore, true, false); $testDB = new LinkDB(self::$testDatastore, true, false);
$this->assertEquals($dbSize + 1, sizeof($testDB)); $this->assertEquals($dbSize + 1, sizeof($testDB));
@ -514,4 +511,3 @@ public function testFilterFullTextMixed()
); );
} }
} }
?>

View file

@ -125,4 +125,3 @@ public function countPrivateLinks()
return $this->_privateCount; return $this->_privateCount;
} }
} }
?>