more refactors

v1.18.x
barisusakli 8 years ago
parent bc8d297377
commit b1b87d339f

@ -13,9 +13,8 @@ var privileges = require('../privileges');
var plugins = require('../plugins'); var plugins = require('../plugins');
var widgets = require('../widgets'); var widgets = require('../widgets');
var translator = require('../../public/src/modules/translator'); var translator = require('../../public/src/modules/translator');
var accountHelpers = require('../controllers/accounts/helpers');
var apiController = {}; var apiController = module.exports;
apiController.getConfig = function (req, res, next) { apiController.getConfig = function (req, res, next) {
var config = {}; var config = {};
@ -220,92 +219,6 @@ apiController.getObject = function (req, res, next) {
}); });
}; };
apiController.getCurrentUser = function (req, res, next) {
if (!req.uid) {
return res.status(401).json('not-authorized');
}
async.waterfall([
function (next) {
user.getUserField(req.uid, 'userslug', next);
},
function (userslug, next) {
accountHelpers.getUserDataByUserSlug(userslug, req.uid, next);
},
], function (err, userData) {
if (err) {
return next(err);
}
res.json(userData);
});
};
apiController.getUserByUID = function (req, res, next) {
byType('uid', req, res, next);
};
apiController.getUserByUsername = function (req, res, next) {
byType('username', req, res, next);
};
apiController.getUserByEmail = function (req, res, next) {
byType('email', req, res, next);
};
function byType(type, req, res, next) {
apiController.getUserDataByField(req.uid, type, req.params[type], function (err, data) {
if (err || !data) {
return next(err);
}
res.json(data);
});
}
apiController.getUserDataByField = function (callerUid, field, fieldValue, callback) {
async.waterfall([
function (next) {
if (field === 'uid') {
next(null, fieldValue);
} else if (field === 'username') {
user.getUidByUsername(fieldValue, next);
} else if (field === 'email') {
user.getUidByEmail(fieldValue, next);
} else {
next();
}
},
function (uid, next) {
if (!uid) {
return next();
}
apiController.getUserDataByUID(callerUid, uid, next);
},
], callback);
};
apiController.getUserDataByUID = function (callerUid, uid, callback) {
if (!parseInt(callerUid, 10) && parseInt(meta.config.privateUserInfo, 10) === 1) {
return callback(new Error('[[error:no-privileges]]'));
}
if (!parseInt(uid, 10)) {
return callback(new Error('[[error:no-user]]'));
}
async.parallel({
userData: async.apply(user.getUserData, uid),
settings: async.apply(user.getSettings, uid),
}, function (err, results) {
if (err || !results.userData) {
return callback(err || new Error('[[error:no-user]]'));
}
results.userData.email = results.settings.showemail ? results.userData.email : undefined;
results.userData.fullname = results.settings.showfullname ? results.userData.fullname : undefined;
callback(null, results.userData);
});
};
apiController.getModerators = function (req, res, next) { apiController.getModerators = function (req, res, next) {
categories.getModerators(req.params.cid, function (err, moderators) { categories.getModerators(req.params.cid, function (err, moderators) {
if (err) { if (err) {
@ -314,16 +227,3 @@ apiController.getModerators = function (req, res, next) {
res.json({ moderators: moderators }); res.json({ moderators: moderators });
}); });
}; };
apiController.getRecentPosts = function (req, res, next) {
posts.getRecentPosts(req.uid, 0, 19, req.params.term, function (err, data) {
if (err) {
return next(err);
}
res.json(data);
});
};
module.exports = apiController;

@ -20,6 +20,7 @@ Controllers.recent = require('./recent');
Controllers.popular = require('./popular'); Controllers.popular = require('./popular');
Controllers.tags = require('./tags'); Controllers.tags = require('./tags');
Controllers.search = require('./search'); Controllers.search = require('./search');
Controllers.user = require('./user');
Controllers.users = require('./users'); Controllers.users = require('./users');
Controllers.groups = require('./groups'); Controllers.groups = require('./groups');
Controllers.accounts = require('./accounts'); Controllers.accounts = require('./accounts');

@ -1,24 +1,38 @@
'use strict'; 'use strict';
var async = require('async');
var posts = require('../posts'); var posts = require('../posts');
var helpers = require('./helpers'); var helpers = require('./helpers');
var postsController = {}; var postsController = module.exports;
postsController.redirectToPost = function (req, res, callback) { postsController.redirectToPost = function (req, res, next) {
var pid = parseInt(req.params.pid, 10); var pid = parseInt(req.params.pid, 10);
if (!pid) { if (!pid) {
return callback(); return next();
} }
posts.generatePostPath(pid, req.uid, function (err, path) { async.waterfall([
if (err || !path) { function (next) {
return callback(err); posts.generatePostPath(pid, req.uid, next);
},
function (path, next) {
if (!path) {
return next();
} }
helpers.redirect(res, path); helpers.redirect(res, path);
}); },
], next);
}; };
postsController.getRecentPosts = function (req, res, next) {
module.exports = postsController; async.waterfall([
function (next) {
posts.getRecentPosts(req.uid, 0, 19, req.params.term, next);
},
function (data) {
res.json(data);
},
], next);
};

@ -0,0 +1,94 @@
'use strict';
var async = require('async');
var user = require('../user');
var meta = require('../meta');
var accountHelpers = require('./accounts/helpers');
var userController = module.exports;
userController.getCurrentUser = function (req, res, next) {
if (!req.uid) {
return res.status(401).json('not-authorized');
}
async.waterfall([
function (next) {
user.getUserField(req.uid, 'userslug', next);
},
function (userslug, next) {
accountHelpers.getUserDataByUserSlug(userslug, req.uid, next);
},
function (userData) {
res.json(userData);
},
], next);
};
userController.getUserByUID = function (req, res, next) {
byType('uid', req, res, next);
};
userController.getUserByUsername = function (req, res, next) {
byType('username', req, res, next);
};
userController.getUserByEmail = function (req, res, next) {
byType('email', req, res, next);
};
function byType(type, req, res, next) {
userController.getUserDataByField(req.uid, type, req.params[type], function (err, data) {
if (err || !data) {
return next(err);
}
res.json(data);
});
}
userController.getUserDataByField = function (callerUid, field, fieldValue, callback) {
async.waterfall([
function (next) {
if (field === 'uid') {
next(null, fieldValue);
} else if (field === 'username') {
user.getUidByUsername(fieldValue, next);
} else if (field === 'email') {
user.getUidByEmail(fieldValue, next);
} else {
next();
}
},
function (uid, next) {
if (!uid) {
return next();
}
userController.getUserDataByUID(callerUid, uid, next);
},
], callback);
};
userController.getUserDataByUID = function (callerUid, uid, callback) {
if (!parseInt(callerUid, 10) && parseInt(meta.config.privateUserInfo, 10) === 1) {
return callback(new Error('[[error:no-privileges]]'));
}
if (!parseInt(uid, 10)) {
return callback(new Error('[[error:no-user]]'));
}
async.parallel({
userData: async.apply(user.getUserData, uid),
settings: async.apply(user.getSettings, uid),
}, function (err, results) {
if (err || !results.userData) {
return callback(err || new Error('[[error:no-user]]'));
}
results.userData.email = results.settings.showemail ? results.userData.email : undefined;
results.userData.fullname = results.settings.showfullname ? results.userData.fullname : undefined;
callback(null, results.userData);
});
};

@ -11,17 +11,17 @@ module.exports = function (app, middleware, controllers) {
router.get('/config', middleware.applyCSRF, controllers.api.getConfig); router.get('/config', middleware.applyCSRF, controllers.api.getConfig);
router.get('/widgets/render', controllers.api.renderWidgets); router.get('/widgets/render', controllers.api.renderWidgets);
router.get('/me', middleware.checkGlobalPrivacySettings, controllers.api.getCurrentUser); router.get('/me', middleware.checkGlobalPrivacySettings, controllers.user.getCurrentUser);
router.get('/user/uid/:uid', middleware.checkGlobalPrivacySettings, controllers.api.getUserByUID); router.get('/user/uid/:uid', middleware.checkGlobalPrivacySettings, controllers.user.getUserByUID);
router.get('/user/username/:username', middleware.checkGlobalPrivacySettings, controllers.api.getUserByUsername); router.get('/user/username/:username', middleware.checkGlobalPrivacySettings, controllers.user.getUserByUsername);
router.get('/user/email/:email', middleware.checkGlobalPrivacySettings, controllers.api.getUserByEmail); router.get('/user/email/:email', middleware.checkGlobalPrivacySettings, controllers.user.getUserByEmail);
router.get('/:type/pid/:id', controllers.api.getObject); router.get('/:type/pid/:id', controllers.api.getObject);
router.get('/:type/tid/:id', controllers.api.getObject); router.get('/:type/tid/:id', controllers.api.getObject);
router.get('/:type/cid/:id', controllers.api.getObject); router.get('/:type/cid/:id', controllers.api.getObject);
router.get('/categories/:cid/moderators', controllers.api.getModerators); router.get('/categories/:cid/moderators', controllers.api.getModerators);
router.get('/recent/posts/:term?', controllers.api.getRecentPosts); router.get('/recent/posts/:term?', controllers.posts.getRecentPosts);
router.get('/unread/:filter?/total', middleware.authenticate, controllers.unread.unreadTotal); router.get('/unread/:filter?/total', middleware.authenticate, controllers.unread.unreadTotal);
router.get('/topic/teaser/:topic_id', controllers.topics.teaser); router.get('/topic/teaser/:topic_id', controllers.topics.teaser);
router.get('/topic/pagination/:topic_id', controllers.topics.pagination); router.get('/topic/pagination/:topic_id', controllers.topics.pagination);

@ -12,7 +12,7 @@ var meta = require('../meta');
var events = require('../events'); var events = require('../events');
var emailer = require('../emailer'); var emailer = require('../emailer');
var db = require('../database'); var db = require('../database');
var apiController = require('../controllers/api'); var userController = require('../controllers/user');
var privileges = require('../privileges'); var privileges = require('../privileges');
var SocketUser = {}; var SocketUser = {};
@ -303,15 +303,15 @@ SocketUser.invite = function (socket, email, callback) {
}; };
SocketUser.getUserByUID = function (socket, uid, callback) { SocketUser.getUserByUID = function (socket, uid, callback) {
apiController.getUserDataByField(socket.uid, 'uid', uid, callback); userController.getUserDataByField(socket.uid, 'uid', uid, callback);
}; };
SocketUser.getUserByUsername = function (socket, username, callback) { SocketUser.getUserByUsername = function (socket, username, callback) {
apiController.getUserDataByField(socket.uid, 'username', username, callback); userController.getUserDataByField(socket.uid, 'username', username, callback);
}; };
SocketUser.getUserByEmail = function (socket, email, callback) { SocketUser.getUserByEmail = function (socket, email, callback) {
apiController.getUserDataByField(socket.uid, 'email', email, callback); userController.getUserDataByField(socket.uid, 'email', email, callback);
}; };
SocketUser.setModerationNote = function (socket, data, callback) { SocketUser.setModerationNote = function (socket, data, callback) {

@ -6,11 +6,11 @@ var _ = require('underscore');
var groups = require('./groups'); var groups = require('./groups');
var plugins = require('./plugins'); var plugins = require('./plugins');
var db = require('./database'); var db = require('./database');
var topics = require('./topics');
var privileges = require('./privileges'); var privileges = require('./privileges');
var meta = require('./meta'); var meta = require('./meta');
(function (User) { var User = module.exports;
User.email = require('./user/email'); User.email = require('./user/email');
User.notifications = require('./user/notifications'); User.notifications = require('./user/notifications');
User.reset = require('./user/reset'); User.reset = require('./user/reset');
@ -35,39 +35,7 @@ var meta = require('./meta');
require('./user/invite')(User); require('./user/invite')(User);
require('./user/password')(User); require('./user/password')(User);
require('./user/info')(User); require('./user/info')(User);
require('./user/online')(User);
User.updateLastOnlineTime = function (uid, callback) {
callback = callback || function () {};
db.getObjectFields('user:' + uid, ['status', 'lastonline'], function (err, userData) {
var now = Date.now();
if (err || userData.status === 'offline' || now - parseInt(userData.lastonline, 10) < 300000) {
return callback(err);
}
User.setUserField(uid, 'lastonline', now, callback);
});
};
User.updateOnlineUsers = function (uid, callback) {
callback = callback || function () {};
var now = Date.now();
async.waterfall([
function (next) {
db.sortedSetScore('users:online', uid, next);
},
function (userOnlineTime, next) {
if (now - parseInt(userOnlineTime, 10) < 300000) {
return callback();
}
db.sortedSetAdd('users:online', now, uid, next);
},
function (next) {
topics.pushUnreadCount(uid);
plugins.fireHook('action:user.online', { uid: uid, timestamp: now });
next();
},
], callback);
};
User.getUidsFromSet = function (set, start, stop, callback) { User.getUidsFromSet = function (set, start, stop, callback) {
if (set === 'users:online') { if (set === 'users:online') {
@ -140,29 +108,6 @@ var meta = require('./meta');
return isOnline ? (userData.status || 'online') : 'offline'; return isOnline ? (userData.status || 'online') : 'offline';
}; };
User.isOnline = function (uid, callback) {
if (Array.isArray(uid)) {
db.sortedSetScores('users:online', uid, function (err, lastonline) {
if (err) {
return callback(err);
}
var now = Date.now();
var isOnline = uid.map(function (uid, index) {
return now - lastonline[index] < 300000;
});
callback(null, isOnline);
});
} else {
db.sortedSetScore('users:online', uid, function (err, lastonline) {
if (err) {
return callback(err);
}
var isOnline = Date.now() - parseInt(lastonline, 10) < 300000;
callback(null, isOnline);
});
}
};
User.exists = function (uid, callback) { User.exists = function (uid, callback) {
db.isSortedSetMember('users:joindate', uid, callback); db.isSortedSetMember('users:joindate', uid, callback);
}; };
@ -192,17 +137,18 @@ var meta = require('./meta');
}; };
User.getUsernamesByUids = function (uids, callback) { User.getUsernamesByUids = function (uids, callback) {
User.getUsersFields(uids, ['username'], function (err, users) { async.waterfall([
if (err) { function (next) {
return callback(err); User.getUsersFields(uids, ['username'], next);
} },
function (users, next) {
users = users.map(function (user) { users = users.map(function (user) {
return user.username; return user.username;
}); });
callback(null, users); next(null, users);
}); },
], callback);
}; };
User.getUsernameByUserslug = function (slug, callback) { User.getUsernameByUserslug = function (slug, callback) {
@ -228,12 +174,14 @@ var meta = require('./meta');
}; };
User.getUsernameByEmail = function (email, callback) { User.getUsernameByEmail = function (email, callback) {
db.sortedSetScore('email:uid', email.toLowerCase(), function (err, uid) { async.waterfall([
if (err) { function (next) {
return callback(err); db.sortedSetScore('email:uid', email.toLowerCase(), next);
} },
User.getUserField(uid, 'username', callback); function (uid, next) {
}); User.getUserField(uid, 'username', next);
},
], callback);
}; };
User.isModerator = function (uid, cid, callback) { User.isModerator = function (uid, cid, callback) {
@ -264,56 +212,57 @@ var meta = require('./meta');
}; };
User.isAdminOrSelf = function (callerUid, uid, callback) { User.isAdminOrSelf = function (callerUid, uid, callback) {
if (parseInt(callerUid, 10) === parseInt(uid, 10)) { isSelfOrMethod(callerUid, uid, User.isAdministrator, callback);
return callback();
}
User.isAdministrator(callerUid, function (err, isAdmin) {
if (err || !isAdmin) {
return callback(err || new Error('[[error:no-privileges]]'));
}
callback();
});
}; };
User.isAdminOrGlobalModOrSelf = function (callerUid, uid, callback) { User.isAdminOrGlobalModOrSelf = function (callerUid, uid, callback) {
isSelfOrMethod(callerUid, uid, User.isAdminOrGlobalMod, callback);
};
function isSelfOrMethod(callerUid, uid, method, callback) {
if (parseInt(callerUid, 10) === parseInt(uid, 10)) { if (parseInt(callerUid, 10) === parseInt(uid, 10)) {
return callback(); return callback();
} }
User.isAdminOrGlobalMod(callerUid, function (err, isAdminOrGlobalMod) { async.waterfall([
if (err || !isAdminOrGlobalMod) { function (next) {
return callback(err || new Error('[[error:no-privileges]]')); method(callerUid, next);
},
function (isPass, next) {
if (!isPass) {
return next(new Error('[[error:no-privileges]]'));
}
next();
},
], callback);
} }
callback();
});
};
User.getAdminsandGlobalMods = function (callback) { User.getAdminsandGlobalMods = function (callback) {
async.parallel({ async.waterfall([
admins: async.apply(groups.getMembers, 'administrators', 0, -1), function (next) {
mods: async.apply(groups.getMembers, 'Global Moderators', 0, -1), async.parallel([
}, function (err, results) { async.apply(groups.getMembers, 'administrators', 0, -1),
if (err) { async.apply(groups.getMembers, 'Global Moderators', 0, -1),
return callback(err); ], next);
} },
var uids = results.admins.concat(results.mods).filter(function (uid, index, array) { function (results, next) {
return uid && array.indexOf(uid) === index; User.getUsersData(_.union(results), next);
}); },
User.getUsersData(uids, callback); ], callback);
});
}; };
User.getAdminsandGlobalModsandModerators = function (callback) { User.getAdminsandGlobalModsandModerators = function (callback) {
async.waterfall([
function (next) {
async.parallel([ async.parallel([
async.apply(groups.getMembers, 'administrators', 0, -1), async.apply(groups.getMembers, 'administrators', 0, -1),
async.apply(groups.getMembers, 'Global Moderators', 0, -1), async.apply(groups.getMembers, 'Global Moderators', 0, -1),
async.apply(User.getModeratorUids), async.apply(User.getModeratorUids),
], function (err, results) { ], next);
if (err) { },
return callback(err); function (results, next) {
} User.getUsersData(_.union.apply(_, results), next);
},
User.getUsersData(_.union.apply(_, results), callback); ], callback);
});
}; };
User.getModeratorUids = function (callback) { User.getModeratorUids = function (callback) {
@ -324,13 +273,10 @@ var meta = require('./meta');
return 'cid:' + cid + ':privileges:mods'; return 'cid:' + cid + ':privileges:mods';
}); });
groups.getMembersOfGroups(groupNames, function (err, memberSets) { groups.getMembersOfGroups(groupNames, next);
if (err) { },
return next(err); function (memberSets, next) {
}
next(null, _.union.apply(_, memberSets)); next(null, _.union.apply(_, memberSets));
});
}, },
], callback); ], callback);
}; };
@ -380,4 +326,4 @@ var meta = require('./meta');
callback(); callback();
}; };
}(exports));

