File Coverage

blib/lib/Mojolicious/Plugin/Piwik.pm
Criterion Covered Total %
statement 160 186 86.0
branch 73 102 71.5
condition 39 56 69.6
subroutine 14 16 87.5
pod 1 1 100.0
total 287 361 79.5


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Piwik;
2 9     9   55404 use Mojo::Base 'Mojolicious::Plugin';
  9         25  
  9         67  
3 9     9   1982 use Mojo::Util qw/deprecated quote/;
  9         28  
  9         549  
4 9     9   64 use Mojo::ByteStream 'b';
  9         18  
  9         450  
5 9     9   55 use Mojo::UserAgent;
  9         21  
  9         85  
6 9     9   263 use Mojo::Promise;
  9         29  
  9         105  
7 9     9   377 use Mojo::IOLoop;
  9         20  
  9         95  
8              
9             our $VERSION = '1.00';
10              
11             # Todo:
12             # - Better test tracking API support
13             # See http://piwik.org/docs/javascript-tracking/
14             # http://piwik.org/docs/tracking-api/reference/
15             # - Support custom values in tracking api.
16             # - Add eCommerce support
17             # http://piwik.org/docs/ecommerce-analytics/
18             # - Improve error handling.
19             # - Introduce piwik_widget helper
20             # - Support site_id and url both in piwik('track_script')
21             # shortcut and in piwik_tag 'as_script'
22              
23             has 'ua';
24              
25             # Register plugin
26             sub register {
27 13     13 1 29473 my ($plugin, $mojo, $plugin_param) = @_;
28              
29 13   50     57 $plugin_param ||= {};
30              
31             # Load parameter from Config file
32 13 50       125 if (my $config_param = $mojo->config('Piwik')) {
33 0         0 $plugin_param = { %$plugin_param, %$config_param };
34             };
35              
36             # Set embed value
37             $mojo->defaults(
38             'piwik.embed' => $plugin_param->{embed} //
39 13 100 100     293 ($mojo->mode eq 'production' ? 1 : 0)
40             );
41              
42             # No script route defined
43 13         417 my $script_route = 0;
44              
45 13   100     84 my $append = '' . ($plugin_param->{append} || '');
46              
47             # Create Mojo::UserAgent
48 13         70 $plugin->ua(
49             Mojo::UserAgent->new(
50             connect_timeout => 15,
51             max_redirects => 2
52             )
53             );
54              
55             # Set app to server
56 13         231 $plugin->ua->server->app($mojo);
57              
58             # Add 'piwik_tag' helper
59             $mojo->helper(
60             piwik_tag => sub {
61             # Controller
62 36     36   109199 my $c = shift;
63              
64             # Do not embed
65 36 100       183 return '' unless $c->stash('piwik.embed');
66              
67             # Per default integrate as inline
68 33         385 my $as_script = 0;
69              
70             # Use opt-out tag
71 33         61 my %opt;
72 33 100       82 if ($_[0]) {
73 23 100       107 if (index(lc $_[0], 'opt-out') == 0) {
    100          
74 10         13 my $opt_out = shift;
75              
76             # Get iframe content
77 10 100       28 my $cb = ref $_[-1] eq 'CODE' ? pop : 0;
78              
79             # Accept parameters
80 10         20 %opt = @_;
81 10         23 $opt{out} = $opt_out;
82 10         18 $opt{cb} = $cb;
83              
84             # Empty arguments
85 10         19 @_ = ();
86             }
87              
88             # Get a CSP compliant script tag
89             elsif (lc($_[0]) eq 'as-script') {
90              
91 4         8 $as_script = 1;
92              
93             # Empty arguments
94 4         8 @_ = ();
95             }
96             };
97              
98 33   100     170 my $site_id = shift || $plugin_param->{site_id} || 1;
99 33   66     137 my $url = shift || $plugin_param->{url};
100              
101             # No piwik url
102 33 50       94 return b('') unless $url;
103              
104             # Clear URL
105 33         82 ($url, my $prot) = _clear_url($url);
106              
107             # Load as script
108 33 100       85 if ($as_script) {
109 4 100       10 unless ($script_route) {
110 1         6 $c->app->log->error('No shortcut for track_script defined');
111 1         94 return '';
112             };
113              
114 3         14 return b('' .
115             "");
116             }
117              
118             # Render opt-out tag
119 29 100       84 if (my $opt_out = delete $opt{out}) {
120              
121             # Upgrade protocol if embedded in https page
122 10 100       21 if ($prot ne 'https') {
123 9         38 my $req_url = $c->req->url;
124 9 100       275 $prot = $req_url->scheme ? lc $req_url->scheme : 'http';
125             };
126              
127 10         108 my $cb = delete $opt{cb};
128 10         32 my $oo_url = "${prot}://${url}index.php?module=CoreAdminHome&action=optOut";
129              
130 10 100       31 if ($opt_out eq 'opt-out-link') {
131 3         6 $opt{href} = $oo_url;
132 3   50     16 $opt{rel} //= 'nofollow';
133 3   100     18 return $c->tag('a', %opt, ($cb || sub { 'Piwik Opt-Out' }));
134             };
135              
136 7         16 $opt{src} = $oo_url;
137 7   100     30 $opt{width} ||= '600px';
138 7   50     32 $opt{height} ||= '200px';
139 7   100     24 $opt{frameborder} ||= 'no';
140              
141 7   100     60 return $c->tag('iframe', %opt, ($cb || sub { '' }));
142             };
143              
144             # Create piwik tag
145 19         146 b(<<"SCRIPTTAG");
146            
153            
154             SCRIPTTAG
155 13         699 });
156              
157              
158             # Add piwik shortcut
159             $mojo->routes->add_shortcut(
160             piwik => sub {
161 2     2   1713 my $r = shift;
162 2   50     9 my $name = shift // 'unknown';
163              
164             # Add track script route
165 2 50       7 if ($name eq 'track_script') {
166              
167 2   50     7 my $site_id = $plugin_param->{site_id} || 1;
168 2         18 my $url = $plugin_param->{url};
169              
170 2 50       6 unless ($url) {
171 0         0 $mojo->log->error('No URL defined for Matomo (Piwik) instance');
172 0         0 return;
173             };
174              
175             # Clear URL
176 2         6 ($url, my $prot) = _clear_url($url);
177              
178 2         7 $script_route = 1;
179              
180             # Return track_script page
181             return $r->to(
182             cb => sub {
183 2         36471 my $c = shift;
184              
185             # Cache for three hours
186 2         12 $c->res->headers->cache_control('max-age=' . (60 * 60 * 3));
187              
188             # Render tracking code
189 2         105 return $c->render(
190             format => 'js',
191             text => 'var _paq=window._paq=window._paq||[];' .
192             "_paq.push(['setTrackerUrl','$prot://${url}piwik.php']);" .
193             "_paq.push(['setSiteId',$site_id]);" .
194             q!_paq.push(['trackPageView']);! .
195             q!_paq.push(['enableLinkTracking']);! .
196             $append
197             );
198             }
199 2         16 )->name('piwik_track_script');
200             };
201              
202 0         0 $mojo->log->error("Unknown Piwik shortcut " . quote($name));
203 0         0 return;
204             }
205 13         1649 );
206              
207              
208             # Establish 'piwik.api_url' helper
209             $mojo->helper(
210             'piwik.api_url' => sub {
211 31     31   89579 my ($c, $method, $param) = @_;
212              
213             # Get piwik url
214 31   66     232 my $url = delete($param->{url}) || $plugin_param->{url};
215              
216             # TODO:
217             # Simplify and deprecate secure parameter
218 31 100 100     209 if (!defined $param->{secure} && index($url, '/') != 0) {
219 4 0 33     23 if ($url =~ s{^(?:http(s)?:)?//}{}i && $1) {
220 0         0 $param->{secure} = 1;
221             };
222 4 50       18 $url = ($param->{secure} ? 'https' : 'http') . '://' . $url;
223             };
224              
225             # Create request URL
226 31         176 $url = Mojo::URL->new($url);
227              
228             # Site id
229             my $site_id = $param->{site_id} ||
230             $param->{idSite} ||
231             $param->{idsite} ||
232 31   50     2873 $plugin_param->{site_id} || 1;
233              
234             # delete unused parameters
235 31         81 delete @{$param}{qw/site_id idSite idsite format module method/};
  31         100  
236              
237             # Token Auth
238             my $token_auth = delete $param->{token_auth} ||
239 31   50     132 $plugin_param->{token_auth} || 'anonymous';
240              
241             # Tracking API
242 31 100       118 if (lc $method eq 'track') {
243              
244 7         26 $url->path('piwik.php');
245              
246             # Request Headers
247 7         1302 my $header = $c->req->headers;
248              
249             # Set default values
250 7         329 for ($param) {
251 7 100 33     29 $_->{ua} //= $header->user_agent if $header->user_agent;
252 7 100 33     147 $_->{urlref} //= $header->referrer if $header->referrer;
253 7         154 $_->{rand} = int(rand(10_000));
254 7         17 $_->{rec} = 1;
255 7         19 $_->{apiv} = 1;
256 7   66     33 $_->{url} = delete $_->{action_url} || $c->url_for->to_abs;
257              
258             # Todo: maybe make optional with parameter
259             # $_->{_id} = rand ...
260             };
261              
262              
263             # Respect do not track
264 7 100       760 if (defined $param->{dnt}) {
    100          
265 1 50       6 return if $param->{dnt};
266 1         4 delete $param->{dnt};
267             }
268             elsif ($header->dnt) {
269 1         15 return;
270             };
271              
272              
273             # Resolution
274 6 100 100     59 if ($param->{res} && ref $param->{res}) {
275 1         2 $param->{res} = join 'x', @{$param->{res}}[0, 1];
  1         5  
276             };
277              
278 6 100       49 $url->query(
279             idsite => ref $site_id ? $site_id->[0] : $site_id,
280             format => 'JSON'
281             );
282              
283 6 50       430 $url->query({token_auth => $token_auth}) if $token_auth;
284             }
285              
286             # Analysis API
287             else {
288              
289             # Create request method
290 24 100       147 $url->query(
291             module => 'API',
292             method => $method,
293             format => 'JSON',
294             idSite => ref $site_id ? join(',', @$site_id) : $site_id,
295             token_auth => $token_auth
296             );
297              
298             # Urls
299 24 100       1865 if ($param->{urls}) {
300              
301             # Urls is arrayref
302 2 50       9 if (ref $param->{urls}) {
303 2         4 my $i = 0;
304 2         6 foreach (@{$param->{urls}}) {
  2         5  
305 4         192 $url->query({ 'urls[' . $i++ . ']' => $_ });
306             };
307             }
308              
309             # Urls as string
310             else {
311 0         0 $url->query({urls => $param->{urls}});
312             };
313 2         127 delete $param->{urls};
314             };
315              
316             # Range with periods
317 24 100       76 if ($param->{period}) {
318              
319             # Delete period
320 2         6 my $period = lc delete $param->{period};
321              
322             # Delete date
323 2         4 my $date = delete $param->{date};
324              
325             # Get range
326 2 50       8 if ($period eq 'range') {
327 2 50       11 $date = ref $date ? join(',', @$date) : $date;
328             };
329              
330 2 50       19 if ($period =~ m/^(?:day|week|month|year|range)$/) {
331 2         19 $url->query({
332             period => $period,
333             date => $date
334             });
335             };
336             };
337             };
338              
339             # Todo: Handle Filter
340              
341             # Merge query
342 30         773 $url->query($param);
343              
344             # Return string for api testing
345 30         3168 return $url;
346             }
347 13         1115 );
348              
349              
350             # Establish 'piwik.api' helper
351             $mojo->helper(
352             'piwik.api' => sub {
353 14     14   49724 my ($c, $method, $param, $cb) = @_;
354              
355             # Get api_test parameter
356 14         42 my $api_test = delete $param->{api_test};
357              
358             # Get URL
359 14 100       51 my $url = $c->piwik->api_url($method, $param)
360             or return;
361              
362 13 100       122 return $url if $api_test;
363              
364             # Todo: Handle json errors!
365              
366             # Blocking
367 9 50       23 unless ($cb) {
368 9         33 my $tx = $plugin->ua->get($url);
369              
370             # Return prepared response
371 9 50       104787 return _prepare_response($tx->res) unless $tx->error;
372              
373 0         0 return;
374             };
375              
376             # Non-Blocking
377              
378             # Create delay object
379             my $delay = Mojo::IOLoop->delay(
380             sub {
381             # Return prepared response
382 0         0 my $res = pop->success;
383              
384             # Release callback with json object
385 0 0       0 $cb->( $res ? _prepare_response($res) : {} );
386             }
387 0         0 );
388              
389             # Get resource non-blocking
390 0         0 $plugin->ua->get($url => $delay->begin);
391              
392             # Start IOLoop if not started already
393 0 0       0 $delay->wait unless Mojo::IOLoop->is_running;
394              
395             # Set api_test to true
396 0         0 return $delay;
397             }
398 13         5358 );
399              
400              
401             # Establish 'piwik.api_p' helper
402             $mojo->helper(
403             'piwik.api_p' => sub {
404 10     10   20664 my ($c, $method, $param) = @_;
405              
406             # Get api_test parameter
407 10         35 my $api_test = delete $param->{api_test};
408              
409             # Get URL
410 10 50       23 my $url = $c->piwik->api_url($method, $param)
411             or return;
412              
413 10 50       69 return Mojo::Promise->resolve($url) if $api_test;
414              
415             # Create promise
416             return $plugin->ua->get_p($url)->then(
417             sub {
418 10         120725 my $tx = shift;
419 10         29 my $res = _prepare_response($tx->res);
420              
421             # Check for error
422 10 50 66     1887 if (ref $res eq 'HASH' && $res->{error}) {
423 0         0 return Mojo::Promise->reject($res->{error});
424             };
425 10         44 return Mojo::Promise->resolve($res);
426             }
427 10         37 );
428             }
429 13         4406 );
430              
431              
432             # Add legacy 'piwik_api' helper
433             $mojo->helper(
434             'piwik_api' => sub {
435 0     0   0 my $c = shift;
436 0         0 deprecated 'Deprecated in favor of piwik->api';
437 0         0 return $c->piwik->api(@_);
438             }
439 13         4740 );
440              
441              
442             # Establish 'piwik_api_url' helper
443             $mojo->helper(
444             piwik_api_url => sub {
445 0     0   0 my $c = shift;
446 0         0 deprecated 'Deprecated in favor of piwik->api_url';
447 0         0 return $c->piwik->api_url(@_);
448             }
449 13         1030 );
450             };
451              
452              
453             # Treat response different
454             sub _prepare_response {
455 19     19   257 my $res = shift;
456 19         59 my $ct = $res->headers->content_type;
457              
458             # No response - fine
459 19 100       320 unless ($res->body) {
460 1         36 return { body => '' };
461             };
462              
463             # Return json response
464 18 50       422 if (index($ct, 'json') >= 0) {
    0          
    0          
465 18         64 return $res->json;
466             }
467              
468             # Prepare erroneous html response
469             elsif (index($ct, 'html') >= 0) {
470              
471             # Find error message in html
472 0         0 my $found = $res->dom->at('#contentsimple > p');
473              
474             # Return unknown error
475 0 0       0 return { error => 'unknown' } unless $found;
476              
477             # Return error message as json
478 0         0 return { error => $found->all_text };
479             }
480              
481             # Prepare image responses
482             elsif ($ct =~ m{^image/(gif|jpe?g)}) {
483             return {
484 0         0 image => 'data:image/' . $1 . ';base64,' . b($res->body)->b64_encode
485             };
486             };
487              
488             # Return unknown response type
489             return {
490 0         0 error => 'Unknown response type',
491             body => $res->body
492             };
493             };
494              
495              
496             sub _clear_url {
497 35     35   65 my $url = shift;
498 35         63 my $prot = 'http';
499              
500             # Clear url
501 35         82 for ($url) {
502 35 100       142 if (s{^http(s?):/*}{}i) {
503 5 100       20 $prot = 'https' if $1;
504             };
505 35         105 s{piwik\.(?:php|js)$}{}i;
506 35         245 s{(?
507             };
508 35         114 return ($url, $prot);
509             };
510              
511             1;
512              
513              
514             __END__