<?php
/**
 * This source file is part of the open source project
 * ExpressionEngine (https://expressionengine.com)
 *
 * @link      https://expressionengine.com/
 * @copyright Copyright (c) 2003-2022, Packet Tide, LLC (https://www.packettide.com)
 * @license   https://expressionengine.com/license Licensed under Apache License, Version 2.0
 */

namespace ExpressionEngine\Updater\Version_3_1_0;

use Exception;

/**
 * Update
 */
class Updater
{
    public $version_suffix = '';
    public $errors = array();

    /**
     * Do Update
     *
     * @return bool Success?
     */
    public function do_update()
    {
        ee()->load->dbforge();

        $steps = new \ProgressIterator(
            array(
                'move_avatars',
                'update_member_data_column_names',
                'add_snippet_edit_date',
                'add_global_variable_edit_date',
                'update_collation_config',
                'fix_table_collations',
                'ensure_upload_directories_are_correct',
                'add_channel_max_entries_columns',
                'synchronize_layouts',
                'template_routes_remove_empty'
            )
        );

        foreach ($steps as $k => $v) {
            try {
                $this->$v();
            } catch (Exception $e) {
                $this->errors[] = $e->getMessage();
            }
        }

        return empty($this->errors);
    }

    /**
     * Fields created in 3.0 were missing the 'm_' prefix on their data columns,
     * so we need to add the prefix back
     */
    private function update_member_data_column_names()
    {
        $member_data_columns = ee()->db->list_fields('member_data');

        $columns_to_modify = array();
        foreach ($member_data_columns as $column) {
            if ($column == 'member_id' or 						// Don't rename the primary key
                substr($column, 0, 2) == 'm_' or 				// or if it already has the prefix
                in_array('m_' . $column, $member_data_columns)) { 	// or if the prefixed column already exists (?!)
                continue;
            }

            $columns_to_modify[$column] = array(
                'name' => 'm_' . $column,
                'type' => (strpos($column, 'field_ft_') !== false) ? 'tinytext' : 'text'
            );
        }

        ee()->smartforge->modify_column('member_data', $columns_to_modify);
    }

    /**
     * Add snippet edit dates so that we know when files are stale
     */
    private function add_snippet_edit_date()
    {
        ee()->smartforge->add_column(
            'snippets',
            array(
                'edit_date' => array(
                    'type' => 'int',
                    'constraint' => 10,
                    'null' => false,
                    'default' => 0
                ),
            )
        );

        if (ee()->config->item('save_tmpl_files') == 'y') {
            $snippets = ee('Model')->get('Snippet')->all();
            $snippets->save();
        }
    }

    /**
     * Add global variable edit dates so that we know when files are stale
     */
    private function add_global_variable_edit_date()
    {
        ee()->smartforge->add_column(
            'global_variables',
            array(
                'edit_date' => array(
                    'type' => 'int',
                    'constraint' => 10,
                    'null' => false,
                    'default' => 0
                ),
            )
        );

        if (ee()->config->item('save_tmpl_files') == 'y') {
            $variables = ee('Model')->get('GlobalVariable')->all();
            $variables->save();
        }
    }

