<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/

/**
* @ignore
*/
if (!defined('IN_PHPBB'))
{
	exit;
}

class acp_styles
{
	public $u_action;

	protected $u_base_action;
	protected $s_hidden_fields;
	protected $mode;
	protected $styles_path;
	protected $styles_path_absolute = 'styles';
	protected $default_style = 0;
	protected $styles_list_cols = 0;
	protected $reserved_style_names = array('adm', 'admin', 'all');

	/** @var \phpbb\config\config */
	protected $config;

	/** @var \phpbb\db\driver\driver_interface */
	protected $db;

	/** @var \phpbb\user */
	protected $user;

	/** @var \phpbb\template\template */
	protected $template;

	/** @var \phpbb\request\request_interface */
	protected $request;

	/** @var \phpbb\cache\driver\driver_interface */
	protected $cache;

	/** @var \phpbb\auth\auth */
	protected $auth;

	/** @var \phpbb\textformatter\cache_interface */
	protected $text_formatter_cache;

	/** @var string */
	protected $phpbb_root_path;

	/** @var string */
	protected $php_ext;

	/** @var \phpbb\event\dispatcher_interface */
	protected $dispatcher;

	public function main($id, $mode)
	{
		global $db, $user, $phpbb_admin_path, $phpbb_root_path, $phpEx, $template, $request, $cache, $auth, $config, $phpbb_dispatcher, $phpbb_container;

		$this->db = $db;
		$this->user = $user;
		$this->template = $template;
		$this->request = $request;
		$this->cache = $cache;
		$this->auth = $auth;
		$this->text_formatter_cache = $phpbb_container->get('text_formatter.cache');
		$this->config = $config;
		$this->phpbb_root_path = $phpbb_root_path;
		$this->php_ext = $phpEx;
		$this->dispatcher = $phpbb_dispatcher;

		$this->default_style = $config['default_style'];
		$this->styles_path = $this->phpbb_root_path . $this->styles_path_absolute . '/';

		$this->u_base_action = append_sid("{$phpbb_admin_path}index.{$this->php_ext}", "i={$id}");
		$this->s_hidden_fields = array(
			'mode'		=> $mode,
		);

		$this->user->add_lang('acp/styles');

		$this->tpl_name = 'acp_styles';
		$this->page_title = 'ACP_CAT_STYLES';
		$this->mode = $mode;

		$action = $this->request->variable('action', '');
		$post_actions = array('install', 'activate', 'deactivate', 'uninstall');

		foreach ($post_actions as $key)
		{
			if ($this->request->is_set_post($key))
			{
				$action = $key;
			}
		}

		// The uninstall action uses confirm_box() to verify the validity of the request,
		// so there is no need to check for a valid token here.
		if (in_array($action, $post_actions) && $action != 'uninstall')
		{
			$is_valid_request = check_link_hash($request->variable('hash', ''), $action) || check_form_key('styles_management');

			if (!$is_valid_request)
			{
				trigger_error($user->lang['FORM_INVALID'] . adm_back_link($this->u_action), E_USER_WARNING);
			}
		}

		if ($action != '')
		{
			$this->s_hidden_fields['action'] = $action;
		}

		$this->template->assign_vars(array(
			'U_ACTION'			=> $this->u_base_action,
			'S_HIDDEN_FIELDS'	=> build_hidden_fields($this->s_hidden_fields)
			)
		);

		/**
		 * Run code before ACP styles action execution
		 *
		 * @event core.acp_styles_action_before
		 * @var	int     id          Module ID
		 * @var	string  mode        Active module
		 * @var	string  action      Module that should be run
		 * @since 3.1.7-RC1
		 */
		$vars = array('id', 'mode', 'action');
		extract($this->dispatcher->trigger_event('core.acp_styles_action_before', compact($vars)));

		// Execute actions
		switch ($action)
		{
			case 'install':
				$this->action_install();
				return;
			case 'uninstall':
				$this->action_uninstall();
				return;
			case 'activate':
				$this->action_activate();
				return;
			case 'deactivate':
				$this->action_deactivate();
				return;
			case 'details':
				$this->action_details();
				return;
			default:
				$this->frontend();
		}
	}

	/**
	* Main page
	*/
	protected function frontend()
	{
		add_form_key('styles_management');

		// Check mode
		switch ($this->mode)
		{
			case 'style':
				$this->welcome_message('ACP_STYLES', 'ACP_STYLES_EXPLAIN');
				$this->show_installed();
				return;
			case 'install':
				$this->welcome_message('INSTALL_STYLES', 'INSTALL_STYLES_EXPLAIN');
				$this->show_available();
				return;
		}
		trigger_error($this->user->lang['NO_MODE'] . adm_back_link($this->u_action), E_USER_WARNING);
	}

