<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing 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  Christopher Graham
 * @package    import
 */

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__hooks__modules__admin_import__smf2()
{
    global $TOPIC_FORUM_CACHE;
    $TOPIC_FORUM_CACHE = [];

    global $STRICT_FILE;
    $STRICT_FILE = false; // Disable this for a quicker import that is quite liable to go wrong if you don't have the files in the right place

    global $ADDITIONAL_DATA;
    $ADDITIONAL_DATA = [];
}

/**
 * Hook class.
 */
class Hook_import_smf2
{
    /**
     * Standard importer hook info function.
     *
     * @return ?array Importer handling details, including lists of all the import types covered (import types are not necessarily the same as actual tables) (null: importer is disabled)
     */
    public function info() : ?array
    {
        require_lang('cns');

        $info = [];
        $info['supports_advanced_import'] = false;
        $info['product'] = 'SMF 2.1.x';
        $info['prefix'] = 'smf_';
        $info['import'] = [
            'config',
            'banners',
            'cns_remove_old_groups',
            'cns_groups',
            'cns_custom_profile_fields',
            'cns_members',
            'cns_member_files',
            'ip_bans',
            'cns_forum_groupings',
            'cns_forums',
            'cns_topics',
            'cns_personal_topics',
            'cns_posts',
            'cns_post_files',
            'cns_polls_and_votes',
            'notifications',
            'wordfilter',
            'calendar',
            'news_and_categories',
        ];

        $info['dependencies'] = [
            // This dependency tree is overdefined, but I wanted to make it clear what depends on what, rather than having a simplified version
           'banners' => ['cns_members'],
           'news_and_categories' => ['cns_members'],
           'cns_members' => ['cns_groups', 'cns_custom_profile_fields'],
           'cns_member_files' => ['cns_members'],
           'cns_forums' => ['cns_forum_groupings', 'cns_members', 'cns_groups'],
           'cns_topics' => ['cns_forums', 'cns_members'],
           'cns_polls_and_votes' => ['cns_topics', 'cns_posts', 'cns_members'],
           'cns_posts' => ['cns_topics', 'cns_members'],
           'cns_post_files' => ['cns_posts', 'cns_personal_topics'],
           'notifications' => ['cns_topics', 'cns_members', 'cns_polls_and_votes'],
           'cns_personal_topics' => ['cns_members'],
           'ip_bans' => ['cns_members'],
        ];

        $_cleanup_url = build_url(['page' => 'admin_cleanup'], get_module_zone('admin_cleanup'));
        $cleanup_url = $_cleanup_url->evaluate();
        $info['final_message'] = do_lang_tempcode('FORUM_CACHE_CLEAR', escape_html($cleanup_url));

        $info['final_tasks'] = [
            ['cns_topics_recache', do_lang('CACHE_TOPICS'), 'f_topics', 100],
            ['cns_recache', do_lang('CACHE_FORUMS'), 'f_topics', 100],
            ['cns_members_recache', do_lang('CACHE_MEMBERS'), 'f_members', 100],
        ];

        return $info;
    }

