Completed
Push — master ( c6c11d...21ab47 )
by
unknown
49:14 queued 16:08
created
lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php 2 patches
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -247,7 +247,7 @@
 block discarded – undo
247 247
 	public function afterException($controller, $methodName, \Exception $exception): Response {
248 248
 		if ($exception instanceof SecurityException) {
249 249
 			if ($exception instanceof StrictCookieMissingException) {
250
-				return new RedirectResponse(\OC::$WEBROOT . '/');
250
+				return new RedirectResponse(\OC::$WEBROOT.'/');
251 251
 			}
252 252
 			if (stripos($this->request->getHeader('Accept'), 'html') === false) {
253 253
 				$response = new JSONResponse(
Please login to merge, or discard this patch.
Indentation   +201 added lines, -201 removed lines patch added patch discarded remove patch
@@ -55,145 +55,145 @@  discard block
 block discarded – undo
55 55
  * check fails
56 56
  */
57 57
 class SecurityMiddleware extends Middleware {
58
-	private ?bool $isAdminUser = null;
59
-	private ?bool $isSubAdmin = null;
58
+    private ?bool $isAdminUser = null;
59
+    private ?bool $isSubAdmin = null;
60 60
 
61
-	public function __construct(
62
-		private readonly IRequest $request,
63
-		private readonly MiddlewareUtils $middlewareUtils,
64
-		private readonly INavigationManager $navigationManager,
65
-		private readonly IURLGenerator $urlGenerator,
66
-		private readonly LoggerInterface $logger,
67
-		private readonly string $appName,
68
-		private readonly bool $isLoggedIn,
69
-		private readonly IGroupManager $groupManager,
70
-		private readonly ISubAdmin $subAdminManager,
71
-		private readonly IAppManager $appManager,
72
-		private readonly IL10N $l10n,
73
-		private readonly AuthorizedGroupMapper $groupAuthorizationMapper,
74
-		private readonly IUserSession $userSession,
75
-		private readonly IRemoteAddress $remoteAddress,
76
-	) {
77
-	}
61
+    public function __construct(
62
+        private readonly IRequest $request,
63
+        private readonly MiddlewareUtils $middlewareUtils,
64
+        private readonly INavigationManager $navigationManager,
65
+        private readonly IURLGenerator $urlGenerator,
66
+        private readonly LoggerInterface $logger,
67
+        private readonly string $appName,
68
+        private readonly bool $isLoggedIn,
69
+        private readonly IGroupManager $groupManager,
70
+        private readonly ISubAdmin $subAdminManager,
71
+        private readonly IAppManager $appManager,
72
+        private readonly IL10N $l10n,
73
+        private readonly AuthorizedGroupMapper $groupAuthorizationMapper,
74
+        private readonly IUserSession $userSession,
75
+        private readonly IRemoteAddress $remoteAddress,
76
+    ) {
77
+    }
78 78
 
79
-	private function isAdminUser(): bool {
80
-		if ($this->isAdminUser === null) {
81
-			$user = $this->userSession->getUser();
82
-			$this->isAdminUser = $user && $this->groupManager->isAdmin($user->getUID());
83
-		}
84
-		return $this->isAdminUser;
85
-	}
79
+    private function isAdminUser(): bool {
80
+        if ($this->isAdminUser === null) {
81
+            $user = $this->userSession->getUser();
82
+            $this->isAdminUser = $user && $this->groupManager->isAdmin($user->getUID());
83
+        }
84
+        return $this->isAdminUser;
85
+    }
86 86
 
87
-	private function isSubAdmin(): bool {
88
-		if ($this->isSubAdmin === null) {
89
-			$user = $this->userSession->getUser();
90
-			$this->isSubAdmin = $user && $this->subAdminManager->isSubAdmin($user);
91
-		}
92
-		return $this->isSubAdmin;
93
-	}
87
+    private function isSubAdmin(): bool {
88
+        if ($this->isSubAdmin === null) {
89
+            $user = $this->userSession->getUser();
90
+            $this->isSubAdmin = $user && $this->subAdminManager->isSubAdmin($user);
91
+        }
92
+        return $this->isSubAdmin;
93
+    }
94 94
 
95
-	/**
96
-	 * This runs all the security checks before a method call. The
97
-	 * security checks are determined by inspecting the controller method
98
-	 * annotations
99
-	 *
100
-	 * @param Controller $controller the controller
101
-	 * @param string $methodName the name of the method
102
-	 * @throws SecurityException when a security check fails
103
-	 *
104
-	 * @suppress PhanUndeclaredClassConstant
105
-	 */
106
-	public function beforeController($controller, $methodName) {
107
-		// this will set the current navigation entry of the app, use this only
108
-		// for normal HTML requests and not for AJAX requests
109
-		$this->navigationManager->setActiveEntry($this->appName);
95
+    /**
96
+     * This runs all the security checks before a method call. The
97
+     * security checks are determined by inspecting the controller method
98
+     * annotations
99
+     *
100
+     * @param Controller $controller the controller
101
+     * @param string $methodName the name of the method
102
+     * @throws SecurityException when a security check fails
103
+     *
104
+     * @suppress PhanUndeclaredClassConstant
105
+     */
106
+    public function beforeController($controller, $methodName) {
107
+        // this will set the current navigation entry of the app, use this only
108
+        // for normal HTML requests and not for AJAX requests
109
+        $this->navigationManager->setActiveEntry($this->appName);
110 110
 
111
-		if (get_class($controller) === \OCA\Talk\Controller\PageController::class && $methodName === 'showCall') {
112
-			$this->navigationManager->setActiveEntry('spreed');
113
-		}
111
+        if (get_class($controller) === \OCA\Talk\Controller\PageController::class && $methodName === 'showCall') {
112
+            $this->navigationManager->setActiveEntry('spreed');
113
+        }
114 114
 
115
-		$reflectionMethod = new ReflectionMethod($controller, $methodName);
115
+        $reflectionMethod = new ReflectionMethod($controller, $methodName);
116 116
 
117
-		// security checks
118
-		$isPublicPage = $this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'PublicPage', PublicPage::class);
117
+        // security checks
118
+        $isPublicPage = $this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'PublicPage', PublicPage::class);
119 119
 
120
-		if ($this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'ExAppRequired', ExAppRequired::class)) {
121
-			if (!$this->userSession instanceof Session || $this->userSession->getSession()->get('app_api') !== true) {
122
-				throw new ExAppRequiredException();
123
-			}
124
-		} elseif (!$isPublicPage) {
125
-			$authorized = false;
126
-			if ($this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, null, AppApiAdminAccessWithoutUser::class)) {
127
-				// this attribute allows ExApp to access admin endpoints only if "userId" is "null"
128
-				if ($this->userSession instanceof Session && $this->userSession->getSession()->get('app_api') === true && $this->userSession->getUser() === null) {
129
-					$authorized = true;
130
-				}
131
-			}
120
+        if ($this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'ExAppRequired', ExAppRequired::class)) {
121
+            if (!$this->userSession instanceof Session || $this->userSession->getSession()->get('app_api') !== true) {
122
+                throw new ExAppRequiredException();
123
+            }
124
+        } elseif (!$isPublicPage) {
125
+            $authorized = false;
126
+            if ($this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, null, AppApiAdminAccessWithoutUser::class)) {
127
+                // this attribute allows ExApp to access admin endpoints only if "userId" is "null"
128
+                if ($this->userSession instanceof Session && $this->userSession->getSession()->get('app_api') === true && $this->userSession->getUser() === null) {
129
+                    $authorized = true;
130
+                }
131
+            }
132 132
 
133
-			if (!$authorized && !$this->isLoggedIn) {
134
-				throw new NotLoggedInException();
135
-			}
133
+            if (!$authorized && !$this->isLoggedIn) {
134
+                throw new NotLoggedInException();
135
+            }
136 136
 
137
-			if (!$authorized && $this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'AuthorizedAdminSetting', AuthorizedAdminSetting::class)) {
138
-				$authorized = $this->isAdminUser();
137
+            if (!$authorized && $this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'AuthorizedAdminSetting', AuthorizedAdminSetting::class)) {
138
+                $authorized = $this->isAdminUser();
139 139
 
140
-				if (!$authorized && $this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)) {
141
-					$authorized = $this->isSubAdmin();
142
-				}
140
+                if (!$authorized && $this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)) {
141
+                    $authorized = $this->isSubAdmin();
142
+                }
143 143
 
144
-				if (!$authorized) {
145
-					$settingClasses = $this->middlewareUtils->getAuthorizedAdminSettingClasses($reflectionMethod);
146
-					$authorizedClasses = $this->groupAuthorizationMapper->findAllClassesForUser($this->userSession->getUser());
147
-					foreach ($settingClasses as $settingClass) {
148
-						$authorized = in_array($settingClass, $authorizedClasses, true);
144
+                if (!$authorized) {
145
+                    $settingClasses = $this->middlewareUtils->getAuthorizedAdminSettingClasses($reflectionMethod);
146
+                    $authorizedClasses = $this->groupAuthorizationMapper->findAllClassesForUser($this->userSession->getUser());
147
+                    foreach ($settingClasses as $settingClass) {
148
+                        $authorized = in_array($settingClass, $authorizedClasses, true);
149 149
 
150
-						if ($authorized) {
151
-							break;
152
-						}
153
-					}
154
-				}
155
-				if (!$authorized) {
156
-					throw new NotAdminException($this->l10n->t('Logged in account must be an admin, a sub admin or gotten special right to access this setting'));
157
-				}
158
-				if (!$this->remoteAddress->allowsAdminActions()) {
159
-					throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn\'t allow you to perform admin actions'));
160
-				}
161
-			}
162
-			if ($this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
163
-				&& !$this->isSubAdmin()
164
-				&& !$this->isAdminUser()
165
-				&& !$authorized) {
166
-				throw new NotAdminException($this->l10n->t('Logged in account must be an admin or sub admin'));
167
-			}
168
-			if (!$this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
169
-				&& !$this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class)
170
-				&& !$this->isAdminUser()
171
-				&& !$authorized) {
172
-				throw new NotAdminException($this->l10n->t('Logged in account must be an admin'));
173
-			}
174
-			if ($this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
175
-				&& !$this->remoteAddress->allowsAdminActions()) {
176
-				throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn\'t allow you to perform admin actions'));
177
-			}
178
-			if (!$this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
179
-				&& !$this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class)
180
-				&& !$this->remoteAddress->allowsAdminActions()) {
181
-				throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn\'t allow you to perform admin actions'));
182
-			}
150
+                        if ($authorized) {
151
+                            break;
152
+                        }
153
+                    }
154
+                }
155
+                if (!$authorized) {
156
+                    throw new NotAdminException($this->l10n->t('Logged in account must be an admin, a sub admin or gotten special right to access this setting'));
157
+                }
158
+                if (!$this->remoteAddress->allowsAdminActions()) {
159
+                    throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn\'t allow you to perform admin actions'));
160
+                }
161
+            }
162
+            if ($this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
163
+                && !$this->isSubAdmin()
164
+                && !$this->isAdminUser()
165
+                && !$authorized) {
166
+                throw new NotAdminException($this->l10n->t('Logged in account must be an admin or sub admin'));
167
+            }
168
+            if (!$this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
169
+                && !$this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class)
170
+                && !$this->isAdminUser()
171
+                && !$authorized) {
172
+                throw new NotAdminException($this->l10n->t('Logged in account must be an admin'));
173
+            }
174
+            if ($this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
175
+                && !$this->remoteAddress->allowsAdminActions()) {
176
+                throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn\'t allow you to perform admin actions'));
177
+            }
178
+            if (!$this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
179
+                && !$this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class)
180
+                && !$this->remoteAddress->allowsAdminActions()) {
181
+                throw new AdminIpNotAllowedException($this->l10n->t('Your current IP address doesn\'t allow you to perform admin actions'));
182
+            }
183 183
 
184
-		}
184
+        }
185 185
 
186
-		// Check for strict cookie requirement
187
-		if ($this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'StrictCookieRequired', StrictCookiesRequired::class)
188
-			|| !$this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) {
189
-			if (!$this->request->passesStrictCookieCheck()) {
190
-				throw new StrictCookieMissingException();
191
-			}
192
-		}
193
-		// CSRF check - also registers the CSRF token since the session may be closed later
194
-		Util::callRegister();
195
-		if ($this->isInvalidCSRFRequired($reflectionMethod)) {
196
-			/*
186
+        // Check for strict cookie requirement
187
+        if ($this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'StrictCookieRequired', StrictCookiesRequired::class)
188
+            || !$this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) {
189
+            if (!$this->request->passesStrictCookieCheck()) {
190
+                throw new StrictCookieMissingException();
191
+            }
192
+        }
193
+        // CSRF check - also registers the CSRF token since the session may be closed later
194
+        Util::callRegister();
195
+        if ($this->isInvalidCSRFRequired($reflectionMethod)) {
196
+            /*
197 197
 			 * Only allow the CSRF check to fail on OCS Requests. This kind of
198 198
 			 * hacks around that we have no full token auth in place yet and we
199 199
 			 * do want to offer CSRF checks for web requests.
@@ -201,89 +201,89 @@  discard block
 block discarded – undo
201 201
 			 * Additionally we allow Bearer authenticated requests to pass on OCS routes.
202 202
 			 * This allows oauth apps (e.g. moodle) to use the OCS endpoints
203 203
 			 */
204
-			if (!$controller instanceof OCSController || !$this->isValidOCSRequest()) {
205
-				throw new CrossSiteRequestForgeryException();
206
-			}
207
-		}
204
+            if (!$controller instanceof OCSController || !$this->isValidOCSRequest()) {
205
+                throw new CrossSiteRequestForgeryException();
206
+            }
207
+        }
208 208
 
209
-		/**
210
-		 * Checks if app is enabled (also includes a check whether user is allowed to access the resource)
211
-		 * The getAppPath() check is here since components such as settings also use the AppFramework and
212
-		 * therefore won't pass this check.
213
-		 * If page is public, app does not need to be enabled for current user/visitor
214
-		 */
215
-		try {
216
-			$appPath = $this->appManager->getAppPath($this->appName);
217
-		} catch (AppPathNotFoundException $e) {
218
-			$appPath = false;
219
-		}
209
+        /**
210
+         * Checks if app is enabled (also includes a check whether user is allowed to access the resource)
211
+         * The getAppPath() check is here since components such as settings also use the AppFramework and
212
+         * therefore won't pass this check.
213
+         * If page is public, app does not need to be enabled for current user/visitor
214
+         */
215
+        try {
216
+            $appPath = $this->appManager->getAppPath($this->appName);
217
+        } catch (AppPathNotFoundException $e) {
218
+            $appPath = false;
219
+        }
220 220
 
221
-		if ($appPath !== false && !$isPublicPage && !$this->appManager->isEnabledForUser($this->appName)) {
222
-			throw new AppNotEnabledException();
223
-		}
224
-	}
221
+        if ($appPath !== false && !$isPublicPage && !$this->appManager->isEnabledForUser($this->appName)) {
222
+            throw new AppNotEnabledException();
223
+        }
224
+    }
225 225
 
226
-	private function isInvalidCSRFRequired(ReflectionMethod $reflectionMethod): bool {
227
-		if ($this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) {
228
-			return false;
229
-		}
226
+    private function isInvalidCSRFRequired(ReflectionMethod $reflectionMethod): bool {
227
+        if ($this->middlewareUtils->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) {
228
+            return false;
229
+        }
230 230
 
231
-		return !$this->request->passesCSRFCheck();
232
-	}
231
+        return !$this->request->passesCSRFCheck();
232
+    }
233 233
 
234
-	private function isValidOCSRequest(): bool {
235
-		return $this->request->getHeader('OCS-APIREQUEST') === 'true'
236
-			|| str_starts_with($this->request->getHeader('Authorization'), 'Bearer ');
237
-	}
234
+    private function isValidOCSRequest(): bool {
235
+        return $this->request->getHeader('OCS-APIREQUEST') === 'true'
236
+            || str_starts_with($this->request->getHeader('Authorization'), 'Bearer ');
237
+    }
238 238
 
239
-	/**
240
-	 * If an SecurityException is being caught, ajax requests return a JSON error
241
-	 * response and non ajax requests redirect to the index
242
-	 *
243
-	 * @param Controller $controller the controller that is being called
244
-	 * @param string $methodName the name of the method that will be called on
245
-	 *                           the controller
246
-	 * @param \Exception $exception the thrown exception
247
-	 * @return Response a Response object or null in case that the exception could not be handled
248
-	 * @throws \Exception the passed in exception if it can't handle it
249
-	 */
250
-	public function afterException($controller, $methodName, \Exception $exception): Response {
251
-		if ($exception instanceof SecurityException) {
252
-			if ($exception instanceof StrictCookieMissingException) {
253
-				return new RedirectResponse(\OC::$WEBROOT . '/');
254
-			}
255
-			if (stripos($this->request->getHeader('Accept'), 'html') === false) {
256
-				$response = new JSONResponse(
257
-					['message' => $exception->getMessage()],
258
-					$exception->getCode()
259
-				);
260
-			} else {
261
-				if ($exception instanceof NotLoggedInException) {
262
-					$params = [];
263
-					if (isset($this->request->server['REQUEST_URI'])) {
264
-						$params['redirect_url'] = $this->request->server['REQUEST_URI'];
265
-					}
266
-					$usernamePrefill = $this->request->getParam('user', '');
267
-					if ($usernamePrefill !== '') {
268
-						$params['user'] = $usernamePrefill;
269
-					}
270
-					if ($this->request->getParam('direct')) {
271
-						$params['direct'] = 1;
272
-					}
273
-					$url = $this->urlGenerator->linkToRoute('core.login.showLoginForm', $params);
274
-					$response = new RedirectResponse($url);
275
-				} else {
276
-					$response = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest');
277
-					$response->setStatus($exception->getCode());
278
-				}
279
-			}
239
+    /**
240
+     * If an SecurityException is being caught, ajax requests return a JSON error
241
+     * response and non ajax requests redirect to the index
242
+     *
243
+     * @param Controller $controller the controller that is being called
244
+     * @param string $methodName the name of the method that will be called on
245
+     *                           the controller
246
+     * @param \Exception $exception the thrown exception
247
+     * @return Response a Response object or null in case that the exception could not be handled
248
+     * @throws \Exception the passed in exception if it can't handle it
249
+     */
250
+    public function afterException($controller, $methodName, \Exception $exception): Response {
251
+        if ($exception instanceof SecurityException) {
252
+            if ($exception instanceof StrictCookieMissingException) {
253
+                return new RedirectResponse(\OC::$WEBROOT . '/');
254
+            }
255
+            if (stripos($this->request->getHeader('Accept'), 'html') === false) {
256
+                $response = new JSONResponse(
257
+                    ['message' => $exception->getMessage()],
258
+                    $exception->getCode()
259
+                );
260
+            } else {
261
+                if ($exception instanceof NotLoggedInException) {
262
+                    $params = [];
263
+                    if (isset($this->request->server['REQUEST_URI'])) {
264
+                        $params['redirect_url'] = $this->request->server['REQUEST_URI'];
265
+                    }
266
+                    $usernamePrefill = $this->request->getParam('user', '');
267
+                    if ($usernamePrefill !== '') {
268
+                        $params['user'] = $usernamePrefill;
269
+                    }
270
+                    if ($this->request->getParam('direct')) {
271
+                        $params['direct'] = 1;
272
+                    }
273
+                    $url = $this->urlGenerator->linkToRoute('core.login.showLoginForm', $params);
274
+                    $response = new RedirectResponse($url);
275
+                } else {
276
+                    $response = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest');
277
+                    $response->setStatus($exception->getCode());
278
+                }
279
+            }
280 280
 
281
-			$this->logger->debug($exception->getMessage(), [
282
-				'exception' => $exception,
283
-			]);
284
-			return $response;
285
-		}
281
+            $this->logger->debug($exception->getMessage(), [
282
+                'exception' => $exception,
283
+            ]);
284
+            return $response;
285
+        }
286 286
 
287
-		throw $exception;
288
-	}
287
+        throw $exception;
288
+    }
289 289
 }
Please login to merge, or discard this patch.
tests/Core/Middleware/TwoFactorMiddlewareTest.php 2 patches
Indentation   +252 added lines, -252 removed lines patch added patch discarded remove patch
@@ -38,271 +38,271 @@
 block discarded – undo
38 38
 use Test\TestCase;
39 39
 
40 40
 class HasTwoFactorAnnotationController extends Controller {
41
-	#[NoTwoFactorRequired]
42
-	public function index(): Response {
43
-		return new Response();
44
-	}
41
+    #[NoTwoFactorRequired]
42
+    public function index(): Response {
43
+        return new Response();
44
+    }
45 45
 }
46 46
 
47 47
 class LoginSetupController extends ALoginSetupController {
48
-	public function index(): Response {
49
-		return new Response();
50
-	}
48
+    public function index(): Response {
49
+        return new Response();
50
+    }
51 51
 }
52 52
 
53 53
 class NoTwoFactorAnnotationController extends Controller {
54
-	public function index(): Response {
55
-		return new Response();
56
-	}
54
+    public function index(): Response {
55
+        return new Response();
56
+    }
57 57
 }
58 58
 
59 59
 class NoTwoFactorChallengeAnnotationController extends TwoFactorChallengeController {
60
-	public function index(): Response {
61
-		return new Response();
62
-	}
60
+    public function index(): Response {
61
+        return new Response();
62
+    }
63 63
 }
64 64
 
65 65
 class HasTwoFactorSetUpDoneAnnotationController extends TwoFactorChallengeController {
66
-	#[TwoFactorSetUpDoneRequired]
67
-	public function index(): Response {
68
-		return new Response();
69
-	}
66
+    #[TwoFactorSetUpDoneRequired]
67
+    public function index(): Response {
68
+        return new Response();
69
+    }
70 70
 }
71 71
 
