<?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    core_forum_drivers
 */

/**
 * Forum driver class.
 *
 * @package    core_forum_drivers
 */
class Forum_driver_aef extends Forum_driver_base
{
    /**
     * Check the connected DB is valid for this forum driver.
     *
     * @return boolean Whether it is valid
     */
    public function check_db()
    {
        $test = $this->connection->query('SELECT COUNT(*) FROM ' . $this->connection->get_table_prefix() . 'users', null, null, true);
        return !is_null($test);
    }

    /**
     * Get the rows for the top given number of posters on the forum.
     *
     * @param  integer $limit The limit to the number of top posters to fetch
     * @return array The rows for the given number of top posters in the forum
     */
    public function get_top_posters($limit)
    {
        return $this->connection->query("SELECT u.*,count(p.pid) AS submited_posts FROM " . $this->connection->get_table_prefix() . "users AS u LEFT JOIN " . $this->connection->get_table_prefix() . "posts AS p ON ( u.id=p.poster_id ) WHERE u.id<>" . strval($this->get_guest_id()) . " GROUP BY u.id,u.language,u.ppic,u.avatar_type,u.avatar,u.username,u.email,u.hideemail,u.r_time,u.temp_ban_time,u.temp_ban,u.user_theme,u.u_member_group");
    }

    /**
     * Attempt to to find the member's language from their forum profile. It converts between language-identifiers using a map (lang/map.ini).
     *
     * @param  MEMBER $member The member who's language needs to be fetched
     * @return ?LANGUAGE_NAME The member's language (null: unknown)
     */
    public function forum_get_lang($member)
    {
        return $this->get_member_row_field($member, 'language');
    }

    /**
     * Find if the login cookie contains the login name instead of the member ID.
     *
     * @return boolean Whether the login cookie contains a login name or a member ID
     */
    public function is_cookie_login_name()
    {
        return false;
    }

    /**
     * Find if login cookie is md5-hashed.
     *
     * @return boolean Whether the login cookie is md5-hashed
     */
    public function is_hashed()
    {
        return false;
    }

    /**
     * Find the member ID of the forum guest member.
     *
     * @return MEMBER The member ID of the forum guest member
     */
    public function get_guest_id()
    {
        return (-1);
    }

    /**
     * Get the forums' table prefix for the database.
     *
     * @return string The forum database table prefix
     */
    public function get_drivered_table_prefix()
    {
        global $SITE_INFO;
        return $SITE_INFO['aef_table_prefix'];
    }

    /**
     * Add the specified custom field to the forum (some forums implemented this using proper custom profile fields, others through adding a new field).
     *
     * @param  string $name The name of the new custom field
     * @param  integer $length The length of the new custom field
     * @return boolean Whether the custom field was created successfully
     */
    public function install_create_custom_field($name, $length)
    {
        $this->connection->query('ALTER TABLE ' . $this->connection->get_table_prefix() . 'users ADD cms_' . $name . ' TEXT', null, null, true);
        return true;
    }

    /**
     * Get an array of attributes to take in from the installer. Almost all forums require a table prefix, which the requirement there-of is defined through this function.
     * The attributes have 4 values in an array
     * - name, the name of the attribute for _config.php
     * - default, the default value (perhaps obtained through autodetection from forum config)
     * - description, a textual description of the attributes
     * - title, a textual title of the attribute
     *
     * @return array The attributes for the forum
     */
    public function install_specifics()
    {
        global $PROBED_FORUM_CONFIG;
        $a = array();
        $a['name'] = 'aef_table_prefix';
        $a['default'] = array_key_exists('sql_tbl_prefix', $PROBED_FORUM_CONFIG) ? $PROBED_FORUM_CONFIG['sql_tbl_prefix'] : 'aef_';
        $a['description'] = do_lang('MOST_DEFAULT');
        $a['title'] = 'AEF ' . do_lang('TABLE_PREFIX');
        return array($a);
    }

    /**
     * Searches for forum auto-config at this path.
     *
     * @param  PATH $path The path in which to search
     * @return boolean Whether the forum auto-config could be found
     */
    public function install_test_load_from($path)
    {
        global $PROBED_FORUM_CONFIG;
        if (@file_exists($path . '/universal.php')) {
            $globals = array();
            @include($path . '/universal.php');
            if (array_key_exists('database', $globals)) {
                $PROBED_FORUM_CONFIG['sql_database'] = $globals['database'];
                $PROBED_FORUM_CONFIG['sql_user'] = $globals['user'];
                $PROBED_FORUM_CONFIG['sql_pass'] = $globals['password'];
                $PROBED_FORUM_CONFIG['board_url'] = $globals['url'];
                $PROBED_FORUM_CONFIG['sql_tbl_prefix'] = $globals['dbprefix'];
                $PROBED_FORUM_CONFIG['cookie_member_id'] = $globals['cookie_name'] . '[loguid]';
                $PROBED_FORUM_CONFIG['cookie_member_hash'] = $globals['cookie_name'] . '[logpass]';
            }
            return true;
        }
        return false;
    }

    /**
     * Get an array of paths to search for config at.
     *
     * @return array The paths in which to search for the forum config
     */
    public function install_get_path_search_list()
    {
        return array(
            0 => '/',
            1 => 'aef',
            2 => 'forum',
            3 => 'forums',
            4 => 'board',
            5 => 'boards',
            6 => 'upload',
            7 => 'uploads',
            8 => '../forums',
            9 => '../forum',
            10 => '../boards',
            11 => '../board',
            12 => '../aef',
            13 => '../upload',
            14 => '../uploads',
            15 => '../themes',
            16 => '../theme',
            17 => '../main'
        );
    }

    /**
     * Get an emoticon chooser template.
     *
     * @param  string $field_name The ID of the form field the emoticon chooser adds to
     * @return Tempcode The emoticon chooser template
     */
    public function get_emoticon_chooser($field_name = 'post')
    {
        $emoticons = $this->connection->query_select('smileys', array('*'), array('smstatus' => 0));
        $em = new Tempcode();
        require_code('comcode_compiler');
        foreach ($emoticons as $emo) {
            $code = $emo['smcode'];
            $em->attach(do_template('EMOTICON_CLICK_CODE', array('_GUID' => '681b0be397b1892c1ee76e58409822f3', 'FIELD_NAME' => $field_name, 'CODE' => $code, 'IMAGE' => apply_emoticons($code))));
        }

        return $em;
    }

