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 demos/compareJSON/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion demos/compareSolutions/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion demos/copyItemInfo/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion demos/copySolutions/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion demos/createSolution/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion demos/deleteSolution/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion demos/deploySolution/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,25 @@ <h3>Deploy one or more Solutions</h3>

<div class="section-title">Custom Params</div>
<div class="labeledItem">
<textarea id="customParams" cols="40" rows="5" placeholder="requires a valid JSON object"></textarea>
<p style="margin:0 0 6px 0; max-width:80ch;">
Paste a JSON object whose properties will be added to the deploy's <code>templateDictionary.params</code>.
Templates can reference these via <code>{{params.&lt;key&gt;...}}</code>.
</p>
<p style="margin:0 0 6px 0; max-width:80ch;">
To exercise a <strong>buildSolution</strong> payload (e.g. from solutions-components), wrap it under a
<code>buildSolution</code> key so templates can resolve it as <code>{{params.buildSolution.items.&lt;id&gt;.title}}</code>:
</p>
<pre style="margin:0 0 6px 0; padding:6px; background:#f4f4f4; border:1px solid #ddd; max-width:80ch; overflow:auto;">{
"buildSolution": {
"solution": { "item": { "id": "&lt;solutionItemId&gt;", ... }, ... },
"items": { "&lt;itemId&gt;": { "title": "...", ... } }
}
}</pre>
<p style="margin:0 0 6px 0; max-width:80ch;">
If the Solution Id field above is empty, it will be pulled from
<code>buildSolution.solution.item.id</code>.
</p>
<textarea id="customParams" cols="80" rows="10" placeholder="requires a valid JSON object"></textarea>
</div>

