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
9 changes: 9 additions & 0 deletions changelog.d/cache-hardener-b1-b2.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- `$cacheSettingsForAction` first-matches like `processAction` and keeps `appendToKey`
- `caches()` with no action throws `Wheels.InvalidArgument` instead of silently becoming `*`
- `cacheActions` / `cachePages` / `cachePartials` / `cacheImages` / `cacheQueries` stay off in development and testing
- Action cache keys include session/user identity so params-only pages do not leak across sessions
- `clearCachableActions` drops this controller's action bodies, not metadata only
- `$clearCache()` clears each category in place and no longer `StructClear`s the parent bucket
- `caches("Foo")` matches action `foo`
- `$getFromCache` returns a stored `false` (or other falsey payload) as a hit. A miss is only absent, expired, or culled. `$isCacheMiss()` reads the last lookup, not the value
- `$addToCache` / `$getFromCache` / `$clearCache` take a named exclusive `wheelsCacheStore` lock
57 changes: 51 additions & 6 deletions vendor/wheels/controller/caching.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ component {
$args(args = arguments, name = "caches", combine = "action/actions");
arguments.action = $listClean(arguments.action);

// When no actions are passed in we assume that all actions should be cacheable and indicate this with a *.
if (!Len(arguments.action)) {
arguments.action = "*";
Throw(type = "Wheels.InvalidArgument", message = "caches() requires one or more actions.");
}

local.actionsArray = ListToArray(arguments.action);
Expand All @@ -42,11 +41,11 @@ component {
* @action Optional. A single action or list of actions to clear. If not provided, clears all cached actions of current controller.
*/
public void function clearCachableActions(string action = "") {
$dropCachedActionBodies(action = arguments.action);
if (!Len(arguments.action)) {
return $clearCachableActions();
}

// Only remove specific actions from the cache list
local.filtered = [];
for (local.i = 1; local.i <= ArrayLen(variables.$class.cachableActions); local.i++) {
local.cachableAction = variables.$class.cachableActions[local.i];
Expand Down Expand Up @@ -75,26 +74,72 @@ component {
* Get cache info, only called from the test suite
*/
public any function $cacheSettingsForAction(required string action) {
local.rv = false;
local.cachableActions = $cachableActions();
local.iEnd = ArrayLen(local.cachableActions);
for (local.i = 1; local.i <= local.iEnd; local.i++) {
if (local.cachableActions[local.i].action == arguments.action || local.cachableActions[local.i].action == "*") {
if (
CompareNoCase(local.cachableActions[local.i].action, arguments.action) == 0
|| local.cachableActions[local.i].action == "*"
) {
local.rv = {};
local.rv.time = local.cachableActions[local.i].time;
local.rv.static = local.cachableActions[local.i].static;
local.rv.appendToKey = StructKeyExists(local.cachableActions[local.i], "appendToKey")
? local.cachableActions[local.i].appendToKey
: "";
return local.rv;
}
}
return local.rv;
return false;
}

/**
* Delete all cache info, only called from the test suite.
*/
public void function $clearCachableActions() {
$dropCachedActionBodies();
ArrayClear(variables.$class.cachableActions);
}

/**
* Drops this controller's action bodies from application.wheels.cache.
* Keys are recorded by $addToCache when category is action.
*/
public void function $dropCachedActionBodies(string action = "") {
if (!StructKeyExists(application.wheels, "cacheActionIndex")) {
return;
}
local.controllerName = variables.$class.name;
if (!StructKeyExists(application.wheels.cacheActionIndex, local.controllerName)) {
return;
}
local.byAction = application.wheels.cacheActionIndex[local.controllerName];
if (!Len(arguments.action)) {
for (local.indexedAction in local.byAction) {
for (local.key in local.byAction[local.indexedAction]) {
$removeFromCache(key = local.key, category = "action");
}
}
StructDelete(application.wheels.cacheActionIndex, local.controllerName);
return;
}
local.keep = {};
for (local.indexedAction in local.byAction) {
if (ListFindNoCase(arguments.action, local.indexedAction)) {
for (local.key in local.byAction[local.indexedAction]) {
$removeFromCache(key = local.key, category = "action");
}
} else {
local.keep[local.indexedAction] = local.byAction[local.indexedAction];
}
}
if (StructIsEmpty(local.keep)) {
StructDelete(application.wheels.cacheActionIndex, local.controllerName);
} else {
application.wheels.cacheActionIndex[local.controllerName] = local.keep;
}
}

/**
* Called when processing a request to see if any actions are cacheable.
*/
Expand Down
5 changes: 3 additions & 2 deletions vendor/wheels/events/init/caching.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@
application.$wheels.cachePlugins = true;
application.$wheels.cacheFileChecking = true;

// Cache settings that are turned off in development mode only.
// Cache settings that are off in development and testing.
application.$wheels.cacheActions = false;
application.$wheels.cacheImages = false;
application.$wheels.cachePages = false;
application.$wheels.cachePartials = false;
application.$wheels.cacheQueries = false;
if (application.$wheels.environment != "development") {
if (!ListFindNoCase("development,testing", application.$wheels.environment)) {
application.$wheels.cacheActions = true;
application.$wheels.cacheImages = true;
application.$wheels.cachePages = true;
application.$wheels.cachePartials = true;
application.$wheels.cacheQueries = true;
}
application.$wheels.cacheActionIndex = {};

// Other caching settings.
application.$wheels.maximumItemsToCache = 5000;
Expand Down
117 changes: 103 additions & 14 deletions vendor/wheels/global/cache.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,40 @@
return Hash(local.rv);
}

/**
* Session/user identity folded into action cache keys so params-only
* pages do not leak across sessions.
*/
public string function $sessionCacheIdentity() {
var identity = "";
try {
if (IsDefined("session.user.id")) {
identity = ToString(session.user.id);
} else if (IsDefined("session.user") && IsSimpleValue(session.user)) {
identity = ToString(session.user);
} else if (IsDefined("session.sessionid")) {
identity = ToString(session.sessionid);
}
} catch (any e) {
}
return identity;
}

/**
* Store key for category=action: hashed key plus session/user identity.
*/
public string function $actionCacheKey(required string key) {
return arguments.key & ":" & $sessionCacheIdentity();
}

/**
* True when the last $getFromCache was a miss (absent, expired, or culled).
* A stored falsey value is a hit. Do not infer miss from the returned value.
*/
public boolean function $isCacheMiss() {
return !IsDefined("request.wheels.cacheLastHit") || !request.wheels.cacheLastHit;
}


/**
* Internal function.
Expand Down Expand Up @@ -96,6 +130,11 @@
numeric time = application.wheels.defaultCacheTime,
string category = "main"
) {
lock name="#application.applicationName#wheelsCacheStore" type="exclusive" timeout="30" {
local.storeKey = arguments.key;
if (arguments.category == "action") {
local.storeKey = $actionCacheKey(arguments.key);
}
local.currentCount = $cacheCount();
if (
application.wheels.cacheCullPercentage > 0
Expand Down Expand Up @@ -141,7 +180,25 @@
} else {
local.cacheItem.value = Duplicate(arguments.value);
}
application.wheels.cache[arguments.category][arguments.key] = local.cacheItem;
application.wheels.cache[arguments.category][local.storeKey] = local.cacheItem;
if (arguments.category == "action" && StructKeyExists(variables, "$class") && StructKeyExists(variables.$class, "name")) {
if (!StructKeyExists(application.wheels, "cacheActionIndex")) {
application.wheels.cacheActionIndex = {};
}
local.owner = variables.$class.name;
local.actionName = "*";
if (StructKeyExists(variables, "params") && IsStruct(variables.params) && StructKeyExists(variables.params, "action")) {
local.actionName = variables.params.action;
}
if (!StructKeyExists(application.wheels.cacheActionIndex, local.owner)) {
application.wheels.cacheActionIndex[local.owner] = {};
}
if (!StructKeyExists(application.wheels.cacheActionIndex[local.owner], local.actionName)) {
application.wheels.cacheActionIndex[local.owner][local.actionName] = {};
}
application.wheels.cacheActionIndex[local.owner][local.actionName][local.storeKey] = true;
}
}
}
}

Expand All @@ -151,20 +208,32 @@
*/
public any function $getFromCache(required string key, string category = "main") {
local.rv = false;
try {
if (StructKeyExists(application.wheels.cache[arguments.category], arguments.key)) {
if (Now() > application.wheels.cache[arguments.category][arguments.key].expiresAt) {
$removeFromCache(key = arguments.key, category = arguments.category);
} else {
if (IsSimpleValue(application.wheels.cache[arguments.category][arguments.key].value)) {
local.rv = application.wheels.cache[arguments.category][arguments.key].value;
local.hit = false;
lock name="#application.applicationName#wheelsCacheStore" type="exclusive" timeout="30" {
try {
local.storeKey = arguments.key;
if (arguments.category == "action") {
local.storeKey = $actionCacheKey(arguments.key);
}
if (StructKeyExists(application.wheels.cache[arguments.category], local.storeKey)) {
if (Now() > application.wheels.cache[arguments.category][local.storeKey].expiresAt) {
$removeFromCache(key = local.storeKey, category = arguments.category);
} else {
local.rv = Duplicate(application.wheels.cache[arguments.category][arguments.key].value);
if (IsSimpleValue(application.wheels.cache[arguments.category][local.storeKey].value)) {
local.rv = application.wheels.cache[arguments.category][local.storeKey].value;
} else {
local.rv = Duplicate(application.wheels.cache[arguments.category][local.storeKey].value);
}
local.hit = true;
}
}
} catch (any e) {
}
} catch (any e) {
}
if (!StructKeyExists(request, "wheels")) {
request.wheels = {};
}
request.wheels.cacheLastHit = local.hit;
return local.rv;
}

Expand Down Expand Up @@ -197,10 +266,30 @@
* Internal function.
*/
public void function $clearCache(string category = "") {
if (Len(arguments.category)) {
StructClear(application.wheels.cache[arguments.category]);
} else {
StructClear(application.wheels.cache);
lock name="#application.applicationName#wheelsCacheStore" type="exclusive" timeout="30" {
if (Len(arguments.category)) {
if (StructKeyExists(application.wheels.cache, arguments.category) && IsStruct(application.wheels.cache[arguments.category])) {
StructClear(application.wheels.cache[arguments.category]);
}
} else {
local.categories = StructKeyArray(application.wheels.cache);
$clearCacheCategories(categories = local.categories);
}
}
}

/**
* Clears each category struct in place. Hoisted so $clearCache() can
* call it from the lock body without a for-loop in a finally-like shape
* that Lucee 7 miscompiles (cross-engine invariant 12).
*/
public void function $clearCacheCategories(required array categories) {
local.iEnd = ArrayLen(arguments.categories);
for (local.i = 1; local.i <= local.iEnd; local.i++) {
local.cacheCategory = arguments.categories[local.i];
if (StructKeyExists(application.wheels.cache, local.cacheCategory) && IsStruct(application.wheels.cache[local.cacheCategory])) {
StructClear(application.wheels.cache[local.cacheCategory]);
}
}
}
</cfscript>
Loading