<?php /*

 Composr
 Copyright (c) ocProducts, 2004-2016

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  ocProducts Ltd
 * @package    stats
 */

/**
 * Module page class.
 */
class Module_admin_stats
{
    /**
     * Find details of the module.
     *
     * @return ?array Map of module info (null: module is disabled).
     */
    public function info()
    {
        $info = array();
        $info['author'] = 'Philip Withnall';
        $info['organisation'] = 'ocProducts';
        $info['hacked_by'] = null;
        $info['hack_version'] = null;
        $info['version'] = 9;
        $info['locked'] = true;
        $info['update_require_upgrade'] = true;
        return $info;
    }

    /**
     * Uninstall the module.
     */
    public function uninstall()
    {
        $GLOBALS['SITE_DB']->drop_table_if_exists('stats');
        $GLOBALS['SITE_DB']->drop_table_if_exists('usersonline_track');
        $GLOBALS['SITE_DB']->drop_table_if_exists('ip_country');
    }

    /**
     * Install the module.
     *
     * @param  ?integer $upgrade_from What version we're upgrading from (null: new install)
     * @param  ?integer $upgrade_from_hack What hack version we're upgrading from (null: new-install/not-upgrading-from-a-hacked-version)
     */
    public function install($upgrade_from = null, $upgrade_from_hack = null)
    {
        if (is_null($upgrade_from)) {
            $GLOBALS['SITE_DB']->create_table('stats', array(
                'id' => '*AUTO',
                'the_page' => 'SHORT_TEXT',
                'ip' => 'IP',
                'member_id' => 'MEMBER',
                'session_id' => 'ID_TEXT',
                'date_and_time' => 'TIME',
                'referer' => 'URLPATH',
                's_get' => 'URLPATH',
                'post' => 'LONG_TEXT',
                'browser' => 'SHORT_TEXT',
                'milliseconds' => 'INTEGER',
                'operating_system' => 'SHORT_TEXT',
                'access_denied_counter' => 'INTEGER'
            ));

            // Note: We have chosen not to create many indices because we want insertion to be very fast
            $GLOBALS['SITE_DB']->create_index('stats', 'member_track_2', array('ip'));
            $GLOBALS['SITE_DB']->create_index('stats', 'pages', array('the_page'));
            $GLOBALS['SITE_DB']->create_index('stats', 'date_and_time', array('date_and_time'));
            $GLOBALS['SITE_DB']->create_index('stats', 'milliseconds', array('milliseconds'));
            $GLOBALS['SITE_DB']->create_index('stats', 'referer', array('referer'));
            $GLOBALS['SITE_DB']->create_index('stats', 'browser', array('browser'));
            $GLOBALS['SITE_DB']->create_index('stats', 'operating_system', array('operating_system'));

            $GLOBALS['SITE_DB']->create_table('usersonline_track', array(
                'date_and_time' => '*TIME',
                'peak' => 'INTEGER'
            ));

            $GLOBALS['SITE_DB']->create_index('usersonline_track', 'peak_track', array('peak'));

            $GLOBALS['SITE_DB']->create_table('ip_country', array(
                'id' => '*AUTO',
                'begin_num' => 'UINTEGER',
                'end_num' => 'UINTEGER',
                'country' => 'SHORT_TEXT'
            ));

            require_lang('stats');

            require_code('crypt');
            $secure_ref = produce_salt();
            $id = $GLOBALS['SITE_DB']->query_insert('task_queue', array(
                't_title' => do_lang('INSTALL_GEOLOCATION_DATA'),
                't_hook' => 'install_geolocation_data',
                't_args' => serialize(array()),
                't_member_id' => $GLOBALS['FORUM_DRIVER']->get_guest_id(),
                't_secure_ref' => $secure_ref, // Used like a temporary password to initiate the task
                't_send_notification' => 0,
                't_locked' => 0,
            ), true);
        }

        if ((!is_null($upgrade_from)) && ($upgrade_from < 8)) {
            $GLOBALS['SITE_DB']->alter_table_field('stats', 'get', 'URLPATH', 's_get');
        }

        if ((!is_null($upgrade_from)) && ($upgrade_from < 9)) {
            $GLOBALS['SITE_DB']->alter_table_field('stats', 'the_user', 'MEMBER', 'member_id');
            $GLOBALS['SITE_DB']->add_table_field('stats', 'session_id', 'ID_TEXT');
            $GLOBALS['SITE_DB']->query_update('db_meta_indices', array('i_fields' => 'member_id'), array('i_name' => 'member_track_1'), '', 1);

            $GLOBALS['SITE_DB']->delete_index_if_exists('stats', 'member_track_1');
            $GLOBALS['SITE_DB']->delete_index_if_exists('stats', 'member_track_3');
        }

        if ((is_null($upgrade_from)) || ($upgrade_from < 9)) {
            $GLOBALS['SITE_DB']->create_index('stats', 'member_track_4', array('session_id'));
            $GLOBALS['SITE_DB']->create_index('stats', 'member_track_1', array('member_id'));
            $GLOBALS['SITE_DB']->create_index('stats', 'member_track_3', array('member_id', 'date_and_time'));
        }
    }

    /**
     * Find entry-points available within this module.
     *
     * @param  boolean $check_perms Whether to check permissions.
     * @param  ?MEMBER $member_id The member to check permissions as (null: current user).
     * @param  boolean $support_crosslinks Whether to allow cross links to other modules (identifiable via a full-page-link rather than a screen-name).
     * @param  boolean $be_deferential Whether to avoid any entry-point (or even return null to disable the page in the Sitemap) if we know another module, or page_group, is going to link to that entry-point. Note that "!" and "browse" entry points are automatically merged with container page nodes (likely called by page-groupings) as appropriate.
     * @return ?array A map of entry points (screen-name=>language-code/string or screen-name=>[language-code/string, icon-theme-image]) (null: disabled).
     */
    public function get_entry_points($check_perms = true, $member_id = null, $support_crosslinks = true, $be_deferential = false)
    {
        require_lang('stats');

        $ret = array(
            'browse' => array('SITE_STATISTICS', 'menu/adminzone/audit/statistics/statistics'),
            'overview' => array('OVERVIEW_STATISTICS', 'menu/adminzone/audit/statistics/statistics'),
            'users_online' => array('USERS_ONLINE_STATISTICS', 'menu/adminzone/audit/statistics/users_online'),
            'submission_rates' => array('SUBMISSION_STATISTICS', 'menu/adminzone/audit/statistics/submits'),
            'referrers' => array('TOP_REFERRERS', 'menu/adminzone/audit/statistics/top_referrers'),
            'keywords' => array('TOP_SEARCH_KEYWORDS', 'menu/adminzone/audit/statistics/top_keywords'),
            'page' => array('PAGES_STATISTICS', 'menu/adminzone/audit/statistics/page_views'),
            'load_times' => array('LOAD_TIMES', 'menu/adminzone/audit/statistics/load_times'),
            'clear' => array('CLEAR_STATISTICS', 'menu/adminzone/audit/statistics/clear_stats'),
        );

        static $has_geolocation_data = null;
        if ($has_geolocation_data === null) {
            $test = $GLOBALS['SITE_DB']->query_select_value_if_there('ip_country', 'id');
            $has_geolocation_data = ($test !== null);
        }
        if (!$has_geolocation_data) {
            $ret['install_data'] = array('INSTALL_GEOLOCATION_DATA', 'menu/adminzone/audit/statistics/geolocate');
        }

        $hooks = find_all_hooks('modules', 'admin_stats');
        foreach (array_keys($hooks) as $hook) {
            require_code('hooks/modules/admin_stats/' . filter_naughty_harsh($hook));
            $ob = object_factory('Hook_admin_stats_' . filter_naughty_harsh($hook), true);
            if (is_null($ob)) {
                continue;
            }
            $info = $ob->info();
            if (!is_null($info)) {
                $ret += $info[0];
            }
        }

        return $ret;
    }

    public $title;

    /**
     * Module pre-run function. Allows us to know metadata for <head> before we start streaming output.
     *
     * @return ?Tempcode Tempcode indicating some kind of exceptional output (null: none).
     */
    public function pre_run()
    {
        $type = get_param_string('type', 'browse');

        require_lang('stats');

        if ($type != 'browse' && $type != '_clear') {
            breadcrumb_set_parents(array(array('_SELF:_SELF:browse', do_lang_tempcode('SITE_STATISTICS'))));
        }

        if ($type == '_clear') {
            breadcrumb_set_parents(array(array('_SELF:_SELF:browse', do_lang_tempcode('SITE_STATISTICS')), array('_SELF:_SELF:clear', do_lang_tempcode('CLEAR_STATISTICS'))));
            breadcrumb_set_self(do_lang_tempcode('DONE'));
        }

        if ($type == 'overview') {
            inform_non_canonical_parameter('sort_views');
        }

        if ($type == '_page') {
            inform_non_canonical_parameter('sort_keywords');
            inform_non_canonical_parameter('sort_regionalities');
            inform_non_canonical_parameter('sort_views');
        }

        set_helper_panel_tutorial('tut_statistics');

        if ($type == 'users_online') {
            $this->title = get_screen_title('USERS_ONLINE_STATISTICS');
        }

        if ($type == 'submission_rates') {
            $this->title = get_screen_title('SUBMISSION_STATISTICS');
        }

        if ($type == 'load_times') {
            $this->title = get_screen_title('LOAD_TIMES');
        }

        if ($type == 'referrers') {
            $this->title = get_screen_title('TOP_REFERRERS');
        }

        if ($type == 'keywords') {
            $this->title = get_screen_title('TOP_SEARCH_KEYWORDS');
        }

        if ($type == 'overview') {
            $this->title = get_screen_title('OVERVIEW_STATISTICS');
        }

        if ($type == 'page') {
            $this->title = get_screen_title('PAGES_STATISTICS');
        }

        if ($type == '_page') {
            $page = get_param_string('iscreen');
            $this->title = get_screen_title(do_lang_tempcode('_PAGE_STATISTICS', escape_html($page)), false);
        }

        if ($type == 'clear') {
            $this->title = get_screen_title('CLEAR_STATISTICS');
        }

        if ($type == 'install_data') {
            $this->title = get_screen_title('INSTALL_GEOLOCATION_DATA');
        }

        return null;
    }

