/**
* @file base class for policies
* @author Andrew Sayers
*/
/**
* @summary manage an arbitrary policy
* @constructor
* @example
* var policy = new Policy( namespace, {
* // objects:
* v : v, // Variables object
* bb: bb, // BulletinBoard object
* mc: mc, // MiscellaneousCache object
* vi: vi, // Violations object
* // configuration:
* severity_level: { html: 'no action', type: 'none' },
* thread_id : 1234,
* thread_desc : 'thread description',
* loading_html : 'loading, please wait...'
* });
*/
function Policy(args) {
this.v = args.v ;
this.bb = args.bb;
this.promise = jQuery.Deferred().resolve().promise();
this._default_violation = args.mc.violation_groups[0].violations[0].name;
var levels = this.get(this.name.concat([ 'severity levels' ]));
if ( levels.text === null )
this.severity_levels = [{ html: 'no action', type: 'none' }];
else
this.severity_levels = args.v.parse(levels.text, {}, 'array of items' ).map(function(level) {
level = level.value.split(/\s*:\s*/);
return { html: level[0], type: level[1], icon: level[2] };
});
this.severity_level = $.extend( {}, args.severity_level || this.severity_levels[args.severity||0] || this.severity_levels[0] );
this.default_keys = [];
this._widget_args = {
v : args.v,
bb : args.bb,
violations : args.vi,
namespace : this._namespace,
violation_groups: args.mc.violation_groups,
thread_id : args.thread_id || this.resolve_post('mod log'),
thread_desc : args.thread_desc || 'the mod log',
loading_html : args.loading_html
};
this.variable_suffix = [this.severity_level.html];
var policy = this;
Object.keys(this._extra_post_values).forEach(function(value) {
if ( !policy._extra_post_values[value].initialised ) {
policy._extra_post_values[value].initialised = true;
policy._extra_post_values[value].thread_id = policy.resolve_post( policy._extra_post_values[value].thread_id );
}
});
}
Policy.prototype = Object.create(null, {
v : { writable: true, configurable: false },
bb: { writable: true, configurable: false },
promise: { writable: true, configurable: false },
root_action: { writable: true, configurable: false },
has_actions: { writable: true, configurable: false },
_namespace : { writable: false, configurable: false },
name : { writable: false, configurable: false, value: [] },
_widget_args : { writable: true, configurable: false },
default_keys : { writable: true, configurable: false },
variable_suffix: { writable: true, configurable: false },
_default_violation: { writable: true, configurable: false },
severity_levels: { writable: true, configurable: false },
severity_level : { writable: true, configurable: false },
_extra_post_values: {
writable: false,
configurable: false,
value: {
'chase': {
visible : true,
checked : true,
text : 'Make a note to chase this up',
thread_id : 'Moderation Chase-Up Thread', // converted to a number later on
thread_desc: 'the chase-up thread',
initialised: false,
var_prefix : 'chase-up'
},
'ip ban': {
visible : true,
checked : true,
text : 'Request that associated IP address(es) be blocked from accessing the site',
thread_id : 'IP Ban Request Thread', // converted to a number later on
thread_desc: 'the IP Ban request thread',
initialised: false,
var_prefix : 'IP ban'
}
}
}
});
/*
* VARIABLE MANAGEMENT FUNCTIONS
*/
/**
* @summary Build keys to pass to an action
* @param {Object} keys keys to pass
* @protected
*/
Policy.prototype.keys = function(keys) {
return $.extend( this._build_keys([], 'public'), keys );
}
/**
* @summary Get a frequently used thread or post ID
* @param {string} name post name
* @return {string} thread/post ID
*/
Policy.prototype.resolve_post = function(name) { return this.v.resolve('frequently used posts/threads', name ) }
/**
* @summary Resolve a name in the policy's namespace
* @param {string|Array.<string>} names name(s) of variable within the namespace
* @param {Object.<string,*>=} keys keys used to instantiate the variable
* @param {string=} parser parser used for the variable ('string', 'array of items' or 'hash of arrays', default: 'string')
* @return {string} variable
*/
Policy.prototype.resolve = function(name, keys, parser) { return this.v.resolve(this._namespace, name, keys, parser ) }
/**
* @summary Resolve a name in the policy's namespace
* @param {string} name variable name
* @return {string} variable
*/
Policy.prototype.get = function(name) { return this.v.get(this._namespace, name ) }
/**
* @summary Check if a name exists in the policy's namespace
* @param {string} name variable name
* @return {string} variable
*/
Policy.prototype.check = function(name) { return this.v.check(this._namespace, name ) }
/*
* SUMMARY BUILDING FUNCTIONS
*/
Policy.prototype.user_link = function(user) {
if ( !user ) user = this.user;
return '[URL="' + location.origin + this.bb.url_for.user_show({ user_id: user.user_id }) + '"]' + user.username + '[/URL]'
}
Policy.prototype.severity_icon = function() { return location.origin + this.severity_level.icon }
Policy.prototype.severity_text = function() { return ' (' + this.severity_level.html + ': ' + this.severity_level.type + ')' }
/*
* WIDGET HELPER FUNCTIONS
*/
/**
* @summary Build keys to pass to an action
* @param {Array.<Object>} keys keys to pass
* @param {string} context some keys should only be passed in certain contexts
* @protected
* @example
* var args = policy._build_keys([
* { type: 'severity' , value: my_severity_level },
* { type: 'username' , name: 'username' , value: user },
* { type: 'usernames', name: 'usernames', value: all_users },
* { type: 'literal' , name: 'name' , value: 'value' }
* ]);
*/
Policy.prototype._build_keys = function(keys, context) {
var date = new Date().getTime();
var policy = this;
var bb = this.bb;
function user_username(user) { return user.username }
function user_link (user) { return '[URL="' + location.origin + bb.url_for.user_show({ user_id: user.user_id }) + '"]' + user.username + '[/URL]' }
var ret = {
me: bb.user_current().username,
};
[ this.default_keys, keys ].forEach(function(keys) {
keys.forEach(function(key) {
switch ( key.type ) {
case 'severity':
var level = key.value;
ret['severity level' ] = level.html;
ret['infraction type'] = level.type;
break;
case 'image':
ret[key.name + ' URL' ] = key.value;
ret[key.name + ' image'] = '[IMG]' + key.value + '[/IMG]';
break;
case 'username':
ret[key.name ] = user_username(key.value);
ret[key.name + ' with link'] = user_link (key.value);
break;
case 'usernames':
var users = key.value;
ret[ key.name ] = users.filter(function(u) { return u.is_target }).map(user_username)[0];
ret[ key.name + ' with link' ] = users.filter(function(u) { return u.is_target }).map(user_link )[0];
ret[ key.name + 's' ] = users .map(user_username);
ret[ key.name + 's with links'] = users .map(user_link );
ret['primary ' + key.name ] = users.filter(function(u) { return u.is_primary }).map(user_username)[0];
ret['primary ' + key.name + ' with link' ] = users.filter(function(u) { return u.is_primary }).map(user_link )[0];
ret['additional ' + key.name + 's' ] = users.filter(function(u) { return !u.is_primary }).map(user_username);
ret['additional ' + key.name + 's with links'] = users.filter(function(u) { return !u.is_primary }).map(user_link );
ret['other ' + key.name + 's' ] = users.filter(function(u) { return !u.is_target }).map(user_username);
ret['other ' + key.name + 's with links'] = users.filter(function(u) { return !u.is_target }).map(user_link );
break;
case 'thread':
ret[key.name + ' id' ] = key.value.thread_id;
ret[key.name + ' url' ] = bb.url_for.thread_show({ thread_id: key.value.thread_id });
ret[key.name + ' title' ] = key.value.thread_desc;
ret[key.name + ' title with link'] = '[thread=' + key.value.thread_id + ']' + key.value.thread_desc + '[/thread]';
break;
case 'forum':
ret[key.name + ' title' ] = key.value.forum_desc;
ret[key.name + ' title with link'] = '[URL="' + location.origin + bb.url_for.forum_show({ forum_id: key.value.forum_id }) + '"]' + key.value.forum_desc + '[/URL]';
break;
case 'action data':
if ( context == 'widget' ) break;
var action_data = key.value;
if ( action_data.hasOwnProperty('deadline') && action_data.deadline != '' ) {
var deadline = parse_duration(action_data.deadline);
if ( deadline ) {
action_data.deadline = deadline.date.getTime();
ret.deadline = deadline.date.toUTCString().replace(/:[0-9][0-9] /, ' ' );
} else {
alert( 'invalid deadline: ' + action_data.deadline );
throw 'invalid deadline: ' + action_data.deadline;
}
}
ret[key.name] = policy.v.escape(
bb.stringify( 'action data', $.extend( { namespace: policy._namespace, name: policy.name, date: date }, action_data ) )
);
break;
case 'literal':
ret[key.name] = key.value;
break;
}
});
});
return ret;
}
/**
* @summary Build arguments to pass to a widget
* @param {Object} args arguments to pass
* @param {Array.<Object>} keys keys to pass
* @protected
* @example
* var args = this._build_widget_args({foo: 'bar'}, [
* { type: 'severity' , value: my_severity_level },
* { type: 'username' , name: 'username' , value: user },
* { type: 'usernames', name: 'usernames', value: all_users },
* { type: 'literal', name: 'name' , value: 'value' }
* ], Array.prototype.slice.call( arguments, 1 ));
*/
Policy.prototype._build_widget_args = function(args, keys, extra_args) {
return $.extend.apply(
$,
[
{ keys: this._build_keys(keys, 'widget') },
this._widget_args,
args
].concat(extra_args)
);
}
/*
* EXTRA POST WIDGETS
*/
/**
* @summary Build values to initialise or update an extra_post widget
* @param {Object=} level severity level
* @param {...Object} more arguments
* @protected
* @return {Object} extra_post arguments
*/
Policy.prototype.extra_post_args = function(level) {
if ( !level ) level = this.severity_level;
var extra_data = this.get( this.name.concat([ 'extra action', level.html ]) ).text;
var ret;
if ( extra_data === null )
ret = { visible: false, checked: false, text: '' }
else
ret = this._extra_post_values[extra_data.toLowerCase()] || { visible: false, checked: false, text: '' };
ret. title_variable = this.name.concat([ ret.var_prefix + ' post title' ]);
ret.bbcode_variable = this.name.concat([ ret.var_prefix + ' post bbcode' ]);
return this._build_widget_args( ret, [{ type: 'severity' , value: level }], Array.prototype.slice.call( arguments, 1 ) );
}
/**
* @summary Build an action for an extra_post widget
* @param {Object=} level severity level
* @param {...Object} more arguments
* @protected
* @return {Action}
*/
Policy.prototype.extra_post_action = function(widget) {
var value = widget.val();
return new Action(
this.name.join(': ') + ' extra post wrapper',
( value.checked && value.text == this._extra_post_values['ip ban'].text )
? new IPAddressReport({ bb: this.bb, users: [this.user] }).action().then(widget.action())
: widget.action()
);
}
/*
* NOTIFICATION SELECTOR WIDGETS
*/
/**
* @summary Build values to initialise or update a notification_selector widget
* @param {Object=} level severity level
* @param {...Object} more arguments
* @protected
* @return {Object} notification_selector arguments
*/
Policy.prototype.notification_selector_args = function(level) {
var title_suffix = ' title';
if ( !level ) level = this.severity_level;
if ( level.type == 'infraction' || level.type == 'warning' ) title_suffix = ' administrative note';
var default_violation = this.get( this.name.concat([ 'default violation' ]).concat(this.variable_suffix) );
return this._build_widget_args({
level: level.type,
title_variable: this.name.concat([ level.type + title_suffix ]).concat(this.variable_suffix),
ban_variable: this.name.concat([ 'ban reason' ]).concat(this.variable_suffix),
bbcode_variable: this.name.concat([ level.type + ' bbcode' ]).concat(this.variable_suffix),
note_title_variable: this.name.concat([ 'note' + ' title' ]).concat(this.variable_suffix),
note_bbcode_variable: this.name.concat([ 'note' + ' bbcode' ]).concat(this.variable_suffix),
show_violations: default_violation.text !== null,
violation : ( default_violation.text === null ) ? this._default_violation : this.v.parse( default_violation.text ),
user: this.user
}, [{ type: 'severity' , value: level }], Array.prototype.slice.call( arguments, 1 ) );
}
/**
* @summary Build an action for a notification_selector widget
* @param {Object=} level severity level
* @param {...Object} more arguments
* @protected
* @return {Action}
*/
Policy.prototype.notification_selector_action = function(widget) { return widget.action() }
/*
* DEADLINE WIDGETS
*/
/**
* @summary Build values to initialise or update a deadline widget
* @param {Object=} level severity level
* @param {...Object} more arguments
* @protected
* @return {Object} deadline arguments
*/
Policy.prototype.deadline_args = function() {
var deadlines = this.resolve( ['deadlines'].concat(this.variable_suffix), {}, 'array of items' ).map(function(deadline) {
deadline = deadline.value.split(/\s*:\s*/, 2 );
return { value: deadline[0], text: deadline[1] };
});
return this._build_widget_args({
deadline : this.check( ['deadline'].concat(this.variable_suffix) ) ? this.resolve( ['deadline'].concat(this.variable_suffix) ) : '',
deadlines: deadlines,
title_variable: ['deadline post title' ].concat(this.variable_suffix),
bbcode_variable: ['deadline post bbcode'].concat(this.variable_suffix)
}, [], Array.prototype.slice.call( arguments, 1 ));
}
/**
* @summary Build an action for a deadline widget
* @param {Object=} level severity level
* @param {...Object} more arguments
* @protected
* @return {Action}
*/
Policy.prototype.deadline_action = function(widget) { return widget.action() }
/*
* SEVERITY SLIDER WIDGETS
*/
/**
* @summary Build values to initialise or update a severity_slider widget
* @param {Object=} level severity level
* @param {...Object} more arguments
* @protected
* @return {Object} severity_slider arguments
*/
Policy.prototype.severity_slider_args = function(level) {
return this._build_widget_args({
levels: this.severity_levels,
value : level || this.severity_level.html
}, [], Array.prototype.slice.call( arguments, 1 ));
}
/*
* DUPLICATE ACCOUNT LIST WIDGETS
*/
/**
* @summary Build values to initialise or update a duplicate_account_list widget
* @param {Object=} level severity level
* @param {...Object} more arguments
* @protected
* @return {Object} duplicate_account_list arguments
*/
Policy.prototype.duplicate_account_list_args = function(level) {
return this._build_widget_args({}, [], Array.prototype.slice.call( arguments, 1 ));
}
/*
* COMMON NON-WIDGET-RELATED ACTIONS
*/
/**
* @summary Close the thread targeted by this policy
* @protected
* @return {Action}
*/
Policy.prototype.close_thread = function() {
var policy = this;
return new Action( 'close thread', {
fire: function() {
return policy.bb.thread_openclose( policy._widget_args.thread_id, false );
},
description: function() {
return [
{
type: 'close',
target: { thread_id: policy._widget_args.thread_id, thread_desc: policy._widget_args.thread_desc }
},
];
},
})
}