sforkowany z mirror/social
rodzic
95f3fc3a5a
commit
d53fc73448
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ class Instance {
|
|||
private string $domain = "";
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="int")
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private int $accountsCount = -1;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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)]);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\Social\Tools\Exceptions;
|
||||
|
||||
class MalformedUriException extends \Exception {
|
||||
}
|
|
@ -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(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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')
|
|
@ -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>
|
|
@ -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>
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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'),
|
||||
|
|
Ładowanie…
Reference in New Issue