Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/src/Controllers/MoveDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ protected function handle()
]);

// empty cache & sync site
$this->managerTheme->getCore()->clearCache('full');
$this->managerTheme->getCore()->clearCache('document');

header('Location: index.php?a=3&id=' . $document->getKey() . '&r=9');
}
Expand Down
8 changes: 6 additions & 2 deletions core/src/Core.php
Original file line number Diff line number Diff line change
Expand Up @@ -4626,7 +4626,7 @@ public function getSnippetName()
/**
* Clear the cache of MODX.
*
* @param string $type
* @param string|array $type '' page caches, 'full', 'document' (no opcache reset / compiled views) or a document id
* @param bool $report
* @return void
*/
Expand All @@ -4635,7 +4635,7 @@ public function clearCache($type = '', $report = false)
$cache_dir = $this->bootstrapPath();

/*$this['command.view.clear']->handle();*/
$path = $this['config']['view.compiled'];
$path = $type === 'document' ? '' : $this['config']['view.compiled'];
if ($path) {
foreach ($this['files']->glob("{$path}/*") as $view) {
$this['files']->delete($view);
Expand All @@ -4651,6 +4651,10 @@ public function clearCache($type = '', $report = false)
$sync->setCachepath($cache_dir);
$sync->setReport($report);
$sync->emptyCache();
} elseif ($type === 'document') {
$sync = new Legacy\Cache();
$sync->setCachepath($cache_dir);
$sync->refreshDocumentCache($this);
} elseif (preg_match('@^[1-9]\d*$@', $type)) {
$key = ($this->getConfig('cache_type') == 2) ? $this->makePageCacheKey($type) : $type;
$file_name = "docid_" . $key . "_*.pageCache.php";
Expand Down
135 changes: 106 additions & 29 deletions core/src/Legacy/Cache.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php namespace EvolutionCMS\Legacy;

use EvolutionCMS\Interfaces;
use EvolutionCMS\Models;

/**
Expand Down Expand Up @@ -111,30 +112,13 @@ public function getParents($id, $path = '')
*/
public function emptyCache($evo = null)
{
if (!($evo instanceof Interfaces\CoreInterface)) {
$evo = $GLOBALS['evo'];
}
if (!isset($this->cachePath)) {
$evo->getService('ExceptionHandler')->messageQuit("Cache path not set.");
}
$evo = $this->resolveCore($evo);
\Illuminate\Support\Facades\Cache::flush();
Models\UserSetting::query()->whereIn('setting_name', ['password', 'password_confirmation', 'clearPassword'])->delete();
$files = glob(realpath($this->cachePath) . '/*.pageCache.php');
$filesincache = count($files);
$deletedfiles = [];
while ($file = array_shift($files)) {
$name = basename($file);
clearstatcache();
if (is_file($file)) {
if (unlink($file)) {
$deletedfiles[] = $name;
}
}
}
$opcache_restrict_api = trim(ini_get('opcache.restrict_api'));
$opcache_restrict_api = $opcache_restrict_api && mb_stripos(__FILE__, $opcache_restrict_api) !== 0;
$filesincache = count(glob(realpath($this->cachePath) . '/*.pageCache.php') ?: []);
$deletedfiles = $this->clearPageCache();

if (!$opcache_restrict_api && function_exists('opcache_get_status')) {
if ($this->opcacheApiAllowed() && function_exists('opcache_get_status')) {
$opcache = opcache_get_status();
if (!empty($opcache['opcache_enabled'])) {
opcache_reset();
Expand Down Expand Up @@ -163,6 +147,83 @@ public function emptyCache($evo = null)
}
}

/**
* Document save/publish/move: drops page caches and rebuilds the site cache
* without resetting opcache or touching compiled views.
* @param null|Interfaces\CoreInterface $evo
* @since 3.5.8
*/
public function refreshDocumentCache($evo = null)
{
$evo = $this->resolveCore($evo);
\Illuminate\Support\Facades\Cache::flush();
$this->clearPageCache();
$this->buildCache($evo);
$this->publishTimeConfig();
}

/**
* @return string[] deleted file names
*/
public function clearPageCache(): array
{
$deleted = [];
foreach (glob(realpath($this->cachePath) . '/*.pageCache.php') ?: [] as $file) {
clearstatcache(false, $file);
if (is_file($file) && @unlink($file)) {
$deleted[] = basename($file);
}
}

return $deleted;
}

/**
* @param mixed $evo
* @return Interfaces\CoreInterface
*/
protected function resolveCore($evo)
{
if (!($evo instanceof Interfaces\CoreInterface)) {
$evo = $GLOBALS['evo'];
}
if (!isset($this->cachePath)) {
$evo->getService('ExceptionHandler')->messageQuit("Cache path not set.");
}

return $evo;
}

protected function opcacheApiAllowed(): bool
{
$restrict = trim((string) ini_get('opcache.restrict_api'));

return !($restrict && mb_stripos(__FILE__, $restrict) !== 0);
}

/**
* Writes through a temp file + rename so concurrent readers never include a partial file,
* then drops the stale opcache entry (needed with opcache.validate_timestamps=0).
*/
protected function writeCacheFile(string $filename, string $content): bool
{
$tmp = $filename . '.' . uniqid('', true) . '.tmp';
if (@file_put_contents($tmp, $content) === false) {
return false;
}
if (!@rename($tmp, $filename)) {
@unlink($tmp);
if (@file_put_contents($filename, $content) === false) {
return false;
}
}
if ($this->opcacheApiAllowed() && function_exists('opcache_invalidate')) {
@opcache_invalidate($filename, true);
}

return true;
}

/**
* @param string|int $cacheRefreshTime
*/
Expand All @@ -179,15 +240,10 @@ public function publishTimeConfig($cacheRefreshTime = '')
$content .= '$recent_update=\'' . $this->request_time . '\';' . "\n";
$content .= '$cacheRefreshTime=\'' . $cacheRefreshTime . '\';' . "\n";

$filename = evo()->getSitePublishingFilePath();
if (!$handle = fopen($filename, 'w')) {
exit("Cannot open file ({$filename}");
}

$content .= "\n";

// Write $somecontent to our opened file.
if (fwrite($handle, $content) === false) {
$filename = evo()->getSitePublishingFilePath();
if (!$this->writeCacheFile($filename, $content)) {
exit("Cannot write publishing info file! Make sure the {$filename} and its directory is writable!");
}
}
Expand Down Expand Up @@ -233,6 +289,27 @@ public function getCacheRefreshTime()
* @return boolean success
*/
public function buildCache($evo)
{
// serialise concurrent rebuilds so the last writer always reflects the latest DB state
$lock = @fopen($evo->getSiteCacheFilePath() . '.lock', 'c');
if ($lock) {
flock($lock, LOCK_EX);
}
try {
return $this->writeSiteCache($evo);
} finally {
if ($lock) {
flock($lock, LOCK_UN);
fclose($lock);
}
}
}

/**
* @param Interfaces\CoreInterface $evo
* @return boolean success
*/
protected function writeSiteCache($evo)
{
$content = "<?php\n";

Expand Down Expand Up @@ -383,7 +460,7 @@ public function buildCache($evo)
// invoke OnBeforeCacheUpdate event
$evo->invokeEvent('OnBeforeCacheUpdate');

if (@file_put_contents($filename, $content) === false) {
if (!$this->writeCacheFile($filename, $content)) {
exit("Cannot write $filename! Make sure file or its directory is writable!");
}

Expand Down
157 changes: 157 additions & 0 deletions core/tests/Mocks/CacheRefreshHarness.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

namespace Tests\Mocks;

use Illuminate\Cache\ArrayStore;
use Illuminate\Cache\Repository;
use Illuminate\Container\Container;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Facade;

/**
* Stand-in for the core: only what Legacy\Cache touches while rebuilding the site cache.
*/
class CacheRefreshHarness
{
public array $config = ['server_offset_time' => 0];
public array $events = [];
public string $dir;

public function getConfig($name = '', $default = null)
{
return $this->config[$name] ?? $default;
}

public function parseProperties($props, $name = null, $type = null): array
{
return [];
}

public function invokeEvent($name, $params = [])
{
$this->events[] = $name;
return false;
}

public function getSiteCacheFilePath(): string
{
return $this->dir . '/siteCache.idx.php';
}

public function getSitePublishingFilePath(): string
{
return $this->dir . '/sitePublishing.idx.php';
}

/** Boots Eloquent (with model events) and the Cache facade against the given sqlite database. */
public static function boot(string $database = ':memory:'): Capsule
{
foreach (['IN_MANAGER_MODE', 'IN_INSTALL_MODE'] as $c) {
defined($c) || define($c, false);
}
defined('EVO_API_MODE') || define('EVO_API_MODE', true);
defined('EVO_CLASS') || define('EVO_CLASS', self::class);
require_once dirname(__DIR__, 2) . '/functions/preload.php';

$database === ':memory:' || is_file($database) || touch($database);
$capsule = new Capsule();
$capsule->addConnection(['driver' => 'sqlite', 'database' => $database, 'prefix' => '']);
$capsule->setEventDispatcher(new Dispatcher(new Container()));
$capsule->setAsGlobal();
$capsule->bootEloquent();
Model::setConnectionResolver($capsule->getDatabaseManager());
Model::clearBootedModels();

$container = new Container();
$container->instance('cache', new Repository(new ArrayStore()));
Facade::setFacadeApplication($container);

return $capsule;
}

public static function schema(Capsule $capsule): void
{
$schema = $capsule->getConnection()->getSchemaBuilder();
$schema->create('site_content', function (Blueprint $t) {
$t->increments('id');
$t->string('type', 20)->default('document');
$t->string('contentType', 50)->default('text/html');
$t->string('pagetitle')->default('');
$t->string('alias', 245)->nullable()->default('');
$t->unsignedInteger('published')->default(0);
$t->unsignedInteger('pub_date')->default(0);
$t->unsignedInteger('unpub_date')->default(0);
$t->unsignedInteger('parent')->default(0);
$t->unsignedInteger('isfolder')->default(0);
$t->unsignedInteger('menuindex')->default(0);
$t->unsignedInteger('createdon')->default(0);
$t->unsignedInteger('editedon')->default(0);
$t->unsignedInteger('deleted')->default(0);
$t->unsignedInteger('deletedon')->default(0);
$t->unsignedInteger('alias_visible')->default(1);
});
$schema->create('site_content_closure', function (Blueprint $t) {
$t->increments('closure_id');
$t->unsignedInteger('ancestor');
$t->unsignedInteger('descendant');
$t->unsignedInteger('depth');
});
$schema->create('system_settings', function (Blueprint $t) {
$t->string('setting_name', 50)->primary();
$t->text('setting_value')->nullable();
});
$schema->create('site_htmlsnippets', function (Blueprint $t) {
$t->increments('id');
$t->string('name')->default('');
$t->text('snippet')->nullable();
$t->boolean('disabled')->default(0);
});
foreach (['site_snippets' => 'snippet', 'site_plugins' => 'plugincode'] as $table => $code) {
$schema->create($table, function (Blueprint $t) use ($code) {
$t->increments('id');
$t->string('name')->default('');
$t->text($code)->nullable();
$t->text('properties')->nullable();
$t->string('moduleguid', 32)->default('');
$t->boolean('disabled')->default(0);
});
}
$schema->create('site_modules', function (Blueprint $t) {
$t->increments('id');
$t->string('guid', 32)->default('');
$t->text('properties')->nullable();
});
$schema->create('system_eventnames', function (Blueprint $t) {
$t->increments('id');
$t->string('name', 50)->default('');
});
$schema->create('site_plugin_events', function (Blueprint $t) {
$t->unsignedInteger('pluginid');
$t->unsignedInteger('evtid');
$t->integer('priority')->default(0);
});
$schema->create('user_settings', function (Blueprint $t) {
$t->unsignedInteger('user');
$t->string('setting_name', 50);
$t->text('setting_value')->nullable();
});
$capsule->table('system_settings')->insert([
['setting_name' => 'friendly_urls', 'setting_value' => '1'],
['setting_name' => 'use_alias_path', 'setting_value' => '1'],
]);
}

/** Includes a generated site cache the way Core::recoverySiteCache() does. */
public static function listing(string $file): array
{
$reader = new class {
public $config = [], $aliasListing = [], $documentListing = [], $documentMap = [], $contentTypes = [], $chunkCache = [], $snippetCache = [], $pluginCache = [], $pluginEvent = [];
public function load(string $file): void { include $file; }
};
$reader->load($file);
return ['aliasListing' => $reader->aliasListing, 'documentListing' => $reader->documentListing, 'config' => $reader->config];
}
}
Loading
Loading