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

/**
 * Model to manage Member Groups.  Manages the following
 * tables:
 * 	exp_member_groups
 * 	exp_channel_member_groups
 * 	exp_module_member_roles
 * 	exp_template_member_groups
 */
class Member_group_model extends CI_Model
{
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Determine whether a group with a certain title exists or not.
     * Throws an error when a title is found.
     *
     * TODO This could probably be made public and generalized,
     * if we're ever going to use it elsewhere.  The show_error
     * call would need to be bumped into parse_add_form and parse_edit_form
     *
     * @param int $group_id The id of the group we want to check.
     * @param string $group_title The title to look for.
     *
     * @return boolean True if a group with the given title is found, false otherwise.
     */
    private function _group_title_exists($site_id, $group_id, $group_title)
    {
        $this->db->from('roles')
            ->where('name', $group_title)
            ->where('role_id !=', $group_id);

        if ($this->db->count_all_results()) {
            return true;
        }

        return false;
    }

    /**
     * Parse the data from the post data we're passed. Builds a data array,
     * containing several permissions sub arrays that are meant to be pulled
     * out before the data array is sent to the database as a row
     * in the exp_member_groups table.  The subarrays to be
     * pulled out are 'channel', 'template' and 'module'.
     *
     * @param int $group_id The id of the group we're parsing data for.
     * @param int $site_id The site id submitted with the form, not necessarily the site we'll be creating a group for.
     */
    private function _parse_form_data($post, $form_site_id, $group_id)
    {

        /** ----------------------------------------------------
        /**  Remove and Store Channel and Template Permissions
        /** ----------------------------------------------------*/
        $data = array('group_title' => $this->input->post('group_title'),
            'group_description' => $this->input->post('group_description'),
            'group_id' => $group_id);

        // If editing Super Admin group, the is_locked field doesn't exist, so make sure we
        // got a value from the form before writing 0 to the database
        if ($this->input->post('is_locked') !== false) {
            $data['is_locked'] = $this->input->post('is_locked');
        }

        $data['channel'] = array();
        $data['module'] = array();
        $data['template'] = array();
        foreach ($post as $key => $val) {
            if (substr($key, 0, strlen($form_site_id . '_channel_id_')) == $form_site_id . '_channel_id_') {
                $channel_id = substr($key, strlen($form_site_id . '_channel_id_'));
                if ($val == 'y') {
                    $data['channel'][$channel_id] = array(
                        'channel_id' => $channel_id,
                        'group_id' => $group_id
                    );
                } else {
                    $data['channel'][$channel_id] = false;
                }
            } elseif (substr($key, 0, strlen('module_id_')) == 'module_id_') {
                $module_id = substr($key, strlen('module_id_'));
                if ($val == 'y') {
                    $data['module'][$module_id] = array(
                        'module_id' => $module_id,
                        'group_id' => $group_id
                    );
                } else {
                    $data['module'][$module_id] = false;
                }
            } elseif (substr($key, 0, strlen($form_site_id . '_template_id_')) == $form_site_id . '_template_id_') {
                $template_id = substr($key, strlen($form_site_id . '_template_id_'));
                if ($val == 'y') {
                    $data['template'][$template_id] = array(
                        'template_group_id' => $template_id,
                        'group_id' => $group_id
                    );
                } else {
                    $data['template'][$template_id] = false;
                }
            } elseif (substr($key, 0, strlen($form_site_id . '_')) == $form_site_id . '_') {
                $data[substr($key, strlen($form_site_id . '_'))] = $post[$key];
            } else {
                continue;
            }

            unset($post[$key]);
        }

        return $data;
    }

    /**
     * TODO Anyone know what and why?
     */
    private function _update_uploads($group_id, $site_id)
    {
        return "deprecated";
    }

    /**
     * Update Category Group Privileges
     *
     * Updates exp_category_groups privilege lists for
     * editing and deleting categories
     *
     * @return	mixed
     */
    private function _update_cat_group_privs($params)
    {
        if (! is_array($params) or empty($params)) {
            return false;
        }

        $expected = array('member_group', 'field', 'allow', 'site_id', 'clone_id');

        // turn parameters into variables
        foreach ($expected as $key) {
            // naughty!
            if (! isset($params[$key])) {
                return false;
            }

            $$key = $params[$key];
        }

        $query = $this->db->query("SELECT group_id, " . $this->db->escape_str($field) . " FROM exp_category_groups WHERE site_id = '" . $this->db->escape_str($site_id) . "'");

        // nothing to do?

        if ($query->num_rows() == 0) {
            return false;
        }

        foreach ($query->result_array() as $row) {
            $can_do = explode('|', rtrim($row[$field], '|'));

            if ($allow === true) {
                if (is_numeric($clone_id)) {
                    if (in_array($clone_id, $can_do) or $clone_id == 1) {
                        $can_do[] = $member_group;
                    }
                } elseif ($clone_id === false) {
                    $can_do[] = $member_group;
                }
            } else {
                $can_do = array_diff($can_do, array($member_group));
            }

            $this->db->query($this->db->update_string('exp_category_groups', array($field => implode('|', $can_do)), "group_id = '{$row['group_id']}'"));
        }
    }

