Merge branch '1.8.x' into 1.x

Conflicts:
	src/Orchard.Web/Modules/Orchard.Indexing/Settings/EditorEvents.cs
	src/Orchard.Web/Themes/TheAdmin/Scripts/admin.js
This commit is contained in:
Sebastien Ros
2014-10-23 20:53:47 -07:00
24 changed files with 193 additions and 112 deletions

View File

@@ -39,6 +39,7 @@ Scenario: HTML markup in any given comment is encoded
And I hit "Submit Comment"
And I am redirected
# because the ToUrlString extension method breaks in this specific (test) environment, the returnUrl is broken...
And I go to "my-blog/my-post"
# And I go to "my-blog/my-post"
Then I should see "This is<br id="bad-anon-br" />a <a href"
And I should not see "<br id="bad-anon-br" />"

View File

@@ -3,7 +3,7 @@
// This code was generated by SpecFlow (http://www.specflow.org/).
// SpecFlow Version:1.9.0.77
// SpecFlow Generator Version:1.9.0.0
// Runtime Version:4.0.30319.34014
// Runtime Version:4.0.30319.0
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -150,11 +150,13 @@ this.ScenarioSetup(scenarioInfo);
testRunner.And("I hit \"Submit Comment\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 40
testRunner.And("I am redirected", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 43
testRunner.Then("I should see \"This is&lt;br id=&quot;bad-anon-br&quot; /&gt;a &lt;a href\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
#line 42
testRunner.And("I go to \"my-blog/my-post\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 44
testRunner.Then("I should see \"This is&lt;br id=&quot;bad-anon-br&quot; /&gt;a &lt;a href\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
#line 45
testRunner.And("I should not see \"<br id=\"bad-anon-br\" />\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 47
#line 48
testRunner.When("I go to \"users/account/logon\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When ");
#line hidden
TechTalk.SpecFlow.Table table5 = new TechTalk.SpecFlow.Table(new string[] {
@@ -166,13 +168,13 @@ this.ScenarioSetup(scenarioInfo);
table5.AddRow(new string[] {
"password",
"6655321"});
#line 48
#line 49
testRunner.And("I fill in", ((string)(null)), table5, "And ");
#line 52
testRunner.And("I hit \"Sign In\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 53
testRunner.And("I am redirected", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
testRunner.And("I hit \"Sign In\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 54
testRunner.And("I am redirected", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 55
testRunner.And("I go to \"admin/settings/comments\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line hidden
TechTalk.SpecFlow.Table table6 = new TechTalk.SpecFlow.Table(new string[] {
@@ -181,17 +183,17 @@ this.ScenarioSetup(scenarioInfo);
table6.AddRow(new string[] {
"CommentSettings.ModerateComments",
"true"});
#line 55
#line 56
testRunner.And("I fill in", ((string)(null)), table6, "And ");
#line 58
testRunner.And("I hit \"Save\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 59
testRunner.And("I am redirected", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
testRunner.And("I hit \"Save\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 60
testRunner.Then("I should see \"Settings updated\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
testRunner.And("I am redirected", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 61
testRunner.When("I go to \"users/account/logoff\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When ");
testRunner.Then("I should see \"Settings updated\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
#line 62
testRunner.When("I go to \"users/account/logoff\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When ");
#line 63
testRunner.And("I go to \"my-blog/my-post\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line hidden
TechTalk.SpecFlow.Table table7 = new TechTalk.SpecFlow.Table(new string[] {
@@ -203,17 +205,17 @@ this.ScenarioSetup(scenarioInfo);
table7.AddRow(new string[] {
"Comments.CommentText",
"This is a moderated comment."});
#line 63
#line 64
testRunner.And("I fill in", ((string)(null)), table7, "And ");
#line 67
testRunner.And("I hit \"Submit Comment\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 68
testRunner.And("I hit \"Submit Comment\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 69
testRunner.And("I am redirected", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 70
testRunner.And("I go to \"my-blog/my-post\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 71
testRunner.Then("I should see \"Hi there\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
testRunner.And("I go to \"my-blog/my-post\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line 72
testRunner.Then("I should see \"Hi there\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
#line 73
testRunner.And("I should not see \"This is a moderated comment\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line hidden
this.ScenarioCleanup();

View File

@@ -237,6 +237,24 @@ namespace Orchard.Tests.DataMigration {
return 3;
}
}
public class FailingDataMigration : DataMigrationImpl {
public override Feature Feature {
get { return new Feature() { Descriptor = new FeatureDescriptor { Id = "Feature4", Extension = new ExtensionDescriptor { Id = "Module4" } } }; }
}
public int Create() {
SchemaBuilder.CreateTable("FOO", table =>
table.Column("Id", DbType.Int32, column =>
column.PrimaryKey().Identity()));
return 1;
}
public int UpdateFrom1() {
throw new Exception();
}
}
public class DataMigrationSimpleBuilder : DataMigrationImpl {
public override Feature Feature {
@@ -471,9 +489,28 @@ Features:
Description: Feature
");
_dataMigrationManager.Update("Feature1");
try {_dataMigrationManager.Update("Feature1"); }
catch (OrchardException) {}
Assert.That(_repository.Table.Count(), Is.EqualTo(0));
_dataMigrationManager.Update("Feature1");
}
[Test]
public void FailingDataMigrationShouldThrowOrchardException() {
Init(new[] { typeof(FailingDataMigration) });
_folders.Manifests.Add("Module4", @"
Name: Module4
Version: 0.1
OrchardVersion: 1
Features:
Feature4:
Description: Feature
");
Assert.Throws<OrchardException>(() => _dataMigrationManager.Update("Feature4"));
}
}
}

View File

@@ -22,8 +22,6 @@ namespace Orchard.Email.Services {
var workContext = _orchardServices.WorkContext;
var smtpSettings = workContext.CurrentSite.As<SmtpSettingsPart>();
var smtpClient = new SmtpClient();
if (smtpSettings == null || !smtpSettings.IsValid()) {
var urlHelper = new UrlHelper(workContext.HttpContext.Request.RequestContext);
var url = urlHelper.Action("Email", "Admin", new {Area = "Settings"});

View File

@@ -5,10 +5,10 @@
@using (Html.BeginFormAntiForgeryPost()) {
Html.ValidationSummary();
<fieldset>
<legend>@T("Choose the types to include in the export file:")</legend>
<ol>
@{var contentTypeIndex = 0;}
<fieldset>
<legend>@T("Choose the types to include in the export file:")</legend>
<ol>
@{var contentTypeIndex = 0;}
@foreach (var contentTypeEntry in Model.ContentTypes) {
<li>
<input type="hidden" value="@Model.ContentTypes[contentTypeIndex].ContentTypeName" name="@Html.NameOf(m => m.ContentTypes[contentTypeIndex].ContentTypeName)"/>
@@ -17,7 +17,7 @@
</li>
contentTypeIndex = contentTypeIndex + 1;
}
</ol>
</ol>
</fieldset>
<hr />
<fieldset>

View File

@@ -12,6 +12,8 @@ namespace Orchard.Indexing.Settings {
private readonly IIndexingTaskManager _indexingTaskManager;
private readonly IContentManager _contentManager;
private const int PageSize = 50;
public EditorEvents(IIndexingTaskManager indexingTaskManager, IContentManager contentManager){
_indexingTaskManager = indexingTaskManager;
_contentManager = contentManager;
@@ -91,13 +93,15 @@ namespace Orchard.Indexing.Settings {
do {
contentItemProcessed = false;
var contentItemsToIndex = _contentManager.Query(VersionOptions.Latest, new [] { type }).Slice(index, 50);
var contentItemsToIndex = _contentManager.Query(VersionOptions.Latest, new [] { type }).Slice(index, PageSize);
foreach (var contentItem in contentItemsToIndex) {
contentItemProcessed = true;
_indexingTaskManager.CreateUpdateIndexTask(contentItem);
}
index += PageSize;
} while (contentItemProcessed);
}
}

View File

@@ -221,7 +221,10 @@ namespace Orchard.Modules.Controllers {
_moduleService.DisableFeatures(enabledFeatures, force == true);
break;
case FeaturesBulkAction.Update:
foreach (var feature in selectedFeatures.Where(x => x.NeedsUpdate)) {
var featuresThatNeedUpdate = _dataMigrationManager.GetFeaturesThatNeedUpdate();
var selectedFeaturesThatNeedUpdate = selectedFeatures.Where(x => featuresThatNeedUpdate.Contains(x.Descriptor.Id));
foreach (var feature in selectedFeaturesThatNeedUpdate) {
var id = feature.Descriptor.Id;
try {
_reportsCoordinator.Register("Data Migration", "Upgrade " + id, "Orchard installation");

View File

@@ -19,10 +19,12 @@ using Orchard.Mvc.Filters;
using Orchard.Services;
using Orchard.Themes;
using Orchard.UI.Admin;
using Orchard.UI.Notify;
using Orchard.Utility.Extensions;
using System.Collections.Specialized;
using Orchard.OutputCache.ViewModels;
using Orchard.UI.Admin.Notification;
using Orchard.DisplayManagement.Shapes;
namespace Orchard.OutputCache.Filters {
public class OutputCacheFilter : FilterProvider, IActionFilter, IResultFilter {
@@ -38,7 +40,6 @@ namespace Orchard.OutputCache.Filters {
private readonly ISignals _signals;
private readonly ShellSettings _shellSettings;
private readonly ICacheControlStrategy _cacheControlStrategy;
private readonly INotificationManager _notificationManager;
TextWriter _originalWriter;
StringWriter _cachingWriter;
@@ -57,9 +58,8 @@ namespace Orchard.OutputCache.Filters {
ICacheService cacheService,
ISignals signals,
ShellSettings shellSettings,
ICacheControlStrategy cacheControlStrategy,
INotificationManager notificationManager
) {
ICacheControlStrategy cacheControlStrategy) {
_cacheManager = cacheManager;
_cacheStorageProvider = cacheStorageProvider;
_tagCache = tagCache;
@@ -71,7 +71,6 @@ namespace Orchard.OutputCache.Filters {
_signals = signals;
_shellSettings = shellSettings;
_cacheControlStrategy = cacheControlStrategy;
_notificationManager = notificationManager;
Logger = NullLogger.Instance;
}
@@ -228,7 +227,8 @@ namespace Orchard.OutputCache.Filters {
var parameters = new Dictionary<string, object>(filterContext.ActionParameters);
foreach (var key in queryString.AllKeys) {
if (key == null) continue;
if (key == null || (_varyQueryStringParameters != null
&& !_varyQueryStringParameters.Contains(key))) continue;
// ignore pages with the RefreshKey
if (String.Equals(RefreshKey, key, StringComparison.OrdinalIgnoreCase)) {
@@ -351,7 +351,9 @@ namespace Orchard.OutputCache.Filters {
}
// don't cache the result if there were some notifications
if (_notificationManager.GetNotifications().Any()) {
var hasNotifications = !String.IsNullOrEmpty(Convert.ToString(filterContext.Controller.TempData["messages"]));
if (hasNotifications) {
Logger.Debug("Not caching: notifications present");
return;
}
@@ -379,10 +381,8 @@ namespace Orchard.OutputCache.Filters {
Logger.Debug("Cache item added: " + _cacheItem.CacheKey);
// remove only the current version of the page
_cacheService.RemoveByTag(_cacheKey);
// add data to cache
// update the cached data
_cacheStorageProvider.Remove(_cacheKey);
_cacheStorageProvider.Set(_cacheKey, _cacheItem);
// add to the tags index
@@ -424,26 +424,23 @@ namespace Orchard.OutputCache.Filters {
Logger.Debug("Redirect on POST");
var redirectUrl = redirectResult.Url;
if (!VirtualPathUtility.IsAbsolute(redirectUrl)) {
var applicationRoot = new UrlHelper(filterContext.HttpContext.Request.RequestContext).MakeAbsolute("/");
if (redirectUrl.StartsWith(applicationRoot, StringComparison.OrdinalIgnoreCase)) {
redirectUrl = "~/" + redirectUrl.Substring(applicationRoot.Length);
redirectUrl = VirtualPathUtility.ToAbsolute(redirectUrl);
}
if (filterContext.HttpContext.Request.IsLocalUrl(redirectUrl)) {
var helper = new UrlHelper(filterContext.HttpContext.Request.RequestContext);
var absolutePath = new Uri(helper.MakeAbsolute(redirectUrl)).AbsolutePath;
// querystring invariant key
var invariantCacheKey = ComputeCacheKey(
_shellSettings.Name,
absolutePath,
() => _workContext.CurrentCulture,
_themeManager.GetRequestTheme(filterContext.RequestContext).Id,
null
);
// remove all cached version of the same page
_cacheService.RemoveByTag(invariantCacheKey);
}
// querystring invariant key
var invariantCacheKey = ComputeCacheKey(
_shellSettings.Name,
redirectUrl,
() => _workContext.CurrentCulture,
_themeManager.GetRequestTheme(filterContext.RequestContext).Id,
null
);
// remove all cached version of the same page
_cacheService.RemoveByTag(invariantCacheKey);
// adding a refresh key so that the redirection doesn't get restored
// from a cached version on a proxy
// this can happen when using public caching, we want to force the
@@ -495,7 +492,6 @@ namespace Orchard.OutputCache.Filters {
response.Cache.SetMaxAge(maxAge);
}
response.Cache.VaryByParams["*"] = true;
response.DisableUserCache();
// keeping this examples for later usage
@@ -510,7 +506,10 @@ namespace Orchard.OutputCache.Filters {
}
}
if (_varyQueryStringParameters != null) {
if (_varyQueryStringParameters == null) {
response.Cache.VaryByParams["*"] = true;
}
else {
foreach (var queryStringParam in _varyQueryStringParameters) {
response.Cache.VaryByParams[queryStringParam] = true;
}
@@ -522,14 +521,7 @@ namespace Orchard.OutputCache.Filters {
}
private string ComputeCacheKey(ControllerContext controllerContext, IEnumerable<KeyValuePair<string, object>> parameters) {
var url = controllerContext.HttpContext.Request.RawUrl;
if (!VirtualPathUtility.IsAbsolute(url)) {
var applicationRoot = new UrlHelper(controllerContext.HttpContext.Request.RequestContext).MakeAbsolute("/");
if (url.StartsWith(applicationRoot, StringComparison.OrdinalIgnoreCase)) {
url = "~/" + url.Substring(applicationRoot.Length);
url = VirtualPathUtility.ToAbsolute(url);
}
}
var url = controllerContext.HttpContext.Request.Url.AbsolutePath;
return ComputeCacheKey(_shellSettings.Name, url, () => _workContext.CurrentCulture, _themeManager.GetRequestTheme(controllerContext.RequestContext).Id, parameters);
}

View File

@@ -40,6 +40,9 @@ namespace Orchard.OutputCache.Services {
foreach(var key in _tagCache.GetTaggedItems(tag)) {
_cacheStorageProvider.Remove(key);
}
// we no longer need the tag entry as the items have been removed
_tagCache.RemoveTag(tag);
}
public IEnumerable<CacheItem> GetCacheItems() {

View File

@@ -37,5 +37,10 @@ namespace Orchard.OutputCache.Services {
return Enumerable.Empty<string>();
}
public void RemoveTag(string tag) {
HashSet<string> set;
_dictionary.TryRemove(tag, out set);
}
}
}

View File

@@ -4,5 +4,6 @@ namespace Orchard.OutputCache.Services {
public interface ITagCache : ISingletonDependency {
void Tag(string tag, params string[] keys);
IEnumerable<string> GetTaggedItems(string tag);
void RemoveTag(string tag);
}
}

View File

@@ -15,6 +15,7 @@
<thead>
<tr>
<th scope="col">@T("Url")</th>
<th scope="col">@T("Cache Key")</th>
<th scope="col">@T("Cached On")</th>
<th scope="col">@T("Cached Until")</th>
<th scope="col">&nbsp;</th>
@@ -23,6 +24,7 @@
@foreach (var cacheItem in Model.CacheItems) {
<tr>
<td><span title="@cacheItem.QueryString">@cacheItem.Url</span></td>
<td>@cacheItem.CacheKey</td>
<td>@Display.DateTimeRelative(DateTimeUtc: cacheItem.CachedOnUtc)</td>
<td>@cacheItem.ValidUntilUtc.ToLocalTime()</td>
<td>@Html.ActionLink(T("Evict").Text, "Evict", new { Area = "Orchard.OutputCache", Controller = "Statistics", cacheKey = cacheItem.CacheKey })</td>

View File

@@ -39,7 +39,7 @@ namespace Orchard.Projections.StandardQueries {
public void Execute(FeedContext context) {
var projectionId = context.ValueProvider.GetValue("projection");
if (projectionId == null)
if (projectionId == null || String.IsNullOrEmpty(projectionId.AttemptedValue))
return;
var limitValue = context.ValueProvider.GetValue("limit");

View File

@@ -38,8 +38,9 @@ namespace Orchard.Templates.Services {
BindingName = "Templates",
Binding = ctx => CoerceHtmlString(_templateService.Execute(
templateResult.Template,
templateResult.Name,
templateResult.Processor, ctx.Value))
templateResult.Name,
templateResult.Processor, ctx.Value)),
ShapeDescriptor = new ShapeDescriptor { ShapeType = shapeType }
};
return true;

View File

@@ -85,7 +85,7 @@ namespace Orchard.Tokens.Providers {
FeedItem<ContentItem> item = feedItem;
context.Response.Contextualize(requestContext => {
description.Value = _tokenizer.Replace(settings.Description, new { Content = item.Item, Text = description.Value });
description.Value = _tokenizer.Replace(settings.Description, new { Content = item.Item, Text = description.Value }, new ReplaceOptions { Encoding = ReplaceOptions.NoEncode });
});
}
}

View File

@@ -15,7 +15,7 @@
<div>
<label for="@Html.FieldIdFor(m => m.Description)">@T("Description")</label>
@Html.TextBoxFor(m => m.Description, new { @class = "tokenized text medium" })
<span class="hint">@T("The description field of the RSS item")</span>
<span class="hint">@T("The description field of the RSS item. The content needs to be raw HTML, i.e. not encoded.")</span>
</div>
<div>
<label for="@Html.FieldIdFor(m => m.Author)">@T("Author")</label>

View File

@@ -64,9 +64,9 @@ namespace Orchard.Users.Controllers {
}
[AlwaysAccessible]
public ActionResult LogOn() {
public ActionResult LogOn(string returnUrl) {
if (_authenticationService.GetAuthenticatedUser() != null)
return Redirect("~/");
return this.RedirectLocal(returnUrl);
var shape = _orchardServices.New.LogOn().Title(T("Log On").Text);
return new ShapeResult(this, shape);

View File

@@ -22,7 +22,7 @@ namespace Orchard.Data.Migration {
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly ITransactionManager _transactionManager;
private List<string> _processedFeatures;
private readonly List<string> _processedFeatures;
public DataMigrationManager(
IEnumerable<IDataMigration> dataMigrations,
@@ -40,6 +40,7 @@ namespace Orchard.Data.Migration {
_processedFeatures = new List<string>();
Logger = NullLogger.Instance;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public ILogger Logger { get; set; }
@@ -118,11 +119,11 @@ namespace Orchard.Data.Migration {
while (lookupTable.ContainsKey(current)) {
try {
Logger.Information("Applying migration for {0} from version {1}", feature, current);
Logger.Information("Applying migration for {0} from version {1}.", feature, current);
current = (int)lookupTable[current].Invoke(migration, new object[0]);
}
catch (Exception ex) {
Logger.Error(ex, "An unexpected error occurred while applying migration on {0} from version {1}", feature, current);
Logger.Error(ex, "An unexpected error occurred while applying migration on {0} from version {1}.", feature, current);
throw;
}
}
@@ -139,15 +140,16 @@ namespace Orchard.Data.Migration {
}
}
catch (Exception e) {
Logger.Error(e, "Error while running migration version {0} for {1}", current, feature);
Logger.Error(e, "Error while running migration version {0} for {1}.", current, feature);
_transactionManager.Cancel();
throw new OrchardException(T("Error while running migration version {0} for {1}.", current, feature), e);
}
}
}
public void Uninstall(string feature) {
Logger.Information("Uninstalling feature: {0}", feature);
Logger.Information("Uninstalling feature: {0}.", feature);
var migrations = GetDataMigrations(feature);

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
@@ -111,6 +112,30 @@ namespace Orchard.Data {
Logger.Debug("Copy {0} {1}", source, target);
var metadata = Session.SessionFactory.GetClassMetadata(typeof (T));
var values = metadata.GetPropertyValues(source, EntityMode.Poco);
//This method is currently only used by StorageVersionFilter<>.Versioning()
//In order to prevent shared references to the same collection instance
//Instances of IList<> need to be copied to a new collection instance
for (var index = 0; index < values.Length; index++) {
var value = values[index];
if (value == null)
continue;
var type = value.GetType();
var isGenericList = type.GetInterfaces()
.Where(i => i.IsGenericType)
.Any(i => i.GetGenericTypeDefinition() == typeof(IList<>));
if(!isGenericList)
continue;
var genericArgument = type.GetGenericArguments().First();
var genericType = typeof(List<>).MakeGenericType(new[] { genericArgument });
var listValues = ((IList)value);
values[index] = Activator.CreateInstance(genericType, new[] { listValues });
}
metadata.SetPropertyValues(target, values, EntityMode.Poco);
}

View File

@@ -12,6 +12,7 @@ namespace Orchard.DisplayManagement.Descriptors.ShapePlacementStrategy {
/// </summary>
public interface IPlacementFileParser : IDependency {
PlacementFile Parse(string virtualPath);
PlacementFile ParseText(string placementText);
}
@@ -37,11 +38,11 @@ namespace Orchard.DisplayManagement.Descriptors.ShapePlacementStrategy {
}
var placementText = _webSiteFolder.ReadFile(virtualPath);
return ParseImplementation(virtualPath, placementText);
return ParseText(placementText);
});
}
private PlacementFile ParseImplementation(string virtualPath, string placementText) {
public PlacementFile ParseText(string placementText) {
if (placementText == null)
return null;