	/**
	* Install style(s)
	*/
	protected function action_install()
	{
		// Get list of styles to install
		$dirs = $this->request_vars('dir', '', true);

		// Get list of styles that can be installed
		$styles = $this->find_available(false);

		// Install each style
		$messages = array();
		$installed_names = array();
		$installed_dirs = array();
		foreach ($dirs as $dir)
		{
			if (in_array($dir, $this->reserved_style_names))
			{
				$messages[] = $this->user->lang('STYLE_NAME_RESERVED', htmlspecialchars($dir));
				continue;
			}

			$found = false;
			foreach ($styles as &$style)
			{
				// Check if:
				// 1. Directory matches directory we are looking for
				// 2. Style is not installed yet
				// 3. Style with same name or directory hasn't been installed already within this function
				if ($style['style_path'] == $dir && empty($style['_installed']) && !in_array($style['style_path'], $installed_dirs) && !in_array($style['style_name'], $installed_names))
				{
					// Install style
					$style['style_active'] = 1;
					$style['style_id'] = $this->install_style($style);
					$style['_installed'] = true;
					$found = true;
					$installed_names[] = $style['style_name'];
					$installed_dirs[] = $style['style_path'];
					$messages[] = sprintf($this->user->lang['STYLE_INSTALLED'], htmlspecialchars($style['style_name']));
				}
			}
			if (!$found)
			{
				$messages[] = sprintf($this->user->lang['STYLE_NOT_INSTALLED'], htmlspecialchars($dir));
			}
		}

		// Invalidate the text formatter's cache for the new styles to take effect
		if (!empty($installed_names))
		{
			$this->text_formatter_cache->invalidate();
		}

		// Show message
		if (!count($messages))
		{
			trigger_error($this->user->lang['NO_MATCHING_STYLES_FOUND'] . adm_back_link($this->u_action), E_USER_WARNING);
		}
		$message = implode('<br />', $messages);
		$message .= '<br /><br /><a href="' . $this->u_base_action . '&amp;mode=style' . '">&laquo; ' . $this->user->lang('STYLE_INSTALLED_RETURN_INSTALLED_STYLES') . '</a>';
		$message .= '<br /><br /><a href="' . $this->u_base_action . '&amp;mode=install' . '">&raquo; ' . $this->user->lang('STYLE_INSTALLED_RETURN_UNINSTALLED_STYLES') . '</a>';
		trigger_error($message, E_USER_NOTICE);
	}

	/**
	* Confirm styles removal
	*/
	protected function action_uninstall()
	{
		// Get list of styles to uninstall
		$ids = $this->request_vars('id', 0, true);

		// Check if confirmation box was submitted
		if (confirm_box(true))
		{
			// Uninstall
			$this->action_uninstall_confirmed($ids, $this->request->variable('confirm_delete_files', false));
			return;
		}

		// Confirm box
		$s_hidden = build_hidden_fields(array(
			'action'	=> 'uninstall',
			'ids'		=> $ids
		));
		$this->template->assign_var('S_CONFIRM_DELETE', true);
		confirm_box(false, $this->user->lang['CONFIRM_UNINSTALL_STYLES'], $s_hidden, 'acp_styles.html');

		// Canceled - show styles list
		$this->frontend();
	}

	/**
	* Uninstall styles(s)
	*
	* @param array $ids List of style IDs
	* @param bool $delete_files If true, script will attempt to remove files for selected styles
	*/
	protected function action_uninstall_confirmed($ids, $delete_files)
	{
		global $user, $phpbb_log;

		$default = $this->default_style;
		$uninstalled = array();
		$messages = array();

		// Check styles list
		foreach ($ids as $id)
		{
			if (!$id)
			{
				trigger_error($this->user->lang['INVALID_STYLE_ID'] . adm_back_link($this->u_action), E_USER_WARNING);
			}
			if ($id == $default)
			{
				trigger_error($this->user->lang['UNINSTALL_DEFAULT'] . adm_back_link($this->u_action), E_USER_WARNING);
			}
			$uninstalled[$id] = false;
		}

		// Order by reversed style_id, so parent styles would be removed after child styles
		// This way parent and child styles can be removed in same function call
		$sql = 'SELECT *
			FROM ' . STYLES_TABLE . '
			WHERE style_id IN (' . implode(', ', $ids) . ')
			ORDER BY style_id DESC';
		$result = $this->db->sql_query($sql);

		$rows = $this->db->sql_fetchrowset($result);
		$this->db->sql_freeresult($result);

		// Uinstall each style
		$uninstalled = array();
		foreach ($rows as $style)
		{
			$result = $this->uninstall_style($style, $delete_files);

			if (is_string($result))
			{
				$messages[] = $result;
				continue;
			}
			$messages[] = sprintf($this->user->lang['STYLE_UNINSTALLED'], $style['style_name']);
			$uninstalled[] = $style['style_name'];

			// Attempt to delete files
			if ($delete_files)
			{
				$messages[] = sprintf($this->user->lang[$this->delete_style_files($style['style_path']) ? 'DELETE_STYLE_FILES_SUCCESS' : 'DELETE_STYLE_FILES_FAILED'], $style['style_name']);
			}
		}

		if (empty($messages))
		{
			// Nothing to uninstall?
			trigger_error($this->user->lang['NO_MATCHING_STYLES_FOUND'] . adm_back_link($this->u_action), E_USER_WARNING);
		}

		// Log action
		if (count($uninstalled))
		{
			$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_STYLE_DELETE', false, array(implode(', ', $uninstalled)));
		}

		// Clear cache
		$this->cache->purge();

		// Show message
		trigger_error(implode('<br />', $messages) . adm_back_link($this->u_action), E_USER_NOTICE);
	}

