From aa961991fdafc8a0371d22c7e42f2424ee1b8898 Mon Sep 17 00:00:00 2001
From: Oliver Schramm <oliver.schramm97@gmail.com>
Date: Fri, 17 Nov 2017 01:29:38 +0100
Subject: [PATCH] [ticket/15339] Allow a module to have multiple parents

Also restore old behaviour from Olympus regarding re-sorting modules

PHPBB3-15339
---
 .../migration/data/v30x/release_3_0_6_rc1.php |   3 +
 .../migration/data/v30x/release_3_0_8_rc1.php |   1 +
 phpBB/phpbb/db/migration/tool/module.php      | 259 +++++++++---------
 3 files changed, 138 insertions(+), 125 deletions(-)

diff --git a/phpBB/phpbb/db/migration/data/v30x/release_3_0_6_rc1.php b/phpBB/phpbb/db/migration/data/v30x/release_3_0_6_rc1.php
index 08b8979e00..40bb58c10d 100644
--- a/phpBB/phpbb/db/migration/data/v30x/release_3_0_6_rc1.php
+++ b/phpBB/phpbb/db/migration/data/v30x/release_3_0_6_rc1.php
@@ -156,6 +156,7 @@ class release_3_0_6_rc1 extends \phpbb\db\migration\migration
 					'module_langname'	=> 'ACP_FEED_SETTINGS',
 					'module_mode'		=> 'feed',
 					'module_auth'		=> 'acl_a_board',
+					'after'				=> array('signature', 'ACP_SIGNATURE_SETTINGS'),
 				),
 			)),
 			array('module.add', array(
@@ -167,6 +168,7 @@ class release_3_0_6_rc1 extends \phpbb\db\migration\migration
 					'module_mode'		=> 'warnings',
 					'module_auth'		=> 'acl_a_user',
 					'module_display'	=> false,
+					'after'				=> array('feedback', 'ACP_USER_FEEDBACK'),
 				),
 			)),
 			array('module.add', array(
@@ -187,6 +189,7 @@ class release_3_0_6_rc1 extends \phpbb\db\migration\migration
 					'module_langname'	=> 'ACP_FORUM_PERMISSIONS_COPY',
 					'module_mode'		=> 'setting_forum_copy',
 					'module_auth'		=> 'acl_a_fauth && acl_a_authusers && acl_a_authgroups && acl_a_mauth',
+					'after'				=> array('setting_forum_local', 'ACP_FORUM_PERMISSIONS'),
 				),
 			)),
 			array('module.add', array(
diff --git a/phpBB/phpbb/db/migration/data/v30x/release_3_0_8_rc1.php b/phpBB/phpbb/db/migration/data/v30x/release_3_0_8_rc1.php
index 0190eeb1af..c018adab46 100644
--- a/phpBB/phpbb/db/migration/data/v30x/release_3_0_8_rc1.php
+++ b/phpBB/phpbb/db/migration/data/v30x/release_3_0_8_rc1.php
@@ -39,6 +39,7 @@ class release_3_0_8_rc1 extends \phpbb\db\migration\migration
 					'module_langname'	=> 'ACP_POST_SETTINGS',
 					'module_mode'		=> 'post',
 					'module_auth'		=> 'acl_a_board',
+					'after'				=> array('message', 'ACP_MESSAGE_SETTINGS'),
 				),
 			)),
 			array('config.add', array('load_unreads_search', 1)),
diff --git a/phpBB/phpbb/db/migration/tool/module.php b/phpBB/phpbb/db/migration/tool/module.php
index b47c426110..3b4728c9d7 100644
--- a/phpBB/phpbb/db/migration/tool/module.php
+++ b/phpBB/phpbb/db/migration/tool/module.php
@@ -84,9 +84,11 @@ class module implements \phpbb\db\migration\tool\tool_interface
 	*		Use false to ignore the parent check and check class wide.
 	* @param int|string $module The module_id|module_langname you would like to
 	* 		check for to see if it exists
