Total Complexity | 69 |
Total Lines | 483 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like AppManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use AppManager, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
47 | class AppManager implements IAppManager { |
||
48 | |||
49 | /** |
||
50 | * Apps with these types can not be enabled for certain groups only |
||
51 | * @var string[] |
||
52 | */ |
||
53 | protected $protectedAppTypes = [ |
||
54 | 'filesystem', |
||
55 | 'prelogin', |
||
56 | 'authentication', |
||
57 | 'logging', |
||
58 | 'prevent_group_restriction', |
||
59 | ]; |
||
60 | |||
61 | /** @var IUserSession */ |
||
62 | private $userSession; |
||
63 | |||
64 | /** @var AppConfig */ |
||
65 | private $appConfig; |
||
66 | |||
67 | /** @var IGroupManager */ |
||
68 | private $groupManager; |
||
69 | |||
70 | /** @var ICacheFactory */ |
||
71 | private $memCacheFactory; |
||
72 | |||
73 | /** @var EventDispatcherInterface */ |
||
74 | private $dispatcher; |
||
75 | |||
76 | /** @var ILogger */ |
||
77 | private $logger; |
||
78 | |||
79 | /** @var string[] $appId => $enabled */ |
||
80 | private $installedAppsCache; |
||
81 | |||
82 | /** @var string[] */ |
||
83 | private $shippedApps; |
||
84 | |||
85 | /** @var string[] */ |
||
86 | private $alwaysEnabled; |
||
87 | |||
88 | /** @var array */ |
||
89 | private $appInfos = []; |
||
90 | |||
91 | /** @var array */ |
||
92 | private $appVersions = []; |
||
93 | |||
94 | /** |
||
95 | * @param IUserSession $userSession |
||
96 | * @param AppConfig $appConfig |
||
97 | * @param IGroupManager $groupManager |
||
98 | * @param ICacheFactory $memCacheFactory |
||
99 | * @param EventDispatcherInterface $dispatcher |
||
100 | */ |
||
101 | public function __construct(IUserSession $userSession, |
||
102 | AppConfig $appConfig, |
||
103 | IGroupManager $groupManager, |
||
104 | ICacheFactory $memCacheFactory, |
||
105 | EventDispatcherInterface $dispatcher, |
||
106 | ILogger $logger) { |
||
107 | $this->userSession = $userSession; |
||
108 | $this->appConfig = $appConfig; |
||
109 | $this->groupManager = $groupManager; |
||
110 | $this->memCacheFactory = $memCacheFactory; |
||
111 | $this->dispatcher = $dispatcher; |
||
112 | $this->logger = $logger; |
||
113 | } |
||
114 | |||
115 | /** |
||
116 | * @return string[] $appId => $enabled |
||
117 | */ |
||
118 | private function getInstalledAppsValues() { |
||
119 | if (!$this->installedAppsCache) { |
||
|
|||
120 | $values = $this->appConfig->getValues(false, 'enabled'); |
||
121 | |||
122 | $alwaysEnabledApps = $this->getAlwaysEnabledApps(); |
||
123 | foreach($alwaysEnabledApps as $appId) { |
||
124 | $values[$appId] = 'yes'; |
||
125 | } |
||
126 | |||
127 | $this->installedAppsCache = array_filter($values, function ($value) { |
||
128 | return $value !== 'no'; |
||
129 | }); |
||
130 | ksort($this->installedAppsCache); |
||
131 | } |
||
132 | return $this->installedAppsCache; |
||
133 | } |
||
134 | |||
135 | /** |
||
136 | * List all installed apps |
||
137 | * |
||
138 | * @return string[] |
||
139 | */ |
||
140 | public function getInstalledApps() { |
||
141 | return array_keys($this->getInstalledAppsValues()); |
||
142 | } |
||
143 | |||
144 | /** |
||
145 | * List all apps enabled for a user |
||
146 | * |
||
147 | * @param \OCP\IUser $user |
||
148 | * @return string[] |
||
149 | */ |
||
150 | public function getEnabledAppsForUser(IUser $user) { |
||
151 | $apps = $this->getInstalledAppsValues(); |
||
152 | $appsForUser = array_filter($apps, function ($enabled) use ($user) { |
||
153 | return $this->checkAppForUser($enabled, $user); |
||
154 | }); |
||
155 | return array_keys($appsForUser); |
||
156 | } |
||
157 | |||
158 | /** |
||
159 | * @param \OCP\IGroup $group |
||
160 | * @return array |
||
161 | */ |
||
162 | public function getEnabledAppsForGroup(IGroup $group): array { |
||
163 | $apps = $this->getInstalledAppsValues(); |
||
164 | $appsForGroups = array_filter($apps, function ($enabled) use ($group) { |
||
165 | return $this->checkAppForGroups($enabled, $group); |
||
166 | }); |
||
167 | return array_keys($appsForGroups); |
||
168 | } |
||
169 | |||
170 | /** |
||
171 | * @param string $appId |
||
172 | * @return array |
||
173 | */ |
||
174 | public function getAppRestriction(string $appId): array { |
||
175 | $values = $this->getInstalledAppsValues(); |
||
176 | |||
177 | if (!isset($values[$appId])) { |
||
178 | return []; |
||
179 | } |
||
180 | |||
181 | if ($values[$appId] === 'yes' || $values[$appId] === 'no') { |
||
182 | return []; |
||
183 | } |
||
184 | return json_decode($values[$appId]); |
||
185 | } |
||
186 | |||
187 | |||
188 | /** |
||
189 | * Check if an app is enabled for user |
||
190 | * |
||
191 | * @param string $appId |
||
192 | * @param \OCP\IUser $user (optional) if not defined, the currently logged in user will be used |
||
193 | * @return bool |
||
194 | */ |
||
195 | public function isEnabledForUser($appId, $user = null) { |
||
196 | if ($this->isAlwaysEnabled($appId)) { |
||
197 | return true; |
||
198 | } |
||
199 | if ($user === null) { |
||
200 | $user = $this->userSession->getUser(); |
||
201 | } |
||
202 | $installedApps = $this->getInstalledAppsValues(); |
||
203 | if (isset($installedApps[$appId])) { |
||
204 | return $this->checkAppForUser($installedApps[$appId], $user); |
||
205 | } else { |
||
206 | return false; |
||
207 | } |
||
208 | } |
||
209 | |||
210 | /** |
||
211 | * @param string $enabled |
||
212 | * @param IUser $user |
||
213 | * @return bool |
||
214 | */ |
||
215 | private function checkAppForUser($enabled, $user) { |
||
216 | if ($enabled === 'yes') { |
||
217 | return true; |
||
218 | } elseif ($user === null) { |
||
219 | return false; |
||
220 | } else { |
||
221 | if(empty($enabled)){ |
||
222 | return false; |
||
223 | } |
||
224 | |||
225 | $groupIds = json_decode($enabled); |
||
226 | |||
227 | if (!is_array($groupIds)) { |
||
228 | $jsonError = json_last_error(); |
||
229 | $this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError, ['app' => 'lib']); |
||
230 | return false; |
||
231 | } |
||
232 | |||
233 | $userGroups = $this->groupManager->getUserGroupIds($user); |
||
234 | foreach ($userGroups as $groupId) { |
||
235 | if (in_array($groupId, $groupIds, true)) { |
||
236 | return true; |
||
237 | } |
||
238 | } |
||
239 | return false; |
||
240 | } |
||
241 | } |
||
242 | |||
243 | /** |
||
244 | * @param string $enabled |
||
245 | * @param IGroup $group |
||
246 | * @return bool |
||
247 | */ |
||
248 | private function checkAppForGroups(string $enabled, IGroup $group): bool { |
||
249 | if ($enabled === 'yes') { |
||
250 | return true; |
||
251 | } elseif ($group === null) { |
||
252 | return false; |
||
253 | } else { |
||
254 | if (empty($enabled)) { |
||
255 | return false; |
||
256 | } |
||
257 | |||
258 | $groupIds = json_decode($enabled); |
||
259 | |||
260 | if (!is_array($groupIds)) { |
||
261 | $jsonError = json_last_error(); |
||
262 | $this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError, ['app' => 'lib']); |
||
263 | return false; |
||
264 | } |
||
265 | |||
266 | return in_array($group->getGID(), $groupIds); |
||
267 | } |
||
268 | } |
||
269 | |||
270 | /** |
||
271 | * Check if an app is enabled in the instance |
||
272 | * |
||
273 | * Notice: This actually checks if the app is enabled and not only if it is installed. |
||
274 | * |
||
275 | * @param string $appId |
||
276 | * @param \OCP\IGroup[]|String[] $groups |
||
277 | * @return bool |
||
278 | */ |
||
279 | public function isInstalled($appId) { |
||
280 | $installedApps = $this->getInstalledAppsValues(); |
||
281 | return isset($installedApps[$appId]); |
||
282 | } |
||
283 | |||
284 | /** |
||
285 | * Enable an app for every user |
||
286 | * |
||
287 | * @param string $appId |
||
288 | * @throws AppPathNotFoundException |
||
289 | */ |
||
290 | public function enableApp($appId) { |
||
291 | // Check if app exists |
||
292 | $this->getAppPath($appId); |
||
293 | |||
294 | $this->installedAppsCache[$appId] = 'yes'; |
||
295 | $this->appConfig->setValue($appId, 'enabled', 'yes'); |
||
296 | $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent( |
||
297 | ManagerEvent::EVENT_APP_ENABLE, $appId |
||
298 | )); |
||
299 | $this->clearAppsCache(); |
||
300 | } |
||
301 | |||
302 | /** |
||
303 | * Whether a list of types contains a protected app type |
||
304 | * |
||
305 | * @param string[] $types |
||
306 | * @return bool |
||
307 | */ |
||
308 | public function hasProtectedAppType($types) { |
||
309 | if (empty($types)) { |
||
310 | return false; |
||
311 | } |
||
312 | |||
313 | $protectedTypes = array_intersect($this->protectedAppTypes, $types); |
||
314 | return !empty($protectedTypes); |
||
315 | } |
||
316 | |||
317 | /** |
||
318 | * Enable an app only for specific groups |
||
319 | * |
||
320 | * @param string $appId |
||
321 | * @param \OCP\IGroup[] $groups |
||
322 | * @throws \InvalidArgumentException if app can't be enabled for groups |
||
323 | * @throws AppPathNotFoundException |
||
324 | */ |
||
325 | public function enableAppForGroups($appId, $groups) { |
||
326 | // Check if app exists |
||
327 | $this->getAppPath($appId); |
||
328 | |||
329 | $info = $this->getAppInfo($appId); |
||
330 | if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) { |
||
331 | throw new \InvalidArgumentException("$appId can't be enabled for groups."); |
||
332 | } |
||
333 | |||
334 | $groupIds = array_map(function ($group) { |
||
335 | /** @var \OCP\IGroup $group */ |
||
336 | return ($group instanceof IGroup) |
||
337 | ? $group->getGID() |
||
338 | : $group; |
||
339 | }, $groups); |
||
340 | |||
341 | $this->installedAppsCache[$appId] = json_encode($groupIds); |
||
342 | $this->appConfig->setValue($appId, 'enabled', json_encode($groupIds)); |
||
343 | $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent( |
||
344 | ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups |
||
345 | )); |
||
346 | $this->clearAppsCache(); |
||
347 | |||
348 | } |
||
349 | |||
350 | /** |
||
351 | * Disable an app for every user |
||
352 | * |
||
353 | * @param string $appId |
||
354 | * @throws \Exception if app can't be disabled |
||
355 | */ |
||
356 | public function disableApp($appId) { |
||
357 | if ($this->isAlwaysEnabled($appId)) { |
||
358 | throw new \Exception("$appId can't be disabled."); |
||
359 | } |
||
360 | unset($this->installedAppsCache[$appId]); |
||
361 | $this->appConfig->setValue($appId, 'enabled', 'no'); |
||
362 | |||
363 | // run uninstall steps |
||
364 | $appData = $this->getAppInfo($appId); |
||
365 | if (!is_null($appData)) { |
||
366 | \OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']); |
||
367 | } |
||
368 | |||
369 | $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent( |
||
370 | ManagerEvent::EVENT_APP_DISABLE, $appId |
||
371 | )); |
||
372 | $this->clearAppsCache(); |
||
373 | } |
||
374 | |||
375 | /** |
||
376 | * Get the directory for the given app. |
||
377 | * |
||
378 | * @param string $appId |
||
379 | * @return string |
||
380 | * @throws AppPathNotFoundException if app folder can't be found |
||
381 | */ |
||
382 | public function getAppPath($appId) { |
||
383 | $appPath = \OC_App::getAppPath($appId); |
||
384 | if($appPath === false) { |
||
385 | throw new AppPathNotFoundException('Could not find path for ' . $appId); |
||
386 | } |
||
387 | return $appPath; |
||
388 | } |
||
389 | |||
390 | /** |
||
391 | * Clear the cached list of apps when enabling/disabling an app |
||
392 | */ |
||
393 | public function clearAppsCache() { |
||
394 | $settingsMemCache = $this->memCacheFactory->createDistributed('settings'); |
||
395 | $settingsMemCache->clear('listApps'); |
||
396 | $this->appInfos = []; |
||
397 | } |
||
398 | |||
399 | /** |
||
400 | * Returns a list of apps that need upgrade |
||
401 | * |
||
402 | * @param string $version Nextcloud version as array of version components |
||
403 | * @return array list of app info from apps that need an upgrade |
||
404 | * |
||
405 | * @internal |
||
406 | */ |
||
407 | public function getAppsNeedingUpgrade($version) { |
||
408 | $appsToUpgrade = []; |
||
409 | $apps = $this->getInstalledApps(); |
||
410 | foreach ($apps as $appId) { |
||
411 | $appInfo = $this->getAppInfo($appId); |
||
412 | $appDbVersion = $this->appConfig->getValue($appId, 'installed_version'); |
||
413 | if ($appDbVersion |
||
414 | && isset($appInfo['version']) |
||
415 | && version_compare($appInfo['version'], $appDbVersion, '>') |
||
416 | && \OC_App::isAppCompatible($version, $appInfo) |
||
417 | ) { |
||
418 | $appsToUpgrade[] = $appInfo; |
||
419 | } |
||
420 | } |
||
421 | |||
422 | return $appsToUpgrade; |
||
423 | } |
||
424 | |||
425 | /** |
||
426 | * Returns the app information from "appinfo/info.xml". |
||
427 | * |
||
428 | * @param string $appId app id |
||
429 | * |
||
430 | * @param bool $path |
||
431 | * @param null $lang |
||
432 | * @return array|null app info |
||
433 | */ |
||
434 | public function getAppInfo(string $appId, bool $path = false, $lang = null) { |
||
435 | if ($path) { |
||
436 | $file = $appId; |
||
437 | } else { |
||
438 | if ($lang === null && isset($this->appInfos[$appId])) { |
||
439 | return $this->appInfos[$appId]; |
||
440 | } |
||
441 | try { |
||
442 | $appPath = $this->getAppPath($appId); |
||
443 | } catch (AppPathNotFoundException $e) { |
||
444 | return null; |
||
445 | } |
||
446 | $file = $appPath . '/appinfo/info.xml'; |
||
447 | } |
||
448 | |||
449 | $parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo')); |
||
450 | $data = $parser->parse($file); |
||
451 | |||
452 | if (is_array($data)) { |
||
453 | $data = \OC_App::parseAppInfo($data, $lang); |
||
454 | } |
||
455 | |||
456 | if ($lang === null) { |
||
457 | $this->appInfos[$appId] = $data; |
||
458 | } |
||
459 | |||
460 | return $data; |
||
461 | } |
||
462 | |||
463 | public function getAppVersion(string $appId, bool $useCache = true): string { |
||
464 | if(!$useCache || !isset($this->appVersions[$appId])) { |
||
465 | $appInfo = \OC::$server->getAppManager()->getAppInfo($appId); |
||
466 | $this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0'; |
||
467 | } |
||
468 | return $this->appVersions[$appId]; |
||
469 | } |
||
470 | |||
471 | /** |
||
472 | * Returns a list of apps incompatible with the given version |
||
473 | * |
||
474 | * @param string $version Nextcloud version as array of version components |
||
475 | * |
||
476 | * @return array list of app info from incompatible apps |
||
477 | * |
||
478 | * @internal |
||
479 | */ |
||
480 | public function getIncompatibleApps(string $version): array { |
||
481 | $apps = $this->getInstalledApps(); |
||
482 | $incompatibleApps = array(); |
||
483 | foreach ($apps as $appId) { |
||
484 | $info = $this->getAppInfo($appId); |
||
485 | if ($info === null) { |
||
486 | $incompatibleApps[] = ['id' => $appId]; |
||
487 | } else if (!\OC_App::isAppCompatible($version, $info)) { |
||
488 | $incompatibleApps[] = $info; |
||
489 | } |
||
490 | } |
||
491 | return $incompatibleApps; |
||
492 | } |
||
493 | |||
494 | /** |
||
495 | * @inheritdoc |
||
496 | * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped() |
||
497 | */ |
||
498 | public function isShipped($appId) { |
||
501 | } |
||
502 | |||
503 | private function isAlwaysEnabled($appId) { |
||
504 | $alwaysEnabled = $this->getAlwaysEnabledApps(); |
||
505 | return in_array($appId, $alwaysEnabled, true); |
||
506 | } |
||
507 | |||
508 | /** |
||
509 | * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson() |
||
510 | * @throws \Exception |
||
511 | */ |
||
512 | private function loadShippedJson() { |
||
521 | } |
||
522 | } |
||
523 | |||
524 | /** |
||
525 | * @inheritdoc |
||
526 | */ |
||
527 | public function getAlwaysEnabledApps() { |
||
528 | $this->loadShippedJson(); |
||
529 | return $this->alwaysEnabled; |
||
530 | } |
||
531 | } |
||
532 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.