    /*
     * Move the default avatars into a subdirectory
     * @return void
     */
    private function move_avatars()
    {
        $avatar_paths = array();
        $avatar_path = ee()->config->item('avatar_path');

        // config file has precedence, otherwise do the per-site ones
        if (! $avatar_path) {
            $sites = ee()->db->get('sites');

            foreach ($sites->result_array() as $site) {
                $data = unserialize(base64_decode($site['site_member_preferences']));

                $avatar_path = $data['avatar_path'];
                $avatar_path = realpath($avatar_path);

                if (! empty($avatar_path)) {
                    $avatar_paths[] = $avatar_path;
                }
            }
        } else {
            $avatar_path = realpath($avatar_path);

            // Does the path exist?
            if (empty($avatar_path)) {
                throw new UpdaterException_3_1_0('Please correct the avatar path in your config file.');
            }

            $avatar_paths[] = $avatar_path;
        }

        foreach ($avatar_paths as $avatar_path) {
            // Check that we haven't already done this
            if (file_exists($avatar_path . '/default/')) {
                return true;
            }

            if (! file_exists($avatar_path)) {
                throw new UpdaterException_3_1_0("Please correct the avatar path in your config file.");
            }

            // Check to see if the directory is writable
            if (! is_writable($avatar_path)) {
                if (! @chmod($avatar_path, DIR_WRITE_MODE)) {
                    throw new UpdaterException_3_1_0("Please correct the permissions on your avatar directory.");
                }
            }

            // Create the default directory
            if (! mkdir($avatar_path . '/default/', DIR_WRITE_MODE)) {
                throw new UpdaterException_3_1_0("Please correct the permissions on your avatar directory.");
            }

            // Copy over the index.html
            if (! copy($avatar_path . '/index.html', $avatar_path . '/default/index.html')) {
                throw new UpdaterException_3_1_0("Please correct the permissions on your avatar directory.");
            }

            $default_avatars = array(
                'avatar_tree_hugger_color.png',
                'bad_fur_day.jpg',
                'big_horns.jpg',
                'eat_it_up.jpg',
                'ee_paint.jpg',
                'expression_radar.jpg',
                'flying_high.jpg',
                'hair.png',
                'hanging_out.jpg',
                'hello_prey.jpg',
                'light_blur.jpg',
                'ninjagirl.png',
                'procotopus.png',
                'sneak_squirrel.jpg',
                'zombie_bunny.png'
            );
            foreach ($default_avatars as $filename) {
                if (file_exists($avatar_path . '/' . $filename)
                    && ! rename($avatar_path . '/' . $filename, $avatar_path . '/default/' . $filename)) {
                    throw new UpdaterException_3_1_0("Please correct the permissions on your avatar directory.");
                }
            }
        }
    }

    private function update_collation_config()
    {
        $db_config = ee()->config->item('database');

        // If coming from the CLI, this will have already been taken care of.
        if (isset($db_config['expressionengine'])) {
            $config = $db_config['expressionengine'];

            if (isset($config['dbcollat']) && $config['dbcollat'] == 'utf8_general_ci') {
                $config['dbcollat'] = 'utf8_unicode_ci';
                ee()->config->_update_dbconfig($config);
            }
        }
    }

    private function fix_table_collations()
    {
        $tables = ee()->db->list_tables(true);

        foreach ($tables as $table) {
            $status = ee()->db->query("SHOW TABLE STATUS LIKE '$table'");

            if ($status->num_rows() != 1 || $status->row('Collation') == 'utf8_unicode_ci') {
                continue;
            }

            ee()->db->query("ALTER TABLE $table CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci");
        }
    }

    private function ensure_upload_directories_are_correct()
    {
        $sites = ee()->db->get('sites');

        foreach ($sites->result_array() as $site) {
            $member_prefs = unserialize(base64_decode($site['site_member_preferences']));
            $member_directories = array();

            $member_directories['Avatars'] = array(
                'server_path' => $member_prefs['avatar_path'],
                'url' => $member_prefs['avatar_url'],
                'allowed_types' => 'img',
                'max_width' => $member_prefs['avatar_max_width'],
                'max_height' => $member_prefs['avatar_max_height'],
                'max_size' => $member_prefs['avatar_max_kb'],
            );

            $member_directories['Default Avatars'] = array(
                'server_path' => rtrim($member_prefs['avatar_path'], '/') . '/default/',
                'url' => rtrim($member_prefs['avatar_url'], '/') . '/default/',
                'allowed_types' => 'img',
                'max_width' => $member_prefs['avatar_max_width'],
                'max_height' => $member_prefs['avatar_max_height'],
                'max_size' => $member_prefs['avatar_max_kb'],
            );

            $member_directories['Member Photos'] = array(
                'server_path' => $member_prefs['photo_path'],
                'url' => $member_prefs['photo_url'],
                'allowed_types' => 'img',
                'max_width' => $member_prefs['photo_max_width'],
                'max_height' => $member_prefs['photo_max_height'],
                'max_size' => $member_prefs['photo_max_kb'],
            );

            $member_directories['Signature Attachments'] = array(
                'server_path' => $member_prefs['sig_img_path'],
                'url' => $member_prefs['sig_img_url'],
                'allowed_types' => 'img',
                'max_width' => $member_prefs['sig_img_max_width'],
                'max_height' => $member_prefs['sig_img_max_height'],
                'max_size' => $member_prefs['sig_img_max_kb'],
            );

            $member_directories['PM Attachments'] = array(
                'server_path' => $member_prefs['prv_msg_upload_path'],
                'url' => str_replace('avatars', 'pm_attachments', $member_prefs['avatar_url']),
                'allowed_types' => 'img',
                'max_size' => $member_prefs['prv_msg_attach_maxsize']
            );

            $existing = ee('db')->from('upload_prefs')
                ->select('name')
                ->where_in('name', array_keys($member_directories))
                ->where('site_id', $site['site_id'])
                ->get();

            foreach ($existing->result() as $row) {
                unset($member_directories[$row->name]);
            }

            foreach ($member_directories as $name => $data) {
                $data['site_id'] = $site['site_id'];
                $data['name'] = $name;
                $data['module_id'] = 1; // this is a terribly named column - should be called `hidden`
                ee()->db->insert('upload_prefs', $data);

                ee()->db->where('upload_id', ee()->db->insert_id());
                ee()->db->delete('upload_no_access');
            }
        }
    }

