case 4721 - client escaping of record text
Record text shown as HTML is escaped: unit warranty summary (vendor name, receipt number), work-order address dialog (now plain text), search excerpt (escaped, search terms highlighted as plain text, not as a regular expression). Confirmation dialog messages are sanitised with DOMPurify. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -646,6 +646,20 @@ export default {
|
|||||||
return str != null && typeof str.valueOf() === "string";
|
return str != null && typeof str.valueOf() === "string";
|
||||||
},
|
},
|
||||||
///////////////////////////////////////////////
|
///////////////////////////////////////////////
|
||||||
|
// case 4721: escape text that goes into HTML (v-html) so it shows as text
|
||||||
|
//
|
||||||
|
escapeHtml: function(str) {
|
||||||
|
if (str === null || str === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
},
|
||||||
|
///////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -31,10 +31,7 @@
|
|||||||
<span v-if="options.title" class="ml-5"> {{ options.title }} </span>
|
<span v-if="options.title" class="ml-5"> {{ options.title }} </span>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<v-card-text
|
<v-card-text class="text-body-1 text-sm-h6 my-5" v-html="safeMessage">
|
||||||
class="text-body-1 text-sm-h6 my-5"
|
|
||||||
v-html="options.message"
|
|
||||||
>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
@@ -82,6 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
export default {
|
export default {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
width: 290,
|
width: 290,
|
||||||
@@ -97,6 +95,12 @@ export default {
|
|||||||
type: "info" //one of success, info, question, warning, and error, see v-alert docs for more info
|
type: "info" //one of success, info, question, warning, and error, see v-alert docs for more info
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
computed: {
|
||||||
|
//case 4721: messages can hold formatting (line breaks), but never script: sanitised before it is shown
|
||||||
|
safeMessage() {
|
||||||
|
return DOMPurify.sanitize(this.options.message || "");
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
open(options) {
|
open(options) {
|
||||||
if (options.message.includes("\n")) {
|
if (options.message.includes("\n")) {
|
||||||
|
|||||||
@@ -592,7 +592,9 @@ export default {
|
|||||||
|
|
||||||
let PurchasedFrom = "-";
|
let PurchasedFrom = "-";
|
||||||
if (r.purchaseFromVendorId) {
|
if (r.purchaseFromVendorId) {
|
||||||
PurchasedFrom = `<a href="/vendors/${r.purchaseFromVendorId}"> ${r.purchasedFromVendor}</a>`;
|
PurchasedFrom = `<a href="/vendors/${
|
||||||
|
r.purchaseFromVendorId
|
||||||
|
}"> ${window.$gz.util.escapeHtml(r.purchasedFromVendor)}</a>`; //case 4721: escaped
|
||||||
}
|
}
|
||||||
let PurchasedDate = "-";
|
let PurchasedDate = "-";
|
||||||
if (r.purchaseDate) {
|
if (r.purchaseDate) {
|
||||||
@@ -609,7 +611,7 @@ export default {
|
|||||||
"UnitPurchasedDate"
|
"UnitPurchasedDate"
|
||||||
)}: ${PurchasedDate}<br/>${this.$ay.t(
|
)}: ${PurchasedDate}<br/>${this.$ay.t(
|
||||||
"UnitReceipt"
|
"UnitReceipt"
|
||||||
)}: ${r.purchaseReceiptNumber ?? "-"}`;
|
)}: ${window.$gz.util.escapeHtml(r.purchaseReceiptNumber ?? "-")}`; //case 4721: escaped
|
||||||
|
|
||||||
let RecentWorkOrderList = "";
|
let RecentWorkOrderList = "";
|
||||||
if (r.recentWorkOrders.length > 0) {
|
if (r.recentWorkOrders.length > 0) {
|
||||||
|
|||||||
@@ -584,7 +584,9 @@ export default {
|
|||||||
|
|
||||||
let PurchasedFrom = "-";
|
let PurchasedFrom = "-";
|
||||||
if (r.purchaseFromVendorId) {
|
if (r.purchaseFromVendorId) {
|
||||||
PurchasedFrom = `<a href="/vendors/${r.purchaseFromVendorId}"> ${r.purchasedFromVendor}</a>`;
|
PurchasedFrom = `<a href="/vendors/${
|
||||||
|
r.purchaseFromVendorId
|
||||||
|
}"> ${window.$gz.util.escapeHtml(r.purchasedFromVendor)}</a>`; //case 4721: escaped
|
||||||
}
|
}
|
||||||
let PurchasedDate = "-";
|
let PurchasedDate = "-";
|
||||||
if (r.purchaseDate) {
|
if (r.purchaseDate) {
|
||||||
@@ -601,7 +603,7 @@ export default {
|
|||||||
"UnitPurchasedDate"
|
"UnitPurchasedDate"
|
||||||
)}: ${PurchasedDate}<br/>${this.$ay.t(
|
)}: ${PurchasedDate}<br/>${this.$ay.t(
|
||||||
"UnitReceipt"
|
"UnitReceipt"
|
||||||
)}: ${r.purchaseReceiptNumber ?? "-"}`;
|
)}: ${window.$gz.util.escapeHtml(r.purchaseReceiptNumber ?? "-")}`; //case 4721: escaped
|
||||||
|
|
||||||
let RecentWorkOrderList = "";
|
let RecentWorkOrderList = "";
|
||||||
if (r.recentWorkOrders.length > 0) {
|
if (r.recentWorkOrders.length > 0) {
|
||||||
|
|||||||
@@ -368,9 +368,9 @@
|
|||||||
</v-list-item-avatar>
|
</v-list-item-avatar>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- case 4721: plain text, the address is never HTML -->
|
||||||
<v-list-item-subtitle
|
<v-list-item-subtitle
|
||||||
v-html="item.viz"
|
v-text="item.viz"
|
||||||
></v-list-item-subtitle>
|
></v-list-item-subtitle>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|||||||
@@ -624,7 +624,9 @@ export default {
|
|||||||
|
|
||||||
let PurchasedFrom = "-";
|
let PurchasedFrom = "-";
|
||||||
if (r.purchaseFromVendorId) {
|
if (r.purchaseFromVendorId) {
|
||||||
PurchasedFrom = `<a href="/vendors/${r.purchaseFromVendorId}"> ${r.purchasedFromVendor}</a>`;
|
PurchasedFrom = `<a href="/vendors/${
|
||||||
|
r.purchaseFromVendorId
|
||||||
|
}"> ${window.$gz.util.escapeHtml(r.purchasedFromVendor)}</a>`; //case 4721: escaped
|
||||||
}
|
}
|
||||||
let PurchasedDate = "-";
|
let PurchasedDate = "-";
|
||||||
if (r.purchaseDate) {
|
if (r.purchaseDate) {
|
||||||
@@ -641,7 +643,7 @@ export default {
|
|||||||
"UnitPurchasedDate"
|
"UnitPurchasedDate"
|
||||||
)}: ${PurchasedDate}<br/>${this.$ay.t(
|
)}: ${PurchasedDate}<br/>${this.$ay.t(
|
||||||
"UnitReceipt"
|
"UnitReceipt"
|
||||||
)}: ${r.purchaseReceiptNumber ?? "-"}`;
|
)}: ${window.$gz.util.escapeHtml(r.purchaseReceiptNumber ?? "-")}`; //case 4721: escaped
|
||||||
|
|
||||||
let RecentWorkOrderList = "";
|
let RecentWorkOrderList = "";
|
||||||
if (r.recentWorkOrders.length > 0) {
|
if (r.recentWorkOrders.length > 0) {
|
||||||
|
|||||||
@@ -206,16 +206,9 @@ export default {
|
|||||||
vm.formState.serverError = res.error;
|
vm.formState.serverError = res.error;
|
||||||
window.$gz.form.setErrorBoxErrors(vm);
|
window.$gz.form.setErrorBoxErrors(vm);
|
||||||
} else {
|
} else {
|
||||||
let showInfo = res.data;
|
//case 4721: the excerpt is record text, so it is escaped; the search terms are highlighted around it
|
||||||
const searchTerms = vm.searchPhrase.replace(/[*]/gi, "").split(" ");
|
const searchTerms = vm.searchPhrase.replace(/[*]/gi, "").split(" ");
|
||||||
for (let i = 0; i < searchTerms.length; i++) {
|
item.info = highlightTerms(res.data || "", searchTerms);
|
||||||
const regEx = new RegExp(searchTerms[i], "ig");
|
|
||||||
const replaceMask =
|
|
||||||
"<span class='v-list-item__mask'>" + searchTerms[i] + "</span>";
|
|
||||||
showInfo = showInfo.replace(regEx, replaceMask);
|
|
||||||
}
|
|
||||||
|
|
||||||
item.info = showInfo;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.$gz.errorHandler.handleFormError(error, vm);
|
window.$gz.errorHandler.handleFormError(error, vm);
|
||||||
@@ -357,4 +350,39 @@ async function populateSelectionLists(vm) {
|
|||||||
window.$gz.form.addNoSelectionItem(vm.selectLists.objectTypes);
|
window.$gz.form.addNoSelectionItem(vm.selectLists.objectTypes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////
|
||||||
|
// case 4721: excerpt as escaped text with the search terms highlighted (case insensitive);
|
||||||
|
// the terms are matched as plain text, not as a regular expression
|
||||||
|
function highlightTerms(text, terms) {
|
||||||
|
const esc = window.$gz.util.escapeHtml;
|
||||||
|
const lowerText = text.toLowerCase();
|
||||||
|
const lowerTerms = terms.map(z => z.toLowerCase()).filter(z => z.length > 0);
|
||||||
|
let html = "";
|
||||||
|
let pos = 0;
|
||||||
|
while (pos < text.length) {
|
||||||
|
let best = -1;
|
||||||
|
let bestLength = 0;
|
||||||
|
lowerTerms.forEach(t => {
|
||||||
|
const i = lowerText.indexOf(t, pos);
|
||||||
|
if (
|
||||||
|
i !== -1 &&
|
||||||
|
(best === -1 || i < best || (i === best && t.length > bestLength))
|
||||||
|
) {
|
||||||
|
best = i;
|
||||||
|
bestLength = t.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (best === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
html +=
|
||||||
|
esc(text.substring(pos, best)) +
|
||||||
|
"<span class='v-list-item__mask'>" +
|
||||||
|
esc(text.substring(best, best + bestLength)) +
|
||||||
|
"</span>";
|
||||||
|
pos = best + bestLength;
|
||||||
|
}
|
||||||
|
return html + esc(text.substring(pos));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user