client/tags: improve privilege checking

- Hide fields that are uneditable, rather than disabling them
- Support fragmented edit privileges (e.g. roles than can edit only some
  aspects of tags) - up until now the client tried to send everything at
  once, which resulted in errors for such cases.
This commit is contained in:
rr- 2016-07-26 20:49:48 +02:00
parent b378ce7ede
commit 7022686b77
6 changed files with 122 additions and 46 deletions

View File

@ -3,23 +3,51 @@
<div class='input'> <div class='input'>
<ul> <ul>
<li class='names'> <li class='names'>
<%= ctx.makeTextInput({text: 'Names', value: ctx.tag.names.join(' '), required: true, readonly: !ctx.canEditNames}) %> <% if (ctx.canEditNames) { %>
<%= ctx.makeTextInput({
text: 'Names',
value: ctx.tag.names.join(' '),
required: true,
}) %>
<% } %>
</li> </li>
<li class='category'> <li class='category'>
<%= ctx.makeSelect({text: 'Category', keyValues: ctx.categories, selectedKey: ctx.tag.category, required: true, readonly: !ctx.canEditCategory}) %> <% if (ctx.canEditCategory) { %>
<%= ctx.makeSelect({
text: 'Category',
keyValues: ctx.categories,
selectedKey: ctx.tag.category,
required: true,
}) %>
<% } %>
</li> </li>
<li class='implications'> <li class='implications'>
<%= ctx.makeTextInput({text: 'Implications', value: ctx.tag.implications.join(' '), readonly: !ctx.canEditImplications}) %> <% if (ctx.canEditImplications) { %>
<%= ctx.makeTextInput({
text: 'Implications',
value: ctx.tag.implications.join(' '),
}) %>
<% } %>
</li> </li>
<li class='suggestions'> <li class='suggestions'>
<%= ctx.makeTextInput({text: 'Suggestions', value: ctx.tag.suggestions.join(' '), readonly: !ctx.canEditSuggestions}) %> <% if (ctx.canEditSuggestions) { %>
<%= ctx.makeTextInput({
text: 'Suggestions',
value: ctx.tag.suggestions.join(' '),
}) %>
<% } %>
</li> </li>
<li class='description'> <li class='description'>
<%= ctx.makeTextarea({text: 'Description', value: ctx.tag.description, readonly: !ctx.canEditDescription}) %> <% if (ctx.canEditDescription) { %>
<%= ctx.makeTextarea({
text: 'Description',
value: ctx.tag.description,
}) %>
<% } %>
</li> </li>
</ul> </ul>
</div> </div>
<% if (ctx.canEditNames || ctx.canEditCategory || ctx.canEditImplications || ctx.canEditSuggestions) { %> <% if (ctx.canEditAnything) { %>
<div class='messages'></div> <div class='messages'></div>
<div class='buttons'> <div class='buttons'>
<input type='submit' class='save' value='Save changes'> <input type='submit' class='save' value='Save changes'>

View File

@ -25,6 +25,7 @@ class TagController {
this._view = new TagView({ this._view = new TagView({
tag: tag, tag: tag,
section: section, section: section,
canEditAnything: api.hasPrivilege('tags:edit'),
canEditNames: api.hasPrivilege('tags:edit:names'), canEditNames: api.hasPrivilege('tags:edit:names'),
canEditCategory: api.hasPrivilege('tags:edit:category'), canEditCategory: api.hasPrivilege('tags:edit:category'),
canEditImplications: api.hasPrivilege('tags:edit:implications'), canEditImplications: api.hasPrivilege('tags:edit:implications'),
@ -53,11 +54,21 @@ class TagController {
_evtChange(e) { _evtChange(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
e.detail.tag.names = e.detail.names; if (e.detail.names !== undefined) {
e.detail.tag.category = e.detail.category; e.detail.tag.names = e.detail.names;
e.detail.tag.implications = e.detail.implications; }
e.detail.tag.suggestions = e.detail.suggestions; if (e.detail.category !== undefined) {
e.detail.tag.description = e.detail.description; e.detail.tag.category = e.detail.category;
}
if (e.detail.implications !== undefined) {
e.detail.tag.implications = e.detail.implications;
}
if (e.detail.suggestions !== undefined) {
e.detail.tag.suggestions = e.detail.suggestions;
}
if (e.detail.description !== undefined) {
e.detail.tag.description = e.detail.description;
}
e.detail.tag.save().then(() => { e.detail.tag.save().then(() => {
this._view.showSuccess('Tag saved.'); this._view.showSuccess('Tag saved.');
this._view.enableForm(); this._view.enableForm();

View File

@ -4,12 +4,7 @@ const api = require('../api.js');
const tags = require('../tags.js'); const tags = require('../tags.js');
const events = require('../events.js'); const events = require('../events.js');
const CommentList = require('./comment_list.js'); const CommentList = require('./comment_list.js');
const misc = require('../util/misc.js');
function _arraysDiffer(source1, source2) {
return (
[...source1].filter(value => !source2.includes(value)).length > 0 ||
[...source2].filter(value => !source1.includes(value)).length > 0);
}
class Post extends events.EventTarget { class Post extends events.EventTarget {
constructor() { constructor() {
@ -111,13 +106,13 @@ class Post extends events.EventTarget {
if (this._safety !== this._orig._safety) { if (this._safety !== this._orig._safety) {
detail.safety = this._safety; detail.safety = this._safety;
} }
if (_arraysDiffer(this._flags, this._orig._flags)) { if (misc.arraysDiffer(this._flags, this._orig._flags)) {
detail.flags = this._flags; detail.flags = this._flags;
} }
if (_arraysDiffer(this._tags, this._orig._tags)) { if (misc.arraysDiffer(this._tags, this._orig._tags)) {
detail.tags = this._tags; detail.tags = this._tags;
} }
if (_arraysDiffer(this._relations, this._orig._relations)) { if (misc.arraysDiffer(this._relations, this._orig._relations)) {
detail.relations = this._relations; detail.relations = this._relations;
} }

View File

@ -2,16 +2,21 @@
const api = require('../api.js'); const api = require('../api.js');
const events = require('../events.js'); const events = require('../events.js');
const misc = require('../util/misc.js');
class Tag extends events.EventTarget { class Tag extends events.EventTarget {
constructor() { constructor() {
super(); super();
this._orig = {};
this._origName = null; this._origName = null;
this._names = null;
this._category = null; this._category = null;
this._description = null; this._description = null;
this._suggestions = null;
this._implications = null; this._names = [];
this._suggestions = [];
this._implications = [];
this._postCount = null; this._postCount = null;
this._creationTime = null; this._creationTime = null;
this._lastEditTime = null; this._lastEditTime = null;
@ -48,13 +53,25 @@ class Tag extends events.EventTarget {
} }
save() { save() {
const detail = { const detail = {};
names: this.names,
category: this.category, // send only changed fields to avoid user privilege violation
description: this.description, if (misc.arraysDiffer(this._names, this._orig._names)) {
implications: this.implications, detail.names = this._names;
suggestions: this.suggestions, }
}; if (this._category !== this._orig._category) {
detail.category = this._category;
}
if (this._description !== this._orig._description) {
detail.description = this._description;
}
if (misc.arraysDiffer(this._implications, this._orig._implications)) {
detail.implications = this._implications;
}
if (misc.arraysDiffer(this._suggestions, this._orig._suggestions)) {
detail.suggestions = this._suggestions;
}
let promise = this._origName ? let promise = this._origName ?
api.put('/tag/' + this._origName, detail) : api.put('/tag/' + this._origName, detail) :
api.post('/tags', detail); api.post('/tags', detail);
@ -104,15 +121,20 @@ class Tag extends events.EventTarget {
} }
_updateFromResponse(response) { _updateFromResponse(response) {
this._origName = response.names ? response.names[0] : null; const map = {
this._names = response.names; _origName: response.names ? response.names[0] : null,
this._category = response.category; _names: response.names,
this._description = response.description; _category: response.category,
this._implications = response.implications; _description: response.description,
this._suggestions = response.suggestions; _implications: response.implications,
this._creationTime = response.creationTime; _suggestions: response.suggestions,
this._lastEditTime = response.lastEditTime; _creationTime: response.creationTime,
this._postCount = response.usages; _lastEditTime: response.lastEditTime,
_postCount: response.usages,
};
Object.assign(this, map);
Object.assign(this._orig, map);
} }
}; };

View File

@ -244,6 +244,12 @@ function escapeHtml(unsafe) {
.replace(/'/g, '&apos;'); .replace(/'/g, '&apos;');
} }
function arraysDiffer(source1, source2) {
return (
[...source1].filter(value => !source2.includes(value)).length > 0 ||
[...source2].filter(value => !source1.includes(value)).length > 0);
}
module.exports = { module.exports = {
range: range, range: range,
formatUrlParameters: formatUrlParameters, formatUrlParameters: formatUrlParameters,
@ -259,4 +265,5 @@ module.exports = {
escapeHtml: escapeHtml, escapeHtml: escapeHtml,
makeCssName: makeCssName, makeCssName: makeCssName,
splitByWhitespace: splitByWhitespace, splitByWhitespace: splitByWhitespace,
arraysDiffer: arraysDiffer,
}; };

View File

@ -79,13 +79,26 @@ class TagEditView extends events.EventTarget {
this.dispatchEvent(new CustomEvent('submit', { this.dispatchEvent(new CustomEvent('submit', {
detail: { detail: {
tag: this._tag, tag: this._tag,
names: misc.splitByWhitespace(this._namesFieldNode.value),
category: this._categoryFieldNode.value, names: this._namesFieldNode ?
implications: misc.splitByWhitespace( misc.splitByWhitespace(this._namesFieldNode.value) :
this._implicationsFieldNode.value), undefined,
suggestions: misc.splitByWhitespace(
this._suggestionsFieldNode.value), category: this._categoryFieldNode ?
description: this._descriptionFieldNode.value, this._categoryFieldNode.value :
undefined,
implications: this._implicationsFieldNode ?
misc.splitByWhitespace(this._implicationsFieldNode.value) :
undefined,
suggestions: this._suggestionsFieldNode ?
misc.splitByWhitespace(this._suggestionsFieldNode.value) :
undefined,
description: this._descriptionFieldNode ?
this._descriptionFieldNode.value :
undefined,
}, },
})); }));
} }