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

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__upgrade_db_upgrade()
{
    require_code('upgrade_lib');
}

/**
 * Do upgrader screen: database upgrade.
 *
 * @ignore
 */
function upgrader_db_upgrade_screen()
{
    echo '<h2>' . do_lang('UPGRADER_DATABASE_UPGRADE') . '</h2>';

    log_it('UPGRADER_DATABASE_UPGRADE');

    clear_caches_2();

    $version_files = cms_version_number();
    $_version_database_cns = get_value('cns_version');
    if ($_version_database_cns === null) { // LEGACY
        $_version_database_cns = get_value('ocf_version');
        set_value('cns_version', $_version_database_cns);
    }
    if ($_version_database_cns === null) {
        $_version_database_cns = get_value('version');
        set_value('cns_version', $_version_database_cns);
    }
    if ($_version_database_cns === null) {
        $version_database_cns = $version_files;
    } else {
        $version_database_cns = floatval($_version_database_cns);
    }

    $something_done = false;

    // Version-specific upgrade
    $version_upgrade = version_specific();
    $something_done = (($something_done) || ($version_upgrade));

    // Conversr upgrade
    if ($version_database_cns < $version_files) {
        echo '<h3>' . do_lang('UPGRADER_UPGRADE_CNS') . '</h3>';
        if (cns_upgrade()) {
            echo '<p>' . do_lang('SUCCESS') . '</p>';
        } else {
            echo do_lang('UPGRADER_NO_CNS_UPGRADE');
        }
        $something_done = true;
    }

    // Modules upgrade
    $done = upgrade_addons($version_database_cns);
    if ($done != '') {
        echo do_lang('UPGRADER_UPGRADE_MODULES', $done);
        $something_done = true;
    }

    // Database-specific upgrade
    $database_upgrade = database_specific();
    $something_done = (($something_done) || ($database_upgrade));

    if (!$something_done) {
        echo do_lang('NO_UPGRADE_DONE');
    }

    set_value('db_version', strval(cms_version_time_db()), true);
}

/**
 * Version specific upgrading. These are things that are relatively major structural changes and therefore will get done outside the module upgrade system.
 *
 * @return boolean Whether we did something
 */