@ -17,13 +17,7 @@ module.exports = function (User) {
}; };
User.getIPs = function (uid, stop, callback) { User.getIPs = function (uid, stop, callback) {
db.getSortedSetRevRange('uid:' + uid + ':ip', 0, stop, function (err, ips) { db.getSortedSetRevRange('uid:' + uid + ':ip', 0, stop, callback);
if (err) {
return callback(err);
}
callback(null, ips);
});
}; };
User.getUsersCSV = function (callback) { User.getUsersCSV = function (callback) {

@ -1,5 +1,6 @@
'use strict'; 'use strict';
var async = require('async');
var validator = require('validator'); var validator = require('validator');
var nconf = require('nconf'); var nconf = require('nconf');
var winston = require('winston'); var winston = require('winston');
@ -54,13 +55,14 @@ module.exports = function (User) {
addField('lastonline'); addField('lastonline');
} }
db.getObjectsFields(keys, fields, function (err, users) { async.waterfall([
if (err) { function (next) {
return callback(err); db.getObjectsFields(keys, fields, next);
} },
function (users, next) {
modifyUserData(users, fieldsToRemove, callback); modifyUserData(users, fieldsToRemove, next);
}); },
], callback);
}; };
User.getMultipleUserFields = function (uids, fields, callback) { User.getMultipleUserFields = function (uids, fields, callback) {
@ -83,13 +85,14 @@ module.exports = function (User) {
return 'user:' + uid; return 'user:' + uid;
}); });
db.getObjects(keys, function (err, users) { async.waterfall([
if (err) { function (next) {
return callback(err); db.getObjects(keys, next);
} },
function (users, next) {
modifyUserData(users, [], callback); modifyUserData(users, [], next);
}); },
], callback);
}; };
function modifyUserData(users, fieldsToRemove, callback) { function modifyUserData(users, fieldsToRemove, callback) {
@ -152,51 +155,53 @@ module.exports = function (User) {
User.setUserField = function (uid, field, value, callback) { User.setUserField = function (uid, field, value, callback) {
callback = callback || function () {}; callback = callback || function () {};
db.setObjectField('user:' + uid, field, value, function (err) { async.waterfall([
if (err) { function (next) {
return callback(err); db.setObjectField('user:' + uid, field, value, next);
} },
function (next) {
plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: 'set' }); plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: 'set' });
callback(); next();
}); },
], callback);
}; };
User.setUserFields = function (uid, data, callback) { User.setUserFields = function (uid, data, callback) {
callback = callback || function () {}; callback = callback || function () {};
db.setObject('user:' + uid, data, function (err) { async.waterfall([
if (err) { function (next) {
return callback(err); db.setObject('user:' + uid, data, next);
} },
function (next) {
for (var field in data) { for (var field in data) {
if (data.hasOwnProperty(field)) { if (data.hasOwnProperty(field)) {
plugins.fireHook('action:user.set', { uid: uid, field: field, value: data[field], type: 'set' }); plugins.fireHook('action:user.set', { uid: uid, field: field, value: data[field], type: 'set' });
} }
} }
callback(); next();
}); },
], callback);
}; };
User.incrementUserFieldBy = function (uid, field, value, callback) { User.incrementUserFieldBy = function (uid, field, value, callback) {
callback = callback || function () {}; incrDecrUserFieldBy(uid, field, value, 'increment', callback);
db.incrObjectFieldBy('user:' + uid, field, value, function (err, value) {
if (err) {
return callback(err);
}
plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: 'increment' });
callback(null, value);
});
}; };
User.decrementUserFieldBy = function (uid, field, value, callback) { User.decrementUserFieldBy = function (uid, field, value, callback) {
incrDecrUserFieldBy(uid, field, -value, 'decrement', callback);
};
function incrDecrUserFieldBy(uid, field, value, type, callback) {
callback = callback || function () {}; callback = callback || function () {};
db.incrObjectFieldBy('user:' + uid, field, -value, function (err, value) { async.waterfall([
if (err) { function (next) {
return callback(err); db.incrObjectFieldBy('user:' + uid, field, value, next);
} },
plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: 'decrement' }); function (value, next) {
plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: type });
callback(null, value); next(null, value);
}); },
}; ], callback);
}
}; };