	/**
	* Activate styles
	*/
	protected function action_activate()
	{
		// Get list of styles to activate
		$ids = $this->request_vars('id', 0, true);

		// Activate styles
		$sql = 'UPDATE ' . STYLES_TABLE . '
			SET style_active = 1
			WHERE style_id IN (' . implode(', ', $ids) . ')';
		$this->db->sql_query($sql);

		// Purge cache
		$this->cache->destroy('sql', STYLES_TABLE);

		// Show styles list
		$this->frontend();
	}

	/**
	* Deactivate styles
	*/
	protected function action_deactivate()
	{
		// Get list of styles to deactivate
		$ids = $this->request_vars('id', 0, true);

		// Check for default style
		foreach ($ids as $id)
		{
			if ($id == $this->default_style)
			{
				trigger_error($this->user->lang['DEACTIVATE_DEFAULT'] . adm_back_link($this->u_action), E_USER_WARNING);
			}
		}

		// Reset default style for users who use selected styles
		$sql = 'UPDATE ' . USERS_TABLE . '
			SET user_style = ' . (int) $this->default_style . '
			WHERE user_style IN (' . implode(', ', $ids) . ')';
		$this->db->sql_query($sql);

		// Deactivate styles
		$sql = 'UPDATE ' . STYLES_TABLE . '
			SET style_active = 0
			WHERE style_id IN (' . implode(', ', $ids) . ')';
		$this->db->sql_query($sql);

		// Purge cache
		$this->cache->destroy('sql', STYLES_TABLE);

		// Show styles list
		$this->frontend();
	}