function version_specific() : bool
{
    cms_extend_time_limit(TIME_LIMIT_EXTEND__SLUGGISH);

    // Version specific (rather than component specific) upgrading
    $version_files = cms_version_number();
    $_version_database = get_value('version');
    $version_database = floatval($_version_database);
    if ($_version_database === null) {
        $version_database = $version_files;
    }
    if ($version_database < $version_files) {
        // LEGACY

        if ($version_database < 9.0) {
            $dh = @opendir(get_custom_file_base() . '/imports/mods');
            if ($dh !== false) {
                while (($f = readdir($dh)) !== false) {
                    if (substr($f, -4) == '.tar') {
                        @rename(get_custom_file_base() . '/imports/mods/' . $f, get_file_base() . '/imports/addons/' . $f);
                        sync_file_move(get_custom_file_base() . '/imports/mods/' . $f, get_file_base() . '/imports/addons/' . $f);
                    }
                }
                closedir($dh);
            }
            echo do_lang('UPGRADER_UPGRADED_CUSTOM', '9', 'Migrated imports/mods to imports/addons');
        }
        if ($version_database < 10.0) {
            $GLOBALS['SITE_DB']->add_table_field('config', 'c_value_trans', '?LONG_TRANS');
            $GLOBALS['SITE_DB']->query('UPDATE ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'config SET c_value_trans=config_value,config_value=\'\' WHERE ' . db_string_not_equal_to('config_value', '') . ' AND (' . db_string_equal_to('the_type', 'transtext') . ' OR ' . db_string_equal_to('the_type', 'transline') . ')');

            $GLOBALS['SITE_DB']->rename_table('adminlogs', 'actionlogs');

            @rename(get_custom_file_base() . '/data_custom/breadcrumbs.xml', get_custom_file_base() . '/data_custom/xml_config/breadcrumbs.xml');
            sync_file_move(get_custom_file_base() . '/data_custom/breadcrumbs.xml', get_custom_file_base() . '/data_custom/xml_config/breadcrumbs.xml');
            @rename(get_custom_file_base() . '/data_custom/fields.xml', get_custom_file_base() . '/data_custom/xml_config/fields.xml');
            sync_file_move(get_custom_file_base() . '/data_custom/fields.xml', get_custom_file_base() . '/data_custom/xml_config/fields.xml');

            $remap = [
                'ocf_post' => 'cns_post',
                'ocf_signature' => 'cns_signature',
            ];
            foreach ($remap as $from => $to) {
                $GLOBALS['SITE_DB']->query_update('attachment_refs', ['r_referer_type' => $to], ['r_referer_type' => $from]);
            }

            $remap = [
                'cedi' => 'wiki',
                'contactmember' => 'contact_member',
                'admin_occle' => 'admin_commandr',
                'admin_flagrant' => 'admin_community_billboard', // Not actually bundled, but can take over existing tables now if installed again
                'onlinemembers' => 'usersonline',
                'leaderboard' => 'leader_board',
                'admin_ocf_categories' => 'admin_cns_categories',
                'admin_ocf_customprofilefields' => 'admin_cns_customprofilefields',
                'admin_ocf_emoticons' => 'admin_cns_emoticons',
                'admin_ocf_forums' => 'admin_cns_forums',
                'admin_ocf_groups' => 'admin_cns_groups',
                'admin_ocf_history' => 'admin_cns_history',
                'admin_ocf_join' => 'admin_cns_members',
                'admin_ocf_ldap' => 'admin_cns_ldap',
                'admin_ocf_merge_members' => 'admin_cns_merge_members',
                'admin_ocf_multimoderations' => 'admin_cns_multimoderations',
                'admin_ocf_post_templates' => 'admin_cns_post_templates',
                'admin_ocf_welcome_emails' => 'admin_cns_welcome_emails',
                'cms_cedi' => 'cms_wiki',
                'cms_ocf_groups' => 'cms_cns_groups',
            ];
            $_out = '';
            foreach ($remap as $from => $to) {
                $_out .= '<li><kbd>' . $from . '</kbd> => <kbd>' . $to . '</kbd></li>';
                $GLOBALS['SITE_DB']->query_delete('modules', ['module_the_name' => $to]);
                $GLOBALS['SITE_DB']->query_update('modules', ['module_the_name' => $to], ['module_the_name' => $from], '', 1);
                $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'menu_items SET i_url=REPLACE(i_url,\'' . $from . '\',\'' . $to . '\')');
            }
            echo do_lang('UPGRADER_RENAMED_MODULES', '10', $_out);

            /*
            $deleted_modules = [
            ];
            foreach ($deleted_modules as $module_name) {
                $GLOBALS['SITE_DB']->query_delete('modules', ['module_the_name' => $module_name]);
            }
            */
            persistent_cache_delete('MODULES');

            $remap = [
                'side_ocf_personal_topics' => 'side_cns_private_topics',
                'side_stored_menu' => 'menu',
                'side_root_galleries' => 'side_galleries',
            ];
            $_out = '';
            foreach ($remap as $from => $to) {
                $_out .= '<li><kbd>' . $from . '</kbd> => <kbd>' . $to . '</kbd></li>';
                $GLOBALS['SITE_DB']->query_delete('blocks', ['block_name' => $to]);
                $GLOBALS['SITE_DB']->query_update('blocks', ['block_name' => $to], ['block_name' => $from], '', 1);
            }
            echo do_lang('UPGRADER_RENAMED_MODULES', '10', $_out);

            $deleted_blocks = [
                'main_feedback',
                'main_sitemap',
                'main_as_zone_access',
                'main_recent_galleries',
                'main_top_galleries',
                'main_recent_cc_entries',
                'main_recent_downloads',
                'main_top_downloads',
                'main_download_tease',
                'main_gallery_tease',
            ];
            $_out = '';
            foreach ($deleted_blocks as $block_name) {
                $_out .= '<li><kbd>' . $block_name . '</kbd></li>';
                $GLOBALS['SITE_DB']->query_delete('blocks', ['block_name' => $block_name]);
            }
            echo do_lang('UPGRADER_UNINSTALL_MODULES', '10', $_out);

            $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'menu_items SET i_url=REPLACE(i_url,\'ocf_\',\'cns_\')');

            $GLOBALS['SITE_DB']->query('DELETE FROM ' . get_table_prefix() . 'values WHERE the_name LIKE \'' . db_encode_like('%cns_%') . '\'');
            $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'values SET the_name=REPLACE(the_name,\'ocf_\',\'cns_\')');

            $GLOBALS['SITE_DB']->query_update('url_id_monikers', ['m_resource_type' => 'browse'], ['m_resource_type' => 'misc'], '', 1);
            $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'f_custom_fields f JOIN ' . get_table_prefix() . 'translate t ON t.id=f.cf_name SET text_original=\'ocp_street_address\' WHERE ' . db_string_equal_to('text_original', 'ocp_building_name_or_number'));
            $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'f_custom_fields f JOIN ' . get_table_prefix() . 'translate t ON t.id=f.cf_name SET text_original=REPLACE(text_original,\'ocp_\',\'cms_\') WHERE text_original LIKE \'ocp\_%\'');
            $GLOBALS['SITE_DB']->alter_table_field('msp', 'specific_permission', '*ID_TEXT', 'privilege');
            $GLOBALS['SITE_DB']->alter_table_field('gsp', 'specific_permission', '*ID_TEXT', 'privilege');
            if ($GLOBALS['SITE_DB']->table_exists('pstore_permissions')) {
                $GLOBALS['SITE_DB']->alter_table_field('pstore_permissions', 'p_specific_permission', 'ID_TEXT', 'p_privilege');
            }
            $GLOBALS['SITE_DB']->rename_table('msp', 'member_privileges');
            $GLOBALS['SITE_DB']->rename_table('gsp', 'group_privileges');
            $GLOBALS['SITE_DB']->rename_table('sp_list', 'privilege_list');
            $GLOBALS['SITE_DB']->rename_table('usersubmitban_ip', 'banned_ip');
            $GLOBALS['SITE_DB']->query_update('db_meta_indices', ['i_fields' => 'member_id'], ['i_name' => 'xas'], '', 1);
            $GLOBALS['SITE_DB']->query_update('db_meta', ['m_type' => 'MEMBER'], ['m_type' => 'USER']);
            $GLOBALS['SITE_DB']->query_update('db_meta', ['m_type' => '?MEMBER'], ['m_type' => '?USER']);
            $GLOBALS['SITE_DB']->query_update('db_meta', ['m_type' => '*MEMBER'], ['m_type' => '*USER']);
            $GLOBALS['SITE_DB']->alter_table_field('actionlogs', 'the_user', 'MEMBER', 'member_id');
            $GLOBALS['SITE_DB']->alter_table_field('sessions', 'the_user', 'MEMBER', 'member_id');
            $GLOBALS['SITE_DB']->alter_table_field('sessions', 'the_session', '*ID_TEXT');
            $GLOBALS['SITE_DB']->query_update('privilege_list', ['p_section' => 'FORUMS_AND_MEMBERS'], ['p_section' => 'SECTION_FORUMS']);
            $GLOBALS['SITE_DB']->query_update('privilege_list', ['p_section' => 'BANNERS'], ['p_section' => '_BANNERS']);
            $GLOBALS['SITE_DB']->query_delete('config', ['c_set' => 0]); // Defaults not saved in in same way in v10
            $GLOBALS['SITE_DB']->delete_table_field('config', 'human_name');
            $GLOBALS['SITE_DB']->delete_table_field('config', 'the_type');
            $GLOBALS['SITE_DB']->delete_table_field('config', 'eval');
            $GLOBALS['SITE_DB']->delete_table_field('config', 'the_page');
            $GLOBALS['SITE_DB']->delete_table_field('config', 'section');
            $GLOBALS['SITE_DB']->delete_table_field('config', 'explanation');
            $GLOBALS['SITE_DB']->delete_table_field('config', 'shared_hosting_restricted');
            $GLOBALS['SITE_DB']->delete_table_field('config', 'c_data');
            $GLOBALS['SITE_DB']->alter_table_field('config', 'the_name', '*ID_TEXT', 'c_name');
            $GLOBALS['SITE_DB']->alter_table_field('config', 'config_value', 'LONG_TEXT', 'c_value');
            $GLOBALS['SITE_DB']->add_table_field('config', 'c_needs_dereference', 'BINARY', 0);
            $hooks = find_all_hook_obs('systems', 'config', 'Hook_config_');
            foreach ($hooks as $hook => $ob) {
                $details = $ob->get_details();
                $needs_dereference = ($details['type'] == 'transtext' || $details['type'] == 'transline' || $details['type'] == 'comcodetext' || $details['type'] == 'comcodeline') ? 1 : 0;
                $GLOBALS['SITE_DB']->query_update('config', ['c_needs_dereference' => $needs_dereference], ['c_name' => $hook], '', 1);
            }
            $GLOBALS['SITE_DB']->query_update('zones', ['zone_theme' => 'admin'], ['zone_name' => 'adminzone'], '', 1);
            $GLOBALS['SITE_DB']->query_update('zones', ['zone_theme' => 'admin'], ['zone_name' => 'cms'], '', 1);
            $GLOBALS['SITE_DB']->query_update('db_meta', ['m_type' => 'SHORT_TEXT'], ['m_type' => 'MD5']);
            $GLOBALS['SITE_DB']->query_update('db_meta', ['m_type' => '*SHORT_TEXT'], ['m_type' => '*MD5']);
            rename_config_option('ocf_show_profile_link', 'cns_show_profile_link');

            delete_value('last_implicit_sync');
            delete_value('last_newsletter_drip_send');
            delete_value('last_confirm_reminder_time');
            delete_value('oracle_index_cleanup_last_time');
            delete_value('last_sitemap_time_calc');
            delete_value('last_ticket_lead_time_calc');
            if (get_value('last_welcome_mail_time') !== null) {
                $GLOBALS['SITE_DB']->query_insert('long_values', ['date_and_time' => time(), 'the_value' => get_value('last_welcome_mail_time'), 'the_name' => 'last_welcome_mail_time']);
                delete_value('last_welcome_mail_time');
            }

            foreach (['INTEGER', 'REAL'] as $bad_type) {
                $bad_fields = $GLOBALS['SITE_DB']->query_select('db_meta', ['m_name'], ['m_type' => $bad_type, 'm_table' => 'f_member_custom_fields']);
                foreach ($bad_fields as $bad_field) {
                    $GLOBALS['SITE_DB']->alter_table_field('f_member_custom_fields', $bad_field['m_name'], '?' . $bad_type);
                }
            }

            // This seems to be a legacy problem on some sites, but would crash on v10 if no-multi-lang was enabled. Generally things would corrupt.
            require_code('cns_members');
            $fields = $GLOBALS['SITE_DB']->query('SELECT id FROM ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'f_custom_fields WHERE cf_type IN (\'long_trans\',\'short_trans\')');
            $member_mappings = cns_get_custom_field_mappings(get_member());
            foreach ($fields as $field) {
                $db_field = 'field_' . strval($field['id']);
                if (is_string($member_mappings[$db_field])) {
                    $GLOBALS['SITE_DB']->promote_text_field_to_comcode('f_member_custom_fields', $db_field, 'mf_member_id');
                }
            }
            echo do_lang('UPGRADER_UPGRADED_CUSTOM', '10', 'applied fix for legacy no-multi-lang crash bug');

            // For old (and renamed) non-bundled addons
            if ($GLOBALS['SITE_DB']->table_exists('bank')) {
                $GLOBALS['SITE_DB']->alter_table_field('bank', 'divident', 'INTEGER', 'dividend');
                rename_config_option('bank_divident', 'bank_dividend');
            }

            // grepimages is now repimages
            move_folder_contents('uploads/grepimages', 'uploads/repimages');
            if (addon_installed('galleries')) {
                $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'galleries SET rep_image=REPLACE(rep_image,\'/grepimages\',\'/repimages\')');
            }
            if (addon_installed('downloads')) {
                $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'download_categories SET rep_image=REPLACE(rep_image,\'/grepimages\',\'/repimages\')');
            }
            if (addon_installed('news')) {
                $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'news_categories SET nc_img=REPLACE(nc_img,\'/grepimages\',\'/repimages\')');
                $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'news SET news_image_url=REPLACE(news_image_url,\'/grepimages\',\'/repimages\')');
            }
            if (addon_installed('catalogues')) {
                $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'catalogue_categories SET rep_image=REPLACE(rep_image,\'/grepimages\',\'/repimages\')');
            }
            echo do_lang('UPGRADER_UPGRADED_CUSTOM', '10', 'uploads/grepimages => uploads/repimages');

            // Delete old files
            @unlink(get_file_base() . '/pages/html_custom/EN/cedi_tree_made.htm');
            @unlink(get_file_base() . '/site/pages/html_custom/EN/cedi_tree_made.htm');
            @unlink(get_file_base() . '/pages/html_custom/EN/download_tree_made.htm');
            @unlink(get_file_base() . '/site/pages/html_custom/EN/download_tree_made.htm');

            // File replacements
            $reps = [
                '#([^\w])cedi([^\w])#' => '$1wiki$2',
                '#([^\w])seedy([^\w])#' => '$1wiki$2',
                '#ocPortal#' => 'Composr',
                '#ocp_#' => 'cms_',
                '#side_ocf_personal_topics#' => 'side_cns_private_topics',
                '#ocf_#' => 'cns_',
                '# filter="#' => ' select="',
                '# select="#' => ' filter="',
                '# ocselect="#' => ' filter="',
                '# caption="#' => ' title="',
                '#main_feedback#' => 'main_contact_us',
                '#side_stored_menu#' => 'menu',
                '#topsites#' => 'top_sites',
                '#internal_box#' => 'box',
                '#external_box#' => 'box',
                '# type="curved"#' => '',
                '# type=&quot;curved&quot;#' => '',
                '#side_root_galleries#' => 'side_galleries',
                '#\[block\]main_sitemap\[/block\]#' => '{$BLOCK,block=menu,param={$_GET,under},use_page_groupings=1,type=sitemap,quick_cache=1}',
                '#\[attachment([^\[\]]*)\]url_([^\[\]]*)\[/attachment[^\[\]]*\]#' => '[media$1]$2[/media]',
                '#\{\$OCF#' => '{$CNS',
                '#:misc#' => ':browse',
                '#type=misc#' => 'type=browse',
                '#:product=#' => ':type_code=',
                '#&product=#' => '&type_code=',
                '#&amp;product=#' => '&amp;type_code=',
                '#solidborder#' => 'results_table',
            ];
            perform_search_replace($reps);
            echo do_lang('UPGRADER_UPGRADED_FILE_REPLACEMENTS', '10');

            // Old-style comment topics
            $comment_topic_forums = [
                'comments_forum_name',
                'messaging_forum_name',
                'ticket_forum_name',
            ];
            foreach ($comment_topic_forums as $_forum) {
                $forum = get_option($_forum, true);
                if ($forum !== null) {
                    $forum_id = $GLOBALS['FORUM_DRIVER']->forum_id_from_name($forum);
                } else {
                    $forum_id = null;
                }
                if ($forum_id !== null) {
                    $table_prefix = $GLOBALS['FORUM_DB']->get_table_prefix();
                    $sql = 'SELECT id,t_cache_first_title FROM ' . $table_prefix . 'f_topics WHERE t_forum_id=' . strval($forum_id) . ' AND t_cache_first_title LIKE \'% (#%)\'';
                    $rows = $GLOBALS['SITE_DB']->query($sql);
                    foreach ($rows as $row) {
                        $matches = [];
                        preg_match('#^(.*) \(\#(.*)\)$#', $row['t_cache_first_title'], $matches);
                        if ($matches[1] == '') {
                            $t_description = $matches[2];
                            $new_title = $matches[2];
                        } else {
                            $t_description = $matches[1] . ': #' . $matches[2];
                            $new_title = $matches[1];
                        }
                        $GLOBALS['FORUM_DB']->query_update('f_topics', ['t_cache_first_title' => $new_title, 't_description' => $t_description], ['id' => $row['id']], '', 1);
                    }
                }
            }
            $GLOBALS['FORUM_DB']->query_update('f_topics', ['t_description' => 'Comment: #block_main_comments_guestbook_main'], ['t_description' => 'guestbook: #block_main_comments_guestbook_main'], '', 1);
            echo do_lang('UPGRADER_UPGRADED_CUSTOM', '10', 'upgraded old-style comment topics');

            echo do_lang('UPGRADER_UPGRADED_CORE_TABLES', '10');
        }

        if ($version_database < 11.0) {
            // Database changes (correcting previous errors in types)
            $GLOBALS['SITE_DB']->change_primary_key('db_meta_indices', ['i_table', 'i_name']);
            $GLOBALS['SITE_DB']->alter_table_field('db_meta_indices', 'i_fields', 'LONG_TEXT');
            $GLOBALS['SITE_DB']->alter_table_field('attachments', 'a_url', 'URLPATH');
            $GLOBALS['SITE_DB']->alter_table_field('attachments', 'a_thumb_url', 'URLPATH');
            $GLOBALS['SITE_DB']->alter_table_field('attachments', 'a_last_downloaded_time', '?TIME');
            $GLOBALS['SITE_DB']->alter_table_field('attachments', 'a_add_time', 'TIME');
            $GLOBALS['SITE_DB']->alter_table_field('group_privileges', 'group_id', '*GROUP');
            $GLOBALS['SITE_DB']->create_index('group_privileges', 'group_id', ['group_id']);
            $GLOBALS['SITE_DB']->alter_table_field('sessions', 'cache_username', 'ID_TEXT');
            $GLOBALS['SITE_DB']->alter_table_field('sessions', 'last_activity', 'TIME', 'last_activity_time');
            $GLOBALS['SITE_DB']->alter_table_field('menu_items', 'i_url', 'SHORT_TEXT', 'i_link');

            echo do_lang('UPGRADER_UPGRADED_CORE_TABLES', '11');

            // Renamed blocks
            $remap = [
                'main_activities' => 'main_activity_feed',
                'main_activities_state' => 'main_activity_feed_state',
            ];
            $_out = '';
            foreach ($remap as $from => $to) {
                $_out .= '<li><kbd>' . $from . '</kbd> => <kbd>' . $to . '</kbd></li>';
                $GLOBALS['SITE_DB']->query_delete('blocks', ['block_name' => $to]);
                $GLOBALS['SITE_DB']->query_update('blocks', ['block_name' => $to], ['block_name' => $from], '', 1);
            }
            echo do_lang('UPGRADER_RENAMED_MODULES', '11', $_out);

            // Delete bundled and non-bundled addons that no longer exist (addon => [whether non-bundled, whether being moved between bundled/non-bundled opposed to deleted])
            //  Note that any old tables etc should be removed from the upgrade code in admin_version
            //  Note that any non-bundled addons are not handled by the software's own upgrade code, and they should ideally be uninstalled manually if they have tables (using safe mode if needed) or cleaned out using the integrity checker if they don't
            $deleted_addons = [
                'bookmarks' => [false, false],
                'cns_reported_posts' => [false, false], // Merged into core
                'collaboration_zone' => [false, false],
                'hphp_buildkit' => [false, false],
                'linux_helper_scripts' => [false, false],
                'msn' => [false, false],
                'pointstore' => [false, false], // Merged into points / eCommerce
                'ssl' => [false, false],
                'staff_messaging' => [false, false],
                'staff' => [false, false],
                'supermember_directory' => [false, false],
                'textbased_persistent_caching' => [false, false],
                'windows_helper_scripts' => [false, false],
                'xml_fields' => [false, false],
                'zone_logos' => [false, false], // Themes / logo wizard now manage logos
                'health_check' => [true, true], // Now bundled
            ];
            $_out = '';
            foreach ($deleted_addons as $addon => $info) {
                list($is_nonbundled, $is_moved) = $info;

                if (!$is_moved) {
                    $_out .= '<li><kbd>' . $addon . '</kbd></li>';
                    $GLOBALS['SITE_DB']->query_delete('addons', ['addon_name' => $addon]);
                    $GLOBALS['SITE_DB']->query_delete('addons_files', ['addon_name' => $addon]);
                    $GLOBALS['SITE_DB']->query_delete('addons_dependencies', ['addon_name' => $addon]);
                    @unlink(get_custom_file_base() . '/imports/addons/' . $addon . '.tar');
                }

                // Just in case the user did not process file integrity yet
                if ($is_nonbundled) {
                    @unlink(get_custom_file_base() . '/sources_custom/hooks/systems/addon_registry/' . $addon . '.php');
                } else {
                    @unlink(get_custom_file_base() . '/sources/hooks/systems/addon_registry/' . $addon . '.php');
                }
            }
            echo do_lang('UPGRADER_UNINSTALL_MODULES', '11', $_out);

            // Renamed addons (old name => new name), just in case the user did not process file integrity yet
            //  Note that any table modifications etc should be handled in the upgrade code for the NEW addon / module
            //  Note that any non-bundled addons are not handled by the software's own upgrade code, and they should ideally be edited manually if they have tables (using safe mode if needed) or cleaned out using the integrity checker if they don't
            $renamed_addons = [
                'unvalidated' => 'validation'
            ];
            $_out = '';
            foreach ($renamed_addons as $old_addon => $new_addon) {
                $_out .= '<li><kbd>' . $old_addon . '</kbd> => <kbd>' . $new_addon . '</kbd></li>';
                if ($GLOBALS['SITE_DB']->query_select_value_if_there('addons', 'addon_name', ['addon_name' => $new_addon]) === null) {
                    $GLOBALS['SITE_DB']->query_update('addons', ['addon_name' => $new_addon], ['addon_name' => $old_addon]);
                    $GLOBALS['SITE_DB']->query_update('addons_dependencies', [
                        'addon_name' => $new_addon,
                    ], ['addon_name' => $old_addon], '', 1);
                    $GLOBALS['SITE_DB']->query_update('addons_files', [
                        'addon_name' => $new_addon,
                    ], ['addon_name' => $old_addon], '', 1);
                }
                @copy(get_custom_file_base() . '/imports/addons/' . $old_addon . '.tar', get_custom_file_base() . '/imports/addons/' . $new_addon . '.tar');
                @fix_permissions(get_custom_file_base() . '/imports/addons/' . $new_addon . '.tar');
                @unlink(get_custom_file_base() . '/imports/addons/' . $old_addon . '.tar');

                // New hook should have been extracted by the upgrader; remove old ones
                @unlink(get_custom_file_base() . '/sources/hooks/systems/addon_registry/' . $old_addon . '.php');
                @unlink(get_custom_file_base() . '/sources_custom/hooks/systems/addon_registry/' . $old_addon . '.php');
            }
            echo do_lang('UPGRADER_RENAMED_MODULES', '11', $_out);

            // Deleted modules
            $deleted_modules = [
                'admin_ecommerce_logs', // Renamed but has no install code
                'admin_messaging',
                'admin_orders',
                'admin_pointstore',
                'admin_ssl',
                'admin_staff',
                'bookmarks',
                'pointstore',
                'staff',
                'supermembers'
            ];
            $_out = '';
            foreach ($deleted_modules as $module_name) {
                $_out .= '<li><kbd>' . $module_name . '</kbd></li>';
                $GLOBALS['SITE_DB']->query_delete('modules', ['module_the_name' => $module_name]);
            }
            echo do_lang('UPGRADER_UNINSTALL_MODULES', '11', $_out);

            // Deleted blocks
            $deleted_blocks = [
                'main_only_if_match',
                'main_pt_notifications',
                'main_staff_website_monitoring',
                'side_network'
            ];
            $_out = '';
            foreach ($deleted_blocks as $block_name) {
                $_out .= '<li><kbd>' . $block_name . '</kbd></li>';
                $GLOBALS['SITE_DB']->query_delete('blocks', ['block_name' => $block_name]);
            }
            echo do_lang('UPGRADER_UNINSTALL_MODULES', '11', $_out);

            // File replacements
            $reps = [
                '#main_activities#' => 'main_activity_feed',
                '#main_activities_state#' => 'main_activity_feed_state',
                '#ocProducts#' => 'Core Development Team',
            ];
            perform_search_replace($reps);
            echo do_lang('UPGRADER_UPGRADED_FILE_REPLACEMENTS', '11');

            // Delete removed collaboration zone
            require_code('zones3');
            actual_delete_zone('collaboration', true);
            echo do_lang('UPGRADER_UPGRADED_CUSTOM', '11', 'removed collaboration zone');

            // Default zone page name change
            $GLOBALS['SITE_DB']->query_update('zones', ['zone_default_page' => 'home'], ['zone_default_page' => 'start']);
            echo do_lang('UPGRADER_UPGRADED_CUSTOM', '11', 'renamed default zone start page to home page');
        }

        // Note: When adding upgrade code for a new version it's a good idea to review old code to get an idea for what might need to be done

        set_value('version', float_to_raw_string($version_files, intval($version_database), true));

        return true;
    }

    return false;
}

