File Coverage

blib/lib/Mojolicious/Plugin/Piwik.pm
Criterion Covered Total %
statement 163 189 86.2
branch 74 104 71.1
condition 40 58 68.9
subroutine 14 16 87.5
pod 1 1 100.0
total 292 368 79.3


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