1414 * @author Roeland Jago Douma <roeland@famdouma.nl>
1515 * @author Thomas Müller <thomas.mueller@tmit.eu>
1616 * @author Kate Döen <kate.doeen@nextcloud.com>
17+ * @author Ferdinand Thiessen <opensource@fthiessen.de>
1718 *
1819 * @license AGPL-3.0
1920 *
3334namespace OCA \Settings \Controller ;
3435
3536use OC \App \AppStore \Bundles \BundleFetcher ;
37+ use OC \App \AppStore \Fetcher \AppDiscoverFetcher ;
3638use OC \App \AppStore \Fetcher \AppFetcher ;
3739use OC \App \AppStore \Fetcher \CategoryFetcher ;
3840use OC \App \AppStore \Version \VersionParser ;
3941use OC \App \DependencyAnalyzer ;
4042use OC \App \Platform ;
4143use OC \Installer ;
4244use OC_App ;
45+ use OCP \App \AppPathNotFoundException ;
4346use OCP \App \IAppManager ;
4447use OCP \AppFramework \Controller ;
4548use OCP \AppFramework \Http ;
4649use OCP \AppFramework \Http \Attribute \OpenAPI ;
4750use OCP \AppFramework \Http \ContentSecurityPolicy ;
51+ use OCP \AppFramework \Http \FileDisplayResponse ;
4852use OCP \AppFramework \Http \JSONResponse ;
53+ use OCP \AppFramework \Http \NotFoundResponse ;
54+ use OCP \AppFramework \Http \Response ;
4955use OCP \AppFramework \Http \TemplateResponse ;
5056use OCP \AppFramework \Services \IInitialState ;
57+ use OCP \Files \AppData \IAppDataFactory ;
58+ use OCP \Files \IAppData ;
59+ use OCP \Files \NotFoundException ;
60+ use OCP \Files \NotPermittedException ;
61+ use OCP \Files \SimpleFS \ISimpleFile ;
62+ use OCP \Files \SimpleFS \ISimpleFolder ;
63+ use OCP \Http \Client \IClientService ;
5164use OCP \IConfig ;
5265use OCP \IL10N ;
5366use OCP \INavigationManager ;
@@ -62,9 +75,12 @@ class AppSettingsController extends Controller {
6275 /** @var array */
6376 private $ allApps = [];
6477
78+ private IAppData $ appData ;
79+
6580 public function __construct (
6681 string $ appName ,
6782 IRequest $ request ,
83+ IAppDataFactory $ appDataFactory ,
6884 private IL10N $ l10n ,
6985 private IConfig $ config ,
7086 private INavigationManager $ navigationManager ,
@@ -77,8 +93,11 @@ public function __construct(
7793 private IURLGenerator $ urlGenerator ,
7894 private LoggerInterface $ logger ,
7995 private IInitialState $ initialState ,
96+ private AppDiscoverFetcher $ discoverFetcher ,
97+ private IClientService $ clientService ,
8098 ) {
8199 parent ::__construct ($ appName , $ request );
100+ $ this ->appData = $ appDataFactory ->get ('appstore ' );
82101 }
83102
84103 /**
@@ -106,6 +125,93 @@ public function viewApps(): TemplateResponse {
106125 return $ templateResponse ;
107126 }
108127
128+ /**
129+ * Get all active entries for the app discover section
130+ *
131+ * @NoCSRFRequired
132+ */
133+ public function getAppDiscoverJSON (): JSONResponse {
134+ $ data = $ this ->discoverFetcher ->get ();
135+ return new JSONResponse ($ data );
136+ }
137+
138+ /**
139+ * @PublicPage
140+ * @NoCSRFRequired
141+ *
142+ * Get a image for the app discover section - this is proxied for privacy and CSP reasons
143+ *
144+ * @param string $image
145+ * @throws \Exception
146+ */
147+ public function getAppDiscoverMedia (string $ fileName ): Response {
148+ $ etag = $ this ->discoverFetcher ->getETag () ?? date ('Y-m ' );
149+ $ folder = null ;
150+ try {
151+ $ folder = $ this ->appData ->getFolder ('app-discover-cache ' );
152+ $ this ->cleanUpImageCache ($ folder , $ etag );
153+ } catch (\Throwable $ e ) {
154+ $ folder = $ this ->appData ->newFolder ('app-discover-cache ' );
155+ }
156+
157+ // Get the current cache folder
158+ try {
159+ $ folder = $ folder ->getFolder ($ etag );
160+ } catch (NotFoundException $ e ) {
161+ $ folder = $ folder ->newFolder ($ etag );
162+ }
163+
164+ $ info = pathinfo ($ fileName );
165+ $ hashName = md5 ($ fileName );
166+ $ allFiles = $ folder ->getDirectoryListing ();
167+ // Try to find the file
168+ $ file = array_filter ($ allFiles , function (ISimpleFile $ file ) use ($ hashName ) {
169+ return str_starts_with ($ file ->getName (), $ hashName );
170+ });
171+ // Get the first entry
172+ $ file = reset ($ file );
173+ // If not found request from Web
174+ if ($ file === false ) {
175+ try {
176+ $ client = $ this ->clientService ->newClient ();
177+ $ fileResponse = $ client ->get ($ fileName );
178+ $ contentType = $ fileResponse ->getHeader ('Content-Type ' );
179+ $ extension = $ info ['extension ' ] ?? '' ;
180+ $ file = $ folder ->newFile ($ hashName . '. ' . base64_encode ($ contentType ) . '. ' . $ extension , $ fileResponse ->getBody ());
181+ } catch (\Throwable $ e ) {
182+ $ this ->logger ->warning ('Could not load media file for app discover section ' , ['media_src ' => $ fileName , 'exception ' => $ e ]);
183+ return new NotFoundResponse ();
184+ }
185+ } else {
186+ // File was found so we can get the content type from the file name
187+ $ contentType = base64_decode (explode ('. ' , $ file ->getName ())[1 ] ?? '' );
188+ }
189+
190+ $ response = new FileDisplayResponse ($ file , Http::STATUS_OK , ['Content-Type ' => $ contentType ]);
191+ // cache for 7 days
192+ $ response ->cacheFor (604800 , false , true );
193+ return $ response ;
194+ }
195+
196+ /**
197+ * Remove orphaned folders from the image cache that do not match the current etag
198+ * @param ISimpleFolder $folder The folder to clear
199+ * @param string $etag The etag (directory name) to keep
200+ */
201+ private function cleanUpImageCache (ISimpleFolder $ folder , string $ etag ): void {
202+ // Cleanup old cache folders
203+ $ allFiles = $ folder ->getDirectoryListing ();
204+ foreach ($ allFiles as $ dir ) {
205+ try {
206+ if ($ dir ->getName () !== $ etag ) {
207+ $ dir ->delete ();
208+ }
209+ } catch (NotPermittedException $ e ) {
210+ // ignore folder for now
211+ }
212+ }
213+ }
214+
109215 private function getAppsWithUpdates () {
110216 $ appClass = new \OC_App ();
111217 $ apps = $ appClass ->listAllApps ();
@@ -190,6 +296,7 @@ private function fetchApps() {
190296 private function getAllApps () {
191297 return $ this ->allApps ;
192298 }
299+
193300 /**
194301 * Get all available apps in a category
195302 *
@@ -291,7 +398,14 @@ private function getAppsForCategory($requestedCategory = ''): array {
291398 $ nextCloudVersionDependencies ['nextcloud ' ]['@attributes ' ]['max-version ' ] = $ nextCloudVersion ->getMaximumVersion ();
292399 }
293400 $ phpVersion = $ versionParser ->getVersion ($ app ['releases ' ][0 ]['rawPhpVersionSpec ' ]);
294- $ existsLocally = \OC_App::getAppPath ($ app ['id ' ]) !== false ;
401+
402+ try {
403+ $ this ->appManager ->getAppPath ($ app ['id ' ]);
404+ $ existsLocally = true ;
405+ } catch (AppPathNotFoundException $ e ) {
406+ $ existsLocally = false ;
407+ }
408+
295409 $ phpDependencies = [];
296410 if ($ phpVersion ->getMinimumVersion () !== '' ) {
297411 $ phpDependencies ['php ' ]['@attributes ' ]['min-version ' ] = $ phpVersion ->getMinimumVersion ();
@@ -310,7 +424,7 @@ private function getAppsForCategory($requestedCategory = ''): array {
310424 }
311425 }
312426
313- $ currentLanguage = substr (\ OC :: $ server -> getL10NFactory () ->findLanguage (), 0 , 2 );
427+ $ currentLanguage = substr ($ this -> l10nFactory ->findLanguage (), 0 , 2 );
314428 $ enabledValue = $ this ->config ->getAppValue ($ app ['id ' ], 'enabled ' , 'no ' );
315429 $ groups = null ;
316430 if ($ enabledValue !== 'no ' && $ enabledValue !== 'yes ' ) {
@@ -397,7 +511,7 @@ public function enableApps(array $appIds, array $groups = []): JSONResponse {
397511
398512 // Check if app is already downloaded
399513 /** @var Installer $installer */
400- $ installer = \OC ::$ server ->query (Installer::class);
514+ $ installer = \OC ::$ server ->get (Installer::class);
401515 $ isDownloaded = $ installer ->isDownloaded ($ appId );
402516
403517 if (!$ isDownloaded ) {
@@ -416,7 +530,7 @@ public function enableApps(array $appIds, array $groups = []): JSONResponse {
416530 }
417531 }
418532 return new JSONResponse (['data ' => ['update_required ' => $ updateRequired ]]);
419- } catch (\Exception $ e ) {
533+ } catch (\Throwable $ e ) {
420534 $ this ->logger ->error ('could not enable apps ' , ['exception ' => $ e ]);
421535 return new JSONResponse (['data ' => ['message ' => $ e ->getMessage ()]], Http::STATUS_INTERNAL_SERVER_ERROR );
422536 }
0 commit comments