diff --git a/composer.json b/composer.json
index 9acce8255..fe656a294 100644
--- a/composer.json
+++ b/composer.json
@@ -16,6 +16,8 @@
"ext-json": "*",
"ext-pdo": "*",
"ext-pdo_sqlite": "*",
+ "symfony/string": "^6.0",
+ "symfony/translation-contracts": "^2.5",
"teamtnt/tntsearch": "^4.2"
},
"require-dev": {
diff --git a/composer.lock b/composer.lock
index 755d5d5e7..77543ff7b 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "9b344fa99c6c4689dcac79184736e09c",
+ "content-hash": "03965594e8e2d0b06d40acf0448daf7c",
"packages": [
{
"name": "predis/predis",
@@ -67,6 +67,487 @@
],
"time": "2023-09-13T16:42:03+00:00"
},
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.31.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-grapheme",
+ "version": "v1.31.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+ "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
+ "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's grapheme_* functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "grapheme",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-normalizer",
+ "version": "v1.31.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+ "reference": "3833d7255cc303546435cb650316bff708a1c75c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c",
+ "reference": "3833d7255cc303546435cb650316bff708a1c75c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's Normalizer class and related functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "intl",
+ "normalizer",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.31.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
+ "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
+ "name": "symfony/string",
+ "version": "v6.0.19",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "d9e72497367c23e08bf94176d2be45b00a9d232a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/string/zipball/d9e72497367c23e08bf94176d2be45b00a9d232a",
+ "reference": "d9e72497367c23e08bf94176d2be45b00a9d232a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.2",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-intl-grapheme": "~1.0",
+ "symfony/polyfill-intl-normalizer": "~1.0",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "conflict": {
+ "symfony/translation-contracts": "<2.0"
+ },
+ "require-dev": {
+ "symfony/error-handler": "^5.4|^6.0",
+ "symfony/http-client": "^5.4|^6.0",
+ "symfony/translation-contracts": "^2.0|^3.0",
+ "symfony/var-exporter": "^5.4|^6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\String\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "grapheme",
+ "i18n",
+ "string",
+ "unicode",
+ "utf-8",
+ "utf8"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/string/tree/v6.0.19"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-01-01T08:36:10+00:00"
+ },
+ {
+ "name": "symfony/translation-contracts",
+ "version": "v2.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation-contracts.git",
+ "reference": "b0073a77ac0b7ea55131020e87b1e3af540f4664"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b0073a77ac0b7ea55131020e87b1e3af540f4664",
+ "reference": "b0073a77ac0b7ea55131020e87b1e3af540f4664",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5"
+ },
+ "suggest": {
+ "symfony/translation-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.5-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Translation\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to translation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/translation-contracts/tree/v2.5.3"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-01-23T13:51:25+00:00"
+ },
{
"name": "teamtnt/tntsearch",
"version": "v4.3.0",
@@ -1175,5 +1656,5 @@
"platform-overrides": {
"php": "8.0.2"
},
- "plugin-api-version": "2.3.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 0083793c1..789034433 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -52,6 +52,8 @@
use OCP\Share\Events\ShareDeletedEvent;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
+use Symfony\Component\String\Slugger\AsciiSlugger;
+use Symfony\Component\String\Slugger\SluggerInterface;
class Application extends App implements IBootstrap {
public const APP_NAME = 'collectives';
@@ -130,6 +132,10 @@ public function register(IRegistrationContext $context): void {
/** @psalm-suppress MissingDependency */
$context->registerSetupCheck(CirclesAppIsEnableCheck::class);
}
+
+ $context->registerService(SluggerInterface::class, function (ContainerInterface $c) {
+ return new AsciiSlugger();
+ });
}
public function boot(IBootcontext $context): void {
diff --git a/lib/Command/CreateCollective.php b/lib/Command/CreateCollective.php
index 03973df6c..459cc152f 100644
--- a/lib/Command/CreateCollective.php
+++ b/lib/Command/CreateCollective.php
@@ -10,7 +10,6 @@
namespace OCA\Collectives\Command;
use OC\Core\Command\Base;
-use OCA\Collectives\Fs\NodeHelper;
use OCA\Collectives\Service\CollectiveService;
use OCP\IUserManager;
use OCP\IUserSession;
@@ -52,11 +51,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$user = $this->userManager->get($userId);
$this->userSession->setUser($user);
$lang = $this->l10nFactory->getUserLanguage($this->userSession->getUser());
- $safeName = $this->nodeHelper->sanitiseFilename($name);
$output->write('Creating new collective ' . $name . ' ... ');
- [$collective, $info] = $this->collectiveService->createCollective($userId, $lang, $safeName);
+ [, $info] = $this->collectiveService->createCollective($userId, $lang, $name);
$output->writeln('' . $info ?: 'done.' . '');
return 0;
diff --git a/lib/Controller/CollectiveController.php b/lib/Controller/CollectiveController.php
index b45006e90..bfd7fefbb 100644
--- a/lib/Controller/CollectiveController.php
+++ b/lib/Controller/CollectiveController.php
@@ -10,9 +10,7 @@
namespace OCA\Collectives\Controller;
use Closure;
-
use OCA\Collectives\Db\Collective;
-use OCA\Collectives\Fs\NodeHelper;
use OCA\Collectives\Service\CollectiveService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
@@ -60,11 +58,10 @@ public function index(): DataResponse {
#[NoAdminRequired]
public function create(string $name, ?string $emoji = null): DataResponse {
return $this->prepareResponse(function () use ($name, $emoji): array {
- $safeName = $this->nodeHelper->sanitiseFilename($name);
[$collective, $info] = $this->service->createCollective(
$this->getUserId(),
$this->getUserLang(),
- $safeName,
+ $name,
$emoji,
);
return [
diff --git a/lib/Db/Collective.php b/lib/Db/Collective.php
index fb50a1812..c7ada415c 100644
--- a/lib/Db/Collective.php
+++ b/lib/Db/Collective.php
@@ -17,9 +17,10 @@
use RuntimeException;
/**
- * Class Collective
* @method int getId()
* @method void setId(int $value)
+ * @method string getSlug()
+ * @method void setSlug(?string $value)
* @method string getCircleUniqueId()
* @method void setCircleUniqueId(string $circleUniqueId)
* @method int getPermissions()
@@ -59,6 +60,7 @@ class Collective extends Entity implements JsonSerializable {
protected ?string $circleUniqueId = null;
protected int $permissions = self::defaultPermissions;
+ protected ?string $slug = null;
protected ?string $emoji = null;
protected ?int $trashTimestamp = null;
protected int $pageMode = self::defaultPageMode;
@@ -258,6 +260,7 @@ public function canShare(): bool {
public function jsonSerialize(): array {
return [
'id' => $this->id,
+ 'slug' => $this->slug,
'circleId' => $this->circleUniqueId,
'emoji' => $this->emoji,
'trashTimestamp' => $this->trashTimestamp,
diff --git a/lib/Db/Page.php b/lib/Db/Page.php
index b0279912f..d3c169b0f 100644
--- a/lib/Db/Page.php
+++ b/lib/Db/Page.php
@@ -18,6 +18,8 @@
* @method void setId(int $value)
* @method int getFileId()
* @method void setFileId(int $value)
+ * @method string getSlug()
+ * @method void setSlug(?string $value)
* @method string getLastUserId()
* @method void setLastUserId(string $value)
* @method string getEmoji()
@@ -31,6 +33,7 @@
*/
class Page extends Entity implements JsonSerializable {
protected ?int $fileId = null;
+ protected ?string $slug = null;
protected ?string $lastUserId = null;
protected ?string $emoji = null;
protected ?string $subpageOrder = null;
diff --git a/lib/Migration/Version021200Date20240820000000.php b/lib/Migration/Version021200Date20240820000000.php
new file mode 100644
index 000000000..2af5b3ef0
--- /dev/null
+++ b/lib/Migration/Version021200Date20240820000000.php
@@ -0,0 +1,76 @@
+getTable('collectives');
+ if (!$table->hasColumn('slug')) {
+ $this->runSlugGeneration = true;
+ $table->addColumn('slug', Types::STRING, [
+ 'notnull' => false,
+ 'default' => false,
+ 'length' => 255,
+ ]);
+
+ return $schema;
+ }
+
+ return null;
+ }
+
+ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+ if (!$this->runSlugGeneration) {
+ return;
+ }
+
+ $query = $this->connection->getQueryBuilder();
+ $query->select(['id', 'circle_unique_id'])->from('collectives');
+ $result = $query->executeQuery();
+
+ $update = $this->connection->getQueryBuilder();
+ $update->update('collectives')
+ ->set('slug', $update->createParameter('slug'))
+ ->where($update->expr()->eq('id', $update->createParameter('id')));
+
+ while ($row = $result->fetch()) {
+ $circle = $this->circleHelper->getCircle($row['circle_unique_id'], null, true);
+ $slug = $this->slugGeneratorService->generateCollectiveSlug($row['id'], $circle->getSanitizedName());
+
+ $update
+ ->setParameter('id', (int)$row['id'], IQueryBuilder::PARAM_INT)
+ ->setParameter('slug', $slug, IQueryBuilder::PARAM_STR)
+ ->executeStatement();
+ }
+ $result->closeCursor();
+ }
+}
diff --git a/lib/Migration/Version021200Date20240820000001.php b/lib/Migration/Version021200Date20240820000001.php
new file mode 100644
index 000000000..fb7d8a9ef
--- /dev/null
+++ b/lib/Migration/Version021200Date20240820000001.php
@@ -0,0 +1,95 @@
+getTable('collectives_pages');
+ if (!$table->hasColumn('slug')) {
+ $this->runSlugGeneration = true;
+ $table->addColumn('slug', Types::STRING, [
+ 'notnull' => false,
+ 'default' => false,
+ 'length' => 255,
+ ]);
+
+ return $schema;
+ }
+
+ return null;
+ }
+
+ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+ if (!$this->runSlugGeneration) {
+ return;
+ }
+
+ $queryCollectives = $this->connection->getQueryBuilder();
+ $queryCollectives->select(['id', 'circle_unique_id'])->from('collectives');
+ $resultCollectives = $queryCollectives->executeQuery();
+
+
+ $queryPages = $this->connection->getQueryBuilder();
+ $queryPages->select(['id'])
+ ->from('collectives_pages');
+ $resultPages = $queryPages->executeQuery();
+
+ $update = $this->connection->getQueryBuilder();
+ $update->update('collectives_pages')
+ ->set('slug', $update->createParameter('slug'))
+ ->where($update->expr()->eq('file_id', $update->createParameter('file_id')));
+
+ while ($rowCollective = $resultCollectives->fetch()) {
+ $circle = $this->circleHelper->getCircle($rowCollective['circle_unique_id'], null, true);
+ /** @var PageInfo[] $pageInfos */
+ $pageInfos = $this->service->findAll($rowCollective['id'], $circle->getOwner()->getUserId());
+
+ foreach ($pageInfos as $pageInfo) {
+ if ($pageInfo->getFileName() === PageInfo::INDEX_PAGE_TITLE . PageInfo::SUFFIX) {
+ continue;
+ }
+
+ $slug = $this->slugGeneratorService->generatePageSlug($pageInfo->getId(), $pageInfo->getTitle());
+ $update
+ ->setParameter('file_id', $pageInfo->getId(), IQueryBuilder::PARAM_INT)
+ ->setParameter('slug', $slug, IQueryBuilder::PARAM_STR)
+ ->executeStatement();
+ }
+ }
+
+ $resultCollectives->closeCursor();
+ $resultPages->closeCursor();
+ }
+}
diff --git a/lib/Model/PageInfo.php b/lib/Model/PageInfo.php
index a4b2cfeee..044d41372 100644
--- a/lib/Model/PageInfo.php
+++ b/lib/Model/PageInfo.php
@@ -21,6 +21,7 @@ class PageInfo implements JsonSerializable {
public const SUFFIX = '.md';
private int $id;
+ private ?string $slug = null;
private ?string $lastUserId = null;
private ?string $lastUserDisplayName = null;
private ?string $emoji = null;
@@ -44,6 +45,14 @@ public function setId(int $id): void {
$this->id = $id;
}
+ public function getSlug(): ?string {
+ return $this->slug;
+ }
+
+ public function setSlug(?string $slug): void {
+ $this->slug = $slug;
+ }
+
public function getLastUserId(): ?string {
return $this->lastUserId;
}
@@ -159,6 +168,7 @@ public function setShareToken(string $shareToken): void {
public function jsonSerialize(): array {
return [
'id' => $this->id,
+ 'slug' => $this->slug,
'lastUserId' => $this->lastUserId,
'lastUserDisplayName' => $this->lastUserDisplayName,
'emoji' => $this->emoji,
@@ -180,7 +190,7 @@ public function jsonSerialize(): array {
* @throws InvalidPathException
* @throws NotFoundException
*/
- public function fromFile(File $file, int $parentId, ?string $lastUserId = null, ?string $lastUserDisplayName = null, ?string $emoji = null, ?string $subpageOrder = null, bool $fullWidth = false): void {
+ public function fromFile(File $file, int $parentId, ?string $lastUserId = null, ?string $lastUserDisplayName = null, ?string $emoji = null, ?string $subpageOrder = null, bool $fullWidth = false, ?string $slug = null): void {
$this->setId($file->getId());
// Set folder name as title for all index pages except the collective landing page
$dirName = dirname($file->getInternalPath());
@@ -213,6 +223,9 @@ public function fromFile(File $file, int $parentId, ?string $lastUserId = null,
if ($subpageOrder !== null) {
$this->setSubpageOrder($subpageOrder);
}
+ if ($slug !== null) {
+ $this->setSlug($slug);
+ }
$this->setParentId($parentId);
}
}
diff --git a/lib/Service/CollectiveService.php b/lib/Service/CollectiveService.php
index dc446b418..e2557a02f 100644
--- a/lib/Service/CollectiveService.php
+++ b/lib/Service/CollectiveService.php
@@ -17,6 +17,7 @@
use OCA\Collectives\Db\CollectiveUserSettingsMapper;
use OCA\Collectives\Db\Page;
use OCA\Collectives\Db\PageMapper;
+use OCA\Collectives\Fs\NodeHelper;
use OCA\Collectives\Model\PageInfo;
use OCA\Collectives\Mount\CollectiveFolderManager;
use OCA\Collectives\Trash\PageTrashBackend;
@@ -44,6 +45,8 @@ public function __construct(
private PageMapper $pageMapper,
private IL10N $l10n,
private IEventDispatcher $eventDispatcher,
+ private NodeHelper $nodeHelper,
+ private SlugGeneratorService $slugGeneratorService,
) {
parent::__construct($collectiveMapper, $circleHelper);
}
@@ -169,8 +172,10 @@ public function getCollectiveNameWithEmoji(Collective $collective): string {
*/
public function createCollective(string $userId,
string $userLang,
- string $safeName,
+ string $name,
?string $emoji = null): array {
+ $safeName = $this->nodeHelper->sanitiseFilename($name);
+
if ($safeName === '') {
throw new UnprocessableEntityException('Empty collective name is not allowed');
}
@@ -202,7 +207,7 @@ public function createCollective(string $userId,
$this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent(null));
}
- // Create collective object
+ // Create a collective object
$collective = new Collective();
$collective->setCircleId($circle->getSingleId());
$collective->setPermissions(Collective::defaultPermissions);
@@ -211,7 +216,11 @@ public function createCollective(string $userId,
}
$collective = $this->collectiveMapper->insert($collective);
- // Decorate collective object
+ $slug = $this->slugGeneratorService->generateCollectiveSlug($collective->getId(), $name);
+ $collective->setSlug($slug);
+ $this->collectiveMapper->update($collective);
+
+ // Decorate a collective object
$collective->setName($circle->getSanitizedName());
$collective->setLevel($this->circleHelper->getLevel($circle->getSingleId(), $userId));
@@ -248,6 +257,7 @@ public function createCollective(string $userId,
public function updateCollective(int $id,
string $userId,
?string $emoji = null): Collective {
+ //fixme: should we update slug?
$collective = $this->getCollective($id, $userId);
if (!$this->circleHelper->isAdmin($collective->getCircleId(), $userId)) {
diff --git a/lib/Service/PageService.php b/lib/Service/PageService.php
index 2f5da770c..f2da979bb 100644
--- a/lib/Service/PageService.php
+++ b/lib/Service/PageService.php
@@ -49,6 +49,7 @@ public function __construct(
private IConfig $config,
ContainerInterface $container,
private SessionService $sessionService,
+ private SlugGeneratorService $slugGeneratorService,
) {
try {
$this->pushQueue = $container->get(IQueue::class);
@@ -200,7 +201,8 @@ private function getPageByFile(File $file, ?Node $parent = null): PageInfo {
$lastUserId = ($page !== null) ? $page->getLastUserId() : null;
$emoji = ($page !== null) ? $page->getEmoji() : null;
$subpageOrder = ($page !== null) ? $page->getSubpageOrder() : null;
- $fullWidth = $page !== null && $page->getFullWidth();
+ $fullWidth = ($page !== null) ? $page->getFullWidth() : false;
+ $slug = ($page !== null) ? $page->getSlug() : null;
$pageInfo = new PageInfo();
try {
$pageInfo->fromFile($file,
@@ -209,7 +211,8 @@ private function getPageByFile(File $file, ?Node $parent = null): PageInfo {
$lastUserId ? $this->userManager->getDisplayName($lastUserId) : null,
$emoji,
$subpageOrder,
- $fullWidth);
+ $fullWidth,
+ $slug);
} catch (FilesNotFoundException|InvalidPathException $e) {
throw new NotFoundException($e->getMessage(), 0, $e);
}
@@ -230,6 +233,7 @@ private function getTrashPageByFile(File $file, string $filename, string $timest
$emoji = ($page !== null) ? $page->getEmoji() : null;
$subpageOrder = ($page !== null) ? $page->getSubpageOrder() : null;
$trashTimestamp = ($page !== null) ? $page->getTrashTimestamp(): (int)$timestamp;
+ $slug = ($page !== null) ? $page->getSlug() : null;
$pageInfo = new PageInfo();
try {
$pageInfo->fromFile($file,
@@ -238,7 +242,8 @@ private function getTrashPageByFile(File $file, string $filename, string $timest
$lastUserId ? $this->userManager->getDisplayName($lastUserId) : null,
$emoji,
$subpageOrder,
- $page && $page->getFullWidth());
+ $page->getFullWidth(),
+ $slug);
$pageInfo->setTrashTimestamp($trashTimestamp);
$pageInfo->setFilePath('');
$pageInfo->setTitle(basename($filename, PageInfo::SUFFIX));
@@ -263,7 +268,7 @@ private function notifyPush(int $collectiveId): void {
}
}
- private function updatePage(int $collectiveId, int $fileId, string $userId, ?string $emoji = null, ?bool $fullWidth = null): void {
+ private function updatePage(int $collectiveId, int $fileId, string $userId, ?string $emoji = null, ?bool $fullWidth = null, ?string $slug = null): void {
$page = new Page();
$page->setFileId($fileId);
$page->setLastUserId($userId);
@@ -273,6 +278,9 @@ private function updatePage(int $collectiveId, int $fileId, string $userId, ?str
if ($fullWidth !== null) {
$page->setFullWidth($fullWidth);
}
+ if ($slug !== null) {
+ $page->setSlug($slug);
+ }
$this->pageMapper->updateOrInsert($page);
$this->notifyPush($collectiveId);
}
@@ -296,7 +304,7 @@ private function updateSubpageOrder(int $collectiveId, int $fileId, string $user
* @throws NotFoundException
* @throws NotPermittedException
*/
- private function newPage(int $collectiveId, Folder $folder, string $filename, string $userId): PageInfo {
+ private function newPage(int $collectiveId, Folder $folder, string $filename, string $userId, ?string $title): PageInfo {
$hasTemplate = NodeHelper::folderHasSubPage($folder, PageInfo::TEMPLATE_PAGE_TITLE);
try {
if ($hasTemplate === 1) {
@@ -325,7 +333,9 @@ private function newPage(int $collectiveId, Folder $folder, string $filename, st
$this->getParentPageId($newFile),
$userId,
$this->userManager->getDisplayName($userId));
- $this->updatePage($collectiveId, $newFile->getId(), $userId);
+ $slug = $title ? $this->generateSlugForPage($title, $newFile) : null;
+ $this->updatePage($collectiveId, $newFile->getId(), $userId, slug: $slug);
+ $pageInfo->setSlug($slug);
} catch (FilesNotFoundException|InvalidPathException $e) {
throw new NotFoundException($e->getMessage(), 0, $e);
}
@@ -416,7 +426,7 @@ public function getPagesFromFolder(int $collectiveId, Folder $folder, string $us
if (!$forceIndex && count($pageInfos) === 0) {
return [];
}
- $indexPage = $this->newPage($collectiveId, $folder, PageInfo::INDEX_PAGE_TITLE, $userId);
+ $indexPage = $this->newPage($collectiveId, $folder, PageInfo::INDEX_PAGE_TITLE, $userId, null);
}
return array_merge([$indexPage], $pageInfos);
@@ -603,7 +613,7 @@ public function create(int $collectiveId, int $parentId, string $title, string $
$safeTitle = $this->nodeHelper->sanitiseFilename($title, self::DEFAULT_PAGE_TITLE);
$filename = NodeHelper::generateFilename($folder, $safeTitle, PageInfo::SUFFIX);
- $pageInfo = $this->newPage($collectiveId, $folder, $filename, $userId);
+ $pageInfo = $this->newPage($collectiveId, $folder, $filename, $userId, $title);
$this->addToSubpageOrder($collectiveId, $parentId, $pageInfo->getId(), 0, $userId);
return $pageInfo;
}
@@ -745,8 +755,9 @@ public function copy(int $collectiveId, int $id, ?int $parentId, ?string $title,
if (null !== $newFile = $this->moveOrCopyPage($collectiveFolder, $file, $parentId, $title, true)) {
$file = $newFile;
}
+ $slug = $title ? $this->generateSlugForPage($title, $file) : $this->generateSlugForPage($page->getTitle(), $file);
try {
- $this->updatePage($collectiveId, $file->getId(), $userId, $page->getEmoji());
+ $this->updatePage($collectiveId, $file->getId(), $userId, $page->getEmoji(), slug: $slug);
} catch (InvalidPathException|FilesNotFoundException $e) {
throw new NotFoundException($e->getMessage(), 0, $e);
}
@@ -772,9 +783,10 @@ public function move(int $collectiveId, int $id, ?int $parentId, ?string $title,
if (null !== $newFile = $this->moveOrCopyPage($collectiveFolder, $file, $parentId, $title, false)) {
$file = $newFile;
}
+ $slug = $title ? $this->generateSlugForPage($title, $file) : null;
try {
- $this->updatePage($collectiveId, $file->getId(), $userId);
+ $this->updatePage($collectiveId, $file->getId(), $userId, slug: $slug);
} catch (InvalidPathException|FilesNotFoundException $e) {
throw new NotFoundException($e->getMessage(), 0, $e);
}
@@ -1084,4 +1096,12 @@ public function getBacklinks(int $collectiveId, int $id, string $userId): array
return $backlinks;
}
+
+ private function generateSlugForPage(string $title, ?File $file): ?string {
+ if (!$file) {
+ return null;
+ }
+
+ return $this->slugGeneratorService->generatePageSlug($file->getId(), $title);
+ }
}
diff --git a/lib/Service/SlugGeneratorService.php b/lib/Service/SlugGeneratorService.php
new file mode 100644
index 000000000..ef394d1aa
--- /dev/null
+++ b/lib/Service/SlugGeneratorService.php
@@ -0,0 +1,20 @@
+slugger->slug($name)->toString() . '-' . $collectiveId;
+ }
+
+ public function generatePageSlug(int $fileId, string $title): string {
+ return $this->slugger->slug($title)->toString() . '-' . $fileId;
+ }
+}
diff --git a/src/Collectives.vue b/src/Collectives.vue
index d65b06b15..c2cf33d94 100644
--- a/src/Collectives.vue
+++ b/src/Collectives.vue
@@ -71,7 +71,9 @@ export default {
$route: {
handler(val) {
this.rootStore.collectiveParam = val.params.collective
+ this.rootStore.collectiveId = val.params.collectiveId ? parseInt(val.params.collectiveId) : null
this.rootStore.pageParam = val.params.page
+ this.rootStore.pageId = val.params.pageId ? parseInt(val.params.pageId) : null
this.rootStore.shareTokenParam = val.params.token
this.rootStore.fileIdQuery = val.query.fileId
},
diff --git a/src/components/Collective.vue b/src/components/Collective.vue
index f9739731c..9e4bcf2f8 100644
--- a/src/components/Collective.vue
+++ b/src/components/Collective.vue
@@ -59,8 +59,9 @@ export default {
},
computed: {
- ...mapState(useRootStore, ['isPublic', 'loading', 'pageParam']),
+ ...mapState(useRootStore, ['isPublic', 'loading', 'pageParam', 'pageId']),
...mapState(useCollectivesStore, [
+ 'collectivePath',
'currentCollective',
'currentCollectiveCanEdit',
'currentCollectiveIsPageShare',
@@ -69,7 +70,9 @@ export default {
...mapState(usePagesStore, [
'currentFileIdPage',
'currentPage',
+ 'isIndexPage',
'pagePath',
+ 'pageSlugPath',
]),
...mapState(useVersionsStore, ['version']),
@@ -91,6 +94,20 @@ export default {
},
'currentPage.id'() {
this.selectVersion(null)
+
+ const routerParams = this.$router.currentRoute.params
+ // If the current page is not the one we are supposed to be on, redirect
+ if (this.currentPage && !this.isIndexPage) {
+ const actualUrl = `${routerParams.collectiveSlugPart}-${routerParams.collectiveId}/${routerParams.pageSlugPart}-${routerParams.pageId}`
+ const expectedUrl = this.pageSlugPath(this.currentPage)
+
+ if (actualUrl !== expectedUrl) {
+ this.$router.replace(this.pagePath(this.currentPage))
+ }
+ } else if (this.currentCollective
+ && `${routerParams.collectiveSlugPart}-${routerParams.collectiveId}` !== this.currentCollective.slug) {
+ this.$router.replace(this.collectivePath(this.currentCollective))
+ }
},
'notFound'(current) {
if (current && this.currentFileIdPage) {
diff --git a/src/components/Nav/CollectiveSettings.vue b/src/components/Nav/CollectiveSettings.vue
index 9766e860e..7b55909df 100644
--- a/src/components/Nav/CollectiveSettings.vue
+++ b/src/components/Nav/CollectiveSettings.vue
@@ -192,6 +192,7 @@ export default {
'collectiveParam',
'loading',
'pageParam',
+ 'pageId',
]),
...mapState(useCollectivesStore, ['isCollectiveOwner']),
...mapState(usePagesStore, ['pages']),
@@ -339,6 +340,7 @@ export default {
// Push new router path if currentCollective was renamed
if (redirect) {
+ // fixme: adjust
this.$router.push(
'/' + encodeURIComponent(this.newCollectiveName)
+ (this.pageParam ? '/' + this.pageParam : ''),
diff --git a/src/components/Page.vue b/src/components/Page.vue
index 75a0b17ac..2f24648c9 100644
--- a/src/components/Page.vue
+++ b/src/components/Page.vue
@@ -171,6 +171,7 @@ export default {
]),
...mapState(usePagesStore, [
'currentPage',
+ 'pagePath',
'isIndexPage',
'isFullWidthView',
'isTemplatePage',
@@ -316,6 +317,7 @@ export default {
// The resulting title may be different due to sanitizing
this.newTitle = this.currentPage.title
this.getPages(false)
+ this.$router.replace(this.pagePath(this.currentPage))
} catch (e) {
console.error(e)
showError(t('collectives', 'Could not rename the page'))
diff --git a/src/components/PageList/SubpageList.vue b/src/components/PageList/SubpageList.vue
index fbe906ba0..450c80817 100644
--- a/src/components/PageList/SubpageList.vue
+++ b/src/components/PageList/SubpageList.vue
@@ -80,7 +80,7 @@ export default {
},
computed: {
- ...mapState(useRootStore, ['pageParam']),
+ ...mapState(useRootStore, ['pageParam', 'pageId']),
...mapState(useCollectivesStore, ['currentCollectiveCanEdit']),
...mapState(usePagesStore, [
'pagePath',
@@ -134,6 +134,9 @@ export default {
'pageParam'() {
this.initCollapsed()
},
+ 'pageId'() {
+ this.initCollapsed()
+ },
},
mounted() {
diff --git a/src/router.js b/src/router.js
index 2c6bc3616..9d90f2283 100644
--- a/src/router.js
+++ b/src/router.js
@@ -16,22 +16,50 @@ const routes = [
path: '/',
component: Home,
},
+ {
+ path: '/_/print/:collectiveSlugPart-:collectiveId(\\d+)',
+ component: CollectivePrintView,
+ props: (route) => route.params,
+ },
{
path: '/_/print/:collective',
component: CollectivePrintView,
props: (route) => route.params,
},
+ {
+ path: '/p/:token/print/:collectiveSlugPart-:collectiveId(\\d+)',
+ component: CollectivePrintView,
+ props: (route) => route.params,
+ },
{
path: '/p/:token/print/:collective',
component: CollectivePrintView,
props: (route) => route.params,
},
+ {
+ path: '/p/:token/:collectiveSlugPart-:collectiveId(\\d+)',
+ component: CollectiveView,
+ props: (route) => route.params,
+ children: [
+ { path: ':pageSlugPart+-:pageId(\\d+)' },
+ { path: ':page*' },
+ ],
+ },
{
path: '/p/:token/:collective',
component: CollectiveView,
props: (route) => route.params,
children: [{ path: ':page*' }],
},
+ {
+ path: '/:collectiveSlugPart-:collectiveId(\\d+)',
+ component: CollectiveView,
+ props: (route) => route.params,
+ children: [
+ { path: ':pageSlugPart+-:pageId(\\d+)' },
+ { path: ':page*' },
+ ],
+ },
{
path: '/:collective',
component: CollectiveView,
diff --git a/src/stores/collectives.js b/src/stores/collectives.js
index 29ad49b1c..d44225f50 100644
--- a/src/stores/collectives.js
+++ b/src/stores/collectives.js
@@ -32,6 +32,11 @@ export const useCollectivesStore = defineStore('collectives', {
currentCollective(state) {
const rootStore = useRootStore()
+ if (rootStore.collectiveId) {
+ return state.collectives.find(
+ (collective) => collective.id === rootStore.collectiveId,
+ )
+ }
return state.collectives.find(
(collective) => collective.name === rootStore.collectiveParam,
)
@@ -40,11 +45,11 @@ export const useCollectivesStore = defineStore('collectives', {
collectivePath() {
return (collective) => {
const rootStore = useRootStore()
+ const slug = collective.slug ? collective.slug : encodeURIComponent(collective.name)
if (rootStore.isPublic) {
- return `/p/${rootStore.shareTokenParam}/${encodeURIComponent(collective.name)}`
- } else {
- return `/${encodeURIComponent(collective.name)}`
+ return `/p/${rootStore.shareTokenParam}/${slug}`
}
+ return `/${slug}`
}
},
@@ -75,7 +80,11 @@ export const useCollectivesStore = defineStore('collectives', {
updatedCollectivePath(state) {
const collective = state.updatedCollective
- return collective?.name && `/${encodeURIComponent(collective.name)}`
+ if (!collective) {
+ return false
+ }
+ const slug = collective.slug ? collective.slug : encodeURIComponent(collective.name)
+ return `/${slug}`
},
collectiveChanged(state) {
diff --git a/src/stores/pages.js b/src/stores/pages.js
index 2d0f01f30..2e9c0a401 100644
--- a/src/stores/pages.js
+++ b/src/stores/pages.js
@@ -48,7 +48,7 @@ export const usePagesStore = defineStore('pages', {
const collectivesStore = useCollectivesStore()
return collectivesStore.currentCollectiveIsPageShare
? false
- : !rootStore.pageParam || rootStore.pageParam === INDEX_PAGE
+ : (!rootStore.pageId && !rootStore.pageParam) || rootStore.pageParam === INDEX_PAGE
},
isIndexPage: (state) => state.currentPage.fileName === INDEX_PAGE + '.md',
isTemplatePage: (state) => state.currentPage.title === TEMPLATE_PAGE,
@@ -67,13 +67,23 @@ export const usePagesStore = defineStore('pages', {
currentPageIds(state) {
const rootStore = useRootStore()
// Return root page
- if (!rootStore.pageParam
+ if ((!rootStore.pageId && !rootStore.pageParam)
|| rootStore.pageParam === INDEX_PAGE) {
return [state.rootPage.id]
}
- // Iterate through all path levels to find the correct page
const pageIds = []
+ if (rootStore.pageId) {
+ let pageId = rootStore.pageId
+ do {
+ const page = state.pageById(pageId)
+ pageIds.unshift(page.id)
+ pageId = page.parentId
+ } while (pageId)
+ return pageIds
+ }
+
+ // Iterate through all path levels to find the correct page
const parts = rootStore.pageParam.split('/').filter(Boolean)
let page = state.rootPage
for (const i in parts) {
@@ -97,21 +107,21 @@ export const usePagesStore = defineStore('pages', {
}
},
- pagePath: () => (page) => {
+ pagePath: (state) => (page) => {
const rootStore = useRootStore()
- const collectivesStore = useCollectivesStore()
- const collective = collectivesStore.currentCollective.name
- const { filePath, fileName, title, id } = page
- const titlePart = fileName !== INDEX_PAGE + '.md' && title
// For public collectives, prepend `/p/{shareToken}`
- const pagePath = [
- rootStore.isPublic ? 'p' : null,
- rootStore.isPublic ? rootStore.shareTokenParam : null,
- collective,
- ...filePath.split('/'),
- titlePart,
- ].filter(Boolean).map(encodeURIComponent).join('/')
- return `/${pagePath}?fileId=${id}`
+ let prefix = ''
+ if (rootStore.isPublic) {
+ prefix = `/p/${encodeURIComponent(rootStore.shareTokenParam)}`
+ }
+ return `${prefix}/${state.pageSlugPath(page)}`
+ },
+
+ pageSlugPath: (state) => (page) => {
+ const collectivesStore = useCollectivesStore()
+ const collective = collectivesStore.currentCollective.slug || collectivesStore.currentCollective.name
+ const slugsPath = pageParents(state)(page.id).map(p => p.slug)
+ return [collective, ...slugsPath].filter(Boolean).map(encodeURIComponent).join('/')
},
pagePathTitle: () => (page) => {
diff --git a/src/stores/root.js b/src/stores/root.js
index 8d879fbbe..3af8f5a5f 100644
--- a/src/stores/root.js
+++ b/src/stores/root.js
@@ -15,7 +15,9 @@ export const useRootStore = defineStore('root', {
printView: false,
activeSidebarTab: 'attachments',
collectiveParam: '',
+ collectiveId: null,
pageParam: '',
+ pageId: null,
shareTokenParam: '',
fileIdQuery: '',
}),
diff --git a/vendor-bin/behat/composer.lock b/vendor-bin/behat/composer.lock
index 1cfd49f59..74ca8a4cd 100644
--- a/vendor-bin/behat/composer.lock
+++ b/vendor-bin/behat/composer.lock
@@ -3422,5 +3422,5 @@
"platform-overrides": {
"php": "8.0.2"
},
- "plugin-api-version": "2.3.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/vendor-bin/cs-fixer/composer.lock b/vendor-bin/cs-fixer/composer.lock
index a0b42e34f..e437286af 100644
--- a/vendor-bin/cs-fixer/composer.lock
+++ b/vendor-bin/cs-fixer/composer.lock
@@ -158,5 +158,5 @@
"platform-overrides": {
"php": "8.0.2"
},
- "plugin-api-version": "2.3.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/vendor-bin/phpunit/composer.lock b/vendor-bin/phpunit/composer.lock
index 3f8fce5cb..21bbbbbbc 100644
--- a/vendor-bin/phpunit/composer.lock
+++ b/vendor-bin/phpunit/composer.lock
@@ -1759,5 +1759,5 @@
"platform-overrides": {
"php": "8.0.2"
},
- "plugin-api-version": "2.3.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/vendor-bin/psalm/composer.lock b/vendor-bin/psalm/composer.lock
index df355abc1..ed828abe9 100644
--- a/vendor-bin/psalm/composer.lock
+++ b/vendor-bin/psalm/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "02a43821754df48354fb0767a7e05b9d",
+ "content-hash": "dacdbcf7c8ba472cce49b9aca87b3ad1",
"packages": [],
"packages-dev": [
{
@@ -524,16 +524,16 @@
},
{
"name": "felixfbecker/language-server-protocol",
- "version": "v1.5.2",
+ "version": "v1.5.3",
"source": {
"type": "git",
"url": "https://github.com/felixfbecker/php-language-server-protocol.git",
- "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842"
+ "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842",
- "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842",
+ "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9",
+ "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9",
"shasum": ""
},
"require": {
@@ -574,9 +574,9 @@
],
"support": {
"issues": "https://github.com/felixfbecker/php-language-server-protocol/issues",
- "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2"
+ "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3"
},
- "time": "2022-03-02T22:36:06+00:00"
+ "time": "2024-04-30T00:40:11+00:00"
},
{
"name": "fidry/cpu-core-counter",
@@ -967,16 +967,16 @@
},
{
"name": "phpstan/phpdoc-parser",
- "version": "1.31.0",
+ "version": "1.32.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
- "reference": "249f15fb843bf240cf058372dad29e100cee6c17"
+ "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/249f15fb843bf240cf058372dad29e100cee6c17",
- "reference": "249f15fb843bf240cf058372dad29e100cee6c17",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6ca22b154efdd9e3c68c56f5d94670920a1c19a4",
+ "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4",
"shasum": ""
},
"require": {
@@ -1008,9 +1008,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
- "source": "https://github.com/phpstan/phpdoc-parser/tree/1.31.0"
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/1.32.0"
},
- "time": "2024-09-22T11:32:18+00:00"
+ "time": "2024-09-26T07:23:32+00:00"
},
{
"name": "psr/clock",
@@ -2171,5 +2171,5 @@
"platform-overrides": {
"php": "8.0.2"
},
- "plugin-api-version": "2.3.0"
+ "plugin-api-version": "2.6.0"
}