    /**
     * Pin a topic.
     *
     * @param  AUTO_LINK $id The topic ID
     * @param  boolean $pin True: pin it, False: unpin it
     */
    public function pin_topic($id, $pin = true)
    {
        $this->connection->query_update('topics', array('t_sticky' => $pin ? 1 : 0), array('tid' => $id), '', 1);
    }

    /**
     * Set a custom profile field's value, if the custom field exists. Only works on specially-named (titled) fields.
     *
     * @param  MEMBER $member The member ID
     * @param  string $field The field name (e.g. "firstname" for the CPF with a title of "cms_firstname")
     * @param  string $value The value
     */
    public function set_custom_field($member, $field, $value)
    {
        $this->connection->query_update('users', array('cms_' . $field => $value), array('id' => $member), '', null, null, false, true);
    }

    /**
     * Get custom profile fields values for all 'cms_' prefixed keys.
     *
     * @param  MEMBER $member The member ID
     * @return ?array A map of the custom profile fields, key_suffix=>value (null: no fields)
     */
    public function get_custom_fields($member)
    {
        $row = $this->get_member_row($member);
        $out = array();
        foreach ($row as $attribute => $value) {
            if (substr($attribute, 0, 4) == 'cms_') {
                $out[substr($attribute, 4)] = $value;
            }
        }
        return $out;
    }

    /**
     * Get a member row for the member of the given name.
     *
     * @param  SHORT_TEXT $name The member name
     * @return ?array The profile-row (null: not found)
     */
    public function get_mrow($name)
    {
        $rows = $this->connection->query_select('users', array('*'), array('username' => $name), '', 1);
        if (!array_key_exists(0, $rows)) {
            return null;
        }
        return $rows[0];
    }

    /**
     * From a member row, get the member's primary usergroup.
     *
     * @param  array $r The profile-row
     * @return GROUP The member's primary usergroup
     */
    public function mrow_group($r)
    {
        return $r['u_member_group'];
    }

    /**
     * From a member row, get the member's member ID.
     *
     * @param  array $r The profile-row
     * @return MEMBER The member ID
     */
    public function mrow_id($r)
    {
        return $r['id'];
    }

    /**
     * From a member row, get the member's last visit date.
     *
     * @param  array $r The profile-row
     * @return TIME The last visit date
     */
    public function mrow_lastvisit($r)
    {
        return $r['lastlogin_1']; // could be used `lastlogin` field for last login
    }

    /**
     * From a member row, get the member's name.
     *
     * @param  array $r The profile-row
     * @return string The member name
     */
    public function mrow_username($r)
    {
        return $r['username'];
    }

    /**
     * From a member row, get the member's e-mail address.
     *
     * @param  array $r The profile-row
     * @return SHORT_TEXT The member e-mail address
     */
    public function mrow_email($r)
    {
        return $r['email'];
    }

    /**
     * Get a URL to the specified member's home (control panel).
     *
     * @param  MEMBER $id The member ID
     * @return URLPATH The URL to the members home
     */
    public function member_home_url($id)
    {
        return get_forum_base_url() . '/index.php?act=usercp';
    }

    /**
     * Get the photo thumbnail URL for the specified member ID.
     *
     * @param  MEMBER $member The member ID
     * @return URLPATH The URL (blank: none)
     */
    public function get_member_photo_url($member)
    {
        $pic = $this->get_member_row_field($member, 'ppic');
        if (is_null($pic)) {
            $pic = '';
        } elseif ((url_is_local($pic)) && ($pic != '')) {
            $pic = ((get_forum_base_url() != get_base_url()) ? get_forum_base_url() : get_custom_base_url()) . '/uploads/personalpic/' . $pic;
        }

        return $pic;
    }

    /**
     * Get the avatar URL for the specified member ID.
     *
     * @param  MEMBER $member The member ID
     * @return URLPATH The URL (blank: none)
     */
    public function get_member_avatar_url($member)
    {
        $avatar_path = get_forum_base_url();

        $type = $this->get_member_row_field($member, 'avatar_type');
        $filename = $this->get_member_row_field($member, 'avatar');

        switch ($type) {
            case '1': // Avatar from Avatars Gallery
                return get_forum_base_url() . '/avatars/' . $filename;
            case '2': // URL of Remote image Avatar
                return $filename;
            case '3': // Uploaded Avatar
                return get_forum_base_url() . '/uploads/avatars/' . $filename;
        }
        return ''; // the avatar is not set
    }

    /**
     * Get a URL to the specified member's profile.
     *
     * @param  MEMBER $id The member ID
     * @return URLPATH The URL to the member profile
     */
    protected function _member_profile_url($id)
    {
        //return get_forum_base_url().'/index.php?act=usercp&ucpact=profile'; //this is the user control panel profile
        return get_forum_base_url() . '/index.php?mid=' . strval($id);
    }

    /**
     * Get a URL to the registration page (for people to create member accounts).
     *
     * @return URLPATH The URL to the registration page
     */
    protected function _join_url()
    {
        return get_forum_base_url() . '/index.php?act=register';
    }

    /**
     * Get a URL to the members-online page.
     *
     * @return URLPATH The URL to the members-online page
     */
    protected function _users_online_url()
    {
        return get_forum_base_url() . '/index.php?act=active';
    }

    /**
     * Get a URL to send a private/personal message to the given member.
     *
     * @param  MEMBER $id The member ID
     * @return URLPATH The URL to the private/personal message page
     */
    protected function _member_pm_url($id)
    {
        return get_forum_base_url() . '/index.php?act=usercp&ucpact=writepm&to=' . strval($id);
    }

    /**
     * Get a URL to the specified forum.
     *
     * @param  integer $id The forum ID
     * @return URLPATH The URL to the specified forum
     */
    protected function _forum_url($id)
    {
        return get_forum_base_url() . '/index.php?fid=' . strval($id);
    }

    /**
     * Get the forum ID from a forum name.
     *
     * @param  SHORT_TEXT $forum_name The forum name
     * @return ?integer The forum ID (null: not found)
     */
    public function forum_id_from_name($forum_name)
    {
        $forum_id = $this->connection->query_select_value_if_there('forums', 'fid', array('fname' => $forum_name));
        if (!is_null($forum_id)) {
            return $forum_id;
        }

        return $forum_id;
    }

