mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-02-09 09:16:41 +08:00
Compare commits
39 Commits
1.10
...
sebros/cac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcc2b2efbc | ||
|
|
9d27e577e0 | ||
|
|
2273d38a7f | ||
|
|
181db18355 | ||
|
|
32bf5ed6c8 | ||
|
|
af7da7963e | ||
|
|
3b7a54f10d | ||
|
|
5e6a1549ad | ||
|
|
447728df03 | ||
|
|
30f163bd39 | ||
|
|
78bab529d2 | ||
|
|
31ed21b061 | ||
|
|
712ff2f487 | ||
|
|
aae9f6270e | ||
|
|
26e2fe9977 | ||
|
|
b1c606becc | ||
|
|
8b8cf0b7e6 | ||
|
|
7effab96a0 | ||
|
|
1b8bcb8a4e | ||
|
|
026acc4aa9 | ||
|
|
ad6a2b96e5 | ||
|
|
72c9f0063b | ||
|
|
a8d4b18f72 | ||
|
|
1907e57902 | ||
|
|
a806ac7487 | ||
|
|
47072f91ae | ||
|
|
bbe4110053 | ||
|
|
5dfda7c0a8 | ||
|
|
89a5953ea3 | ||
|
|
c61666bfdc | ||
|
|
2749f2fa21 | ||
|
|
fe5c8ba233 | ||
|
|
290f8e4594 | ||
|
|
5811598dff | ||
|
|
2a01771a37 | ||
|
|
a1531da11b | ||
|
|
49022cd1b5 | ||
|
|
4cc48a380d | ||
|
|
f3971484b8 |
@@ -25,6 +25,7 @@ Scenario: A new tenant is created
|
||||
And I fill in
|
||||
| name | value |
|
||||
| Name | Scott |
|
||||
| RequestUrlPrefix | scott |
|
||||
And I hit "Save"
|
||||
And I am redirected
|
||||
Then I should see "<h3>Scott\s*</h3>"
|
||||
@@ -37,6 +38,7 @@ Scenario: A new tenant is created with uninitialized state
|
||||
And I fill in
|
||||
| name | value |
|
||||
| Name | Scott |
|
||||
| RequestUrlPrefix | scott |
|
||||
And I hit "Save"
|
||||
And I am redirected
|
||||
Then I should see "<li class="tenant Uninitialized">"
|
||||
@@ -199,4 +201,4 @@ Scenario: Listing tenants from command line
|
||||
And I have tenant "Alpha" on "example.org" as "New-site-name"
|
||||
When I execute >tenant list
|
||||
Then I should see "Name: Alpha"
|
||||
And I should see "Request Url Host: example.org"
|
||||
And I should see "Request Url Host: example.org"
|
||||
|
||||
6
src/Orchard.Specs/MultiTenancy.feature.cs
generated
6
src/Orchard.Specs/MultiTenancy.feature.cs
generated
@@ -129,6 +129,9 @@ this.ScenarioSetup(scenarioInfo);
|
||||
table1.AddRow(new string[] {
|
||||
"Name",
|
||||
"Scott"});
|
||||
table1.AddRow(new string[] {
|
||||
"RequestUrlPrefix",
|
||||
"scott"});
|
||||
#line 25
|
||||
testRunner.And("I fill in", ((string)(null)), table1, "And ");
|
||||
#line 28
|
||||
@@ -163,6 +166,9 @@ this.ScenarioSetup(scenarioInfo);
|
||||
table2.AddRow(new string[] {
|
||||
"Name",
|
||||
"Scott"});
|
||||
table2.AddRow(new string[] {
|
||||
"RequestUrlPrefix",
|
||||
"scott"});
|
||||
#line 37
|
||||
testRunner.And("I fill in", ((string)(null)), table2, "And ");
|
||||
#line 40
|
||||
|
||||
@@ -106,5 +106,14 @@
|
||||
</Properties>
|
||||
</Component>
|
||||
|
||||
<Component Type="Orchard.Environment.DefaultOrchardHost">
|
||||
<Properties>
|
||||
<!-- The number of retries when a tenant can't be loaded -->
|
||||
<Property Name="Retries" Value="1"/>
|
||||
<Property Name="DelayRetries" Value="true"/>
|
||||
|
||||
</Properties>
|
||||
</Component>
|
||||
|
||||
</Components>
|
||||
</HostComponents>
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Orchard.DynamicForms.Drivers {
|
||||
|
||||
var bindingsEditor = context.ShapeFactory.EditorTemplate(TemplateName: "FormBindings", Model: viewModel);
|
||||
|
||||
bindingsEditor.Metadata.Position = "Bindings:10";
|
||||
bindingsEditor.Metadata.Position = "Bindings:20";
|
||||
|
||||
return Editor(context, bindingsEditor);
|
||||
}
|
||||
|
||||
@@ -45,9 +45,9 @@ namespace Orchard.DynamicForms.Drivers {
|
||||
_IsRequired: shape.Checkbox(
|
||||
Id: "IsMandatory",
|
||||
Name: "IsMandatory",
|
||||
Title: "Mandatory",
|
||||
Title: "Required",
|
||||
Value: "true",
|
||||
Description: T("Tick this checkbox to make this check box element mandatory.")),
|
||||
Description: T("Tick this checkbox to make this check box element required.")),
|
||||
_CustomValidationMessage: shape.Textbox(
|
||||
Id: "CustomValidationMessage",
|
||||
Name: "CustomValidationMessage",
|
||||
|
||||
@@ -83,7 +83,6 @@ namespace Orchard.DynamicForms.Drivers {
|
||||
Name: "HtmlEncode",
|
||||
Title: "Html Encode",
|
||||
Value: "true",
|
||||
Checked: true,
|
||||
Description: T("Check this option to automatically HTML encode submitted values to prevent code injection.")),
|
||||
_CreateContent: shape.Checkbox(
|
||||
Id: "CreateContent",
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace Orchard.DynamicForms.Elements {
|
||||
}
|
||||
|
||||
public bool HtmlEncode {
|
||||
get { return this.Retrieve(x => x.HtmlEncode, () => true); }
|
||||
get { return this.Retrieve(x => x.HtmlEncode); }
|
||||
set { this.Store(x => x.HtmlEncode, value); }
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Orchard.DynamicForms.ValidationRules {
|
||||
|
||||
private LocalizedString GetValidationMessage(ValidationContext context) {
|
||||
return String.IsNullOrWhiteSpace(ErrorMessage)
|
||||
? T("{0} is a mandatory field.", context.FieldName)
|
||||
? T("{0} is a required field.", context.FieldName)
|
||||
: T(ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace Orchard.Lists {
|
||||
builder.Describe("ListNavigation").OnDisplaying(context => {
|
||||
var containable = (ContainablePart) context.Shape.ContainablePart;
|
||||
var container = _containerService.Value.GetContainer(containable, VersionOptions.Latest);
|
||||
if (container == null) return;
|
||||
|
||||
var previous = _containerService.Value.Previous(container.Id, containable);
|
||||
var next = _containerService.Value.Next(container.Id, containable);
|
||||
|
||||
@@ -32,4 +34,4 @@ namespace Orchard.Lists {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@using Orchard.ContentManagement;
|
||||
@{
|
||||
Style.Require("jQueryColorBox");
|
||||
Style.Include("nprogress.css", "nprogress.min.css");
|
||||
Style.Include("nprogress.css");
|
||||
Style.Include("common-admin.css", "common-admin.min.css");
|
||||
Style.Include("list-admin.css", "list-admin.min.css");
|
||||
Script.Require("ContentPicker").AtFoot();
|
||||
@@ -39,4 +39,4 @@
|
||||
</div>
|
||||
@if (Model.ListNavigation != null) {
|
||||
@Display(Model.ListNavigation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,21 @@ html.dyn #main ul.features button { display:none; }
|
||||
.features.detail-view .category > ul {
|
||||
border:1px solid #EAEAEA;
|
||||
margin-bottom:2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.features.summary-view .category {
|
||||
overflow:hidden;
|
||||
padding-bottom:1em;
|
||||
}
|
||||
|
||||
.features.summary-view .category > ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.features.summary-view .feature {
|
||||
border:1px solid #EAEAEA;
|
||||
display:block;
|
||||
float:left;
|
||||
height:6em;
|
||||
margin:0 .5% 1% .5%;
|
||||
position:relative;
|
||||
width:32%;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,10 @@ namespace Orchard.MultiTenancy.Controllers {
|
||||
ModelState.AddModelError("Name", T("Invalid tenant name. Must contain characters only and no spaces.").Text);
|
||||
}
|
||||
|
||||
if (!string.Equals(viewModel.Name, "default", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace( viewModel.RequestUrlHost) && string.IsNullOrWhiteSpace(viewModel.RequestUrlPrefix)) {
|
||||
ModelState.AddModelError("RequestUrlHostRequestUrlPrefix", T("RequestUrlHost and RequestUrlPrefix can not be empty at the same time.").Text);
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid) {
|
||||
return View(viewModel);
|
||||
}
|
||||
@@ -139,6 +143,10 @@ namespace Orchard.MultiTenancy.Controllers {
|
||||
if (tenant == null)
|
||||
return HttpNotFound();
|
||||
|
||||
if (!string.Equals(viewModel.Name, "default", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(viewModel.RequestUrlHost) && string.IsNullOrWhiteSpace(viewModel.RequestUrlPrefix)) {
|
||||
ModelState.AddModelError("RequestUrlHostRequestUrlPrefix", T("RequestUrlHost and RequestUrlPrefix can not be empty at the same time.").Text);
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid) {
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Web.Mvc;
|
||||
using System.Web.Routing;
|
||||
@@ -39,10 +39,13 @@ namespace Orchard.OutputCache.Filters {
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly ISignals _signals;
|
||||
private readonly ShellSettings _shellSettings;
|
||||
private readonly IOutputCacheFilterState _state;
|
||||
|
||||
private bool _isDisposed = false;
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
|
||||
public OutputCacheFilter(
|
||||
ICacheManager cacheManager,
|
||||
IOutputCacheStorageProvider cacheStorageProvider,
|
||||
@@ -53,8 +56,9 @@ namespace Orchard.OutputCache.Filters {
|
||||
IClock clock,
|
||||
ICacheService cacheService,
|
||||
ISignals signals,
|
||||
ShellSettings shellSettings) {
|
||||
|
||||
ShellSettings shellSettings,
|
||||
IOutputCacheFilterState state) {
|
||||
_state = state;
|
||||
_cacheManager = cacheManager;
|
||||
_cacheStorageProvider = cacheStorageProvider;
|
||||
_tagCache = tagCache;
|
||||
@@ -71,6 +75,7 @@ namespace Orchard.OutputCache.Filters {
|
||||
|
||||
// State.
|
||||
private CacheSettings _cacheSettings;
|
||||
private CacheRouteConfig _cacheRouteConfig;
|
||||
private DateTime _now;
|
||||
private WorkContext _workContext;
|
||||
private string _cacheKey;
|
||||
@@ -92,11 +97,18 @@ namespace Orchard.OutputCache.Filters {
|
||||
_now = _clock.UtcNow;
|
||||
_workContext = _workContextAccessor.GetContext();
|
||||
|
||||
var configurations = _cacheService.GetRouteConfigs();
|
||||
if (configurations.Any()) {
|
||||
var route = filterContext.Controller.ControllerContext.RouteData.Route;
|
||||
var key = _cacheService.GetRouteDescriptorKey(filterContext.HttpContext, route);
|
||||
_cacheRouteConfig = configurations.FirstOrDefault(c => c.RouteKey == key);
|
||||
}
|
||||
|
||||
if (!RequestIsCacheable(filterContext))
|
||||
return;
|
||||
|
||||
// Computing the cache key after we know that the request is cacheable means that we are only performing this calculation on requests that require it
|
||||
_cacheKey = String.Intern(ComputeCacheKey(filterContext, GetCacheKeyParameters(filterContext)));
|
||||
_cacheKey = ComputeCacheKey(filterContext, GetCacheKeyParameters(filterContext));
|
||||
_invariantCacheKey = ComputeCacheKey(filterContext, null);
|
||||
|
||||
Logger.Debug("Cache key '{0}' was created.", _cacheKey);
|
||||
@@ -109,53 +121,44 @@ namespace Orchard.OutputCache.Filters {
|
||||
if (allowServeFromCache && cacheItem != null) {
|
||||
|
||||
Logger.Debug("Item '{0}' was found in cache.", _cacheKey);
|
||||
|
||||
if (cacheItem.IsValid(_now)) {
|
||||
Logger.Debug("Cached item is valid: {0}", _cacheKey);
|
||||
ServeCachedItem(filterContext, cacheItem);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
Logger.Debug("Cached item is invalid: {0}", _cacheKey);
|
||||
|
||||
// Is the cached item in its grace period?
|
||||
if (cacheItem.IsInGracePeriod(_now)) {
|
||||
|
||||
// Render the content unless another request is already doing so.
|
||||
if (Monitor.TryEnter(_cacheKey)) {
|
||||
// If the cached item is in its grace period
|
||||
// and there is no current renderer, this request renders the new content.
|
||||
if (cacheItem.IsInGracePeriod(_now) &&
|
||||
!_state.Renderers.ContainsKey(_cacheKey)) {
|
||||
Logger.Debug("Item '{0}' is in grace period and not currently being rendered; rendering item...", _cacheKey);
|
||||
BeginRenderItem(filterContext);
|
||||
BeginRenderItem(filterContext, _cacheKey);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Cached item is not yet in its grace period, or is already being
|
||||
// rendered by another request; serve it from cache.
|
||||
Logger.Debug("Serving item '{0}' from cache.", _cacheKey);
|
||||
ServeCachedItem(filterContext, cacheItem);
|
||||
return;
|
||||
}
|
||||
|
||||
// No cached item found, or client doesn't want it; acquire the cache key
|
||||
// lock to render the item.
|
||||
Logger.Debug("Item '{0}' was not found in cache or client refuses it. Acquiring cache key lock...", _cacheKey);
|
||||
if (Monitor.TryEnter(_cacheKey)) {
|
||||
Logger.Debug("Cache key lock for item '{0}' was acquired.", _cacheKey);
|
||||
|
||||
// Item might now have been rendered and cached by another request; if so serve it from cache.
|
||||
if (allowServeFromCache) {
|
||||
cacheItem = GetCacheItem(_cacheKey);
|
||||
if (cacheItem != null) {
|
||||
Logger.Debug("Item '{0}' was now found; releasing cache key lock and serving from cache.", _cacheKey);
|
||||
Monitor.Exit(_cacheKey);
|
||||
else {
|
||||
// Cached item not is in its grace period
|
||||
// or is already being rendered by another request; serve it from cache.
|
||||
Logger.Debug("Cache item is invalid: {0}", _cacheKey);
|
||||
ServeCachedItem(filterContext, cacheItem);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Either we acquired the cache key lock and the item was still not in cache, or
|
||||
// the lock acquisition timed out. In either case render the item.
|
||||
Logger.Debug("Rendering item '{0}'...", _cacheKey);
|
||||
BeginRenderItem(filterContext);
|
||||
|
||||
BeginRenderItem(filterContext, _cacheKey);
|
||||
}
|
||||
catch {
|
||||
// Remember to release the cache key lock in the event of an exception!
|
||||
Logger.Debug("Exception occurred for item '{0}'; releasing any acquired lock.", _cacheKey);
|
||||
ReleaseCacheKeyLock();
|
||||
|
||||
ReleaseWorkers();
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -169,96 +172,91 @@ namespace Orchard.OutputCache.Filters {
|
||||
|
||||
public void OnResultExecuted(ResultExecutedContext filterContext) {
|
||||
|
||||
var captureHandlerIsAttached = false;
|
||||
|
||||
try {
|
||||
|
||||
// This filter is not reentrant (multiple executions within the same request are
|
||||
// not supported) so child actions are ignored completely.
|
||||
if (filterContext.IsChildAction || !_isCachingRequest)
|
||||
// This filter is not reentrant (multiple executions within the same request are
|
||||
// not supported) so child actions are ignored completely.
|
||||
if (filterContext.IsChildAction || !_isCachingRequest)
|
||||
return;
|
||||
|
||||
Logger.Debug("Item '{0}' was rendered.", _cacheKey);
|
||||
Logger.Debug("Item '{0}' was rendered.", _cacheKey);
|
||||
|
||||
// Obtain individual route configuration, if any.
|
||||
CacheRouteConfig configuration = null;
|
||||
var configurations = _cacheService.GetRouteConfigs();
|
||||
if (configurations.Any()) {
|
||||
var route = filterContext.Controller.ControllerContext.RouteData.Route;
|
||||
var key = _cacheService.GetRouteDescriptorKey(filterContext.HttpContext, route);
|
||||
configuration = configurations.FirstOrDefault(c => c.RouteKey == key);
|
||||
|
||||
if (!ResponseIsCacheable(filterContext)) {
|
||||
filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
|
||||
filterContext.HttpContext.Response.Cache.SetNoStore();
|
||||
filterContext.HttpContext.Response.Cache.SetMaxAge(new TimeSpan(0));
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine duration and grace time.
|
||||
var cacheDuration = _cacheRouteConfig != null && _cacheRouteConfig.Duration.HasValue ? _cacheRouteConfig.Duration.Value : CacheSettings.DefaultCacheDuration;
|
||||
var cacheGraceTime = _cacheRouteConfig != null && _cacheRouteConfig.GraceTime.HasValue ? _cacheRouteConfig.GraceTime.Value : CacheSettings.DefaultCacheGraceTime;
|
||||
|
||||
// Include each content item ID as tags for the cache entry.
|
||||
var contentItemIds = _displayedContentItemHandler.GetDisplayed().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToArray();
|
||||
|
||||
// Capture the response output using a custom filter stream.
|
||||
var response = filterContext.HttpContext.Response;
|
||||
var captureStream = new CaptureStream(response.Filter);
|
||||
response.Filter = captureStream;
|
||||
|
||||
// Add ETag header for the newly created item
|
||||
var etag = Guid.NewGuid().ToString("n");
|
||||
if (HttpRuntime.UsingIntegratedPipeline) {
|
||||
if (response.Headers.Get("ETag") == null) {
|
||||
response.Headers["ETag"] = etag;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ResponseIsCacheable(filterContext, configuration)) {
|
||||
filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
|
||||
filterContext.HttpContext.Response.Cache.SetNoStore();
|
||||
filterContext.HttpContext.Response.Cache.SetMaxAge(new TimeSpan(0));
|
||||
return;
|
||||
}
|
||||
captureStream.Captured += (output) => {
|
||||
try {
|
||||
// Since this is a callback any call to injected dependencies can result in an Autofac exception: "Instances
|
||||
// cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it has already been disposed."
|
||||
// To prevent access to the original lifetime scope a new work context scope should be created here and dependencies
|
||||
// should be resolved from it.
|
||||
|
||||
// Determine duration and grace time.
|
||||
var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : CacheSettings.DefaultCacheDuration;
|
||||
var cacheGraceTime = configuration != null && configuration.GraceTime.HasValue ? configuration.GraceTime.Value : CacheSettings.DefaultCacheGraceTime;
|
||||
using (var scope = _workContextAccessor.CreateWorkContextScope()) {
|
||||
var cacheItem = new CacheItem() {
|
||||
CachedOnUtc = _now,
|
||||
Duration = cacheDuration,
|
||||
GraceTime = cacheGraceTime,
|
||||
Output = output,
|
||||
ContentType = response.ContentType,
|
||||
QueryString = filterContext.HttpContext.Request.Url.Query,
|
||||
CacheKey = _cacheKey,
|
||||
InvariantCacheKey = _invariantCacheKey,
|
||||
Url = filterContext.HttpContext.Request.Url.AbsolutePath,
|
||||
Tenant = scope.Resolve<ShellSettings>().Name,
|
||||
StatusCode = response.StatusCode,
|
||||
Tags = new[] { _invariantCacheKey }.Union(contentItemIds).ToArray(),
|
||||
ETag = etag
|
||||
};
|
||||
|
||||
// Include each content item ID as tags for the cache entry.
|
||||
var contentItemIds = _displayedContentItemHandler.GetDisplayed().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToArray();
|
||||
// Notifying any awaiting thread that the content is ready
|
||||
// for this page
|
||||
TaskCompletionSource<CacheItem> tcs;
|
||||
if (_state.Renderers.TryRemove(_cacheKey, out tcs)) {
|
||||
Logger.Debug("Page rendered, notifying awaiters: {0}", _cacheKey);
|
||||
tcs.SetResult(cacheItem);
|
||||
}
|
||||
|
||||
// Capture the response output using a custom filter stream.
|
||||
var response = filterContext.HttpContext.Response;
|
||||
var captureStream = new CaptureStream(response.Filter);
|
||||
response.Filter = captureStream;
|
||||
captureStream.Captured += (output) => {
|
||||
try {
|
||||
// Since this is a callback any call to injected dependencies can result in an Autofac exception: "Instances
|
||||
// cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it has already been disposed."
|
||||
// To prevent access to the original lifetime scope a new work context scope should be created here and dependencies
|
||||
// should be resolved from it.
|
||||
// Write the rendered item to the cache.
|
||||
var cacheStorageProvider = scope.Resolve<IOutputCacheStorageProvider>();
|
||||
cacheStorageProvider.Set(_cacheKey, cacheItem);
|
||||
|
||||
using (var scope = _workContextAccessor.CreateWorkContextScope()) {
|
||||
var cacheItem = new CacheItem() {
|
||||
CachedOnUtc = _now,
|
||||
Duration = cacheDuration,
|
||||
GraceTime = cacheGraceTime,
|
||||
Output = output,
|
||||
ContentType = response.ContentType,
|
||||
QueryString = filterContext.HttpContext.Request.Url.Query,
|
||||
CacheKey = _cacheKey,
|
||||
InvariantCacheKey = _invariantCacheKey,
|
||||
Url = filterContext.HttpContext.Request.Url.AbsolutePath,
|
||||
Tenant = scope.Resolve<ShellSettings>().Name,
|
||||
StatusCode = response.StatusCode,
|
||||
Tags = new[] { _invariantCacheKey }.Union(contentItemIds).ToArray()
|
||||
};
|
||||
Logger.Debug("Item '{0}' was written to cache.", _cacheKey);
|
||||
|
||||
// Write the rendered item to the cache.
|
||||
var cacheStorageProvider = scope.Resolve<IOutputCacheStorageProvider>();
|
||||
cacheStorageProvider.Remove(_cacheKey);
|
||||
cacheStorageProvider.Set(_cacheKey, cacheItem);
|
||||
|
||||
Logger.Debug("Item '{0}' was written to cache.", _cacheKey);
|
||||
|
||||
// Also add the item tags to the tag cache.
|
||||
var tagCache = scope.Resolve<ITagCache>();
|
||||
foreach (var tag in cacheItem.Tags) {
|
||||
tagCache.Tag(tag, _cacheKey);
|
||||
}
|
||||
// Also add the item tags to the tag cache.
|
||||
var tagCache = scope.Resolve<ITagCache>();
|
||||
foreach (var tag in cacheItem.Tags) {
|
||||
tagCache.Tag(tag, _cacheKey);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Always release the cache key lock when the request ends.
|
||||
ReleaseCacheKeyLock();
|
||||
}
|
||||
};
|
||||
|
||||
captureHandlerIsAttached = true;
|
||||
}
|
||||
finally {
|
||||
// If the response filter stream capture handler was attached then we'll trust
|
||||
// it to release the cache key lock at some point in the future when the stream
|
||||
// is flushed; otherwise we'll make sure we'll release it here.
|
||||
if (!captureHandlerIsAttached)
|
||||
ReleaseCacheKeyLock();
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Release the renderer task in case something happened
|
||||
ReleaseWorkers();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual bool RequestIsCacheable(ActionExecutingContext filterContext) {
|
||||
@@ -312,6 +310,12 @@ namespace Orchard.OutputCache.Filters {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't cache if individual route configuration says no.
|
||||
if (_cacheRouteConfig != null && _cacheRouteConfig.Duration == 0) {
|
||||
Logger.Debug("Request for item '{0}' ignored because route is configured to not be cached.", itemDescriptor);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ignore requests with the refresh key on the query string.
|
||||
foreach (var key in filterContext.RequestContext.HttpContext.Request.QueryString.AllKeys) {
|
||||
if (String.Equals(_refreshKey, key, StringComparison.OrdinalIgnoreCase)) {
|
||||
@@ -323,7 +327,7 @@ namespace Orchard.OutputCache.Filters {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual bool ResponseIsCacheable(ResultExecutedContext filterContext, CacheRouteConfig configuration) {
|
||||
protected virtual bool ResponseIsCacheable(ResultExecutedContext filterContext) {
|
||||
|
||||
if (filterContext.HttpContext.Request.Url == null) {
|
||||
return false;
|
||||
@@ -335,12 +339,6 @@ namespace Orchard.OutputCache.Filters {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't cache in individual route configuration says no.
|
||||
if (configuration != null && configuration.Duration == 0) {
|
||||
Logger.Debug("Response for item '{0}' will not be cached because route is configured to not be cached.", _cacheKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't cache if request created notifications.
|
||||
var hasNotifications = !String.IsNullOrEmpty(Convert.ToString(filterContext.Controller.TempData["messages"]));
|
||||
if (hasNotifications) {
|
||||
@@ -456,7 +454,7 @@ namespace Orchard.OutputCache.Filters {
|
||||
|
||||
private CacheSettings CacheSettings {
|
||||
get {
|
||||
return _cacheSettings ?? (_cacheSettings = _cacheManager.Get(CacheSettings.CacheKey, true, context => {
|
||||
return _cacheSettings ?? (_cacheSettings = _cacheManager.Get(CacheSettings.CacheKey, context => {
|
||||
context.Monitor(_signals.When(CacheSettings.CacheKey));
|
||||
return new CacheSettings(_workContext.CurrentSite.As<CacheSettingsPart>());
|
||||
}));
|
||||
@@ -465,6 +463,7 @@ namespace Orchard.OutputCache.Filters {
|
||||
|
||||
private void ServeCachedItem(ActionExecutingContext filterContext, CacheItem cacheItem) {
|
||||
var response = filterContext.HttpContext.Response;
|
||||
var request = filterContext.HttpContext.Request;
|
||||
|
||||
// Fix for missing charset in response headers
|
||||
response.Charset = response.Charset;
|
||||
@@ -474,23 +473,68 @@ namespace Orchard.OutputCache.Filters {
|
||||
response.AddHeader("X-Cached-On", cacheItem.CachedOnUtc.ToString("r"));
|
||||
response.AddHeader("X-Cached-Until", cacheItem.ValidUntilUtc.ToString("r"));
|
||||
}
|
||||
|
||||
|
||||
// Shorcut action execution.
|
||||
filterContext.Result = new FileContentResult(cacheItem.Output, cacheItem.ContentType);
|
||||
|
||||
response.StatusCode = cacheItem.StatusCode;
|
||||
|
||||
// Add ETag header
|
||||
if (HttpRuntime.UsingIntegratedPipeline && response.Headers.Get("ETag") == null) {
|
||||
response.Headers["ETag"] = cacheItem.ETag;
|
||||
}
|
||||
|
||||
// Check ETag in request
|
||||
// https://www.w3.org/2005/MWI/BPWG/techs/CachingWithETag.html
|
||||
var etag = request.Headers["If-None-Match"];
|
||||
if (!String.IsNullOrEmpty(etag)) {
|
||||
if (String.Equals(etag, cacheItem.ETag, StringComparison.Ordinal)) {
|
||||
// ETag matches the cached item, we return a 304
|
||||
filterContext.Result = new HttpStatusCodeResult(HttpStatusCode.NotModified);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ApplyCacheControl(response);
|
||||
}
|
||||
|
||||
private void BeginRenderItem(ActionExecutingContext filterContext) {
|
||||
|
||||
private void BeginRenderItem(ActionExecutingContext filterContext, string key) {
|
||||
var response = filterContext.HttpContext.Response;
|
||||
CacheItem cacheItem = null;
|
||||
|
||||
ApplyCacheControl(response);
|
||||
// Tries to acquire a lock to render an item, or wait for the
|
||||
// actual render to finish an get the CachedItem directly
|
||||
TaskCompletionSource<CacheItem> tcs;
|
||||
if (!_state.Renderers.TryGetValue(key, out tcs)) {
|
||||
Logger.Debug("Acquired processing role: {0}", _cacheKey);
|
||||
|
||||
// Remember that we should intercept the rendered response output.
|
||||
_isCachingRequest = true;
|
||||
tcs = new TaskCompletionSource<CacheItem>();
|
||||
|
||||
// If the key could not be added it means another thread got there first,
|
||||
// and in this case they will both try to render the page which is fine.
|
||||
// The chances are very low however as even at it needs to happen between two
|
||||
// statements, even at a high RPS it would be uncommon.
|
||||
_state.Renderers.TryAdd(key, tcs);
|
||||
}
|
||||
else {
|
||||
Logger.Debug("Waiting for processed page: {0}", _cacheKey);
|
||||
|
||||
// We got a task completion source that will have the CacheItem once finished
|
||||
cacheItem = tcs.Task.Result;
|
||||
}
|
||||
|
||||
// the cache item can be null if an error occured while rendering
|
||||
if (cacheItem != null) {
|
||||
Logger.Debug("Serving result: {0}", _cacheKey);
|
||||
ServeCachedItem(filterContext, cacheItem);
|
||||
}
|
||||
else {
|
||||
Logger.Debug("Processing page: {0}", _cacheKey);
|
||||
|
||||
ApplyCacheControl(response);
|
||||
|
||||
// Remember that we should intercept the rendered response output.
|
||||
_isCachingRequest = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyCacheControl(HttpResponseBase response) {
|
||||
@@ -509,15 +553,6 @@ namespace Orchard.OutputCache.Filters {
|
||||
// response.DisableKernelCache();
|
||||
// response.Cache.SetOmitVaryStar(true);
|
||||
|
||||
// An ETag is a string that uniquely identifies a specific version of a component.
|
||||
// We use the cache item to detect if it's a new one.
|
||||
if (HttpRuntime.UsingIntegratedPipeline) {
|
||||
if (response.Headers.Get("ETag") == null) {
|
||||
// What is the point of GetHashCode() of a newly generated item? /DanielStolt
|
||||
response.Cache.SetETag(new CacheItem().GetHashCode().ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
if (CacheSettings.VaryByQueryStringParameters == null) {
|
||||
response.Cache.VaryByParams["*"] = true;
|
||||
}
|
||||
@@ -531,15 +566,7 @@ namespace Orchard.OutputCache.Filters {
|
||||
response.Cache.VaryByHeaders[varyRequestHeader] = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseCacheKeyLock() {
|
||||
if (_cacheKey != null && Monitor.IsEntered(_cacheKey)) {
|
||||
Logger.Debug("Releasing cache key lock for item '{0}'.", _cacheKey);
|
||||
Monitor.Exit(_cacheKey);
|
||||
_cacheKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected virtual bool IsIgnoredUrl(string url, IEnumerable<string> ignoredUrls) {
|
||||
if (ignoredUrls == null || !ignoredUrls.Any()) {
|
||||
return false;
|
||||
@@ -586,15 +613,53 @@ namespace Orchard.OutputCache.Filters {
|
||||
}
|
||||
|
||||
protected virtual CacheItem GetCacheItem(string key) {
|
||||
try {
|
||||
var cacheItem = _cacheStorageProvider.GetCacheItem(key);
|
||||
return cacheItem;
|
||||
}
|
||||
catch (Exception e) {
|
||||
Logger.Error(e, "An unexpected error occured while reading a cache entry");
|
||||
}
|
||||
TaskCompletionSource<CacheItem> tcs;
|
||||
if (!_state.Cachers.TryGetValue(key, out tcs)) {
|
||||
tcs = new TaskCompletionSource<CacheItem>();
|
||||
|
||||
return null;
|
||||
// If the key could not be added it means another thread got there first,
|
||||
// and in this case they will both try to get the cache which is fine.
|
||||
// The chances are very low however as even at it needs to happen between two
|
||||
// statements, even at a high RPS it would be uncommon.
|
||||
_state.Cachers.TryAdd(key, tcs);
|
||||
|
||||
Logger.Debug("Fetching cache item: {0}", _cacheKey);
|
||||
|
||||
try {
|
||||
var cacheItem = _cacheStorageProvider.GetCacheItem(key);
|
||||
if (_state.Cachers.TryRemove(key, out tcs)) {
|
||||
tcs.SetResult(cacheItem);
|
||||
}
|
||||
return cacheItem;
|
||||
}
|
||||
catch (Exception e) {
|
||||
Logger.Error(e, "An unexpected error occured while reading a cache entry");
|
||||
if (_state.Cachers.TryRemove(key, out tcs)) {
|
||||
tcs.SetResult(null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Logger.Debug("Awaiting cache worker: {0}", _cacheKey);
|
||||
|
||||
// We got a task completion source that will have the CacheItem once finished
|
||||
return tcs.Task.Result;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseWorkers() {
|
||||
if (_cacheKey != null) {
|
||||
TaskCompletionSource<CacheItem> tcs;
|
||||
|
||||
if (_state.Renderers.TryRemove(_cacheKey, out tcs)) {
|
||||
tcs.SetResult(null);
|
||||
}
|
||||
|
||||
if (_state.Cachers.TryRemove(_cacheKey, out tcs)) {
|
||||
tcs.SetResult(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
@@ -608,9 +673,7 @@ namespace Orchard.OutputCache.Filters {
|
||||
// Free other state (managed objects).
|
||||
}
|
||||
|
||||
if (_cacheKey != null && Monitor.IsEntered(_cacheKey)) {
|
||||
Monitor.Exit(_cacheKey);
|
||||
}
|
||||
ReleaseWorkers();
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
@@ -626,4 +689,4 @@ namespace Orchard.OutputCache.Filters {
|
||||
public class ViewDataContainer : IViewDataContainer {
|
||||
public ViewDataDictionary ViewData { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace Orchard.OutputCache.Models {
|
||||
[Serializable]
|
||||
public class CacheItem {
|
||||
// used for serialization compatibility
|
||||
public static readonly string Version = "1";
|
||||
public static readonly string Version = "2";
|
||||
|
||||
public DateTime CachedOnUtc { get; set; }
|
||||
public int Duration { get; set; }
|
||||
@@ -18,6 +18,7 @@ namespace Orchard.OutputCache.Models {
|
||||
public string Tenant { get; set; }
|
||||
public int StatusCode { get; set; }
|
||||
public string[] Tags { get; set; }
|
||||
public string ETag { get; set; }
|
||||
|
||||
public int ValidFor {
|
||||
get { return Duration; }
|
||||
|
||||
@@ -115,11 +115,13 @@
|
||||
<Compile Include="Services\DefaultTagCache.cs" />
|
||||
<Compile Include="Services\ICacheControlStrategy.cs" />
|
||||
<Compile Include="Services\IDisplayedContentItemHandler.cs" />
|
||||
<Compile Include="Services\IOutputCacheFilterState.cs" />
|
||||
<Compile Include="Services\ITagCache.cs" />
|
||||
<Compile Include="Services\DefaultCacheStorageProvider.cs" />
|
||||
<Compile Include="Services\ICacheService.cs" />
|
||||
<Compile Include="Services\IOutputCacheStorageProvider.cs" />
|
||||
<Compile Include="Models\CacheSettings.cs" />
|
||||
<Compile Include="Services\OutputCacheFilterState.cs" />
|
||||
<Compile Include="ViewModels\StatisticsViewModel.cs" />
|
||||
<Compile Include="ViewModels\IndexViewModel.cs" />
|
||||
<Compile Include="Models\CacheRouteConfig.cs" />
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace Orchard.OutputCache.Services {
|
||||
}
|
||||
|
||||
public IEnumerable<CacheRouteConfig> GetRouteConfigs() {
|
||||
return _cacheManager.Get(RouteConfigsCacheKey, true,
|
||||
return _cacheManager.Get(RouteConfigsCacheKey,
|
||||
ctx => {
|
||||
ctx.Monitor(_signals.When(RouteConfigsCacheKey));
|
||||
return _repository.Fetch(c => true).Select(c => new CacheRouteConfig { RouteKey = c.RouteKey, Duration = c.Duration, GraceTime = c.GraceTime }).ToReadOnlyCollection();
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard.Environment.Configuration;
|
||||
using Orchard.Logging;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
public class DefaultCacheStorageProvider : IOutputCacheStorageProvider {
|
||||
@@ -15,7 +16,12 @@ namespace Orchard.OutputCache.Services {
|
||||
_tenantName = shellSettings.Name;
|
||||
}
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public void Set(string key, CacheItem cacheItem) {
|
||||
Logger.Debug("Set {0}", key);
|
||||
|
||||
_workContext.HttpContext.Cache.Remove(key);
|
||||
_workContext.HttpContext.Cache.Add(
|
||||
key,
|
||||
cacheItem,
|
||||
@@ -27,10 +33,14 @@ namespace Orchard.OutputCache.Services {
|
||||
}
|
||||
|
||||
public void Remove(string key) {
|
||||
Logger.Debug("Remove {0}", key);
|
||||
|
||||
_workContext.HttpContext.Cache.Remove(key);
|
||||
}
|
||||
|
||||
public void RemoveAll() {
|
||||
Logger.Debug("RemoveAll");
|
||||
|
||||
var items = GetCacheItems(0, 100).ToList();
|
||||
while (items.Any()) {
|
||||
foreach (var item in items) {
|
||||
@@ -41,10 +51,14 @@ namespace Orchard.OutputCache.Services {
|
||||
}
|
||||
|
||||
public CacheItem GetCacheItem(string key) {
|
||||
Logger.Debug("GetCacheItem {0}", key);
|
||||
|
||||
return _workContext.HttpContext.Cache.Get(key) as CacheItem;
|
||||
}
|
||||
|
||||
public IEnumerable<CacheItem> GetCacheItems(int skip, int count) {
|
||||
Logger.Debug("GetCacheItems");
|
||||
|
||||
// the ASP.NET cache can also contain other types of items
|
||||
return _workContext.HttpContext.Cache.AsParallel()
|
||||
.Cast<DictionaryEntry>()
|
||||
@@ -56,6 +70,8 @@ namespace Orchard.OutputCache.Services {
|
||||
}
|
||||
|
||||
public int GetCacheItemsCount() {
|
||||
Logger.Debug("GetCacheItemsCount");
|
||||
|
||||
return _workContext.HttpContext.Cache.AsParallel()
|
||||
.Cast<DictionaryEntry>()
|
||||
.Select(x => x.Value)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Tasks;
|
||||
using Orchard.OutputCache.Models;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
public interface IOutputCacheFilterState : ISingletonDependency {
|
||||
|
||||
/// <summary>
|
||||
/// Tasks that are currently rendering pages
|
||||
/// </summary>
|
||||
ConcurrentDictionary<string, TaskCompletionSource<CacheItem>> Renderers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tasks that are currently retrieving cached items
|
||||
/// </summary>
|
||||
ConcurrentDictionary<string, TaskCompletionSource<CacheItem>> Cachers { get; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Tasks;
|
||||
using Orchard.OutputCache.Models;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
public class OutputCacheFilterState : IOutputCacheFilterState {
|
||||
public OutputCacheFilterState() {
|
||||
Cachers = new ConcurrentDictionary<string, TaskCompletionSource<CacheItem>>();
|
||||
Renderers = new ConcurrentDictionary<string, TaskCompletionSource<CacheItem>>();
|
||||
}
|
||||
|
||||
public ConcurrentDictionary<string, TaskCompletionSource<CacheItem>> Cachers { get; private set; }
|
||||
|
||||
public ConcurrentDictionary<string, TaskCompletionSource<CacheItem>> Renderers { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
float:left;
|
||||
}
|
||||
.extensionName.installed {
|
||||
background: url("images/installed.gif") no-repeat 0px 8px #fff;
|
||||
background: url("images/installed.gif") no-repeat 0px 8px;
|
||||
padding:0 0 0 60px;
|
||||
}
|
||||
.contentItems .related {
|
||||
|
||||
@@ -251,7 +251,7 @@ namespace Orchard.Projections.FilterEditors.Forms {
|
||||
case DateTimeOperator.LessThan:
|
||||
return T("{0} is less than {1}{2}", fieldName, value, T(valueUnit));
|
||||
case DateTimeOperator.LessThanEquals:
|
||||
return T("{0} is less or equal than {1}{2}", fieldName, value, T(valueUnit));
|
||||
return T("{0} is less than or equal to {1}{2}", fieldName, value, T(valueUnit));
|
||||
case DateTimeOperator.Equals:
|
||||
return T("{0} equals {1}{2}", fieldName, value, T(valueUnit));
|
||||
case DateTimeOperator.NotEquals:
|
||||
@@ -259,7 +259,7 @@ namespace Orchard.Projections.FilterEditors.Forms {
|
||||
case DateTimeOperator.GreaterThan:
|
||||
return T("{0} is greater than {1}{2}", fieldName, value, T(valueUnit));
|
||||
case DateTimeOperator.GreaterThanEquals:
|
||||
return T("{0} is greater or equal than {1}{2}", fieldName, value, T(valueUnit));
|
||||
return T("{0} is greater than or equal to {1}{2}", fieldName, value, T(valueUnit));
|
||||
case DateTimeOperator.Between:
|
||||
return T("{0} is between {1}{2} and {3}{4}", fieldName, min, T(minUnit), max, T(maxUnit));
|
||||
case DateTimeOperator.NotBetween:
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace Orchard.Projections.FilterEditors.Forms {
|
||||
case NumericOperator.LessThan:
|
||||
return T("{0} is less than {1}", fieldName, value);
|
||||
case NumericOperator.LessThanEquals:
|
||||
return T("{0} is less or equal than {1}", fieldName, value);
|
||||
return T("{0} is less than or equal to {1}", fieldName, value);
|
||||
case NumericOperator.Equals:
|
||||
return T("{0} equals {1}", fieldName, value);
|
||||
case NumericOperator.NotEquals:
|
||||
@@ -136,7 +136,7 @@ namespace Orchard.Projections.FilterEditors.Forms {
|
||||
case NumericOperator.GreaterThan:
|
||||
return T("{0} is greater than {1}", fieldName, value);
|
||||
case NumericOperator.GreaterThanEquals:
|
||||
return T("{0} is greater or equal than {1}", fieldName, value);
|
||||
return T("{0} is greater than or equal to {1}", fieldName, value);
|
||||
case NumericOperator.Between:
|
||||
return T("{0} is between {1} and {2}", fieldName, min, max);
|
||||
case NumericOperator.NotBetween:
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace Orchard.Taxonomies.Controllers {
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult Delete(int id) {
|
||||
if (!Services.Authorizer.Authorize(Permissions.CreateTaxonomy, T("Couldn't delete taxonomy")))
|
||||
if (!Services.Authorizer.Authorize(Permissions.ManageTaxonomies, T("Couldn't delete taxonomy")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
var taxonomy = _taxonomyService.GetTaxonomy(id);
|
||||
|
||||
@@ -10,6 +10,7 @@ using Orchard.UI.Admin;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.UI.Notify;
|
||||
using Orchard.Taxonomies.Helpers;
|
||||
using Orchard.Mvc.Html;
|
||||
|
||||
namespace Orchard.Taxonomies.Controllers {
|
||||
[ValidateInput(false), Admin]
|
||||
@@ -44,15 +45,24 @@ namespace Orchard.Taxonomies.Controllers {
|
||||
var checkedEntries = viewModel.Terms.Where(t => t.IsChecked).ToList();
|
||||
switch (viewModel.BulkAction) {
|
||||
case TermsAdminIndexBulkAction.None:
|
||||
Services.Notifier.Information(T("No action selected."));
|
||||
break;
|
||||
case TermsAdminIndexBulkAction.Delete:
|
||||
if (!Services.Authorizer.Authorize(Permissions.ManageTerms, T("Couldn't delete term")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
if(!checkedEntries.Any()) {
|
||||
Services.Notifier.Information(T("No terms selected."));
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var entry in checkedEntries) {
|
||||
var term = _taxonomyService.GetTerm(entry.Id);
|
||||
_taxonomyService.DeleteTerm(term);
|
||||
}
|
||||
|
||||
Services.Notifier.Information(T.Plural("{0} term has been removed.", "{0} terms have been removed.", checkedEntries.Count));
|
||||
|
||||
break;
|
||||
case TermsAdminIndexBulkAction.Merge:
|
||||
if (!Services.Authorizer.Authorize(Permissions.ManageTerms, T("Couldn't delete term")))
|
||||
@@ -74,8 +84,6 @@ namespace Orchard.Taxonomies.Controllers {
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
Services.Notifier.Information(T("{0} term have been removed.", checkedEntries.Count));
|
||||
|
||||
return RedirectToAction("Index", new { taxonomyId = viewModel.TaxonomyId });
|
||||
}
|
||||
|
||||
@@ -181,7 +189,7 @@ namespace Orchard.Taxonomies.Controllers {
|
||||
|
||||
public ActionResult Edit(int id) {
|
||||
|
||||
if (!Services.Authorizer.Authorize(Permissions.ManageTerms, T("Not allowed to manage taxonomies")))
|
||||
if (!Services.Authorizer.Authorize(Permissions.ManageTerms, T("Not allowed to manage terms")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
var term = _taxonomyService.GetTerm(id);
|
||||
@@ -194,7 +202,7 @@ namespace Orchard.Taxonomies.Controllers {
|
||||
|
||||
[HttpPost, ActionName("Edit")]
|
||||
public ActionResult EditPost(int id) {
|
||||
if (!Services.Authorizer.Authorize(Permissions.ManageTaxonomies, T("Couldn't edit taxonomy")))
|
||||
if (!Services.Authorizer.Authorize(Permissions.ManageTerms, T("Couldn't edit term")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
var term = _taxonomyService.GetTerm(id);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Drivers;
|
||||
using Orchard.Core.Title.Models;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Templates.Models;
|
||||
using System.Linq;
|
||||
|
||||
namespace Orchard.Templates.Drivers {
|
||||
public class TitlePartDriver : ContentPartDriver<TitlePart> {
|
||||
private readonly IContentManager _contentManager;
|
||||
|
||||
public Localizer T { get; set; }
|
||||
|
||||
public TitlePartDriver(IContentManager contentManager) {
|
||||
_contentManager = contentManager;
|
||||
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
protected override DriverResult Editor(TitlePart part, IUpdateModel updater, dynamic shapeHelper) {
|
||||
if (!part.ContentItem.Has<ShapePart>()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
updater.TryUpdateModel(part, Prefix, null, null);
|
||||
|
||||
// We need to query for the content type names because querying for content parts has no effect on the database side.
|
||||
var contentTypesWithShapePart = _contentManager
|
||||
.GetContentTypeDefinitions()
|
||||
.Where(typeDefinition => typeDefinition.Parts.Any(partDefinition => partDefinition.PartDefinition.Name == "ShapePart"))
|
||||
.Select(typeDefinition => typeDefinition.Name);
|
||||
|
||||
// If ShapePart is only dynamically added to this content type or even this content item then we won't find
|
||||
// a corresponding content type definition, so using the current content type too.
|
||||
contentTypesWithShapePart = contentTypesWithShapePart.Union(new[] { part.ContentItem.ContentType });
|
||||
|
||||
var existingShapeCount = _contentManager
|
||||
.Query(VersionOptions.Latest, contentTypesWithShapePart.ToArray())
|
||||
.Where<TitlePartRecord>(record => record.Title == part.Title && record.ContentItemRecord.Id != part.ContentItem.Id)
|
||||
.Count();
|
||||
|
||||
if (existingShapeCount > 0) {
|
||||
updater.AddModelError("ShapeNameAlreadyExists", T("A template with the given name already exists."));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,7 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="AdminMenu.cs" />
|
||||
<Compile Include="Controllers\AdminController.cs" />
|
||||
<Compile Include="Drivers\TitlePartDriver.cs" />
|
||||
<Compile Include="Helpers\StringExtensions.cs" />
|
||||
<Compile Include="Handlers\ShapePartHandler.cs" />
|
||||
<Compile Include="Drivers\ShapePartDriver.cs" />
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Orchard.Widgets.RuleEngine {
|
||||
public bool Matches(string expression) {
|
||||
var evaluator = _evaluators.FirstOrDefault();
|
||||
if (evaluator == null) {
|
||||
throw new OrchardException(T("There are currently not scripting engine enabled"));
|
||||
throw new OrchardException(T("There are currently no scripting engines enabled"));
|
||||
}
|
||||
|
||||
var result = evaluator.Evaluate(expression, new List<IGlobalMethodProvider> { new GlobalMethodProvider(this) });
|
||||
|
||||
@@ -60,12 +60,18 @@ namespace Orchard.DisplayManagement.Descriptors.ShapeAttributeStrategy {
|
||||
var output = new HtmlStringWriter();
|
||||
var arguments = methodInfo.GetParameters()
|
||||
.Select(parameter => BindParameter(displayContext, parameter, output));
|
||||
|
||||
var returnValue = methodInfo.Invoke(serviceInstance, arguments.ToArray());
|
||||
if (methodInfo.ReturnType != typeof(void)) {
|
||||
output.Write(CoerceHtmlString(returnValue));
|
||||
try {
|
||||
var returnValue = methodInfo.Invoke(serviceInstance, arguments.ToArray());
|
||||
if (methodInfo.ReturnType != typeof(void)) {
|
||||
output.Write(CoerceHtmlString(returnValue));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
catch(TargetInvocationException e) {
|
||||
// Throwing a TIE here will probably kill the web process
|
||||
// in Azure. For unknown reasons.
|
||||
throw e.InnerException;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private static IHtmlString CoerceHtmlString(object invoke) {
|
||||
|
||||
@@ -14,6 +14,7 @@ using Orchard.Localization;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Utility.Extensions;
|
||||
using Orchard.Exceptions;
|
||||
using System.Threading;
|
||||
|
||||
namespace Orchard.Environment {
|
||||
// All the event handlers that DefaultOrchardHost implements have to be declared in OrchardStarter
|
||||
@@ -32,6 +33,10 @@ namespace Orchard.Environment {
|
||||
private IEnumerable<ShellContext> _shellContexts;
|
||||
|
||||
private readonly ContextState<IList<ShellSettings>> _tenantsToRestart;
|
||||
public int Retries { get; set; }
|
||||
|
||||
public bool DelayRetries { get; set; }
|
||||
|
||||
public DefaultOrchardHost(
|
||||
IShellSettingsManager shellSettingsManager,
|
||||
IShellContextFactory shellContextFactory,
|
||||
@@ -137,16 +142,32 @@ namespace Orchard.Environment {
|
||||
// load all tenants, and activate their shell
|
||||
if (allSettings.Any()) {
|
||||
Parallel.ForEach(allSettings, settings => {
|
||||
try {
|
||||
var context = CreateShellContext(settings);
|
||||
ActivateShell(context);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
if (ex.IsFatal()) {
|
||||
throw;
|
||||
}
|
||||
Logger.Error(ex, "A tenant could not be started: " + settings.Name);
|
||||
for (var i = 0; i <= Retries; i++) {
|
||||
|
||||
// Not the first attempt, wait for a while ...
|
||||
if (DelayRetries && i > 0) {
|
||||
// Waits for i^2 which means 1, 2, 4, 8 ... seconds
|
||||
Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(i, 2)));
|
||||
}
|
||||
|
||||
bool failed = false;
|
||||
try {
|
||||
var context = CreateShellContext(settings);
|
||||
ActivateShell(context);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// An exception at this point is always fatal as it literally kills the
|
||||
// tenant. What is more fatal than something that kills you?
|
||||
Logger.Error(ex, "A tenant could not be started: " + settings.Name + " Attempt number: " + i);
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if(failed && i == Retries) {
|
||||
Logger.Fatal("A tenant could not be started: {0} after {1} retries.", settings.Name, Retries);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (_processingEngine.AreTasksPending()) {
|
||||
Logger.Debug("Processing pending task after activate Shell");
|
||||
_processingEngine.ExecuteNextTask();
|
||||
|
||||
Reference in New Issue
Block a user