	/**
	* Show style details
	*/
	protected function action_details()
	{
		global $user, $phpbb_log;

		$id = $this->request->variable('id', 0);
		if (!$id)
		{
			trigger_error($this->user->lang['NO_MATCHING_STYLES_FOUND'] . adm_back_link($this->u_action), E_USER_WARNING);
		}

		// Get all styles
		$styles = $this->get_styles();
		usort($styles, array($this, 'sort_styles'));

		// Find current style
		$style = false;
		foreach ($styles as $row)
		{
			if ($row['style_id'] == $id)
			{
				$style = $row;
				break;
			}
		}

		if ($style === false)
		{
			trigger_error($this->user->lang['NO_MATCHING_STYLES_FOUND'] . adm_back_link($this->u_action), E_USER_WARNING);
		}

		// Read style configuration file
		$style_cfg = $this->read_style_cfg($style['style_path']);

		// Find all available parent styles
		$list = $this->find_possible_parents($styles, $id);

		// Add form key
		$form_key = 'acp_styles';
		add_form_key($form_key);

		// Change data
		if ($this->request->variable('update', false))
		{
			if (!check_form_key($form_key))
			{
				trigger_error($this->user->lang['FORM_INVALID'] . adm_back_link($this->u_action), E_USER_WARNING);
			}

			$update = array(
				'style_name'		=> trim($this->request->variable('style_name', $style['style_name'])),
				'style_parent_id'	=> $this->request->variable('style_parent', (int) $style['style_parent_id']),
				'style_active'		=> $this->request->variable('style_active', (int) $style['style_active']),
			);
			$update_action = $this->u_action . '&amp;action=details&amp;id=' . $id;

			// Check style name
			if ($update['style_name'] != $style['style_name'])
			{
				if (!strlen($update['style_name']))
				{
					trigger_error($this->user->lang['STYLE_ERR_STYLE_NAME'] . adm_back_link($update_action), E_USER_WARNING);
				}
				foreach ($styles as $row)
				{
					if ($row['style_name'] == $update['style_name'])
					{
						trigger_error($this->user->lang['STYLE_ERR_NAME_EXIST'] . adm_back_link($update_action), E_USER_WARNING);
					}
				}
			}
			else
			{
				unset($update['style_name']);
			}

			// Check parent style id
			if ($update['style_parent_id'] != $style['style_parent_id'])
			{
				if ($update['style_parent_id'] != 0)
				{
					$found = false;
					foreach ($list as $row)
					{
						if ($row['style_id'] == $update['style_parent_id'])
						{
							$found = true;
							$update['style_parent_tree'] = ($row['style_parent_tree'] != '' ? $row['style_parent_tree'] . '/' : '') . $row['style_path'];
							break;
						}
					}
					if (!$found)
					{
						trigger_error($this->user->lang['STYLE_ERR_INVALID_PARENT'] . adm_back_link($update_action), E_USER_WARNING);
					}
				}
				else
				{
					$update['style_parent_tree'] = '';
				}
			}
			else
			{
				unset($update['style_parent_id']);
			}

			// Check style_active
			if ($update['style_active'] != $style['style_active'])
			{
				if (!$update['style_active'] && $this->default_style == $style['style_id'])
				{
					trigger_error($this->user->lang['DEACTIVATE_DEFAULT'] . adm_back_link($update_action), E_USER_WARNING);
				}
			}
			else
			{
				unset($update['style_active']);
			}

			// Update data
			if (count($update))
			{
				$sql = 'UPDATE ' . STYLES_TABLE . '
					SET ' . $this->db->sql_build_array('UPDATE', $update) . "
					WHERE style_id = $id";
				$this->db->sql_query($sql);

				$style = array_merge($style, $update);

				if (isset($update['style_parent_id']))
				{
					// Update styles tree
					$styles = $this->get_styles();
					if ($this->update_styles_tree($styles, $style))
					{
						// Something was changed in styles tree, purge all cache
						$this->cache->purge();
					}
				}

				$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_STYLE_EDIT_DETAILS', false, array($style['style_name']));
			}

			// Update default style
			$default = $this->request->variable('style_default', 0);
			if ($default)
			{
				if (!$style['style_active'])
				{
					trigger_error($this->user->lang['STYLE_DEFAULT_CHANGE_INACTIVE'] . adm_back_link($update_action), E_USER_WARNING);
				}
				$this->config->set('default_style', $id);
				$this->cache->purge();
			}

			// Show styles list
			$this->frontend();
			return;
		}

		// Show page title
		$this->welcome_message('ACP_STYLES', null);

		// Show parent styles
		foreach ($list as $row)
		{
			$this->template->assign_block_vars('parent_styles', array(
				'STYLE_ID'		=> $row['style_id'],
				'STYLE_NAME'	=> htmlspecialchars($row['style_name']),
				'LEVEL'			=> $row['level'],
				'SPACER'		=> str_repeat('&nbsp; ', $row['level']),
				)
			);
		}

		// Show style details
		$this->template->assign_vars(array(
			'S_STYLE_DETAILS'	=> true,
			'STYLE_ID'			=> $style['style_id'],
			'STYLE_NAME'		=> htmlspecialchars($style['style_name']),
			'STYLE_PATH'		=> htmlspecialchars($style['style_path']),
			'STYLE_VERSION'		=> htmlspecialchars($style_cfg['style_version']),
			'STYLE_COPYRIGHT'	=> strip_tags($style['style_copyright']),
			'STYLE_PARENT'		=> $style['style_parent_id'],
			'S_STYLE_ACTIVE'	=> $style['style_active'],
			'S_STYLE_DEFAULT'	=> ($style['style_id'] == $this->default_style)
			)
		);
	}

	/**
	* List installed styles
	*/
	protected function show_installed()
	{
		// Get all installed styles
		$styles = $this->get_styles();

		if (!count($styles))
		{
			trigger_error($this->user->lang['NO_MATCHING_STYLES_FOUND'] . adm_back_link($this->u_action), E_USER_WARNING);
		}

		usort($styles, array($this, 'sort_styles'));

		// Get users
		$users = $this->get_users();

		// Add users counter to rows
		foreach ($styles as &$style)
		{
			$style['_users'] = isset($users[$style['style_id']]) ? $users[$style['style_id']] : 0;
		}

		// Set up styles list variables
		// Addons should increase this number and update template variable
		$this->styles_list_cols = 4;
		$this->template->assign_var('STYLES_LIST_COLS', $this->styles_list_cols);

		// Show styles list
		$this->show_styles_list($styles, 0, 0);

		// Show styles with invalid inherits_id
		foreach ($styles as $style)
		{
			if (empty($style['_shown']))
			{
				$style['_note'] = sprintf($this->user->lang['REQUIRES_STYLE'], htmlspecialchars($style['style_parent_tree']));
				$this->list_style($style, 0);
			}
		}

		// Add buttons
		$this->template->assign_block_vars('extra_actions', array(
				'ACTION_NAME'	=> 'activate',
				'L_ACTION'		=> $this->user->lang['STYLE_ACTIVATE'],
			)
		);

		$this->template->assign_block_vars('extra_actions', array(
				'ACTION_NAME'	=> 'deactivate',
				'L_ACTION'		=> $this->user->lang['STYLE_DEACTIVATE'],
			)
		);

		if (isset($this->style_counters) && $this->style_counters['total'] > 1)
		{
			$this->template->assign_block_vars('extra_actions', array(
					'ACTION_NAME'	=> 'uninstall',
					'L_ACTION'		=> $this->user->lang['STYLE_UNINSTALL'],
				)
			);
		}
	}