72 72
 class TwoFactorMiddlewareTest extends TestCase {
73
-	private Manager&MockObject $twoFactorManager;
74
-	private IUserSession&MockObject $userSession;
75
-	private ISession&MockObject $session;
76
-	private IURLGenerator&MockObject $urlGenerator;
77
-	private ControllerMethodReflector&MockObject $reflector;
78
-	private IRequest $request;
79
-	private TwoFactorMiddleware $middleware;
80
-	private LoggerInterface&MockObject $logger;
81
-
82
-	protected function setUp(): void {
83
-		parent::setUp();
84
-
85
-		$this->twoFactorManager = $this->getMockBuilder(Manager::class)
86
-			->disableOriginalConstructor()
87
-			->getMock();
88
-		$this->userSession = $this->getMockBuilder(Session::class)
89
-			->disableOriginalConstructor()
90
-			->getMock();
91
-		$this->session = $this->createMock(ISession::class);
92
-		$this->urlGenerator = $this->createMock(IURLGenerator::class);
93
-		$this->reflector = $this->createMock(ControllerMethodReflector::class);
94
-		$this->logger = $this->createMock(LoggerInterface::class);
95
-		$this->request = new Request(
96
-			[
97
-				'server' => [
98
-					'REQUEST_URI' => 'test/url'
99
-				]
100
-			],
101
-			$this->createMock(IRequestId::class),
102
-			$this->createMock(IConfig::class)
103
-		);
104
-
105
-		$this->middleware = new TwoFactorMiddleware($this->twoFactorManager, $this->userSession, $this->session, $this->urlGenerator, new MiddlewareUtils($this->reflector, $this->logger), $this->request);
106
-	}
107
-
108
-	public function testBeforeControllerNotLoggedIn(): void {
109
-		$this->userSession->expects($this->once())
110
-			->method('isLoggedIn')
111
-			->willReturn(false);
112
-
113
-		$this->userSession->expects($this->never())
114
-			->method('getUser');
115
-
116
-		$controller = $this->getMockBuilder(NoTwoFactorAnnotationController::class)
117
-			->disableOriginalConstructor()
118
-			->getMock();
119
-		$this->middleware->beforeController($controller, 'index');
120
-	}
121
-
122
-	public function testBeforeSetupController(): void {
123
-		$user = $this->createMock(IUser::class);
124
-		$this->userSession->expects($this->any())
125
-			->method('getUser')
126
-			->willReturn($user);
127
-		$this->twoFactorManager->expects($this->once())
128
-			->method('needsSecondFactor')
129
-			->willReturn(true);
130
-		$this->userSession->expects($this->never())
131
-			->method('isLoggedIn');
132
-
133
-		$this->middleware->beforeController(new LoginSetupController('foo', $this->request), 'index');
134
-	}
135
-
136
-	public function testBeforeControllerNoTwoFactorCheckNeeded(): void {
137
-		$user = $this->createMock(IUser::class);
138
-
139
-		$this->userSession->expects($this->once())
140
-			->method('isLoggedIn')
141
-			->willReturn(true);
142
-		$this->userSession->expects($this->once())
143
-			->method('getUser')
144
-			->willReturn($user);
145
-		$this->twoFactorManager->expects($this->once())
146
-			->method('isTwoFactorAuthenticated')
147
-			->with($user)
148
-			->willReturn(false);
149
-
150
-		$controller = $this->getMockBuilder(NoTwoFactorAnnotationController::class)
151
-			->disableOriginalConstructor()
152
-			->getMock();
153
-		$this->middleware->beforeController($controller, 'index');
154
-	}
155
-
156
-
157
-	public function testBeforeControllerTwoFactorAuthRequired(): void {
158
-		$this->expectException(TwoFactorAuthRequiredException::class);
159
-
160
-		$user = $this->createMock(IUser::class);
161
-
162
-		$this->userSession->expects($this->once())
163
-			->method('isLoggedIn')
164
-			->willReturn(true);
165
-		$this->userSession->expects($this->once())
166
-			->method('getUser')
167
-			->willReturn($user);
168
-		$this->twoFactorManager->expects($this->once())
169
-			->method('isTwoFactorAuthenticated')
170
-			->with($user)
171
-			->willReturn(true);
172
-		$this->twoFactorManager->expects($this->once())
173
-			->method('needsSecondFactor')
174
-			->with($user)
175
-			->willReturn(true);
176
-
177
-		$controller = $this->getMockBuilder(NoTwoFactorAnnotationController::class)
178
-			->disableOriginalConstructor()
179
-			->getMock();
180
-		$this->middleware->beforeController($controller, 'index');
181
-	}
182
-
183
-
184
-	public function testBeforeControllerUserAlreadyLoggedIn(): void {
185
-		$this->expectException(UserAlreadyLoggedInException::class);
186
-
187
-		$user = $this->createMock(IUser::class);
188
-
189
-		$this->userSession->expects($this->once())
190
-			->method('isLoggedIn')
191
-			->willReturn(true);
192
-		$this->userSession
193
-			->method('getUser')
194
-			->willReturn($user);
195
-		$this->twoFactorManager->expects($this->once())
196
-			->method('isTwoFactorAuthenticated')
197
-			->with($user)
198
-			->willReturn(true);
199
-		$this->twoFactorManager->expects($this->once())
200
-			->method('needsSecondFactor')
201
-			->with($user)
202
-			->willReturn(false);
203
-
204
-		$twoFactorChallengeController = $this->getMockBuilder(NoTwoFactorChallengeAnnotationController::class)
205
-			->disableOriginalConstructor()
206
-			->getMock();
207
-		$this->middleware->beforeController($twoFactorChallengeController, 'index');
208
-	}
209
-
210
-	public function testAfterExceptionTwoFactorAuthRequired(): void {
211
-		$ex = new TwoFactorAuthRequiredException();
212
-
213
-		$this->urlGenerator->expects($this->once())
214
-			->method('linkToRoute')
215
-			->with('core.TwoFactorChallenge.selectChallenge')
216
-			->willReturn('test/url');
217
-		$expected = new RedirectResponse('test/url');
218
-
219
-		$controller = new HasTwoFactorAnnotationController('foo', $this->request);
220
-		$this->assertEquals($expected, $this->middleware->afterException($controller, 'index', $ex));
221
-	}
222
-
223
-	public function testAfterException(): void {
224
-		$ex = new UserAlreadyLoggedInException();
225
-
226
-		$this->urlGenerator->expects($this->once())
227
-			->method('linkToRoute')
228
-			->with('files.view.index')
229
-			->willReturn('redirect/url');
230
-		$expected = new RedirectResponse('redirect/url');
231
-
232
-		$controller = new HasTwoFactorAnnotationController('foo', $this->request);
233
-		$this->assertEquals($expected, $this->middleware->afterException($controller, 'index', $ex));
234
-	}
235
-
236
-	public function testRequires2FASetupDoneAnnotated(): void {
237
-		$user = $this->createMock(IUser::class);
238
-
239
-		$this->userSession->expects($this->once())
240
-			->method('isLoggedIn')
241
-			->willReturn(true);
242
-		$this->userSession
243
-			->method('getUser')
244
-			->willReturn($user);
245
-		$this->twoFactorManager->expects($this->once())
246
-			->method('isTwoFactorAuthenticated')
247
-			->with($user)
248
-			->willReturn(true);
249
-		$this->twoFactorManager->expects($this->once())
250
-			->method('needsSecondFactor')
251
-			->with($user)
252
-			->willReturn(false);
253
-
254
-		$this->expectException(UserAlreadyLoggedInException::class);
255
-
256
-		$controller = $this->getMockBuilder(HasTwoFactorSetUpDoneAnnotationController::class)
257
-			->disableOriginalConstructor()
258
-			->getMock();
259
-		$this->middleware->beforeController($controller, 'index');
260
-	}
261
-
262
-	public static function dataRequires2FASetupDone(): array {
263
-		return [
264
-			[false, false, false],
265
-			[false,  true,  true],
266
-			[true, false, true],
267
-			[true, true,  true],
268
-		];
269
-	}
270
-
271
-	#[DataProvider('dataRequires2FASetupDone')]
272
-	public function testRequires2FASetupDone(bool $hasProvider, bool $missingProviders, bool $expectEception): void {
273
-		if ($hasProvider) {
274
-			$provider = $this->createMock(IProvider::class);
275
-			$provider->method('getId')
276
-				->willReturn('2FAftw');
277
-			$providers = [$provider];
278
-		} else {
279
-			$providers = [];
280
-		}
281
-
282
-
283
-		$user = $this->createMock(IUser::class);
284
-
285
-		$this->userSession
286
-			->method('getUser')
287
-			->willReturn($user);
288
-		$providerSet = new ProviderSet($providers, $missingProviders);
289
-		$this->twoFactorManager->method('getProviderSet')
290
-			->with($user)
291
-			->willReturn($providerSet);
292
-		$this->userSession
293
-			->method('isLoggedIn')
294
-			->willReturn(false);
295
-
296
-		if ($expectEception) {
297
-			$this->expectException(TwoFactorAuthRequiredException::class);
298
-		} else {
299
-			// hack to make phpunit shut up. Since we don't expect an exception here...
300
-			$this->assertTrue(true);
301
-		}
302
-
303
-		$controller = $this->getMockBuilder(NoTwoFactorChallengeAnnotationController::class)
304
-			->disableOriginalConstructor()
305
-			->getMock();
306
-		$this->middleware->beforeController($controller, 'index');
307
-	}
73
+    private Manager&MockObject $twoFactorManager;
74
+    private IUserSession&MockObject $userSession;
75
+    private ISession&MockObject $session;
76
+    private IURLGenerator&MockObject $urlGenerator;
77
+    private ControllerMethodReflector&MockObject $reflector;
78
+    private IRequest $request;
79
+    private TwoFactorMiddleware $middleware;
80
+    private LoggerInterface&MockObject $logger;
81
+
82
+    protected function setUp(): void {
83
+        parent::setUp();
84
+
85
+        $this->twoFactorManager = $this->getMockBuilder(Manager::class)
86
+            ->disableOriginalConstructor()
87
+            ->getMock();
88
+        $this->userSession = $this->getMockBuilder(Session::class)
89
+            ->disableOriginalConstructor()
90
+            ->getMock();
91
+        $this->session = $this->createMock(ISession::class);
92
+        $this->urlGenerator = $this->createMock(IURLGenerator::class);
93
+        $this->reflector = $this->createMock(ControllerMethodReflector::class);
94
+        $this->logger = $this->createMock(LoggerInterface::class);
95
+        $this->request = new Request(
96
+            [
97
+                'server' => [
98
+                    'REQUEST_URI' => 'test/url'
99
+                ]
100
+            ],
101
+            $this->createMock(IRequestId::class),
102
+            $this->createMock(IConfig::class)
103
+        );
104
+
105
+        $this->middleware = new TwoFactorMiddleware($this->twoFactorManager, $this->userSession, $this->session, $this->urlGenerator, new MiddlewareUtils($this->reflector, $this->logger), $this->request);
106
+    }
107
+
108
+    public function testBeforeControllerNotLoggedIn(): void {
109
+        $this->userSession->expects($this->once())
110
+            ->method('isLoggedIn')
111
+            ->willReturn(false);
112
+
113
+        $this->userSession->expects($this->never())
114
+            ->method('getUser');
115
+
116
+        $controller = $this->getMockBuilder(NoTwoFactorAnnotationController::class)
117
+            ->disableOriginalConstructor()
118
+            ->getMock();
119
+        $this->middleware->beforeController($controller, 'index');
120
+    }
121
+
122
+    public function testBeforeSetupController(): void {
123
+        $user = $this->createMock(IUser::class);
124
+        $this->userSession->expects($this->any())
125
+            ->method('getUser')
126
+            ->willReturn($user);
127
+        $this->twoFactorManager->expects($this->once())
128
+            ->method('needsSecondFactor')
129
+            ->willReturn(true);
130
+        $this->userSession->expects($this->never())
131
+            ->method('isLoggedIn');
132
+
133
+        $this->middleware->beforeController(new LoginSetupController('foo', $this->request), 'index');
134
+    }
135
+
136
+    public function testBeforeControllerNoTwoFactorCheckNeeded(): void {
137
+        $user = $this->createMock(IUser::class);
138
+
139
+        $this->userSession->expects($this->once())
140
+            ->method('isLoggedIn')
141
+            ->willReturn(true);
142
+        $this->userSession->expects($this->once())
143
+            ->method('getUser')
144
+            ->willReturn($user);
145
+        $this->twoFactorManager->expects($this->once())
146
+            ->method('isTwoFactorAuthenticated')
147
+            ->with($user)
148
+            ->willReturn(false);
149
+
150
+        $controller = $this->getMockBuilder(NoTwoFactorAnnotationController::class)
151
+            ->disableOriginalConstructor()
152
+            ->getMock();
153
+        $this->middleware->beforeController($controller, 'index');
154
+    }
155
+
156
+
157
+    public function testBeforeControllerTwoFactorAuthRequired(): void {
158
+        $this->expectException(TwoFactorAuthRequiredException::class);
159
+
160
+        $user = $this->createMock(IUser::class);
161
+
162
+        $this->userSession->expects($this->once())
163
+            ->method('isLoggedIn')
164
+            ->willReturn(true);
165
+        $this->userSession->expects($this->once())
166
+            ->method('getUser')
167
+            ->willReturn($user);
168
+        $this->twoFactorManager->expects($this->once())
169
+            ->method('isTwoFactorAuthenticated')
170
+            ->with($user)
171
+            ->willReturn(true);
172
+        $this->twoFactorManager->expects($this->once())
173
+            ->method('needsSecondFactor')
174
+            ->with($user)
175
+            ->willReturn(true);
176
+
177
+        $controller = $this->getMockBuilder(NoTwoFactorAnnotationController::class)
178
+            ->disableOriginalConstructor()
179
+            ->getMock();
180
+        $this->middleware->beforeController($controller, 'index');
181
+    }
182
+
183
+
184
+    public function testBeforeControllerUserAlreadyLoggedIn(): void {
185
+        $this->expectException(UserAlreadyLoggedInException::class);
186
+
187
+        $user = $this->createMock(IUser::class);
188
+
189
+        $this->userSession->expects($this->once())
190
+            ->method('isLoggedIn')
191
+            ->willReturn(true);
192
+        $this->userSession
193
+            ->method('getUser')
194
+            ->willReturn($user);
195
+        $this->twoFactorManager->expects($this->once())
196
+            ->method('isTwoFactorAuthenticated')
197
+            ->with($user)
198
+            ->willReturn(true);
199
+        $this->twoFactorManager->expects($this->once())
200
+            ->method('needsSecondFactor')
201
+            ->with($user)
202
+            ->willReturn(false);
203
+
204
+        $twoFactorChallengeController = $this->getMockBuilder(NoTwoFactorChallengeAnnotationController::class)
205
+            ->disableOriginalConstructor()
206
+            ->getMock();
207
+        $this->middleware->beforeController($twoFactorChallengeController, 'index');
208
+    }
209
+
210
+    public function testAfterExceptionTwoFactorAuthRequired(): void {
211
+        $ex = new TwoFactorAuthRequiredException();
212
+
213
+        $this->urlGenerator->expects($this->once())
214
+            ->method('linkToRoute')
215
+            ->with('core.TwoFactorChallenge.selectChallenge')
216
+            ->willReturn('test/url');
217
+        $expected = new RedirectResponse('test/url');
218
+
219
+        $controller = new HasTwoFactorAnnotationController('foo', $this->request);
220
+        $this->assertEquals($expected, $this->middleware->afterException($controller, 'index', $ex));
221
+    }
222
+
223
+    public function testAfterException(): void {
224
+        $ex = new UserAlreadyLoggedInException();
225
+
226
+        $this->urlGenerator->expects($this->once())
227
+            ->method('linkToRoute')
228
+            ->with('files.view.index')
229
+            ->willReturn('redirect/url');
230
+        $expected = new RedirectResponse('redirect/url');
231
+
232
+        $controller = new HasTwoFactorAnnotationController('foo', $this->request);
233
+        $this->assertEquals($expected, $this->middleware->afterException($controller, 'index', $ex));
234
+    }
235
+
236
+    public function testRequires2FASetupDoneAnnotated(): void {
237
+        $user = $this->createMock(IUser::class);
238
+
239
+        $this->userSession->expects($this->once())
240
+            ->method('isLoggedIn')
241
+            ->willReturn(true);
242
+        $this->userSession
243
+            ->method('getUser')
244
+            ->willReturn($user);
245
+        $this->twoFactorManager->expects($this->once())
246
+            ->method('isTwoFactorAuthenticated')
247
+            ->with($user)
248
+            ->willReturn(true);
249
+        $this->twoFactorManager->expects($this->once())
250
+            ->method('needsSecondFactor')
251
+            ->with($user)
252
+            ->willReturn(false);
253
+
254
+        $this->expectException(UserAlreadyLoggedInException::class);
255
+
256
+        $controller = $this->getMockBuilder(HasTwoFactorSetUpDoneAnnotationController::class)
257
+            ->disableOriginalConstructor()
258
+            ->getMock();
259
+        $this->middleware->beforeController($controller, 'index');
260
+    }
261
+
262
+    public static function dataRequires2FASetupDone(): array {
263
+        return [
264
+            [false, false, false],
265
+            [false,  true,  true],
266
+            [true, false, true],
267
+            [true, true,  true],
268
+        ];
269
+    }
270
+
271
+    #[DataProvider('dataRequires2FASetupDone')]
272
+    public function testRequires2FASetupDone(bool $hasProvider, bool $missingProviders, bool $expectEception): void {
273
+        if ($hasProvider) {
274
+            $provider = $this->createMock(IProvider::class);
275
+            $provider->method('getId')
276
+                ->willReturn('2FAftw');
277
+            $providers = [$provider];
278
+        } else {
279
+            $providers = [];
280
+        }
281
+
282
+
283
+        $user = $this->createMock(IUser::class);
284
+
285
+        $this->userSession
286
+            ->method('getUser')
287
+            ->willReturn($user);
288
+        $providerSet = new ProviderSet($providers, $missingProviders);
289
+        $this->twoFactorManager->method('getProviderSet')
290
+            ->with($user)
291
+            ->willReturn($providerSet);
292
+        $this->userSession
293
+            ->method('isLoggedIn')
294
+            ->willReturn(false);
295
+
296
+        if ($expectEception) {
297
+            $this->expectException(TwoFactorAuthRequiredException::class);
298
+        } else {
299
+            // hack to make phpunit shut up. Since we don't expect an exception here...
300
+            $this->assertTrue(true);
301
+        }
302
+
303
+        $controller = $this->getMockBuilder(NoTwoFactorChallengeAnnotationController::class)
304
+            ->disableOriginalConstructor()
305
+            ->getMock();
306
+        $this->middleware->beforeController($controller, 'index');
307
+    }
308 308
 }
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -262,9 +262,9 @@
 block discarded – undo
262 262
 	public static function dataRequires2FASetupDone(): array {
263 263
 		return [
264 264
 			[false, false, false],
265
-			[false,  true,  true],
265
+			[false, true, true],
266 266
 			[true, false, true],
267
-			[true, true,  true],
267
+			[true, true, true],
268 268
 		];
269 269
 	}
270 270
 
Please login to merge, or discard this patch.
tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php 1 patch
Indentation   +313 added lines, -313 removed lines patch added patch discarded remove patch
@@ -25,317 +25,317 @@
 block discarded – undo
25 25
 use Test\AppFramework\Middleware\Security\Mock\CORSMiddlewareController;
26 26
 
27 27
 class CORSMiddlewareTest extends \Test\TestCase {
28
-	private ControllerMethodReflector $reflector;
29
-	private Session&MockObject $session;
30
-	private IThrottler&MockObject $throttler;
31
-	private CORSMiddlewareController $controller;
32
-	private LoggerInterface $logger;
33
-
34
-	protected function setUp(): void {
35
-		parent::setUp();
36
-		$this->reflector = new ControllerMethodReflector();
37
-		$this->session = $this->createMock(Session::class);
38
-		$this->throttler = $this->createMock(IThrottler::class);
39
-		$this->logger = $this->createMock(LoggerInterface::class);
40
-		$this->controller = new CORSMiddlewareController(
41
-			'test',
42
-			$this->createMock(IRequest::class)
43
-		);
44
-	}
45
-
46
-	public static function dataSetCORSAPIHeader(): array {
47
-		return [
48
-			['testSetCORSAPIHeader'],
49
-			['testSetCORSAPIHeaderAttribute'],
50
-		];
51
-	}
52
-
53
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataSetCORSAPIHeader')]
54
-	public function testSetCORSAPIHeader(string $method): void {
55
-		$request = new Request(
56
-			[
57
-				'server' => [
58
-					'HTTP_ORIGIN' => 'test'
59
-				]
60
-			],
61
-			$this->createMock(IRequestId::class),
62
-			$this->createMock(IConfig::class)
63
-		);
64
-		$this->reflector->reflect($this->controller, $method);
65
-		$middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
66
-
67
-		$response = $middleware->afterController($this->controller, $method, new Response());
68
-		$headers = $response->getHeaders();
69
-		$this->assertEquals('test', $headers['Access-Control-Allow-Origin']);
70
-	}
71
-
72
-	public function testNoAnnotationNoCORSHEADER(): void {
73
-		$request = new Request(
74
-			[
75
-				'server' => [
76
-					'HTTP_ORIGIN' => 'test'
77
-				]
78
-			],
79
-			$this->createMock(IRequestId::class),
80
-			$this->createMock(IConfig::class)
81
-		);
82
-		$middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
83
-
84
-		$response = $middleware->afterController($this->controller, __FUNCTION__, new Response());
85
-		$headers = $response->getHeaders();
86
-		$this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers));
87
-	}
88
-
89
-	public static function dataNoOriginHeaderNoCORSHEADER(): array {
90
-		return [
91
-			['testNoOriginHeaderNoCORSHEADER'],
92
-			['testNoOriginHeaderNoCORSHEADERAttribute'],
93
-		];
94
-	}
95
-
96
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoOriginHeaderNoCORSHEADER')]
97
-	public function testNoOriginHeaderNoCORSHEADER(string $method): void {
98
-		$request = new Request(
99
-			[],
100
-			$this->createMock(IRequestId::class),
101
-			$this->createMock(IConfig::class)
102
-		);
103
-		$this->reflector->reflect($this->controller, $method);
104
-		$middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
105
-
106
-		$response = $middleware->afterController($this->controller, $method, new Response());
107
-		$headers = $response->getHeaders();
108
-		$this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers));
109
-	}
110
-
111
-	public static function dataCorsIgnoredIfWithCredentialsHeaderPresent(): array {
112
-		return [
113
-			['testCorsIgnoredIfWithCredentialsHeaderPresent'],
114
-			['testCorsAttributeIgnoredIfWithCredentialsHeaderPresent'],
115
-		];
116
-	}
117
-
118
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataCorsIgnoredIfWithCredentialsHeaderPresent')]
119
-	public function testCorsIgnoredIfWithCredentialsHeaderPresent(string $method): void {
120
-		$this->expectException(SecurityException::class);
121
-
122
-		$request = new Request(
123
-			[
124
-				'server' => [
125
-					'HTTP_ORIGIN' => 'test'
126
-				]
127
-			],
128
-			$this->createMock(IRequestId::class),
129
-			$this->createMock(IConfig::class)
130
-		);
131
-		$this->reflector->reflect($this->controller, $method);
132
-		$middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler, $this->logger);
133
-
134
-		$response = new Response();
135
-		$response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE');
136
-		$middleware->afterController($this->controller, $method, $response);
137
-	}
138
-
139
-	public static function dataNoCORSOnAnonymousPublicPage(): array {
140
-		return [
141
-			['testNoCORSOnAnonymousPublicPage'],
142
-			['testNoCORSOnAnonymousPublicPageAttribute'],
143
-			['testNoCORSAttributeOnAnonymousPublicPage'],
144
-			['testNoCORSAttributeOnAnonymousPublicPageAttribute'],
145
-		];
146
-	}
147
-
148
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoCORSOnAnonymousPublicPage')]
149
-	public function testNoCORSOnAnonymousPublicPage(string $method): void {
150
-		$request = new Request(
151
-			[],
152
-			$this->createMock(IRequestId::class),
153
-			$this->createMock(IConfig::class)
154
-		);
155
-		$this->reflector->reflect($this->controller, $method);
156
-		$middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler, $this->logger);
157
-		$this->session->expects($this->once())
158
-			->method('isLoggedIn')
159
-			->willReturn(false);
160
-		$this->session->expects($this->never())
161
-			->method('logout');
162
-		$this->session->expects($this->never())
163
-			->method('logClientIn')
164
-			->with($this->equalTo('user'), $this->equalTo('pass'))
165
-			->willReturn(true);
166
-		$this->reflector->reflect($this->controller, $method);
167
-
168
-		$middleware->beforeController($this->controller, $method);
169
-	}
170
-
171
-	public static function dataCORSShouldNeverAllowCookieAuth(): array {
172
-		return [
173
-			['testCORSShouldNeverAllowCookieAuth'],
174
-			['testCORSShouldNeverAllowCookieAuthAttribute'],
175
-			['testCORSAttributeShouldNeverAllowCookieAuth'],
176
-			['testCORSAttributeShouldNeverAllowCookieAuthAttribute'],
177
-		];
178
-	}
179
-
180
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldNeverAllowCookieAuth')]
181
-	public function testCORSShouldNeverAllowCookieAuth(string $method): void {
182
-		$request = new Request(
183
-			[],
184
-			$this->createMock(IRequestId::class),
185
-			$this->createMock(IConfig::class)
186
-		);
187
-		$this->reflector->reflect($this->controller, $method);
188
-		$middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
189
-		$this->session->expects($this->once())
190
-			->method('isLoggedIn')
191
-			->willReturn(true);
192
-		$this->session->expects($this->once())
193
-			->method('logout');
194
-		$this->session->expects($this->never())
195
-			->method('logClientIn')
196
-			->with($this->equalTo('user'), $this->equalTo('pass'))
197
-			->willReturn(true);
198
-
199
-		$this->expectException(SecurityException::class);
200
-		$middleware->beforeController($this->controller, $method);
201
-	}
202
-
203
-	public static function dataCORSShouldRelogin(): array {
204
-		return [
205
-			['testCORSShouldRelogin'],
206
-			['testCORSAttributeShouldRelogin'],
207
-		];
208
-	}
209
-
210
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldRelogin')]
211
-	public function testCORSShouldRelogin(string $method): void {
212
-		$request = new Request(
213
-			['server' => [
214
-				'PHP_AUTH_USER' => 'user',
215
-				'PHP_AUTH_PW' => 'pass'
216
-			]],
217
-			$this->createMock(IRequestId::class),
218
-			$this->createMock(IConfig::class)
219
-		);
220
-		$this->session->expects($this->once())
221
-			->method('logout');
222
-		$this->session->expects($this->once())
223
-			->method('logClientIn')
224
-			->with($this->equalTo('user'), $this->equalTo('pass'))
225
-			->willReturn(true);
226
-		$this->reflector->reflect($this->controller, $method);
227
-		$middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
228
-
229
-		$middleware->beforeController($this->controller, $method);
230
-	}
231
-
232
-	public static function dataCORSShouldFailIfPasswordLoginIsForbidden(): array {
233
-		return [
234
-			['testCORSShouldFailIfPasswordLoginIsForbidden'],
235
-			['testCORSAttributeShouldFailIfPasswordLoginIsForbidden'],
236
-		];
237
-	}
238
-
239
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldFailIfPasswordLoginIsForbidden')]
240
-	public function testCORSShouldFailIfPasswordLoginIsForbidden(string $method): void {
241
-		$this->expectException(SecurityException::class);
242
-
243
-		$request = new Request(
244
-			['server' => [
245
-				'PHP_AUTH_USER' => 'user',
246
-				'PHP_AUTH_PW' => 'pass'
247
-			]],
248
-			$this->createMock(IRequestId::class),
249
-			$this->createMock(IConfig::class)
250
-		);
251
-		$this->session->expects($this->once())
252
-			->method('logout');
253
-		$this->session->expects($this->once())
254
-			->method('logClientIn')
255
-			->with($this->equalTo('user'), $this->equalTo('pass'))
256
-			->willThrowException(new PasswordLoginForbiddenException);
257
-		$this->reflector->reflect($this->controller, $method);
258
-		$middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
259
-
260
-		$middleware->beforeController($this->controller, $method);
261
-	}
262
-
263
-	public static function dataCORSShouldNotAllowCookieAuth(): array {
264
-		return [
265
-			['testCORSShouldNotAllowCookieAuth'],
266
-			['testCORSAttributeShouldNotAllowCookieAuth'],
267
-		];
268
-	}
269
-
270
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldNotAllowCookieAuth')]
271
-	public function testCORSShouldNotAllowCookieAuth(string $method): void {
272
-		$this->expectException(SecurityException::class);
273
-
274
-		$request = new Request(
275
-			['server' => [
276
-				'PHP_AUTH_USER' => 'user',
277
-				'PHP_AUTH_PW' => 'pass'
278
-			]],
279
-			$this->createMock(IRequestId::class),
280
-			$this->createMock(IConfig::class)
281
-		);
282
-		$this->session->expects($this->once())
283
-			->method('logout');
284
-		$this->session->expects($this->once())
285
-			->method('logClientIn')
286
-			->with($this->equalTo('user'), $this->equalTo('pass'))
287
-			->willReturn(false);
288
-		$this->reflector->reflect($this->controller, $method);
289
-		$middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
290
-
291
-		$middleware->beforeController($this->controller, $method);
292
-	}
293
-
294
-	public function testAfterExceptionWithSecurityExceptionNoStatus(): void {
295
-		$request = new Request(
296
-			['server' => [
297
-				'PHP_AUTH_USER' => 'user',
298
-				'PHP_AUTH_PW' => 'pass'
299
-			]],
300
-			$this->createMock(IRequestId::class),
301
-			$this->createMock(IConfig::class)
302
-		);
303
-		$middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
304
-		$response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception'));
305
-
306
-		$expected = new JSONResponse(['message' => 'A security exception'], 500);
307
-		$this->assertEquals($expected, $response);
308
-	}
309
-
310
-	public function testAfterExceptionWithSecurityExceptionWithStatus(): void {
311
-		$request = new Request(
312
-			['server' => [
313
-				'PHP_AUTH_USER' => 'user',
314
-				'PHP_AUTH_PW' => 'pass'
315
-			]],
316
-			$this->createMock(IRequestId::class),
317
-			$this->createMock(IConfig::class)
318
-		);
319
-		$middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
320
-		$response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception', 501));
321
-
322
-		$expected = new JSONResponse(['message' => 'A security exception'], 501);
323
-		$this->assertEquals($expected, $response);
324
-	}
325
-
326
-	public function testAfterExceptionWithRegularException(): void {
327
-		$this->expectException(\Exception::class);
328
-		$this->expectExceptionMessage('A regular exception');
329
-
330
-		$request = new Request(
331
-			['server' => [
332
-				'PHP_AUTH_USER' => 'user',
333
-				'PHP_AUTH_PW' => 'pass'
334
-			]],
335
-			$this->createMock(IRequestId::class),
336
-			$this->createMock(IConfig::class)
337
-		);
338
-		$middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
339
-		$middleware->afterException($this->controller, __FUNCTION__, new \Exception('A regular exception'));
340
-	}
28
+    private ControllerMethodReflector $reflector;
29
+    private Session&MockObject $session;
30
+    private IThrottler&MockObject $throttler;
31
+    private CORSMiddlewareController $controller;
32
+    private LoggerInterface $logger;
33
+
34
+    protected function setUp(): void {
35
+        parent::setUp();
36
+        $this->reflector = new ControllerMethodReflector();
37
+        $this->session = $this->createMock(Session::class);
38
+        $this->throttler = $this->createMock(IThrottler::class);
39
+        $this->logger = $this->createMock(LoggerInterface::class);
40
+        $this->controller = new CORSMiddlewareController(
41
+            'test',
42
+            $this->createMock(IRequest::class)
43
+        );
44
+    }
45
+
46
+    public static function dataSetCORSAPIHeader(): array {
47
+        return [
48
+            ['testSetCORSAPIHeader'],
49
+            ['testSetCORSAPIHeaderAttribute'],
50
+        ];
51
+    }
52
+
53
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataSetCORSAPIHeader')]
54
+    public function testSetCORSAPIHeader(string $method): void {
55
+        $request = new Request(
56
+            [
57
+                'server' => [
58
+                    'HTTP_ORIGIN' => 'test'
59
+                ]
60
+            ],
61
+            $this->createMock(IRequestId::class),
62
+            $this->createMock(IConfig::class)
63
+        );
64
+        $this->reflector->reflect($this->controller, $method);
65
+        $middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
66
+
67
+        $response = $middleware->afterController($this->controller, $method, new Response());
68
+        $headers = $response->getHeaders();
69
+        $this->assertEquals('test', $headers['Access-Control-Allow-Origin']);
70
+    }
71
+
72
+    public function testNoAnnotationNoCORSHEADER(): void {
73
+        $request = new Request(
74
+            [
75
+                'server' => [
76
+                    'HTTP_ORIGIN' => 'test'
77
+                ]
78
+            ],
79
+            $this->createMock(IRequestId::class),
80
+            $this->createMock(IConfig::class)
81
+        );
82
+        $middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
83
+
84
+        $response = $middleware->afterController($this->controller, __FUNCTION__, new Response());
85
+        $headers = $response->getHeaders();
86
+        $this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers));
87
+    }
88
+
89
+    public static function dataNoOriginHeaderNoCORSHEADER(): array {
90
+        return [
91
+            ['testNoOriginHeaderNoCORSHEADER'],
92
+            ['testNoOriginHeaderNoCORSHEADERAttribute'],
93
+        ];
94
+    }
95
+
96
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoOriginHeaderNoCORSHEADER')]
97
+    public function testNoOriginHeaderNoCORSHEADER(string $method): void {
98
+        $request = new Request(
99
+            [],
100
+            $this->createMock(IRequestId::class),
101
+            $this->createMock(IConfig::class)
102
+        );
103
+        $this->reflector->reflect($this->controller, $method);
104
+        $middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
105
+
106
+        $response = $middleware->afterController($this->controller, $method, new Response());
107
+        $headers = $response->getHeaders();
108
+        $this->assertFalse(array_key_exists('Access-Control-Allow-Origin', $headers));
109
+    }
110
+
111
+    public static function dataCorsIgnoredIfWithCredentialsHeaderPresent(): array {
112
+        return [
113
+            ['testCorsIgnoredIfWithCredentialsHeaderPresent'],
114
+            ['testCorsAttributeIgnoredIfWithCredentialsHeaderPresent'],
115
+        ];
116
+    }
117
+
118
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataCorsIgnoredIfWithCredentialsHeaderPresent')]
119
+    public function testCorsIgnoredIfWithCredentialsHeaderPresent(string $method): void {
120
+        $this->expectException(SecurityException::class);
121
+
122
+        $request = new Request(
123
+            [
124
+                'server' => [
125
+                    'HTTP_ORIGIN' => 'test'
126
+                ]
127
+            ],
128
+            $this->createMock(IRequestId::class),
129
+            $this->createMock(IConfig::class)
130
+        );
131
+        $this->reflector->reflect($this->controller, $method);
132
+        $middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler, $this->logger);
133
+
134
+        $response = new Response();
135
+        $response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE');
136
+        $middleware->afterController($this->controller, $method, $response);
137
+    }
138
+
139
+    public static function dataNoCORSOnAnonymousPublicPage(): array {
140
+        return [
141
+            ['testNoCORSOnAnonymousPublicPage'],
142
+            ['testNoCORSOnAnonymousPublicPageAttribute'],
143
+            ['testNoCORSAttributeOnAnonymousPublicPage'],
144
+            ['testNoCORSAttributeOnAnonymousPublicPageAttribute'],
145
+        ];
146
+    }
147
+
148
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCORSOnAnonymousPublicPage')]
149
+    public function testNoCORSOnAnonymousPublicPage(string $method): void {
150
+        $request = new Request(
151
+            [],
152
+            $this->createMock(IRequestId::class),
153
+            $this->createMock(IConfig::class)
154
+        );
155
+        $this->reflector->reflect($this->controller, $method);
156
+        $middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler, $this->logger);
157
+        $this->session->expects($this->once())
158
+            ->method('isLoggedIn')
159
+            ->willReturn(false);
160
+        $this->session->expects($this->never())
161
+            ->method('logout');
162
+        $this->session->expects($this->never())
163
+            ->method('logClientIn')
164
+            ->with($this->equalTo('user'), $this->equalTo('pass'))
165
+            ->willReturn(true);
166
+        $this->reflector->reflect($this->controller, $method);
167
+
168
+        $middleware->beforeController($this->controller, $method);
169
+    }
170
+
171
+    public static function dataCORSShouldNeverAllowCookieAuth(): array {
172
+        return [
173
+            ['testCORSShouldNeverAllowCookieAuth'],
174
+            ['testCORSShouldNeverAllowCookieAuthAttribute'],
175
+            ['testCORSAttributeShouldNeverAllowCookieAuth'],
176
+            ['testCORSAttributeShouldNeverAllowCookieAuthAttribute'],
177
+        ];
178
+    }
179
+
180
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldNeverAllowCookieAuth')]
181
+    public function testCORSShouldNeverAllowCookieAuth(string $method): void {
182
+        $request = new Request(
183
+            [],
184
+            $this->createMock(IRequestId::class),
185
+            $this->createMock(IConfig::class)
186
+        );
187
+        $this->reflector->reflect($this->controller, $method);
188
+        $middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
189
+        $this->session->expects($this->once())
190
+            ->method('isLoggedIn')
191
+            ->willReturn(true);
192
+        $this->session->expects($this->once())
193
+            ->method('logout');
194
+        $this->session->expects($this->never())
195
+            ->method('logClientIn')
196
+            ->with($this->equalTo('user'), $this->equalTo('pass'))
197
+            ->willReturn(true);
198
+
199
+        $this->expectException(SecurityException::class);
200
+        $middleware->beforeController($this->controller, $method);
201
+    }
202
+
203
+    public static function dataCORSShouldRelogin(): array {
204
+        return [
205
+            ['testCORSShouldRelogin'],
206
+            ['testCORSAttributeShouldRelogin'],
207
+        ];
208
+    }
209
+
210
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldRelogin')]
211
+    public function testCORSShouldRelogin(string $method): void {
212
+        $request = new Request(
213
+            ['server' => [
214
+                'PHP_AUTH_USER' => 'user',
215
+                'PHP_AUTH_PW' => 'pass'
216
+            ]],
217
+            $this->createMock(IRequestId::class),
218
+            $this->createMock(IConfig::class)
219
+        );
220
+        $this->session->expects($this->once())
221
+            ->method('logout');
222
+        $this->session->expects($this->once())
223
+            ->method('logClientIn')
224
+            ->with($this->equalTo('user'), $this->equalTo('pass'))
225
+            ->willReturn(true);
226
+        $this->reflector->reflect($this->controller, $method);
227
+        $middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
228
+
229
+        $middleware->beforeController($this->controller, $method);
230
+    }
231
+
232
+    public static function dataCORSShouldFailIfPasswordLoginIsForbidden(): array {
233
+        return [
234
+            ['testCORSShouldFailIfPasswordLoginIsForbidden'],
235
+            ['testCORSAttributeShouldFailIfPasswordLoginIsForbidden'],
236
+        ];
237
+    }
238
+
239
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldFailIfPasswordLoginIsForbidden')]
240
+    public function testCORSShouldFailIfPasswordLoginIsForbidden(string $method): void {
241
+        $this->expectException(SecurityException::class);
242
+
243
+        $request = new Request(
244
+            ['server' => [
245
+                'PHP_AUTH_USER' => 'user',
246
+                'PHP_AUTH_PW' => 'pass'
247
+            ]],
248
+            $this->createMock(IRequestId::class),
249
+            $this->createMock(IConfig::class)
250
+        );
251
+        $this->session->expects($this->once())
252
+            ->method('logout');
253
+        $this->session->expects($this->once())
254
+            ->method('logClientIn')
255
+            ->with($this->equalTo('user'), $this->equalTo('pass'))
256
+            ->willThrowException(new PasswordLoginForbiddenException);
257
+        $this->reflector->reflect($this->controller, $method);
258
+        $middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
259
+
260
+        $middleware->beforeController($this->controller, $method);
261
+    }
262
+
263
+    public static function dataCORSShouldNotAllowCookieAuth(): array {
264
+        return [
265
+            ['testCORSShouldNotAllowCookieAuth'],
266
+            ['testCORSAttributeShouldNotAllowCookieAuth'],
267
+        ];
268
+    }
269
+
270
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataCORSShouldNotAllowCookieAuth')]
271
+    public function testCORSShouldNotAllowCookieAuth(string $method): void {
272
+        $this->expectException(SecurityException::class);
273
+
274
+        $request = new Request(
275
+            ['server' => [
276
+                'PHP_AUTH_USER' => 'user',
277
+                'PHP_AUTH_PW' => 'pass'
278
+            ]],
279
+            $this->createMock(IRequestId::class),
280
+            $this->createMock(IConfig::class)
281
+        );
282
+        $this->session->expects($this->once())
283
+            ->method('logout');
284
+        $this->session->expects($this->once())
285
+            ->method('logClientIn')
286
+            ->with($this->equalTo('user'), $this->equalTo('pass'))
287
+            ->willReturn(false);
288
+        $this->reflector->reflect($this->controller, $method);
289
+        $middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
290
+
291
+        $middleware->beforeController($this->controller, $method);
292
+    }
293
+
294
+    public function testAfterExceptionWithSecurityExceptionNoStatus(): void {
295
+        $request = new Request(
296
+            ['server' => [
297
+                'PHP_AUTH_USER' => 'user',
298
+                'PHP_AUTH_PW' => 'pass'
299
+            ]],
300
+            $this->createMock(IRequestId::class),
301
+            $this->createMock(IConfig::class)
302
+        );
303
+        $middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
304
+        $response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception'));
305
+
306
+        $expected = new JSONResponse(['message' => 'A security exception'], 500);
307
+        $this->assertEquals($expected, $response);
308
+    }
309
+
310
+    public function testAfterExceptionWithSecurityExceptionWithStatus(): void {
311
+        $request = new Request(
312
+            ['server' => [
313
+                'PHP_AUTH_USER' => 'user',
314
+                'PHP_AUTH_PW' => 'pass'
315
+            ]],
316
+            $this->createMock(IRequestId::class),
317
+            $this->createMock(IConfig::class)
318
+        );
319
+        $middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
320
+        $response = $middleware->afterException($this->controller, __FUNCTION__, new SecurityException('A security exception', 501));
321
+
322
+        $expected = new JSONResponse(['message' => 'A security exception'], 501);
323
+        $this->assertEquals($expected, $response);
324
+    }
325
+
326
+    public function testAfterExceptionWithRegularException(): void {
327
+        $this->expectException(\Exception::class);
328
+        $this->expectExceptionMessage('A regular exception');
329
+
330
+        $request = new Request(
331
+            ['server' => [
332
+                'PHP_AUTH_USER' => 'user',
333
+                'PHP_AUTH_PW' => 'pass'
334
+            ]],
335
+            $this->createMock(IRequestId::class),
336
+            $this->createMock(IConfig::class)
337
+        );
338
+        $middleware = new CORSMiddleware($request, new MiddlewareUtils($this->reflector, $this->logger), $this->session, $this->throttler);
339
+        $middleware->afterException($this->controller, __FUNCTION__, new \Exception('A regular exception'));
340
+    }
341 341
 }
