]> git.p6c8.net - jirafeau_project.git/blob - lib/functions.js.php
[TASK] Fix php.ini for docker
[jirafeau_project.git] / lib / functions.js.php
1 <?php
2 /*
3 * Jirafeau, your web file repository
4 * Copyright (C) 2015 Jerome Jutteau <j.jutteau@gmail.com>
5 * Copyright (C) 2015 Nicola Spanti (RyDroid) <dev@nicola-spanti.info>
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21 header('Content-Type: text/javascript');
22 define('JIRAFEAU_ROOT', dirname(__FILE__) . '/../');
23
24 require(JIRAFEAU_ROOT . 'lib/settings.php');
25 require(JIRAFEAU_ROOT . 'lib/functions.php');
26 require(JIRAFEAU_ROOT . 'lib/lang.php');
27 ?>
28 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
29 var web_root = "<?php echo $cfg['web_root']; ?>";
30
31 var lang_array = <?php echo json_lang_generator(null); ?>;
32 var lang_array_fallback = <?php echo json_lang_generator("en"); ?>;
33
34 function translate (expr) {
35 if (lang_array.hasOwnProperty(expr)) {
36 var e = lang_array[expr];
37 if (!isEmpty(e))
38 return e;
39 }
40 if (lang_array_fallback.hasOwnProperty(expr)) {
41 var e = lang_array_fallback[expr];
42 if (!isEmpty(e))
43 return e;
44 }
45 return "FIXME: " + expr;
46 }
47
48 function isEmpty(str) {
49 return (!str || 0 === str.length);
50 }
51
52 // Extend date object with format method
53 Date.prototype.format = function(format) {
54 format = format || 'YYYY-MM-DD hh:mm';
55
56 var zeropad = function(number, length) {
57 number = number.toString();
58 length = length || 2;
59 while(number.length < length)
60 number = '0' + number;
61 return number;
62 },
63 formats = {
64 YYYY: this.getFullYear(),
65 MM: zeropad(this.getMonth() + 1),
66 DD: zeropad(this.getDate()),
67 hh: zeropad(this.getHours()),
68 mm: zeropad(this.getMinutes()),
69 O: (function() {
70 localDate = new Date;
71 sign = (localDate.getTimezoneOffset() > 0) ? '-' : '+';
72 offset = Math.abs(localDate.getTimezoneOffset());
73 hours = zeropad(Math.floor(offset / 60));
74 minutes = zeropad(offset % 60);
75 return sign + hours + ":" + minutes;
76 })()
77 },
78 pattern = '(' + Object.keys(formats).join(')|(') + ')';
79
80 return format.replace(new RegExp(pattern, 'g'), function(match) {
81 return formats[match];
82 });
83 };
84
85 function dateFromUtcString(datestring) {
86 // matches »YYYY-MM-DD hh:mm«
87 var m = datestring.match(/(\d+)-(\d+)-(\d+)\s+(\d+):(\d+)/);
88 return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], 0));
89 }
90
91 function dateFromUtcTimestamp(datetimestamp) {
92 return new Date(parseInt(datetimestamp) * 1000)
93 }
94
95 function dateToUtcString(datelocal) {
96 return new Date(
97 datelocal.getUTCFullYear(),
98 datelocal.getUTCMonth(),
99 datelocal.getUTCDate(),
100 datelocal.getUTCHours(),
101 datelocal.getUTCMinutes(),
102 datelocal.getUTCSeconds()
103 ).format();
104 }
105
106 function dateToUtcTimestamp(datelocal) {
107 return (Date.UTC(
108 datelocal.getUTCFullYear(),
109 datelocal.getUTCMonth(),
110 datelocal.getUTCDate(),
111 datelocal.getUTCHours(),
112 datelocal.getUTCMinutes(),
113 datelocal.getUTCSeconds()
114 ) / 1000);
115 }
116
117 function convertAllDatetimeFields() {
118 datefields = document.getElementsByClassName('datetime')
119 for(var i=0; i<datefields.length; i++) {
120 dateUTC = datefields[i].getAttribute('data-datetime');
121 datefields[i].setAttribute('title', dateUTC + ' (GMT)');
122 datefields[i].innerHTML = dateFromUtcString(dateUTC).format('YYYY-MM-DD hh:mm (GMT O)');
123 }
124 }
125
126 function show_link (reference, delete_code, crypt_key, date)
127 {
128 // Upload finished
129 document.getElementById('uploading').style.display = 'none';
130 document.getElementById('upload').style.display = 'none';
131 document.getElementById('upload_finished').style.display = '';
132 document.title = "100% - <?php echo empty($cfg['title']) ? 'Jirafeau' : $cfg['title']; ?>";
133
134 // Download page
135 var download_link_href = 'f.php?h=' + reference;
136 if (crypt_key.length > 0)
137 {
138 download_link_href += '&k=' + crypt_key;
139 }
140 if (!!document.getElementById('upload_finished_download_page'))
141 {
142 document.getElementById('upload_link').href = download_link_href;
143 document.getElementById('upload_link_text').innerHTML = web_root + download_link_href;
144 }
145
146 // Email link
147 var filename = document.getElementById('file_select').files[0].name;
148 var b = encodeURIComponent("<?php echo t("DL"); ?> \"" + filename + "\":") + "%0D" + "%0A";
149 b += encodeURIComponent(web_root + download_link_href) + "%0D" + "%0A";
150 if (false == isEmpty(date))
151 {
152 b += "%0D" + "%0A" + encodeURIComponent("<?php echo t("VALID_UNTIL"); ?>: " + date.format('YYYY-MM-DD hh:mm (GMT O)')) + "%0D" + "%0A";
153 document.getElementById('upload_link_email').href = "mailto:?body=" + b + "&subject=" + encodeURIComponent(filename);
154 }
155
156 // Delete link
157 var delete_link_href = 'f.php?h=' + reference + '&d=' + delete_code;
158 document.getElementById('delete_link').href = delete_link_href;
159 document.getElementById('delete_link_text').innerHTML = web_root + delete_link_href;
160
161 // Validity date
162 if (isEmpty(date))
163 {
164 document.getElementById('date').style.display = 'none';
165 }
166 else {
167 document.getElementById('date').innerHTML = '<span class="datetime" title="'
168 + dateToUtcString(date) + ' (GMT)">'
169 + date.format('YYYY-MM-DD hh:mm (GMT O)')
170 + '</span>';
171 document.getElementById('date').style.display = '';
172 }
173
174 // Preview link (if allowed)
175 if (!!document.getElementById('preview_link'))
176 {
177 document.getElementById('upload_finished_preview').style.display = 'none';
178 var preview_link_href = 'f.php?h=' + reference + '&p=1';
179 if (crypt_key.length > 0)
180 {
181 preview_link_href += '&k=' + crypt_key;
182 }
183
184 // Test if content can be previewed
185 type = document.getElementById('file_select').files[0].type;
186 if (type.indexOf("image") > -1 ||
187 type.indexOf("audio") > -1 ||
188 type.indexOf("text") > -1 ||
189 type.indexOf("video") > -1)
190 {
191 document.getElementById('preview_link').href = preview_link_href;
192 document.getElementById('preview_link_text').innerHTML = web_root + preview_link_href;
193 document.getElementById('upload_finished_preview').style.display = '';
194 }
195 }
196
197 // Direct download link
198 var direct_download_link_href = 'f.php?h=' + reference + '&d=1';
199 if (crypt_key.length > 0)
200 {
201 direct_download_link_href += '&k=' + crypt_key;
202 }
203 document.getElementById('direct_link').href = direct_download_link_href;
204 document.getElementById('direct_link_text').innerHTML = web_root + direct_download_link_href;
205
206 // Hide preview and direct download link if password is set
207 if (document.getElementById('input_key').value.length > 0)
208 {
209 if (!!document.getElementById('preview_link'))
210 document.getElementById('upload_finished_preview').style.display = 'none';
211 document.getElementById('upload_direct_download').style.display = 'none';
212 }
213 }
214
215 function show_upload_progression (percentage, speed, time_left)
216 {
217 document.getElementById('uploaded_percentage').innerHTML = percentage;
218 document.getElementById('uploaded_speed').innerHTML = speed;
219 document.getElementById('uploaded_time').innerHTML = time_left;
220 document.title = percentage + " - <?php echo empty($cfg['title']) ? 'Jirafeau' : $cfg['title']; ?>";
221 }
222
223 function hide_upload_progression ()
224 {
225 document.getElementById('uploaded_percentage').style.display = 'none';
226 document.getElementById('uploaded_speed').style.display = 'none';
227 document.getElementById('uploaded_time').style.display = 'none';
228 document.title = "<?php echo empty($cfg['title']) ? 'Jirafeau' : $cfg['title']; ?>";
229 }
230
231 function upload_progress (e)
232 {
233 if (e == undefined || e == null || !e.lengthComputable)
234 return;
235
236 // Init time estimation if needed
237 if (upload_time_estimation_total_size == 0)
238 upload_time_estimation_total_size = e.total;
239
240 // Compute percentage
241 var p = Math.round (e.loaded * 100 / e.total);
242 var p_str = ' ';
243 if (p != 100)
244 p_str = p.toString() + '%';
245 // Update estimation speed
246 upload_time_estimation_add(e.loaded);
247 // Get speed string
248 var speed_str = upload_time_estimation_speed_string();
249 speed_str = upload_speed_refresh_limiter(speed_str);
250 // Get time string
251 var time_str = chrono_update(upload_time_estimation_time());
252
253 show_upload_progression (p_str, speed_str, time_str);
254 }
255
256 function control_selected_file_size(max_size, error_str)
257 {
258 f_size = document.getElementById('file_select').files[0].size;
259 if (max_size > 0 && f_size > max_size * 1024 * 1024)
260 {
261 pop_failure(error_str);
262 document.getElementById('send').style.display = 'none';
263 }
264 else
265 {
266 // add class to restyle upload form in next step
267 document.getElementById('upload').setAttribute('class', 'file-selected');
268 // display options
269 document.getElementById('options').style.display = 'block';
270 document.getElementById('send').style.display = 'block';
271 document.getElementById('error_pop').style.display = 'none';
272 document.getElementById('send').focus();
273 }
274 }
275
276 function pop_failure (e)
277 {
278 var text = "An error occured";
279 if (typeof e !== 'undefined')
280 text = e;
281 text = "<p>" + text + "</p>";
282 document.getElementById('error_pop').innerHTML = e;
283
284 document.getElementById('uploading').style.display = 'none';
285 document.getElementById('error_pop').style.display = '';
286 document.getElementById('upload').style.display = '';
287 document.getElementById('send').style.display = '';
288 }
289
290 function add_time_string_to_date(d, time)
291 {
292 if(typeof(d) != 'object' || !(d instanceof Date))
293 {
294 return false;
295 }
296
297 if (time == 'minute')
298 {
299 d.setSeconds (d.getSeconds() + 60);
300 return true;
301 }
302 if (time == 'hour')
303 {
304 d.setSeconds (d.getSeconds() + 3600);
305 return true;
306 }
307 if (time == 'day')
308 {
309 d.setSeconds (d.getSeconds() + 86400);
310 return true;
311 }
312 if (time == 'week')
313 {
314 d.setSeconds (d.getSeconds() + 604800);
315 return true;
316 }
317 if (time == 'month')
318 {
319 d.setSeconds (d.getSeconds() + 2592000);
320 return true;
321 }
322 if (time == 'quarter')
323 {
324 d.setSeconds (d.getSeconds() + 7776000);
325 return true;
326 }
327 if (time == 'year')
328 {
329 d.setSeconds (d.getSeconds() + 31536000);
330 return true;
331 }
332 return false;
333 }
334
335 function classic_upload (file, time, password, one_time, upload_password)
336 {
337 // Delay time estimation init as we can't have file size
338 upload_time_estimation_init(0);
339
340 var req = new XMLHttpRequest ();
341 req.upload.addEventListener ("progress", upload_progress, false);
342 req.addEventListener ("error", pop_failure, false);
343 req.addEventListener ("abort", pop_failure, false);
344 req.onreadystatechange = function ()
345 {
346 if (req.readyState == 4 && req.status == 200)
347 {
348 var res = req.responseText;
349
350 // if response starts with "Error" then show a failure
351 if (/^Error/.test(res))
352 {
353 pop_failure (res);
354 return;
355 }
356
357 res = res.split ("\n");
358 var expiryDate = '';
359 if (time != 'none')
360 {
361 // convert time (local time + selected expiry date)
362 var localDatetime = new Date();
363 if(!add_time_string_to_date(localDatetime, time))
364 {
365 pop_failure ('Error: Date can not be parsed');
366 return;
367 }
368 expiryDate = localDatetime;
369 }
370
371 show_link (res[0], res[1], res[2], expiryDate);
372 }
373 }
374 req.open ("POST", 'script.php' , true);
375
376 var form = new FormData();
377 form.append ("file", file);
378 if (time)
379 form.append ("time", time);
380 if (password)
381 form.append ("key", password);
382 if (one_time)
383 form.append ("one_time_download", '1');
384 if (upload_password.length > 0)
385 form.append ("upload_password", upload_password);
386
387 req.send (form);
388 }
389
390 function check_html5_file_api ()
391 {
392 return window.File && window.FileReader && window.FileList && window.Blob;
393 }
394
395 var async_global_transfered = 0;
396 var async_global_file;
397 var async_global_ref = '';
398 var async_global_max_size = 0;
399 var async_global_time;
400 var async_global_transfering = 0;
401
402 function async_upload_start (max_size, file, time, password, one_time, upload_password)
403 {
404 async_global_transfered = 0;
405 async_global_file = file;
406 async_global_max_size = max_size;
407 async_global_time = time;
408
409 var req = new XMLHttpRequest ();
410 req.addEventListener ("error", pop_failure, false);
411 req.addEventListener ("abort", pop_failure, false);
412 req.onreadystatechange = function ()
413 {
414 if (req.readyState == 4 && req.status == 200)
415 {
416 var res = req.responseText;
417
418 if (/^Error/.test(res))
419 {
420 pop_failure (res);
421 return;
422 }
423
424 res = res.split ("\n");
425 async_global_ref = res[0];
426 var code = res[1];
427 async_upload_push (code);
428 }
429 }
430 req.open ("POST", 'script.php?init_async' , true);
431
432 var form = new FormData();
433 form.append ("filename", async_global_file.name);
434 form.append ("type", async_global_file.type);
435 if (time)
436 form.append ("time", time);
437 if (password)
438 form.append ("key", password);
439 if (one_time)
440 form.append ("one_time_download", '1');
441 if (upload_password.length > 0)
442 form.append ("upload_password", upload_password);
443
444 // Start time estimation
445 upload_time_estimation_init(async_global_file.size);
446
447 req.send (form);
448 }
449
450 function async_upload_progress (e)
451 {
452 if (e == undefined || e == null || !e.lengthComputable && async_global_file.size != 0)
453 return;
454
455 // Compute percentage
456 var p = Math.round ((e.loaded + async_global_transfered) * 100 / (async_global_file.size));
457 var p_str = ' ';
458 if (p != 100)
459 p_str = p.toString() + '%';
460 // Update estimation speed
461 upload_time_estimation_add(e.loaded + async_global_transfered);
462 // Get speed string
463 var speed_str = upload_time_estimation_speed_string();
464 speed_str = upload_speed_refresh_limiter(speed_str);
465 // Get time string
466 var time_str = chrono_update(upload_time_estimation_time());
467
468 show_upload_progression (p_str, speed_str, time_str);
469 }
470
471 function async_upload_push (code)
472 {
473 if (async_global_transfered == async_global_file.size)
474 {
475 hide_upload_progression ();
476 async_upload_end (code);
477 return;
478 }
479 var req = new XMLHttpRequest ();
480 req.upload.addEventListener ("progress", async_upload_progress, false);
481 req.addEventListener ("error", pop_failure, false);
482 req.addEventListener ("abort", pop_failure, false);
483 req.onreadystatechange = function ()
484 {
485 if (req.readyState == 4 && req.status == 200)
486 {
487 var res = req.responseText;
488
489 if (/^Error/.test(res))
490 {
491 pop_failure (res);
492 return;
493 }
494
495 res = res.split ("\n");
496 var code = res[0]
497 async_global_transfered = async_global_transfering;
498 async_upload_push (code);
499 }
500 }
501 req.open ("POST", 'script.php?push_async' , true);
502
503 var chunk_size = parseInt (async_global_max_size * 0.50);
504 var start = async_global_transfered;
505 var end = start + chunk_size;
506 if (end >= async_global_file.size)
507 end = async_global_file.size;
508 var blob = async_global_file.slice (start, end);
509 async_global_transfering = end;
510
511 var form = new FormData();
512 form.append ("ref", async_global_ref);
513 form.append ("data", blob);
514 form.append ("code", code);
515 req.send (form);
516 }
517
518 function async_upload_end (code)
519 {
520 var req = new XMLHttpRequest ();
521 req.addEventListener ("error", pop_failure, false);
522 req.addEventListener ("abort", pop_failure, false);
523 req.onreadystatechange = function ()
524 {
525 if (req.readyState == 4 && req.status == 200)
526 {
527 var res = req.responseText;
528
529 if (/^Error/.test(res))
530 {
531 pop_failure (res);
532 return;
533 }
534
535 res = res.split ("\n");
536 var expiryDate = '';
537 if (async_global_time != 'none')
538 {
539 // convert time (local time + selected expiry date)
540 var localDatetime = new Date();
541 if(!add_time_string_to_date(localDatetime, async_global_time)) {
542 pop_failure ('Error: Date can not be parsed');
543 return;
544 }
545 expiryDate = localDatetime;
546 }
547
548 show_link (res[0], res[1], res[2], expiryDate);
549 }
550 }
551 req.open ("POST", 'script.php?end_async' , true);
552
553 var form = new FormData();
554 form.append ("ref", async_global_ref);
555 form.append ("code", code);
556 req.send (form);
557 }
558
559 function upload (max_size)
560 {
561 if (check_html5_file_api ()
562 && document.getElementById('file_select').files[0].size >= max_size)
563 {
564 async_upload_start (
565 max_size,
566 document.getElementById('file_select').files[0],
567 document.getElementById('select_time').value,
568 document.getElementById('input_key').value,
569 document.getElementById('one_time_download').checked,
570 document.getElementById('upload_password').value
571 );
572 }
573 else
574 {
575 classic_upload (
576 document.getElementById('file_select').files[0],
577 document.getElementById('select_time').value,
578 document.getElementById('input_key').value,
579 document.getElementById('one_time_download').checked,
580 document.getElementById('upload_password').value
581 );
582 }
583 }
584
585 var upload_time_estimation_total_size = 42;
586 var upload_time_estimation_transfered_size = 42;
587 var upload_time_estimation_transfered_date = 42;
588 var upload_time_estimation_moving_average_speed = 42;
589
590 function upload_time_estimation_init(total_size)
591 {
592 upload_time_estimation_total_size = total_size;
593 upload_time_estimation_transfered_size = 0;
594 upload_time_estimation_moving_average_speed = 0;
595 var d = new Date();
596 upload_time_estimation_transfered_date = d.getTime();
597 }
598
599 function upload_time_estimation_add(total_transfered_size)
600 {
601 // Let's compute the current speed
602 var d = new Date();
603 var speed = upload_time_estimation_moving_average_speed;
604 if (d.getTime() - upload_time_estimation_transfered_date != 0)
605 speed = (total_transfered_size - upload_time_estimation_transfered_size)
606 / (d.getTime() - upload_time_estimation_transfered_date);
607 // Let's compute moving average speed on 30 values
608 var m = (upload_time_estimation_moving_average_speed * 29 + speed) / 30;
609 // Update global values
610 upload_time_estimation_transfered_size = total_transfered_size;
611 upload_time_estimation_transfered_date = d.getTime();
612 upload_time_estimation_moving_average_speed = m;
613 }
614
615 function upload_time_estimation_speed_string()
616 {
617 // speed ms -> s
618 var s = upload_time_estimation_moving_average_speed * 1000;
619 var res = 0;
620 var scale = '';
621 if (s <= 1000)
622 {
623 res = s.toString();
624 scale = "Bit/s";
625 }
626 else if (s < 1000000)
627 {
628 res = Math.floor(s/100) / 10;
629 scale = "KBit/s";
630 }
631 else
632 {
633 res = Math.floor(s/100000) / 10;
634 scale = "Mbit/s";
635 }
636 if (res == 0)
637 return '';
638 return res.toString() + ' ' + scale;
639 }
640
641 function milliseconds_to_time_string (milliseconds)
642 {
643 function numberEnding (number) {
644 return (number > 1) ? translate ('PLURAL_ENDING') : '';
645 }
646
647 var temp = Math.floor(milliseconds / 1000);
648 var years = Math.floor(temp / 31536000);
649 if (years) {
650 return years + ' ' + translate ('YEAR') + numberEnding(years);
651 }
652 var days = Math.floor((temp %= 31536000) / 86400);
653 if (days) {
654 return days + ' ' + translate ('DAY') + numberEnding(days);
655 }
656 var hours = Math.floor((temp %= 86400) / 3600);
657 if (hours) {
658 return hours + ' ' + translate ('HOUR') + numberEnding(hours);
659 }
660 var minutes = Math.floor((temp %= 3600) / 60);
661 if (minutes) {
662 return minutes + ' ' + translate ('MINUTE') + numberEnding(minutes);
663 }
664 var seconds = temp % 60;
665 if (seconds) {
666 return seconds + ' ' + translate ('SECOND') + numberEnding(seconds);
667 }
668 return translate ('LESS_1_SEC');
669 }
670
671 function upload_time_estimation_time()
672 {
673 // Estimate remaining time
674 if (upload_time_estimation_moving_average_speed == 0)
675 return 0;
676 return (upload_time_estimation_total_size - upload_time_estimation_transfered_size)
677 / upload_time_estimation_moving_average_speed;
678 }
679
680 var chrono_last_update = 0;
681 var chrono_time_ms = 0;
682 var chrono_time_ms_last_update = 0;
683 function chrono_update(time_ms)
684 {
685 var d = new Date();
686 var chrono = 0;
687 // Don't update too often
688 if (d.getTime() - chrono_last_update < 3000 &&
689 chrono_time_ms_last_update > 0)
690 chrono = chrono_time_ms;
691 else
692 {
693 chrono_last_update = d.getTime();
694 chrono_time_ms = time_ms;
695 chrono = time_ms;
696 chrono_time_ms_last_update = d.getTime();
697 }
698
699 // Adjust chrono for smooth estimation
700 chrono = chrono - (d.getTime() - chrono_time_ms_last_update);
701
702 // Let's update chronometer
703 var time_str = '';
704 if (chrono > 0)
705 time_str = milliseconds_to_time_string (chrono);
706 return time_str;
707 }
708
709 var upload_speed_refresh_limiter_last_update = 0;
710 var upload_speed_refresh_limiter_last_value = '';
711 function upload_speed_refresh_limiter(speed_str)
712 {
713 var d = new Date();
714 if (d.getTime() - upload_speed_refresh_limiter_last_update > 1500)
715 {
716 upload_speed_refresh_limiter_last_value = speed_str;
717 upload_speed_refresh_limiter_last_update = d.getTime();
718 }
719 return upload_speed_refresh_limiter_last_value;
720 }
721
722 // document.ready()
723 document.addEventListener('DOMContentLoaded', function(event) {
724 // Search for all datetime fields and convert the time to local timezone
725 convertAllDatetimeFields();
726 });
727
728 // Add copy event listeners
729 function copyLinkToClipboard(link_id) {
730 var focus = document.activeElement;
731 var e = document.getElementById(link_id);
732
733 var tmp = document.createElement("textarea");
734 document.body.appendChild(tmp);
735 tmp.textContent = e.href;
736 tmp.focus();
737 tmp.setSelectionRange(0, tmp.value.length);
738 document.execCommand("copy");
739 document.body.removeChild(tmp);
740
741 focus.focus();
742 }
743
744 function addCopyListener(button_id, link_id) {
745 if(document.getElementById(button_id)){
746 document.getElementById(button_id)
747 .addEventListener("click", function() {
748 copyLinkToClipboard(link_id);});
749 }
750 }
751 // @license-end

patrick-canterino.de