    /**
     * Execute the module.
     *
     * @return Tempcode The result of execution.
     */
    public function run()
    {
        if (!$GLOBALS['SITE_DB']->table_is_locked('stats')) {
            $GLOBALS['SITE_DB']->query('DELETE FROM ' . get_table_prefix() . 'stats WHERE date_and_time<' . strval(time() - 60 * 60 * 24 * intval(get_option('stats_store_time'))), 500/*to reduce lock times*/);
        }

        require_code('svg');
        require_css('stats');

        if (get_param_integer('csv', 0) == 1) {
            require_code('files2');
        } else {
            send_http_output_ping();
        }

        $type = get_param_string('type', 'browse');

        if (!file_exists(get_custom_file_base() . '/data_custom/modules/admin_stats')) {
            require_code('files2');
            make_missing_directory(get_custom_file_base() . '/data_custom/modules/admin_stats');
        }

        if ($type == 'browse') {
            return $this->browse();
        } elseif ($type == 'overview') {
            return $this->overview();
        } elseif ($type == 'users_online') {
            return $this->users_online();
        } elseif ($type == 'submission_rates') {
            return $this->submission_rates();
        } elseif ($type == 'referrers') {
            return $this->referrers();
        } elseif ($type == 'keywords') {
            return $this->keywords();
        } elseif ($type == '_page') {
            return $this->show_page();
        } elseif ($type == 'load_times') {
            return $this->load_times();
        } elseif ($type == 'clear') {
            return $this->clear();
        } elseif ($type == '_clear') {
            return $this->_clear();
        } elseif ($type == 'install_data') {
            return $this->install_geolocation_data();
        } elseif ($type == 'page') {
            return $this->page_stats();
        } else {
            $hooks = find_all_hooks('modules', 'admin_stats');
            foreach (array_keys($hooks) as $hook) {
                require_code('hooks/modules/admin_stats/' . filter_naughty_harsh($hook));
                $ob = object_factory('Hook_admin_stats_' . filter_naughty_harsh($hook));
                if (method_exists($ob, $type)) {
                    return call_user_func_array(array(&$ob, $type), array(&$this, $type));
                }
            }
        }
        return new Tempcode();
    }

    /**
     * The do-next manager for before content management.
     *
     * @return Tempcode The UI
     */
    public function browse()
    {
        require_code('templates_donext');

        $actions = array(
            array('menu/adminzone/audit/statistics/statistics', array('_SELF', array('type' => 'overview'), '_SELF'), do_lang('OVERVIEW_STATISTICS'), 'DESCRIPTION_OVERVIEW_STATISTICS'),
            array('menu/adminzone/audit/statistics/page_views', array('_SELF', array('type' => 'page'), '_SELF'), do_lang('PAGES_STATISTICS'), 'DOC_PAGE_STATISTICS'),
            array('menu/adminzone/audit/statistics/users_online', array('_SELF', array('type' => 'users_online'), '_SELF'), do_lang('USERS_ONLINE_STATISTICS'), 'DOC_USERS_ONLINE_STATISTICS'),
            array('menu/adminzone/audit/statistics/submits', array('_SELF', array('type' => 'submission_rates'), '_SELF'), do_lang('SUBMISSION_STATISTICS'), 'DOC_SUBMISSION_STATISTICS'),
            array('menu/adminzone/audit/statistics/load_times', array('_SELF', array('type' => 'load_times'), '_SELF'), do_lang('LOAD_TIMES'), 'DOC_LOAD_TIMES'),
            array('menu/adminzone/audit/statistics/top_referrers', array('_SELF', array('type' => 'referrers'), '_SELF'), do_lang('TOP_REFERRERS'), 'DOC_TOP_REFERRERS'),
            array('menu/adminzone/audit/statistics/top_keywords', array('_SELF', array('type' => 'keywords'), '_SELF'), do_lang('TOP_SEARCH_KEYWORDS'), 'DOC_TOP_SEARCH_KEYWORDS'),
        );

        $hooks = find_all_hooks('modules', 'admin_stats');
        foreach (array_keys($hooks) as $hook) {
            require_code('hooks/modules/admin_stats/' . filter_naughty_harsh($hook));
            $ob = object_factory('Hook_admin_stats_' . filter_naughty_harsh($hook), true);
            if (is_null($ob)) {
                continue;
            }
            $info = $ob->info();
            if (!is_null($info)) {
                $actions = array_merge($actions, array($info[1]));
            }
        }

        $test = $GLOBALS['SITE_DB']->query_select_value_if_there('ip_country', 'id');
        if ($test === null) {
            $actions[] = array('menu/adminzone/audit/statistics/geolocate', array('_SELF', array('type' => 'install_data'), '_SELF'), do_lang('INSTALL_GEOLOCATION_DATA'), 'DOC_INSTALL_GEOLOCATION_DATA');
        }

        $actions[] = array('menu/adminzone/audit/statistics/clear_stats', array('_SELF', array('type' => 'clear'), '_SELF'), do_lang('CLEAR_STATISTICS'), do_lang_tempcode('DESCRIPTION_CLEAR_STATISTICS'));

        return do_next_manager(get_screen_title('SITE_STATISTICS'), comcode_lang_string('DOC_STATISTICS'),
            $actions,
            do_lang('SITE_STATISTICS')
        );
    }

    /**
     * An interface for choosing between dates.
     *
     * @param  Tempcode $title The title to display.
     * @param  boolean $stats_table Whether display is dependent on what we kept in our stats table.
     * @param  ?Tempcode $extra_fields Extra fields to request (null: none).
     * @param  ?Tempcode $message The message to show for date selection (null: default).
     * @return Tempcode The result of execution.
     */
    public function get_between($title, $stats_table = false, $extra_fields = null, $message = null)
    {
        require_code('form_templates');

        $fields = new Tempcode();
        $month_start = utctime_to_usertime(mktime(0, 0, 0, intval(date('m')), 1, intval(date('Y'))));
        $prior_month = intval(date('m')) - 1;
        $prior_year = intval(date('Y'));
        if ($prior_month == 0) {
            $prior_month = 12;
            $prior_year--;
        }
        $prior_month_start = utctime_to_usertime(mktime(0, 0, 0, $prior_month, 1, $prior_year));
        $first_stat = $stats_table ? $GLOBALS['SITE_DB']->query_select_value_if_there('stats', 'MIN(date_and_time)') : null;
        if (is_null($first_stat)) {
            $year_start = intval(date('Y')) - 5;
            $years_ahead = 5;
            $first_stat = time();
        } else {
            $year_start = intval(date('Y', $first_stat));
            $years_ahead = intval(date('Y')) - $year_start;
        }
        if ($stats_table) {
            if (utctime_to_usertime($first_stat) > $month_start) {
                $prior_month_start = $first_stat;
                $month_start = time();
            }
        }
        $fields->attach(form_input_date(do_lang_tempcode('FROM'), do_lang_tempcode('TIME_RANGE_START'), 'time_start', false, false, false, $prior_month_start, $years_ahead, $year_start));
        $fields->attach(form_input_date(do_lang_tempcode('TO'), do_lang_tempcode('TIME_RANGE_END'), 'time_end', false, false, false, $month_start, $years_ahead, $year_start));
        if (!is_null($extra_fields)) {
            $fields->attach($extra_fields);
        }

        $post_url = get_self_url(false, false, array('dated' => 1), false, true);

        if (is_null($message)) {
            $message = do_lang_tempcode($stats_table ? 'SELECT_STATS_RANGE' : '_SELECT_STATS_RANGE', escape_html(get_timezoned_date($first_stat, false)));
        }

        return do_template('FORM_SCREEN', array('_GUID' => '3e76584f20ecfb947b00638211e63321', 'SKIP_WEBSTANDARDS' => true, 'GET' => true, 'TITLE' => $title, 'FIELDS' => $fields, 'TEXT' => $message, 'HIDDEN' => '', 'URL' => $post_url, 'SUBMIT_ICON' => 'buttons__proceed', 'SUBMIT_NAME' => do_lang_tempcode('CHOOSE')));
    }