	/**
	* Show list of styles that can be installed
	*/
	protected function show_available()
	{
		// Get list of styles
		$styles = $this->find_available(true);

		// Show styles
		if (empty($styles))
		{
			trigger_error($this->user->lang['NO_UNINSTALLED_STYLE'] . adm_back_link($this->u_base_action), E_USER_NOTICE);
		}

		usort($styles, array($this, 'sort_styles'));

		$this->styles_list_cols = 3;
		$this->template->assign_vars(array(
			'STYLES_LIST_COLS'	=> $this->styles_list_cols,
			'STYLES_LIST_HIDE_COUNT'	=> true
			)
		);

		// Show styles
		foreach ($styles as &$style)
		{
			// Check if style has a parent style in styles list
			$has_parent = false;
			if ($style['_inherit_name'] != '')
			{
				foreach ($styles as $parent_style)
				{
					if ($parent_style['style_name'] == $style['_inherit_name'] && empty($parent_style['_shown']))
					{
						// Show parent style first
						$has_parent = true;
					}
				}
			}
			if (!$has_parent)
			{
				$this->list_style($style, 0);
				$this->show_available_child_styles($styles, $style['style_name'], 1);
			}
		}

		// Show styles that do not have parent style in styles list
		foreach ($styles as $style)
		{
			if (empty($style['_shown']))
			{
				$this->list_style($style, 0);
			}
		}

		// Add button
		if (isset($this->style_counters) && $this->style_counters['caninstall'] > 0)
		{
			$this->template->assign_block_vars('extra_actions', array(
					'ACTION_NAME'	=> 'install',
					'L_ACTION'		=> $this->user->lang['INSTALL_STYLES'],
				)
			);
		}
	}

	/**
	* Find styles available for installation
	*
	* @param bool $all if true, function will return all installable styles. if false, function will return only styles that can be installed
	* @return array List of styles
	*/
	protected function find_available($all)
	{
		// Get list of installed styles
		$installed = $this->get_styles();

		$installed_dirs = array();
		$installed_names = array();
		foreach ($installed as $style)
		{
			$installed_dirs[] = $style['style_path'];
			$installed_names[$style['style_name']] = array(
				'path'		=> $style['style_path'],
				'id'		=> $style['style_id'],
				'parent'	=> $style['style_parent_id'],
				'tree'		=> (strlen($style['style_parent_tree']) ? $style['style_parent_tree'] . '/' : '') . $style['style_path'],
			);
		}

		// Get list of directories
		$dirs = $this->find_style_dirs();

		// Find styles that can be installed
		$styles = array();
		foreach ($dirs as $dir)
		{
			if (in_array($dir, $installed_dirs))
			{
				// Style is already installed
				continue;
			}
			$cfg = $this->read_style_cfg($dir);
			if ($cfg === false)
			{
				// Invalid style.cfg
				continue;
			}

			// Style should be available for installation
			$parent = $cfg['parent'];
			$style = array(
				'style_id'			=> 0,
				'style_name'		=> $cfg['name'],
				'style_copyright'	=> $cfg['copyright'],
				'style_active'		=> 0,
				'style_path'		=> $dir,
				'bbcode_bitfield'	=> $cfg['template_bitfield'],
				'style_parent_id'	=> 0,
				'style_parent_tree'	=> '',
				// Extra values for styles list
				// All extra variable start with _ so they won't be confused with data that can be added to styles table
				'_inherit_name'			=> $parent,
				'_available'			=> true,
				'_note'					=> '',
			);

			// Check style inheritance
			if ($parent != '')
			{
				if (isset($installed_names[$parent]))
				{
					// Parent style is installed
					$row = $installed_names[$parent];
					$style['style_parent_id'] = $row['id'];
					$style['style_parent_tree'] = $row['tree'];
				}
				else
				{
					// Parent style is not installed yet
					$style['_available'] = false;
					$style['_note'] = sprintf($this->user->lang['REQUIRES_STYLE'], htmlspecialchars($parent));
				}
			}

			if ($all || $style['_available'])
			{
				$styles[] = $style;
			}
		}

		return $styles;
	}

	/**
	* Show styles list
	*
	* @param array $styles styles list
	* @param int $parent parent style id
	* @param int $level style inheritance level
	*/
	protected function show_styles_list(&$styles, $parent, $level)
	{
		foreach ($styles as &$style)
		{
			if (empty($style['_shown']) && $style['style_parent_id'] == $parent)
			{
				$this->list_style($style, $level);
				$this->show_styles_list($styles, $style['style_id'], $level + 1);
			}
		}
	}

