From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

#!/usr/bin/perl
#FEATURE: admin: clear stats (for specific rage/for hits older than ...)
#FEATURE: graphical representation
=head1 NAME
Konstrukt::Plugin::hitstats - Hit statistics plugin
=head1 SYNOPSIS
<!-- count hit. use the specified title -->
<& hitstats title="some page" / &>
<!-- count hit. use the current filename as title -->
<& hitstats / &>
<!-- display the overall top sites -->
<& hitstats show="all" / &>
<!-- display the top sites grouped by year -->
<!-- month and day will also work, if the data is stored in such a fine granularity -->
<!-- the display aggregation should not be finer than the setting hitstats/aggregate -->
<& hitstats show="year" / &>
<!-- only display the top 20 sites -->
<& hitstats show="all" limit="20" / &>
<!-- display a counter for the current site -->
<& hitstats show="counter" / &>
<!-- with optional title attribute -->
<& hitstats show="counter" title="some page" / &>
=head1 DESCRIPTION
Creates statistics about the number of hits of your homepage.
You may simply integrate it by putting the tag into your page. See </SYNOPSIS>
for details.
=head1 CONFIGURATION
You may do some configuration in your konstrukt.settings to let the
plugin know where to get its data and which layout to use. Defaults:
#backend
hitstats/backend DBI
See the documentation of the backend modules
(e.g. L<Konstrukt::Plugin::hitstats::DBI/CONFIGURATION>) for their configuration.
#granularity
hitstats/aggregate all #specifies the granularity of the logs. may be all, year, month, day
#layout
hitstats/template_path /templates/hitstats/
#only count unique visitors (determined by session)
hitstats/unique 0
#don't count hits by robots
hitstats/ignore_robots 1
#access control
hitstats/userlevel_view 1 #userlevel to view the stats
hitstats/userlevel_clear 2 #userlevel to clear the logs
=cut
use strict;
use base 'Konstrukt::Plugin'; #inheritance
use Konstrukt::Plugin; #import use_plugin
=head1 METHODS
=head2 execute_again
Yes, this plugin may return dynamic nodes (i.e. template nodes).
=cut
sub execute_again {
return 1;
}
#= /execute_again
=head2 init
Initializes this object. Sets $self->{backend} and $self->{template_path}layout/.
init will be called by the constructor.
=cut
sub init {
my ($self) = @_;
#set default settings
$Konstrukt::Settings->default("hitstats/backend" => "DBI");
$Konstrukt::Settings->default("hitstats/template_path" => "/templates/hitstats/");
$Konstrukt::Settings->default("hitstats/aggregate" => "all");
$Konstrukt::Settings->default("hitstats/unique" => 0);
$Konstrukt::Settings->default("hitstats/ignore_robots" => 1);
$Konstrukt::Settings->default("hitstats/userlevel_view" => 1);
$Konstrukt::Settings->default("hitstats/userlevel_admin" => 2);
#create user management objects, if needed
if ($Konstrukt::Settings->get("hitstats/userlevel_view") or $Konstrukt::Settings->get("hitstats/userlevel_admin")) {
#dependencies
$self->{user_level} = use_plugin 'usermanagement::level' or return undef;
}
$self->{backend} = use_plugin "hitstats::" . $Konstrukt::Settings->get("hitstats/backend") or return undef;
$self->{template_path} = $Konstrukt::Settings->get('hitstats/template_path');
return 1;
}
#= /init
=head2 install
Installs the templates.
B<Parameters:>
none
=cut
sub install {
my ($self) = @_;
return $Konstrukt::Lib->plugin_file_install_helper($self->{template_path});
}
# /install
=head2 prepare
Prepare method
B<Parameters>:
=over
=item * $tag - Reference to the tag (and its children) that shall be handled.
=back
=cut
sub prepare {
my ($self, $tag) = @_;
#Don't do anything beside setting the dynamic-flag
$tag->{dynamic} = 1;
return undef;
}
#= /prepare
=head2 execute
All the work is done in the execute step.
B<Parameters>:
=over
=item * $tag - Reference to the tag (and its children) that shall be handled.
=back
=cut
sub execute {
my ($self, $tag) = @_;
#reset the collected nodes
$self->reset_nodes();
my $attributes = $tag->{tag}->{attributes};
if (defined $attributes->{show}) {
if ($attributes->{show} eq 'counter') {
#show a counter
$self->show_counter($attributes->{title});
} else {
#show the statistics
$self->show_stats($attributes->{show}, $attributes->{limit});
}
} else {
#log a hit
$self->hit($attributes->{title});
}
return $self->get_nodes();
}
#= /execute
=head2 hit
Logs a hit.
B<Parameters>:
=over
=item * $title - The title of the page to log. (optional)
If not defined, the filename of the current page will be used.
=back
=cut
sub hit {
my ($self, $title) = @_;
#ignore robots?
if ($Konstrukt::Settings->get('hitstats/ignore_robots')) {
my $browser = new HTTP::BrowserDetect($Konstrukt::Request->header('User-Agent'));
return if $browser->robot();
}
#only log unique visitors?
if ($Konstrukt::Settings->get('hitstats/unique')) {
return unless $Konstrukt::Session->activated();
if ($Konstrukt::Session->get('hitstats/visited')) {
#don't log again
return;
} else {
#set visited flag
$Konstrukt::Session->set('hitstats/visited', 1);
}
}
$title = $Konstrukt::Handler->{filename} unless $title;
unless ($self->{backend}->hit($title, $Konstrukt::Settings->get('hitstats/aggregate'))) {
$Konstrukt::Debug->error_message("An internal error occured while logging the hit for the page '$title'!") if Konstrukt::Debug::ERROR;
}
}
#= /hit
=head2 show_stats
Displays the results of the hit logging.
B<Parameters>:
=over
=item * $aggregate - The range over which the hits should be aggregated.
May be C<all>, C<year>, C<month> and C<day>. Should not be finer than
the setting C<hitstats/aggregate>
=item * $limit - Max. number of returned entries.
=back
=cut
sub show_stats {
my ($self, $aggregate, $limit) = @_;
$aggregate ||= 'all';
$limit ||= 0;
my $template = use_plugin 'template';
my $level_view = $Konstrukt::Settings->get('hitstats/userlevel_view');
if ($level_view > 0 and $self->{user_level}->level() >= $level_view) {
if (my $stats = $self->{backend}->get($aggregate, $limit)) {
if (@{$stats}) {
#create groups of aggregation ranges
my @groups;
my $last_date = '';
foreach my $entry (@{$stats}) {
if ($entry->{date} ne $last_date) {
last if $limit and @groups == $limit;
#new group
$last_date = $entry->{date};
push @groups, { entries => [], sum => 0 };
}
push @{$groups[-1]->{entries}}, $entry;
$groups[-1]->{sum} += $entry->{count};
}
$groups[-1]->{last_one} = 1;
#put out groups
foreach my $group (@groups) {
$self->add_node($template->node("$self->{template_path}layout/group.template", { last_one => exists $group->{last_one}, date => $group->{entries}->[0]->{date}, aggregate => $aggregate, entries => [ map { $_->{share} = sprintf("%.1f%%", 100 * $_->{count} / $group->{sum}); { fields => $_ } } @{$group->{entries}} ] }));
}
} else {
$self->add_node($template->node("$self->{template_path}layout/empty.template"));
}
} else {
$self->add_node($template->node("$self->{template_path}messages/view_failed.template"));
}
} else {
$self->add_node($template->node("$self->{template_path}messages/view_failed_permission_denied.template"));
}
}
#= /show_stats
=head2 show_counter
Displays a simple counter for the specified page. Won't show if the user doesn't
have the needed user level.
B<Parameters>:
=over
=item * $title - The title of the page
=back
=cut
sub show_counter {
my ($self, $title) = @_;
my $template = use_plugin 'template';
my $level_view = $Konstrukt::Settings->get('hitstats/userlevel_view');
$title = $Konstrukt::Handler->{filename} unless $title;
if ($level_view > 0 and $self->{user_level}->level() >= $level_view) {
if (defined (my $count = $self->{backend}->get_count($title))) {
$self->add_node($template->node("$self->{template_path}layout/counter.template", { count => $count }));
} else {
$self->add_node($template->node("$self->{template_path}messages/counter_failed.template"));
}
}
}
#= /show_counter
1;
=head1 AUTHOR
Copyright 2006 Thomas Wittek (mail at gedankenkonstrukt dot de). All rights reserved.
This document is free software.
It is distributed under the same terms as Perl itself.
=head1 SEE ALSO
L<Konstrukt::Plugin::hitstats::DBI>, L<Konstrukt::Plugin>, L<Konstrukt>
=cut
__DATA__
== 8< == textfile: layout/counter.template == >8 ==
<p style="text-align: center">Hits: <+$ count $+>(Counter not available)<+$ / $+></p>
== 8< == textfile: layout/empty.template == >8 ==
<p>No hit statistics available yet.</p>
== 8< == textfile: layout/group.template == >8 ==
<div class="hitstats group">
<h1>
<& perl &>
my $date = '<+$ date $+>0000-00-00<+$ / $+>';
my $aggregate = '<+$ aggregate $+>all<+$ / $+>';
my %months = qw/01 January 02 February 03 March 04 April 05 May 06 June 07 July 08 August 09 September 10 October 11 November 12 December/;
if ($aggregate eq 'year') {
print 'Visits in year ' . substr($date, 0, 4);
} elsif ($aggregate eq 'month') {
print 'Visits in month ' . $months{substr($date, 5, 2)} . ' ' . substr($date, 0, 4);
} elsif ($aggregate eq 'day') {
print 'Visits on ' . $months{substr($date, 5, 2)} . substr($date, 8, 2) . '., ' . substr($date, 0, 4);
} else {
print 'All visits';
}
<& / &>
</h1>
<table>
<colgroup>
<col width="*" />
<col width="70" />
<col width="70" />
</colgroup>
<tr><th>Page</th><th>Count</th><th>Share</th></tr>
<+@ entries @+>
<tr>
<td><+$ title $+>(no title)<+$ / $+></td>
<td><+$ count $+>(no count)<+$ / $+></td>
<td><+$ share $+>(no share)<+$ / $+></td>
</tr>
<+@ / @+>
</table>
</div>
<& if condition="not '<+$ last_one $+>0<+$ / $+>'" &><hr /><& / &>
== 8< == textfile: messages/counter_failed.template == >8 ==
<div class="hitstats message failure">
<h1>Counter cannot be shown</h1>
<p>An internal error occurred!</p>
</div>
== 8< == textfile: messages/view_failed.template == >8 ==
<div class="hitstats message failure">
<h1>Hit statistics cannot be shown</h1>
<p>An internal error occurred!</p>
</div>
== 8< == textfile: messages/view_failed_permission_denied.template == >8 ==
<div class="hitstats message failure">
<h1>Hit statistics cannot be shown</h1>
<p>The hit statistics cannot be shown, because you don't have the appropriate permissions!</p>
</div>
== 8< == textfile: /styles/hitstats.css == >8 ==
/* CSS definitions for the Konstrukt hitstats plugin */
/* nothing to see here */