diff --git a/apps/nominatim/appinfo/info.xml b/apps/nominatim/appinfo/info.xml new file mode 100644 index 0000000000000..f247fa7114b44 --- /dev/null +++ b/apps/nominatim/appinfo/info.xml @@ -0,0 +1,18 @@ + + + nominatim + Nominatim OpenStreetMap + Location provider for the Nominatim OpenStreetMap backend + + 0.1.0 + agpl + Thomas Citharel + Nominatim + integration + location + https://github.com/nextcloud/server + + + + diff --git a/apps/nominatim/appinfo/routes.php b/apps/nominatim/appinfo/routes.php new file mode 100644 index 0000000000000..3e76f54d52fce --- /dev/null +++ b/apps/nominatim/appinfo/routes.php @@ -0,0 +1,26 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +return []; diff --git a/apps/nominatim/composer/autoload.php b/apps/nominatim/composer/autoload.php new file mode 100644 index 0000000000000..f04cad745c78d --- /dev/null +++ b/apps/nominatim/composer/autoload.php @@ -0,0 +1,25 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/apps/nominatim/composer/composer/InstalledVersions.php b/apps/nominatim/composer/composer/InstalledVersions.php new file mode 100644 index 0000000000000..51e734a774b3e --- /dev/null +++ b/apps/nominatim/composer/composer/InstalledVersions.php @@ -0,0 +1,359 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/apps/nominatim/composer/composer/LICENSE b/apps/nominatim/composer/composer/LICENSE new file mode 100644 index 0000000000000..f27399a042d95 --- /dev/null +++ b/apps/nominatim/composer/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/apps/nominatim/composer/composer/autoload_classmap.php b/apps/nominatim/composer/composer/autoload_classmap.php new file mode 100644 index 0000000000000..3c42d8be09ba3 --- /dev/null +++ b/apps/nominatim/composer/composer/autoload_classmap.php @@ -0,0 +1,12 @@ + $vendorDir . '/composer/InstalledVersions.php', + 'OCA\\Nominatim\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', + 'OCA\\Nominatim\\LocationProvider' => $baseDir . '/../lib/LocationProvider.php', +); diff --git a/apps/nominatim/composer/composer/autoload_namespaces.php b/apps/nominatim/composer/composer/autoload_namespaces.php new file mode 100644 index 0000000000000..3f5c929625125 --- /dev/null +++ b/apps/nominatim/composer/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($baseDir . '/../lib'), +); diff --git a/apps/nominatim/composer/composer/autoload_real.php b/apps/nominatim/composer/composer/autoload_real.php new file mode 100644 index 0000000000000..8a18ead210d8c --- /dev/null +++ b/apps/nominatim/composer/composer/autoload_real.php @@ -0,0 +1,37 @@ +setClassMapAuthoritative(true); + $loader->register(true); + + return $loader; + } +} diff --git a/apps/nominatim/composer/composer/autoload_static.php b/apps/nominatim/composer/composer/autoload_static.php new file mode 100644 index 0000000000000..2cf64cd88c4c0 --- /dev/null +++ b/apps/nominatim/composer/composer/autoload_static.php @@ -0,0 +1,38 @@ + + array ( + 'OCA\\Nominatim\\' => 14, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'OCA\\Nominatim\\' => + array ( + 0 => __DIR__ . '/..' . '/../lib', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'OCA\\Nominatim\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', + 'OCA\\Nominatim\\LocationProvider' => __DIR__ . '/..' . '/../lib/LocationProvider.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitNominatim::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitNominatim::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInitNominatim::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/apps/nominatim/composer/composer/installed.json b/apps/nominatim/composer/composer/installed.json new file mode 100644 index 0000000000000..f20a6c47c6d4f --- /dev/null +++ b/apps/nominatim/composer/composer/installed.json @@ -0,0 +1,5 @@ +{ + "packages": [], + "dev": false, + "dev-package-names": [] +} diff --git a/apps/nominatim/composer/composer/installed.php b/apps/nominatim/composer/composer/installed.php new file mode 100644 index 0000000000000..ddac92049b9c5 --- /dev/null +++ b/apps/nominatim/composer/composer/installed.php @@ -0,0 +1,23 @@ + array( + 'name' => '__root__', + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => '5a456ef35c4f8d0d0dfe84108df4e751c5901153', + 'type' => 'library', + 'install_path' => __DIR__ . '/../', + 'aliases' => array(), + 'dev' => false, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => '5a456ef35c4f8d0d0dfe84108df4e751c5901153', + 'type' => 'library', + 'install_path' => __DIR__ . '/../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/apps/nominatim/l10n/.gitkeep b/apps/nominatim/l10n/.gitkeep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/apps/nominatim/lib/AppInfo/Application.php b/apps/nominatim/lib/AppInfo/Application.php new file mode 100644 index 0000000000000..3e7dfa73b1ac8 --- /dev/null +++ b/apps/nominatim/lib/AppInfo/Application.php @@ -0,0 +1,50 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Nominatim\AppInfo; + +use OCA\Nominatim\LocationProvider; +use OCP\AppFramework\App; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; + +class Application extends App implements IBootstrap { + public const APP_ID = 'nominatim'; + + public function __construct(array $urlParams = []) { + parent::__construct(self::APP_ID, $urlParams); + } + + /** + * @inheritDoc + */ + public function register(IRegistrationContext $context): void { + $context->registerLocationProvider(LocationProvider::class); + } + + public function boot(IBootContext $context): void { + } +} diff --git a/apps/nominatim/lib/LocationProvider.php b/apps/nominatim/lib/LocationProvider.php new file mode 100644 index 0000000000000..62117920f4f06 --- /dev/null +++ b/apps/nominatim/lib/LocationProvider.php @@ -0,0 +1,181 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Nominatim; + +use Exception; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\Location\ILocationAddress; +use OCP\Location\ILocationProvider; +use OCP\Location\LocationAddress; +use OCP\Util; +use Psr\Log\LoggerInterface; + +class LocationProvider implements ILocationProvider { + + private IClient $client; + private string $version; + private ICache $cache; + + public function __construct(private LoggerInterface $logger, IClientService $clientService, ICacheFactory $cacheFactory) { + $this->client = $clientService->newClient(); + $this->version = implode('.', Util::getVersion()); + $this->cache = $cacheFactory->createDistributed('nominatim'); + } + + /** + * @throws Exception + */ + public function geocode(float $longitude, float $latitude, array $options = []): array { + + $params = [ + 'lat' => number_format($latitude, 5), + 'lon' => number_format($longitude, 5), + 'addressdetails' => 1, + 'format' => 'json', + ]; + $url = 'https://nominatim.openstreetmap.org/reverse'; + $result = $this->requestJSON($url, $params); + return [$this->formatOsmAddress($result)]; + } + + /** + * @throws Exception + */ + public function search(string $address, array $options = []): array { + $params = [ + 'q' => $address, + 'format' => 'json', + 'addressdetails' => '1', + 'extratags' => '1', + 'namedetails' => '1', + 'limit' => '1', + ]; + $url = 'https://nominatim.openstreetmap.org/search'; + $results = $this->requestJSON($url, $params); + return array_map(fn ($result) => $this->formatOsmAddress($result), $results); + } + + private function formatOsmAddress(array $json): ILocationAddress { + $address = new LocationAddress(); + if (isset($json['address'])) { + $jsonAddr = $json['address']; + $address->setStreetName($jsonAddr['road']); + $cityAddress = ''; + // priority : city, town, village, municipality + if (isset($jsonAddr['city'])) { + $cityAddress = $jsonAddr['city']; + } elseif (isset($jsonAddr['town'])) { + $cityAddress = $jsonAddr['town']; + } elseif (isset($jsonAddr['village'])) { + $cityAddress = $jsonAddr['village']; + } elseif (isset($jsonAddr['municipality'])) { + $cityAddress = $jsonAddr['municipality']; + } + $address->setLocality($cityAddress); + // post code + if (isset($jsonAddr['postcode'])) { + $address->setPostalCode($jsonAddr['postcode']); + } + if (isset($jsonAddr['state'])) { + $address->setRegion($jsonAddr['state']); + } elseif (isset($jsonAddr['region'])) { + $address->setRegion($jsonAddr['region']); + } + // country + if (isset($jsonAddr['country'])) { + $address->setCountry($jsonAddr['country']); + } + } + if (isset($json['osm_id'])) { + $address->setOriginId((string)$json['osm_id']); + } + if (isset($json['type'])) { + $address->setType($json['type']); + } + if (isset($json['display_name'])) { + $address->setDescription($json['display_name']); + } + return $address; + } + + /** + * Make a HTTP GET request and parse JSON result. + * Request results are cached until the 'Expires' response header says so + * + * @param string $url Base URL to query + * @param array $params GET parameters + * @return array which contains the error message or the parsed JSON result + * @throws Exception + */ + private function requestJSON(string $url, array $params = []): array { + $cacheKey = $url . '|' . implode(',', $params) . '|' . implode(',', array_keys($params)); + $cacheValue = $this->cache->get($cacheKey); + if ($cacheValue !== null) { + return $cacheValue; + } + + $options = [ + 'headers' => [ + 'User-Agent' => 'Nextcloud/' . $this->version . ' nextcloud.com' + ], + ]; + + $reqUrl = $url; + if (count($params) > 0) { + $paramsContent = http_build_query($params); + $reqUrl = $url . '?' . $paramsContent; + } + $this->logger->debug('Requesting Nominatim with URL ' . $reqUrl); + $response = $this->client->get($reqUrl, $options); + $body = $response->getBody(); + $headers = $response->getHeaders(); + $respCode = $response->getStatusCode(); + + if ($respCode >= 400) { + throw new \RuntimeException(); + } else { + $json = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + + // default cache duration is one hour + $cacheDuration = 60 * 60; + if (isset($headers['Expires']) && count($headers['Expires']) > 0) { + // if the Expires response header is set, use it to define cache duration + $expireTs = (new \DateTime($headers['Expires'][0]))->getTimestamp(); + $nowTs = (new \DateTime())->getTimestamp(); + $duration = $expireTs - $nowTs; + if ($duration > $cacheDuration) { + $cacheDuration = $duration; + } + } + $this->cache->set($cacheKey, $json, $cacheDuration); + + return $json; + } + } +} diff --git a/core/Controller/LocationApiController.php b/core/Controller/LocationApiController.php new file mode 100644 index 0000000000000..8dd415d682e0b --- /dev/null +++ b/core/Controller/LocationApiController.php @@ -0,0 +1,143 @@ + + * + * @author Julius Härtl + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +namespace OC\Core\Controller; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\IL10N; +use OCP\IRequest; +use OCP\Location\CouldNotSearchLocationException; +use OCP\Location\ILocationAddress; +use OCP\Location\ILocationManager; +use OCP\PreConditionNotMetException; + +class LocationApiController extends \OCP\AppFramework\OCSController { + public function __construct( + string $appName, + IRequest $request, + private ILocationManager $locationManager, + private IL10N $l10n, + ) { + parent::__construct($appName, $request); + } + + /** + * @PublicPage + * + * Get the location providers configuration + * + * @return DataResponse + * + * 200: Supported languages returned + */ + public function config(): DataResponse { + return new DataResponse([ + 'autocomplete' => $this->locationManager->canAutocomplete(), + ]); + } + + /** + * @PublicPage + * @UserRateThrottle(limit=25, period=120) + * @AnonRateThrottle(limit=10, period=120) + * + * Search for an address + * + * @param string $address Address to be searched for + * @param array $options Options for searching + * @return DataResponse|DataResponse + * + * 200: Address found + * 400: Failed to search for location + * 412: No location provider available + */ + public function search(string $address, array $options = []): DataResponse { + try { + $addresses = $this->locationManager->search($address, $options); + return new DataResponse($addresses); + } catch (PreConditionNotMetException) { + return new DataResponse(['message' => $this->l10n->t('No location provider available')], Http::STATUS_PRECONDITION_FAILED); + } catch (CouldNotSearchLocationException $e) { + return new DataResponse(['message' => $this->l10n->t('Unable to search for location')], Http::STATUS_BAD_REQUEST); + } + } + + /** + * @PublicPage + * @UserRateThrottle(limit=25, period=120) + * @AnonRateThrottle(limit=10, period=120) + * + * Geocode an address + * + * @param float $longitude + * @param float $latitude + * @param array $options Options for searching + * @return DataResponse|DataResponse + * + * 200: Address found + * 400: Failed to geocode location + * 412: No location provider available + */ + public function geocode(float $longitude, float $latitude, array $options = []): DataResponse { + try { + $addresses = $this->locationManager->geocode($longitude, $latitude, $options); + + return new DataResponse($addresses); + } catch (PreConditionNotMetException) { + return new DataResponse(['message' => $this->l10n->t('No location provider available')], Http::STATUS_PRECONDITION_FAILED); + } catch (CouldNotSearchLocationException $e) { + return new DataResponse(['message' => $this->l10n->t('Unable to geocode location')], Http::STATUS_BAD_REQUEST); + } + } + + /** + * @PublicPage + * @UserRateThrottle(limit=25, period=120) + * @AnonRateThrottle(limit=10, period=120) + * + * Autocomplete an address + * + * @param string $address Address to be autocompleted + * @param array $options Options for searching + * @return DataResponse|DataResponse + * + * 200: Address found + * 400: Failed to autocomplete location + * 412: No location provider available + */ + public function autocomplete(string $address, array $options = []): DataResponse { + try { + $addresses = $this->locationManager->autocomplete($address, $options); + return new DataResponse($addresses); + } catch (PreConditionNotMetException) { + return new DataResponse(['message' => $this->l10n->t('No location autocomplete provider available')], Http::STATUS_PRECONDITION_FAILED); + } catch (CouldNotSearchLocationException $e) { + return new DataResponse(['message' => $this->l10n->t('Unable to autocomplete location')], Http::STATUS_BAD_REQUEST); + } + } +} diff --git a/core/routes.php b/core/routes.php index fe1fe6fcd7500..d87099d0093d3 100644 --- a/core/routes.php +++ b/core/routes.php @@ -162,6 +162,11 @@ ['root' => '/text2image', 'name' => 'TextToImageApi#getImage', 'url' => '/task/{id}/image/{index}', 'verb' => 'GET'], ['root' => '/text2image', 'name' => 'TextToImageApi#deleteTask', 'url' => '/task/{id}', 'verb' => 'DELETE'], ['root' => '/text2image', 'name' => 'TextToImageApi#listTasksByApp', 'url' => '/tasks/app/{appId}', 'verb' => 'GET'], + + ['root' => '/location', 'name' => 'LocationApi#config', 'url' => '/config', 'verb' => 'GET'], + ['root' => '/location', 'name' => 'LocationApi#search', 'url' => '/search', 'verb' => 'POST'], + ['root' => '/location', 'name' => 'LocationApi#geocode', 'url' => '/geocode', 'verb' => 'POST'], + ['root' => '/location', 'name' => 'LocationApi#autocomplete', 'url' => '/autocomplete', 'verb' => 'POST'], ], ]); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index b2d0b2255749b..0e7f8f3ad2abb 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -513,6 +513,13 @@ 'OCP\\LDAP\\IDeletionFlagSupport' => $baseDir . '/lib/public/LDAP/IDeletionFlagSupport.php', 'OCP\\LDAP\\ILDAPProvider' => $baseDir . '/lib/public/LDAP/ILDAPProvider.php', 'OCP\\LDAP\\ILDAPProviderFactory' => $baseDir . '/lib/public/LDAP/ILDAPProviderFactory.php', + 'OCP\\Location\\CouldNotGeocodeException' => $baseDir . '/lib/public/Location/CouldNotGeocodeException.php', + 'OCP\\Location\\CouldNotSearchLocationException' => $baseDir . '/lib/public/Location/CouldNotSearchLocationException.php', + 'OCP\\Location\\ILocationAddress' => $baseDir . '/lib/public/Location/ILocationAddress.php', + 'OCP\\Location\\ILocationAutocompleteProvider' => $baseDir . '/lib/public/Location/ILocationAutocompleteProvider.php', + 'OCP\\Location\\ILocationManager' => $baseDir . '/lib/public/Location/ILocationManager.php', + 'OCP\\Location\\ILocationProvider' => $baseDir . '/lib/public/Location/ILocationProvider.php', + 'OCP\\Location\\LocationAddress' => $baseDir . '/lib/public/Location/LocationAddress.php', 'OCP\\Lock\\ILockingProvider' => $baseDir . '/lib/public/Lock/ILockingProvider.php', 'OCP\\Lock\\LockedException' => $baseDir . '/lib/public/Lock/LockedException.php', 'OCP\\Lock\\ManuallyLockedException' => $baseDir . '/lib/public/Lock/ManuallyLockedException.php', @@ -1098,6 +1105,7 @@ 'OC\\Core\\Controller\\GuestAvatarController' => $baseDir . '/core/Controller/GuestAvatarController.php', 'OC\\Core\\Controller\\HoverCardController' => $baseDir . '/core/Controller/HoverCardController.php', 'OC\\Core\\Controller\\JsController' => $baseDir . '/core/Controller/JsController.php', + 'OC\\Core\\Controller\\LocationApiController' => $baseDir . '/core/Controller/LocationApiController.php', 'OC\\Core\\Controller\\LoginController' => $baseDir . '/core/Controller/LoginController.php', 'OC\\Core\\Controller\\LostController' => $baseDir . '/core/Controller/LostController.php', 'OC\\Core\\Controller\\NavigationController' => $baseDir . '/core/Controller/NavigationController.php', @@ -1437,6 +1445,7 @@ 'OC\\L10N\\LazyL10N' => $baseDir . '/lib/private/L10N/LazyL10N.php', 'OC\\LDAP\\NullLDAPProviderFactory' => $baseDir . '/lib/private/LDAP/NullLDAPProviderFactory.php', 'OC\\LargeFileHelper' => $baseDir . '/lib/private/LargeFileHelper.php', + 'OC\\Location\\LocationManager' => $baseDir . '/lib/private/Location/LocationManager.php', 'OC\\Lock\\AbstractLockingProvider' => $baseDir . '/lib/private/Lock/AbstractLockingProvider.php', 'OC\\Lock\\DBLockingProvider' => $baseDir . '/lib/private/Lock/DBLockingProvider.php', 'OC\\Lock\\MemcacheLockingProvider' => $baseDir . '/lib/private/Lock/MemcacheLockingProvider.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 7e73255b29b2e..3d5be19e42e00 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -546,6 +546,13 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\LDAP\\IDeletionFlagSupport' => __DIR__ . '/../../..' . '/lib/public/LDAP/IDeletionFlagSupport.php', 'OCP\\LDAP\\ILDAPProvider' => __DIR__ . '/../../..' . '/lib/public/LDAP/ILDAPProvider.php', 'OCP\\LDAP\\ILDAPProviderFactory' => __DIR__ . '/../../..' . '/lib/public/LDAP/ILDAPProviderFactory.php', + 'OCP\\Location\\CouldNotGeocodeException' => __DIR__ . '/../../..' . '/lib/public/Location/CouldNotGeocodeException.php', + 'OCP\\Location\\CouldNotSearchLocationException' => __DIR__ . '/../../..' . '/lib/public/Location/CouldNotSearchLocationException.php', + 'OCP\\Location\\ILocationAddress' => __DIR__ . '/../../..' . '/lib/public/Location/ILocationAddress.php', + 'OCP\\Location\\ILocationAutocompleteProvider' => __DIR__ . '/../../..' . '/lib/public/Location/ILocationAutocompleteProvider.php', + 'OCP\\Location\\ILocationManager' => __DIR__ . '/../../..' . '/lib/public/Location/ILocationManager.php', + 'OCP\\Location\\ILocationProvider' => __DIR__ . '/../../..' . '/lib/public/Location/ILocationProvider.php', + 'OCP\\Location\\LocationAddress' => __DIR__ . '/../../..' . '/lib/public/Location/LocationAddress.php', 'OCP\\Lock\\ILockingProvider' => __DIR__ . '/../../..' . '/lib/public/Lock/ILockingProvider.php', 'OCP\\Lock\\LockedException' => __DIR__ . '/../../..' . '/lib/public/Lock/LockedException.php', 'OCP\\Lock\\ManuallyLockedException' => __DIR__ . '/../../..' . '/lib/public/Lock/ManuallyLockedException.php', @@ -1131,6 +1138,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Controller\\GuestAvatarController' => __DIR__ . '/../../..' . '/core/Controller/GuestAvatarController.php', 'OC\\Core\\Controller\\HoverCardController' => __DIR__ . '/../../..' . '/core/Controller/HoverCardController.php', 'OC\\Core\\Controller\\JsController' => __DIR__ . '/../../..' . '/core/Controller/JsController.php', + 'OC\\Core\\Controller\\LocationApiController' => __DIR__ . '/../../..' . '/core/Controller/LocationApiController.php', 'OC\\Core\\Controller\\LoginController' => __DIR__ . '/../../..' . '/core/Controller/LoginController.php', 'OC\\Core\\Controller\\LostController' => __DIR__ . '/../../..' . '/core/Controller/LostController.php', 'OC\\Core\\Controller\\NavigationController' => __DIR__ . '/../../..' . '/core/Controller/NavigationController.php', @@ -1470,6 +1478,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\L10N\\LazyL10N' => __DIR__ . '/../../..' . '/lib/private/L10N/LazyL10N.php', 'OC\\LDAP\\NullLDAPProviderFactory' => __DIR__ . '/../../..' . '/lib/private/LDAP/NullLDAPProviderFactory.php', 'OC\\LargeFileHelper' => __DIR__ . '/../../..' . '/lib/private/LargeFileHelper.php', + 'OC\\Location\\LocationManager' => __DIR__ . '/../../..' . '/lib/private/Location/LocationManager.php', 'OC\\Lock\\AbstractLockingProvider' => __DIR__ . '/../../..' . '/lib/private/Lock/AbstractLockingProvider.php', 'OC\\Lock\\DBLockingProvider' => __DIR__ . '/../../..' . '/lib/private/Lock/DBLockingProvider.php', 'OC\\Lock\\MemcacheLockingProvider' => __DIR__ . '/../../..' . '/lib/private/Lock/MemcacheLockingProvider.php', diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index 5ff2dcd796943..27dad3777dc07 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -33,6 +33,7 @@ use OCP\Calendar\Resource\IBackend as IResourceBackend; use OCP\Calendar\Room\IBackend as IRoomBackend; use OCP\Collaboration\Reference\IReferenceProvider; +use OCP\Location\ILocationProvider; use OCP\TextProcessing\IProvider as ITextProcessingProvider; use OCP\SpeechToText\ISpeechToTextProvider; use OCP\Talk\ITalkBackend; @@ -126,6 +127,9 @@ class RegistrationContext { /** @var ServiceRegistration[] */ private $translationProviders = []; + /** @var ServiceRegistration[] */ + private array $locationProviders = []; + /** @var ServiceRegistration[] */ private $notifierServices = []; @@ -300,6 +304,13 @@ public function registerTranslationProvider(string $providerClass): void { ); } + public function registerLocationProvider(string $providerClass): void { + $this->context->registerLocationProvider( + $this->appId, + $providerClass + ); + } + public function registerNotifierService(string $notifierClass): void { $this->context->registerNotifierService( $this->appId, @@ -475,6 +486,10 @@ public function registerTranslationProvider(string $appId, string $class): void $this->translationProviders[] = new ServiceRegistration($appId, $class); } + public function registerLocationProvider(string $appId, string $class): void { + $this->locationProviders[] = new ServiceRegistration($appId, $class); + } + public function registerNotifierService(string $appId, string $class): void { $this->notifierServices[] = new ServiceRegistration($appId, $class); } @@ -777,6 +792,10 @@ public function getTranslationProviders(): array { return $this->translationProviders; } + public function getLocationProviders(): array { + return $this->locationProviders; + } + /** * @return ServiceRegistration[] */ diff --git a/lib/private/Location/LocationManager.php b/lib/private/Location/LocationManager.php new file mode 100644 index 0000000000000..8262fbddfa79b --- /dev/null +++ b/lib/private/Location/LocationManager.php @@ -0,0 +1,148 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +namespace OC\Location; + +use OC\AppFramework\Bootstrap\Coordinator; +use OCP\Location\CouldNotGeocodeException; +use OCP\Location\CouldNotSearchLocationException; +use OCP\Location\ILocationAutocompleteProvider; +use OCP\Location\ILocationManager; +use OCP\Location\ILocationProvider; +use OCP\PreConditionNotMetException; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; +use Psr\Log\LoggerInterface; +use RuntimeException; +use Throwable; + +class LocationManager implements ILocationManager { + /** @var ?ILocationProvider[] */ + private ?array $providers = null; + + public function __construct( + private ContainerInterface $serverContainer, + private Coordinator $coordinator, + private LoggerInterface $logger, + ) { + } + + /** + * @throws PreConditionNotMetException + */ + public function geocode(float $longitude, float $latitude, array $options = []): array { + if (!$this->hasProviders()) { + throw new PreConditionNotMetException('No location providers available'); + } + + foreach ($this->getProviders() as $provider) { + try { + return $provider->geocode($longitude, $latitude, $options); + } catch (RuntimeException $e) { + $this->logger->warning("Failed to geocode coords {$longitude}:${latitude} using provider {$provider->getName()}", ['exception' => $e]); + } + } + + throw new CouldNotGeocodeException($longitude, $latitude); + } + + /** + * @throws PreConditionNotMetException + */ + public function search(string $address, array $options = []): array { + if (!$this->hasProviders()) { + throw new PreConditionNotMetException('No location providers available'); + } + + foreach ($this->getProviders() as $provider) { + try { + return $provider->search($address, $options); + } catch (RuntimeException $e) { + $this->logger->warning("Failed to search for location {$address} using provider {$provider->getName()}", ['exception' => $e]); + } + } + + throw new CouldNotSearchLocationException($address); + } + + /** + * @throws PreConditionNotMetException + */ + public function autocomplete(string $address, array $options = []): array { + if (!$this->canAutocomplete()) { + throw new PreConditionNotMetException('No location autocomplete providers available'); + } + + foreach ($this->getProviders() as $provider) { + try { + if ($provider instanceof ILocationAutocompleteProvider) { + return $provider->autocomplete($address, $options); + } + } catch (RuntimeException $e) { + $this->logger->warning("Failed to autocomplete location {$address} using provider {$provider->getName()}", ['exception' => $e]); + } + } + + throw new CouldNotSearchLocationException($address); + } + + public function getProviders(): array { + $context = $this->coordinator->getRegistrationContext(); + + if ($this->providers !== null) { + return $this->providers; + } + + $this->providers = []; + foreach ($context->getLocationProviders() as $providerRegistration) { + $class = $providerRegistration->getService(); + try { + $this->providers[$class] = $this->serverContainer->get($class); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface|Throwable $e) { + $this->logger->error('Failed to load location provider ' . $class, [ + 'exception' => $e + ]); + } + } + + return $this->providers; + } + + public function hasProviders(): bool { + $context = $this->coordinator->getRegistrationContext(); + return !empty($context->getLocationProviders()); + } + + public function canAutocomplete(): bool { + foreach ($this->getProviders() as $provider) { + if ($provider instanceof ILocationAutocompleteProvider) { + return true; + } + } + return false; + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index 77791c49e8b0d..b3aa7d1b56680 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -111,6 +111,7 @@ use OC\IntegrityCheck\Helpers\FileAccessHelper; use OC\LDAP\NullLDAPProviderFactory; use OC\KnownUser\KnownUserService; +use OC\Location\LocationManager; use OC\Lock\DBLockingProvider; use OC\Lock\MemcacheLockingProvider; use OC\Lock\NoopLockingProvider; @@ -230,6 +231,7 @@ use OCP\L10N\IFactory; use OCP\LDAP\ILDAPProvider; use OCP\LDAP\ILDAPProviderFactory; +use OCP\Location\ILocationManager; use OCP\Lock\ILockingProvider; use OCP\Lockdown\ILockdownManager; use OCP\Log\ILogFactory; @@ -1440,6 +1442,8 @@ public function __construct($webRoot, \OC\Config $config) { $this->registerAlias(IProfileManager::class, ProfileManager::class); + $this->registerAlias(ILocationManager::class, LocationManager::class); + $this->connectDispatcher(); } diff --git a/lib/public/Location/CouldNotGeocodeException.php b/lib/public/Location/CouldNotGeocodeException.php new file mode 100644 index 0000000000000..acdaa14adaba2 --- /dev/null +++ b/lib/public/Location/CouldNotGeocodeException.php @@ -0,0 +1,55 @@ + + * + * @author Joas Schilling + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCP\Location; + +/** + * @since 28.0.0 + */ +class CouldNotGeocodeException extends \RuntimeException { + /** + * @since 28.0.0 + */ + public function __construct( + protected ?float $longitude, + protected ?float $latitude, + ) { + parent::__construct(); + } + + /** + * @since 28.0.0 + */ + public function getLongitude(): ?float { + return $this->longitude; + } + + /** + * @since 28.0.0 + */ + public function getLatitude(): ?float { + return $this->latitude; + } +} diff --git a/lib/public/Location/CouldNotSearchLocationException.php b/lib/public/Location/CouldNotSearchLocationException.php new file mode 100644 index 0000000000000..e34600669e22d --- /dev/null +++ b/lib/public/Location/CouldNotSearchLocationException.php @@ -0,0 +1,45 @@ + + * + * @author Joas Schilling + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCP\Location; + +/** + * @since 28.0.0 + */ +class CouldNotSearchLocationException extends \RuntimeException { + /** + * @since 28.0.0 + */ + public function __construct(protected ?string $location) { + parent::__construct(); + } + + /** + * @since 28.0.0 + */ + public function getLocation(): ?string { + return $this->location; + } +} diff --git a/lib/public/Location/ILocationAddress.php b/lib/public/Location/ILocationAddress.php new file mode 100644 index 0000000000000..f488531c69380 --- /dev/null +++ b/lib/public/Location/ILocationAddress.php @@ -0,0 +1,43 @@ +. + * + * @author Thomas Citharel + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCP\Location; + +/** + * This interface represents a location address + * + * @since 28.0.0 + */ +interface ILocationAddress { + public function getCountry(): ?string; + public function getLocality(): ?string; + public function getRegion(): ?string; + public function getDescription(): ?string; + public function getGeometry(): ?string; + public function getPostalCode(): ?string; + public function getStreetName(): ?string; + public function getType(): ?string; + public function getOriginId(): ?string; + public function getTimezone(): ?string; +} diff --git a/lib/public/Location/ILocationAutocompleteProvider.php b/lib/public/Location/ILocationAutocompleteProvider.php new file mode 100644 index 0000000000000..a02ab9dbf9653 --- /dev/null +++ b/lib/public/Location/ILocationAutocompleteProvider.php @@ -0,0 +1,49 @@ + + * + * @author Thomas Citharel + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCP\Location; + +/** + * This interface must be used when creating a Location Autocomplete Provider + * + * @since 28.0.0 + */ +interface ILocationAutocompleteProvider extends ILocationProvider { + /** + * Autocomplete an address + * + * ## Options + * + * In addition to [the shared options](#module-shared-options), the function also accepts the following options: + * + * `"coords"` Map of coordinates (ex: `["lon" => 48.11, "lat" => -1.77]`) allowing to + * give a geographic priority to the search. Defaults to `null`. + * `"type"` Filter by type of results. Allowed values: + * `"administrative"` (cities, regions) + * + * @return ILocationAddress[] + * @since 28.0.0 + */ + public function autocomplete(string $address, array $options = []): array; +} diff --git a/lib/public/Location/ILocationManager.php b/lib/public/Location/ILocationManager.php new file mode 100644 index 0000000000000..38158febc505f --- /dev/null +++ b/lib/public/Location/ILocationManager.php @@ -0,0 +1,106 @@ + + * + * @author Thomas Citharel + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +namespace OCP\Location; + +use OCP\PreConditionNotMetException; + +/** + * @since 28.0.0 + */ +interface ILocationManager { + /** + * @since 28.0.0 + */ + public function hasProviders(): bool; + + /** + * @return ILocationProvider[] + * @since 28.0.0 + */ + public function getProviders(): array; + + /** + * Gets an address from longitude and latitude coordinates. + * + * ## Options + * + * In addition to [the shared options](#module-shared-options), the function also accepts the following options: + * + * - `zoom` Level of detail required for the address. Default: 15 + * + * @return ILocationAddress[] + * @throws CouldNotGeocodeException + * @throws PreConditionNotMetException + * @since 28.0.0 + */ + public function geocode(float $longitude, float $latitude, array $options = []): array; + + /** + * Search for an address + * + * ## Options + * + * In addition to [the shared options](#module-shared-options), the function also accepts the following options: + * + * `"coords"` Map of coordinates (ex: `["lon" => 48.11, "lat" => -1.77]`) allowing to + * give a geographic priority to the search. Defaults to `null`. + * `"type"` Filter by type of results. Allowed values: + * `"administrative"` (cities, regions) + * + * @return ILocationAddress[] + * @throws CouldNotSearchLocationException + * @throws PreConditionNotMetException + * @since 28.0.0 + */ + public function search(string $address, array $options = []): array; + + /** + * Autocomplete an address + * + * ## Options + * + * In addition to [the shared options](#module-shared-options), the function also accepts the following options: + * + * `"coords"` Map of coordinates (ex: `["lon" => 48.11, "lat" => -1.77]`) allowing to + * give a geographic priority to the search. Defaults to `null`. + * `"type"` Filter by type of results. Allowed values: + * `"administrative"` (cities, regions) + * + * @return ILocationAddress[] + * @throws CouldNotSearchLocationException + * @throws PreConditionNotMetException + * @since 28.0.0 + */ + public function autocomplete(string $address, array $options = []): array; + + + /** + * Returns whether a location provider capable of auto-complete has been registered + * @since 28.0.0 + */ + public function canAutocomplete(): bool; +} diff --git a/lib/public/Location/ILocationProvider.php b/lib/public/Location/ILocationProvider.php new file mode 100644 index 0000000000000..7b6225cabe25b --- /dev/null +++ b/lib/public/Location/ILocationProvider.php @@ -0,0 +1,74 @@ + + * + * @author Thomas Citharel + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCP\Location; + +/** + * This interface must be used when creating a Location Provider + * + * ## Shared options + * + * `"lang"` Lang in which to prefer results. Used as a request parameter or + * through an `Accept-Language` HTTP header. Defaults to `"en"`. + * `"country_code"` An ISO 3166 country code. String or `nil` + * `"limit"` Maximum limit for the number of results returned by the backend. + * Defaults to `10` + * `"api_key"` Allows to override the API key (if the backend requires one) set + * inside the configuration. + * `"endpoint"` Allows to override the endpoint set inside the configuration. + * + * @since 28.0.0 + */ +interface ILocationProvider { + /** + * Gets an address from longitude and latitude coordinates. + * + * ## Options + * + * In addition to [the shared options](#module-shared-options), the function also accepts the following options: + * + * - `zoom` Level of detail required for the address. Default: 15 + * + * @return ILocationAddress[] + * @since 28.0.0 + */ + public function geocode(float $longitude, float $latitude, array $options = []): array; + + /** + * Search for an address + * + * ## Options + * + * In addition to [the shared options](#module-shared-options), the function also accepts the following options: + * + * `"coords"` Map of coordinates (ex: `["lon" => 48.11, "lat" => -1.77]`) allowing to + * give a geographic priority to the search. Defaults to `null`. + * `"type"` Filter by type of results. Allowed values: + * `"administrative"` (cities, regions) + * + * @return ILocationAddress[] + * @since 28.0.0 + */ + public function search(string $address, array $options = []): array; +} diff --git a/lib/public/Location/LocationAddress.php b/lib/public/Location/LocationAddress.php new file mode 100644 index 0000000000000..4db9776ffd577 --- /dev/null +++ b/lib/public/Location/LocationAddress.php @@ -0,0 +1,146 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +namespace OCP\Location; + +class LocationAddress implements ILocationAddress, \JsonSerializable { + + private ?string $streetName = null; + private ?string $locality = null; + private ?string $postalCode = null; + private ?string $region = null; + private ?string $country = null; + private ?string $description = null; + private ?string $type = null; + private ?string $geometry = null; + private ?string $originId = null; + private ?string $timezone = null; + + public function getStreetName(): ?string { + return $this->streetName; + } + + public function setStreetName(?string $streetName): LocationAddress { + $this->streetName = $streetName; + return $this; + } + + public function getLocality(): ?string { + return $this->locality; + } + + public function setLocality(?string $locality): LocationAddress { + $this->locality = $locality; + return $this; + } + + public function getPostalCode(): ?string { + return $this->postalCode; + } + + public function setPostalCode(?string $postalCode): LocationAddress { + $this->postalCode = $postalCode; + return $this; + } + + public function getRegion(): ?string { + return $this->region; + } + + public function setRegion(?string $region): LocationAddress { + $this->region = $region; + return $this; + } + + public function getCountry(): ?string { + return $this->country; + } + + public function setCountry(?string $country): LocationAddress { + $this->country = $country; + return $this; + } + + public function getDescription(): ?string { + return $this->description; + } + + public function setDescription(?string $description): LocationAddress { + $this->description = $description; + return $this; + } + + public function getType(): ?string { + return $this->type; + } + + public function setType(?string $type): LocationAddress { + $this->type = $type; + return $this; + } + + public function getGeometry(): ?string { + return $this->geometry; + } + + public function setGeometry(?string $geometry): LocationAddress { + $this->geometry = $geometry; + return $this; + } + + public function getOriginId(): ?string { + return $this->originId; + } + + public function setOriginId(?string $originId): LocationAddress { + $this->originId = $originId; + return $this; + } + + public function getTimezone(): ?string { + return $this->timezone; + } + + public function setTimezone(?string $timezone): LocationAddress { + $this->timezone = $timezone; + return $this; + } + + function jsonSerialize(): array { + return [ + 'streetName' => $this->streetName, + 'locality' => $this->locality, + 'postalCode' => $this->postalCode, + 'region' => $this->region, + 'country' => $this->country, + 'description' => $this->description, + 'type' => $this->type, + 'geometry' => $this->geometry, + 'originId' => $this->originId, + 'timezone' => $this->timezone, + ]; + } +}