/**
 * Apply changes specific to a database timestamp version. This should be run after version_specific, install_cns, and upgrade_addons.
 * LEGACY: Generally this code should be migrated to version_specific(), install_cns(), or other relevant areas on the next minor/major release.
 *
 * @return boolean Whether we did something
 */
function database_specific() : bool
{
    cms_extend_time_limit(TIME_LIMIT_EXTEND__MODEST);

    require_code('version');

    $upgrade_from = get_value('db_version', null, true);
    $upgrade_to = cms_version_time_db();

    $done_something = false;

    // LEGACY: Handle database field edits from 11 alpha to 11.beta1
    if ((is_numeric($upgrade_from)) && (intval($upgrade_from) >= 1711670588/*11.alpha1*/) && (intval($upgrade_from) < 1721661975)) {
        $GLOBALS['SITE_DB']->change_primary_key('db_meta_indices', ['i_table', 'i_name']);
        $GLOBALS['SITE_DB']->alter_table_field('db_meta_indices', 'i_fields', 'LONG_TEXT');
        $GLOBALS['SITE_DB']->alter_table_field('attachments', 'a_url', 'URLPATH');
        $GLOBALS['SITE_DB']->alter_table_field('attachments', 'a_thumb_url', 'URLPATH');
        $GLOBALS['SITE_DB']->alter_table_field('attachments', 'a_last_downloaded_time', '?TIME');
        $GLOBALS['SITE_DB']->alter_table_field('attachments', 'a_add_time', 'TIME');
        $GLOBALS['SITE_DB']->alter_table_field('group_privileges', 'group_id', '*GROUP');
        $GLOBALS['SITE_DB']->alter_table_field('sessions', 'cache_username', 'ID_TEXT');
        $GLOBALS['SITE_DB']->alter_table_field('sessions', 'last_activity', 'TIME', 'last_activity_time');
        $GLOBALS['SITE_DB']->alter_table_field('menu_items', 'i_url', 'SHORT_TEXT', 'i_link');

        $GLOBALS['SITE_DB']->create_index('group_privileges', 'group_id', ['group_id']);

        $GLOBALS['FORUM_DB']->alter_table_field('f_forums', 'f_cache_last_username', 'ID_TEXT');
        $GLOBALS['FORUM_DB']->alter_table_field('f_poll_answers', 'pa_poll_id', 'AUTO_LINK');
        $GLOBALS['FORUM_DB']->alter_table_field('f_group_join_log', 'usergroup_id', '?GROUP');
        $GLOBALS['FORUM_DB']->alter_table_field('f_invites', 'i_inviter', 'MEMBER', 'i_invite_member');
        $GLOBALS['FORUM_DB']->alter_table_field('f_members', 'm_on_probation_until', '?TIME', 'm_probation_expiration_time');
        $GLOBALS['FORUM_DB']->alter_table_field('f_groups', 'g_group_leader', '?MEMBER', 'g_group_lead_member');
        $GLOBALS['FORUM_DB']->alter_table_field('f_groups', 'g_promotion_target', '?GROUP', 'g_promotion_target_group');
        $GLOBALS['FORUM_DB']->alter_table_field('f_forums', 'f_parent_forum', '?AUTO_LINK', 'f_parent_forum_id');
        $GLOBALS['FORUM_DB']->alter_table_field('f_topics', 't_pt_from', '?MEMBER', 't_pt_from_member');
        $GLOBALS['FORUM_DB']->alter_table_field('f_topics', 't_pt_to', '?MEMBER', 't_pt_to_member');
        $GLOBALS['FORUM_DB']->alter_table_field('f_posts', 'p_poster', 'MEMBER', 'p_posting_member');
        $GLOBALS['FORUM_DB']->alter_table_field('f_posts', 'p_intended_solely_for', '?MEMBER', 'p_whisper_to_member');
        $GLOBALS['FORUM_DB']->alter_table_field('f_posts', 'p_last_edit_by', '?MEMBER', 'p_last_edit_member');
        $GLOBALS['FORUM_DB']->alter_table_field('f_forum_intro_ip', 'i_ip', '*IP', 'i_ip_address');
        $GLOBALS['FORUM_DB']->alter_table_field('f_poll_votes', 'pv_ip', 'IP', 'pv_ip_address');
        $GLOBALS['FORUM_DB']->alter_table_field('f_poll_votes', 'pv_cache_points_at_voting_time', 'INTEGER', 'pv_points_when_voted');
        $GLOBALS['FORUM_DB']->alter_table_field('f_multi_moderations', 'mm_move_to', '?AUTO_LINK', 'mm_move_to_forum_id');
        $GLOBALS['FORUM_DB']->alter_table_field('f_warnings', 'w_by', 'MEMBER', 'w_issuing_member');
        $GLOBALS['FORUM_DB']->alter_table_field('f_moderator_logs', 'l_by', 'MEMBER', 'l_by_member');
        $GLOBALS['FORUM_DB']->alter_table_field('f_member_known_login_ips', 'i_ip', '*IP', 'i_ip_address');
        $GLOBALS['FORUM_DB']->alter_table_field('f_pposts_fulltext_index', 'i_poster_id', 'MEMBER', 'i_posting_member');
        $GLOBALS['FORUM_DB']->alter_table_field('f_posts_fulltext_index', 'i_poster_id', 'MEMBER', 'i_posting_member');

        $GLOBALS['FORUM_DB']->delete_index_if_exists('f_posts', 'last_edit_by');
        $GLOBALS['FORUM_DB']->delete_index_if_exists('f_posts_fulltext_index', 'main');
        $GLOBALS['FORUM_DB']->delete_index_if_exists('f_topics', 't_pt_to');
        $GLOBALS['FORUM_DB']->delete_index_if_exists('f_topics', 't_pt_from');

        $GLOBALS['FORUM_DB']->change_primary_key('f_poll_answers', ['id']);
        $GLOBALS['FORUM_DB']->create_index('f_invites', 'inviter', ['i_invite_member']);
        $GLOBALS['FORUM_DB']->create_index('f_posts', 'last_edit_member', ['p_last_edit_member']);
        $GLOBALS['FORUM_DB']->create_index('f_topics', 't_pt_to_member', ['t_pt_to_member']);
        $GLOBALS['FORUM_DB']->create_index('f_topics', 't_pt_from_member', ['t_pt_from_member']);
        $GLOBALS['FORUM_DB']->create_index('f_poll_votes', 'voting_member_id', ['pv_member_id']);
        $GLOBALS['FORUM_DB']->create_index('f_poll_votes', 'voting_ip_address', ['pv_ip_address']);

        $done_something = true;
    }

    // LEGACY: (11.beta2) Copy declarations in CPF on upgrade so members are not forced to re-agree to the rules
    if ((!is_numeric($upgrade_from)) || (intval($upgrade_from) < 1721686113)) {
        if ((get_option('join_declarations') != '') && (get_option('show_first_join_page') == '1')) {
            $member_id = null;
            do {
                $rows = $GLOBALS['FORUM_DRIVER']->get_next_members($member_id, 100);
                foreach ($rows as $row) {
                    $member_id = $GLOBALS['FORUM_DRIVER']->mrow_member_id($row);

                    if ($member_id == $GLOBALS['FORUM_DRIVER']->get_guest_id()) {
                        continue;
                    }

                    $cpfs = $GLOBALS['FORUM_DRIVER']->get_custom_fields($member_id);
                    if ($cpfs === null) {
                        continue;
                    }

                    // Only copy declarations in for members whose declarations field was blank
                    if (!isset($cpfs['agreed_declarations']) || ($cpfs['agreed_declarations'] == '')) {
                        $GLOBALS['FORUM_DRIVER']->set_custom_field($member_id, 'agreed_declarations', get_option('join_declarations'));
                    }
                }
            } while (count($rows) > 0);

            $done_something = true;
        }
    }

    // LEGACY: (11.beta2) Add new privilege for recommend addon
    if ((!is_numeric($upgrade_from)) || (intval($upgrade_from) < 1722461012)) {
        require_code('permissions3');
        add_privilege('RECOMMEND', 'use_own_recommend_message', true, false, true);

        $done_something = true;
    }

    // LEGACY: (11.beta2) login keys should be stored as hashes just like passwords
    if ((!is_numeric($upgrade_from)) || (intval($upgrade_from) < 1723865750)) {
        $GLOBALS['FORUM_DB']->alter_table_field('f_members', 'm_login_key', 'SHORT_TEXT', 'm_login_key_hash');
    }


    return $done_something;
}

