refactor: PHP 8 syntax migration

Signed-off-by: Varun Patil <radialapps@gmail.com>
pull/877/head
Varun Patil 2023-10-15 12:46:35 -07:00
parent 73624ce5f2
commit bd2101e7bb
40 changed files with 381 additions and 310 deletions

View File

@ -26,7 +26,7 @@ $config
'@PhpCsFixer' => true, '@PhpCsFixer' => true,
'@PhpCsFixer:risky' => true, '@PhpCsFixer:risky' => true,
'general_phpdoc_annotation_remove' => ['annotations' => ['expectedDeprecation']], // one should use PHPUnit built-in method instead 'general_phpdoc_annotation_remove' => ['annotations' => ['expectedDeprecation']], // one should use PHPUnit built-in method instead
'phpdoc_to_comment' => ['ignored_tags' => ['psalm-suppress', 'template-implements']], 'phpdoc_to_comment' => ['ignored_tags' => ['psalm-suppress', 'template-implements', 'var']],
'trailing_comma_in_multiline' => ['elements' => ['arrays', 'parameters', 'arguments']], 'trailing_comma_in_multiline' => ['elements' => ['arrays', 'parameters', 'arguments']],
'modernize_strpos' => true, 'modernize_strpos' => true,
'no_alias_functions' => true, 'no_alias_functions' => true,

View File