Please login to merge, or discard this patch.
tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php 2 patches
Indentation   +642 added lines, -642 removed lines patch added patch discarded remove patch
@@ -45,646 +45,646 @@
 block discarded – undo
45 45
 use Test\AppFramework\Middleware\Security\Mock\SecurityMiddlewareController;
46 46
 
47 47
 class SecurityMiddlewareTest extends \Test\TestCase {
48
-	private SecurityMiddleware $middleware;
49
-	private ControllerMethodReflector $reader;
50
-	private SecurityMiddlewareController $controller;
51
-	private SecurityException $secAjaxException;
52
-	private IRequest|MockObject $request;
53
-	private MiddlewareUtils $middlewareUtils;
54
-	private LoggerInterface&MockObject $logger;
55
-	private INavigationManager&MockObject $navigationManager;
56
-	private IURLGenerator&MockObject $urlGenerator;
57
-	private IAppManager&MockObject $appManager;
58
-	private IL10N&MockObject $l10n;
59
-	private IUserSession&MockObject $userSession;
60
-	private AuthorizedGroupMapper&MockObject $authorizedGroupMapper;
61
-
62
-	protected function setUp(): void {
63
-		parent::setUp();
64
-
65
-		$this->authorizedGroupMapper = $this->createMock(AuthorizedGroupMapper::class);
66
-		$this->userSession = $this->createMock(Session::class);
67
-		$user = $this->createMock(IUser::class);
68
-		$user->method('getUID')->willReturn('test');
69
-		$this->userSession->method('getUser')->willReturn($user);
70
-		$this->request = $this->createMock(IRequest::class);
71
-		$this->controller = new SecurityMiddlewareController(
72
-			'test',
73
-			$this->request
74
-		);
75
-		$this->reader = new ControllerMethodReflector();
76
-		$this->logger = $this->createMock(LoggerInterface::class);
77
-		$this->navigationManager = $this->createMock(INavigationManager::class);
78
-		$this->urlGenerator = $this->createMock(IURLGenerator::class);
79
-		$this->l10n = $this->createMock(IL10N::class);
80
-		$this->middlewareUtils = new MiddlewareUtils($this->reader, $this->logger);
81
-		$this->middleware = $this->getMiddleware(true, true, false);
82
-		$this->secAjaxException = new SecurityException('hey', true);
83
-	}
84
-
85
-	private function getMiddleware(bool $isLoggedIn, bool $isAdminUser, bool $isSubAdmin, bool $isAppEnabledForUser = true): SecurityMiddleware {
86
-		$this->appManager = $this->createMock(IAppManager::class);
87
-		$this->appManager->expects($this->any())
88
-			->method('isEnabledForUser')
89
-			->willReturn($isAppEnabledForUser);
90
-		$remoteIpAddress = $this->createMock(IRemoteAddress::class);
91
-		$remoteIpAddress->method('allowsAdminActions')->willReturn(true);
92
-
93
-		$groupManager = $this->createMock(IGroupManager::class);
94
-		$groupManager->method('isAdmin')
95
-			->willReturn($isAdminUser);
96
-		$subAdminManager = $this->createMock(ISubAdmin::class);
97
-		$subAdminManager->method('isSubAdmin')
98
-			->willReturn($isSubAdmin);
99
-
100
-		return new SecurityMiddleware(
101
-			$this->request,
102
-			$this->middlewareUtils,
103
-			$this->navigationManager,
104
-			$this->urlGenerator,
105
-			$this->logger,
106
-			'files',
107
-			$isLoggedIn,
108
-			$groupManager,
109
-			$subAdminManager,
110
-			$this->appManager,
111
-			$this->l10n,
112
-			$this->authorizedGroupMapper,
113
-			$this->userSession,
114
-			$remoteIpAddress
115
-		);
116
-	}
117
-
118
-	public static function dataNoCSRFRequiredPublicPage(): array {
119
-		return [
120
-			['testAnnotationNoCSRFRequiredPublicPage'],
121
-			['testAnnotationNoCSRFRequiredAttributePublicPage'],
122
-			['testAnnotationPublicPageAttributeNoCSRFRequired'],
123
-			['testAttributeNoCSRFRequiredPublicPage'],
124
-		];
125
-	}
126
-
127
-	public static function dataPublicPage(): array {
128
-		return [
129
-			['testAnnotationPublicPage'],
130
-			['testAttributePublicPage'],
131
-		];
132
-	}
133
-
134
-	public static function dataNoCSRFRequired(): array {
135
-		return [
136
-			['testAnnotationNoCSRFRequired'],
137
-			['testAttributeNoCSRFRequired'],
138
-		];
139
-	}
140
-
141
-	public static function dataPublicPageStrictCookieRequired(): array {
142
-		return [
143
-			['testAnnotationPublicPageStrictCookieRequired'],
144
-			['testAnnotationStrictCookieRequiredAttributePublicPage'],
145
-			['testAnnotationPublicPageAttributeStrictCookiesRequired'],
146
-			['testAttributePublicPageStrictCookiesRequired'],
147
-		];
148
-	}
149
-
150
-	public static function dataNoCSRFRequiredPublicPageStrictCookieRequired(): array {
151
-		return [
152
-			['testAnnotationNoCSRFRequiredPublicPageStrictCookieRequired'],
153
-			['testAttributeNoCSRFRequiredPublicPageStrictCookiesRequired'],
154
-		];
155
-	}
156
-
157
-	public static function dataNoAdminRequiredNoCSRFRequired(): array {
158
-		return [
159
-			['testAnnotationNoAdminRequiredNoCSRFRequired'],
160
-			['testAttributeNoAdminRequiredNoCSRFRequired'],
161
-		];
162
-	}
163
-
164
-	public static function dataNoAdminRequiredNoCSRFRequiredPublicPage(): array {
165
-		return [
166
-			['testAnnotationNoAdminRequiredNoCSRFRequiredPublicPage'],
167
-			['testAttributeNoAdminRequiredNoCSRFRequiredPublicPage'],
168
-		];
169
-	}
170
-
171
-	public static function dataNoCSRFRequiredSubAdminRequired(): array {
172
-		return [
173
-			['testAnnotationNoCSRFRequiredSubAdminRequired'],
174
-			['testAnnotationNoCSRFRequiredAttributeSubAdminRequired'],
175
-			['testAnnotationSubAdminRequiredAttributeNoCSRFRequired'],
176
-			['testAttributeNoCSRFRequiredSubAdminRequired'],
177
-		];
178
-	}
179
-
180
-	public static function dataExAppRequired(): array {
181
-		return [
182
-			['testAnnotationExAppRequired'],
183
-			['testAttributeExAppRequired'],
184
-		];
185
-	}
186
-
187
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
188
-	public function testSetNavigationEntry(string $method): void {
189
-		$this->navigationManager->expects($this->once())
190
-			->method('setActiveEntry')
191
-			->with($this->equalTo('files'));
192
-
193
-		$this->reader->reflect($this->controller, $method);
194
-		$this->middleware->beforeController($this->controller, $method);
195
-	}
196
-
197
-
198
-	/**
199
-	 * @param string $method
200
-	 * @param string $test
201
-	 */
202
-	private function ajaxExceptionStatus($method, $test, $status) {
203
-		$isLoggedIn = false;
204
-		$isAdminUser = false;
205
-
206
-		// isAdminUser requires isLoggedIn call to return true
207
-		if ($test === 'isAdminUser') {
208
-			$isLoggedIn = true;
209
-		}
210
-
211
-		$sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false);
212
-
213
-		try {
214
-			$this->reader->reflect($this->controller, $method);
215
-			$sec->beforeController($this->controller, $method);
216
-		} catch (SecurityException $ex) {
217
-			$this->assertEquals($status, $ex->getCode());
218
-		}
219
-
220
-		// add assertion if everything should work fine otherwise phpunit will
221
-		// complain
222
-		if ($status === 0) {
223
-			$this->addToAssertionCount(1);
224
-		}
225
-	}
226
-
227
-	public function testAjaxStatusLoggedInCheck(): void {
228
-		$this->ajaxExceptionStatus(
229
-			'testNoAnnotationNorAttribute',
230
-			'isLoggedIn',
231
-			Http::STATUS_UNAUTHORIZED
232
-		);
233
-	}
234
-
235
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')]
236
-	public function testAjaxNotAdminCheck(string $method): void {
237
-		$this->ajaxExceptionStatus(
238
-			$method,
239
-			'isAdminUser',
240
-			Http::STATUS_FORBIDDEN
241
-		);
242
-	}
243
-
244
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')]
245
-	public function testAjaxStatusCSRFCheck(string $method): void {
246
-		$this->ajaxExceptionStatus(
247
-			$method,
248
-			'passesCSRFCheck',
249
-			Http::STATUS_PRECONDITION_FAILED
250
-		);
251
-	}
252
-
253
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
254
-	public function testAjaxStatusAllGood(string $method): void {
255
-		$this->ajaxExceptionStatus(
256
-			$method,
257
-			'isLoggedIn',
258
-			0
259
-		);
260
-		$this->ajaxExceptionStatus(
261
-			$method,
262
-			'isAdminUser',
263
-			0
264
-		);
265
-		$this->ajaxExceptionStatus(
266
-			$method,
267
-			'passesCSRFCheck',
268
-			0
269
-		);
270
-	}
271
-
272
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
273
-	public function testNoChecks(string $method): void {
274
-		$this->request->expects($this->never())
275
-			->method('passesCSRFCheck')
276
-			->willReturn(false);
277
-
278
-		$sec = $this->getMiddleware(false, false, false);
279
-
280
-		$this->reader->reflect($this->controller, $method);
281
-		$sec->beforeController($this->controller, $method);
282
-	}
283
-
284
-	/**
285
-	 * @param string $method
286
-	 * @param string $expects
287
-	 */
288
-	private function securityCheck($method, $expects, $shouldFail = false) {
289
-		// admin check requires login
290
-		if ($expects === 'isAdminUser') {
291
-			$isLoggedIn = true;
292
-			$isAdminUser = !$shouldFail;
293
-		} else {
294
-			$isLoggedIn = !$shouldFail;
295
-			$isAdminUser = false;
296
-		}
297
-
298
-		$sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false);
299
-
300
-		if ($shouldFail) {
301
-			$this->expectException(SecurityException::class);
302
-		} else {
303
-			$this->addToAssertionCount(1);
304
-		}
305
-
306
-		$this->reader->reflect($this->controller, $method);
307
-		$sec->beforeController($this->controller, $method);
308
-	}
309
-
310
-
311
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')]
312
-	public function testCsrfCheck(string $method): void {
313
-		$this->expectException(CrossSiteRequestForgeryException::class);
314
-
315
-		$this->request->expects($this->once())
316
-			->method('passesCSRFCheck')
317
-			->willReturn(false);
318
-		$this->request->expects($this->once())
319
-			->method('passesStrictCookieCheck')
320
-			->willReturn(true);
321
-		$this->reader->reflect($this->controller, $method);
322
-		$this->middleware->beforeController($this->controller, $method);
323
-	}
324
-
325
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
326
-	public function testNoCsrfCheck(string $method): void {
327
-		$this->request->expects($this->never())
328
-			->method('passesCSRFCheck')
329
-			->willReturn(false);
330
-
331
-		$this->reader->reflect($this->controller, $method);
332
-		$this->middleware->beforeController($this->controller, $method);
333
-	}
334
-
335
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')]
336
-	public function testPassesCsrfCheck(string $method): void {
337
-		$this->request->expects($this->once())
338
-			->method('passesCSRFCheck')
339
-			->willReturn(true);
340
-		$this->request->expects($this->once())
341
-			->method('passesStrictCookieCheck')
342
-			->willReturn(true);
343
-
344
-		$this->reader->reflect($this->controller, $method);
345
-		$this->middleware->beforeController($this->controller, $method);
346
-	}
347
-
348
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')]
349
-	public function testFailCsrfCheck(string $method): void {
350
-		$this->expectException(CrossSiteRequestForgeryException::class);
351
-
352
-		$this->request->expects($this->once())
353
-			->method('passesCSRFCheck')
354
-			->willReturn(false);
355
-		$this->request->expects($this->once())
356
-			->method('passesStrictCookieCheck')
357
-			->willReturn(true);
358
-
359
-		$this->reader->reflect($this->controller, $method);
360
-		$this->middleware->beforeController($this->controller, $method);
361
-	}
362
-
363
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPageStrictCookieRequired')]
364
-	public function testStrictCookieRequiredCheck(string $method): void {
365
-		$this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException::class);
366
-
367
-		$this->request->expects($this->never())
368
-			->method('passesCSRFCheck');
369
-		$this->request->expects($this->once())
370
-			->method('passesStrictCookieCheck')
371
-			->willReturn(false);
372
-
373
-		$this->reader->reflect($this->controller, $method);
374
-		$this->middleware->beforeController($this->controller, $method);
375
-	}
376
-
377
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
378
-	public function testNoStrictCookieRequiredCheck(string $method): void {
379
-		$this->request->expects($this->never())
380
-			->method('passesStrictCookieCheck')
381
-			->willReturn(false);
382
-
383
-		$this->reader->reflect($this->controller, $method);
384
-		$this->middleware->beforeController($this->controller, $method);
385
-	}
386
-
387
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPageStrictCookieRequired')]
388
-	public function testPassesStrictCookieRequiredCheck(string $method): void {
389
-		$this->request
390
-			->expects($this->once())
391
-			->method('passesStrictCookieCheck')
392
-			->willReturn(true);
393
-
394
-		$this->reader->reflect($this->controller, $method);
395
-		$this->middleware->beforeController($this->controller, $method);
396
-	}
397
-
398
-	public static function dataCsrfOcsController(): array {
399
-		return [
400
-			[NormalController::class, false, false, true],
401
-			[NormalController::class, false,  true, true],
402
-			[NormalController::class,  true, false, true],
403
-			[NormalController::class,  true,  true, true],
404
-
405
-			[OCSController::class, false, false,  true],
406
-			[OCSController::class, false,  true, false],
407
-			[OCSController::class,  true, false, false],
408
-			[OCSController::class,  true,  true, false],
409
-		];
410
-	}
411
-
412
-	/**
413
-	 * @param string $controllerClass
414
-	 * @param bool $hasOcsApiHeader
415
-	 * @param bool $hasBearerAuth
416
-	 * @param bool $exception
417
-	 */
418
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataCsrfOcsController')]
419
-	public function testCsrfOcsController(string $controllerClass, bool $hasOcsApiHeader, bool $hasBearerAuth, bool $exception): void {
420
-		$this->request
421
-			->method('getHeader')
422
-			->willReturnCallback(function ($header) use ($hasOcsApiHeader, $hasBearerAuth) {
423
-				if ($header === 'OCS-APIREQUEST' && $hasOcsApiHeader) {
424
-					return 'true';
425
-				}
426
-				if ($header === 'Authorization' && $hasBearerAuth) {
427
-					return 'Bearer TOKEN!';
428
-				}
429
-				return '';
430
-			});
431
-		$this->request->expects($this->once())
432
-			->method('passesStrictCookieCheck')
433
-			->willReturn(true);
434
-
435
-		$controller = new $controllerClass('test', $this->request);
436
-
437
-		try {
438
-			$this->middleware->beforeController($controller, 'foo');
439
-			$this->assertFalse($exception);
440
-		} catch (CrossSiteRequestForgeryException $e) {
441
-			$this->assertTrue($exception);
442
-		}
443
-	}
444
-
445
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')]
446
-	public function testLoggedInCheck(string $method): void {
447
-		$this->securityCheck($method, 'isLoggedIn');
448
-	}
449
-
450
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')]
451
-	public function testFailLoggedInCheck(string $method): void {
452
-		$this->securityCheck($method, 'isLoggedIn', true);
453
-	}
454
-
455
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')]
456
-	public function testIsAdminCheck(string $method): void {
457
-		$this->securityCheck($method, 'isAdminUser');
458
-	}
459
-
460
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')]
461
-	public function testIsNotSubAdminCheck(string $method): void {
462
-		$this->reader->reflect($this->controller, $method);
463
-		$sec = $this->getMiddleware(true, false, false);
464
-
465
-		$this->expectException(SecurityException::class);
466
-		$sec->beforeController($this->controller, $method);
467
-	}
468
-
469
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')]
470
-	public function testIsSubAdminCheck(string $method): void {
471
-		$this->reader->reflect($this->controller, $method);
472
-		$sec = $this->getMiddleware(true, false, true);
473
-
474
-		$sec->beforeController($this->controller, $method);
475
-		$this->addToAssertionCount(1);
476
-	}
477
-
478
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')]
479
-	public function testIsSubAdminAndAdminCheck(string $method): void {
480
-		$this->reader->reflect($this->controller, $method);
481
-		$sec = $this->getMiddleware(true, true, true);
482
-
483
-		$sec->beforeController($this->controller, $method);
484
-		$this->addToAssertionCount(1);
485
-	}
486
-
487
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')]
488
-	public function testFailIsAdminCheck(string $method): void {
489
-		$this->securityCheck($method, 'isAdminUser', true);
490
-	}
491
-
492
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequiredPublicPage')]
493
-	public function testRestrictedAppLoggedInPublicPage(string $method): void {
494
-		$middleware = $this->getMiddleware(true, false, false);
495
-		$this->reader->reflect($this->controller, $method);
496
-
497
-		$this->appManager->method('getAppPath')
498
-			->with('files')
499
-			->willReturn('foo');
500
-
501
-		$this->appManager->method('isEnabledForUser')
502
-			->with('files')
503
-			->willReturn(false);
504
-
505
-		$middleware->beforeController($this->controller, $method);
506
-		$this->addToAssertionCount(1);
507
-	}
508
-
509
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequiredPublicPage')]
510
-	public function testRestrictedAppNotLoggedInPublicPage(string $method): void {
511
-		$middleware = $this->getMiddleware(false, false, false);
512
-		$this->reader->reflect($this->controller, $method);
513
-
514
-		$this->appManager->method('getAppPath')
515
-			->with('files')
516
-			->willReturn('foo');
517
-
518
-		$this->appManager->method('isEnabledForUser')
519
-			->with('files')
520
-			->willReturn(false);
521
-
522
-		$middleware->beforeController($this->controller, $method);
523
-		$this->addToAssertionCount(1);
524
-	}
525
-
526
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')]
527
-	public function testRestrictedAppLoggedIn(string $method): void {
528
-		$middleware = $this->getMiddleware(true, false, false, false);
529
-		$this->reader->reflect($this->controller, $method);
530
-
531
-		$this->appManager->method('getAppPath')
532
-			->with('files')
533
-			->willReturn('foo');
534
-
535
-		$this->expectException(AppNotEnabledException::class);
536
-		$middleware->beforeController($this->controller, $method);
537
-	}
538
-
539
-
540
-	public function testAfterExceptionNotCaughtThrowsItAgain(): void {
541
-		$ex = new \Exception();
542
-		$this->expectException(\Exception::class);
543
-		$this->middleware->afterException($this->controller, 'test', $ex);
544
-	}
545
-
546
-	public function testAfterExceptionReturnsRedirectForNotLoggedInUser(): void {
547
-		$this->request = new Request(
548
-			[
549
-				'server'
550
-					=> [
551
-						'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
552
-						'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp'
553
-					]
554
-			],
555
-			$this->createMock(IRequestId::class),
556
-			$this->createMock(IConfig::class)
557
-		);
558
-		$this->middleware = $this->getMiddleware(false, false, false);
559
-		$this->urlGenerator
560
-			->expects($this->once())
561
-			->method('linkToRoute')
562
-			->with(
563
-				'core.login.showLoginForm',
564
-				[
565
-					'redirect_url' => 'nextcloud/index.php/apps/specialapp',
566
-				]
567
-			)
568
-			->willReturn('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp');
569
-		$this->logger
570
-			->expects($this->once())
571
-			->method('debug');
572
-		$response = $this->middleware->afterException(
573
-			$this->controller,
574
-			'test',
575
-			new NotLoggedInException()
576
-		);
577
-		$expected = new RedirectResponse('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp');
578
-		$this->assertEquals($expected, $response);
579
-	}
580
-
581
-	public function testAfterExceptionRedirectsToWebRootAfterStrictCookieFail(): void {
582
-		$this->request = new Request(
583
-			[
584
-				'server' => [
585
-					'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
586
-					'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp',
587
-				],
588
-			],
589
-			$this->createMock(IRequestId::class),
590
-			$this->createMock(IConfig::class)
591
-		);
592
-
593
-		$this->middleware = $this->getMiddleware(false, false, false);
594
-		$response = $this->middleware->afterException(
595
-			$this->controller,
596
-			'test',
597
-			new StrictCookieMissingException()
598
-		);
599
-
600
-		$expected = new RedirectResponse(\OC::$WEBROOT . '/');
601
-		$this->assertEquals($expected, $response);
602
-	}
603
-
604
-
605
-	/**
606
-	 * @return array
607
-	 */
608
-	public static function exceptionProvider(): array {
609
-		return [
610
-			[
611
-				new AppNotEnabledException(),
612
-			],
613
-			[
614
-				new CrossSiteRequestForgeryException(),
615
-			],
616
-			[
617
-				new NotAdminException(''),
618
-			],
619
-		];
620
-	}
621
-
622
-	/**
623
-	 * @param SecurityException $exception
624
-	 */
625
-	#[\PHPUnit\Framework\Attributes\DataProvider('exceptionProvider')]
626
-	public function testAfterExceptionReturnsTemplateResponse(SecurityException $exception): void {
627
-		$this->request = new Request(
628
-			[
629
-				'server'
630
-					=> [
631
-						'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
632
-						'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp'
633
-					]
634
-			],
635
-			$this->createMock(IRequestId::class),
636
-			$this->createMock(IConfig::class)
637
-		);
638
-		$this->middleware = $this->getMiddleware(false, false, false);
639
-		$this->logger
640
-			->expects($this->once())
641
-			->method('debug');
642
-		$response = $this->middleware->afterException(
643
-			$this->controller,
644
-			'test',
645
-			$exception
646
-		);
647
-		$expected = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest');
648
-		$expected->setStatus($exception->getCode());
649
-		$this->assertEquals($expected, $response);
650
-	}
651
-
652
-	public function testAfterAjaxExceptionReturnsJSONError(): void {
653
-		$response = $this->middleware->afterException($this->controller, 'test',
654
-			$this->secAjaxException);
655
-
656
-		$this->assertTrue($response instanceof JSONResponse);
657
-	}
658
-
659
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataExAppRequired')]
660
-	public function testExAppRequired(string $method): void {
661
-		$middleware = $this->getMiddleware(true, false, false);
662
-		$this->reader->reflect($this->controller, $method);
663
-
664
-		$session = $this->createMock(ISession::class);
665
-		$session->method('get')->with('app_api')->willReturn(true);
666
-		$this->userSession->method('getSession')->willReturn($session);
667
-
668
-		$this->request->expects($this->once())
669
-			->method('passesStrictCookieCheck')
670
-			->willReturn(true);
671
-		$this->request->expects($this->once())
672
-			->method('passesCSRFCheck')
673
-			->willReturn(true);
674
-
675
-		$middleware->beforeController($this->controller, $method);
676
-	}
677
-
678
-	#[\PHPUnit\Framework\Attributes\DataProvider('dataExAppRequired')]
679
-	public function testExAppRequiredError(string $method): void {
680
-		$middleware = $this->getMiddleware(true, false, false, false);
681
-		$this->reader->reflect($this->controller, $method);
682
-
683
-		$session = $this->createMock(ISession::class);
684
-		$session->method('get')->with('app_api')->willReturn(false);
685
-		$this->userSession->method('getSession')->willReturn($session);
686
-
687
-		$this->expectException(ExAppRequiredException::class);
688
-		$middleware->beforeController($this->controller, $method);
689
-	}
48
+    private SecurityMiddleware $middleware;
49
+    private ControllerMethodReflector $reader;
50
+    private SecurityMiddlewareController $controller;
51
+    private SecurityException $secAjaxException;
52
+    private IRequest|MockObject $request;
53
+    private MiddlewareUtils $middlewareUtils;
54
+    private LoggerInterface&MockObject $logger;
55
+    private INavigationManager&MockObject $navigationManager;
56
+    private IURLGenerator&MockObject $urlGenerator;
57
+    private IAppManager&MockObject $appManager;
58
+    private IL10N&MockObject $l10n;
59
+    private IUserSession&MockObject $userSession;
60
+    private AuthorizedGroupMapper&MockObject $authorizedGroupMapper;
61
+
62
+    protected function setUp(): void {
63
+        parent::setUp();
64
+
65
+        $this->authorizedGroupMapper = $this->createMock(AuthorizedGroupMapper::class);
66
+        $this->userSession = $this->createMock(Session::class);
67
+        $user = $this->createMock(IUser::class);
68
+        $user->method('getUID')->willReturn('test');
69
+        $this->userSession->method('getUser')->willReturn($user);
70
+        $this->request = $this->createMock(IRequest::class);
71
+        $this->controller = new SecurityMiddlewareController(
72
+            'test',
73
+            $this->request
74
+        );
75
+        $this->reader = new ControllerMethodReflector();
76
+        $this->logger = $this->createMock(LoggerInterface::class);
77
+        $this->navigationManager = $this->createMock(INavigationManager::class);
78
+        $this->urlGenerator = $this->createMock(IURLGenerator::class);
79
+        $this->l10n = $this->createMock(IL10N::class);
80
+        $this->middlewareUtils = new MiddlewareUtils($this->reader, $this->logger);
81
+        $this->middleware = $this->getMiddleware(true, true, false);
82
+        $this->secAjaxException = new SecurityException('hey', true);
83
+    }
84
+
85
+    private function getMiddleware(bool $isLoggedIn, bool $isAdminUser, bool $isSubAdmin, bool $isAppEnabledForUser = true): SecurityMiddleware {
86
+        $this->appManager = $this->createMock(IAppManager::class);
87
+        $this->appManager->expects($this->any())
88
+            ->method('isEnabledForUser')
89
+            ->willReturn($isAppEnabledForUser);
90
+        $remoteIpAddress = $this->createMock(IRemoteAddress::class);
91
+        $remoteIpAddress->method('allowsAdminActions')->willReturn(true);
92
+
93
+        $groupManager = $this->createMock(IGroupManager::class);
94
+        $groupManager->method('isAdmin')
95
+            ->willReturn($isAdminUser);
96
+        $subAdminManager = $this->createMock(ISubAdmin::class);
97
+        $subAdminManager->method('isSubAdmin')
98
+            ->willReturn($isSubAdmin);
99
+
100
+        return new SecurityMiddleware(
101
+            $this->request,
102
+            $this->middlewareUtils,
103
+            $this->navigationManager,
104
+            $this->urlGenerator,
105
+            $this->logger,
106
+            'files',
107
+            $isLoggedIn,
108
+            $groupManager,
109
+            $subAdminManager,
110
+            $this->appManager,
111
+            $this->l10n,
112
+            $this->authorizedGroupMapper,
113
+            $this->userSession,
114
+            $remoteIpAddress
115
+        );
116
+    }
117
+
118
+    public static function dataNoCSRFRequiredPublicPage(): array {
119
+        return [
120
+            ['testAnnotationNoCSRFRequiredPublicPage'],
121
+            ['testAnnotationNoCSRFRequiredAttributePublicPage'],
122
+            ['testAnnotationPublicPageAttributeNoCSRFRequired'],
123
+            ['testAttributeNoCSRFRequiredPublicPage'],
124
+        ];
125
+    }
126
+
127
+    public static function dataPublicPage(): array {
128
+        return [
129
+            ['testAnnotationPublicPage'],
130
+            ['testAttributePublicPage'],
131
+        ];
132
+    }
133
+
134
+    public static function dataNoCSRFRequired(): array {
135
+        return [
136
+            ['testAnnotationNoCSRFRequired'],
137
+            ['testAttributeNoCSRFRequired'],
138
+        ];
139
+    }
140
+
141
+    public static function dataPublicPageStrictCookieRequired(): array {
142
+        return [
143
+            ['testAnnotationPublicPageStrictCookieRequired'],
144
+            ['testAnnotationStrictCookieRequiredAttributePublicPage'],
145
+            ['testAnnotationPublicPageAttributeStrictCookiesRequired'],
146
+            ['testAttributePublicPageStrictCookiesRequired'],
147
+        ];
148
+    }
149
+
150
+    public static function dataNoCSRFRequiredPublicPageStrictCookieRequired(): array {
151
+        return [
152
+            ['testAnnotationNoCSRFRequiredPublicPageStrictCookieRequired'],
153
+            ['testAttributeNoCSRFRequiredPublicPageStrictCookiesRequired'],
154
+        ];
155
+    }
156
+
157
+    public static function dataNoAdminRequiredNoCSRFRequired(): array {
158
+        return [
159
+            ['testAnnotationNoAdminRequiredNoCSRFRequired'],
160
+            ['testAttributeNoAdminRequiredNoCSRFRequired'],
161
+        ];
162
+    }
163
+
164
+    public static function dataNoAdminRequiredNoCSRFRequiredPublicPage(): array {
165
+        return [
166
+            ['testAnnotationNoAdminRequiredNoCSRFRequiredPublicPage'],
167
+            ['testAttributeNoAdminRequiredNoCSRFRequiredPublicPage'],
168
+        ];
169
+    }
170
+
171
+    public static function dataNoCSRFRequiredSubAdminRequired(): array {
172
+        return [
173
+            ['testAnnotationNoCSRFRequiredSubAdminRequired'],
174
+            ['testAnnotationNoCSRFRequiredAttributeSubAdminRequired'],
175
+            ['testAnnotationSubAdminRequiredAttributeNoCSRFRequired'],
176
+            ['testAttributeNoCSRFRequiredSubAdminRequired'],
177
+        ];
178
+    }
179
+
180
+    public static function dataExAppRequired(): array {
181
+        return [
182
+            ['testAnnotationExAppRequired'],
183
+            ['testAttributeExAppRequired'],
184
+        ];
185
+    }
186
+
187
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
188
+    public function testSetNavigationEntry(string $method): void {
189
+        $this->navigationManager->expects($this->once())
190
+            ->method('setActiveEntry')
191
+            ->with($this->equalTo('files'));
192
+
193
+        $this->reader->reflect($this->controller, $method);
194
+        $this->middleware->beforeController($this->controller, $method);
195
+    }
196
+
197
+
198
+    /**
199
+     * @param string $method
200
+     * @param string $test
201
+     */
202
+    private function ajaxExceptionStatus($method, $test, $status) {
203
+        $isLoggedIn = false;
204
+        $isAdminUser = false;
205
+
206
+        // isAdminUser requires isLoggedIn call to return true
207
+        if ($test === 'isAdminUser') {
208
+            $isLoggedIn = true;
209
+        }
210
+
211
+        $sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false);
212
+
213
+        try {
214
+            $this->reader->reflect($this->controller, $method);
215
+            $sec->beforeController($this->controller, $method);
216
+        } catch (SecurityException $ex) {
217
+            $this->assertEquals($status, $ex->getCode());
218
+        }
219
+
220
+        // add assertion if everything should work fine otherwise phpunit will
221
+        // complain
222
+        if ($status === 0) {
223
+            $this->addToAssertionCount(1);
224
+        }
225
+    }
226
+
227
+    public function testAjaxStatusLoggedInCheck(): void {
228
+        $this->ajaxExceptionStatus(
229
+            'testNoAnnotationNorAttribute',
230
+            'isLoggedIn',
231
+            Http::STATUS_UNAUTHORIZED
232
+        );
233
+    }
234
+
235
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')]
236
+    public function testAjaxNotAdminCheck(string $method): void {
237
+        $this->ajaxExceptionStatus(
238
+            $method,
239
+            'isAdminUser',
240
+            Http::STATUS_FORBIDDEN
241
+        );
242
+    }
243
+
244
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')]
245
+    public function testAjaxStatusCSRFCheck(string $method): void {
246
+        $this->ajaxExceptionStatus(
247
+            $method,
248
+            'passesCSRFCheck',
249
+            Http::STATUS_PRECONDITION_FAILED
250
+        );
251
+    }
252
+
253
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
254
+    public function testAjaxStatusAllGood(string $method): void {
255
+        $this->ajaxExceptionStatus(
256
+            $method,
257
+            'isLoggedIn',
258
+            0
259
+        );
260
+        $this->ajaxExceptionStatus(
261
+            $method,
262
+            'isAdminUser',
263
+            0
264
+        );
265
+        $this->ajaxExceptionStatus(
266
+            $method,
267
+            'passesCSRFCheck',
268
+            0
269
+        );
270
+    }
271
+
272
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
273
+    public function testNoChecks(string $method): void {
274
+        $this->request->expects($this->never())
275
+            ->method('passesCSRFCheck')
276
+            ->willReturn(false);
277
+
278
+        $sec = $this->getMiddleware(false, false, false);
279
+
280
+        $this->reader->reflect($this->controller, $method);
281
+        $sec->beforeController($this->controller, $method);
282
+    }
283
+
284
+    /**
285
+     * @param string $method
286
+     * @param string $expects
287
+     */
288
+    private function securityCheck($method, $expects, $shouldFail = false) {
289
+        // admin check requires login
290
+        if ($expects === 'isAdminUser') {
291
+            $isLoggedIn = true;
292
+            $isAdminUser = !$shouldFail;
293
+        } else {
294
+            $isLoggedIn = !$shouldFail;
295
+            $isAdminUser = false;
296
+        }
297
+
298
+        $sec = $this->getMiddleware($isLoggedIn, $isAdminUser, false);
299
+
300
+        if ($shouldFail) {
301
+            $this->expectException(SecurityException::class);
302
+        } else {
303
+            $this->addToAssertionCount(1);
304
+        }
305
+
306
+        $this->reader->reflect($this->controller, $method);
307
+        $sec->beforeController($this->controller, $method);
308
+    }
309
+
310
+
311
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')]
312
+    public function testCsrfCheck(string $method): void {
313
+        $this->expectException(CrossSiteRequestForgeryException::class);
314
+
315
+        $this->request->expects($this->once())
316
+            ->method('passesCSRFCheck')
317
+            ->willReturn(false);
318
+        $this->request->expects($this->once())
319
+            ->method('passesStrictCookieCheck')
320
+            ->willReturn(true);
321
+        $this->reader->reflect($this->controller, $method);
322
+        $this->middleware->beforeController($this->controller, $method);
323
+    }
324
+
325
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
326
+    public function testNoCsrfCheck(string $method): void {
327
+        $this->request->expects($this->never())
328
+            ->method('passesCSRFCheck')
329
+            ->willReturn(false);
330
+
331
+        $this->reader->reflect($this->controller, $method);
332
+        $this->middleware->beforeController($this->controller, $method);
333
+    }
334
+
335
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')]
336
+    public function testPassesCsrfCheck(string $method): void {
337
+        $this->request->expects($this->once())
338
+            ->method('passesCSRFCheck')
339
+            ->willReturn(true);
340
+        $this->request->expects($this->once())
341
+            ->method('passesStrictCookieCheck')
342
+            ->willReturn(true);
343
+
344
+        $this->reader->reflect($this->controller, $method);
345
+        $this->middleware->beforeController($this->controller, $method);
346
+    }
347
+
348
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPage')]
349
+    public function testFailCsrfCheck(string $method): void {
350
+        $this->expectException(CrossSiteRequestForgeryException::class);
351
+
352
+        $this->request->expects($this->once())
353
+            ->method('passesCSRFCheck')
354
+            ->willReturn(false);
355
+        $this->request->expects($this->once())
356
+            ->method('passesStrictCookieCheck')
357
+            ->willReturn(true);
358
+
359
+        $this->reader->reflect($this->controller, $method);
360
+        $this->middleware->beforeController($this->controller, $method);
361
+    }
362
+
363
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataPublicPageStrictCookieRequired')]
364
+    public function testStrictCookieRequiredCheck(string $method): void {
365
+        $this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException::class);
366
+
367
+        $this->request->expects($this->never())
368
+            ->method('passesCSRFCheck');
369
+        $this->request->expects($this->once())
370
+            ->method('passesStrictCookieCheck')
371
+            ->willReturn(false);
372
+
373
+        $this->reader->reflect($this->controller, $method);
374
+        $this->middleware->beforeController($this->controller, $method);
375
+    }
376
+
377
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPage')]
378
+    public function testNoStrictCookieRequiredCheck(string $method): void {
379
+        $this->request->expects($this->never())
380
+            ->method('passesStrictCookieCheck')
381
+            ->willReturn(false);
382
+
383
+        $this->reader->reflect($this->controller, $method);
384
+        $this->middleware->beforeController($this->controller, $method);
385
+    }
386
+
387
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredPublicPageStrictCookieRequired')]
388
+    public function testPassesStrictCookieRequiredCheck(string $method): void {
389
+        $this->request
390
+            ->expects($this->once())
391
+            ->method('passesStrictCookieCheck')
392
+            ->willReturn(true);
393
+
394
+        $this->reader->reflect($this->controller, $method);
395
+        $this->middleware->beforeController($this->controller, $method);
396
+    }
397
+
398
+    public static function dataCsrfOcsController(): array {
399
+        return [
400
+            [NormalController::class, false, false, true],
401
+            [NormalController::class, false,  true, true],
402
+            [NormalController::class,  true, false, true],
403
+            [NormalController::class,  true,  true, true],
404
+
405
+            [OCSController::class, false, false,  true],
406
+            [OCSController::class, false,  true, false],
407
+            [OCSController::class,  true, false, false],
408
+            [OCSController::class,  true,  true, false],
409
+        ];
410
+    }
411
+
412
+    /**
413
+     * @param string $controllerClass
414
+     * @param bool $hasOcsApiHeader
415
+     * @param bool $hasBearerAuth
416
+     * @param bool $exception
417
+     */
418
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataCsrfOcsController')]
419
+    public function testCsrfOcsController(string $controllerClass, bool $hasOcsApiHeader, bool $hasBearerAuth, bool $exception): void {
420
+        $this->request
421
+            ->method('getHeader')
422
+            ->willReturnCallback(function ($header) use ($hasOcsApiHeader, $hasBearerAuth) {
423
+                if ($header === 'OCS-APIREQUEST' && $hasOcsApiHeader) {
424
+                    return 'true';
425
+                }
426
+                if ($header === 'Authorization' && $hasBearerAuth) {
427
+                    return 'Bearer TOKEN!';
428
+                }
429
+                return '';
430
+            });
431
+        $this->request->expects($this->once())
432
+            ->method('passesStrictCookieCheck')
433
+            ->willReturn(true);
434
+
435
+        $controller = new $controllerClass('test', $this->request);
436
+
437
+        try {
438
+            $this->middleware->beforeController($controller, 'foo');
439
+            $this->assertFalse($exception);
440
+        } catch (CrossSiteRequestForgeryException $e) {
441
+            $this->assertTrue($exception);
442
+        }
443
+    }
444
+
445
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')]
446
+    public function testLoggedInCheck(string $method): void {
447
+        $this->securityCheck($method, 'isLoggedIn');
448
+    }
449
+
450
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')]
451
+    public function testFailLoggedInCheck(string $method): void {
452
+        $this->securityCheck($method, 'isLoggedIn', true);
453
+    }
454
+
455
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')]
456
+    public function testIsAdminCheck(string $method): void {
457
+        $this->securityCheck($method, 'isAdminUser');
458
+    }
459
+
460
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')]
461
+    public function testIsNotSubAdminCheck(string $method): void {
462
+        $this->reader->reflect($this->controller, $method);
463
+        $sec = $this->getMiddleware(true, false, false);
464
+
465
+        $this->expectException(SecurityException::class);
466
+        $sec->beforeController($this->controller, $method);
467
+    }
468
+
469
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')]
470
+    public function testIsSubAdminCheck(string $method): void {
471
+        $this->reader->reflect($this->controller, $method);
472
+        $sec = $this->getMiddleware(true, false, true);
473
+
474
+        $sec->beforeController($this->controller, $method);
475
+        $this->addToAssertionCount(1);
476
+    }
477
+
478
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequiredSubAdminRequired')]
479
+    public function testIsSubAdminAndAdminCheck(string $method): void {
480
+        $this->reader->reflect($this->controller, $method);
481
+        $sec = $this->getMiddleware(true, true, true);
482
+
483
+        $sec->beforeController($this->controller, $method);
484
+        $this->addToAssertionCount(1);
485
+    }
486
+
487
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoCSRFRequired')]
488
+    public function testFailIsAdminCheck(string $method): void {
489
+        $this->securityCheck($method, 'isAdminUser', true);
490
+    }
491
+
492
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequiredPublicPage')]
493
+    public function testRestrictedAppLoggedInPublicPage(string $method): void {
494
+        $middleware = $this->getMiddleware(true, false, false);
495
+        $this->reader->reflect($this->controller, $method);
496
+
497
+        $this->appManager->method('getAppPath')
498
+            ->with('files')
499
+            ->willReturn('foo');
500
+
501
+        $this->appManager->method('isEnabledForUser')
502
+            ->with('files')
503
+            ->willReturn(false);
504
+
505
+        $middleware->beforeController($this->controller, $method);
506
+        $this->addToAssertionCount(1);
507
+    }
508
+
509
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequiredPublicPage')]
510
+    public function testRestrictedAppNotLoggedInPublicPage(string $method): void {
511
+        $middleware = $this->getMiddleware(false, false, false);
512
+        $this->reader->reflect($this->controller, $method);
513
+
514
+        $this->appManager->method('getAppPath')
515
+            ->with('files')
516
+            ->willReturn('foo');
517
+
518
+        $this->appManager->method('isEnabledForUser')
519
+            ->with('files')
520
+            ->willReturn(false);
521
+
522
+        $middleware->beforeController($this->controller, $method);
523
+        $this->addToAssertionCount(1);
524
+    }
525
+
526
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataNoAdminRequiredNoCSRFRequired')]
527
+    public function testRestrictedAppLoggedIn(string $method): void {
528
+        $middleware = $this->getMiddleware(true, false, false, false);
529
+        $this->reader->reflect($this->controller, $method);
530
+
531
+        $this->appManager->method('getAppPath')
532
+            ->with('files')
533
+            ->willReturn('foo');
534
+
535
+        $this->expectException(AppNotEnabledException::class);
536
+        $middleware->beforeController($this->controller, $method);
537
+    }
538
+
539
+
540
+    public function testAfterExceptionNotCaughtThrowsItAgain(): void {
541
+        $ex = new \Exception();
542
+        $this->expectException(\Exception::class);
543
+        $this->middleware->afterException($this->controller, 'test', $ex);
544
+    }
545
+
546
+    public function testAfterExceptionReturnsRedirectForNotLoggedInUser(): void {
547
+        $this->request = new Request(
548
+            [
549
+                'server'
550
+                    => [
551
+                        'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
552
+                        'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp'
553
+                    ]
554
+            ],
555
+            $this->createMock(IRequestId::class),
556
+            $this->createMock(IConfig::class)
557
+        );
558
+        $this->middleware = $this->getMiddleware(false, false, false);
559
+        $this->urlGenerator
560
+            ->expects($this->once())
561
+            ->method('linkToRoute')
562
+            ->with(
563
+                'core.login.showLoginForm',
564
+                [
565
+                    'redirect_url' => 'nextcloud/index.php/apps/specialapp',
566
+                ]
567
+            )
568
+            ->willReturn('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp');
569
+        $this->logger
570
+            ->expects($this->once())
571
+            ->method('debug');
572
+        $response = $this->middleware->afterException(
573
+            $this->controller,
574
+            'test',
575
+            new NotLoggedInException()
576
+        );
577
+        $expected = new RedirectResponse('http://localhost/nextcloud/index.php/login?redirect_url=nextcloud/index.php/apps/specialapp');
578
+        $this->assertEquals($expected, $response);
579
+    }
580
+
581
+    public function testAfterExceptionRedirectsToWebRootAfterStrictCookieFail(): void {
582
+        $this->request = new Request(
583
+            [
584
+                'server' => [
585
+                    'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
586
+                    'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp',
587
+                ],
588
+            ],
589
+            $this->createMock(IRequestId::class),
590
+            $this->createMock(IConfig::class)
591
+        );
592
+
593
+        $this->middleware = $this->getMiddleware(false, false, false);
594
+        $response = $this->middleware->afterException(
595
+            $this->controller,
596
+            'test',
597
+            new StrictCookieMissingException()
598
+        );
599
+
600
+        $expected = new RedirectResponse(\OC::$WEBROOT . '/');
601
+        $this->assertEquals($expected, $response);
602
+    }
603
+
604
+
605
+    /**
606
+     * @return array
607
+     */
608
+    public static function exceptionProvider(): array {
609
+        return [
610
+            [
611
+                new AppNotEnabledException(),
612
+            ],
613
+            [
614
+                new CrossSiteRequestForgeryException(),
615
+            ],
616
+            [
617
+                new NotAdminException(''),
618
+            ],
619
+        ];
620
+    }
621
+
622
+    /**
623
+     * @param SecurityException $exception
624
+     */
625
+    #[\PHPUnit\Framework\Attributes\DataProvider('exceptionProvider')]
626
+    public function testAfterExceptionReturnsTemplateResponse(SecurityException $exception): void {
627
+        $this->request = new Request(
628
+            [
629
+                'server'
630
+                    => [
631
+                        'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
632
+                        'REQUEST_URI' => 'nextcloud/index.php/apps/specialapp'
633
+                    ]
634
+            ],
635
+            $this->createMock(IRequestId::class),
636
+            $this->createMock(IConfig::class)
637
+        );
638
+        $this->middleware = $this->getMiddleware(false, false, false);
639
+        $this->logger
640
+            ->expects($this->once())
641
+            ->method('debug');
642
+        $response = $this->middleware->afterException(
643
+            $this->controller,
644
+            'test',
645
+            $exception
646
+        );
647
+        $expected = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest');
648
+        $expected->setStatus($exception->getCode());
649
+        $this->assertEquals($expected, $response);
650
+    }
651
+
652
+    public function testAfterAjaxExceptionReturnsJSONError(): void {
653
+        $response = $this->middleware->afterException($this->controller, 'test',
654
+            $this->secAjaxException);
655
+
656
+        $this->assertTrue($response instanceof JSONResponse);
657
+    }
658
+
659
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataExAppRequired')]
660
+    public function testExAppRequired(string $method): void {
661
+        $middleware = $this->getMiddleware(true, false, false);
662
+        $this->reader->reflect($this->controller, $method);
663
+
664
+        $session = $this->createMock(ISession::class);
665
+        $session->method('get')->with('app_api')->willReturn(true);
666
+        $this->userSession->method('getSession')->willReturn($session);
667
+
668
+        $this->request->expects($this->once())
669
+            ->method('passesStrictCookieCheck')
670
+            ->willReturn(true);
671
+        $this->request->expects($this->once())
672
+            ->method('passesCSRFCheck')
673
+            ->willReturn(true);
674
+
675
+        $middleware->beforeController($this->controller, $method);
676
+    }
677
+
678
+    #[\PHPUnit\Framework\Attributes\DataProvider('dataExAppRequired')]
679
+    public function testExAppRequiredError(string $method): void {
680
+        $middleware = $this->getMiddleware(true, false, false, false);
681
+        $this->reader->reflect($this->controller, $method);
682
+
683
+        $session = $this->createMock(ISession::class);
684
+        $session->method('get')->with('app_api')->willReturn(false);
685
+        $this->userSession->method('getSession')->willReturn($session);
686
+
687
+        $this->expectException(ExAppRequiredException::class);
688
+        $middleware->beforeController($this->controller, $method);
689
+    }
690 690
 }
