Repository: miraheze/ManageWiki
Pull request #601: Improve ManageWiki requirements checks
Author: login: Universal-Omega
Created: 2025-04-27T17:31:41Z | Merged: 2025-04-27T21:26:21Z



Diff:
diff --git a/i18n/en.json b/i18n/en.json
index 1e19641ad..0e3bea8e5 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -69,13 +69,15 @@
 	"managewiki-desc": "Allows users to manage their wiki's configuration with ease through a web interface.",
 	"managewiki-disabled": "This \"$1\" module has not been enabled on this wiki and cannot be used.",
 	"managewiki-error-conflict": "The extension \"$1\" cannot be enabled because it conflicts with $2.",
+	"managewiki-error-dependency": "The extension \"$1\" cannot be disabled because it is a dependency of \"$2\".",
 	"managewiki-error-disallowednamespace": "The namespace \"$1\" is not allowed.",
 	"managewiki-error-emptygroup": "You cannot create a group without any permissions assigned.",
 	"managewiki-error-install": "The extension \"$1\" failed to be enabled. If this keeps occurring, please ",
 	"managewiki-error-missingpermission": "Cannot remove the 'managewiki-permissions' right from this group, as no other groups have the right. Removing it would prevent access to ManageWiki.",
 	"managewiki-error-nopermission": "You may view this wiki's configuration but you do not have permission to edit it.",
 	"managewiki-error-nopermission-remote": "You are remotely viewing a wiki's configuration. If you are a wiki operator, please navigate to your wiki to modify its configuration. You may not edit it from here.",
-	"managewiki-error-requirements": "The extension \"$1\" cannot be enabled because it fails to meet its requirements.",
+	"managewiki-error-requirements-disabled": "The extension \"$1\" cannot be disabled because you are missing the required permissions.",
+	"managewiki-error-requirements-enabled": "The extension \"$1\" cannot be enabled because it fails to meet its requirements.",
 	"managewiki-extension-name": "[$1 $2]",
 	"managewiki-extension-settings": "Manage available configuration options for this extension",
 	"managewiki-extensionname": "ManageWiki",
