5a920362 |
1 | /** |
2 | * This code is mostly from the old Etherpad. Please help us to comment this code. |
3 | * This helps other people to understand this code better and helps them to improve it. |
4 | * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED |
5 | */ |
6 | |
7 | /** |
8 | * Copyright 2009 Google Inc. |
9 | * |
10 | * Licensed under the Apache License, Version 2.0 (the "License"); |
11 | * you may not use this file except in compliance with the License. |
12 | * You may obtain a copy of the License at |
13 | * |
14 | * http://www.apache.org/licenses/LICENSE-2.0 |
15 | * |
16 | * Unless required by applicable law or agreed to in writing, software |
17 | * distributed under the License is distributed on an "AS-IS" BASIS, |
18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
19 | * See the License for the specific language governing permissions and |
20 | * limitations under the License. |
21 | */ |
22 | |
23 | var Security = require('/security'); |
24 | |
25 | /** |
26 | * Generates a random String with the given length. Is needed to generate the Author, Group, readonly, session Ids |
27 | */ |
28 | |
29 | function randomString(len) |
30 | { |
31 | var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; |
32 | var randomstring = ''; |
33 | len = len || 20 |
34 | for (var i = 0; i < len; i++) |
35 | { |
36 | var rnum = Math.floor(Math.random() * chars.length); |
37 | randomstring += chars.substring(rnum, rnum + 1); |
38 | } |
39 | return randomstring; |
40 | } |
41 | |
42 | function createCookie(name, value, days, path) |
43 | { |
44 | if (days) |
45 | { |
46 | var date = new Date(); |
47 | date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); |
48 | var expires = "; expires=" + date.toGMTString(); |
49 | } |
50 | else var expires = ""; |
51 | |
52 | if(!path) |
53 | path = "/"; |
54 | |
55 | document.cookie = name + "=" + value + expires + "; path=" + path; |
56 | } |
57 | |
58 | function readCookie(name) |
59 | { |
60 | var nameEQ = name + "="; |
61 | var ca = document.cookie.split(';'); |
62 | for (var i = 0; i < ca.length; i++) |
63 | { |
64 | var c = ca[i]; |
65 | while (c.charAt(0) == ' ') c = c.substring(1, c.length); |
66 | if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); |
67 | } |
68 | return null; |
69 | } |
70 | |
71 | var padutils = { |
72 | escapeHtml: function(x) |
73 | { |
74 | return Security.escapeHTML(String(x)); |
75 | }, |
76 | uniqueId: function() |
77 | { |
78 | var pad = require('/pad').pad; // Sidestep circular dependency |
79 | function encodeNum(n, width) |
80 | { |
81 | // returns string that is exactly 'width' chars, padding with zeros |
82 | // and taking rightmost digits |
83 | return (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width); |
84 | } |
85 | return [pad.getClientIp(), encodeNum(+new Date, 7), encodeNum(Math.floor(Math.random() * 1e9), 4)].join('.'); |
86 | }, |
87 | uaDisplay: function(ua) |
88 | { |
89 | var m; |
90 | |
91 | function clean(a) |
92 | { |
93 | var maxlen = 16; |
94 | a = a.replace(/[^a-zA-Z0-9\.]/g, ''); |
95 | if (a.length > maxlen) |
96 | { |
97 | a = a.substr(0, maxlen); |
98 | } |
99 | return a; |
100 | } |
101 | |
102 | function checkver(name) |
103 | { |
104 | var m = ua.match(RegExp(name + '\\/([\\d\\.]+)')); |
105 | if (m && m.length > 1) |
106 | { |
107 | return clean(name + m[1]); |
108 | } |
109 | return null; |
110 | } |
111 | |
112 | // firefox |
113 | if (checkver('Firefox')) |
114 | { |
115 | return checkver('Firefox'); |
116 | } |
117 | |
118 | // misc browsers, including IE |
119 | m = ua.match(/compatible; ([^;]+);/); |
120 | if (m && m.length > 1) |
121 | { |
122 | return clean(m[1]); |
123 | } |
124 | |
125 | // iphone |
126 | if (ua.match(/\(iPhone;/)) |
127 | { |
128 | return 'iPhone'; |
129 | } |
130 | |
131 | // chrome |
132 | if (checkver('Chrome')) |
133 | { |
134 | return checkver('Chrome'); |
135 | } |
136 | |
137 | // safari |
138 | m = ua.match(/Safari\/[\d\.]+/); |
139 | if (m) |
140 | { |
141 | var v = '?'; |
142 | m = ua.match(/Version\/([\d\.]+)/); |
143 | if (m && m.length > 1) |
144 | { |
145 | v = m[1]; |
146 | } |
147 | return clean('Safari' + v); |
148 | } |
149 | |
150 | // everything else |
151 | var x = ua.split(' ')[0]; |
152 | return clean(x); |
153 | }, |
154 | // e.g. "Thu Jun 18 2009 13:09" |
155 | simpleDateTime: function(date) |
156 | { |
157 | var d = new Date(+date); // accept either number or date |
158 | var dayOfWeek = (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])[d.getDay()]; |
159 | var month = (['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])[d.getMonth()]; |
160 | var dayOfMonth = d.getDate(); |
161 | var year = d.getFullYear(); |
162 | var hourmin = d.getHours() + ":" + ("0" + d.getMinutes()).slice(-2); |
163 | return dayOfWeek + ' ' + month + ' ' + dayOfMonth + ' ' + year + ' ' + hourmin; |
164 | }, |
165 | findURLs: function(text) |
166 | { |
167 | // copied from ACE |
168 | var _REGEX_WORDCHAR = /[\u0030-\u0039\u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u1FFF\u3040-\u9FFF\uF900-\uFDFF\uFE70-\uFEFE\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFDC]/; |
169 | var _REGEX_URLCHAR = new RegExp('(' + /[-:@a-zA-Z0-9_.,~%+\/?=&#;()$]/.source + '|' + _REGEX_WORDCHAR.source + ')'); |
170 | var _REGEX_URL = new RegExp(/(?:(?:https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man|gopher|txmt):\/\/|mailto:)/.source + _REGEX_URLCHAR.source + '*(?![:.,;])' + _REGEX_URLCHAR.source, 'g'); |
171 | |
172 | // returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...] |
173 | |
174 | |
175 | function _findURLs(text) |
176 | { |
177 | _REGEX_URL.lastIndex = 0; |
178 | var urls = null; |
179 | var execResult; |
180 | while ((execResult = _REGEX_URL.exec(text))) |
181 | { |
182 | urls = (urls || []); |
183 | var startIndex = execResult.index; |
184 | var url = execResult[0]; |
185 | urls.push([startIndex, url]); |
186 | } |
187 | |
188 | return urls; |
189 | } |
190 | |
191 | return _findURLs(text); |
192 | }, |
193 | escapeHtmlWithClickableLinks: function(text, target) |
194 | { |
195 | var idx = 0; |
196 | var pieces = []; |
197 | var urls = padutils.findURLs(text); |
198 | |
199 | function advanceTo(i) |
200 | { |
201 | if (i > idx) |
202 | { |
203 | pieces.push(Security.escapeHTML(text.substring(idx, i))); |
204 | idx = i; |
205 | } |
206 | } |
207 | if (urls) |
208 | { |
209 | for (var j = 0; j < urls.length; j++) |
210 | { |
211 | var startIndex = urls[j][0]; |
212 | var href = urls[j][1]; |
213 | advanceTo(startIndex); |
214 | pieces.push('<a ', (target ? 'target="' + Security.escapeHTMLAttribute(target) + '" ' : ''), 'href="', Security.escapeHTMLAttribute(href), '">'); |
215 | advanceTo(startIndex + href.length); |
216 | pieces.push('</a>'); |
217 | } |
218 | } |
219 | advanceTo(text.length); |
220 | return pieces.join(''); |
221 | }, |
222 | bindEnterAndEscape: function(node, onEnter, onEscape) |
223 | { |
224 | |
225 | // Use keypress instead of keyup in bindEnterAndEscape |
226 | // Keyup event is fired on enter in IME (Input Method Editor), But |
227 | // keypress is not. So, I changed to use keypress instead of keyup. |
228 | // It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox 3.6.10, Chrome 6.0.472, Safari 5.0). |
229 | if (onEnter) |
230 | { |
231 | node.keypress(function(evt) |
232 | { |
233 | if (evt.which == 13) |
234 | { |
235 | onEnter(evt); |
236 | } |
237 | }); |
238 | } |
239 | |
240 | if (onEscape) |
241 | { |
242 | node.keydown(function(evt) |
243 | { |
244 | if (evt.which == 27) |
245 | { |
246 | onEscape(evt); |
247 | } |
248 | }); |
249 | } |
250 | }, |
251 | timediff: function(d) |
252 | { |
253 | var pad = require('/pad').pad; // Sidestep circular dependency |
254 | function format(n, word) |
255 | { |
256 | n = Math.round(n); |
257 | return ('' + n + ' ' + word + (n != 1 ? 's' : '') + ' ago'); |
258 | } |
259 | d = Math.max(0, (+(new Date) - (+d) - pad.clientTimeOffset) / 1000); |
260 | if (d < 60) |
261 | { |
262 | return format(d, 'second'); |
263 | } |
264 | d /= 60; |
265 | if (d < 60) |
266 | { |
267 | return format(d, 'minute'); |
268 | } |
269 | d /= 60; |
270 | if (d < 24) |
271 | { |
272 | return format(d, 'hour'); |
273 | } |
274 | d /= 24; |
275 | return format(d, 'day'); |
276 | }, |
277 | makeAnimationScheduler: function(funcToAnimateOneStep, stepTime, stepsAtOnce) |
278 | { |
279 | if (stepsAtOnce === undefined) |
280 | { |
281 | stepsAtOnce = 1; |
282 | } |
283 | |
284 | var animationTimer = null; |
285 | |
286 | function scheduleAnimation() |
287 | { |
288 | if (!animationTimer) |
289 | { |
290 | animationTimer = window.setTimeout(function() |
291 | { |
292 | animationTimer = null; |
293 | var n = stepsAtOnce; |
294 | var moreToDo = true; |
295 | while (moreToDo && n > 0) |
296 | { |
297 | moreToDo = funcToAnimateOneStep(); |
298 | n--; |
299 | } |
300 | if (moreToDo) |
301 | { |
302 | // more to do |
303 | scheduleAnimation(); |
304 | } |
305 | }, stepTime * stepsAtOnce); |
306 | } |
307 | } |
308 | return { |
309 | scheduleAnimation: scheduleAnimation |
310 | }; |
311 | }, |
312 | makeShowHideAnimator: function(funcToArriveAtState, initiallyShown, fps, totalMs) |
313 | { |
314 | var animationState = (initiallyShown ? 0 : -2); // -2 hidden, -1 to 0 fade in, 0 to 1 fade out |
315 | var animationFrameDelay = 1000 / fps; |
316 | var animationStep = animationFrameDelay / totalMs; |
317 | |
318 | var scheduleAnimation = padutils.makeAnimationScheduler(animateOneStep, animationFrameDelay).scheduleAnimation; |
319 | |
320 | function doShow() |
321 | { |
322 | animationState = -1; |
323 | funcToArriveAtState(animationState); |
324 | scheduleAnimation(); |
325 | } |
326 | |
327 | function doQuickShow() |
328 | { // start showing without losing any fade-in progress |
329 | if (animationState < -1) |
330 | { |
331 | animationState = -1; |
332 | } |
333 | else if (animationState <= 0) |
334 | { |
335 | animationState = animationState; |
336 | } |
337 | else |
338 | { |
339 | animationState = Math.max(-1, Math.min(0, -animationState)); |
340 | } |
341 | funcToArriveAtState(animationState); |
342 | scheduleAnimation(); |
343 | } |
344 | |
345 | function doHide() |
346 | { |
347 | if (animationState >= -1 && animationState <= 0) |
348 | { |
349 | animationState = 1e-6; |
350 | scheduleAnimation(); |
351 | } |
352 | } |
353 | |
354 | function animateOneStep() |
355 | { |
356 | if (animationState < -1 || animationState == 0) |
357 | { |
358 | return false; |
359 | } |
360 | else if (animationState < 0) |
361 | { |
362 | // animate show |
363 | animationState += animationStep; |
364 | if (animationState >= 0) |
365 | { |
366 | animationState = 0; |
367 | funcToArriveAtState(animationState); |
368 | return false; |
369 | } |
370 | else |
371 | { |
372 | funcToArriveAtState(animationState); |
373 | return true; |
374 | } |
375 | } |
376 | else if (animationState > 0) |
377 | { |
378 | // animate hide |
379 | animationState += animationStep; |
380 | if (animationState >= 1) |
381 | { |
382 | animationState = 1; |
383 | funcToArriveAtState(animationState); |
384 | animationState = -2; |
385 | return false; |
386 | } |
387 | else |
388 | { |
389 | funcToArriveAtState(animationState); |
390 | return true; |
391 | } |
392 | } |
393 | } |
394 | |
395 | return { |
396 | show: doShow, |
397 | hide: doHide, |
398 | quickShow: doQuickShow |
399 | }; |
400 | }, |
401 | _nextActionId: 1, |
402 | uncanceledActions: {}, |
403 | getCancellableAction: function(actionType, actionFunc) |
404 | { |
405 | var o = padutils.uncanceledActions[actionType]; |
406 | if (!o) |
407 | { |
408 | o = {}; |
409 | padutils.uncanceledActions[actionType] = o; |
410 | } |
411 | var actionId = (padutils._nextActionId++); |
412 | o[actionId] = true; |
413 | return function() |
414 | { |
415 | var p = padutils.uncanceledActions[actionType]; |
416 | if (p && p[actionId]) |
417 | { |
418 | actionFunc(); |
419 | } |
420 | }; |
421 | }, |
422 | cancelActions: function(actionType) |
423 | { |
424 | var o = padutils.uncanceledActions[actionType]; |
425 | if (o) |
426 | { |
427 | // clear it |
428 | delete padutils.uncanceledActions[actionType]; |
429 | } |
430 | }, |
431 | makeFieldLabeledWhenEmpty: function(field, labelText) |
432 | { |
433 | field = $(field); |
434 | |
435 | function clear() |
436 | { |
437 | field.addClass('editempty'); |
438 | field.val(labelText); |
439 | } |
440 | field.focus(function() |
441 | { |
442 | if (field.hasClass('editempty')) |
443 | { |
444 | field.val(''); |
445 | } |
446 | field.removeClass('editempty'); |
447 | }); |
448 | field.blur(function() |
449 | { |
450 | if (!field.val()) |
451 | { |
452 | clear(); |
453 | } |
454 | }); |
455 | return { |
456 | clear: clear |
457 | }; |
458 | }, |
459 | getCheckbox: function(node) |
460 | { |
461 | return $(node).is(':checked'); |
462 | }, |
463 | setCheckbox: function(node, value) |
464 | { |
465 | if (value) |
466 | { |
467 | $(node).attr('checked', 'checked'); |
468 | } |
469 | else |
470 | { |
471 | $(node).removeAttr('checked'); |
472 | } |
473 | }, |
474 | bindCheckboxChange: function(node, func) |
475 | { |
476 | $(node).bind("click change", func); |
477 | }, |
478 | encodeUserId: function(userId) |
479 | { |
480 | return userId.replace(/[^a-y0-9]/g, function(c) |
481 | { |
482 | if (c == ".") return "-"; |
483 | return 'z' + c.charCodeAt(0) + 'z'; |
484 | }); |
485 | }, |
486 | decodeUserId: function(encodedUserId) |
487 | { |
488 | return encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, function(cc) |
489 | { |
490 | if (cc == '-') return '.'; |
491 | else if (cc.charAt(0) == 'z') |
492 | { |
493 | return String.fromCharCode(Number(cc.slice(1, -1))); |
494 | } |
495 | else |
496 | { |
497 | return cc; |
498 | } |
499 | }); |
500 | } |
501 | }; |
502 | |
503 | var globalExceptionHandler = undefined; |
504 | function setupGlobalExceptionHandler() { |
505 | if (!globalExceptionHandler) { |
506 | globalExceptionHandler = function test (msg, url, linenumber) |
507 | { |
508 | var errorId = randomString(20); |
509 | if ($("#editorloadingbox").attr("display") != "none"){ |
510 | //show javascript errors to the user |
511 | $("#editorloadingbox").css("padding", "10px"); |
512 | $("#editorloadingbox").css("padding-top", "45px"); |
513 | $("#editorloadingbox").html("<div style='text-align:left;color:red;font-size:16px;'><b>An error occured</b><br>The error was reported with the following id: '" + errorId + "'<br><br><span style='color:black;font-weight:bold;font-size:16px'>Please send this error message to us: </span><div style='color:black;font-size:14px'>'" |
514 | + "ErrorId: " + errorId + "<br>UserAgent: " + navigator.userAgent + "<br>" + msg + " in " + url + " at line " + linenumber + "'</div></div>"); |
515 | } |
516 | |
517 | //send javascript errors to the server |
518 | var errObj = {errorInfo: JSON.stringify({errorId: errorId, msg: msg, url: url, linenumber: linenumber, userAgent: navigator.userAgent})}; |
519 | var loc = document.location; |
520 | var url = loc.protocol + "//" + loc.hostname + ":" + loc.port + "/" + loc.pathname.substr(1, loc.pathname.indexOf("/p/")) + "jserror"; |
521 | |
522 | $.post(url, errObj); |
523 | |
524 | return false; |
525 | }; |
526 | window.onerror = globalExceptionHandler; |
527 | } |
528 | } |
529 | |
530 | padutils.setupGlobalExceptionHandler = setupGlobalExceptionHandler; |
531 | |
532 | padutils.binarySearch = require('/ace2_common').binarySearch; |
533 | |
534 | exports.randomString = randomString; |
535 | exports.createCookie = createCookie; |
536 | exports.readCookie = readCookie; |
537 | exports.padutils = padutils; |