    /**
     * Update the related permissions tables
     *
     * 	exp_channel_member_groups
     * 	exp_module_member_roles
     * 	exp_template_member_groups
     *
     * @param int $group_id The id of the group who's relations we're updating.
     * @param array $permissions The packed permissions array that comes from _parse_form_data()
     */
    private function _update_permissions($group_id, array $permissions)
    {
        // Unpack the data we packed into the data
        // array.  Yes, this isn't optimal.
        $channel_ids = array();
        $channel_ids_yes = array();
        foreach ($permissions['channel'] as $id => $value) {
            $channel_ids[] = $id;
            if ($value !== false) {
                $channel_ids_yes[] = $value;
            }
        }

        $module_ids = array();
        $module_ids_yes = array();
        foreach ($permissions['module'] as $id => $value) {
            $module_ids[] = $id;
            if ($value !== false) {
                $module_ids_yes[] = $value;
            }
        }

        $template_ids = array();
        $template_ids_yes = array();
        foreach ($permissions['template'] as $id => $value) {
            $template_ids[] = $id;
            if ($value !== false) {
                $template_ids_yes[] = $value;
            }
        }

        // First, delete old channel, module and template permissions for this site
        if (! empty($channel_ids)) {
            $this->delete_channel_permissions($group_id, $channel_ids);
        }
        if (! empty($module_ids)) {
            $this->delete_module_permissions($group_id, $module_ids);
        }
        if (! empty($template_ids)) {
            $this->delete_template_permissions($group_id, $template_ids);
        }

        // Then, add back in the only ones that should exist based on the form submission
        if (! empty($channel_ids_yes)) {
            $this->db->insert_batch('channel_member_roles', $channel_ids_yes);
        }
        if (! empty($module_ids_yes)) {
            $this->db->insert_batch('module_member_roles', $module_ids_yes);
        }
        if (! empty($template_ids_yes)) {
            $this->db->insert_batch('template_groups_roles', $template_ids_yes);
        }
    }

    /**
     * Handle the input from the Member Groups form when we're adding.
     * 	(CP Home > Members > Member Groups > Add Member Group)
     *
     * Update the exp_member_groups table and several child and related tables.
     *
     * @param array $post The post data from the form that we are processing.
     * @param int $site_id The id of the site to which the group belongs.
     * @param int $clone_id TODO  Anyone know what this is?
     * @param string $group_title The title of the group we're adding.
     *
     * @return The message to send to the CP.
     */
    public function parse_add_form(array $post, $form_site_id, $clone_id, $group_title)
    {
        ee()->load->library('logger');
        ee()->logger->deprecated('6.0.0');
        // This is less than optimal, but it allows us to use
        // that foreach loop for both multi-site manager and
        // single site.
        // FIXME This could be done much better. -Daniel
        if ($this->config->item('multiple_sites_enabled') == 'y') {
            $this->load->model('site_model');
            $site_ids = $this->site_model->get_site_ids();
        } else {
            $site_ids = array($this->config->item('site_id'));
        }

        // Get the next available group id to use for our new group.
        $query = $this->db->query("SELECT MAX(group_id) as max_group FROM exp_member_groups");
        $group_id = $query->row('max_group') + 1;

        $data = $this->_parse_form_data($post, $form_site_id, $group_id);
        // We'll need this later when we call $this->_update_permissions()
        // We'll have to unpack them and it's klutzy, but for now, this is the best I got. -Daniel
        $permissions = array('channel' => $data['channel'],'module' => $data['module'],'template' => $data['template']);
        // And we don't want them hanging around in the data arround to be stuck into the database.
        unset($data['channel']);
        unset($data['module']);
        unset($data['template']);

        $created_group = false;
        foreach ($site_ids as $site_id) {
            if ($this->_group_title_exists($site_id, $group_id, $group_title)) {
                continue;
            }
            // Override the site id set by _parse_form_data() with
            // the one we're using in our loop.
            $data['site_id'] = $site_id;

            $this->create($data);

            $this->_update_uploads($group_id, $site_id);

            if ($group_id != 1) {
                $cat_group_privs = array('can_edit_categories', 'can_delete_categories');
                foreach ($cat_group_privs as $field) {
                    $privs = array(
                        'member_group' => $group_id,
                        'field' => $field,
                        'allow' => ($data[$field] == 'y') ? true : false,
                        'site_id' => $site_id,
                        'clone_id' => $clone_id
                    );

                    $this->_update_cat_group_privs($privs);
                }
            }

            $created_group = true;
        }

        // This doesn't use site_id, only group_id.  The ids recieved
        // in the form for permissions will either be specific to the
        // site the form is submitted for, or they'll be common across
        // all sites (in the case of modules).  So this can happen
        // out side the creation loop.
        $this->_update_permissions($group_id, $permissions);

        if ($created_group) {
            return lang('member_group_created') . NBS . NBS . $group_title;
        } else {
            show_error(lang('group_title_exists'));

            return;
        }
    }