Please login to merge, or discard this patch.
Spacing   +11 added lines, -11 removed lines patch added patch discarded remove patch
@@ -49,7 +49,7 @@  discard block
 block discarded – undo
49 49
 	private ControllerMethodReflector $reader;
50 50
 	private SecurityMiddlewareController $controller;
51 51
 	private SecurityException $secAjaxException;
52
-	private IRequest|MockObject $request;
52
+	private IRequest | MockObject $request;
53 53
 	private MiddlewareUtils $middlewareUtils;
54 54
 	private LoggerInterface&MockObject $logger;
55 55
 	private INavigationManager&MockObject $navigationManager;
@@ -398,14 +398,14 @@  discard block
 block discarded – undo
398 398
 	public static function dataCsrfOcsController(): array {
399 399
 		return [
400 400
 			[NormalController::class, false, false, true],
401
-			[NormalController::class, false,  true, true],
402
-			[NormalController::class,  true, false, true],
403
-			[NormalController::class,  true,  true, true],
404
-
405
-			[OCSController::class, false, false,  true],
406
-			[OCSController::class, false,  true, false],
407
-			[OCSController::class,  true, false, false],
408
-			[OCSController::class,  true,  true, false],
401
+			[NormalController::class, false, true, true],
402
+			[NormalController::class, true, false, true],
403
+			[NormalController::class, true, true, true],
404
+
405
+			[OCSController::class, false, false, true],
406
+			[OCSController::class, false, true, false],
407
+			[OCSController::class, true, false, false],
408
+			[OCSController::class, true, true, false],
409 409
 		];
410 410
 	}