    /**
     * Convert an IP address into phpBB hexadecimal string format.
     *
     * @param  IP $ip The normal IP address
     * @return string The phpBB IP address
     */
    protected function _phpbb_ip($ip)
    {
        $ip_apart = explode('.', $ip);
        $_ip = dechex($ip_apart[0]) . dechex($ip_apart[1]) . dechex($ip_apart[2]) . dechex($ip_apart[3]);
        return $_ip;
    }

    /**
     * Convert an IP address from phpBB hexadecimal string format.
     *
     * @param  string $ip The phpBB IP address
     * @return IP The normal IP address
     */
    protected function _un_phpbb_ip($ip)
    {
        $_ip = strval(hexdec($ip[0] . $ip[1])) . '.' . strval(hexdec($ip[2] . $ip[3])) . '.' . strval(hexdec($ip[4] . $ip[5])) . '.' . strval(hexdec($ip[6] . $ip[7]));
        return $_ip;
    }

    /**
     * Makes a post in the specified forum, in the specified topic according to the given specifications. If the topic doesn't exist, it is created along with a spacer-post.
     * Spacer posts exist in order to allow staff to delete the first true post in a topic. Without spacers, this would not be possible with most forum systems. They also serve to provide meta information on the topic that cannot be encoded in the title (such as a link to the content being commented upon).
     *
     * @param  SHORT_TEXT $forum_name The forum name
     * @param  SHORT_TEXT $topic_identifier The topic identifier (usually <content-type>_<content-id>)
     * @param  MEMBER $member The member ID
     * @param  LONG_TEXT $post_title The post title
     * @param  LONG_TEXT $_post The post content in Comcode format
     * @param  string $content_title The topic title; must be same as content title if this is for a comment topic
     * @param  string $topic_identifier_encapsulation_prefix This is put together with the topic identifier to make a more-human-readable topic title or topic description (hopefully the latter and a $content_title title, but only if the forum supports descriptions)
     * @param  ?URLPATH $content_url URL to the content (null: do not make spacer post)
     * @param  ?TIME $time The post time (null: use current time)
     * @param  ?IP $ip The post IP address (null: use current members IP address)
     * @param  ?BINARY $validated Whether the post is validated (null: unknown, find whether it needs to be marked unvalidated initially). This only works with the Conversr driver.
     * @param  ?BINARY $topic_validated Whether the topic is validated (null: unknown, find whether it needs to be marked unvalidated initially). This only works with the Conversr driver.
     * @param  boolean $skip_post_checks Whether to skip post checks
     * @param  SHORT_TEXT $poster_name_if_guest The name of the poster
     * @param  ?AUTO_LINK $parent_id ID of post being replied to (null: N/A)
     * @param  boolean $staff_only Whether the reply is only visible to staff
     * @return array Topic ID (may be null), and whether a hidden post has been made
     */
    public function make_post_forum_topic($forum_name, $topic_identifier, $member, $post_title, $_post, $content_title, $topic_identifier_encapsulation_prefix, $content_url = null, $time = null, $ip = null, $validated = null, $topic_validated = 1, $skip_post_checks = false, $poster_name_if_guest = '', $parent_id = null, $staff_only = false)
    {
        $__post = comcode_to_tempcode($_post);
        $post = $__post->evaluate();

        if (is_null($time)) {
            $time = time();
        }
        if (is_null($ip)) {
            $ip = get_ip_address();
        }
        $forum_id = $this->forum_id_from_name($forum_name);
        if (is_null($forum_id)) {
            warn_exit(do_lang_tempcode('MISSING_FORUM', escape_html($forum_name)));
        }
        $test = $this->connection->query_select('forums', array('*'), null, '', 1);
        $fm = array_key_exists('status', $test[0]);
        $ip_address = $ip;
        $local_ip_address = '127.0.0.1';

        $topic_id = $this->find_topic_id_for_topic_identifier($forum_name, $topic_identifier);

        $is_new = is_null($topic_id);
        if ($is_new) {
            $map = array('t_bid' => $forum_id, 'topic' => $content_title, 't_mem_id' => $member, 'n_views' => 0, 'n_posts' => 0, 't_status' => 1, 'type_image' => 0, 'first_post_id' => 0, 'last_post_id' => 0, 't_description' => $topic_identifier_encapsulation_prefix . ': #' . $topic_identifier_encapsulation_prefix . ': ' . $topic_identifier);

            if ($fm) {
                $map = array_merge($map, array('t_status' => 0));
            }
            $topic_id = $this->connection->query_insert('topics', $map, true);
        }

        $GLOBALS['LAST_TOPIC_ID'] = $topic_id;
        $GLOBALS['LAST_TOPIC_IS_NEW'] = $is_new;

        if ($post == '') {
            return array($topic_id, false);
        }

        $map = array('post_tid' => $topic_id, 'post_fid' => $forum_id, 'poster_id' => $member, 'ptime' => $time, 'poster_ip' => $ip_address, 'gposter_name' => '', 'use_smileys' => 1, 'modtime' => 'now()', 'post_title' => $post_title, 'post' => $post);
        if ($fm) {
            $map = array_merge($map, array('num_attachments' => 0, 'modifiers_id' => ''));
        }
        $post_id = $this->connection->query_insert('posts', $map, true);

        $this->connection->query('UPDATE ' . $this->connection->get_table_prefix() . 'topics SET first_post_id=' . strval($post_id) . ', last_post_id=' . strval($post_id) . ' WHERE tid=' . strval($topic_id), 1);
        $this->connection->query('UPDATE ' . $this->connection->get_table_prefix() . 'forums SET ntopic=(ntopic+1),nposts=(nposts+1), f_last_pid=' . strval($post_id) . ' WHERE fid=' . strval($forum_id), 1);

        return array($topic_id, false);
    }