@ -27,6 +27,7 @@ use OCA\Memories\Db\AlbumsQuery;
use OCA\Memories\Db\TimelineQuery; use OCA\Memories\Db\TimelineQuery;
use OCA\Memories\Exceptions; use OCA\Memories\Exceptions;
use OCA\Memories\Util; use OCA\Memories\Util;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IRequest; use OCP\IRequest;
class AlbumsBackend extends Backend class AlbumsBackend extends Backend
@ -57,7 +58,7 @@ class AlbumsBackend extends Backend
return explode('/', $name)[1]; return explode('/', $name)[1];
} }
public function transformDayQuery(&$query, bool $aggregate): void public function transformDayQuery(IQueryBuilder &$query, bool $aggregate): void
{ {
$albumId = (string) $this->request->getParam(self::clusterType()); $albumId = (string) $this->request->getParam(self::clusterType());
@ -102,7 +103,7 @@ class AlbumsBackend extends Backend
// Add display names for users // Add display names for users
$userManager = \OC::$server->get(\OCP\IUserManager::class); $userManager = \OC::$server->get(\OCP\IUserManager::class);
array_walk($list, static function (&$item) use ($userManager) { array_walk($list, static function (array &$item) use ($userManager) {
$user = $userManager->get($item['user']); $user = $userManager->get($item['user']);
$item['user_display'] = $user ? $user->getDisplayName() : null; $item['user_display'] = $user ? $user->getDisplayName() : null;
}); });

View File

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace OCA\Memories\ClustersBackend; namespace OCA\Memories\ClustersBackend;
use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\SimpleFS\ISimpleFile;
abstract class Backend abstract class Backend
{ {
@ -49,7 +50,7 @@ abstract class Backend
* @param IQueryBuilder $query Query builder * @param IQueryBuilder $query Query builder
* @param bool $aggregate Whether this is an aggregate query * @param bool $aggregate Whether this is an aggregate query
*/ */
abstract public function transformDayQuery(&$query, bool $aggregate): void; abstract public function transformDayQuery(IQueryBuilder &$query, bool $aggregate): void;
/** /**
* Apply post-query transformations for the given photo object. * Apply post-query transformations for the given photo object.
@ -110,12 +111,12 @@ abstract class Backend
/** /**
* Perform any post processing and get the blob from the preview file. * Perform any post processing and get the blob from the preview file.
* *
* @param \OCP\Files\SimpleFS\ISimpleFile $file Preview file * @param ISimpleFile $file Preview file
* @param array $photo Photo object * @param array $photo Photo object
* *
* @return array [Blob, mimetype] of data * @return array [Blob, mimetype] of data
*/ */
public function getPreviewBlob($file, $photo): array public function getPreviewBlob(ISimpleFile $file, array $photo): array
{ {
return [$file->getContent(), $file->getMimeType()]; return [$file->getContent(), $file->getMimeType()];
} }

View File

@ -25,6 +25,8 @@ namespace OCA\Memories\ClustersBackend;
use OCA\Memories\Db\TimelineQuery; use OCA\Memories\Db\TimelineQuery;
use OCA\Memories\Util; use OCA\Memories\Util;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\IConfig; use OCP\IConfig;
use OCP\IRequest; use OCP\IRequest;
@ -54,7 +56,7 @@ class FaceRecognitionBackend extends Backend
&& Util::facerecognitionIsEnabled(); && Util::facerecognitionIsEnabled();
} }
public function transformDayQuery(&$query, bool $aggregate): void public function transformDayQuery(IQueryBuilder &$query, bool $aggregate): void
{ {
$personStr = (string) $this->request->getParam('facerecognition'); $personStr = (string) $this->request->getParam('facerecognition');
@ -196,7 +198,7 @@ class FaceRecognitionBackend extends Backend
$this->sortByScores($photos); $this->sortByScores($photos);
} }
public function getPreviewBlob($file, $photo): array public function getPreviewBlob(ISimpleFile $file, array $photo): array
{ {
return $this->cropFace($file, $photo, 1.8); return $this->cropFace($file, $photo, 1.8);
} }

View File

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace OCA\Memories\ClustersBackend; namespace OCA\Memories\ClustersBackend;
use OCP\Files\SimpleFS\ISimpleFile;
trait PeopleBackendUtils trait PeopleBackendUtils
{ {
/** /**
@ -83,7 +85,7 @@ trait PeopleBackendUtils
* - width: width of the face in the image (percentage) * - width: width of the face in the image (percentage)
* - height: height of the face in the image (percentage) * - height: height of the face in the image (percentage)
* *
* @param \OCP\Files\SimpleFS\ISimpleFile $file Actual file containing the image * @param ISimpleFile $file Actual file containing the image
* @param array $photo The face object * @param array $photo The face object
* @param float $padding The padding to add around the face * @param float $padding The padding to add around the face
* *
@ -93,7 +95,7 @@ trait PeopleBackendUtils
* *
* @psalm-return list{string, string} * @psalm-return list{string, string}
*/ */
private function cropFace($file, array $photo, float $padding): array private function cropFace(ISimpleFile $file, array $photo, float $padding): array
{ {
$img = new \OCP\Image(); $img = new \OCP\Image();
$img->loadFromData($file->getContent()); $img->loadFromData($file->getContent());
@ -124,11 +126,8 @@ trait PeopleBackendUtils
$img->scaleDownToFit(512, 512); $img->scaleDownToFit(512, 512);
// Get blob and mimetype // Get blob and mimetype
$data = $img->data(); $data = $img->data() ?: throw new \Exception('Could not get image data');
$mime = $img->mimeType(); $mime = $img->mimeType() ?: throw new \Exception('Could not get image mimetype');
if (null === $data || null === $mime) {
throw new \Exception('Could not get image data');
}
return [$data, $mime]; return [$data, $mime];
} }

View File

@ -25,6 +25,7 @@ namespace OCA\Memories\ClustersBackend;
use OCA\Memories\Db\TimelineQuery; use OCA\Memories\Db\TimelineQuery;
use OCA\Memories\Util; use OCA\Memories\Util;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IRequest; use OCP\IRequest;
class PlacesBackend extends Backend class PlacesBackend extends Backend
@ -49,7 +50,7 @@ class PlacesBackend extends Backend
return Util::placesGISType() > 0; return Util::placesGISType() > 0;
} }
public function transformDayQuery(&$query, bool $aggregate): void public function transformDayQuery(IQueryBuilder &$query, bool $aggregate): void
{ {
$locationId = (int) $this->request->getParam('places'); $locationId = (int) $this->request->getParam('places');

View File

@ -25,6 +25,8 @@ namespace OCA\Memories\ClustersBackend;
use OCA\Memories\Db\TimelineQuery; use OCA\Memories\Db\TimelineQuery;
use OCA\Memories\Util; use OCA\Memories\Util;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\IRequest; use OCP\IRequest;
class RecognizeBackend extends Backend class RecognizeBackend extends Backend
@ -51,7 +53,7 @@ class RecognizeBackend extends Backend
return Util::recognizeIsEnabled(); return Util::recognizeIsEnabled();
} }
public function transformDayQuery(&$query, bool $aggregate): void public function transformDayQuery(IQueryBuilder &$query, bool $aggregate): void
{ {
// Check if Recognize is enabled // Check if Recognize is enabled
if (!$this->isEnabled()) { if (!$this->isEnabled()) {
@ -232,7 +234,7 @@ class RecognizeBackend extends Backend
$this->sortByScores($photos); $this->sortByScores($photos);
} }
public function getPreviewBlob($file, $photo): array public function getPreviewBlob(ISimpleFile $file, array $photo): array
{ {
return $this->cropFace($file, $photo, 1.5); return $this->cropFace($file, $photo, 1.5);
} }

View File

@ -50,7 +50,7 @@ class TagsBackend extends Backend
return Util::tagsIsEnabled(); return Util::tagsIsEnabled();
} }
public function transformDayQuery(&$query, bool $aggregate): void public function transformDayQuery(IQueryBuilder &$query, bool $aggregate): void
{ {
$tagName = (string) $this->request->getParam('tags'); $tagName = (string) $this->request->getParam('tags');

View File

@ -57,11 +57,13 @@ class IndexOpts
class Index extends Command class Index extends Command
{ {
// IO /** @psalm-suppress PropertyNotSetInConstructor */
private InputInterface $input; private InputInterface $input;
/** @psalm-suppress PropertyNotSetInConstructor */
private OutputInterface $output; private OutputInterface $output;
// Command options /** @psalm-suppress PropertyNotSetInConstructor */
private IndexOpts $opts; private IndexOpts $opts;
public function __construct( public function __construct(
@ -184,9 +186,9 @@ class Index extends Command
/** /**
* Run function for all users (or selected user if set). * Run function for all users (or selected user if set).
* *
* @param mixed $closure * @param \Closure(IUser $user): void $closure
*/ */
private function runForUsers($closure): void private function runForUsers(\Closure $closure): void
{ {
if ($uid = $this->opts->user) { if ($uid = $this->opts->user) {
if ($user = $this->userManager->get($uid)) { if ($user = $this->userManager->get($uid)) {
@ -203,7 +205,7 @@ class Index extends Command
$this->output->writeln("<error>Group {$gid} not found</error>\n"); $this->output->writeln("<error>Group {$gid} not found</error>\n");
} }
} else { } else {
$this->userManager->callForSeenUsers(static fn (IUser $user) => $closure($user)); $this->userManager->callForSeenUsers($closure);
} }
} }
} }

View File

@ -44,7 +44,10 @@ class MigrateGoogleTakeout extends Command
protected const MIGRATOR_VERSION = 1; protected const MIGRATOR_VERSION = 1;
protected const MIGRATED_KEY = 'memoriesMigratorVersion'; protected const MIGRATED_KEY = 'memoriesMigratorVersion';
/** @psalm-suppress PropertyNotSetInConstructor */
protected OutputInterface $output; protected OutputInterface $output;
/** @psalm-suppress PropertyNotSetInConstructor */
protected InputInterface $input; protected InputInterface $input;
// Stats // Stats
@ -124,18 +127,22 @@ class MigrateGoogleTakeout extends Command
protected function migrateUser(IUser $user): void protected function migrateUser(IUser $user): void
{ {
$this->output->writeln("Migrating user {$user->getUID()}"); $uid = $user->getUID();
$this->output->writeln("Migrating user {$uid}");
// Get user's root folder // Get user's root folder
\OC_Util::tearDownFS(); \OC_Util::tearDownFS();
\OC_Util::setupFS($user->getUID()); \OC_Util::setupFS($uid);
$folder = $this->rootFolder->getUserFolder($user->getUID()); $folder = $this->rootFolder->getUserFolder($uid);
// Check if we need to migrate a specific folder // Check if we need to migrate a specific folder
if ($path = $this->input->getOption('folder')) { if ($path = $this->input->getOption('folder')) {
try { try {
$folder = $folder->get($path); $folder = $folder->get($path);
} catch (\Exception $e) { if (!$folder instanceof Folder) {
throw new \Exception();
}
} catch (\Exception) {
$this->output->writeln("<error>Folder {$path} does not exist</error>"); $this->output->writeln("<error>Folder {$path} does not exist</error>");
return; return;
@ -282,15 +289,10 @@ class MigrateGoogleTakeout extends Command
++$this->nProcessed; ++$this->nProcessed;
} }
/**
* @return (float|mixed|string)[]
*
* @psalm-return array<string, float|mixed|string>
*/
protected function takeoutToExiftoolJson(array $json): array protected function takeoutToExiftoolJson(array $json): array
{ {
// Helper to get a value from nested JSON // Helper to get a value from nested JSON
$get = static function (string $source) use ($json) { $get = static function (string $source) use ($json): mixed {
$keys = array_reverse(explode('.', $source)); $keys = array_reverse(explode('.', $source));
while (\count($keys) > 0) { while (\count($keys) > 0) {
$key = array_pop($keys); $key = array_pop($keys);
@ -336,8 +338,6 @@ class MigrateGoogleTakeout extends Command
$txf['GPSAltitude'] = $get('geoData.altitude'); $txf['GPSAltitude'] = $get('geoData.altitude');
// Remove all null values // Remove all null values
return array_filter($txf, static function ($value) { return array_filter($txf, static fn (mixed $value) => null !== $value);
return null !== $value;
});
} }
} }

View File

@ -31,6 +31,7 @@ use Symfony\Component\Console\Output\OutputInterface;
class PlacesSetup extends Command class PlacesSetup extends Command
{ {
/** @psalm-suppress PropertyNotSetInConstructor */
protected OutputInterface $output; protected OutputInterface $output;
public function __construct(protected Places $places) public function __construct(protected Places $places)

View File

@ -49,10 +49,8 @@ class AdminController extends GenericApiController
/** /**
* @AdminRequired * @AdminRequired
*
* @param mixed $value
*/ */
public function setSystemConfig(string $key, $value): Http\Response public function setSystemConfig(string $key, mixed $value): Http\Response
{ {
return Util::guardEx(function () use ($key, $value) { return Util::guardEx(function () use ($key, $value) {
// Make sure not running in read-only mode // Make sure not running in read-only mode
@ -94,7 +92,7 @@ class AdminController extends GenericApiController
$exiftoolNoLocal = Util::getSystemConfig('memories.exiftool_no_local'); $exiftoolNoLocal = Util::getSystemConfig('memories.exiftool_no_local');
$status['exiftool'] = $this->getExecutableStatus( $status['exiftool'] = $this->getExecutableStatus(
static fn () => BinExt::getExiftoolPBin(), static fn () => BinExt::getExiftoolPBin(),
static fn ($p) => BinExt::testExiftool(), static fn () => BinExt::testExiftool(),
!$exiftoolNoLocal, !$exiftoolNoLocal,
!$exiftoolNoLocal, !$exiftoolNoLocal,
); );
@ -103,7 +101,7 @@ class AdminController extends GenericApiController
/** @psalm-suppress ForbiddenCode */ /** @psalm-suppress ForbiddenCode */
$status['perl'] = $this->getExecutableStatus( $status['perl'] = $this->getExecutableStatus(
trim(shell_exec('which perl') ?: '/bin/perl'), trim(shell_exec('which perl') ?: '/bin/perl'),
static fn ($p) => BinExt::testSystemPerl($p), static fn (string $p) => BinExt::testSystemPerl($p),
); );
// Check number of indexed files // Check number of indexed files
@ -220,14 +218,14 @@ class AdminController extends GenericApiController
/** /**
* Get the status of an executable. * Get the status of an executable.
* *
* @param \Closure|string $path Path to the executable * @param (\Closure():string)|string $path Path to the executable
* @param ?\Closure $testFunction Function to test the executable * @param null|(\Closure(string):string) $testFunction Function to test the executable
* @param bool $testIfFile Test if the path is a file * @param bool $testIfFile Test if the path is a file
* @param bool $testIfExecutable Test if the path is executable * @param bool $testIfExecutable Test if the path is executable
*/ */
private function getExecutableStatus( private function getExecutableStatus(
$path, \Closure|string $path,
?\Closure $testFunction = null, null|\Closure $testFunction = null,
bool $testIfFile = true, bool $testIfFile = true,
bool $testIfExecutable = true, bool $testIfExecutable = true,
): string { ): string {
@ -239,10 +237,6 @@ class AdminController extends GenericApiController
} }
} }
if (!\is_string($path)) {
return 'not_found';
}
if ($testIfFile && !is_file($path)) { if ($testIfFile && !is_file($path)) {
return 'not_found'; return 'not_found';
} }

View File

@ -36,7 +36,7 @@ class ArchiveController extends GenericApiController
* *
* Move one file to the archive folder * Move one file to the archive folder
* *
* @param string fileid * @param string $id File ID to archive / unarchive
*/ */
public function archive(string $id): Http\Response public function archive(string $id): Http\Response
{ {
@ -73,6 +73,7 @@ class ArchiveController extends GenericApiController
$parent = $file->getParent(); $parent = $file->getParent();
$isArchived = false; $isArchived = false;
while (true) { while (true) {
/** @psalm-suppress DocblockTypeContradiction */
if (null === $parent) { if (null === $parent) {
throw new \Exception('Cannot get correct parent of file'); throw new \Exception('Cannot get correct parent of file');
} }
@ -108,6 +109,9 @@ class ArchiveController extends GenericApiController
// Get path of current file relative to the parent folder // Get path of current file relative to the parent folder
$relativeFilePath = $parent->getRelativePath($file->getPath()); $relativeFilePath = $parent->getRelativePath($file->getPath());
if (!$relativeFilePath) {
throw new \Exception('Cannot get relative path of file');
}
// Check if we want to archive or unarchive // Check if we want to archive or unarchive
$body = $this->request->getParams(); $body = $this->request->getParams();

View File

@ -32,7 +32,11 @@ use OCP\AppFramework\Http\JSONResponse;
class ClustersController extends GenericApiController class ClustersController extends GenericApiController
{ {
/** Current backend for this instance */ /**
* Current backend for this instance.
*
* @psalm-suppress PropertyNotSetInConstructor
*/
protected ClustersBackend\Backend $backend; protected ClustersBackend\Backend $backend;
/** /**

View File

@ -42,17 +42,11 @@ class DownloadController extends GenericApiController
* *
* Request to download one or more files * Request to download one or more files
* *
* @param mixed $files * @param int[] $files List of file IDs
*/ */
public function request($files): Http\Response public function request(array $files): Http\Response
{ {
return Util::guardEx(static function () use ($files) { return Util::guardEx(static function () use ($files) {
// Get ids from body
if (null === $files || !\is_array($files)) {
throw Exceptions::MissingParameter('files');
}
// Return id
$handle = self::createHandle('memories', $files); $handle = self::createHandle('memories', $files);
return new JSONResponse(['handle' => $handle]); return new JSONResponse(['handle' => $handle]);
@ -65,7 +59,7 @@ class DownloadController extends GenericApiController
* The calling controller must have the UseSession annotation. * The calling controller must have the UseSession annotation.
* *
* @param string $name Name of zip file * @param string $name Name of zip file
* @param int[] $files * @param int[] $files List of file IDs
*/ */
public static function createHandle(string $name, array $files): string public static function createHandle(string $name, array $files): string
{ {
@ -282,7 +276,7 @@ class DownloadController extends GenericApiController
break; break;
} }
/** @var bool|resource */ /** @var false|resource */
$handle = false; $handle = false;
/** @var ?\OCP\Files\File */ /** @var ?\OCP\Files\File */

View File

@ -21,7 +21,7 @@ class FoldersController extends GenericApiController
return Util::guardEx(function () use ($folder) { return Util::guardEx(function () use ($folder) {
try { try {
$node = Util::getUserFolder()->get($folder); $node = Util::getUserFolder()->get($folder);
} catch (\OCP\Files\NotFoundException $e) { } catch (\OCP\Files\NotFoundException) {
throw Exceptions::NotFound('Folder not found'); throw Exceptions::NotFound('Folder not found');
} }

View File

@ -81,10 +81,12 @@ class ImageController extends GenericApiController
return Util::guardExDirect(function (Http\IOutput $out) { return Util::guardExDirect(function (Http\IOutput $out) {
// read body to array // read body to array
$body = file_get_contents('php://input'); $body = file_get_contents('php://input');
/** @var array<array> */
$files = json_decode($body, true); $files = json_decode($body, true);
// Filter files with valid parameters // Filter files with valid parameters
$files = array_filter($files, static function ($file) { $files = array_filter($files, static function (array $file) {
return isset($file['reqid'], $file['fileid'], $file['x'], $file['y'], $file['a']) return isset($file['reqid'], $file['fileid'], $file['x'], $file['y'], $file['a'])
&& (int) $file['fileid'] > 0 && (int) $file['fileid'] > 0
&& (int) $file['x'] > 0 && (int) $file['x'] > 0
@ -92,14 +94,14 @@ class ImageController extends GenericApiController
}); });
// Sort files by size, ascending // Sort files by size, ascending
usort($files, static function ($a, $b) { usort($files, static function (array $a, array $b) {
$aArea = (int) $a['x'] * (int) $a['y']; $aArea = (int) $a['x'] * (int) $a['y'];
$bArea = (int) $b['x'] * (int) $b['y']; $bArea = (int) $b['x'] * (int) $b['y'];
return $aArea <=> $bArea; return $aArea <=> $bArea;
}); });
/** @var \OCP\IPreview $previewManager */ /** @var \OCP\IPreview */
$previewManager = \OC::$server->get(\OCP\IPreview::class); $previewManager = \OC::$server->get(\OCP\IPreview::class);
// For checking max previews // For checking max previews
@ -202,8 +204,7 @@ class ImageController extends GenericApiController
$info['basename'] = $file->getName(); $info['basename'] = $file->getName();
// Allow these ony for logged in users // Allow these ony for logged in users
$user = $this->userSession->getUser(); if ($user = $this->userSession->getUser()) {
if (null !== $user) {
// Get the path of the file relative to current user // Get the path of the file relative to current user
// "/admin/files/Photos/Camera/20230821_135017.jpg" => "/Photos/..." // "/admin/files/Photos/Camera/20230821_135017.jpg" => "/Photos/..."
$parts = explode('/', $file->getPath()); $parts = explode('/', $file->getPath());
@ -242,9 +243,6 @@ class ImageController extends GenericApiController
* @NoAdminRequired * @NoAdminRequired
* *
* Set the exif data for a file. * Set the exif data for a file.
*
* @param int fileid
* @param array raw exif data
*/ */
public function setExif(int $id, array $raw): Http\Response public function setExif(int $id, array $raw): Http\Response
{ {
@ -398,7 +396,9 @@ class ImageController extends GenericApiController
* @param string $blob Blob of image data in any format * @param string $blob Blob of image data in any format
* @param string $mimetype Mimetype of image data * @param string $mimetype Mimetype of image data
* *
* @return array [blob, mimetype] * @return string[] [blob, mimetype]
*
* @psalm-return list{string, string}
*/ */
private function getImageJPEG($blob, $mimetype): array private function getImageJPEG($blob, $mimetype): array
{ {
@ -435,6 +435,8 @@ class ImageController extends GenericApiController
/** /**
* Get the tags for a file. * Get the tags for a file.
*
* @return string[]
*/ */
private function getTags(int $fileId): array private function getTags(int $fileId): array
{ {
@ -447,12 +449,10 @@ class ImageController extends GenericApiController
$objectMapper = \OC::$server->get(\OCP\SystemTag\ISystemTagObjectMapper::class); $objectMapper = \OC::$server->get(\OCP\SystemTag\ISystemTagObjectMapper::class);
$tagIds = $objectMapper->getTagIdsForObjects([$fileId], 'files')[(string) $fileId]; $tagIds = $objectMapper->getTagIdsForObjects([$fileId], 'files')[(string) $fileId];
// Get the tag names and filter out the ones that are not user visible // Get all matching tag objects
$tagManager = \OC::$server->get(\OCP\SystemTag\ISystemTagManager::class); $tags = \OC::$server->get(\OCP\SystemTag\ISystemTagManager::class)->getTagsByIds($tagIds);
/** @var \OCP\SystemTag\ISystemTag[] */
$tags = $tagManager->getTagsByIds($tagIds);
// Filter out the tags that are not user visible
$visible = array_filter($tags, static fn ($t) => $t->isUserVisible()); $visible = array_filter($tags, static fn ($t) => $t->isUserVisible());
// Get the tag names // Get the tag names

View File

@ -40,7 +40,7 @@ class OtherController extends GenericApiController
* @param string key the identifier to change * @param string key the identifier to change
* @param string value the value to set * @param string value the value to set
* *
* @return JSONResponse an empty JSONResponse with respective http status code * @return Http\Response empty JSONResponse with respective http status code
*/ */
public function setUserConfig(string $key, string $value): Http\Response public function setUserConfig(string $key, string $value): Http\Response
{ {
@ -77,7 +77,7 @@ class OtherController extends GenericApiController
} }
// helper function to get user config values // helper function to get user config values
$getAppConfig = function ($key, $default) use ($uid) { $getAppConfig = function (string $key, mixed $default) use ($uid): mixed {
return $this->config->getUserValue($uid, Application::APPNAME, $key, $default); return $this->config->getUserValue($uid, Application::APPNAME, $key, $default);
}; };
@ -124,7 +124,7 @@ class OtherController extends GenericApiController
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function describeApi(): JSONResponse public function describeApi(): Http\Response
{ {
return Util::guardEx(static function () { return Util::guardEx(static function () {
$appManager = \OC::$server->get(\OCP\App\IAppManager::class); $appManager = \OC::$server->get(\OCP\App\IAppManager::class);
@ -138,7 +138,7 @@ class OtherController extends GenericApiController
try { try {
$info['uid'] = Util::getUID(); $info['uid'] = Util::getUID();
} catch (\Exception $e) { } catch (\Exception) {
$info['uid'] = null; $info['uid'] = null;
} }

View File

@ -10,6 +10,7 @@ use OCA\Memories\Service\BinExt;
use OCA\Memories\Util; use OCA\Memories\Util;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\Template\PublicTemplateResponse; use OCP\AppFramework\Http\Template\PublicTemplateResponse;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\TemplateResponse;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
@ -29,7 +30,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function main() public function main(): Response
{ {
// Check native version if available // Check native version if available
$nativeVer = Util::callerNativeVersion(); $nativeVer = Util::callerNativeVersion();
@ -59,11 +60,11 @@ class PageController extends Controller
} }
/** Get the common content security policy */ /** Get the common content security policy */
public static function getCSP() public static function getCSP(): ContentSecurityPolicy
{ {
// Image domains MUST be added to the connect domain list // Image domains MUST be added to the connect domain list
// because of the service worker fetch() call // because of the service worker fetch() call
$addImageDomain = static function ($url) use (&$policy): void { $addImageDomain = static function (string $url) use (&$policy): void {
$policy->addAllowedImageDomain($url); $policy->addAllowedImageDomain($url);
$policy->addAllowedConnectDomain($url); $policy->addAllowedConnectDomain($url);
}; };
@ -114,7 +115,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function folder() public function folder(): Response
{ {
return $this->main(); return $this->main();
} }
@ -124,7 +125,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function favorites() public function favorites(): Response
{ {
return $this->main(); return $this->main();
} }
@ -134,7 +135,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function albums() public function albums(): Response
{ {
return $this->main(); return $this->main();
} }
@ -144,7 +145,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function videos() public function videos(): Response
{ {
return $this->main(); return $this->main();
} }
@ -154,7 +155,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function archive() public function archive(): Response
{ {
return $this->main(); return $this->main();
} }
@ -164,7 +165,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function thisday() public function thisday(): Response
{ {
return $this->main(); return $this->main();
} }
@ -174,7 +175,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function recognize() public function recognize(): Response
{ {
return $this->main(); return $this->main();
} }
@ -184,7 +185,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function facerecognition() public function facerecognition(): Response
{ {
return $this->main(); return $this->main();
} }
@ -194,7 +195,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function places() public function places(): Response
{ {
return $this->main(); return $this->main();
} }
@ -204,7 +205,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function tags() public function tags(): Response
{ {
return $this->main(); return $this->main();
} }
@ -214,7 +215,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function map() public function map(): Response
{ {
return $this->main(); return $this->main();
} }
@ -224,7 +225,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function explore() public function explore(): Response
{ {
return $this->main(); return $this->main();
} }
@ -234,7 +235,7 @@ class PageController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function nxsetup() public function nxsetup(): Response
{ {
return $this->main(); return $this->main();
} }

View File

@ -45,7 +45,7 @@ class PublicAlbumController extends Controller
* *
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function showShare(string $token) public function showShare(string $token): Response
{ {
// Validate token exists // Validate token exists
$album = $this->albumsQuery->getAlbumByLink($token); $album = $this->albumsQuery->getAlbumByLink($token);

View File

@ -25,6 +25,7 @@ use OCP\Share\IShare;
class PublicController extends AuthPublicShareController class PublicController extends AuthPublicShareController
{ {
/** @psalm-suppress PropertyNotSetInConstructor */
protected IShare $share; protected IShare $share;
public function __construct( public function __construct(
@ -65,7 +66,7 @@ class PublicController extends AuthPublicShareController
$this->share = $this->shareManager->getShareByToken($this->getToken()); $this->share = $this->shareManager->getShareByToken($this->getToken());
return true; return true;
} catch (\Exception $e) { } catch (\Exception) {
return false; return false;
} }
} }
@ -80,7 +81,7 @@ class PublicController extends AuthPublicShareController
// Check whether share exists // Check whether share exists
try { try {
$share = $this->shareManager->getShareByToken($this->getToken()); $share = $this->shareManager->getShareByToken($this->getToken());
} catch (\Exception $e) { } catch (\Exception) {
throw new NotFoundException(); throw new NotFoundException();
} }
@ -144,6 +145,7 @@ class PublicController extends AuthPublicShareController
protected function isPasswordProtected(): bool protected function isPasswordProtected(): bool
{ {
/** @psalm-suppress RedundantConditionGivenDocblockType */
return null !== $this->share->getPassword(); return null !== $this->share->getPassword();
} }
@ -215,11 +217,7 @@ class PublicController extends AuthPublicShareController
*/ */
private function getSingleItemInitialState(\OCP\Files\File $file): array private function getSingleItemInitialState(\OCP\Files\File $file): array
{ {
$data = $this->tq->getSingleItem($file->getId()); return $this->tq->getSingleItem($file->getId())
if (null === $data) { ?? throw new NotFoundException();
throw new NotFoundException();
}
return $data;
} }
} }

View File

@ -27,6 +27,8 @@ use OCA\Memories\Exceptions;
use OCA\Memories\Util; use OCA\Memories\Util;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\Share\IManager;
use OCP\Share\IShare;
class ShareController extends GenericApiController class ShareController extends GenericApiController
{ {
@ -34,19 +36,16 @@ class ShareController extends GenericApiController
* @NoAdminRequired * @NoAdminRequired
* *
* Get the tokens of a node shared using an external link * Get the tokens of a node shared using an external link
*
* @param mixed $id
* @param mixed $path
*/ */
public function links($id, $path): Http\Response public function links(?int $id, ?string $path): Http\Response
{ {
return Util::guardEx(function () use ($id, $path) { return Util::guardEx(function () use ($id, $path) {
$file = $this->getNodeByIdOrPath($id, $path); $file = $this->getNodeByIdOrPath($id, $path);
/** @var \OCP\Share\IManager $shareManager */ $shares = \OC::$server->get(IManager::class)
$shareManager = \OC::$server->get(\OCP\Share\IManager::class); ->getSharesBy(Util::getUID(), IShare::TYPE_LINK, $file, true, 50, 0)
;
$shares = $shareManager->getSharesBy(Util::getUID(), \OCP\Share\IShare::TYPE_LINK, $file, true, 50, 0);
if (empty($shares)) { if (empty($shares)) {
throw Exceptions::NotFound('external links'); throw Exceptions::NotFound('external links');
} }
@ -61,24 +60,21 @@ class ShareController extends GenericApiController
* @NoAdminRequired * @NoAdminRequired
* *
* Share a node using an external link * Share a node using an external link
*
* @param mixed $id
* @param mixed $path
*/ */
public function createNode($id, $path): Http\Response public function createNode(?int $id, ?string $path): Http\Response
{ {
return Util::guardEx(function () use ($id, $path) { return Util::guardEx(function () use ($id, $path) {
$file = $this->getNodeByIdOrPath($id, $path); $file = $this->getNodeByIdOrPath($id, $path);
$shareManager = \OC::$server->get(\OCP\Share\IManager::class); $manager = \OC::$server->get(IManager::class);
$share = $shareManager->newShare(); $share = $manager->createShare(
$share->setNode($file); $manager->newShare()
$share->setShareType(\OCP\Share\IShare::TYPE_LINK); ->setNode($file)
$share->setSharedBy($this->userSession->getUser()->getUID()); ->setShareType(\OCP\Share\IShare::TYPE_LINK)
$share->setPermissions(\OCP\Constants::PERMISSION_READ); ->setSharedBy(Util::getUID())
->setPermissions(\OCP\Constants::PERMISSION_READ),
$share = $shareManager->createShare($share); );
return new JSONResponse($this->makeShareResponse($share), Http::STATUS_OK); return new JSONResponse($this->makeShareResponse($share), Http::STATUS_OK);
}); });
@ -94,22 +90,21 @@ class ShareController extends GenericApiController
return Util::guardEx(static function () use ($id) { return Util::guardEx(static function () use ($id) {
$uid = Util::getUID(); $uid = Util::getUID();
/** @var \OCP\Share\IManager $shareManager */ $manager = \OC::$server->get(\OCP\Share\IManager::class);
$shareManager = \OC::$server->get(\OCP\Share\IManager::class);
$share = $shareManager->getShareById($id); $share = $manager->getShareById($id);
if ($share->getSharedBy() !== $uid) { if ($share->getSharedBy() !== $uid) {
throw Exceptions::Forbidden('You are not the owner of this share'); throw Exceptions::Forbidden('You are not the owner of this share');
} }
$shareManager->deleteShare($share); $manager->deleteShare($share);
return new JSONResponse([], Http::STATUS_OK); return new JSONResponse([], Http::STATUS_OK);
}); });
} }
private function getNodeByIdOrPath($id, $path): \OCP\Files\Node private function getNodeByIdOrPath(?int $id, ?string $path): \OCP\Files\Node
{ {
$uid = Util::getUID(); $uid = Util::getUID();
@ -120,7 +115,7 @@ class ShareController extends GenericApiController
} elseif ($path) { } elseif ($path) {
$file = Util::getUserFolder($uid)->get($path); $file = Util::getUserFolder($uid)->get($path);
} }
} catch (\OCP\Files\NotFoundException $e) { } catch (\OCP\Files\NotFoundException) {
throw Exceptions::NotFoundFile($path ?? $id); throw Exceptions::NotFoundFile($path ?? $id);
} }
@ -131,24 +126,26 @@ class ShareController extends GenericApiController
return $file; return $file;
} }
private function makeShareResponse(\OCP\Share\IShare $share): array private function makeShareResponse(IShare $share): array
{ {
/** @var \OCP\IURLGenerator $urlGenerator */ $token = $share->getToken();
$urlGenerator = \OC::$server->get(\OCP\IURLGenerator::class); $url = \OC::$server->get(\OCP\IURLGenerator::class)
->linkToRouteAbsolute('memories.Public.showShare', ['token' => $token])
;
$tok = $share->getToken(); /**
$exp = $share->getExpirationDate(); * @psalm-suppress RedundantConditionGivenDocblockType
$url = $urlGenerator->linkToRouteAbsolute('memories.Public.showShare', [ * @psalm-suppress DocblockTypeContradiction
'token' => $tok, */
]); $expiration = $share->getExpirationDate()?->getTimestamp();
return [ return [
'id' => $share->getFullId(), 'id' => $share->getFullId(),
'label' => $share->getLabel(), 'label' => $share->getLabel(),
'token' => $tok, 'token' => $token,
'url' => $url, 'url' => $url,
'hasPassword' => $share->getPassword() ? true : false, 'hasPassword' => $share->getPassword() ? true : false,
'expiration' => $exp ? $exp->getTimestamp() : null, 'expiration' => $expiration,
'editable' => $share->getPermissions() & \OCP\Constants::PERMISSION_UPDATE, 'editable' => $share->getPermissions() & \OCP\Constants::PERMISSION_UPDATE,
]; ];
} }

View File

@ -78,7 +78,7 @@ class VideoController extends GenericApiController
} }
// Request and check data was received // Request and check data was received
return Util::guardExDirect(function ($out) use ($client, $path, $profile) { return Util::guardExDirect(function (Http\IOutput $out) use ($client, $path, $profile) {
try { try {
$status = $this->getUpstream($client, $path, $profile); $status = $this->getUpstream($client, $path, $profile);
if (409 === $status || -1 === $status) { if (409 === $status || -1 === $status) {
@ -261,7 +261,7 @@ class VideoController extends GenericApiController
// Make sure query params are repeated // Make sure query params are repeated
// For example, in folder sharing, we need the params on every request // For example, in folder sharing, we need the params on every request
$url = BinExt::getGoVodUrl($client, $path, $profile); $url = BinExt::getGoVodUrl($client, $path, $profile);
if ($params = $_SERVER['QUERY_STRING']) { if (\array_key_exists('QUERY_STRING', $_SERVER) && !empty($params = $_SERVER['QUERY_STRING'])) {
$url .= "?{$params}"; $url .= "?{$params}";
} }
@ -275,7 +275,7 @@ class VideoController extends GenericApiController
// Stream the response to the browser without reading it into memory // Stream the response to the browser without reading it into memory
$headersWritten = false; $headersWritten = false;
curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($curl, $data) use (&$headersWritten, $profile) { curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function (mixed $curl, mixed $data) use (&$headersWritten, $profile) {
$returnCode = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE); $returnCode = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
if (200 === $returnCode) { if (200 === $returnCode) {
@ -318,9 +318,9 @@ class VideoController extends GenericApiController
/** /**
* POST to go-vod to create a temporary file. * POST to go-vod to create a temporary file.
* *
* @param mixed $blob * @return mixed The response from upstream
*/ */
private static function postFile(string $client, $blob) private static function postFile(string $client, mixed $blob): mixed
{ {
try { try {
return self::postFileInternal($client, $blob); return self::postFileInternal($client, $blob);
@ -333,7 +333,7 @@ class VideoController extends GenericApiController
} }
} }
private static function postFileInternal(string $client, $blob) private static function postFileInternal(string $client, mixed $blob): mixed
{ {
$url = BinExt::getGoVodUrl($client, '/create', 'ignore'); $url = BinExt::getGoVodUrl($client, '/create', 'ignore');
@ -352,7 +352,7 @@ class VideoController extends GenericApiController
throw new \Exception("Could not create temporary file ({$returnCode})"); throw new \Exception("Could not create temporary file ({$returnCode})");
} }
return json_decode($response, true); return json_decode((string) $response, true);
} }
/** /**
@ -366,6 +366,8 @@ class VideoController extends GenericApiController
// Get file paths for all live photos // Get file paths for all live photos
$liveFiles = array_map(fn ($r) => $this->rootFolder->getById((int) $r['fileid']), $liveRecords); $liveFiles = array_map(fn ($r) => $this->rootFolder->getById((int) $r['fileid']), $liveRecords);
$liveFiles = array_filter($liveFiles, static fn ($files) => \count($files) > 0 && $files[0] instanceof File); $liveFiles = array_filter($liveFiles, static fn ($files) => \count($files) > 0 && $files[0] instanceof File);
/** @var File[] (checked above) */
$liveFiles = array_map(static fn ($files) => $files[0], $liveFiles); $liveFiles = array_map(static fn ($files) => $files[0], $liveFiles);
// Should be filtered enough by now // Should be filtered enough by now
@ -380,7 +382,7 @@ class VideoController extends GenericApiController
// Remove extension so the filename itself counts in the path // Remove extension so the filename itself counts in the path
if (str_contains($filename, '.')) { if (str_contains($filename, '.')) {
$filename = substr($filename, 0, strrpos($filename, '.')); $filename = substr($filename, 0, strrpos($filename, '.') ?: null);
} }
// Get components with the filename as lowercase // Get components with the filename as lowercase

View File

@ -91,9 +91,9 @@ class AlbumsQuery
/** /**
* Check if an album has a file. * Check if an album has a file.
* *
* @return ?string owner of file * @return false|string owner of file if found
*/ */
public function hasFile(int $albumId, int $fileId): ?string public function hasFile(int $albumId, int $fileId): false|string
{ {
$query = $this->connection->getQueryBuilder(); $query = $this->connection->getQueryBuilder();
$query->select('owner')->from('photos_albums_files')->where( $query->select('owner')->from('photos_albums_files')->where(
@ -103,15 +103,15 @@ class AlbumsQuery
), ),
); );
return $query->executeQuery()->fetchOne() ?: null; return $query->executeQuery()->fetchOne();
} }
/** /**
* Check if a file belongs to a user through an album. * Check if a file belongs to a user through an album.
* *
* @return bool|string owner of file * @return false|string owner of file if found
*/ */
public function userHasFile(string $uid, int $fileId) public function userHasFile(string $uid, int $fileId): false|string
{ {
$query = $this->connection->getQueryBuilder(); $query = $this->connection->getQueryBuilder();
$query->select('paf.owner')->from('photos_albums_files', 'paf')->where( $query->select('paf.owner')->from('photos_albums_files', 'paf')->where(
@ -173,6 +173,7 @@ class AlbumsQuery
} }
// Check if user is owner // Check if user is owner
/** @psalm-suppress PossiblyUndefinedVariable */
if ($albumUid === $uid) { if ($albumUid === $uid) {
return $album; return $album;
} }
@ -264,15 +265,21 @@ class AlbumsQuery
return $result; return $result;
} }
/** Get list of collaborator ids including user id and groups */ /**
* Get the various collaborator IDs that a user has.
* This includes the groups the user is in and the user itself.
*
* @return string[] List of collaborator IDs
*/
private function getSelfCollaborators(string $uid) private function getSelfCollaborators(string $uid)
{ {
// Get the user in question
$user = \OC::$server->get(\OCP\IUserManager::class)->get($uid)
?: throw new \Exception('User not found');
// Get groups for the user // Get groups for the user
$groupManager = \OC::$server->get(\OCP\IGroupManager::class); $groups = \OC::$server->get(\OCP\IGroupManager::class)->getUserGroupIds($user);
$user = \OC::$server->get(\OCP\IUserManager::class)->get($uid);
$groups = $groupManager->getUserGroupIds($user);
// And albums shared with user // Add the user itself as a collaborator
$groups[] = $uid; $groups[] = $uid;
return $groups; return $groups;
@ -284,8 +291,7 @@ class AlbumsQuery
private function collaboratorsTable(): string private function collaboratorsTable(): string
{ {
// https://github.com/nextcloud/photos/commit/20e3e61ad577014e5f092a292c90a8476f630355 // https://github.com/nextcloud/photos/commit/20e3e61ad577014e5f092a292c90a8476f630355
$appManager = \OC::$server->get(\OCP\App\IAppManager::class); $photosVersion = \OC::$server->get(\OCP\App\IAppManager::class)->getAppVersion('photos');
$photosVersion = $appManager->getAppVersion('photos');
if (version_compare($photosVersion, '2.0.1', '>=')) { if (version_compare($photosVersion, '2.0.1', '>=')) {
return 'photos_albums_collabs'; return 'photos_albums_collabs';
} }

View File

@ -151,6 +151,8 @@ class FsManager
* *
* @param Folder $root root folder * @param Folder $root root folder
* @param string $key cache key * @param string $key cache key
*
* @return string[] List of paths
*/ */
public function getNoMediaFolders(Folder $root, string $key): array public function getNoMediaFolders(Folder $root, string $key): array
{ {
@ -303,7 +305,7 @@ class FsManager
} }
// Check if share is password protected // Check if share is password protected
if (($password = $share->getPassword()) !== null) { if (!empty($password = $share->getPassword())) {
$session = \OC::$server->get(\OCP\ISession::class); $session = \OC::$server->get(\OCP\ISession::class);
// https://github.com/nextcloud/server/blob/0447b53bda9fe95ea0cbed765aa332584605d652/lib/public/AppFramework/PublicShareController.php#L119 // https://github.com/nextcloud/server/blob/0447b53bda9fe95ea0cbed765aa332584605d652/lib/public/AppFramework/PublicShareController.php#L119
@ -318,7 +320,7 @@ class FsManager
return $share; return $share;
} }
public function getShareNode() public function getShareNode(): ?Node
{ {
$share = $this->getShareObject(); $share = $this->getShareObject();
if (null === $share) { if (null === $share) {
@ -400,6 +402,7 @@ class FsManager
Util::forcePermissions($file, $perm); Util::forcePermissions($file, $perm);
} }
/** @var File */
return $file; return $file;
} }

View File

@ -78,7 +78,9 @@ trait TimelineQueryCTE
return self::bundleCTEs([self::CTE_FOLDERS_ALL($hidden), $cte]); return self::bundleCTEs([self::CTE_FOLDERS_ALL($hidden), $cte]);
} }
/** CTE to get all archive folders recursively in the given top folders */ /**
* CTE to get all archive folders recursively in the given top folders.
*/
protected static function CTE_FOLDERS_ARCHIVE(): string protected static function CTE_FOLDERS_ARCHIVE(): string
{ {
$cte = "*PREFIX*cte_folders(fileid) AS ( $cte = "*PREFIX*cte_folders(fileid) AS (
@ -102,6 +104,9 @@ trait TimelineQueryCTE
return self::bundleCTEs([self::CTE_FOLDERS_ALL(true), $cte]); return self::bundleCTEs([self::CTE_FOLDERS_ALL(true), $cte]);
} }
/**
* @param string[] $ctes The CTEs to bundle
*/
protected static function bundleCTEs(array $ctes): string protected static function bundleCTEs(array $ctes): string
{ {
return 'WITH RECURSIVE '.implode(',', $ctes); return 'WITH RECURSIVE '.implode(',', $ctes);

View File

@ -33,10 +33,8 @@ trait TimelineQueryMap
); );
} }
public function getMapClusters( public function getMapClusters(float $gridLen, string $bounds): array
float $gridLen, {
string $bounds,
): array {
$query = $this->connection->getQueryBuilder(); $query = $this->connection->getQueryBuilder();
// Get the average location of each cluster // Get the average location of each cluster
@ -84,6 +82,11 @@ trait TimelineQueryMap
return $clusters; return $clusters;
} }
/**
* Gets previews for a list of map clusters.
*
* @param int[] $clusterIds
*/
public function getMapClusterPreviews(array $clusterIds): array public function getMapClusterPreviews(array $clusterIds): array
{ {
$query = $this->connection->getQueryBuilder(); $query = $this->connection->getQueryBuilder();
@ -131,6 +134,8 @@ trait TimelineQueryMap
/** /**
* Gets the suggested initial coordinates for the map. * Gets the suggested initial coordinates for the map.
* Uses the coordinates of the newest photo (by date). * Uses the coordinates of the newest photo (by date).
*
* @psalm-return array{lat: float, lon: float}|null
*/ */
public function getMapInitialPosition(): ?array public function getMapInitialPosition(): ?array
{ {

View File

@ -8,7 +8,10 @@ use OCP\Files\FileInfo;
class TimelineRoot class TimelineRoot
{ {
/** @var array<int, \OCP\Files\FileInfo> */
protected array $folders = []; protected array $folders = [];
/** @var array<int, string> */
protected array $folderPaths = []; protected array $folderPaths = [];
/** /**
@ -29,7 +32,7 @@ class TimelineRoot
} }
// Add top level folder // Add top level folder
$this->setFolder($info->getId(), $info, $path); $this->setFolder($info->getId() ?? 0, $info, $path);
} }
/** /**
@ -89,28 +92,18 @@ class TimelineRoot
} }
} }
public function getFolderPath(int $id) /** @return int[] */
{
return $this->folderPaths[$id];
}
/**
* @return int[]
*/
public function getIds(): array public function getIds(): array
{ {
return array_keys($this->folderPaths); return array_keys($this->folderPaths);
} }
/** public function getOneId(): ?int
* @return null|int
*/
public function getOneId()
{ {
return array_key_first($this->folders); return array_key_first($this->folders);
} }
public function getFolder(int $id) public function getFolder(int $id): ?FileInfo
{ {
return $this->folders[$id]; return $this->folders[$id];
} }

View File

@ -138,7 +138,7 @@ class TimelineWrite
$buid = $prevRow ? $prevRow['buid'] : ''; $buid = $prevRow ? $prevRow['buid'] : '';
if (empty($buid)) { if (empty($buid)) {
$imageUniqueId = \array_key_exists('ImageUniqueID', $exif) ? $exif['ImageUniqueID'] : null; $imageUniqueId = \array_key_exists('ImageUniqueID', $exif) ? $exif['ImageUniqueID'] : null;
$buid = Exif::getBUID($file->getName(), $imageUniqueId, $file->getSize()); $buid = Exif::getBUID($file->getName(), $imageUniqueId, (int) $file->getSize());
} }
// Get exif json // Get exif json
@ -185,7 +185,7 @@ class TimelineWrite
/** /**
* Remove a file from the exif database. * Remove a file from the exif database.
*/ */
public function deleteFile(File &$file): void public function deleteFile(File $file): void
{ {
// Get full record // Get full record
$query = $this->connection->getQueryBuilder(); $query = $this->connection->getQueryBuilder();
@ -261,7 +261,7 @@ class TimelineWrite
*/ */
private function getCurrentRow(int $fileId): ?array private function getCurrentRow(int $fileId): ?array
{ {
$fetch = function (string $table) use ($fileId) { $fetch = function (string $table) use ($fileId): false|null|array {
$query = $this->connection->getQueryBuilder(); $query = $this->connection->getQueryBuilder();
return $query->select('*') return $query->select('*')
@ -277,6 +277,8 @@ class TimelineWrite
/** /**
* Convert EXIF data to filtered JSON string. * Convert EXIF data to filtered JSON string.
*
* @param array<string, mixed> $exif EXIF data
*/ */
private function getExifJson(array $exif): string private function getExifJson(array $exif): string
{ {

View File

@ -49,7 +49,7 @@ trait TimelineWriteOrphans
* *
* @param array $fields list of fields to select * @param array $fields list of fields to select
* @param int $txnSize number of rows to process in a single transaction * @param int $txnSize number of rows to process in a single transaction
* @param \Closure $callback will be passed each row * @param \Closure(array): void $callback will be passed each row
*/ */
public function orphanAndRun(array $fields, int $txnSize, \Closure $callback): void public function orphanAndRun(array $fields, int $txnSize, \Closure $callback): void
{ {
@ -74,6 +74,9 @@ trait TimelineWriteOrphans
/** /**
* Get a list of orphaned files. * Get a list of orphaned files.
*
* @param int $count max number of rows to return
* @param string[] $fields list of fields to select
*/ */
protected function getSomeOrphans(int $count, array $fields): array protected function getSomeOrphans(int $count, array $fields): array
{ {

View File

@ -19,12 +19,12 @@ trait TimelineWritePlaces
* Add places data for a file. * Add places data for a file.
* *
* @param int $fileId The file ID * @param int $fileId The file ID
* @param null|float $lat The latitude of the file * @param ?float $lat The latitude of the file
* @param null|float $lon The longitude of the file * @param ?float $lon The longitude of the file
* *
* @return array The list of osm_id of the places * @return int[] The list of osm_id of the places
*/ */
public function updatePlacesData(int $fileId, $lat, $lon): array public function updatePlacesData(int $fileId, ?float $lat, ?float $lon): array
{ {
// Get GIS type // Get GIS type
$gisType = \OCA\Memories\Util::placesGISType(); $gisType = \OCA\Memories\Util::placesGISType();
@ -74,7 +74,7 @@ trait TimelineWritePlaces
$this->connection->commit(); $this->connection->commit();
// Return list of osm_id // Return list of osm_id
return array_map(static fn ($row) => $row['osm_id'], $rows); return array_map(static fn ($row) => (int) $row['osm_id'], $rows);
} }
/** /**
@ -155,7 +155,7 @@ trait TimelineWritePlaces
/** /**
* Read coordinates from array and round to 6 decimal places. * Read coordinates from array and round to 6 decimal places.
* *
* Modifies the array to remove invalid coordinates. * Modifies the EXIF array to remove invalid coordinates.
* *
* @return (null|float)[] * @return (null|float)[]
* *

View File

@ -44,7 +44,7 @@ class Exceptions
], Http::STATUS_NOT_FOUND)); ], Http::STATUS_NOT_FOUND));
} }
public static function NotFoundFile($identifier): HttpResponseException public static function NotFoundFile(null|int|string $identifier): HttpResponseException
{ {
return new HttpResponseException(new DataResponse([ return new HttpResponseException(new DataResponse([
'message' => "File not found ({$identifier})", 'message' => "File not found ({$identifier})",

View File

@ -15,21 +15,31 @@ class Exif
private const EXIFTOOL_ARGS = ['-api', 'QuickTimeUTC=1', '-n', '-json']; private const EXIFTOOL_ARGS = ['-api', 'QuickTimeUTC=1', '-n', '-json'];
/** Opened instance of exiftool when running in command mode */ /** Opened instance of exiftool when running in command mode */
/** @var null|resource */
private static $staticProc; private static $staticProc;
/** @var null|resource[] */
private static $staticPipes; private static $staticPipes;
/** Disable uisage of static process */
private static bool $noStaticProc = false; private static bool $noStaticProc = false;
public static function closeStaticExiftoolProc(): void public static function closeStaticExiftoolProc(): void
{ {
try { try {
if (self::$staticProc) { // Close I/O pipes
if (self::$staticPipes) {
fclose(self::$staticPipes[0]); fclose(self::$staticPipes[0]);
fclose(self::$staticPipes[1]); fclose(self::$staticPipes[1]);
fclose(self::$staticPipes[2]); fclose(self::$staticPipes[2]);
self::$staticPipes = null;
}
// Close process
if (self::$staticProc) {
proc_terminate(self::$staticProc); proc_terminate(self::$staticProc);
proc_close(self::$staticProc); proc_close(self::$staticProc);
self::$staticProc = null; self::$staticProc = null;
self::$staticPipes = null;
} }
} catch (\Exception $ex) { } catch (\Exception $ex) {
} }
@ -50,10 +60,13 @@ class Exif
if (!self::$staticProc) { if (!self::$staticProc) {
self::initializeStaticExiftoolProc(); self::initializeStaticExiftoolProc();
usleep(500000); // wait if error usleep(500000); // wait if error
/** @psalm-suppress NullArgument */
if (!proc_get_status(self::$staticProc)['running']) { if (!proc_get_status(self::$staticProc)['running']) {
error_log('WARN: Failed to create stay_open exiftool process'); error_log('WARN: Failed to create stay_open exiftool process');
self::$noStaticProc = true; self::$noStaticProc = true;
self::$staticProc = null; self::$staticProc = null;
self::$staticPipes = null;
} }
return; return;
@ -61,12 +74,15 @@ class Exif
if (!proc_get_status(self::$staticProc)['running']) { if (!proc_get_status(self::$staticProc)['running']) {
self::$staticProc = null; self::$staticProc = null;
self::$staticPipes = null;
self::ensureStaticExiftoolProc(); self::ensureStaticExiftoolProc();
} }
} }
/** /**
* Get exif data as a JSON object from a Nextcloud file. * Get exif data as a JSON object from a Nextcloud file.
*
* @return array<string, mixed>
*/ */
public static function getExifFromFile(File $file): array public static function getExifFromFile(File $file): array
{ {
@ -108,6 +124,8 @@ class Exif
/** /**
* Get exif data as a JSON object from a local file path. * Get exif data as a JSON object from a local file path.
*
* @return array<string, mixed>
*/ */
public static function getExifFromLocalPath(string $path): array public static function getExifFromLocalPath(string $path): array
{ {
@ -122,6 +140,8 @@ class Exif
/** /**
* Parse date from exif format and throw error if invalid. * Parse date from exif format and throw error if invalid.
*
* @param array<string, mixed> $exif
*/ */
public static function parseExifDate(array $exif): \DateTime public static function parseExifDate(array $exif): \DateTime
{ {
@ -140,9 +160,12 @@ class Exif
// Get timezone from exif // Get timezone from exif
try { try {
$exifTz = $exif['OffsetTimeOriginal'] ?? $exif['OffsetTime'] ?? $exif['LocationTZID'] ?? null; $tzStr = $exif['OffsetTimeOriginal']
$exifTz = new \DateTimeZone($exifTz); ?: $exif['OffsetTime']
} catch (\Error $e) { ?: $exif['LocationTZID']
?: throw new \Exception();
$exifTz = new \DateTimeZone((string) $tzStr);
} catch (\Exception) {
$exifTz = null; $exifTz = null;
} }
@ -200,13 +223,15 @@ class Exif
/** /**
* Get the date taken from either the file or exif data if available. * Get the date taken from either the file or exif data if available.
*
* @param array<string, mixed> $exif
*/ */
public static function getDateTaken(File $file, array $exif): \DateTime public static function getDateTaken(File $file, array $exif): \DateTime
{ {
try { try {
return self::parseExifDate($exif); return self::parseExifDate($exif);
} catch (\Exception $ex) { } catch (\Exception) {
} catch (\ValueError $ex) { } catch (\ValueError) {
} }
// Fall back to modification time // Fall back to modification time
@ -217,7 +242,7 @@ class Exif
try { try {
$dt->setTimezone(new \DateTimeZone($tz)); $dt->setTimezone(new \DateTimeZone($tz));
} catch (\Exception $e) { } catch (\Exception) {
throw new \Error("FATAL: system timezone is invalid (TZ): {$tz}"); throw new \Error("FATAL: system timezone is invalid (TZ): {$tz}");
} }
@ -235,9 +260,13 @@ class Exif
/** /**
* Get image dimensions from Exif data. * Get image dimensions from Exif data.
* *
* @return array [width, height] * @param array<string, mixed> $exif
*
* @return int[]
*
* @psalm-return list{int, int}
*/ */
public static function getDimensions(array $exif) public static function getDimensions(array $exif): array
{ {
$width = $exif['ImageWidth'] ?? 0; $width = $exif['ImageWidth'] ?? 0;
$height = $exif['ImageHeight'] ?? 0; $height = $exif['ImageHeight'] ?? 0;
@ -274,7 +303,7 @@ class Exif
* @param mixed $imageUniqueID EXIF field * @param mixed $imageUniqueID EXIF field
* @param int $size the file size in bytes (fallback) * @param int $size the file size in bytes (fallback)
*/ */
public static function getBUID(string $basename, $imageUniqueID, int $size): string public static function getBUID(string $basename, mixed $imageUniqueID, int $size): string
{ {
$sfx = "size={$size}"; $sfx = "size={$size}";
if (null !== $imageUniqueID && \strlen((string) $imageUniqueID) >= 4) { if (null !== $imageUniqueID && \strlen((string) $imageUniqueID) >= 4) {
@ -296,7 +325,7 @@ class Exif
* Set exif data using raw json. * Set exif data using raw json.
* *
* @param string $path to local file * @param string $path to local file
* @param array $data exif data * @param array<string, mixed> $data exif data
* *
* @throws \Exception on failure * @throws \Exception on failure
*/ */
@ -330,6 +359,11 @@ class Exif
} }
} }
/**
* Set exif data using a raw array.
*
* @param array<string, mixed> $data exif data
*/
public static function setFileExif(File $file, array $data): void public static function setFileExif(File $file, array $data): void
{ {
// Get path to local file so we can skip reading // Get path to local file so we can skip reading
@ -389,6 +423,7 @@ class Exif
private static function initializeStaticExiftoolProc(): void private static function initializeStaticExiftoolProc(): void
{ {
self::closeStaticExiftoolProc(); self::closeStaticExiftoolProc();
self::$staticPipes = [];
self::$staticProc = proc_open(array_merge(self::getExiftool(), ['-stay_open', 'true', '-@', '-']), [ self::$staticProc = proc_open(array_merge(self::getExiftool(), ['-stay_open', 'true', '-@', '-']), [
0 => ['pipe', 'r'], 0 => ['pipe', 'r'],
1 => ['pipe', 'w'], 1 => ['pipe', 'w'],
@ -429,19 +464,34 @@ class Exif
private static function getExifFromLocalPathWithStaticProc(string $path): array private static function getExifFromLocalPathWithStaticProc(string $path): array
{ {
// This function should not be called if there is no static process
if (!self::$staticPipes) {
throw new \Error('[BUG] No static pipes found');
}
// Create arguments for exiftool
$args = implode("\n", self::EXIFTOOL_ARGS); $args = implode("\n", self::EXIFTOOL_ARGS);
fwrite(self::$staticPipes[0], "{$path}\n{$args}\n-execute\n"); fwrite(self::$staticPipes[0], "{$path}\n{$args}\n-execute\n");
fflush(self::$staticPipes[0]); fflush(self::$staticPipes[0]);
// The output of exiftool's stay_open process ends with this token
$readyToken = "\n{ready}\n"; $readyToken = "\n{ready}\n";
try { try {
$buf = self::readOrTimeout(self::$staticPipes[1], self::EXIFTOOL_TIMEOUT, $readyToken); $buf = self::readOrTimeout(self::$staticPipes[1], self::EXIFTOOL_TIMEOUT, $readyToken);
// The output buffer should always contain the ready token
// (this is the point of readOrTimeout)
$tokPos = strrpos($buf, $readyToken); $tokPos = strrpos($buf, $readyToken);
if (false === $tokPos) {
throw new \Error('[BUG] No ready token found in output buffer');
}
// Slice everything before the ready token
$buf = substr($buf, 0, $tokPos); $buf = substr($buf, 0, $tokPos);
return self::processStdout($buf); return self::processStdout($buf);
} catch (\Exception $ex) { } catch (\Exception) {
error_log("ERROR: Exiftool may have crashed, restarting process [{$path}]"); error_log("ERROR: Exiftool may have crashed, restarting process [{$path}]");
self::restartStaticExiftoolProc(); self::restartStaticExiftoolProc();

View File

@ -126,10 +126,8 @@ class BinExt
/** /**
* Detect the exiftool binary to use. * Detect the exiftool binary to use.
*
* @return false|string
*/ */
public static function detectExiftool() public static function detectExiftool(): false|string
{ {
if (!empty($path = Util::getSystemConfig('memories.exiftool'))) { if (!empty($path = Util::getSystemConfig('memories.exiftool'))) {
return $path; return $path;
@ -371,10 +369,8 @@ class BinExt
/** /**
* Detect the go-vod binary to use. * Detect the go-vod binary to use.
*
* @return false|string
*/ */
public static function detectGoVod() public static function detectGoVod(): false|string
{ {
$goVodPath = Util::getSystemConfig('memories.vod.path'); $goVodPath = Util::getSystemConfig('memories.vod.path');
@ -445,7 +441,7 @@ class BinExt
return explode(' ', $matches[0])[2]; return explode(' ', $matches[0])[2];
} }
public static function testSystemPerl(string $path): ?string public static function testSystemPerl(string $path): string
{ {
/** @psalm-suppress ForbiddenCode */ /** @psalm-suppress ForbiddenCode */
if (($out = shell_exec("{$path} -e 'print \"OK\";'")) !== 'OK') { if (($out = shell_exec("{$path} -e 'print \"OK\";'")) !== 'OK') {
@ -453,6 +449,6 @@ class BinExt
} }
/** @psalm-suppress ForbiddenCode */ /** @psalm-suppress ForbiddenCode */
return shell_exec("{$path} -e 'print $^V;'") ?: null; return shell_exec("{$path} -e 'print $^V;'") ?: 'unknown version';
} }
} }

View File

@ -49,7 +49,7 @@ class FileRobotImageState
/** 0 to 200 */ /** 0 to 200 */
public ?float $warmth = null; public ?float $warmth = null;
/** Order of filters */ /** @var string[] Order of filters */
public array $finetuneOrder = []; public array $finetuneOrder = [];
/** Crop X coordinate */ /** Crop X coordinate */

View File

@ -46,9 +46,12 @@ class Index
/** /**
* Callback to check if the process should continue. * Callback to check if the process should continue.
* This is called before every file is indexed. * This is called before every file is indexed.
*
* @var null|\Closure(): bool
*/ */
public ?\Closure $continueCheck = null; public ?\Closure $continueCheck = null;
/** @var string[] */
private static ?array $mimeList = null; private static ?array $mimeList = null;
public function __construct( public function __construct(

View File

@ -23,11 +23,17 @@ class Admin implements ISettings
return new TemplateResponse('memories', 'main', PageController::getMainParams()); return new TemplateResponse('memories', 'main', PageController::getMainParams());
} }
/**
* @return string
*/
public function getSection() public function getSection()
{ {
return Application::APPNAME; return Application::APPNAME;
} }
/**
* @return int
*/
public function getPriority() public function getPriority()
{ {
return 50; return 50;

View File

@ -21,27 +21,36 @@ class AdminSection implements IIconSection
*/ */
public function getForm() public function getForm()
{ {
$parameters = [ return new TemplateResponse('memories', 'admin', []);
];
return new TemplateResponse('memories', 'admin', $parameters);
} }
/**
* @return string
*/
public function getID() public function getID()
{ {
return 'memories'; return 'memories';
} }
/**
* @return string
*/
public function getName() public function getName()
{ {
return $this->l->t('Memories'); return $this->l->t('Memories');
} }
/**
* @return int
*/
public function getPriority() public function getPriority()
{ {
return 75; return 75;
} }
/**
* @return string
*/
public function getIcon() public function getIcon()
{ {
return $this->urlGenerator->imagePath('memories', 'app-dark.svg'); return $this->urlGenerator->imagePath('memories', 'app-dark.svg');

View File

@ -27,11 +27,11 @@ class Util
*/ */
public static function getArch(): ?string public static function getArch(): ?string
{ {
$uname = php_uname('m'); $uname = strtolower(php_uname('m') ?: 'unknown');
if (false !== stripos($uname, 'aarch64') || false !== stripos($uname, 'arm64')) { if (str_contains($uname, 'aarch64') || str_contains($uname, 'arm64')) {
return 'aarch64'; return 'aarch64';
} }
if (false !== stripos($uname, 'x86_64') || false !== stripos($uname, 'amd64')) { if (str_contains($uname, 'x86_64') || str_contains($uname, 'amd64')) {
return 'amd64'; return 'amd64';
} }
@ -46,14 +46,13 @@ class Util
public static function getLibc(): ?string public static function getLibc(): ?string
{ {
/** @psalm-suppress ForbiddenCode */ /** @psalm-suppress ForbiddenCode */
if ($ldd = shell_exec('ldd --version 2>&1')) { $ldd = strtolower(shell_exec('ldd --version 2>&1') ?: 'unknown');
if (false !== stripos($ldd, 'musl')) { if (str_contains($ldd, 'musl')) {
return 'musl'; return 'musl';
} }
if (false !== stripos($ldd, 'glibc')) { if (str_contains($ldd, 'glibc')) {
return 'glibc'; return 'glibc';
} }
}
return null; return null;
} }
@ -79,9 +78,7 @@ class Util
*/ */
public static function tagsIsEnabled(): bool public static function tagsIsEnabled(): bool
{ {
$appManager = \OC::$server->get(IAppManager::class); return \OC::$server->get(IAppManager::class)->isEnabledForUser('systemtags');
return $appManager->isEnabledForUser('systemtags');
} }
/** /**
@ -130,16 +127,14 @@ class Util
} }
try { try {
$uid = self::getUID(); return 'true' === \OC::$server->get(IConfig::class)
} catch (\Exception $e) { ->getUserValue(self::getUID(), 'facerecognition', 'enabled', 'false')
return false; ;
} catch (\Exception) {
// not logged in
} }
$enabled = \OC::$server->get(IConfig::class) return false;
->getUserValue($uid, 'facerecognition', 'enabled', 'false')
;
return 'true' === $enabled;
} }
/** /**
@ -163,9 +158,7 @@ class Util
*/ */
public static function previewGeneratorIsEnabled(): bool public static function previewGeneratorIsEnabled(): bool
{ {
$appManager = \OC::$server->get(IAppManager::class); return \OC::$server->get(IAppManager::class)->isEnabledForUser('previewgenerator');
return $appManager->isEnabledForUser('previewgenerator');
} }
/** /**
@ -196,11 +189,11 @@ class Util
* Force a fileinfo value on a node. * Force a fileinfo value on a node.
* This is a hack to avoid subclassing everything. * This is a hack to avoid subclassing everything.
* *
* @param mixed $node File to patch * @param Node $node File to patch
* @param mixed $key Key to set * @param string $key Key to set
* @param mixed $value Value to set * @param mixed $value Value to set
*/ */
public static function forceFileInfo(Node &$node, $key, $value): void public static function forceFileInfo(Node &$node, string $key, mixed $value): void
{ {
/** @var \OC\Files\Node\Node */ /** @var \OC\Files\Node\Node */
$node = $node; $node = $node;
@ -212,8 +205,8 @@ class Util
/** /**
* Force permissions on a node. * Force permissions on a node.
* *
* @param mixed $node File to patch * @param Node $node File to patch
* @param mixed $permissions Permissions to set * @param int $permissions Permissions to set
*/ */
public static function forcePermissions(Node &$node, int $permissions): void public static function forcePermissions(Node &$node, int $permissions): void
{ {
@ -248,10 +241,10 @@ class Util
/** /**
* Add OG metadata to a page for a node. * Add OG metadata to a page for a node.
* *
* @param $node Node to get metadata from * @param Node $node Node to get metadata from
* @param $title Title of the page * @param string $title Title of the page
* @param $url URL of the page * @param string $url URL of the page
* @param $previewArgs Preview arguments (e.g. token) * @param array $previewArgs Preview arguments (e.g. token)
*/ */
public static function addOgMetadata(Node $node, string $title, string $url, array $previewArgs): void public static function addOgMetadata(Node $node, string $title, string $url, array $previewArgs): void
{ {
@ -291,8 +284,6 @@ class Util
/** /**
* Get a random image or video from a given folder. * Get a random image or video from a given folder.
*
* @param $folder Folder to search
*/ */
public static function getAnyMedia(\OCP\Files\Folder $folder): ?Node public static function getAnyMedia(\OCP\Files\Folder $folder): ?Node
{ {
@ -335,36 +326,40 @@ class Util
/** /**
* Get list of timeline paths as array. * Get list of timeline paths as array.
*
* @return string[] List of paths
*/ */
public static function getTimelinePaths(string $uid): array public static function getTimelinePaths(string $uid): array
{ {
$config = \OC::$server->get(IConfig::class); $paths = \OC::$server->get(IConfig::class)
$paths = $config->getUserValue($uid, Application::APPNAME, 'timelinePath', null) ->getUserValue($uid, Application::APPNAME, 'timelinePath', null)
?? self::getSystemConfig('memories.timeline.default_path'); ?: self::getSystemConfig('memories.timeline.default_path');
return array_map(static fn ($p) => self::sanitizePath(trim($p)), explode(';', $paths)); return array_map(
static fn ($p) => self::sanitizePath(trim($p)),
explode(';', $paths),
);
} }
/** /**
* Sanitize a path to keep only ASCII characters and special characters. * Sanitize a path to keep only ASCII characters and special characters.
* Blank will be returned on error.
*/ */
public static function sanitizePath(string $path): ?string public static function sanitizePath(string $path): string
{ {
$path = str_replace("\0", '', $path); // remove null characters $path = str_replace("\0", '', $path); // remove null characters
return mb_ereg_replace('\/\/+', '/', $path) ?: null; // remove extra slashes return mb_ereg_replace('\/\/+', '/', $path) ?: ''; // remove extra slashes
} }
/** /**
* Convert SQL UTC date to timestamp. * Convert SQL UTC date to timestamp.
*
* @param mixed $sqlDate
*/ */
public static function sqlUtcToTimestamp($sqlDate): int public static function sqlUtcToTimestamp(string $sqlDate): int
{ {
try { try {
return (new \DateTime($sqlDate, new \DateTimeZone('UTC')))->getTimestamp(); return (new \DateTime($sqlDate, new \DateTimeZone('UTC')))->getTimestamp();
} catch (\Throwable $e) { } catch (\Throwable) {
return 0; return 0;
} }
} }
@ -372,7 +367,7 @@ class Util
/** /**
* Explode a string into fixed number of components. * Explode a string into fixed number of components.
* *
* @param string $delimiter Delimiter * @param non-empty-string $delimiter Delimiter
* @param string $string String to explode * @param string $string String to explode
* @param int $count Number of components * @param int $count Number of components
* *
@ -387,9 +382,9 @@ class Util
* Get a system config key with the correct default. * Get a system config key with the correct default.
* *
* @param string $key System config key * @param string $key System config key
* @param null|mixed $default Default value * @param mixed $default Default value
*/ */
public static function getSystemConfig(string $key, $default = null) public static function getSystemConfig(string $key, mixed $default = null): mixed
{ {
$config = \OC::$server->get(\OCP\IConfig::class); $config = \OC::$server->get(\OCP\IConfig::class);
@ -404,11 +399,9 @@ class Util
/** /**
* Set a system config key. * Set a system config key.
* *
* @param mixed $value
*
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public static function setSystemConfig(string $key, $value): void public static function setSystemConfig(string $key, mixed $value): void
{ {
$config = \OC::$server->get(\OCP\IConfig::class); $config = \OC::$server->get(\OCP\IConfig::class);

View File

@ -12,12 +12,12 @@ trait UtilController
/** /**
* Run a function and catch exceptions to return HTTP response. * Run a function and catch exceptions to return HTTP response.
* *
* @param mixed $function * @param \Closure(): Http\Response $closure
*/ */
public static function guardEx($function): Http\Response public static function guardEx(\Closure $closure): Http\Response
{ {
try { try {
return $function(); return $closure();
} catch (\OCA\Memories\HttpResponseException $e) { } catch (\OCA\Memories\HttpResponseException $e) {
return $e->response; return $e->response;
} catch (\Exception $e) { } catch (\Exception $e) {
@ -29,23 +29,25 @@ trait UtilController
/** /**
* Return a callback response with guarded exceptions. * Return a callback response with guarded exceptions.
*
* @param \Closure(Http\IOutput): void $closure
*/ */
public static function guardExDirect(\Closure $closure): Http\Response public static function guardExDirect(\Closure $closure): Http\Response
{ {
/** @psalm-suppress MissingTemplateParam */ /** @psalm-suppress MissingTemplateParam */
return new class($closure) extends Http\Response implements Http\ICallbackResponse { return new class($closure) extends Http\Response implements Http\ICallbackResponse {
private \Closure $_closure; /**
* @param \Closure(Http\IOutput): void $closure
public function __construct(\Closure $closure) */
public function __construct(private \Closure $closure)
{ {
parent::__construct(); parent::__construct();
$this->_closure = $closure;
} }
public function callback(Http\IOutput $output) public function callback(Http\IOutput $output)
{ {
try { try {
($this->_closure)($output); ($this->closure)($output);
} catch (\OCA\Memories\HttpResponseException $e) { } catch (\OCA\Memories\HttpResponseException $e) {
$res = $e->response; $res = $e->response;
$output->setHttpResponseCode($res->getStatus()); $output->setHttpResponseCode($res->getStatus());
@ -73,12 +75,8 @@ trait UtilController
*/ */
public static function getUser(): \OCP\IUser public static function getUser(): \OCP\IUser
{ {
$user = \OC::$server->get(\OCP\IUserSession::class)->getUser(); return \OC::$server->get(\OCP\IUserSession::class)->getUser()
if (null === $user) { ?? throw Exceptions::NotLoggedIn();
throw Exceptions::NotLoggedIn();
}
return $user;
} }
/** /**
@ -102,17 +100,15 @@ trait UtilController
/** /**
* Get a user's home folder. * Get a user's home folder.
* *
* @param null|string $uid User ID, or null for current user * @param null|string $uid User ID, or null for the user
* *
* @throws \OCA\Memories\HttpResponseException if the user is not logged in * @throws \OCA\Memories\HttpResponseException if the user is not logged in
*/ */
public static function getUserFolder(?string $uid = null): \OCP\Files\Folder public static function getUserFolder(?string $uid = null): \OCP\Files\Folder
{ {
if (null === $uid) { return \OC::$server->get(\OCP\Files\IRootFolder::class)
$uid = self::getUID(); ->getUserFolder($uid ?? self::getUID())
} ;
return \OC::$server->get(\OCP\Files\IRootFolder::class)->getUserFolder($uid);
} }
/** /**
@ -124,14 +120,12 @@ trait UtilController
$config = \OC::$server->get(\OCP\IConfig::class); $config = \OC::$server->get(\OCP\IConfig::class);
$default = $config->getSystemValue('default_language', 'en'); $default = $config->getSystemValue('default_language', 'en');
// Get UID of the user
try { try {
$uid = self::getUID();
} catch (\Exception $e) {
return 'en';
}
// Get language of the user // Get language of the user
return $config->getUserValue($uid, 'core', 'lang', $default); return $config->getUserValue(self::getUID(), 'core', 'lang', $default);
} catch (\Exception) {
// Fallback to server language
return $default;
}
} }
} }