/**
 * Upgrade all addons, modules, and blocks.
 *
 * @param  float $from_cms_version From which version of the software we are upgrading
 * @return string List of upgraded/installed modules/blocks
 */
function upgrade_addons(float $from_cms_version) : string
{
    cms_extend_time_limit(TIME_LIMIT_EXTEND__SLUGGISH);

    $out = '';

    require_code('zones2');
    require_code('zones3');

    // Define which modules must be upgraded first as other modules may depend on it
    $must_upgrade_first = [
        'admin_version' => 'adminzone',
    ];
    if ($from_cms_version < 11.0) { // LEGACY
        $must_upgrade_first['admin_addons'] = 'adminzone'; // DB changes
        $must_upgrade_first['catalogues'] = 'site'; // Required for any module installing new custom profile fields (e.g. points)
    }
    foreach ($must_upgrade_first as $module => $zone) {
        $ret = upgrade_module($zone, $module);
        if ($ret == 1) {
            $out .= '<li>' . do_lang('UPGRADER_UPGRADED_MODULE', '<kbd>' . $module . '</kbd>') . '</li>';
        }
    }

    $zones = find_all_zones();
    if (!in_array('adminzone', $zones)) {
        $zones[] = 'adminzone';
    }
    if (!in_array('cms', $zones)) {
        $zones[] = 'cms';
    }
    if (!in_array('site', $zones)) {
        $zones[] = 'site';
    }
    foreach ($zones as $zone) {
        $modules = find_all_modules($zone);
        foreach ($modules as $module => $type) {
            $ret = upgrade_module($zone, $module);
            if ($ret == 1) {
                $out .= '<li>' . do_lang('UPGRADER_UPGRADED_MODULE', '<kbd>' . escape_html($module) . '</kbd>') . '</li>';
            } elseif ($ret == -2) {
                if (reinstall_module($zone, $module)) {
                    $out .= '<li>' . do_lang('UPGRADER_INSTALLED_MODULE', '<kbd>' . escape_html($module) . '</kbd>') . '</li>';
                }
            }
        }
    }

    $blocks = find_all_blocks();
    foreach ($blocks as $block => $type) {
        $ret = upgrade_block($block);
        if ($ret == 1) {
            $out .= '<li>' . do_lang('UPGRADER_UPGRADED_BLOCK', '<kbd>' . escape_html($block) . '</kbd>') . '</li>';
        } elseif ($ret == -2) {
            if (reinstall_block($block)) {
                $out .= '<li>' . do_lang('UPGRADER_INSTALLED_BLOCK', '<kbd>' . escape_html($block) . '</kbd>') . '</li>';
            }
        }
    }

    require_code('addons2');
    $addons = find_all_hooks('systems', 'addon_registry');
    foreach ($addons as $addon_name => $type) {
        if ($type == 'sources_custom') {
            $out .= '<li>' . do_lang('UPGRADER_SKIPPED_NONBUNDLED_ADDON', '<kbd>' . escape_html($addon_name) . '</kbd>') . '</li>';
            continue;
        }

        $ret = upgrade_addon_soft($addon_name);
        if ($ret == 1) {
            $out .= '<li>' . do_lang('UPGRADER_UPGRADED_ADDON', '<kbd>' . escape_html($addon_name) . '</kbd>') . '</li>';
        } elseif ($ret == -2) {
            reinstall_addon_soft($addon_name);

            $out .= '<li>' . do_lang('UPGRADER_INSTALLED_ADDON', '<kbd>' . escape_html($addon_name) . '</kbd>') . '</li>';
        } elseif ($ret == -1) {
            $out .= '<li>' . do_lang('UPGRADER_ADDON_INCOMPATIBLE', '<kbd>' . escape_html($addon_name) . '</kbd>') . '</li>';
        }
    }

    return $out;
}