411 411
 
@@ -419,7 +419,7 @@  discard block
 block discarded – undo
419 419
 	public function testCsrfOcsController(string $controllerClass, bool $hasOcsApiHeader, bool $hasBearerAuth, bool $exception): void {
420 420
 		$this->request
421 421
 			->method('getHeader')
422
-			->willReturnCallback(function ($header) use ($hasOcsApiHeader, $hasBearerAuth) {
422
+			->willReturnCallback(function($header) use ($hasOcsApiHeader, $hasBearerAuth) {
423 423
 				if ($header === 'OCS-APIREQUEST' && $hasOcsApiHeader) {
424 424
 					return 'true';
425 425
 				}
@@ -597,7 +597,7 @@  discard block
 block discarded – undo
597 597
 			new StrictCookieMissingException()
598 598
 		);
599 599
 
600
-		$expected = new RedirectResponse(\OC::$WEBROOT . '/');
600
+		$expected = new RedirectResponse(\OC::$WEBROOT.'/');
601 601
 		$this->assertEquals($expected, $response);
602 602
 	}
603 603
 
Please login to merge, or discard this patch.
tests/lib/AppFramework/Middleware/Security/SameSiteCookieMiddlewareTest.php 1 patch
Indentation   +79 added lines, -79 removed lines patch added patch discarded remove patch
@@ -22,114 +22,114 @@
 block discarded – undo
22 22
 use Test\TestCase;
23 23
 
24 24
 class HasAnnotationController extends Controller {
25
-	#[NoSameSiteCookieRequired]
26
-	public function foo(): Response {
27
-		return new Response();
28
-	}
25
+    #[NoSameSiteCookieRequired]
26
+    public function foo(): Response {
27
+        return new Response();
28
+    }
29 29
 }
30 30
 
31 31
 class NoAnnotationController extends Controller {
32
-	public function foo(): Response {
33
-		return new Response();
34
-	}
32
+    public function foo(): Response {
33
+        return new Response();
34
+    }
35 35
 }
36 36
 
37 37
 class SameSiteCookieMiddlewareTest extends TestCase {
38
-	private SameSiteCookieMiddleware $middleware;
39
-	private Request&MockObject $request;
40
-	private ControllerMethodReflector&MockObject $reflector;
41
-	private LoggerInterface&MockObject $logger;
38
+    private SameSiteCookieMiddleware $middleware;
39
+    private Request&MockObject $request;
40
+    private ControllerMethodReflector&MockObject $reflector;
41
+    private LoggerInterface&MockObject $logger;
42 42
 
43
-	protected function setUp(): void {
44
-		parent::setUp();
43
+    protected function setUp(): void {
44
+        parent::setUp();
45 45
 
46
-		$this->request = $this->createMock(Request::class);
47
-		$this->logger = $this->createMock(LoggerInterface::class);
48
-		$this->reflector = $this->createMock(ControllerMethodReflector::class);
49
-		$this->middleware = new SameSiteCookieMiddleware($this->request, new MiddlewareUtils($this->reflector, $this->logger));
50
-	}
46
+        $this->request = $this->createMock(Request::class);
47
+        $this->logger = $this->createMock(LoggerInterface::class);
48
+        $this->reflector = $this->createMock(ControllerMethodReflector::class);
49
+        $this->middleware = new SameSiteCookieMiddleware($this->request, new MiddlewareUtils($this->reflector, $this->logger));
50
+    }
51 51
 
52
-	public function testBeforeControllerNoIndex(): void {
53
-		$this->request->method('getScriptName')
54
-			->willReturn('/ocs/v2.php');
52
+    public function testBeforeControllerNoIndex(): void {
53
+        $this->request->method('getScriptName')
54
+            ->willReturn('/ocs/v2.php');
55 55
 
56
-		$this->middleware->beforeController(new NoAnnotationController('foo', $this->request), 'foo');
57
-		$this->addToAssertionCount(1);
58
-	}
56
+        $this->middleware->beforeController(new NoAnnotationController('foo', $this->request), 'foo');
57
+        $this->addToAssertionCount(1);
58
+    }
59 59
 
60
-	public function testBeforeControllerIndexHasAnnotation(): void {
61
-		$this->request->method('getScriptName')
62
-			->willReturn('/index.php');
60
+    public function testBeforeControllerIndexHasAnnotation(): void {
61
+        $this->request->method('getScriptName')
62
+            ->willReturn('/index.php');
63 63
 
64
-		$this->reflector->method('hasAnnotation')
65
-			->with('NoSameSiteCookieRequired')
66
-			->willReturn(true);
64
+        $this->reflector->method('hasAnnotation')
65
+            ->with('NoSameSiteCookieRequired')
66
+            ->willReturn(true);
67 67
 
68
-		$this->middleware->beforeController(new HasAnnotationController('foo', $this->request), 'foo');
69
-		$this->addToAssertionCount(1);
70
-	}
68
+        $this->middleware->beforeController(new HasAnnotationController('foo', $this->request), 'foo');
69
+        $this->addToAssertionCount(1);
70
+    }
71 71
 
72
-	public function testBeforeControllerIndexNoAnnotationPassingCheck(): void {
73
-		$this->request->method('getScriptName')
74
-			->willReturn('/index.php');
72
+    public function testBeforeControllerIndexNoAnnotationPassingCheck(): void {
73
+        $this->request->method('getScriptName')
74
+            ->willReturn('/index.php');
75 75
 
76
-		$this->reflector->method('hasAnnotation')
77
-			->with('NoSameSiteCookieRequired')
78
-			->willReturn(false);
76
+        $this->reflector->method('hasAnnotation')
77
+            ->with('NoSameSiteCookieRequired')
78
+            ->willReturn(false);
79 79
 
80
-		$this->request->method('passesLaxCookieCheck')
81
-			->willReturn(true);
80
+        $this->request->method('passesLaxCookieCheck')
81
+            ->willReturn(true);
82 82
 
83
-		$this->middleware->beforeController(new NoAnnotationController('foo', $this->request), 'foo');
84
-		$this->addToAssertionCount(1);
85
-	}
83
+        $this->middleware->beforeController(new NoAnnotationController('foo', $this->request), 'foo');
84
+        $this->addToAssertionCount(1);
85
+    }
86 86
 
87
-	public function testBeforeControllerIndexNoAnnotationFailingCheck(): void {
88
-		$this->expectException(LaxSameSiteCookieFailedException::class);
87
+    public function testBeforeControllerIndexNoAnnotationFailingCheck(): void {
88
+        $this->expectException(LaxSameSiteCookieFailedException::class);
89 89
 
90
-		$this->request->method('getScriptName')
91
-			->willReturn('/index.php');
90
+        $this->request->method('getScriptName')
91
+            ->willReturn('/index.php');
92 92
 
93
-		$this->reflector->method('hasAnnotation')
94
-			->with('NoSameSiteCookieRequired')
95
-			->willReturn(false);
93
+        $this->reflector->method('hasAnnotation')
94
+            ->with('NoSameSiteCookieRequired')
95
+            ->willReturn(false);
96 96
 
97
-		$this->request->method('passesLaxCookieCheck')
98
-			->willReturn(false);
97
+        $this->request->method('passesLaxCookieCheck')
98
+            ->willReturn(false);
99 99
 
100
-		$this->middleware->beforeController(new NoAnnotationController('foo', $this->request), 'foo');
101
-	}
100
+        $this->middleware->beforeController(new NoAnnotationController('foo', $this->request), 'foo');
101
+    }
102 102
 
103
-	public function testAfterExceptionNoLaxCookie(): void {
104
-		$ex = new SecurityException();
103
+    public function testAfterExceptionNoLaxCookie(): void {
104
+        $ex = new SecurityException();
105 105
 
106
-		try {
107
-			$this->middleware->afterException(new NoAnnotationController('foo', $this->request), 'foo', $ex);
108
-			$this->fail();
109
-		} catch (\Exception $e) {
110
-			$this->assertSame($ex, $e);
111
-		}
112
-	}
106
+        try {
107
+            $this->middleware->afterException(new NoAnnotationController('foo', $this->request), 'foo', $ex);
108
+            $this->fail();
109
+        } catch (\Exception $e) {
110
+            $this->assertSame($ex, $e);
111
+        }
112
+    }
113 113
 
114
-	public function testAfterExceptionLaxCookie(): void {
115
-		$ex = new LaxSameSiteCookieFailedException();
114
+    public function testAfterExceptionLaxCookie(): void {
115
+        $ex = new LaxSameSiteCookieFailedException();
116 116
 
117
-		$this->request->method('getRequestUri')
118
-			->willReturn('/myrequri');
117
+        $this->request->method('getRequestUri')
118
+            ->willReturn('/myrequri');
119 119
 
120
-		$middleware = $this->getMockBuilder(SameSiteCookieMiddleware::class)
121
-			->setConstructorArgs([$this->request, new MiddlewareUtils($this->reflector, $this->logger)])
122
-			->onlyMethods(['setSameSiteCookie'])
123
-			->getMock();
120
+        $middleware = $this->getMockBuilder(SameSiteCookieMiddleware::class)
121
+            ->setConstructorArgs([$this->request, new MiddlewareUtils($this->reflector, $this->logger)])
122
+            ->onlyMethods(['setSameSiteCookie'])
123
+            ->getMock();
124 124
 
125
-		$middleware->expects($this->once())
126
-			->method('setSameSiteCookie');
125
+        $middleware->expects($this->once())
126
+            ->method('setSameSiteCookie');
127 127
 
128
-		$resp = $middleware->afterException(new NoAnnotationController('foo', $this->request), 'foo', $ex);
128
+        $resp = $middleware->afterException(new NoAnnotationController('foo', $this->request), 'foo', $ex);
129 129
 
130
-		$this->assertSame(Http::STATUS_FOUND, $resp->getStatus());
130
+        $this->assertSame(Http::STATUS_FOUND, $resp->getStatus());
131 131
 
132
-		$headers = $resp->getHeaders();
133
-		$this->assertSame('/myrequri', $headers['Location']);
134
-	}
132
+        $headers = $resp->getHeaders();
133
+        $this->assertSame('/myrequri', $headers['Location']);
134
+    }
135 135
 }
Please login to merge, or discard this patch.
apps/theming/lib/Controller/ThemingController.php 1 patch
Indentation   +436 added lines, -436 removed lines patch added patch discarded remove patch
@@ -44,466 +44,466 @@
 block discarded – undo
44 44
  * @package OCA\Theming\Controller
45 45
  */
46 46
 class ThemingController extends Controller {
47
-	public const VALID_UPLOAD_KEYS = ['header', 'logo', 'logoheader', 'background', 'favicon'];
47
+    public const VALID_UPLOAD_KEYS = ['header', 'logo', 'logoheader', 'background', 'favicon'];
48 48
 
49
-	public function __construct(
50
-		string $appName,
51
-		IRequest $request,
52
-		private IConfig $config,
53
-		private IAppConfig $appConfig,
54
-		private ThemingDefaults $themingDefaults,
55
-		private IL10N $l10n,
56
-		private IURLGenerator $urlGenerator,
57
-		private IAppManager $appManager,
58
-		private ImageManager $imageManager,
59
-		private ThemesService $themesService,
60
-		private INavigationManager $navigationManager,
61
-	) {
62
-		parent::__construct($appName, $request);
63
-	}
49
+    public function __construct(
50
+        string $appName,
51
+        IRequest $request,
52
+        private IConfig $config,
53
+        private IAppConfig $appConfig,
54
+        private ThemingDefaults $themingDefaults,
55
+        private IL10N $l10n,
56
+        private IURLGenerator $urlGenerator,
57
+        private IAppManager $appManager,
58
+        private ImageManager $imageManager,
59
+        private ThemesService $themesService,
60
+        private INavigationManager $navigationManager,
61
+    ) {
62
+        parent::__construct($appName, $request);
63
+    }
64 64
 
65
-	/**
66
-	 * @throws NotPermittedException
67
-	 */
68
-	#[AuthorizedAdminSetting(settings: Admin::class)]
69
-	public function updateStylesheet(string $setting, string $value): DataResponse {
70
-		$value = trim($value);
71
-		$error = null;
72
-		$saved = false;
73
-		switch ($setting) {
74
-			case 'name':
75
-				if (strlen($value) > 250) {
76
-					$error = $this->l10n->t('The given name is too long');
77
-				}
78
-				break;
79
-			case 'url':
80
-				if (strlen($value) > 500) {
81
-					$error = $this->l10n->t('The given web address is too long');
82
-				}
83
-				if ($value !== '' && !$this->isValidUrl($value)) {
84
-					$error = $this->l10n->t('The given web address is not a valid URL');
85
-				}
86
-				break;
87
-			case 'legalNoticeUrl':
88
-				$setting = 'imprintUrl';
89
-				// no break
90
-			case 'imprintUrl':
91
-				if (strlen($value) > 500) {
92
-					$error = $this->l10n->t('The given legal notice address is too long');
93
-				}
94
-				if ($value !== '' && !$this->isValidUrl($value)) {
95
-					$error = $this->l10n->t('The given legal notice address is not a valid URL');
96
-				}
97
-				break;
98
-			case 'privacyPolicyUrl':
99
-				$setting = 'privacyUrl';
100
-				// no break
101
-			case 'privacyUrl':
102
-				if (strlen($value) > 500) {
103
-					$error = $this->l10n->t('The given privacy policy address is too long');
104
-				}
105
-				if ($value !== '' && !$this->isValidUrl($value)) {
106
-					$error = $this->l10n->t('The given privacy policy address is not a valid URL');
107
-				}
108
-				break;
109
-			case 'slogan':
110
-				if (strlen($value) > 500) {
111
-					$error = $this->l10n->t('The given slogan is too long');
112
-				}
113
-				break;
114
-			case 'primaryColor':
115
-				$setting = 'primary_color';
116
-				// no break
117
-			case 'primary_color':
118
-				if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
119
-					$error = $this->l10n->t('The given color is invalid');
120
-				}
121
-				break;
122
-			case 'backgroundColor':
123
-				$setting = 'background_color';
124
-				// no break
125
-			case 'background_color':
126
-				if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
127
-					$error = $this->l10n->t('The given color is invalid');
128
-				}
129
-				break;
130
-			case 'disableUserTheming':
131
-			case 'disable-user-theming':
132
-				if (!in_array($value, ['yes', 'true', 'no', 'false'])) {
133
-					$error = $this->l10n->t('%1$s should be true or false', ['disable-user-theming']);
134
-				} else {
135
-					$this->appConfig->setAppValueBool('disable-user-theming', $value === 'yes' || $value === 'true');
136
-					$saved = true;
137
-				}
138
-				break;
139
-			case 'backgroundMime':
140
-				if ($value !== 'backgroundColor') {
141
-					$error = $this->l10n->t('%1$s can only be set to %2$s through the API', ['backgroundMime', 'backgroundColor']);
142
-				}
143
-				break;
144
-			default:
145
-				$error = $this->l10n->t('Invalid setting key');
146
-		}
147
-		if ($error !== null) {
148
-			return new DataResponse([
149
-				'data' => [
150
-					'message' => $error,
151
-				],
152
-				'status' => 'error'
153
-			], Http::STATUS_BAD_REQUEST);
154
-		}
65
+    /**
66
+     * @throws NotPermittedException
67
+     */
68
+    #[AuthorizedAdminSetting(settings: Admin::class)]
69
+    public function updateStylesheet(string $setting, string $value): DataResponse {
70
+        $value = trim($value);
71
+        $error = null;
72
+        $saved = false;
73
+        switch ($setting) {
74
+            case 'name':
75
+                if (strlen($value) > 250) {
76
+                    $error = $this->l10n->t('The given name is too long');
77
+                }
78
+                break;
79
+            case 'url':
80
+                if (strlen($value) > 500) {
81
+                    $error = $this->l10n->t('The given web address is too long');
82
+                }
83
+                if ($value !== '' && !$this->isValidUrl($value)) {
84
+                    $error = $this->l10n->t('The given web address is not a valid URL');
85
+                }
86
+                break;
87
+            case 'legalNoticeUrl':
88
+                $setting = 'imprintUrl';
89
+                // no break
90
+            case 'imprintUrl':
91
+                if (strlen($value) > 500) {
92
+                    $error = $this->l10n->t('The given legal notice address is too long');
93
+                }
94
+                if ($value !== '' && !$this->isValidUrl($value)) {
95
+                    $error = $this->l10n->t('The given legal notice address is not a valid URL');
96
+                }
97
+                break;
98
+            case 'privacyPolicyUrl':
99
+                $setting = 'privacyUrl';
100
+                // no break
101
+            case 'privacyUrl':
102
+                if (strlen($value) > 500) {
103
+                    $error = $this->l10n->t('The given privacy policy address is too long');
104
+                }
105
+                if ($value !== '' && !$this->isValidUrl($value)) {
106
+                    $error = $this->l10n->t('The given privacy policy address is not a valid URL');
107
+                }
108
+                break;
109
+            case 'slogan':
110
+                if (strlen($value) > 500) {
111
+                    $error = $this->l10n->t('The given slogan is too long');
112
+                }
113
+                break;
114
+            case 'primaryColor':
115
+                $setting = 'primary_color';
116
+                // no break
117
+            case 'primary_color':
118
+                if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
119
+                    $error = $this->l10n->t('The given color is invalid');
120
+                }
121
+                break;
122
+            case 'backgroundColor':
123
+                $setting = 'background_color';
124
+                // no break
125
+            case 'background_color':
126
+                if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
127
+                    $error = $this->l10n->t('The given color is invalid');
128
+                }
129
+                break;
130
+            case 'disableUserTheming':
131
+            case 'disable-user-theming':
132
+                if (!in_array($value, ['yes', 'true', 'no', 'false'])) {
133
+                    $error = $this->l10n->t('%1$s should be true or false', ['disable-user-theming']);
134
+                } else {
135
+                    $this->appConfig->setAppValueBool('disable-user-theming', $value === 'yes' || $value === 'true');
136
+                    $saved = true;
137
+                }
138
+                break;
139
+            case 'backgroundMime':
140
+                if ($value !== 'backgroundColor') {
141
+                    $error = $this->l10n->t('%1$s can only be set to %2$s through the API', ['backgroundMime', 'backgroundColor']);
142
+                }
143
+                break;
144
+            default:
145
+                $error = $this->l10n->t('Invalid setting key');
146
+        }
147
+        if ($error !== null) {
148
+            return new DataResponse([
149
+                'data' => [
150
+                    'message' => $error,
151
+                ],
152
+                'status' => 'error'
153
+            ], Http::STATUS_BAD_REQUEST);
154
+        }
155 155
 
156
-		if (!$saved) {
157
-			$this->themingDefaults->set($setting, $value);
158
-		}
156
+        if (!$saved) {
157
+            $this->themingDefaults->set($setting, $value);
158
+        }
159 159
 
160
-		return new DataResponse([
161
-			'data' => [
162
-				'message' => $this->l10n->t('Saved'),
163
-			],
164
-			'status' => 'success'
165
-		]);
166
-	}
160
+        return new DataResponse([
161
+            'data' => [
162
+                'message' => $this->l10n->t('Saved'),
163
+            ],
164
+            'status' => 'success'
165
+        ]);
166
+    }
167 167
 
