/* handy localstorage functions for quick usage */ function set(key, val) { return localStorage.setItem(key, val); } function get(key, df) { // df is default value = localStorage.getItem(key); if(value == null) value = df; return value; } function remove(key) { return localStorage.removeItem(key); } function getParameter(name) { var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search); return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); } /* usermap localStorage stuff */ var usermap = JSON.parse(get('usermap', '{}')); var postmap = JSON.parse(get('postmap', '{}')) function getUserMap() { return usermap; } function getPostMap(hash) { if(hash !== undefined) { if(hash in postmap) return postmap[hash]; return null; } return postmap; } function deserializeUser(id) { if(!(id in getUserMap())) return null; var serialized = getUserMap()[id] var user = new User(); user.setName(serialized['name']); user.setID(serialized['id']); user.setIcon(serialized['icon']); user.setDescription(serialized['description']); return user; } function getCurrentUser() { var user = get('currentUser', null); if(user === null) return null; return User.getUser(user, function() {}); } function setCurrentUser(user) { set('currentUser', user.getID()); } /* returns a relative date format, e.g. "5 minutes" */ function timeSince(date, size) { // taken from https://stackoverflow.com/a/3177838/3678023 var seconds = Math.floor((new Date() - date) / 1000); var interval = Math.floor(seconds / 31536000); if (size === null) size = 'desktop'; var dates = { 'mobile' : { 'yr' : 'yrs', 'mo' : 'mo', 'd' : 'd', 'hr' : 'h', 'min' : 'm', 'secs' : 's', 'sec' : 's', }, 'desktop' : { 'yr' : ' years', 'mo' : ' months', 'd' : ' days', 'hr' : ' hours', 'min' : ' minutes', 'secs' : ' seconds', 'sec' : ' second', }, }; if (interval > 1) return interval + dates[size]['yr']; interval = Math.floor(seconds / 2592000); if (interval > 1) return interval + dates[size]['mo']; interval = Math.floor(seconds / 86400); if (interval > 1) return interval + dates[size]['d']; interval = Math.floor(seconds / 3600); if (interval > 1) return interval + dates[size]['hr']; interval = Math.floor(seconds / 60); if (interval > 1) return interval + dates[size]['min']; if(Math.floor(seconds) !== 1) return Math.floor(seconds) + dates[size]['secs']; return '1' + dates[size]['sec']; } /* replace all instances of string */ String.prototype.replaceAll = function(search, replacement, limit) { // taken from https://stackoverflow.com/a/17606289/3678023 var target = this; return target.split(search, limit).join(replacement); }; /* useful functions to sanitize data */ class Sanitize { /* sanitizes HTML in a string */ static html(html) { return String(html).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } /* URL encodes a string */ static url(url) { return encodeURIComponent(url); } /* usernames */ static username(username) { return String(username).replace(/[\W_]+/g, " ").substring(0, 25); } /* profile descriptions */ static description(description) { return String(description).substring(0, 128); } } /* config stuff */ function getWebPassword() { return get("web-password", null); } function setWebPassword(password) { return set("web-password", password); } function getTimingToken() { return get("timing-token", null); } function setTimingToken(token) { return set("timing-token", token); } /* user class */ class User { constructor() { this.name = 'Unknown'; this.id = 'unknown'; this.image = 'img/default.png'; } setName(name) { this.name = name; } getName() { return this.name; } setID(id) { this.id = id; } getID() { return this.id; } setIcon(image) { this.image = image; } getIcon() { return this.image; } setDescription(description) { this.description = description; } getDescription() { return this.description; } serialize() { return { 'name' : this.getName(), 'id' : this.getID(), 'icon' : this.getIcon(), 'description' : this.getDescription() }; } /* save in usermap */ remember() { usermap[this.getID()] = this.serialize(); set('usermap', JSON.stringify(usermap)); } /* save as a block */ save(callback) { var block = new Block(); block.setType('onionr-user'); block.setContent(JSON.stringify(this.serialize())); return block.save(true, callback); } static getUser(id, callback) { // console.log(callback); var user = deserializeUser(id); if(user === null) { Block.getBlocks({'type' : 'onionr-user-info', 'signed' : true, 'reverse' : true}, function(data) { if(data.length !== 0) { try { user = new User(); var userInfo = JSON.parse(data[0].getContent()); if(userInfo['id'] === id) { user.setName(userInfo['name']); user.setIcon(userInfo['icon']); user.setDescription(userInfo['description']); user.setID(id); user.remember(); // console.log(callback); callback(user); return user; } } catch(e) { console.log(e); callback(null); return null; } } else { callback(null); return null; } }); } else { // console.log(callback); callback(user); return user; } } } /* post class */ class Post { /* returns the html content of a post */ getHTML(type) { var replyTemplate = '<$= jsTemplate('onionr-timeline-reply') $>'; var postTemplate = '<$= jsTemplate('onionr-timeline-post') $>'; var template = ''; if(type !== undefined && type !== null && type == 'reply') template = replyTemplate; else template = postTemplate; var device = (jQuery(document).width() < 768 ? 'mobile' : 'desktop'); template = template.replaceAll('$user-name-url', Sanitize.html(Sanitize.url(this.getUser().getName()))); template = template.replaceAll('$user-name', Sanitize.html(this.getUser().getName())); template = template.replaceAll('$user-id-url', Sanitize.html(Sanitize.url(this.getUser().getID()))); template = template.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().substring(0, 12) + '...')); // template = template.replaceAll('$user-id-truncated', Sanitize.html(this.getUser().getID().split('-').slice(0, 4).join('-'))); template = template.replaceAll('$user-id', Sanitize.html(this.getUser().getID())); template = template.replaceAll('$user-image', "data:image/jpeg;base64," + Sanitize.html(this.getUser().getIcon())); template = template.replaceAll('$content', Sanitize.html(this.getContent()).replaceAll('\n', '
', 16)); // Maximum of 16 lines template = template.replaceAll('$post-hash', this.getHash()); template = template.replaceAll('$date-relative-truncated', timeSince(this.getPostDate(), 'mobile')); template = template.replaceAll('$date-relative', timeSince(this.getPostDate(), device) + (device === 'desktop' ? ' ago' : '')); template = template.replaceAll('$date', this.getPostDate().toLocaleString()); if(this.getHash() in getPostMap() && getPostMap()[this.getHash()]['liked']) { template = template.replaceAll('$liked', '<$= LANG.POST_UNLIKE $>'); } else { template = template.replaceAll('$liked', '<$= LANG.POST_LIKE $>'); } return template; } setUser(user) { this.user = user; } getUser() { return this.user; } setContent(content) { this.content = content; } getContent() { return this.content; } setParent(parent) { this.parent = parent; } getParent() { return this.parent; } setPostDate(date) { // unix timestamp input if(date instanceof Date) this.date = date; else this.date = new Date(date * 1000); } getPostDate() { return this.date; } setHash(hash) { this.hash = hash; } getHash() { return this.hash; } save(callback) { var args = {'type' : 'onionr-post', 'sign' : true, 'content' : JSON.stringify({'content' : this.getContent()})}; if(this.getParent() !== undefined && this.getParent() !== null) args['parent'] = (this.getParent() instanceof Post ? this.getParent().getHash() : (this.getParent() instanceof Block ? this.getParent().getHash() : this.getParent())); var url = '/client/?action=insertBlock&data=' + Sanitize.url(JSON.stringify(args)) + '&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken()); console.log(url); var http = new XMLHttpRequest(); if(callback !== undefined) { // async var thisObject = this; http.addEventListener('load', function() { thisObject.setHash(Block.parseBlockArray(JSON.parse(http.responseText)['hash'])); callback(thisObject.getHash()); }, false); http.open('GET', url, true); http.timeout = 5000; http.send(null); } else { // sync http.open('GET', url, false); http.send(null); this.setHash(Block.parseBlockArray(JSON.parse(http.responseText)['hash'])); return this.getHash(); } } } /* block class */ class Block { constructor(type, content) { this.type = type; this.content = content; } // returns the block hash, if any getHash() { return this.hash; } // returns the block type getType() { return this.type; } // returns the block header getHeader(key, df) { // df is default if(key !== undefined) { if(this.getHeader().hasOwnProperty(key)) return this.getHeader()[key]; else return (df === undefined ? null : df); } else return this.header; } // returns the block metadata getMetadata(key, df) { // df is default if(key !== undefined) { if(this.getMetadata().hasOwnProperty(key)) return this.getMetadata()[key]; else return (df === undefined ? null : df); } else return this.metadata; } // returns the block content getContent() { return this.content; } // returns the parent block's hash (not Block object, for performance) getParent() { // console.log(this.parent); // TODO: Create a function to fetch the block contents and parse it from the server; right now it is only possible to search for types of blocks (see Block.getBlocks), so it is impossible to return a Block object here // if(!(this.parent instanceof Block) && this.parent !== undefined && this.parent !== null) // this.parent = Block.openBlock(this.parent); // convert hash to Block object return this.parent; } // returns the date that the block was received getDate() { return this.date; } // returns a boolean that indicates whether or not the block is valid isValid() { return this.valid; } // returns a boolean thati ndicates whether or not the block is signed isSigned() { return this.signed; } // returns the block signature getSignature() { return this.signature; } // returns the block type setType(type) { this.type = type; return this; } // sets block metadata by key setMetadata(key, val) { this.metadata[key] = val; return this; } // sets block content setContent(content) { this.content = content; return this; } // sets the block parent by hash or Block object setParent(parent) { this.parent = parent; return this; } // indicates if the Block exists or not exists() { return !(this.hash === null || this.hash === undefined); } // saves the block, returns the hash save(sign, callback) { var type = this.getType(); var content = this.getContent(); var parent = this.getParent(); if(content !== undefined && content !== null && type !== '') { var args = {'content' : content}; if(type !== undefined && type !== null && type !== '') args['type'] = type; if(parent !== undefined && parent !== null && parent.getHash() !== undefined && parent.getHash() !== null && parent.getHash() !== '') args['parent'] = parent.getHash(); if(sign !== undefined && sign !== null) args['sign'] = String(sign) !== 'false' var url = '/client/?action=insertBlock&data=' + Sanitize.url(JSON.stringify(args)) + '&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken()); console.log(url); var http = new XMLHttpRequest(); if(callback !== undefined) { // async http.addEventListener('load', function() { callback(Block.parseBlockArray(JSON.parse(http.responseText)['hash'])); }, false); http.open('GET', url, true); http.timeout = 5000; http.send(null); } else { // sync http.open('GET', url, false); http.send(null); return Block.parseBlockArray(JSON.parse(http.responseText)['hash']); } } return false; } /* static functions */ // recreates a block by hash static openBlock(hash) { return Block.parseBlock(hash); } // converts an associative array to a Block static parseBlock(val) { var block = new Block(); block.type = val['type']; block.content = val['content']; block.header = val['header']; block.metadata = val['metadata']; block.date = new Date(val['date'] * 1000); block.hash = val['hash']; block.signature = val['signature']; block.signed = val['signed']; block.valid = val['valid']; block.parent = val['parent']; if(block.getParent() !== null) { // if the block data is already in the associative array /* if (blocks.hasOwnProperty(block.getParent())) block.setParent(Block.parseAssociativeArray({blocks[block.getParent()]})[0]); */ } return block; } // converts an array of associative arrays to an array of Blocks static parseBlockArray(blocks) { var outputBlocks = []; for(var key in blocks) { if(blocks.hasOwnProperty(key)) { var val = blocks[key]; var block = Block.parseBlock(val); outputBlocks.push(block); } } return outputBlocks; } static getBlocks(args, callback) { // callback is optional args = args || {} var url = '/client/?action=searchBlocks&data=' + Sanitize.url(JSON.stringify(args)) + '&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken()); console.log(url); var http = new XMLHttpRequest(); if(callback !== undefined) { // async http.addEventListener('load', function() { callback(Block.parseBlockArray(JSON.parse(http.responseText)['blocks'])); }, false); http.open('GET', url, true); http.timeout = 5000; http.send(null); } else { // sync http.open('GET', url, false); http.send(null); return Block.parseBlockArray(JSON.parse(http.responseText)['blocks']); } } } /* temporary code */ var tt = getParameter("timingToken"); if(tt !== null && tt !== undefined) { setTimingToken(tt); } if(getWebPassword() === null) { var password = ""; while(password.length != 64) { password = prompt("Please enter the web password (run `./RUN-LINUX.sh --details`)"); } setWebPassword(password); } if(getCurrentUser() === null) { jQuery('#modal').modal('show'); var url = '/client/?action=info&token=' + Sanitize.url(getWebPassword()) + '&timingToken=' + Sanitize.url(getTimingToken()); console.log(url); var http = new XMLHttpRequest(); // sync http.addEventListener('load', function() { var id = JSON.parse(http.responseText)['pubkey']; User.getUser(id, function(data) { if(data === null || data === undefined) { var user = new User(); user.setName('New User'); user.setID(id); user.setIcon('<$= Template.jsTemplate("default-icon") $>'); user.setDescription('A new OnionrUI user'); user.remember(); user.save(); setCurrentUser(user); } else { setCurrentUser(data); } window.location.reload(); }); }, false); http.open('GET', url, true); http.send(null); } currentUser = getCurrentUser();