<br /><br />
Expand Down
2 changes: 1 addition & 1 deletion demos/deploySolution/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 18 additions & 1 deletion demos/deploySolution/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function go(
document.getElementById("output").style.display = "block";

// use the manually entered value, falling back to the select lists
const solutionId =
let solutionId =
htmlUtil.getHTMLValue("solutionId") ||
htmlUtil.getHTMLValue("solutionPicklist");
const folderId = htmlUtil.getHTMLValue("foldersPicklist");
Expand All @@ -170,6 +170,23 @@ function go(
// Custom Params
const customParams = htmlUtil.getHTMLValue("customParams");

// If no Solution Id was entered, fall back to buildSolution.solution.item.id
// when present in the custom params.
if (!solutionId && typeof customParams === "string" && customParams.trim().length > 0) {
try {
const paramsObj = JSON.parse(customParams);
const embeddedId = paramsObj?.buildSolution?.solution?.item?.id;
if (typeof embeddedId === "string" && embeddedId.length > 0) {
solutionId = embeddedId;
}
} catch (e) {
document.getElementById("input").style.display = "block";
document.getElementById("output").style.display = "none";
alert("Custom Params is not valid JSON: " + (e as Error).message);
return;
}
}

// Source credentials
const srcHtmlValue = htmlUtil.getHTMLValue("srcPortal");
const srcPortalStr = srcHtmlValue.endsWith('/') ? srcHtmlValue.slice(0, -1) : srcHtmlValue;
Expand Down
2 changes: 1 addition & 1 deletion demos/getItemInfo/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion demos/implementedTypes/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion demos/recreateSolution/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion demos/reuseDeployedItems/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion demos/verifySolution/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions packages/common/src/featureServiceHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,10 +553,6 @@ export function setNamesAndTitles(templates: IItemTemplate[]): IItemTemplate[] {
// If the name already contains a GUID remove it
baseName = baseName.replace(/_[0-9A-F]{32}/gi, "");

// Replace characters that are not allowed in a feature service name with "_"
// Disallowed: '#', '%', '&', '"', '\', '/', '+', '?', ':', '*', '<', '>', ' ', '\t'
baseName = baseName.replace(/[#%&"\\/+?:*<> \t]/g, "_");

// The name length limit is 98
// Limit the baseName to 50 characters before the _<guid>
// If the baseName includes '{{params' it is likely being used in a template replacement, so do not truncate.
Expand Down
12 changes: 12 additions & 0 deletions packages/common/src/restHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2105,6 +2105,17 @@ export function _countRelationships(layers: any[]): number {
return layers.reduce(reducer, 0);
}

/**
* Remove spaces and replace other characters that are not allowed in a feature service name with "_".
* Spaces are removed entirely. Disallowed (replaced with "_"): '#', '%', '&', '"', '\', '/', '+', '?', ':', '*', '<', '>', '\t'
*
* @param name The candidate service name
* @returns The sanitized name, or the input unchanged if it is not a string
*/
export function sanitizeFeatureServiceName(name: string): string {
return typeof name === "string" ? name.replace(/ /g, "").replace(/[#%&"\\/+?:*<>\t]/g, "_") : name;
}

/**
* Gets the full definitions of the layers affiliated with a hosted service.
*
Expand Down Expand Up @@ -2171,6 +2182,7 @@ export function _getCreateServiceOptions(
}
createOptions.item = replaceInTemplate(createOptions.item, templateDictionary);
createOptions.params = replaceInTemplate(createOptions.params, templateDictionary);
createOptions.item.name = sanitizeFeatureServiceName(createOptions.item.name);

if (newItemTemplate.item.thumbnail) {
// Pass thumbnail file in via params because item property is serialized, which discards a blob
Expand Down
21 changes: 0 additions & 21 deletions packages/common/test/featureServiceHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1973,27 +1973,6 @@ describe("Module `featureServiceHelpers`: utility functions for feature-service
expect(actual).toEqual(expected);
});

it("should replace disallowed characters in the name with '_'", () => {
const t: IItemTemplate = templates.getItemTemplateSkeleton();
t.item.type = "Feature Service";
// Includes each disallowed character: # % & " \ / + ? : * < > space tab
const disallowedName = 'a#b%c&d"e\\f/g+h?i:j*k<l>m n\to';
t.item.name = disallowedName;
t.item.title = disallowedName;
const _templates: IItemTemplate[] = [t];

spyOn(generalHelpers, "generateGUID").and.returnValue("212dbc19b03943008fdfaf8d6adca00e");

const expectedTemplate: IItemTemplate = templates.getItemTemplateSkeleton();
expectedTemplate.item.type = "Feature Service";
expectedTemplate.item.name = `a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_212dbc19b03943008fdfaf8d6adca00e`;
expectedTemplate.item.title = disallowedName;
const expected: IItemTemplate[] = [expectedTemplate];

const actual: IItemTemplate[] = setNamesAndTitles(_templates);
expect(actual).toEqual(expected);
});

it("should not truncate if '{{param' is used", () => {
const t: IItemTemplate = templates.getItemTemplateSkeleton();
t.item.type = "Feature Service";
Expand Down
52 changes: 52 additions & 0 deletions packages/common/test/restHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4573,6 +4573,58 @@ describe("Module `restHelpers`: common REST utility functions shared across pack
expect(templateDictionary[itemId].solutionExtent).toBeUndefined();
expect(templateDictionary[itemId].defaultSpatialReference).toEqual({ wkid: serviceWkid });
});

it("sanitizes disallowed characters in service name resolved from a {{params}} placeholder", async () => {
const userSession: UserSession = new UserSession({
username: "jsmith",
password: "123456",
});

const templateDictionary: any = {
folderId: "aabb123456",
isPortal: false,
solutionItemId: "sol1234567890",
ab766cba0dd44ec080420acc10990282: {},
organization: organization,
solutionItemExtent: solutionItemExtent,
params: {
buildSolution: {
items: {
ab766cba0dd44ec080420acc10990282: {
title: 'My & Service #1: a/b\\c <d> "e" +f?g*h%i j\tk',
},
},
},
},
};

itemTemplate.item.name =
"{{params.buildSolution.items.ab766cba0dd44ec080420acc10990282.title}}_0a25612a2fc54f6e8828c679e2300a49";
itemTemplate.item.title = "{{params.buildSolution.items.ab766cba0dd44ec080420acc10990282.title}}";
itemTemplate.properties.service.spatialReference = { wkid: 102100 };
itemTemplate.itemId = "ab766cba0dd44ec080420acc10990282";

const options = await restHelpers._getCreateServiceOptions(itemTemplate, userSession, templateDictionary);
expect(options.item.name).toEqual("My_Service_1_a_b_c_d__e__f_g_h_ij_k_0a25612a2fc54f6e8828c679e2300a49");
// Title is *not* sanitized — only the service name has the character restriction.
expect(options.item.title).toEqual('My & Service #1: a/b\\c <d> "e" +f?g*h%i j\tk');
});
});

describe("sanitizeFeatureServiceName", () => {
it("removes spaces and replaces every other disallowed character with '_'", () => {
const disallowedName = 'a#b%c&d"e\\f/g+h?i:j*k<l>m n\to';
expect(restHelpers.sanitizeFeatureServiceName(disallowedName)).toEqual("a_b_c_d_e_f_g_h_i_j_k_l_mn_o");
});

it("returns allowed names unchanged", () => {
expect(restHelpers.sanitizeFeatureServiceName("AcceptableName_1")).toEqual("AcceptableName_1");
});

it("returns non-string inputs unchanged", () => {
expect(restHelpers.sanitizeFeatureServiceName(undefined as any)).toBeUndefined();
expect(restHelpers.sanitizeFeatureServiceName(null as any)).toBeNull();
});
});

describe("_extentIsValid", () => {
Expand Down
Loading