    /**
     * Get an array of maps for the topic in the given forum.
     *
     * @param  integer $topic_id The topic ID
     * @param  integer $count The comment count will be returned here by reference
     * @param  integer $max Maximum comments to returned
     * @param  integer $start Comment to start at
     * @param  boolean $mark_read Whether to mark the topic read (ignored for this forum driver)
     * @param  boolean $reverse Whether to show in reverse
     * @return mixed The array of maps (Each map is: title, message, member, date) (-1 for no such forum, -2 for no such topic)
     */
    public function get_forum_topic_posts($topic_id, &$count, $max = 100, $start = 0, $mark_read = true, $reverse = false)
    {
        if (is_null($topic_id)) {
            return (-2);
        }
        $order = $reverse ? 'ptime DESC' : 'ptime';
        $rows = $this->connection->query('SELECT * FROM ' . $this->connection->get_table_prefix() . 'posts p WHERE post_tid=' . strval($topic_id) . ' AND post NOT LIKE \'' . db_encode_like(substr(do_lang('SPACER_POST', '', '', '', get_site_default_lang()), 0, 20) . '%') . '\' ORDER BY ' . $order, $max, $start);
        $count = $this->connection->query_value_if_there('SELECT COUNT(*) FROM ' . $this->connection->get_table_prefix() . 'posts p WHERE post_tid=' . strval($topic_id) . ' AND post NOT LIKE \'' . db_encode_like(substr(do_lang('SPACER_POST', '', '', '', get_site_default_lang()), 0, 20) . '%') . '\'');

        $out = array();
        foreach ($rows as $myrow) {
            $temp = array();

            $temp['title'] = $myrow['post_title'];
            if (is_null($temp['title'])) {
                $temp['title'] = '';
            }
            global $LAX_COMCODE;
            $temp2 = $LAX_COMCODE;
            $LAX_COMCODE = true;
            $temp['message'] = $myrow['post'];
            $LAX_COMCODE = $temp2;
            $temp['member'] = $myrow['poster_id'];
            $temp['date'] = $myrow['ptime'];

            $out[] = $temp;
        }

        return $out;
    }

    /**
     * Get a URL to the specified topic ID. Most forums don't require the second parameter, but some do, so it is required in the interface.
     *
     * @param  integer $id The topic ID
     * @param  string $forum The forum ID
     * @return URLPATH The URL to the topic
     */
    public function topic_url($id, $forum)
    {
        return get_forum_base_url() . '/index.php?tid=' . strval($id);
    }

    /**
     * Get a URL to the specified post ID.
     *
     * @param  integer $id The post ID
     * @param  string $forum The forum ID
     * @return URLPATH The URL to the post
     */
    public function post_url($id, $forum)
    {
        $topic_id = $this->connection->query_select_value_if_there('posts', 'post_tid', array('pid' => $id));
        if (is_null($topic_id)) {
            return '?';
        }
        $url = get_forum_base_url() . '/index.php?tid=' . strval($topic_id) . '&tpg=1#p' . strval($id);
        return $url;
    }

    /**
     * Get the topic ID from a topic identifier in the specified forum. It is used by comment topics, which means that the unique-topic-name assumption holds valid.
     *
     * @param  string $forum The forum name / ID
     * @param  SHORT_TEXT $topic_identifier The topic identifier
     * @return ?integer The topic ID (null: not found)
     */
    public function find_topic_id_for_topic_identifier($forum, $topic_identifier)
    {
        if (is_integer($forum)) {
            $forum_id = $forum;
        } else {
            $forum_id = $this->forum_id_from_name($forum);
        }
        $query = 'SELECT tid FROM ' . $this->connection->get_table_prefix() . 'topics WHERE t_bid=' . strval($forum_id);
        $query .= ' AND (' . db_string_equal_to('t_description', $topic_identifier) . ' OR t_description LIKE \'%: #' . db_encode_like($topic_identifier) . '\')';

        return $this->connection->query_value_if_there($query);
    }

