[FIX] *: views: add component instance to rendering context

Owl last update fixes the leaking `this` in rendering context of
templates called with `t-call-context` [1]. Since [2], we use the
`t-call-context` directive to control the rendering context of
templates compiled from an arch (e.g. in form and kanban views).
We can now better control what people use in archs, where we don't
want them to access js implementation details, as `this` is no
longer available.

However, the component instance still needs to be accessible in
the compiled template. We thus add the `__comp__` key in the
rendering context. Since we do not want people to access it in
archs, we add a check in the view validation that this string
isn't used in dynamic attributes.

[1] https://github.com/odoo/owl/commit/df59ec49aefce2e0913fdc1792d42b9680fb28b6
[2] https://github.com/odoo/odoo/commit/4c5b867ff6b0b674cb83d1a1262ae354ebaa6d57

X-original-commit: 264f313012aa99449d1576ae1110252c13755142
Part-of: odoo/odoo#110196
This commit is contained in:
Aaron Bohy
2023-01-18 10:16:12 +01:00
parent 020ef1a694
commit 942fcbfb3a
18 changed files with 149 additions and 97 deletions
@@ -45,6 +45,7 @@ export class ActivityRecord extends Component {
user_context: this.user.context,
widget: this.widget,
luxon,
__comp__: Object.assign(Object.create(this), { this: this }),
};
}
}
@@ -3,7 +3,7 @@
<t t-name="mail.ActivityRecord" owl="1">
<td class="o_activity_record p-3 cursor-pointer" t-on-click="() => this.props.openRecord(this.props.record)">
<t t-call="{{ recordTemplate }}" t-call-context="getRenderingContext()"/>
<t t-call="{{ recordTemplate }}" t-call-context="this.getRenderingContext()"/>
</td>
</t>
@@ -86,7 +86,7 @@ export class MailFormCompiler extends ViewCompiler {
const chatterContainerHookXml = res.querySelector(".o_FormRenderer_chatterContainer");
if (chatterContainerHookXml) {
setAttributes(chatterContainerHookXml, {
"t-if": `!this.hasAttachmentViewer() and this.uiService.size >= ${SIZES.XXL}`,
"t-if": `!__comp__.hasAttachmentViewer() and __comp__.uiService.size >= ${SIZES.XXL}`,
"t-attf-class": "o-aside",
});
const chatterContainerXml = chatterContainerHookXml.querySelector("ChatterContainer");
@@ -99,7 +99,7 @@ export class MailFormCompiler extends ViewCompiler {
const attachmentViewHookXml = res.querySelector(".o_attachment_preview");
if (attachmentViewHookXml) {
setAttributes(attachmentViewHookXml, {
"t-if": `this.hasAttachmentViewer()`,
"t-if": `__comp__.hasAttachmentViewer()`,
});
}
return res;
@@ -112,7 +112,7 @@ export class MailFormCompiler extends ViewCompiler {
let compiledChild = this.compileNode(child, params, false);
compiledChild = this.applyInvisible(invisible, compiledChild, {
...params,
recordExpr: "this.model.root",
recordExpr: "__comp__.model.root",
});
append(compiledRoot, compiledChild);
}
@@ -122,16 +122,16 @@ export class MailFormCompiler extends ViewCompiler {
compileChatter(node) {
return compileChatter(node, {
chatter: "chatter",
threadId: "this.model.root.resId or undefined",
threadModel: "this.model.root.resModel",
webRecord: "this.model.root",
threadId: "__comp__.model.root.resId or undefined",
threadModel: "__comp__.model.root.resModel",
webRecord: "__comp__.model.root",
});
}
compileAttachmentPreview(node) {
return compileAttachmentPreview(node, {
threadId: "this.model.root.resId or undefined",
threadModel: "this.model.root.resModel",
threadId: "__comp__.model.root.resId or undefined",
threadModel: "__comp__.model.root.resModel",
});
}
}
@@ -140,10 +140,10 @@ registry.category("form_compilers").add("chatter_compiler", {
selector: "div.oe_chatter",
fn: (node) =>
compileChatter(node, {
chatter: "this.props.chatter",
threadId: "this.props.record.resId or undefined",
threadModel: "this.props.record.resModel",
webRecord: "this.props.record",
chatter: "__comp__.props.chatter",
threadId: "__comp__.props.record.resId or undefined",
threadModel: "__comp__.props.record.resModel",
webRecord: "__comp__.props.record",
}),
});
@@ -151,8 +151,8 @@ registry.category("form_compilers").add("attachment_preview_compiler", {
selector: "div.o_attachment_preview",
fn: (node) =>
compileAttachmentPreview(node, {
threadId: "this.props.record.resId or undefined",
threadModel: "this.props.record.resModel",
threadId: "__comp__.props.record.resId or undefined",
threadModel: "__comp__.props.record.resModel",
}),
});
@@ -183,7 +183,7 @@ patch(FormCompiler.prototype, "mail", {
const sheetBgChatterContainerHookXml = chatterContainerHookXml.cloneNode(true);
sheetBgChatterContainerHookXml.classList.add("o-isInFormSheetBg");
setAttributes(sheetBgChatterContainerHookXml, {
"t-if": `this.props.hasAttachmentViewer`,
"t-if": `__comp__.props.hasAttachmentViewer`,
});
append(formSheetBgXml, sheetBgChatterContainerHookXml);
const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector(
@@ -195,7 +195,7 @@ patch(FormCompiler.prototype, "mail", {
}
// after sheet bg (standard position, below form)
setAttributes(chatterContainerHookXml, {
"t-if": `!this.props.hasAttachmentViewer and this.uiService.size < ${SIZES.XXL}`,
"t-if": `!__comp__.props.hasAttachmentViewer and __comp__.uiService.size < ${SIZES.XXL}`,
});
append(parentXml, chatterContainerHookXml);
return res;
@@ -4,7 +4,7 @@
<t t-inherit="web.FormView" t-inherit-mode="extension">
<xpath expr="//div[hasclass('o_form_view_container')]" position="after">
<t t-if="mailTemplate">
<t t-call="{{ mailTemplate }}" />
<t t-call="{{ mailTemplate }}" t-call-context="{ __comp__: Object.assign(Object.create(this), { this: this }) }"/>
</t>
</xpath>
<xpath expr="//Layout/t[@t-component='props.Renderer']" position="attributes">
@@ -40,7 +40,7 @@ export class ProjectSharingChatterCompiler extends ViewCompiler {
const chatterContainerHookXml = res.querySelector(".o_FormRenderer_chatterContainer");
if (chatterContainerHookXml) {
setAttributes(chatterContainerHookXml, {
"t-if": `this.uiService.size >= ${SIZES.XXL}`,
"t-if": `__comp__.uiService.size >= ${SIZES.XXL}`,
});
chatterContainerHookXml.classList.add('overflow-x-hidden', 'overflow-y-auto', 'o-aside', 'h-100');
}
@@ -54,7 +54,7 @@ export class ProjectSharingChatterCompiler extends ViewCompiler {
let compiledChild = this.compileNode(child, params, false);
compiledChild = this.applyInvisible(invisible, compiledChild, {
...params,
recordExpr: "this.model.root",
recordExpr: "__comp__.model.root",
});
append(compiledRoot, compiledChild);
}
@@ -63,9 +63,9 @@ export class ProjectSharingChatterCompiler extends ViewCompiler {
compileChatter(node) {
return compileChatter(node, {
resId: 'this.model.root.resId or undefined',
resModel: 'this.model.root.resModel',
projectSharingId: 'this.model.root.context.active_id',
resId: '__comp__.model.root.resId or undefined',
resModel: '__comp__.model.root.resModel',
projectSharingId: '__comp__.model.root.context.active_id',
});
}
}
@@ -74,9 +74,9 @@ registry.category("form_compilers").add("portal_chatter_compiler", {
selector: "div.oe_chatter",
fn: (node) =>
compileChatter(node, {
resId: "this.props.record.resId or undefined",
resModel: "this.props.record.resModel",
projectSharingId: "this.props.record.context.active_id",
resId: "__comp__.props.record.resId or undefined",
resModel: "__comp__.props.record.resModel",
projectSharingId: "__comp__.props.record.context.active_id",
}),
});
@@ -97,7 +97,7 @@ patch(FormCompiler.prototype, 'project_sharing_chatter', {
}
// after sheet bg (standard position, below form)
setAttributes(chatterContainerHookXml, {
't-if': `this.uiService.size < ${SIZES.XXL}`,
't-if': `__comp__.uiService.size < ${SIZES.XXL}`,
});
append(parentXml, chatterContainerHookXml);
return res;
@@ -4,7 +4,7 @@
<t t-inherit="web.FormView" t-inherit-mode="extension">
<xpath expr="//div[hasclass('o_form_view_container')]" position="after">
<t t-if="mailTemplate">
<t t-call="{{ mailTemplate }}" />
<t t-call="{{ mailTemplate }}" t-call-context="{ __comp__: Object.assign(Object.create(this), { this: this }) }"/>
</t>
</xpath>
</t>
@@ -76,13 +76,13 @@ export class FormCompiler extends ViewCompiler {
} else {
labelText = labelText
? toStringExpression(labelText)
: `this.props.record.fields['${fieldName}'].string`;
: `__comp__.props.record.fields['${fieldName}'].string`;
}
const formLabel = createElement("FormLabel", {
id: `'${fieldId}'`,
fieldName: `'${fieldName}'`,
record: `this.props.record`,
fieldInfo: `this.props.archInfo.fieldNodes['${fieldId}']`,
record: `__comp__.props.record`,
fieldInfo: `__comp__.props.archInfo.fieldNodes['${fieldId}']`,
className: `"${label.className}"`,
string: labelText,
});
@@ -140,7 +140,7 @@ export class FormCompiler extends ViewCompiler {
if (typeof invisible === "boolean") {
isVisibleExpr = `${invisible ? false : true}`;
} else {
isVisibleExpr = `!this.evalDomainFromRecord(this.props.record,${JSON.stringify(
isVisibleExpr = `!__comp__.evalDomainFromRecord(__comp__.props.record,${JSON.stringify(
invisible
)})`;
}
@@ -176,8 +176,8 @@ export class FormCompiler extends ViewCompiler {
compileButton(el, params) {
const compiled = super.compileButton(el, params);
compiled.setAttribute("disable", "this.props.disableViewButtons");
compiled.setAttribute("enable", "this.props.enableViewButtons");
compiled.setAttribute("disable", "__comp__.props.disableViewButtons");
compiled.setAttribute("enable", "__comp__.props.enableViewButtons");
return compiled;
}
@@ -219,13 +219,13 @@ export class FormCompiler extends ViewCompiler {
compileForm(el, params) {
const sheetNode = el.querySelector("sheet");
const displayClasses = sheetNode
? `d-flex {{ this.uiService.size < ${SIZES.XXL} ? "flex-column" : "flex-nowrap h-100" }}`
? `d-flex {{ __comp__.uiService.size < ${SIZES.XXL} ? "flex-column" : "flex-nowrap h-100" }}`
: "d-block";
const stateClasses =
"{{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}";
"{{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}";
const form = createElement("div", {
"t-att-class": "this.props.class",
"t-attf-class": `{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} ${displayClasses} ${stateClasses}`,
"t-att-class": "__comp__.props.class",
"t-attf-class": `{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} ${displayClasses} ${stateClasses}`,
});
if (!sheetNode) {
for (const child of el.childNodes) {
@@ -249,8 +249,9 @@ export class FormCompiler extends ViewCompiler {
if (localization.multiLang) {
const statusBar = form.querySelector(".o_form_statusbar");
const translateAlert = createElement("t", {
"t-if": "this.props.translateAlert",
"t-if": "__comp__.props.translateAlert",
"t-call": "web.TranslateAlert",
"t-call-context": "__comp__",
});
if (statusBar) {
statusBar.parentElement.insertBefore(translateAlert, statusBar.nextSibling);
@@ -332,14 +333,14 @@ export class FormCompiler extends ViewCompiler {
const props = {
id: `${fieldId}`,
fieldName: `'${fieldName}'`,
record: `this.props.record`,
record: `__comp__.props.record`,
string: child.hasAttribute("string")
? toStringExpression(child.getAttribute("string"))
: `this.props.record.fields.${fieldName}.string`,
fieldInfo: `this.props.archInfo.fieldNodes[${fieldId}]`,
: `__comp__.props.record.fields.${fieldName}.string`,
fieldInfo: `__comp__.props.archInfo.fieldNodes[${fieldId}]`,
};
mainSlot.setAttribute("props", objectToString(props));
mainSlot.setAttribute("Component", "this.constructor.components.FormLabel");
mainSlot.setAttribute("Component", "__comp__.constructor.components.FormLabel");
mainSlot.setAttribute("subType", "'item_component'");
}
} else {
@@ -360,7 +361,7 @@ export class FormCompiler extends ViewCompiler {
if (typeof invisible === "boolean") {
isVisibleExpr = `${invisible ? false : true}`;
} else {
isVisibleExpr = `!this.evalDomainFromRecord(this.props.record,${JSON.stringify(
isVisibleExpr = `!__comp__.evalDomainFromRecord(__comp__.props.record,${JSON.stringify(
invisible
)})`;
}
@@ -504,11 +505,11 @@ export class FormCompiler extends ViewCompiler {
noteBook.setAttribute(
"defaultPage",
`this.props.record.isNew ? undefined : this.props.activeNotebookPages[${noteBookId}]`
`__comp__.props.record.isNew ? undefined : __comp__.props.activeNotebookPages[${noteBookId}]`
);
noteBook.setAttribute(
"onPageUpdate",
`(page) => this.props.onNotebookPageChange(${noteBookId}, page)`
`(page) => __comp__.props.onNotebookPageChange(${noteBookId}, page)`
);
for (const child of el.children) {
@@ -539,7 +540,7 @@ export class FormCompiler extends ViewCompiler {
if (child.getAttribute("autofocus") === "autofocus") {
noteBook.setAttribute(
"defaultPage",
`this.props.record.isNew ? "${pageId}" : (this.props.activeNotebookPages[${noteBookId}] || "${pageId}")`
`__comp__.props.record.isNew ? "${pageId}" : (__comp__.props.activeNotebookPages[${noteBookId}] || "${pageId}")`
);
}
@@ -558,7 +559,7 @@ export class FormCompiler extends ViewCompiler {
if (typeof invisible === "boolean") {
isVisible = `${!invisible}`;
} else {
isVisible = `!this.evalDomainFromRecord(this.props.record,${JSON.stringify(
isVisible = `!__comp__.evalDomainFromRecord(__comp__.props.record,${JSON.stringify(
invisible
)})`;
}
@@ -632,7 +633,7 @@ export class FormCompiler extends ViewCompiler {
*/
compileWidget(el) {
const widget = super.compileWidget(el);
widget.setAttribute("readonly", `!this.props.record.isInEdition`);
widget.setAttribute("readonly", `!__comp__.props.record.isInEdition`);
return widget;
}
}
@@ -66,16 +66,15 @@
<!--
Used in FormRenderer
Note: `props` is only accessible through `this` as we call the compiled template with
`t-call-context="{}"`
Note: `props` is only accessible through `this` as we call the compiled template a restricted context (t-call-context)
-->
<t t-name="web.TranslateAlert" owl="1">
<div class="alert alert-info alert-dismissible" role="alertdialog">
Please update translations of :
<t t-foreach="Array.from(this.props.translateAlert.fields)" t-as="field" t-key="field.name">
<t t-foreach="Array.from(props.translateAlert.fields)" t-as="field" t-key="field.name">
<strong><a class="o_field_translate" t-att-name="field.name" href="#"><t t-esc="field.string"/><t t-if="!field_last">, </t></a></strong>
</t>
<button t-on-click="this.props.translateAlert.close" type="button" class="btn-close" aria-label="Close"></button>
<button t-on-click="props.translateAlert.close" type="button" class="btn-close" aria-label="Close"></button>
</div>
</t>
</templates>
@@ -93,7 +93,7 @@ export class FormRenderer extends Component {
}
}
FormRenderer.template = xml`<t t-call="{{ templates.FormRenderer }}" t-call-context="{}" />`;
FormRenderer.template = xml`<t t-call="{{ templates.FormRenderer }}" t-call-context="{ __comp__: Object.assign(Object.create(this), { this: this }) }" />`;
FormRenderer.components = {
Field,
FormLabel,
@@ -80,7 +80,7 @@ export class KanbanCompiler extends ViewCompiler {
const strParams = Object.entries(nodeParams)
.map(([k, v]) => [k, toStringExpression(v)].join(":"))
.join(",");
el.setAttribute("t-on-click", `()=>this.triggerAction({${strParams}})`);
el.setAttribute("t-on-click", `()=>__comp__.triggerAction({${strParams}})`);
const compiled = createElement(el.nodeName);
for (const { name, value } of el.attributes) {
@@ -100,7 +100,10 @@ export class KanbanCompiler extends ViewCompiler {
* @returns {Element}
*/
compileColorPicker() {
return createElement("t", { "t-call": "web.KanbanColorPicker" });
return createElement("t", {
"t-call": "web.KanbanColorPicker",
"t-call-context": "__comp__",
});
}
/**
@@ -117,7 +120,7 @@ export class KanbanCompiler extends ViewCompiler {
} else {
compiled = super.compileField(el, params);
const fieldId = el.getAttribute("field_id") || el.getAttribute("name");
compiled.setAttribute("id", `'${fieldId}_' + this.props.record.id`);
compiled.setAttribute("id", `'${fieldId}_' + __comp__.props.record.id`);
}
const { bold, display } = extractAttributes(el, ["bold", "display"]);
@@ -177,7 +180,7 @@ export class KanbanCompiler extends ViewCompiler {
const compiled = this.compileGenericNode(el, params);
const tname = el.getAttribute("t-call");
if (tname in this.templates) {
compiled.setAttribute("t-call", `{{this.templates[${toStringExpression(tname)}]}}`);
compiled.setAttribute("t-call", `{{__comp__.templates[${toStringExpression(tname)}]}}`);
}
return compiled;
}
@@ -304,6 +304,7 @@ export class KanbanRecord extends Component {
selection_mode: this.props.forceGlobalClick,
user_context: this.user.context,
widget: this.widget,
__comp__: Object.assign(Object.create(this), { this: this }),
};
}
}
@@ -9,7 +9,7 @@
t-att-tabindex="props.record.model.useSampleModel ? -1 : 0"
t-on-click="onGlobalClick"
t-ref="root">
<t t-call="{{ templates[this.constructor.KANBAN_BOX_ATTRIBUTE] }}" t-call-context="renderingContext"/>
<t t-call="{{ templates[this.constructor.KANBAN_BOX_ATTRIBUTE] }}" t-call-context="this.renderingContext"/>
<t t-call="{{ this.constructor.menuTemplate }}"/>
</div>
</t>
+9 -7
View File
@@ -223,7 +223,7 @@ export class ViewCompiler {
{ selector: "widget", fn: this.compileWidget },
];
this.templates = templates;
this.ctx = { readonly: "this.props.readonly" };
this.ctx = { readonly: "__comp__.props.readonly" };
this.owlDirectiveRegexesWhitelist = this.constructor.OWL_DIRECTIVE_WHITELIST.map(
(d) => new RegExp(d)
@@ -246,8 +246,10 @@ export class ViewCompiler {
if (typeof invisible === "boolean") {
return;
}
const recordExpr = params.recordExpr || "this.props.record";
let isVisileExpr = `!this.evalDomainFromRecord(${recordExpr},${JSON.stringify(invisible)})`;
const recordExpr = params.recordExpr || "__comp__.props.record";
let isVisileExpr = `!__comp__.evalDomainFromRecord(${recordExpr},${JSON.stringify(
invisible
)})`;
if (compiled.hasAttribute("t-if")) {
const formerTif = compiled.getAttribute("t-if");
isVisileExpr = `( ${formerTif} ) and ${isVisileExpr}`;
@@ -323,7 +325,7 @@ export class ViewCompiler {
}
const button = createElement("ViewButton", {
tag: toStringExpression(tag),
record: "this.props.record",
record: "__comp__.props.record",
});
assignOwlDirectives(button, el);
@@ -382,8 +384,8 @@ export class ViewCompiler {
const field = createElement("Field");
field.setAttribute("id", `'${fieldId}'`);
field.setAttribute("name", `'${fieldName}'`);
field.setAttribute("record", params.recordExpr || "this.props.record");
field.setAttribute("fieldInfo", `this.props.archInfo.fieldNodes['${fieldId}']`);
field.setAttribute("record", params.recordExpr || "__comp__.props.record");
field.setAttribute("fieldInfo", `__comp__.props.archInfo.fieldNodes['${fieldId}']`);
if (el.hasAttribute("widget")) {
field.setAttribute("type", `'${el.getAttribute("widget")}'`);
@@ -422,7 +424,7 @@ export class ViewCompiler {
*/
compileWidget(el) {
const attrs = {};
const props = { record: "this.props.record", readonly: this.ctx.readonly };
const props = { record: "__comp__.props.record", readonly: this.ctx.readonly };
for (const { name, value } of el.attributes) {
switch (name) {
case "class":
@@ -17,8 +17,11 @@ export class SettingsFormCompiler extends FormCompiler {
compileForm(el, params) {
const settingsPage = createElement("SettingsPage");
settingsPage.setAttribute("slots", "{NoContentHelper:this.props.slots.NoContentHelper}");
settingsPage.setAttribute("initialTab", "this.props.initialApp");
settingsPage.setAttribute(
"slots",
"{NoContentHelper:__comp__.props.slots.NoContentHelper}"
);
settingsPage.setAttribute("initialTab", "__comp__.props.initialApp");
settingsPage.setAttribute("t-slot-scope", "settings");
//props
@@ -91,7 +94,7 @@ export class SettingsFormCompiler extends FormCompiler {
help: toStringExpression(el.getAttribute("help") || ""),
companyDependent: el.getAttribute("company_dependent") === "1" || "false",
documentation: toStringExpression(el.getAttribute("documentation") || ""),
record: `this.props.record`,
record: `__comp__.props.record`,
});
let string = toStringExpression(el.getAttribute("string") || "");
let addLabel = true;
@@ -38,7 +38,7 @@ QUnit.module("Form Compiler", (hooks) => {
const arch = /*xml*/ `<form><div>lol</div></form>`;
const expected = /*xml*/ `
<t>
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
<div>lol</div>
</div>
</t>`;
@@ -52,9 +52,9 @@ QUnit.module("Form Compiler", (hooks) => {
const arch = /*xml*/ `<form><field name="test"/><label for="test" string=""/></form>`;
const expected = /*xml*/ `
<t>
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
<Field id="'test'" name="'test'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['test']" />
<FormLabel id="'test'" fieldName="'test'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['test']" className="&quot;&quot;" string="\`\`" />
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
<Field id="'test'" name="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']" />
<FormLabel id="'test'" fieldName="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']" className="&quot;&quot;" string="\`\`" />
</div>
</t>`;
assert.areEquivalent(compileTemplate(arch), expected);
@@ -65,10 +65,10 @@ QUnit.module("Form Compiler", (hooks) => {
const arch = /*xml*/ `<form><div class="someClass">lol<field name="display_name"/></div></form>`;
const expected = /*xml*/ `
<t>
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
<div class="someClass">
lol
<Field id="'display_name'" name="'display_name'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['display_name']"/>
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']"/>
</div>
</div>
</t>`;
@@ -88,15 +88,15 @@ QUnit.module("Form Compiler", (hooks) => {
<OuterGroup>
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" isVisible="true" itemSpan="1">
<InnerGroup class="scope &amp;&amp; scope.className">
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" props="{id:'display_name',fieldName:'display_name',record:this.props.record,string:this.props.record.fields.display_name.string,fieldInfo:this.props.archInfo.fieldNodes['display_name']}" Component="this.constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
<Field id="'display_name'" name="'display_name'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['display_name']" class="scope &amp;&amp; scope.className" />
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" props="{id:'display_name',fieldName:'display_name',record:__comp__.props.record,string:__comp__.props.record.fields.display_name.string,fieldInfo:__comp__.props.archInfo.fieldNodes['display_name']}" Component="__comp__.constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" class="scope &amp;&amp; scope.className" />
</t>
</InnerGroup>
</t>
<t t-set-slot="item_1" type="'item'" sequence="1" t-slot-scope="scope" isVisible="true" itemSpan="1">
<InnerGroup class="scope &amp;&amp; scope.className">
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" props="{id:'charfield',fieldName:'charfield',record:this.props.record,string:this.props.record.fields.charfield.string,fieldInfo:this.props.archInfo.fieldNodes['charfield']}" Component="this.constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
<Field id="'charfield'" name="'charfield'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['charfield']" class="scope &amp;&amp; scope.className" />
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" props="{id:'charfield',fieldName:'charfield',record:__comp__.props.record,string:__comp__.props.record.fields.charfield.string,fieldInfo:__comp__.props.archInfo.fieldNodes['charfield']}" Component="__comp__.constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
<Field id="'charfield'" name="'charfield'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['charfield']" class="scope &amp;&amp; scope.className" />
</t>
</InnerGroup>
</t>
@@ -120,13 +120,13 @@ QUnit.module("Form Compiler", (hooks) => {
</form>`;
const expected = /*xml*/ `
<t>
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
<OuterGroup>
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" isVisible="true" itemSpan="1">
<InnerGroup class="scope &amp;&amp; scope.className">
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" isVisible="true" itemSpan="1">
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }} {{scope &amp;&amp; scope.className || &quot;&quot; }}" class="o_form_nosheet">
<div><Field id="'test'" name="'test'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['test']"/></div>
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }} {{scope &amp;&amp; scope.className || &quot;&quot; }}" class="o_form_nosheet">
<div><Field id="'test'" name="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']"/></div>
</div>
</t>
</InnerGroup>
@@ -149,12 +149,12 @@ QUnit.module("Form Compiler", (hooks) => {
</form>`;
const expected = /*xml*/ `
<Notebook defaultPage="this.props.record.isNew ? undefined : this.props.activeNotebookPages[0]" onPageUpdate="(page) =&gt; this.props.onNotebookPageChange(0, page)">
<Notebook defaultPage="__comp__.props.record.isNew ? undefined : __comp__.props.activeNotebookPages[0]" onPageUpdate="(page) =&gt; __comp__.props.onNotebookPageChange(0, page)">
<t t-set-slot="page_1" title="\`Page1\`" name="\`p1\`" isVisible="true">
<Field id="'charfield'" name="'charfield'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['charfield']"/>
<Field id="'charfield'" name="'charfield'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['charfield']"/>
</t>
<t t-set-slot="page_2" title="\`Page2\`" name="\`p2\`" isVisible="true">
<Field id="'display_name'" name="'display_name'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['display_name']"/>
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']"/>
</t>
</Notebook>`;
@@ -168,7 +168,7 @@ QUnit.module("Form Compiler", (hooks) => {
</form>`;
const expected = /*xml*/ `
<Field id="'display_name'" name="'display_name'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['display_name']"/>
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']"/>
`;
assert.areContentEquivalent(compileTemplate(arch), expected);
@@ -183,7 +183,7 @@ QUnit.module("Form Compiler", (hooks) => {
const expected = /*xml*/ `
<t>
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom"><StatusBarButtons/></div>
<div>someDiv</div>
</div>
@@ -205,7 +205,7 @@ QUnit.module("Form Compiler", (hooks) => {
const expected = /*xml*/ `
<t>
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ this.uiService.size &lt; 6 ? &quot;flex-column&quot; : &quot;flex-nowrap h-100&quot; }} {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ __comp__.uiService.size &lt; 6 ? &quot;flex-column&quot; : &quot;flex-nowrap h-100&quot; }} {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
<div class="o_form_sheet_bg">
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom"><StatusBarButtons/></div>
<div>someDiv</div>
@@ -239,7 +239,7 @@ QUnit.module("Form Compiler", (hooks) => {
const expected = /*xml*/ `
<div class="visible3" />
<div t-if="!this.evalDomainFromRecord(this.props.record,[[&quot;display_name&quot;,&quot;=&quot;,&quot;take&quot;]])" />
<div t-if="!__comp__.evalDomainFromRecord(__comp__.props.record,[[&quot;display_name&quot;,&quot;=&quot;,&quot;take&quot;]])" />
`;
assert.areContentEquivalent(compileTemplate(arch), expected);
@@ -255,7 +255,7 @@ QUnit.module("Form Compiler", (hooks) => {
const expected = /*xml*/ `
<div class="visible3" />
<div t-if="!this.evalDomainFromRecord(this.props.record,&quot;[['display_name','=','take']]&quot;)" />
<div t-if="!__comp__.evalDomainFromRecord(__comp__.props.record,&quot;[['display_name','=','take']]&quot;)" />
`;
assert.areContentEquivalent(compileTemplate(arch), expected);
});
@@ -444,7 +444,7 @@ QUnit.module("Form Renderer", (hooks) => {
const arch = `<myNode modifiers="{&quot;invisible&quot;: [[&quot;field&quot;, &quot;=&quot;, &quot;value&quot;]]}" />`;
const expected = `<t><div class="myNode" t-if="( myCondition or myOtherCondition ) and !this.evalDomainFromRecord(this.props.record,[[&quot;field&quot;,&quot;=&quot;,&quot;value&quot;]])" t-ref="compiled_view_root"/></t>`;
const expected = `<t><div class="myNode" t-if="( myCondition or myOtherCondition ) and !__comp__.evalDomainFromRecord(__comp__.props.record,[[&quot;field&quot;,&quot;=&quot;,&quot;value&quot;]])" t-ref="compiled_view_root"/></t>`;
assert.areEquivalent(compileTemplate(arch), expected);
});
});
@@ -1457,11 +1457,11 @@ QUnit.module("SettingsFormView", (hooks) => {
);
const expectedCompiled = `
<SettingsPage slots="{NoContentHelper:this.props.slots.NoContentHelper}" initialTab="this.props.initialApp" t-slot-scope="settings" modules="[{&quot;key&quot;:&quot;crm&quot;,&quot;string&quot;:&quot;CRM&quot;,&quot;imgurl&quot;:&quot;/crm/static/description/icon.png&quot;}]">
<SettingsPage slots="{NoContentHelper:__comp__.props.slots.NoContentHelper}" initialTab="__comp__.props.initialApp" t-slot-scope="settings" modules="[{&quot;key&quot;:&quot;crm&quot;,&quot;string&quot;:&quot;CRM&quot;,&quot;imgurl&quot;:&quot;/crm/static/description/icon.png&quot;}]">
<SettingsApp key="\`crm\`" string="\`CRM\`" imgurl="\`/crm/static/description/icon.png\`" selectedTab="settings.selectedTab">
<Setting title="\`\`" help="\`\`" companyDependent="false" documentation="\`\`" record="this.props.record" string="\`\`" addLabel="true" labels="[&quot;\`My\\&quot; little ' Label\`&quot;]">
<FormLabel id="'display_name'" fieldName="'display_name'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['display_name']" className="&quot;highhopes&quot;" string="\`My&quot; little ' Label\`"/>
<Field id="'display_name'" name="'display_name'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['display_name']"/>
<Setting title="\`\`" help="\`\`" companyDependent="false" documentation="\`\`" record="__comp__.props.record" string="\`\`" addLabel="true" labels="[&quot;\`My\\&quot; little ' Label\`&quot;]">
<FormLabel id="'display_name'" fieldName="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" className="&quot;highhopes&quot;" string="\`My&quot; little ' Label\`"/>
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']"/>
</Setting>
</SettingsApp>
</SettingsPage>`;
@@ -1502,7 +1502,7 @@ QUnit.module("SettingsFormView", (hooks) => {
const expectedCompiled = `
<HighlightText originalText="\`this is Baz value: \`"/>
<Field id="'baz'" name="'baz'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['baz']"/>
<Field id="'baz'" name="'baz'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['baz']"/>
<HighlightText originalText="\` and this is the after text\`"/>`;
assert.areEquivalent(
compiled.querySelector("Setting div.text-muted").innerHTML,
+8
View File
@@ -34,6 +34,12 @@ _logger = logging.getLogger(__name__)
MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath', 'data-oe-source-id']
# Some views have a js compiler that generates an owl template from the arch. In that template,
# `__comp__` is a reserved keyword giving access to the component instance (e.g. the form renderer
# or the kanban record). However, we don't want to see implementation details leaking in archs, so
# we use the following regex to detect the use of `__comp__` in dynamic attributes, to forbid it.
COMP_REGEX = r'(^|[^\w])\s*__comp__\s*([^\w]|$)'
ref_re = re.compile(r"""
# first match 'form_view_ref' key, backrefs are used to handle single or
# double quoting of the value
@@ -1826,6 +1832,8 @@ actual arch.
elif (attr.startswith("t-")):
self._validate_qweb_directive(node, attr, node_info["view_type"])
if (re.search(COMP_REGEX, expr)):
self._raise_view_error(_("Forbidden use of `__comp__` in arch."), node)
def _validate_classes(self, node, expr):
""" Validate the classes present on node. """
+35 -1
View File
@@ -3,6 +3,7 @@
import ast
import json
import logging
import re
import time
from functools import partial
@@ -16,7 +17,7 @@ from odoo.exceptions import AccessError, ValidationError
from odoo.tests import common, tagged
from odoo.tools import get_cache_key_counter, mute_logger, view_validation
from odoo.addons.base.models.ir_ui_view import (
transfer_field_to_modifiers, transfer_node_to_modifiers, simplify_modifiers,
transfer_field_to_modifiers, transfer_node_to_modifiers, simplify_modifiers, COMP_REGEX
)
_logger = logging.getLogger(__name__)
@@ -3401,6 +3402,17 @@ Forbidden attribute used in arch (t-att-data-tooltip)."""
Forbidden attribute used in arch (t-attf-data-tooltip-template)."""
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_forbidden_use_of___comp___in_kanban(self):
arch = "<kanban><templates><t t-name='kanban-box'>%s</t></templates></kanban>"
self.assertInvalid(
arch % '<t t-esc="__comp__.props.resId"/>',
"""Error while validating view near:
<kanban __validate__="1"><templates><t t-name="kanban-box"><t t-esc="__comp__.props.resId"/></t></templates></kanban>
Forbidden use of `__comp__` in arch."""
)
@tagged('post_install', '-at_install')
class TestDebugger(common.TransactionCase):
@@ -4147,3 +4159,25 @@ class TestRenderAllViews(common.TransactionCase):
_logger.info('Rendered %d views as %s using (best of 5) %ss',
count, self.env.user.name, elapsed)
class CompRegexTest(common.TransactionCase):
def test_comp_regex(self):
self.assertIsNone(re.search(COMP_REGEX, ""))
self.assertIsNone(re.search(COMP_REGEX, "__comp__2"))
self.assertIsNone(re.search(COMP_REGEX, "__comp___that"))
self.assertIsNone(re.search(COMP_REGEX, "a__comp__"))
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__"))
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__ "))
self.assertIsNotNone(re.search(COMP_REGEX, " __comp__ "))
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__.props"))
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__ .props"))
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__['props']"))
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__ ['props']"))
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__[\"props\"]"))
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__ [\"props\"]"))
self.assertIsNotNone(re.search(COMP_REGEX, " __comp__ [\"props\"] "))
self.assertIsNotNone(re.search(COMP_REGEX, "record ? __comp__ : false"))
self.assertIsNotNone(re.search(COMP_REGEX, "!__comp__.props.resId"))
self.assertIsNotNone(re.search(COMP_REGEX, "{{ __comp__ }}"))