-	* @return bool true/false if module exists
+	* @param bool $lazy Checks lazily if the module exists. Returns true if it exists in at
+	*       least one given parent.
+	* @return bool true if module exists in *all* given parents, false if not
 	*/
-	public function exists($class, $parent, $module)
+	public function exists($class, $parent, $module, $lazy = false)
 	{
 		// the main root directory should return true
 		if (!$module)
@@ -94,33 +96,44 @@ class module implements \phpbb\db\migration\tool\tool_interface
 			return true;
 		}
 
-		$parent_sql = '';
+		$parent_sqls = [];
 		if ($parent !== false)
 		{
-			$parent = $this->get_parent_module_id($parent, $module, false);
-			if ($parent === false)
+			$parents = $this->get_parent_module_id($parent, $module, false);
+			if ($parents === false)
 			{
 				return false;
 			}
 
-			$parent_sql = 'AND parent_id = ' . (int) $parent;
+			foreach ((array) $parents as $parent_id)
+			{
+				$parent_sqls[] = 'AND parent_id = ' . (int) $parent_id;
+			}
 		}
 
-		$sql = 'SELECT module_id
-			FROM ' . $this->modules_table . "
-			WHERE module_class = '" . $this->db->sql_escape($class) . "'
-				$parent_sql
-				AND " . ((is_numeric($module)) ? 'module_id = ' . (int) $module : "module_langname = '" . $this->db->sql_escape($module) . "'");
-		$result = $this->db->sql_query($sql);
-		$module_id = $this->db->sql_fetchfield('module_id');
-		$this->db->sql_freeresult($result);
-
-		if ($module_id)
+		foreach ($parent_sqls as $parent_sql)
 		{
-			return true;
+			$sql = 'SELECT module_id
+				FROM ' . $this->modules_table . "
+				WHERE module_class = '" . $this->db->sql_escape($class) . "'
+					$parent_sql
+					AND " . ((is_numeric($module)) ? 'module_id = ' . (int) $module : "module_langname = '" . $this->db->sql_escape($module) . "'");
+			$result = $this->db->sql_query($sql);
+			$module_id = $this->db->sql_fetchfield('module_id');
+			$this->db->sql_freeresult($result);
+
+			if (!$lazy && !$module_id)
+			{
+				return false;
+			}
+			else if ($lazy && $module_id)
+			{
+				return true;
+			}
 		}
 
