From 73df8a111b6d042eadd49e4811432066c0314025 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Tue, 30 Jul 2024 20:36:41 -0400 Subject: [PATCH 01/27] shell script to pull certs from redis --- ops/cert.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 ops/cert.sh diff --git a/ops/cert.sh b/ops/cert.sh new file mode 100644 index 0000000..d74469c --- /dev/null +++ b/ops/cert.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# William Mantly +# MIT License +# https://github.com/wmantly + +ME="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")" + +if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then + echo "Usage: $ME [domain] [cert]" + echo "Usage: $ME chat.theta42.com cert_pem" + echo "This will extract the cert and private key from the proxy for given domain" + echo "Valid cert options, only one at a time: 'cert_pem', 'expiry', 'fullchain_pem', 'privkey_pem'" + + exit 0 +fi + +if [ "$2" == "" ]; then + (>&2 echo "This command takes exectly 2 arguments. See $ME --help") + + exit 1 +fi + +redis-cli get "$1:latest" | python3 -c "import json;print( json.loads(input())['$2'] )" From a641ed0f9183c37f5f025812fa94a3456018194e Mon Sep 17 00:00:00 2001 From: William Mantly Date: Tue, 30 Jul 2024 20:37:09 -0400 Subject: [PATCH 02/27] JS util to sleep --- nodejs/utils/sleep.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 nodejs/utils/sleep.js diff --git a/nodejs/utils/sleep.js b/nodejs/utils/sleep.js new file mode 100644 index 0000000..cae21ed --- /dev/null +++ b/nodejs/utils/sleep.js @@ -0,0 +1,9 @@ +'use strict'; + +function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +module.exports = sleep From 30cb62c53c97926cdf611a0582c2abe75ad8b097 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Tue, 30 Jul 2024 20:37:43 -0400 Subject: [PATCH 03/27] API wrapper for porkbun DNS --- nodejs/utils/porkbun.js | 110 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 nodejs/utils/porkbun.js diff --git a/nodejs/utils/porkbun.js b/nodejs/utils/porkbun.js new file mode 100644 index 0000000..743ab90 --- /dev/null +++ b/nodejs/utils/porkbun.js @@ -0,0 +1,110 @@ +'use strict'; + +const axios = require('axios'); +const conf = require('../conf/conf'); + +class PorkBun{ + baseUrl = 'https://api.porkbun.com/api/json/v3'; + + constructor(apiKey, secretApiKey){ + this.apiKey = apiKey; + this.secretApiKey = secretApiKey; + } + + async post(url, data){ + let res; + try{ + data = { + ...(data || {}), + secretapikey: this.secretApiKey, + apikey: this.apiKey, + }; + res = await axios.post(`${this.baseUrl}${url}`, data); + + return res; + }catch(error){ + throw new Error(`PorkPun API ${error.response.status}: ${error.response.data.message}`) + } + } + + __typeCheck(type){ + if(!['A', 'MX', 'CNAME', 'ALIAS', 'TXT', 'NS', 'AAAA', 'SRV', 'TLSA', 'CAA', 'HTTPS', 'SVCB'].includes(type)) throw new Error('PorkBun API: Invalid type passed') + } + + __parseName(domain, name){ + if(name && !name.endsWith('.'+domain)){ + return `${name}.${domain}` + } + return name; + } + + async getRecords(domain, options){ + let res = await this.post(`/dns/retrieve/${domain}`); + if(!options) return res.data.records; + + if(options.type) this.__typeCheck(options.type); + if(options.name) options.name = this.__parseName(domain, options.name); + let records = []; + + for(let record of res.data.records){ + let matchCount = 0 + for(let option in options){ + if(record[option] === options[option] && ++matchCount === Object.keys(options).length){ + records.push(record) + } + // console.log('option', option, options[option], record[option], matchCount) + } + } + + return records; + } + + async deleteRecordById(domain, id){ + let res = this.post(`/dns/delete/${domain}/${id}`); + return res.data; + } + + async deleteRecords(domain, options){ + let records = await this.getRecords(domain, options); + // console.log('PorkBun.deleteRecords', records) + for(let record of records){ + await this.deleteRecordById(domain, record.id) + } + } + + async createRecord(domain, options){ + this.__typeCheck(options.type); + if(!options.content) throw new Error('PorkBun API: `content` key is required for this action') + // if(options.name) options.name = this.__parseName(domain, options.name); + // console.log('PorkBun.createRecord to send:', domain, options) + + let res = this.post(`/dns/create/${domain}`, options); + return res.data; + } + + async createRecordForce(domain, options){ + let {content, ...removed} = options; + // console.log('new options', removed) + let records = await this.getRecords(domain, removed); + // console.log('createRecordForce', records) + if(records.length){ + // console.log('calling delete on', records[0].id) + // process.exit(0) + await this.deleteRecordById(domain, records[0].id) + } + return await this.createRecord(domain, options) + } +} + +module.exports = PorkBun; + + +if(require.main === module){(async function(){try{ + let porkBun = new PorkBun(conf.porkBun.apiKey, conf.porkBun.secretApiKey); + + // console.log(await porkBun.deleteRecordById('holycore.quest', '415509355')) + // console.log('IIFE', await porkBun.createRecordForce('holycore.quest', {type:'A', name: 'testapi', content: '127.0.0.5'})) + console.log('IIFE', await porkBun.getRecords('holycore.quest', {type:'A', name: 'testapi'})) +}catch(error){ + console.log('IIFE Error:', error) +}})()} From efc0fe81f003afcb3fdabe38bec27b1222462ecb Mon Sep 17 00:00:00 2001 From: William Mantly Date: Tue, 30 Jul 2024 20:38:22 -0400 Subject: [PATCH 04/27] LE class to manage wildcard cert --- nodejs/utils/letsencrypt.js | 135 ++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 nodejs/utils/letsencrypt.js diff --git a/nodejs/utils/letsencrypt.js b/nodejs/utils/letsencrypt.js new file mode 100644 index 0000000..b01bcc9 --- /dev/null +++ b/nodejs/utils/letsencrypt.js @@ -0,0 +1,135 @@ +'use strict'; + +const fs = require('fs') +const axios = require('axios'); +const AcmeClient = require('acme-client'); +const sleep = require('./sleep'); + +// https://dns.google/resolve?name=${name}&type=TXT + +class LetsEncrypt{ + static AcmeClient = AcmeClient; + + constructor(options){ + this.loadAccountKey(options.accountKeyPath || './le_key', (key)=>{ + this.client = new AcmeClient.Client({ + directoryUrl: options.directoryUrl || AcmeClient.directory.letsencrypt.production, + accountKey: key, + }); + }); + } + + loadAccountKey(accountKeyPath, cb){ + try{ + // Load the account key if it exists + cb(fs.readFileSync(accountKeyPath, 'utf8')); + }catch(error){ + if(error.code === 'ENOENT'){ + // Generate a new account key if it doesn't exist + AcmeClient.crypto.createPrivateKey().then(function(accountKey){ + fs.writeFileSync(accountKeyPath, accountKey.toString(), 'utf8'); + cb(accountKey.toString()); + }); + }else{ + throw error; + } + } + } + + async dnsWildcard(domain, options){ + /* + https://github.com/publishlab/node-acme-client/tree/master/examples/dns-01 + */ + + try{ + domain = domain.replace(/^\*\./, ''); + const [key, csr] = await AcmeClient.crypto.createCsr({ + altNames: [domain, `*.${domain}`], + }); + + const cert = await this.client.auto({ + csr, + email: 'wmantly@gmail.com', + termsOfServiceAgreed: true, + challengePriority: ['dns-01'], + challengeCreateFn: async (authz, challenge, keyAuthorization) => { + // console.log(`start TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`) + + let resCheck = await axios.get(`https://dns.google/resolve?name=_acme-challenge.${authz.identifier.value}&type=TXT`); + if(resCheck.data.Answer.some(record => record.data === keyAuthorization)) return; + + await options.challengeCreateFn(authz, challenge, keyAuthorization); + + let checkCount = 0; + while(true){ + await sleep(1500); + let res = await axios.get(`https://dns.google/resolve?name=_acme-challenge.${authz.identifier.value}&type=TXT`); + if(res.data.Answer.some(record => record.data === keyAuthorization)){ + // console.log(`found record for key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`) + break; + } + if(checkCount++ > 60) throw new Error('challengeCreateFn validation timed out'); + } + }, + challengeRemoveFn: options.challengeRemoveFn, + }); + + return { + key, + csr, + cert, + }; + + }catch(error){ + console.log('Error in LetsEncrypt.dnsChallenge', error) + } + } +} + +module.exports = LetsEncrypt; + +if(require.main === module){(async function(){try{ + + const tldExtract = require('tld-extract').parse_host; + const PorkBun = require('./porkbun'); + const conf = require('../conf/conf'); + + let porkBun = new PorkBun(conf.porkBun.apiKey, conf.porkBun.secretApiKey); + let letsEncrypt = new LetsEncrypt({ + directoryUrl: AcmeClient.directory.letsencrypt.staging, + }); + + let cert = await letsEncrypt.dnsWildcard('dev.test.holycore.quest', { + challengeCreateFn: async (authz, challenge, keyAuthorization) => { + let parts = tldExtract(authz.identifier.value); + let res = await porkBun.createRecordForce(parts.domain, {type:'TXT', name: `_acme-challenge${parts.sub ? `.${parts.sub}` : ''}`, content: `${keyAuthorization}`}); + }, + challengeRemoveFn: async (authz, challenge, keyAuthorization)=>{ + let parts = tldExtract(authz.identifier.value); + await porkBun.deleteRecords(parts.domain, {type:'TXT', name: `_acme-challenge${parts.sub ? `.${parts.sub}` : ''}`, content: `${keyAuthorization}`}); + }, + }); + + console.log('IIFE csr', cert.csr.toString()) + // console.log(cert.cert.split('\n\n')[0]) + // console.log('IIFE cert info', +AcmeClient.crypto.readCertificateInfo(cert.cert).notAfter/1000, 'IIFE cert:\n', String(cert.cert).split('\n\n') , /*'IIFE privKey\n', cert.key.toString()*/); +}catch(error){ + console.log('IIFE Error:', error) +}})()} + + + + + + + + + + + + + + + + + From da1f1a4127eb687fccfadcdeb9b1c18240410837 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Tue, 30 Jul 2024 20:40:02 -0400 Subject: [PATCH 05/27] consolidate front end routes to single file --- nodejs/routes/render.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 nodejs/routes/render.js diff --git a/nodejs/routes/render.js b/nodejs/routes/render.js new file mode 100644 index 0000000..114f191 --- /dev/null +++ b/nodejs/routes/render.js @@ -0,0 +1,27 @@ +'use strict'; + +const router = require('express').Router(); +const conf = require('../conf') +const middleware = require('../middleware/auth'); + +const values ={ + title: conf.environment !== 'production' ? `` : '' +} + +router.get('/', async function(req, res, next) { + res.render('hosts', {...values}); +}); + +router.get('/hosts', async function(req, res, next) { + res.render('hosts', {...values}); +}); + +router.get('/users', async function(req, res, next) { + res.render('users', {...values}); +}); + +router.get('/login', async function(req, res, next) { + res.render('login', {...values, redirect: req.query.redirect}); +}); + +module.exports = router; From baad9b0eece372b07ebdd0ca5cd6b1ffdd8d47b1 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Tue, 30 Jul 2024 20:46:37 -0400 Subject: [PATCH 06/27] moved front end JS libs to correct folder --- nodejs/public/lib/js/app-base.js | 355 ++++++++++++++++++++++++++ nodejs/public/lib/js/jq-repeat_new.js | 264 +++++++++++++++++++ nodejs/public/lib/js/val.js | 147 +++++++++++ 3 files changed, 766 insertions(+) create mode 100644 nodejs/public/lib/js/app-base.js create mode 100644 nodejs/public/lib/js/jq-repeat_new.js create mode 100755 nodejs/public/lib/js/val.js diff --git a/nodejs/public/lib/js/app-base.js b/nodejs/public/lib/js/app-base.js new file mode 100644 index 0000000..c4ec287 --- /dev/null +++ b/nodejs/public/lib/js/app-base.js @@ -0,0 +1,355 @@ +var app = {}; + +// app.pubsub = (function(){ +// app.topics = {}; + +// app.subscribe = function(topic, listener){ +// if(topic instanceof RegExp){ +// listener.match = topic; +// topic = "__REGEX__"; +// } + +// // create the topic if not yet created +// if(!app.topics[topic]) app.topics[topic] = []; + +// // add the listener +// app.topics[topic].push(listener); +// } + +// app.matchTopics = function(topic){ +// topic = topic || ''; +// var matches = [... app.topics[topic] ? app.topics[topic] : []]; + +// if(!app.topics['__REGEX__']) return matches; + +// for(var listener of app.topics['__REGEX__']){ +// if(topic.match(listener.match)) matches.push(listener); +// } + +// return matches; +// } + +// app.publish = function(topic, data){ + +// // send the event to all listeners +// app.matchTopics(topic).forEach(function(listener){ +// setTimeout(function(data, topic){ +// listener(data || {}, topic); +// }, 0, data, topic); +// }); +// } + +// return this; +// })(app); + +// app.socket = (function(app){ +// var socket = io(); +// // socket.emit('chat message', $('#m').val()); +// socket.on('P2PSub', function(msg){ +// msg.data.__noSocket = true; +// app.publish(msg.topic, msg.data); +// }); + +// app.subscribe(/./g, function(data, topic){ +// // console.log('local_pubs', data, topic) +// if(data.__noSocket) return; +// // console.log('local_pubs 2', data, topic) + +// socket.emit('P2PSub', { topic, data }) +// }); + +// return socket; + +// })(app); + +app.api = (function(app){ + var baseURL = '/api/' + + function post(url, data, callback){ + $.ajax({ + type: 'POST', + url: baseURL+url, + headers:{ + 'auth-token': app.auth.getToken() + }, + data: JSON.stringify(data), + contentType: "application/json; charset=utf-8", + dataType: "json", + complete: function(res, text){ + callback( + text !== 'success' ? res.statusText : null, + JSON.parse(res.responseText), + res.status + ) + } + }); + } + + function put(url, data, callback){ + $.ajax({ + type: 'PUT', + url: baseURL+url, + headers:{ + 'auth-token': app.auth.getToken() + }, + data: JSON.stringify(data), + contentType: "application/json; charset=utf-8", + dataType: "json", + complete: function(res, text){ + callback( + text !== 'success' ? res.statusText : null, + JSON.parse(res.responseText), + res.status + ) + } + }); + } + + function remove(url, callback, callback2){ + if(!$.isFunction(callback)) callback = callback2; + $.ajax({ + type: 'delete', + url: baseURL+url, + headers:{ + 'auth-token': app.auth.getToken() + }, + contentType: "application/json; charset=utf-8", + dataType: "json", + complete: function(res, text){ + callback( + text !== 'success' ? res.statusText : null, + JSON.parse(res.responseText), + res.status + ) + } + }); + } + + function get(url, callback){ + $.ajax({ + type: 'GET', + url: baseURL+url, + headers:{ + 'auth-token': app.auth.getToken() + }, + contentType: "application/json; charset=utf-8", + dataType: "json", + complete: function(res, text){ + callback( + text !== 'success' ? res.statusText : null, + JSON.parse(res.responseText), + res.status + ) + } + }); + } + + return {post: post, get: get, put: put, delete: remove} +})(app) + +app.auth = (function(app){ + var user = {} + function setToken(token){ + localStorage.setItem('APIToken', token); + } + + function getToken(){ + return localStorage.getItem('APIToken'); + } + + function isLoggedIn(callback){ + if(getToken()){ + return app.api.get('user/me', function(error, data){ + if(!error) app.auth.user = data; + return callback(error, data); + }); + }else{ + callback(null, false); + } + } + + function logIn(args, callback){ + app.api.post('auth/login', args, function(error, data){ + if(data.login){ + setToken(data.token); + } + callback(error, !!data.token); + }); + } + + function logOut(callback){ + localStorage.removeItem('APIToken'); + callback(); + } + + function forceLogin(){ + $.holdReady(true); + app.auth.isLoggedIn(function(error, isLoggedIn){ + if(error || !isLoggedIn){ + app.auth.logOut(function(){}) + location.replace(`/login${location.href.replace(location.origin, '')}`); + }else{ + $.holdReady(false); + } + }); + } + + function logInRedirect(){ + window.location.href = location.href.replace(location.origin+'/login', '') || '/' + } + + return { + getToken: getToken, + setToken: setToken, + isLoggedIn: isLoggedIn, + logIn: logIn, + logOut: logOut, + forceLogin, + logInRedirect, + } + +})(app); + +app.user = (function(app){ + function list(callback){ + app.api.get('user/?detail=true', function(error, data){ + callback(error, data); + }) + } + + function add(args, callback){ + app.api.post('user/', args, function(error, data){ + callback(error, data); + }); + } + + function remove(args, callback){ + app.api.delete('user/'+ args.username, function(error, data){ + callback(error, data); + }); + } + + function changePassword(args, callback){ + app.api.put('users/'+ arg.username || '', args, function(error, data){ + callback(error, data); + }); + } + + return {list, remove}; + +})(app); + +app.util = (function(app){ + + function getUrlParameter(name){ + name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); + var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); + var results = regex.exec(location.search); + return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); + }; + + function actionMessage(message, $target, type, callback){ + message = message || ''; + $target = $target.closest('div.card').find('.actionMessage'); + type = type || 'info'; + callback = callback || function(){}; + + if($target.html() === message) return; + + if($target.html()){ + $target.slideUp('fast', function(){ + $target.html('') + $target.removeClass (function(index, className){ + return (className.match (/(^|\s)bg-\S+/g) || []).join(' '); + }); + if(message) return actionMessage(message, $target, type, callback); + $target.hide() + }) + }else{ + if(type) $target.addClass('bg-' + type); + message = '' + message + '' + $target.html(message).slideDown('fast'); + } + setTimeout(callback,10) + } + + $.fn.serializeObject = function(){ + var + arr = $(this).serializeArray(), + obj = {}; + + for(var i = 0; i < arr.length; i++){ + if(obj[arr[i].name] === undefined) { + obj[arr[i].name] = arr[i].value; + } else { + if(!(obj[arr[i].name] instanceof Array)) { + obj[arr[i].name] = [obj[arr[i].name]]; + } + obj[arr[i].name].push(arr[i].value); + } + } + return obj; + }; + + return { + getUrlParameter: getUrlParameter, + actionMessage: actionMessage + } +})(app); + +$( document ).ready(function(){ + $('div.row').fadeIn('slow'); //show the page + + //panel button's + $('.fa-arrows-v').click(function(){ + $(this).closest('.card').find('.card-body').slideToggle('fast'); + }); + + $('.fa-circle-minus').click(function(){ + $(this).closest('.card').find('.card-body').slideToggle('fast'); + }); + + $('.fa-circle-xmark').click(function(){ + $(this).closest('.card').slideUp('fast'); + }); + + $('.actionMessage').on('click', 'button.action-close', function(event){ + app.util.actionMessage(null, $(this)); + }); + + setInterval(()=>{ + $('.momentFromNow').each((idx,el)=>{ + var $el = $(el); + try{ + $el.html(moment($(el).data('date')).fromNow()); + }catch{} + }) + }, 30000,); +}); + +//ajax form submit +function formAJAX(btn, del){ + event.preventDefault(); // avoid to execute the actual submit of the form. + var $form = $(btn).closest('[action]'); // gets the 'form' parent + var formData = $form.find('[name]').serializeObject(); // builds query formDataing + var method = $form.attr('method') || 'post'; + + // if( !$form.validate()){ + // app.util.actionMessage('Please fix the form errors.', $form, 'danger') + // return false; + // } + + app.util.actionMessage( + '
Loading...
', + $form, + 'info' + ); + + app.api[method]($form.attr('action'), formData, function(error, data){ + app.util.actionMessage(data.message, $form, error ? 'danger' : 'success'); //re-populate table + if(!error){ + $form.trigger("reset"); + eval($form.attr('evalAJAX')); //gets JS to run after completion + } + }); +} diff --git a/nodejs/public/lib/js/jq-repeat_new.js b/nodejs/public/lib/js/jq-repeat_new.js new file mode 100644 index 0000000..10f333e --- /dev/null +++ b/nodejs/public/lib/js/jq-repeat_new.js @@ -0,0 +1,264 @@ +/* +Author William Mantly Jr +https://github.com/wmantly/jq-repeat +MIT license +*/ + +(function($, Mustache){ +'use strict'; + if (!$.scope) { + $.scope = {}; + } + + var make = function( element ){ + var result = []; + + result.splice = function(inputValue, ...args){ + //splice does all the heavy lifting by interacting with the DOM elements. + + var toProto = [...args] + + var index; + //if a string is submitted as the index, try to match it to index number + if( typeof arguments[0] === 'string' ){ + index = this.indexOf( arguments[0] );//set where to start + if ( index === -1 ) { + return []; + } + }else{ + index = arguments[0]; //set where to start + } + + toProto.unshift(index) + + var howMany = arguments[1]; //sets the amount of fields to remove + var args = Array.prototype.slice.call( arguments ); // coverts arguments into array + var toAdd = args.slice(2); // only keeps fields to add to array + + // if the starting point is higher then the total index count, start at the end + if( index > this.length ) { + index = this.length; + } + // if the starting point is negative, start form the end of the array, minus the start point + if( index < 0 ) { + index = this.length - Math.abs( index ); + } + + // if there are things to add, figure out the how many new indexes we need + if( !howMany && howMany !== 0 ) { + howMany = this.length - index; + } + //not sure why i put this here... but it does matter! + if( howMany > this.length - index ) { + howMany = this.length - index; + } + + //figure out how many positions we need to shift the current elements + var shift = toAdd.length - howMany; + + // figure out how big the new array will be + // var newLength = this.length + shift; + + //removes fields from array based on howMany needs to be removed + for( var i = index; i < +index+howMany; i++ ) { + this.__take(this[index].__jq_$el, this[index], this); + // this.__take.apply( $( '.jq-repeat-'+ this.__jqRepeatId +'[jq-repeat-index="'+ ( i + index ) +'"]' ) ); + } + + //re-factor element index's + for(var i = 0; i < this.length; i++){ + if( i >= index){ + this[i].__jq_$el.attr( 'jq-repeat-index', i+shift ); + } + } + + //if there are fields to add to the array, add them + if( toAdd.length > 0 ){ + + //$.each( toAdd, function( key, value ){ + for(var I = 0; I < toAdd.length; I++){ + + //figure out new elements index + var key = I + index; + // apply values to template + var render = Mustache.render(this.__jqTemplate, toAdd[I] ); + + //set call name and index keys to DOM element + var $render = $( render ).addClass( 'jq-repeat-'+ this.__jqRepeatId ).attr( 'jq-repeat-index', key ); + + //if add new elements in proper stop, or after the place holder. + if( key === 0 ){ + this.$this.after( $render ); + }else{ + $( '.jq-repeat-'+ this.__jqRepeatId +'[jq-repeat-index="' + ( key -1 ) + '"]' ).after( $render ); + } + + Object.defineProperty( toAdd[I], "__jq_$el", { + value: $render, + writable: true, + enumerable: false, + configurable: true + } ); + + //animate element + this.__put($render, toAdd[I], this); + } + } + + //set and return new array + return Array.prototype.splice.apply(this, toProto); + }; + result.push = function(){ + //add one or more objects to the array + + //set the index value, if none is set make it zero + var index = this.length || 0; + + //loop each passed object and pass it to slice + for (var i = 0 ; i < arguments.length; ++i) { + this.splice( ( index + i ), 0, arguments[i] ); + } + + //return new array length + return this.length; + }; + result.pop = function(){ + //remove and return array element + + return this.splice( -1, 1 )[0]; + }; + result.reverse = function() { + var temp = this.splice( 0 ); + Array.prototype.reverse.apply( temp ); + + for( var i = 0; i < temp.length; i++ ){ + this.push( temp[i] ); + } + + return this; + }; + + result.remove = function(key, value){ + let index = this.indexOf(key, value) + if(index === -1) return; + this.splice(index, 1) + } + + result.shift = function() { + return this.splice( 0, 1 )[0]; + }; + + result.loop = function(){ + var temp = this[0]; + this.splice( 0,1 ); + this.push( temp ); + + return temp; + }; + result.loopUp = function(){ + var temp = this[this.length-1]; + this.splice( -1, 1 ); + this.splice( 0, 0, temp ); + return temp; + }; + result.indexOf = function( key, value ){ + if( typeof value !== 'string' ){ + value = arguments[0]; + key = this.__index; + } + for ( var index = 0; index < this.length; ++index ) { + if( this[index][key] === value ){ + + return index; + } + } + return -1; + }; + result.update = function( key, value, update ){ + //set variables using sting for index + + // If update is called with no index/key, assume its the 0 + if(typeof key === 'object'){ + if(this[0]){ + return this.update(0, key); + } + return this.splice(0, 1, key); + } + + if( typeof value !== 'string' ){ + update = arguments[1]; + value = arguments[0]; + key = this.__index; + } + var index = this.indexOf( key, value ); + if(index === -1) { + return []; + } + var object = $.extend( true, {}, this[index], update ); + return this.splice( index, 1, object )[0]; + }; + result.__put = function($el, item, list){ + $el.show(); + }; + result.__take = function($el, item, list){ + $el.remove(); + }; + + result.__setPut = function(fn) { + Object.defineProperty(this, '__put', { + value: fn, + writable: true, + enumerable: false, + configurable: true + }); + } + + result.__setTake = function(fn) { + Object.defineProperty(this, '__take', { + value: fn, + writable: true, + enumerable: false, + configurable: true + }); + } + + var $this = $( element ); + result.__jqRepeatId = $this.attr( 'jq-repeat' ); + $this.removeAttr('jq-repeat'); + result.__index = $this.attr('jq-repeat-index'); + result.__jqTemplate = $this[0].outerHTML; + $this.replaceWith( ' + + + +