Signed-off-by: Carl Schwan <carl@carlschwan.eu>
db-rewrite
Carl Schwan 2022-07-09 20:44:01 +02:00
rodzic 95f3fc3a5a
commit d53fc73448
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: C3AA6B3A5EFA7AC5
38 zmienionych plików z 2450 dodań i 729 usunięć

Wyświetl plik

@ -77,4 +77,9 @@
<contactsmenu>
<provider>OCA\Social\Providers\ContactsMenuProvider</provider>
</contactsmenu>
<settings>
<personal>OCA\Social\Settings\Personal</personal>
<personal-section>OCA\Social\Settings\PersonalSection</personal-section>
</settings>
</info>

Wyświetl plik

@ -32,9 +32,14 @@ declare(strict_types=1);
namespace OCA\Social\AppInfo;
use Closure;
use OCA\Social\Entity\Account;
use OCA\Social\Notification\Notifier;
use OCA\Social\Search\UnifiedSearchProvider;
use OCA\Social\Serializer\AccountSerializer;
use OCA\Social\Serializer\SerializerFactory;
use OCA\Social\Service\ConfigService;
use OCA\Social\Service\Feed\RedisFeedProvider;
use OCA\Social\Service\IFeedProvider;
use OCA\Social\Service\UpdateService;
use OCA\Social\WellKnown\WebfingerHandler;
use OCP\AppFramework\App;
@ -46,6 +51,7 @@ use OCP\IDBConnection;
use OCP\IServerContainer;
use OC\DB\SchemaWrapper;
use OCP\DB\ISchemaWrapper;
use Psr\Container\ContainerInterface;
use Throwable;
require_once __DIR__ . '/../../vendor/autoload.php';
@ -69,6 +75,15 @@ class Application extends App implements IBootstrap {
public function register(IRegistrationContext $context): void {
$context->registerSearchProvider(UnifiedSearchProvider::class);
$context->registerWellKnownHandler(WebfingerHandler::class);
$context->registerNotifierService(Notifier::class);
/** @var SerializerFactory $serializerFactory */
$serializerFactory = $this->getContainer()->get(SerializerFactory::class);
$serializerFactory->registerSerializer(Account::class, AccountSerializer::class);
$context->registerService(IFeedProvider::class, function (ContainerInterface $container): IFeedProvider {
return $container->get(RedisFeedProvider::class);
});
}
@ -76,43 +91,5 @@ class Application extends App implements IBootstrap {
* @param IBootContext $context
*/
public function boot(IBootContext $context): void {
$manager = $context->getServerContainer()
->getNotificationManager();
$manager->registerNotifierService(Notifier::class);
try {
$context->injectFn(Closure::fromCallable([$this, 'checkUpgradeStatus']));
} catch (Throwable $e) {
}
}
/**
* Register Navigation Tab
*
* @param IServerContainer $container
*/
protected function checkUpgradeStatus(IServerContainer $container) {
$upgradeChecked = $container->getConfig()
->getAppValue(Application::APP_NAME, 'update_checked', '');
if ($upgradeChecked === '0.3') {
return;
}
try {
$configService = $container->query(ConfigService::class);
$updateService = $container->query(UpdateService::class);
} catch (QueryException $e) {
return;
}
/** @var ISchemaWrapper $schema */
$schema = new SchemaWrapper($container->get(IDBConnection::class));
if ($schema->hasTable('social_a2_stream')) {
$updateService->checkUpdateStatus();
}
$configService->setAppValue('update_checked', '0.3');
}
}

Wyświetl plik

@ -30,6 +30,8 @@ declare(strict_types=1);
namespace OCA\Social\Controller;
use OCA\Social\Entity\Account;
use OCA\Social\Serializer\SerializerFactory;
use OCA\Social\Tools\Traits\TNCLogger;
use OCA\Social\Tools\Traits\TNCDataResponse;
use OCA\Social\Tools\Traits\TAsync;
@ -57,6 +59,8 @@ use OCA\Social\Service\StreamService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\Response;
use OCP\DB\ORM\IEntityManager;
use OCP\DB\ORM\IEntityRepository;
use OCP\IRequest;
class ActivityPubController extends Controller {
@ -76,13 +80,14 @@ class ActivityPubController extends Controller {
private StreamService $streamService;
private ConfigService $configService;
private MiscService $miscService;
private IEntityManager $entityManager;
public function __construct(
IRequest $request, SocialPubController $socialPubController, FediverseService $fediverseService,
CacheActorService $cacheActorService, SignatureService $signatureService,
StreamQueueService $streamQueueService, ImportService $importService, AccountService $accountService,
FollowService $followService, StreamService $streamService, ConfigService $configService,
MiscService $miscService
MiscService $miscService, IEntityManager $entityManager, SerializerFactory $serializerFactory
) {
parent::__construct(Application::APP_NAME, $request);
@ -97,11 +102,12 @@ class ActivityPubController extends Controller {
$this->streamService = $streamService;
$this->configService = $configService;
$this->miscService = $miscService;
$this->entityManager = $entityManager;
}
/**
* returns information about an Actor, based on the username.
* Returns the actor information
*
* This method should be called when a remote ActivityPub server require information
* about a local Social account
@ -111,9 +117,6 @@ class ActivityPubController extends Controller {
* @NoCSRFRequired
* @PublicPage
*
* @param string $username
*
* @return Response
* @throws UrlCloudException
* @throws SocialAppConfigException
*/
@ -122,15 +125,17 @@ class ActivityPubController extends Controller {
return $this->socialPubController->actor($username);
}
try {
$actor = $this->cacheActorService->getFromLocalAccount($username);
$actor->setDisplayW3ContextSecurity(true);
return $this->directSuccess($actor);
} catch (Exception $e) {
/** @var IEntityRepository<Account> $accountRepository */
$accountRepository = $this->entityManager->getRepository(Account::class);
$account = $accountRepository->findOneBy([
'userName' => $username,
]);
if ($account === null || !$account->isLocal()) {
http_response_code(404);
exit();
}
return $account->toJsonLd($this->request);
}

Wyświetl plik

@ -31,6 +31,7 @@ declare(strict_types=1);
namespace OCA\Social\Controller;
use OCA\Social\Service\AccountFinder;
use OCA\Social\Tools\Traits\TNCDataResponse;
use OCA\Social\Tools\Traits\TArrayTools;
use Exception;
@ -49,14 +50,13 @@ use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\DB\ORM\IEntityManager;
use OCP\IConfig;
use OCP\IInitialStateService;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IGroupManager;
use OCP\Server;
use OCP\IUserSession;
/**
* Class NavigationController
@ -70,41 +70,43 @@ class NavigationController extends Controller {
private ?string $userId = null;
private IConfig $config;
private IURLGenerator $urlGenerator;
private AccountService $accountService;
private AccountFinder $accountFinder;
private DocumentService $documentService;
private ConfigService $configService;
private MiscService $miscService;
private IL10N $l10n;
private CheckService $checkService;
private IInitialStateService $initialStateService;
private IInitialState $initialState;
private IUserSession $userSession;
public function __construct(
IL10N $l10n,
IRequest $request,
?string $userId,
IConfig $config,
IInitialStateService $initialStateService,
IInitialState $initialState,
IURLGenerator $urlGenerator,
AccountService $accountService,
AccountFinder $accountFinder,
DocumentService $documentService,
ConfigService $configService,
CheckService $checkService,
MiscService $miscService,
IEntityManager $manager
IUserSession $userSession
) {
parent::__construct(Application::APP_NAME, $request);
$this->userId = $userId;
$this->l10n = $l10n;
$this->config = $config;
$this->initialStateService = $initialStateService;
$this->initialState = $initialState;
$this->urlGenerator = $urlGenerator;
$this->checkService = $checkService;
$this->accountService = $accountService;
$this->accountFinder = $accountFinder;
$this->documentService = $documentService;
$this->configService = $configService;
$this->miscService = $miscService;
$this->userSession = $userSession;
}
@ -120,33 +122,31 @@ class NavigationController extends Controller {
public function navigate(string $path = ''): TemplateResponse {
$serverData = [
'public' => false,
'firstrun' => false,
'setup' => false,
'isAdmin' => Server::get(IGroupManager::class)
->isAdmin($this->userId),
'cliUrl' => $this->getCliUrl()
];
try {
$serverData['cloudAddress'] = $this->configService->getCloudUrl();
} catch (SocialAppConfigException $e) {
$this->checkService->checkInstallationStatus(true);
$cloudAddress = $this->setupCloudAddress();
if ($cloudAddress !== '') {
$serverData['cloudAddress'] = $cloudAddress;
} else {
$serverData['setup'] = true;
// TODO redirect to admin page
//$this->checkService->checkInstallationStatus(true);
//$cloudAddress = $this->setupCloudAddress();
//if ($cloudAddress !== '') {
// $serverData['cloudAddress'] = $cloudAddress;
//} else {
// $serverData['setup'] = true;
if ($serverData['isAdmin']) {
$cloudAddress = $this->request->getParam('cloudAddress');
if ($cloudAddress !== null) {
$this->configService->setCloudUrl($cloudAddress);
} else {
$this->initialStateService->provideInitialState(Application::APP_NAME, 'serverData', $serverData);
return new TemplateResponse(Application::APP_NAME, 'main');
}
}
}
// if ($serverData['isAdmin']) {
// $cloudAddress = $this->request->getParam('cloudAddress');
// if ($cloudAddress !== null) {
// $this->configService->setCloudUrl($cloudAddress);
// } else {
// $this->initialState->provideInitialState( 'serverData', $serverData);
// return new TemplateResponse(Application::APP_NAME, 'main');
// }
// }
//}
}
try {
@ -155,26 +155,9 @@ class NavigationController extends Controller {
$this->configService->setSocialUrl();
}
if ($serverData['isAdmin']) {
$checks = $this->checkService->checkDefault();
$serverData['checks'] = $checks;
}
$account = $this->accountFinder->getCurrentAccount($this->userSession->getUser());
/*
* Create social user account if it doesn't exist yet
*/
try {
$this->accountService->createActor($this->userId, $this->userId);
$serverData['firstrun'] = true;
} catch (AccountAlreadyExistsException $e) {
// we do nothing
} catch (NoUserException $e) {
// well, should not happens
} catch (SocialAppConfigException $e) {
// neither.
}
$this->initialStateService->provideInitialState(Application::APP_NAME, 'serverData', $serverData);
$this->initialState->provideInitialState('serverData', $serverData);
return new TemplateResponse(Application::APP_NAME, 'main');
}
@ -196,7 +179,7 @@ class NavigationController extends Controller {
return '';
}
private function getCliUrl() {
private function getCliUrl(): string {
$url = rtrim($this->urlGenerator->getBaseUrl(), '/');
$frontControllerActive =
($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true

Wyświetl plik

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Controller;
use OCA\Social\Entity\Account;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCA\Social\AppInfo\Application as App;
use OCP\DB\ORM\IEntityManager;
use OCP\DB\ORM\IEntityRepository;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\IUserSession;
/**
* Controller responsible to set up social
*/
class SetupController extends Controller {
private IUserSession $userSession;
private IEntityManager $entityManager;
private IEntityRepository $accountRepository;
private IURLGenerator $generator;
public function __construct(IRequest $request, IUserSession $userSession, IEntityManager $entityManager, IURLGenerator $generator) {
parent::__construct(App::APP_NAME, $request);
$this->userSession = $userSession;
$this->entityManager = $entityManager;
$this->accountRepository = $entityManager->getRepository(Account::class);
$this->generator = $generator;
}
/**
* Display the account creation page
*
* @NoAdminRequired
* @NoCSRFRequired
*/
public function setupUser(): Response {
$account = $this->accountRepository->findOneBy([
'userId' => $this->userSession->getUser()->getUID(),
]);
if ($account !== null) {
return new RedirectResponse($this->generator->linkToRoute('social.Navigation.timeline'));
}
return new TemplateResponse(App::APP_NAME, 'setup-user');
}
/**
* @NoAdminRequired
*/
public function createAccount(string $userName): DataResponse {
}
}

Wyświetl plik

@ -9,6 +9,7 @@ namespace OCA\Social\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
/**
@ -16,28 +17,48 @@ use Doctrine\ORM\Mapping as ORM;
* @ORM\Table(name="social_account")
*/
class Account {
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
private ?int $id = null;
const REPRESENTATIVE_ID = '-99';
const TYPE_APPLICATION = 'Application';
const TYPE_PERSON = 'Person';
const TYPE_GROUP = 'Group';
const TYPE_ORGANIZATION = 'Organization';
const TYPE_SERVICE = 'Service';
/**
* @ORM\Column(name="user_name", type="string", nullable=false)
* @ORM\Id
* @ORM\Column(type="bigint")
* @ORM\GeneratedValue
*/
private ?string $id = null;
/**
* Username of the user e.g. alice from alice@cloud.social
*
* @ORM\Column(name="user_name", nullable=false)
*/
private string $userName = "";
/**
* @ORM\Column(name="user_id", type="string", nullable=true)
* Internal userId of the user
*
* Only set for local users.
*
* @ORM\Column(name="user_id", nullable=true, unique=true)
*/
private ?string $userId = null;
/**
* Display name: e.g. "Alice Müller"
* @ORM\Column(nullable=true)
*/
private ?string $name = null;
/**
* @ORM\ManyToOne
* @ORM\JoinColumn(name="domain", referencedColumnName="domain", nullable=true)
*/
private ?Instance $instance;
private ?Instance $instance = null;
/**
* @ORM\Column(name="private_key", type="text", nullable=false)
@ -127,7 +148,7 @@ class Account {
/**
* @ORM\Column(type="string", nullable=false)
*/
private string $actorType = "person";
private string $actorType = self::TYPE_PERSON;
/**
* @ORM\Column(nullable=false)
@ -135,31 +156,36 @@ class Account {
private bool $discoverable = true;
/**
* @ORM\OneToMany(targetEntity="Follow", mappedBy="account", fetch="EXTRA_LAZY")
* @ORM\OneToMany(targetEntity="Follow", mappedBy="account", fetch="EXTRA_LAZY", cascade={"persist", "remove"})
* @var Collection<Follow>
*/
private Collection $follow;
/**
* @ORM\OneToMany(targetEntity="Follow", mappedBy="targetAccount", fetch="EXTRA_LAZY")
* @ORM\OneToMany(targetEntity="Follow", mappedBy="targetAccount", fetch="EXTRA_LAZY", cascade={"persist", "remove"})
* @var Collection<Follow>
*/
private Collection $followedBy;
/**
* @ORM\OneToMany(targetEntity="Follow", mappedBy="account", fetch="EXTRA_LAZY")
* @ORM\OneToMany(targetEntity="Follow", mappedBy="account", fetch="EXTRA_LAZY", cascade={"persist", "remove"})
* @var Collection<Block>
*/
private Collection $block;
/**
* @ORM\OneToMany(targetEntity="Follow", mappedBy="targetAccount", fetch="EXTRA_LAZY")
* @ORM\OneToMany(targetEntity="Follow", mappedBy="targetAccount", fetch="EXTRA_LAZY", cascade={"persist", "remove"})
* @var Collection<Block>
*/
private Collection $blockedBy;
private Collection $activeMentions;
/**
* @var ?array{private: string, public: string}
*/
private ?array $keyPair = null;
public function __construct() {
$this->block = new ArrayCollection();
$this->blockedBy = new ArrayCollection();
@ -167,195 +193,269 @@ class Account {
$this->followedBy = new ArrayCollection();
$this->updatedAt = new \DateTime();
$this->createdAt = new \DateTime();
$this->instance = new Instance();
}
public function getId(): int {
static public function newLocal(string $userId = null, string $userName = null, string $displayName = null): self {
$account = new Account();
if ($userId !== null) {
$account->setUserId($userId);
if ($userName !== null) {
$account->setUserName($userName);
} else {
$account->setUserName($userId);
}
if ($displayName !== null) {
$account->setName($displayName);
} else {
$account->setName($account->getUserName());
}
}
$account->generateKeys();
return $account;
}
public function generateKeys(): self {
if (!$this->isLocal() || ($this->publicKey !== '' && $this->privateKey !== '')) {
return $this;
}
$res = openssl_pkey_new([
"digest_alg" => "rsa",
"private_key_bits" => 2048,
"private_key_type" => OPENSSL_KEYTYPE_RSA,
]);
openssl_pkey_export($res, $privateKey);
$publicKey = openssl_pkey_get_details($res)['key'];
$this->setPublicKey($publicKey);
$this->setPrivateKey($privateKey);
return $this;
}
public function getId(): string {
return $this->id;
}
public function getUserId(): string {
public function setRepresentative(): self {
$this->id = self::REPRESENTATIVE_ID;
}
public function getUserId(): ?string {
return $this->userId;
}
public function setUserId(string $userId): void {
public function setUserId(string $userId): self {
$this->userId = $userId;
return $this;
}
public function getUserName(): string {
return $this->userName;
}
public function setUserName(string $userName): void {
public function setUserName(string $userName): self {
$this->userName = $userName;
return $this;
}
public function getInstance(): Instance {
public function getName(): string {
return $this->name;
}
public function setName(string $displayName): self {
$this->name = $displayName;
return $this;
}
public function getInstance(): ?Instance {
return $this->instance;
}
public function setInstance(Instance $instance): void {
public function setInstance(Instance $instance): self {
$this->instance = $instance;
return $this;
}
public function getPrivateKey(): string {
return $this->privateKey;
}
public function setPrivateKey(string $privateKey): void {
public function setPrivateKey(string $privateKey): self {
$this->privateKey = $privateKey;
return $this;
}
public function getPublicKey(): string {
return $this->publicKey;
}
public function setPublicKey(string $publicKey): void {
public function setPublicKey(string $publicKey): self {
$this->publicKey = $publicKey;
return $this;
}
public function getCreatedAt(): \DateTime {
return $this->createdAt;
}
public function setCreatedAt(\DateTime $createdAt): void {
public function setCreatedAt(\DateTime $createdAt): self {
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): \DateTime {
return $this->updatedAt;
}
public function setUpdatedAt(\DateTime $updatedAt): void {
public function setUpdatedAt(\DateTime $updatedAt): self {
$this->updatedAt = $updatedAt;
return $this;
}
public function getUri(): string {
return $this->uri;
}
public function setUri(string $uri): void {
public function setUri(string $uri): self {
$this->uri = $uri;
return $this;
}
public function getUrl(): string {
return $this->url;
}
public function setUrl(string $url): void {
public function setUrl(string $url): self {
$this->url = $url;
return $this;
}
public function isLocked(): bool {
return $this->locked;
}
public function setLocked(bool $locked): void {
public function setLocked(bool $locked): self {
$this->locked = $locked;
return $this;
}
public function getAvatarRemoteUrl(): string {
return $this->avatarRemoteUrl;
}
public function setAvatarRemoteUrl(string $avatarRemoteUrl): void {
public function setAvatarRemoteUrl(string $avatarRemoteUrl): self {
$this->avatarRemoteUrl = $avatarRemoteUrl;
return $this;
}
public function getHeaderRemoteUrl(): string {
return $this->headerRemoteUrl;
}
public function setHeaderRemoteUrl(string $headerRemoteUrl): void {
public function setHeaderRemoteUrl(string $headerRemoteUrl): self {
$this->headerRemoteUrl = $headerRemoteUrl;
return $this;
}
public function getLastWebfingeredAt(): ?\DateTimeInterface {
return $this->lastWebfingeredAt;
}
public function setLastWebfingeredAt(?\DateTimeInterface $lastWebfingeredAt): void {
public function setLastWebfingeredAt(?\DateTimeInterface $lastWebfingeredAt): self {
$this->lastWebfingeredAt = $lastWebfingeredAt;
return $this;
}
public function getInboxUrl(): string {
return $this->inboxUrl;
}
public function setInboxUrl(string $inboxUrl): void {
public function setInboxUrl(string $inboxUrl): self {
$this->inboxUrl = $inboxUrl;
return $this;
}
public function getOutboxUrl(): string {
return $this->outboxUrl;
}
public function setOutboxUrl(string $outboxUrl): void {
public function setOutboxUrl(string $outboxUrl): self {
$this->outboxUrl = $outboxUrl;
return $this;
}
public function getSharedInboxUrl(): string {
return $this->sharedInboxUrl;
}
public function setSharedInboxUrl(string $sharedInboxUrl): void {
public function setSharedInboxUrl(string $sharedInboxUrl): self {
$this->sharedInboxUrl = $sharedInboxUrl;
return $this;
}
public function getFollowersUrl(): string {
return $this->followersUrl;
}
public function setFollowersUrl(string $followersUrl): void {
public function setFollowersUrl(string $followersUrl): self {
$this->followersUrl = $followersUrl;
return $this;
}
public function getProtocol(): string {
return $this->protocol;
}
public function setProtocol(string $protocol): void {
public function setProtocol(string $protocol): self {
$this->protocol = $protocol;
return $this;
}
public function isMemorial(): bool {
return $this->memorial;
}
public function setMemorial(bool $memorial): void {
public function setMemorial(bool $memorial): self {
$this->memorial = $memorial;
return $this;
}
public function getFields(): array {
return $this->fields;
}
public function setFields(array $fields): void {
public function setFields(array $fields): self {
$this->fields = $fields;
return $this;
}
public function getActorType(): string {
return $this->actorType;
}
public function setActorType(string $actorType): void {
public function setActorType(string $actorType): self {
$this->actorType = $actorType;
return $this;
}
public function isDiscoverable(): bool {
return $this->discoverable;
}
public function setDiscoverable(bool $discoverable): void {
public function setDiscoverable(bool $discoverable): self {
$this->discoverable = $discoverable;
return $this;
}
public function getFollow(): Collection {
return $this->follow;
}
public function setFollow(Collection $follow): void {
public function setFollow(Collection $follow): self {
$this->follow = $follow;
return $this;
}
public function getFollowedBy(): Collection {
@ -370,8 +470,9 @@ class Account {
return $this->block;
}
public function setBlock(Collection $block): void {
public function setBlock(Collection $block): self {
$this->block = $block;
return $this;
}
public function getBlockedBy(): Collection {
@ -394,7 +495,36 @@ class Account {
return $this->isLocal() ? $this->getUserName() : $this->getUserName() . '@' . $this->getDomain();
}
public function possiblyStale() {
public function possiblyStale(): bool {
return $this->lastWebfingeredAt === null || $this->lastWebfingeredAt->diff((new \DateTime('now')))->days > 1;
}
/**
* @return Collection<Follow>
*/
public function getFollowersForLocalDistribution(): Collection {
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('', false));
return $this->followedBy->matching($criteria);
}
/**
* Add a new follower to this account
*/
public function addFollower(Account $account): void {
$follow = new Follow();
$follow->setTargetAccount($this);
$follow->setAccount($account);
$this->followedBy->add($follow);
}
/**
* Follow a new account
*/
public function follow(Account $account): void {
$follow = new Follow();
$follow->setAccount($this);
$follow->setTargetAccount($account);
$this->followedBy->add($follow);
}
}

Wyświetl plik

@ -21,29 +21,29 @@ class Follow {
* @ORM\Column(type="bigint")
* @ORM\GeneratedValue
*/
private string $id = "-1";
private ?string $id = null;
/**
* @ORM\Column(name="created_at")
* @ORM\Column(name="created_at", type="datetime", nullable=false)
*/
private DateTimeInterface $createdAt;
private \DateTime $createdAt;
/**
* @ORM\Column(name="updated_at")
* @ORM\Column(name="updated_at", type="datetime", nullable=false)
*/
private DateTimeInterface $updatedAt;
private \DateTime $updatedAt;
/**
* @ORM\ManyToOne
* @ORM\ManyToOne(cascade={"persist", "remove"})
* @ORM\JoinColumn(nullable=false)
*/
private Account $account;
private ?Account $account = null;
/**
* @ORM\ManyToOne
* @ORM\ManyToOne(cascade={"persist", "remove"})
* @ORM\JoinColumn(nullable=false)
*/
private Account $targetAccount;
private ?Account $targetAccount = null;
/**
* @ORM\Column
@ -66,4 +66,64 @@ class Follow {
$this->account = new Account();
$this->targetAccount = new Account();
}
public function getId(): string {
return $this->id;
}
public function getCreatedAt():\DateTimeInterface {
return $this->createdAt;
}
public function setCreatedAt(\DateTime $createdAt): void {
$this->createdAt = $createdAt;
}
public function getUpdatedAt(): \DateTimeInterface {
return $this->updatedAt;
}
public function setUpdatedAt(\DateTime $updatedAt): void {
$this->updatedAt = $updatedAt;
}
public function getAccount(): Account {
return $this->account;
}
public function setAccount(Account $account): void {
$this->account = $account;
}
public function getTargetAccount(): Account {
return $this->targetAccount;
}
public function setTargetAccount(Account $targetAccount): void {
$this->targetAccount = $targetAccount;
}
public function isShowReblogs(): bool {
return $this->showReblogs;
}
public function setShowReblogs(bool $showReblogs): void {
$this->showReblogs = $showReblogs;
}
public function getUri(): string {
return $this->uri;
}
public function setUri(string $uri): void {
$this->uri = $uri;
}
public function isNotify(): bool {
return $this->notify;
}
public function setNotify(bool $notify): void {
$this->notify = $notify;
}
}

Wyświetl plik

@ -21,7 +21,7 @@ class Instance {
private string $domain = "";
/**
* @ORM\Column(type="int")
* @ORM\Column(type="integer")
*/
private int $accountsCount = -1;

Wyświetl plik

@ -144,7 +144,7 @@ class Status {
private array $orderedMediaAttachmentIds = [];
/**
* @ORM\OneToMany
* @ORM\OneToMany(targetEntity="Mention", mappedBy="status")
*/
private Collection $mentions;
@ -355,4 +355,8 @@ class Status {
public function setMentions(Collection $mentions): void {
$this->mentions = $mentions;
}
public function isReblog(): bool {
return $this->reblogOf !== null;
}
}

Wyświetl plik

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Serializer;
use OCA\Social\Entity\Account;
use OCP\IRequest;
use OCP\IUserManager;
class AccountSerializer extends ActivityPubSerializer {
private IRequest $request;
private IUserManager $userManager;
public function __construct(IRequest $request, IUserManager $userManager) {
$this->request = $request;
$this->userManager = $userManager;
}
public function toJsonLd(object $account): array {
assert($account instanceof Account && $account->isLocal());
$user = $this->userManager->get($account->getUserId());
$baseUrl = "https://" . $this->request->getServerHost() . '/';
$baseUserUrl = $baseUrl . "/users/" . $account->getUserName() . '/';
return array_merge($this->getContext(), [
"id" => $baseUrl . $account->getUserName(),
"type" => $account->getActorType(),
"following" => $baseUserUrl . "following",
"followers" => $baseUserUrl . "followers",
"inbox" => $baseUserUrl . "inbox",
"outbox" => $baseUserUrl . "outbox",
"preferredUsername" => $account->getUserName(),
"name" => $user->getDisplayName(),
"publicKey" => [
"id" => $baseUrl . $account->getUserName() . "#main-key",
"owner" => $baseUrl . $account->getUserName(),
"publicKeyPem" => $account->getPublicKey(),
]
]);
}
}

Wyświetl plik

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Serializer;
/**
* @template T
*/
abstract class ActivityPubSerializer {
/**
* @param T $account
* @return array
*/
abstract public function toJsonLd(object $account): array;
protected function getContext(): array {
// Provide namespace information
return [
"@context" => [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
[
"manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
"toot" => "http://joinmastodon.org/ns#",
"featured" => [
"@id" => "toot:featured",
"@type" => "@id",
],
"featuredTags" => [
"@id" => "toot:featuredTags",
"@type" => "@id",
],
"alsoKnownAs" => [
"@id" => "as:alsoKnownAs",
"@type" => "@id",
],
"movedTo" => [
"@id" => "as=>movedTo",
"@type" => "@id"
],
"schema" => "http=>//schema.org#",
"PropertyValue" => "schema:PropertyValue",
"value" => "schema:value",
"discoverable" => "toot:discoverable",
"Device" => "toot:Device",
"Ed25519Signature" => "toot:Ed25519Signature",
"Ed25519Key" => "toot:Ed25519Key",
"Curve25519Key" => "toot:Curve25519Key",
"EncryptedMessage" => "toot:EncryptedMessage",
"publicKeyBase64" => "toot:publicKeyBase64",
"deviceId" => "toot:deviceId",
"claim" => [
"@type" => "@id",
"@id" => "toot:claim"
],
"fingerprintKey" => [
"@type" => "@id",
"@id" => "toot:fingerprintKey"
],
"identityKey" => [
"@type" => "@id",
"@id" => "toot:identityKey"
],
"devices" => [
"@type" => "@id",
"@id" => "toot:devices"
],
"messageFranking" => "toot:messageFranking",
"messageType" => "toot:messageType",
"cipherText" => "toot:cipherText",
"suspended" => "toot:suspended",
"focalPoint" => [
"@container" => "@list",
"@id" => "toot:focalPoint"
]
]
]
];
}
}

Wyświetl plik

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Serializer;
use Psr\Container\ContainerInterface;
class SerializerFactory {
/**
* @template T
* @var array<class-string<T>, class-string<ActivityPubSerializer<T>>>
*/
private array $serializers = [];
private ContainerInterface $container;
public function __construct(ContainerInterface $container) {
$this->container = $container;
}
/**
* @template T
* @param class-string<T> $className
* @param class-string<ActivityPubSerializer<T>> $serializerName
*/
public function registerSerializer(string $className, string $serializerName): void {
$this->serializers[$className] = $serializerName;
}
/**
* @template T
* @param T $object
* @return ActivityPubSerializer<T>
*/
public function getSerializerFor(object $object): ActivityPubSerializer {
return $this->container->get($this->serializers[get_class($object)]);
}
}

Wyświetl plik

@ -1,26 +1,88 @@
<?php
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Service;
use Doctrine\Common\Collections\Collection;
use OCA\Social\Entity\Account;
use OCP\DB\ORM\IEntityManager;
use OCP\DB\ORM\IEntityRepository;
use OCP\IRequest;
use OCP\IUser;
class AccountFinder {
private IEntityManager $entityManager;
private IEntityRepository $repository;
private IRequest $request;
public function __construct(IEntityManager $entityManager) {
public function __construct(IEntityManager $entityManager, IRequest $request) {
$this->entityManager = $entityManager;
$this->repository = $this->entityManager->getRepository(Account::class);
$this->request = $request;
}
public function findRemote(string $userName, ?string $domain): ?Account {
return $this->entityManager->getRepository(Account::class)
->findOneBy([
'domain' => $domain,
'userName' => $userName,
]);
return $this->repository->findOneBy([
'domain' => $domain,
'userName' => $userName,
]);
}
public function findLocal(string $userName): ?Account {
return $this->findRemote($userName, null);
}
public function getAccountByNextcloudId(string $userId): ?Account {
return $this->repository->findOneBy([
'userId' => $userId,
]);
}
public function getCurrentAccount(IUser $user): Account {
$account = $this->getAccountByNextcloudId($user->getUID());
if ($account) {
return $account;
}
$account = Account::newLocal();
$account->setUserName($user->getUID());
$account->setUserId($user->getUID());
$account->setName($user->getDisplayName());
$account->generateKeys();
$this->entityManager->persist($account);
$this->entityManager->flush();
return $account;
}
public function getRepresentative(): Account {
$account = $this->repository->findOneBy([
'id' => Account::REPRESENTATIVE_ID,
]);
if ($account) {
return $account;
}
$account = Account::newLocal();
$account->setRepresentative()
->setActorType(Account::TYPE_APPLICATION)
->setUserName($this->request->getServerHost())
->setUserId('__self')
->setLocked(true)
->generateKeys();
$this->entityManager->persist($account);
$this->entityManager->flush();
return $account;
}
/**
* @param Account $account
* @return array<Account>
*/
public function getLocalFollowersOf(Account $account): array {
echo $this->entityManager->createQuery('SELECT a, f FROM \OCA\Social\Entity\Follow f LEFT JOIN f.account a WHERE f.targetAccount = :target')
->setParameters(['target' => $account])->getSql() . ' ' . $account->getId() ;
return $this->entityManager->createQuery('SELECT f FROM \OCA\Social\Entity\Follow f LEFT JOIN f.account a WHERE f.targetAccount = :target')
->setParameters(['target' => $account])->getArrayResult();
}
}

Wyświetl plik

@ -0,0 +1,49 @@
<?php
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Service\ActivityPub;
use OCA\Social\Entity\Account;
use OCP\IRequest;
class RemoteAccountFetchOption {
public bool $id = true;
public ?string $prefetchedBody = null;
public bool $breakOnRedirect = false;
public bool $onlyKey = false;
static public function default(): self {
return new self();
}
}
class RemoteAccountFetcher {
private IRequest $request;
private TagManager $tagManager;
public function __construct(IRequest $request, TagManager $tagManager) {
$this->request = $request;
$this->tagManager = $tagManager;
}
public function fetch(?string $uri, RemoteAccountFetchOption $fetchOption): ?Account {
if ($this->tagManager->isLocalUri($uri)) {
return $this->tagManager->uriToResource($uri, Account::class);
}
if ($fetchOption->prefetchedBody !== null) {
$json = json_decode($fetchOption->prefetchedBody);
} else {
$json = $this->fetchResource($uri, $fetchOption->id);
}
return null;
}
public function fetchResource() {
}
}

Wyświetl plik

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Service\ActivityPub;
use OCA\Social\Entity\Account;
use OCA\Social\Service\AccountFinder;
use OCA\Social\Tools\Model\NCRequest;
use OCA\Social\Tools\Model\Request;
use OCA\Social\Tools\Model\Uri;
class SignedRequest extends Request {
private ?Account $account = null;
const FORMAT_URI = 'URI';
const FORMAT_ACCOUNT = 'ACCOUNT';
public const DATE_HEADER = 'D, d M Y H:i:s T';
public const DATE_OBJECT = 'Y-m-d\TH:i:s\Z';
private bool $alreadySigned = false;
public function __construct(Uri $url, int $type = 0, bool $binary = false) {
parent::__construct($url, $type, $binary);
}
/**
* @param self::FORMAT_* $keyIdFormat
*/
public function setOnBehalfOf(Account $onBehalfOf): self {
$this->account = $onBehalfOf;
return $this;
}
public function setKeyIdFormat(string $keyIdFormat = self::FORMAT_URI): self {
$this->format = $keyIdFormat;
return $this;
}
public function sign() {
if ($this->alreadySigned) {
throw new \RuntimeException('Trying to sign a request two times');
}
$date = gmdate(self::DATE_HEADER);
$headersElements = ['(request-target)', 'content-length', 'date', 'host', 'digest'];
$allElements = [
'(request-target)' => Request::method($this->getType()) . ' ' . $this->getPath(),
'date' => $date,
'host' => $this->getHost(),
'digest' => $this->generateDigest($this->getDataBody()),
'content-length' => strlen($this->getDataBody())
];
$signing = $this->generateHeaders($headersElements, $allElements);
openssl_sign($signing, $signed, $this->account->getPrivateKey(), OPENSSL_ALGO_SHA256);
$signed = base64_encode($signed);
$signature = $this->generateSignature($headersElements, $this->account->getUserName(), $signed);
$this->addHeader('Signature', $signature);
}
private function generateHeaders(array $elements, array $data): string {
$signingElements = [];
foreach ($elements as $element) {
$signingElements[] = $element . ': ' . $data[$element];
$this->addHeader($element, $data[$element]);
}
return implode("\n", $signingElements);
}
private function generateSignature(array $elements, string $actorId, string $signed): string {
$signatureElements[] = 'keyId="' . $actorId . '#main-key"';
$signatureElements[] = 'algorithm="rsa-sha256"';
$signatureElements[] = 'headers="' . implode(' ', $elements) . '"';
$signatureElements[] = 'signature="' . $signed . '"';
return implode(',', $signatureElements);
}
private function generateDigest(string $data): string {
$encoded = hash("sha256", utf8_encode($data), true);
return 'SHA-256=' . base64_encode($encoded);
}
}

Wyświetl plik

@ -0,0 +1,49 @@
<?php
namespace OCA\Social\Service\ActivityPub;
use OCA\Social\Entity\Account;
use OCA\Social\Tools\Model\NCRequest;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\ICache;
use OCP\ICacheFactory;
/**
* Service responsible for fetching and caching JSON-Ld activity stream
*/
class JsonLdService {
private IClient $client;
private ICache $jsonLdCache;
public function __construct(ICacheFactory $cacheFactory, IClientService $clientService) {
$this->client = $clientService->newClient();
$this->jsonLdCache = $cacheFactory->createLocal('social.jsonld');
}
public function fetchResource(string $uri, bool $id, ?Account $onBehalfOf = null) {
if (!$id) {
$json = $this->fetchResourceWithoutIdValidation($uri, $onBehalfOf);
}
}
private function fetchResourceWithoutIdValidation(string $uri, ?Account $onBehalfOf): array {
$this->client->get($uri, [
'header' => [
'Accept' => 'application/activity+json, application/ld+json',
],
]);
}
public function onBehalfOf(Account $account, $keyIdFormat, string $signWith): array {
return [];
}
private function buildRequest(?Account $onBehalfOf): SignedRequest {
$request = new SignedRequest();
$request->setOnBehalfOf($onBehalfOf);
$request->addHeader('Accept', 'application/activity+json, application/ld+json');
return $request;
}
}

Wyświetl plik

@ -0,0 +1,53 @@
<?php
namespace OCA\Social\Service\ActivityPub;
use OCA\Social\Entity\Account;
use OCA\Social\Entity\Status;
use OCP\IRequest;
class TagManager {
private IRequest $request;
public function __construct(IRequest $request) {
$this->request = $request;
}
/**
* @template T
* @param class-string<T> $className
* @return ?T
*/
public function uriToResource(string $uri, string $className): object {
if ($this->isLocalUri($uri)) {
// Find resource but from the DB
switch ($className) {
case Account::class:
return null; // TODO
case Status::class:
return null; // TODO
return null;
}
} else {
// Find remote resource
}
}
public function isLocalUri(?string $uri) {
if ($uri === null) {
return false;
}
$parsedUrl = parse_url($uri);
if (!isset($parsedUrl['host'])) {
return false;
}
$host = $parsedUrl['host'];
if (isset($parsedUrl['port'])) {
$host = $host . ':' . $parsedUrl['port'];
}
return $host === $this->request->getServerHost();
}
}

Wyświetl plik

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Service;
use OCA\Social\Entity\Status;
/**
* This service handle storing and retrieving the feeds from Redis
*/
class FeedManager {
/**
* Number of items in the feed since last reblog of status
* before the new reblog will be inserted. Must be <= MAX_ITEMS
* or the tracking sets will grow forever
*/
const REBLOG_FALLOFF = 40;
const HOME_FEED = "home";
private IFeedProvider $feedProvider;
public function __construct(IFeedProvider $feedProvider) {
$this->feedProvider = $feedProvider;
}
public function addToHome(string $accountId, Status $status): bool {
return $this->addToFeed(self::HOME_FEED, $accountId, $status);
}
public function addToFeed(string $timelineType, string $accountId, Status $status, bool $aggregateReblog = true): bool {
return $this->feedProvider->addToFeed($timelineType, $accountId, $status, $aggregateReblog);
}
public function removeFromFeed(string $timelineType, string $accountId, Status $status, bool $aggregateReblog = true): bool {
return $this->feedProvider->removeFromFeed($timelineType, $accountId, $status, $aggregateReblog);
}
}

Wyświetl plik

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Service;
use OCA\Social\Entity\Status;
interface IFeedProvider {
public function addToFeed(string $timelineType, string $accountId, Status $status, bool $aggregateReblog = true): bool;
public function removeFromFeed(string $timelineType, string $accountId, Status $status, bool $aggregateReblog = true): bool;
}

Wyświetl plik

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Service;
use OCA\Social\Entity\Account;
use OCA\Social\Entity\Mention;
use OCA\Social\Entity\Status;
class PostDeliveryService {
private FeedManager $feedManager;
private AccountFinder $accountFinder;
public function __construct(FeedManager $feedManager, AccountFinder $accountFinder) {
$this->feedManager = $feedManager;
$this->accountFinder = $accountFinder;
}
public function run(Account $author, Status $status): void {
// deliver to self
if ($status->isLocal()) {
$this->feedManager->addToHome($author->getId(), $status);
}
// deliver to mentioned accounts
$localFollowers = $this->accountFinder->getLocalFollowersOf($author);
$status->getActiveMentions()->forAll(function (Mention $mention) use ($status): void{
if ($mention->getAccount()->isLocal()) {
$this->deliverLocalAccount($status, $mention->getAccount());
}
});
// deliver to local followers
$localFollowers->forAll(function (Account $account) use ($status): void {
$this->deliverLocalAccount($status, $account);
});
}
public function deliverLocalAccount(Status $status, Account $account) {
assert($account->isLocal());
// TODO create notification
$this->feedManager->addToHome($account->getId(), $status);
}
}

Wyświetl plik

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Service\Feed;
use OC\RedisFactory;
use OCA\Social\Entity\Status;
use OCA\Social\Service\FeedManager;
use OCA\Social\Service\IFeedProvider;
class RedisFeedProvider implements IFeedProvider {
private \Redis $redis;
public function __construct(RedisFactory $redisFactory) {
$this->redis = $redisFactory->getInstance();
}
private function key(string $feedName, string $accountId, ?string $subType = null) {
if ($subType === null) {
return 'feed:' . $feedName . ':' . $accountId;
}
return 'feed:' . $feedName . ':' . $accountId . ':' . $subType;
}
public function addToFeed(string $timelineType, string $accountId, Status $status, bool $aggregateReblog = true): bool {
$timelineKey = $this->key($timelineType, $accountId);
$reblogKey = $this->key($timelineType, $accountId, 'reblogs');
if ($status->isReblog() !== null && $aggregateReblog) {
$rank = $this->redis->zRevRank($timelineKey, $status->getReblogOf()->getId());
if ($rank !== null && $rank < FeedManager::REBLOG_FALLOFF) {
return false;
}
if ($this->redis->zAdd($reblogKey, ['NX'], $status->getId(), $status->getReblogOf()->getId())) {
$this->redis->zAdd($timelineKey, $status->getId(), $status->getReblogOf()->getId());
} else {
$reblogSetKey = $this->key($timelineType, $accountId, 'reblogs:' . $status->getReblogOf()->getId());
$this->redis->sAdd($reblogSetKey, $status->getId());
return false;
}
} else {
if ($this->redis->zScore($reblogKey, $status->getId()) === false) {
return false;
}
$this->redis->zAdd($timelineKey, $status->getId(), $status->getId());
}
return true;
}
public function removeFromFeed(string $timelineType, string $accountId, Status $status, bool $aggregateReblog = true): bool {
return false;
}
}

Wyświetl plik

@ -17,17 +17,29 @@ class PostServiceStatus {
private ICache $idempotenceCache;
private IConfig $config;
private IEntityManager $entityManager;
private ProcessMentionsService $mentionsService;
private FeedManager $feedManager;
public function __construct(ICacheFactory $cacheFactory, IConfig $config, IEntityManager $entityManager) {
public function __construct(
ICacheFactory $cacheFactory,
IConfig $config,
IEntityManager $entityManager,
ProcessMentionsService $mentionsService,
FeedManager $feedManager
) {
$this->idempotenceCache = $cacheFactory->createDistributed('social.idempotence');
$this->config = $config;
$this->entityManager = $entityManager;
$this->mentionsService = $mentionsService;
$this->feedManager = $feedManager;
}
/**
* @psalm-param array{?text: string, ?spoilerText: string, ?sensitive: bool, ?visibility: Status::STATUS_*} $options
*/
public function create(Account $account, array $options): void {
$this->checkIdempotenceDuplicate($account, $options);
$status = new Status();
$status->setText($options['text'] ?? '');
$status->setSensitive(isset($options['spoilerText'])
@ -44,10 +56,16 @@ class PostServiceStatus {
throw new ApiException('Invalid visibility');
}
// Add mentioned user to CC
$this->mentionsService->run($status);
// Save status
$this->entityManager->persist($account);
$this->entityManager->flush();
$this->updateIdempotency($status);
$this->sendStatus($status);
$this->updateIdempotency($account, $status);
}
private function idempotencyKey(Account $account, string $idempotency): string {
@ -64,11 +82,16 @@ class PostServiceStatus {
}
}
private function updateIdempotency(Status $id): void {
private function updateIdempotency(Account $account, Status $status): void {
if (!isset($options['idempotency'])) {
return;
}
$this->idempotenceCache->set($this->idempotencyKey($account, $options['idempotency']), $status->getId(), 3600);
}
public function sendStatus(Account $account, Status $status): void {
// to self
$this->feedManager->addToHome($account->getId(), $status);
}
}

Wyświetl plik

@ -2,30 +2,48 @@
namespace OCA\Social\Service;
use OCA\Circles\Tools\Model\NCWebfinger;
use OCA\Social\Entity\Account;
use OCA\Social\Service\ActivityPub\RemoteAccountFetcher;
use OCA\Social\Service\ActivityPub\RemoteAccountFetchOption;
use OCP\Http\Client\IClientService;
use OCP\IRequest;
class AccountResolverOption {
/**
* @var bool Whether we should follow webfinger redirection
*/
public bool $followWebfingerRediction = true;
public bool $followWebfingerRedirection = true;
/**
* @var bool Whether we should attempt to fetch the account from webfinger
*/
public bool $queryWebfinger = true;
static public function default(): AccountResolverOption {
return new AccountResolverOption();
static public function default(): self {
return new self();
}
}
class ResolveAccountService {
private IClientService $clientService;
private IRequest $request;
private TrustedDomainChecker $trustedDomainChecker;
private AccountFinder $accountFinder;
private RemoteAccountFetcher $remoteAccountFetcher;
public function __construct(IClientService $clientService) {
public function __construct(
IClientService $clientService,
IRequest $request,
TrustedDomainChecker $trustedDomainChecker,
AccountFinder $accountFinder,
RemoteAccountFetcher $remoteAccountFetcher
) {
$this->clientService = $clientService;
$this->request = $request;
$this->trustedDomainChecker = $trustedDomainChecker;
$this->accountFinder = $accountFinder;
$this->remoteAccountFetcher = $remoteAccountFetcher;
}
/**
@ -39,28 +57,45 @@ class ResolveAccountService {
return null;
}
[$confirmedUserName, $confirmedDomain] = $webFinger;
[$confirmedUserName, $confirmedDomain] = $webFinger->getSubject();
if ($confirmedDomain !== $domain || $confirmedUserName !== $userName) {
$webFinger = $this->requestWebfinger($confirmedUserName, $confirmedDomain);
if (!$option->followWebfingerRedirection) {
return null;
}
$webFinger = $this->requestWebfinger($confirmedUserName, $confirmedDomain);
if ($webFinger === null) {
return null;
}
[$newConfirmedUserName, $newConfirmedDomain] = $webFinger;
[$newConfirmedUserName, $newConfirmedDomain] = $webFinger->getSubject();
if ($confirmedDomain !== $newConfirmedDomain || $confirmedUserName !== $newConfirmedUserName) {
// Hijack attempt
return null;
}
$confirmedDomain = $newConfirmedDomain;
$confirmedUserName = $newConfirmedUserName;
}
// TODO
if ($confirmedDomain === $this->request->getServerHost()) {
$confirmedDomain = null;
}
return null;
if ($this->trustedDomainChecker->check($confirmedDomain)) {
return null; // blocked
}
$account = $this->accountFinder->findRemote($userName, $domain);
if ($account !== null && ($account->isLocal() || !$account->possiblyStale())) {
return $account;
}
return $this->fetchAccount($webFinger);
}
private function requestWebfinger($userName, $domain): ?array {
private function requestWebfinger($userName, $domain): ?NCWebfinger {
$client = $this->clientService->newClient();
$uri = 'acct:' . $userName . '@' . $domain;
@ -81,12 +116,11 @@ class ResolveAccountService {
}
try {
$webFinger = json_decode($response->getBody());
$subject = $webFinger['subject'];
[$confirmedUserName, $confirmedDomain] = explode('@');
$webFinger = new NCWebfinger(json_decode($response->getBody()));
} catch (\Exception $e) {
return null;
}
return $webFinger;
}
public function resolveAccount(Account $account, AccountResolverOption $option): ?Account {
@ -96,5 +130,14 @@ class ResolveAccountService {
return $account;
}
public function requestWebfinger():
public function fetchAccount(NCWebfinger $webfinger): ?Account {
// TODO lock
$actorUrl = $webfinger->getLink('self');
if (!$actorUrl) {
return null;
}
return $this->remoteAccountFetcher->fetch($actorUrl, RemoteAccountFetchOption::default());
}
}

Wyświetl plik

@ -30,6 +30,7 @@ declare(strict_types=1);
namespace OCA\Social\Service;
use OCA\Social\Entity\Account;
use OCA\Social\Tools\Exceptions\DateTimeException;
use OCA\Social\Tools\Exceptions\MalformedArrayException;
use OCA\Social\Tools\Exceptions\RequestContentException;
@ -43,8 +44,6 @@ use DateTime;
use Exception;
use JsonLdException;
use OCA\Social\AppInfo\Application;
use OCA\Social\Db\ActorsRequest;
use OCA\Social\Exceptions\ActorDoesNotExistException;
use OCA\Social\Exceptions\InvalidOriginException;
use OCA\Social\Exceptions\InvalidResourceException;
use OCA\Social\Exceptions\ItemUnknownException;
@ -77,31 +76,7 @@ class SignatureService {
public const DATE_HEADER = 'D, d M Y H:i:s T';
public const DATE_OBJECT = 'Y-m-d\TH:i:s\Z';
public const DATE_DELAY = 300;
private CacheActorService $cacheActorService;
private ActorsRequest $actorsRequest;
private CurlService $curlService;
private ConfigService $configService;
private MiscService $miscService;
public function __construct(
ActorsRequest $actorsRequest, CacheActorService $cacheActorService,
CurlService $curlService,
ConfigService $configService, MiscService $miscService
) {
$this->actorsRequest = $actorsRequest;
$this->cacheActorService = $cacheActorService;
$this->curlService = $curlService;
$this->configService = $configService;
$this->miscService = $miscService;
}
/**
* @param Person $actor
*/
public function generateKeys(Person &$actor) {
public function generateKeys(Account $account): void {
$res = openssl_pkey_new(
[
"digest_alg" => "rsa",
@ -113,19 +88,11 @@ class SignatureService {
openssl_pkey_export($res, $privateKey);
$publicKey = openssl_pkey_get_details($res)['key'];
$actor->setPublicKey($publicKey);
$actor->setPrivateKey($privateKey);
$account->setPublicKey($publicKey);
$account->setPrivateKey($privateKey);
}
/**
* @param NCRequest $request
* @param RequestQueue $queue
*
* @throws ActorDoesNotExistException
* @throws SocialAppConfigException // TODO: implement in TNCRequest ?
*/
public function signRequest(NCRequest $request, RequestQueue $queue): void {
public function signRequest(SignedRequest $request): void {
$date = gmdate(self::DATE_HEADER);
$path = $queue->getInstance();
@ -133,7 +100,7 @@ class SignatureService {
$headersElements = ['(request-target)', 'content-length', 'date', 'host', 'digest'];
$allElements = [
'(request-target)' => 'post ' . $path->getPath(),
'(request-target)' => 'path' . $request->getPath(),
'date' => $date,
'host' => $path->getAddress(),
'digest' => $this->generateDigest($request->getDataBody()),

Wyświetl plik

@ -0,0 +1,11 @@
<?php
namespace OCA\Social\Service;
class TrustedDomainChecker {
public function check(string $domain): bool {
// TODO extends to optionally support federation trusted domain list
// and social domain block list
return true;
}
}

Wyświetl plik

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Settings;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Settings\ISettings;
class Personal implements ISettings {
public function getForm(): TemplateResponse {
return new TemplateResponse('social', 'settings-personal');
}
public function getSection(): string {
return 'social';
}
public function getPriority(): int {
return 99;
}
}

Wyświetl plik

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Settings;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class PersonalSection implements IIconSection {
private IURLGenerator $url;
private IL10N $l;
public function __construct(IURLGenerator $generator, IL10N $l) {
$this->url = $generator;
$this->l = $l;
}
public function getID() {
return 'social';
}
public function getName() {
return $this->l->t('Social');
}
public function getPriority() {
return 99;
}
public function getIcon(): string {
return $this->url->imagePath('social', 'social-dark.svg');
}
}

Wyświetl plik

@ -0,0 +1,6 @@
<?php
namespace OCA\Social\Tools\Exceptions;
class MalformedUriException extends \Exception {
}

Wyświetl plik

@ -31,54 +31,15 @@ declare(strict_types=1);
namespace OCA\Social\Tools\Model;
use OCP\Http\Client\IClient;
/**
* Class NCRequest
*
* @package OCA\Social\Tools\Model
*/
class NCRequest extends Request {
private IClient $client;
private array $clientOptions = [];
private bool $localAddressAllowed = false;
private ?Account $onBehalfOf = null;
public function setClient(IClient $client): self {
$this->client = $client;
return $this;
public function getOnBehalfOf(): ?Account {
return $this->onBehalfOf;
}
public function getClient(): IClient {
return $this->client;
public function setOnBehalfOf(?Account $onBehalfOf): void {
$this->onBehalfOf = $onBehalfOf;
}
public function getClientOptions(): array {
return $this->clientOptions;
}
public function setClientOptions(array $clientOptions): self {
$this->clientOptions = $clientOptions;
return $this;
}
public function isLocalAddressAllowed(): bool {
return $this->localAddressAllowed;
}
public function setLocalAddressAllowed(bool $allowed): self {
$this->localAddressAllowed = $allowed;
return $this;
}
public function jsonSerialize(): array {
return array_merge(
parent::jsonSerialize(),
[
'clientOptions' => $this->getClientOptions(),
'localAddressAllowed' => $this->isLocalAddressAllowed(),
]
);
}
}

Wyświetl plik

@ -33,6 +33,7 @@ namespace OCA\Social\Tools\Model;
use OCA\Social\Tools\Traits\TArrayTools;
use JsonSerializable;
use OCP\Http\Client\IClient;
/**
* Class Request
@ -42,211 +43,96 @@ use JsonSerializable;
class Request implements JsonSerializable {
use TArrayTools;
public const TYPE_GET = 0;
public const TYPE_POST = 1;
public const TYPE_PUT = 2;
public const TYPE_DELETE = 3;
public const QS_VAR_DUPLICATE = 1;
public const QS_VAR_ARRAY = 2;
private string $protocol = '';
private array $protocols = ['https'];
private string $host = '';
private int $port = 0;
private ?Uri $url;
private string $baseUrl = '';
private int $type = 0;
private bool $binary = false;
private bool $verifyPeer = true;
private bool $httpErrorsAllowed = false;
private bool $followLocation = true;
private array $headers = [];
private array $cookies = [];
private array $params = [];
private array $data = [];
private int $queryStringType = self::QS_VAR_DUPLICATE;
private int $timeout = 10;
private string $userAgent = '';
private int $resultCode = 0;
private string $contentType = '';
private IClient $client;
/** @var string */
private $protocol = '';
/** @var array */
private $protocols = ['https'];
/** @var string */
private $host = '';
/** @var int */
private $port = 0;
/** @var string */
private $url = '';
/** @var string */
private $baseUrl = '';
/** @var int */
private $type = 0;
/** @var bool */
private $binary = false;
/** @var bool */
private $verifyPeer = true;
/** @var bool */
private $httpErrorsAllowed = false;
/** @var bool */
private $followLocation = true;
/** @var array */
private $headers = [];
/** @var array */
private $cookies = [];
/** @var array */
private $params = [];
/** @var array */
private $data = [];
/** @var int */
private $queryStringType = self::QS_VAR_DUPLICATE;
/** @var int */
private $timeout = 10;
/** @var string */
private $userAgent = '';
/** @var int */
private $resultCode = 0;
/** @var string */
private $contentType = '';
/**
* Request constructor.
*
* @param string $url
* @param int $type
* @param bool $binary
*/
public function __construct(string $url = '', int $type = 0, bool $binary = false) {
public function __construct(?Uri $url = null, int $type = 0, bool $binary = false) {
$this->url = $url;
$this->type = $type;
$this->binary = $binary;
}
/**
* @param string $protocol
*
* @return Request
*/
public function setProtocol(string $protocol): Request {
$this->protocols = [$protocol];
public function setClient(IClient $client): self {
$this->client = $client;
return $this;
}
/**
* @param array $protocols
*
* @return Request
*/
public function setProtocols(array $protocols): Request {
$this->protocols = $protocols;
public function getClient(): IClient {
return $this->client;
}
public function setProtocol(string $protocol): self {
$this->protocol = $protocol;
return $this;
}
/**
* @return string[]
*/
public function getProtocols(): array {
return $this->protocols;
}
/**
* @return string
*/
public function getUsedProtocol(): string {
public function getProtocol(): string {
return $this->protocol;
}
/**
* @param string $protocol
*
* @return Request
*/
public function setUsedProtocol(string $protocol): Request {
$this->protocol = $protocol;
return $this;
}
/**
* @return string
* @deprecated - 19 - use getHost();
*/
public function getAddress(): string {
return $this->getHost();
}
/**
* @param string $address
*
* @return Request
* @deprecated - 19 - use setHost();
*/
public function setAddress(string $address): Request {
$this->setHost($address);
return $this;
}
/**
* @return string
*/
public function getHost(): string {
return $this->host;
return $this->url->getHost();
}
/**
* @param string $host
*
* @return Request
*/
public function setHost(string $host): Request {
$this->host = $host;
public function setHost(string $host): self {
$this->url->setHost($host);
return $this;
}
/**
* @return int
*/
public function getPort(): int {
return $this->port;
public function getPort(): ?int {
return $this->url->getPort();
}
/**
* @param int $port
*
* @return Request
*/
public function setPort(int $port): Request {
$this->port = $port;
public function setPort(?int $port): self {
$this->url->setPort($port);
return $this;
}
/**
* @param string $instance
* Set the instance
*
* @return Request
* @param string $instance The instance for example floss.social, cloud.com:442, 4u3849u3.onion
* @return $this
*/
public function setInstance(string $instance): Request {
public function setInstance(string $instance): self {
$this->setPort(null);
if (strpos($instance, ':') === false) {
$this->setHost($instance);
return $this;
}
list($host, $port) = explode(':', $instance, 2);
[$host, $port] = explode(':', $instance, 2);
$this->setHost($host);
if ($port !== '') {
$this->setPort((int)$port);
@ -255,63 +141,20 @@ class Request implements JsonSerializable {
return $this;
}
/**
* @return string
*/
public function getInstance(): string {
$instance = $this->getHost();
if ($this->getPort() > 0) {
if ($this->getPort() !== null) {
$instance .= ':' . $this->getPort();
}
return $instance;
}
/**
* @param string $url
*
* @deprecated - 19 - use basedOnUrl();
*/
public function setAddressFromUrl(string $url) {
$this->basedOnUrl($url);
public function parse(string $url): void {
$this->url = new Uri($url);
}
/**
* @param string $url
*/
public function basedOnUrl(string $url) {
$protocol = parse_url($url, PHP_URL_SCHEME);
if ($protocol === null) {
if (strpos($url, '/') > -1) {
list($address, $baseUrl) = explode('/', $url, 2);
$this->setBaseUrl('/' . $baseUrl);
} else {
$address = $url;
}
if (strpos($address, ':') > -1) {
list($address, $port) = explode(':', $address, 2);
$this->setPort((int)$port);
}
$this->setHost($address);
} else {
$this->setProtocols([$protocol]);
$this->setUsedProtocol($protocol);
$this->setHost(parse_url($url, PHP_URL_HOST));
$this->setBaseUrl(parse_url($url, PHP_URL_PATH));
if (is_numeric($port = parse_url($url, PHP_URL_PORT))) {
$this->setPort($port);
}
}
}
/**
* @param string|null $baseUrl
*
* @return Request
*/
public function setBaseUrl(?string $baseUrl): Request {
public function setBaseUrl(?string $baseUrl): self {
if ($baseUrl !== null) {
$this->baseUrl = $baseUrl;
}
@ -319,92 +162,40 @@ class Request implements JsonSerializable {
return $this;
}
/**
* @return bool
*/
public function isBinary(): bool {
return $this->binary;
}
/**
* @param bool $verifyPeer
*
* @return $this
*/
public function setVerifyPeer(bool $verifyPeer): Request {
public function setVerifyPeer(bool $verifyPeer): self {
$this->verifyPeer = $verifyPeer;
return $this;
}
/**
* @return bool
*/
public function isVerifyPeer(): bool {
return $this->verifyPeer;
}
/**
* @param bool $httpErrorsAllowed
*
* @return Request
*/
public function setHttpErrorsAllowed(bool $httpErrorsAllowed): Request {
public function setHttpErrorsAllowed(bool $httpErrorsAllowed): self {
$this->httpErrorsAllowed = $httpErrorsAllowed;
return $this;
}
/**
* @return bool
*/
public function isHttpErrorsAllowed(): bool {
return $this->httpErrorsAllowed;
}
/**
* @param bool $followLocation
*
* @return $this
*/
public function setFollowLocation(bool $followLocation): Request {
public function setFollowLocation(bool $followLocation): self {
$this->followLocation = $followLocation;
return $this;
}
/**
* @return bool
*/
public function isFollowLocation(): bool {
return $this->followLocation;
}
/**
* @return string
* @deprecated - 19 - use getParametersUrl() + addParam()
*/
public function getParsedUrl(): string {
$url = $this->getPath();
$ak = array_keys($this->getData());
foreach ($ak as $k) {
if (!is_string($this->data[$k])) {
continue;
}
$url = str_replace(':' . $k, $this->data[$k], $url);
}
return $url;
}
/**
* @return string
*/
public function getParametersUrl(): string {
$url = $this->getPath();
$ak = array_keys($this->getParams());
@ -419,43 +210,19 @@ class Request implements JsonSerializable {
return $url;
}
/**
* @return string
*/
public function getPath(): string {
return $this->baseUrl . $this->url;
}
/**
* @return string
* @deprecated - 19 - use getPath()
*/
public function getUrl(): string {
return $this->getPath();
}
/**
* @return string
*/
public function getCompleteUrl(): string {
$port = ($this->getPort() > 0) ? ':' . $this->getPort() : '';
return $this->getUsedProtocol() . '://' . $this->getHost() . $port . $this->getParametersUrl();
return (string)$this->url;
}
/**
* @return int
*/
public function getType(): int {
return $this->type;
}
public function addHeader($key, $value): Request {
public function addHeader($key, $value): self {
$header = $this->get($key, $this->headers);
if ($header !== '') {
$header .= ', ' . $value;
@ -474,213 +241,92 @@ class Request implements JsonSerializable {
public function getHeaders(): array {
return array_merge(['User-Agent' => $this->getUserAgent()], $this->headers);
}
/**
* @param array $headers
*
* @return Request
*/
public function setHeaders(array $headers): Request {
public function setHeaders(array $headers): self {
$this->headers = $headers;
return $this;
}
/**
* @return array
*/
public function getCookies(): array {
return $this->cookies;
}
/**
* @param array $cookies
*
* @return Request
*/
public function setCookies(array $cookies): Request {
public function setCookies(array $cookies): self {
$this->cookies = $cookies;
return $this;
}
/**
* @param int $queryStringType
*
* @return Request
*/
public function setQueryStringType(int $queryStringType): self {
$this->queryStringType = $queryStringType;
return $this;
}
/**
* @return int
*/
public function getQueryStringType(): int {
return $this->queryStringType;
}
/**
* @return array
*/
public function getData(): array {
return $this->data;
}
/**
* @param array $data
*
* @return Request
*/
public function setData(array $data): Request {
public function setData(array $data): self {
$this->data = $data;
return $this;
}
/**
* @param string $data
*
* @return Request
*/
public function setDataJson(string $data): Request {
public function setDataJson(string $data): self {
$this->setData(json_decode($data, true));
return $this;
}
/**
* @param JsonSerializable $data
*
* @return Request
*/
public function setDataSerialize(JsonSerializable $data): Request {
public function setDataSerialize(JsonSerializable $data): self {
$this->setDataJson(json_encode($data));
return $this;
}
/**
* @return array
*/
public function getParams(): array {
return $this->params;
}
/**
* @param array $params
*
* @return Request
*/
public function setParams(array $params): Request {
public function setParams(array $params): self {
$this->params = $params;
return $this;
}
/**
* @param string $k
* @param string $v
*
* @return Request
*/
public function addParam(string $k, string $v): Request {
public function addParam(string $k, string $v): self {
$this->params[$k] = $v;
return $this;
}
/**
* @param string $k
* @param int $v
*
* @return Request
*/
public function addParamInt(string $k, int $v): Request {
public function addParamInt(string $k, int $v): self {
$this->params[$k] = $v;
return $this;
}
/**
* @param string $k
* @param string $v
*
* @return Request
*/
public function addData(string $k, string $v): Request {
public function addData(string $k, string $v): self {
$this->data[$k] = $v;
return $this;
}
/**
* @param string $k
* @param int $v
*
* @return Request
*/
public function addDataInt(string $k, int $v): Request {
public function addDataInt(string $k, int $v): self {
$this->data[$k] = $v;
return $this;
}
/**
* @return string
*/
public function getDataBody(): string {
return json_encode($this->getData());
}
/**
* @return string
* @deprecated - 19 - use getUrlParams();
*/
public function getUrlData(): string {
if ($this->getData() === []) {
return '';
}
return preg_replace(
'/([(%5B)]{1})[0-9]+([(%5D)]{1})/', '$1$2', http_build_query($this->getData())
);
}
/**
* @return string
* @deprecated - 21 - use getQueryString();
*/
public function getUrlParams(): string {
if ($this->getParams() === []) {
return '';
}
return preg_replace(
'/([(%5B)]{1})[0-9]+([(%5D)]{1})/', '$1$2', http_build_query($this->getParams())
);
}
/**
* @param int $type
*
* @return string
*/
public function getQueryString(): string {
if (empty($this->getParams())) {
return '';
@ -698,90 +344,48 @@ class Request implements JsonSerializable {
}
}
/**
* @return int
*/
public function getTimeout(): int {
return $this->timeout;
}
/**
* @param int $timeout
*
* @return Request
*/
public function setTimeout(int $timeout): Request {
public function setTimeout(int $timeout): self {
$this->timeout = $timeout;
return $this;
}
/**
* @return string
*/
public function getUserAgent(): string {
return $this->userAgent;
}
/**
* @param string $userAgent
*
* @return Request
*/
public function setUserAgent(string $userAgent): Request {
public function setUserAgent(string $userAgent): self {
$this->userAgent = $userAgent;
return $this;
}
/**
* @return int
*/
public function getResultCode(): int {
return $this->resultCode;
}
/**
* @param int $resultCode
*
* @return Request
*/
public function setResultCode(int $resultCode): Request {
public function setResultCode(int $resultCode): self {
$this->resultCode = $resultCode;
return $this;
}
/**
* @return string
*/
public function getContentType(): string {
return $this->contentType;
}
/**
* @param string $contentType
*
* @return Request
*/
public function setContentType(string $contentType): Request {
public function setContentType(string $contentType): self {
$this->contentType = $contentType;
return $this;
}
/**
* @return array
*/
public function jsonSerialize(): array {
return [
'protocols' => $this->getProtocols(),
'used_protocol' => $this->getUsedProtocol(),
'port' => $this->getPort(),
'host' => $this->getHost(),
'url' => $this->getPath(),
@ -798,12 +402,6 @@ class Request implements JsonSerializable {
];
}
/**
* @param string $type
*
* @return int
*/
public static function type(string $type): int {
switch (strtoupper($type)) {
case 'GET':
@ -819,7 +417,6 @@ class Request implements JsonSerializable {
return 0;
}
public static function method(int $type): string {
switch ($type) {
case self::TYPE_GET:

Wyświetl plik

@ -0,0 +1,910 @@
<?php
namespace OCA\Social\Tools\Model;
use OCA\Social\Tools\Exceptions\MalformedUriException;
use Psr\Http\Message\UriInterface;
class UriResolver {
/**
* Removes dot segments from a path and returns the new path.
*
* @link http://tools.ietf.org/html/rfc3986#section-5.2.4
*/
public static function removeDotSegments(string $path): string
{
if ($path === '' || $path === '/') {
return $path;
}
$results = [];
$segments = explode('/', $path);
foreach ($segments as $segment) {
if ($segment === '..') {
array_pop($results);
} elseif ($segment !== '.') {
$results[] = $segment;
}
}
$newPath = implode('/', $results);
if ($path[0] === '/' && (!isset($newPath[0]) || $newPath[0] !== '/')) {
// Re-add the leading slash if necessary for cases like "/.."
$newPath = '/' . $newPath;
} elseif ($newPath !== '' && ($segment === '.' || $segment === '..')) {
// Add the trailing slash if necessary
// If newPath is not empty, then $segment must be set and is the last segment from the foreach
$newPath .= '/';
}
return $newPath;
}
/**
* Converts the relative URI into a new URI that is resolved against the base URI.
*
* @link http://tools.ietf.org/html/rfc3986#section-5.2
*/
public static function resolve(UriInterface $base, UriInterface $rel): UriInterface {
if ((string) $rel === '') {
// we can simply return the same base URI instance for this same-document reference
return $base;
}
if ($rel->getScheme() != '') {
return $rel->withPath(self::removeDotSegments($rel->getPath()));
}
if ($rel->getAuthority() != '') {
$targetAuthority = $rel->getAuthority();
$targetPath = self::removeDotSegments($rel->getPath());
$targetQuery = $rel->getQuery();
} else {
$targetAuthority = $base->getAuthority();
if ($rel->getPath() === '') {
$targetPath = $base->getPath();
$targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery();
} else {
if ($rel->getPath()[0] === '/') {
$targetPath = $rel->getPath();
} else {
if ($targetAuthority != '' && $base->getPath() === '') {
$targetPath = '/' . $rel->getPath();
} else {
$lastSlashPos = strrpos($base->getPath(), '/');
if ($lastSlashPos === false) {
$targetPath = $rel->getPath();
} else {
$targetPath = substr($base->getPath(), 0, $lastSlashPos + 1) . $rel->getPath();
}
}
}
$targetPath = self::removeDotSegments($targetPath);
$targetQuery = $rel->getQuery();
}
}
return new Uri(Uri::composeComponents(
$base->getScheme(),
$targetAuthority,
$targetPath,
$targetQuery,
$rel->getFragment()
));
}
/**
* Returns the target URI as a relative reference from the base URI.
*
* This method is the counterpart to resolve():
*
* (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target))
*
* One use-case is to use the current request URI as base URI and then generate relative links in your documents
* to reduce the document size or offer self-contained downloadable document archives.
*
* $base = new Uri('http://example.com/a/b/');
* echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'.
* echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'.
* echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'.
* echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'.
*
* This method also accepts a target that is already relative and will try to relativize it further. Only a
* relative-path reference will be returned as-is.
*
* echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well
*/
public static function relativize(UriInterface $base, UriInterface $target): UriInterface
{
if ($target->getScheme() !== '' &&
($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '')
) {
return $target;
}
if (Uri::isRelativePathReference($target)) {
// As the target is already highly relative we return it as-is. It would be possible to resolve
// the target with `$target = self::resolve($base, $target);` and then try make it more relative
// by removing a duplicate query. But let's not do that automatically.
return $target;
}
if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) {
return $target->withScheme('');
}
// We must remove the path before removing the authority because if the path starts with two slashes, the URI
// would turn invalid. And we also cannot set a relative path before removing the authority, as that is also
// invalid.
$emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost('');
if ($base->getPath() !== $target->getPath()) {
return $emptyPathUri->withPath(self::getRelativePath($base, $target));
}
if ($base->getQuery() === $target->getQuery()) {
// Only the target fragment is left. And it must be returned even if base and target fragment are the same.
return $emptyPathUri->withQuery('');
}
// If the base URI has a query but the target has none, we cannot return an empty path reference as it would
// inherit the base query component when resolving.
if ($target->getQuery() === '') {
$segments = explode('/', $target->getPath());
/** @var string $lastSegment */
$lastSegment = end($segments);
return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment);
}
return $emptyPathUri;
}
private static function getRelativePath(UriInterface $base, UriInterface $target): string
{
$sourceSegments = explode('/', $base->getPath());
$targetSegments = explode('/', $target->getPath());
array_pop($sourceSegments);
$targetLastSegment = array_pop($targetSegments);
foreach ($sourceSegments as $i => $segment) {
if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) {
unset($sourceSegments[$i], $targetSegments[$i]);
} else {
break;
}
}
$targetSegments[] = $targetLastSegment;
$relativePath = str_repeat('../', count($sourceSegments)) . implode('/', $targetSegments);
// A reference to am empty last segment or an empty first sub-segment must be prefixed with "./".
// This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
// as the first segment of a relative-path reference, as it would be mistaken for a scheme name.
if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) {
$relativePath = "./$relativePath";
} elseif ('/' === $relativePath[0]) {
if ($base->getAuthority() != '' && $base->getPath() === '') {
// In this case an extra slash is added by resolve() automatically. So we must not add one here.
$relativePath = ".$relativePath";
} else {
$relativePath = "./$relativePath";
}
}
return $relativePath;
}
private function __construct()
{
// cannot be instantiated
}
}
/**
* This class provides an abstraction over an uri
*
* @since 25.0.0
*/
class Uri implements UriInterface, \JsonSerializable {
/**
* Absolute http and https URIs require a host per RFC 7230 Section 2.7
* but in generic URIs the host can be empty. So for http(s) URIs
* we apply this default host when no host is given yet to form a
* valid URI.
*/
private const HTTP_DEFAULT_HOST = 'localhost';
private const DEFAULT_PORTS = [
'http' => 80,
'https' => 443,
'ftp' => 21,
'gopher' => 70,
'nntp' => 119,
'news' => 119,
'telnet' => 23,
'tn3270' => 23,
'imap' => 143,
'pop' => 110,
'ldap' => 389,
];
/**
* Unreserved characters for use in a regex.
*
* @link https://tools.ietf.org/html/rfc3986#section-2.3
*/
private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
/**
* Sub-delims for use in a regex.
*
* @link https://tools.ietf.org/html/rfc3986#section-2.2
*/
private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26'];
private string $scheme = '';
private string $userInfo = '';
private string $host = '';
private ?int $port = null;
private string $path = '';
private string $query = '';
private string $fragment = '';
private ?string $composedComponents = null;
public function __construct(string $uri = '')
{
if ($uri !== '') {
$parts = self::parse($uri);
if ($parts === false) {
throw new \Exception("Unable to parse URI: $uri");
}
$this->applyParts($parts);
}
}
/**
* UTF-8 aware parse_url replacement
* @param string $url
* @return array|false
*/
private function parse(string $url) {
// If IPv6
$prefix = '';
if (preg_match('%^(.*://\[[0-9:a-f]+\])(.*?)$%', $url, $matches)) {
/** @var array{0:string, 1:string, 2:string} $matches */
$prefix = $matches[1];
$url = $matches[2];
}
/** @var string */
$encodedUrl = preg_replace_callback(
'%[^:/@?&=#]+%usD',
static function ($matches) {
return urlencode($matches[0]);
},
$url
);
$result = parse_url($prefix . $encodedUrl);
if ($result === false) {
return false;
}
return array_map('urldecode', $result);
}
public function __toString(): string
{
if ($this->composedComponents === null) {
$this->composedComponents = self::composeComponents(
$this->scheme,
$this->getAuthority(),
$this->path,
$this->query,
$this->fragment
);
}
return $this->composedComponents;
}
/**
* Composes a URI reference string from its various components.
*
* Usually this method does not need to be called manually but instead is used indirectly via
* `Psr\Http\Message\UriInterface::__toString`.
*
* PSR-7 UriInterface treats an empty component the same as a missing component as
* getQuery(), getFragment() etc. always return a string. This explains the slight
* difference to RFC 3986 Section 5.3.
*
* Another adjustment is that the authority separator is added even when the authority is missing/empty
* for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with
* `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But
* `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to
* that format).
*
* @link https://tools.ietf.org/html/rfc3986#section-5.3
*/
public static function composeComponents(?string $scheme, ?string $authority, string $path, ?string $query, ?string $fragment): string
{
$uri = '';
// weak type checks to also accept null until we can add scalar type hints
if ($scheme != '') {
$uri .= $scheme . ':';
}
if ($authority != ''|| $scheme === 'file') {
$uri .= '//' . $authority;
}
$uri .= $path;
if ($query != '') {
$uri .= '?' . $query;
}
if ($fragment != '') {
$uri .= '#' . $fragment;
}
return $uri;
}
/**
* Whether the URI has the default port of the current scheme.
*
* `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used
* independently of the implementation.
*/
public static function isDefaultPort(UriInterface $uri): bool
{
return $uri->getPort() === null
|| (isset(self::DEFAULT_PORTS[$uri->getScheme()]) && $uri->getPort() === self::DEFAULT_PORTS[$uri->getScheme()]);
}
/**
* Whether the URI is absolute, i.e. it has a scheme.
*
* An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true
* if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative
* to another URI, the base URI. Relative references can be divided into several forms:
* - network-path references, e.g. '//example.com/path'
* - absolute-path references, e.g. '/path'
* - relative-path references, e.g. 'subpath'
*
* @see Uri::isNetworkPathReference
* @see Uri::isAbsolutePathReference
* @see Uri::isRelativePathReference
* @link https://tools.ietf.org/html/rfc3986#section-4
*/
public static function isAbsolute(UriInterface $uri): bool
{
return $uri->getScheme() !== '';
}
/**
* Whether the URI is a network-path reference.
*
* A relative reference that begins with two slash characters is termed an network-path reference.
*
* @link https://tools.ietf.org/html/rfc3986#section-4.2
*/
public static function isNetworkPathReference(UriInterface $uri): bool
{
return $uri->getScheme() === '' && $uri->getAuthority() !== '';
}
/**
* Whether the URI is a absolute-path reference.
*
* A relative reference that begins with a single slash character is termed an absolute-path reference.
*
* @link https://tools.ietf.org/html/rfc3986#section-4.2
*/
public static function isAbsolutePathReference(UriInterface $uri): bool
{
return $uri->getScheme() === ''
&& $uri->getAuthority() === ''
&& isset($uri->getPath()[0])
&& $uri->getPath()[0] === '/';
}
/**
* Whether the URI is a relative-path reference.
*
* A relative reference that does not begin with a slash character is termed a relative-path reference.
*
* @link https://tools.ietf.org/html/rfc3986#section-4.2
*/
public static function isRelativePathReference(UriInterface $uri): bool
{
return $uri->getScheme() === ''
&& $uri->getAuthority() === ''
&& (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/');
}
/**
* Whether the URI is a same-document reference.
*
* A same-document reference refers to a URI that is, aside from its fragment
* component, identical to the base URI. When no base URI is given, only an empty
* URI reference (apart from its fragment) is considered a same-document reference.
*
* @param UriInterface $uri The URI to check
* @param UriInterface|null $base An optional base URI to compare against
*
* @link https://tools.ietf.org/html/rfc3986#section-4.4
*/
public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null): bool
{
if ($base !== null) {
$uri = UriResolver::resolve($base, $uri);
return ($uri->getScheme() === $base->getScheme())
&& ($uri->getAuthority() === $base->getAuthority())
&& ($uri->getPath() === $base->getPath())
&& ($uri->getQuery() === $base->getQuery());
}
return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === '';
}
/**
* Creates a new URI with a specific query string value removed.
*
* Any existing query string values that exactly match the provided key are
* removed.
*
* @param UriInterface $uri URI to use as a base.
* @param string $key Query string key to remove.
*/
public static function withoutQueryValue(UriInterface $uri, string $key): UriInterface
{
$result = self::getFilteredQueryString($uri, [$key]);
return $uri->withQuery(implode('&', $result));
}
/**
* Creates a new URI with a specific query string value.
*
* Any existing query string values that exactly match the provided key are
* removed and replaced with the given key value pair.
*
* A value of null will set the query string key without a value, e.g. "key"
* instead of "key=value".
*
* @param UriInterface $uri URI to use as a base.
* @param string $key Key to set.
* @param string|null $value Value to set
*/
public static function withQueryValue(UriInterface $uri, string $key, ?string $value): UriInterface
{
$result = self::getFilteredQueryString($uri, [$key]);
$result[] = self::generateQueryString($key, $value);
return $uri->withQuery(implode('&', $result));
}
/**
* Creates a new URI with multiple specific query string values.
*
* It has the same behavior as withQueryValue() but for an associative array of key => value.
*
* @param UriInterface $uri URI to use as a base.
* @param array<string, string|null> $keyValueArray Associative array of key and values
*/
public static function withQueryValues(UriInterface $uri, array $keyValueArray): UriInterface
{
$result = self::getFilteredQueryString($uri, array_keys($keyValueArray));
foreach ($keyValueArray as $key => $value) {
$result[] = self::generateQueryString((string) $key, $value !== null ? (string) $value : null);
}
return $uri->withQuery(implode('&', $result));
}
/**
* Creates a URI from a hash of `parse_url` components.
*
* @link http://php.net/manual/en/function.parse-url.php
*
* @throws MalformedUriException If the components do not form a valid URI.
*/
public static function fromParts(array $parts): UriInterface
{
$uri = new self();
$uri->applyParts($parts);
$uri->validateState();
return $uri;
}
public function getScheme(): string
{
return $this->scheme;
}
public function getAuthority(): string
{
$authority = $this->host;
if ($this->userInfo !== '') {
$authority = $this->userInfo . '@' . $authority;
}
if ($this->port !== null) {
$authority .= ':' . $this->port;
}
return $authority;
}
public function getUserInfo(): string {
return $this->userInfo;
}
public function getHost(): string {
return $this->host;
}
public function setHost(string $host): self {
$this->host = $host;
}
public function getPort(): ?int {
return $this->port;
}
public function setPort(?int $port): self {
$this->port = $port !== null ? $this->filterPort($port) : null;
return $this;
}
public function getPath(): string
{
return $this->path;
}
public function getQuery(): string
{
return $this->query;
}
public function getFragment(): string
{
return $this->fragment;
}
public function withScheme($scheme): UriInterface
{
$scheme = $this->filterScheme($scheme);
if ($this->scheme === $scheme) {
return $this;
}
$new = clone $this;
$new->scheme = $scheme;
$new->composedComponents = null;
$new->removeDefaultPort();
$new->validateState();
return $new;
}
public function withUserInfo($user, $password = null): UriInterface
{
$info = $this->filterUserInfoComponent($user);
if ($password !== null) {
$info .= ':' . $this->filterUserInfoComponent($password);
}
if ($this->userInfo === $info) {
return $this;
}
$new = clone $this;
$new->userInfo = $info;
$new->composedComponents = null;
$new->validateState();
return $new;
}
public function withHost($host): UriInterface
{
$host = $this->filterHost($host);
if ($this->host === $host) {
return $this;
}
$new = clone $this;
$new->host = $host;
$new->composedComponents = null;
$new->validateState();
return $new;
}
public function withPort($port): UriInterface
{
$port = $this->filterPort($port);
if ($this->port === $port) {
return $this;
}
$new = clone $this;
$new->port = $port;
$new->composedComponents = null;
$new->removeDefaultPort();
$new->validateState();
return $new;
}
public function withPath($path): UriInterface
{
$path = $this->filterPath($path);
if ($this->path === $path) {
return $this;
}
$new = clone $this;
$new->path = $path;
$new->composedComponents = null;
$new->validateState();
return $new;
}
public function withQuery($query): UriInterface
{
$query = $this->filterQueryAndFragment($query);
if ($this->query === $query) {
return $this;
}
$new = clone $this;
$new->query = $query;
$new->composedComponents = null;
return $new;
}
public function withFragment($fragment): UriInterface
{
$fragment = $this->filterQueryAndFragment($fragment);
if ($this->fragment === $fragment) {
return $this;
}
$new = clone $this;
$new->fragment = $fragment;
$new->composedComponents = null;
return $new;
}
public function jsonSerialize(): string
{
return $this->__toString();
}
/**
* Apply parse_url parts to a URI.
*
* @param array $parts Array of parse_url parts to apply.
*/
private function applyParts(array $parts): void
{
$this->scheme = isset($parts['scheme'])
? $this->filterScheme($parts['scheme'])
: '';
$this->userInfo = isset($parts['user'])
? $this->filterUserInfoComponent($parts['user'])
: '';
$this->host = isset($parts['host'])
? $this->filterHost($parts['host'])
: '';
$this->port = isset($parts['port'])
? $this->filterPort($parts['port'])
: null;
$this->path = isset($parts['path'])
? $this->filterPath($parts['path'])
: '';
$this->query = isset($parts['query'])
? $this->filterQueryAndFragment($parts['query'])
: '';
$this->fragment = isset($parts['fragment'])
? $this->filterQueryAndFragment($parts['fragment'])
: '';
if (isset($parts['pass'])) {
$this->userInfo .= ':' . $this->filterUserInfoComponent($parts['pass']);
}
$this->removeDefaultPort();
}
/**
* @param mixed $scheme
*
* @throws \InvalidArgumentException If the scheme is invalid.
*/
private function filterScheme($scheme): string
{
if (!is_string($scheme)) {
throw new \InvalidArgumentException('Scheme must be a string');
}
return \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
}
/**
* @param mixed $component
*
* @throws \InvalidArgumentException If the user info is invalid.
*/
private function filterUserInfoComponent($component): string
{
if (!is_string($component)) {
throw new \InvalidArgumentException('User info must be a string');
}
return preg_replace_callback(
'/(?:[^%' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ']+|%(?![A-Fa-f0-9]{2}))/',
[$this, 'rawurlencodeMatchZero'],
$component
);
}
/**
* @param mixed $host
*
* @throws \InvalidArgumentException If the host is invalid.
*/
private function filterHost($host): string
{
if (!is_string($host)) {
throw new \InvalidArgumentException('Host must be a string');
}
return \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
}
/**
* @param mixed $port
*
* @throws \InvalidArgumentException If the port is invalid.
*/
private function filterPort($port): ?int
{
if ($port === null) {
return null;
}
$port = (int) $port;
if (0 > $port || 0xffff < $port) {
throw new \InvalidArgumentException(
sprintf('Invalid port: %d. Must be between 0 and 65535', $port)
);
}
return $port;
}
/**
* @param string[] $keys
*
* @return string[]
*/
private static function getFilteredQueryString(UriInterface $uri, array $keys): array
{
$current = $uri->getQuery();
if ($current === '') {
return [];
}
$decodedKeys = array_map('rawurldecode', $keys);
return array_filter(explode('&', $current), function ($part) use ($decodedKeys) {
return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true);
});
}
private static function generateQueryString(string $key, ?string $value): string
{
// Query string separators ("=", "&") within the key or value need to be encoded
// (while preventing double-encoding) before setting the query string. All other
// chars that need percent-encoding will be encoded by withQuery().
$queryString = strtr($key, self::QUERY_SEPARATORS_REPLACEMENT);
if ($value !== null) {
$queryString .= '=' . strtr($value, self::QUERY_SEPARATORS_REPLACEMENT);
}
return $queryString;
}
private function removeDefaultPort(): void
{
if ($this->port !== null && self::isDefaultPort($this)) {
$this->port = null;
}
}
/**
* Filters the path of a URI
*
* @param mixed $path
*
* @throws \InvalidArgumentException If the path is invalid.
*/
private function filterPath($path): string
{
if (!is_string($path)) {
throw new \InvalidArgumentException('Path must be a string');
}
return preg_replace_callback(
'/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
[$this, 'rawurlencodeMatchZero'],
$path
);
}
/**
* Filters the query string or fragment of a URI.
*
* @param mixed $str
*
* @throws \InvalidArgumentException If the query or fragment is invalid.
*/
private function filterQueryAndFragment($str): string
{
if (!is_string($str)) {
throw new \InvalidArgumentException('Query and fragment must be a string');
}
return preg_replace_callback(
'/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
[$this, 'rawurlencodeMatchZero'],
$str
);
}
private function rawurlencodeMatchZero(array $match): string
{
return rawurlencode($match[0]);
}
private function validateState(): void
{
if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) {
$this->host = self::HTTP_DEFAULT_HOST;
}
if ($this->getAuthority() === '') {
if (0 === strpos($this->path, '//')) {
throw new MalformedUriException('The path of a URI without an authority must not start with two slashes "//"');
}
if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) {
throw new MalformedUriException('A relative URI must not have a path beginning with a segment containing a colon');
}
} elseif (isset($this->path[0]) && $this->path[0] !== '/') {
throw new MalformedUriException('The path of a URI with an authority must start with a slash "/" or be empty');
}
}
}

Wyświetl plik

@ -0,0 +1,26 @@
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
import Vue from 'vue'
import App from './views/SettingsPersonal.vue'
// CSP config for webpack dynamic chunk loading
// eslint-disable-next-line
__webpack_nonce__ = btoa(OC.requestToken)
// Correct the root of the app for chunk loading
// OC.linkTo matches the apps folders
// eslint-disable-next-line
__webpack_public_path__ = OC.linkTo('social', 'js/')
Vue.prototype.t = t
Vue.prototype.n = n
Vue.prototype.OC = OC
Vue.prototype.OCA = OCA
/* eslint-disable-next-line no-new */
new Vue({
render: h => h(App),
}).$mount('#settings-personal')

Wyświetl plik

@ -0,0 +1,85 @@
<template>
<SettingsSection :title="t('social', 'Social')" :description="t('social', 'Configure social .......')">
<div class="form-field">
<label>{{ t('social', 'Default post visibility:') }}</label>
<CheckboxRadioSwitch :checked.sync="defaultVisibility"
value="PUBLIC" name="style"
type="radio">
{{ t('social', 'Public') }}
</CheckboxRadioSwitch>
<p class="form-info-checkbox">
{{ t('social', 'Make your post publicly visible') }}
</p>
<CheckboxRadioSwitch :checked.sync="defaultVisibility"
value="INSTANCE" name="style"
type="radio">
{{ t('social', 'Instance Only') }}
</CheckboxRadioSwitch>
<p class="form-info-checkbox">
{{ t('social', 'Make your post visible only to the user of this instance') }}
</p>
<CheckboxRadioSwitch :checked.sync="defaultVisibility"
value="FOLLOWER" name="style"
type="radio">
{{ t('social', 'Followers only') }}
</CheckboxRadioSwitch>
<p class="form-info-checkbox">
{{ t('social', 'Make your post visible only to your follower') }}
</p>
</div>
<div class="form-field">
<CheckboxRadioSwitch :checked.sync="style" value="text" name="style"
type="switch">
{{ t('social', 'Require follow requests') }}
</CheckboxRadioSwitch>
<p class="form-info-checkbox">
{{ t('social', 'Manually control who can follow you by approving follow requests') }}
</p>
</div>
</SettingsSection>
</template>
<script>
import CheckboxRadioSwitch from '@nextcloud/vue/dist/Components/CheckboxRadioSwitch'
import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection'
export default {
name: 'SetupUser',
components: {
CheckboxRadioSwitch,
SettingsSection,
},
data() {
return {
defaultVisibility: 'PUBLIC'
}
}
}
</script>
<style scoped lang="scss">
.form-info-checkbox {
padding-left: 26px;
font-size: 14px;
margin-top: -2px;
}
.form-info-field {
font-size: 14px;
margin-top: -2px;
}
.form-field {
margin-top: 1.5rem;
}
.form-input {
width: 300px;
max-width: 100%;
display: block;
}
</style>

Wyświetl plik

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
\OCP\Util::addScript('social', 'settings-personal');
?>
<div id="settings-personal"></div>

Wyświetl plik

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Tests\Entitiy;
use OCA\Social\Entity\Account;
use OCA\Social\Serializer\AccountSerializer;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserManager;
use Test\TestCase;
class AccountSerializerTest extends TestCase {
public function testJsonLd(): void {
$localDomain = "helloworld.social";
$request = $this->createMock(IRequest::class);
$request->expects($this->once())
->method('getServerHost')
->willReturn($localDomain);
$alice = $this->createMock(IUser::class);
$alice->expects($this->atLeastOnce())
->method('getDisplayName')
->willReturn('Alice Alice');
$userManager = $this->createMock(IUserManager::class);
$userManager->expects($this->once())
->method('get')
->with('alice_id')
->willReturn($alice);
$account = Account::newLocal();
$account->setUserName('alice');
$account->setUserId('alice_id');
$accountSerializer = new AccountSerializer($request, $userManager);
$jsonLd = $accountSerializer->toJsonLd($account);
$this->assertSame($jsonLd['id'], 'https://' . $localDomain . '/alice');
$this->assertSame($jsonLd['name'], 'Alice Alice');
}
}

Wyświetl plik

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Tests\Service;
use OCA\Social\Entity\Account;
use OCA\Social\Entity\Follow;
use OCA\Social\Service\AccountFinder;
use OCP\DB\ORM\IEntityManager;
use OCP\Server;
use Test\TestCase;
/**
* @group DB
*/
class AccountFinderTest extends TestCase {
private ?Account $account1 = null;
private ?Account $account2 = null;
public function setUp(): void {
parent::setUp();
$em = Server::get(IEntityManager::class);
$this->account1 = Account::newLocal('user1', 'user1', 'User1');
$this->account2 = Account::newLocal('user2', 'user2', 'User2');
$this->account2->follow($this->account1);
$em->persist($this->account1);
$em->persist($this->account2);
$em->flush();
}
public function tearDown(): void {
$em = Server::get(IEntityManager::class);
$em->remove($this->account1);
$em->remove($this->account2);
$em->flush();
parent::tearDown();
}
public function testGetLocalFollower(): void {
$accountFinder = Server::get(AccountFinder::class);
$accounts = $accountFinder->getLocalFollowersOf($this->account1);
var_dump(count($accounts));
}
}

Wyświetl plik

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
// Nextcloud - Social Support
// SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Social\Tests\Service\ActivityPub;
use OCA\Social\Service\ActivityPub\TagManager;
use OCP\IRequest;
use Test\TestCase;
class TagManagerTest extends TestCase {
public function localUriProvider(): array {
return [
[null, 'helloworld.com', false],
['https://helloworld.com', 'helloworld.com', true],
['https://helloworld.com/rehie', 'helloworld.com', true],
['https://helloworld.com:3000/rehie', 'helloworld.com', false],
['https://helloworld1.com', 'helloworld.com', false],
['https://floss.social/@carlschwan', 'helloworld.com', false],
];
}
/**
* @dataProvider localUriProvider
*/
public function testIsLocalUri(?string $url, string $localDomain, bool $result): void {
$request = $this->createMock(IRequest::class);
$request->expects($this->once())
->method('getServerHost')
->willReturn($localDomain);
$tagManager = new TagManager($request);
$this->assertSame($tagManager->isLocalUri($url), $result);
}
}

Wyświetl plik

@ -6,6 +6,7 @@ module.exports = {
entry: {
social: path.join(__dirname, 'src', 'main.js'),
ostatus: path.join(__dirname, 'src', 'ostatus.js'),
'settings-personal': path.join(__dirname, 'src', 'settings-personal.js'),
},
output: {
path: path.resolve(__dirname, './js'),