	/**
	* Show available styles tree
	*
	* @param array $styles Styles list, passed as reference
	* @param string $name Name of parent style
	* @param int $level Styles tree level
	*/
	protected function show_available_child_styles(&$styles, $name, $level)
	{
		foreach ($styles as &$style)
		{
			if (empty($style['_shown']) && $style['_inherit_name'] == $name)
			{
				$this->list_style($style, $level);
				$this->show_available_child_styles($styles, $style['style_name'], $level + 1);
			}
		}
	}

	/**
	* Update styles tree
	*
	* @param array $styles Styles list, passed as reference
	* @param array|false $style Current style, false if root
	* @return bool True if something was updated, false if not
	*/
	protected function update_styles_tree(&$styles, $style = false)
	{
		$parent_id = ($style === false) ? 0 : $style['style_id'];
		$parent_tree = ($style === false) ? '' : ($style['style_parent_tree'] == '' ? '' : $style['style_parent_tree']) . $style['style_path'];
		$update = false;
		$updated = false;
		foreach ($styles as &$row)
		{
			if ($row['style_parent_id'] == $parent_id)
			{
				if ($row['style_parent_tree'] != $parent_tree)
				{
					$row['style_parent_tree'] = $parent_tree;
					$update = true;
				}
				$updated |= $this->update_styles_tree($styles, $row);
			}
		}
		if ($update)
		{
			$sql = 'UPDATE ' . STYLES_TABLE . "
				SET style_parent_tree = '" . $this->db->sql_escape($parent_tree) . "'
				WHERE style_parent_id = {$parent_id}";
			$this->db->sql_query($sql);
			$updated = true;
		}
		return $updated;
	}

	/**
	* Find all possible parent styles for style
	*
	* @param array $styles list of styles
	* @param int $id id of style
	* @param int $parent current parent style id
	* @param int $level current tree level
	* @return array Style ids, names and levels
	*/
	protected function find_possible_parents($styles, $id = -1, $parent = 0, $level = 0)
	{
		$results = array();
		foreach ($styles as $style)
		{
			if ($style['style_id'] != $id && $style['style_parent_id'] == $parent)
			{
				$results[] = array(
					'style_id'		=> $style['style_id'],
					'style_name'	=> $style['style_name'],
					'style_path'	=> $style['style_path'],
					'style_parent_id'	=> $style['style_parent_id'],
					'style_parent_tree'	=> $style['style_parent_tree'],
					'level'			=> $level
				);
				$results = array_merge($results, $this->find_possible_parents($styles, $id, $style['style_id'], $level + 1));
			}
		}
		return $results;
	}

	/**
	* Show item in styles list
	*
	* @param array $style style row
	* @param int $level style inheritance level
	*/
	protected function list_style(&$style, $level)
	{
		// Mark row as shown
		if (!empty($style['_shown']))
		{
			return;
		}

		$style['_shown'] = true;

		// Generate template variables
		$actions = array();
		$row = array(
			// Style data
			'STYLE_ID'		=> $style['style_id'],
			'STYLE_NAME'	=> htmlspecialchars($style['style_name']),
			'STYLE_PHPBB_VERSION'	=> $this->read_style_cfg($style['style_path'])['phpbb_version'],
			'STYLE_PATH'	=> htmlspecialchars($style['style_path']),
			'STYLE_COPYRIGHT'	=> strip_tags($style['style_copyright']),
			'STYLE_ACTIVE'	=> $style['style_active'],

			// Additional data
			'DEFAULT'		=> ($style['style_id'] && $style['style_id'] == $this->default_style),
			'USERS'			=> (isset($style['_users'])) ? $style['_users'] : '',
			'LEVEL'			=> $level,
			'PADDING'		=> (4 + 16 * $level),
			'SHOW_COPYRIGHT'	=> ($style['style_id']) ? false : true,
			'STYLE_PATH_FULL'	=> htmlspecialchars($this->styles_path_absolute . '/' . $style['style_path']) . '/',

			// Comment to show below style
			'COMMENT'		=> (isset($style['_note'])) ? $style['_note'] : '',

			// The following variables should be used by hooks to add custom HTML code
			'EXTRA'			=> '',
			'EXTRA_OPTIONS'	=> ''
		);

		// Status specific data
		if ($style['style_id'])
		{
			// Style is installed

			// Details
			$actions[] = array(
				'U_ACTION'	=> $this->u_action . '&amp;action=details&amp;id=' . $style['style_id'],
				'L_ACTION'	=> $this->user->lang['DETAILS']
			);

			// Activate/Deactive
			$action_name = ($style['style_active'] ? 'de' : '') . 'activate';

			$actions[] = array(
				'U_ACTION'	=> $this->u_action . '&amp;action=' . $action_name . '&amp;hash=' . generate_link_hash($action_name) . '&amp;id=' . $style['style_id'],
				'L_ACTION'	=> $this->user->lang['STYLE_' . ($style['style_active'] ? 'DE' : '') . 'ACTIVATE']
			);

/*			// Export
			$actions[] = array(
				'U_ACTION'	=> $this->u_action . '&amp;action=export&amp;hash=' . generate_link_hash('export') . '&amp;id=' . $style['style_id'],
				'L_ACTION'	=> $this->user->lang['EXPORT']
			); */

			// Uninstall
			$actions[] = array(
				'U_ACTION'	=> $this->u_action . '&amp;action=uninstall&amp;hash=' . generate_link_hash('uninstall') . '&amp;id=' . $style['style_id'],
				'L_ACTION'	=> $this->user->lang['STYLE_UNINSTALL']
			);

			// Preview
			$actions[] = array(
				'U_ACTION'	=> append_sid($this->phpbb_root_path . 'index.' . $this->php_ext, 'style=' . $style['style_id']),
				'L_ACTION'	=> $this->user->lang['PREVIEW']
			);
		}
		else
		{
			// Style is not installed
			if (empty($style['_available']))
			{
				$actions[] = array(
					'HTML'		=> $this->user->lang['CANNOT_BE_INSTALLED']
				);
			}
			else
			{
				$actions[] = array(
					'U_ACTION'	=> $this->u_action . '&amp;action=install&amp;hash=' . generate_link_hash('install') . '&amp;dir=' . urlencode($style['style_path']),
					'L_ACTION'	=> $this->user->lang['INSTALL_STYLE']
				);
			}
		}

		// todo: add hook

		// Assign template variables
		$this->template->assign_block_vars('styles_list', $row);
		foreach ($actions as $action)
		{
			$this->template->assign_block_vars('styles_list.actions', $action);
		}

		// Increase counters
		$counter = ($style['style_id']) ? ($style['style_active'] ? 'active' : 'inactive') : (empty($style['_available']) ? 'cannotinstall' : 'caninstall');
		if (!isset($this->style_counters))
		{
			$this->style_counters = array(
				'total'		=> 0,
				'active'	=> 0,
				'inactive'	=> 0,
				'caninstall'	=> 0,
				'cannotinstall'	=> 0
				);
		}
		$this->style_counters[$counter]++;
		$this->style_counters['total']++;
	}