    /**
     * Get an array of topics in the given forum. Each topic is an array with the following attributes:
     * - id, the topic ID
     * - title, the topic title
     * - lastusername, the username of the last poster
     * - lasttime, the timestamp of the last reply
     * - closed, a Boolean for whether the topic is currently closed or not
     * - firsttitle, the title of the first post
     * - firstpost, the first post (only set if $show_first_posts was true)
     *
     * @param  mixed $name The forum name or an array of forum IDs
     * @param  integer $limit The limit
     * @param  integer $start The start position
     * @param  integer $max_rows The total rows (not a parameter: returns by reference)
     * @param  SHORT_TEXT $filter_topic_title The topic title filter
     * @param  boolean $show_first_posts Whether to show the first posts
     * @param  string $date_key The date key to sort by
     * @set    lasttime firsttime
     * @param  boolean $hot Whether to limit to hot topics
     * @param  SHORT_TEXT $filter_topic_description The topic description filter
     * @return ?array The array of topics (null: error)
     */
    public function show_forum_topics($name, $limit, $start, &$max_rows, $filter_topic_title = '', $show_first_posts = false, $date_key = 'lasttime', $hot = false, $filter_topic_description = '')
    {
        if (is_integer($name)) {
            $id_list = 't_bid=' . strval($name);
        } elseif (!is_array($name)) {
            $id = $this->forum_id_from_name($name);
            if (is_null($id)) {
                return null;
            }
            $id_list = 't_bid=' . strval($id);
        } else {
            $id_list = '';
            foreach (array_keys($name) as $id) {
                if ($id_list != '') {
                    $id_list .= ' OR ';
                }
                $id_list .= 't_bid=' . strval($id);
            }
            if ($id_list == '') {
                return null;
            }
        }

        $topic_filter = ($filter_topic_title != '') ? 'AND topic LIKE \'' . db_encode_like($filter_topic_title) . '\'' : '';
        if ($filter_topic_description != '') {
            $topic_filter .= ' AND t_description LIKE \'' . db_encode_like($filter_topic_description) . '\'';
        }
        $topic_filter .= ' ORDER BY ' . (($date_key == 'lasttime') ? 'last_post_id' : 'last_post_id') . ' DESC'; // there is no 'topic_time' or something like it, so we will sort by the 'last_post_id' field
        $rows = $this->connection->query('SELECT * FROM ' . $this->connection->get_table_prefix() . 'topics WHERE (' . $id_list . ') ' . $topic_filter, $limit, $start);
        $max_rows = $this->connection->query_value_if_there('SELECT COUNT(*) FROM ' . $this->connection->get_table_prefix() . 'topics WHERE (' . $id_list . ') ' . $topic_filter);
        $i = 0;
        $firsttime = array();
        $username = array();
        $memberid = array();
        $datetimes = array();
        $rs = array();
        while (array_key_exists($i, $rows)) {
            $r = $rows[$i];

            $id = $r['tid'];

            $topic_first_post_id = $r['first_post_id']; // because there is no info in topics for time we will use the first and last posts in the topic
            $topic_first_post_row = $this->connection->query_select('posts', array('*'), array('pid' => $topic_first_post_id));
            $topic_first_post_row = (!empty($topic_first_post_row[0])) ? $topic_first_post_row[0] : array();

            $topic_last_post_id = $r['last_post_id'];
            $topic_last_post_row = $this->connection->query_select('posts', array('*'), array('pid' => $topic_last_post_id));
            $topic_last_post_row = (!empty($topic_last_post_row[0])) ? $topic_last_post_row[0] : array();

            $r['topic_time'] = $topic_first_post_row['ptime'];
            $r['topic_poster'] = $topic_first_post_row['poster_id'];
            $r['last_poster'] = $topic_last_post_row['poster_id'];
            $r['last_time'] = $topic_last_post_row['ptime'];

            $firsttime[$id] = $topic_first_post_row['ptime'];

            $post_rows = $this->connection->query('SELECT * FROM ' . $this->connection->get_table_prefix() . 'posts p WHERE post_tid=' . strval($id) . ' AND post NOT LIKE \'' . db_encode_like(substr(do_lang('SPACER_POST', '', '', '', get_site_default_lang()), 0, 20) . '%') . '\' ORDER BY ptime DESC', 1);

            if (!array_key_exists(0, $post_rows)) {
                $i++;
                continue;
            }
            $r2 = $post_rows[0];

            $username[$id] = $this->get_username($r2['poster_id']);
            $username[$id] = $r2['poster_id'];
            $datetimes[$id] = $r2['ptime'];
            $rs[$id] = $r;

            $i++;
        }
        if ($i > 0) {
            arsort($datetimes);
            $i = 0;
            $out = array();
            if (count($datetimes) > 0) {
                foreach ($datetimes as $id => $datetime) {
                    $r = $rs[$id];

                    $out[$i] = array();
                    $out[$i]['id'] = $id;
                    $out[$i]['num'] = $r['n_posts'] + 1;
                    $out[$i]['title'] = $r['topic'];
                    $out[$i]['description'] = $r['t_description'];
                    $out[$i]['firsttime'] = $r['topic_time'];
                    $out[$i]['firstusername'] = $this->get_username($r['topic_poster']);
                    $out[$i]['lastusername'] = $this->get_username($r['last_poster']);//$username[$id];
                    $out[$i]['firstmemberid'] = $r['topic_poster'];
                    $out[$i]['lastmemberid'] = $r['last_poster'];//$memberid[$id];
                    $out[$i]['lasttime'] = $r['last_time'];//$datetime;
                    $out[$i]['closed'] = ($r['t_status'] == 1);

                    $fp_rows = $this->connection->query('SELECT post_title,post,poster_id FROM ' . $this->connection->get_table_prefix() . 'posts p WHERE post NOT LIKE \'' . db_encode_like(substr(do_lang('SPACER_POST', '', '', '', get_site_default_lang()), 0, 20) . '%') . '\' AND ptime=' . strval($firsttime[$id]) . ' AND post_tid=' . strval($id), 1);

                    if (!array_key_exists(0, $fp_rows)) {
                        unset($out[$i]);
                        continue;
                    }
                    $out[$i]['firsttitle'] = $fp_rows[0]['post_title'];
                    if ($show_first_posts) {
                        global $LAX_COMCODE;
                        $temp = $LAX_COMCODE;
                        $LAX_COMCODE = true;
                        $out[$i]['firstpost'] = $fp_rows[0]['post'];
                        $LAX_COMCODE = $temp;
                    }

                    $i++;
                    if ($i == $limit) {
                        break;
                    }
                }
            }

            return $out;
        }
        return null;
    }

    /**
     * Get an array of members who are in at least one of the given array of groups.
     *
     * @param  array $groups The array of groups
     * @param  ?integer $max Return up to this many entries for primary members and this many entries for secondary members (null: no limit, only use no limit if querying very restricted usergroups!)
     * @param  integer $start Return primary members after this offset and secondary members after this offset
     * @return ?array The array of members (null: no members)
     */
    public function member_group_query($groups, $max = null, $start = 0)
    {
        $_groups = '';
        foreach ($groups as $group) {
            if ($_groups != '') {
                $_groups .= ' OR ';
            }
            $_groups .= 'u.u_member_group=' . strval($group);
        }
        if ($_groups == '') {
            return array();
        }

        // Query looks like this: SELECT * FROM `aef_users` u LEFT JOIN aef_user_groups g on u.`u_member_group`=g.member_group WHERE u.`u_member_group`=0
        return $this->connection->query('SELECT * FROM ' . $this->connection->get_table_prefix() . 'users u LEFT JOIN ' . $this->connection->get_table_prefix() . 'user_groups g ON u.u_member_group=g.member_group WHERE ' . $_groups . ' ORDER BY u.u_member_group ASC', $max, $start, false, true);
    }

    /**
     * This is the opposite of the get_next_member function.
     *
     * @param  MEMBER $member The member ID to decrement
     * @return ?MEMBER The previous member ID (null: no previous member)
     */
    public function get_previous_member($member)
    {
        $tempid = $this->connection->query_value_if_there('SELECT id FROM ' . $this->connection->get_table_prefix() . 'users WHERE id<' . strval($member) . ' AND id>0 ORDER BY id DESC');
        return $tempid;
    }

    /**
     * Get the member ID of the next member after the given one, or null.
     * It cannot be assumed there are no gaps in member IDs, as members may be deleted.
     *
     * @param  MEMBER $member The member ID to increment
     * @return ?MEMBER The next member ID (null: no next member)
     */
    public function get_next_member($member)
    {
        $tempid = $this->connection->query_value_if_there('SELECT id FROM ' . $this->connection->get_table_prefix() . 'users WHERE id>' . strval($member) . ' ORDER BY id');
        return $tempid;
    }

    /**
     * Try to find a member with the given IP address
     *
     * @param  IP $ip The IP address
     * @return array The distinct rows found
     */
    public function probe_ip($ip)
    {
        return $this->connection->query_select('posts', array('DISTINCT poster_id AS id'), array('poster_ip' => $ip));
    }

    /**
     * Get the name relating to the specified member ID.
     * If this returns null, then the member has been deleted. Always take potential null output into account.
     *
     * @param  MEMBER $member The member ID
     * @return ?SHORT_TEXT The member name (null: member deleted)
     */
    protected function _get_username($member)
    {
        if ($member == $this->get_guest_id()) {
            return do_lang('GUEST');
        }
        return $this->get_member_row_field($member, 'username');
    }

    /**
     * Get the e-mail address for the specified member ID.
     *
     * @param  MEMBER $member The member ID
     * @return SHORT_TEXT The e-mail address
     */
    protected function _get_member_email_address($member)
    {
        return $this->get_member_row_field($member, 'email');
    }

