File Coverage

blib/lib/Mojolicious/Plugin/TagHelpers/MailToChiffre.pm
Criterion Covered Total %
statement 161 172 93.6
branch 37 48 77.0
condition 6 12 50.0
subroutine 16 16 100.0
pod 1 3 33.3
total 221 251 88.0


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::TagHelpers::MailToChiffre;
2 2     2   2062 use Mojo::Base 'Mojolicious::Plugin';
  2         4  
  2         14  
3 2     2   507 use Mojo::ByteStream 'b';
  2         15  
  2         147  
4 2     2   15 use Mojo::Collection 'c';
  2         5  
  2         95  
5 2     2   11 use Mojo::URL;
  2         5  
  2         16  
6              
7             our $VERSION = '0.11';
8              
9             # Cache for generated CSS and JavaScript
10             has [qw/js css pattern_rotate/];
11              
12             # Register Plugin
13             sub register {
14 4     4 1 16141 my ($plugin, $app, $plugin_param) = @_;
15              
16             # Load random string plugin with specific profile
17 4         35 $app->plugin('Util::RandomString' => {
18             mail_to_chiffre => {
19             alphabet => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
20             entropy => 128
21             }
22             });
23              
24 4         8470 delete $plugin->{js};
25 4         11 delete $plugin->{css};
26              
27             # Load parameters from Config file
28 4 50       22 if (my $config_param = $app->config('TagHelpers-MailToChiffre')) {
29 0         0 $plugin_param = { %$config_param, %$plugin_param };
30             };
31              
32             # Generate method name in case it is not given
33 4   66     85 my $method_name = $plugin_param->{method_name} // $app->random_string('mail_to_chiffre');
34              
35             # Set pattern shift in case it is not given
36 4         1577 my $pattern_rotate = 2;
37 4 50 33     49 if ($plugin_param->{pattern_rotate} && $plugin_param->{pattern_rotate} =~ /^\d+$/) {
38 4         11 $pattern_rotate = $plugin_param->{pattern_rotate};
39             };
40 4         19 $plugin->pattern_rotate($pattern_rotate);
41              
42             # Add pseudo condition for manipulating the stash for the fallback
43 4         42 my $routes = $app->routes;
44              
45             # Add fallback shortcut
46             $routes->add_shortcut(
47             mail_to_chiffre => sub {
48 2     2   2636 my $r = shift;
49              
50 2         7 state $name = 'mailToChiffre';
51              
52             # In case method name is given, set asset paths
53 2 100       11 if ($plugin_param->{method_name}) {
54              
55             # Styles
56             $r->get('/style.css')->to(
57             cb => sub {
58 0         0 my $c = shift;
59 0         0 $c->render(
60             text => $c->mail_to_chiffre_css,
61             format => 'css'
62             );
63             }
64 1         5 )->name($name . 'CSS');
65              
66             # Styles
67             $r->get('/script.js')->to(
68             cb => sub {
69 0         0 my $c = shift;
70 0         0 $c->render(
71             text => $c->mail_to_chiffre_js,
72             format => 'js'
73             );
74             }
75 1         313 )->name($name . 'JS');
76             };
77              
78             # Fallback path
79             $r->under('/:xor/:host')->to(
80             cb => sub {
81 12         166083 $plugin->_chiffre_to_mail(shift)
82             }
83 2         322 )->get('/')->name($name)->to(@_);
84             }
85 4         62 );
86              
87              
88             # Add obfuscation tag helper
89             $app->helper(
90             mail_to_chiffre => sub {
91 16     16   78657 my $c = shift;
92              
93 16 100       90 my $address = shift or return b('');
94              
95             # Create one time pad
96 15         103 my $xor = substr($c->random_string('mail_to_chiffre'), 0, length($address));
97              
98             # Get embedded code
99 15         1792 my $text;
100 15 50 66     87 if (ref($_[-1]) && ref($_[-1]) eq 'CODE') {
101 0         0 $text = pop;
102             };
103              
104 15         65 my %param = @_;
105              
106             # Split the address and do some encodings
107 15         92 my $obf_address = b($address)->xml_escape->split('@');
108 15         1351 my $account = $obf_address->first;
109              
110 15         182 my $host = join '@', @{$obf_address}[1 .. $obf_address->size - 1];
  15         175  
111              
112             # Reget the pattern rotate (maybe)
113 15         136 my $pattern_rotate = $plugin->pattern_rotate;
114              
115             # Obfuscate address parts
116 15         141 $host = $plugin->to_sequence(
117             $host,
118             $xor,
119             $pattern_rotate
120             );
121              
122 15         83 $account = $plugin->to_sequence(
123             $account,
124             $xor,
125             $pattern_rotate
126             );
127              
128             # Create Mojo::URL for path
129 15         42 my ($url, $no_fallback);
130 15 100       115 if ($routes->lookup('mailToChiffre')) {
131 14         417 $url = $c->url_for('mailToChiffre', xor => $xor, host => $host);
132             }
133             else {
134 1         49 $url = $c->url_for("/$xor/$host");
135 1         530 $no_fallback = 1;
136             };
137              
138             # Encrypt certain mail parameters
139 15         9601 foreach (qw/to cc bcc/) {
140              
141             # No parameter
142 45 100       139 next unless exists $param{$_};
143              
144             # Parameter invalid
145 10 100       50 unless ($param{$_}) {
146 1         3 delete $param{$_};
147 1         4 next;
148             };
149              
150             # Array for this parameter
151 9 100       36 if (ref $param{$_}) {
152 3         8 my @temp;
153 3         7 foreach (@{$param{$_}}) {
  3         11  
154 4 50       19 push(@temp, $plugin->to_sequence($_, $xor, $pattern_rotate)) if $_;
155             };
156              
157             # Check if there are converted parameters
158 3 100       13 if (@temp) {
159 2         9 $param{$_} = \@temp;
160             }
161             # Remove parameter from list
162             else {
163 1         5 delete $param{$_};
164             };
165             }
166              
167             # Single value
168             else {
169             $param{$_} = $plugin->to_sequence(
170 6         26 $param{$_},
171             $xor,
172             $pattern_rotate
173             );
174             };
175             };
176              
177             # Return path
178 15         138 $url->query({sid => $account, %param});
179              
180 15 100       2304 if ($no_fallback) {
181 1         6 $url = qq!javascript:$method_name(false,'$url')!;
182             };
183              
184             # Create anchor link
185 15         855 my $str = qq! 186 15 100       7009 $str .= 'return true;' if $no_fallback;
187 15         62 $str .= 'return ' . $method_name . '(this,false)';
188              
189             # Obfuscate display string using css
190 15 50       50 unless ($text) {
191 15         94 my ($pre, @post) = split('@', reverse($address));
192 15         80 $str .= '">' .
193             '' . b($pre)->xml_escape . '' .
194             '' . b($xor)->split('')->reverse->join . '' .
195             c(@post)->join->xml_escape;
196             }
197             else {
198 0         0 $str .= ';' . int(rand(50)) . '">' . $text->();
199             };
200              
201 15         4416 $str .= '';
202              
203 15         54 return b($str);
204             }
205 4         301 );
206              
207             # Create css code helper
208             $app->helper(
209             mail_to_chiffre_css => sub {
210 5 100   5   3441 return $plugin->css if $plugin->css;
211 3         25 my $css = qq!a[onclick\$='return $method_name(this,false)']!;
212 3         17 $css = $css . '{direction:rtl;unicode-bidi:bidi-override;text-align:left}'.
213             $css . '>span:nth-child(1n+2){display:none}' .
214             $css . '>span:nth-child(1):after{content:\'@\'}';
215 3         16 $plugin->css(b($css));
216 3         64 return $plugin->css;
217             }
218 4         327 );
219              
220              
221             # Create javascript code helper
222             $app->helper(
223             mail_to_chiffre_js => sub {
224 3     3   2075 my $c = shift;
225              
226 3 50       14 return $plugin->js if $plugin->js;
227              
228             # Replacement variables
229 3         50 my $v = c(qw/o s u c p n t r g f a x e d q b l m k/)->shuffle;
230              
231             # Template variables
232 3         127 my ($i, %v) = (0);
233 3         13 foreach (qw/obj seq url char pos num str regex string_obj
234             from_char_code param_array temp to_seq
235             path_array query padded str_len pow bool/) {
236 57         138 $v{$_} = $v->[$i++];
237             };
238              
239             # Obfuscate pattern rotate
240 3         11 my $factor_pattern_rotate = _factorize($plugin->pattern_rotate, $v{pow});
241              
242             # Create javascript code
243 3         123 my $js = qq!function ${method_name}($v{obj},$v{bool}){
244             if($v{bool}){
245             $v{obj}=document.createElement('a');$v{obj}.href=$v{bool}
246             }
247             var $v{query}=$v{obj}.search,$v{regex}=RegExp,$v{from_char_code}=String.fromCharCode,$v{url}='il',$v{param_array}=[],$v{temp},$v{pow}=Math.pow;
248             $v{path_array}=$v{obj}.pathname.match(/([^\\/]+)\\/([^\\/]+)\$/);
249             $v{to_seq}=function($v{seq}){
250             var $v{pos}=0,$v{num},$v{str}='',$v{char};
251             while($v{pos}<$v{seq}.length){
252             $v{char}=$v{seq}.charAt($v{pos}++);
253             if($v{char}.match(/[A-Za-z]/)){
254             $v{str}+=$v{from_char_code}(($v{char}<='Z'?90:122)>=($v{char}=$v{char}.charCodeAt(0)+13)?$v{char}:$v{char}-26)
255             }
256             else if($v{char}=='-'){
257             $v{num}='';
258             $v{char}=$v{seq}.charAt($v{pos}++);
259             while($v{char}.match(/\\d/)){
260             $v{num}+=$v{char};
261             $v{char}=$v{seq}.charAt($v{pos}++)
262             }
263             $v{pos}--;
264             $v{str}+=$v{from_char_code}(parseInt($v{num}))
265             }
266             else return
267             }
268             $v{str_len}=$v{str}.length;
269             $v{padded}=Math.abs(${factor_pattern_rotate}%$v{str_len}-$v{str_len});
270             $v{str}=$v{str}.substr($v{padded})+$v{str}.substr(0,$v{padded});
271             $v{temp}='';
272             for(i=0;i<$v{str_len};i++){
273             $v{temp}+=$v{from_char_code}($v{str}.charCodeAt(i)^$v{path_array}\[1\].charCodeAt($v{path_array}\[1\].length%(i+1)))
274             }
275             return $v{temp}
276             };
277             while($v{query}){
278             $v{query}=$v{query}.replace(/^[\\?\\&]([^\\&]+)/,'');
279             $v{temp}=$v{regex}.\$1;
280             if($v{temp}.match(/^(sid|b?cc|to)=(.+)\$/)){
281             if($v{regex}.\$1=='sid')
282             $v{param_array}.push('to='+$v{to_seq}($v{regex}.\$2)+'\@'+$v{to_seq}($v{path_array}\[2\]));
283             else $v{param_array}.push($v{regex}.\$1+'='+$v{to_seq}($v{regex}.\$2));
284             }else $v{param_array}.push($v{temp}.replace(/\\+/g,' '))
285             }
286             location.href='ma'+$v{url}+'to:?'+$v{param_array}.join('&');
287             return false
288             }!;
289 3         134 $js =~ s/\s*\n\s*//g;
290 3         17 $plugin->js(b($js));
291 3         65 return $plugin->js;
292             }
293 4         324 );
294             };
295              
296              
297             sub _chiffre_to_mail {
298 12     12   45 my ($plugin, $c) = @_;
299 12         50 my $xor = $c->stash('xor');
300 12         170 my $p = $c->req->url->query;
301              
302             # Set header for searc engines
303 12         319 $c->res->headers->header('X-Robots-Tag' => 'noindex,nofollow');
304              
305             # Deobfuscate host
306 12         781 my $host = $plugin->to_string(
307             $c->stash('host'),
308             $xor,
309             $plugin->pattern_rotate
310             );
311              
312             # Deobfuscate account
313 12         63 my $account = $plugin->to_string(
314             scalar $p->param('sid'),
315             $xor,
316             $plugin->pattern_rotate
317             );
318 12         66 $p->remove('sid');
319              
320             # Something went wrong
321 12 50 33     352 unless ($host && $account) {
322 0         0 $c->app->log->warn('Path doesn\'t contain a valid email address');
323 0         0 return;
324             };
325              
326             # Create url
327 12         69 my $url = Mojo::URL->new;
328 12         191 $url->scheme('mailto');
329 12         170 $url->path($account . '@' . $host);
330              
331             # Deobfuscate further address parameters
332 12         2621 foreach my $type (qw/to cc bcc/) {
333 36 100       515 if (my @val = @{$p->every_param($type)}) {
  36         94  
334              
335             # Delete obfuscated parameters
336 8         184 $p->remove($type);
337              
338             # Append new deobfuscated parameters
339             $p->append($type => [map {
340 8         138 $plugin->to_string(
  10         34  
341             $_,
342             $xor,
343             $plugin->pattern_rotate
344             )
345             } @val]);
346             };
347             };
348              
349 12         246 $url->query->append($p);
350              
351             # Store the deobfuscated mail in the stash
352 12         584 $c->stash(mail_to_chiffre => $url);
353              
354 12         288 return 1;
355             };
356              
357              
358             # Simple string based xor function with looping key
359             sub _xor {
360 74     74   167 my $str = '';
361 74         293 for (my $i = 0; $i < length($_[0]); $i++) {
362 670         2401 $str .= substr($_[0], $i, 1) ^ substr($_[1], length($_[1]) % ($i + 1), 1);
363             };
364 74         392 return $str;
365             };
366              
367              
368             # Rotate with pattern
369             sub _rotate {
370 40     40   101 my $p = $_[1] % length($_[0]);
371 40         139 substr($_[0], $p) . substr($_[0], 0, $p)
372             };
373              
374              
375             # Unrotate with pattern_rotate
376             sub _unrotate {
377 34     34   135 my $p = abs($_[1] % length($_[0]) - length($_[0]));
378 34         133 substr($_[0], $p) . substr($_[0], 0, $p);
379             };
380              
381              
382             # Obfuscate the pattern shift a little bit
383             # by simple prime factorization
384             sub _factorize {
385 3     3   23 my $x = shift;
386 3         8 my %factors;
387 3         11 foreach (qw/2 3 5 7/) {
388 12         39 while (!($x % $_)) {
389 8         29 $factors{$_}++;
390 8         22 $x = $x / $_;
391             };
392             };
393 3         9 my @factors;
394 3         10 foreach (keys %factors) {
395 3 50       13 if ($factors{$_} > 1) {
396 3         17 push(@factors, $_[0] . '(' . $_ . ',' . $factors{$_} . ')');
397             }
398             else {
399 0         0 push(@factors, $_);
400             };
401             };
402 3 50       20 push(@factors, $x) unless $x == 1;
403 3         17 return join('*', @factors);
404             };
405              
406              
407             # Serialize to string
408             sub to_string {
409 34     34 0 2425 shift;
410 34 50       106 my $seq = shift or return;
411 34         90 my ($xor, $p) = @_;
412              
413 34         76 my ($str, $c, $num);
414 34         74 my $pos = 0;
415              
416 34         80 my $length = length $seq;
417              
418             # parse sequence
419 34         87 while ($pos < $length) {
420 308         620 $c = substr($seq, $pos++, 1);
421              
422             # Parse alphabetical character (ROT13)
423 308 100       757 if ($c =~ tr/n-za-mN-ZA-M/a-zA-Z/) {
    50          
424 18         46 $str .= $c;
425             }
426              
427             # Parse number
428             elsif ($c eq '-') {
429 290         454 $num = '';
430 290         500 $c = substr($seq, $pos++, 1);
431              
432             # Collect number segments
433 290         835 while ($c =~ /[0-9]/) {
434 534         925 $num .= $c;
435 534         1520 $c = substr($seq, $pos++, 1);
436             };
437              
438 290         466 $pos--;
439 290         849 $str .= chr($num);
440             }
441              
442             # Error
443             else {
444 0         0 return;
445             };
446             };
447 34         111 return _xor(_unrotate($str, $p), $xor);
448             };
449              
450              
451             # Serialize to sequence
452             sub to_sequence {
453 40     40 0 78 shift;
454 40         109 my ($s, $k, $p) = @_;
455              
456             # _xor is not allowed to be null
457 40         111 my $src = _rotate(_xor($s, $k), $p);
458 40         84 my $str;
459              
460             # Parse string
461 40         178 foreach my $c (split('', $src)) {
462              
463             # Change alphabetical character (ROT13)
464 362 100       899 if ($c =~ /[a-zA-Z]/) {
465 21         74 $c =~ tr/a-zA-Z/n-za-mN-ZA-M/;
466 21         51 $str .= $c;
467             }
468              
469             # Add numerical value
470             else {
471 341         699 $str .= '-' . ord($c);
472             };
473             };
474              
475 40         142 return $str;
476             };
477              
478              
479             1;
480              
481              
482             __END__