    /**
     * Probe a file path for DB access details.
     *
     * @param  string $file_base The probe path
     * @return array A quartet of the details (db_name, db_user, db_pass, table_prefix)
     */
    public function probe_db_access(string $file_base) : array
    {
        $db_name = '';
        $db_user = '';
        $db_passwd = '';
        $db_prefix = '';
        $db_server = '';
        if (!file_exists($file_base . '/Settings.php')) {
            warn_exit(do_lang_tempcode('BAD_IMPORT_PATH', escape_html('Settings.php')));
        }
        require $file_base . '/Settings.php';

        return [$db_name, $db_user, $db_passwd, $db_prefix, $db_server];
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     */
    public function import_config(object $db, string $table_prefix, string $file_base)
    {
        $webmaster_email = '';
        $mbname = '';
        $boardurl = '';
        $cookiename = '';
        $auth_secret = null;

        require $file_base . '/Settings.php';

        $rows = $db->query_select('settings');

        $config_remapping = [];
        $config_remapping['staff_address'] = $webmaster_email;
        $config_remapping['site_name'] = $mbname;
        $board_url = $boardurl;

        // Cookie secret
        if ($auth_secret !== null) {
            set_value('smf_auth_secret', $auth_secret);
        }

        global $ADDITIONAL_DATA;

        foreach ($rows as $row) {
            if ($row['variable'] == 'topicSummaryPosts') {
                $config_remapping['forum_posts_per_page'] = $row['value'];
                continue;
            }

            if ($row['variable'] == 'smtp_port') {
                $config_remapping['smtp_sockets_port'] = $row['value'];
                continue;
            }

            if ($row['variable'] == 'smtp_username') {
                $config_remapping['smtp_sockets_username'] = $row['value'];
                continue;
            }

            if ($row['variable'] == 'smtp_password') {
                $config_remapping['smtp_sockets_password'] = $row['value'];
                continue;
            }

            if ($row['variable'] == 'smtp_host') {
                $config_remapping['smtp_sockets_host'] = $row['value'];
                continue;
            }

            if ($row['variable'] == 'mail_type') {
                $config_remapping['smtp_sockets_use'] = ($row['value'] == '1') ? 1 : 0;
                continue;
            }

            if ($row['variable'] == 'databaseSession_lifetime') {
                $config_remapping['session_expiry_time'] = $row['value'];
                continue;
            }

            if ($row['variable'] == 'hotTopicPosts') {
                $config_remapping['hot_topic_definition'] = $row['value'];
                continue;
            }

            if ($row['variable'] == 'reserveNames') {
                $config_remapping['restricted_usernames'] = $row['value'];
                continue;
            }

            if ($row['variable'] == 'defaultMaxTopics') {
                $config_remapping['forum_topics_per_page'] = $row['value'];
                continue;
            }
            /*
            // Disabled due to possible problems on some systems.
            if ($row['variable'] == 'enableCompressedOutput') {
                $config_remapping['output_compression'] = $row['value'];
                continue;
            }
            */
            if ($row['variable'] == 'default_timezone') {
                $config_remapping['timezone'] = str_replace('Etc/GMT+', '', $row['value']);
                continue;
            }

            if ($row['variable'] == 'attachmentNumPerPostLimit') {
                $ADDITIONAL_DATA['maxattachments'] = intval($row['value']);
                continue;
            }

            if ($row['variable'] == 'avatar_max_width_upload') {
                $ADDITIONAL_DATA['avatar_max_width'] = intval($row['value']);
                continue;
            }

            if ($row['variable'] == 'avatar_max_height_upload') {
                $ADDITIONAL_DATA['avatar_max_height'] = intval($row['value']);
                continue;
            }
        }

        foreach ($config_remapping as $key => $value) {
            set_option($key, is_string($value) ? $value : strval($value));
        }
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     */
    public function import_cns_groups(object $db, string $table_prefix)
    {
        global $ADDITIONAL_DATA;

        $avatar_max_width = !empty($ADDITIONAL_DATA['avatar_max_width']) ? $ADDITIONAL_DATA['avatar_max_width'] : 100;
        $avatar_max_height = !empty($ADDITIONAL_DATA['avatar_max_height']) ? $ADDITIONAL_DATA['avatar_max_height'] : 100;
        $max_attachments_upload = !@cms_empty_safe($ADDITIONAL_DATA['maxattachments']) ? $ADDITIONAL_DATA['maxattachments'] : 10;

        $group_leaders = [];
        $grps = $db->query_select('group_moderators', ['*'], [], 'ORDER BY id_group');
        foreach ($grps as $grp) {
            $group_leaders[$grp['id_group']] = $grp['id_member'];
        }

        $rows = $db->query_select('membergroups', ['*'], [], 'ORDER BY id_group');
        foreach ($rows as $row) {
            if (import_check_if_imported('group', strval($row['id_group']))) {
                continue;
            }

            $leader = array_key_exists($row['id_group'], $group_leaders) ? $group_leaders[$row['id_group']] : null;
            if ($row['group_name'] == 'Administrator') {
                $group_name = 'Administrators';
            } elseif ($row['group_name'] == 'Global Moderator') {
                $group_name = 'Super-moderators';
            } else {
                $group_name = $row['group_name'];
            }
            $is_super_admin = 0;
            $is_super_moderator = 0;

            $id_new = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_groups', 'id', [$GLOBALS['FORUM_DB']->translate_field_ref('g_name') => $group_name]);
            if ($id_new === null) {
                $id_new = cns_make_group($group_name, 0, $is_super_admin, $is_super_moderator, '', '', null, null, 0, $leader, null, null, null, $max_attachments_upload, $avatar_max_width, $avatar_max_height, null);
            }

            set_privilege($id_new, 'allow_html', true);

            if (!import_check_if_imported('group', strval($row['id_group']))) {
                import_id_remap_put('group', strval($row['id_group']), $id_new);
            }
        }
        $this->update_group_promotions($db, $table_prefix);
    }

    /**
     * Update Promotion Thresholds for Imported Groups.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     */
    public function update_group_promotions(object $db, string $table_prefix)
    {
        // This will be needed when we finish our cycle to point to the last updated group
        $default_group = get_first_default_group();
        // Set this to Null for the first group updated as we progress it will be updated to the last updated id
        $promotion_target = null;
        // Set this to Null for the first group updated as we progress it will be updated to the last updated threshold
        $promotion_threshold = null;
        // We willl us this to decide whether to update the default group or not.
        $updates = false;

        $rows = $db->query('SELECT id_group,group_name,min_posts FROM ' . $table_prefix . 'membergroups WHERE min_posts>0 ORDER BY min_posts DESC');
        // Lets check we are actually going to be performing some updates
        if (!empty($rows)) {
            $updates = true;
        }
        foreach ($rows as $row) {
            // Cast this to string to be used in remap get function
            $old_id = strval($row['id_group']);
            // Here we get the software ID of the group
            $id_new = import_id_remap_get('group', $old_id, true);
            $GLOBALS['FORUM_DB']->query_update('f_groups', ['g_promotion_target_group' => $promotion_target, 'g_promotion_threshold' => $promotion_threshold], ['id' => $id_new]);
            // On the next run the promotion target will be this last updated group
            $promotion_target = $id_new;
            // On the next run the promotion threshold will be this last updated groups required posts
            $promotion_threshold = $row['min_posts'];
        }
        // Now we've done all the groups based on posts lets check whether to update the default group
        if ($updates) {
            $GLOBALS['FORUM_DB']->query_update('f_groups', ['g_promotion_target_group' => $promotion_target, 'g_promotion_threshold' => $promotion_threshold], ['id' => $default_group]);
        }
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     */
    public function import_cns_members(object $db, string $table_prefix, string $file_base)
    {
        $row_start = 0;
        $rows = [];

        $default_group = get_first_default_group();

        do {
            $rows = $db->query('SELECT * FROM ' . $table_prefix . 'members  WHERE id_member<>-1 ORDER BY id_member', 200, $row_start);

            foreach ($rows as $row) {
                if (import_check_if_imported('member', strval($row['id_member']))) {
                    continue;
                }

                $test = $GLOBALS['CNS_DRIVER']->get_member_from_username($row['member_name']);
                if ($test !== null) {
                    import_id_remap_put('member', strval($row['id_member']), $test);
                    continue;
                }

                $language = 'EN';

                $secondary = explode(',', $row['additional_groups']);
                $secondary_groups = [];
                foreach ($secondary as $g) {
                    if (trim($g) != '') {
                        $g = import_id_remap_get('group', $g, true);
                        if ($g !== null) {
                            $secondary_groups[] = intval($g);
                        }
                    }
                }

                $primary_group = $row['id_group'];
                if ($primary_group == 0) {
                    $primary_group = $row['id_post_group'];
                }
                if ($primary_group == 0) {
                    $primary_group = $default_group;
                } else {
                    $primary_group = import_id_remap_get('group', strval($primary_group));
                }

                $custom_fields = [];

                // Parse SMF's real_name into our first and last name
                $firstname_field = find_cms_cpf_field_id('cms_firstname');
                $lastname_field = find_cms_cpf_field_id('cms_lastname');
                if (($row['real_name'] != '') && (($firstname_field !== null) || ($lastname_field !== null))) {
                    if ($lastname_field !== null) {
                        $name_parts = explode(' ', $row['real_name']);
                        $custom_fields['cms_lastname'] = $name_parts[count($name_parts) - 1];
                        if ($firstname_field !== null) {
                            unset($name_parts[count($name_parts) - 1]);
                            $custom_fields['cms_firstname'] = implode(' ', $name_parts);
                        }
                    } else {
                        $custom_fields['cms_firstname'] = $row['real_name'];
                    }
                }

                if ($row['website_url'] != '') {
                    $custom_fields[cns_make_predefined_content_field('website')] = $row['website_url'];
                }
                $signature = str_replace(['[html]', '[/html]'], ['', ''], html_to_comcode($row['signature']));
                $signature = $this->fix_links($signature, $db, $table_prefix, $file_base);
                $validated = 1;
                $reveal_age = 0;

                if ($row['birthdate'] != '') {
                    $birthdate = $row['birthdate'];
                    $birthdata = explode('-', $birthdate);
                    $bday_year = (empty($birthdata[0]) || (intval($birthdata[0]) < 1100)) ? null : $birthdata[0];
                    $bday_month = empty($birthdata[1]) ? null : $birthdata[1];
                    $bday_day = empty($birthdata[2]) ? null : $birthdata[2];
                } else {
                    list($bday_day, $bday_month, $bday_year) = [null, null, null];
                }

                $views_signatures = 1;
                $preview_posts = 1;
                $title = '';

                // These are done in the members-files stage
                $avatar_url = '';
                $photo_url = '';

                $password = $row['passwd'];
                $type = (substr($password, 0, 4) == '$2y$') ? 'smf2' : 'smf';
                $salt = $row['password_salt'];
                $allow_emails = (intval($row['instant_messages']) > 0) ? 1 : 0;

                if ($row['date_registered'] == 0) {
                    $row['date_registered'] = time();
                }
                $id_new = cns_make_member(
                    $row['member_name'], // username
                    $password, // password
                    $row['email_address'], // email_address
                    $primary_group, // primary_group
                    null, // secondary_groups
                    intval($bday_day), // dob_day
                    intval($bday_month), // dob_month
                    intval($bday_year), // dob_year
                    $custom_fields, // custom_fields
                    ($row['time_offset'] == 0) ? '' : strval($row['time_offset']), // timezone
                    '', // TODO: region
                    $language, // language
                    '', // theme
                    $title, // title
                    $photo_url, // photo_url
                    $avatar_url, // avatar_url
                    $signature, // signature
                    $preview_posts, // preview_posts
                    $reveal_age, // reveal_age
                    $views_signatures, // views_signatures
                    null, // auto_monitor_contrib_content
                    null, // smart_topic_notification
                    null, // mailing_list_style
                    1, // auto_mark_read
                    null, // sound_enabled
                    $allow_emails, // allow_emails
                    1, // allow_emails_from_staff
                    0, // highlighted_name
                    '*', // pt_allow
                    '', // pt_rules_text
                    $validated, // validated
                    '', // validated_email_confirm_code
                    null, // probation_expiration_time
                    '0', // is_perm_banned
                    false, // check_correctness
                    '', // ip_address
                    $type, // password_compatibility_scheme
                    $salt, // salt
                    $row['date_registered'], // join_time
                    $row['last_login'] // last_visit_time
                );

                // CPF stuff
                $cpf_rows = $db->query_select('custom_fields', ['id_field', 'col_name']);
                foreach ($cpf_rows as $cpf_row) {
                    $cpf_id = import_id_remap_get('cpf', strval($cpf_row['id_field']));
                    $cpf_value = $db->query_select('themes', ['value'], ['id_member' => $row['id_member'], 'variable' => $cpf_row['col_name']]);
                    if (!isset($cpf_value[0])) {
                        continue;
                    }
                    $value = isset($cpf_value[0]['value']) ? $cpf_value[0]['value'] : '';
                    cns_set_custom_field($id_new, $cpf_id, $value);
                }

                // Fix usergroup leadership
                $GLOBALS['FORUM_DB']->query_update('f_groups', ['g_group_lead_member' => $id_new], ['g_group_lead_member' => $row['id_member']]);

                import_id_remap_put('member', strval($row['id_member']), $id_new);

                // Set up usergroup membership
                foreach ($secondary_groups as $s) {
                    cns_add_member_to_secondary_group($id_new, $s, 1);
                }
                cns_update_group_approvals($id_new, get_member());
            }

            $row_start += 200;
        } while (!empty($rows));
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     */
    public function import_cns_custom_profile_fields(object $db, string $table_prefix)
    {
        $rows = $db->query_select('custom_fields', ['*']);

        foreach ($rows as $row) {
            if (import_check_if_imported('cpf', strval($row['id_field']))) {
                continue;
            }

            $name = $row['field_name'];
            $id_new = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_custom_fields', 'id', [$GLOBALS['FORUM_DB']->translate_field_ref('cf_name') => $name]);
            if ($id_new === null) {
                $default = $row['default_value'];
                $options = $row['field_options'];
                $pub_view = 1;
                $own_view = 1;
                $own_set = 1;
                $req = 0;
                $on_join = 0;
                $show_in_posts = $row['show_display'];
                $desc = $row['field_desc'];
                $private = intval($row['private']);

                switch ($private) {
                    case 0:
                        $pub_view = 1;
                        $own_view = 1;
                        $own_set = 1;
                        break;
                    case 1:
                        $pub_view = 1;
                        $own_view = 1;
                        $own_set = 0;
                        break;
                    case 2:
                        $pub_view = 0;
                        $own_view = 1;
                        $own_set = 1;
                        break;
                    case 3:
                        $pub_view = 0;
                        $own_view = 0;
                        $own_set = 0;
                        break;
                }

                switch ($row['show_reg']) {
                    case 0:
                        $on_join = 0;
                        $req = 0;
                        break;
                    case 1:
                        $on_join = 1;
                        break;
                    case 2:
                        $on_join = 1;
                        $req = 1;
                        break;
                }

                $type = 'short_text';
                $options = '';
                switch ($row['field_type']) {
                    case 'text':
                        if ($row['bbc'] == 1) {
                            $type = 'short_trans';
                        } else {
                            $type = 'short_text';
                        }
                        break;

                    case 'textarea':
                        $default = '';
                        if ($row['bbc'] == 1) {
                            $type = 'long_trans';
                        } else {
                            $type = 'long_text';
                        }
                        break;

                    case 'select':
                        $type = 'list';
                        break;

                    case 'check':
                        $type = 'tick';
                        break;

                    case 'radio':
                        $type = 'list';
                        $options = 'widget=radio';
                        break;
                }
                if ((!empty($options)) && (!cms_empty_safe($default))) {
                    $def = $this->cpf_options_string($default, $options);
                } else {
                    $def = '';
                }

                $id_new = cns_make_custom_field($name, 0, $desc, $def, $pub_view, $own_view, $own_set, 0, $type, $req, $show_in_posts, 0, null, '', $on_join, $options, 0, 0, '', '', '', false);
            }

            import_id_remap_put('cpf', strval($row['id_field']), $id_new);
        }
    }

    /**
     * Used with cns_custom_profile_fields.
     *
     * @param  string $default Default value
     * @param  string $options List of coma separated options
     * @return string Imploded with pipe
     */
    public function cpf_options_string(string $default, string $options) : string
    {
        $temp_array = [];
        $default = trim($default);
        $temp_array[] = $default;
        $values = explode(',', $options);
        foreach ($values as $value) {
            $value = trim($value);
            if ($value != $default) {
                if ($value == '') {
                    continue;
                }
                $temp_array[] = $value;
            }
        }
        $result = implode('|', $temp_array);
        return $result;
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     */
    public function import_cns_member_files(object $db, string $table_prefix, string $file_base)
    {
        global $STRICT_FILE;
        require $file_base . '/Settings.php';

        $avatar_path = $this->get_setting($db, 'custom_avatar_dir');
        $avatar_gallery_path = $this->get_setting($db, 'avatar_directory');

        $row_start = 0;
        $rows = [];
        do {
            $query = 'SELECT id_member,avatar FROM ' . $table_prefix . 'members WHERE id_member<>-1 ORDER BY id_member';

            $rows = $db->query($query, 200, $row_start);
            foreach ($rows as $row) {
                if (import_check_if_imported('member_files', strval($row['id_member']))) {
                    continue;
                }

                $member_id = import_id_remap_get('member', strval($row['id_member']));

                $avatar_url = '';
                if (empty($row['avatar'])) {
                    $query_attachments = 'SELECT id_member,filename,width,height,size,attachment_type FROM ' . $table_prefix . 'attachments WHERE ' . db_string_equal_to('attachment_type', '1') . ' AND ' . db_string_equal_to('id_member', strval($row['id_member']));

                    $attachment_data = $db->query($query_attachments, 1, 0);
                    if (!empty($attachment_data[0]['filename'])) {
                        // Uploaded avatar
                        $filename = $attachment_data[0]['filename'];
                        if ((file_exists(get_custom_file_base() . '/uploads/cns_avatars/' . $filename)) || (@copy($avatar_path . '/' . $filename, get_custom_file_base() . '/uploads/cns_avatars/' . $filename))) {
                            $avatar_url = 'uploads/cns_avatars/' . $filename;
                            sync_file(get_custom_file_base() . '/' . $avatar_url);
                        } else {
                            if ($STRICT_FILE) {
                                warn_exit(do_lang_tempcode('MISSING_AVATAR', escape_html($filename)));
                            }
                            $avatar_url = '';
                        }
                    }
                } else {
                    if (!url_is_local($row['avatar'])) {
                        // Remote file is set as avatar
                        $avatar_url = $row['avatar'];
                    } elseif (strlen($row['avatar']) > 0) {
                        // Gallery
                        $filename_with_subdir = $row['avatar'];
                        $filename = preg_replace('#.*\/#', '', $filename_with_subdir); // We need just a filename
                        $subdir = strrpos($filename, '/');

                        if ((file_exists(get_custom_file_base() . '/uploads/cns_avatars/' . $filename)) || (@copy($avatar_gallery_path . '/' . $filename_with_subdir, get_custom_file_base() . '/uploads/cns_avatars/' . $filename))) {
                            $avatar_url = 'uploads/cns_avatars/' . substr($filename, (($subdir !== false) ? $subdir : 0));
                            sync_file(get_custom_file_base() . '/' . $avatar_url);
                        } else {
                            // Try as a pack avatar then
                            $striped_filename = str_replace('/', '_', $filename);
                            if (file_exists(get_custom_file_base() . '/uploads/cns_avatars/' . $striped_filename)) {
                                $avatar_url = 'uploads/cns_avatars/' . substr($filename, (($subdir !== false) ? $subdir : 0));
                            } else {
                                if ($STRICT_FILE) {
                                    warn_exit(do_lang_tempcode('MISSING_AVATAR', escape_html($filename)));
                                }
                                $avatar_url = '';
                            }
                        }
                    }
                }

                $GLOBALS['FORUM_DB']->query_update('f_members', ['m_avatar_url' => $avatar_url], ['id' => $member_id], '', 1);

                import_id_remap_put('member_files', strval($row['id_member']), 1);
            }

            $row_start += 200;
        } while (!empty($rows));
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     */
    public function import_ip_bans(object $db, string $table_prefix, string $file_base)
    {
        require_code('failure');

        $rows = $db->query_select('ban_groups u LEFT JOIN ' . $table_prefix . 'ban_items b ON u.id_ban_group=b.id_ban_group', ['*']);

        foreach ($rows as $row) {
            if (import_check_if_imported('ip_ban_group', strval($row['id_ban_group']))) {
                continue;
            }

            $ban_till = $row['expire_time']; // Member is banned until

            if (($ban_till === null) || ($ban_till > time())) {
                $uid = ($row['id_member'] > 0) ? import_id_remap_get('member', strval($row['id_member'])) : 0;

                // Member banning
                if (($uid !== null) && ($row['id_member'] > 1)) { // 0 = no member-based banning, and 1 = our super-admin which should never be banned
                    if ($ban_till === null) {
                        if (($row['cannot_access'] == 1) || ($row['cannot_login'] == 1)) { // Permanent ban
                            $GLOBALS['FORUM_DB']->query_update('f_members', ['m_is_perm_banned' => '1'], ['id' => $uid]);
                        } elseif ($row['cannot_post'] == 1) { // "Permanent" probation
                            $GLOBALS['FORUM_DB']->query_update('f_members', ['m_probation_expiration_time' => 2147483647/*TODO: #3046 in tracker*/], ['id' => $uid]);
                        }
                    } else {
                        // treat temporary bans as probation since the software does not have temporary member bans
                        $GLOBALS['FORUM_DB']->query_update('f_members', ['m_probation_expiration_time' => $ban_till], ['id' => $uid]);
                    }
                }

                // IP banning
                if (($row['ip_low'] !== null) && ($row['ip_high'] !== null)) {
                    $ip_low = inet_ntop($row['ip_low']);
                    $ip_high = inet_ntop($row['ip_high']);

                    if (($ip_low == $ip_high) && (!import_check_if_imported('ip_ban', $ip_low))) {
                        add_ip_ban($ip_low, 'SMF imported ban', $ban_till, true, true);
                        import_id_remap_put('ip_ban', $ip_low, 0);
                    }
                }
            }

            import_id_remap_put('ip_ban_group', strval($row['id_ban_group']), 0);
        }
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $old_base_dir The base directory we are importing from
     */
    public function import_cns_forum_groupings(object $db, string $table_prefix, string $old_base_dir)
    {
        $rows = $db->query_select('categories');
        foreach ($rows as $row) {
            if (import_check_if_imported('category', strval($row['id_cat']))) {
                continue;
            }

            $title = $row['name'];
            $title = @html_entity_decode($title, ENT_QUOTES);

            $test = $GLOBALS['FORUM_DB']->query_select_value_if_there('f_forum_groupings', 'id', ['c_title' => $title]);
            if ($test !== null) {
                import_id_remap_put('category', strval($row['id_cat']), $test);
                continue;
            }

            $id_new = cns_make_forum_grouping($title, '', 1);

            import_id_remap_put('category', strval($row['id_cat']), $id_new);
        }
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $old_base_dir The base directory we are importing from
     */
    public function import_cns_forums(object $db, string $table_prefix, string $old_base_dir)
    {
        require_code('cns_forums_action2');

        $remap_id = [];

        $rows = $db->query_select('boards');
        foreach ($rows as $row) {
            $remapped = import_id_remap_get('forum', strval($row['id_board']), true);
            if ($remapped !== null) {
                continue;
            }

            $name = html_entity_decode($row['name'], ENT_QUOTES);

            $description = str_replace(['[html]', '[/html]'], ['', ''], html_to_comcode($row['description']));

            $position = $row['board_order'];
            $post_count_increment = 1;

            $parent_forum = ($row['id_parent'] > 0) ? $row['id_parent'] : db_get_first_id();
            $cat_id = $row['id_cat'];

            $profile_id = $row['id_profile'];
            $redirection = (!empty($row['redirect'])) ? $row['redirect'] : '';

            $category_id = import_id_remap_get('category', strval($cat_id), true);

            $access_mapping = null;

            $id_new = cns_make_forum($name, $description, $category_id, $access_mapping, $parent_forum, $position, $post_count_increment, 0, '', '', $redirection);
            $done_all_groups = false;
            /// Now lets do Permissions
            if (($row['member_groups'] != '') && ($profile_id !== null)) {
                $permissions_on_groups = array_map('intval', explode(',', $row['member_groups']));
                if (in_array('0', $permissions_on_groups)) {
                    $this->fill_static_perms_all($profile_id, $id_new, $db, $table_prefix);
                    $done_all_groups = true;
                }
                foreach ($permissions_on_groups as $gid) {
                    // Let's deal with Guest group
                    if ($gid == -1) {
                        // Yup its Guests so let's get value
                        $v = $this->get_role_value(-1, $profile_id, $db, $table_prefix);
                        // Now set Forum view access
                        $this->set_forum_view_access(1, $id_new);
                        // Now Set the static permissions array
                        $this->static_perm_arr(1, $profile_id, $v);
                        continue;
                    }
                    if ($gid == 0) {
                        continue; // We already did all groups so skip
                    }
                    if ($gid == 1) {
                        continue; // Admin so skip
                    }
                    // Check for SMF Global Moderators Group and map to cms SuperModerators
                    if ($gid == 2) {
                        // Yup its Moderators so let's get value
                        $v = $this->get_role_value(2, $profile_id, $db, $table_prefix);
                        // Now set Forum view access
                        if (!$done_all_groups) {
                            $this->set_forum_view_access(3, $id_new);
                        }
                        // Now Set the static permissions array
                        $this->static_perm_arr(3, $profile_id, $v);
                        continue;
                    }
                    // Ok now it's regular groups so lets deal with them
                    // First let's get value
                    $v = $this->get_role_value($gid, $profile_id, $db, $table_prefix);
                    // Get the mapped group id
                    $new_gid = import_id_remap_get('group', strval($gid), true);
                    // Now set Forum view access
                    if ((!$done_all_groups) && ($new_gid !== null)) {
                        $this->set_forum_view_access($new_gid, $id_new);
                    }
                    // Now Set the static permissions array
                    if ($new_gid !== null) {
                        $this->static_perm_arr($new_gid, $profile_id, $v);
                    }
                }
                // Let's get the Array map of groups and profile IDs
                $arr = $this->static_perm_arr(0, $profile_id, $v, true);
                // Now let's set permissions as we cycle through the static array map and apply permission based on best values
                $this->sort_set_forum_perms_array($arr, $id_new);
            }

            $remap_id[$row['id_board']] = $id_new;
            import_id_remap_put('forum', strval($row['id_board']), $id_new);
        }

        // Now we must fix parenting
        foreach ($rows as $row) {
            if (($row['id_parent'] !== null) && (isset($remap_id[$row['id_board']]))) {
                $parent_id = array_key_exists($row['id_parent'], $remap_id) ? $remap_id[$row['id_parent']] : db_get_first_id();
                $GLOBALS['FORUM_DB']->query_update('f_forums', ['f_parent_forum_id' => $parent_id], ['id' => $remap_id[$row['id_board']]], '', 1);
            }
        }
    }

    /**
     * Fills the static_perm_arr with profile permissions for all software groups.
     *
     * @param  integer $pid Profile ID to use
     * @param  integer $fid Forum ID to use
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     */
    public function fill_static_perms_all(int $pid, int $fid, object $db, string $table_prefix)
    {
        // Get the permission profile
        $v = $this->get_role_value(0, $pid, $db, $table_prefix);

        $rows = $GLOBALS['FORUM_DB']->query_select('f_groups', ['id', 'g_name']);
        foreach ($rows as $row) {
            $gid = $row['id'];

            // Set them to view
            $this->set_forum_view_access($gid, $fid);

            // Fill the static profile array map
            $this->static_perm_arr($gid, $pid, $v);
        }
    }

    /**
     * Gets the role value for permissions of group.
     *
     * @param  integer $gid Group ID to use
     * @param  integer $pid Profile ID to use
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @return integer The role value
     */
    public function get_role_value(int $gid, int $pid, object $db, string $table_prefix) : int
    {
        // Set default 0 as Read Only
        $v = 0;
        // Get the permission profile
        $permissions = $db->query_select('board_permissions', ['*'], ['id_group' => $gid, 'id_profile' => $pid]);
        // Loop it
        foreach ($permissions as $p) {
            // Close as we can guess to a Post Role
            if ($p['permission'] == 'post_reply_any' && $p['add_deny'] == 1 && $v < 1) {
                $v = 1;
            }
            // Close as we can guess to an unvetted Role
            if ($p['permission'] == 'post_unapproved_topics' && $p['add_deny'] == 1 && $v < 2) {
                $v = 2;
            }
            // Close as we can guess to a moderator Role
            if ($p['permission'] == 'moderate_board' && $p['add_deny'] == 1 && $v < 3) {
                $v = 3;
            }
        }
        // Now send it back
        return $v;
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     */
    public function import_cns_topics(object $db, string $table_prefix, string $file_base)
    {
        $row_start = 0;
        $rows = [];
        do {
            $rows = $db->query('SELECT * FROM ' . $table_prefix . 'topics t LEFT JOIN ' . $table_prefix . 'messages m ON t.id_first_msg=m.id_msg', 200, $row_start);
            foreach ($rows as $row) {
                if (import_check_if_imported('topic', strval($row['id_topic']))) {
                    continue;
                }

                $forum_id = import_id_remap_get('forum', strval($row['id_board']));
                $emoticon = $this->convert_topic_emoticon($row['icon']);
                $id_new = cns_make_topic($forum_id, $row['subject'], $emoticon, 1, ($row['locked'] == 0) ? 1 : 0, 0, 0, null, null, false, $row['num_views']);

                import_id_remap_put('topic', strval($row['id_topic']), $id_new);
            }

            $row_start += 200;
        } while (!empty($rows));
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     */
    public function import_cns_posts(object $db, string $table_prefix, string $file_base)
    {
        global $STRICT_FILE;

        $row_start = 0;

        // Optimisation to speed through quickly, as can be slow scrolling through so many posts we may have already imported!
        do {
            $rows = $db->query('SELECT p.id_msg FROM ' . $table_prefix . 'messages p ORDER BY p.id_msg', 1, $row_start + 200 - 1);
            if ((!array_key_exists(0, $rows)) || (!import_check_if_imported('post', strval($rows[0]['id_msg'])))) {
                break;
            }

            $row_start += 200;
        } while (true);

        $rows = [];
        do {
            $rows = $db->query_select('messages p', ['*'], [], 'ORDER BY p.id_msg', 200, $row_start);
            foreach ($rows as $row) {
                if (import_check_if_imported('post', strval($row['id_msg']))) {
                    continue;
                }

                $topic_id = import_id_remap_get('topic', strval($row['id_topic']), true);
                if ($topic_id === null) {
                    import_id_remap_put('post', strval($row['id_msg']), -1);
                    continue;
                }
                $member_id = import_id_remap_get('member', strval($row['id_member']), true);
                if ($member_id === null) {
                    $member_id = db_get_first_id();
                }

                $forum_id = import_id_remap_get('forum', strval($row['id_board']), true);

                $title = $row['subject'];
                $title = @html_entity_decode($title, ENT_QUOTES);

                $post_description = str_replace(['[html]', '[/html]'], ['', ''], $row['body']);
                $post_description = '[semihtml]' . $post_description . '[/semihtml]';

                $post = $this->fix_links($post_description, $db, $table_prefix, $file_base);

                $last_edit_member = null;
                $last_edit_time = $row['modified_time'];

                $post_username = $row['poster_name'];

                $id_new = cns_make_post($topic_id, $title, $post, 0, true, 1, 0, $post_username, $row['poster_ip'], $row['poster_time'], $member_id, null, $last_edit_time, $last_edit_member, false, false, $forum_id, false);

                import_id_remap_put('post', strval($row['id_msg']), $id_new);
            }

            $row_start += 200;
        } while (!empty($rows));
    }

    /**
     * Substitution callback for 'fix_links'.
     *
     * @param  array $m The match
     * @return string The substitution string
     */
    protected function _fix_links_callback_topic(array $m) : string
    {
        return 'index.php?topic=' . strval(import_id_remap_get('topic', $m[2], true));
    }

    /**
     * Substitution callback for 'fix_links'.
     *
     * @param  array $m The match
     * @return string The substitution string
     */
    protected function _fix_links_callback_forum(array $m) : string
    {
        return 'index.php?board=' . strval(import_id_remap_get('forum', $m[2], true));
    }

    /**
     * Substitution callback for 'fix_links'.
     *
     * @param  array $m The match
     * @return string The substitution string
     */
    protected function _fix_links_callback_member(array $m) : string
    {
        return 'index.php?action=profile;u=' . strval(import_id_remap_get('member', $m[2], true));
    }

    /**
     * Convert SMF URLs pasted in text fields into software ones.
     *
     * @param  string $post The text field text (e.g. a post)
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     * @return string The new text field text
     */
    public function fix_links(string $post, object $db, string $table_prefix, string $file_base = '') : string
    {
        $boardurl = '';

        require $file_base . '/Settings.php';
        $old_base_url = $boardurl;

        $post = preg_replace_callback('#' . preg_quote($old_base_url) . '/(index\.php\?topic=)(\d*)#', [$this, '_fix_links_callback_topic'], $post);
        $post = preg_replace_callback('#' . preg_quote($old_base_url) . '/(index\.php\?board=)(\d*)#', [$this, '_fix_links_callback_forum'], $post);
        $post = preg_replace_callback('#' . preg_quote($old_base_url) . '/(index\.php\?action=profile;u=)(\d*)#', [$this, '_fix_links_callback_member'], $post);
        $post = preg_replace('#:[0-9a-f]{10}#', '', $post);
        return $post;
    }

    /**
     * Convert an SMF database file to a software uploaded file (stored on disk).
     *
     * @param  string $data The file data
     * @param  string $filename The optimal filename
     * @param  ID_TEXT $sections The upload type (e.g. cns_photos)
     * @param  integer $id_folder The attachmentUploadDir where the file is located
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  string $output_filename The filename to output to
     * @param  PATH $file_base The base directory we are importing from
     * @param  string $attachment_id The attachment ID
     * @param  string $ext The file extension to use
     * @return URLPATH The URL
     */
    public function data_to_disk(string $data, string $filename, string $sections, int $id_folder, object $db, string $table_prefix = '', string $output_filename = '', string $file_base = '', string $attachment_id = '', string $ext = '.png') : string
    {
        $boardurl = '';
        $boarddir = '';

        require $file_base . '/Settings.php';
        $homeurl = $boardurl;

        $forum_dir = preg_replace('#\\\\#', '/', $boarddir); // Full path to the forum folder

        $attachments_dirs = json_decode($this->get_setting($db, 'attachmentUploadDir'), true);
        $attachments_dir = $attachments_dirs[strval($id_folder)]; // Forum attachments directory

        $filename_fixed = $filename . $ext;
        $file_path = $attachments_dir . '/' . $filename . '.dat';
        $data = ($data == '') ? cms_file_get_contents_safe($file_path, FILE_READ_LOCK) : $data;
        $filename = ($output_filename == '') ? $filename_fixed : $output_filename;

        list($path, $url) = find_unique_path('uploads/' . $sections, $filename);

        require_code('files');
        cms_file_put_contents_safe($path, $data, FILE_WRITE_FIX_PERMISSIONS | FILE_WRITE_SYNC_FILE);

        return $url;
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     */
    public function import_cns_post_files(object $db, string $table_prefix, string $file_base)
    {
        global $STRICT_FILE;
        require_code('attachments2');
        require_code('attachments3');

        $row_start = 0;
        $rows = [];
        do {
            $rows = $db->query('SELECT * FROM ' . $table_prefix . 'attachments a JOIN ' . $table_prefix . 'messages m ON a.id_msg=m.id_msg WHERE a.id_msg<>0', 200, $row_start);
            foreach ($rows as $row) {
                if (substr($row['filename'], -6) == '_thumb') {
                    continue;
                }
                if (import_check_if_imported('post_files', strval($row['id_attach']))) {
                    continue;
                }

                $post_id = import_id_remap_get('post', strval($row['id_msg']));

                $post_row = $GLOBALS['FORUM_DB']->query_select('f_posts', ['p_time', 'p_posting_member', 'p_post'], ['id' => $post_id], '', 1);
                if (!array_key_exists(0, $post_row)) {
                    import_id_remap_put('post_files', strval($row['id_attach']), 1);
                    continue; // Orphaned post
                }
                $post = get_translated_text($post_row[0]['p_post']);
                $member_id = $post_row[0]['p_posting_member'];
                $ext = '.' . $row['fileext'];
                $filename = strval($row['id_attach']) . '_' . $row['file_hash'];

                $url = $this->data_to_disk('', $filename, 'attachments', $row['id_folder'], $db, $table_prefix, $row['filename'], $file_base, strval($row['id_attach']), $ext);
                $a_id = $GLOBALS['FORUM_DB']->query_insert('attachments', ['a_member_id' => $member_id, 'a_file_size' => $row['size'], 'a_url' => $url, 'a_thumb_url' => $url, 'a_original_filename' => $row['filename'], 'a_num_downloads' => $row['downloads'], 'a_last_downloaded_time' => null, 'a_add_time' => $row['poster_time'], 'a_description' => ''], true);

                $GLOBALS['FORUM_DB']->query_insert('attachment_refs', ['r_referer_type' => 'cns_post', 'r_referer_id' => strval($post_id), 'a_id' => $a_id]);

                // Replace the SMF attach tag with the Comcode attachment tag, or append to the end of the post if not found.
                $post_before = $post;
                $comcode_for_attachment = '[attachment]' . strval($a_id) . '[/attachment]';
                $post = preg_replace('#\[attach[^\]]*\sid=' . strval($row['id_attach']) . '[^\]]*\](.*)\[\/attach\]#Ui', $comcode_for_attachment, $post);
                if ($post == $post_before) {
                    $post .= "\n\n" . $comcode_for_attachment;
                }

                $GLOBALS['FORUM_DB']->query_update('f_posts', update_lang_comcode_attachments('p_post', $post_row[0]['p_post'], $post, 'cns_post', strval($post_id)), ['id' => $post_id], '', 1);

                import_id_remap_put('post_files', strval($row['id_attach']), 1);
            }

            $row_start += 200;
        } while (!empty($rows));
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     */
    public function import_cns_polls_and_votes(object $db, string $table_prefix, string $file_base)
    {
        $rows = $db->query_select('polls', ['*']);
        foreach ($rows as $row) {
            if (import_check_if_imported('poll', strval($row['id_poll']))) {
                continue;
            }

            $poll_topic_id = $db->query_select('topics', ['id_topic'], ['id_poll' => $row['id_poll']]);
            if (!isset($poll_topic_id[0]['id_topic'])) {
                continue;
            }
            $poll_topic_id = $poll_topic_id[0]['id_topic'];

            $topic_id = import_id_remap_get('topic', strval($poll_topic_id), true);
            if ($topic_id === null) {
                import_id_remap_put('poll', strval($row['id_poll']), -1);
                continue;
            }

            $is_open = ($row['voting_locked'] == 0);
            // $is_open = ($row['expire_time'] == 0 || $row['expire_time'] > time()) ? 1 : 0;

            $answers = [];
            $poll_choices = $db->query_select('poll_choices', ['*'], ['id_poll' => $row['id_poll']]);

            $answers_array = [];
            foreach ($poll_choices as $key => $value) {
                $answers_array[] = $value['label'];
            }

            $answer_map = [];
            $maximum = 0;
            foreach ($answers_array as $key => $answer) {
                $answer_map[$key] = $poll_choices[$key]['votes'];
                $answers[$key] = $answer;
                $maximum += $poll_choices[$key]['votes'];
            }

            $rows2 = $db->query_select('log_polls', ['*'], ['id_poll' => $row['id_poll']]);
            foreach ($rows2 as $row2) {
                $row2['id_member'] = import_id_remap_get('member', strval($row2['id_member']), true);
            }

            $id_new = cns_make_poll($topic_id, $row['question'], ($row['hide_results'] == 2) ? 1 : 0, ($row['voting_locked'] == 0) ? 1 : 0, 1, $maximum, 0, $answers, 0, $row['change_vote'], $row['guest_vote'], 0, false, ($row['expire_time'] == 0) ? null : $row['expire_time']);

            $answers = collapse_1d_complexity('id', $GLOBALS['FORUM_DB']->query_select('f_poll_answers', ['id'], ['pa_poll_id' => $id_new]));

            foreach ($rows2 as $row2) {
                $member_id = $row2['id_member'];
                if (!empty($member_id)) {
                    if (!isset($answers[strval($row2['id_choice'])])) {
                        continue; // Safety
                    }

                    $answer = $answers[strval($row2['id_choice'])];

                    $GLOBALS['FORUM_DB']->query_insert('f_poll_votes', ['pv_poll_id' => $id_new, 'pv_member_id' => $member_id, 'pv_answer_id' => $answer, 'pv_ip_address' => '', 'pv_revoked' => 0, 'pv_date_time' => 0, 'pv_points_when_voted' => 0]);
                }
            }

            import_id_remap_put('poll', strval($row['id_poll']), $id_new);
        }
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $old_base_dir The base directory we are importing from
     */
    public function import_cns_personal_topics(object $db, string $table_prefix, string $old_base_dir)
    {
        $member_rows = $db->query_select('members', ['id_member']);
        foreach ($member_rows as $member_row) {
            $member_id = $member_row['id_member'];
            $rows = $db->query('SELECT * FROM ' . $table_prefix . 'personal_messages p LEFT JOIN ' . $table_prefix . 'pm_recipients r ON p.id_pm=r.id_pm WHERE r.id_member=' . strval($member_id) . ' OR id_member_from=' . strval($member_id) . ' ORDER BY msgtime');

            // Group them up into what will become topics
            $groups = [];
            foreach ($rows as $row) {
                // Do some fiddling around for duplication
                if ($row['id_member_from'] > $row['id_member']) {
                    $a = $row['id_member'];
                    $b = $row['id_member_from'];
                } else {
                    $a = $row['id_member_from'];
                    $b = $row['id_member'];
                }
                $row['subject'] = str_replace('Re: ', '', $row['subject']);
                $groups[strval($a) . ':' . strval($b) . ':' . $row['subject']][] = $row;
            }

            $msg_id = 0;
            // Import topics
            foreach ($groups as $group) {
                $row = $group[0];
                $msg_id++;

                if (import_check_if_imported('pt', strval($row['id_pm']))) {
                    continue;
                }

                // Create topic
                $from_id = import_id_remap_get('member', strval($row['id_member_from']), true);
                if ($from_id === null) {
                    $from_id = $GLOBALS['CNS_DRIVER']->get_guest_id();
                }
                $to_id = import_id_remap_get('member', strval($row['id_member']), true);
                if ($to_id === null) {
                    $to_id = $GLOBALS['CNS_DRIVER']->get_guest_id();
                }
                $topic_id = cns_make_topic(null, '', '', 1, 1, 0, 0, $from_id, $to_id, false);

                $first_post = true;
                foreach ($group as $_post) {
                    if ($first_post) {
                        $title = $row['subject'];
                    } else {
                        $title = '';
                    }

                    $title = @html_entity_decode($title, ENT_QUOTES);

                    $post_description = str_replace(['[html]', '[/html]'], ['', ''], html_to_comcode($_post['body']));

                    $post = $this->fix_links($post_description, $db, $table_prefix, $old_base_dir);
                    $validated = 1;
                    $from_id = import_id_remap_get('member', strval($_post['id_member_from']), true);
                    if ($from_id === null) {
                        $from_id = $GLOBALS['CNS_DRIVER']->get_guest_id();
                    }
                    $poster_name_if_guest = $GLOBALS['CNS_DRIVER']->get_username($from_id);
                    $ip_address = '';
                    $time = $_post['msgtime'];
                    $poster = $from_id;
                    $last_edit_time = null;
                    $last_edit_member = null;

                    cns_make_post($topic_id, $title, $post, 0, $first_post, $validated, 0, $poster_name_if_guest, $ip_address, $time, $poster, null, $last_edit_time, $last_edit_member, false, false, null, false);
                    $first_post = false;
                }

                import_id_remap_put('pt', strval($row['id_pm']), $topic_id);
            }
        }
    }

    /**
     * Convert a SMF topic icon code into a standard software theme image code.
     *
     * @param  string $icon smf icon
     * @return ID_TEXT Software code
     */
    public function convert_topic_emoticon(string $icon) : string
    {
        switch ($icon) {
            case 'smiley':
                return 'cns_emoticons/smile';
            case 'wink':
                return 'cns_emoticons/wink';
            case 'cheesy':
                return 'cns_emoticons/cheeky';
            case 'grin':
                return 'cns_emoticons/grin';
            case 'angry':
                return 'cns_emoticons/angry';
            case 'sad':
                return 'cns_emoticons/sad';
            case 'exclamation':
                return 'cns_emoticons/shocked';
            case 'lamp':
                return 'cns_emoticons/cool';
            case 'question':
                return 'cns_emoticons/blink';
            case 'thumbup':
                return 'cns_emoticons/thumbs';
            case 'thumbdown':
                return 'cns_emoticons/hand';
            case 'xx':
                return '';
        }
        return '';
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     */
    public function import_notifications(object $db, string $table_prefix, string $file_base)
    {
        require_code('notifications');

        $row_start = 0;
        $rows = [];
        do {
            $rows = $db->query('SELECT * FROM ' . $table_prefix . 'log_notify WHERE id_topic<>0', 200, $row_start);
            foreach ($rows as $row) {
                if (import_check_if_imported('topic_notification', strval($row['id_topic']) . '-' . strval($row['id_member']))) {
                    continue;
                }

                $member_id = import_id_remap_get('member', strval($row['id_member']), true);
                if ($member_id === null) {
                    continue;
                }
                $topic_id = import_id_remap_get('topic', strval($row['id_topic']), true);
                if ($topic_id === null) {
                    continue;
                }
                set_notifications('cns_topic', strval($topic_id), $member_id);

                import_id_remap_put('topic_notification', strval($row['id_topic']) . '-' . strval($row['id_member']), 1);
            }

            $row_start += 200;
        } while (!empty($rows));
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     */
    public function import_wordfilter(object $db, string $table_prefix, string $file_base)
    {
        $censor_vulgar = preg_split('/[\n\r]+/', $this->get_setting($db, 'censor_vulgar'));
        $censor_proper = preg_split('/[\n\r]+/', $this->get_setting($db, 'censor_proper'));

        foreach ($censor_vulgar as $key => $row) {
            add_wordfilter_word($censor_vulgar[$key], array_key_exists($key, $censor_proper) ? $censor_proper[$key] : '');
        }
    }

    /**
     * Used to build privilege permission access to Forums.
     *
     * @param  integer $gid group ID to map
     * @param  integer $pid profile ID to map
     * @param  integer $v Value to map
     * @param  boolean $r Whether to return the map or not
     * @return array The mapped Groups and Profile with highest privilege calculated
     */
    public function static_perm_arr(int $gid, int $pid, int $v, bool $r = false) : array
    {
        // Create the static array
        static $st = [];
        // Are we returning this run or not?
        if ($r) {
            // Yes we are so lets move the static array to a temp one so we can unset it
            $rt = $st;
            // Ok Cleanup
            unset($st);
            // Now send back the info
            return $rt;
        }
        // Lets see if we've added the group to map yet?
        if (array_key_exists($gid, $st)) {
            // Yes we have so lets check if the Forum ID for this group?
            if (array_key_exists($pid, $st[$gid])) {
                // Yes we have now let's check if its current value is less than the on sent
                if ($st[$gid][$pid] < $v) {
                    // Yes it is so let's update it to the new higher value
                    $st[$gid][$pid] = $v;
                }
            } else {
                // No the Forum ID wasn't set yet so we can set it with the Value
                $st[$gid][$pid] = $v;
            }
        } else {
            // No the Group ID wasn't set yet so we can set it with the Forum ID and Value
            $st[$gid] = [$pid => $v];
        }
        return $st;
    }

    /**
     * Used to set view access to Forums.
     *
     * @param  integer $gid The group ID to set
     * @param  integer $fid The forum ID to set
     */
    public function set_forum_view_access(int $gid, int $fid)
    {
        // Make a Compounded Name ID for the importer map
        $check_import_id = strval($gid) . '_' . strval($fid);
        // Now Check we didn't import this already?
        if (!import_check_if_imported('forum_view', $check_import_id)) {
            // We didn't so update
            $GLOBALS['FORUM_DB']->query_insert('group_category_access', [
                'module_the_name' => 'forums',
                'category_name' => strval($fid),
                'group_id' => $gid,
            ]);
            // Now we put the remap in so we know it is imported.
            import_id_remap_put('forum_view', $check_import_id, 1);
        }
    }

    /**
     * Used to set permissions for Forums.
     *
     * @param  array $arr The static array map built from static_perm_arr
     * @param  integer $forum_id The forum ID to set
     */
    public function sort_set_forum_perms_array(array $arr, int $forum_id)
    {
        // Let's start the cycle
        foreach ($arr as $key => $val) {
            if (empty($key)) {
                continue; // That's the one we set for return so skip!
            }
            // Set default value of 0 lowest so we can sort to highest
            $v = 0;
            // So it should be!
            if (is_array($val)) {
                // And it is so let's loop it!
                foreach ($val as $key2 => $val2) {
                    // ok check what's highest and set if needed
                    if ($val2 > $v) {
                        $v = $val2;
                    }
                }
            } else {
                if ($val > $v) {
                    $v = $val;
                }
            }
            // Now set the permissions!
            $this->set_forums_perms($key, $forum_id, $v);
        }
    }

    /**
     * Set Forum Permissions.
     *
     * @param  integer $group_id The group ID to set
     * @param  integer $forum_id The forum ID to set
     * @param  integer $role The basic Role they have: 0=ReadOnly, 1=Post/Submit, 2=Unvetted, 3=Moderate
     */
    public function set_forums_perms(int $group_id, int $forum_id, int $role = 0)
    {
        // Make a Compounded Name ID for the importer map
        $check_import_id = strval($group_id) . '_' . strval($forum_id);
        // Now Check we didn't import this already?
        if (!import_check_if_imported('forum_perms', $check_import_id)) {
            switch ($role) {
                // Read only
                case 0:
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'submit_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'submit_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'bypass_validation_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'bypass_validation_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_own_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_own_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_own_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_own_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    break;
                // Post
                case 1:
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'submit_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'submit_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'bypass_validation_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'bypass_validation_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_own_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_own_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_own_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_own_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    break;
                // Unvetted
                case 2:
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'submit_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'submit_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'bypass_validation_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'bypass_validation_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_own_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_own_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_own_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_own_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 0]);
                    break;
                // Moderate
                case 3:
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'submit_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'submit_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'bypass_validation_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'bypass_validation_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_own_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_own_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_own_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_own_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'edit_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_lowrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    $GLOBALS['FORUM_DB']->query_insert('group_privileges', ['privilege' => 'delete_midrange_content', 'group_id' => $group_id, 'the_page' => '', 'module_the_name' => 'forums', 'category_name' => strval($forum_id), 'the_value' => 1]);
                    break;
            }
            // Now we put the remap in so we know it is imported.
            import_id_remap_put('forum_perms', $check_import_id, 1);
        }
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     */
    public function import_calendar(object $db, string $table_prefix, string $file_base)
    {
        require_code('calendar2');

        // Calendar events
        $rows = $db->query_select('calendar');
        foreach ($rows as $row) {
            if (import_check_if_imported('event', strval($row['id_event']))) {
                continue;
            }

            $submitter = import_id_remap_get('member', strval($row['id_member']), true);
            if ($submitter === null) {
                $submitter = $GLOBALS['CNS_DRIVER']->get_guest_id();
            }

            $recurrence = 'none';
            $recurrences = null;

            $days = intval(floor((strtotime($row['end_date']) - strtotime($row['start_date'])) / (60 * 60 * 24))); // Max 7 days in SMF
            if ($days == 0) {
                $recurrence = 'none';
            } else {
                $recurrence = 'daily ';
                for ($i = 1; $i <= $days; $i++) {
                    $recurrence .= '1';
                }
            }

            list($start_year, $start_month, $start_day) = array_map('intval', explode('-', date('Y-m-d', strtotime($row['start_date']))));
            list($end_year, $end_month, $end_day) = array_map('intval', explode('-', date('Y-m-d', strtotime($row['end_date']))));

            if (($row['start_time'] !== null) && ($row['end_time'] !== null)) {
                list($start_hour, $start_minute) = array_map('intval', explode('-', date('h-i', strtotime($row['start_time']))));
                list($end_hour, $end_minute) = array_map('intval', explode('-', date('h-i', strtotime($row['end_time']))));
            } else {
                // All-day event
                $start_hour = null;
                $start_minute = null;
                $end_hour = 23;
                $end_minute = 59;
            }

            // SMF does not use null when there is no end date; they use a very low year.
            if ($end_year < 1970) {
                $end_year = null;
                $end_month = null;
                $end_day = null;
                $end_hour = null;
                $end_minute = null;
            }

            $description = '';
            $att_imported = false;
            if (!empty($row['id_topic'])) {
                $atts = $db->query_select('attachments', ['*'], ['id_msg' => $row['id_topic']], 'ORDER BY id_msg ASC');
                $attid = isset($atts[0]['id_attach']) ? $atts[0]['id_attach'] : null;
                $att_imported = (($attid !== null) && (import_check_if_imported('post_files', strval($attid))));
                $messages = $db->query_select('messages', ['*'], ['id_topic' => $row['id_topic']], 'ORDER BY id_topic ASC');
                $description = empty($messages[0]['body']) ? '' : str_replace(['[html]', '[/html]'], ['', ''], html_to_comcode($messages[0]['body']));
            }
            if ($att_imported) {
                $attid_new = import_id_remap_get('post_files', strval($attid), true);
                $description .= "\n\n" . '[attachment]' . strval($attid_new) . '[/attachment]';
            }

            $id_new = add_calendar_event(db_get_first_id() + 1, $recurrence, $recurrences, 0, $row['title'], $description, 3, ($start_year < 1970) ? 1970 : $start_year, $start_month, $start_day, 'day_of_month', $start_hour, $start_minute, $end_year, $end_month, $end_day, 'day_of_month', $end_hour, $end_minute, null, 1, null, 1, 1, 1, 1, '', $submitter);
            if ($att_imported) {
                $GLOBALS['FORUM_DB']->query_insert('attachment_refs', ['r_referer_type' => 'calendar', 'r_referer_id' => strval($id_new), 'a_id' => $attid_new]);
            }

            import_id_remap_put('event', strval($row['id_event']), $id_new);
        }

        // Holidays
        $rows = [];
        $rows = $db->query_select('calendar_holidays');
        foreach ($rows as $row) {
            if (import_check_if_imported('event_holiday', strval($row['id_holiday']))) {
                continue;
            }

            $submitter = $GLOBALS['CNS_DRIVER']->get_guest_id();

            $recurrence = 'none';
            $recurrences = null;

            // Holidays are all-day events
            $start_hour = null;
            $start_minute = null;
            $end_hour = null;
            $end_minute = null;

            list($start_year, $start_month, $start_day) = array_map('intval', explode('-', date('Y-m-d', strtotime($row['event_date']))));
            list($end_year, $end_month, $end_day) = array_map('intval', explode('-', date('Y-m-d', strtotime($row['event_date']))));

            // SMF does not use null when there is no end date; they use a very low year.
            if ($end_year < 1970) {
                $end_year = null;
                $end_month = null;
                $end_day = null;
                $recurrence = 'yearly 1';
            }

            $id_new = add_calendar_event(db_get_first_id() + 1, $recurrence, $recurrences, 0, $row['title'], $row['title'], 3, ($start_year < 1970) ? 1970 : $start_year, $start_month, $start_day, 'day_of_month', $start_hour, $start_minute, $end_year, $end_month, $end_day, 'day_of_month', $end_hour, $end_minute, null, 1, null, 1, 1, 1, 1, '', $submitter);

            import_id_remap_put('event_holiday', strval($row['id_holiday']), $id_new);
        }
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     */
    public function import_banners(object $db, string $table_prefix, string $file_base)
    {
        require_code('banners');
        require_code('banners2');

        $rows = $db->query_select('ads', ['*'], [], '', null, 0, true);
        if ($rows === null) {
            return; // SMFAds addon not installed
        }
        foreach ($rows as $row) {
            $test = $GLOBALS['SITE_DB']->query_select_value_if_there('banners', 'name', ['name' => $row['NAME']]);
            if ($test === null) {
                $submitter = $GLOBALS['FORUM_DRIVER']->get_guest_id();
                add_banner(fix_id($row['NAME']), '', '', $row['NAME'], stripslashes($row['CONTENT']), null, '', 1, '', BANNER_PERMANENT, null, $submitter, $row['show_topofpage'], '', [], [], time(), 0, $row['HITS'], 0, $row['HITS'], null);
            }
        }
    }

    /**
     * Standard import function.
     *
     * @param  object $db The database connector to import from
     * @param  string $table_prefix The table prefix the target prefix is using
     * @param  PATH $file_base The base directory we are importing from
     */
    public function import_news_and_categories(object $db, string $table_prefix, string $file_base)
    {
        require_code('news');
        require_code('news2');
        require_code('feedback');

        $rows = $db->query_select('tp_variables', ['value1 AS title', 'id'], ['type' => 'category'], '', null, 0, true);
        if ($rows === null) {
            return; // Not TinyPortal
        }
        foreach ($rows as $row) {
            if (import_check_if_imported('news_category', strval($row['id']))) {
                continue;
            }

            $id_new = add_news_category($row['title'], '', '');
            require_code('permissions2');
            set_global_category_access('news', $id_new);

            import_id_remap_put('news_category', strval($row['id']), $id_new);
        }

        $forum = (find_overridden_comment_forum('news') === null) ? get_option('comments_forum_name') : find_overridden_comment_forum('news');

        require_code('files');

        $boardurl = '';
        require $file_base . '/Settings.php';

        $row_start = 0;
        $rows = [];
        do {
            $rows = $db->query_select('tp_articles', ['*'], [], '', 200, $row_start);
            foreach ($rows as $row) {
                if (import_check_if_imported('news', strval($row['id']))) {
                    continue;
                }

                $news = ($row['intro'] == '' || $row['useintro'] == 0) ? '' : ('[html]' . $row['intro'] . '[/html]');
                $subject = $row['subject'];
                $news_article = $row['body'];
                if ($row['type'] == 'html') {
                    $news_article = '[html]' . $news_article . '[/html]';
                }

                $main_news_category = import_id_remap_get('news_category', strval($row['category']), true);
                if ($main_news_category === null) {
                    $main_news_category = $GLOBALS['SITE_DB']->query_select_value('news', 'MIN(id)');
                }

                $validated = $row['approved'];
                if ($row['off'] == 1) {
                    $validated = 0;
                }
                $allow_rating = 1 - $row['locked'];
                $allow_comments = 1 - $row['locked'];
                $allow_trackbacks = 1;
                $news_categories = [];
                $time = $row['date'];
                $views = $row['views'];
                $edit_date = null;

                if ($row['illustration'] != '') {
                    list($out_path, $image) = find_unique_path('uploads/repimages', basename($row['illustration']));
                    $out_handle = fopen($out_path, 'wb');
                    http_get_contents($boardurl . '/tp-files/tp-articles/illustrations/' . $row['illustration'], ['trigger_error' => false, 'write_to_file' => $out_handle]);
                    fclose($out_handle);
                } else {
                    $image = '';
                }

                $submitter = import_id_remap_get('member', strval($row['author_id']), true);
                if ($submitter === null) {
                    $submitter = $GLOBALS['FORUM_DRIVER']->get_guest_id();
                }
                $author = ($row['author'] === null) ? $GLOBALS['FORUM_DRIVER']->get_username($submitter) : $row['author'];

                $id_new = add_news($subject, $news, $author, $validated, $allow_rating, $allow_comments, $allow_trackbacks, '', $news_article, $main_news_category, $news_categories, $time, $submitter, $views, $edit_date, null, $image);

                $content_url = build_url(['page' => 'news', 'type' => 'view', 'id' => $id_new], get_module_zone('news'));

                // Comments
                $comments = $db->query_select('tp_variables', ['value1 AS subject', 'value2 AS post', 'value3 AS poster', 'value4 AS time'], ['type' => 'article_comment', 'value5' => $row['id']]);
                foreach ($comments as $comment) {
                    $comment['poster'] = import_id_remap_get('member', strval($comment['poster']), true);
                    if ($comment['poster'] === null) {
                        $comment['poster'] = $GLOBALS['FORUM_DRIVER']->get_guest_id();
                    }

                    $result = $GLOBALS['FORUM_DRIVER']->make_post_forum_topic(
                        $forum,
                        'news_' . strval($id_new),
                        $comment['poster'],
                        $comment['subject'],
                        '[html]' . $comment['post'] . '[/html]',
                        $subject,
                        do_lang('COMMENT'),
                        $content_url->evaluate(),
                        $comment['time'],
                        '',
                        1,
                        1,
                        false,
                        $GLOBALS['FORUM_DRIVER']->get_username($comment['poster']),
                        null
                    );
                }

                // Rating
                if ($row['rating'] !== null) {
                    $ratings = explode(',', $row['rating']);
                    $voters = explode(',', $row['voters']);
                    foreach ($ratings as $i => $rating) {
                        if (isset($voters[$i])) {
                            $member_id = import_id_remap_get('member', $voters[$i], true);
                            if ($member_id === null) {
                                $member_id = $GLOBALS['FORUM_DRIVER']->get_guest_id();
                            }

                            $rating_map = [
                                'rating_for_type' => 'news',
                                'rating_for_id' => $id_new,
                                'rating_member' => $member_id,
                                'rating_ip_address' => '',
                                'rating_time' => time(),
                                'rating' => intval($rating) * 2,
                            ];
                            $GLOBALS['SITE_DB']->query_insert('rating', $rating_map);
                        }
                    }
                }

                import_id_remap_put('news', strval($row['id']), $id_new);
            }

            $row_start += 200;
        } while (!empty($rows));
    }

    /**
     * Get an SMF setting.
     *
     * @param  object $db The database connector to import from
     * @param  ID_TEXT $setting The name of the setting
     * @param  boolean $allow_missing Whether to allow a missing setting without throwing an error
     * @return ?string The setting value (null: Setting does not exist and $allow_setting is true)
     */
    protected function get_setting(object $db, string $setting, bool $allow_missing = false) : ?string
    {
        static $settings_cache = [];

        if (isset($settings_cache[$setting])) {
            return $settings_cache[$setting];
        }
        if ($allow_missing) {
            $settings_cache[$setting] = $db->query_select_value_if_there('settings', 'value', ['variable' => $setting]);
            $ret = $db->query_select_value_if_there('settings', 'value', ['variable' => $setting]);
        } else {
            $settings_cache[$setting] = $db->query_select_value('settings', 'value', ['variable' => $setting]);
            $ret = $db->query_select_value_if_there('settings', 'value', ['variable' => $setting]);
        }
        return $settings_cache[$setting];
    }
}