    /**
     * Find if this member may have e-mails sent to them
     *
     * @param  MEMBER $member The member ID
     * @return boolean Whether the member may have e-mails sent to them
     */
    public function get_member_email_allowed($member)
    {
        $v = $this->get_member_row_field($member, 'hideemail');
        if ($v == 0) {
            return true;
        }
        return false;
    }

    /**
     * Get the timestamp of a member's join date.
     *
     * @param  MEMBER $member The member ID
     * @return TIME The timestamp
     */
    public function get_member_join_timestamp($member)
    {
        return $this->get_member_row_field($member, 'r_time');
    }

    /**
     * Find all members with a name matching the given SQL LIKE string.
     *
     * @param  string $pattern The pattern
     * @param  ?integer $limit Maximum number to return (limits to the most recent active) (null: no limit)
     * @return ?array The array of matched members (null: none found)
     */
    public function get_matching_members($pattern, $limit = null)
    {
        $rows = $this->connection->query('SELECT * FROM ' . $this->connection->get_table_prefix() . 'users WHERE username LIKE \'' . db_encode_like($pattern) . '\' AND id<>' . strval($this->get_guest_id()) . ' ORDER BY lastlogin_1 DESC', $limit); // it could be ordered by 'lastlogin' too
        sort_maps_by($rows, 'username');
        return $rows;
    }

    /**
     * Get the given member's post count.
     *
     * @param  MEMBER $member The member ID
     * @return integer The post count
     */
    public function get_post_count($member)
    {
        return $this->get_member_row_field($member, 'posts');
    }

    /**
     * Get the given member's topic count.
     *
     * @param  MEMBER $member The member ID
     * @return integer The topic count
     */
    public function get_topic_count($member)
    {
        return $this->connection->query_select_value('topics', 'COUNT(*)', array('t_mem_id' => $member));
    }

    /**
     * Find out if the given member ID is banned.
     *
     * @param  MEMBER $member The member ID
     * @return boolean Whether the member is banned
     */
    public function is_banned($member)
    {
        $ban_time = $this->get_member_row_field($member, 'temp_ban_time'); // when is banned user
        $ban_period = $this->get_member_row_field($member, 'temp_ban'); // how many days is banned
        $ban_till = $ban_time + $ban_period; // the member is banned until this date/time

        if (empty($ban_till)) {
            return false; // the member is never banned
        } elseif ($ban_till < time()) {
            return false; // the ban time is over
        } else {
            return true; // the member is still banned
        }
    }

    /**
     * Find the base URL to the emoticons.
     *
     * @return URLPATH The base URL
     */
    public function get_emo_dir()
    {
        return get_forum_base_url() . '/smileys/';
    }

    /**
     * Get a map between emoticon codes and templates representing the HTML-image-code for this emoticon. The emoticons presented of course depend on the forum involved.
     *
     * @return array The map
     */
    public function find_emoticons()
    {
        if (!is_null($this->EMOTICON_CACHE)) {
            return $this->EMOTICON_CACHE;
        }
        $rows = $this->connection->query_select('smileys', array('*'));
        $this->EMOTICON_CACHE = array();
        foreach ($rows as $myrow) {
            $src = $myrow['smfile'];
            $smfolder = $myrow['smfolder'];
            if (url_is_local($src)) {
                $src = $this->get_emo_dir() . $smfolder . '/' . $src;
            }
            $this->EMOTICON_CACHE[$myrow['smcode']] = array('EMOTICON_IMG_CODE_DIR', $src, $myrow['smcode']);
        }
        uksort($this->EMOTICON_CACHE, '_strlen_sort');
        $this->EMOTICON_CACHE = array_reverse($this->EMOTICON_CACHE);
        return $this->EMOTICON_CACHE;
    }

    /**
     * Find a list of all forum skins (aka themes).
     *
     * @return array The list of skins
     */
    public function get_skin_list()
    {
        $table = 'themes';
        $codename = 'th_name';

        $rows = $this->connection->query_select($table, array($codename));
        return collapse_1d_complexity($codename, $rows);
    }

    /**
     * Try to find the theme that the logged-in/guest member is using, and map it to a Composr theme.
     * The themes/map.ini file functions to provide this mapping between forum themes, and Composr themes, and has a slightly different meaning for different forum drivers. For example, some drivers map the forum themes theme directory to the Composr theme name, while others made the humanly readeable name.
     *
     * @param  boolean $skip_member_specific Whether to avoid member-specific lookup (i.e. find via what forum theme is currently configured as the default)
     * @param  ?MEMBER $member The member to find for (null: current member)
     * @return ID_TEXT The theme
     */
    public function _get_theme($skip_member_specific = false, $member = null)
    {
        $def = '';

        // Load in remapper
        require_code('files');
        $map = file_exists(get_file_base() . '/themes/map.ini') ? better_parse_ini_file(get_file_base() . '/themes/map.ini') : array();

        // Work out
        if (!$skip_member_specific) {
            if ($member === null) {
                $member = get_member();
            }
            if ($member > 0) {
                $skin = $this->get_member_row_field($member, 'user_theme');
            } else {
                $skin = 0;
            }
            if ($skin > 0) { // User has a custom theme
                $user_theme = $this->connection->query("SELECT * FROM " . $this->connection->get_table_prefix() . "themes t LEFT JOIN " . $this->connection->get_table_prefix() . "theme_registry tr ON tr.thid =t.thid WHERE t.thid=" . strval($skin));

                $user_theme = (!empty($user_theme[0])) ? $user_theme[0] : '';
                $user_theme = (!empty($user_theme['user_theme'])) ? $user_theme['user_theme'] : '';

                if (!is_null($user_theme)) {
                    $def = array_key_exists($user_theme, $map) ? $map[$user_theme] : $user_theme;
                }
            }
        }

        // Look for a skin according to our site name (we bother with this instead of 'default' because Composr itself likes to never choose a theme when forum-theme integration is on: all forum [via map] or all Composr seems cleaner, although it is complex)
        if ((!(strlen($def) > 0)) || (!file_exists(get_custom_file_base() . '/themes/' . $def))) {
            $aeftheme = $this->connection->query_select_value_if_there('themes', 'th_name', array('th_name' => get_site_name()));
            if (!is_null($aeftheme)) {
                $def = array_key_exists($aeftheme, $map) ? $map[$aeftheme] : $aeftheme;
            }
        }

        // Default then!
        if ((!(strlen($def) > 0)) || (!file_exists(get_custom_file_base() . '/themes/' . $def))) {
            $def = array_key_exists('default', $map) ? $map['default'] : 'default';
        }

        return $def;
    }