168
-	/**
169
-	 * @throws NotPermittedException
170
-	 */
171
-	#[AuthorizedAdminSetting(settings: Admin::class)]
172
-	public function updateAppMenu(string $setting, mixed $value): DataResponse {
173
-		$error = null;
174
-		switch ($setting) {
175
-			case 'defaultApps':
176
-				if (is_array($value)) {
177
-					try {
178
-						$this->navigationManager->setDefaultEntryIds($value);
179
-					} catch (InvalidArgumentException $e) {
180
-						$error = $this->l10n->t('Invalid app given');
181
-					}
182
-				} else {
183
-					$error = $this->l10n->t('Invalid type for setting "defaultApp" given');
184
-				}
185
-				break;
186
-			default:
187
-				$error = $this->l10n->t('Invalid setting key');
188
-		}
189
-		if ($error !== null) {
190
-			return new DataResponse([
191
-				'data' => [
192
-					'message' => $error,
193
-				],
194
-				'status' => 'error'
195
-			], Http::STATUS_BAD_REQUEST);
196
-		}
168
+    /**
169
+     * @throws NotPermittedException
170
+     */
171
+    #[AuthorizedAdminSetting(settings: Admin::class)]
172
+    public function updateAppMenu(string $setting, mixed $value): DataResponse {
173
+        $error = null;
174
+        switch ($setting) {
175
+            case 'defaultApps':
176
+                if (is_array($value)) {
177
+                    try {
178
+                        $this->navigationManager->setDefaultEntryIds($value);
179
+                    } catch (InvalidArgumentException $e) {
180
+                        $error = $this->l10n->t('Invalid app given');
181
+                    }
182
+                } else {
183
+                    $error = $this->l10n->t('Invalid type for setting "defaultApp" given');
184
+                }
185
+                break;
186
+            default:
187
+                $error = $this->l10n->t('Invalid setting key');
188
+        }
189
+        if ($error !== null) {
190
+            return new DataResponse([
191
+                'data' => [
192
+                    'message' => $error,
193
+                ],
194
+                'status' => 'error'
195
+            ], Http::STATUS_BAD_REQUEST);
196
+        }
197 197
 
198
-		return new DataResponse([
199
-			'data' => [
200
-				'message' => $this->l10n->t('Saved'),
201
-			],
202
-			'status' => 'success'
203
-		]);
204
-	}
198
+        return new DataResponse([
199
+            'data' => [
200
+                'message' => $this->l10n->t('Saved'),
201
+            ],
202
+            'status' => 'success'
203
+        ]);
204
+    }
205 205
 
206
-	/**
207
-	 * Check that a string is a valid http/https url.
208
-	 * Also validates that there is no way for XSS through HTML
209
-	 */
210
-	private function isValidUrl(string $url): bool {
211
-		return ((str_starts_with($url, 'http://') || str_starts_with($url, 'https://'))
212
-			&& filter_var($url, FILTER_VALIDATE_URL) !== false)
213
-			&& !str_contains($url, '"');
214
-	}
206
+    /**
207
+     * Check that a string is a valid http/https url.
208
+     * Also validates that there is no way for XSS through HTML
209
+     */
210
+    private function isValidUrl(string $url): bool {
211
+        return ((str_starts_with($url, 'http://') || str_starts_with($url, 'https://'))
212
+            && filter_var($url, FILTER_VALIDATE_URL) !== false)
213
+            && !str_contains($url, '"');
214
+    }
215 215
 
216
-	/**
217
-	 * @throws NotPermittedException
218
-	 */
219
-	#[AuthorizedAdminSetting(settings: Admin::class)]
220
-	public function uploadImage(): DataResponse {
221
-		$key = $this->request->getParam('key');
222
-		if (!in_array($key, self::VALID_UPLOAD_KEYS, true)) {
223
-			return new DataResponse(
224
-				[
225
-					'data' => [
226
-						'message' => 'Invalid key'
227
-					],
228
-					'status' => 'failure',
229
-				],
230
-				Http::STATUS_BAD_REQUEST
231
-			);
232
-		}
233
-		$image = $this->request->getUploadedFile('image');
234
-		$error = null;
235
-		$phpFileUploadErrors = [
236
-			UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
237
-			UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
238
-			UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
239
-			UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
240
-			UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
241
-			UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
242
-			UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
243
-			UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
244
-		];
245
-		if (empty($image)) {
246
-			$error = $this->l10n->t('No file uploaded');
247
-		}
248
-		if (!empty($image) && array_key_exists('error', $image) && $image['error'] !== UPLOAD_ERR_OK) {
249
-			$error = $phpFileUploadErrors[$image['error']];
250
-		}
216
+    /**
217
+     * @throws NotPermittedException
218
+     */
219
+    #[AuthorizedAdminSetting(settings: Admin::class)]
220
+    public function uploadImage(): DataResponse {
221
+        $key = $this->request->getParam('key');
222
+        if (!in_array($key, self::VALID_UPLOAD_KEYS, true)) {
223
+            return new DataResponse(
224
+                [
225
+                    'data' => [
226
+                        'message' => 'Invalid key'
227
+                    ],
228
+                    'status' => 'failure',
229
+                ],
230
+                Http::STATUS_BAD_REQUEST
231
+            );
232
+        }
233
+        $image = $this->request->getUploadedFile('image');
234
+        $error = null;
235
+        $phpFileUploadErrors = [
236
+            UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
237
+            UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
238
+            UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
239
+            UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
240
+            UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
241
+            UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
242
+            UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
243
+            UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
244
+        ];
245
+        if (empty($image)) {
246
+            $error = $this->l10n->t('No file uploaded');
247
+        }
248
+        if (!empty($image) && array_key_exists('error', $image) && $image['error'] !== UPLOAD_ERR_OK) {
249
+            $error = $phpFileUploadErrors[$image['error']];
250
+        }
251 251
 
252
-		if ($error !== null) {
253
-			return new DataResponse(
254
-				[
255
-					'data' => [
256
-						'message' => $error
257
-					],
258
-					'status' => 'failure',
259
-				],
260
-				Http::STATUS_UNPROCESSABLE_ENTITY
261
-			);
262
-		}
252
+        if ($error !== null) {
253
+            return new DataResponse(
254
+                [
255
+                    'data' => [
256
+                        'message' => $error
257
+                    ],
258
+                    'status' => 'failure',
259
+                ],
260
+                Http::STATUS_UNPROCESSABLE_ENTITY
261
+            );
262
+        }
263 263
 
264
-		try {
265
-			$mime = $this->imageManager->updateImage($key, $image['tmp_name']);
266
-			$this->themingDefaults->set($key . 'Mime', $mime);
267
-		} catch (\Exception $e) {
268
-			return new DataResponse(
269
-				[
270
-					'data' => [
271
-						'message' => $e->getMessage()
272
-					],
273
-					'status' => 'failure',
274
-				],
275
-				Http::STATUS_UNPROCESSABLE_ENTITY
276
-			);
277
-		}
264
+        try {
265
+            $mime = $this->imageManager->updateImage($key, $image['tmp_name']);
266
+            $this->themingDefaults->set($key . 'Mime', $mime);
267
+        } catch (\Exception $e) {
268
+            return new DataResponse(
269
+                [
270
+                    'data' => [
271
+                        'message' => $e->getMessage()
272
+                    ],
273
+                    'status' => 'failure',
274
+                ],
275
+                Http::STATUS_UNPROCESSABLE_ENTITY
276
+            );
277
+        }
278 278
 
279
-		$name = $image['name'];
279
+        $name = $image['name'];
280 280
 
281
-		return new DataResponse(
282
-			[
283
-				'data'
284
-					=> [
285
-						'name' => $name,
286
-						'url' => $this->imageManager->getImageUrl($key),
287
-						'message' => $this->l10n->t('Saved'),
288
-					],
289
-				'status' => 'success'
290
-			]
291
-		);
292
-	}
281
+        return new DataResponse(
282
+            [
283
+                'data'
284
+                    => [
285
+                        'name' => $name,
286
+                        'url' => $this->imageManager->getImageUrl($key),
287
+                        'message' => $this->l10n->t('Saved'),
288
+                    ],
289
+                'status' => 'success'
290
+            ]
291
+        );
292
+    }
293 293
 
294
-	/**
295
-	 * Revert setting to default value
296
-	 *
297
-	 * @param string $setting setting which should be reverted
298
-	 * @return DataResponse
299
-	 * @throws NotPermittedException
300
-	 */
301
-	#[AuthorizedAdminSetting(settings: Admin::class)]
302
-	public function undo(string $setting): DataResponse {
303
-		$setting = match ($setting) {
304
-			'primaryColor' => 'primary_color',
305
-			'backgroundColor' => 'background_color',
306
-			'legalNoticeUrl' => 'imprintUrl',
307
-			'privacyPolicyUrl' => 'privacyUrl',
308
-			default => $setting,
309
-		};
310
-		$value = $this->themingDefaults->undo($setting);
294
+    /**
295
+     * Revert setting to default value
296
+     *
297
+     * @param string $setting setting which should be reverted
298
+     * @return DataResponse
299
+     * @throws NotPermittedException
300
+     */
301
+    #[AuthorizedAdminSetting(settings: Admin::class)]
302
+    public function undo(string $setting): DataResponse {
303
+        $setting = match ($setting) {
304
+            'primaryColor' => 'primary_color',
305
+            'backgroundColor' => 'background_color',
306
+            'legalNoticeUrl' => 'imprintUrl',
307
+            'privacyPolicyUrl' => 'privacyUrl',
308
+            default => $setting,
309
+        };
310
+        $value = $this->themingDefaults->undo($setting);
311 311
 
312
-		return new DataResponse(
313
-			[
314
-				'data'
315
-					=> [
316
-						'value' => $value,
317
-						'message' => $this->l10n->t('Saved'),
318
-					],
319
-				'status' => 'success'
320
-			]
321
-		);
322
-	}
312
+        return new DataResponse(
313
+            [
314
+                'data'
315
+                    => [
316
+                        'value' => $value,
317
+                        'message' => $this->l10n->t('Saved'),
318
+                    ],
319
+                'status' => 'success'
320
+            ]
321
+        );
322
+    }
323 323
 
324
-	/**
325
-	 * Revert all theming settings to their default values
326
-	 *
327
-	 * @return DataResponse
328
-	 * @throws NotPermittedException
329
-	 */
330
-	#[AuthorizedAdminSetting(settings: Admin::class)]
331
-	public function undoAll(): DataResponse {
332
-		$this->themingDefaults->undoAll();
333
-		$this->navigationManager->setDefaultEntryIds([]);
324
+    /**
325
+     * Revert all theming settings to their default values
326
+     *
327
+     * @return DataResponse
328
+     * @throws NotPermittedException
329
+     */
330
+    #[AuthorizedAdminSetting(settings: Admin::class)]
331
+    public function undoAll(): DataResponse {
332
+        $this->themingDefaults->undoAll();
333
+        $this->navigationManager->setDefaultEntryIds([]);
334 334
 
335
-		return new DataResponse(
336
-			[
337
-				'data'
338
-					=> [
339
-						'message' => $this->l10n->t('Saved'),
340
-					],
341
-				'status' => 'success'
342
-			]
343
-		);
344
-	}
335
+        return new DataResponse(
336
+            [
337
+                'data'
338
+                    => [
339
+                        'message' => $this->l10n->t('Saved'),
340
+                    ],
341
+                'status' => 'success'
342
+            ]
343
+        );
344
+    }
345 345
 
346
-	/**
347
-	 * @NoSameSiteCookieRequired
348
-	 *
349
-	 * Get an image
350
-	 *
351
-	 * @param string $key Key of the image
352
-	 * @param bool $useSvg Return image as SVG
353
-	 * @return FileDisplayResponse<Http::STATUS_OK, array{}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
354
-	 * @throws NotPermittedException
355
-	 *
356
-	 * 200: Image returned
357
-	 * 404: Image not found
358
-	 */
359
-	#[PublicPage]
360
-	#[NoCSRFRequired]
361
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
362
-	public function getImage(string $key, bool $useSvg = true) {
363
-		try {
364
-			$useSvg = $useSvg && $this->imageManager->canConvert('SVG');
365
-			$file = $this->imageManager->getImage($key, $useSvg);
366
-		} catch (NotFoundException $e) {
367
-			return new NotFoundResponse();
368
-		}
346
+    /**
347
+     * @NoSameSiteCookieRequired
348
+     *
349
+     * Get an image
350
+     *
351
+     * @param string $key Key of the image
352
+     * @param bool $useSvg Return image as SVG
353
+     * @return FileDisplayResponse<Http::STATUS_OK, array{}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
354
+     * @throws NotPermittedException
355
+     *
356
+     * 200: Image returned
357
+     * 404: Image not found
358
+     */
359
+    #[PublicPage]
360
+    #[NoCSRFRequired]
361
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
362
+    public function getImage(string $key, bool $useSvg = true) {
363
+        try {
364
+            $useSvg = $useSvg && $this->imageManager->canConvert('SVG');
365
+            $file = $this->imageManager->getImage($key, $useSvg);
366
+        } catch (NotFoundException $e) {
367
+            return new NotFoundResponse();
368
+        }
369 369
 
370
-		$response = new FileDisplayResponse($file);
371
-		$csp = new ContentSecurityPolicy();
372
-		$csp->allowInlineStyle();
373
-		$response->setContentSecurityPolicy($csp);
374
-		$response->cacheFor(3600);
375
-		$response->addHeader('Content-Type', $file->getMimeType());
376
-		$response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"');
377
-		return $response;
378
-	}
370
+        $response = new FileDisplayResponse($file);
371
+        $csp = new ContentSecurityPolicy();
372
+        $csp->allowInlineStyle();
373
+        $response->setContentSecurityPolicy($csp);
374
+        $response->cacheFor(3600);
375
+        $response->addHeader('Content-Type', $file->getMimeType());
376
+        $response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"');
377
+        return $response;
378
+    }
379 379
 
380
-	/**
381
-	 * Get the CSS stylesheet for a theme
382
-	 *
383
-	 * @param string $themeId ID of the theme
384
-	 * @param bool $plain Let the browser decide the CSS priority
385
-	 * @param bool $withCustomCss Include custom CSS
386
-	 * @return DataDisplayResponse<Http::STATUS_OK, array{Content-Type: 'text/css'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
387
-	 *
388
-	 * 200: Stylesheet returned
389
-	 * 404: Theme not found
390
-	 */
391
-	#[PublicPage]
392
-	#[NoCSRFRequired]
393
-	#[NoTwoFactorRequired]
394
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
395
-	#[NoSameSiteCookieRequired]
396
-	public function getThemeStylesheet(string $themeId, bool $plain = false, bool $withCustomCss = false) {
397
-		$themes = $this->themesService->getThemes();
398
-		if (!in_array($themeId, array_keys($themes))) {
399
-			return new NotFoundResponse();
400
-		}
380
+    /**
381
+     * Get the CSS stylesheet for a theme
382
+     *
383
+     * @param string $themeId ID of the theme
384
+     * @param bool $plain Let the browser decide the CSS priority
385
+     * @param bool $withCustomCss Include custom CSS
386
+     * @return DataDisplayResponse<Http::STATUS_OK, array{Content-Type: 'text/css'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
387
+     *
388
+     * 200: Stylesheet returned
389
+     * 404: Theme not found
390
+     */
391
+    #[PublicPage]
392
+    #[NoCSRFRequired]
393
+    #[NoTwoFactorRequired]
394
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
395
+    #[NoSameSiteCookieRequired]
396
+    public function getThemeStylesheet(string $themeId, bool $plain = false, bool $withCustomCss = false) {
397
+        $themes = $this->themesService->getThemes();
398
+        if (!in_array($themeId, array_keys($themes))) {
399
+            return new NotFoundResponse();
400
+        }
401 401
 
402
-		$theme = $themes[$themeId];
403
-		$customCss = $theme->getCustomCss();
402
+        $theme = $themes[$themeId];
403
+        $customCss = $theme->getCustomCss();
404 404
 
405
-		// Generate variables
406
-		$variables = '';
407
-		foreach ($theme->getCSSVariables() as $variable => $value) {
408
-			$variables .= "$variable:$value; ";
409
-		};
405
+        // Generate variables
406
+        $variables = '';
407
+        foreach ($theme->getCSSVariables() as $variable => $value) {
408
+            $variables .= "$variable:$value; ";
409
+        };
410 410
 
411
-		// If plain is set, the browser decides of the css priority
412
-		if ($plain) {
413
-			$css = ":root { $variables } " . $customCss;
414
-		} else {
415
-			// If not set, we'll rely on the body class
416
-			// We need to separate @-rules from normal selectors, as they can't be nested
417
-			// This is a replacement for the SCSS compiler that did this automatically before f1448fcf0777db7d4254cb0a3ef94d63be9f7a24
418
-			// We need a better way to handle this, but for now we just remove comments and split the at-rules
419
-			// from the rest of the CSS.
420
-			$customCssWithoutComments = preg_replace('!/\*.*?\*/!s', '', $customCss);
421
-			$customCssWithoutComments = preg_replace('!//.*!', '', $customCssWithoutComments);
422
-			preg_match_all('/(@[^{]+{(?:[^{}]*|(?R))*})/', $customCssWithoutComments, $atRules);
423
-			$atRulesCss = implode('', $atRules[0]);
424
-			$scopedCss = preg_replace('/(@[^{]+{(?:[^{}]*|(?R))*})/', '', $customCssWithoutComments);
411
+        // If plain is set, the browser decides of the css priority
412
+        if ($plain) {
413
+            $css = ":root { $variables } " . $customCss;
414
+        } else {
415
+            // If not set, we'll rely on the body class
416
+            // We need to separate @-rules from normal selectors, as they can't be nested
417
+            // This is a replacement for the SCSS compiler that did this automatically before f1448fcf0777db7d4254cb0a3ef94d63be9f7a24
418
+            // We need a better way to handle this, but for now we just remove comments and split the at-rules
419
+            // from the rest of the CSS.
420
+            $customCssWithoutComments = preg_replace('!/\*.*?\*/!s', '', $customCss);
421
+            $customCssWithoutComments = preg_replace('!//.*!', '', $customCssWithoutComments);
422
+            preg_match_all('/(@[^{]+{(?:[^{}]*|(?R))*})/', $customCssWithoutComments, $atRules);
423
+            $atRulesCss = implode('', $atRules[0]);
424
+            $scopedCss = preg_replace('/(@[^{]+{(?:[^{}]*|(?R))*})/', '', $customCssWithoutComments);
425 425
 
426
-			$css = "$atRulesCss [data-theme-$themeId] { $variables $scopedCss }";
427
-		}
426
+            $css = "$atRulesCss [data-theme-$themeId] { $variables $scopedCss }";
427
+        }
428 428
 
429
-		try {
430
-			$response = new DataDisplayResponse($css, Http::STATUS_OK, ['Content-Type' => 'text/css']);
431
-			$response->cacheFor(86400);
432
-			return $response;
433
-		} catch (NotFoundException $e) {
434
-			return new NotFoundResponse();
435
-		}
436
-	}
429
+        try {
430
+            $response = new DataDisplayResponse($css, Http::STATUS_OK, ['Content-Type' => 'text/css']);
431
+            $response->cacheFor(86400);
432
+            return $response;
433
+        } catch (NotFoundException $e) {
434
+            return new NotFoundResponse();
435
+        }
436
+    }
437 437
 
438
-	/**
439
-	 * Get the manifest for an app
440
-	 *
441
-	 * @param string $app ID of the app
442
-	 * @psalm-suppress LessSpecificReturnStatement The content of the Manifest doesn't need to be described in the return type
443
-	 * @return JSONResponse<Http::STATUS_OK, array{name: string, short_name: string, start_url: string, theme_color: string, background_color: string, description: string, icons: list<array{src: non-empty-string, type: string, sizes: string}>, display_override: list<string>, display: string}, array{}>|JSONResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}>
444
-	 *
445
-	 * 200: Manifest returned
446
-	 * 404: App not found
447
-	 */
448
-	#[PublicPage]
449
-	#[NoCSRFRequired]
450
-	#[BruteForceProtection(action: 'manifest')]
451
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
452
-	public function getManifest(string $app): JSONResponse {
453
-		$cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
454
-		if ($app === 'core' || $app === 'settings') {
455
-			$name = $this->themingDefaults->getName();
456
-			$shortName = $this->themingDefaults->getName();
457
-			$startUrl = $this->urlGenerator->getBaseUrl();
458
-			$description = $this->themingDefaults->getSlogan();
459
-		} else {
460
-			if (!$this->appManager->isEnabledForUser($app)) {
461
-				$response = new JSONResponse([], Http::STATUS_NOT_FOUND);
462
-				$response->throttle(['action' => 'manifest', 'app' => $app]);
463
-				return $response;
464
-			}
438
+    /**
439
+     * Get the manifest for an app
440
+     *
441
+     * @param string $app ID of the app
442
+     * @psalm-suppress LessSpecificReturnStatement The content of the Manifest doesn't need to be described in the return type
443
+     * @return JSONResponse<Http::STATUS_OK, array{name: string, short_name: string, start_url: string, theme_color: string, background_color: string, description: string, icons: list<array{src: non-empty-string, type: string, sizes: string}>, display_override: list<string>, display: string}, array{}>|JSONResponse<Http::STATUS_NOT_FOUND, list<empty>, array{}>
444
+     *
445
+     * 200: Manifest returned
446
+     * 404: App not found
447
+     */
448
+    #[PublicPage]
449
+    #[NoCSRFRequired]
450
+    #[BruteForceProtection(action: 'manifest')]
451
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
452
+    public function getManifest(string $app): JSONResponse {
453
+        $cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
454
+        if ($app === 'core' || $app === 'settings') {
455
+            $name = $this->themingDefaults->getName();
456
+            $shortName = $this->themingDefaults->getName();
457
+            $startUrl = $this->urlGenerator->getBaseUrl();
458
+            $description = $this->themingDefaults->getSlogan();
459
+        } else {
460
+            if (!$this->appManager->isEnabledForUser($app)) {
461
+                $response = new JSONResponse([], Http::STATUS_NOT_FOUND);
462
+                $response->throttle(['action' => 'manifest', 'app' => $app]);
463
+                return $response;
464
+            }
465 465
 
466
-			$info = $this->appManager->getAppInfo($app, false, $this->l10n->getLanguageCode());
467
-			$name = $info['name'] . ' - ' . $this->themingDefaults->getName();
468
-			$shortName = $info['name'];
469
-			if (str_contains($this->request->getRequestUri(), '/index.php/')) {
470
-				$startUrl = $this->urlGenerator->getBaseUrl() . '/index.php/apps/' . $app . '/';
471
-			} else {
472
-				$startUrl = $this->urlGenerator->getBaseUrl() . '/apps/' . $app . '/';
473
-			}
474
-			$description = $info['summary'] ?? '';
475
-		}
476
-		/**
477
-		 * @var string $description
478
-		 * @var string $shortName
479
-		 */
480
-		$responseJS = [
481
-			'name' => $name,
482
-			'short_name' => $shortName,
483
-			'start_url' => $startUrl,
484
-			'theme_color' => $this->themingDefaults->getColorPrimary(),
485
-			'background_color' => $this->themingDefaults->getColorPrimary(),
486
-			'description' => $description,
487
-			'icons'
488
-				=> [
489
-					[
490
-						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon',
491
-							['app' => $app]) . '?v=' . $cacheBusterValue,
492
-						'type' => 'image/png',
493
-						'sizes' => '512x512'
494
-					],
495
-					[
496
-						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getFavicon',
497
-							['app' => $app]) . '?v=' . $cacheBusterValue,
498
-						'type' => 'image/svg+xml',
499
-						'sizes' => '16x16'
500
-					]
501
-				],
502
-			'display_override' => [$this->config->getSystemValueBool('theming.standalone_window.enabled', true) ? 'minimal-ui' : ''],
503
-			'display' => $this->config->getSystemValueBool('theming.standalone_window.enabled', true) ? 'standalone' : 'browser'
504
-		];
505
-		$response = new JSONResponse($responseJS);
506
-		$response->cacheFor(3600);
507
-		return $response;
508
-	}
466
+            $info = $this->appManager->getAppInfo($app, false, $this->l10n->getLanguageCode());
467
+            $name = $info['name'] . ' - ' . $this->themingDefaults->getName();
468
+            $shortName = $info['name'];
469
+            if (str_contains($this->request->getRequestUri(), '/index.php/')) {
470
+                $startUrl = $this->urlGenerator->getBaseUrl() . '/index.php/apps/' . $app . '/';
471
+            } else {
472
+                $startUrl = $this->urlGenerator->getBaseUrl() . '/apps/' . $app . '/';
473
+            }
474
+            $description = $info['summary'] ?? '';
475
+        }
476
+        /**
477
+         * @var string $description
478
+         * @var string $shortName
479
+         */
480
+        $responseJS = [
481
+            'name' => $name,
482
+            'short_name' => $shortName,
483
+            'start_url' => $startUrl,
484
+            'theme_color' => $this->themingDefaults->getColorPrimary(),
485
+            'background_color' => $this->themingDefaults->getColorPrimary(),
486
+            'description' => $description,
487
+            'icons'
488
+                => [
489
+                    [
490
+                        'src' => $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon',
491
+                            ['app' => $app]) . '?v=' . $cacheBusterValue,
492
+                        'type' => 'image/png',
493
+                        'sizes' => '512x512'
494
+                    ],
495
+                    [
496
+                        'src' => $this->urlGenerator->linkToRoute('theming.Icon.getFavicon',
497
+                            ['app' => $app]) . '?v=' . $cacheBusterValue,
498
+                        'type' => 'image/svg+xml',
499
+                        'sizes' => '16x16'
500
+                    ]
501
+                ],
502
+            'display_override' => [$this->config->getSystemValueBool('theming.standalone_window.enabled', true) ? 'minimal-ui' : ''],
503
+            'display' => $this->config->getSystemValueBool('theming.standalone_window.enabled', true) ? 'standalone' : 'browser'
504
+        ];
505
+        $response = new JSONResponse($responseJS);
506
+        $response->cacheFor(3600);
507
+        return $response;
508
+    }
509 509
 }
Please login to merge, or discard this patch.
apps/files_sharing/lib/Controller/ShareController.php 2 patches
Indentation   +349 added lines, -349 removed lines patch added patch discarded remove patch
@@ -51,353 +51,353 @@
 block discarded – undo
51 51
  */
52 52
 #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