    /**
     * Handle the input from the Member Groups form when we're editing.
     * 	(CP Home > Members > Member Groups > Edit Member Group)
     *
     * Update the exp_member_groups table and several child and related tables.
     *
     * @param array $post The post data from the form that we are processing.
     * @param int $group_id The id of the group we're editing.
     * @param int $site_id The id of the site to which the group belongs.
     * @param int $clone_id TODO  Anyone know what this is?
     * @param string $group_title The title of the group we're editing.
     *
     * @return The message to send to the CP.
     */
    public function parse_edit_form(array $post, $group_id, $site_id, $clone_id, $group_title)
    {
        ee()->load->library('logger');
        ee()->logger->deprecated('6.0.0');
        if ($this->_group_title_exists($site_id, $group_id, $group_title)) {
            show_error(lang('group_title_exists'));

            return;
        }

        $is_multisite = ($this->config->item('multiple_sites_enabled') == 'y' ? true : false);
        if ($is_multisite) {
            $groups_query = $this->get(array('group_id' => $group_id));
            $title_changed = false;
            foreach ($groups_query->result_array() as $group) {
                if ($group['site_id'] != $site_id && $group['group_title'] != $group_title) {
                    $title_changed = true;
                }
            }
        }

        if ($is_multisite && $title_changed) {
            $data = array('group_title' => $group_title);
            $this->update_all_sites($group_id, $data);
        }

        $query = $this->db->query('SELECT site_id, can_edit_categories, can_delete_categories FROM exp_member_groups WHERE group_id = "' . $this->db->escape_str($group_id) . '"');

        $old_cat_privs = array();
        foreach ($query->result_array() as $row) {
            $old_cat_privs[$row['site_id']]['can_edit_categories'] = $row['can_edit_categories'];
            $old_cat_privs[$row['site_id']]['can_delete_categories'] = $row['can_delete_categories'];
        }

        $data = $this->_parse_form_data($post, $site_id, $group_id);
        unset($data['group_id']);

        // We'll need this later when we call $this->_update_permissions()
        // We'll have to unpack them and it's klutzy, but for now, this is the best I got.
        $permissions = array('channel' => $data['channel'], 'module' => $data['module'], 'template' => $data['template']);
        // And we don't want them hanging around in the data arround to be stuck into the database.
        unset($data['channel']);
        unset($data['module']);
        unset($data['template']);

        $this->update($group_id, $site_id, $data);

        if ($group_id != 1) {
            // update category group discrete privileges
            $cat_group_privs = array('can_edit_categories', 'can_delete_categories');
            foreach ($cat_group_privs as $field) {
                // only modify category group privs if value changed, so we do not
                // globally overwrite existing defined privileges carelessly
                if ($old_cat_privs[$site_id][$field] != $data[$field]) {
                    $privs = array(
                        'member_group' => $group_id,
                        'field' => $field,
                        'allow' => ($data[$field] == 'y') ? true : false,
                        'site_id' => $site_id,
                        'clone_id' => $clone_id
                    );

                    $this->_update_cat_group_privs($privs);
                }
            }
        }

        $this->_update_permissions($group_id, $permissions);

        return lang('member_group_updated') . NBS . NBS . $group_title;
    }