    /**
     * Find if the specified member ID is marked as staff or not.
     *
     * @param  MEMBER $member The member ID
     * @return boolean Whether the member is staff
     */
    protected function _is_staff($member)
    {
        $user_level = $this->get_member_row_field($member, 'u_member_group');
        if (in_array($user_level, array(1, 2, 3))) {
            return true; // return all administrators + all moderators
        }
        return false;
    }

    /**
     * Find if the specified member ID is marked as a super admin or not.
     *
     * @param  MEMBER $member The member ID
     * @return boolean Whether the member is a super admin
     */
    protected function _is_super_admin($member)
    {
        $user_level = $this->get_member_row_field($member, 'u_member_group');
        if ($user_level == 1) {
            return true;
        }
        return false;
    }

    /**
     * If we can't get a list of admins via a usergroup query, we have to disable the staff filter - else the staff filtering can cause disaster at the point of being turned on (because it can't automatically sync).
     *
     * @return boolean Whether to disable the staff filter
     */
    protected function _disable_staff_filter()
    {
        return true;
    }

    /**
     * Get the number of members currently online on the forums.
     *
     * @return integer The number of members
     */
    public function get_num_users_forums()
    {
        return $this->connection->query_value_if_there('SELECT COUNT(*) FROM ' . $this->connection->get_table_prefix() . 'sessions WHERE time>' . strval(time() - 60 * intval(get_option('users_online_time'))));
    }

    /**
     * Get the number of members registered on the forum.
     *
     * @return integer The number of members
     */
    public function get_members()
    {
        return $this->connection->query_select_value('users', 'COUNT(*)') - 1;
    }

    /**
     * Get the total topics ever made on the forum.
     *
     * @return integer The number of topics
     */
    public function get_topics()
    {
        return $this->connection->query_select_value('topics', 'COUNT(*)');
    }

    /**
     * Get the total posts ever made on the forum.
     *
     * @return integer The number of posts
     */
    public function get_num_forum_posts()
    {
        return $this->connection->query_select_value('posts', 'COUNT(*)');
    }

    /**
     * Get the number of new forum posts.
     *
     * @return integer The number of posts
     */
    protected function _get_num_new_forum_posts()
    {
        return $this->connection->query_value_if_there('SELECT COUNT(*) FROM ' . $this->connection->get_table_prefix() . 'posts WHERE ptime>' . strval(time() - 60 * 60 * 24));
    }

    /**
     * Get a member ID from the given member's username.
     *
     * @param  SHORT_TEXT $name The member name
     * @return MEMBER The member ID
     */
    public function get_member_from_username($name)
    {
        if ($name == do_lang('GUEST')) {
            return $this->get_guest_id();
        }

        return $this->connection->query_select_value_if_there('users', 'id', array('username' => $name));
    }

    /**
     * Get the IDs of the admin groups.
     *
     * @return array The admin group IDs
     */
    protected function _get_super_admin_groups()
    {
        $admin_group = $this->connection->query_select_value_if_there('user_groups', 'member_group', array('mem_gr_name' => 'Administrator'), 'ORDER BY member_group DESC');
        if (is_null($admin_group)) {
            return array();
        }
        return array($admin_group);
    }

    /**
     * Get the IDs of the moderator groups.
     * It should not be assumed that a member only has one group - this depends upon the forum the driver works for. It also does not take the staff site filter into account.
     *
     * @return array The moderator group IDs
     */
    protected function _get_moderator_groups()
    {
        $moderator_group = $this->connection->query_value_if_there('SELECT member_group FROM ' . $this->connection->get_table_prefix() . 'user_groups WHERE mem_gr_name LIKE \'%Moderator%\'');
        if (is_null($moderator_group)) {
            return array();
        }
        return array($moderator_group);
    }

    /**
     * Get the forum usergroup list.
     *
     * @return array The usergroup list
     */
    protected function _get_usergroup_list()
    {
        $results = $this->connection->query('SELECT member_group,mem_gr_name FROM ' . $this->connection->get_table_prefix() . 'user_groups WHERE post_count=-1');
        $mod_results = array();
        foreach ($results as $key => $value) {
            $mod_results[] = array('group_id' => $value['member_group'], 'group_name' => $value['mem_gr_name']);
        }
        $results2 = collapse_2d_complexity('group_id', 'group_name', $mod_results);
        return $results2;
    }

    /**
     * Get the forum usergroup relating to the specified member ID.
     *
     * @param  MEMBER $member The member ID
     * @return array The array of forum usergroups
     */
    protected function _get_members_groups($member)
    {
        if ($member == $this->get_guest_id()) {
            return array(-1);
        }

        $groups = collapse_1d_complexity('u_member_group', $this->connection->query_select('users', array('u_member_group'), array('id' => $member)));

        if (count($groups) <= 1) {
            $all_groups = $this->get_usergroup_list();

            $group = (!empty($groups[0]) || $groups[0] == 0) ? $groups[0] : '';

            if (!array_key_exists($group, $all_groups)) {
                $groups[] = -1;
            }
        }

        return $groups;
    }

    /**
     * generates random strings
     *
     * @param   integer $length The length of the generated string
     * @return  string  The generated string
     */
    public function generateRandStr($length)
    {
        $randstr = "";

        for ($i = 0; $i < $length; $i++) {
            $randnum = mt_rand(0, 61);

            if ($randnum < 10) {
                $randstr .= chr($randnum + 48);
            } elseif ($randnum < 36) {
                $randstr .= chr($randnum + 55);
            } else {
                $randstr .= chr($randnum + 61);
            }
        }

        return strtolower($randstr);
    }