-		return false;
+		// Returns true, if modules exist in all parents and false otherwise
+		return !$lazy;
 	}
 
 	/**
@@ -172,7 +185,7 @@ class module implements \phpbb\db\migration\tool\tool_interface
 			$data = array('module_langname' => $data);
 		}
 
-		$parent = $data['parent_id'] = $this->get_parent_module_id($parent, $data);
+		$parents = (array) $this->get_parent_module_id($parent, $data);
 
 		if (!isset($data['module_langname']))
 		{
@@ -195,95 +208,129 @@ class module implements \phpbb\db\migration\tool\tool_interface
 					);
 
 					// Run the "manual" way with the data we've collected.
-					$this->add($class, $parent, $new_module);
+					foreach ($parents as $parent)
+					{
+						$this->add($class, $parent, $new_module);
+					}
 				}
 			}
 
 			return;
 		}
 
-		// The "manual" way
-		if (!$this->exists($class, false, $parent))
+		foreach ($parents as $parent)
 		{
-			throw new \phpbb\db\migration\exception('MODULE_NOT_EXIST', $parent);
-		}
+			$data['parent_id'] = $parent;
 
-		if ($this->exists($class, $parent, $data['module_langname']))
-		{
-			throw new \phpbb\db\migration\exception('MODULE_EXISTS', $data['module_langname']);
-		}
-
-		$module_data = array(
-			'module_enabled'	=> (isset($data['module_enabled'])) ? $data['module_enabled'] : 1,
-			'module_display'	=> (isset($data['module_display'])) ? $data['module_display'] : 1,
-			'module_basename'	=> (isset($data['module_basename'])) ? $data['module_basename'] : '',
-			'module_class'		=> $class,
-			'parent_id'			=> (int) $parent,
-			'module_langname'	=> (isset($data['module_langname'])) ? $data['module_langname'] : '',
-			'module_mode'		=> (isset($data['module_mode'])) ? $data['module_mode'] : '',
-			'module_auth'		=> (isset($data['module_auth'])) ? $data['module_auth'] : '',
-		);
-
-		try
-		{
-			$this->module_manager->update_module_data($module_data);
-
-			// Success
-			$module_log_name = ((isset($this->user->lang[$data['module_langname']])) ? $this->user->lang[$data['module_langname']] : $data['module_langname']);
-			$phpbb_log->add('admin', (isset($user->data['user_id'])) ? $user->data['user_id'] : ANONYMOUS, $user->ip, 'LOG_MODULE_ADD', false, array($module_log_name));
-
-			// Move the module if requested above/below an existing one
-			if (isset($data['before']) && $data['before'])
+			// The "manual" way
+			if (!$this->exists($class, false, $parent))
 			{
-				$sql = 'SELECT left_id
+				throw new \phpbb\db\migration\exception('MODULE_NOT_EXIST', $parent);
+			}
+
+			if ($this->exists($class, $parent, $data['module_langname']))
+			{
+				throw new \phpbb\db\migration\exception('MODULE_EXISTS', $data['module_langname']);
+			}
+
+			$module_data = array(
+				'module_enabled'	=> (isset($data['module_enabled'])) ? $data['module_enabled'] : 1,
+				'module_display'	=> (isset($data['module_display'])) ? $data['module_display'] : 1,
+				'module_basename'	=> (isset($data['module_basename'])) ? $data['module_basename'] : '',
+				'module_class'		=> $class,
+				'parent_id'			=> (int) $parent,
+				'module_langname'	=> (isset($data['module_langname'])) ? $data['module_langname'] : '',
+				'module_mode'		=> (isset($data['module_mode'])) ? $data['module_mode'] : '',
+				'module_auth'		=> (isset($data['module_auth'])) ? $data['module_auth'] : '',
+			);
+
+			try
+			{
+				$this->module_manager->update_module_data($module_data);
+
+				// Success
+				$module_log_name = ((isset($this->user->lang[$data['module_langname']])) ? $this->user->lang[$data['module_langname']] : $data['module_langname']);
+				$phpbb_log->add('admin', (isset($user->data['user_id'])) ? $user->data['user_id'] : ANONYMOUS, $user->ip, 'LOG_MODULE_ADD', false, array($module_log_name));
+
+				// Move the module if requested above/below an existing one
+				if (isset($data['before']) && $data['before'])
+				{
+					$before_mode = $before_langname = '';
+					if (is_array($data['before']))
+					{
+						// Restore legacy-legacy behaviour from phpBB 3.0
+						list($before_mode, $before_langname) = $data['before'];
+					}
+					else
+					{
+						// Legacy behaviour from phpBB 3.1+
+						$before_langname = $data['before'];
+					}
+
+					$sql = 'SELECT left_id
 					FROM ' . $this->modules_table . "
 					WHERE module_class = '" . $this->db->sql_escape($class) . "'
 						AND parent_id = " . (int) $parent . "
-						AND module_langname = '" . $this->db->sql_escape($data['before']) . "'";
-				$this->db->sql_query($sql);
-				$to_left = (int) $this->db->sql_fetchfield('left_id');
+						AND module_langname = '" . $this->db->sql_escape($before_langname) . "'"
+						. (($before_mode) ? " AND module_mode = '" . $this->db->sql_escape($before_mode) . "'" : '');
+					$this->db->sql_query($sql);
+					$to_left = (int) $this->db->sql_fetchfield('left_id');
 
-				$sql = 'UPDATE ' . $this->modules_table . "
+					$sql = 'UPDATE ' . $this->modules_table . "
 					SET left_id = left_id + 2, right_id = right_id + 2
 					WHERE module_class = '" . $this->db->sql_escape($class) . "'
 						AND left_id >= $to_left
 						AND left_id < {$module_data['left_id']}";
-				$this->db->sql_query($sql);
+					$this->db->sql_query($sql);
 
-				$sql = 'UPDATE ' . $this->modules_table . "
+					$sql = 'UPDATE ' . $this->modules_table . "
 					SET left_id = $to_left, right_id = " . ($to_left + 1) . "
 					WHERE module_class = '" . $this->db->sql_escape($class) . "'
 						AND module_id = {$module_data['module_id']}";
-				$this->db->sql_query($sql);
-			}
-			else if (isset($data['after']) && $data['after'])
-			{
-				$sql = 'SELECT right_id
+					$this->db->sql_query($sql);
+				}
+				else if (isset($data['after']) && $data['after'])
+				{
+					$after_mode = $after_langname = '';
+					if (is_array($data['after']))
+					{
+						// Restore legacy-legacy behaviour from phpBB 3.0
+						list($after_mode, $after_langname) = $data['after'];
+					}
+					else
+					{
+						// Legacy behaviour from phpBB 3.1+
+						$after_langname = $data['after'];
+					}
+
+					$sql = 'SELECT right_id
 					FROM ' . $this->modules_table . "
 					WHERE module_class = '" . $this->db->sql_escape($class) . "'
 						AND parent_id = " . (int) $parent . "
-						AND module_langname = '" . $this->db->sql_escape($data['after']) . "'";
-				$this->db->sql_query($sql);
-				$to_right = (int) $this->db->sql_fetchfield('right_id');
+						AND module_langname = '" . $this->db->sql_escape($after_langname) . "'"
+						. (($after_mode) ? " AND module_mode = '" . $this->db->sql_escape($after_mode) . "'" : '');
+					$this->db->sql_query($sql);
+					$to_right = (int) $this->db->sql_fetchfield('right_id');
 
-				$sql = 'UPDATE ' . $this->modules_table . "
+					$sql = 'UPDATE ' . $this->modules_table . "
 					SET left_id = left_id + 2, right_id = right_id + 2
 					WHERE module_class = '" . $this->db->sql_escape($class) . "'
 						AND left_id >= $to_right
 						AND left_id < {$module_data['left_id']}";
-				$this->db->sql_query($sql);
+					$this->db->sql_query($sql);
 
-				$sql = 'UPDATE ' . $this->modules_table . '
+					$sql = 'UPDATE ' . $this->modules_table . '
 					SET left_id = ' . ($to_right + 1) . ', right_id = ' . ($to_right + 2) . "
 					WHERE module_class = '" . $this->db->sql_escape($class) . "'
 						AND module_id = {$module_data['module_id']}";
-				$this->db->sql_query($sql);
+					$this->db->sql_query($sql);
+				}
+			}
+			catch (module_exception $e)
+			{
+				// Error
+				throw new \phpbb\db\migration\exception('MODULE_ERROR', $e->getMessage());
 			}
-		}
-		catch (module_exception $e)
-		{
-			// Error
-			throw new \phpbb\db\migration\exception('MODULE_ERROR', $e->getMessage());
 		}
 
 		// Clear the Modules Cache
@@ -334,7 +381,7 @@ class module implements \phpbb\db\migration\tool\tool_interface
 		}
 		else
 		{
-			if (!$this->exists($class, $parent, $module))
+			if (!$this->exists($class, $parent, $module, true))
 			{
 				return;
 			}
@@ -342,8 +389,8 @@ class module implements \phpbb\db\migration\tool\tool_interface
 			$parent_sql = '';
 			if ($parent !== false)
 			{
-				$parent = $this->get_parent_module_id($parent, $module);
-				$parent_sql = 'AND parent_id = ' . (int) $parent;
+				$parents = (array)$this->get_parent_module_id($parent, $module);
+				$parent_sql = 'AND ' . $this->db->sql_in_set('parent_id', $parents);
 			}
 
 			$module_ids = array();
@@ -457,14 +504,11 @@ class module implements \phpbb\db\migration\tool\tool_interface
 	* @param string|int $parent_id The parent module_id|module_langname
 	* @param int|string|array $data The module_id, module_langname for existance checking or module data array for adding
 	* @param bool $throw_exception The flag indicating if exception should be thrown on error
-	* @return mixed The int parent module_id or false
+	* @return mixed The int parent module_id, an array of int parent module_id values or false
 	* @throws \phpbb\db\migration\exception
 	*/
 	public function get_parent_module_id($parent_id, $data = '', $throw_exception = true)
 	{
-		// Initialize exception object placeholder
-		$exception = false;
-
 		// Allow '' to be sent as 0
 		$parent_id = $parent_id ?: 0;
 
@@ -486,61 +530,26 @@ class module implements \phpbb\db\migration\tool\tool_interface
 			{
 				// No parent with the given module_langname exist
 				case 0:
-					$exception = new \phpbb\db\migration\exception('MODULE_NOT_EXIST', $parent_id);
+					if ($throw_exception)
+					{
+						throw new \phpbb\db\migration\exception('MODULE_NOT_EXIST', $parent_id);
+					}
+
+					return false;
 				break;
 
 				// Return the module id
 				case 1:
-					$parent_id = (int) $ids[0];
+					return (int) $ids[0];
 				break;
 
-				// Several modules with the given module_langname were found
-				// Try to determine the parent_id by the neighbour module parent
 				default:
-					if (is_array($data) && (isset($data['before']) || isset($data['after'])))
-					{
-						$neighbour_module_langname = isset($data['before']) ? $data['before'] : $data['after'];
-						$sql = 'SELECT parent_id
-							FROM ' . $this->modules_table . "
-							WHERE module_langname = '" . $this->db->sql_escape($neighbour_module_langname) . "'
-								AND " . $this->db->sql_in_set('parent_id', $ids);
-						$result = $this->db->sql_query($sql);
-						$parent_id = (int) $this->db->sql_fetchfield('parent_id');
-						if (!$parent_id)
-						{
-							$exception = new \phpbb\db\migration\exception('PARENT_MODULE_FIND_ERROR', $data['parent_id']);
-						}
-					}
-					else if (!empty($data) && !is_array($data))
-					{
-						// The module_langname is set, checking for the module existance
-						// As more than 1 parents were found already, there's no way for null parent_id here
-						$sql = 'SELECT m2.module_id as module_parent_id
-							FROM ' . $this->modules_table . ' m1, ' . $this->modules_table . " m2
-							WHERE " . ((is_numeric($data)) ? 'm1.module_id = ' . (int) $data : "m1.module_langname = '" . $this->db->sql_escape($data)) . "'
-								AND m2.module_id = m1.parent_id
-								AND " . $this->db->sql_in_set('m2.module_id', $ids);
-						$result = $this->db->sql_query($sql);
-						$parent_id = (int) $this->db->sql_fetchfield('module_parent_id');
-					}
-					else
-					{
-						//Unable to get the parent module id, throwing an exception
-						$exception = new \phpbb\db\migration\exception('MODULE_EXIST_MULTIPLE', $parent_id);
-					}
+					// This represents the old behaviour of phpBB 3.0
+					return $ids;
 				break;
 			}
 		}
 
-		if ($exception !== false)
-		{
-			if ($throw_exception)
-			{
-				throw $exception;
-			}
-			return false;
-		}
-
 		return $parent_id;
 	}
 }