    /**
     * Retrieve rows from the exp_member_groups table.  $fields is an array
     * of fields to select.  It defaults to group_id, site_id and group_title.
     * $where_group is an array of where condition groupings.
     *
     * @param array $where_conditions The where conditions you wish to limit your search by.
     * @param array $fields The fields you wish to select.
     */
    public function get(array $where_conditions = array(), array $fields = array('group_id', 'site_id', 'group_title'))
    {
        ee()->load->library('logger');
        ee()->logger->deprecated('6.0.0', "ee('Model')->get('Role')");
        if ($key = array_search('group_id', $fields)) {
            $fields[$key] = 'role_id AS group_id';
        }
        if ($key = array_search('site_id', $fields)) {
            unset($fields[$key]);
        }
        if ($key = array_search('group_title', $fields)) {
            $fields[$key] = 'name AS group_title';
        }
        $this->db->select(implode(',', $fields));

        foreach ($where_conditions as $field => $value) {
            if (is_array($value)) {
                $this->db->where_in($field, $value);
            } else {
                $this->db->where($field, $value);
            }
        }

        return $this->db->get('exp_roles');
    }

    /**
     * Create a row in the exp_member_groups table.
     *
     * @param array $data  The data we will insert into the table.
     *
     * @return void
     */
    public function create(array $data)
    {
        ee()->load->library('logger');
        ee()->logger->deprecated('6.0.0', "ee('Model')->make('Role')");
        $this->db->insert('exp_roles', $data);
    }

    /**
     * Apply the update to all groups with with the given group_id.
     *
     * NOTE: Made this a seperate method instead of making site_id
     * optional in update, because leaving out the site_id is not
     * something we want to happen accidentially.  If we're going
     * to be updating all member groups, it should be done with
     * intention.
     *
     * @param int $group_id The id of the member group that we'll be updating.
     * @param array $data The data we'll changing to in the form db_field=>value.
     */
    public function update_all_sites($group_id, array $data)
    {
        ee()->load->library('logger');
        ee()->logger->deprecated('6.0.0');
        $this->db->where('role_id', $group_id);
        $this->db->update('exp_roles', $data);

        return $this->db->affected_rows();
    }

    /**
     * Update a row in the exp_member_groups table.
     *
     * @param int $group_id The id of the group we're updating
     * @param int $site_id The id of the site that the group belongs to, this is the second half of the primary key
     * @param array $data The array of data we will use to update.
     *
     * @return void
     */
    public function update($group_id, $site_id, array $data)
    {
        ee()->load->library('logger');
        ee()->logger->deprecated('6.0.0', "ee('Model')->get('Role', \$role_id)->save()");
        $this->db->where('role_id', $group_id);
        $this->db->update('exp_roles', $data);

        return $this->db->affected_rows();
    }

    /**
     * Delete a group of rows from the exp_channel_member_groups table.  The
     * rows have ids in the $channel_ids array and a group_id equal to $group_id.
     *
     * @param int $group_id The group for which to delete the permissions rows.
     * @param array $channel_ids A list of channel ids to delete.
     */
    public function delete_channel_permissions($group_id, array $channel_ids)
    {
        ee()->load->library('logger');
        ee()->logger->deprecated('6.0.0');
        $this->db->where('role_id', $group_id);
        $this->db->where_in('channel_id', $channel_ids);
        $this->db->delete('channel_member_roles');
    }

    /**
     * Delete a group of rows from the exp_template_member_groups table.  The
     * rows have ids in the $template_ids array and a group_id equal to $group_id.
     *
     * @param int $group_id The group for which to delete the permissions rows.
     * @param array $template_ids A list of template ids to delete.
     */
    public function delete_template_permissions($group_id, array $template_ids)
    {
        ee()->load->library('logger');
        ee()->logger->deprecated('6.0.0');
        $this->db->where('role_id', $group_id);
        $this->db->where_in('template_group_id', $template_ids);
        $this->db->delete('templates_roles');
    }

    /**
     * Delete a group of rows from the exp_module_member_roles table.  The
     * rows have ids in the $module_ids array and a group_id equal to $group_id.
     *
     * @param int $group_id The group for which to delete the permissions rows.
     * @param array $module_ids A list of channel ids to delete.
     *
     * @return void
     */
    public function delete_module_permissions($group_id, array $module_ids)
    {
        ee()->load->library('logger');
        ee()->logger->deprecated('6.0.0');
        $this->db->where('role_id', $group_id);
        $this->db->where_in('module_id', $module_ids);
        $this->db->delete('module_member_roles');
    }
}

// EOF