    /**
     * The UI to show user online statistics.
     *
     * @return Tempcode The UI
     */
    public function users_online()
    {
        // This needs to show a big scatter graph with the users online every day

        $start = get_param_integer('start', 0);
        $max = get_param_integer('max', 50); // Intentionally the browse is disabled, as the graph will show all - we fudge $max_rows to $i
        $csv = get_param_integer('csv', 0) == 1;
        if ($csv) {
            if (php_function_allowed('set_time_limit')) {
                @set_time_limit(0);
            }
            $start = 0;
            $max = 10000;
        }
        $sortables = array('date_and_time' => do_lang_tempcode('DATE_TIME'), 'peak' => do_lang_tempcode('PEAK'));
        $test = explode(' ', get_param_string('sort', 'date_and_time DESC'), 2);
        if (count($test) == 1) {
            $test[1] = 'DESC';
        }
        list($sortable, $sort_order) = $test;
        if (((strtoupper($sort_order) != 'ASC') && (strtoupper($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
            log_hack_attack_and_exit('ORDERBY_HACK');
        }

        $rows = $GLOBALS['SITE_DB']->query_select('usersonline_track', array('date_and_time', 'peak'), null, 'ORDER BY ' . $sortable . ' ' . $sort_order);
        if (count($rows) < 1) {
            return warn_screen($this->title, do_lang_tempcode('NO_DATA'));
        }

        $base = $rows[0]['date_and_time'];
        foreach ($rows as $value) {
            if ($value['date_and_time'] < $base) {
                $base = $value['date_and_time'];
            }
        }

        $data = array();

        foreach ($rows as $value) {
            $date = get_timezoned_date($value['date_and_time'], false);
            // If there's no data, or if this isn't the same as the last record and is more than an hour later than it
            if ((count($data) == 0) || ($data[count($data) - 1]['key'] != $date)) {
                $data[] = array('t' => $value['date_and_time'] - $base, 'key' => $date, 'value' => $value['peak']);
            } else {
                $data[count($data) - 1]['value'] = max($value['peak'], $data[count($data) - 1]['value']);
            }
        }

        require_code('templates_results_table');
        $fields_title = results_field_title(array(do_lang_tempcode('DATE_TIME'), do_lang_tempcode('PEAK')), $sortables, 'sort', $sortable . ' ' . $sort_order);
        $fields = new Tempcode();
        $real_data = array();
        for ($i = 0; $i < $max; $i++) {
            if (!array_key_exists($i, $data)) {
                continue;
            }

            $real_data[] = array(
                do_lang('DATE_TIME') => get_timezoned_date($data[$i]['t'] + $base),
                do_lang('COUNT_TOTAL') => $data[$i]['value'],
            );

            $fields->attach(results_entry(array(get_timezoned_date($data[$i]['t'] + $base, false), integer_format($data[$i]['value'])), true));
        }
        if ($csv) {
            make_csv($real_data, 'users_online.csv');
        }
        $list = results_table(do_lang_tempcode('USERS_ONLINE_STATISTICS'), $start, 'start', $max, 'max', $i, $fields_title, $fields, $sortables, $sortable, $sort_order, 'sort', new Tempcode());

        $output = create_scatter_graph($data, do_lang('DATE'), do_lang('USERS_ONLINE'), '', '');
        $this->save_graph('Global-Users-online', $output);

        $graph = do_template('STATS_GRAPH', array('_GUID' => '9688722e526a814f3b90ca93a21333ad', 'GRAPH' => $this->get_stats_url('Global-Users-online'), 'TITLE' => do_lang_tempcode('USERS_ONLINE_STATISTICS'), 'TEXT' => do_lang_tempcode('DESCRIPTION_USERS_ONLINE_STATISTICS')));

        $tpl = do_template('STATS_SCREEN', array('_GUID' => '2e5a6a2f7317c80464c518996728d839', 'TITLE' => $this->title, 'GRAPH' => $graph, 'STATS' => $list));

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl);
    }

    /**
     * The UI to show submission rates.
     *
     * @return Tempcode The UI
     */
    public function submission_rates()
    {
        // Like the users online above, we need to use a nice scatter graph
        $start = get_param_integer('start', 0);
        $max = get_param_integer('max', 50); // Intentionally the browse is disabled, as the graph will show all - we fudge $max_rows to $i
        $csv = get_param_integer('csv', 0) == 1;
        if ($csv) {
            if (php_function_allowed('set_time_limit')) {
                @set_time_limit(0);
            }
            $start = 0;
            $max = 10000;
        }
        $sortables = array('date_and_time' => do_lang_tempcode('DATE_TIME'));
        $test = explode(' ', either_param_string('sort', 'date_and_time DESC'));
        if (count($test) == 1) {
            $test[1] = 'DESC';
        }
        list($sortable, $sort_order) = $test;
        if (((strtoupper($sort_order) != 'ASC') && (strtoupper($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
            log_hack_attack_and_exit('ORDERBY_HACK');
        }

        $rows = $GLOBALS['SITE_DB']->query_select('actionlogs', array('date_and_time', 'COUNT(*) AS cnt'), null, 'GROUP BY date_and_time ORDER BY ' . $sortable . ' ' . $sort_order, 3000/*reasonable limit*/);
        if (count($rows) < 1) {
            return warn_screen($this->title, do_lang_tempcode('NO_DATA'));
        }
        //$max_rows = $GLOBALS['SITE_DB']->query_select_value('actionlogs', 'COUNT(DISTINCT date_and_time)');   Cannot do this as the DB does not do all the processing

        $data = array();
        $base = $rows[0]['date_and_time'];
        foreach ($rows as $value) {
            $date = get_timezoned_date($value['date_and_time'], false);
            $t = $value['date_and_time'] - $base;
            if ($t < 0) {
                $t = 0 - $t;
            }
            if ((count($data) == 0) || ($data[count($data) - 1]['key'] != $date)) {
                $data[] = array('t' => $t, 'key' => $date, 'value' => $value['cnt']);
            } else {
                $data[count($data) - 1]['value'] += $value['cnt'];
            }
        }

        require_code('templates_results_table');
        $fields_title = results_field_title(array(do_lang_tempcode('DATE_TIME'), do_lang_tempcode('PEAK')), $sortables, 'sort', $sortable . ' ' . $sort_order);
        $fields = new Tempcode();
        $real_data = array();
        for ($i = 0; $i < $max; $i++) {
            if (!array_key_exists($i, $data)) {
                continue;
            }

            $real_data[] = array(
                do_lang('DATE_TIME') => $data[$i]['key'],
                do_lang('COUNT_TOTAL') => $data[$i]['value'],
            );

            $fields->attach(results_entry(array($data[$i]['key'], integer_format($data[$i]['value'])), true));
        }
        $list = results_table(do_lang_tempcode('SUBMISSION_STATISTICS'), $start, 'start', $max, 'max', $i, $fields_title, $fields, $sortables, $sortable, $sort_order, 'sort', new Tempcode());
        if ($csv) {
            make_csv($real_data, 'submission_rates.csv');
        }

        $output = create_scatter_graph($data, do_lang('DATE'), do_lang('SUBMISSION_STATISTICS'), '', '');
        $this->save_graph('Global-Submissions', $output);

        $graph = do_template('STATS_GRAPH', array('_GUID' => 'f6d5a58eae148a555e0f868eda245304', 'GRAPH' => $this->get_stats_url('Global-Submissions'), 'TITLE' => do_lang_tempcode('SUBMISSION_STATISTICS'), 'TEXT' => do_lang_tempcode('DESCRIPTION_SUBMISSION_STATISTICS')));

        $tpl = do_template('STATS_SCREEN', array('_GUID' => '66e8534ef342c1d0197f4ddb8f767025', 'TITLE' => $this->title, 'GRAPH' => $graph, 'STATS' => $list));

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl);
    }

    /**
     * The UI to show page load times.
     *
     * @return Tempcode The UI
     */
    public function load_times()
    {
        // Handle time range
        if (get_param_integer('dated', 0) == 0) {
            return $this->get_between($this->title, true);
        }
        $time_start = post_param_date('time_start', true);
        $time_end = post_param_date('time_end', true);
        if (!is_null($time_end)) {
            $time_end += 60 * 60 * 24 - 1; // So it is end of day not start
        }
        if (is_null($time_start)) {
            $time_start = 0;
        }
        if (is_null($time_end)) {
            $time_end = time();
        }
        $first_stat = $GLOBALS['SITE_DB']->query_select_value_if_there('stats', 'MIN(date_and_time)');
        if ($time_end < $first_stat) {
            warn_exit(do_lang_tempcode('NO_DATA_SPECIFIC'));
        }

        $start = get_param_integer('start', 0);
        $max = get_param_integer('max', 30);
        $csv = get_param_integer('csv', 0) == 1;
        if ($csv) {
            if (php_function_allowed('set_time_limit')) {
                @set_time_limit(0);
            }
            $start = 0;
            $max = 10000;
            /*$time_start = 0;     Actually, this is annoying. We have legitimate reason to filter, and cannot re-filter the data in Excel retro-actively
            $time_end = time();*/
        }

        $this->title = get_screen_title('LOAD_TIMES_RANGE', true, array(escape_html(get_timezoned_date($time_start, false)), escape_html(get_timezoned_date($time_end, false))));

        // We calculate MIN not AVG, because data can be made very dirty by slow clients or if the server is having trouble at one specific point. It's a shame.
        $rows = $GLOBALS['SITE_DB']->query('SELECT the_page,MIN(milliseconds) AS avg FROM ' . get_table_prefix() . 'stats WHERE date_and_time>' . strval($time_start) . ' AND date_and_time<' . strval($time_end) . ' GROUP BY the_page');
        if (count($rows) < 1) {
            return warn_screen($this->title, do_lang_tempcode('NO_DATA'));
        }

        $data = array();
        foreach ($rows as $row) {
            $page = $row['the_page'];
            $page2 = page_path_to_page_link($page);
            if ($page2 == '') {
                $page2 = $page;
            }

            $avg = $row['avg'] / 1000.0;

            $data[$page2] = array($avg, $page);
        }

        $sortables = array('AVG(milliseconds)' => do_lang_tempcode('LOAD_TIME'));
        $test = explode(' ', get_param_string('sort', 'AVG(milliseconds) DESC'), 2);
        if (count($test) == 1) {
            $test[1] = 'DESC';
        }
        list($sortable, $sort_order) = $test;
        if (((strtoupper($sort_order) != 'ASC') && (strtoupper($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
            log_hack_attack_and_exit('ORDERBY_HACK');
        }

        sort_maps_by($data, 0);
        if ($sort_order == 'DESC') {
            $data = array_reverse($data, true);
        }

        require_code('templates_results_table');
        $fields_title = results_field_title(array(do_lang_tempcode('URL'), do_lang_tempcode('LOAD_TIME')), $sortables, 'sort', $sortable . ' ' . $sort_order);
        $fields = new Tempcode();
        $real_data = array();
        $i = 0;
        foreach ($data as $url => $_value) {
            if ($i < $start) {
                $i++;
                continue;
            } elseif ($i >= $start + $max) {
                break;
            }
            list($value, $page) = $_value;

            $real_data[] = array(
                'URL' => $url,
                'Milliseconds' => $value,
            );

            $fields->attach(results_entry(array(hyperlink(build_url(array('page' => '_SELF', 'type' => '_page', 'iscreen' => $page), '_SELF'), $url, false, true), float_format($value), false, true), true));

            $i++;
        }
        $list = results_table(do_lang_tempcode('LOAD_TIMES'), $start, 'start', $max, 'max', count($data), $fields_title, $fields, $sortables, $sortable, $sort_order, 'sort', new Tempcode());
        if ($csv) {
            make_csv($real_data, 'load_times.csv');
        }

        $output = create_bar_chart($data, do_lang('PAGE'), do_lang('LOAD_TIME'), '', do_lang('dates:DPLU_SECONDS'));
        $this->save_graph('Global-Load-times', $output);

        $graph = do_template('STATS_GRAPH', array('_GUID' => '3f1ef4ebbed1e064c0ec89481dc39afc', 'GRAPH' => $this->get_stats_url('Global-Load-times'), 'TITLE' => do_lang_tempcode('LOAD_TIMES'), 'TEXT' => do_lang_tempcode('DESCRIPTION_LOAD_TIMES')));

        $tpl = do_template('STATS_SCREEN', array('_GUID' => '8f7c585bdbc0180ed116693723108e2b', 'TITLE' => $this->title, 'GRAPH' => $graph, 'STATS' => $list));

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl);
    }

    /**
     * The UI to show referrers.
     *
     * @return Tempcode The UI
     */
    public function referrers()
    {
        // Handle time range
        if (get_param_integer('dated', 0) == 0) {
            return $this->get_between($this->title, true);
        }
        $time_start = post_param_date('time_start', true);
        $time_end = post_param_date('time_end', true);
        if (!is_null($time_end)) {
            $time_end += 60 * 60 * 24 - 1; // So it is end of day not start
        }
        if (is_null($time_start)) {
            $time_start = 0;
        }
        if (is_null($time_end)) {
            $time_end = time();
        }
        $first_stat = $GLOBALS['SITE_DB']->query_select_value_if_there('stats', 'MIN(date_and_time)');
        if ($time_end < $first_stat) {
            warn_exit(do_lang_tempcode('NO_DATA_SPECIFIC'));
        }

        $start = get_param_integer('start', 0);
        $max = get_param_integer('max', 25);
        $csv = get_param_integer('csv', 0) == 1;
        if ($csv) {
            if (php_function_allowed('set_time_limit')) {
                @set_time_limit(0);
            }
            $start = 0;
            $max = 10000;
            /*$time_start = 0;     Actually, this is annoying. We have legitimate reason to filter, and cannot re-filter the data in Excel retro-actively
            $time_end = time();*/
        }

        $this->title = get_screen_title('TOP_REFERRERS_RANGE', true, array(escape_html(get_timezoned_date($time_start, false)), escape_html(get_timezoned_date($time_end, false))));

        $non_local_filter = 'referer NOT LIKE \'' . db_encode_like(str_replace('_', '\\_', preg_replace('#^https?://#', 'http://', get_base_url())) . '%') . '\'';
        $non_local_filter .= ' AND referer NOT LIKE \'' . db_encode_like(str_replace('_', '\\_', preg_replace('#^https?://#', 'https://', get_base_url())) . '%') . '\'';
        if (get_param_integer('debug', 0) == 1) {
            $non_local_filter = '1=1';
        }

        $where = $non_local_filter . ' AND date_and_time>' . strval($time_start) . ' AND date_and_time<' . strval($time_end);

        $rows = $GLOBALS['SITE_DB']->query('SELECT COUNT(*) AS cnt,referer FROM ' . get_table_prefix() . 'stats WHERE ' . $where . ' GROUP BY referer');
        if (count($rows) < 1) {
            return warn_screen($this->title, do_lang_tempcode('NO_DATA'));
        }

        $referrers = array();
        $total = 0;
        foreach ($rows as $value) {
            $referrers[$value['referer']] = $value['cnt'];
            $total += $referrers[$value['referer']];
        }

        $sortables = array('referer' => do_lang_tempcode('TOP_REFERRERS'));
        $test = explode(' ', get_param_string('sort', 'referer DESC'), 2);
        if (count($test) == 1) {
            $test[1] = 'DESC';
        }
        list($sortable, $sort_order) = $test;
        if (((strtoupper($sort_order) != 'ASC') && (strtoupper($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
            log_hack_attack_and_exit('ORDERBY_HACK');
        }

        if ($sort_order == 'ASC') {
            asort($referrers);
        } else {
            arsort($referrers);
        }

        require_code('templates_results_table');
        $fields_title = results_field_title(array(do_lang_tempcode('URL'), do_lang_tempcode('COUNT_VIEWS')), $sortables, 'sort', $sortable . ' ' . $sort_order);
        $fields = new Tempcode();
        $i = 0;
        $degrees = 360.0 / $total;
        $done_total = 0;
        $data = array();

        $real_data = array();
        foreach ($referrers as $referrer => $views) {
            if ($i < $start) {
                $i++;
                continue;
            } elseif ($i >= $start + $max) {
                break;
            }
            if ($referrer == '') {
                $link = do_lang('_UNKNOWN');
            } else {
                $link = hyperlink($referrer, $referrer, false, true);
            }
            $fields->attach(results_entry(array($link, integer_format($views)), true));

            $real_data[] = array(
                do_lang('DATE_TIME') => $referrer,
                do_lang('COUNT_TOTAL') => $views,
            );

            $data[$referrer] = $views * $degrees;
            $done_total += $data[$referrer];
            $i++;
        }
        if ($csv) {
            make_csv($real_data, 'referrers.csv');
        }

        if ((360 - $done_total) > 0) {
            $data[do_lang('OTHER')] = 360 - $done_total;
            $fields->attach(results_entry(array(do_lang('OTHER'), float_format((360 - $done_total) / $degrees)), true));
        }

        $list = results_table(do_lang_tempcode('TOP_REFERRERS'), $start, 'start', $max, 'max', count($referrers), $fields_title, $fields, $sortables, $sortable, $sort_order, 'sort', new Tempcode());

        $output = create_pie_chart($data);
        $this->save_graph('Global-Referrers', $output);

        $graph = do_template('STATS_GRAPH', array('_GUID' => '22c565665d8a98528659bbfc25526855', 'GRAPH' => $this->get_stats_url('Global-Referrers'), 'TITLE' => do_lang_tempcode('REFERRER_SHARE'), 'TEXT' => do_lang_tempcode('DESCRIPTION_REFERRER_SHARE')));

        $tpl = do_template('STATS_SCREEN', array('_GUID' => '777bc5f8573a5cef54aa0bc9bdc0ee29', 'TITLE' => $this->title, 'GRAPH' => $graph, 'STATS' => $list));

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl);
    }

    /**
     * The UI to show top search keywords.
     *
     * @return Tempcode The UI
     */
    public function keywords()
    {
        // Handle time range
        if (get_param_integer('dated', 0) == 0) {
            return $this->get_between($this->title, true);
        }
        $time_start = post_param_date('time_start', true);
        $time_end = post_param_date('time_end', true);
        if (!is_null($time_end)) {
            $time_end += 60 * 60 * 24 - 1; // So it is end of day not start
        }
        if (is_null($time_start)) {
            $time_start = 0;
        }
        if (is_null($time_end)) {
            $time_end = time();
        }
        $first_stat = $GLOBALS['SITE_DB']->query_select_value_if_there('stats', 'MIN(date_and_time)');
        if ($time_end < $first_stat) {
            warn_exit(do_lang_tempcode('NO_DATA_SPECIFIC'));
        }

        $start = get_param_integer('start', 0);
        $max = get_param_integer('max', 25);
        $csv = get_param_integer('csv', 0) == 1;
        if ($csv) {
            if (php_function_allowed('set_time_limit')) {
                @set_time_limit(0);
            }
            $start = 0;
            $max = 10000;
            /*$time_start = 0;     Actually, this is annoying. We have legitimate reason to filter, and cannot re-filter the data in Excel retro-actively
            $time_end = time();*/
        }

        $this->title = get_screen_title('TOP_SEARCH_KEYWORDS_RANGE', true, array(escape_html(get_timezoned_date($time_start, false)), escape_html(get_timezoned_date($time_end, false))));

        $sortables = array('referer' => do_lang_tempcode('TOP_SEARCH_KEYWORDS'));
        $test = explode(' ', get_param_string('sort', 'referer DESC'), 2);
        if (count($test) == 1) {
            $test[1] = 'DESC';
        }
        list($sortable, $sort_order) = $test;
        if (((strtoupper($sort_order) != 'ASC') && (strtoupper($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
            log_hack_attack_and_exit('ORDERBY_HACK');
        }

        $rows = $GLOBALS['SITE_DB']->query('SELECT referer FROM ' . get_table_prefix() . 'stats WHERE referer LIKE \'' . db_encode_like('http://www.google.%q=%') . '\' AND date_and_time>' . strval($time_start) . ' AND date_and_time<' . strval($time_end) . ' ORDER BY ' . $sortable . ' ' . $sort_order);
        if (count($rows) < 1) {
            return warn_screen($this->title, do_lang_tempcode('NO_DATA'));
        }

        $keywords = array();
        $total = 0;
        foreach ($rows as $value) {
            $matches = array();
            preg_match('#(&|\?)q=([^&]*)#', $value['referer'], $matches);
            if (!array_key_exists(1, $matches)) {
                continue;
            }
            $_keywords = explode('+', rawurldecode($matches[2]));
            foreach ($_keywords as $keyword) {
                $keyword = str_replace('"', '', $keyword);
                if (trim($keyword) == '') {
                    continue;
                }
                if (substr($keyword, 0, strlen('cache:')) == 'cache:') {
                    continue;
                }

                if (!array_key_exists($keyword, $keywords)) {
                    $keywords[$keyword] = 1;
                } else {
                    $keywords[$keyword]++;
                }
                $total++;
            }
        }

        if ($sort_order == 'ASC') {
            asort($keywords);
        } else {
            arsort($keywords);
        }

        require_code('templates_results_table');
        $fields_title = results_field_title(array(do_lang_tempcode('KEYWORD'), do_lang_tempcode('COUNT_VIEWS')), $sortables, 'sort', $sortable . ' ' . $sort_order);
        $fields = new Tempcode();
        $degrees = ($total == 0) ? 360.0 : (360 / $total);
        $done_total = 0;
        $data = array();
        $i = 0;

        $real_data = array();
        foreach ($keywords as $keyword => $views) {
            if ($i < $start) {
                $i++;
                continue;
            } elseif ($i >= $start + $max) {
                break;
            }
            if ($keyword == '') {
                $link = do_lang('_UNKNOWN');
            } else {
                $link = escape_html($keyword);
            }
            $fields->attach(results_entry(array($link, integer_format($views)), true));

            $real_data[] = array(
                do_lang('KEYWORD') => $keyword,
                do_lang('COUNT_TOTAL') => $views,
            );

            $data[$keyword] = $keywords[$keyword] * $degrees;
            $done_total += $data[$keyword];
            $i++;
        }
        if ((360 - $done_total) > 0) {
            $data[do_lang('OTHER')] = 360 - $done_total;
            $fields->attach(results_entry(array(do_lang('OTHER'), float_format((360 - $done_total) / $degrees)), true));
        }
        $list = results_table(do_lang_tempcode('TOP_SEARCH_KEYWORDS'), $start, 'start', $max, 'max', count($keywords), $fields_title, $fields, $sortables, $sortable, $sort_order, 'sort', new Tempcode());
        if ($csv) {
            make_csv($real_data, 'search_keywords.csv');
        }

        $output = create_pie_chart($data);
        $this->save_graph('Global-Keywords', $output);

        $graph = do_template('STATS_GRAPH', array('_GUID' => 'a199b095199a0e337d38c78564909e52', 'GRAPH' => $this->get_stats_url('Global-Keywords'), 'TITLE' => do_lang_tempcode('KEYWORDS_SHARE'), 'TEXT' => do_lang_tempcode('DESCRIPTION_KEYWORDS_SHARE')));

        $tpl = do_template('STATS_SCREEN', array('_GUID' => 'ab791072361184a05c7e60a3127ee439', 'TITLE' => $this->title, 'GRAPH' => $graph, 'STATS' => $list));

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl);
    }

    /**
     * The UI to show page view statistics.
     *
     * @return Tempcode The UI
     */
    public function page_stats()
    {
        // This will show a plain bar chart with all the pages listed

        // Handle time range
        if (get_param_integer('dated', 0) == 0) {
            return $this->get_between($this->title, true);
        }
        $time_start = post_param_date('time_start', true);
        $time_end = post_param_date('time_end', true);
        if (!is_null($time_end)) {
            $time_end += 60 * 60 * 24 - 1; // So it is end of day not start
        }
        if (is_null($time_start)) {
            $time_start = 0;
        }
        if (is_null($time_end)) {
            $time_end = time();
        }
        $first_stat = $GLOBALS['SITE_DB']->query_select_value_if_there('stats', 'MIN(date_and_time)');
        if ($time_end < $first_stat) {
            warn_exit(do_lang_tempcode('NO_DATA_SPECIFIC'));
        }

        $start = get_param_integer('start', 0);
        $max = get_param_integer('max', 30);
        $csv = get_param_integer('csv', 0) == 1;
        if ($csv) {
            if (php_function_allowed('set_time_limit')) {
                @set_time_limit(0);
            }
            $start = 0;
            $max = 10000;
            /*$time_start = 0;     Actually, this is annoying. We have legitimate reason to filter, and cannot re-filter the data in Excel retro-actively
            $time_end = time();*/
        }

        $this->title = get_screen_title('PAGES_STATISTICS_RANGE', true, array(escape_html(get_timezoned_date($time_start, false)), escape_html(get_timezoned_date($time_end, false))));

        $rows = $GLOBALS['SITE_DB']->query_select('stats', array('the_page'), null, 'GROUP BY the_page ORDER BY COUNT(*) DESC', 3000);
        if (count($rows) < 1) {
            return warn_screen($this->title, do_lang_tempcode('NO_DATA'));
        }

        $views = array(do_lang('_ALL') => 0);
        $total = 0;
        foreach ($rows as $row) {
            $page = $row['the_page'];
            $matches = array();
            if ((preg_match('#^/?([^/]+)/pages/([^/]+)/(\w\w/)?([^/\.]+)\.(php|txt|htm)$#', $page, $matches) == 1) && ($matches[4] == 'catalogues') && (addon_installed('catalogues')) && ($GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'COUNT(*)', null, '', true) < 300)) {
                require_lang('catalogues');
                $categories = $GLOBALS['SITE_DB']->query_select('catalogue_categories', array('id', 'cc_title'), null, '', null, null, true);
                foreach ($categories as $cat) {
                    $where = db_string_equal_to('the_page', $page);
                    if (substr($page, 0, 6) == 'pages/') {
                        $where .= ' OR ' . db_string_equal_to('the_page', '/' . $page); // Legacy compatibility
                    }
                    $count = $GLOBALS['SITE_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'stats WHERE (' . $where . ') AND s_get LIKE \'' . db_encode_like('<param>page=catalogues</param>\n<param>type=category</param>\n<param>id=' . strval($cat['id']) . '</param>%') . '\' AND date_and_time>' . strval($time_start) . ' AND date_and_time<' . strval($time_end));
                    $cc_cat_written = do_lang('CATALOGUE_CATEGORY') . ': ' . get_translated_text($cat['cc_title']);
                    $views[$cc_cat_written] = array($count, $page);
                    $total += $count;
                }
                continue;
            } else {
                $page2 = page_path_to_page_link($page);
                if ($page2 == '') {
                    $page2 = $page;
                }
            }
            $where = db_string_equal_to('the_page', $page);
            if (substr($page, 0, 6) == 'pages/') {
                $where .= ' OR ' . db_string_equal_to('the_page', '/' . $page); // Legacy compatibility
            }
            $views[$page2] = array($GLOBALS['SITE_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'stats WHERE (' . $where . ') AND date_and_time>' . strval($time_start) . ' AND date_and_time<' . strval($time_end)), $page);
            $total += $views[$page2][0];
        }
        $views[do_lang('_ALL')] = array($total, null);

        $sortables = array('views' => do_lang_tempcode('COUNT_VIEWS'));
        $test = explode(' ', get_param_string('sort', 'views DESC'), 2);
        if (count($test) == 1) {
            $test[1] = 'DESC';
        }
        list($sortable, $sort_order) = $test;
        if (((strtoupper($sort_order) != 'ASC') && (strtoupper($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
            log_hack_attack_and_exit('ORDERBY_HACK');
        }

        sort_maps_by($views, 0);
        if ($sort_order == 'DESC') {
            $views = array_reverse($views, true);
        }

        require_code('templates_results_table');
        $fields_title = results_field_title(array(do_lang_tempcode('URL'), do_lang_tempcode('COUNT_VIEWS')), $sortables, 'sort', $sortable . ' ' . $sort_order);
        $fields = new Tempcode();
        $i = 0;
        $real_data = array();
        foreach ($views as $url => $_value) {
            if ($i < $start) {
                $i++;
                continue;
            } elseif ($i >= $start + $max) {
                break;
            }
            list($value, $page) = $_value;

            $real_data[] = array(
                do_lang('PAGE_OR_URL') => is_null($page) ? $url : $page,
                do_lang('COUNT_TOTAL') => $value,
            );

            $fields->attach(results_entry(array(is_null($page) ? make_string_tempcode(escape_html($url)) : hyperlink(build_url(array('page' => '_SELF', 'type' => '_page', 'iscreen' => $page), '_SELF'), $url, false, true), integer_format($value)), false));

            $i++;
        }
        unset($views['(' . do_lang('ALL') . ')']);
        $list = results_table(do_lang_tempcode('PAGES_STATISTICS'), $start, 'start', $max, 'max', count($views), $fields_title, $fields, $sortables, $sortable, $sort_order, 'sort', new Tempcode());
        if ($csv) {
            make_csv($real_data, 'page_stats.csv');
        }

        $output = create_bar_chart(array_slice($views, $start, $max), do_lang('PAGE'), do_lang('COUNT_VIEWS'), '', '');
        $this->save_graph('Global-Views', $output);

        $graph = do_template('STATS_GRAPH', array('_GUID' => 'ea79fdc013046ef94992daeab961f2da', 'GRAPH' => $this->get_stats_url('Global-Views'), 'TITLE' => do_lang_tempcode('PAGES_STATISTICS'), 'TEXT' => do_lang_tempcode('DESCRIPTION_PAGES_STATISTICS')));

        $tpl = do_template('STATS_SCREEN', array('_GUID' => 'cfe7d5aee8aa3c0d3a54bd3bf2d09e7f', 'TITLE' => $this->title, 'GRAPH' => $graph, 'STATS' => $list));

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl);
    }

    /**
     * The UI to show page view statistics for the front page.
     *
     * @return Tempcode The UI
     */
    public function overview()
    {
        $page_request = _request_page(get_zone_default_page(''), '');
        $page = $page_request[count($page_request) - 1];
        if (is_array($page)) {
            $page = $page['r_to_page'];
        }

        list($graph_views_monthly, $list_views_monthly) = array_values($this->views_per_x($page, 'views_hourly', 'VIEWS_PER_MONTH', 'DESCRIPTION_VIEWS_PER_MONTH', 730, 8766));

        //************************************************************************************************
        // Views
        //************************************************************************************************
        $start = get_param_integer('start_views', 0);
        $max = get_param_integer('max_views', 30);
        $sortables = array('date_and_time' => do_lang_tempcode('DATE_TIME'));
        list($sortable, $sort_order) = explode(' ', get_param_string('sort_views', 'date_and_time DESC'));
        if (((strtoupper($sort_order) != 'ASC') && (strtoupper($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
            log_hack_attack_and_exit('ORDERBY_HACK');
        }

        // NB: not used in default templates
        $where = db_string_equal_to('the_page', $page);
        if (substr($page, 0, 6) == 'pages/') {
            $where .= ' OR ' . db_string_equal_to('the_page', '/' . $page); // Legacy compatibility
        }
        $reasonable_max = 10000;
        $rows = $GLOBALS['SITE_DB']->query('SELECT date_and_time FROM ' . get_table_prefix() . 'stats WHERE (' . $where . ') ORDER BY ' . $sortable . ' ' . $sort_order, $reasonable_max, null, false, true);
        if (count($rows) < 1) {
            $list_views = new Tempcode();
        } else {
            require_code('templates_results_table');
            $fields_title = results_field_title(array(do_lang_tempcode('DATE_TIME'), do_lang_tempcode('COUNT_VIEWS')), $sortables, 'sort', $sortable . ' ' . $sort_order);
            $fields = new Tempcode();
            $i = 0;
            while (array_key_exists($i, $rows)) {
                $row = $rows[$i];
                $week = intval(round($row['date_and_time'] / (60 * 60 * 24 * 7)));
                $date = get_timezoned_date(($week - 1) * (60 * 60 * 24 * 7), false, false, false, true) . ' - ' . get_timezoned_date(($week) * (60 * 60 * 24 * 7), false, false, false, true);
                $views = 0;
                while (array_key_exists($i + $views, $rows)) {
                    $_week = intval(round($row['date_and_time'] / (60 * 60 * 24 * 7)));

                    if ($_week != $week) {
                        break;
                    }

                    $views++;
                }
                $i += $views;
                if ($i < $start) {
                    continue;
                }
                $fields->attach(results_entry(array($date, ($i < $reasonable_max) ? integer_format($views) : ('>' . integer_format($views))), true));
                if ($i >= $start + $max) {
                    break;
                }
            }
            $list_views = results_table(do_lang_tempcode('PAGES_STATISTICS', escape_html($page)), $start, 'start_views', $max, 'max_views', $i, $fields_title, $fields, $sortables, $sortable, $sort_order, 'sort_views');
        }

        $tpl = do_template('STATS_OVERVIEW_SCREEN', array('_GUID' => '71be91ba0d83368e1e1ceaf39e506610', 'TITLE' => $this->title, 'STATS_VIEWS' => $list_views, 'GRAPH_VIEWS_MONTHLY' => $graph_views_monthly, 'STATS_VIEWS_MONTHLY' => $list_views_monthly,));

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl);
    }

    /**
     * The UI to show page view statistics for a single page.
     *
     * @return Tempcode The UI
     */
    public function show_page()
    {
        $page = get_param_string('iscreen');

        require_code('locations');

        //************************************************************************************************
        // Views per hour/day/week/month
        //************************************************************************************************
        list($graph_views_hourly, $list_views_hourly) = array_values($this->views_per_x($page, 'views_hourly', 'VIEWS_PER_HOUR', 'DESCRIPTION_VIEWS_PER_HOUR', 1));
        list($graph_views_daily, $list_views_daily) = array_values($this->views_per_x($page, 'views_hourly', 'VIEWS_PER_DAY', 'DESCRIPTION_VIEWS_PER_DAY', 24, 168));
        list($graph_views_weekly, $list_views_weekly) = array_values($this->views_per_x($page, 'views_hourly', 'VIEWS_PER_WEEK', 'DESCRIPTION_VIEWS_PER_WEEK', 168, 730));
        list($graph_views_monthly, $list_views_monthly) = array_values($this->views_per_x($page, 'views_hourly', 'VIEWS_PER_MONTH', 'DESCRIPTION_VIEWS_PER_MONTH', 730, 8766));

        //************************************************************************************************
        // Page browser/referrer/operating/IP system share
        //************************************************************************************************
        list($graph_share_browser, $list_share_browser) = array_values($this->page_x_share($page, 'browser', 'BROWSER_SHARE', 'DESCRIPTION_BROWSER_SHARE', 'USER_AGENT'));
        list($graph_share_referrer, $list_share_referrer) = array_values($this->page_x_share($page, 'referer', 'REFERRER_SHARE', 'DESCRIPTION_REFERRER_SHARE', 'REFERER'));
        list($graph_share_os, $list_share_os) = array_values($this->page_x_share($page, 'operating_system', 'OS_SHARE', 'DESCRIPTION_OS_SHARE', 'USER_OS'));
        list($graph_ip, $list_ip) = array_values($this->page_x_share($page, 'ip', 'IP_ADDRESS_DISTRIBUTION', 'DESCRIPTION_IP_ADDRESS_DISTRIBUTION', 'IP_ADDRESS'));

        //************************************************************************************************
        // Keywords
        //************************************************************************************************
        $start = get_param_integer('start_keywords', 0);
        $max = get_param_integer('max_keywords', 30);
        $sortables = array('referer' => do_lang_tempcode('TOP_SEARCH_KEYWORDS'));
        $test = explode(' ', get_param_string('sort_keywords', 'referer DESC'));
        if (count($test) == 1) {
            $test[1] = 'DESC';
        }
        list($sortable, $sort_order) = $test;
        if (((strtoupper($sort_order) != 'ASC') && (strtoupper($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
            log_hack_attack_and_exit('ORDERBY_HACK');
        }

        $where = db_string_equal_to('the_page', $page);
        if (substr($page, 0, 6) == 'pages/') {
            $where .= ' OR ' . db_string_equal_to('the_page', '/' . $page); // Legacy compatibility
        }
        $ip_filter = $GLOBALS['DEV_MODE'] ? '' : (' AND ' . db_string_not_equal_to('ip', get_ip_address()));
        $rows = $GLOBALS['SITE_DB']->query('SELECT id,referer FROM ' . get_table_prefix() . 'stats WHERE (' . $where . ')' . $ip_filter . ' AND referer LIKE \'' . db_encode_like('http://www.google.%q=%') . '\' ORDER BY ' . $sortable . ' ' . $sort_order, 2000/*reasonable limit*/);
        if (count($rows) < 1) {
            $list_keywords = new Tempcode();
            $graph_keywords = new Tempcode();
        } else {
            $keywords = array();
            $num_keywords = 0;
            foreach ($rows as $value) {
                $matches = array();
                preg_match('#(&|\?)q=([^&]*)#', $value['referer'], $matches);
                if (!array_key_exists(2, $matches)) {
                    continue;
                }
                $_keywords = explode('+', rawurldecode($matches[2]));
                foreach ($_keywords as $keyword) {
                    $keyword = str_replace('"', '', $keyword);
                    if (trim($keyword) == '') {
                        continue;
                    }
                    if (substr($keyword, 0, strlen('cache:')) == 'cache:') {
                        continue;
                    }

                    $num_keywords++;

                    if (!array_key_exists($keyword, $keywords)) {
                        $keywords[$keyword] = 1;
                    } else {
                        $keywords[$keyword]++;
                    }
                }
            }
            if ($num_keywords == 0) {
                $list_keywords = new Tempcode();
                $graph_keywords = new Tempcode();
            } else {
                $degrees = 360 / $num_keywords;

                require_code('templates_results_table');
                $fields_title = results_field_title(array(do_lang_tempcode('KEYWORD'), do_lang_tempcode('PEAK')), $sortables, 'sort', $sortable . ' ' . $sort_order);
                $fields = new Tempcode();
                $done_total = 0;
                $data = array();
                $i = 0;

                foreach ($keywords as $key => $value) {
                    if ($i < $start) {
                        $i++;
                        continue;
                    } elseif ($i >= $start + $max) {
                        break;
                    }
                    if ($key == '') {
                        $link = do_lang('_UNKNOWN');
                    } else {
                        $link = escape_html($key);
                    }
                    $fields->attach(results_entry(array($link, integer_format($value)), true));

                    $data[$key] = $degrees * $value;
                    $done_total += $data[$key];
                    $i++;
                }
                if ((360 - $done_total) > 0) {
                    $data[do_lang('OTHER')] = 360 - $done_total;
                    $fields->attach(results_entry(array(do_lang('OTHER'), integer_format((int)((360 - $done_total) / $degrees))), true));
                }
                $list_keywords = results_table(do_lang_tempcode('TOP_SEARCH_KEYWORDS'), $start, 'start_keywords', $max, 'max_keywords', $i, $fields_title, $fields, $sortables, $sortable, $sort_order, 'sort_keywords');

                $output = create_pie_chart($data);
                $this->save_graph(strval($rows[0]['id']) . '-Keywords', $output);

                $graph_keywords = do_template('STATS_GRAPH', array('_GUID' => '6e3a48274f2e3babf546292b8eec2f9b', 'GRAPH' => $this->get_stats_url(strval($rows[0]['id']) . '-Keywords'), 'TITLE' => do_lang_tempcode('KEYWORDS_SHARE'), 'TEXT' => do_lang_tempcode('DESCRIPTION_KEYWORDS_SHARE')));
            }
        }

        //************************************************************************************************
        // Regionalities
        //************************************************************************************************
        $regionalities_test = $GLOBALS['SITE_DB']->query_select_value_if_there('ip_country', 'id');
        if ($regionalities_test !== null) {
            $start = get_param_integer('start_regionalities', 0);
            $max = get_param_integer('max_regionalities', 15);
            $sortables = array('ip' => do_lang_tempcode('REGIONALITY'));
            list($sortable, $sort_order) = explode(' ', get_param_string('sort_regionalities', 'ip DESC'));
            if (((strtoupper($sort_order) != 'ASC') && (strtoupper($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
                log_hack_attack_and_exit('ORDERBY_HACK');
            }

            $where = db_string_equal_to('the_page', $page);
            if (substr($page, 0, 6) == 'pages/') {
                $where .= ' OR ' . db_string_equal_to('the_page', '/' . $page); // Legacy compatibility
            }
            $ip_filter = $GLOBALS['DEV_MODE'] ? '' : (' AND ' . db_string_not_equal_to('ip', get_ip_address()));
            $rows = $GLOBALS['SITE_DB']->query('SELECT DISTINCT ip,' . $sortable . ' FROM ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'stats WHERE (' . $where . ')' . $ip_filter . ' ORDER BY ' . $sortable . ' ' . $sort_order, 1000/*reasonable limit*/);
            shuffle($rows);
            if (count($rows) < 1) {
                $list_regionality = new Tempcode();
                $graph_regionality = new Tempcode();
            } else {
                $regions = array();
                $data = array();
                $degrees = 360 / count($rows);
                foreach ($rows as $value) {
                    $region = geolocate_ip($value['ip']);
                    if ((is_null($region)) || ($region == '')) {
                        $region = do_lang('_UNKNOWN');
                    }

                    if (!array_key_exists($region, $regions)) {
                        $regions[$region] = 1;
                    } else {
                        $regions[$region]++;
                    }
                }

                require_code('templates_results_table');
                $fields_title = results_field_title(array(do_lang_tempcode('REGIONALITY'), do_lang_tempcode('COUNT_VIEWS')), $sortables, 'sort', $sortable . ' ' . $sort_order);
                $fields = new Tempcode();
                $done_total = 0;
                $i = 0;
                foreach ($regions as $key => $value) {
                    if ($i < $start) {
                        $i++;
                        continue;
                    } elseif ($i >= $start + $max) {
                        break;
                    }
                    if ($key == '') {
                        $link = do_lang('_UNKNOWN');
                    } else {
                        $link = escape_html($key);
                    }
                    $fields->attach(results_entry(array($link, integer_format($value)), true));

                    $data[$key] = $degrees * $value;
                    $done_total += $data[$key];
                    $i++;
                }
                if ((360 - $done_total) > 0) {
                    $data[do_lang('OTHER')] = 360 - $done_total;
                    $fields->attach(results_entry(array(do_lang('OTHER'), integer_format((int)((360 - $done_total) / $degrees))), true));
                }
                $list_regionality = results_table(do_lang_tempcode('PAGES_STATISTICS', escape_html($page)), $start, 'start_views', $max, 'max_views', $i, $fields_title, $fields, $sortables, $sortable, $sort_order, 'sort_views');

                $output = create_pie_chart($data);
                $this->save_graph('Regionality', $output);

                $graph_regionality = do_template('STATS_GRAPH', array('_GUID' => '1087a34b5aa2ec808dcdce234dfe492e', 'GRAPH' => $this->get_stats_url(strval($rows[0]['ip']) . '-Regionality'), 'TITLE' => do_lang_tempcode('REGIONALITY_SHARE'), 'TEXT' => do_lang_tempcode('DESCRIPTION_REGIONALITY_SHARE')));
            }
        } else {
            // Geo-IP data isn't installed
            $list_regionality = new Tempcode();
            $graph_regionality = new Tempcode();
        }

        //************************************************************************************************
        // Views
        //************************************************************************************************
        $start = get_param_integer('start_views', 0);
        $max = get_param_integer('max_views', 30);
        $sortables = array('date_and_time' => do_lang_tempcode('DATE_TIME'));
        $test = explode(' ', get_param_string('sort_views', 'date_and_time DESC'));
        if (count($test) == 1) {
            $test[1] = 'DESC';
        }
        list($sortable, $sort_order) = $test;
        if (((strtoupper($sort_order) != 'ASC') && (strtoupper($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
            log_hack_attack_and_exit('ORDERBY_HACK');
        }

        $reasonable_max = 10000;
        $where = db_string_equal_to('the_page', $page);
        if (substr($page, 0, 6) == 'pages/') {
            $where .= ' OR ' . db_string_equal_to('the_page', '/' . $page); // Legacy compatibility
        }
        $ip_filter = $GLOBALS['DEV_MODE'] ? '' : (' AND ' . db_string_not_equal_to('ip', get_ip_address()));
        $reasonable_max = 10000;
        $rows = $GLOBALS['SITE_DB']->query('SELECT date_and_time FROM ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'stats WHERE (' . $where . ')' . $ip_filter . ' ORDER BY ' . $sortable . ' ' . $sort_order, $reasonable_max/*reasonable limit*/);
        if (count($rows) < 1) {
            $list_views = new Tempcode();
        } else {
            require_code('templates_results_table');
            $fields_title = results_field_title(array(do_lang_tempcode('DATE_TIME'), do_lang_tempcode('COUNT_VIEWS')), $sortables, 'sort', $sortable . ' ' . $sort_order);
            $fields = new Tempcode();
            $i = 0;
            while (array_key_exists($i, $rows)) {
                $row = $rows[$i];
                $week = intval(round($row['date_and_time'] / (60 * 60 * 24 * 7)));
                $date = get_timezoned_date(($week - 1) * (60 * 60 * 24 * 7), false, false, false, true) . ' - ' . get_timezoned_date(($week) * (60 * 60 * 24 * 7), false, false, false, true);
                $views = 0;
                while (array_key_exists($i + $views, $rows)) {
                    $_week = intval(round($row['date_and_time'] / (60 * 60 * 24 * 7)));

                    if ($_week != $week) {
                        break;
                    }

                    $views++;
                }
                $i += $views;
                if ($i < $start) {
                    continue;
                }
                $fields->attach(results_entry(array($date, ($i < $reasonable_max) ? integer_format($views) : ('>' . integer_format($views))), true));
                if ($i >= $start + $max) {
                    break;
                }
            }
            $list_views = results_table(do_lang_tempcode('PAGES_STATISTICS', escape_html($page)), $start, 'start_views', $max, 'max_views', $i, $fields_title, $fields, $sortables, $sortable, $sort_order, 'sort_views');
        }

        $tpl = do_template('STATS_SCREEN_ISCREEN', array(
            '_GUID' => '1ac86992de9fc7095883ae23b900d84c',
            'TITLE' => $this->title,
            'GRAPH_REGIONALITY' => $graph_regionality,
            'STATS_REGIONALITY' => $list_regionality,
            'STATS_VIEWS' => $list_views,
            'GRAPH_KEYWORDS' => $graph_keywords,
            'STATS_KEYWORDS' => $list_keywords,
            'GRAPH_VIEWS_HOURLY' => $graph_views_hourly,
            'STATS_VIEWS_HOURLY' => $list_views_hourly,
            'GRAPH_VIEWS_DAILY' => $graph_views_daily,
            'STATS_VIEWS_DAILY' => $list_views_daily,
            'GRAPH_VIEWS_WEEKLY' => $graph_views_weekly,
            'STATS_VIEWS_WEEKLY' => $list_views_weekly,
            'GRAPH_VIEWS_MONTHLY' => $graph_views_monthly,
            'STATS_VIEWS_MONTHLY' => $list_views_monthly,
            'GRAPH_IP' => $graph_ip,
            'STATS_IP' => $list_ip,
            'GRAPH_BROWSER' => $graph_share_browser,
            'STATS_BROWSER' => $list_share_browser,
            'GRAPH_REFERRER' => $graph_share_referrer,
            'STATS_REFERRER' => $list_share_referrer,
            'GRAPH_OS' => $graph_share_os,
            'STATS_OS' => $list_share_os,
        ));

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl);
    }

    /**
     * The UI to clear statistics.
     *
     * @return Tempcode The UI
     */
    public function clear()
    {
        // Someone obviously wants to clear out all their statistics.
        // Let's give them the option of only clearing out stored graphs, or deleting everything.

        require_code('form_templates');
        $controls = new Tempcode();

        $controls->attach(form_input_radio_entry('clear', 'some', true, do_lang_tempcode('DESCRIPTION_CLEAR_GRAPHS')));
        $controls->attach(form_input_radio_entry('clear', 'all', false, do_lang_tempcode('DESCRIPTION_CLEAR_ALL')));

        $fields = form_input_radio(do_lang_tempcode('CLEAR_STATISTICS'), '', 'clear', $controls);

        return do_template('FORM_SCREEN', array(
            '_GUID' => '82f3410d45e4d9ea53b2c033792a3207',
            'SKIP_WEBSTANDARDS' => true,
            'TITLE' => $this->title,
            'SUBMIT_ICON' => 'buttons__clear',
            'SUBMIT_NAME' => do_lang_tempcode('CLEAR_STATISTICS'),
            'TEXT' => paragraph(do_lang_tempcode('DESCRIPTION_CLEAR_STATISTICS')),
            'URL' => build_url(array('page' => '_SELF', 'type' => '_clear'), '_SELF'),
            'HIDDEN' => '',
            'FIELDS' => $fields,
        ));
    }

    /**
     * The actualiser to clear statistics.
     *
     * @return Tempcode The UI
     */
    public function _clear()
    {
        // Let's clear out the saved graphs
        $handle = opendir(get_custom_file_base() . '/data_custom/modules/admin_stats/');
        if ($handle !== false) {
            require_code('files');

            while (false !== ($file = readdir($handle))) {
                if ((!should_ignore_file(get_custom_file_base() . '/data_custom/modules/admin_stats/' . $file, IGNORE_ACCESS_CONTROLLERS | IGNORE_HIDDEN_FILES)) && ($file != 'IP_Country.txt') && (!is_dir($file))) {
                    $path = get_custom_file_base() . '/data_custom/modules/admin_stats/' . $file;
                    @unlink($path) or intelligent_write_error($path);
                    sync_file($path);
                }
            }

            if (post_param_string('clear') == 'all') {
                // Clear ALL of the data stored stats in the db
                $GLOBALS['SITE_DB']->query_delete('hackattack');
                $GLOBALS['SITE_DB']->query_delete('failedlogins');
                $GLOBALS['SITE_DB']->query_delete('stats');
                $GLOBALS['SITE_DB']->query_delete('usersonline_track');
            }
        }

        return inform_screen(get_screen_title('CLEAR_STATISTICS'), do_lang_tempcode('SUCCESS'));
    }

    /**
     * Install geolocation data.
     *
     * @return Tempcode The UI, showing the result of the installation
     */
    public function install_geolocation_data()
    {
        require_code('tasks');
        return call_user_func_array__long_task(do_lang('INSTALL_GEOLOCATION_DATA'), $this->title, 'install_geolocation_data');
    }

    /**
     * Create a bar chart of the views the specified page has received in relation to the specified hours. The bar chart is stored in /data_custom/admin_stats/ as an SVG image, and the Tempcode for display of the graph and results table is returned.
     *
     * @param  PATH $page The page path
     * @param  string $type The statistic type (for use in sort parameters and such)
     * @param  string $graph_title Language string ID for the graph title
     * @param  string $graph_description Language string ID for the graph description
     * @param  integer $hours The steps of hours to use
     * @param  integer $total The total hours to plot
     * @return array A linear array containing the graph and list Tempcode objects, respectively
     */
    public function views_per_x($page, $type, $graph_title, $graph_description, $hours = 1, $total = 24)
    {
        // Return a graph with the views per hour for a specified page
        $start = get_param_integer('start_' . $type, 0);
        $max = get_param_integer('max_' . $type, 50);
        $sortables = array('date_and_time' => do_lang_tempcode('DATE_TIME'));
        $test = explode(' ', get_param_string('sort', 'date_and_time ASC'), 2);
        if (count($test) == 1) {
            $test[1] = 'DESC';
        }
        list($sortable, $sort_order) = $test;
        if (((strtoupper($sort_order) != 'ASC') && (strtoupper($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
            log_hack_attack_and_exit('ORDERBY_HACK');
        }

        $where = db_string_equal_to('the_page', $page);
        if (substr($page, 0, 6) == 'pages/') {
            $where .= ' OR ' . db_string_equal_to('the_page', '/' . $page); // Legacy compatibility
        }
        $ip_filter = $GLOBALS['DEV_MODE'] ? '' : (' AND ' . db_string_not_equal_to('ip', get_ip_address()));
        $query = 'SELECT id,date_and_time FROM ' . get_table_prefix() . 'stats WHERE (' . $where . ')' . $ip_filter . ' AND date_and_time>' . strval(time() - ($total * 60 * 60)) . ' AND date_and_time<=' . strval(time()) . ' ORDER BY ' . $sortable . ' ' . $sort_order;
        $rows = $GLOBALS['SITE_DB']->query($query, 10000/*reasonable limit*/);
        if ((count($rows) < 1) || (count($rows) == 10000)) {
            $list = paragraph(do_lang_tempcode('TOO_MUCH_DATA'), '', 'red_alert');
            $graph = new Tempcode();
            return array($graph, $list);
        }

        require_code('templates_results_table');
        $fields_title = results_field_title(array(do_lang_tempcode('DATE_TIME'), do_lang_tempcode('COUNT_VIEWS')), $sortables, 'sort', $sortable . ' ' . $sort_order);
        $fields = new Tempcode();
        $data = array();
        $round = 60 * 60 * $hours;
        $start_date_and_time = time() + $round - (time() + $round) % $round - $total * 60 * 60;
        $i = 0;

        for ($a = 0; $a < intval($total / $hours); $a++) {
            if ($i < $start) {
                $i++;
                continue;
            } elseif ($i >= $start + $max) {
                break;
            }

            $date = get_timezoned_date($start_date_and_time + $a * $hours * 60 * 60, ($hours < 24));
            $to_date = get_timezoned_date($start_date_and_time + ($a + 1) * $hours * 60 * 60, ($hours < 24));
            $data[$date] = 0;
            foreach ($rows as $value) {
                if (($value['date_and_time'] >= $start_date_and_time + $a * $hours * 60 * 60) && ($value['date_and_time'] < $start_date_and_time + ($a + 1) * $hours * 60 * 60)) {
                    $data[$date]++;
                } elseif ($value['date_and_time'] > $start_date_and_time + ($a + 1) * $hours * 60 * 60) {
                    break;
                }
            }

            $i++;
            $fields->attach(results_entry(array($date . ' - ' . $to_date, integer_format($data[$date])), true));
        }
        $list = results_table(do_lang_tempcode('PAGES_STATISTICS', escape_html($page)), $start, 'start_' . $type, $max, 'max_' . $type, $i, $fields_title, $fields, $sortables, $sortable, $sort_order, 'sort_' . $type);

        $output = create_bar_chart($data, do_lang('DATE_TIME'), do_lang('COUNT_VIEWS'), '', '');
        $this->save_graph(strval($rows[0]['id']) . '-Views-' . strval($hours) . '_' . strval($start_date_and_time), $output);

        $graph = do_template('STATS_GRAPH', array(
            '_GUID' => 'b4cf5df74c012c2df5e3988a0ca0e622',
            'GRAPH' => $this->get_stats_url(strval($rows[0]['id']) . '-Views-' . strval($hours) . '_' . strval($start_date_and_time)),
            'TITLE' => do_lang_tempcode($graph_title),
            'TEXT' => do_lang_tempcode($graph_description),
        ));

        return array($graph, $list);
    }

    /**
     * Create a pie chart of the ratios of the specified statistic for the specified page. The chart is saved as an SVG image in /data_custom/admin_stats/, and the Tempcode for display of the graph and results table is returned
     *
     * @param  PATH $page The page path
     * @param  string $type The statistic to use
     * @param  string $graph_title Language string ID for the graph title
     * @param  string $graph_description Language string ID for the graph description
     * @param  string $list_title Language string ID for the list title
     * @return array A linear array containing the graph and list Tempcode objects, respectively
     */
    public function page_x_share($page, $type, $graph_title, $graph_description, $list_title)
    {
        // Return a pie chart with the $type used to view this page
        $start = get_param_integer('start_' . $type, 0);
        $max = get_param_integer('max_' . $type, 25);
        $sortables = array('views' => do_lang_tempcode('COUNT_VIEWS'));
        list($sortable, $sort_order) = explode(' ', get_param_string('sort', 'views ASC'), 2);
        if (((strtoupper($sort_order) != 'ASC') && (strtoupper($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
            log_hack_attack_and_exit('ORDERBY_HACK');
        }

        $where = db_string_equal_to('the_page', $page);
        if (substr($page, 0, 6) == 'pages/') {
            $where .= ' OR ' . db_string_equal_to('the_page', '/' . $page); // Legacy compatibility
        }
        $ip_filter = $GLOBALS['DEV_MODE'] ? '' : (' AND ' . db_string_not_equal_to('ip', get_ip_address()));
        $rows = $GLOBALS['SITE_DB']->query('SELECT id,' . $type . ' FROM ' . get_table_prefix() . 'stats WHERE (' . $where . ')' . $ip_filter, 10000/*reasonable limit*/);
        if (count($rows) < 1) {
            $list = new Tempcode();
            $graph = new Tempcode();
            return array($graph, $list);
        }

        $data1 = array();
        $degrees = 360 / count($rows);
        foreach ($rows as $value) {
            if ($type == 'browser') {
                // Strip point versions in e.g. Chrome
                $value[$type] = preg_replace('#[/ ]\d+\.\d+(\.\d+)+#', '', $value[$type]);
                $value[$type] = preg_replace('#[/ ]\d+\.\d\d+#', '', $value[$type]);
                $value[$type] = preg_replace('#/\d\d\d\d\d+#', '', $value[$type]);
                for ($i = 0; $i < 10; $i++) {
                    $value[$type] = preg_replace('#; \.NET .*([;\)])#U', '$1', $value[$type]);
                }
                $value[$type] = preg_replace('# \((Windows|Linux|Macintosh).*\)#U', '', $value[$type]);
                $value[$type] = preg_replace('#; (Windows|Linux|Macintosh).*\)#U', ')', $value[$type]);
            }

            if (!array_key_exists($value[$type], $data1)) {
                $data1[$value[$type]] = 0;
            }
            $data1[$value[$type]]++;
        }

        if ($sortable == 'views') {
            asort($data1);
            if ($sort_order == 'DESC') {
                $data1 = array_reverse($data1);
            }
        }

        require_code('templates_results_table');
        $fields_title = results_field_title(array(do_lang_tempcode($list_title), do_lang_tempcode('COUNT_VIEWS')), $sortables, 'sort', $sortable . ' ' . $sort_order);
        $fields = new Tempcode();
        $data = array();
        $done_total = 0;
        $i = 0;

        foreach ($data1 as $key => $value) {
            if ($i < $start) {
                $i++;
                continue;
            } elseif ($i >= $start + $max) {
                break;
            }
            if ($key == '') {
                $link = do_lang('_UNKNOWN');
            } else {
                $link = escape_html($key);
            }
            $fields->attach(results_entry(array($link, integer_format($value)), true));

            $data[$key] = $value * $degrees;
            $done_total += $value;
            $i++;
        }
        if (count($rows) > $done_total) {
            $data[do_lang('OTHER')] = 360.0 - $done_total * $degrees;
            $fields->attach(results_entry(array(do_lang('OTHER'), integer_format(count($rows) - $done_total)), true));
        }
        $list = results_table(do_lang_tempcode('PAGES_STATISTICS', escape_html($page)), $start, 'start_' . $type, $max, 'max_' . $type, $i, $fields_title, $fields, $sortables, $sortable, $sort_order, 'sort_' . $type);

        $output = create_pie_chart($data);
        $this->save_graph(strval($rows[0]['id']) . '-' . $type, $output);

        $graph = do_template('STATS_GRAPH', array('_GUID' => '5a88fdf891e9af4eb1cca3470f263c7d', 'GRAPH' => $this->get_stats_url(strval($rows[0]['id']) . '-' . $type), 'TITLE' => do_lang_tempcode($graph_title), 'TEXT' => do_lang_tempcode($graph_description)));

        return array($graph, $list);
    }

    /**
     * Save a graph to the server so it can be viewed client-side.
     *
     * @param  string $file Name of the graph (no path or extension)
     * @param  string $graph SVG markup
     */
    public function save_graph($file, $graph)
    {
        require_code('files');
        $path = get_custom_file_base() . '/data_custom/modules/admin_stats/' . filter_naughty_harsh($file) . '.xml';
        cms_file_put_contents_safe($path, $graph, FILE_WRITE_FIX_PERMISSIONS | FILE_WRITE_SYNC_FILE);
    }

    /**
     * Get the URL to a graph.
     *
     * @param  string $file Name of the graph (no path or extension)
     * @return URLPATH URL to graph
     */
    public function get_stats_url($file)
    {
        //return get_custom_base_url() . '/data_custom/modules/admin_stats/' . $file . '.xml'; We do not allow direct access, as it would not be secure
        $keep = symbol_tempcode('KEEP');
        return find_script('stats_graph') . '?file=' . urlencode($file) . $keep->evaluate();
    }
}
