-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathshorten.js
322 lines (304 loc) · 12.8 KB
/
shorten.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
/* Kish.cm Node.js powered URL Shortener */
var app = false;
let log = {info: console.log, error: console.error};
var Shorten = function(parentapp){
this.app = parentapp;
app = parentapp;
log = app.get('bunyan');
};
/*
* Look up information about a given shortened URL hash
* Accepts: linkHash (6 to 32 alpha-numeric character string)
* callback (callback function to return result to)
*
* Returns: A single Object looking like this
* {'originalURL': 'http://originalurldestination.com', // Link destination
* 'linkHash': 'xxxYYY', // Kish.cm hash ( http://kish.cm/xxxYYY )
* 'timestamp': '2012-02-19T02:48:26.000Z', // Timestamp of when this was created
* 'id': ObjectId("4f6e58edaf5c268231000000") // Database ID
* }
*/
Shorten.prototype.linkHashLookUp = function(linkHash, userInfo, callback){
//console.log("Looking up linkhash: " + linkHash);
var mongoose = app.get('mongoose');
var dbCursor = mongoose.model('LinkMaps');
dbCursor.find({linkHash}, function(err, results){
if (err === null){
if (typeof (callback) === "function"){
if (results.length === 0){
callback(false);
return false;
}
//Log the data back if we have it
if (results[0].linkStats.length > 0){
results[0].linkStats.push(userInfo);
}else{
results[0].linkStats = new Array(userInfo);
}
results[0].save();
return callback(results[0]);
}
}else{
log.info("Some DB error:");
log.error(err);
callback(false);
return false;
}
});
};
/*
* Adds a shortened URL to the database
* Accepts: originalURL a verifed-as-URL string
* callback (callback function to return result to)
* Returns: Result of DB insertion (unused)
*/
Shorten.prototype.addNewShortenLink = function(originalURL, callback){
var that = this; //Grab this context for after we make a DB query
var mongoose = app.get('mongoose');
that.genHash(function(newHash){
var dbCursor = mongoose.model('LinkMaps');
dbCursor = new dbCursor({linkDestination: originalURL, linkHash: newHash, timestamp: new Date()});
dbCursor.save(function(err){
if (err === null){
if (typeof (callback) === "function"){
//Get and return our newly created short URL
that.originalURLLookUp(originalURL, callback);
return true;
}
}else{
log.error(err);
return false;
}
});
});
};
/*
* Checks if a URL already exists in the shortened database. If so, return the hash
* Accepts: originalURL a verifed-as-URL string
* callback (callback function to return result to)
*
* Returns: Boolean `False` if URL isn't found in database
* OR
* A single Object looking like this
* {'originalURL': 'http://originalurldestination.com', // Link destination
* 'linkHash': 'xxxYYY', // A lookup hash ( http://kish.cm/xxxYYY )
* 'timestamp': '2012-02-19T02:48:26.000Z', // Timestamp of when this was created
* 'id': ObjectId("4f6e58edaf5c268231000000") // Database ID
* }
*/
Shorten.prototype.originalURLLookUp = function (originalURL, callback){
var mongoose = app.get('mongoose');
var dbCursor = mongoose.model('LinkMaps');
dbCursor.find({"linkDestination": originalURL}, function(err, results){
if (err === null){
if (typeof (callback) === "function"){
if (results[0]){
callback(results[0]);
return results[0];
}
return callback(false);
}
}else{
log.error(err);
}
});
};
/*
* Gets all stats about a given shortened URL
* Accepts: A URL shortened with this site URL (http://kish.cm/xxxYYY) or just a shortened hash (xxxYYY)
* callback (callback function to return result to)
* Returns: MongoDB result set of logged shortened URL redirections
*/
Shorten.prototype.shortenedURLStats = function(shortenedURL, callback) {
var that = this; //We need this context deep in callback hell. I feel like I'm doing something wrong...
var alreadyShortTest = new RegExp("^http://" + this.app.settings.domain + "/[a-zA-Z0-9]{6,32}$");
if (alreadyShortTest.test(shortenedURL)){
var shortenedURLStats = {
'originalURL': null,
'linkHash': shortenedURL,
'timesUsed': null,
'lastUse': null,
'dateShortened': null,
'topReferrals': [],
'topUserAgents': [],
'error' : 'Not a ' + this.app.settings.domain + ' shortened URL'
};
return callback(shortenedURLStats);
}
// Strip domain and slashes
shortenedURL = shortenedURL.replace("http://" + this.app.settings.domain + "/", "");
//console.log("Looking up stats for: " + shortenedURL);
var mongoose = app.get('mongoose');
var dbCursor = mongoose.model('LinkMaps');
// We need the link_id, look up the shortened URL first. This is an artifact of being ported over from an ancient PHP app. Will be revised when moving databases
dbCursor.find({"linkHash": shortenedURL}, function(err, results){
if (err){
log.error(err);
}
if (results[0]){
shortenedURL = results[0];
if (shortenedURL.linkStats.length > 0){
that.convertResultsToStats(shortenedURL.linkStats, shortenedURL, callback);
return shortenedURL.linkStats;
}
var shortenURLStats = {
'originalURL': shortenedURL.linkDestination,
'linkHash': shortenedURL.linkHash,
'timesUsed': 0,
'lastUse': null,
'dateShortened': null,
'topReferrals': [],
'topUserAgents': [],
'error' : 'Shortened URL hasn\'t been used yet'
};
return callback(shortenURLStats);
}
return callback({
'originalURL': null,
'linkHash': shortenedURL,
'timesUsed': null,
'lastUse': null,
'dateShortened': null,
'topReferrals': [],
'topUserAgents': [],
'error' : 'No shortened URL found with this hash'
});
});
};
/*
* Converts MongoDB Result set into expected JSON object format for pass to frontend stats requests
* Accepts: MongoDB Result Array
* Returns: JSON object with stats about the URL. Looks like this
* {'originalURL': 'http://google.com/ig', //Shortened URL destination
* 'linkHash': 'xxxYYY', //Shortened URL hash (http://kish.cm/xxxYYY)
* 'timesUsed': 420, //Number of times shortened URL was redirected
* 'lastUse': '1329244179', //Last time shortened URL was used/redirected (UNIX timestamp)
* 'dateShortened': '2012-02-19T02:53:42.000Z', //Date the URL was shortened
* 'topReferrals': {'http://twitter.com/kishcom': 5, //Object containing top 5 referals for this shortened URL
* 'http://facebook.com/kishcom': 4,
* 'http://google.com/s?=kishcom': 2,
* 'http://duckduckgo.com/s?=kishcom': 1},
* 'topUserAgents':{'Chrome 17': 5, //Object containing top 5 user agents for this shortened URL
* 'Chrome 15': 4,
* 'IE8': 3,
* 'Android': 2,
* 'iPhone': 1},
* 'error': 'Error string or boolean false'
* }
*/
Shorten.prototype.convertResultsToStats = function(resultSet, shortenedURL, callback){
// Setup the object to fill in and return
var shortenedURLStats = {
'originalURL': shortenedURL.linkDestination,
'linkHash': shortenedURL.linkHash,
'timesUsed': resultSet.length,
'lastUse': resultSet[0].timestamp,
'dateShortened': shortenedURL.timestamp,
'topReferrals': [],
'topUserAgents': [],
'error' : false
};
// Get the top 10 useragents and referrers
var knownAgents = [], knownReferrals = [];
for (var i = 0; resultSet.length > i; i++){
// Check known agents for this result
var found = false;
for (var a = 0; knownAgents.length > a; a++){
if (knownAgents[a].userAgent === resultSet[i].userAgent){
knownAgents[a].agentCount++;
found = true;
}
}
if (!found){
knownAgents.push({'userAgent': resultSet[i].userAgent, 'agentCount': 1});
}
found = false;
// Check known referrers for this result
for (var b = 0; knownReferrals.length > b; b++){
if (knownReferrals[b].referrer === resultSet[i].referrer){
knownReferrals[b].referrerCount++;
found = true;
}
}
if (!found){
knownReferrals.push({'referrer': resultSet[i].referrer, 'referrerCount': 1});
}
}
function compareReferrer(aa, bb) {
if (aa.referrerCount > bb.referrerCount) { return -1; }
if (aa.referrerCount < bb.referrerCount) { return 1; }
return 0;
}
function compareAgent(aa, bb) {
if (aa.agentCount > bb.agentCount) { return -1; }
if (aa.agentCount < bb.agentCount) { return 1; }
return 0;
}
shortenedURLStats.topReferrals = knownReferrals.sort(compareReferrer).slice(0, 9);
shortenedURLStats.topUserAgents = knownAgents.sort(compareAgent).slice(0, 9);
callback(shortenedURLStats);
return shortenedURLStats;
};
/*
* Generates a new 6 character alpha-num link hash
* Accepts: Nothing
* Returns: A 6 character alpha-numeric string that can be used as a link hash
*/
Shorten.prototype.genHash = function(callback){
// Simple random number generator helper
var randomRange = function(min, max){
return Math.floor(Math.random() * (max - min + 1)) + min;
};
var newHash = "";
// 6 digits, alphanumeric
for (var i = 0; i < 6; i++){
var digit = (randomRange(0, 1) === 0) ? String(randomRange(0, 9)) : String.fromCharCode(randomRange(97, 122));
newHash += digit;
}
if (this.app){ // Tests don't need to check the database
var mongoose = app.get('mongoose');
var dbCursor = mongoose.model('LinkMaps');
dbCursor.find({"linkHash": newHash}, function(err, results){
if (err !== null){
console.log(err);
}else{
// Call this function recursively until we find a hash that doesn't exist yet
if (results.length > 0){
// Statistically, this will likely never be called ever
this.genHash();
}
return callback(newHash);
}
});
}else{
callback(newHash);
return newHash;
}
};
/*
* Tests to ensure the a given linkHash is ONLY alphanumeric and between 6-32 characters
* Accepts: A string to test
* Returns: Boolean `True` if linkHash is ONLY alphanumeric and between 6-32 characters
*/
Shorten.prototype.isValidLinkHash = function(linkHash){
var linkHashRegex = /^[a-z0-9]{6,32}$/i;
return linkHashRegex.test(linkHash);
};
/*
* Tests if this is a URL Kish.cm will shorten or not
* Accepts: A URL String ("http://www.kishcom.com")
* Returns: Boolean `true` if the URL can be shortened (is valid and not already shortened)
*
* TODO: DRY and write this function in one spot for both frontend and backend, you'll notice this function is also in public/media/js/shortener.js
*/
Shorten.prototype.isURL = function(testURL) {
//Make sure the URL isn't already a URL we've shortened
var alreadyKishcmTest = new RegExp("^http://" + this.domain + "/[a-zA-Z0-9]+$");
if (alreadyKishcmTest.test(testURL) === false){
//Make sure URL mostly looks like a URL should
var mainURLTest = /^(https?):\/\/((?:[a-z0-9.-]|%[0-9A-F]{2}){3,})(?::(\d+))?((?:\/(?:[a-z0-9-._~!$&'()*+,;=:@]|%[0-9A-F]{2})*)*)(?:\?((?:[a-z0-9-._~!$&'()*+,;=:\/?@]|%[0-9A-F]{2})*))?(?:#((?:[a-z0-9-._~!$&'()*+,;=:\/?@]|%[0-9A-F]{2})*))?$/i;
return mainURLTest.test(testURL);
}
return false;
};
module.exports = Shorten;