@ -0,0 +1,70 @@
'use strict';
var async = require('async');
var db = require('../database');
var topics = require('../topics');
var plugins = require('../plugins');
module.exports = function (User) {
User.updateLastOnlineTime = function (uid, callback) {
callback = callback || function () {};
db.getObjectFields('user:' + uid, ['status', 'lastonline'], function (err, userData) {
var now = Date.now();
if (err || userData.status === 'offline' || now - parseInt(userData.lastonline, 10) < 300000) {
return callback(err);
}
User.setUserField(uid, 'lastonline', now, callback);
});
};
User.updateOnlineUsers = function (uid, callback) {
callback = callback || function () {};
var now = Date.now();
async.waterfall([
function (next) {
db.sortedSetScore('users:online', uid, next);
},
function (userOnlineTime, next) {
if (now - parseInt(userOnlineTime, 10) < 300000) {
return callback();
}
db.sortedSetAdd('users:online', now, uid, next);
},
function (next) {
topics.pushUnreadCount(uid);
plugins.fireHook('action:user.online', { uid: uid, timestamp: now });
next();
},
], callback);
};
User.isOnline = function (uid, callback) {
var now = Date.now();
async.waterfall([
function (next) {
if (Array.isArray(uid)) {
db.sortedSetScores('users:online', uid, next);
} else {
db.sortedSetScore('users:online', uid, next);
}
},
function (lastonline, next) {
function checkOnline(lastonline) {
return now - lastonline < 300000;
}
var isOnline;
if (Array.isArray(uid)) {
isOnline = uid.map(function (uid, index) {
return checkOnline(lastonline[index]);
});
} else {
isOnline = checkOnline(lastonline);
}
next(null, isOnline);
},
], callback);
};
};
Loading…
Cancel
Save