	/**
	* Show welcome message
	*
	* @param string $title main title
	* @param string $description page description
	*/
	protected function welcome_message($title, $description)
	{
		$this->template->assign_vars(array(
			'L_TITLE'	=> $this->user->lang[$title],
			'L_EXPLAIN'	=> (isset($this->user->lang[$description])) ? $this->user->lang[$description] : ''
			)
		);
	}

	/**
	* Find all directories that have styles
	*
	* @return array Directory names
	*/
	protected function find_style_dirs()
	{
		$styles = array();

		$dp = @opendir($this->styles_path);
		if ($dp)
		{
			while (($file = readdir($dp)) !== false)
			{
				$dir = $this->styles_path . $file;
				if ($file[0] == '.' || !is_dir($dir))
				{
					continue;
				}

				if (file_exists("{$dir}/style.cfg"))
				{
					$styles[] = $file;
				}
			}
			closedir($dp);
		}

		return $styles;
	}

	/**
	* Sort styles
	*/
	public function sort_styles($style1, $style2)
	{
		if ($style1['style_active'] != $style2['style_active'])
		{
			return ($style1['style_active']) ? -1 : 1;
		}
		if (isset($style1['_available']) && $style1['_available'] != $style2['_available'])
		{
			return ($style1['_available']) ? -1 : 1;
		}
		return strcasecmp(isset($style1['style_name']) ? $style1['style_name'] : $style1['name'], isset($style2['style_name']) ? $style2['style_name'] : $style2['name']);
	}

	/**
	* Read style configuration file
	*
	* @param string $dir style directory
	* @return array|bool Style data, false on error
	*/
	protected function read_style_cfg($dir)
	{
		static $required = array('name', 'phpbb_version', 'copyright');
		$cfg = parse_cfg_file($this->styles_path . $dir . '/style.cfg');

		// Check if it is a valid file
		foreach ($required as $key)
		{
			if (!isset($cfg[$key]))
			{
				return false;
			}
		}

		// Check data
		if (!isset($cfg['parent']) || !is_string($cfg['parent']) || $cfg['parent'] == $cfg['name'])
		{
			$cfg['parent'] = '';
		}
		if (!isset($cfg['template_bitfield']))
		{
			$cfg['template_bitfield'] = $this->default_bitfield();
		}

		return $cfg;
	}

