11?"pm":"am";break;case"A":f+=c.getHours()>11?"PM":"AM";break;case"g":d=c.getHours()%12,f+=0===d?"12":d;break;case"G":f+=c.getHours();break;case"h":d=c.getHours()%12,0!==d&&10>d&&(d="0"+d),f+=0===d?"12":d;break;case"H":d=c.getHours(),a===v&&(d=24),f+=d>9?d:"0"+d;break;case"i":var h=c.getMinutes();f+=h>9?h:"0"+h;break;case"s":a=c.getSeconds(),f+=a>9?a:"0"+a;break;default:f+=e}return f}}}function s(a){if(""===a)return null;if(!a||a+0==a)return a;"object"==typeof a&&(a=a.getHours()+":"+t(a.getMinutes())+":"+t(a.getSeconds())),a=a.toLowerCase(),new Date(0);var b;if(-1===a.indexOf(":")?(b=a.match(/^([0-9]):?([0-5][0-9])?:?([0-5][0-9])?\s*([pa]?)m?$/),b||(b=a.match(/^([0-2][0-9]):?([0-5][0-9])?:?([0-5][0-9])?\s*([pa]?)m?$/))):b=a.match(/^(\d{1,2})(?::([0-5][0-9]))?(?::([0-5][0-9]))?\s*([pa]?)m?$/),!b)return null;var c,d=parseInt(1*b[1],10);c=b[4]?12==d?"p"==b[4]?12:0:d+("p"==b[4]?12:0):d;var e=1*b[2]||0,f=1*b[3]||0;return 3600*c+60*e+f}function t(a){return("0"+a).slice(-2)}var u=f(),v=86400,w={className:null,minTime:null,maxTime:null,durationTime:null,step:30,showDuration:!1,timeFormat:"g:ia",scrollDefaultNow:!1,scrollDefaultTime:!1,selectOnBlur:!1,disableTouchKeyboard:!1,forceRoundTime:!1,appendTo:"body",disableTimeRanges:[],closeOnWindowScroll:!1,typeaheadHighlight:!0,noneOption:!1},x={decimal:".",mins:"mins",hr:"hr",hrs:"hrs"},y={init:function(d){return this.each(function(){var e=a(this),f=[];for(key in w)e.data(key)&&(f[key]=e.data(key));var g=a.extend({},w,f,d);g.lang&&(x=a.extend(x,g.lang)),g=b(g),e.data("timepicker-settings",g),e.addClass("ui-timepicker-input"),g.useSelect?c(e):(e.prop("autocomplete","off"),e.on("click.timepicker focus.timepicker",y.show),e.on("change.timepicker",k),e.on("keydown.timepicker",n),e.on("keyup.timepicker",o),k.call(e.get(0)))})},show:function(b){b&&b.preventDefault();var d=a(this),e=d.data("timepicker-settings");if(e.useSelect)return d.data("timepicker-list").focus(),void 0;h(d)&&d.blur();var f=d.data("timepicker-list");if(!d.prop("readonly")&&(f&&0!==f.length&&"function"!=typeof e.durationTime||(c(d),f=d.data("timepicker-list")),!f.is(":visible"))){y.hide(),f.show(),d.offset().top+d.outerHeight(!0)+f.outerHeight()>a(window).height()+a(window).scrollTop()?f.offset({left:d.offset().left+parseInt(f.css("marginLeft").replace("px",""),10),top:d.offset().top-f.outerHeight()+parseInt(f.css("marginTop").replace("px",""),10)}):f.offset({left:d.offset().left+parseInt(f.css("marginLeft").replace("px",""),10),top:d.offset().top+d.outerHeight()+parseInt(f.css("marginTop").replace("px",""),10)});var j=f.find(".ui-timepicker-selected");if(j.length||(l(d)?j=i(d,f,s(l(d))):e.scrollDefaultNow?j=i(d,f,s(new Date)):e.scrollDefaultTime!==!1&&(j=i(d,f,s(e.scrollDefaultTime)))),j&&j.length){var k=f.scrollTop()+j.position().top-j.outerHeight();f.scrollTop(k)}else f.scrollTop(0);return a(document).on("touchstart.ui-timepicker mousedown.ui-timepicker",g),e.closeOnWindowScroll&&a(document).on("scroll.ui-timepicker",g),d.trigger("showTimepicker"),this}},hide:function(){var b=a(this),c=b.data("timepicker-settings");return c&&c.useSelect&&b.blur(),a(".ui-timepicker-wrapper:visible").each(function(){var b=a(this),c=b.data("timepicker-input"),d=c.data("timepicker-settings");d&&d.selectOnBlur&&p(c),b.hide(),c.trigger("hideTimepicker")}),this},option:function(d,e){var f=this,g=f.data("timepicker-settings"),h=f.data("timepicker-list");if("object"==typeof d)g=a.extend(g,d);else if("string"==typeof d&&"undefined"!=typeof e)g[d]=e;else if("string"==typeof d)return g[d];return g=b(g),f.data("timepicker-settings",g),h&&(h.remove(),f.data("timepicker-list",!1)),g.useSelect&&c(f),this},getSecondsFromMidnight:function(){return s(l(this))},getTime:function(a){var b=this,c=l(b);if(!c)return null;a||(a=new Date);var d=s(c),e=new Date(a);return e.setHours(d/3600),e.setMinutes(d%3600/60),e.setSeconds(d%60),e.setMilliseconds(0),e},setTime:function(a){var b=this,c=r(s(a),b.data("timepicker-settings").timeFormat);return m(b,c),b.data("timepicker-list")&&j(b,b.data("timepicker-list")),this},remove:function(){var a=this;if(a.hasClass("ui-timepicker-input"))return a.removeAttr("autocomplete","off"),a.removeClass("ui-timepicker-input"),a.removeData("timepicker-settings"),a.off(".timepicker"),a.data("timepicker-list")&&a.data("timepicker-list").remove(),a.removeData("timepicker-list"),this}};a.fn.timepicker=function(b){return this.length?y[b]?this.hasClass("ui-timepicker-input")?y[b].apply(this,Array.prototype.slice.call(arguments,1)):this:"object"!=typeof b&&b?(a.error("Method "+b+" does not exist on jQuery.timepicker"),void 0):y.init.apply(this,arguments):this}});
--------------------------------------------------------------------------------
/assets/js/prng4.js:
--------------------------------------------------------------------------------
1 | /*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/
2 | */
3 | // prng4.js - uses Arcfour as a PRNG
4 |
5 | function Arcfour() {
6 | this.i = 0;
7 | this.j = 0;
8 | this.S = new Array();
9 | }
10 |
11 | // Initialize arcfour context from key, an array of ints, each from [0..255]
12 | function ARC4init(key) {
13 | var i, j, t;
14 | for(i = 0; i < 256; ++i)
15 | this.S[i] = i;
16 | j = 0;
17 | for(i = 0; i < 256; ++i) {
18 | j = (j + this.S[i] + key[i % key.length]) & 255;
19 | t = this.S[i];
20 | this.S[i] = this.S[j];
21 | this.S[j] = t;
22 | }
23 | this.i = 0;
24 | this.j = 0;
25 | }
26 |
27 | function ARC4next() {
28 | var t;
29 | this.i = (this.i + 1) & 255;
30 | this.j = (this.j + this.S[this.i]) & 255;
31 | t = this.S[this.i];
32 | this.S[this.i] = this.S[this.j];
33 | this.S[this.j] = t;
34 | return this.S[(t + this.S[this.i]) & 255];
35 | }
36 |
37 | Arcfour.prototype.init = ARC4init;
38 | Arcfour.prototype.next = ARC4next;
39 |
40 | // Plug in your RNG constructor here
41 | function prng_newstate() {
42 | return new Arcfour();
43 | }
44 |
45 | // Pool size must be a multiple of 4 and greater than 32.
46 | // An array of bytes the size of the pool will be passed to init()
47 | var rng_psize = 256;
48 |
--------------------------------------------------------------------------------
/assets/js/rng.js:
--------------------------------------------------------------------------------
1 | /*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/
2 | */
3 | // Random number generator - requires a PRNG backend, e.g. prng4.js
4 |
5 | // For best results, put code like
6 | //
7 | // in your main HTML document.
8 |
9 | var rng_state;
10 | var rng_pool;
11 | var rng_pptr;
12 |
13 | // Mix in a 32-bit integer into the pool
14 | function rng_seed_int(x) {
15 | rng_pool[rng_pptr++] ^= x & 255;
16 | rng_pool[rng_pptr++] ^= (x >> 8) & 255;
17 | rng_pool[rng_pptr++] ^= (x >> 16) & 255;
18 | rng_pool[rng_pptr++] ^= (x >> 24) & 255;
19 | if(rng_pptr >= rng_psize) rng_pptr -= rng_psize;
20 | }
21 |
22 | // Mix in the current time (w/milliseconds) into the pool
23 | function rng_seed_time() {
24 | rng_seed_int(new Date().getTime());
25 | }
26 |
27 | // Initialize the pool with junk if needed.
28 | if(rng_pool == null) {
29 | rng_pool = new Array();
30 | rng_pptr = 0;
31 | var t;
32 | if(navigator.appName == "Netscape" && navigator.appVersion < "5" && window.crypto) {
33 | // Extract entropy (256 bits) from NS4 RNG if available
34 | var z = window.crypto.random(32);
35 | for(t = 0; t < z.length; ++t)
36 | rng_pool[rng_pptr++] = z.charCodeAt(t) & 255;
37 | }
38 | while(rng_pptr < rng_psize) { // extract some randomness from Math.random()
39 | t = Math.floor(65536 * Math.random());
40 | rng_pool[rng_pptr++] = t >>> 8;
41 | rng_pool[rng_pptr++] = t & 255;
42 | }
43 | rng_pptr = 0;
44 | rng_seed_time();
45 | //rng_seed_int(window.screenX);
46 | //rng_seed_int(window.screenY);
47 | }
48 |
49 | function rng_get_byte() {
50 | if(rng_state == null) {
51 | rng_seed_time();
52 | rng_state = prng_newstate();
53 | rng_state.init(rng_pool);
54 | for(rng_pptr = 0; rng_pptr < rng_pool.length; ++rng_pptr)
55 | rng_pool[rng_pptr] = 0;
56 | rng_pptr = 0;
57 | //rng_pool = null;
58 | }
59 | // TODO: allow reseeding after first request
60 | return rng_state.next();
61 | }
62 |
63 | function rng_get_bytes(ba) {
64 | var i;
65 | for(i = 0; i < ba.length; ++i) ba[i] = rng_get_byte();
66 | }
67 |
68 | function SecureRandom() {}
69 |
70 | SecureRandom.prototype.nextBytes = rng_get_bytes;
71 |
--------------------------------------------------------------------------------
/assets/js/rsa.js:
--------------------------------------------------------------------------------
1 | /*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/
2 | */
3 | // Depends on jsbn.js and rng.js
4 |
5 | // Version 1.1: support utf-8 encoding in pkcs1pad2
6 |
7 | // convert a (hex) string to a bignum object
8 | function parseBigInt(str,r) {
9 | return new BigInteger(str,r);
10 | }
11 |
12 | function linebrk(s,n) {
13 | var ret = "";
14 | var i = 0;
15 | while(i + n < s.length) {
16 | ret += s.substring(i,i+n) + "\n";
17 | i += n;
18 | }
19 | return ret + s.substring(i,s.length);
20 | }
21 |
22 | function byte2Hex(b) {
23 | if(b < 0x10)
24 | return "0" + b.toString(16);
25 | else
26 | return b.toString(16);
27 | }
28 |
29 | // PKCS#1 (type 2, random) pad input string s to n bytes, and return a bigint
30 | function pkcs1pad2(s,n) {
31 | if(n < s.length + 11) { // TODO: fix for utf-8
32 | alert("Message too long for RSA");
33 | return null;
34 | }
35 | var ba = new Array();
36 | var i = s.length - 1;
37 | while(i >= 0 && n > 0) {
38 | var c = s.charCodeAt(i--);
39 | if(c < 128) { // encode using utf-8
40 | ba[--n] = c;
41 | }
42 | else if((c > 127) && (c < 2048)) {
43 | ba[--n] = (c & 63) | 128;
44 | ba[--n] = (c >> 6) | 192;
45 | }
46 | else {
47 | ba[--n] = (c & 63) | 128;
48 | ba[--n] = ((c >> 6) & 63) | 128;
49 | ba[--n] = (c >> 12) | 224;
50 | }
51 | }
52 | ba[--n] = 0;
53 | var rng = new SecureRandom();
54 | var x = new Array();
55 | while(n > 2) { // random non-zero pad
56 | x[0] = 0;
57 | while(x[0] == 0) rng.nextBytes(x);
58 | ba[--n] = x[0];
59 | }
60 | ba[--n] = 2;
61 | ba[--n] = 0;
62 | return new BigInteger(ba);
63 | }
64 |
65 | // PKCS#1 (OAEP) mask generation function
66 | function oaep_mgf1_arr(seed, len, hash)
67 | {
68 | var mask = '', i = 0;
69 |
70 | while (mask.length < len)
71 | {
72 | mask += hash(String.fromCharCode.apply(String, seed.concat([
73 | (i & 0xff000000) >> 24,
74 | (i & 0x00ff0000) >> 16,
75 | (i & 0x0000ff00) >> 8,
76 | i & 0x000000ff])));
77 | i += 1;
78 | }
79 |
80 | return mask;
81 | }
82 |
83 | var SHA1_SIZE = 20;
84 |
85 | // PKCS#1 (OAEP) pad input string s to n bytes, and return a bigint
86 | function oaep_pad(s, n, hash)
87 | {
88 | if (s.length + 2 * SHA1_SIZE + 2 > n)
89 | {
90 | throw "Message too long for RSA";
91 | }
92 |
93 | var PS = '', i;
94 |
95 | for (i = 0; i < n - s.length - 2 * SHA1_SIZE - 2; i += 1)
96 | {
97 | PS += '\x00';
98 | }
99 |
100 | var DB = rstr_sha1('') + PS + '\x01' + s;
101 | var seed = new Array(SHA1_SIZE);
102 | new SecureRandom().nextBytes(seed);
103 |
104 | var dbMask = oaep_mgf1_arr(seed, DB.length, hash || rstr_sha1);
105 | var maskedDB = [];
106 |
107 | for (i = 0; i < DB.length; i += 1)
108 | {
109 | maskedDB[i] = DB.charCodeAt(i) ^ dbMask.charCodeAt(i);
110 | }
111 |
112 | var seedMask = oaep_mgf1_arr(maskedDB, seed.length, rstr_sha1);
113 | var maskedSeed = [0];
114 |
115 | for (i = 0; i < seed.length; i += 1)
116 | {
117 | maskedSeed[i + 1] = seed[i] ^ seedMask.charCodeAt(i);
118 | }
119 |
120 | return new BigInteger(maskedSeed.concat(maskedDB));
121 | }
122 |
123 | // "empty" RSA key constructor
124 | function RSAKey() {
125 | this.n = null;
126 | this.e = 0;
127 | this.d = null;
128 | this.p = null;
129 | this.q = null;
130 | this.dmp1 = null;
131 | this.dmq1 = null;
132 | this.coeff = null;
133 | }
134 |
135 | // Set the public key fields N and e from hex strings
136 | function RSASetPublic(N,E) {
137 | this.isPublic = true;
138 | if (typeof N !== "string")
139 | {
140 | this.n = N;
141 | this.e = E;
142 | }
143 | else if(N != null && E != null && N.length > 0 && E.length > 0) {
144 | this.n = parseBigInt(N,16);
145 | this.e = parseInt(E,16);
146 | }
147 | else
148 | alert("Invalid RSA public key");
149 | }
150 |
151 | // Perform raw public operation on "x": return x^e (mod n)
152 | function RSADoPublic(x) {
153 | return x.modPowInt(this.e, this.n);
154 | }
155 |
156 | // Return the PKCS#1 RSA encryption of "text" as an even-length hex string
157 | function RSAEncrypt(text) {
158 | // console.log("Encrypting: "+text);
159 | // console.log("bitlength: "+this.n.bitLength()+7);
160 | var m = pkcs1pad2(text,(this.n.bitLength()+7)>>3);
161 | // console.log("result of pkcs1pad2: "+m);
162 | if(m == null) return null;
163 | var c = this.doPublic(m);
164 | if(c == null) return null;
165 | var h = c.toString(16);
166 | if((h.length & 1) == 0) return h; else return "0" + h;
167 | }
168 |
169 | // Return the PKCS#1 OAEP RSA encryption of "text" as an even-length hex string
170 | function RSAEncryptOAEP(text, hash) {
171 | var m = oaep_pad(text, (this.n.bitLength()+7)>>3, hash);
172 | if(m == null) return null;
173 | var c = this.doPublic(m);
174 | if(c == null) return null;
175 | var h = c.toString(16);
176 | if((h.length & 1) == 0) return h; else return "0" + h;
177 | }
178 |
179 | // Return the PKCS#1 RSA encryption of "text" as a Base64-encoded string
180 | //function RSAEncryptB64(text) {
181 | // var h = this.encrypt(text);
182 | // if(h) return hex2b64(h); else return null;
183 | //}
184 |
185 | // protected
186 | RSAKey.prototype.doPublic = RSADoPublic;
187 |
188 | // public
189 | RSAKey.prototype.setPublic = RSASetPublic;
190 | RSAKey.prototype.encrypt = RSAEncrypt;
191 | RSAKey.prototype.encryptOAEP = RSAEncryptOAEP;
192 | //RSAKey.prototype.encrypt_b64 = RSAEncryptB64;
193 |
194 | RSAKey.prototype.type = "RSA";
195 |
--------------------------------------------------------------------------------
/assets/js/rsa2.js:
--------------------------------------------------------------------------------
1 | /*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/
2 | */
3 | // Depends on rsa.js and jsbn2.js
4 |
5 | // Version 1.1: support utf-8 decoding in pkcs1unpad2
6 |
7 | // Undo PKCS#1 (type 2, random) padding and, if valid, return the plaintext
8 | function pkcs1unpad2(d,n) {
9 | var b = d.toByteArray();
10 | var i = 0;
11 | while(i < b.length && b[i] == 0) ++i;
12 | if(b.length-i != n-1 || b[i] != 2)
13 | return null;
14 | ++i;
15 | while(b[i] != 0)
16 | if(++i >= b.length) return null;
17 | var ret = "";
18 | while(++i < b.length) {
19 | var c = b[i] & 255;
20 | if(c < 128) { // utf-8 decode
21 | ret += String.fromCharCode(c);
22 | }
23 | else if((c > 191) && (c < 224)) {
24 | ret += String.fromCharCode(((c & 31) << 6) | (b[i+1] & 63));
25 | ++i;
26 | }
27 | else {
28 | ret += String.fromCharCode(((c & 15) << 12) | ((b[i+1] & 63) << 6) | (b[i+2] & 63));
29 | i += 2;
30 | }
31 | }
32 | return ret;
33 | }
34 |
35 | // PKCS#1 (OAEP) mask generation function
36 | function oaep_mgf1_str(seed, len, hash)
37 | {
38 | var mask = '', i = 0;
39 |
40 | while (mask.length < len)
41 | {
42 | mask += hash(seed + String.fromCharCode.apply(String, [
43 | (i & 0xff000000) >> 24,
44 | (i & 0x00ff0000) >> 16,
45 | (i & 0x0000ff00) >> 8,
46 | i & 0x000000ff]));
47 | i += 1;
48 | }
49 |
50 | return mask;
51 | }
52 |
53 | var SHA1_SIZE = 20;
54 |
55 | // Undo PKCS#1 (OAEP) padding and, if valid, return the plaintext
56 | function oaep_unpad(d, n, hash)
57 | {
58 | d = d.toByteArray();
59 |
60 | var i;
61 |
62 | for (i = 0; i < d.length; i += 1)
63 | {
64 | d[i] &= 0xff;
65 | }
66 |
67 | while (d.length < n)
68 | {
69 | d.unshift(0);
70 | }
71 |
72 | d = String.fromCharCode.apply(String, d);
73 |
74 | if (d.length < 2 * SHA1_SIZE + 2)
75 | {
76 | throw "Cipher too short";
77 | }
78 |
79 | var maskedSeed = d.substr(1, SHA1_SIZE)
80 | var maskedDB = d.substr(SHA1_SIZE + 1);
81 |
82 | var seedMask = oaep_mgf1_str(maskedDB, SHA1_SIZE, hash || rstr_sha1);
83 | var seed = [], i;
84 |
85 | for (i = 0; i < maskedSeed.length; i += 1)
86 | {
87 | seed[i] = maskedSeed.charCodeAt(i) ^ seedMask.charCodeAt(i);
88 | }
89 |
90 | var dbMask = oaep_mgf1_str(String.fromCharCode.apply(String, seed),
91 | d.length - SHA1_SIZE, rstr_sha1);
92 |
93 | var DB = [];
94 |
95 | for (i = 0; i < maskedDB.length; i += 1)
96 | {
97 | DB[i] = maskedDB.charCodeAt(i) ^ dbMask.charCodeAt(i);
98 | }
99 |
100 | DB = String.fromCharCode.apply(String, DB);
101 |
102 | if (DB.substr(0, SHA1_SIZE) !== rstr_sha1(''))
103 | {
104 | throw "Hash mismatch";
105 | }
106 |
107 | DB = DB.substr(SHA1_SIZE);
108 |
109 | var first_one = DB.indexOf('\x01');
110 | var last_zero = (first_one != -1) ? DB.substr(0, first_one).lastIndexOf('\x00') : -1;
111 |
112 | if (last_zero + 1 != first_one)
113 | {
114 | throw "Malformed data";
115 | }
116 |
117 | return DB.substr(first_one + 1);
118 | }
119 |
120 | // Set the private key fields N, e, and d from hex strings
121 | function RSASetPrivate(N,E,D) {
122 | this.isPrivate = true;
123 | if (typeof N !== "string")
124 | {
125 | this.n = N;
126 | this.e = E;
127 | this.d = D;
128 | }
129 | else if(N != null && E != null && N.length > 0 && E.length > 0) {
130 | this.n = parseBigInt(N,16);
131 | this.e = parseInt(E,16);
132 | this.d = parseBigInt(D,16);
133 | }
134 | else
135 | alert("Invalid RSA private key");
136 | }
137 |
138 | // Set the private key fields N, e, d and CRT params from hex strings
139 | function RSASetPrivateEx(N,E,D,P,Q,DP,DQ,C) {
140 | this.isPrivate = true;
141 | if (N == null) throw "RSASetPrivateEx N == null";
142 | if (E == null) throw "RSASetPrivateEx E == null";
143 | if (N.length == 0) throw "RSASetPrivateEx N.length == 0";
144 | if (E.length == 0) throw "RSASetPrivateEx E.length == 0";
145 |
146 | if (N != null && E != null && N.length > 0 && E.length > 0) {
147 | this.n = parseBigInt(N,16);
148 | this.e = parseInt(E,16);
149 | this.d = parseBigInt(D,16);
150 | this.p = parseBigInt(P,16);
151 | this.q = parseBigInt(Q,16);
152 | this.dmp1 = parseBigInt(DP,16);
153 | this.dmq1 = parseBigInt(DQ,16);
154 | this.coeff = parseBigInt(C,16);
155 | } else {
156 | alert("Invalid RSA private key in RSASetPrivateEx");
157 | }
158 | }
159 |
160 | // Generate a new random private key B bits long, using public expt E
161 | function RSAGenerate(B,E) {
162 | var rng = new SecureRandom();
163 | var qs = B>>1;
164 | this.e = parseInt(E,16);
165 | var ee = new BigInteger(E,16);
166 | for(;;) {
167 | for(;;) {
168 | this.p = new BigInteger(B-qs,1,rng);
169 | if(this.p.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) == 0 && this.p.isProbablePrime(10)) break;
170 | }
171 | for(;;) {
172 | this.q = new BigInteger(qs,1,rng);
173 | if(this.q.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) == 0 && this.q.isProbablePrime(10)) break;
174 | }
175 | if(this.p.compareTo(this.q) <= 0) {
176 | var t = this.p;
177 | this.p = this.q;
178 | this.q = t;
179 | }
180 | var p1 = this.p.subtract(BigInteger.ONE); // p1 = p - 1
181 | var q1 = this.q.subtract(BigInteger.ONE); // q1 = q - 1
182 | var phi = p1.multiply(q1);
183 | if(phi.gcd(ee).compareTo(BigInteger.ONE) == 0) {
184 | this.n = this.p.multiply(this.q); // this.n = p * q
185 | this.d = ee.modInverse(phi); // this.d =
186 | this.dmp1 = this.d.mod(p1); // this.dmp1 = d mod (p - 1)
187 | this.dmq1 = this.d.mod(q1); // this.dmq1 = d mod (q - 1)
188 | this.coeff = this.q.modInverse(this.p); // this.coeff = (q ^ -1) mod p
189 | break;
190 | }
191 | }
192 | }
193 |
194 | // Perform raw private operation on "x": return x^d (mod n)
195 | function RSADoPrivate(x) {
196 | if(this.p == null || this.q == null)
197 | return x.modPow(this.d, this.n);
198 |
199 | // TODO: re-calculate any missing CRT params
200 | var xp = x.mod(this.p).modPow(this.dmp1, this.p); // xp=cp?
201 | var xq = x.mod(this.q).modPow(this.dmq1, this.q); // xq=cq?
202 |
203 | while(xp.compareTo(xq) < 0)
204 | xp = xp.add(this.p);
205 | // NOTE:
206 | // xp.subtract(xq) => cp -cq
207 | // xp.subtract(xq).multiply(this.coeff).mod(this.p) => (cp - cq) * u mod p = h
208 | // xp.subtract(xq).multiply(this.coeff).mod(this.p).multiply(this.q).add(xq) => cq + (h * q) = M
209 | return xp.subtract(xq).multiply(this.coeff).mod(this.p).multiply(this.q).add(xq);
210 | }
211 |
212 | // Return the PKCS#1 RSA decryption of "ctext".
213 | // "ctext" is an even-length hex string and the output is a plain string.
214 | function RSADecrypt(ctext) {
215 | var c = parseBigInt(ctext, 16);
216 | var m = this.doPrivate(c);
217 | if(m == null) return null;
218 | return pkcs1unpad2(m, (this.n.bitLength()+7)>>3);
219 | }
220 |
221 | // Return the PKCS#1 OAEP RSA decryption of "ctext".
222 | // "ctext" is an even-length hex string and the output is a plain string.
223 | function RSADecryptOAEP(ctext, hash) {
224 | var c = parseBigInt(ctext, 16);
225 | var m = this.doPrivate(c);
226 | if(m == null) return null;
227 | return oaep_unpad(m, (this.n.bitLength()+7)>>3, hash);
228 | }
229 |
230 | // Return the PKCS#1 RSA decryption of "ctext".
231 | // "ctext" is a Base64-encoded string and the output is a plain string.
232 | //function RSAB64Decrypt(ctext) {
233 | // var h = b64tohex(ctext);
234 | // if(h) return this.decrypt(h); else return null;
235 | //}
236 |
237 | // protected
238 | RSAKey.prototype.doPrivate = RSADoPrivate;
239 |
240 | // public
241 | RSAKey.prototype.setPrivate = RSASetPrivate;
242 | RSAKey.prototype.setPrivateEx = RSASetPrivateEx;
243 | RSAKey.prototype.generate = RSAGenerate;
244 | RSAKey.prototype.decrypt = RSADecrypt;
245 | RSAKey.prototype.decryptOAEP = RSADecryptOAEP;
246 | //RSAKey.prototype.b64_decrypt = RSAB64Decrypt;
247 |
--------------------------------------------------------------------------------
/classes/class-support-hub-account.php:
--------------------------------------------------------------------------------
1 | load($shub_account_id);
7 | }
8 |
9 | public $shub_account_id = false; // the current user id in our system.
10 | public $shub_extension = ''; // the current extension name
11 | public $details = array();
12 |
13 | /* @var $items SupportHub_item[] */
14 | public $items = array();
15 |
16 | public $json_fields = array('account_data');
17 |
18 | public function reset(){
19 | $this->shub_account_id = false;
20 | $this->details = array(
21 | 'shub_account_id' => false,
22 | 'shub_extension' => '',
23 | 'account_name' => false,
24 | 'shub_user_id' => 0,
25 | 'last_checked' => false,
26 | 'account_data' => array(),
27 | 'items' => array(),
28 | );
29 | foreach($this->details as $field_id => $field_data){
30 | $this->{$field_id} = $field_data;
31 | }
32 | }
33 |
34 | public function is_item_active($network_key){
35 | if(isset($this->items[$network_key]) && $this->items[$network_key]->get('network_key') == $network_key){
36 | return true;
37 | }else{
38 | return false;
39 | }
40 | }
41 |
42 |
43 | public function create_new(){
44 | $this->reset();
45 | $this->shub_account_id = shub_update_insert('shub_account_id',false,'shub_account',array(
46 | 'account_name' => '',
47 | ));
48 | $this->load($this->shub_account_id);
49 | }
50 |
51 | public function load($shub_account_id = false){
52 | if(!$shub_account_id)$shub_account_id = $this->shub_account_id;
53 | $this->reset();
54 | $this->shub_account_id = (int)$shub_account_id;
55 | if($this->shub_account_id){
56 | $data = shub_get_single('shub_account','shub_account_id',$this->shub_account_id);
57 | foreach($this->details as $key=>$val){
58 | $this->details[$key] = $data && isset($data[$key]) ? $data[$key] : $val;
59 | if(in_array($key,$this->json_fields)){
60 | $this->details[$key] = @json_decode($this->details[$key],true);
61 | if(!is_array($this->details[$key]))$this->details[$key] = array();
62 | }
63 | }
64 | if(!is_array($this->details) || $this->details['shub_account_id'] != $this->shub_account_id){
65 | $this->reset();
66 | return false;
67 | }
68 | }
69 | foreach($this->details as $key=>$val){
70 | $this->{$key} = $val;
71 | }
72 |
73 | $this->items = array();
74 | if(!$this->shub_account_id)return false;
75 | foreach(shub_get_multiple('shub_item',array('shub_account_id'=>$this->shub_account_id),'shub_item_id') as $item){
76 | $item = $this->get_item($item['shub_item_id']);
77 | $this->items[$item->get('network_key')] = $item;
78 | }
79 | return $this->shub_account_id;
80 | }
81 |
82 | public function get($field){
83 | if(isset($this->{$field})){
84 | return $this->{$field};
85 | }else{
86 | // check in data
87 | $data = $this->get('account_data');
88 | if(!empty($data) && isset($data[$field])){
89 | return $data[$field];
90 | }
91 | }
92 | return false;
93 | }
94 |
95 | public function save_data($post_data){
96 | if(!$this->get('shub_account_id')){
97 | $this->create_new();
98 | }
99 | if(is_array($post_data)){
100 | foreach($this->details as $details_key => $details_val){
101 | if(isset($post_data[$details_key])){
102 | if(is_array($post_data[$details_key])){
103 | foreach($post_data[$details_key] as $key=>$val){
104 | if($val == _SUPPORT_HUB_PASSWORD_FIELD_FUZZ){
105 | unset($post_data[$details_key][$key]);
106 | }
107 | }
108 | }else if($post_data[$details_key] == _SUPPORT_HUB_PASSWORD_FIELD_FUZZ)continue;
109 |
110 | $this->update($details_key,$post_data[$details_key]);
111 | }
112 | }
113 | }
114 | // save the active envato items.
115 | if(isset($post_data['save_account_items']) && $post_data['save_account_items'] == 'yep') {
116 | $currently_active_items = $this->items;
117 | $data = $this->get('account_data');
118 | $available_items = isset($data['items']) && is_array($data['items']) ? $data['items'] : array();
119 | if(isset($post_data['item']) && is_array($post_data['item'])){
120 | foreach($post_data['item'] as $network_key => $yesno){
121 | if(isset($currently_active_items[$network_key])){
122 | if(isset($post_data['item_product'][$network_key])){
123 | $currently_active_items[$network_key]->update('shub_product_id',$post_data['item_product'][$network_key]);
124 | }
125 | unset($currently_active_items[$network_key]);
126 | }
127 | if($yesno && isset($available_items[$network_key])){
128 | // we are adding this item to the list. check if it doesn't already exist.
129 | if(!isset($this->items[$network_key])){
130 | $item = new SupportHub_item($this);
131 | $item->create_new();
132 | $item->update('shub_account_id', $this->shub_account_id);
133 | //$item->update('envato_token', 'same'); // $available_items[$network_key]['access_token']
134 | $item->update('item_name', $post_data['item_name'][$network_key]); //$available_items[$network_key]['item']);
135 | $item->update('network_key', $network_key);
136 | $item->update('item_data', $available_items[$network_key]);
137 | $item->update('shub_product_id', isset($post_data['item_product'][$network_key]) ? $post_data['item_product'][$network_key] : 0);
138 | }
139 | }
140 | }
141 | }
142 | // remove any items that are no longer active.
143 | foreach($currently_active_items as $item){
144 | $item->delete();
145 | }
146 | }
147 | $this->load();
148 | return $this->get('shub_account_id');
149 | }
150 | public function update($field,$value){
151 | // what fields to we allow? or not allow?
152 | if(in_array($field,array('shub_account_id')))return;
153 | if($this->shub_account_id){
154 | if($field == 'account_data'){
155 | if(is_array($value)){
156 | // merge data with existing.
157 | $existing_data = $this->get('account_data');
158 | if(!is_array($existing_data))$existing_data=array();
159 | foreach($existing_data as $key=>$val){
160 | if(is_numeric($key))unset($existing_data[$key]);
161 | }
162 | $value = array_merge($existing_data,$value);
163 | }
164 | }
165 | $this->{$field} = $value;
166 | if (in_array($field, $this->json_fields)) {
167 | $value = json_encode($value);
168 | }
169 | shub_update_insert('shub_account_id', $this->shub_account_id, 'shub_account', array(
170 | $field => $value,
171 | ));
172 | }
173 | }
174 | public function delete(){
175 | if($this->shub_account_id) {
176 | // delete all the items for this twitter account.
177 | $items = $this->get('items');
178 | foreach($items as $item){
179 | $item->delete();
180 | }
181 | shub_delete_from_db( 'shub_account', 'shub_account_id', $this->shub_account_id );
182 | }
183 | }
184 |
185 | public function is_active(){
186 | // is there a 'last_checked' date?
187 | if(!$this->get('last_checked')){
188 | return false; // never checked this account, not active yet.
189 | }else{
190 | return true;
191 | }
192 | }
193 |
194 | public function save_account_data($user_data){
195 | $this->update('account_data',$user_data);
196 |
197 | }
198 |
199 | public function run_cron( $debug = false ){
200 |
201 |
202 | }
203 |
204 | /**
205 | * Links for wordpress
206 | */
207 | public function link_connect(){
208 | return 'admin.php?page=support_hub_settings&tab='.$this->shub_extension.'&do_connect&shub_account_id='.$this->get('shub_account_id');
209 | }
210 | public function link_edit(){
211 | return 'admin.php?page=support_hub_settings&tab='.$this->shub_extension.'&shub_account_id='.$this->get('shub_account_id');
212 | }
213 | public function link_new_message(){
214 | return 'admin.php?page=support_hub_main&shub_account_id='.$this->get('shub_account_id').'&shub_message_id=new';
215 | }
216 | public function link_refresh(){
217 | return 'admin.php?page=support_hub_settings&tab='.$this->shub_extension.'&manualrefresh&shub_account_id='.$this->get('shub_account_id').'';
218 | }
219 |
220 |
221 | }
--------------------------------------------------------------------------------
/classes/class-support-hub-item.php:
--------------------------------------------------------------------------------
1 | account = $account;
7 | $this->load($shub_item_id);
8 | }
9 |
10 | /* @var $account SupportHub_account */
11 | public $account = false;
12 | public $shub_item_id = false; // the current user id in our system.
13 | public $details = array();
14 | public $json_fields = array('item_data');
15 |
16 | private function reset(){
17 | $this->shub_item_id = false;
18 | $this->details = array(
19 | 'shub_item_id' => '',
20 | 'shub_account_id' => '',
21 | 'shub_product_id' => '',
22 | 'item_name' => '',
23 | 'last_message' => '',
24 | 'last_checked' => '',
25 | 'network_key' => '',
26 | 'item_data' => array(),
27 | );
28 | foreach($this->details as $field_id => $field_data){
29 | $this->{$field_id} = $field_data;
30 | }
31 | }
32 |
33 | public function create_new(){
34 | $this->reset();
35 | $this->shub_item_id = shub_update_insert('shub_item_id',false,'shub_item',array(
36 | 'item_name' => '',
37 | ));
38 | $this->load($this->shub_item_id);
39 | }
40 |
41 | public function load($shub_item_id = false){
42 | if(!$shub_item_id)$shub_item_id = $this->shub_item_id;
43 | $this->reset();
44 | $this->shub_item_id = $shub_item_id;
45 | if($this->shub_item_id){
46 | $data = shub_get_single('shub_item','shub_item_id',$this->shub_item_id);
47 | foreach($this->details as $key=>$val){
48 | $this->details[$key] = $data && isset($data[$key]) ? $data[$key] : $val;
49 | if(in_array($key,$this->json_fields)){
50 | $this->details[$key] = @json_decode($this->details[$key],true);
51 | if(!is_array($this->details[$key]))$this->details[$key] = array();
52 | }
53 | }
54 | if(!is_array($this->details) || $this->details['shub_item_id'] != $this->shub_item_id){
55 | $this->reset();
56 | return false;
57 | }
58 | }
59 | foreach($this->details as $key=>$val){
60 | $this->{$key} = $val;
61 | }
62 | return $this->shub_item_id;
63 | }
64 |
65 | public function get($field){
66 | return isset($this->{$field}) ? $this->{$field} : false;
67 | }
68 |
69 | public function update($field,$value){
70 | // what fields to we allow? or not allow?
71 | if(in_array($field,array('shub_item_id')))return;
72 | if($this->shub_item_id){
73 | $this->{$field} = $value;
74 | if(in_array($field,$this->json_fields)){
75 | $value = json_encode($value);
76 | }
77 | shub_update_insert('shub_item_id',$this->shub_item_id,'shub_item',array(
78 | $field => $value,
79 | ));
80 | }
81 | }
82 | public function delete(){
83 | if($this->shub_item_id) {
84 | // delete all the messages for this item.
85 | $messages = shub_get_multiple('shub_message',array(
86 | 'shub_item_id' => $this->shub_item_id,
87 | ),'shub_message_id');
88 | foreach($messages as $message){
89 | if($message && isset($message['shub_item_id']) && $message['shub_item_id'] == $this->shub_item_id){
90 | shub_delete_from_db( 'shub_message', 'shub_message_id', $message['shub_message_id'] );
91 | shub_delete_from_db( 'shub_message_comment', 'shub_message_id', $message['shub_message_id'] );
92 | shub_delete_from_db( 'shub_message_link', 'shub_message_id', $message['shub_message_id'] );
93 | shub_delete_from_db( 'shub_message_read', 'shub_message_id', $message['shub_message_id'] );
94 | }
95 | }
96 | shub_delete_from_db( 'shub_item', 'shub_item_id', $this->shub_item_id );
97 | }
98 | }
99 |
100 | }
--------------------------------------------------------------------------------
/classes/class-support-hub-outbox.php:
--------------------------------------------------------------------------------
1 | load($shub_outbox_id);
13 | }
14 | }
15 |
16 | private $shub_outbox_id = false; // the current outbox id in our system.
17 | public $details = array();
18 | private $json_fields = array('message_data');
19 |
20 | public $db_table = 'shub_outbox';
21 | public $db_primary_key = 'shub_outbox_id';
22 |
23 | public function reset(){
24 | $this->{$this->db_primary_key} = false;
25 | $this->details = array(
26 | 'shub_outbox_id' => '',
27 | 'shub_extension' => '',
28 | 'shub_account_id' => '',
29 | 'shub_message_id' => '',
30 | 'shub_message_comment_id' => '',
31 | 'queue_time' => '',
32 | 'shub_status' => '',
33 | 'message_data' => array(),
34 | );
35 | foreach($this->details as $field_id => $field_data){
36 | $this->{$field_id} = $field_data;
37 | }
38 | }
39 |
40 | public function create_new(){
41 | $this->reset();
42 | $this->{$this->db_primary_key} = shub_update_insert($this->db_primary_key,false,$this->db_table,array(
43 | 'queue_time' => time(),
44 | 'shub_status' => _SHUB_OUTBOX_STATUS_QUEUED,
45 | ));
46 | $this->load($this->{$this->db_primary_key});
47 | }
48 |
49 | public function load_by($field, $value){
50 | $this->reset();
51 | if(!empty($field) && !empty($value) && isset($this->details[$field])){
52 | $data = shub_get_single($this->db_table,$field,$value);
53 | if($data && isset($data[$field]) && $data[$field] == $value && $data[$this->db_primary_key]){
54 | $this->load($data[$this->db_primary_key]);
55 | return true;
56 | }
57 | }
58 | return false;
59 | }
60 |
61 | public function load($shub_outbox_id = false){
62 | if(!$shub_outbox_id)$shub_outbox_id = $this->{$this->db_primary_key};
63 | $this->reset();
64 | $this->{$this->db_primary_key} = $shub_outbox_id;
65 | if($this->{$this->db_primary_key}){
66 | $data = shub_get_single($this->db_table,$this->db_primary_key,$this->{$this->db_primary_key});
67 | foreach($this->details as $key=>$val){
68 | $this->details[$key] = $data && isset($data[$key]) ? $data[$key] : $val;
69 | if(in_array($key,$this->json_fields)){
70 | $this->details[$key] = @json_decode($this->details[$key],true);
71 | if(!is_array($this->details[$key]))$this->details[$key] = array();
72 | }
73 | }
74 | if(!is_array($this->details) || $this->details[$this->db_primary_key] != $this->{$this->db_primary_key}){
75 | $this->reset();
76 | return false;
77 | }
78 | }
79 | foreach($this->details as $key=>$val){
80 | $this->{$key} = $val;
81 | }
82 | return $this->{$this->db_primary_key};
83 | }
84 |
85 | public function get($field){
86 | return isset($this->{$field}) ? $this->{$field} : false;
87 | }
88 |
89 | public function update($field,$value=false){
90 | if(is_array($field)){
91 | foreach($field as $key=>$val){
92 | $this->update($key,$val);
93 | }
94 | return;
95 | }
96 | // what fields to we allow? or not allow?
97 | if(in_array($field,array($this->db_primary_key)))return;
98 | if($this->{$this->db_primary_key} && isset($this->details[$field])){
99 | $this->{$field} = $value;
100 | $this->details[$field] = $value;
101 | if(in_array($field,$this->json_fields)){
102 | $value = json_encode($value);
103 | }
104 | shub_update_insert($this->db_primary_key,$this->{$this->db_primary_key},$this->db_table,array(
105 | $field => $value,
106 | ));
107 | }
108 | }
109 |
110 | public function update_outbox_data($outbox_data){
111 | if(is_array($outbox_data)){
112 | // yes, this member has some items, save these items to the account ready for selection in the settings area.
113 | $save_data = $this->get('message_data');
114 | if(!is_array($save_data))$save_data=array();
115 | $save_data = array_merge($save_data,$outbox_data);
116 | $this->update('message_data',$save_data);
117 | }
118 | }
119 | public function delete(){
120 | if($this->{$this->db_primary_key}) {
121 | shub_delete_from_db( $this->db_table, $this->db_primary_key, $this->{$this->db_primary_key} );
122 | }
123 | }
124 |
125 | public function send_queued($force=false){
126 | if($this->shub_outbox_id){
127 | // check the status of it.
128 | // todo - find any ones that are stuck in 'SENDING' status for too long and send those as well.
129 | if($force || $this->get('shub_status') == _SHUB_OUTBOX_STATUS_QUEUED){
130 | $managers = SupportHub::getInstance()->message_managers;
131 | if(!empty($this->shub_extension) && isset($managers[$this->shub_extension]) && $managers[$this->shub_extension]->is_enabled()){
132 | // find the message manager responsible for this message and fire off the reply.
133 | $message = $managers[$this->shub_extension]->get_message(false, false, $this->shub_message_id);
134 | if($message->get('shub_message_id') == $this->shub_message_id){
135 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_INFO,'sending','Starting Send: '.$this->shub_message_id);
136 | // todo: look at adding a better "lock" so we don't sent duplicate messages between the QUEUE/SENDING get/update
137 | $this->update('shub_status', _SHUB_OUTBOX_STATUS_SENDING);
138 | // sweet! we're here, send the reply.
139 | ob_start();
140 | $status = $message->send_queued_comment_reply($this->shub_message_comment_id, $this, true);
141 | $errors = ob_get_clean();
142 | if($status){
143 | // success! it worked! flag it as sent.
144 | // todo: remove from this table? not sure.
145 | $this->update('shub_status', _SHUB_OUTBOX_STATUS_SENT);
146 | }else{
147 | $this->update('shub_status', _SHUB_OUTBOX_STATUS_FAILED);
148 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_ERROR,'sending','Failed to Send: '.$this->shub_message_id.': error: '.$errors);
149 | }
150 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_INFO,'sending','Finished Send: '.$this->shub_message_id);
151 | return $errors;
152 |
153 |
154 | }
155 | }
156 | }
157 | }
158 | return false;
159 | }
160 |
161 | public static function get_pending(){
162 | return array_merge(shub_get_multiple('shub_outbox',array('shub_status'=>_SHUB_OUTBOX_STATUS_QUEUED),'shub_outbox_id'),shub_get_multiple('shub_outbox',array('shub_status'=>_SHUB_OUTBOX_STATUS_SENDING),'shub_outbox_id'));
163 | }
164 | public static function get_failed(){
165 | return shub_get_multiple('shub_outbox',array('shub_status'=>_SHUB_OUTBOX_STATUS_FAILED),'shub_outbox_id');
166 | }
167 |
168 | }
169 |
--------------------------------------------------------------------------------
/classes/class-support-hub-product.php:
--------------------------------------------------------------------------------
1 | load($shub_product_id);
8 | }
9 | }
10 |
11 | private $shub_product_id = false; // the current user id in our system.
12 | private $details = array();
13 | private $json_fields = array('product_data');
14 |
15 | private function reset(){
16 | $this->shub_product_id = false;
17 | $this->details = array(
18 | 'shub_product_id' => '',
19 | 'product_name' => '',
20 | 'product_data' => array(),
21 | );
22 | foreach($this->details as $field_id => $field_data){
23 | $this->{$field_id} = $field_data;
24 | }
25 | }
26 |
27 | public function create_new(){
28 | $this->reset();
29 | $this->shub_product_id = shub_update_insert('shub_product_id',false,'shub_product',array(
30 | 'product_name' => '',
31 | ));
32 | $this->load($this->shub_product_id);
33 | }
34 |
35 | public function load($shub_product_id = false){
36 | if(!$shub_product_id)$shub_product_id = $this->shub_product_id;
37 | $this->reset();
38 | $this->shub_product_id = $shub_product_id;
39 | if($this->shub_product_id){
40 | $data = shub_get_single('shub_product','shub_product_id',$this->shub_product_id);
41 | foreach($this->details as $key=>$val){
42 | $this->details[$key] = $data && isset($data[$key]) ? $data[$key] : $val;
43 | if(in_array($key,$this->json_fields)){
44 | $this->details[$key] = @json_decode($this->details[$key],true);
45 | if(!is_array($this->details[$key]))$this->details[$key] = array();
46 | }
47 | }
48 | if(!is_array($this->details) || $this->details['shub_product_id'] != $this->shub_product_id){
49 | $this->reset();
50 | return false;
51 | }
52 | }
53 | foreach($this->details as $key=>$val){
54 | $this->{$key} = $val;
55 | }
56 | return $this->shub_product_id;
57 | }
58 |
59 | public function get($field){
60 | return isset($this->{$field}) ? $this->{$field} : false;
61 | }
62 |
63 | public function update( $field, $value = false ) {
64 | if ( is_array( $field ) ) {
65 | foreach ( $field as $key => $val ) {
66 | if ( isset( $this->details[ $key ] ) ) {
67 | $this->update( $key, $val );
68 | }
69 | }
70 | return;
71 | }
72 | // what fields to we allow? or not allow?
73 | if ( in_array( $field, array( 'shub_product_id' ) ) ) {
74 | return;
75 | }
76 | if ( $this->shub_product_id ) {
77 | $this->{$field} = $value;
78 | if ( in_array( $field, $this->json_fields ) ) {
79 | $value = json_encode( $value );
80 | }
81 | shub_update_insert( 'shub_product_id', $this->shub_product_id, 'shub_product', array(
82 | $field => $value,
83 | ) );
84 | }
85 | }
86 | public function delete(){
87 | if($this->shub_product_id) {
88 | shub_delete_from_db( 'shub_product', 'shub_product_id', $this->shub_product_id );
89 | }
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/classes/class-support-hub-user.php:
--------------------------------------------------------------------------------
1 | load($shub_user_id);
8 | }
9 | }
10 |
11 | public $shub_user_id = false; // the current user id in our system.
12 | public $details = array();
13 | private $json_fields = array('user_data');
14 |
15 | public $db_table = 'shub_user'; // overwritten by individual network user classes
16 | public $db_table_meta = 'shub_user_meta'; // overwritten by individual network user classes
17 | public $db_primary_key = 'shub_user_id'; // overwritten by individual network user classes
18 |
19 | public function reset(){
20 | $this->{$this->db_primary_key} = false;
21 | $this->details = array(
22 | 'shub_user_id' => '',
23 | 'user_fname' => '',
24 | 'user_lname' => '',
25 | 'user_username' => '',
26 | 'user_email' => '',
27 | 'shub_linked_user_id' => 0,
28 | 'user_data' => array(),
29 | );
30 | foreach($this->details as $field_id => $field_data){
31 | $this->{$field_id} = $field_data;
32 | }
33 | }
34 |
35 | public function create_new(){
36 | $this->reset();
37 | $this->{$this->db_primary_key} = shub_update_insert($this->db_primary_key,false,$this->db_table,array(
38 | 'user_fname' => '',
39 | ));
40 | $this->load($this->{$this->db_primary_key});
41 | }
42 |
43 | // private static $_latest_load_create = array();
44 | public function load_by($field, $value){
45 | $this->reset();
46 | if(!empty($field) && !empty($value) && isset($this->details[$field])){
47 | // if(isset(self::$_latest_load_create[$field][$value]) && self::$_latest_load_create[$field][$value] > 0){
48 | // $this->load(self::$_latest_load_create[$field][$value]);
49 | // return true;
50 | // }
51 | //$data = shub_get_single($this->db_table,$field,$value);
52 | $data = shub_get_multiple($this->db_table,array($field=>$value),$this->db_primary_key);
53 | if(!$data){
54 | // check if it was recently created? gets around weird WP caching issue, resuling in mass duplicate of user details on bulk import
55 | // if(!isset(self::$_latest_load_create[$field]))self::$_latest_load_create[$field]=array();
56 | // self::$_latest_load_create[$field][$value] = false; // pending creating maybe?
57 | }else if($data){
58 | // && isset($data[$field]) && $data[$field] == $value && $data[$this->db_primary_key]){
59 | foreach($data as $possible_match) {
60 | if(!empty($possible_match[$field]) && strtolower($possible_match[$field]) == strtolower($value) && (int)$possible_match[$this->db_primary_key] > 0) {
61 | $this->load( $possible_match[ $this->db_primary_key ] );
62 | if ( ! $this->get( 'shub_user_id' ) ) {
63 | // didn't work?
64 | }else{
65 | // successfully loaded one!
66 | return $data;
67 | }
68 | }
69 | }
70 | }
71 | }
72 | return false;
73 | }
74 | public function load_by_meta($meta_key, $meta_val){
75 | $this->reset();
76 | if($meta_key && $meta_val){
77 | $data = shub_get_multiple($this->db_table_meta,array('meta_key'=>$meta_key,'meta_val'=>$meta_val),$this->db_primary_key);
78 | if($data){
79 | foreach($data as $possible_match) {
80 | if(!empty($possible_match['meta_key']) && !empty($possible_match['meta_val']) && $possible_match['meta_key'] == $meta_key && (int)$possible_match[$this->db_primary_key] > 0) {
81 | $this->load( $possible_match[ $this->db_primary_key ] );
82 | if ( ! $this->get( 'shub_user_id' ) ) {
83 | // todo: ran into this error where I cleared the user table without clearing the meta table/
84 | // it tries to load a user that doesn't exist
85 | shub_delete_from_db( $this->db_table_meta, 'shub_user_id', $possible_match[ $this->db_primary_key ] );
86 | }else{
87 | // successfully loaded one!
88 | return $data;
89 | }
90 | }
91 | }
92 | }else{
93 | // echo 'No match'; print_r($data);
94 | }
95 | }
96 | return false;
97 | }
98 |
99 | public function load($shub_user_id = false){
100 | if(!$shub_user_id)$shub_user_id = $this->{$this->db_primary_key};
101 | $this->reset();
102 | $this->{$this->db_primary_key} = $shub_user_id;
103 | if($this->{$this->db_primary_key}){
104 | $data = shub_get_single($this->db_table,$this->db_primary_key,$this->{$this->db_primary_key});
105 | foreach($this->details as $key=>$val){
106 | $this->details[$key] = $data && isset($data[$key]) ? $data[$key] : $val;
107 | if(in_array($key,$this->json_fields)){
108 | $this->details[$key] = @json_decode($this->details[$key],true);
109 | if(!is_array($this->details[$key]))$this->details[$key] = array();
110 | }
111 | }
112 | if(!is_array($this->details) || $this->details[$this->db_primary_key] != $this->{$this->db_primary_key}){
113 | $this->reset();
114 | return false;
115 | }
116 | }
117 | foreach($this->details as $key=>$val){
118 | $this->{$key} = $val;
119 | }
120 | return $this->{$this->db_primary_key};
121 | }
122 |
123 | public function get($field){
124 | return isset($this->{$field}) ? $this->{$field} : false;
125 | }
126 |
127 | public function update($field,$value){
128 | // what fields to we allow? or not allow?
129 | if(in_array($field,array($this->db_primary_key)))return;
130 | if($this->{$this->db_primary_key}){
131 | $this->{$field} = $value;
132 | // if(isset(self::$_latest_load_create[$field][$value])){
133 | // self::$_latest_load_create[$field][$value] = $this->{$this->db_primary_key};
134 | // }
135 | if(in_array($field,$this->json_fields)){
136 | $value = json_encode($value);
137 | }
138 | shub_update_insert($this->db_primary_key,$this->{$this->db_primary_key},$this->db_table,array(
139 | $field => $value,
140 | ));
141 | }
142 | }
143 |
144 | public function update_user_data($user_data){
145 | if(is_array($user_data)){
146 | // yes, this member has some items, save these items to the account ready for selection in the settings area.
147 | $save_data = $this->get('user_data');
148 | if(!is_array($save_data))$save_data=array();
149 | $save_data = array_merge($save_data,$user_data);
150 | $this->update('user_data',$save_data);
151 | }
152 | }
153 | public function get_meta($key=false,$val=false){
154 | $return = array();
155 | if(!$key){
156 | // return all meta values in an associative array.
157 | $all_meta = shub_get_multiple($this->db_table_meta,array('shub_user_id'=>$this->shub_user_id));
158 | foreach($all_meta as $meta){
159 | if(!isset($return[$meta['meta_key']]))$return[$meta['meta_key']]=array();
160 | $return[$meta['meta_key']][] = $meta['meta_val'];
161 | }
162 | }else if($key && !$val){
163 | // return all matching meta values in an associative array.
164 | $all_meta = shub_get_multiple($this->db_table_meta,array('shub_user_id'=>$this->shub_user_id,'meta_key'=>$key));
165 | foreach($all_meta as $meta){
166 | $return[] = $meta['meta_val'];
167 | }
168 | }else{
169 | // return all matching meta values in an associative array.
170 | $all_meta = shub_get_single($this->db_table_meta,array('shub_user_id','meta_key','meta_val'),array($this->shub_user_id,$key,$val));
171 | $return = $all_meta['meta_val'];
172 | }
173 | return $return;
174 | }
175 | public function add_meta($key,$val){
176 | if((int)$this->shub_user_id>0) {
177 | shub_update_insert(false, false, $this->db_table_meta, array(
178 | 'shub_user_id' => $this->shub_user_id,
179 | 'meta_key' => $key,
180 | 'meta_val' => $val,
181 | ));
182 | }
183 | }
184 | public function add_unique_meta($key,$val){
185 | if((int)$this->shub_user_id>0) {
186 | $existing = $this->get_meta($key,$val);
187 | if(!$existing){
188 | $this->add_meta($key,$val);
189 | }
190 | }
191 | }
192 | public function update_meta($key,$oldval,$newval){
193 | $existing = $this->get_meta($key,$oldval);
194 | if(!$existing){
195 | $this->add_meta($key,$newval);
196 | }else{
197 | global $wpdb;
198 | $wpdb->update(_support_hub_DB_PREFIX.$this->db_table_meta,array(
199 | 'meta_key' => $key,
200 | 'meta_val' => $newval,
201 | ), array(
202 | 'shub_user_id' => $this->shub_user_id,
203 | 'meta_key' => $key,
204 | 'meta_val' => $oldval,
205 | ));
206 | }
207 |
208 | }
209 | public function delete(){
210 | if($this->{$this->db_primary_key}) {
211 | shub_delete_from_db( $this->db_table, $this->db_primary_key, $this->{$this->db_primary_key} );
212 | }
213 | }
214 |
215 | public function get_link(){
216 | return '#';
217 | }
218 | public function get_full_link(){
219 | $data = $this->get('user_data');
220 | $return = '';
221 | // this is for UCM:
222 | if(!empty($data['name']) && !empty($data['last_name'])){
223 | $return .= htmlspecialchars($data['name'] .' '.$data['last_name'] .' ');
224 | }
225 | // this is for Envato
226 | if(!empty($data['envato']['username'])){
227 | $return .= '';
228 | $return .= htmlspecialchars($data['envato']['username']);
229 | $return .= ' ';
230 | }
231 | // this is fallback
232 | if(!strlen($return)){
233 | $return .= '';
234 | $return .= htmlspecialchars($this->get('user_username'));
235 | $return .= ' ';
236 | }
237 | return $return;
238 | }
239 |
240 | public function get_image(){
241 |
242 | $data = $this->get('user_data');
243 | if($data && !empty($data['image'])){
244 | return $data['image'];
245 | }
246 | if($this->get('user_email')){
247 | $hash = md5(trim($this->get('user_email')));
248 | return '//www.gravatar.com/avatar/'.$hash.'?d=wavatar';
249 | }
250 | return '';
251 | }
252 | public function get_name(){
253 | return $this->get('user_username');
254 | }
255 |
256 |
257 | }
258 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dtbaker/support-hub",
3 | "description": "Import all support messages into an easy to use inbox.",
4 | "type": "wordpress-plugin",
5 | "keywords": ["support"],
6 | "homepage": "http://supporthub.co",
7 | "time": "2015-05-24",
8 | "license": "GPL-2.0+",
9 | "authors": [
10 | {
11 | "name": "David Baker",
12 | "email": "dtbaker@gmail.com",
13 | "homepage": "http://www.dtbaker.net",
14 | "role": "Developer"
15 | }
16 | ],
17 | "require": {
18 | "hieu-le/wordpress-xmlrpc-client":"~2.0"
19 | }
20 | }
--------------------------------------------------------------------------------
/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
5 | "This file is @generated automatically"
6 | ],
7 | "hash": "0798c2826aa12845c12e8e2d19cfc0a2",
8 | "packages": [
9 | {
10 | "name": "hieu-le/wordpress-xmlrpc-client",
11 | "version": "2.4.2",
12 | "source": {
13 | "type": "git",
14 | "url": "https://github.com/letrunghieu/wordpress-xmlrpc-client.git",
15 | "reference": "07a1ad3f2fed562c01f00161c37f6ab2e276d272"
16 | },
17 | "dist": {
18 | "type": "zip",
19 | "url": "https://api.github.com/repos/letrunghieu/wordpress-xmlrpc-client/zipball/07a1ad3f2fed562c01f00161c37f6ab2e276d272",
20 | "reference": "07a1ad3f2fed562c01f00161c37f6ab2e276d272",
21 | "shasum": ""
22 | },
23 | "require": {
24 | "ext-xmlrpc": "*",
25 | "php": ">=5.3.0"
26 | },
27 | "require-dev": {
28 | "illuminate/support": "~4.0",
29 | "php-vcr/php-vcr": "dev-master",
30 | "php-vcr/phpunit-testlistener-vcr": "*",
31 | "phpunit/phpunit": "*",
32 | "symfony/yaml": "2.*"
33 | },
34 | "type": "library",
35 | "autoload": {
36 | "psr-4": {
37 | "HieuLe\\WordpressXmlrpcClient\\": "src/"
38 | }
39 | },
40 | "notification-url": "https://packagist.org/downloads/",
41 | "license": [
42 | "MIT"
43 | ],
44 | "authors": [
45 | {
46 | "name": "Hieu Le",
47 | "email": "letrunghieu.cse09@gmail.com",
48 | "homepage": "http://www.hieule.info"
49 | }
50 | ],
51 | "description": "A PHP client for Wordpress websites that closely implement the XML-RPC WordPress API with full test suite built in",
52 | "keywords": [
53 | "api",
54 | "client",
55 | "remote",
56 | "wordpress",
57 | "xmlrpc"
58 | ],
59 | "time": "2015-05-29 03:17:43"
60 | }
61 | ],
62 | "packages-dev": [],
63 | "aliases": [],
64 | "minimum-stability": "stable",
65 | "stability-flags": [],
66 | "prefer-stable": false,
67 | "prefer-lowest": false,
68 | "platform": [],
69 | "platform-dev": []
70 | }
71 |
--------------------------------------------------------------------------------
/extensions/bbpress/bbpress_message.php:
--------------------------------------------------------------------------------
1 |
5 |
6 | get('shub_account_id') == $shub_account_id){
11 | $bbpress_message = new shub_bbpress_message( $bbpress, false, $shub_message_id );
12 | $bbpress_message->output_message_page('popup');
13 |
14 | }
15 | }
16 |
17 | if($shub_account_id && !(int)$shub_message_id){
18 | $bbpress = new shub_bbpress_account($shub_account_id);
19 | if($shub_account_id && $bbpress->get('shub_account_id') == $shub_account_id){
20 |
21 | /* @var $groups shub_bbpress_item[] */
22 | $groups = $bbpress->get('groups');
23 | //print_r($groups);
24 | ?>
25 |
188 |
189 |
224 |
225 |
229 |
--------------------------------------------------------------------------------
/extensions/bbpress/class.shub_bbpress_forum.php:
--------------------------------------------------------------------------------
1 | get_messages(array(
8 | 'shub_status' => _shub_MESSAGE_STATUS_PENDINGSEND,
9 | ));
10 | $now = time();
11 | foreach($messages as $message){
12 | if(isset($message['message_time']) && $message['message_time'] < $now){
13 | $shub_bbpress_message = new shub_bbpress_message(false, $this, $message['shub_message_id']);
14 | $shub_bbpress_message->send_queued($debug);
15 | }
16 | }*/
17 |
18 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_INFO,'bbpress','Starting bbPress Cron for Forum: '.$this->get('item_name'));
19 | $this->load_latest_item_data($debug);
20 | }
21 |
22 | public function load_latest_item_data($debug = false){
23 | // serialise this result into account_data.
24 | if(!$this->account){
25 | echo 'No bbpress account linked, please try again';
26 | return;
27 | }
28 |
29 | $api = $this->account->get_api();
30 |
31 | $network_key = $this->get('network_key');
32 | if(!$network_key){
33 | echo 'No bbpress forum id found';
34 | return;
35 | }
36 |
37 | // first we seed the cache with the latest bbpress replies and topics
38 | // we do this because it's not possible to filter based on "post_parent" through the WordPress API (SILLY!)
39 | // so this saves us calling getPost() a lot of times.
40 | $filter_replies = array(
41 | 'post_type' => 'reply',
42 | 'number' => 100,
43 | 'post_status' => 'publish',
44 | //'post_parent' =>
45 | );
46 | $api_result_latest_replies = $this->account->get_api_cache($filter_replies);
47 | $api_result_latest_replies = $api_result_latest_replies ? $api_result_latest_replies : $api->getPosts($filter_replies);
48 |
49 | $filter_topics = array(
50 | 'post_type' => 'topic',
51 | 'number' => 100,
52 | 'post_status' => 'publish',
53 | //'post_parent' =>
54 | );
55 | $api_result_latest_topics = $this->account->get_api_cache($filter_topics);
56 | $api_result_latest_topics = $api_result_latest_topics ? $api_result_latest_topics : $api->getPosts($filter_topics);
57 |
58 |
59 | // loop through our latest replies and see if any of them are from a thread that sits under this forum
60 | // COMPLETELY THE REVERSE WAY THAT WE SHOULD BE DOING IT! rar!
61 |
62 | $forum_topics = array();
63 |
64 | foreach($api_result_latest_topics as $forum_topic){
65 | if($forum_topic['post_parent'] == $network_key){
66 | $forum_topic['timestamp'] = $forum_topic['post_date']->timestamp;
67 | // yay! this reply is part of a topic that is part of this forum. keep it.
68 | if(!isset($forum_topics[$forum_topic['post_id']])){
69 | $forum_topics[$forum_topic['post_id']] = $forum_topic;
70 | }
71 | if(!isset($forum_topics[$forum_topic['post_id']]['replies'])){
72 | $forum_topics[$forum_topic['post_id']]['replies'] = array();
73 | }
74 | // we need to add our main forum_topic onto the replies array so that all messages go into the 'comments' database table.
75 | $forum_topics[$forum_topic['post_id']]['replies'][] = $forum_topic;
76 | }
77 | }
78 | foreach($api_result_latest_replies as $forum_reply){
79 |
80 | // find its parent and see if it is from this forum.
81 | $found_parent = false;
82 | foreach($api_result_latest_topics as $forum_topic){
83 | if($forum_topic['post_id'] == $forum_reply['post_parent']){
84 | $found_parent = $forum_topic;
85 | break;
86 | }
87 | }
88 | if(!$found_parent){
89 | $api_result_parent = $api->getPost($forum_reply['post_parent']);
90 | if($api_result_parent){
91 | $found_parent = $api_result_parent;
92 | $api_result_latest_topics[] = $api_result_parent; // add to cache so we hopefully dont have to hit it again if it's a popular topic
93 | }
94 | }
95 | if($found_parent){
96 | // found a parent post, check if it's part of this forum.
97 | if($found_parent['post_parent'] == $network_key){
98 | $found_parent['timestamp'] = $found_parent['post_date']->timestamp;
99 | $forum_reply['timestamp'] = $forum_reply['post_date']->timestamp;
100 | // yay! this reply is part of a topic that is part of this forum. keep it.
101 | if(!isset($forum_topics[$found_parent['post_id']])){
102 | $forum_topics[$found_parent['post_id']] = $found_parent;
103 | }
104 | if(!isset($forum_topics[$found_parent['post_id']]['replies'])){
105 | $forum_topics[$found_parent['post_id']]['replies'] = array();
106 | }
107 | $forum_topics[$found_parent['post_id']]['replies'][] = $found_parent;
108 | $forum_topics[$found_parent['post_id']]['replies'][] = $forum_reply;
109 | if(!isset($forum_topics[$found_parent['post_id']]['timestamp'])){
110 | $forum_topics[$found_parent['post_id']]['timestamp'] = $found_parent['timestamp'];
111 | }
112 | $forum_topics[$found_parent['post_id']]['timestamp'] = max($forum_reply['post_date']->timestamp,$forum_topics[$found_parent['post_id']]['timestamp']);
113 | }
114 |
115 | /*echo date('Y-m-d',$forum_reply['post_date']->timestamp);
116 | echo " '".$forum_reply['link'].' ';
117 | echo $forum_reply['post_content'];
118 | echo "Parent is: ";
119 | echo date('Y-m-d',$found_parent['post_date']->timestamp);
120 | echo " '".$found_parent['link'].' ';
121 | echo '
';*/
122 | }else{
123 |
124 | }
125 | }
126 | uasort($forum_topics,function($a,$b){
127 | return $a['timestamp'] < $b['timestamp'];
128 | });
129 | // cache them for any other bbpress forum calls that are run during the same cron job process.
130 | $this->account->set_api_cache($filter_replies,$api_result_latest_replies);
131 | $this->account->set_api_cache($filter_topics,$api_result_latest_topics);
132 |
133 |
134 | // we keep a record of the last message received so we know where to stop checking the feed
135 | $last_message_received = (int)$this->get('last_message');
136 | if($debug)echo "Getting the latest replies for forum: ".$network_key." (last message in database is from ".shub_print_date($last_message_received,true).")
\n";
137 |
138 | $newest_message_received = 0;
139 |
140 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_INFO,'bbpress','Found total of '.count($forum_topics)." forum topics from API calls");
141 | $count = 0;
142 | foreach($forum_topics as $forum_topic){
143 | $message_time = $forum_topic['timestamp'];
144 | $newest_message_received = max($newest_message_received,$message_time);
145 | if($message_time <= $last_message_received)break; // all done here.
146 |
147 | $bbpress_message = new shub_bbpress_message($this->account, $this, false);
148 | $bbpress_message -> load_by_bbpress_id($forum_topic['post_id'], $forum_topic, 'forum_topic', $debug);
149 | $count++;
150 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_INFO,'bbpress','Imported forum topic ID '.$bbpress_message->get( 'network_key' )." with ".count($forum_topic['replies']).' replies');
151 | if($debug) {
152 | ?>
153 |
154 |
Imported forum topic ID: get( 'network_key' ); ?> with replies.
155 |
156 | log_data(_SUPPORT_HUB_LOG_INFO, 'bbpress', 'Completed Cron Import: '.$count.' new forum topics');
162 | if($debug)echo " imported $count new forum comments
";
163 |
164 | $this->update('last_message',$newest_message_received);
165 | $this->update('last_checked',time());
166 | }
167 |
168 | public function link_refresh(){
169 | return 'admin.php?page=support_hub_settings&tab=bbpress&manualrefresh&shub_account_id='.$this->get('shub_account_id').'&network_key='.$this->get('network_key');
170 | }
171 |
172 |
173 | }
174 |
--------------------------------------------------------------------------------
/extensions/bbpress/class.shub_bbpress_user.php:
--------------------------------------------------------------------------------
1 | get('user_email')){
8 | $hash = md5(trim($this->get('user_email')));
9 | return '//www.gravatar.com/avatar/'.$hash.'?d=identicon';
10 | }
11 | $default = parent::get_image();
12 | if(!$default){
13 | return plugins_url('extensions/bbpress/default-user.jpg',_DTBAKER_SUPPORT_HUB_CORE_FILE_);
14 | }
15 | return $default;
16 | }
17 |
18 |
19 | }
--------------------------------------------------------------------------------
/extensions/bbpress/default-user.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtbaker/support-hub/21466135d584b1c3c991dc8c09c3ca3d888ec125/extensions/bbpress/default-user.jpg
--------------------------------------------------------------------------------
/extensions/bbpress/init.bbpress.php:
--------------------------------------------------------------------------------
1 | id = 'bbpress';
17 | $shub['bbpress']->friendly_name = 'bbPress';
18 | $shub['bbpress']->desc = 'Import and Reply to WordPress bbPress forum posts.';
19 | return $shub;
20 | }
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/extensions/bbpress/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtbaker/support-hub/21466135d584b1c3c991dc8c09c3ca3d888ec125/extensions/bbpress/logo.png
--------------------------------------------------------------------------------
/extensions/bbpress/shub_bbpress.css:
--------------------------------------------------------------------------------
1 | #bbpress_edit_form a{
2 | text-decoration: none;
3 | }
4 | .bbpress_message_full{
5 | display: none;
6 | background: #edeff4;
7 | border-radius: 5px;
8 | padding: 0 5px;
9 | }
10 | .bbpress_message_picture{
11 | float: left;
12 | width: 35px;
13 | margin: 0 5px 0 0;
14 | }
15 | .bbpress_message_picture img{
16 | max-width: 100%;
17 | height: auto;
18 | }
19 | .bbpress_message_reply{
20 | margin-left: 40px;
21 | }
22 | .bbpress_message_reply button{
23 | float: left;
24 | height:2em;
25 | border:1px solid #bdc7d8;
26 | margin:0 0 0 5px;
27 | }
28 | .bbpress_message_compose textarea{
29 | background: #FFF;
30 | border:1px solid #bdc7d8;
31 | width:88%;
32 | min-height:2em;
33 | resize: none;
34 | }
35 | .bbpress_message_reply textarea{
36 | float: left;
37 | background: #FFF;
38 | border:1px solid #bdc7d8;
39 | width:88%;
40 | min-height:2em;
41 | resize: none;
42 | }
43 | .bbpress_message_body{
44 | margin-left: 40px;
45 | }
46 | .bbpress_message_actions{
47 | clear: both;
48 | font-size: 0.9em;
49 | margin-left: 40px;
50 | padding: 0 0 5px 0;
51 | }
52 | .bbpress_message_header{
53 | /*display: inline;
54 | float: left;*/
55 | padding: 0 5px 0 0;
56 | font-weight: bold;
57 | }
58 | .bbpress_message_header span{
59 | font-weight: normal;
60 | color:#CCC;
61 | }
62 | .bbpress_message{
63 | border-bottom: 1px solid #d0d4e4;
64 | padding: 5px 0 0 5px;
65 | }
66 | #bbpress_edit_form > .bbpress_message_replies{
67 | margin-left: 0;
68 | border-left: none;
69 | }
70 | .bbpress_message_reply_box{
71 | display: none;
72 | }
73 | .bbpress_message_reply_box_level1{
74 | display: block;
75 | }
76 | .bbpress_message_replies{
77 | margin-left: 40px;
78 | border-left: 1px solid #d0d4e4;
79 | }
80 | .bbpress_picture{
81 | background: #FFF;
82 | padding: 4px;
83 | display: inline-block;
84 | }
85 | .bbpress_picture img{
86 | max-width: 100%;
87 | height:auto;
88 | }
89 | .bbpress_icon{
90 | height:27px;
91 | width: auto;
92 | margin: 5px 5px 0 0;
93 | float: left;
94 | }
95 | .bbpress_icon.small{
96 | height:15px;
97 | float: none;
98 | vertical-align: middle;
99 | margin:0 5px 0 0;
100 | }
101 |
102 | #bbpress_message_header{
103 | border:1px solid #d0d4e4;
104 | background: #FFF;
105 | margin:5px;
106 | padding: 8px 5px 14px 10px;
107 | }
108 | #bbpress_message_holder{
109 | }
110 |
111 | .bbpress_message_summary.unread,
112 | .message_row_unread .bbpress_message_summary{
113 | font-weight: bold;
114 | }
115 | .bbpress_compose_account_select img{
116 | width:20px;
117 | height:auto;
118 | vertical-align: middle;
119 | margin:0 5px;
120 | }
--------------------------------------------------------------------------------
/extensions/bbpress/shub_bbpress.js:
--------------------------------------------------------------------------------
1 | ucm.social.bbpress = {
2 | init: function(){
3 |
4 | jQuery('body').delegate('.bbpress_check_all','change',function(){
5 | jQuery('.check_item').prop('checked', !!jQuery(this).prop('checked'));
6 |
7 | });
8 | }
9 | };
--------------------------------------------------------------------------------
/extensions/envato/class.shub_envato_item.php:
--------------------------------------------------------------------------------
1 | get_messages(array(
10 | 'shub_status' => _shub_MESSAGE_STATUS_PENDINGSEND,
11 | ));
12 | $now = time();
13 | foreach($messages as $message){
14 | if(isset($message['message_time']) && $message['message_time'] < $now){
15 | $shub_message = new shub_envato_message(false, $this, $message['shub_message_id']);
16 | $shub_message->send_queued($debug);
17 | }
18 | }*/
19 |
20 | $this->load_latest_item_data($debug);
21 | }
22 |
23 | public function load_latest_item_data($debug = false){
24 | // serialise this result into envato_data.
25 | if(!$this->account){
26 | echo 'No envato account linked, please try again';
27 | return;
28 | }
29 |
30 | $api = $this->account->get_api();
31 |
32 | $network_key = $this->get('network_key');
33 | if(!$network_key){
34 | echo 'No envato item id found';
35 | return;
36 | }
37 |
38 | // we keep a record of the last message received so we know where to stop checking the feed
39 | $last_message_received = (int)$this->get('last_message');
40 | if($debug)echo "Getting the latest 60 comments for item: ".$network_key." (last message in database is from ".shub_print_date($last_message_received,true).")
\n";
41 |
42 | $newest_message_received = 0;
43 |
44 | $endpoint = 'v1/discovery/search/search/comment?term=&item_id='.$network_key.'&sort_by=newest&page_size=60';
45 | $api_result = $api->api($endpoint);
46 | if($debug){
47 | echo "API Result took :".$api_result['took'].' seconds and produced '.count($api_result['matches']).' results';
48 | }
49 |
50 | $count = 0;
51 | if(isset($api_result['matches']) && is_array($api_result['matches'])){
52 | //foreach($api_result['matches'] as $item_message){
53 | while($api_result['matches']){
54 | $item_message = array_pop($api_result['matches']);
55 | if(!$item_message['id'])continue;
56 | $message_time = strtotime($item_message['last_comment_at']);
57 | $newest_message_received = max($newest_message_received,$message_time);
58 | if($message_time <= $last_message_received)continue; // all done here.
59 |
60 | // check if we have this message in our database already.
61 | $envato_message = new shub_envato_message($this->account, $this, false);
62 | $envato_message -> load_by_network_key($item_message['id'], $item_message, 'item_comment', $debug);
63 | $count++;
64 | if($debug) {
65 | ?>
66 |
67 |
Imported message ID: get( 'network_key' ); ?>
68 |
69 | log_data(_SUPPORT_HUB_LOG_INFO, 'envato', 'Imported '.$count.' new messages into database');
74 | if($debug)echo " imported $count new item comments
";
75 |
76 | $this->update('last_message',$newest_message_received);
77 | $this->update('last_checked',time());
78 | }
79 |
80 | public function link_refresh(){
81 | return 'admin.php?page=support_hub_settings&tab=envato&manualrefresh&shub_account_id='.$this->get('shub_account_id').'&network_key='.$this->get('network_key');
82 | }
83 |
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/extensions/envato/default-user.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtbaker/support-hub/21466135d584b1c3c991dc8c09c3ca3d888ec125/extensions/envato/default-user.jpg
--------------------------------------------------------------------------------
/extensions/envato/envato_message.php:
--------------------------------------------------------------------------------
1 | get('shub_account_id') == $shub_account_id){
9 | $envato_message = new shub_envato_message( $envato, false, $shub_message_id );
10 | $envato_message->output_message_page('popup');
11 |
12 | }
13 | }
14 |
15 | if($shub_account_id && !(int)$shub_message_id){
16 | $envato = new shub_envato_account($shub_account_id);
17 | if($shub_account_id && $envato->get('shub_account_id') == $shub_account_id){
18 |
19 | /* @var $groups shub_item[] */
20 | $groups = $envato->get('groups');
21 | //print_r($groups);
22 | ?>
23 |
186 |
187 |
222 |
223 |
227 |
--------------------------------------------------------------------------------
/extensions/envato/init.envato.php:
--------------------------------------------------------------------------------
1 | id = 'envato';
19 | $shub['envato']->friendly_name = 'Envato';
20 | $shub['envato']->desc = 'Import and Reply to Envato item messages.';
21 | return $shub;
22 | }
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/extensions/envato/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtbaker/support-hub/21466135d584b1c3c991dc8c09c3ca3d888ec125/extensions/envato/logo.png
--------------------------------------------------------------------------------
/extensions/envato/shub_envato.css:
--------------------------------------------------------------------------------
1 |
2 | .envato_message_full{
3 | display: none;
4 | background: #edeff4;
5 | border-radius: 5px;
6 | padding: 0 5px;
7 | }
8 | .envato_message_compose textarea{
9 | background: #FFF;
10 | border:1px solid #bdc7d8;
11 | width:88%;
12 | min-height:2em;
13 | resize: none;
14 | }
15 |
16 | .envato_message{
17 | border-bottom: 1px solid #d0d4e4;
18 | padding: 5px 0 0 5px;
19 | }
20 | .envato_message_replies{
21 | margin-left: 40px;
22 | border-left: 1px solid #d0d4e4;
23 | }
24 | .envato_picture{
25 | background: #FFF;
26 | padding: 4px;
27 | display: inline-block;
28 | }
29 | .envato_picture img{
30 | max-width: 100%;
31 | height:auto;
32 | }
33 | .envato_icon{
34 | height:27px;
35 | width: auto;
36 | margin: 5px 5px 0 0;
37 | float: left;
38 | }
39 | .envato_icon.small{
40 | height:15px;
41 | float: none;
42 | vertical-align: middle;
43 | margin:0 5px 0 0;
44 | }
45 |
46 | #envato_message_header{
47 | border:1px solid #d0d4e4;
48 | background: #FFF;
49 | margin:5px;
50 | padding: 8px 5px 14px 10px;
51 | }
52 | #envato_message_holder{
53 | }
54 |
55 | .envato_message_summary.unread,
56 | .message_row_unread .envato_message_summary{
57 | font-weight: bold;
58 | }
59 | .envato_compose_account_select img{
60 | width:20px;
61 | height:auto;
62 | vertical-align: middle;
63 | margin:0 5px;
64 | }
--------------------------------------------------------------------------------
/extensions/envato/shub_envato.js:
--------------------------------------------------------------------------------
1 | ucm.social.envato = {
2 | init: function(){
3 |
4 | jQuery('body').delegate('.envato_check_all','change',function(){
5 | jQuery('.check_item').prop('checked', !!jQuery(this).prop('checked'));
6 |
7 | });
8 |
9 | }
10 | };
--------------------------------------------------------------------------------
/extensions/index.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/extensions/ucm/class.shub_ucm_account.php:
--------------------------------------------------------------------------------
1 | shub_extension = 'ucm';
8 | }
9 |
10 | public function confirm_api(){
11 | // confirm API and do a call to get the ucm user id and save it in the account shub_user_id field so we can display when composing a message.
12 |
13 | $api = $this->get_api();
14 |
15 | $api_result = $api->api('user','get');
16 | if($api_result && !empty($api_result['email'])){
17 | $shub_user_id = $this->get_api_user_to_id($api_result);
18 | if($shub_user_id){
19 | $this->update('shub_user_id',$shub_user_id);
20 | return true;
21 | }
22 | }
23 | echo 'Failed to get User ID from api. Please confirm API details.';
24 | exit;
25 | }
26 |
27 | public function load_available_items(){
28 | // serialise this result into ucm_data.
29 |
30 | $api = $this->get_api();
31 |
32 | $api_result = $api->api('faq','list_products');
33 | /*Array
34 | (
35 | [version] => 1
36 | [user_id] => 1
37 | [faq] => 1
38 | [faq_products] => Array
39 | (
40 | [4] => Array
41 | (
42 | [faq_product_id] => 4
43 | [item_ids] => 48670|144064
44 | [default_type_id] => 3
45 | [name] => asdfasdf333
46 | [date_created] => 2012-12-11 14:22:12
47 | [date_updated] => 2015-03-14 20:19:19
48 | [id] => 4
49 | [default_type] => Array
50 | (
51 | [ticket_type_id] => 3
52 | [name] => CodeCanyon PHP Support
53 | [public] => 1
54 | [create_user_id] => 1
55 | [update_user_id] => 1
56 | [date_updated] => 2012-04-20
57 | [date_created] => 2012
58 | [default_user_id] => 0
59 | )
60 |
61 | [items] => Array
62 | (
63 | [0] => Array
64 | (
65 | [item_id] => 230
66 | [envato_account_id] => 1
67 | [item_id] => 48670
68 | [marketplace] => themeforest
69 | [name] => Blue Business - 3 Pages - HTML & PSD
70 | [url] => http://themeforest.net/item/blue-business-3-pages-html-psd/48670
71 | [launch_date] => 2009-07-07
72 | [cost] => 12.00
73 | [cache] => a:16:{s:2:"id";s:5:"48670";s:4:"item";s:36:"Blue Business - 3 Pages - HTML & PSD";s:3:"url";s:64:"http://themeforest.net/item/blue-business-3-pages-html-psd/48670";s:4:"user";s:7:"dtbaker";s:9:"thumbnail";s:39:"http://3.s3.envato.com/files/140746.jpg";s:5:"sales";s:3:"246";s:6:"rating";s:1:"4";s:4:"cost";s:5:"12.00";s:11:"uploaded_on";s:30:"Tue Jul 07 04:26:11 +1000 2009";s:11:"last_update";s:30:"Tue Jul 07 04:26:11 +1000 2009";s:4:"tags";s:129:"blue, business, clean, clean, clear, corporate, crisp, education, google maps, html, icons, medical, php contact form, psd, white";s:8:"category";s:24:"site-templates/corporate";s:16:"live_preview_url";s:57:"http://3.s3.envato.com/files/142413/1.__large_preview.jpg";s:11:"marketplace";s:11:"themeforest";s:4:"name";s:36:"Blue Business - 3 Pages - HTML & PSD";s:4:"data";a:0:{}}
74 | [date_created] => 2012-10-20 12:53:46
75 | [date_updated] => 2012-11-29 20:06:00
76 | [id] => 230
77 | )*/
78 |
79 | if(is_array($api_result) && isset($api_result['faq_products']) && count($api_result['faq_products'])){
80 | $this->save_account_data(array(
81 | 'items' => $api_result['faq_products'],
82 | ));
83 | }else{
84 | echo 'Failed to find any FAQ products, please create some in UCM first. Please check logs for any errors.';
85 | }
86 |
87 | }
88 |
89 | public function run_cron( $debug = false ){
90 |
91 |
92 | }
93 |
94 | private $api = false;
95 | public function get_api($use_db_code = true){
96 | if(!$this->api){
97 |
98 | require_once trailingslashit(dirname(_DTBAKER_SUPPORT_HUB_CORE_FILE_)) . 'extensions/ucm/class.ucm-api.php';
99 |
100 | $this->api = ucm_api_basic::getInstance();
101 | $this->api->set_api_url($this->get( 'ucm_api_url' ));
102 | $this->api->set_api_key($this->get( 'ucm_api_key' ));
103 |
104 | }
105 | return $this->api;
106 | }
107 | public function get_api_user_to_id($ucm_user_data){
108 | //print_r($ucm_user_data);exit;
109 | $comment_user = new SupportHubUser_ucm();
110 | if(!empty($ucm_user_data['email'])){
111 | $comment_user->load_by( 'user_email', trim(strtolower($ucm_user_data['email'])));
112 | }
113 | if(!$comment_user->get('shub_user_id')){
114 | // didn't find one yet.
115 | // find by envato username?
116 | if(isset($ucm_user_data['envato']['user'])){
117 | $first = current($ucm_user_data['envato']['user']);
118 | if($first && !empty($first['envato_username'])){
119 | if ($comment_user->load_by_meta('envato_username', strtolower($first['envato_username']))) {
120 | // found! yay!
121 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_INFO,'ucm','Found a user based on envato username.',array(
122 | 'username' => $first['envato_username'],
123 | 'found_user_id' => $comment_user->get('shub_user_id'),
124 | ));
125 | }
126 | }
127 | }
128 | }
129 |
130 |
131 | if(isset($ucm_user_data['envato']['purchases']) && is_array($ucm_user_data['envato']['purchases'])){
132 | // find a matching user account with these purchases.
133 | foreach($ucm_user_data['envato']['purchases'] as $purchase){
134 | if(!empty($purchase['license_code'])) {
135 | // pull in the license code using the envato module if it's enabled.
136 | if(isset(SupportHub::getInstance()->message_managers['envato'])) {
137 | $result = SupportHub::getInstance()->message_managers['envato']->pull_purchase_code(false, $purchase['license_code'], array(), $comment_user->get('shub_user_id'));
138 | if ($result && !empty($result['shub_user_id'])) {
139 | $comment_user->load($result['shub_user_id']);
140 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_INFO,'ucm','Found a user based on license code.',array(
141 | 'license_code' => $purchase['license_code'],
142 | 'found_user_id' => $comment_user->get('shub_user_id'),
143 | ));
144 | break;
145 | }
146 | }
147 | }
148 | }
149 | }
150 |
151 | if(!$comment_user->get('shub_user_id')){
152 | // find a match based on email.
153 | if(!empty($ucm_user_data['email'])){
154 | $comment_user->load_by( 'user_email', trim(strtolower($ucm_user_data['email'])));
155 | }
156 | }
157 | if(!$comment_user->get('shub_user_id')){
158 | // no existing matches yet, create a new user with the above meta values so that we can find them again in the future.
159 | $comment_user->create_new();
160 | }
161 | // now we add/update various meta/values of the user if anything is missing.
162 | if(!empty($ucm_user_data['email']) && !$comment_user->get('user_email')) {
163 | $comment_user->update('user_email', trim(strtolower($ucm_user_data['email'])));
164 | }
165 | if(isset($ucm_user_data['envato']['user'])){
166 | $first = current($ucm_user_data['envato']['user']);
167 | if($first && !empty($first['envato_username']) && !$comment_user->get_meta('envato_username',strtolower($first['envato_username']))){
168 | $comment_user->add_meta('envato_username', strtolower($first['envato_username']));
169 | if(!$comment_user->get('user_username')){
170 | $comment_user->update('user_username',strtolower($first['envato_username']));
171 | }
172 | }
173 | }
174 | if(isset($ucm_user_data['envato']['purchases'])) {
175 | foreach ($ucm_user_data['envato']['purchases'] as $purchase) {
176 | if (!empty($purchase['license_code']) && !$comment_user->get_meta('envato_license_code', strtolower($purchase['license_code']))) {
177 | $comment_user->add_meta('envato_license_code', strtolower($purchase['license_code']));
178 | }
179 | }
180 | }
181 | if(!empty($ucm_user_data['name'])){
182 | if(empty($ucm_user_data['last_name'])){
183 | $bits = explode(" ",$ucm_user_data['name']);
184 | $ucm_user_data['name'] = array_shift($bits);
185 | $ucm_user_data['last_name'] = implode(" ",$bits);
186 | }
187 | }
188 | if(!$comment_user->get('user_fname') && !empty($ucm_user_data['name'])){
189 | $comment_user->update('user_fname',$ucm_user_data['name']);
190 | }
191 | if(!$comment_user->get('user_lname') && !empty($ucm_user_data['last_name'])){
192 | $comment_user->update('user_lname',$ucm_user_data['last_name']);
193 | }
194 | $comment_user->update_user_data($ucm_user_data);
195 | return $comment_user->get('shub_user_id');
196 | }
197 |
198 | public function get_picture(){
199 | $data = $this->get('ucm_data');
200 | return $data && isset($data['pictureUrl']) && !empty($data['pictureUrl']) ? $data['pictureUrl'] : false;
201 | }
202 |
203 |
204 | public function get_item($shub_item_id){
205 | return new shub_ucm_item($this, $shub_item_id);
206 | }
207 |
208 | }
209 |
--------------------------------------------------------------------------------
/extensions/ucm/class.shub_ucm_item.php:
--------------------------------------------------------------------------------
1 | get_messages(array(
9 | 'shub_status' => _shub_MESSAGE_STATUS_PENDINGSEND,
10 | ));
11 | $now = time();
12 | foreach($messages as $message){
13 | if(isset($message['message_time']) && $message['message_time'] < $now){
14 | $shub_ucm_message = new shub_ucm_message(false, $this, $message['shub_ucm_message_id']);
15 | $shub_ucm_message->send_queued($debug);
16 | }
17 | }*/
18 |
19 | $this->load_latest_item_data($debug);
20 | }
21 |
22 | public function load_latest_item_data($debug = false){
23 | // serialise this result into ucm_data.
24 | if(!$this->account){
25 | echo 'No ucm account linked, please try again';
26 | return;
27 | }
28 |
29 | $api = $this->account->get_api();
30 |
31 | $ucm_product_id = $this->get('network_key');
32 | if(!$ucm_product_id){
33 | echo 'No ucm product id found';
34 | return;
35 | }
36 |
37 | // we keep a record of the last message received so we know where to stop checking the feed
38 | $last_message_received = (int)$this->get('last_message');
39 |
40 | // dont want to import ALL tickets, so we pick a 20 day limit if we haven't done this yet
41 | if(!$last_message_received){
42 | $last_message_received = strtotime('-20 days');
43 | }
44 | // $last_message_received = false;
45 |
46 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_INFO,'ucm','Loading latest tickets for product ('.$ucm_product_id.') "'.$this->get('product_name').'" modified since '.shub_print_date($last_message_received,true));
47 | // find any messages from this particular UCM product that have been updated since our last scrape time.
48 | $tickets = $api->api('ticket','list',array('search'=>array('faq_product_id'=>$ucm_product_id,'time_from'=>$last_message_received,'status_id'=>0)));
49 | if($debug)echo "Getting the latest tickets for product: ".$ucm_product_id." (last message in database is from ".shub_print_date($last_message_received,true).")
\n";
50 |
51 | $newest_message_received = 0;
52 |
53 | $count = 0;
54 | if(isset($tickets['reply_options'])) {
55 | $this->account->save_account_data(array(
56 | 'reply_options' => $tickets['reply_options']
57 | ));
58 | }
59 | if(isset($tickets['tickets'])) {
60 | foreach ($tickets['tickets'] as $ticket) {
61 | $message_time = $ticket['last_message_timestamp'];
62 | $newest_message_received = max($newest_message_received, $message_time);
63 | //if($message_time <= $last_message_received)break; // all done here.
64 |
65 | $ucm_message = new shub_ucm_message($this->account, $this, false);
66 | $ucm_message->load_by_network_key($ticket['ticket_id'], $ticket, 'ticket', $debug);
67 | $count++;
68 | if ($debug) {
69 | ?>
70 |
71 |
Imported Ticket ID: get('network_key'); ?>
72 | with message.
73 |
74 | log_data(_SUPPORT_HUB_LOG_ERROR,'ucm','Failed to get a reply from the API for product '.$ucm_product_id.' "',$tickets);
80 | }
81 | // get user, return envato_codes in meta
82 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_INFO, 'ucm', 'Imported '.$count.' product tickets into database (from a total of '.count($tickets['tickets']).' returned by the api)');
83 | if($debug)echo " imported $count new product tickets
";
84 |
85 | $this->update('last_message',$newest_message_received);
86 | $this->update('last_checked',time());
87 | }
88 |
89 | public function link_refresh(){
90 | return 'admin.php?page=support_hub_settings&tab=ucm&manualrefresh&shub_account_id='.$this->get('shub_account_id').'&network_key='.$this->get('network_key');
91 | }
92 |
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/extensions/ucm/class.shub_ucm_user.php:
--------------------------------------------------------------------------------
1 | _api_url = $api_url;
18 | }
19 | public function set_api_key($token){
20 | $this->_api_key = $token;
21 | }
22 |
23 | public function api($endpoint, $method=false, $params=array()){
24 | $headers = array(
25 | 'user-agent' => 'SupportHub WP Plugin',
26 | 'timeout' => 20,
27 | );
28 | //$headers['headers'] = array('Authorization' => $this->_api_key,);
29 | $params['auth'] = $this->_api_key;
30 | if($params){
31 | $headers['body'] = $params;
32 | $response = wp_remote_post($this->_api_url . (strpos($this->_api_url,'?') ? '&' : '?') . "endpoint=$endpoint&method=$method", $headers);
33 | }else{
34 | $response = wp_remote_get($this->_api_url . (strpos($this->_api_url,'?') ? '&' : '?') . "endpoint=$endpoint&method=$method", $headers);
35 | }
36 | if( is_array($response) && isset($response['body']) && isset($response['response']['code']) && $response['response']['code'] == 200 ) {
37 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_INFO, 'ucm', 'API Call: '.$endpoint .'/' .$method,$response['body']);
38 | $header = $response['headers'];
39 | $body = @json_decode($response['body'],true);
40 | if(!$body){
41 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_ERROR, 'ucm', 'API Error, unable to JSON decode: '.$endpoint. ' '.(isset($response['response']['code']) ? $response['response']['code'] .' / ': '').(isset($response['body']) ? $response['body'] : ''));
42 | }
43 | return $body;
44 | }else if(is_array($response) && isset($response['response']['code']) && $response['response']['code']){
45 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_ERROR, 'ucm', 'API Error: '.$endpoint. ' '.(isset($response['response']['code']) ? $response['response']['code'] .' / ': '').(isset($response['body']) ? $response['body'] : ''), $response);
46 | }else if(is_wp_error($response)){
47 | SupportHub::getInstance()->log_data(_SUPPORT_HUB_LOG_ERROR, 'ucm', 'API Error: '.$endpoint. ' '.$response->get_error_message(),$response);
48 | }
49 | return false;
50 | }
51 |
52 |
53 |
54 | }
--------------------------------------------------------------------------------
/extensions/ucm/init.ucm.php:
--------------------------------------------------------------------------------
1 | id = 'ucm';
18 | $shub['ucm']->friendly_name = 'UCM';
19 | $shub['ucm']->desc = 'View and Reply to Ultimate Client Manager Email Support Tickets.';
20 | return $shub;
21 | }
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/extensions/ucm/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtbaker/support-hub/21466135d584b1c3c991dc8c09c3ca3d888ec125/extensions/ucm/logo.png
--------------------------------------------------------------------------------
/extensions/ucm/shub_ucm.js:
--------------------------------------------------------------------------------
1 | ucm.social.ucm = {
2 | init: function(){
3 |
4 | jQuery('body').delegate('.ucm_reply_button','click',function(){
5 | var f = jQuery(this).parents('.ucm_message').first().next('.ucm_message_replies').find('.ucm_message_reply_box');
6 | f.show();
7 | f.find('textarea')[0].focus();
8 | }).delegate('.ucm_check_all','change',function(){
9 | jQuery('.check_item').prop('checked', !!jQuery(this).prop('checked'));
10 |
11 | });
12 | }
13 | };
--------------------------------------------------------------------------------
/extensions/ucm/ucm_message.php:
--------------------------------------------------------------------------------
1 | get('shub_account_id') == $shub_account_id){
9 | $ucm_message = new shub_ucm_message( $ucm, false, $shub_message_id );
10 | $ucm_message->output_message_page('popup');
11 |
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/extensions/ucm/ucm_settings.php:
--------------------------------------------------------------------------------
1 | message_managers['ucm'];
5 | if($current_account !== false){
6 | $shub_ucm_account = new shub_ucm_account($current_account);
7 | if($shub_ucm_account->get('shub_extension') != 'ucm')die('Wrong extension:' .$shub_ucm_account->get('shub_extension'));
8 | if(isset($_GET['manualrefresh'])){
9 |
10 | $network_key = isset( $_REQUEST['network_key'] ) ? (int) $_REQUEST['network_key'] : 0;
11 | if(!$network_key){
12 | // update?? products?
13 | $shub_ucm_account->confirm_api();
14 | $shub_ucm_account->load_available_items();
15 | }else {
16 | /* @var $items shub_item[] */
17 | $items = $shub_ucm_account->get('items');
18 | if (!$network_key || !$items || !isset($items[$network_key])) {
19 | die('No items found to refresh');
20 | }
21 | ?>
22 | Manually refreshing item data... please wait...
23 | run_cron(true);
25 | }
26 |
27 | }else if(isset($_GET['do_connect'])){
28 | // connect to ucm. and if that isnt' found
29 | ?>
30 |
31 |
32 |
33 |
34 | get('shub_account_id') && $shub_ucm_account->get('shub_account_id') == $current_account && $shub_ucm_account->get( 'ucm_api_url' ) &&
36 | $shub_ucm_account->get( 'ucm_api_key' )) {
37 |
38 | // now we load in a list of ucm products to manage and redirect the user back to the 'edit' screen where they can continue managing the account.
39 | $shub_ucm_account->confirm_api();
40 | $shub_ucm_account->load_available_items();
41 | $url = $shub_ucm_account->link_edit();
42 | ?>
43 |
You have successfully connected UCM with the Support Hub plugin. Please click the button below:
44 |
Click here to continue.
45 |
46 |
47 |
48 |
49 |
52 | Please go back and make sure all fields are entered.
53 |
58 |
59 |
60 |
61 |
62 |
63 |
209 |
210 | get_accounts();
216 | foreach($accounts as $account_id => $account){
217 | $a = new shub_ucm_account($account['shub_account_id']);
218 | $accounts[$account_id]['edit_link'] = $a->link_edit();
219 | $accounts[$account_id]['title'] = $a->get('account_name');
220 | $accounts[$account_id]['last_checked'] = $a->get('last_checked') ? shub_print_date( $a->get('last_checked') ) : 'N/A';
221 | }
222 | $myListTable->set_data($accounts);
223 | $myListTable->prepare_items();
224 | ?>
225 |
226 |
227 |
228 |
229 |
230 | search_box( 'search', 'search_id' );
232 | $myListTable->display();
233 | ?>
234 |
235 |
239 |
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 | 0){
6 | $post = get_post($_GET['post_id']);
7 | if($post && $post->ID == $_GET['post_id']){
8 | // woo!
9 | $defaults['post_id'] = $post->ID;
10 | $defaults['facebook_type'] = 'link';
11 | $defaults['facebook_link'] = get_permalink($post->ID);
12 | if ( has_post_thumbnail($post->ID)) {
13 | $large_image_url = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'large' );
14 | if($large_image_url[0]){
15 | $defaults['facebook_link_picture'] = $large_image_url[0];
16 | }
17 | }
18 | $defaults['facebook_title'] = get_the_title($post->ID);
19 | $defaults['facebook_description'] = substr( strip_tags(strip_shortcodes($post->post_excerpt ? $post->post_excerpt : $post->post_content)) , 0 , 50 );
20 | $defaults['facebook_message'] = trim(strip_tags(strip_shortcodes($post->post_excerpt ? $post->post_excerpt : $post->post_content)));
21 | $defaults['google_message'] = trim(strip_tags(strip_shortcodes($post->post_excerpt ? $post->post_excerpt : $post->post_content))) . ' ' . get_permalink($post->ID);
22 | $defaults['twitter_message'] = trim(substr( strip_tags(strip_shortcodes($post->post_excerpt ? $post->post_excerpt : $post->post_content)) , 0 , 118 )) . ' ' . get_permalink($post->ID);
23 | $defaults['facebook_caption'] = get_bloginfo('description');
24 | }
25 | }
26 | ?>
27 |
28 |
29 |
30 |
31 |
199 |
200 |
--------------------------------------------------------------------------------
/pages/dashboard.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Interesting graphs and stats are coming here soon. If you have suggestions please let me know. Some ideas:
10 |
11 |
12 |
13 | - Current ticket turn around time
14 | - Pre-sale tickets vs sale tickets
15 | - Time it takes to convert a pre-sale ticket to a purchase
16 | - Percentage of total buyers who submit a support ticket
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/pages/extra-data-view.php:
--------------------------------------------------------------------------------
1 | get('shub_extra_id'));
6 |
7 | if(!$extra->get('shub_extra_id'))die('Failed to load extra');
8 |
9 | ?>
10 |
11 |
--------------------------------------------------------------------------------
/pages/inbox.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | get_current_layout_type();
20 |
21 | // grab a mysql resource from all available social plugins (hardcoded for now - todo: hook)
22 | $search = isset($_REQUEST['search']) && is_array($_REQUEST['search']) ? $_REQUEST['search'] : array();
23 | if(!isset($search['shub_status'])){
24 | $search['shub_status'] = _shub_MESSAGE_STATUS_UNANSWERED;
25 | }
26 | $order = array();
27 | if(!empty($_REQUEST['orderquery'])) {
28 | $bits = explode(':',$_REQUEST['orderquery']);
29 | $order = array(
30 | 'orderby' => $bits[0],
31 | 'order' => $bits[1],
32 | );
33 | }else{
34 | $order = array(
35 | 'orderby' => 'shub_column_time',
36 | 'order' => 'desc',
37 | );
38 | }
39 |
40 | // retuin a combined copy of all available messages, based on search, as a MySQL resource
41 | // so we can loop through them on the global messages combined page.
42 |
43 |
44 |
45 | $myListTable = new SupportHubMessageList(array(
46 | 'screen' => 'shub_inbox'
47 | ));
48 | $myListTable->set_columns( array(
49 | 'cb' => 'Select All',
50 | 'shub_column_account' => __( 'Account', 'support_hub' ),
51 | 'shub_column_product' => __( 'Product', 'support_hub' ),
52 | 'shub_column_time' => __( 'Time', 'support_hub' ),
53 | 'shub_column_from' => __( 'From', 'support_hub' ),
54 | 'shub_column_summary' => __( 'Summary', 'support_hub' ),
55 | 'shub_column_action' => __( 'Action', 'support_hub' ),
56 | ) );
57 | /*$myListTable->set_sortable_columns( array(
58 | 'shub_column_time' => array(
59 | 'shub_column_time',
60 | 1
61 | ),
62 | ) );*/
63 | $myListTable->process_bulk_action(); // before we do the search on messages.
64 |
65 | $this_search = $search;
66 | if (isset($this_search['shub_status']) && $this_search['shub_status'] == -1) {
67 | unset($this_search['shub_status']);
68 | }
69 | SupportHub::getInstance()->load_all_messages($this_search, $order, $layout_type == 'continuous' ? 5 : false);
70 | // we store this data in the session so that continuous mode can access it and perform hte same search when loading more data.
71 |
72 | $all_messages = SupportHub::getInstance()->all_messages;
73 | $has_more = false;
74 | if($layout_type == 'continuous'){
75 |
76 | $message_ids = array();
77 | foreach($all_messages as $all_message){
78 | $message_ids[]=$all_message['shub_message_id'];
79 | }
80 | // this is used in class-support-hub.php to load the next batch of messages.
81 | // todo: pull this out of sessions into ajax variables so we can have two tabs open with different searches going at the same time.
82 | $_SESSION['_shub_search_rules'] = array($this_search, $order, $message_ids);
83 | // todo: ajax notification when currently listed messages are updated or new ones appear at the top of the list.
84 |
85 | }else{
86 | $screen = get_current_screen();
87 | // retrieve the "per_page" option
88 | $screen_option = $screen->get_option('per_page', 'option');
89 | // retrieve the value of the option stored for the current user
90 | $per_page = get_user_meta(get_current_user_id(), $screen_option, true);
91 | if ( empty ( $per_page) || $per_page < 1 || is_array($per_page) ) {
92 | // get the default value if none is set
93 | $per_page = $screen->get_option( 'per_page', 'default' );
94 | }
95 | if(!$per_page)$per_page=20;
96 | $myListTable->items_per_page = $per_page;
97 | $limit_pages = 2; // get about 10 pages of data to display in WordPress.
98 | if(count($all_messages) >= ($myListTable->get_pagenum() * $myListTable->items_per_page) + ($limit_pages * $myListTable->items_per_page)){
99 | $has_more = true; // a flag so we can show "more" in the pagination listing.
100 | }
101 | }
102 |
103 |
104 | // todo - hack in here some sort of cache so pagination works nicer ?
105 | //module_debug::log(array( 'title' => 'Finished social messages', 'data' => '', ));
106 |
107 | $myListTable->set_layout_type($layout_type);
108 | $myListTable->set_data($all_messages);
109 | $myListTable->prepare_items();
110 | $myListTable->pagination_has_more = $has_more;
111 |
112 | // for ajax it would be $myListTable->single_row($message_array);
113 | ?>
114 |
181 |
182 |
183 |
191 |
192 |
193 |
--------------------------------------------------------------------------------
/pages/interactions.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | set_columns( array(
22 | 'cb' => '',
23 | 'shub_column_account' => __( 'Social Account', 'support_hub' ),
24 | 'shub_column_time' => __( 'Date/Time', 'support_hub' ),
25 | 'shub_column_from' => __( 'From', 'support_hub' ),
26 | 'shub_column_summary' => __( 'Summary', 'support_hub' ),
27 | 'shub_column_action' => __( 'Action', 'support_hub' ),
28 | ) );
29 | $myListTable->process_bulk_action(); // before we do the search on messages.
30 |
31 |
32 | /* @var $message_manager shub_facebook */
33 | foreach($this->message_managers as $message_id => $message_manager){
34 | $message_manager->load_all_messages($search, $order);
35 | }
36 |
37 | // filter through each mysql resource so we get the date views. output each row using their individual classes.
38 | $all_messages = array();
39 | $loop_messages = array();
40 | $last_timestamp = false;
41 | while(true){
42 | // fill them up
43 | $has_messages = false;
44 | foreach($this->message_managers as $type => $message_manager){
45 | if(!isset($loop_messages[$type])){
46 | $loop_messages[$type] = $message_manager->get_next_message();
47 | if($loop_messages[$type]){
48 | //echo "Got $type with date of ".print_date($loop_messages[$type]['message_time'],true)."
\n";
49 | $loop_messages[$type]['message_manager'] = $message_manager;
50 | $has_messages = true;
51 | }else{
52 | unset($loop_messages[$type]);
53 | }
54 | }
55 | }
56 | if(!$has_messages && empty($loop_messages)){
57 | break;
58 | }// todo - limit count here.
59 | // pick the lowest one and replenish its spot
60 | $next_type = false;
61 | foreach($loop_messages as $type => $message){
62 | if(!$next_type || $message['message_time'] > $last_timestamp){
63 | $next_type = $type;
64 | $last_timestamp = $message['message_time'];
65 | }
66 | }
67 | //echo "Message $next_type :
\n";
68 | $all_messages[] = $loop_messages[$next_type];
69 | unset($loop_messages[$next_type]);
70 | // repeat.
71 |
72 | }
73 |
74 | // todo - hack in here some sort of cache so pagination works nicer ?
75 | //module_debug::log(array( 'title' => 'Finished social messages', 'data' => '', ));
76 | //print_r($all_messages);
77 |
78 | $myListTable->set_data($all_messages);
79 | $myListTable->prepare_items();
80 | ?>
81 |
99 |
100 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/pages/message.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | message_managers[$network]) && $shub_message_id > 0){
13 | $shub_extension_message = $this->message_managers[$network]->get_message( false, false, $shub_message_id);
14 | if($shub_extension_message->get('shub_message_id') == $shub_message_id){
15 | $shub_account_id = $shub_extension_message->get('account')->get('shub_account_id');
16 | include( trailingslashit( SupportHub::getInstance()->dir ) . 'extensions/'.$network.'/'.$network.'_message.php');
17 | }
18 | }
19 | ?>
20 |
21 |
22 |
23 |
24 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/pages/metabox.php:
--------------------------------------------------------------------------------
1 | Share via Support Hub
--------------------------------------------------------------------------------
/pages/outbox.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | $bits[0],
21 | 'order' => $bits[1],
22 | );
23 | }
24 |
25 | // retuin a combined copy of all available messages, based on search, as a MySQL resource
26 | // so we can loop through them on the global messages combined page.
27 |
28 |
29 |
30 | $myListTable = new SupportHubMessageList();
31 | $screen = get_current_screen();
32 | // retrieve the "per_page" option
33 | $screen_option = $screen->get_option('per_page', 'option');
34 | // retrieve the value of the option stored for the current user
35 | $per_page = get_user_meta(get_current_user_id(), $screen_option, true);
36 | if ( empty ( $per_page) || $per_page < 1 || is_array($per_page) ) {
37 | // get the default value if none is set
38 | $per_page = $screen->get_option( 'per_page', 'default' );
39 | }
40 | if(!$per_page)$per_page=20;
41 | $myListTable->items_per_page = $per_page;
42 | $myListTable->set_columns( array(
43 | 'cb' => 'Select All',
44 | 'shub_column_account' => __( 'Account', 'support_hub' ),
45 | 'shub_column_product' => __( 'Product', 'support_hub' ),
46 | 'shub_column_time' => __( 'Time', 'support_hub' ),
47 | 'shub_column_from' => __( 'From', 'support_hub' ),
48 | 'shub_column_summary' => __( 'Summary', 'support_hub' ),
49 | 'shub_column_action' => __( 'Action', 'support_hub' ),
50 | ) );
51 | /*$myListTable->set_sortable_columns( array(
52 | 'shub_column_time' => array(
53 | 'shub_column_time',
54 | 1
55 | ),
56 | ) );*/
57 | $myListTable->process_bulk_action(); // before we do the search on messages.
58 |
59 |
60 | $this_search = $search;
61 | if (isset($this_search['shub_status']) && $this_search['shub_status'] == -1) {
62 | unset($this_search['shub_status']);
63 | }
64 | // SupportHub::getInstance()->load_all_messages($this_search, $order);
65 | // $all_messages = SupportHub::getInstance()->all_messages;
66 | $all_messages = array_merge(SupportHubOutbox::get_failed(),SupportHubOutbox::get_pending());
67 | $limit_pages = 2; // get about 10 pages of data to display in WordPress.
68 | $has_more = false;
69 | if(count($all_messages) >= ($myListTable->get_pagenum() * $myListTable->items_per_page) + ($limit_pages * $myListTable->items_per_page)){
70 | $has_more = true; // a flag so we can show "more" in the pagination listing.
71 | }
72 |
73 |
74 | // todo - hack in here some sort of cache so pagination works nicer ?
75 | //module_debug::log(array( 'title' => 'Finished social messages', 'data' => '', ));
76 |
77 | $myListTable->set_layout_type($layout_type);
78 | $myListTable->set_data($all_messages);
79 | $myListTable->prepare_items();
80 | $myListTable->pagination_has_more = $has_more;
81 | ?>
82 |
148 |
149 |
150 |
158 |
159 |
160 |
--------------------------------------------------------------------------------
/pages/sent.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | set_columns( array(
10 | 'shub_column_time' => __( 'Date/Time', 'support_hub' ),
11 | 'shub_column_account' => __( 'Social Accounts', 'support_hub' ),
12 | 'shub_column_summary' => __( 'Summary', 'support_hub' ),
13 | 'shub_column_links' => __( 'Link Clicks', 'support_hub' ),
14 | //'shub_column_stats' => __( 'Stats', 'support_hub' ),
15 | //'shub_column_action' => __( 'Action', 'support_hub' ),
16 | 'shub_column_post' => __( 'WP Post', 'support_hub' ),
17 | ) );
18 |
19 | /* @var $message_manager shub_facebook */
20 | /*foreach($this->message_managers as $message_id => $message_manager){
21 | $message_manager->load_all_messages($search, $order);
22 | }*/
23 |
24 | global $wpdb;
25 | $sql = "SELECT * FROM `"._support_hub_DB_PREFIX."shub_message` ORDER BY `shub_message_id` DESC ";
26 | $messages = $wpdb->get_results($sql, ARRAY_A);
27 |
28 |
29 | $myListTable->set_message_managers($this->message_managers);
30 | $myListTable->set_data($messages);
31 | $myListTable->prepare_items();
32 | ?>
33 |
39 |
40 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/pages/settings-logs.php:
--------------------------------------------------------------------------------
1 | set_columns( array(
6 | 'log_time' => __( 'Time', 'support_hub' ),
7 | 'log_extension' => __( 'Extension', 'support_hub' ),
8 | 'log_error_level' => __( 'Error Level', 'support_hub' ),
9 | 'log_subject' => __( 'Subject', 'support_hub' ),
10 | 'log_data' => __( 'Data', 'support_hub' ),
11 | ) );
12 | $latest_logs = shub_get_multiple('shub_log',isset($_REQUEST['search']) && is_array($_REQUEST['search']) ? $_REQUEST['search'] : array(), 'shub_log_id','shub_log_id DESC LIMIT 1000');
13 | $myListTable->set_data($latest_logs);
14 | $myListTable->prepare_items();
15 | ?>
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
60 |
61 |
62 |
63 |
64 |
65 | display();
67 | ?>
68 |
69 | load($current_product);
7 | ?>
8 |
9 |
10 |
11 |
12 |
13 |
47 |
48 | set_columns( array(
53 | 'product_name' => __( 'Product Name', 'support_hub' ),
54 | 'edit_link' => __( 'Action', 'support_hub' ),
55 | ) );
56 | $product_details = SupportHub::getInstance()->get_products();
57 | foreach($product_details as $product_detail_id => $product_detail){
58 | $product_details[$product_detail_id]['edit_link'] = ''.__('Edit','support_hub').'';
59 | }
60 | $myListTable->set_data($product_details);
61 | $myListTable->prepare_items();
62 | ?>
63 |
64 |
65 |
66 |
67 |
68 | search_box( 'search', 'search_id' );
70 | $myListTable->display();
71 | ?>
72 |
73 |
74 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 | message_managers as $message_manager_id => $message_manager) {
16 | if ( $message_manager->is_enabled() ) {
17 | ?>
18 | get_friendly_icon();?>friendly_name;?>
20 |
22 |
23 |
28 |
29 |
30 |
31 | message_managers[$tab])){
33 | $SupportHub->message_managers[$tab]->settings_page();
34 | }else{
35 | if(is_file(dirname(_DTBAKER_SUPPORT_HUB_CORE_FILE_).'/pages/settings-'.basename($tab).'.php')){
36 | include dirname(_DTBAKER_SUPPORT_HUB_CORE_FILE_).'/pages/settings-'.basename($tab).'.php';
37 | }
38 | }
39 | }else{
40 | ?>
41 |
72 |
75 |
76 |
--------------------------------------------------------------------------------
/pages/setup.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Please setup your accounts to get started.
7 |
Check the "Getting Started" guide on supporthub.co for help.
8 |
--------------------------------------------------------------------------------
/readme.txt:
--------------------------------------------------------------------------------
1 | === Support Hub ===
2 | Contributors: dtbaker
3 | Donate link: http://supporthub.co
4 | Tags: support, facebook, twitter, email
5 | Requires at least: 4.2.2
6 | Tested up to: 4.2.2
7 | Stable tag: trunk
8 | License: GPLv2 or later
9 | License URI: http://www.gnu.org/licenses/gpl-2.0.html
10 |
11 | This will import your messages from Facebook, Twitter, bbPress, Envato and more into an easy to use "support inbox".
12 |
13 | == Description ==
14 |
15 | This plugin will import all your Support Questions from Facebook, Twitter, LinkedIn, Google+, POP3/IMAP Email, bbPress Forum Posts, WordPress Comments and Envato Item Comments into an easy to use "support inbox".
16 |
17 | Here you can quickly see and reply to all your messages no matter what medium they come through.
18 |
19 | This is the Support System you need when you already have a Support System. A great way to give Support Staff access to various social networks without giving them access.
20 |
21 | More platforms will be added (e.g. ticksy, helpscout and zendesk are planned) along with some sample code so you can add your own 3rd party support system.
22 |
23 | Details available at http://supporthub.co
24 |
25 |
26 | == Installation ==
27 |
28 | This section describes how to install the plugin and get it working.
29 |
30 | 1. Upload the `supporthub` folder to the `/wp-content/plugins/` directory
31 | 1. Activate the plugin through the 'Plugins' menu in WordPress
32 | 1. Navigate to Support Hub > Settings to start configuration
33 |
34 | == Frequently Asked Questions ==
35 |
36 | = What requirements does this plugin need? =
37 |
38 | This is a rather advanced plugin and it will not work on some cheap shared hosting accounts. The best way to check if it will work is to simply install the plugin and go to Support Hub > Settings, any warnings will be displayed there.
39 |
40 | == Screenshots ==
41 |
42 | 1. This screen shot description corresponds to screenshot-1.(png|jpg|jpeg|gif). Note that the screenshot is taken from
43 | the /assets directory or the directory that contains the stable readme.txt (tags or trunk). Screenshots in the /assets
44 | directory take precedence. For example, `/assets/screenshot-1.png` would win over `/tags/4.3/screenshot-1.png`
45 | (or jpg, jpeg, gif).
46 | 2. This is the second screen shot
47 |
48 |
49 | == Upgrade Notice ==
50 |
51 | = 1.1 =
52 | Major re-write and speed improvements
53 |
54 | = 1.0 =
55 | Initial Release
56 |
57 |
58 |
59 | == Changelog ==
60 |
61 | = 1.1 =
62 | * Major core re-write
63 | * Speed improvements
64 | * Currently re-writing all extensions to be compatible with new framework
65 | * Envato extension working
66 |
67 | = 1.0 =
68 | * Initial release
69 |
--------------------------------------------------------------------------------
/support-hub.php:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |