!/apps/lookup_server_connector
!/apps/user_ldap
!/apps/oauth2
+!/apps/profile
!/apps/provisioning_api
!/apps/settings
!/apps/systemtags
source_lang = en
type = PO
+[o:nextcloud:p:nextcloud:r:profile]
+file_filter = translationfiles/<lang>/profile.po
+source_file = translationfiles/templates/profile.pot
+source_lang = en
+type = PO
+
[o:nextcloud:p:nextcloud:r:provisioning_api]
file_filter = translationfiles/<lang>/provisioning_api.po
source_file = translationfiles/templates/provisioning_api.pot
new Text(
$vCard,
'X-SOCIALPROFILE',
- $this->urlGenerator->linkToRouteAbsolute('core.ProfilePage.index', ['targetUserId' => $user->getUID()]),
+ $this->urlGenerator->linkToRouteAbsolute('profile.ProfilePage.index', ['targetUserId' => $user->getUID()]),
[
'TYPE' => 'NEXTCLOUD',
'X-NC-SCOPE' => IAccountManager::SCOPE_PUBLISHED
--- /dev/null
+<?xml version="1.0"?>
+<!--
+ - SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
+<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
+ <id>profile</id>
+ <name>Profile</name>
+ <summary>This application provides the profile</summary>
+ <description>Provides a customisable user profile interface.</description>
+ <version>1.0.0</version>
+ <licence>agpl</licence>
+ <author>Chris Ng</author>
+ <namespace>Profile</namespace>
+ <category>social</category>
+ <bugs>https://github.com/nextcloud/server/issues</bugs>
+ <dependencies>
+ <nextcloud min-version="31" max-version="31"/>
+ </dependencies>
+</info>
--- /dev/null
+<?php
+
+// autoload.php @generated by Composer
+
+if (PHP_VERSION_ID < 50600) {
+ if (!headers_sent()) {
+ header('HTTP/1.1 500 Internal Server Error');
+ }
+ $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+ if (!ini_get('display_errors')) {
+ if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+ fwrite(STDERR, $err);
+ } elseif (!headers_sent()) {
+ echo $err;
+ }
+ }
+ trigger_error(
+ $err,
+ E_USER_ERROR
+ );
+}
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInitProfile::getLoader();
--- /dev/null
+{
+ "config" : {
+ "vendor-dir": ".",
+ "optimize-autoloader": true,
+ "classmap-authoritative": true,
+ "autoloader-suffix": "Profile"
+ },
+ "autoload" : {
+ "psr-4": {
+ "OCA\\Profile\\": "../lib/"
+ }
+ }
+}
--- /dev/null
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "d751713988987e9331980363e24189ce",
+ "packages": [],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "2.1.0"
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ * Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * 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 <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @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<string, array<string, int>>
+ */
+ private $prefixLengthsPsr4 = array();
+ /**
+ * @var array<string, list<string>>
+ */
+ private $prefixDirsPsr4 = array();
+ /**
+ * @var list<string>
+ */
+ 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<string, array<string, list<string>>>
+ */
+ private $prefixesPsr0 = array();
+ /**
+ * @var list<string>
+ */
+ private $fallbackDirsPsr0 = array();
+
+ /** @var bool */
+ private $useIncludePath = false;
+
+ /**
+ * @var array<string, string>
+ */
+ private $classMap = array();
+
+ /** @var bool */
+ private $classMapAuthoritative = false;
+
+ /**
+ * @var array<string, bool>
+ */
+ private $missingClasses = array();
+
+ /** @var string|null */
+ private $apcuPrefix;
+
+ /**
+ * @var array<string, self>
+ */
+ private static $registeredLoaders = array();
+
+ /**
+ * @param string|null $vendorDir
+ */
+ public function __construct($vendorDir = null)
+ {
+ $this->vendorDir = $vendorDir;
+ self::initializeIncludeClosure();
+ }
+
+ /**
+ * @return array<string, list<string>>
+ */
+ public function getPrefixes()
+ {
+ if (!empty($this->prefixesPsr0)) {
+ return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
+ }
+
+ return array();
+ }
+
+ /**
+ * @return array<string, list<string>>
+ */
+ public function getPrefixesPsr4()
+ {
+ return $this->prefixDirsPsr4;
+ }
+
+ /**
+ * @return list<string>
+ */
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirsPsr0;
+ }
+
+ /**
+ * @return list<string>
+ */
+ public function getFallbackDirsPsr4()
+ {
+ return $this->fallbackDirsPsr4;
+ }
+
+ /**
+ * @return array<string, string> Array of classname => path
+ */
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param array<string, string> $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>|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>|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>|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>|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<string, self>
+ */
+ 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);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ * Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * 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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
+ */
+ private static $installed;
+
+ /**
+ * @var bool|null
+ */
+ private static $canGetVendors;
+
+ /**
+ * @var array[]
+ * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+ */
+ 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<string>
+ */
+ 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<string>
+ */
+ 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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
+ */
+ 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<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+ */
+ 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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
+ */
+ public static function reload($data)
+ {
+ self::$installed = $data;
+ self::$installedByVendor = array();
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+ */
+ 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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+ $required = require __DIR__ . '/installed.php';
+ self::$installed = $required;
+ } else {
+ self::$installed = array();
+ }
+ }
+
+ if (self::$installed !== array()) {
+ $installed[] = self::$installed;
+ }
+
+ return $installed;
+ }
+}
--- /dev/null
+
+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.
+
--- /dev/null
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = $vendorDir;
+
+return array(
+ 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
+ 'OCA\\Profile\\Controller\\ProfilePageController' => $baseDir . '/../lib/Controller/ProfilePageController.php',
+);
--- /dev/null
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = $vendorDir;
+
+return array(
+);
--- /dev/null
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = $vendorDir;
+
+return array(
+ 'OCA\\Profile\\' => array($baseDir . '/../lib'),
+);
--- /dev/null
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInitProfile
+{
+ private static $loader;
+
+ public static function loadClassLoader($class)
+ {
+ if ('Composer\Autoload\ClassLoader' === $class) {
+ require __DIR__ . '/ClassLoader.php';
+ }
+ }
+
+ /**
+ * @return \Composer\Autoload\ClassLoader
+ */
+ public static function getLoader()
+ {
+ if (null !== self::$loader) {
+ return self::$loader;
+ }
+
+ spl_autoload_register(array('ComposerAutoloaderInitProfile', 'loadClassLoader'), true, true);
+ self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
+ spl_autoload_unregister(array('ComposerAutoloaderInitProfile', 'loadClassLoader'));
+
+ require __DIR__ . '/autoload_static.php';
+ call_user_func(\Composer\Autoload\ComposerStaticInitProfile::getInitializer($loader));
+
+ $loader->setClassMapAuthoritative(true);
+ $loader->register(true);
+
+ return $loader;
+ }
+}
--- /dev/null
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInitProfile
+{
+ public static $prefixLengthsPsr4 = array (
+ 'O' =>
+ array (
+ 'OCA\\Profile\\' => 12,
+ ),
+ );
+
+ public static $prefixDirsPsr4 = array (
+ 'OCA\\Profile\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/../lib',
+ ),
+ );
+
+ public static $classMap = array (
+ 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+ 'OCA\\Profile\\Controller\\ProfilePageController' => __DIR__ . '/..' . '/../lib/Controller/ProfilePageController.php',
+ );
+
+ public static function getInitializer(ClassLoader $loader)
+ {
+ return \Closure::bind(function () use ($loader) {
+ $loader->prefixLengthsPsr4 = ComposerStaticInitProfile::$prefixLengthsPsr4;
+ $loader->prefixDirsPsr4 = ComposerStaticInitProfile::$prefixDirsPsr4;
+ $loader->classMap = ComposerStaticInitProfile::$classMap;
+
+ }, null, ClassLoader::class);
+ }
+}
--- /dev/null
+{
+ "packages": [],
+ "dev": false,
+ "dev-package-names": []
+}
--- /dev/null
+<?php return array(
+ 'root' => array(
+ 'name' => '__root__',
+ 'pretty_version' => 'dev-master',
+ 'version' => 'dev-master',
+ 'reference' => 'a489d88a2b2203cffdd08f4a5f1ae03a497bc52f',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../',
+ 'aliases' => array(),
+ 'dev' => false,
+ ),
+ 'versions' => array(
+ '__root__' => array(
+ 'pretty_version' => 'dev-master',
+ 'version' => 'dev-master',
+ 'reference' => 'a489d88a2b2203cffdd08f4a5f1ae03a497bc52f',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ ),
+);
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Profile\Controller;
+
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\Attribute\AnonRateLimit;
+use OCP\AppFramework\Http\Attribute\BruteForceProtection;
+use OCP\AppFramework\Http\Attribute\FrontpageRoute;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\OpenAPI;
+use OCP\AppFramework\Http\Attribute\PublicPage;
+use OCP\AppFramework\Http\Attribute\UserRateLimit;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IInitialState;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\INavigationManager;
+use OCP\IRequest;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\Profile\BeforeTemplateRenderedEvent;
+use OCP\Profile\IProfileManager;
+use OCP\Share\IManager as IShareManager;
+use OCP\UserStatus\IManager as IUserStatusManager;
+
+#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
+class ProfilePageController extends Controller {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private IInitialState $initialStateService,
+ private IProfileManager $profileManager,
+ private IShareManager $shareManager,
+ private IUserManager $userManager,
+ private IUserSession $userSession,
+ private IUserStatusManager $userStatusManager,
+ private INavigationManager $navigationManager,
+ private IEventDispatcher $eventDispatcher,
+ ) {
+ parent::__construct($appName, $request);
+ }
+
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[FrontpageRoute(verb: 'GET', url: '/u/{targetUserId}', root: '')]
+ #[BruteForceProtection(action: 'user')]
+ #[UserRateLimit(limit: 30, period: 120)]
+ #[AnonRateLimit(limit: 30, period: 120)]
+ public function index(string $targetUserId): TemplateResponse {
+ $profileNotFoundTemplate = new TemplateResponse(
+ 'profile',
+ '404-profile',
+ [],
+ TemplateResponse::RENDER_AS_GUEST,
+ );
+
+ $targetUser = $this->userManager->get($targetUserId);
+ if ($targetUser === null) {
+ $profileNotFoundTemplate->throttle();
+ return $profileNotFoundTemplate;
+ }
+ if (!$targetUser->isEnabled()) {
+ return $profileNotFoundTemplate;
+ }
+ $visitingUser = $this->userSession->getUser();
+
+ if (!$this->profileManager->isProfileEnabled($targetUser)) {
+ return $profileNotFoundTemplate;
+ }
+
+ // Run user enumeration checks only if viewing another user's profile
+ if ($targetUser !== $visitingUser) {
+ if (!$this->shareManager->currentUserCanEnumerateTargetUser($visitingUser, $targetUser)) {
+ return $profileNotFoundTemplate;
+ }
+ }
+
+ if ($visitingUser !== null) {
+ $userStatuses = $this->userStatusManager->getUserStatuses([$targetUserId]);
+ $status = $userStatuses[$targetUserId] ?? null;
+ if ($status !== null) {
+ $this->initialStateService->provideInitialState('status', [
+ 'icon' => $status->getIcon(),
+ 'message' => $status->getMessage(),
+ ]);
+ }
+ }
+
+ $this->initialStateService->provideInitialState(
+ 'profileParameters',
+ $this->profileManager->getProfileFields($targetUser, $visitingUser),
+ );
+
+ if ($targetUser === $visitingUser) {
+ $this->navigationManager->setActiveEntry('profile');
+ }
+
+ $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($targetUserId));
+
+ \OCP\Util::addScript('profile', 'main');
+
+ return new TemplateResponse(
+ 'profile',
+ 'profile',
+ [],
+ $this->userSession->isLoggedIn() ? TemplateResponse::RENDER_AS_USER : TemplateResponse::RENDER_AS_PUBLIC,
+ );
+ }
+}
--- /dev/null
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getCSPNonce } from '@nextcloud/auth'
+import Vue from 'vue'
+
+import Profile from './views/Profile.vue'
+import ProfileSections from './services/ProfileSections.js'
+
+__webpack_nonce__ = getCSPNonce()
+
+if (!window.OCA) {
+ window.OCA = {}
+}
+
+if (!window.OCA.Core) {
+ window.OCA.Core = {}
+}
+Object.assign(window.OCA.Core, { ProfileSections: new ProfileSections() })
+
+const View = Vue.extend(Profile)
+
+window.addEventListener('DOMContentLoaded', () => {
+ new View().$mount('#content')
+})
--- /dev/null
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+export default class ProfileSections {
+
+ _sections
+
+ constructor() {
+ this._sections = []
+ }
+
+ /**
+ * @param {registerSectionCallback} section To be called to mount the section to the profile page
+ */
+ registerSection(section) {
+ this._sections.push(section)
+ }
+
+ getSections() {
+ return this._sections
+ }
+
+}
--- /dev/null
+<!--
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <NcContent app-name="profile">
+ <NcAppContent>
+ <div class="profile__header">
+ <div class="profile__header__container">
+ <div class="profile__header__container__placeholder" />
+ <div class="profile__header__container__displayname">
+ <h2>{{ displayname || userId }}</h2>
+ <span v-if="pronouns">·</span>
+ <span v-if="pronouns" class="profile__header__container__pronouns">{{ pronouns }}</span>
+ <NcButton v-if="isCurrentUser"
+ type="primary"
+ :href="settingsUrl">
+ <template #icon>
+ <PencilIcon :size="20" />
+ </template>
+ {{ t('profile', 'Edit Profile') }}
+ </NcButton>
+ </div>
+ <NcButton v-if="status.icon || status.message"
+ :disabled="!isCurrentUser"
+ :type="isCurrentUser ? 'tertiary' : 'tertiary-no-background'"
+ @click="openStatusModal">
+ {{ status.icon }} {{ status.message }}
+ </NcButton>
+ </div>
+ </div>
+
+ <div class="profile__wrapper">
+ <div class="profile__content">
+ <div class="profile__sidebar">
+ <NcAvatar class="avatar"
+ :class="{ interactive: isCurrentUser }"
+ :user="userId"
+ :size="180"
+ :show-user-status="true"
+ :show-user-status-compact="false"
+ :disable-menu="true"
+ :disable-tooltip="true"
+ :is-no-user="!isUserAvatarVisible"
+ @click.native.prevent.stop="openStatusModal" />
+
+ <div class="user-actions">
+ <!-- When a tel: URL is opened with target="_blank", a blank new tab is opened which is inconsistent with the handling of other URLs so we set target="_self" for the phone action -->
+ <NcButton v-if="primaryAction"
+ type="primary"
+ class="user-actions__primary"
+ :href="primaryAction.target"
+ :icon="primaryAction.icon"
+ :target="primaryAction.id === 'phone' ? '_self' :'_blank'">
+ <template #icon>
+ <!-- Fix for https://github.com/nextcloud-libraries/nextcloud-vue/issues/2315 -->
+ <img :src="primaryAction.icon" alt="" class="user-actions__primary__icon">
+ </template>
+ {{ primaryAction.title }}
+ </NcButton>
+ <NcActions class="user-actions__other" :inline="4">
+ <NcActionLink v-for="action in otherActions"
+ :key="action.id"
+ :close-after-click="true"
+ :href="action.target"
+ :target="action.id === 'phone' ? '_self' :'_blank'">
+ <template #icon>
+ <!-- Fix for https://github.com/nextcloud-libraries/nextcloud-vue/issues/2315 -->
+ <img :src="action.icon" alt="" class="user-actions__other__icon">
+ </template>
+ {{ action.title }}
+ </NcActionLink>
+ </NcActions>
+ </div>
+ </div>
+
+ <div class="profile__blocks">
+ <div v-if="organisation || role || address" class="profile__blocks-details">
+ <div v-if="organisation || role" class="detail">
+ <p>{{ organisation }} <span v-if="organisation && role">•</span> {{ role }}</p>
+ </div>
+ <div v-if="address" class="detail">
+ <p>
+ <MapMarkerIcon class="map-icon"
+ :size="16" />
+ {{ address }}
+ </p>
+ </div>
+ </div>
+ <template v-if="headline || biography || sections.length > 0">
+ <h3 v-if="headline" class="profile__blocks-headline">
+ {{ headline }}
+ </h3>
+ <p v-if="biography" class="profile__blocks-biography">
+ {{ biography }}
+ </p>
+
+ <!-- additional entries, use it with cautious -->
+ <div v-for="(section, index) in sections"
+ :ref="'section-' + index"
+ :key="index"
+ class="profile__additionalContent">
+ <component :is="section($refs['section-'+index], userId)" :user-id="userId" />
+ </div>
+ </template>
+ <NcEmptyContent v-else
+ class="profile__blocks-empty-info"
+ :name="emptyProfileMessage"
+ :description="t('profile', 'The headline and about sections will show up here')">
+ <template #icon>
+ <AccountIcon :size="60" />
+ </template>
+ </NcEmptyContent>
+ </div>
+ </div>
+ </div>
+ </NcAppContent>
+ </NcContent>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+import { generateUrl } from '@nextcloud/router'
+import { getCurrentUser } from '@nextcloud/auth'
+import { loadState } from '@nextcloud/initial-state'
+import { showError } from '@nextcloud/dialogs'
+import { subscribe, unsubscribe } from '@nextcloud/event-bus'
+import { translate as t } from '@nextcloud/l10n'
+
+import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
+import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
+import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
+import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
+import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import NcContent from '@nextcloud/vue/dist/Components/NcContent.js'
+import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
+import AccountIcon from 'vue-material-design-icons/Account.vue'
+import MapMarkerIcon from 'vue-material-design-icons/MapMarker.vue'
+import PencilIcon from 'vue-material-design-icons/Pencil.vue'
+
+interface IProfileAction {
+ target: string
+ icon: string
+ id: string
+ title: string
+}
+
+interface IStatus {
+ icon: string,
+ message: string,
+ userId: string,
+}
+
+export default defineComponent({
+ name: 'Profile',
+
+ components: {
+ AccountIcon,
+ MapMarkerIcon,
+ NcActionLink,
+ NcActions,
+ NcAppContent,
+ NcAvatar,
+ NcButton,
+ NcContent,
+ NcEmptyContent,
+ PencilIcon,
+ },
+
+ setup() {
+ return {
+ t,
+ }
+ },
+
+ data() {
+ const profileParameters = loadState('profile', 'profileParameters', {
+ userId: null as string|null,
+ displayname: null as string|null,
+ address: null as string|null,
+ organisation: null as string|null,
+ role: null as string|null,
+ headline: null as string|null,
+ biography: null as string|null,
+ actions: [] as IProfileAction[],
+ isUserAvatarVisible: false,
+ pronouns: null as string|null,
+ })
+
+ return {
+ ...profileParameters,
+ status: loadState<Partial<IStatus>>('profile', 'status', {}),
+ sections: window.OCA.Core.ProfileSections.getSections(),
+ }
+ },
+
+ computed: {
+ isCurrentUser() {
+ return getCurrentUser()?.uid === this.userId
+ },
+
+ allActions() {
+ return this.actions
+ },
+
+ primaryAction() {
+ if (this.allActions.length) {
+ return this.allActions[0]
+ }
+ return null
+ },
+
+ otherActions() {
+ if (this.allActions.length > 1) {
+ return this.allActions.slice(1)
+ }
+ return []
+ },
+
+ settingsUrl() {
+ return generateUrl('/settings/user')
+ },
+
+ emptyProfileMessage() {
+ return this.isCurrentUser
+ ? t('profile', 'You have not added any info yet')
+ : t('profile', '{user} has not added any info yet', { user: (this.displayname || this.userId || '') })
+ },
+ },
+
+ mounted() {
+ // Set the user's displayname or userId in the page title and preserve the default title of "Nextcloud" at the end
+ document.title = `${this.displayname || this.userId} - ${document.title}`
+ subscribe('user_status:status.updated', this.handleStatusUpdate)
+ },
+
+ beforeDestroy() {
+ unsubscribe('user_status:status.updated', this.handleStatusUpdate)
+ },
+
+ methods: {
+ handleStatusUpdate(status: IStatus) {
+ if (this.isCurrentUser && status.userId === this.userId) {
+ this.status = status
+ }
+ },
+
+ openStatusModal() {
+ const statusMenuItem = document.querySelector<HTMLButtonElement>('.user-status-menu-item')
+ // Changing the user status is only enabled if you are the current user
+ if (this.isCurrentUser) {
+ if (statusMenuItem) {
+ statusMenuItem.click()
+ } else {
+ showError(t('profile', 'Error opening the user status modal, try hard refreshing the page'))
+ }
+ }
+ },
+ },
+})
+</script>
+
+<style lang="scss" scoped>
+$profile-max-width: 1024px;
+$content-max-width: 640px;
+
+:deep(#app-content-vue) {
+ background-color: unset;
+}
+
+.profile {
+ width: 100%;
+ overflow-y: auto;
+
+ &__header {
+ display: flex;
+ position: sticky;
+ height: 190px;
+ top: -40px;
+ background-color: var(--color-main-background-blur);
+ backdrop-filter: var(--filter-background-blur);
+ -webkit-backdrop-filter: var(--filter-background-blur);
+
+ &__container {
+ align-self: flex-end;
+ width: 100%;
+ max-width: $profile-max-width;
+ margin: 8px auto;
+ row-gap: 8px;
+ display: grid;
+ grid-template-rows: max-content max-content;
+ grid-template-columns: 240px 1fr;
+ justify-content: center;
+
+ &__placeholder {
+ grid-row: 1 / 3;
+ }
+
+ &__displayname {
+ padding-inline: 16px; // same as the status text button, see NcButton
+ width: $content-max-width;
+ height: 45px;
+ margin-block: 125px 0;
+ display: flex;
+ align-items: center;
+ gap: 18px;
+
+ h2 {
+ font-size: 30px;
+ margin: 0;
+ }
+
+ span {
+ font-size: 20px;
+ }
+ }
+ }
+ }
+
+ &__sidebar {
+ position: sticky;
+ top: 0;
+ align-self: flex-start;
+ padding-top: 20px;
+ min-width: 220px;
+ margin-block: -150px 0;
+ margin-inline: 0 20px;
+
+ // Specificity hack is needed to override Avatar component styles
+ :deep(.avatar.avatardiv) {
+ text-align: center;
+ margin: auto;
+ display: block;
+ padding: 8px;
+
+ &.interactive {
+ .avatardiv__user-status {
+ // Show that the status is interactive
+ cursor: pointer;
+ }
+ }
+
+ .avatardiv__user-status {
+ inset-inline-end: 14px;
+ bottom: 14px;
+ width: 34px;
+ height: 34px;
+ background-size: 28px;
+ border: none;
+ // Styles when custom status icon and status text are set
+ background-color: var(--color-main-background);
+ line-height: 34px;
+ font-size: 20px;
+ }
+ }
+ }
+
+ &__wrapper {
+ background-color: var(--color-main-background);
+ min-height: 100%;
+ }
+
+ &__content {
+ max-width: $profile-max-width;
+ margin: 0 auto;
+ display: flex;
+ width: 100%;
+ }
+
+ &__blocks {
+ margin: 18px 0 80px 0;
+ display: grid;
+ gap: 16px 0;
+ width: $content-max-width;
+
+ p, h3 {
+ cursor: text;
+ overflow-wrap: anywhere;
+ }
+
+ &-details {
+ display: flex;
+ flex-direction: column;
+ gap: 2px 0;
+
+ .detail {
+ display: inline-block;
+ color: var(--color-text-maxcontrast);
+
+ p .map-icon {
+ display: inline-block;
+ vertical-align: middle;
+ }
+ }
+ }
+
+ &-headline {
+ margin-inline: 0;
+ margin-block: 10px 0;
+ font-weight: bold;
+ font-size: 20px;
+ }
+
+ &-biography {
+ white-space: pre-line;
+ }
+ }
+}
+
+@media only screen and (max-width: 1024px) {
+ .profile {
+ &__header {
+ height: 250px;
+ position: unset;
+
+ &__container {
+ grid-template-columns: unset;
+ margin-bottom: 110px;
+
+ &__displayname {
+ margin: 80px 20px 0px 0px!important;
+ width: unset;
+ text-align: center;
+ padding-inline: 12px;
+ }
+
+ &__edit-button {
+ width: fit-content;
+ display: block;
+ margin: 60px auto;
+ }
+
+ &__status-text {
+ margin: 4px auto;
+ }
+ }
+ }
+
+ &__content {
+ display: block;
+
+ .avatar {
+ // Overlap avatar to top header
+ margin-top: -110px !important;
+ }
+ }
+
+ &__blocks {
+ width: unset;
+ max-width: 600px;
+ margin: 0 auto;
+ padding: 20px 50px 50px 50px;
+ }
+
+ &__sidebar {
+ margin: unset;
+ position: unset;
+ }
+ }
+}
+
+.user-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 8px 0;
+ margin-top: 20px;
+
+ &__primary {
+ margin: 0 auto;
+
+ &__icon {
+ filter: var(--primary-invert-if-dark);
+ }
+ }
+
+ &__other {
+ display: flex;
+ justify-content: center;
+ gap: 0 4px;
+
+ &__icon {
+ height: 20px;
+ width: 20px;
+ object-fit: contain;
+ filter: var(--background-invert-if-dark);
+ align-self: center;
+ margin: 12px; // so we get 44px x 44px
+ }
+ }
+}
+</style>
--- /dev/null
+<?php
+/**
+ * SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+/** @var array $_ */
+/** @var \OCP\IL10N $l */
+/** @var \OCP\Defaults $theme */
+// @codeCoverageIgnoreStart
+if (!isset($_)) { //standalone page is not supported anymore - redirect to /
+ require_once '../../lib/base.php';
+
+ $urlGenerator = \OC::$server->getURLGenerator();
+ header('Location: ' . $urlGenerator->getAbsoluteURL('/'));
+ exit;
+}
+// @codeCoverageIgnoreEnd
+?>
+<?php if (isset($_['content'])) : ?>
+ <?php print_unescaped($_['content']) ?>
+<?php else : ?>
+ <div class="body-login-container update">
+ <div class="icon-big icon-error"></div>
+ <h2><?php p($l->t('Profile not found')); ?></h2>
+ <p class="infogroup"><?php p($l->t('The profile does not exist.')); ?></p>
+ <p><a class="button primary" href="<?php p(\OC::$server->getURLGenerator()->linkTo('', 'index.php')) ?>">
+ <?php p($l->t('Back to %s', [$theme->getName()])); ?>
+ </a></p>
+ </div>
+<?php endif; ?>
--- /dev/null
+<?php
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+?>
+<div id="content"></div>
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Profile\Tests\Controller;
+
+use OC\Profile\ProfileManager;
+use OC\UserStatus\Manager;
+use OCA\Profile\Controller\ProfilePageController;
+use OCP\AppFramework\Services\IInitialState;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\INavigationManager;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\Share\IManager;
+use Test\TestCase;
+
+class ProfilePageControllerTest extends TestCase {
+
+ private IUserManager $userManager;
+ private ProfilePageController $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $request = $this->createMock(IRequest::class);
+ $initialStateService = $this->createMock(IInitialState::class);
+ $profileManager = $this->createMock(ProfileManager::class);
+ $shareManager = $this->createMock(IManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $userSession = $this->createMock(IUserSession::class);
+ $userStatusManager = $this->createMock(Manager::class);
+ $navigationManager = $this->createMock(INavigationManager::class);
+ $eventDispatcher = $this->createMock(IEventDispatcher::class);
+
+ $this->controller = new ProfilePageController(
+ 'profile',
+ $request,
+ $initialStateService,
+ $profileManager,
+ $shareManager,
+ $this->userManager,
+ $userSession,
+ $userStatusManager,
+ $navigationManager,
+ $eventDispatcher,
+ );
+ }
+
+ public function testUserNotFound(): void {
+ $this->userManager->method('get')
+ ->willReturn(null);
+
+ $response = $this->controller->index('bob');
+
+ $this->assertTrue($response->isThrottled());
+ }
+
+ public function testUserDisabled(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('isEnabled')
+ ->willReturn(false);
+
+ $this->userManager->method('get')
+ ->willReturn($user);
+
+ $response = $this->controller->index('bob');
+
+ $this->assertFalse($response->isThrottled());
+ }
+}
$widgetData['icon'] . ($widgetData['icon'] ? ' ' : '') . $widgetData['message'] . ', ' . $formattedDate,
// https://nextcloud.local/index.php/u/julien
$this->urlGenerator->getAbsoluteURL(
- $this->urlGenerator->linkToRoute('core.ProfilePage.index', ['targetUserId' => $widgetData['userId']])
+ $this->urlGenerator->linkToRoute('profile.ProfilePage.index', ['targetUserId' => $widgetData['userId']])
),
$this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute('core.avatar.getAvatar', ['userId' => $widgetData['userId'], 'size' => 44])
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-/**
- * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-namespace OC\Core\Controller;
-
-use OC\Profile\ProfileManager;
-use OCP\AppFramework\Controller;
-use OCP\AppFramework\Http\Attribute\AnonRateLimit;
-use OCP\AppFramework\Http\Attribute\BruteForceProtection;
-use OCP\AppFramework\Http\Attribute\FrontpageRoute;
-use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
-use OCP\AppFramework\Http\Attribute\OpenAPI;
-use OCP\AppFramework\Http\Attribute\PublicPage;
-use OCP\AppFramework\Http\Attribute\UserRateLimit;
-use OCP\AppFramework\Http\TemplateResponse;
-use OCP\AppFramework\Services\IInitialState;
-use OCP\EventDispatcher\IEventDispatcher;
-use OCP\INavigationManager;
-use OCP\IRequest;
-use OCP\IUserManager;
-use OCP\IUserSession;
-use OCP\Profile\BeforeTemplateRenderedEvent;
-use OCP\Share\IManager as IShareManager;
-use OCP\UserStatus\IManager as IUserStatusManager;
-
-#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
-class ProfilePageController extends Controller {
- public function __construct(
- string $appName,
- IRequest $request,
- private IInitialState $initialStateService,
- private ProfileManager $profileManager,
- private IShareManager $shareManager,
- private IUserManager $userManager,
- private IUserSession $userSession,
- private IUserStatusManager $userStatusManager,
- private INavigationManager $navigationManager,
- private IEventDispatcher $eventDispatcher,
- ) {
- parent::__construct($appName, $request);
- }
-
- #[PublicPage]
- #[NoCSRFRequired]
- #[FrontpageRoute(verb: 'GET', url: '/u/{targetUserId}')]
- #[BruteForceProtection(action: 'user')]
- #[UserRateLimit(limit: 30, period: 120)]
- #[AnonRateLimit(limit: 30, period: 120)]
- public function index(string $targetUserId): TemplateResponse {
- $profileNotFoundTemplate = new TemplateResponse(
- 'core',
- '404-profile',
- [],
- TemplateResponse::RENDER_AS_GUEST,
- );
-
- $targetUser = $this->userManager->get($targetUserId);
- if ($targetUser === null) {
- $profileNotFoundTemplate->throttle();
- return $profileNotFoundTemplate;
- }
- if (!$targetUser->isEnabled()) {
- return $profileNotFoundTemplate;
- }
- $visitingUser = $this->userSession->getUser();
-
- if (!$this->profileManager->isProfileEnabled($targetUser)) {
- return $profileNotFoundTemplate;
- }
-
- // Run user enumeration checks only if viewing another user's profile
- if ($targetUser !== $visitingUser) {
- if (!$this->shareManager->currentUserCanEnumerateTargetUser($visitingUser, $targetUser)) {
- return $profileNotFoundTemplate;
- }
- }
-
- if ($visitingUser !== null) {
- $userStatuses = $this->userStatusManager->getUserStatuses([$targetUserId]);
- $status = $userStatuses[$targetUserId] ?? null;
- if ($status !== null) {
- $this->initialStateService->provideInitialState('status', [
- 'icon' => $status->getIcon(),
- 'message' => $status->getMessage(),
- ]);
- }
- }
-
- $this->initialStateService->provideInitialState(
- 'profileParameters',
- $this->profileManager->getProfileFields($targetUser, $visitingUser),
- );
-
- if ($targetUser === $visitingUser) {
- $this->navigationManager->setActiveEntry('profile');
- }
-
- $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($targetUserId));
-
- \OCP\Util::addScript('core', 'profile');
-
- return new TemplateResponse(
- 'core',
- 'profile',
- [],
- $this->userSession->isLoggedIn() ? TemplateResponse::RENDER_AS_USER : TemplateResponse::RENDER_AS_PUBLIC,
- );
- }
-}
"password_policy",
"photos",
"privacy",
+ "profile",
"provisioning_api",
"recommendations",
"related_resources",
"password_policy",
"photos",
"privacy",
+ "profile",
"provisioning_api",
"recommendations",
"related_resources",
+++ /dev/null
-/**
- * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import { getCSPNonce } from '@nextcloud/auth'
-import Vue from 'vue'
-
-import Profile from './views/Profile.vue'
-import ProfileSections from './profile/ProfileSections.js'
-
-__webpack_nonce__ = getCSPNonce()
-
-if (!window.OCA) {
- window.OCA = {}
-}
-
-if (!window.OCA.Core) {
- window.OCA.Core = {}
-}
-Object.assign(window.OCA.Core, { ProfileSections: new ProfileSections() })
-
-const View = Vue.extend(Profile)
-
-window.addEventListener('DOMContentLoaded', () => {
- new View().$mount('#content')
-})
+++ /dev/null
-/**
- * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-export default class ProfileSections {
-
- _sections
-
- constructor() {
- this._sections = []
- }
-
- /**
- * @param {registerSectionCallback} section To be called to mount the section to the profile page
- */
- registerSection(section) {
- this._sections.push(section)
- }
-
- getSections() {
- return this._sections
- }
-
-}
+++ /dev/null
-<!--
- - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
- - SPDX-License-Identifier: AGPL-3.0-or-later
--->
-
-<template>
- <NcContent app-name="profile">
- <NcAppContent>
- <div class="profile__header">
- <div class="profile__header__container">
- <div class="profile__header__container__placeholder" />
- <div class="profile__header__container__displayname">
- <h2>{{ displayname || userId }}</h2>
- <span v-if="pronouns">·</span>
- <span v-if="pronouns" class="profile__header__container__pronouns">{{ pronouns }}</span>
- <NcButton v-if="isCurrentUser"
- type="primary"
- :href="settingsUrl">
- <template #icon>
- <PencilIcon :size="20" />
- </template>
- {{ t('core', 'Edit Profile') }}
- </NcButton>
- </div>
- <NcButton v-if="status.icon || status.message"
- :disabled="!isCurrentUser"
- :type="isCurrentUser ? 'tertiary' : 'tertiary-no-background'"
- @click="openStatusModal">
- {{ status.icon }} {{ status.message }}
- </NcButton>
- </div>
- </div>
-
- <div class="profile__wrapper">
- <div class="profile__content">
- <div class="profile__sidebar">
- <NcAvatar class="avatar"
- :class="{ interactive: isCurrentUser }"
- :user="userId"
- :size="180"
- :show-user-status="true"
- :show-user-status-compact="false"
- :disable-menu="true"
- :disable-tooltip="true"
- :is-no-user="!isUserAvatarVisible"
- @click.native.prevent.stop="openStatusModal" />
-
- <div class="user-actions">
- <!-- When a tel: URL is opened with target="_blank", a blank new tab is opened which is inconsistent with the handling of other URLs so we set target="_self" for the phone action -->
- <NcButton v-if="primaryAction"
- type="primary"
- class="user-actions__primary"
- :href="primaryAction.target"
- :icon="primaryAction.icon"
- :target="primaryAction.id === 'phone' ? '_self' :'_blank'">
- <template #icon>
- <!-- Fix for https://github.com/nextcloud-libraries/nextcloud-vue/issues/2315 -->
- <img :src="primaryAction.icon" alt="" class="user-actions__primary__icon">
- </template>
- {{ primaryAction.title }}
- </NcButton>
- <NcActions class="user-actions__other" :inline="4">
- <NcActionLink v-for="action in otherActions"
- :key="action.id"
- :close-after-click="true"
- :href="action.target"
- :target="action.id === 'phone' ? '_self' :'_blank'">
- <template #icon>
- <!-- Fix for https://github.com/nextcloud-libraries/nextcloud-vue/issues/2315 -->
- <img :src="action.icon" alt="" class="user-actions__other__icon">
- </template>
- {{ action.title }}
- </NcActionLink>
- </NcActions>
- </div>
- </div>
-
- <div class="profile__blocks">
- <div v-if="organisation || role || address" class="profile__blocks-details">
- <div v-if="organisation || role" class="detail">
- <p>{{ organisation }} <span v-if="organisation && role">•</span> {{ role }}</p>
- </div>
- <div v-if="address" class="detail">
- <p>
- <MapMarkerIcon class="map-icon"
- :size="16" />
- {{ address }}
- </p>
- </div>
- </div>
- <template v-if="headline || biography || sections.length > 0">
- <h3 v-if="headline" class="profile__blocks-headline">
- {{ headline }}
- </h3>
- <p v-if="biography" class="profile__blocks-biography">
- {{ biography }}
- </p>
-
- <!-- additional entries, use it with cautious -->
- <div v-for="(section, index) in sections"
- :ref="'section-' + index"
- :key="index"
- class="profile__additionalContent">
- <component :is="section($refs['section-'+index], userId)" :user-id="userId" />
- </div>
- </template>
- <NcEmptyContent v-else
- class="profile__blocks-empty-info"
- :name="emptyProfileMessage"
- :description="t('core', 'The headline and about sections will show up here')">
- <template #icon>
- <AccountIcon :size="60" />
- </template>
- </NcEmptyContent>
- </div>
- </div>
- </div>
- </NcAppContent>
- </NcContent>
-</template>
-
-<script lang="ts">
-import { getCurrentUser } from '@nextcloud/auth'
-import { showError } from '@nextcloud/dialogs'
-import { subscribe, unsubscribe } from '@nextcloud/event-bus'
-import { loadState } from '@nextcloud/initial-state'
-import { translate as t } from '@nextcloud/l10n'
-import { generateUrl } from '@nextcloud/router'
-import { defineComponent } from 'vue'
-
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
-import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcContent from '@nextcloud/vue/dist/Components/NcContent.js'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
-import AccountIcon from 'vue-material-design-icons/Account.vue'
-import MapMarkerIcon from 'vue-material-design-icons/MapMarker.vue'
-import PencilIcon from 'vue-material-design-icons/Pencil.vue'
-
-interface IProfileAction {
- target: string
- icon: string
- id: string
- title: string
-}
-
-interface IStatus {
- icon: string,
- message: string,
- userId: string,
-}
-
-export default defineComponent({
- name: 'Profile',
-
- components: {
- AccountIcon,
- MapMarkerIcon,
- NcActionLink,
- NcActions,
- NcAppContent,
- NcAvatar,
- NcButton,
- NcContent,
- NcEmptyContent,
- PencilIcon,
- },
-
- data() {
- const profileParameters = loadState('core', 'profileParameters', {
- userId: null as string|null,
- displayname: null as string|null,
- address: null as string|null,
- organisation: null as string|null,
- role: null as string|null,
- headline: null as string|null,
- biography: null as string|null,
- actions: [] as IProfileAction[],
- isUserAvatarVisible: false,
- pronouns: null as string|null,
- })
-
- return {
- ...profileParameters,
- status: loadState<Partial<IStatus>>('core', 'status', {}),
- sections: window.OCA.Core.ProfileSections.getSections(),
- }
- },
-
- computed: {
- isCurrentUser() {
- return getCurrentUser()?.uid === this.userId
- },
-
- allActions() {
- return this.actions
- },
-
- primaryAction() {
- if (this.allActions.length) {
- return this.allActions[0]
- }
- return null
- },
-
- otherActions() {
- console.warn(this.allActions)
- if (this.allActions.length > 1) {
- return this.allActions.slice(1)
- }
- return []
- },
-
- settingsUrl() {
- return generateUrl('/settings/user')
- },
-
- emptyProfileMessage() {
- return this.isCurrentUser
- ? t('core', 'You have not added any info yet')
- : t('core', '{user} has not added any info yet', { user: (this.displayname || this.userId!) })
- },
- },
-
- mounted() {
- // Set the user's displayname or userId in the page title and preserve the default title of "Nextcloud" at the end
- document.title = `${this.displayname || this.userId} - ${document.title}`
- subscribe('user_status:status.updated', this.handleStatusUpdate)
- },
-
- beforeDestroy() {
- unsubscribe('user_status:status.updated', this.handleStatusUpdate)
- },
-
- methods: {
- t,
-
- handleStatusUpdate(status: IStatus) {
- if (this.isCurrentUser && status.userId === this.userId) {
- this.status = status
- }
- },
-
- openStatusModal() {
- const statusMenuItem = document.querySelector<HTMLButtonElement>('.user-status-menu-item')
- // Changing the user status is only enabled if you are the current user
- if (this.isCurrentUser) {
- if (statusMenuItem) {
- statusMenuItem.click()
- } else {
- showError(t('core', 'Error opening the user status modal, try hard refreshing the page'))
- }
- }
- },
- },
-})
-</script>
-
-<style lang="scss" scoped>
-$profile-max-width: 1024px;
-$content-max-width: 640px;
-
-:deep(#app-content-vue) {
- background-color: unset;
-}
-
-.profile {
- width: 100%;
- overflow-y: auto;
-
- &__header {
- display: flex;
- position: sticky;
- height: 190px;
- top: -40px;
- background-color: var(--color-main-background-blur);
- backdrop-filter: var(--filter-background-blur);
- -webkit-backdrop-filter: var(--filter-background-blur);
-
- &__container {
- align-self: flex-end;
- width: 100%;
- max-width: $profile-max-width;
- margin: 8px auto;
- row-gap: 8px;
- display: grid;
- grid-template-rows: max-content max-content;
- grid-template-columns: 240px 1fr;
- justify-content: center;
-
- &__placeholder {
- grid-row: 1 / 3;
- }
-
- &__displayname {
- padding-inline: 16px; // same as the status text button, see NcButton
- width: $content-max-width;
- height: 45px;
- margin-block: 125px 0;
- display: flex;
- align-items: center;
- gap: 18px;
-
- h2 {
- font-size: 30px;
- margin: 0;
- }
-
- span {
- font-size: 20px;
- }
- }
- }
- }
-
- &__sidebar {
- position: sticky;
- top: 0;
- align-self: flex-start;
- padding-top: 20px;
- min-width: 220px;
- margin-block: -150px 0;
- margin-inline: 0 20px;
-
- // Specificity hack is needed to override Avatar component styles
- :deep(.avatar.avatardiv) {
- text-align: center;
- margin: auto;
- display: block;
- padding: 8px;
-
- &.interactive {
- .avatardiv__user-status {
- // Show that the status is interactive
- cursor: pointer;
- }
- }
-
- .avatardiv__user-status {
- inset-inline-end: 14px;
- bottom: 14px;
- width: 34px;
- height: 34px;
- background-size: 28px;
- border: none;
- // Styles when custom status icon and status text are set
- background-color: var(--color-main-background);
- line-height: 34px;
- font-size: 20px;
- }
- }
- }
-
- &__wrapper {
- background-color: var(--color-main-background);
- min-height: 100%;
- }
-
- &__content {
- max-width: $profile-max-width;
- margin: 0 auto;
- display: flex;
- width: 100%;
- }
-
- &__blocks {
- margin: 18px 0 80px 0;
- display: grid;
- gap: 16px 0;
- width: $content-max-width;
-
- p, h3 {
- cursor: text;
- overflow-wrap: anywhere;
- }
-
- &-details {
- display: flex;
- flex-direction: column;
- gap: 2px 0;
-
- .detail {
- display: inline-block;
- color: var(--color-text-maxcontrast);
-
- p .map-icon {
- display: inline-block;
- vertical-align: middle;
- }
- }
- }
-
- &-headline {
- margin-inline: 0;
- margin-block: 10px 0;
- font-weight: bold;
- font-size: 20px;
- }
-
- &-biography {
- white-space: pre-line;
- }
- }
-}
-
-@media only screen and (max-width: 1024px) {
- .profile {
- &__header {
- height: 250px;
- position: unset;
-
- &__container {
- grid-template-columns: unset;
- margin-bottom: 110px;
-
- &__displayname {
- margin: 80px 20px 0px 0px!important;
- width: unset;
- text-align: center;
- padding-inline: 12px;
- }
-
- &__edit-button {
- width: fit-content;
- display: block;
- margin: 60px auto;
- }
-
- &__status-text {
- margin: 4px auto;
- }
- }
- }
-
- &__content {
- display: block;
-
- .avatar {
- // Overlap avatar to top header
- margin-top: -110px !important;
- }
- }
-
- &__blocks {
- width: unset;
- max-width: 600px;
- margin: 0 auto;
- padding: 20px 50px 50px 50px;
- }
-
- &__sidebar {
- margin: unset;
- position: unset;
- }
- }
-}
-
-.user-actions {
- display: flex;
- flex-direction: column;
- gap: 8px 0;
- margin-top: 20px;
-
- &__primary {
- margin: 0 auto;
-
- &__icon {
- filter: var(--primary-invert-if-dark);
- }
- }
-
- &__other {
- display: flex;
- justify-content: center;
- gap: 0 4px;
-
- &__icon {
- height: 20px;
- width: 20px;
- object-fit: contain;
- filter: var(--background-invert-if-dark);
- align-self: center;
- margin: 12px; // so we get 44px x 44px
- }
- }
-}
-</style>
+++ /dev/null
-<?php
-/**
- * SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-/** @var array $_ */
-/** @var \OCP\IL10N $l */
-/** @var \OCP\Defaults $theme */
-// @codeCoverageIgnoreStart
-if (!isset($_)) { //standalone page is not supported anymore - redirect to /
- require_once '../../lib/base.php';
-
- $urlGenerator = \OC::$server->getURLGenerator();
- header('Location: ' . $urlGenerator->getAbsoluteURL('/'));
- exit;
-}
-// @codeCoverageIgnoreEnd
-?>
-<?php if (isset($_['content'])) : ?>
- <?php print_unescaped($_['content']) ?>
-<?php else : ?>
- <div class="body-login-container update">
- <div class="icon-big icon-error"></div>
- <h2><?php p($l->t('Profile not found')); ?></h2>
- <p class="infogroup"><?php p($l->t('The profile does not exist.')); ?></p>
- <p><a class="button primary" href="<?php p(\OC::$server->getURLGenerator()->linkTo('', 'index.php')) ?>">
- <?php p($l->t('Back to %s', [$theme->getName()])); ?>
- </a></p>
- </div>
-<?php endif; ?>
+++ /dev/null
-<?php
-/**
- * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-?>
-<div id="content"></div>
user = $user
cy.modifyUser(user, 'language', 'en')
cy.modifyUser(user, 'locale', 'en_US')
- })
- cy.wait(500)
+ // Make sure the user is logged in at least once
+ // before the snapshot is taken to speed up the tests
+ cy.login(user)
+ cy.visit('/settings/user')
- cy.backupDB().then(($snapshot) => {
- snapshot = $snapshot
+ cy.backupDB().then(($snapshot) => {
+ snapshot = $snapshot
+ })
})
})
'OC\\Core\\Controller\\OCSController' => $baseDir . '/core/Controller/OCSController.php',
'OC\\Core\\Controller\\PreviewController' => $baseDir . '/core/Controller/PreviewController.php',
'OC\\Core\\Controller\\ProfileApiController' => $baseDir . '/core/Controller/ProfileApiController.php',
- 'OC\\Core\\Controller\\ProfilePageController' => $baseDir . '/core/Controller/ProfilePageController.php',
'OC\\Core\\Controller\\RecommendedAppsController' => $baseDir . '/core/Controller/RecommendedAppsController.php',
'OC\\Core\\Controller\\ReferenceApiController' => $baseDir . '/core/Controller/ReferenceApiController.php',
'OC\\Core\\Controller\\ReferenceController' => $baseDir . '/core/Controller/ReferenceController.php',
array (
0 => __DIR__ . '/../../..' . '/lib/public',
),
- 'NCU\\' =>
+ 'NCU\\' =>
array (
0 => __DIR__ . '/../../..' . '/lib/unstable',
),
'OC\\Core\\Controller\\OCSController' => __DIR__ . '/../../..' . '/core/Controller/OCSController.php',
'OC\\Core\\Controller\\PreviewController' => __DIR__ . '/../../..' . '/core/Controller/PreviewController.php',
'OC\\Core\\Controller\\ProfileApiController' => __DIR__ . '/../../..' . '/core/Controller/ProfileApiController.php',
- 'OC\\Core\\Controller\\ProfilePageController' => __DIR__ . '/../../..' . '/core/Controller/ProfilePageController.php',
'OC\\Core\\Controller\\RecommendedAppsController' => __DIR__ . '/../../..' . '/core/Controller/RecommendedAppsController.php',
'OC\\Core\\Controller\\ReferenceApiController' => __DIR__ . '/../../..' . '/core/Controller/ReferenceApiController.php',
'OC\\Core\\Controller\\ReferenceController' => __DIR__ . '/../../..' . '/core/Controller/ReferenceController.php',
'core',
'files_sharing',
'files',
+ 'profile',
'settings',
'spreed',
];
'core',
'files_sharing',
'files',
+ 'profile',
'settings',
'spreed',
];
if (!empty($targetUser)) {
if ($this->profileManager->isProfileEnabled($targetUser)) {
$entry->setProfileTitle($this->l10nFactory->get('lib')->t('View profile'));
- $entry->setProfileUrl($this->urlGenerator->linkToRouteAbsolute('core.ProfilePage.index', ['targetUserId' => $targetUserId]));
+ $entry->setProfileUrl($this->urlGenerator->linkToRouteAbsolute('profile.ProfilePage.index', ['targetUserId' => $targetUserId]));
}
}
}
if ($this->profileManager->isProfileEnabled($targetUser)) {
$iconUrl = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'actions/profile.svg'));
$profileActionText = $this->l10nFactory->get('lib')->t('View profile');
- $profileUrl = $this->urlGenerator->linkToRouteAbsolute('core.ProfilePage.index', ['targetUserId' => $targetUserId]);
+ $profileUrl = $this->urlGenerator->linkToRouteAbsolute('profile.ProfilePage.index', ['targetUserId' => $targetUserId]);
$action = $this->actionFactory->newLinkAction($iconUrl, $profileActionText, $profileUrl, 'profile');
// Set highest priority (by descending order), other actions have the default priority 10 as defined in lib/private/Contacts/ContactsMenu/Actions/LinkAction.php
$action->setPriority(20);
'id' => 'profile',
'order' => 1,
'href' => $this->urlGenerator->linkToRoute(
- 'core.ProfilePage.index',
+ 'profile.ProfilePage.index',
['targetUserId' => $this->userSession->getUser()->getUID()],
),
'name' => $l->t('View profile'),
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-namespace Core\Controller;
-
-use OC\Core\Controller\ProfilePageController;
-use OC\Profile\ProfileManager;
-use OC\UserStatus\Manager;
-use OCP\AppFramework\Services\IInitialState;
-use OCP\EventDispatcher\IEventDispatcher;
-use OCP\INavigationManager;
-use OCP\IRequest;
-use OCP\IUser;
-use OCP\IUserManager;
-use OCP\IUserSession;
-use OCP\Share\IManager;
-use Test\TestCase;
-
-class ProfilePageControllerTest extends TestCase {
-
- private IUserManager $userManager;
- private ProfilePageController $controller;
-
- protected function setUp(): void {
- parent::setUp();
-
- $request = $this->createMock(IRequest::class);
- $initialStateService = $this->createMock(IInitialState::class);
- $profileManager = $this->createMock(ProfileManager::class);
- $shareManager = $this->createMock(IManager::class);
- $this->userManager = $this->createMock(IUserManager::class);
- $userSession = $this->createMock(IUserSession::class);
- $userStatusManager = $this->createMock(Manager::class);
- $navigationManager = $this->createMock(INavigationManager::class);
- $eventDispatcher = $this->createMock(IEventDispatcher::class);
-
- $this->controller = new ProfilePageController(
- 'core',
- $request,
- $initialStateService,
- $profileManager,
- $shareManager,
- $this->userManager,
- $userSession,
- $userStatusManager,
- $navigationManager,
- $eventDispatcher,
- );
- }
-
- public function testUserNotFound(): void {
- $this->userManager->method('get')
- ->willReturn(null);
-
- $response = $this->controller->index('bob');
-
- $this->assertTrue($response->isThrottled());
- }
-
- public function testUserDisabled(): void {
- $user = $this->createMock(IUser::class);
- $user->method('isEnabled')
- ->willReturn(false);
-
- $this->userManager->method('get')
- ->willReturn($user);
-
- $response = $this->controller->index('bob');
-
- $this->assertFalse($response->isThrottled());
- }
-}
login: path.join(__dirname, 'core/src', 'login.js'),
main: path.join(__dirname, 'core/src', 'main.js'),
maintenance: path.join(__dirname, 'core/src', 'maintenance.js'),
- profile: path.join(__dirname, 'core/src', 'profile.ts'),
'public-page-menu': path.resolve(__dirname, 'core/src', 'public-page-menu.ts'),
recommendedapps: path.join(__dirname, 'core/src', 'recommendedapps.js'),
systemtags: path.resolve(__dirname, 'core/src', 'systemtags/merged-systemtags.js'),
'vue-settings-admin': path.join(__dirname, 'apps/federatedfilesharing/src', 'main-admin.js'),
'vue-settings-personal': path.join(__dirname, 'apps/federatedfilesharing/src', 'main-personal.js'),
},
+ profile: {
+ main: path.join(__dirname, 'apps/profile/src', 'main.ts'),
+ },
settings: {
apps: path.join(__dirname, 'apps/settings/src', 'apps.js'),
'legacy-admin': path.join(__dirname, 'apps/settings/src', 'admin.js'),