	/**
	* Install style
	*
	* @param array $style style data
	* @return int Style id
	*/
	protected function install_style($style)
	{
		global $user, $phpbb_log;

		// Generate row
		$sql_ary = array();
		foreach ($style as $key => $value)
		{
			if ($key != 'style_id' && substr($key, 0, 1) != '_')
			{
				$sql_ary[$key] = $value;
			}
		}

		// Add to database
		$this->db->sql_transaction('begin');

		$sql = 'INSERT INTO ' . STYLES_TABLE . '
			' . $this->db->sql_build_array('INSERT', $sql_ary);
		$this->db->sql_query($sql);

		$id = $this->db->sql_nextid();

		$this->db->sql_transaction('commit');

		$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_STYLE_ADD', false, array($sql_ary['style_name']));

		return $id;
	}

	/**
	* Lists all styles
	*
	* @return array Rows with styles data
	*/
	protected function get_styles()
	{
		$sql = 'SELECT *
			FROM ' . STYLES_TABLE;
		$result = $this->db->sql_query($sql);

		$rows = $this->db->sql_fetchrowset($result);
		$this->db->sql_freeresult($result);

		return $rows;
	}

	/**
	* Count users for each style
	*
	* @return array Styles in following format: [style_id] = number of users
	*/
	protected function get_users()
	{
		$sql = 'SELECT user_style, COUNT(user_style) AS style_count
			FROM ' . USERS_TABLE . '
			GROUP BY user_style';
		$result = $this->db->sql_query($sql);

		$style_count = array();
		while ($row = $this->db->sql_fetchrow($result))
		{
			$style_count[$row['user_style']] = $row['style_count'];
		}
		$this->db->sql_freeresult($result);

		return $style_count;
	}

	/**
	* Uninstall style
	*
	* @param array $style Style data
	* @return bool|string True on success, error message on error
	*/
	protected function uninstall_style($style)
	{
		$id = $style['style_id'];
		$path = $style['style_path'];

		// Check if style has child styles
		$sql = 'SELECT style_id
			FROM ' . STYLES_TABLE . '
			WHERE style_parent_id = ' . (int) $id . " OR style_parent_tree = '" . $this->db->sql_escape($path) . "'";
		$result = $this->db->sql_query($sql);

		$conflict = $this->db->sql_fetchrow($result);
		$this->db->sql_freeresult($result);

		if ($conflict !== false)
		{
			return sprintf($this->user->lang['STYLE_UNINSTALL_DEPENDENT'], $style['style_name']);
		}

		// Change default style for users
		$sql = 'UPDATE ' . USERS_TABLE . '
			SET user_style = ' . (int) $this->default_style . '
			WHERE user_style = ' . $id;
		$this->db->sql_query($sql);

		// Uninstall style
		$sql = 'DELETE FROM ' . STYLES_TABLE . '
			WHERE style_id = ' . $id;
		$this->db->sql_query($sql);
		return true;
	}

	/**
	* Delete all files in style directory
	*
	* @param string $path Style directory
	* @param string $dir Directory to remove inside style's directory
	* @return bool True on success, false on error
	*/
	protected function delete_style_files($path, $dir = '')
	{
		$dirname = $this->styles_path . $path . $dir;
		$result = true;

		$dp = @opendir($dirname);

		if ($dp)
		{
			while (($file = readdir($dp)) !== false)
			{
				if ($file == '.' || $file == '..')
				{
					continue;
				}
				$filename = $dirname . '/' . $file;
				if (is_dir($filename))
				{
					if (!$this->delete_style_files($path, $dir . '/' . $file))
					{
						$result = false;
					}
				}
				else
				{
					if (!@unlink($filename))
					{
						$result = false;
					}
				}
			}
			closedir($dp);
		}
		if (!@rmdir($dirname))
		{
			return false;
		}

		return $result;
	}

	/**
	* Get list of items from posted data
	*
	* @param string $name Variable name
	* @param string|int $default Default value for array
	* @param bool $error If true, error will be triggered if list is empty
	* @return array Items
	*/
	protected function request_vars($name, $default, $error = false)
	{
		$item = $this->request->variable($name, $default);
		$items = $this->request->variable($name . 's', array($default));

		if (count($items) == 1 && $items[0] == $default)
		{
			$items = array();
		}

		if ($item != $default && !count($items))
		{
			$items[] = $item;
		}

		if ($error && !count($items))
		{
			trigger_error($this->user->lang['NO_MATCHING_STYLES_FOUND'] . adm_back_link($this->u_action), E_USER_WARNING);
		}

		return $items;
	}

	/**
	* Generates default bitfield
	*
	* This bitfield decides which bbcodes are defined in a template.
	*
	* @return string Bitfield
	*/
	protected function default_bitfield()
	{
		static $value;
		if (isset($value))
		{
			return $value;
		}

		// Hardcoded template bitfield to add for new templates
		$default_bitfield = '1111111111111';

		$bitfield = new bitfield();
		for ($i = 0; $i < strlen($default_bitfield); $i++)
		{
			if ($default_bitfield[$i] == '1')
			{
				$bitfield->set($i);
			}
		}

		return $bitfield->get_base64();
	}

}