diff --git a/includes/Helpers/ManageWikiExtensions.php b/includes/Helpers/ManageWikiExtensions.php
index ea67b97f4..c0ff8ec64 100644
--- a/includes/Helpers/ManageWikiExtensions.php
+++ b/includes/Helpers/ManageWikiExtensions.php
@@ -164,6 +164,7 @@ public function getLogParams(): array {
 	public function commit(): void {
 		// We use this to check for conflicts only for
 		// extensions we are currently enabling.
+		// TODO: can we just use liveExtensions for this?
 		$enabling = array_keys(
 			array_filter(
 				$this->changes,
@@ -171,13 +172,13 @@ public function commit(): void {
 			)
 		);
 
-		foreach ( $this->liveExtensions as $name => $extensionsConfig ) {
+		foreach ( $this->liveExtensions as $name => $config ) {
 			// Check if we have a conflict first
-			if ( in_array( $extensionsConfig['conflicts'], $enabling, true ) ) {
+			if ( in_array( $config['conflicts'], $enabling, true ) ) {
 				$this->errors[] = [
 					'managewiki-error-conflict' => [
-						$this->getName( $extensionsConfig['conflicts'] ),
-						$extensionsConfig['name'],
+						$this->getName( $config['conflicts'] ),
+						$config['name'],
 					],
 				];
 
@@ -185,62 +186,120 @@ public function commit(): void {
 				continue;
 			}
 
-			if ( $this->getErrors() ) {
-				// If we have errors we don't want to save anything
-				continue;
+			$requirements = $config['requires'] ?? [];
+			// If we aren't making any changes to this extension,
+			// we don't need to check for permissions.
+			if ( !isset( $this->changes[$name] ) ) {
+				unset( $requirements['permissions'] );
 			}
 
-			// Now we need to check if we fulfil the requirements to enable this extension
-			$requirementsCheck = ManageWikiRequirements::process(
-				$extensionsConfig['requires'] ?? [], $this->list()
-			);
-
-			if ( $requirementsCheck ) {
-				$installResult = true;
-				// Define a 'current' extension as one with no changes entry
-				$enabledExt = !isset( $this->changes[$name] );
-				if ( isset( $extensionsConfig['install'] ) && !$enabledExt ) {
-					$installResult = ManageWikiInstaller::process(
-						$this->dbname,
-						$extensionsConfig['install'],
-						install: true
-					);
+			// Now we need to check if we fulfill the requirements to enable this extension.
+			$requirementsCheck = ManageWikiRequirements::process( $requirements, $this->list() );
+
+			if ( !$requirementsCheck ) {
+				if ( !isset( $this->changes[$name] ) ) {
+					// If we are not changing this extension but we are changing (i.e. disabling)
+					// an extension that this extension depends on.
+					foreach ( $requirements['extensions'] ?? [] as $exts ) {
+						foreach ( (array)$exts as $ext ) {
+							if ( isset( $this->changes[$ext] ) ) {
+								$this->errors[] = [
+									'managewiki-error-dependency' => [
+										$this->getName( $ext ),
+										$config['name'],
+									],
+								];
+								// Continue the parent loop
+								continue 3;
+							}
+						}
+					}
 				}
 
-				if ( !$installResult ) {
-					$this->errors[] = [
-						'managewiki-error-install' => [
-							$extensionsConfig['name'],
-						],
-					];
-				}
+				// If we are changing this extension or there is no extension requirements.
+				$this->errors[] = [
+					'managewiki-error-requirements-enabled' => [ $config['name'] ],
+				];
 
+				// Requirements failed, we have nothing else to do for this extension.
 				continue;
 			}
 
-			$this->errors[] = [
-				'managewiki-error-requirements' => [
-					$extensionsConfig['name'],
-				],
-			];
+			// If we aren't making any changes to this extension,
+			// we are done with it.
+			if ( !isset( $this->changes[$name] ) ) {
+				continue;
+			}
+
+			if ( $this->getErrors() ) {
+				// If we have errors we don't want to save anything
+				continue;
+			}
+
+			// Requirements passed, proceed to installer
+			$installResult = true;
+			if ( isset( $config['install'] ) ) {
+				$installResult = ManageWikiInstaller::process(
+					$this->dbname, $config['install'],
+					install: true
+				);
+			}
+
+			if ( !$installResult ) {
+				$this->errors[] = [
+					'managewiki-error-install' => [ $config['name'] ],
+				];
+			}
 		}
 
+		// Early exit if we already have errors
 		if ( $this->getErrors() ) {
 			// If we have errors we don't want to save anything
 			return;
 		}
 
-		foreach ( $this->removedExtensions as $name => $extensionsConfig ) {
-			// Unlike installing, we are not too fussed about whether this fails, let us just do it
-			if ( isset( $extensionsConfig['remove'] ) ) {
+		foreach ( $this->removedExtensions as $name => $config ) {
+			$requirementsCheck = true;
+			$permissionRequirements = $config['requires']['permissions'] ?? [];
+			if ( $permissionRequirements ) {
+				$requirementsCheck = ManageWikiRequirements::process(
+					// We only need to check for permissions when an
+					// extension is being disabled.
+					actions: [ 'permissions' => $permissionRequirements ],
+					// We don't need this since it's not used for permissions,
+					// which is the only thing we need to check here.
+					extList: []
+				);
+			}
+
+			if ( !$requirementsCheck ) {
+				$this->errors[] = [
+					'managewiki-error-requirements-disabled' => [ $config['name'] ],
+				];
+
+				// Requirements failed, we have nothing else to do for this extension.
+				continue;
+			}
+
+			if ( $this->getErrors() ) {
+				// If we have errors we don't want to save anything
+				continue;
+			}
+
+			// Unlike installing, we are not too fussed about whether this fails, let us just do it.
+			if ( isset( $config['remove'] ) ) {
 				ManageWikiInstaller::process(
-					$this->dbname,
-					$extensionsConfig['remove'],
+					$this->dbname, $config['remove'],
 					install: false
 				);
 			}
 		}
 
+		if ( $this->getErrors() ) {
+			// If we have errors we don't want to save anything
+			return;
+		}
+
 		$this->dbw->newInsertQueryBuilder()
 			->insertInto( 'mw_settings' )
 			->row( [


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>i18n/en.json (2)</summary><blockquote>

`72-72`: **Document the new message key in `qqq.json`.**

`managewiki-error-dependency` is newly introduced, but the corresponding documentation string is still missing from `i18n/qqq.json`.  Without the qqq entry, translators will not know the expected placeholder order (`$1` dependency, `$2` dependant).  
Please add a short, descriptive entry to `qqq.json` so that TranslateWiki contributors can localise the message correctly.

---

`79-80`: **Add `qqq.json` docs for the split requirement-errors as well.**

The newly-split keys `managewiki-error-requirements-disabled` and `managewiki-error-requirements-enabled` also need documentation lines in `qqq.json` (and in other language files, if they previously inherited the old unsplit key).  
This prevents orphaned or inconsistent translations across languages.

</blockquote></details>
<details>
<summary>includes/Helpers/ManageWikiExtensions.php (3)</summary><blockquote>

`167-174`: **Remove obsolete TODO & redundant list construction.**

The `TODO` comment suggests replacing `$enabling` with `$this->liveExtensions`, but after `add()/remove()` calls `liveExtensions` already contains the *post-change* state (i.e. everything that *will* be enabled).  
Maintaining the extra `$enabling` list adds cognitive load and can easily get out of sync.  Consider dropping it and computing conflicts directly from `array_keys( $this->changes, 1, true )` when needed, or just checking inside the loop:

```diff
-        $enabling = array_keys(
-            array_filter(
-                $this->changes,
-                static fn ( array $c ): bool => ( $c['new'] ?? 0 ) === 1
-            )
-        );
+        // No need for a separate $enabling array; we can derive it on the fly:
```

---

`189-195`: **Silently unsetting permission requirements may hide real problems.**

For unchanged extensions we `unset( $requirements['permissions'] )` to skip the check, but that means we never detect *global* permission regressions that make the extension invalid after unrelated changes.  
Instead of deleting the key entirely, consider passing a flag to `ManageWikiRequirements::process()` so it can *optionally* bypass the permission sub-check while still validating other requirement types.

---

`234-238`: **Repeated `$this->getErrors()` guards are redundant.**

Inside the primary loop we guard the installer call with `if ( $this->getErrors() ) { continue; }`, and then *again* call `getErrors()` immediately after the loop to early-return.  
The inner guard is unnecessary: once an error is recorded for *any* extension, the outer early-return will abort the commit before anything is written. Removing the per-iteration guard simplifies flow control and avoids accidentally skipping installation of later extensions after a transient warning.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 87d4ed07ca48e8798d47ba71017264ef88fa3168 and 64dc5ed5af7958dce0a7aff1266db8ebc37d823d.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `i18n/en.json` (1 hunks)
* `includes/Helpers/ManageWikiExtensions.php` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (1)</summary>

<details>
<summary>`**/*.php`: Follow MediaWiki PHP Coding Standards:

Indentation:
   - Use tabs for indentation, not spaces.

Spaces:
   - Use spaces on either side of binary operators.
     Exampl...</summary>

> `**/*.php`: Follow MediaWiki PHP Coding Standards:
> 
> Indentation:
>    - Use tabs for indentation, not spaces.
> 
> Spaces:
>    - Use spaces on either side of binary operators.
>      Example: $a = $b + $c; (Correct) $a=$b+$c; (Incorrect)
>    - Add spaces inside parentheses, except where they are empty.
>      Example: $a = getFoo( $b ); (Correct) $a = getFoo($b); (Incorrect)
>    - Do not add a space after a function name.
>      Example: $c = getBar(); (Correct) $c = getBar (); (Incorrect)
> 
> Function Return Type Hint:
>    - Add a space after the colon in the function return type hint.
>      Example: function square( int $x ): int { return $x * $x; }
> 
> Arrays:
>    - Use spaces in brackets when declaring an array, except where the array is empty.
>      Example: $a = [ 'foo', 'bar' ]; (Correct) $a = ['foo', 'bar']; (Incorrect)
>    - Do not put spaces in brackets when accessing array elements.
>      Example: $c = $a[0]; (Correct) $c = $a[ 0 ]; (Incorrect)
> 
> Control Structures:
>    - Add a space after control structure keywords.
>      Example: if ( isFoo() ) { ... } (Correct) if(isFoo()){ ... } (Incorrect)
> 
> Type Casting:
>    - Do not use a space within or after the cast operator.
>      Example: (int)$foo; (Correct) (int) $bar; (Incorrect)
> 
> Comments:
>    - In comments, there should be one space between the # or // and the comment text.
>      Example: // This is a comment. (Correct) //This is a comment. (Incorrect)
> 
> Ternary Operator:
>    - Keep ternary operator expressions short and obvious.
>    - For multi-line ternary expressions, place the '?' and ':' at the beginning of the new lines.
> 
> File Formatting:
>     - Avoid trailing whitespace on lines.

- `includes/Helpers/ManageWikiExtensions.php`

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: call-mediawiki-tests-workflow (master, 8.1, 81, false, false, true) / master | PHP 8.1
* GitHub Check: call-mediawiki-tests-workflow (REL1_43, 8.1, 81, true, true, false) / REL1_43 | PHP 8.1

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-27T21:02:59Z

Review comments:
diff_hunk: @@ -164,83 +164,142 @@ public function getLogParams(): array {
 	public function commit(): void {
 		// We use this to check for conflicts only for
 		// extensions we are currently enabling.
+		// TODO: can we just use liveExtensions for this?
 		$enabling = array_keys(
 			array_filter(
 				$this->changes,
 				static fn ( array $change ): bool => ( $change['new'] ?? 0 ) === 1
 			)
 		);
 
-		foreach ( $this->liveExtensions as $name => $extensionsConfig ) {
+		foreach ( $this->liveExtensions as $name => $config ) {
 			// Check if we have a conflict first
-			if ( in_array( $extensionsConfig['conflicts'], $enabling, true ) ) {
+			if ( in_array( $config['conflicts'], $enabling, true ) ) {
 				$this->errors[] = [
 					'managewiki-error-conflict' => [
-						$this->getName( $extensionsConfig['conflicts'] ),
-						$extensionsConfig['name'],
+						$this->getName( $config['conflicts'] ),
+						$config['name'],
 					],
 				];
 
 				// We have a conflict, we have nothing else to do for this extension.
 				continue;
 			}
 
-			if ( $this->getErrors() ) {
-				// If we have errors we don't want to save anything
-				continue;
+			$requirements = $config['requires'] ?? [];
+			// If we aren't making any changes to this extension,
+			// we don't need to check for permissions.
+			if ( !isset( $this->changes[$name] ) ) {
+				unset( $requirements['permissions'] );
 			}
 
-			// Now we need to check if we fulfil the requirements to enable this extension
-			$requirementsCheck = ManageWikiRequirements::process(
-				$extensionsConfig['requires'] ?? [], $this->list()
-			);
-
-			if ( $requirementsCheck ) {
-				$installResult = true;
-				// Define a 'current' extension as one with no changes entry
-				$enabledExt = !isset( $this->changes[$name] );
-				if ( isset( $extensionsConfig['install'] ) && !$enabledExt ) {
-					$installResult = ManageWikiInstaller::process(
-						$this->dbname,
-						$extensionsConfig['install'],
-						install: true
-					);
+			// Now we need to check if we fulfill the requirements to enable this extension.
+			$requirementsCheck = ManageWikiRequirements::process( $requirements, $this->list() );
+
+			if ( !$requirementsCheck ) {
+				if ( !isset( $this->changes[$name] ) ) {
+					// If we are not changing this extension but we are changing (i.e. disabling)
+					// an extension that this extension depends on.
+					foreach ( $requirements['extensions'] ?? [] as $exts ) {
+						foreach ( (array)$exts as $ext ) {
+							if ( isset( $this->changes[$ext] ) ) {
+								$this->errors[] = [
+									'managewiki-error-dependency' => [
+										$this->getName( $ext ),
+										$config['name'],
+									],
+								];
+								// Continue the parent loop
+								continue 3;
+							}
path: includes/Helpers/ManageWikiExtensions.php
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Complex triple-nested `continue 3` hurts readability.**

The dependency error branch uses `continue 3;` to jump out of three nested loops. This is hard to follow and fragile when code is refactored.  
Extract the dependency-check into a small helper method that returns a boolean, e.g. `hasDependencyBeingDisabled( $requirements )`, and replace the triple continue with an early `if ( … ) { continue; }`.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-27T21:02:58Z
author_association: NONE
diff_hunk: @@ -164,83 +164,142 @@
 	public function commit(): void {
 		// We use this to check for conflicts only for
 		// extensions we are currently enabling.
+		// TODO: can we just use liveExtensions for this?
 		$enabling = array_keys(
 			array_filter(
 				$this->changes,
 				static fn ( array $change ): bool => ( $change['new'] ?? 0 ) === 1
 			)
 		);
 
-		foreach ( $this->liveExtensions as $name => $extensionsConfig ) {
+		foreach ( $this->liveExtensions as $name => $config ) {
 			// Check if we have a conflict first
-			if ( in_array( $extensionsConfig['conflicts'], $enabling, true ) ) {
+			if ( in_array( $config['conflicts'], $enabling, true ) ) {
 				$this->errors[] = [
 					'managewiki-error-conflict' => [
-						$this->getName( $extensionsConfig['conflicts'] ),
-						$extensionsConfig['name'],
+						$this->getName( $config['conflicts'] ),
+						$config['name'],
 					],
 				];
 
 				// We have a conflict, we have nothing else to do for this extension.
 				continue;
 			}
 
-			if ( $this->getErrors() ) {
-				// If we have errors we don't want to save anything
-				continue;
+			$requirements = $config['requires'] ?? [];
+			// If we aren't making any changes to this extension,
+			// we don't need to check for permissions.
+			if ( !isset( $this->changes[$name] ) ) {
+				unset( $requirements['permissions'] );
 			}
 
-			// Now we need to check if we fulfil the requirements to enable this extension
-			$requirementsCheck = ManageWikiRequirements::process(
-				$extensionsConfig['requires'] ?? [], $this->list()
-			);
-
-			if ( $requirementsCheck ) {
-				$installResult = true;
-				// Define a 'current' extension as one with no changes entry
-				$enabledExt = !isset( $this->changes[$name] );
-				if ( isset( $extensionsConfig['install'] ) && !$enabledExt ) {
-					$installResult = ManageWikiInstaller::process(
-						$this->dbname,
-						$extensionsConfig['install'],
-						install: true
-					);
+			// Now we need to check if we fulfill the requirements to enable this extension.
+			$requirementsCheck = ManageWikiRequirements::process( $requirements, $this->list() );
+
+			if ( !$requirementsCheck ) {
+				if ( !isset( $this->changes[$name] ) ) {
+					// If we are not changing this extension but we are changing (i.e. disabling)
+					// an extension that this extension depends on.
+					foreach ( $requirements['extensions'] ?? [] as $exts ) {
+						foreach ( (array)$exts as $ext ) {
+							if ( isset( $this->changes[$ext] ) ) {
+								$this->errors[] = [
+									'managewiki-error-dependency' => [
+										$this->getName( $ext ),
+										$config['name'],
+									],
+								];
+								// Continue the parent loop
+								continue 3;
+							}
+						}
+					}
 				}
 
-				if ( !$installResult ) {
-					$this->errors[] = [
-						'managewiki-error-install' => [
-							$extensionsConfig['name'],
-						],
-					];
-				}
+				// If we are changing this extension or there is no extension requirements.
+				$this->errors[] = [
+					'managewiki-error-requirements-enabled' => [ $config['name'] ],
+				];
 
+				// Requirements failed, we have nothing else to do for this extension.
 				continue;
 			}
 
-			$this->errors[] = [
-				'managewiki-error-requirements' => [
-					$extensionsConfig['name'],
-				],
-			];
+			// If we aren't making any changes to this extension,
+			// we are done with it.
+			if ( !isset( $this->changes[$name] ) ) {
+				continue;
+			}
+
+			if ( $this->getErrors() ) {
+				// If we have errors we don't want to save anything
+				continue;
+			}
+
+			// Requirements passed, proceed to installer
+			$installResult = true;
+			if ( isset( $config['install'] ) ) {
+				$installResult = ManageWikiInstaller::process(
+					$this->dbname, $config['install'],
+					install: true
+				);
+			}
+
+			if ( !$installResult ) {
+				$this->errors[] = [
+					'managewiki-error-install' => [ $config['name'] ],
+				];
+			}
 		}
 
+		// Early exit if we already have errors
 		if ( $this->getErrors() ) {
 			// If we have errors we don't want to save anything
 			return;
 		}
 
-		foreach ( $this->removedExtensions as $name => $extensionsConfig ) {
-			// Unlike installing, we are not too fussed about whether this fails, let us just do it
-			if ( isset( $extensionsConfig['remove'] ) ) {
+		foreach ( $this->removedExtensions as $name => $config ) {
+			$requirementsCheck = true;
+			$permissionRequirements = $config['requires']['permissions'] ?? [];
+			if ( $permissionRequirements ) {
+				$requirementsCheck = ManageWikiRequirements::process(
+					// We only need to check for permissions when an
+					// extension is being disabled.
+					actions: [ 'permissions' => $permissionRequirements ],
+					// We don't need this since it's not used for permissions,
+					// which is the only thing we need to check here.
+					extList: []
+				);
+			}
+
+			if ( !$requirementsCheck ) {
+				$this->errors[] = [
+					'managewiki-error-requirements-disabled' => [ $config['name'] ],
+				];
+
+				// Requirements failed, we have nothing else to do for this extension.
+				continue;
+			}
+
+			if ( $this->getErrors() ) {
+				// If we have errors we don't want to save anything
+				continue;
+			}
+
+			// Unlike installing, we are not too fussed about whether this fails, let us just do it.
+			if ( isset( $config['remove'] ) ) {
 				ManageWikiInstaller::process(
-					$this->dbname,
-					$extensionsConfig['remove'],
+					$this->dbname, $config['remove'],
 					install: false
 				);
path: includes/Helpers/ManageWikiExtensions.php
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Removal installer result is ignored – may record false success.**

When disabling an extension we call `ManageWikiInstaller::process( …, install: false )` but we neither:

1. Check its return value, nor  
2. Surface any error to the user.

This can write the new state to `mw_settings` even though the uninstall routine failed, leading to a wiki thinking the extension is gone while its database tables or hooks remain.  

Treat removal failures symmetrically with installs:

```diff
-            if ( isset( $config['remove'] ) ) {
-                ManageWikiInstaller::process(
-                    $this->dbname, $config['remove'],
-                    install: false
-                );
-            }
+            if ( isset( $config['remove'] ) ) {
+                $ok = ManageWikiInstaller::process(
+                    $this->dbname,
+                    $config['remove'],
+                    install: false
+                );
+                if ( !$ok ) {
+                    $this->errors[] = [
+                        'managewiki-error-uninstall' => [ $config['name'] ],
+                    ];
+                }
+            }
```

Add a matching i18n message key (`managewiki-error-uninstall`) for parity.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-27T21:02:58Z
author_association: NONE
diff_hunk: @@ -164,83 +164,142 @@
 	public function commit(): void {
 		// We use this to check for conflicts only for
 		// extensions we are currently enabling.
+		// TODO: can we just use liveExtensions for this?
 		$enabling = array_keys(
 			array_filter(
 				$this->changes,
 				static fn ( array $change ): bool => ( $change['new'] ?? 0 ) === 1
 			)
 		);
 
-		foreach ( $this->liveExtensions as $name => $extensionsConfig ) {
+		foreach ( $this->liveExtensions as $name => $config ) {
 			// Check if we have a conflict first
-			if ( in_array( $extensionsConfig['conflicts'], $enabling, true ) ) {
+			if ( in_array( $config['conflicts'], $enabling, true ) ) {
 				$this->errors[] = [
 					'managewiki-error-conflict' => [
-						$this->getName( $extensionsConfig['conflicts'] ),
-						$extensionsConfig['name'],
+						$this->getName( $config['conflicts'] ),
+						$config['name'],
 					],
 				];
 
path: includes/Helpers/ManageWikiExtensions.php
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`conflicts` handling fails when multiple conflicts are defined.**

`in_array( $config['conflicts'], $enabling, true )` assumes `conflicts` is a *scalar*.  
If `$config['conflicts']` can be an array (quite common for extensions that conflict with several others), the check produces false negatives.  Prefer normalising to an array and using `array_intersect`:

```diff
-            if ( in_array( $config['conflicts'], $enabling, true ) ) {
+            $conflicts = (array)( $config['conflicts'] ?? [] );
+            if ( array_intersect( $conflicts, $enabling ) ) {
```

This also avoids the PHP warning you get when `in_array()` receives an array as its needle.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
		foreach ( $this->liveExtensions as $name => $config ) {
			// Check if we have a conflict first
			$conflicts = (array)( $config['conflicts'] ?? [] );
			if ( array_intersect( $conflicts, $enabling ) ) {
				$this->errors[] = [
					'managewiki-error-conflict' => [
						$this->getName( $config['conflicts'] ),
						$config['name'],
					],
				];
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-27T21:02:58Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-27T17:31:47Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

The changes introduce refined error handling for extension management in the ManageWiki system. New and split error message keys are added to the English localization file to distinguish between different error scenarios related to extension dependencies and requirements. The `commit()` method in the extension management helper is refactored to improve the logic for enabling and disabling extensions, with clearer separation of concerns, expanded checks for dependencies and requirements, and more granular error reporting. The control flow is restructured to prevent partial commits when errors are detected.

## Changes

| File(s)                                      | Change Summary                                                                                                    |
|----------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| i18n/en.json                                | Added `"managewiki-error-dependency"` key. Split `"managewiki-error-requirements"` into `"managewiki-error-requirements-disabled"` and `"managewiki-error-requirements-enabled"`. |
| includes/Helpers/ManageWikiExtensions.php   | Refactored the `commit()` method to improve extension enable/disable logic, enhance error handling, and restructure control flow for clearer separation of concerns and prevention of partial commits. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant ManageWikiExtensions
    participant ExtensionInstaller

    User->>ManageWikiExtensions: Request to enable/disable extensions
    ManageWikiExtensions->>ManageWikiExtensions: Check extension config and requirements
    alt Requirements not met and extension not changed
        ManageWikiExtensions->>User: Report dependency error
    else Requirements not met and extension changed
        ManageWikiExtensions->>User: Report requirements error
    else No changes to extension
        ManageWikiExtensions->>ManageWikiExtensions: Skip processing
    else Extension to be enabled
        ManageWikiExtensions->>ExtensionInstaller: Attempt install
        ExtensionInstaller-->>ManageWikiExtensions: Return install result
        ManageWikiExtensions->>User: Report install errors (if any)
    else Extension to be disabled
        ManageWikiExtensions->>ManageWikiExtensions: Check permission requirements
        alt Requirements not met
            ManageWikiExtensions->>User: Report disabled-requirements error
        else
            ManageWikiExtensions->>ExtensionInstaller: Attempt removal
            ExtensionInstaller-->>ManageWikiExtensions: Return removal result
            ManageWikiExtensions->>User: Report removal errors (if any)
        end
    end
    ManageWikiExtensions->>User: Save changes if no errors
```


</details>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=miraheze/ManageWiki&utm_content=601):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: Universal-Omega
created_at: 2025-04-27T20:59:31Z
body: @coderabbitai review
author_association: MEMBER
user: login: coderabbitai[bot]
created_at: 2025-04-27T20:59:36Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>✅ Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: NONE