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'>
<ul>
<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 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 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 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 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>
</ul>
</div>
<% if (ctx.canEditNames || ctx.canEditCategory || ctx.canEditImplications || ctx.canEditSuggestions) { %>
<% if (ctx.canEditAnything) { %>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' class='save' value='Save changes'>

View file

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

View file

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

View file

@ -2,16 +2,21 @@
const api = require('../api.js');
const events = require('../events.js');
const misc = require('../util/misc.js');
class Tag extends events.EventTarget {
constructor() {
super();
this._orig = {};
this._origName = null;
this._names = null;
this._category = null;
this._description = null;
this._suggestions = null;
this._implications = null;
this._names = [];
this._suggestions = [];
this._implications = [];
this._postCount = null;
this._creationTime = null;
this._lastEditTime = null;
@ -48,13 +53,25 @@ class Tag extends events.EventTarget {
}
save() {
const detail = {
names: this.names,
category: this.category,
description: this.description,
implications: this.implications,
suggestions: this.suggestions,
};
const detail = {};
// send only changed fields to avoid user privilege violation
if (misc.arraysDiffer(this._names, this._orig._names)) {
detail.names = this._names;
}
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 ?
api.put('/tag/' + this._origName, detail) :
api.post('/tags', detail);
@ -104,15 +121,20 @@ class Tag extends events.EventTarget {
}
_updateFromResponse(response) {
this._origName = response.names ? response.names[0] : null;
this._names = response.names;
this._category = response.category;
this._description = response.description;
this._implications = response.implications;
this._suggestions = response.suggestions;
this._creationTime = response.creationTime;
this._lastEditTime = response.lastEditTime;
this._postCount = response.usages;
const map = {
_origName: response.names ? response.names[0] : null,
_names: response.names,
_category: response.category,
_description: response.description,
_implications: response.implications,
_suggestions: response.suggestions,
_creationTime: response.creationTime,
_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;');
}
function arraysDiffer(source1, source2) {
return (
[...source1].filter(value => !source2.includes(value)).length > 0 ||
[...source2].filter(value => !source1.includes(value)).length > 0);
}
module.exports = {
range: range,
formatUrlParameters: formatUrlParameters,
@ -259,4 +265,5 @@ module.exports = {
escapeHtml: escapeHtml,
makeCssName: makeCssName,
splitByWhitespace: splitByWhitespace,
arraysDiffer: arraysDiffer,
};

View file

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