    /**
     * Adds the max_entries and total_records column to the exp_channels table
     * for the new Max Entries feature for Channels
     *
     * NOTE: These columns were added in 3.4 but they need to be added here for
     * folks upgrading from an earlier version because we access the Channel
     * model below, and a 3.4+ Channel model needs these columns present
     */
    private function add_channel_max_entries_columns()
    {
        ee()->smartforge->add_column(
            'channels',
            array(
                'max_entries' => array(
                    'type' => 'int',
                    'null' => false,
                    'unsigned' => true,
                    'default' => 0
                ),
            )
        );

        ee()->smartforge->add_column(
            'channels',
            array(
                'total_records' => array(
                    'type' => 'mediumint',
                    'constraint' => 8,
                    'null' => false,
                    'unsigned' => true,
                    'default' => 0
                ),
            ),
            'total_entries'
        );
    }

    /**
     * Fields added after a layout was crated, never made it into the layout.
     *
     * @return void
     */
    private function synchronize_layouts()
    {
        // Fix for running this update routine in a >= 4.1 context, preview_url
        // column must be present to access Channel model below
        ee()->smartforge->add_column(
            'channels',
            array(
                'preview_url' => array(
                    'type' => 'VARCHAR(100)',
                    'null' => true,
                )
            )
        );

        $custom_fields = array();

        $layouts = ee('Model')->get('ChannelLayout')
            ->with('Channel')
            ->fields('field_layout', 'Channel.field_group')
            ->all();

        foreach ($layouts as $layout) {
            if (! isset($layout->Channel)) {
                continue;
            }

            // Account for any new fields that have been added to the channel
            // since the last edit

            $query = ee()->db->select('field_id')
                ->where('group_id', $layout->Channel->field_group)
                ->get('channel_fields');

            foreach ($query->result_array() as $row) {
                $custom_fields[$row['field_id']] = $row['field_id'];
            }

            foreach ($layout->field_layout as $section) {
                foreach ($section['fields'] as $field_info) {
                    if (strpos($field_info['field'], 'field_id_') == 0) {
                        $id = str_replace('field_id_', '', $field_info['field']);
                        unset($custom_fields[$id]);
                    }
                }
            }

            $field_layout = $layout->field_layout;

            foreach ($custom_fields as $id => $val) {
                $field_info = array(
                    'field' => 'field_id_' . $id,
                    'visible' => true,
                    'collapsed' => false
                );
                $field_layout[0]['fields'][] = $field_info;
            }

            $layout->field_layout = $field_layout;
            $layout->save();
        }
    }

    /**
     * We were putting all templates into the routes table, regardless of whether
     * they had a route
     *
     * @return void
     */
    private function template_routes_remove_empty()
    {
        if (ee()->db->table_exists('template_routes')) {
            ee()->db->or_where('route IS NULL OR route = ""');
            ee()->db->delete('template_routes');
        }
    }
}

class UpdaterException_3_1_0 extends Exception
{
    public function __construct($message)
    {
        parent::__construct($message . ' <a href="https://docs.expressionengine.com/v3/installation/version_notes_3.1.0.html">Please see 3.1.0 version notes.</a>');
    }
}

// EOF