    /**
     * Create a member login cookie.
     *
     * @param  MEMBER $id The member ID
     * @param  ?SHORT_TEXT $name The username (null: lookup)
     * @param  string $password The password
     */
    public function forum_create_cookie($id, $name, $password)
    {
        global $SITE_INFO;

        $cookie_prefix = preg_replace('/\[logpass\]/', '', $SITE_INFO['pass_cookie']);

        unset($name);
        unset($password);

        $row = $this->get_member_row($id);
        $logpass = $row['cookpass']; // cookie var [logpass]
        $loguid = $row['id']; // cookie var [loguid]
        if (empty($logpass)) { // this means that it is not set
            $logpass = $this->generateRandStr(32);
            $this->connection->query_update('users', array('cookpass' => $logpass), array('id' => $id), '', 1);
        }

        // Set a COOKIE of User ID
        cms_setcookie($cookie_prefix . '[loguid]', $loguid);

        // Set a CookPass
        cms_setcookie($cookie_prefix . '[logpass]', $logpass);

        if (substr(get_member_cookie(), 0, 5) != 'cms__') {
            $session_row = $this->connection->query('SELECT * FROM ' . $this->connection->get_table_prefix() . 'sessions WHERE uid=' . strval($id), 1);
            $session_row = (!empty($session_row[0])) ? $session_row[0] : array();
            $session_id = (!empty($session_row['sid'])) ? $session_row['sid'] : '';

            if (!empty($session_id)) {
                $this->connection->query_update('sessions', array('time' => time()), array('uid' => $id), '', 1);
            } else {
                $session_id = strtolower($this->generateRandStr(32));
                $this->connection->query_insert('sessions', array('sid' => $session_id, 'uid' => $id, 'time' => time(), 'data' => '', 'ip' => $row['r_ip']));
            }

            // Now lets try and set a COOKIE of AEF Session ID
            cms_setcookie($cookie_prefix . '[aefsid]', $session_id);
        }
    }

    /**
     * Get a custom md5 for a string
     *
     * @param  string $string The inputted string
     * @param  SHORT_TEXT $member The username
     * @param  SHORT_TEXT $salt The salt
     * @return string The md5-ed string
     */
    public function forum_md5($string, $member = '', $salt = '')
    {
        if (empty($salt) && !empty($member)) {
            $member_row = $this->get_member_row($this->get_member_from_username($member));
            $salt = (!empty($member_row['salt'])) ? $member_row['salt'] : '';
        }
        $string = addslashes(trim($string));
        $string = (!empty($salt)) ? md5($salt . $string) : md5($string);

        return $string;
    }

    /**
     * Find if the given member ID and password is valid. If username is null, then the member ID is used instead.
     * All authorisation, cookies, and form-logins, are passed through this function.
     * Some forums do cookie logins differently, so a Boolean is passed in to indicate whether it is a cookie login.
     *
     * @param  ?SHORT_TEXT $username The member username (null: don't use this in the authentication - but look it up using the ID if needed)
     * @param  MEMBER $userid The member ID
     * @param  SHORT_TEXT $password_hashed The md5-hashed password
     * @param  string $password_raw The raw password
     * @param  boolean $cookie_login Whether this is a cookie login
     * @return array A map of 'id' and 'error'. If 'id' is null, an error occurred and 'error' is set
     */
    public function forum_authorise_login($username, $userid, $password_hashed, $password_raw, $cookie_login = false)
    {
        ////////////////////////////////////////////
        // When a user is considered to be logged-in
        // 1 - Has Session VAR UID
        // 2 - Has a Cookie for login information
        ////////////////////////////////////////////

        global $SITE_INFO;

        $cookie_prefix = preg_replace('/\[logpass\]/', '', $SITE_INFO['pass_cookie']); // get cookie prefix

        $out = array();
        $out['id'] = null;

        if (is_null($userid)) {
            $rows = $this->connection->query_select('users', array('*'), array('username' => $username), '', 1);
            if (array_key_exists(0, $rows)) {
                $this->MEMBER_ROWS_CACHED[$rows[0]['id']] = $rows[0];
            }
        } else {
            $rows = array();
            $rows[0] = $this->get_member_row($userid);
        }

        if (!array_key_exists(0, $rows) || $rows[0] === null) { // All hands to lifeboats
            $out['error'] = do_lang_tempcode((get_option('login_error_secrecy') == '1') ? 'MEMBER_INVALID_LOGIN' : '_MEMBER_NO_EXIST', $username);
            return $out;
        }
        $row = $rows[0];
        if ($this->is_banned($row['id'])) { // All hands to the guns
            $out['error'] = do_lang_tempcode('YOU_ARE_BANNED');
            return $out;
        }

        if ($cookie_login) {
            $cookpass = (!empty($_COOKIE[$cookie_prefix . '[logpass]'])) ? $_COOKIE[$cookie_prefix . '[logpass]'] : '';
            $lookup = $this->connection->query_select_value_if_there('users', 'id', array('cookpass' => $cookpass, 'id' => $row['id']));
            if ($row['id'] !== $lookup) {
                $out['error'] = do_lang_tempcode((get_option('login_error_secrecy') == '1') ? 'MEMBER_INVALID_LOGIN' : 'MEMBER_BAD_PASSWORD');
                return $out;
            }
        } else {
            if ($row['password'] != $password_hashed) {
                $out['error'] = do_lang_tempcode((get_option('login_error_secrecy') == '1') ? 'MEMBER_INVALID_LOGIN' : 'MEMBER_BAD_PASSWORD');
                return $out;
            }
        }

        $out['id'] = $row['id'];
        return $out;
    }

    /**
     * Get a first known IP address of the given member.
     *
     * @param  MEMBER $member The member ID
     * @return IP The IP address
     */
    public function get_member_ip($member)
    {
        $ip = $this->connection->query_select_value_if_there('posts', 'poster_ip', array('poster_id' => $member));
        if (!is_null($ip)) {
            return $ip; // the IP is not stored in phpBB format
        }
        return '';
    }

    /**
     * Gets a whole member row from the database.
     *
     * @param  MEMBER $member The member ID
     * @return ?array The member row (null: no such member)
     */
    public function get_member_row($member)
    {
        if (array_key_exists($member, $this->MEMBER_ROWS_CACHED)) {
            return $this->MEMBER_ROWS_CACHED[$member];
        }

        $rows = $this->connection->query_select('users', array('*'), array('id' => $member), '', 1);
        if (!array_key_exists(0, $rows)) {
            return null;
        }
        $this->MEMBER_ROWS_CACHED[$member] = $rows[0];
        return $this->MEMBER_ROWS_CACHED[$member];
    }

    /**
     * Gets a named field of a member row from the database.
     *
     * @param  MEMBER $member The member ID
     * @param  string $field The field identifier
     * @return mixed The field
     */
    public function get_member_row_field($member, $field)
    {
        $row = $this->get_member_row($member);
        return is_null($row) ? null : $row[$field];
    }
}