53 53
 class ShareController extends AuthPublicShareController {
54
-	protected ?IShare $share = null;
55
-
56
-	public const SHARE_ACCESS = 'access';
57
-	public const SHARE_AUTH = 'auth';
58
-	public const SHARE_DOWNLOAD = 'download';
59
-
60
-	public function __construct(
61
-		string $appName,
62
-		IRequest $request,
63
-		protected IConfig $config,
64
-		IURLGenerator $urlGenerator,
65
-		protected IUserManager $userManager,
66
-		protected \OCP\Activity\IManager $activityManager,
67
-		protected ShareManager $shareManager,
68
-		ISession $session,
69
-		protected IPreview $previewManager,
70
-		protected IRootFolder $rootFolder,
71
-		protected FederatedShareProvider $federatedShareProvider,
72
-		protected IAccountManager $accountManager,
73
-		protected IEventDispatcher $eventDispatcher,
74
-		protected IL10N $l10n,
75
-		protected ISecureRandom $secureRandom,
76
-		protected Defaults $defaults,
77
-		private IPublicShareTemplateFactory $publicShareTemplateFactory,
78
-	) {
79
-		parent::__construct($appName, $request, $session, $urlGenerator);
80
-	}
81
-
82
-	/**
83
-	 * Show the authentication page
84
-	 * The form has to submit to the authenticate method route
85
-	 */
86
-	#[PublicPage]
87
-	#[NoCSRFRequired]
88
-	public function showAuthenticate(): TemplateResponse {
89
-		$templateParameters = ['share' => $this->share];
90
-
91
-		$this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
92
-
93
-		$response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
94
-		if ($this->share->getSendPasswordByTalk()) {
95
-			$csp = new ContentSecurityPolicy();
96
-			$csp->addAllowedConnectDomain('*');
97
-			$csp->addAllowedMediaDomain('blob:');
98
-			$response->setContentSecurityPolicy($csp);
99
-		}
100
-
101
-		return $response;
102
-	}
103
-
104
-	/**
105
-	 * The template to show when authentication failed
106
-	 */
107
-	protected function showAuthFailed(): TemplateResponse {
108
-		$templateParameters = ['share' => $this->share, 'wrongpw' => true];
109
-
110
-		$this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
111
-
112
-		$response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
113
-		if ($this->share->getSendPasswordByTalk()) {
114
-			$csp = new ContentSecurityPolicy();
115
-			$csp->addAllowedConnectDomain('*');
116
-			$csp->addAllowedMediaDomain('blob:');
117
-			$response->setContentSecurityPolicy($csp);
118
-		}
119
-
120
-		return $response;
121
-	}
122
-
123
-	/**
124
-	 * The template to show after user identification
125
-	 */
126
-	protected function showIdentificationResult(bool $success = false): TemplateResponse {
127
-		$templateParameters = ['share' => $this->share, 'identityOk' => $success];
128
-
129
-		$this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
130
-
131
-		$response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
132
-		if ($this->share->getSendPasswordByTalk()) {
133
-			$csp = new ContentSecurityPolicy();
134
-			$csp->addAllowedConnectDomain('*');
135
-			$csp->addAllowedMediaDomain('blob:');
136
-			$response->setContentSecurityPolicy($csp);
137
-		}
138
-
139
-		return $response;
140
-	}
141
-
142
-	/**
143
-	 * Validate the identity token of a public share
144
-	 *
145
-	 * @param ?string $identityToken
146
-	 * @return bool
147
-	 */
148
-	protected function validateIdentity(?string $identityToken = null): bool {
149
-		if ($this->share->getShareType() !== IShare::TYPE_EMAIL) {
150
-			return false;
151
-		}
152
-
153
-		if ($identityToken === null || $this->share->getSharedWith() === null) {
154
-			return false;
155
-		}
156
-
157
-		return $identityToken === $this->share->getSharedWith();
158
-	}
159
-
160
-	/**
161
-	 * Generates a password for the share, respecting any password policy defined
162
-	 */
163
-	protected function generatePassword(): void {
164
-		$event = new GenerateSecurePasswordEvent(PasswordContext::SHARING);
165
-		$this->eventDispatcher->dispatchTyped($event);
166
-		$password = $event->getPassword() ?? $this->secureRandom->generate(20);
167
-
168
-		$this->share->setPassword($password);
169
-		$this->shareManager->updateShare($this->share);
170
-	}
171
-
172
-	protected function verifyPassword(string $password): bool {
173
-		return $this->shareManager->checkPassword($this->share, $password);
174
-	}
175
-
176
-	protected function getPasswordHash(): ?string {
177
-		return $this->share->getPassword();
178
-	}
179
-
180
-	public function isValidToken(): bool {
181
-		try {
182
-			$this->share = $this->shareManager->getShareByToken($this->getToken());
183
-		} catch (ShareNotFound $e) {
184
-			return false;
185
-		}
186
-
187
-		return true;
188
-	}
189
-
190
-	protected function isPasswordProtected(): bool {
191
-		return $this->share->getPassword() !== null;
192
-	}
193
-
194
-	protected function authSucceeded() {
195
-		if ($this->share === null) {
196
-			throw new NotFoundException();
197
-		}
198
-
199
-		// For share this was always set so it is still used in other apps
200
-		$allowedShareIds = $this->session->get(PublicAuth::DAV_AUTHENTICATED);
201
-		if (!is_array($allowedShareIds)) {
202
-			$allowedShareIds = [];
203
-		}
204
-
205
-		$this->session->set(PublicAuth::DAV_AUTHENTICATED, array_merge($allowedShareIds, [$this->share->getId()]));
206
-	}
207
-
208
-	protected function authFailed() {
209
-		$this->emitAccessShareHook($this->share, 403, 'Wrong password');
210
-		$this->emitShareAccessEvent($this->share, self::SHARE_AUTH, 403, 'Wrong password');
211
-	}
212
-
213
-	/**
214
-	 * throws hooks when a share is attempted to be accessed
215
-	 *
216
-	 * @param IShare|string $share the Share instance if available,
217
-	 *                             otherwise token
218
-	 * @param int $errorCode
219
-	 * @param string $errorMessage
220
-	 *
221
-	 * @throws HintException
222
-	 * @throws \OC\ServerNotAvailableException
223
-	 *
224
-	 * @deprecated use OCP\Files_Sharing\Event\ShareLinkAccessedEvent
225
-	 */
226
-	protected function emitAccessShareHook($share, int $errorCode = 200, string $errorMessage = '') {
227
-		$itemType = $itemSource = $uidOwner = '';
228
-		$token = $share;
229
-		$exception = null;
230
-		if ($share instanceof IShare) {
231
-			try {
232
-				$token = $share->getToken();
233
-				$uidOwner = $share->getSharedBy();
234
-				$itemType = $share->getNodeType();
235
-				$itemSource = $share->getNodeId();
236
-			} catch (\Exception $e) {
237
-				// we log what we know and pass on the exception afterwards
238
-				$exception = $e;
239
-			}
240
-		}
241
-
242
-		\OC_Hook::emit(Share::class, 'share_link_access', [
243
-			'itemType' => $itemType,
244
-			'itemSource' => $itemSource,
245
-			'uidOwner' => $uidOwner,
246
-			'token' => $token,
247
-			'errorCode' => $errorCode,
248
-			'errorMessage' => $errorMessage
249
-		]);
250
-
251
-		if (!is_null($exception)) {
252
-			throw $exception;
253
-		}
254
-	}
255
-
256
-	/**
257
-	 * Emit a ShareLinkAccessedEvent event when a share is accessed, downloaded, auth...
258
-	 */
259
-	protected function emitShareAccessEvent(IShare $share, string $step = '', int $errorCode = 200, string $errorMessage = ''): void {
260
-		if ($step !== self::SHARE_ACCESS
261
-			&& $step !== self::SHARE_AUTH
262
-			&& $step !== self::SHARE_DOWNLOAD) {
263
-			return;
264
-		}
265
-		$this->eventDispatcher->dispatchTyped(new ShareLinkAccessedEvent($share, $step, $errorCode, $errorMessage));
266
-	}
267
-
268
-	/**
269
-	 * Validate the permissions of the share
270
-	 *
271
-	 * @param Share\IShare $share
272
-	 * @return bool
273
-	 */
274
-	private function validateShare(IShare $share) {
275
-		// If the owner is disabled no access to the link is granted
276
-		$owner = $this->userManager->get($share->getShareOwner());
277
-		if ($owner === null || !$owner->isEnabled()) {
278
-			return false;
279
-		}
280
-
281
-		// If the initiator of the share is disabled no access is granted
282
-		$initiator = $this->userManager->get($share->getSharedBy());
283
-		if ($initiator === null || !$initiator->isEnabled()) {
284
-			return false;
285
-		}
286
-
287
-		return $share->getNode()->isReadable() && $share->getNode()->isShareable();
288
-	}
289
-
290
-	/**
291
-	 * @param string $path
292
-	 * @return TemplateResponse
293
-	 * @throws NotFoundException
294
-	 * @throws \Exception
295
-	 */
296
-	#[PublicPage]
297
-	#[NoCSRFRequired]
298
-	public function showShare($path = ''): TemplateResponse {
299
-		\OC_User::setIncognitoMode(true);
300
-
301
-		// Check whether share exists
302
-		try {
303
-			$share = $this->shareManager->getShareByToken($this->getToken());
304
-		} catch (ShareNotFound $e) {
305
-			// The share does not exists, we do not emit an ShareLinkAccessedEvent
306
-			$this->emitAccessShareHook($this->getToken(), 404, 'Share not found');
307
-			throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
308
-		}
309
-
310
-		if (!$this->validateShare($share)) {
311
-			throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
312
-		}
313
-
314
-		$shareNode = $share->getNode();
315
-
316
-		try {
317
-			$templateProvider = $this->publicShareTemplateFactory->getProvider($share);
318
-			$response = $templateProvider->renderPage($share, $this->getToken(), $path);
319
-		} catch (NotFoundException $e) {
320
-			$this->emitAccessShareHook($share, 404, 'Share not found');
321
-			$this->emitShareAccessEvent($share, ShareController::SHARE_ACCESS, 404, 'Share not found');
322
-			throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
323
-		}
324
-
325
-		// We can't get the path of a file share
326
-		try {
327
-			if ($shareNode instanceof File && $path !== '') {
328
-				$this->emitAccessShareHook($share, 404, 'Share not found');
329
-				$this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found');
330
-				throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
331
-			}
332
-		} catch (\Exception $e) {
333
-			$this->emitAccessShareHook($share, 404, 'Share not found');
334
-			$this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found');
335
-			throw $e;
336
-		}
337
-
338
-
339
-		$this->emitAccessShareHook($share);
340
-		$this->emitShareAccessEvent($share, self::SHARE_ACCESS);
341
-
342
-		return $response;
343
-	}
344
-
345
-	/**
346
-	 * @throws NotFoundException
347
-	 * @deprecated 31.0.0 Users are encouraged to use the DAV endpoint
348
-	 */
349
-	#[PublicPage]
350
-	#[NoCSRFRequired]
351
-	#[NoSameSiteCookieRequired]
352
-	public function downloadShare(string $token, ?string $files = null, string $path = ''): NotFoundResponse|RedirectResponse|DataResponse {
353
-		\OC_User::setIncognitoMode(true);
354
-
355
-		$share = $this->shareManager->getShareByToken($token);
356
-
357
-		if (!($share->getPermissions() & Constants::PERMISSION_READ)) {
358
-			return new DataResponse('Share has no read permission');
359
-		}
360
-
361
-		$attributes = $share->getAttributes();
362
-		if ($attributes?->getAttribute('permissions', 'download') === false) {
363
-			return new DataResponse('Share has no download permission');
364
-		}
365
-
366
-		if (!$this->validateShare($share)) {
367
-			throw new NotFoundException();
368
-		}
369
-
370
-		$node = $share->getNode();
371
-		if ($node instanceof Folder) {
372
-			// Directory share
373
-
374
-			// Try to get the path
375
-			if ($path !== '') {
376
-				try {
377
-					$node = $node->get($path);
378
-				} catch (NotFoundException $e) {
379
-					$this->emitAccessShareHook($share, 404, 'Share not found');
380
-					$this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD, 404, 'Share not found');
381
-					return new NotFoundResponse();
382
-				}
383
-			}
384
-
385
-			if ($node instanceof Folder) {
386
-				if ($files === null || $files === '') {
387
-					if ($share->getHideDownload()) {
388
-						throw new NotFoundException('Downloading a folder');
389
-					}
390
-				}
391
-			}
392
-		}
393
-
394
-		$this->emitAccessShareHook($share);
395
-		$this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD);
396
-
397
-		$davUrl = '/public.php/dav/files/' . $token . '/?accept=zip';
398
-		if ($files !== null) {
399
-			$davUrl .= '&files=' . $files;
400
-		}
401
-		return new RedirectResponse($this->urlGenerator->getAbsoluteURL($davUrl));
402
-	}
54
+    protected ?IShare $share = null;
55
+
56
+    public const SHARE_ACCESS = 'access';
57
+    public const SHARE_AUTH = 'auth';
58
+    public const SHARE_DOWNLOAD = 'download';
59
+
60
+    public function __construct(
61
+        string $appName,
62
+        IRequest $request,
63
+        protected IConfig $config,
64
+        IURLGenerator $urlGenerator,
65
+        protected IUserManager $userManager,
66
+        protected \OCP\Activity\IManager $activityManager,
67
+        protected ShareManager $shareManager,
68
+        ISession $session,
69
+        protected IPreview $previewManager,
70
+        protected IRootFolder $rootFolder,
71
+        protected FederatedShareProvider $federatedShareProvider,
72
+        protected IAccountManager $accountManager,
73
+        protected IEventDispatcher $eventDispatcher,
74
+        protected IL10N $l10n,
75
+        protected ISecureRandom $secureRandom,
76
+        protected Defaults $defaults,
77
+        private IPublicShareTemplateFactory $publicShareTemplateFactory,
78
+    ) {
79
+        parent::__construct($appName, $request, $session, $urlGenerator);
80
+    }
81
+
82
+    /**
83
+     * Show the authentication page
84
+     * The form has to submit to the authenticate method route
85
+     */
86
+    #[PublicPage]
87
+    #[NoCSRFRequired]
88
+    public function showAuthenticate(): TemplateResponse {
89
+        $templateParameters = ['share' => $this->share];
90
+
91
+        $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
92
+
93
+        $response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
94
+        if ($this->share->getSendPasswordByTalk()) {
95
+            $csp = new ContentSecurityPolicy();
96
+            $csp->addAllowedConnectDomain('*');
97
+            $csp->addAllowedMediaDomain('blob:');
98
+            $response->setContentSecurityPolicy($csp);
99
+        }
100
+
101
+        return $response;
102
+    }
103
+
104
+    /**
105
+     * The template to show when authentication failed
106
+     */
107
+    protected function showAuthFailed(): TemplateResponse {
108
+        $templateParameters = ['share' => $this->share, 'wrongpw' => true];
109
+
110
+        $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
111
+
112
+        $response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
113
+        if ($this->share->getSendPasswordByTalk()) {
114
+            $csp = new ContentSecurityPolicy();
115
+            $csp->addAllowedConnectDomain('*');
116
+            $csp->addAllowedMediaDomain('blob:');
117
+            $response->setContentSecurityPolicy($csp);
118
+        }
119
+
120
+        return $response;
121
+    }
122
+
123
+    /**
124
+     * The template to show after user identification
125
+     */
126
+    protected function showIdentificationResult(bool $success = false): TemplateResponse {
127
+        $templateParameters = ['share' => $this->share, 'identityOk' => $success];
128
+
129
+        $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
130
+
131
+        $response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
132
+        if ($this->share->getSendPasswordByTalk()) {
133
+            $csp = new ContentSecurityPolicy();
134
+            $csp->addAllowedConnectDomain('*');
135
+            $csp->addAllowedMediaDomain('blob:');
136
+            $response->setContentSecurityPolicy($csp);
137
+        }
138
+
139
+        return $response;
140
+    }
141
+
142
+    /**
143
+     * Validate the identity token of a public share
144
+     *
145
+     * @param ?string $identityToken
146
+     * @return bool
147
+     */
148
+    protected function validateIdentity(?string $identityToken = null): bool {
149
+        if ($this->share->getShareType() !== IShare::TYPE_EMAIL) {
150
+            return false;
151
+        }
152
+
153
+        if ($identityToken === null || $this->share->getSharedWith() === null) {
154
+            return false;
155
+        }
156
+
157
+        return $identityToken === $this->share->getSharedWith();
158
+    }
159
+
160
+    /**
161
+     * Generates a password for the share, respecting any password policy defined
162
+     */
163
+    protected function generatePassword(): void {
164
+        $event = new GenerateSecurePasswordEvent(PasswordContext::SHARING);
165
+        $this->eventDispatcher->dispatchTyped($event);
166
+        $password = $event->getPassword() ?? $this->secureRandom->generate(20);
167
+
168
+        $this->share->setPassword($password);
169
+        $this->shareManager->updateShare($this->share);
170
+    }
171
+
172
+    protected function verifyPassword(string $password): bool {
173
+        return $this->shareManager->checkPassword($this->share, $password);
174
+    }
175
+
176
+    protected function getPasswordHash(): ?string {
177
+        return $this->share->getPassword();
178
+    }
179
+
180
+    public function isValidToken(): bool {
181
+        try {
182
+            $this->share = $this->shareManager->getShareByToken($this->getToken());
183
+        } catch (ShareNotFound $e) {
184
+            return false;
185
+        }
186
+
187
+        return true;
188
+    }
189
+
190
+    protected function isPasswordProtected(): bool {
191
+        return $this->share->getPassword() !== null;
192
+    }
193
+
194
+    protected function authSucceeded() {
195
+        if ($this->share === null) {
196
+            throw new NotFoundException();
197
+        }
198
+
199
+        // For share this was always set so it is still used in other apps
200
+        $allowedShareIds = $this->session->get(PublicAuth::DAV_AUTHENTICATED);
201
+        if (!is_array($allowedShareIds)) {
202
+            $allowedShareIds = [];
203
+        }
204
+
205
+        $this->session->set(PublicAuth::DAV_AUTHENTICATED, array_merge($allowedShareIds, [$this->share->getId()]));
206
+    }
207
+
208
+    protected function authFailed() {
209
+        $this->emitAccessShareHook($this->share, 403, 'Wrong password');
210
+        $this->emitShareAccessEvent($this->share, self::SHARE_AUTH, 403, 'Wrong password');
211
+    }
212
+
213
+    /**
214
+     * throws hooks when a share is attempted to be accessed
215
+     *
216
+     * @param IShare|string $share the Share instance if available,
217
+     *                             otherwise token
218
+     * @param int $errorCode
219
+     * @param string $errorMessage
220
+     *
221
+     * @throws HintException
222
+     * @throws \OC\ServerNotAvailableException
223
+     *
224
+     * @deprecated use OCP\Files_Sharing\Event\ShareLinkAccessedEvent
225
+     */
226
+    protected function emitAccessShareHook($share, int $errorCode = 200, string $errorMessage = '') {
227
+        $itemType = $itemSource = $uidOwner = '';
228
+        $token = $share;
229
+        $exception = null;
230
+        if ($share instanceof IShare) {
231
+            try {
232
+                $token = $share->getToken();
233
+                $uidOwner = $share->getSharedBy();
234
+                $itemType = $share->getNodeType();
235
+                $itemSource = $share->getNodeId();
236
+            } catch (\Exception $e) {
237
+                // we log what we know and pass on the exception afterwards
238
+                $exception = $e;
239
+            }
240
+        }
241
+
242
+        \OC_Hook::emit(Share::class, 'share_link_access', [
243
+            'itemType' => $itemType,
244
+            'itemSource' => $itemSource,
245
+            'uidOwner' => $uidOwner,
246
+            'token' => $token,
247
+            'errorCode' => $errorCode,
248
+            'errorMessage' => $errorMessage
249
+        ]);
250
+
251
+        if (!is_null($exception)) {
252
+            throw $exception;
253
+        }
254
+    }
255
+
256
+    /**
257
+     * Emit a ShareLinkAccessedEvent event when a share is accessed, downloaded, auth...
258
+     */
259
+    protected function emitShareAccessEvent(IShare $share, string $step = '', int $errorCode = 200, string $errorMessage = ''): void {
260
+        if ($step !== self::SHARE_ACCESS
261
+            && $step !== self::SHARE_AUTH
262
+            && $step !== self::SHARE_DOWNLOAD) {
263
+            return;
264
+        }
265
+        $this->eventDispatcher->dispatchTyped(new ShareLinkAccessedEvent($share, $step, $errorCode, $errorMessage));
266
+    }
267
+
268
+    /**
269
+     * Validate the permissions of the share
270
+     *
271
+     * @param Share\IShare $share
272
+     * @return bool
273
+     */
274
+    private function validateShare(IShare $share) {
275
+        // If the owner is disabled no access to the link is granted
276
+        $owner = $this->userManager->get($share->getShareOwner());
277
+        if ($owner === null || !$owner->isEnabled()) {
278
+            return false;
279
+        }
280
+
281
+        // If the initiator of the share is disabled no access is granted
282
+        $initiator = $this->userManager->get($share->getSharedBy());
283
+        if ($initiator === null || !$initiator->isEnabled()) {
284
+            return false;
285
+        }
286
+
287
+        return $share->getNode()->isReadable() && $share->getNode()->isShareable();
288
+    }
289
+
290
+    /**
291
+     * @param string $path
292
+     * @return TemplateResponse
293
+     * @throws NotFoundException
294
+     * @throws \Exception
295
+     */
296
+    #[PublicPage]
297
+    #[NoCSRFRequired]
298
+    public function showShare($path = ''): TemplateResponse {
299
+        \OC_User::setIncognitoMode(true);
300
+
301
+        // Check whether share exists
302
+        try {
303
+            $share = $this->shareManager->getShareByToken($this->getToken());
304
+        } catch (ShareNotFound $e) {
305
+            // The share does not exists, we do not emit an ShareLinkAccessedEvent
306
+            $this->emitAccessShareHook($this->getToken(), 404, 'Share not found');
307
+            throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
308
+        }
309
+
310
+        if (!$this->validateShare($share)) {
311
+            throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
312
+        }
313
+
314
+        $shareNode = $share->getNode();
315
+
316
+        try {
317
+            $templateProvider = $this->publicShareTemplateFactory->getProvider($share);
318
+            $response = $templateProvider->renderPage($share, $this->getToken(), $path);
319
+        } catch (NotFoundException $e) {
320
+            $this->emitAccessShareHook($share, 404, 'Share not found');
321
+            $this->emitShareAccessEvent($share, ShareController::SHARE_ACCESS, 404, 'Share not found');
322
+            throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
323
+        }
324
+
325
+        // We can't get the path of a file share
326
+        try {
327
+            if ($shareNode instanceof File && $path !== '') {
328
+                $this->emitAccessShareHook($share, 404, 'Share not found');
329
+                $this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found');
330
+                throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
331
+            }
332
+        } catch (\Exception $e) {
333
+            $this->emitAccessShareHook($share, 404, 'Share not found');
334
+            $this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found');
335
+            throw $e;
336
+        }
337
+
338
+
339
+        $this->emitAccessShareHook($share);
340
+        $this->emitShareAccessEvent($share, self::SHARE_ACCESS);
341
+
342
+        return $response;
343
+    }
344
+
345
+    /**
346
+     * @throws NotFoundException
347
+     * @deprecated 31.0.0 Users are encouraged to use the DAV endpoint
348
+     */
349
+    #[PublicPage]
350
+    #[NoCSRFRequired]
351
+    #[NoSameSiteCookieRequired]
352
+    public function downloadShare(string $token, ?string $files = null, string $path = ''): NotFoundResponse|RedirectResponse|DataResponse {
353
+        \OC_User::setIncognitoMode(true);
354
+
355
+        $share = $this->shareManager->getShareByToken($token);
356
+
357
+        if (!($share->getPermissions() & Constants::PERMISSION_READ)) {
358
+            return new DataResponse('Share has no read permission');
359
+        }
360
+
361
+        $attributes = $share->getAttributes();
362
+        if ($attributes?->getAttribute('permissions', 'download') === false) {
363
+            return new DataResponse('Share has no download permission');
364
+        }
365
+
366
+        if (!$this->validateShare($share)) {
367
+            throw new NotFoundException();
368
+        }
369
+
370
+        $node = $share->getNode();
371
+        if ($node instanceof Folder) {
372
+            // Directory share
373
+
374
+            // Try to get the path
375
+            if ($path !== '') {
376
+                try {
377
+                    $node = $node->get($path);
378
+                } catch (NotFoundException $e) {
379
+                    $this->emitAccessShareHook($share, 404, 'Share not found');
380
+                    $this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD, 404, 'Share not found');
381
+                    return new NotFoundResponse();
382
+                }
383
+            }
384
+
385
+            if ($node instanceof Folder) {
386
+                if ($files === null || $files === '') {
387
+                    if ($share->getHideDownload()) {
388
+                        throw new NotFoundException('Downloading a folder');
389
+                    }
390
+                }
391
+            }
392
+        }
393
+
394
+        $this->emitAccessShareHook($share);
395
+        $this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD);
396
+
397
+        $davUrl = '/public.php/dav/files/' . $token . '/?accept=zip';
398
+        if ($files !== null) {
399
+            $davUrl .= '&files=' . $files;
400
+        }
401
+        return new RedirectResponse($this->urlGenerator->getAbsoluteURL($davUrl));
402
+    }
403 403
 }
Please login to merge, or discard this patch.
Spacing   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -349,7 +349,7 @@  discard block
 block discarded – undo
349 349
 	#[PublicPage]
350 350
 	#[NoCSRFRequired]
351 351
 	#[NoSameSiteCookieRequired]
352
-	public function downloadShare(string $token, ?string $files = null, string $path = ''): NotFoundResponse|RedirectResponse|DataResponse {
352
+	public function downloadShare(string $token, ?string $files = null, string $path = ''): NotFoundResponse | RedirectResponse | DataResponse {
353 353
 		\OC_User::setIncognitoMode(true);
354 354
 
355 355
 		$share = $this->shareManager->getShareByToken($token);
@@ -394,9 +394,9 @@  discard block
 block discarded – undo
394 394
 		$this->emitAccessShareHook($share);
395 395
 		$this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD);
396 396
 
397
-		$davUrl = '/public.php/dav/files/' . $token . '/?accept=zip';
397
+		$davUrl = '/public.php/dav/files/'.$token.'/?accept=zip';
398 398
 		if ($files !== null) {
399
-			$davUrl .= '&files=' . $files;
399
+			$davUrl .= '&files='.$files;
400 400
 		}
401 401
 		return new RedirectResponse($this->urlGenerator->getAbsoluteURL($davUrl));
402 402
 	}
Please login to merge, or discard this patch.
apps/files_sharing/lib/Controller/PublicPreviewController.php 1 patch
Indentation   +175 added lines, -175 removed lines patch added patch discarded remove patch
@@ -28,179 +28,179 @@
 block discarded – undo
28 28
 