/**
 * Upgrade Conversr if appropriate.
 *
 * @return boolean Whether anything was done
 */
function cns_upgrade() : bool
{
    $version_files = cms_version_number();
    $_version_database_cns = get_value('cns_version');
    if ($_version_database_cns === null) { // LEGACY
        $_version_database_cns = get_value('ocf_version');
        set_value('cns_version', $_version_database_cns);
        delete_value('ocf_version');
    }
    if ($_version_database_cns === null) {
        $_version_database_cns = get_value('version');
    }
    if ($_version_database_cns === null) {
        $version_database_cns = $version_files;
    } else {
        $version_database_cns = floatval($_version_database_cns);
    }

    if ($version_files != $version_database_cns) {
        global $SITE_INFO;
        $SITE_INFO['db_forums'] = $SITE_INFO['db_site'];
        $SITE_INFO['db_forums_host'] = (!empty($SITE_INFO['db_site_host'])) ? $SITE_INFO['db_site_host'] : 'localhost';
        $SITE_INFO['db_forums_user'] = (!empty($SITE_INFO['db_site_user'])) ? $SITE_INFO['db_site_user'] : 'root';
        $SITE_INFO['db_forums_password'] = array_key_exists('db_site_password', $SITE_INFO) ? $SITE_INFO['db_site_password'] : '';
        $GLOBALS['FORUM_DB'] = $GLOBALS['SITE_DB'];

        require_code('forum/cns');
        $GLOBALS['FORUM_DRIVER'] = object_factory('Forum_driver_cns');
        $GLOBALS['FORUM_DRIVER']->db = $GLOBALS['SITE_DB'];

        require_code('cns_install');
        install_cns($version_database_cns);

        set_value('cns_version', float_to_raw_string($version_files, 10, true));

        return true;
    }
    return false;
}