29 29
 class PublicPreviewController extends PublicShareController {
30 30
 
31
-	/** @var IShare */
32
-	private $share;
33
-
34
-	public function __construct(
35
-		string $appName,
36
-		IRequest $request,
37
-		private ShareManager $shareManager,
38
-		ISession $session,
39
-		private IPreview $previewManager,
40
-		private IMimeIconProvider $mimeIconProvider,
41
-	) {
42
-		parent::__construct($appName, $request, $session);
43
-	}
44
-
45
-	protected function getPasswordHash(): ?string {
46
-		return $this->share->getPassword();
47
-	}
48
-
49
-	public function isValidToken(): bool {
50
-		try {
51
-			$this->share = $this->shareManager->getShareByToken($this->getToken());
52
-			return true;
53
-		} catch (ShareNotFound $e) {
54
-			return false;
55
-		}
56
-	}
57
-
58
-	protected function isPasswordProtected(): bool {
59
-		return $this->share->getPassword() !== null;
60
-	}
61
-
62
-
63
-	/**
64
-	 * Get a preview for a shared file
65
-	 *
66
-	 * @param string $token Token of the share
67
-	 * @param string $file File in the share
68
-	 * @param int $x Width of the preview
69
-	 * @param int $y Height of the preview
70
-	 * @param bool $a Whether to not crop the preview
71
-	 * @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available
72
-	 * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
73
-	 *
74
-	 * 200: Preview returned
75
-	 * 303: Redirect to the mime icon url if mimeFallback is true
76
-	 * 400: Getting preview is not possible
77
-	 * 403: Getting preview is not allowed
78
-	 * 404: Share or preview not found
79
-	 */
80
-	#[PublicPage]
81
-	#[NoCSRFRequired]
82
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
83
-	public function getPreview(
84
-		string $token,
85
-		string $file = '',
86
-		int $x = 32,
87
-		int $y = 32,
88
-		$a = false,
89
-		bool $mimeFallback = false,
90
-	) {
91
-		$cacheForSeconds = 60 * 60 * 24; // 1 day
92
-
93
-		if ($token === '' || $x === 0 || $y === 0) {
94
-			return new DataResponse([], Http::STATUS_BAD_REQUEST);
95
-		}
96
-
97
-		try {
98
-			$share = $this->shareManager->getShareByToken($token);
99
-		} catch (ShareNotFound $e) {
100
-			return new DataResponse([], Http::STATUS_NOT_FOUND);
101
-		}
102
-
103
-		if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) {
104
-			return new DataResponse([], Http::STATUS_FORBIDDEN);
105
-		}
106
-
107
-		// Only explicitly set to false will forbid the download!
108
-		$downloadForbidden = !$share->canSeeContent();
109
-
110
-		// Is this header is set it means our UI is doing a preview for no-download shares
111
-		// we check a header so we at least prevent people from using the link directly (obfuscation)
112
-		$isPublicPreview = $this->request->getHeader('x-nc-preview') === 'true';
113
-
114
-		if ($isPublicPreview && $downloadForbidden) {
115
-			// Only cache for 15 minutes on public preview requests to quickly remove from cache
116
-			$cacheForSeconds = 15 * 60;
117
-		} elseif ($downloadForbidden) {
118
-			// This is not a public share preview so we only allow a preview if download permissions are granted
119
-			return new DataResponse([], Http::STATUS_FORBIDDEN);
120
-		}
121
-
122
-		try {
123
-			$node = $share->getNode();
124
-			if ($node instanceof Folder) {
125
-				$file = $node->get($file);
126
-			} else {
127
-				$file = $node;
128
-			}
129
-
130
-			$f = $this->previewManager->getPreview($file, $x, $y, !$a);
131
-			$response = new FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]);
132
-			$response->cacheFor($cacheForSeconds);
133
-			return $response;
134
-		} catch (NotFoundException $e) {
135
-			// If we have no preview enabled, we can redirect to the mime icon if any
136
-			if ($mimeFallback) {
137
-				if ($url = $this->mimeIconProvider->getMimeIconUrl($file->getMimeType())) {
138
-					return new RedirectResponse($url);
139
-				}
140
-			}
141
-			return new DataResponse([], Http::STATUS_NOT_FOUND);
142
-		} catch (\InvalidArgumentException $e) {
143
-			return new DataResponse([], Http::STATUS_BAD_REQUEST);
144
-		}
145
-	}
146
-
147
-	/**
148
-	 * Get a direct link preview for a shared file
149
-	 *
150
-	 * @param string $token Token of the share
151
-	 * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>
152
-	 *
153
-	 * 200: Preview returned
154
-	 * 400: Getting preview is not possible
155
-	 * 403: Getting preview is not allowed
156
-	 * 404: Share or preview not found
157
-	 */
158
-	#[PublicPage]
159
-	#[NoCSRFRequired]
160
-	#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
161
-	#[NoSameSiteCookieRequired]
162
-	public function directLink(string $token) {
163
-		// No token no image
164
-		if ($token === '') {
165
-			return new DataResponse([], Http::STATUS_BAD_REQUEST);
166
-		}
167
-
168
-		// No share no image
169
-		try {
170
-			$share = $this->shareManager->getShareByToken($token);
171
-		} catch (ShareNotFound $e) {
172
-			return new DataResponse([], Http::STATUS_NOT_FOUND);
173
-		}
174
-
175
-		// No permissions no image
176
-		if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) {
177
-			return new DataResponse([], Http::STATUS_FORBIDDEN);
178
-		}
179
-
180
-		// Password protected shares have no direct link!
181
-		if ($share->getPassword() !== null) {
182
-			return new DataResponse([], Http::STATUS_FORBIDDEN);
183
-		}
184
-
185
-		if (!$share->canSeeContent()) {
186
-			return new DataResponse([], Http::STATUS_FORBIDDEN);
187
-		}
188
-
189
-		try {
190
-			$node = $share->getNode();
191
-			if ($node instanceof Folder) {
192
-				// Direct link only works for single files
193
-				return new DataResponse([], Http::STATUS_BAD_REQUEST);
194
-			}
195
-
196
-			$f = $this->previewManager->getPreview($node, -1, -1, false);
197
-			$response = new FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]);
198
-			$response->cacheFor(3600 * 24);
199
-			return $response;
200
-		} catch (NotFoundException $e) {
201
-			return new DataResponse([], Http::STATUS_NOT_FOUND);
202
-		} catch (\InvalidArgumentException $e) {
203
-			return new DataResponse([], Http::STATUS_BAD_REQUEST);
204
-		}
205
-	}
31
+    /** @var IShare */
32
+    private $share;
33
+
34
+    public function __construct(
35
+        string $appName,
36
+        IRequest $request,
37
+        private ShareManager $shareManager,
38
+        ISession $session,
39
+        private IPreview $previewManager,
40
+        private IMimeIconProvider $mimeIconProvider,
41
+    ) {
42
+        parent::__construct($appName, $request, $session);
43
+    }
44
+
45
+    protected function getPasswordHash(): ?string {
46
+        return $this->share->getPassword();
47
+    }
48
+
49
+    public function isValidToken(): bool {
50
+        try {
51
+            $this->share = $this->shareManager->getShareByToken($this->getToken());
52
+            return true;
53
+        } catch (ShareNotFound $e) {
54
+            return false;
55
+        }
56
+    }
57
+
58
+    protected function isPasswordProtected(): bool {
59
+        return $this->share->getPassword() !== null;
60
+    }
61
+
62
+
63
+    /**
64
+     * Get a preview for a shared file
65
+     *
66
+     * @param string $token Token of the share
67
+     * @param string $file File in the share
68
+     * @param int $x Width of the preview
69
+     * @param int $y Height of the preview
70
+     * @param bool $a Whether to not crop the preview
71
+     * @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available
72
+     * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
73
+     *
74
+     * 200: Preview returned
75
+     * 303: Redirect to the mime icon url if mimeFallback is true
76
+     * 400: Getting preview is not possible
77
+     * 403: Getting preview is not allowed
78
+     * 404: Share or preview not found
79
+     */
80
+    #[PublicPage]
81
+    #[NoCSRFRequired]
82
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
83
+    public function getPreview(
84
+        string $token,
85
+        string $file = '',
86
+        int $x = 32,
87
+        int $y = 32,
88
+        $a = false,
89
+        bool $mimeFallback = false,
90
+    ) {
91
+        $cacheForSeconds = 60 * 60 * 24; // 1 day
92
+
93
+        if ($token === '' || $x === 0 || $y === 0) {
94
+            return new DataResponse([], Http::STATUS_BAD_REQUEST);
95
+        }
96
+
97
+        try {
98
+            $share = $this->shareManager->getShareByToken($token);
99
+        } catch (ShareNotFound $e) {
100
+            return new DataResponse([], Http::STATUS_NOT_FOUND);
101
+        }
102
+
103
+        if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) {
104
+            return new DataResponse([], Http::STATUS_FORBIDDEN);
105
+        }
106
+
107
+        // Only explicitly set to false will forbid the download!
108
+        $downloadForbidden = !$share->canSeeContent();
109
+
110
+        // Is this header is set it means our UI is doing a preview for no-download shares
111
+        // we check a header so we at least prevent people from using the link directly (obfuscation)
112
+        $isPublicPreview = $this->request->getHeader('x-nc-preview') === 'true';
113
+
114
+        if ($isPublicPreview && $downloadForbidden) {
115
+            // Only cache for 15 minutes on public preview requests to quickly remove from cache
116
+            $cacheForSeconds = 15 * 60;
117
+        } elseif ($downloadForbidden) {
118
+            // This is not a public share preview so we only allow a preview if download permissions are granted
119
+            return new DataResponse([], Http::STATUS_FORBIDDEN);
120
+        }
121
+
122
+        try {
123
+            $node = $share->getNode();
124
+            if ($node instanceof Folder) {
125
+                $file = $node->get($file);
126
+            } else {
127
+                $file = $node;
128
+            }
129
+
130
+            $f = $this->previewManager->getPreview($file, $x, $y, !$a);
131
+            $response = new FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]);
132
+            $response->cacheFor($cacheForSeconds);
133
+            return $response;
134
+        } catch (NotFoundException $e) {
135
+            // If we have no preview enabled, we can redirect to the mime icon if any
136
+            if ($mimeFallback) {
137
+                if ($url = $this->mimeIconProvider->getMimeIconUrl($file->getMimeType())) {
138
+                    return new RedirectResponse($url);
139
+                }
140
+            }
141
+            return new DataResponse([], Http::STATUS_NOT_FOUND);
142
+        } catch (\InvalidArgumentException $e) {
143
+            return new DataResponse([], Http::STATUS_BAD_REQUEST);
144
+        }
145
+    }
146
+
147
+    /**
148
+     * Get a direct link preview for a shared file
149
+     *
150
+     * @param string $token Token of the share
151
+     * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>
152
+     *
153
+     * 200: Preview returned
154
+     * 400: Getting preview is not possible
155
+     * 403: Getting preview is not allowed
156
+     * 404: Share or preview not found
157
+     */
158
+    #[PublicPage]
159
+    #[NoCSRFRequired]
160
+    #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
161
+    #[NoSameSiteCookieRequired]
162
+    public function directLink(string $token) {
163
+        // No token no image
164
+        if ($token === '') {
165
+            return new DataResponse([], Http::STATUS_BAD_REQUEST);
166
+        }
167
+
168
+        // No share no image
169
+        try {
170
+            $share = $this->shareManager->getShareByToken($token);
171
+        } catch (ShareNotFound $e) {
172
+            return new DataResponse([], Http::STATUS_NOT_FOUND);
173
+        }
174
+
175
+        // No permissions no image
176
+        if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) {
177
+            return new DataResponse([], Http::STATUS_FORBIDDEN);
178
+        }
179
+
180
+        // Password protected shares have no direct link!
181
+        if ($share->getPassword() !== null) {
182
+            return new DataResponse([], Http::STATUS_FORBIDDEN);
183
+        }
184
+
185
+        if (!$share->canSeeContent()) {
186
+            return new DataResponse([], Http::STATUS_FORBIDDEN);
187
+        }
188
+
189
+        try {
190
+            $node = $share->getNode();
191
+            if ($node instanceof Folder) {
192
+                // Direct link only works for single files
193
+                return new DataResponse([], Http::STATUS_BAD_REQUEST);
194
+            }
195
+
196
+            $f = $this->previewManager->getPreview($node, -1, -1, false);
197
+            $response = new FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]);
198
+            $response->cacheFor(3600 * 24);
199
+            return $response;
200
+        } catch (NotFoundException $e) {
201
+            return new DataResponse([], Http::STATUS_NOT_FOUND);
202
+        } catch (\InvalidArgumentException $e) {
203
+            return new DataResponse([], Http::STATUS_BAD_REQUEST);
204
+        }
205
+    }
206 206
 }
Please login to merge, or discard this patch.
lib/private/AppFramework/Http/Dispatcher.php 1 patch
Indentation   +180 added lines, -180 removed lines patch added patch discarded remove patch
@@ -26,184 +26,184 @@
 block discarded – undo
26 26
  * Class to dispatch the request to the middleware dispatcher
27 27
  */
28 28
 class Dispatcher {
29
-	/**
30
-	 * @param Http $protocol the http protocol with contains all status headers
31
-	 * @param MiddlewareDispatcher $middlewareDispatcher the dispatcher which
32
-	 *                                                   runs the middleware
33
-	 */
34
-	public function __construct(
35
-		private readonly Http $protocol,
36
-		private readonly MiddlewareDispatcher $middlewareDispatcher,
37
-		private readonly ControllerMethodReflector $reflector,
38
-		private readonly IRequest $request,
39
-		private readonly IConfig $config,
40
-		private readonly ConnectionAdapter $connection,
41
-		private readonly LoggerInterface $logger,
42
-		private readonly IEventLogger $eventLogger,
43
-		private readonly ContainerInterface $appContainer,
44
-	) {
45
-	}
46
-
47
-
48
-	/**
49
-	 * Handles a request and calls the dispatcher on the controller
50
-	 * @param Controller $controller the controller which will be called
51
-	 * @param string $methodName the method name which will be called on
52
-	 *                           the controller
53
-	 * @return array{0: string, 1: array, 2: array, 3: string, 4: Response}
54
-	 *                                                                      $array[0] contains the http status header as a string,
55
-	 *                                                                      $array[1] contains response headers as an array,
56
-	 *                                                                      $array[2] contains response cookies as an array,
57
-	 *                                                                      $array[3] contains the response output as a string,
58
-	 *                                                                      $array[4] contains the response object
59
-	 * @throws \Exception
60
-	 */
61
-	public function dispatch(Controller $controller, string $methodName): array {
62
-		try {
63
-			// prefill reflector with everything that's needed for the
64
-			// middlewares
65
-			$this->reflector->reflect($controller, $methodName);
66
-
67
-			$this->middlewareDispatcher->beforeController($controller,
68
-				$methodName);
69
-
70
-			$databaseStatsBefore = [];
71
-			if ($this->config->getSystemValueBool('debug', false)) {
72
-				$databaseStatsBefore = $this->connection->getInner()->getStats();
73
-			}
74
-
75
-			$response = $this->executeController($controller, $methodName);
76
-
77
-			if (!empty($databaseStatsBefore)) {
78
-				$databaseStatsAfter = $this->connection->getInner()->getStats();
79
-				$numBuilt = $databaseStatsAfter['built'] - $databaseStatsBefore['built'];
80
-				$numExecuted = $databaseStatsAfter['executed'] - $databaseStatsBefore['executed'];
81
-
82
-				if ($numBuilt > 50) {
83
-					$this->logger->debug('Controller {class}::{method} created {count} QueryBuilder objects, please check if they are created inside a loop by accident.', [
84
-						'class' => get_class($controller),
85
-						'method' => $methodName,
86
-						'count' => $numBuilt,
87
-					]);
88
-				}
89
-
90
-				if ($numExecuted > 100) {
91
-					$this->logger->warning('Controller {class}::{method} executed {count} queries.', [
92
-						'class' => get_class($controller),
93
-						'method' => $methodName,
94
-						'count' => $numExecuted,
95
-					]);
96
-				}
97
-			}
98
-
99
-			// if an exception appears, the middleware checks if it can handle the
100
-			// exception and creates a response. If no response is created, it is
101
-			// assumed that there's no middleware who can handle it and the error is
102
-			// thrown again
103
-		} catch (\Exception $exception) {
104
-			$response = $this->middlewareDispatcher->afterException(
105
-				$controller, $methodName, $exception);
106
-		} catch (\Throwable $throwable) {
107
-			$exception = new \Exception($throwable->getMessage() . ' in file \'' . $throwable->getFile() . '\' line ' . $throwable->getLine(), $throwable->getCode(), $throwable);
108
-			$response = $this->middlewareDispatcher->afterException(
109
-				$controller, $methodName, $exception);
110
-		}
111
-
112
-		$response = $this->middlewareDispatcher->afterController(
113
-			$controller, $methodName, $response);
114
-
115
-		// depending on the cache object the headers need to be changed
116
-		return [
117
-			$this->protocol->getStatusHeader($response->getStatus()),
118
-			array_merge($response->getHeaders()),
119
-			$response->getCookies(),
120
-			$this->middlewareDispatcher->beforeOutput(
121
-				$controller, $methodName, $response->render()
122
-			),
123
-			$response,
124
-		];
125
-	}
126
-
127
-
128
-	/**
129
-	 * Uses the reflected parameters, types and request parameters to execute
130
-	 * the controller
131
-	 * @param Controller $controller the controller to be executed
132
-	 * @param string $methodName the method on the controller that should be executed
133
-	 * @return Response
134
-	 */
135
-	private function executeController(Controller $controller, string $methodName): Response {
136
-		$arguments = [];
137
-
138
-		// valid types that will be cast
139
-		$types = ['int', 'integer', 'bool', 'boolean', 'float', 'double'];
140
-
141
-		foreach ($this->reflector->getParameters() as $param => $default) {
142
-			// try to get the parameter from the request object and cast
143
-			// it to the type annotated in the @param annotation
144
-			$value = $this->request->getParam($param, $default);
145
-			$type = $this->reflector->getType($param);
146
-
147
-			// Converted the string `'false'` to false when the controller wants a boolean
148
-			if ($value === 'false' && ($type === 'bool' || $type === 'boolean')) {
149
-				$value = false;
150
-			} elseif ($value !== null && \in_array($type, $types, true)) {
151
-				settype($value, $type);
152
-				$this->ensureParameterValueSatisfiesRange($param, $value);
153
-			} elseif ($value === null && $type !== null && $this->appContainer->has($type)) {
154
-				$value = $this->appContainer->get($type);
155
-			}
156
-
157
-			$arguments[] = $value;
158
-		}
159
-
160
-		$this->eventLogger->start('controller:' . get_class($controller) . '::' . $methodName, 'App framework controller execution');
161
-		try {
162
-			$response = \call_user_func_array([$controller, $methodName], $arguments);
163
-		} catch (\TypeError $e) {
164
-			// Only intercept TypeErrors occurring on the first line, meaning that the invocation of the controller method failed.
165
-			// Any other TypeError happens inside the controller method logic and should be logged as normal.
166
-			if ($e->getFile() === $this->reflector->getFile() && $e->getLine() === $this->reflector->getStartLine()) {
167
-				$this->logger->debug('Failed to call controller method: ' . $e->getMessage(), ['exception' => $e]);
168
-				return new Response(Http::STATUS_BAD_REQUEST);
169
-			}
170
-
171
-			throw $e;
172
-		}
173
-		$this->eventLogger->end('controller:' . get_class($controller) . '::' . $methodName);
174
-
175
-		if (!($response instanceof Response)) {
176
-			$this->logger->debug($controller::class . '::' . $methodName . ' returned raw data. Please wrap it in a Response or one of it\'s inheritors.');
177
-		}
178
-
179
-		// format response
180
-		if ($response instanceof DataResponse || !($response instanceof Response)) {
181
-			$format = $this->request->getFormat();
182
-			if ($format !== null && $controller->isResponderRegistered($format)) {
183
-				$response = $controller->buildResponse($response, $format);
184
-			} else {
185
-				$response = $controller->buildResponse($response);
186
-			}
187
-		}
188
-
189
-		return $response;
190
-	}
191
-
192
-	/**
193
-	 * @psalm-param mixed $value
194
-	 * @throws ParameterOutOfRangeException
195
-	 */
196
-	private function ensureParameterValueSatisfiesRange(string $param, $value): void {
197
-		$rangeInfo = $this->reflector->getRange($param);
198
-		if ($rangeInfo) {
199
-			if ($value < $rangeInfo['min'] || $value > $rangeInfo['max']) {
200
-				throw new ParameterOutOfRangeException(
201
-					$param,
202
-					$value,
203
-					$rangeInfo['min'],
204
-					$rangeInfo['max'],
205
-				);
206
-			}
207
-		}
208
-	}
29
+    /**
30
+     * @param Http $protocol the http protocol with contains all status headers
31
+     * @param MiddlewareDispatcher $middlewareDispatcher the dispatcher which
32
+     *                                                   runs the middleware
33
+     */
34
+    public function __construct(
35
+        private readonly Http $protocol,
36
+        private readonly MiddlewareDispatcher $middlewareDispatcher,
37
+        private readonly ControllerMethodReflector $reflector,
38
+        private readonly IRequest $request,
39
+        private readonly IConfig $config,
40
+        private readonly ConnectionAdapter $connection,
41
+        private readonly LoggerInterface $logger,
42
+        private readonly IEventLogger $eventLogger,
43
+        private readonly ContainerInterface $appContainer,
44
+    ) {
45
+    }
46
+
47
+
48
+    /**
49
+     * Handles a request and calls the dispatcher on the controller
50
+     * @param Controller $controller the controller which will be called
51
+     * @param string $methodName the method name which will be called on
52
+     *                           the controller
53
+     * @return array{0: string, 1: array, 2: array, 3: string, 4: Response}
54
+     *                                                                      $array[0] contains the http status header as a string,
55
+     *                                                                      $array[1] contains response headers as an array,
56
+     *                                                                      $array[2] contains response cookies as an array,
57
+     *                                                                      $array[3] contains the response output as a string,
58
+     *                                                                      $array[4] contains the response object
59
+     * @throws \Exception
60
+     */
61
+    public function dispatch(Controller $controller, string $methodName): array {
62
+        try {
63
+            // prefill reflector with everything that's needed for the
64
+            // middlewares
65
+            $this->reflector->reflect($controller, $methodName);
66
+
67
+            $this->middlewareDispatcher->beforeController($controller,
68
+                $methodName);
69
+
70
+            $databaseStatsBefore = [];
71
+            if ($this->config->getSystemValueBool('debug', false)) {
72
+                $databaseStatsBefore = $this->connection->getInner()->getStats();
73
+            }
74
+
75
+            $response = $this->executeController($controller, $methodName);
76
+
77
+            if (!empty($databaseStatsBefore)) {
78
+                $databaseStatsAfter = $this->connection->getInner()->getStats();
79
+                $numBuilt = $databaseStatsAfter['built'] - $databaseStatsBefore['built'];
80
+                $numExecuted = $databaseStatsAfter['executed'] - $databaseStatsBefore['executed'];
81
+
82
+                if ($numBuilt > 50) {
83
+                    $this->logger->debug('Controller {class}::{method} created {count} QueryBuilder objects, please check if they are created inside a loop by accident.', [
84
+                        'class' => get_class($controller),
85
+                        'method' => $methodName,
86
+                        'count' => $numBuilt,
87
+                    ]);
88
+                }
89
+
90
+                if ($numExecuted > 100) {
91
+                    $this->logger->warning('Controller {class}::{method} executed {count} queries.', [
92
+                        'class' => get_class($controller),
93
+                        'method' => $methodName,
94
+                        'count' => $numExecuted,
95
+                    ]);
96
+                }
97
+            }
98
+
99
+            // if an exception appears, the middleware checks if it can handle the
100
+            // exception and creates a response. If no response is created, it is
101
+            // assumed that there's no middleware who can handle it and the error is
102
+            // thrown again
103
+        } catch (\Exception $exception) {
104
+            $response = $this->middlewareDispatcher->afterException(
105
+                $controller, $methodName, $exception);
106
+        } catch (\Throwable $throwable) {
107
+            $exception = new \Exception($throwable->getMessage() . ' in file \'' . $throwable->getFile() . '\' line ' . $throwable->getLine(), $throwable->getCode(), $throwable);
108
+            $response = $this->middlewareDispatcher->afterException(
109
+                $controller, $methodName, $exception);
110
+        }
111
+
112
+        $response = $this->middlewareDispatcher->afterController(
113
+            $controller, $methodName, $response);
114
+
115
+        // depending on the cache object the headers need to be changed
116
+        return [
117
+            $this->protocol->getStatusHeader($response->getStatus()),
118
+            array_merge($response->getHeaders()),
119
+            $response->getCookies(),
120
+            $this->middlewareDispatcher->beforeOutput(
121
+                $controller, $methodName, $response->render()
122
+            ),
123
+            $response,
124
+        ];
125
+    }
126
+
127
+
128
+    /**
129
+     * Uses the reflected parameters, types and request parameters to execute
130
+     * the controller
131
+     * @param Controller $controller the controller to be executed
132
+     * @param string $methodName the method on the controller that should be executed
133
+     * @return Response
134
+     */
135
+    private function executeController(Controller $controller, string $methodName): Response {
136
+        $arguments = [];
137
+
138
+        // valid types that will be cast
139
+        $types = ['int', 'integer', 'bool', 'boolean', 'float', 'double'];
140
+
141
+        foreach ($this->reflector->getParameters() as $param => $default) {
142
+            // try to get the parameter from the request object and cast
143
+            // it to the type annotated in the @param annotation
144
+            $value = $this->request->getParam($param, $default);
145
+            $type = $this->reflector->getType($param);
146
+
147
+            // Converted the string `'false'` to false when the controller wants a boolean
148
+            if ($value === 'false' && ($type === 'bool' || $type === 'boolean')) {
149
+                $value = false;
150
+            } elseif ($value !== null && \in_array($type, $types, true)) {
151
+                settype($value, $type);
152
+                $this->ensureParameterValueSatisfiesRange($param, $value);
153
+            } elseif ($value === null && $type !== null && $this->appContainer->has($type)) {
154
+                $value = $this->appContainer->get($type);
155
+            }
156
+
157
+            $arguments[] = $value;
158
+        }
159
+
160
+        $this->eventLogger->start('controller:' . get_class($controller) . '::' . $methodName, 'App framework controller execution');
161
+        try {
162
+            $response = \call_user_func_array([$controller, $methodName], $arguments);
163
+        } catch (\TypeError $e) {
164
+            // Only intercept TypeErrors occurring on the first line, meaning that the invocation of the controller method failed.
165
+            // Any other TypeError happens inside the controller method logic and should be logged as normal.
166
+            if ($e->getFile() === $this->reflector->getFile() && $e->getLine() === $this->reflector->getStartLine()) {
167
+                $this->logger->debug('Failed to call controller method: ' . $e->getMessage(), ['exception' => $e]);
168
+                return new Response(Http::STATUS_BAD_REQUEST);
169
+            }
170
+
171
+            throw $e;
172
+        }
173
+        $this->eventLogger->end('controller:' . get_class($controller) . '::' . $methodName);
174
+
175
+        if (!($response instanceof Response)) {
176
+            $this->logger->debug($controller::class . '::' . $methodName . ' returned raw data. Please wrap it in a Response or one of it\'s inheritors.');
177
+        }
178
+
179
+        // format response
180
+        if ($response instanceof DataResponse || !($response instanceof Response)) {
181
+            $format = $this->request->getFormat();
182
+            if ($format !== null && $controller->isResponderRegistered($format)) {
183
+                $response = $controller->buildResponse($response, $format);
184
+            } else {
185
+                $response = $controller->buildResponse($response);
186
+            }
187
+        }
188
+
189
+        return $response;
190
+    }
191
+
192
+    /**
193
+     * @psalm-param mixed $value
194
+     * @throws ParameterOutOfRangeException
195
+     */
196
+    private function ensureParameterValueSatisfiesRange(string $param, $value): void {
197
+        $rangeInfo = $this->reflector->getRange($param);
198
+        if ($rangeInfo) {
199
+            if ($value < $rangeInfo['min'] || $value > $rangeInfo['max']) {
200
+                throw new ParameterOutOfRangeException(
201
+                    $param,
202
+                    $value,
203
+                    $rangeInfo['min'],
204
+                    $rangeInfo['max'],
205
+                );
206
+            }
207
+        }
208
+    }
209 209
 }
Please login to merge, or discard this patch.