Compare commits

..

42 commits

Author SHA1 Message Date
Yannis Vogel
61f6fe3e7b
minor fixes for testing-file 2025-06-11 18:16:43 +02:00
Yannis Vogel
2ae81a3748
initial rough support for sending follow to CN
we now send follows to CN to the api/profile/profileName/fedfollow endpoint, either with a post, or a delete.
- commit also contains files where line-endings suddenly changed to crlf (now it's back to lf).
- targetRequestType (post/delete) now depends on the activity
- we now also set the id to username when fetching a user from CN
2025-06-11 18:15:13 +02:00
Sascha Nitsch
097f871ed6 yet unsupported objects seen in the wild 2025-06-11 03:22:06 +02:00
Sascha Nitsch
c1cf2a6be8 added manual tests, WIP 2025-06-11 03:21:51 +02:00
Sascha Nitsch
d99479c188 fixed path 2025-06-11 03:21:32 +02:00
Sascha Nitsch
4cc9cfdc8c converted from date field to timestamp
fixed reloading logic on paging
2025-06-11 03:21:19 +02:00
Sascha Nitsch
474631dff2 extended factory and constructing from json
renaming to not mix total count with paging
2025-06-11 03:15:55 +02:00
Yannis Vogel
207d876254
minor return-type fixes for orderedCollections
- fixed issue where followers, following and outbox didn't properly return OrderedCollection-type if requested without page-param (without page param, they return OrderedCollection, with page-param they return OrderedCollectionPage).
2025-06-09 11:33:33 +02:00
Yannis Vogel
9b3ae63c7e
ap prefix for ext-username sent to ContentNation 2025-05-28 18:02:05 +02:00
Yannis Vogel
96bb1efe16
support external services to comment&like on CN
- integrate support to send new posts to CN
- save original article-id in DB (needs db-migration)
- votes and comments on CN-articles and comments are sent to CN, with proper signing and format
- fixed minor issue where delete-activity was not properly working with objects
- fixed minor issue where tombstone wasn't supported (which prevented being able to delete mastodon-posts from the db)
2025-05-28 16:52:20 +02:00
Yannis Vogel
8891234617
"fix" previous commit to make update.php lowercase
windows is case-insensitive so we needed the tmp-commit and this one in order to change the case of the update.php file
2025-05-27 16:43:36 +02:00
Yannis Vogel
90fe8fab5c
tmp commit 2025-05-27 16:42:43 +02:00
Yannis Vogel
f8539e479e
minor hotfix for db-saving
save activity to db *before* mutating it for the target (e.g. save before transforming article into note)
2025-05-26 18:16:41 +02:00
Yannis Vogel
10dec5ebd3
conditionally convert article to note
- fix bug in which inReplyTo isn't correctly set from contentnation-comments
- added dio/article which has functions to convert article to note based on new file
- added formatsupport.json to manage special cases (f.e. includes which servers can handle articles)
2025-05-26 16:17:23 +02:00
Yannis Vogel
30c577c82f
fix following from mastodon -> CN 2025-05-25 20:22:34 +02:00
Yannis Vogel
5c90b4cfc9
initial support for actually sending NewContent
- integrated functionality to actually send new content to federated recipients and followers (IT WORKS!!)
- changed the way we remove a follow to return the removed followId (used in order to build the undo follow activity)
2025-05-23 19:58:47 +02:00
Yannis Vogel
7a5870de95
proper support for undoing votes and articles
- added option to remove the like/dislike from an article/comment with creating the correct Undo activity
- added option to remove article with creating the correct Undo activity
- fixed problem where post might not be accepted/saved when no receiver was set (posterName was previously excluded from receivers)
2025-05-23 13:42:15 +02:00
Yannis Vogel
572bb376c1
hotfix article title not being used 2025-05-22 20:55:21 +02:00
Yannis Vogel
62cfd6ef0d
initial support for articles from CN
- fixed how To and CC field (recipients) are handled in general
- fixed posts in database
- improved some error exceptions and prevented early breaks through try-catch blocks
- we now support CN-articles on our newcontent endpoint, with create and update calls
2025-05-22 20:44:37 +02:00
Yannis Vogel
ba88adcebd
refactored ContentNation data-format
- also fixed issue where we didn't retrieve posts from the DB
- also fixed issue where posts from db were malformatted
- changed contentnation data-converter for comments & votes to support the new data-structure (more consistent, easier to read)
2025-05-21 20:06:43 +02:00
Yannis Vogel
d355b5a7cd
integrate queue for NewContent
- also integrated better support for newContent types
- Integrated saving posts to database (posts gotten via outbox request as well as posts received in the NewContent endpoint)
- proper support for handling comments
- support for likes/dislikes
- support for requesting followers / following endpoints
- better inbox support

database needs changes, don't forget to run migration
2025-05-20 16:34:50 +02:00
Yannis Vogel
767f51cc5b
queue-redis connection + fqdn->CN follow-support
- the resque queue integration is now connected to our redis instance.
- IMPORTANT NOTICE: In order for this to work, we needed to change the plugins file under vendor/resque/php-resque/lib/Resque/Redis.php #137 ($this->driver->auth($password, $user);    (this change is neccessary for our implementation, as they normally don't support the $user attribute as it would be upstream breaking change). I might create a fork for this, to which we bind with composer
- The inboxJob now inherits from api instead of creating its own api instance
- inbox now actually works with follows and Undo-follows from mastodon->CN (adding the follower to our followers-db and removing it on undo).
- fixed bug where paths for templates was incorrectly adjusted
- better/proper support for comment-activity in getExternalPosts
2025-05-18 08:51:18 +02:00
Yannis Vogel
6cf9a030a4
added queue support and paths-refactoring
- added php-resque for queue job and worker support
- removed all document_root dependencies (as that doesn't work in the queue-environment
- added project_root global variable for more robust setup
- some file-constistency fixed
- config referring paths are now starting in project-root
- inbox now does the heavy-duty on a queue
2025-05-15 17:39:12 +02:00
Sascha Nitsch
49a4bee76a small bug fixes 2025-05-07 17:01:11 +02:00
Yannis Vogel
da18d37a79
very rudimental follower-support
- fixed rewrites to properly support @username/outbox, username/outbox, users/username/outbox, ...
- initial support for sending follow to e.g. mastodon (code commented out in api.php)
- database migration for follows table and fedusers table
- save retrieved fedusers in cache and db
- depend more on configs externaldomain, less on server_name
- properly implemented following-logic in dio
- made postForuUser static in newContent and inbox
- provide rsaprivate for signing reuests -> actPub-Servers
- change contenttype depending on use-case
- changed user json-template to have loop-back between user-template and webfinger (mastodon f.e. needs this)
2025-05-07 07:57:04 +02:00
Yannis Vogel
ce7aa5c72d
remove debug/todos for live tests 2025-04-30 22:49:20 +02:00
Yannis Vogel
8ea9bdcf9a
major structure rework
- checkSignature for our fqdn now happens in the connector
- cache public key for 1 hour
- change author on some files
- refactored outbox to api/v1/newcontent (CN->Federator communicates to here)
- disabled post for outbox (as that should go to v1/newcontent)
- initial support for receiving votes from CN, not fully working/clean yet
2025-04-30 22:44:50 +02:00
Yannis Vogel
10a3b1e0f9
remove key-files (don't belong in git) and minor cleanup 2025-04-29 07:41:24 +02:00
Yannis Vogel
352377887c
added proper application-signing
- we now check whether the request was sent from contentnation and approve the request with contentnations pub key
- fix issue where inbox wasn't gotten as it also got a username
- rough initial draft of parsing of connector-specific data (plugins/federator/contentnation::jsonToActivity)
- added key paths and headername to config
2025-04-23 21:07:26 +02:00
Yannis Vogel
0edc4c0db6
remove mastodon.ini 2025-04-23 14:02:54 +02:00
Yannis Vogel
21f73fb56f
cleanup and removal of mastodon-integration
- In-&Outbox now use factories to convert data
- initial creation of followers-data (wip, not working)
- fixed some issue where connector would always fail (as we deliberately always pass username@domain and the regex there always fails)
- deleted all mastodon-files (mastodon.ini and mastodon plugin)
- minor fix where toObject of create would override id with an empty string if no url is set
2025-04-23 14:02:20 +02:00
Yannis Vogel
4d36fc3c61
fix remaining phan errors
- also cleaned up dummy
- added new rewrite for apache for sharedInbox (your.fqdn/inbox)
- fixed json-formatting of publicKey when requesting user via api
2025-04-22 14:30:26 +02:00
Yannis Vogel
d9b02bd95b
fix phan errors
fix lots of phan errors, now we only have phan errors left from temporary implementations. These will be fixed in the next commits as we properly re-integrate and remove temporary-code
2025-04-21 21:06:03 +02:00
Yannis Vogel
957b4f5266
fix line endings
fix line endings to be in LF format
2025-04-21 14:56:44 +02:00
Yannis Vogel
64ee4c518f
forgot to clean-up outbox logging as well. 2025-04-17 12:40:50 +02:00
Yannis Vogel
f6d6c74bc6
minor logging cleanup 2025-04-17 12:38:45 +02:00
Yannis Vogel
20c2db4b35
support outbox post
- we can now receive content into outbox (f.e. from contentnation) and post it into the outbox-user-logfile for now
- we now verify post-requests according to ActivityPub standard
- minor bugfix where user specified by passed user-parameter is possibly not included in postForUser-call
2025-04-17 12:13:34 +02:00
Yannis Vogel
305ded4986
WIP inbox-support
- includes hacky following-mechanic in order to simulate a follow on mastodon (not properly working, need to also inject the user this creates into the followers db for the target mastodon-user)
- created endpoint for inbox. SharedInbox is used when no user is provided (/api/federator/fedusers/inbox'), the regular inbox link now works (/users/username/inbox).
- Retrieve all followers of sender and, if they're part of our system, send the activity into their personal inbox
- Support Announce and Undo Activity-Types
- Inbox currently converts to proper ActPub-objects and saves data to log-files
2025-04-15 16:42:46 +02:00
Yannis Vogel
823283183e
minor fixes
- permissions are always lower-case
- revert webfinger changes to initial state
- re-add setFirst and setLast for outbox
- we now save the current host (f.e. https://contentnation.net) to save users with their respective host (a request to fedusers/grumpydevelop will then result in saving the user as grumpydevelop@contentnation.net)
2025-04-10 19:23:09 +02:00
Yannis Vogel
721e37882d
revert to single-connector support
- removed support for multiple connector-plugins
- fixed issue where outbox wasn't properly retrieved from contentnation
2025-04-10 09:02:45 +02:00
Yannis Vogel
1a7b8264a1
fixes for outbox
- mastodon now properly sets available type-attributes
- database should now save user with his platform (as that's the actual unique identifier, plain username isn't)
- properly retrieve items from mastodon and print them on api-call
(example call for mastodon: federator/fedusers/gutenberg_org@mastodon.social/outbox?page=0)
2025-04-09 12:52:27 +02:00
Yannis Vogel
530caa7ea6
initial mastodon support and minor improvements
- reworked plugin handling. Main now registers and keeps all connectors and based on request we select the correct one and pass it (mainly for clean async-request-handling)
- added outbox functionality for mastodon
- changed image-mime-type approach to retrieve mime-type from the url, this way we don't need to store the images on our server/download each image
- added host for connector for better debugging
- minor bug-fixes
2025-04-08 22:03:19 +02:00
45 changed files with 904 additions and 886 deletions

16
.gitattributes vendored Normal file
View file

@ -0,0 +1,16 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
*.php text
# Declare files that will always have LF line endings on checkout.
*.sln text eol=lf
*.php text eol.lf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary

3
.gitignore vendored
View file

@ -6,6 +6,3 @@ php-docs
phpdoc phpdoc
html html
/cache /cache
contentnation.ini
*.pem*
composer.phar

View file

@ -1,7 +1,5 @@
[generic] [generic]
protocol = 'https' externaldomain = 'contentnation.net'
externaldomain = 'federator.your.fqdn'
sourcedomain = 'your.fqdn'
[database] [database]
host = '127.0.0.1' host = '127.0.0.1'
@ -10,17 +8,18 @@ password = '*change*me*'
database = 'federator' database = 'federator'
[templates] [templates]
path = '../templates/federator/' path = 'templates/federator/'
compiledir = '../cache' compiledir = 'cache'
[plugins] [plugins]
rediscache = 'rediscache.php' rediscache = 'rediscache.php'
dummy = 'dummyconnector.php' # dummy = 'dummyconnector.php'
contentnation = 'contentnation.php'
[maintenance] [maintenance]
username = 'federatoradmin' username = 'federatoradmin'
password = '*change*me*as*well' password = '*change*me*as*well'
[keys] [keys]
federatorPrivateKeyPath = 'federator.pem' federatorPrivateKeyPath = 'federator.key'
federatorPublicKeyPath = 'federator.pem.pub' federatorPublicKeyPath = 'federator.pub'

10
contentnation.ini Normal file
View file

@ -0,0 +1,10 @@
[contentnation]
service-uri = http://local.contentnation.net
[userdata]
path = '/home/net/contentnation/userdata/htdocs/' // need to download local copy of image and put img-path here
url = 'https://userdata.contentnation.net'
[keys]
headerSenderName = 'contentnation'
publicKeyPath = 'contentnation.pub'

View file

@ -14,6 +14,7 @@ date_default_timezone_set("Europe/Berlin");
spl_autoload_register(static function (string $className) { spl_autoload_register(static function (string $className) {
include '../php/' . str_replace("\\", "/", strtolower($className)) . '.php'; include '../php/' . str_replace("\\", "/", strtolower($className)) . '.php';
}); });
define('PROJECT_ROOT', dirname(__DIR__, 1));
/// main instance /// main instance
$contentnation = new \Federator\Api(); $contentnation = new \Federator\Api();

113
htdocs/index.html Normal file
View file

@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Request UI</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="flex justify-center items-center min-h-screen bg-gray-100">
<div class="w-full max-w-3xl bg-white shadow-lg rounded-lg p-6 m-6">
<h2 class="text-2xl font-semibold mb-4 text-center">API Request UI</h2>
<div id="request-container">
<!-- Request Form Template -->
<div class="request-box border p-4 rounded-lg mb-4 bg-gray-50 overflow-y-auto">
<label class="block font-medium">API target link</label>
<input type="text" class="target-link-input w-full p-2 border rounded-md mb-2"
placeholder="Enter target link" value="users/grumpydevelop/outbox?page=0">
<label class="block font-medium">Request type</label>
<input type="text" class="request-type-input w-full p-2 border rounded-md mb-2"
placeholder="POST or GET" value="GET">
<label class="block font-medium">X-Session:</label>
<input type="text" class="session-input w-full p-2 border rounded-md mb-2"
placeholder="Enter X-Session token" value="">
<label class="block font-medium">X-Profile:</label>
<input type="text" class="profile-input w-full p-2 border rounded-md mb-2" placeholder="Enter X-Profile"
value="">
<div class="buttonContainer">
<button class="send-btn bg-blue-500 text-white px-4 py-2 rounded-md w-full hover:bg-blue-600">
Send Request
</button>
</div>
<p class="mt-2 text-sm text-gray-700">Response:</p>
<p class="response mt-2 text-sm text-gray-700 font-mono whitespace-pre-wrap">Waiting for response</p>
</div>
</div>
<button id="add-request" class="mt-4 bg-green-500 text-white px-4 py-2 rounded-md w-full hover:bg-green-600">
Add Another Request
</button>
</div>
<script>
function sendRequest(button) {
const container = button.parentElement.parentElement;
const targetLink = container.querySelector(".target-link-input").value;
const requestType = container.querySelector(".request-type-input").value;
const session = container.querySelector(".session-input").value;
const profile = container.querySelector(".profile-input").value;
const responseField = container.querySelector(".response");
button.parentElement.style.cursor = "not-allowed";
button.style.pointerEvents = "none";
button.textContent = "Sending...";
responseField.textContent = "Waiting for response";
const headers = {
...(session ? { "X-Session": session } : {}),
...(profile ? { "X-Profile": profile } : {}),
"HTTP_HOST": "localhost",
};
fetch("http://localhost/" + targetLink, {
method: requestType,
headers
})
.then(response => response.text())
.then(data => {
responseField.textContent = data;
button.parentElement.style.cursor = "";
button.style.pointerEvents = "";
button.textContent = "Send Request";
})
.catch(error => {
responseField.textContent = "Error: " + error;
button.parentElement.style.cursor = "";
button.style.pointerEvents = "";
button.textContent = "Send Request";
});
}
document.querySelectorAll(".send-btn").forEach(btn => {
btn.addEventListener("click", function () {
sendRequest(this);
});
});
document.getElementById("add-request").addEventListener("click", function () {
const container = document.getElementById("request-container");
const requestBox = container.firstElementChild.cloneNode(true);
requestBox.querySelector(".target-link-input").value = "users/grumpydevelop@contentnation.net/outbox?page=0";
requestBox.querySelector(".request-type-input").value = "GET";
requestBox.querySelector(".session-input").value = "";
requestBox.querySelector(".profile-input").value = "";
requestBox.querySelector(".response").textContent = "Waiting for response";
requestBox.querySelector(".send-btn").addEventListener("click", function () {
sendRequest(this);
});
container.appendChild(requestBox);
});
</script>
</body>
</html>

View file

@ -46,7 +46,7 @@ class Api extends Main
*/ */
public function __construct() public function __construct()
{ {
$this->contentType = 'application/json'; $this->contentType = "application/json";
parent::__construct(); parent::__construct();
} }
@ -63,7 +63,7 @@ class Api extends Main
while ($this->path[0] === '/') { while ($this->path[0] === '/') {
$this->path = substr($this->path, 1); $this->path = substr($this->path, 1);
} }
$this->paths = explode('/', $this->path); $this->paths = explode("/", $this->path);
} }
/** /**
@ -74,7 +74,8 @@ class Api extends Main
$this->setPath((string) $_REQUEST['_call']); $this->setPath((string) $_REQUEST['_call']);
$this->openDatabase(); $this->openDatabase();
$this->loadPlugins(); $this->loadPlugins();
$retval = '';
$retval = "";
$handler = null; $handler = null;
if ($this->connector === null) { if ($this->connector === null) {
http_response_code(500); http_response_code(500);
@ -100,7 +101,7 @@ class Api extends Main
break; break;
case 'fedusers': case 'fedusers':
$handler = new Api\FedUsers($this); $handler = new Api\FedUsers($this);
$this->setContentType('application/activity+json'); $this->setContentType("application/activity+json");
break; break;
case 'v1': case 'v1':
switch ($this->paths[1]) { switch ($this->paths[1]) {
@ -110,6 +111,39 @@ class Api extends Main
case 'newcontent': case 'newcontent':
$handler = new Api\V1\NewContent($this); $handler = new Api\V1\NewContent($this);
break; break;
/* case 'sendFollow': { // hacky implementation for testing purposes
$username = $this->paths[2];
$domain = $this->config['generic']['externaldomain'];
$response = \Federator\DIO\Followers::sendFollowRequest(
$this->dbh,
$this->connector,
$this->cache,
$username,
"admin@mastodon.local",
$domain
);
header("Content-type: " . $this->contentType);
header("Access-Control-Allow-Origin: *");
header("Cache-Control: no-cache, no-store, must-revalidate");
header("Pragma: no-cache");
header("Expires: 0");
if (is_string($response)) {
$this->setResponseCode(200);
$retval = json_encode(array(
"status" => "ok",
"message" => $response
));
} else {
$this->setResponseCode(500);
$retval = json_encode(array(
"status" => "error",
"message" => "Failed to send follow request"
));
}
http_response_code($this->responseCode);
echo $retval;
return;
} */
} }
break; break;
} }
@ -123,7 +157,7 @@ class Api extends Main
} catch (Exceptions\Exception $e) { } catch (Exceptions\Exception $e) {
$this->setResponseCode($e->getRetCode()); $this->setResponseCode($e->getRetCode());
$retval = json_encode(array( $retval = json_encode(array(
'error' => $e->getMessage() "error" => $e->getMessage()
)); ));
} }
} else { } else {
@ -139,26 +173,26 @@ class Api extends Main
} }
if ($printresponse) { if ($printresponse) {
if ($this->redirect !== null) { if ($this->redirect !== null) {
header('Location: ' . $this->redirect); header("Location: $this->redirect");
} }
if ($this->responseCode != 404) { if ($this->responseCode != 404) {
header('Content-type: ' . $this->contentType); header("Content-type: " . $this->contentType);
header('Access-Control-Allow-Origin: *'); header("Access-Control-Allow-Origin: *");
} }
if ($this->cacheTime == 0) { if ($this->cacheTime == 0) {
header('Cache-Control: no-cache, no-store, must-revalidate'); header("Cache-Control: no-cache, no-store, must-revalidate");
header('Pragma: no-cache'); header("Pragma: no-cache");
header('Expires: 0'); header("Expires: 0");
} else { } else {
$ts = gmdate('D, d M Y H:i:s', time() + $this->cacheTime) . ' GMT'; $ts = gmdate("D, d M Y H:i:s", time() + $this->cacheTime) . " GMT";
header('Expires: ' . $ts); header("Expires: $ts");
header('Pragma: cache'); header("Pragma: cache");
header('Cache-Control: max-age=' . $this->cacheTime); header("Cache-Control: max-age=" . $this->cacheTime);
} }
echo $retval; echo $retval;
} else { } else {
if (!headers_sent()) { if (!headers_sent()) {
header('Content-type: ' . $this->contentType); header("Content-type: " . $this->contentType);
} }
} }
} }
@ -172,7 +206,7 @@ class Api extends Main
* @param string $message optional message * @param string $message optional message
* @throws Exceptions\PermissionDenied * @throws Exceptions\PermissionDenied
*/ */
public function checkPermission($permission, $exception = '\Exceptions\PermissionDenied', $message = null): void public function checkPermission($permission, $exception = "\Exceptions\PermissionDenied", $message = null): void
{ {
// generic check first // generic check first
if ($this->user === false) { if ($this->user === false) {
@ -216,7 +250,7 @@ class Api extends Main
$signatureHeader = $headers['Signature'] ?? null; $signatureHeader = $headers['Signature'] ?? null;
if (!isset($signatureHeader)) { if (!isset($signatureHeader)) {
throw new Exceptions\PermissionDenied('Missing Signature header'); throw new Exceptions\PermissionDenied("Missing Signature header");
} }
// Parse Signature header // Parse Signature header
@ -226,36 +260,32 @@ class Api extends Main
$signature = base64_decode($signatureParts['signature']); $signature = base64_decode($signatureParts['signature']);
$signedHeaders = explode(' ', $signatureParts['headers']); $signedHeaders = explode(' ', $signatureParts['headers']);
$keyId = $signatureParts['keyId']; $keyId = $signatureParts['keyId'];
$publicKeyPem = false;
if ($this->cache !== null) {
$publicKeyPem = $this->cache->getPublicKey($keyId);
}
if ($publicKeyPem === false) { $publicKeyPem = $this->cache->getPublicKey($keyId);
if (!isset($publicKeyPem) || $publicKeyPem === false) {
// Fetch public key from `keyId` (usually actor URL + #main-key) // Fetch public key from `keyId` (usually actor URL + #main-key)
[$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']); [$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
throw new Exceptions\PermissionDenied('Failed to fetch public key from keyId: ' . $keyId); throw new Exceptions\PermissionDenied("Failed to fetch public key from keyId: $keyId");
} }
$actor = json_decode($publicKeyData, true); $actor = json_decode($publicKeyData, true);
if (!is_array($actor) || !isset($actor['id'])) { if (!is_array($actor) || !isset($actor['id'])) {
throw new Exceptions\PermissionDenied('Invalid actor data'); throw new Exceptions\PermissionDenied("Invalid actor data");
} }
$publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null; $publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null;
if (!isset($publicKeyPem) || $publicKeyPem === false) { if (!isset($publicKeyPem) || $publicKeyPem === false) {
http_response_code(500); http_response_code(500);
throw new Exceptions\PermissionDenied('Public key couldn\'t be determined'); throw new Exceptions\PermissionDenied("Public key couldn't be determined");
} }
// Cache the public key for 1 hour // Cache the public key for 1 hour
if ($this->cache !== null) { $this->cache->savePublicKey($keyId, $publicKeyPem);
$this->cache->savePublicKey($keyId, $publicKeyPem);
}
} }
// Reconstruct the signed string // Reconstruct the signed string
@ -269,7 +299,7 @@ class Api extends Main
$headerValue = $headers[ucwords($header, '-')] ?? ''; $headerValue = $headers[ucwords($header, '-')] ?? '';
} }
$signedString .= strtolower($header) . ': ' . $headerValue . "\n"; $signedString .= strtolower($header) . ": " . $headerValue . "\n";
} }
$signedString = rtrim($signedString); $signedString = rtrim($signedString);
@ -282,11 +312,11 @@ class Api extends Main
} }
if ($verified != 1) { if ($verified != 1) {
http_response_code(500); http_response_code(500);
throw new Exceptions\PermissionDenied('Signature verification failed'); throw new Exceptions\PermissionDenied("Signature verification failed");
} }
// Signature is valid! // Signature is valid!
return 'Signature verified.'; return "Signature verified.";
} }
/** /**

View file

@ -47,7 +47,7 @@ class FedUsers implements APIInterface
*/ */
public function exec($paths, $user) public function exec($paths, $user)
{ {
$method = $_SERVER['REQUEST_METHOD']; $method = $_SERVER["REQUEST_METHOD"];
$handler = null; $handler = null;
$_username = $paths[1]; $_username = $paths[1];
switch (sizeof($paths)) { switch (sizeof($paths)) {
@ -58,7 +58,7 @@ class FedUsers implements APIInterface
} else { } else {
switch ($paths[1]) { switch ($paths[1]) {
case 'inbox': case 'inbox':
$_username = null; $_username = NULL;
$handler = new FedUsers\Inbox($this->main); $handler = new FedUsers\Inbox($this->main);
break; break;
default: default:
@ -127,7 +127,6 @@ class FedUsers implements APIInterface
} }
$config = $this->main->getConfig(); $config = $this->main->getConfig();
$domain = $config['generic']['externaldomain']; $domain = $config['generic']['externaldomain'];
$sourcedomain = $config['generic']['sourcedomain'];
$jsonKey = json_encode($user->publicKey); $jsonKey = json_encode($user->publicKey);
if (!is_string($jsonKey)) { if (!is_string($jsonKey)) {
throw new \Federator\Exceptions\FileNotFound(); throw new \Federator\Exceptions\FileNotFound();
@ -138,7 +137,6 @@ class FedUsers implements APIInterface
'imageMediaType' => $user->imageMediaType, 'imageMediaType' => $user->imageMediaType,
'imageURL' => $user->imageURL, 'imageURL' => $user->imageURL,
'fqdn' => $domain, 'fqdn' => $domain,
'sourcedomain' => $sourcedomain,
'name' => $user->name, 'name' => $user->name,
'username' => $user->id, 'username' => $user->id,
'publickey' => trim($jsonKey, '"'), 'publickey' => trim($jsonKey, '"'),
@ -149,7 +147,6 @@ class FedUsers implements APIInterface
$this->response = $this->main->renderTemplate('user.json', $data); $this->response = $this->main->renderTemplate('user.json', $data);
return true; return true;
} }
/** /**
* set response * set response
* *

View file

@ -59,17 +59,16 @@ class Followers implements \Federator\Api\FedUsers\FedUsersInterface
$followerItems = \Federator\DIO\Followers::getFollowersByUser($dbh, $user->id, $connector, $cache); $followerItems = \Federator\DIO\Followers::getFollowersByUser($dbh, $user->id, $connector, $cache);
$config = $this->main->getConfig(); $config = $this->main->getConfig();
$protocol = $config['generic']['protocol'];
$domain = $config['generic']['externaldomain']; $domain = $config['generic']['externaldomain'];
$baseUrl = $protocol . '://' . $domain . '/' . $_user . '/followers'; $baseUrl = 'https://' . $domain . '/' . $_user . '/followers';
$pageSize = 10; $pageSize = 10;
$page = $this->main->extractFromURI('page', ''); $page = $this->main->extractFromURI("page", "");
$id = $baseUrl; $id = $baseUrl;
$items = []; $items = [];
$totalItems = count($followerItems); $totalItems = count($followerItems);
if ($page !== '') { if ($page !== "") {
$pageNum = max(0, (int) $page); $pageNum = max(0, (int) $page);
$offset = (int)($pageNum * $pageSize); $offset = (int)($pageNum * $pageSize);
$pagedItems = array_slice($followerItems, $offset, $pageSize); $pagedItems = array_slice($followerItems, $offset, $pageSize);
@ -88,11 +87,11 @@ class Followers implements \Federator\Api\FedUsers\FedUsersInterface
// Pagination navigation // Pagination navigation
$lastPage = max(0, ceil($totalItems / $pageSize) - 1); $lastPage = max(0, ceil($totalItems / $pageSize) - 1);
if ($page === '' || $followers->getCount() == 0) { if ($page === "" || $followers->getCount() == 0) {
$followers->setFirst($baseUrl . '?page=0'); $followers->setFirst($baseUrl . '?page=0');
$followers->setLast($baseUrl . '?page=' . $lastPage); $followers->setLast($baseUrl . '?page=' . $lastPage);
} }
if ($page !== '') { if ($page !== "") {
$pageNum = max(0, (int) $page); $pageNum = max(0, (int) $page);
if ($pageNum < $lastPage) { if ($pageNum < $lastPage) {
$followers->setNext($baseUrl . '?page=' . ($pageNum + 1)); $followers->setNext($baseUrl . '?page=' . ($pageNum + 1));

View file

@ -43,6 +43,7 @@ class Following implements \Federator\Api\FedUsers\FedUsersInterface
$dbh = $this->main->getDatabase(); $dbh = $this->main->getDatabase();
$cache = $this->main->getCache(); $cache = $this->main->getCache();
$connector = $this->main->getConnector(); $connector = $this->main->getConnector();
// get user // get user
$user = \Federator\DIO\User::getUserByName( $user = \Federator\DIO\User::getUserByName(
$dbh, $dbh,
@ -55,20 +56,19 @@ class Following implements \Federator\Api\FedUsers\FedUsersInterface
} }
$following = new \Federator\Data\ActivityPub\Common\Following(); $following = new \Federator\Data\ActivityPub\Common\Following();
$followingItems = \Federator\DIO\Followers::getFollowingByUser($dbh, $user->id, $connector, $cache); $followingItems = \Federator\DIO\Followers::getFollowingForUser($dbh, $user->id, $connector, $cache);
$config = $this->main->getConfig(); $config = $this->main->getConfig();
$protocol = $config['generic']['protocol'];
$domain = $config['generic']['externaldomain']; $domain = $config['generic']['externaldomain'];
$baseUrl = $protocol . '://' . $domain . '/users/' . $_user . '/following'; $baseUrl = 'https://' . $domain . '/users/' . $_user . '/following';
$pageSize = 10; $pageSize = 10;
$page = $this->main->extractFromURI('page', ''); $page = $this->main->extractFromURI("page", "");
$id = $baseUrl; $id = $baseUrl;
$items = []; $items = [];
$totalItems = count($followingItems); $totalItems = count($followingItems);
if ($page !== '') { if ($page !== "") {
$pageNum = max(0, (int) $page); $pageNum = max(0, (int) $page);
$offset = (int) ($pageNum * $pageSize); $offset = (int) ($pageNum * $pageSize);
$pagedItems = array_slice($followingItems, $offset, $pageSize); $pagedItems = array_slice($followingItems, $offset, $pageSize);
@ -87,11 +87,11 @@ class Following implements \Federator\Api\FedUsers\FedUsersInterface
// Pagination navigation // Pagination navigation
$lastPage = max(0, ceil($totalItems / $pageSize) - 1); $lastPage = max(0, ceil($totalItems / $pageSize) - 1);
if ($page === '' || $following->getCount() == 0) { if ($page === "" || $following->getCount() == 0) {
$following->setFirst($baseUrl . '?page=0'); $following->setFirst($baseUrl . '?page=0');
$following->setLast($baseUrl . '?page=' . $lastPage); $following->setLast($baseUrl . '?page=' . $lastPage);
} }
if ($page !== '') { if ($page !== "") {
$pageNum = max(0, (int) $page); $pageNum = max(0, (int) $page);
if ($pageNum < $lastPage) { if ($pageNum < $lastPage) {
$following->setNext($baseUrl . '?page=' . ($pageNum + 1)); $following->setNext($baseUrl . '?page=' . ($pageNum + 1));

View file

@ -54,8 +54,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
try { try {
$this->main->checkSignature($allHeaders); $this->main->checkSignature($allHeaders);
} catch (\Federator\Exceptions\PermissionDenied $e) { } catch (\Federator\Exceptions\PermissionDenied $e) {
error_log("signature check failed"); throw new \Federator\Exceptions\Unauthorized("Inbox::post Signature check failed: " . $e->getMessage());
throw new \Federator\Exceptions\Unauthorized('Inbox::post Signature check failed: ' . $e->getMessage());
} }
$activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null; $activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
@ -65,18 +64,19 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$connector = $this->main->getConnector(); $connector = $this->main->getConnector();
$config = $this->main->getConfig(); $config = $this->main->getConfig();
if (!is_array($activity)) { if (!is_array($activity)) {
throw new \Federator\Exceptions\ServerError('Inbox::post Input wasn\'t of type array'); throw new \Federator\Exceptions\ServerError("Inbox::post Input wasn't of type array");
} }
$inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity); $inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
if ($inboxActivity === false) { if ($inboxActivity === false) {
throw new \Federator\Exceptions\ServerError('Inbox::post couldn\'t create inboxActivity'); throw new \Federator\Exceptions\ServerError("Inbox::post couldn't create inboxActivity");
} }
$actor = $inboxActivity->getAActor(); // url of the sender https://contentnation.net/username $user = $inboxActivity->getAActor(); // url of the sender https://contentnation.net/username
$username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? '')); $username = basename((string) (parse_url($user, PHP_URL_PATH) ?? ''));
$domain = parse_url($actor, PHP_URL_HOST); $domain = parse_url($user, PHP_URL_HOST);
$userId = $username . '@' . $domain; $userId = $username . '@' . $domain;
$user = \Federator\DIO\FedUser::getUserByName( $user = \Federator\DIO\FedUser::getUserByName(
$dbh, $dbh,
@ -84,8 +84,8 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$cache $cache
); );
if ($user === null || $user->id === null) { if ($user === null || $user->id === null) {
error_log('Inbox::post couldn\'t find user: ' . $userId); error_log("Inbox::post couldn't find user: $userId");
throw new \Federator\Exceptions\ServerError('Inbox::post couldn\'t find user: ' . $userId); throw new \Federator\Exceptions\ServerError("Inbox::post couldn't find user: $userId");
} }
$users = []; $users = [];
@ -132,56 +132,29 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
} }
$ourDomain = $config['generic']['externaldomain']; $ourDomain = $config['generic']['externaldomain'];
$finalReceivers = [];
foreach ($receivers as $receiver) { foreach ($receivers as $receiver) {
if ($receiver === '' || !is_string($receiver)) { if ($receiver === '' || !is_string($receiver)) {
continue; continue;
} }
if (!str_contains($receiver, $ourDomain) && $receiver !== $_user) {
continue;
}
// check if receiver is an actor url from our domain
if ($receiver !== $_user) {
$receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? ''));
$ourDomain = parse_url($receiver, PHP_URL_HOST);
if ($receiverName === null || $ourDomain === null) {
error_log('Inbox::post no receiverName or domain found for receiver: ' . $receiver);
continue;
}
if ($receiverName[0] === '@') {
$receiverName = substr($receiverName, 1);
}
$receiver = $receiverName;
}
$finalReceivers[] = $receiver;
}
$finalReceivers = array_unique($finalReceivers); // remove duplicates
foreach ($finalReceivers as $receiver) {
if (str_ends_with($receiver, '/followers')) { if (str_ends_with($receiver, '/followers')) {
$actor = $inboxActivity->getAActor(); $actor = $inboxActivity->getAActor();
if ($actor === null || !is_string($actor)) { if ($actor === null || !is_string($actor)) {
error_log('Inbox::post no actor found'); error_log("Inbox::post no actor found");
continue; continue;
} }
// Extract username from the actor URL // Extract username from the actor URL
$username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? '')); $username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
$domain = parse_url($actor, PHP_URL_HOST); $domain = parse_url($actor, PHP_URL_HOST);
error_log("url $actor to username $username domain $domain");
if ($username === null || $domain === null) { if ($username === null || $domain === null) {
error_log('Inbox::post no username or domain found for recipient: ' . $receiver); error_log("Inbox::post no username or domain found for recipient: $receiver");
continue; continue;
} }
try { try {
$followers = \Federator\DIO\Followers::getFollowersByFedUser( $followers = \Federator\DIO\Followers::getFollowersByFedUser($dbh, $connector, $cache, $username . '@' . $domain);
$dbh,
$connector,
$cache,
$username . '@' . $domain
);
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log('Inbox::post get followers for user: ' . $username . '@' . $domain . '. Exception: ' error_log("Inbox::post get followers for user: " . $username . '@' . $domain . ". Exception: " . $e->getMessage());
. $e->getMessage());
continue; continue;
} }
@ -189,6 +162,19 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$users = array_merge($users, array_column($followers, 'id')); $users = array_merge($users, array_column($followers, 'id'));
} }
} else { } else {
// check if receiver is an actor url from our domain
if (!str_contains($receiver, $ourDomain) && $receiver !== $_user) {
continue;
}
if ($receiver !== $_user) {
$receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? ''));
$ourDomain = parse_url($receiver, PHP_URL_HOST);
if ($receiverName === null || $ourDomain === null) {
error_log("Inbox::post no receiverName or domain found for receiver: " . $receiver);
continue;
}
$receiver = $receiverName;
}
try { try {
$localUser = \Federator\DIO\User::getUserByName( $localUser = \Federator\DIO\User::getUserByName(
$dbh, $dbh,
@ -197,11 +183,11 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$cache $cache
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log('Inbox::post get user by name: ' . $receiver . '. Exception: ' . $e->getMessage()); error_log("Inbox::post get user by name: " . $receiver . ". Exception: " . $e->getMessage());
continue; continue;
} }
if ($localUser === null || $localUser->id === null) { if ($localUser === null || $localUser->id === null) {
error_log('Inbox::post 210 couldn\'t find user: ' . $receiver); error_log("Inbox::post couldn't find user: $receiver");
continue; continue;
} }
$users[] = $localUser->id; $users[] = $localUser->id;
@ -211,27 +197,15 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$users = array_unique($users); // remove duplicates $users = array_unique($users); // remove duplicates
if (empty($users)) { // todo remove after proper implementation, debugging for now if (empty($users)) { // todo remove after proper implementation, debugging for now
$rootDir = '/tmp/'; $rootDir = PROJECT_ROOT . '/';
// Save the raw input and parsed JSON to a file for inspection // Save the raw input and parsed JSON to a file for inspection
file_put_contents( file_put_contents(
$rootDir . 'logs/inbox.log', $rootDir . 'logs/inbox.log',
date('Y-m-d H:i:s') . ": ==== POST Inbox Activity ====\n" date('Y-m-d H:i:s') . ": ==== POST Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
. json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND FILE_APPEND
); );
} }
// Set the Redis backend for Resque
$rconfig = parse_ini_file($_SERVER['DOCUMENT_ROOT'] . '/../rediscache.ini');
$redisUrl = sprintf(
'redis://%s:%s@%s:%d?password-encoding=u',
urlencode($rconfig['username']),
urlencode($rconfig['password']),
$rconfig['host'],
intval($rconfig['port'], 10)
);
\Resque::setBackend($redisUrl);
foreach ($users as $receiver) { foreach ($users as $receiver) {
if (!isset($receiver)) { if (!isset($receiver)) {
continue; continue;
@ -241,7 +215,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
'recipientId' => $receiver, 'recipientId' => $receiver,
'activity' => $inboxActivity->toObject(), 'activity' => $inboxActivity->toObject(),
]); ]);
error_log('Inbox::post enqueued job for user: ' . $user->id . ' with token: ' . $token); error_log("Inbox::post enqueued job for user: $user->id with token: $token");
} }
if (empty($users)) { if (empty($users)) {
$type = strtolower($inboxActivity->getType()); $type = strtolower($inboxActivity->getType());
@ -251,12 +225,211 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
'recipientId' => "", 'recipientId' => "",
'activity' => $inboxActivity->toObject(), 'activity' => $inboxActivity->toObject(),
]); ]);
error_log('Inbox::post enqueued job for user: ' . $user->id . ' with token: ' . $token); error_log("Inbox::post enqueued job for user: $user->id with token: $token");
} else { } else {
error_log('Inbox::post no users found for activity, doing nothing: ' error_log("Inbox::post no users found for activity, doing nothing: " . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
. json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
} }
} }
return 'success';
$connector->sendActivity($user, $inboxActivity);
return "success";
}
/**
* handle post call for specific user
*
* @param \mysqli $dbh database handle
* @param \Federator\Connector\Connector $connector connector to use
* @param \Federator\Cache\Cache|null $cache optional caching service
* @param string $_user user that triggered the post
* @param string $_recipientId recipient of the post
* @param \Federator\Data\ActivityPub\Common\Activity $inboxActivity the activity that we received
* @return boolean response
*/
public static function postForUser($dbh, $connector, $cache, $_user, $_recipientId, $inboxActivity)
{
if (!isset($_user)) {
error_log("Inbox::postForUser no user given");
return false;
}
// get sender
$user = \Federator\DIO\FedUser::getUserByName(
$dbh,
$_user,
$cache
);
if ($user === null || $user->id === null) {
error_log("Inbox::postForUser couldn't find user: $_user");
return false;
}
$type = strtolower($inboxActivity->getType());
if ($_recipientId === '') {
if ($type === 'undo' || $type === 'delete') {
switch ($type) {
case 'delete':
// Delete Note/Post
$object = $inboxActivity->getObject();
if (is_string($object)) {
\Federator\DIO\Posts::deletePost($dbh, $object);
} elseif (is_object($object)) {
$objectId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $objectId);
} else {
error_log("Inbox::postForUser Error in Delete Post for user $user->id, object is not a string or object");
error_log(" object of type " . gettype($object));
return false;
}
break;
case 'undo':
$object = $inboxActivity->getObject();
if (is_object($object)) {
switch (strtolower($object->getType())) {
case 'like':
case 'dislike':
// Undo Like/Dislike (remove like/dislike)
$targetId = $object->getID();
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike');
\Federator\DIO\Posts::deletePost($dbh, $targetId);
break;
case 'note':
case 'article':
// Undo Note (remove note)
$noteId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $noteId);
break;
}
}
break;
default:
error_log("Inbox::postForUser Unhandled activity type $type for user $user->id");
break;
}
return true;
}
}
$atPos = strpos($_recipientId, '@');
if ($atPos !== false) {
$_recipientId = substr($_recipientId, 0, $atPos);
}
// get recipient
$recipient = \Federator\DIO\User::getUserByName(
$dbh,
$_recipientId,
$connector,
$cache
);
if ($recipient === null || $recipient->id === null) {
error_log("Inbox::postForUser couldn't find recipient: $_recipientId");
return false;
}
$rootDir = PROJECT_ROOT . '/';
// Save the raw input and parsed JSON to a file for inspection
file_put_contents(
$rootDir . 'logs/inbox_' . $recipient->id . '.log',
date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND
);
switch ($type) {
case 'follow':
$success = \Federator\DIO\Followers::addExternalFollow($dbh, $inboxActivity->getID(), $user->id, $recipient->id);
if ($success === false) {
error_log("Inbox::postForUser Failed to add follower for user $user->id");
}
break;
case 'delete':
// Delete Note/Post
$object = $inboxActivity->getObject();
if (is_string($object)) {
\Federator\DIO\Posts::deletePost($dbh, $object);
} elseif (is_object($object)) {
$objectId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $objectId);
}
break;
case 'undo':
$object = $inboxActivity->getObject();
if (is_object($object)) {
switch (strtolower($object->getType())) {
case 'follow':
$success = false;
if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
$actor = $object->getAActor();
if ($actor !== '') {
$success = \Federator\DIO\Followers::removeFollow($dbh, $user->id, $recipient->id);
}
}
if ($success === false) {
error_log("Inbox::postForUser Failed to remove follower for user $user->id");
}
break;
case 'like':
case 'dislike':
// Undo Like/Dislike (remove like/dislike)
$targetId = $object->getID();
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike');
\Federator\DIO\Posts::deletePost($dbh, $targetId);
break;
case 'note':
// Undo Note (remove note)
$noteId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $noteId);
break;
}
}
break;
case 'like':
case 'dislike':
// Add Like/Dislike
$targetId = $inboxActivity->getObject();
if (is_string($targetId)) {
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'dislike');
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
} else {
error_log("Inbox::postForUser Error in Add Like/Dislike for user $user->id, targetId is not a string");
return false;
}
break;
case 'create':
case 'update':
$object = $inboxActivity->getObject();
if (is_object($object)) {
switch (strtolower($object->getType())) {
case 'note':
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
break;
case 'article':
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
break;
default:
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
break;
}
}
break;
default:
error_log("Inbox::postForUser Unhandled activity type $type for user $user->id");
break;
}
return true;
} }
} }

View file

@ -58,7 +58,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
$outbox = new \Federator\Data\ActivityPub\Common\Outbox(); $outbox = new \Federator\Data\ActivityPub\Common\Outbox();
$min = intval($this->main->extractFromURI('min', '0'), 10); $min = intval($this->main->extractFromURI('min', '0'), 10);
$max = intval($this->main->extractFromURI('max', '0'), 10); $max = intval($this->main->extractFromURI('max', '0'), 10);
$page = $this->main->extractFromURI('page', ''); $page = $this->main->extractFromURI("page", "");
if ($page !== "") { if ($page !== "") {
$items = \Federator\DIO\Posts::getPostsByUser($dbh, $user->id, $connector, $cache, $min, $max, 20); $items = \Federator\DIO\Posts::getPostsByUser($dbh, $user->id, $connector, $cache, $min, $max, 20);
$outbox->setItems($items); $outbox->setItems($items);
@ -68,9 +68,8 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
$items = []; $items = [];
} }
$config = $this->main->getConfig(); $config = $this->main->getConfig();
$protocol = $config['generic']['protocol'];
$domain = $config['generic']['externaldomain']; $domain = $config['generic']['externaldomain'];
$id = $protocol . '://' . $domain . '/' . $_user . '/outbox'; $id = 'https://' . $domain . '/' . $_user . '/outbox';
$outbox->setPartOf($id); $outbox->setPartOf($id);
$outbox->setID($id); $outbox->setID($id);
if ($page === '') { if ($page === '') {

View file

@ -20,11 +20,7 @@ class Dummy implements \Federator\Api\APIInterface
*/ */
private $main; private $main;
/** /** @var array<string, string> $message internal message to output */
* internal message to output
*
* @var Array<string, mixed> $message
*/
private $message = []; private $message = [];
/** /**
@ -55,8 +51,11 @@ class Dummy implements \Federator\Api\APIInterface
case 'GET': case 'GET':
switch (sizeof($paths)) { switch (sizeof($paths)) {
case 3: case 3:
if ($paths[2] === 'moo') { switch ($paths[2]) {
return $this->getDummy(); case 'moo':
return $this->getDummy();
default:
break;
} }
break; break;
} }
@ -64,12 +63,14 @@ class Dummy implements \Federator\Api\APIInterface
case 'POST': case 'POST':
switch (sizeof($paths)) { switch (sizeof($paths)) {
case 3: case 3:
if ($paths[2] === 'moo') { switch ($paths[2]) {
return $this->postDummy(); case 'moo':
return $this->postDummy();
default:
break;
} }
break; break;
} }
break;
} }
$this->main->setResponseCode(404); $this->main->setResponseCode(404);
return false; return false;

View file

@ -47,11 +47,10 @@ class NewContent implements \Federator\Api\APIInterface
*/ */
public function exec($paths, $user) public function exec($paths, $user)
{ {
$method = $_SERVER['REQUEST_METHOD']; $method = $_SERVER["REQUEST_METHOD"];
$_username = $paths[2]; $_username = $paths[2];
if ($method === 'GET') { // unsupported if ($method === 'GET') { // unsupported
/// TODO: throw unsupported method exception throw new \Federator\Exceptions\InvalidArgument("GET not supported");
throw new \Federator\Exceptions\InvalidArgument('GET not supported');
} }
switch (sizeof($paths)) { switch (sizeof($paths)) {
case 3: case 3:
@ -68,6 +67,7 @@ class NewContent implements \Federator\Api\APIInterface
return false; return false;
} }
/** /**
* handle post call * handle post call
* *
@ -81,7 +81,7 @@ class NewContent implements \Federator\Api\APIInterface
try { try {
$this->main->checkSignature($allHeaders); $this->main->checkSignature($allHeaders);
} catch (\Federator\Exceptions\PermissionDenied $e) { } catch (\Federator\Exceptions\PermissionDenied $e) {
error_log('NewContent::post Signature check failed: ' . $e->getMessage()); error_log("NewContent::post Signature check failed: " . $e->getMessage());
http_response_code(401); http_response_code(401);
return false; return false;
} }
@ -96,19 +96,19 @@ class NewContent implements \Federator\Api\APIInterface
$config = $this->main->getConfig(); $config = $this->main->getConfig();
$domain = $config['generic']['externaldomain']; $domain = $config['generic']['externaldomain'];
if (!is_array($input)) { if (!is_array($input)) {
error_log('NewContent::post Input wasn\'t of type array'); error_log("NewContent::post Input wasn't of type array");
return false; return false;
} }
$articleId = ''; $articleId = "";
if (isset($allHeaders['X-Sender'])) { if (isset($allHeaders['X-Sender'])) {
$newActivity = $connector->jsonToActivity($input, $articleId); $newActivity = $connector->jsonToActivity($input, $articleId);
} else { } else {
error_log('NewContent::post No X-Sender header found'); error_log("NewContent::post No X-Sender header found");
return false; return false;
} }
if ($newActivity === false) { if ($newActivity === false) {
error_log('NewContent::post couldn\'t create newActivity'); error_log("NewContent::post couldn't create newActivity");
return false; return false;
} }
if (!isset($_user)) { if (!isset($_user)) {
@ -147,15 +147,15 @@ class NewContent implements \Federator\Api\APIInterface
} }
if (str_ends_with($receiver, '/followers')) { if (str_ends_with($receiver, '/followers')) {
if ($posterName === null) { if ($posterName === null) {
error_log('NewContent::post no username found'); error_log("NewContent::post no username found");
continue; continue;
} }
try { try {
$followers = \Federator\DIO\Followers::getFollowersByUser($dbh, $posterName, $connector, $cache); $followers = \Federator\DIO\Followers::getFollowersByUser($dbh, $posterName, $connector, $cache);
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log('NewContent::post get followers for user: ' . $posterName . '. Exception: ' error_log("NewContent::post get followers for user: " . $posterName . ". Exception: " . $e->getMessage());
. $e->getMessage());
continue; continue;
} }
@ -173,7 +173,7 @@ class NewContent implements \Federator\Api\APIInterface
if ($receiver === $posterName) { if ($receiver === $posterName) {
continue; continue;
} }
error_log('NewContent::post no receiverName or domain found for receiver: ' . $receiver); error_log("NewContent::post no receiverName or domain found for receiver: " . $receiver);
continue; continue;
} }
$receiver = $receiverName . '@' . $domain; $receiver = $receiverName . '@' . $domain;
@ -184,11 +184,11 @@ class NewContent implements \Federator\Api\APIInterface
$cache $cache
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log('NewContent::post get user by name: ' . $receiver . '. Exception: ' . $e->getMessage()); error_log("NewContent::post get user by name: " . $receiver . ". Exception: " . $e->getMessage());
continue; continue;
} }
if ($user === null || $user->id === null) { if ($user === null || $user->id === null) {
error_log('NewContent::post couldn\'t find user: ' . $receiver); error_log("NewContent::post couldn't find user: $receiver");
continue; continue;
} }
$users[] = $user->id; $users[] = $user->id;
@ -202,8 +202,7 @@ class NewContent implements \Federator\Api\APIInterface
// Save the raw input and parsed JSON to a file for inspection // Save the raw input and parsed JSON to a file for inspection
file_put_contents( file_put_contents(
$rootDir . 'logs/newContent.log', $rootDir . 'logs/newContent.log',
date('Y-m-d H:i:s') . ": ==== POST NewContent Activity ====\n" date('Y-m-d H:i:s') . ": ==== POST NewContent Activity ====\n" . json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
. json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND FILE_APPEND
); );
} }
@ -219,7 +218,7 @@ class NewContent implements \Federator\Api\APIInterface
'activity' => $newActivity->toObject(), 'activity' => $newActivity->toObject(),
'articleId' => $articleId, 'articleId' => $articleId,
]); ]);
error_log('Inbox::post enqueued job for receiver: ' . $receiver . ' with token: ' . $token); error_log("Inbox::post enqueued job for receiver: $receiver with token: $token");
} }
return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
@ -245,7 +244,7 @@ class NewContent implements \Federator\Api\APIInterface
public static function postForUser($dbh, $connector, $cache, $host, $_user, $_recipientId, $newActivity, $articleId) public static function postForUser($dbh, $connector, $cache, $host, $_user, $_recipientId, $newActivity, $articleId)
{ {
if (!isset($_user)) { if (!isset($_user)) {
error_log('NewContent::postForUser no user given'); error_log("NewContent::postForUser no user given");
return false; return false;
} }
@ -257,7 +256,7 @@ class NewContent implements \Federator\Api\APIInterface
$cache $cache
); );
if ($user === null || $user->id === null) { if ($user === null || $user->id === null) {
error_log('NewContent::postForUser couldn\'t find user: ' . $_user); error_log("NewContent::postForUser couldn't find user: $_user");
return false; return false;
} }
@ -268,7 +267,7 @@ class NewContent implements \Federator\Api\APIInterface
$cache $cache
); );
if ($recipient === null || $recipient->id === null) { if ($recipient === null || $recipient->id === null) {
error_log('NewContent::postForUser couldn\'t find user: ' . $_recipientId); error_log("NewContent::postForUser couldn't find user: $_recipientId");
return false; return false;
} }
@ -276,8 +275,7 @@ class NewContent implements \Federator\Api\APIInterface
// Save the raw input and parsed JSON to a file for inspection // Save the raw input and parsed JSON to a file for inspection
file_put_contents( file_put_contents(
$rootDir . 'logs/newcontent_' . $recipient->id . '.log', $rootDir . 'logs/newcontent_' . $recipient->id . '.log',
date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " NewContent Activity ====\n" date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " NewContent Activity ====\n" . json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
. json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND FILE_APPEND
); );
@ -288,13 +286,14 @@ class NewContent implements \Federator\Api\APIInterface
// $success = false; // $success = false;
$actor = $newActivity->getAActor(); $actor = $newActivity->getAActor();
if ($actor !== '') { if ($actor !== '') {
// $followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
// $followerDomain = parse_url($actor, PHP_URL_HOST);
$newIdUrl = \Federator\DIO\Followers::generateNewFollowId($dbh, $host); $newIdUrl = \Federator\DIO\Followers::generateNewFollowId($dbh, $host);
$newActivity->setID($newIdUrl); $newActivity->setID($newIdUrl);
/*if (is_string($followerDomain)) { /* if (is_string($followerDomain)) {
$followerId = "{$followerUsername}@{$followerDomain}"; $followerId = "{$followerUsername}@{$followerDomain}";
$success = \Federator\DIO\Followers::sendFollowRequest($dbh, $connector, $cache, $user->id, $success = \Federator\DIO\Followers::sendFollowRequest($dbh, $connector, $cache, $user->id, $followerId, $followerDomain);
$followerId, $followerDomain); } */
}*/
} }
/* if ($success === false) { /* if ($success === false) {
error_log("NewContent::postForUser Failed to add follower for user $user->id"); error_log("NewContent::postForUser Failed to add follower for user $user->id");
@ -324,25 +323,20 @@ class NewContent implements \Federator\Api\APIInterface
$followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? '')); $followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
$followerDomain = parse_url($actor, PHP_URL_HOST); $followerDomain = parse_url($actor, PHP_URL_HOST);
if (is_string($followerDomain)) { if (is_string($followerDomain)) {
$followerId = $followerUsername . '@' . $followerDomain; $followerId = "{$followerUsername}@{$followerDomain}";
$removedId = \Federator\DIO\Followers::removeFollow( $removedId = \Federator\DIO\Followers::removeFollow($dbh, $followerId, $user->id);
$dbh,
$followerId,
$user->id
);
if ($removedId !== false) { if ($removedId !== false) {
$object->setID($removedId); $object->setID($removedId);
$newActivity->setObject($object); $newActivity->setObject($object);
$success = true; $success = true;
} else { } else {
error_log('NewContent::postForUser Failed to remove follow for user ' error_log("NewContent::postForUser Failed to remove follow for user $user->id");
. $user->id);
} }
} }
} }
} }
if ($success === false) { if ($success === false) {
error_log('NewContent::postForUser Failed to remove follower for user ' . $user->id); error_log("NewContent::postForUser Failed to remove follower for user $user->id");
} }
break; break;
case 'like': case 'like':
@ -350,10 +344,10 @@ class NewContent implements \Federator\Api\APIInterface
if (method_exists($object, 'getObject')) { if (method_exists($object, 'getObject')) {
$targetId = $object->getObject(); $targetId = $object->getObject();
if (is_string($targetId)) { if (is_string($targetId)) {
\Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId); // \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId);
\Federator\DIO\Posts::deletePost($dbh, $targetId);
} else { } else {
error_log('NewContent::postForUser Error in Undo Like/Dislike for user ' . $user->id error_log("NewContent::postForUser Error in Undo Like/Dislike for user $user->id, targetId is not a string");
. ', targetId is not a string');
} }
} }
break; break;
@ -361,6 +355,7 @@ class NewContent implements \Federator\Api\APIInterface
// Undo Note (remove note) // Undo Note (remove note)
$noteId = $object->getID(); $noteId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $noteId); \Federator\DIO\Posts::deletePost($dbh, $noteId);
break; break;
case 'article': case 'article':
$articleId = $object->getID(); $articleId = $object->getID();
@ -371,8 +366,7 @@ class NewContent implements \Federator\Api\APIInterface
// Undo Article (remove article) // Undo Article (remove article)
$idPart = strrchr($recipient->id, '@'); $idPart = strrchr($recipient->id, '@');
if ($idPart === false) { if ($idPart === false) {
error_log('NewContent::postForUser Error in Undo Article. ' . $recipient->id error_log("NewContent::postForUser Error in Undo Article. $recipient->id, recipient ID is not valid");
. ', recipient ID is not valid');
return false; return false;
} else { } else {
$targetUrl = ltrim($idPart, '@'); $targetUrl = ltrim($idPart, '@');
@ -381,18 +375,16 @@ class NewContent implements \Federator\Api\APIInterface
$object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl); $object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl);
$newActivity->setObject($object); $newActivity->setObject($object);
} else { } else {
error_log('NewContent::postForUser Error in Undo Article for recipient ' error_log("NewContent::postForUser Error in Undo Article for recipient $recipient->id, object is not an Article");
. $recipient->id . ', object is not an Article');
} }
} }
break; break;
} }
} elseif (is_string($object)) { } else if (is_string($object)) {
\Federator\DIO\Posts::deletePost($dbh, $object); \Federator\DIO\Posts::deletePost($dbh, $object);
} else { } else {
error_log('NewContent::postForUser Error in Undo for recipient ' . $recipient->id error_log("NewContent::postForUser Error in Undo for recipient $recipient->id, object is not a string or object");
. ', object is not a string or object');
} }
break; break;
@ -401,11 +393,10 @@ class NewContent implements \Federator\Api\APIInterface
// Add Like/Dislike // Add Like/Dislike
$targetId = $newActivity->getObject(); $targetId = $newActivity->getObject();
if (is_string($targetId)) { if (is_string($targetId)) {
\Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, $type); // \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
// \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId); \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
} else { } else {
error_log('NewContent::postForUser Error in Add Like/Dislike for recipient ' . $recipient->id error_log("NewContent::postForUser Error in Add Like/Dislike for recipient $recipient->id, targetId is not a string");
. ', targetId is not a string');
return false; return false;
} }
break; break;
@ -424,8 +415,7 @@ class NewContent implements \Federator\Api\APIInterface
$idPart = strrchr($recipient->id, '@'); $idPart = strrchr($recipient->id, '@');
if ($idPart === false) { if ($idPart === false) {
error_log('NewContent::postForUser Error in Create/Update Article. ' . $recipient->id error_log("NewContent::postForUser Error in Create/Update Article. $recipient->id, recipient ID is not valid");
. ', recipient ID is not valid');
return false; return false;
} else { } else {
$targetUrl = ltrim($idPart, '@'); $targetUrl = ltrim($idPart, '@');
@ -434,8 +424,7 @@ class NewContent implements \Federator\Api\APIInterface
$object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl); $object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl);
$newActivity->setObject($object); $newActivity->setObject($object);
} else { } else {
error_log('NewContent::postForUser Error in Create/Update Article for recipient ' error_log("NewContent::postForUser Error in Create/Update Article for recipient $recipient->id, object is not an Article");
. $recipient->id . ', object is not an Article');
} }
} }
@ -449,26 +438,117 @@ class NewContent implements \Federator\Api\APIInterface
break; break;
default: default:
error_log('NewContent::postForUser Unhandled activity type $type for user ' . $user->id); error_log("NewContent::postForUser Unhandled activity type $type for user $user->id");
break; break;
} }
try { try {
$response = \Federator\DIO\Server::sendActivity($dbh, $host, $user, $recipient, $newActivity); $response = self::sendActivity($dbh, $host, $user, $recipient, $newActivity);
} catch (\Exception $e) { } catch (\Exception $e) {
error_log('NewContent::postForUser Failed to send activity: ' . $e->getMessage()); error_log("NewContent::postForUser Failed to send activity: " . $e->getMessage());
return false; return false;
} }
if (empty($response)) { if (empty($response)) {
error_log('NewContent::postForUser Sent activity to ' . $recipient->id); error_log("NewContent::postForUser Sent activity to $recipient->id");
} else { } else {
error_log('NewContent::postForUser Sent activity to ' . $recipient->id . ' with response: ' error_log("NewContent::postForUser Sent activity to $recipient->id with response: " . json_encode($response, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
. json_encode($response, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
} }
return true; return true;
} }
/**
* send activity to federated server
*
* @param \mysqli $dbh database handle
* @param string $host host url of our server (e.g. federator)
* @param \Federator\Data\User $sender source user
* @param \Federator\Data\FedUser $target federated target user
* @param \Federator\Data\ActivityPub\Common\Activity $activity activity to send
* @return string|true the generated follow ID on success, false on failure
*/
public static function sendActivity($dbh, $host, $sender, $target, $activity)
{
if ($dbh === false) {
throw new \Federator\Exceptions\ServerError("NewContent::sendActivity Failed to get database handle");
}
$inboxUrl = $target->inboxURL;
$json = json_encode($activity, JSON_UNESCAPED_SLASHES);
if ($json === false) {
throw new \Exception('Failed to encode JSON: ' . json_last_error_msg());
}
$digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true));
$date = gmdate('D, d M Y H:i:s') . ' GMT';
$parsed = parse_url($inboxUrl);
if ($parsed === false) {
throw new \Exception('Failed to parse URL: ' . $inboxUrl);
}
if (!isset($parsed['host']) || !isset($parsed['path'])) {
throw new \Exception('Invalid inbox URL: missing host or path');
}
$extHost = $parsed['host'];
$path = $parsed['path'];
// Build the signature string
$signatureString = "(request-target): post {$path}\n" .
"host: {$extHost}\n" .
"date: {$date}\n" .
"digest: {$digest}";
// Get rsa private key
$privateKey = \Federator\DIO\User::getrsaprivate($dbh, $sender->id); // OR from DB
if ($privateKey === false) {
throw new \Exception('Failed to get private key');
}
$pkeyId = openssl_pkey_get_private($privateKey);
if ($pkeyId === false) {
throw new \Exception('Invalid private key');
}
openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
$signature_b64 = base64_encode($signature);
// Build keyId (public key ID from your actor object)
$keyId = $host . '/' . $sender->id . '#main-key';
$signatureHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
$ch = curl_init($inboxUrl);
if ($ch === false) {
throw new \Exception('Failed to initialize cURL');
}
$headers = [
'Host: ' . $extHost,
'Date: ' . $date,
'Digest: ' . $digest,
'Content-Type: application/activity+json',
'Signature: ' . $signatureHeader,
'Accept: application/activity+json',
];
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
curl_close($ch);
if ($response === false) {
throw new \Exception("Failed to send activity: " . curl_error($ch));
} else {
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpcode != 200 && $httpcode != 202) {
throw new \Exception("Unexpected HTTP code $httpcode: $response");
}
}
return $response;
}
/** /**
* get internal represenation as json string * get internal represenation as json string
* @return string json string or html * @return string json string or html

View file

@ -61,7 +61,7 @@ class WellKnown implements APIInterface
*/ */
public function exec($paths, $user) public function exec($paths, $user)
{ {
$method = $_SERVER['REQUEST_METHOD']; $method = $_SERVER["REQUEST_METHOD"];
switch ($method) { switch ($method) {
case 'GET': case 'GET':
switch (sizeof($paths)) { switch (sizeof($paths)) {

View file

@ -67,6 +67,7 @@ class NodeInfo
$template = 'nodeinfo2.0.json'; $template = 'nodeinfo2.0.json';
} }
$stats = \Federator\DIO\Stats::getStats($this->main); $stats = \Federator\DIO\Stats::getStats($this->main);
echo "fetch usercount via connector\n";
$data['usercount'] = $stats->userCount; $data['usercount'] = $stats->userCount;
$data['postcount'] = $stats->postCount; $data['postcount'] = $stats->postCount;
$data['commentcount'] = $stats->commentCount; $data['commentcount'] = $stats->commentCount;

View file

@ -48,10 +48,10 @@ class WebFinger
$matches = []; $matches = [];
$config = $this->main->getConfig(); $config = $this->main->getConfig();
$domain = $config['generic']['externaldomain']; $domain = $config['generic']['externaldomain'];
$sourcedomain = $config['generic']['sourcedomain']; if (preg_match("/^acct:([^@]+)@(.*)$/", $_resource, $matches) != 1 || $matches[2] !== $domain) {
if (preg_match("/^acct:([^@]+)@(.*)$/", $_resource, $matches) != 1 || ($matches[2] !== $sourcedomain && $matches[2] !== $domain)) {
throw new \Federator\Exceptions\InvalidArgument(); throw new \Federator\Exceptions\InvalidArgument();
} }
$domain = $matches[2];
$user = \Federator\DIO\User::getUserByName( $user = \Federator\DIO\User::getUserByName(
$this->main->getDatabase(), $this->main->getDatabase(),
$matches[1], $matches[1],
@ -59,12 +59,12 @@ class WebFinger
$this->main->getCache() $this->main->getCache()
); );
if ($user->id == 0) { if ($user->id == 0) {
echo "not found";
throw new \Federator\Exceptions\FileNotFound(); throw new \Federator\Exceptions\FileNotFound();
} }
$data = [ $data = [
'username' => $user->id, 'username' => $user->id,
'domain' => $domain, 'domain' => $domain,
'sourcedomain' => $sourcedomain,
]; ];
$response = $this->main->renderTemplate('webfinger_acct.json', $data); $response = $this->main->renderTemplate('webfinger_acct.json', $data);
$this->wellKnown->setResponse($response); $this->wellKnown->setResponse($response);

View file

@ -20,7 +20,7 @@ interface Cache extends \Federator\Connector\Connector
* @param \Federator\Data\FedUser[]|false $followers user followers * @param \Federator\Data\FedUser[]|false $followers user followers
* @return void * @return void
*/ */
public function saveFollowersByUser($user, $followers); public function saveRemoteFollowersOfUser($user, $followers);
/** /**
* save remote following for user * save remote following for user
@ -29,7 +29,7 @@ interface Cache extends \Federator\Connector\Connector
* @param \Federator\Data\FedUser[]|false $following user following * @param \Federator\Data\FedUser[]|false $following user following
* @return void * @return void
*/ */
public function saveFollowingByUser($user, $following); public function saveRemoteFollowingForUser($user, $following);
/** /**
* save remote posts by user * save remote posts by user

View file

@ -17,17 +17,19 @@ interface Connector
* get followers of given user * get followers of given user
* *
* @param string $id user id * @param string $id user id
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getFollowersByUser($id); public function getRemoteFollowersOfUser($id);
/** /**
* get following of given user * get following of given user
* *
* @param string $id user id * @param string $id user id
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getFollowingByUser($id); public function getRemoteFollowingForUser($id);
/** /**
* get posts by given user * get posts by given user
@ -36,6 +38,7 @@ interface Connector
* @param int $min min value * @param int $min min value
* @param int $max max value * @param int $max max value
* @param int $limit maximum number of results * @param int $limit maximum number of results
* @return \Federator\Data\ActivityPub\Common\Activity[]|false * @return \Federator\Data\ActivityPub\Common\Activity[]|false
*/ */
public function getRemotePostsByUser($id, $min, $max, $limit); public function getRemotePostsByUser($id, $min, $max, $limit);

View file

@ -10,7 +10,7 @@ namespace Federator\Data\ActivityPub\Common;
class Collection extends APObject class Collection extends APObject
{ {
protected int $totalItems = -1; protected int $totalItems = 0;
private string $first = ''; private string $first = '';
private string $last = ''; private string $last = '';
@ -28,7 +28,7 @@ class Collection extends APObject
{ {
$return = parent::toObject(); $return = parent::toObject();
$return['type'] = 'Collection'; $return['type'] = 'Collection';
if ($this->totalItems >= 0) { if ($this->totalItems > 0) {
$return['totalItems'] = $this->totalItems; $return['totalItems'] = $this->totalItems;
} }
if ($this->first !== '') { if ($this->first !== '') {

View file

@ -49,14 +49,15 @@ class Article
*/ */
public static function conditionalConvertToNote($article, $targetUrl) public static function conditionalConvertToNote($article, $targetUrl)
{ {
$supportFile = file_get_contents($_SERVER['DOCUMENT_ROOT'] . '../formatsupport.json'); $supportFile = file_get_contents(PROJECT_ROOT . '/formatsupport.json');
if ($supportFile === false) { if ($supportFile === false) {
error_log("Article::conditionalConvertToNote Failed to read support file for article conversion."); error_log("Article::conditionalConvertToNote Failed to read support file for article conversion.");
return $article; // Fallback to original article if file read fails return $article; // Fallback to original article if file read fails
} }
$supportlist = json_decode($supportFile, true); $supportlist = json_decode($supportFile, true);
if (!isset($supportlist['activitypub']['article']) || if (
!isset($supportlist['activitypub']['article']) ||
!is_array($supportlist['activitypub']['article']) || !is_array($supportlist['activitypub']['article']) ||
!in_array($targetUrl, $supportlist['activitypub']['article'], true) !in_array($targetUrl, $supportlist['activitypub']['article'], true)
) { ) {

View file

@ -20,15 +20,15 @@ class FedUser
* @param string $_user user/profile name * @param string $_user user/profile name
* @return void * @return void
*/ */
protected static function addUserToDB($dbh, $user, $_user) protected static function addLocalUser($dbh, $user, $_user)
{ {
// check if it is timed out user // check if it is timed out user
$sql = 'select unix_timestamp(`validuntil`) from fedusers where id=?'; $sql = 'select unix_timestamp(`validuntil`) from fedusers where id=?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('FedUser::addLocalUser Failed to prepare statement'); throw new \Federator\Exceptions\ServerError("FedUser::addLocalUser Failed to prepare statement");
} }
$stmt->bind_param('s', $_user); $stmt->bind_param("s", $_user);
$validuntil = 0; $validuntil = 0;
$ret = $stmt->bind_result($validuntil); $ret = $stmt->bind_result($validuntil);
$stmt->execute(); $stmt->execute();
@ -42,10 +42,10 @@ class FedUser
$sql .= ' values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now() + interval 1 day)'; $sql .= ' values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now() + interval 1 day)';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('FedUser::addLocalUser Failed to prepare create statement'); throw new \Federator\Exceptions\ServerError("FedUser::addLocalUser Failed to prepare create statement");
} }
$stmt->bind_param( $stmt->bind_param(
'ssssssssssss', "ssssssssssss",
$_user, $_user,
$user->actorURL, $user->actorURL,
$user->name, $user->name,
@ -66,10 +66,10 @@ class FedUser
$sql .= ' where id=?'; $sql .= ' where id=?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('FedUser::extendUser Failed to prepare update statement'); throw new \Federator\Exceptions\ServerError("FedUser::extendUser Failed to prepare update statement");
} }
$stmt->bind_param( $stmt->bind_param(
'ssssssssssss', "ssssssssssss",
$user->actorURL, $user->actorURL,
$user->name, $user->name,
$user->publicKey, $user->publicKey,
@ -106,9 +106,9 @@ class FedUser
$sql = 'select id,unix_timestamp(`validuntil`) from fedusers where id=?'; $sql = 'select id,unix_timestamp(`validuntil`) from fedusers where id=?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('FedUser::extendUser Failed to prepare statement'); throw new \Federator\Exceptions\ServerError("FedUser::extendUser Failed to prepare statement");
} }
$stmt->bind_param('s', $_user); $stmt->bind_param("s", $_user);
$validuntil = 0; $validuntil = 0;
$ret = $stmt->bind_result($user->id, $validuntil); $ret = $stmt->bind_result($user->id, $validuntil);
$stmt->execute(); $stmt->execute();
@ -118,7 +118,7 @@ class FedUser
$stmt->close(); $stmt->close();
// if a new user, create own database entry with additionally needed info // if a new user, create own database entry with additionally needed info
if ($user->id === null || $validuntil < time()) { if ($user->id === null || $validuntil < time()) {
self::addUserToDB($dbh, $user, $_user); self::addLocalUser($dbh, $user, $_user);
} }
// no further processing for now // no further processing for now
@ -147,14 +147,14 @@ class FedUser
return $user; return $user;
} }
// check our db // check our db
$sql = 'select `id`, `url`, `name`, `publickey`, `summary`, `type`, `inboxurl`, `sharedinboxurl`, '; $sql = 'select `id`, `url`, `name`, `publickey`, `summary`, `type`, `inboxurl`, `sharedinboxurl`, `followersurl`,';
$sql .= '`followersurl`, `followingurl`, `publickeyid`, `outboxurl`'; $sql .= ' `followingurl`, `publickeyid`, `outboxurl`';
$sql .= ' from fedusers where `id`=? and `validuntil`>=now()'; $sql .= ' from fedusers where `id`=? and `validuntil`>=now()';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to prepare statement'); throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to prepare statement");
} }
$stmt->bind_param('s', $_name); $stmt->bind_param("s", $_name);
$user = new \Federator\Data\FedUser(); $user = new \Federator\Data\FedUser();
$ret = $stmt->bind_result( $ret = $stmt->bind_result(
$user->id, $user->id,
@ -184,13 +184,11 @@ class FedUser
$headers = ['Accept: application/activity+json']; $headers = ['Accept: application/activity+json'];
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers); [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to fetch webfinger for ' throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to fetch webfinger for " . $_name);
. $_name);
} }
$r = json_decode($response, true); $r = json_decode($response, true);
if ($r === false || $r === null || !is_array($r)) { if ($r === false || $r === null || !is_array($r)) {
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to decode webfinger for ' throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to decode webfinger for " . $_name);
. $_name);
} }
// get the webwinger user url and fetch the user // get the webwinger user url and fetch the user
if (isset($r['links'])) { if (isset($r['links'])) {
@ -202,268 +200,43 @@ class FedUser
} }
} }
if (!isset($remoteURL)) { if (!isset($remoteURL)) {
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to find self link ' throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to find self link in webfinger for " . $_name);
. 'in webfinger for ' . $_name);
} }
} else { // fetch the user
$remoteURL = $_name; $headers = ['Accept: application/activity+json'];
} [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
// fetch the user if ($info['http_code'] != 200) {
$headers = ['Accept: application/activity+json']; throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to fetch user from remoteUrl for " . $_name);
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
if ($info['http_code'] != 200) {
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to fetch user from '
. 'remoteUrl for ' . $_name);
}
$r = json_decode($response, true);
if ($r === false || $r === null || !is_array($r)) {
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to decode user for '
. $_name);
}
$r['publicKeyId'] = $r['publicKey']['id'];
$r['publicKey'] = $r['publicKey']['publicKeyPem'];
if (isset($r['endpoints'])) {
if (isset($r['endpoints']['sharedInbox'])) {
$r['sharedInbox'] = $r['endpoints']['sharedInbox'];
} }
$r = json_decode($response, true);
if ($r === false || $r === null || !is_array($r)) {
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to decode user for " . $_name);
}
$r['publicKeyId'] = $r['publicKey']['id'];
$r['publicKey'] = $r['publicKey']['publicKeyPem'];
if (isset($r['endpoints'])) {
if (isset($r['endpoints']['sharedInbox'])) {
$r['sharedInbox'] = $r['endpoints']['sharedInbox'];
}
}
$r['actorURL'] = $remoteURL;
$data = json_encode($r);
if ($data === false) {
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to encode userdata " . $_name);
}
$user = \Federator\Data\FedUser::createFromJson($data);
} }
$r['actorURL'] = $remoteURL;
$data = json_encode($r);
if ($data === false) {
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to encode userdata '
. $_name);
}
$user = \Federator\Data\FedUser::createFromJson($data);
} }
if ($cache !== null && $user !== false) { if ($cache !== null && $user !== false) {
if ($user->id !== null && $user->actorURL !== null) { if ($user->id !== null && $user->actorURL !== null) {
self::addUserToDB($dbh, $user, $_name); self::addLocalUser($dbh, $user, $_name);
} }
$cache->saveRemoteFedUserByName($_name, $user); $cache->saveRemoteFedUserByName($_name, $user);
} }
if ($user === false) { if ($user === false) {
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName User not found'); throw new \Federator\Exceptions\ServerError("FedUser::getUserByName User not found");
} }
return $user; return $user;
} }
/**
* handle post call for specific user
*
* @param \Federator\Main $main main instance
* @param \mysqli $dbh database handle
* @param \Federator\Connector\Connector $connector connector to use
* @param \Federator\Cache\Cache|null $cache optional caching service
* @param string $_user user that triggered the post
* @param string $_recipientId recipient of the post
* @param \Federator\Data\ActivityPub\Common\Activity $inboxActivity the activity that we received
* @return boolean response
*/
public static function inboxForUser($main, $dbh, $connector, $cache, $_user, $_recipientId, $inboxActivity)
{
if (!isset($_user)) {
error_log('Inbox::postForUser no user given');
return false;
}
// get sender
$user = \Federator\DIO\FedUser::getUserByName(
$dbh,
$_user,
$cache
);
if ($user === null || $user->id === null) {
error_log('Inbox::postForUser couldn\'t find user: ' . $_user);
return false;
}
$type = strtolower($inboxActivity->getType());
if ($_recipientId === '') {
if ($type === 'undo' || $type === 'delete') {
switch ($type) {
case 'delete':
// Delete Note/Post
$object = $inboxActivity->getObject();
if (is_string($object)) {
\Federator\DIO\Posts::deletePost($dbh, $object);
} elseif (is_object($object)) {
$objectId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $objectId);
} else {
error_log('Inbox::postForUser Error in Delete Post for user ' . $user->id
. ', object is not a string or object');
error_log(' object of type ' . gettype($object));
return false;
}
break;
case 'undo':
$object = $inboxActivity->getObject();
if (is_object($object)) {
switch (strtolower($object->getType())) {
case 'like':
case 'dislike':
// Undo Like/Dislike (remove like/dislike)
$targetId = $object->getID();
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike');
\Federator\DIO\Posts::deletePost($dbh, $targetId);
break;
case 'note':
case 'article':
// Undo Note (remove note)
$noteId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $noteId);
break;
}
}
break;
default:
error_log('Inbox::postForUser Unhandled activity type ' . $type . ' for user ' . $user->id);
break;
}
return true;
}
}
$atPos = strpos($_recipientId, '@');
if ($atPos !== false) {
$_recipientId = substr($_recipientId, 0, $atPos);
}
// get recipient
$recipient = \Federator\DIO\User::getUserByName(
$dbh,
$_recipientId,
$connector,
$cache
);
if ($recipient === null || $recipient->id === null) {
error_log('Inbox::postForUser couldn\'t find recipient: ' . $_recipientId);
return false;
}
$rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
// Save the raw input and parsed JSON to a file for inspection
file_put_contents(
$rootDir . 'logs/inbox_' . $recipient->id . '.log',
date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " Inbox Activity ====\n"
. json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND
);
switch ($type) {
case 'follow':
$success = \Federator\DIO\Followers::addExternalFollow(
$dbh,
$inboxActivity->getID(),
$user->id,
$recipient->id
);
if ($success === true) {
// send accept back
$accept = new \Federator\Data\ActivityPub\Common\Accept();
$local = $inboxActivity->getObject();
if (is_string($local)) {
$accept->setAActor($local);
$id = bin2hex(openssl_random_pseudo_bytes(4));
$accept->setID($local . '#accepts/follows/' . $id);
$obj = new \Federator\Data\ActivityPub\Common\Activity($inboxActivity->getType());
$config = $main->getConfig();
$ourhost = $config['generic']['protocol'] . '://' . $config['generic']['externaldomain'];
$obj->setID($ourhost . '/' . $id);
$obj->setAActor($inboxActivity->getAActor());
$obj->setObject($local);
$accept->setObject($obj);
// send
\Federator\DIO\Server::sendActivity($dbh, $ourhost, $recipient, $user, $accept);
}
} else {
error_log('Inbox::postForUser Failed to add follower for user ' . $user->id);
}
break;
case 'delete':
// Delete Note/Post
$object = $inboxActivity->getObject();
if (is_string($object)) {
\Federator\DIO\Posts::deletePost($dbh, $object);
} elseif (is_object($object)) {
$objectId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $objectId);
}
break;
case 'undo':
$object = $inboxActivity->getObject();
if (is_object($object)) {
switch (strtolower($object->getType())) {
case 'follow':
$success = false;
if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
$actor = $object->getAActor();
if ($actor !== '') {
$success = \Federator\DIO\Followers::removeFollow($dbh, $user->id, $recipient->id);
}
}
if ($success === false) {
error_log('Inbox::postForUser Failed to remove follower for user ' . $user->id);
}
break;
case 'like':
case 'dislike':
// Undo Like/Dislike (remove like/dislike)
$targetId = $object->getID();
\Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId);
// \Federator\DIO\Posts::deletePost($dbh, $targetId);
break;
case 'note':
// Undo Note (remove note)
$noteId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $noteId);
break;
}
}
break;
case 'like':
case 'dislike':
// Add Like/Dislike
$targetId = $inboxActivity->getObject();
if (is_string($targetId)) {
\Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, $type);
} else {
error_log('Inbox::postForUser Error in Add Like/Dislike for user ' . $user->id
. ', targetId is not a string');
return false;
}
break;
case 'create':
case 'update':
$object = $inboxActivity->getObject();
if (is_object($object)) {
switch (strtolower($object->getType())) {
case 'note':
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
break;
case 'article':
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
break;
default:
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
break;
}
}
break;
default:
error_log('Inbox::postForUser Unhandled activity type $type for user ' . $user->id);
break;
}
return true;
}
} }

View file

@ -30,7 +30,7 @@ class Followers
{ {
// ask cache // ask cache
if ($cache !== null) { if ($cache !== null) {
$followers = $cache->getFollowersByUser($id); $followers = $cache->getRemoteFollowersOfUser($id);
if ($followers !== false) { if ($followers !== false) {
return $followers; return $followers;
} }
@ -39,9 +39,9 @@ class Followers
$sql = 'select source_user from follows where target_user = ?'; $sql = 'select source_user from follows where target_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('Followers::getFollowersByUser Failed to prepare statement'); throw new \Federator\Exceptions\ServerError("Followers::getFollowersByUser Failed to prepare statement");
} }
$stmt->bind_param('s', $id); $stmt->bind_param("s", $id);
$stmt->execute(); $stmt->execute();
$followerIds = []; $followerIds = [];
$stmt->bind_result($sourceUser); $stmt->bind_result($sourceUser);
@ -57,7 +57,7 @@ class Followers
$cache, $cache,
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log('Followers::getFollowersByUser Exception: ' . $e->getMessage()); error_log("Followers::getFollowersByUser Exception: " . $e->getMessage());
continue; // Skip this user if an exception occurs continue; // Skip this user if an exception occurs
} }
if ($user !== false && $user->id !== null) { if ($user !== false && $user->id !== null) {
@ -67,14 +67,14 @@ class Followers
if ($followers === []) { if ($followers === []) {
// ask connector for user-id // ask connector for user-id
$followers = $connector->getFollowersByUser($id); $followers = $connector->getRemoteFollowersOfUser($id);
if ($followers === false) { if ($followers === false) {
$followers = []; $followers = [];
} }
} }
// save followers to cache // save followers to cache
if ($cache !== null) { if ($cache !== null) {
$cache->saveFollowersByUser($id, $followers); $cache->saveRemoteFollowersOfUser($id, $followers);
} }
return $followers; return $followers;
} }
@ -91,11 +91,12 @@ class Followers
* optional caching service * optional caching service
* @return \Federator\Data\FedUser[] * @return \Federator\Data\FedUser[]
*/ */
public static function getFollowingByUser($dbh, $id, $connector, $cache)
public static function getFollowingForUser($dbh, $id, $connector, $cache)
{ {
// ask cache // ask cache
if ($cache !== null) { if ($cache !== null) {
$following = $cache->getFollowingByUser($id); $following = $cache->getRemoteFollowingForUser($id);
if ($following !== false) { if ($following !== false) {
return $following; return $following;
} }
@ -104,9 +105,9 @@ class Followers
$sql = 'select target_user from follows where source_user = ?'; $sql = 'select target_user from follows where source_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('Followers::getFollowingForUser Failed to prepare statement'); throw new \Federator\Exceptions\ServerError("Followers::getFollowingForUser Failed to prepare statement");
} }
$stmt->bind_param('s', $id); $stmt->bind_param("s", $id);
$stmt->execute(); $stmt->execute();
$followingIds = []; $followingIds = [];
$stmt->bind_result($sourceUser); $stmt->bind_result($sourceUser);
@ -122,7 +123,7 @@ class Followers
$cache, $cache,
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log('Followers::getFollowingByUser Exception: ' . $e->getMessage()); error_log("Followers::getFollowingForUser Exception: " . $e->getMessage());
continue; // Skip this user if an exception occurs continue; // Skip this user if an exception occurs
} }
if ($user !== false && $user->id !== null) { if ($user !== false && $user->id !== null) {
@ -132,14 +133,14 @@ class Followers
if ($following === []) { if ($following === []) {
// ask connector for user-id // ask connector for user-id
$following = $connector->getFollowingByUser($id); $following = $connector->getRemoteFollowingForUser($id);
if ($following === false) { if ($following === false) {
$following = []; $following = [];
} }
} }
// save posts to DB // save posts to DB
if ($cache !== null) { if ($cache !== null) {
$cache->saveFollowingByUser($id, $following); $cache->saveRemoteFollowingForUser($id, $following);
} }
return $following; return $following;
} }
@ -165,7 +166,7 @@ class Followers
$sql = 'select source_user from follows where target_user = ?'; $sql = 'select source_user from follows where target_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('Followers::getFollowersByFedUser Failed to prepare statement'); throw new \Federator\Exceptions\ServerError("Followers::getFollowersByFedUser Failed to prepare statement");
} }
$stmt->bind_param("s", $id); $stmt->bind_param("s", $id);
$stmt->execute(); $stmt->execute();
@ -183,7 +184,7 @@ class Followers
$cache $cache
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log('Followers::getFollowersByFedUser Exception: ' . $e->getMessage()); error_log("Followers::getFollowersByFedUser Exception: " . $e->getMessage());
continue; // Skip this user if an exception occurs continue; // Skip this user if an exception occurs
} }
if ($user !== false && $user->id !== null) { if ($user !== false && $user->id !== null) {
@ -207,7 +208,7 @@ class Followers
public static function sendFollowRequest($dbh, $connector, $cache, $_user, $_targetUser, $host) public static function sendFollowRequest($dbh, $connector, $cache, $_user, $_targetUser, $host)
{ {
if ($dbh === false) { if ($dbh === false) {
throw new \Federator\Exceptions\ServerError('Followers::sendFollowRequest Failed to get database handle'); throw new \Federator\Exceptions\ServerError("Followers::sendFollowRequest Failed to get database handle");
} }
$user = \Federator\DIO\User::getUserByName( $user = \Federator\DIO\User::getUserByName(
$dbh, $dbh,
@ -288,8 +289,7 @@ class Followers
// Build keyId (public key ID from your actor object) // Build keyId (public key ID from your actor object)
$keyId = 'https://' . $host . '/' . $user->id . '#main-key'; $keyId = 'https://' . $host . '/' . $user->id . '#main-key';
$signatureHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest"' $signatureHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
. ',signature="' . $signature_b64 . '"';
$ch = curl_init($inboxUrl); $ch = curl_init($inboxUrl);
if ($ch === false) { if ($ch === false) {
@ -315,12 +315,12 @@ class Followers
// Log the response for debugging if needed // Log the response for debugging if needed
if ($response === false) { if ($response === false) {
self::removeFollow($dbh, $sourceUser, $fedUser->id); self::removeFollow($dbh, $sourceUser, $fedUser->id);
throw new \Exception('Failed to send Follow activity: ' . curl_error($ch)); throw new \Exception("Failed to send Follow activity: " . curl_error($ch));
} else { } else {
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpcode != 200 && $httpcode != 202) { if ($httpcode != 200 && $httpcode != 202) {
self::removeFollow($dbh, $sourceUser, $fedUser->id); self::removeFollow($dbh, $sourceUser, $fedUser->id);
throw new \Exception('Unexpected HTTP code ' . $httpcode . ':' . $response); throw new \Exception("Unexpected HTTP code $httpcode: $response");
} }
} }
return $idUrl; return $idUrl;
@ -341,9 +341,9 @@ class Followers
$sql = 'select id from follows where source_user = ? and target_user = ?'; $sql = 'select id from follows where source_user = ? and target_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('Followers::addFollow Failed to prepare statement'); throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare statement");
} }
$stmt->bind_param('ss', $sourceUser, $targetUserId); $stmt->bind_param("ss", $sourceUser, $targetUserId);
$foundId = 0; $foundId = 0;
$ret = $stmt->bind_result($foundId); $ret = $stmt->bind_result($foundId);
$stmt->execute(); $stmt->execute();
@ -364,10 +364,9 @@ class Followers
$sql = 'select id from follows where id = ?'; $sql = 'select id from follows where id = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('Followers::addFollow Failed to prepare id-check' throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare id-check statement");
. 'statement');
} }
$stmt->bind_param('s', $idurl); $stmt->bind_param("s", $idurl);
$foundId = 0; $foundId = 0;
$ret = $stmt->bind_result($foundId); $ret = $stmt->bind_result($foundId);
$stmt->execute(); $stmt->execute();
@ -381,9 +380,9 @@ class Followers
$sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())'; $sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('Followers::addFollow Failed to prepare insert statement'); throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare insert statement");
} }
$stmt->bind_param('sss', $idurl, $sourceUser, $targetUserId); $stmt->bind_param("sss", $idurl, $sourceUser, $targetUserId);
$stmt->execute(); $stmt->execute();
$stmt->close(); $stmt->close();
return $idurl; // Return the generated follow ID return $idurl; // Return the generated follow ID
@ -404,9 +403,9 @@ class Followers
$sql = 'select id from follows where source_user = ? and target_user = ?'; $sql = 'select id from follows where source_user = ? and target_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('Followers::addExternalFollow Failed to prepare statement'); throw new \Federator\Exceptions\ServerError("Followers::addExternalFollow Failed to prepare statement");
} }
$stmt->bind_param('ss', $sourceUserId, $targetUserId); $stmt->bind_param("ss", $sourceUserId, $targetUserId);
$foundId = 0; $foundId = 0;
$ret = $stmt->bind_result($foundId); $ret = $stmt->bind_result($foundId);
$stmt->execute(); $stmt->execute();
@ -422,10 +421,9 @@ class Followers
$sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())'; $sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('Followers::addExternalFollow Failed to prepare insert ' throw new \Federator\Exceptions\ServerError("Followers::addExternalFollow Failed to prepare insert statement");
. 'statement');
} }
$stmt->bind_param('sss', $followId, $sourceUserId, $targetUserId); $stmt->bind_param("sss", $followId, $sourceUserId, $targetUserId);
$stmt->execute(); $stmt->execute();
$stmt->close(); $stmt->close();
return true; return true;
@ -449,10 +447,9 @@ class Followers
$sql = 'select id from follows where id = ?'; $sql = 'select id from follows where id = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('Followers::generateNewFollowId Failed to prepare id-check' throw new \Federator\Exceptions\ServerError("Followers::generateNewFollowId Failed to prepare id-check statement");
. ' statement');
} }
$stmt->bind_param('s', $newIdUrl); $stmt->bind_param("s", $newIdUrl);
$foundId = 0; $foundId = 0;
$ret = $stmt->bind_result($foundId); $ret = $stmt->bind_result($foundId);
$stmt->execute(); $stmt->execute();
@ -479,7 +476,7 @@ class Followers
$sql = 'delete from follows where source_user = ? and target_user = ? RETURNING id'; $sql = 'delete from follows where source_user = ? and target_user = ? RETURNING id';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt !== false) { if ($stmt !== false) {
$stmt->bind_param('ss', $sourceUser, $targetUserId); $stmt->bind_param("ss", $sourceUser, $targetUserId);
if ($stmt->execute()) { if ($stmt->execute()) {
$stmt->bind_result($followId); $stmt->bind_result($followId);
if ($stmt->fetch() === true) { if ($stmt->fetch() === true) {
@ -498,10 +495,9 @@ class Followers
$sql = 'select id from follows where source_user = ? and target_user = ?'; $sql = 'select id from follows where source_user = ? and target_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('Followers::removeFollow Failed to prepare select ' throw new \Federator\Exceptions\ServerError("Followers::removeFollow Failed to prepare select statement");
. 'statement');
} }
$stmt->bind_param('ss', $sourceUser, $targetUserId); $stmt->bind_param("ss", $sourceUser, $targetUserId);
$stmt->execute(); $stmt->execute();
$stmt->bind_result($followId); $stmt->bind_result($followId);
$found = $stmt->fetch(); $found = $stmt->fetch();
@ -515,10 +511,9 @@ class Followers
$sql = 'delete from follows where source_user = ? and target_user = ?'; $sql = 'delete from follows where source_user = ? and target_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('Followers::removeFollow Failed to prepare delete ' throw new \Federator\Exceptions\ServerError("Followers::removeFollow Failed to prepare delete statement");
. 'statement');
} }
$stmt->bind_param('ss', $sourceUser, $targetUserId); $stmt->bind_param("ss", $sourceUser, $targetUserId);
$stmt->execute(); $stmt->execute();
$affectedRows = $stmt->affected_rows; $affectedRows = $stmt->affected_rows;
$stmt->close(); $stmt->close();

View file

@ -75,7 +75,7 @@ class Posts
} }
foreach ($newPosts as $newPost) { foreach ($newPosts as $newPost) {
if (!isset($existingIds[$newPost->getID()])) { if (!isset($existingIds[$newPost->getID()])) {
if ($newPost->getID() !== '') { if ($newPost->getID() !== "") {
self::savePost($dbh, $userid, $newPost); self::savePost($dbh, $userid, $newPost);
} }
if (sizeof($posts) < $limit) { if (sizeof($posts) < $limit) {
@ -94,12 +94,12 @@ class Posts
$parsed = parse_url($origin); $parsed = parse_url($origin);
if (isset($parsed) && isset($parsed['host'])) { if (isset($parsed) && isset($parsed['host'])) {
$parsedHost = $parsed['host']; $parsedHost = $parsed['host'];
if (is_string($parsedHost) && $parsedHost !== '') { if (is_string($parsedHost) && $parsedHost !== "") {
$originUrl = $parsedHost; $originUrl = $parsedHost;
} }
} }
} }
if (!isset($originUrl) || $originUrl === '') { if (!isset($originUrl) || $originUrl === "") {
$originUrl = 'localhost'; // Fallback to localhost if no origin is set $originUrl = 'localhost'; // Fallback to localhost if no origin is set
} }
@ -156,8 +156,7 @@ class Posts
*/ */
public static function getPostsFromDb($dbh, $userId, $min, $max, $limit = 20) public static function getPostsFromDb($dbh, $userId, $min, $max, $limit = 20)
{ {
$sql = 'SELECT `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, unix_timestamp(`published`) as ' $sql = 'SELECT `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, unix_timestamp(`published`) as published FROM posts WHERE user_id = ?';
. 'published FROM posts WHERE user_id = ?';
$params = [$userId]; $params = [$userId];
$types = 's'; $types = 's';
if ($min > 0) { if ($min > 0) {
@ -267,7 +266,7 @@ class Posts
$publishedStr = $published ? gmdate('Y-m-d H:i:s', $published) : gmdate('Y-m-d H:i:s'); $publishedStr = $published ? gmdate('Y-m-d H:i:s', $published) : gmdate('Y-m-d H:i:s');
$stmt->bind_param( $stmt->bind_param(
'ssssssssss', "ssssssssss",
$id, $id,
$url, $url,
$userId, $userId,
@ -298,7 +297,7 @@ class Posts
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError(); throw new \Federator\Exceptions\ServerError();
} }
$stmt->bind_param('s', $id); $stmt->bind_param("s", $id);
$stmt->execute(); $stmt->execute();
$affectedRows = $stmt->affected_rows; $affectedRows = $stmt->affected_rows;
$stmt->close(); $stmt->close();
@ -322,7 +321,7 @@ class Posts
$object = $post->getObject(); $object = $post->getObject();
if (is_object($object)) { if (is_object($object)) {
$inReplyTo = $object->getInReplyTo(); $inReplyTo = $object->getInReplyTo();
if ($inReplyTo !== '') { if ($inReplyTo !== "") {
$id = $inReplyTo; // Use inReplyTo as ID if it's a string $id = $inReplyTo; // Use inReplyTo as ID if it's a string
} else { } else {
$id = $object->getObject(); $id = $object->getObject();
@ -330,7 +329,7 @@ class Posts
} elseif (is_string($object)) { } elseif (is_string($object)) {
$id = $object; // If object is a string, use it directly $id = $object; // If object is a string, use it directly
} }
$stmt->bind_param('s', $id); $stmt->bind_param("s", $id);
$articleId = null; $articleId = null;
$ret = $stmt->bind_result($articleId); $ret = $stmt->bind_result($articleId);
$stmt->execute(); $stmt->execute();

View file

@ -1,108 +0,0 @@
<?php
/**
* SPDX-FileCopyrightText: 2025 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
* SPDX-License-Identifier: GPL-3.0-or-later
*
* @author Sascha Nitsch (grumpyveveloper)
**/
namespace Federator\DIO;
/**
* Do the Server2Server communication
*/
class Server
{
/**
* send activity to federated server
*
* @param \mysqli $dbh database handle
* @param string $ourhost host url of our server (e.g. federator)
* @param \Federator\Data\User $sender source user
* @param \Federator\Data\FedUser $receiver federated user to receive the activity
* @param \Federator\Data\ActivityPub\Common\Activity $activity activity to send
* @return boolean true on success
*/
public static function sendActivity($dbh, $ourhost, $sender, $receiver, $activity)
{
$receiverInboxUrl = $receiver->inboxURL;
$json = json_encode($activity, JSON_UNESCAPED_SLASHES);
if ($json === false) {
throw new \Exception('Failed to encode JSON: ' . json_last_error_msg());
}
$digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true));
$date = gmdate('D, d M Y H:i:s') . ' GMT';
echo "inboxurl $receiverInboxUrl\n";
$parsedReceiverInboxUrl = parse_url($receiverInboxUrl);
if ($parsedReceiverInboxUrl === false) {
throw new \Exception('Failed to parse URL: ' . $receiverInboxUrl);
}
if (!isset($parsedReceiverInboxUrl['host']) || !isset($parsedReceiverInboxUrl['path'])) {
throw new \Exception('Invalid inbox URL: missing host or path');
}
$extHost = $parsedReceiverInboxUrl['host'];
$path = $parsedReceiverInboxUrl['path'];
// Build the signature string
$signatureString = "(request-target): post {$path}\n" .
"host: {$extHost}\n" .
"date: {$date}\n" .
"digest: {$digest}";
// Get rsa private key
$privateKey = \Federator\DIO\User::getrsaprivate($dbh, $sender->id); // OR from DB
if ($privateKey === false) {
throw new \Exception('Failed to get private key');
}
$pkeyId = openssl_pkey_get_private($privateKey);
if ($pkeyId === false) {
throw new \Exception('Invalid private key');
}
echo "signaturestring $signatureString\n";
openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
$signature_b64 = base64_encode($signature);
// Build keyId (public key ID from your actor object)
$keyId = $ourhost . '/' . $sender->id . '#main-key';
$signatureHeader = 'keyId="' . $keyId
. '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
$ch = curl_init($receiverInboxUrl);
if ($ch === false) {
throw new \Exception('Failed to initialize cURL');
}
$headers = [
'Host: ' . $extHost,
'Date: ' . $date,
'Digest: ' . $digest,
'Content-Type: application/activity+json',
'Signature: ' . $signatureHeader,
'Accept: application/activity+json',
];
print_r($headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
curl_close($ch);
if ($response === false) {
throw new \Exception('Failed to send activity: ' . curl_error($ch));
} else {
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpcode != 200 && $httpcode != 202) {
throw new \Exception('Unexpected HTTP code ' . $httpcode . ':' . $response);
}
}
if ($response !== true) {
error_log($response);
}
return true;
}
}

View file

@ -17,8 +17,7 @@ class Stats
/** /**
* get remote stats * get remote stats
* *
* @param \Federator\Main $main * @param \Federator\Main $main main instance
* main instance
* @return \Federator\Data\Stats * @return \Federator\Data\Stats
*/ */
public static function getStats($main) public static function getStats($main)

View file

@ -26,9 +26,9 @@ class User
$sql = 'select unix_timestamp(`validuntil`) from users where id=?'; $sql = 'select unix_timestamp(`validuntil`) from users where id=?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('User::addLocalUser Failed to prepare statement'); throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare statement");
} }
$stmt->bind_param('s', $_user); $stmt->bind_param("s", $_user);
$validuntil = 0; $validuntil = 0;
$ret = $stmt->bind_result($validuntil); $ret = $stmt->bind_result($validuntil);
$stmt->execute(); $stmt->execute();
@ -50,11 +50,11 @@ class User
$sql .= ' values (?, ?, ?, ?, now() + interval 1 day, ?, ?, ?, ?, ?, ?, ?, ?)'; $sql .= ' values (?, ?, ?, ?, now() + interval 1 day, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('User::addLocalUser Failed to prepare create statement'); throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare create statement");
} }
$registered = gmdate('Y-m-d H:i:s', $user->registered); $registered = gmdate('Y-m-d H:i:s', $user->registered);
$stmt->bind_param( $stmt->bind_param(
'ssssssssssss', "ssssssssssss",
$_user, $_user,
$user->externalid, $user->externalid,
$public, $public,
@ -74,11 +74,11 @@ class User
$sql .= ' iconmediatype=?, iconurl=?, imagemediatype=?, imageurl=? where id=?'; $sql .= ' iconmediatype=?, iconurl=?, imagemediatype=?, imageurl=? where id=?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('User::addLocalUser Failed to prepare update statement'); throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare update statement");
} }
$registered = gmdate('Y-m-d H:i:s', $user->registered); $registered = gmdate('Y-m-d H:i:s', $user->registered);
$stmt->bind_param( $stmt->bind_param(
'sssssssss', "sssssssss",
$user->type, $user->type,
$user->name, $user->name,
$user->summary, $user->summary,
@ -107,12 +107,12 @@ class User
*/ */
public static function getrsaprivate(\mysqli $dbh, string $_user) public static function getrsaprivate(\mysqli $dbh, string $_user)
{ {
$sql = 'select rsaprivate from users where id=?'; $sql = "select rsaprivate from users where id=?";
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('User::getrsaprivate Failed to prepare statement'); throw new \Federator\Exceptions\ServerError("User::getrsaprivate Failed to prepare statement");
} }
$stmt->bind_param('s', $_user); $stmt->bind_param("s", $_user);
$ret = $stmt->bind_result($rsaPrivateKey); $ret = $stmt->bind_result($rsaPrivateKey);
$stmt->execute(); $stmt->execute();
if ($ret) { if ($ret) {
@ -136,7 +136,7 @@ class User
$sql = 'select id,unix_timestamp(`validuntil`) from users where id=?'; $sql = 'select id,unix_timestamp(`validuntil`) from users where id=?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('User::extendUser Failed to prepare statement'); throw new \Federator\Exceptions\ServerError("User::extendUser Failed to prepare statement");
} }
$stmt->bind_param("s", $_user); $stmt->bind_param("s", $_user);
$validuntil = 0; $validuntil = 0;
@ -170,6 +170,7 @@ class User
public static function getUserByName($dbh, $_name, $connector, $cache) public static function getUserByName($dbh, $_name, $connector, $cache)
{ {
$user = false; $user = false;
// ask cache // ask cache
if ($cache !== null) { if ($cache !== null) {
$user = $cache->getRemoteUserByName($_name); $user = $cache->getRemoteUserByName($_name);
@ -182,9 +183,9 @@ class User
$sql .= 'iconmediatype,iconurl,imagemediatype,imageurl from users where id=? and validuntil>=now()'; $sql .= 'iconmediatype,iconurl,imagemediatype,imageurl from users where id=? and validuntil>=now()';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError('User::getUserByName Failed to prepare statement'); throw new \Federator\Exceptions\ServerError("User::getUserByName Failed to prepare statement");
} }
$stmt->bind_param('s', $_name); $stmt->bind_param("s", $_name);
$user = new \Federator\Data\User(); $user = new \Federator\Data\User();
$ret = $stmt->bind_result( $ret = $stmt->bind_result(
$user->id, $user->id,
@ -204,15 +205,18 @@ class User
$stmt->fetch(); $stmt->fetch();
} }
$stmt->close(); $stmt->close();
if ($user->id === null) { if ($user->id === null) {
// ask connector for user-id // ask connector for user-id
$ruser = $connector->getRemoteUserByName($_name); $ruser = $connector->getRemoteUserByName($_name);
if ($ruser !== false) { if ($ruser !== false) {
$user = $ruser; $user = $ruser;
self::addLocalUser($dbh, $user, $_name);
} }
} }
if ($cache !== null) { if ($cache !== null) {
if ($user->id === null && $user->externalid !== null) {
self::addLocalUser($dbh, $user, $_name);
}
$cache->saveRemoteUserByName($_name, $user); $cache->saveRemoteUserByName($_name, $user);
} }
return $user; return $user;

View file

@ -30,7 +30,7 @@ class Votes
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError(); throw new \Federator\Exceptions\ServerError();
} }
$stmt->bind_param('sss', $userId, $targetId, $type); $stmt->bind_param("sss", $userId, $targetId, $type);
$foundId = 0; $foundId = 0;
$ret = $stmt->bind_result($foundId); $ret = $stmt->bind_result($foundId);
$stmt->execute(); $stmt->execute();
@ -51,7 +51,7 @@ class Votes
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError(); throw new \Federator\Exceptions\ServerError();
} }
$stmt->bind_param('s', $id); $stmt->bind_param("s", $id);
$foundId = 0; $foundId = 0;
$ret = $stmt->bind_result($foundId); $ret = $stmt->bind_result($foundId);
$stmt->execute(); $stmt->execute();
@ -67,7 +67,7 @@ class Votes
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError(); throw new \Federator\Exceptions\ServerError();
} }
$stmt->bind_param('ssss', $id, $userId, $targetId, $type); $stmt->bind_param("ssss", $id, $userId, $targetId, $type);
$stmt->execute(); $stmt->execute();
$stmt->close(); $stmt->close();
return $id; // Return the generated vote ID return $id; // Return the generated vote ID
@ -88,7 +88,7 @@ class Votes
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError(); throw new \Federator\Exceptions\ServerError();
} }
$stmt->bind_param('ss', $userId, $targetId); $stmt->bind_param("ss", $userId, $targetId);
$stmt->execute(); $stmt->execute();
$affectedRows = $stmt->affected_rows; $affectedRows = $stmt->affected_rows;
$stmt->close(); $stmt->close();

View file

@ -52,7 +52,7 @@ class InboxJob extends \Federator\Api
*/ */
public function perform(): bool public function perform(): bool
{ {
error_log('InboxJob: Starting job'); error_log("InboxJob: Starting job");
$user = $this->args['user']; $user = $this->args['user'];
$recipientId = $this->args['recipientId']; $recipientId = $this->args['recipientId'];
$activity = $this->args['activity']; $activity = $this->args['activity'];
@ -60,19 +60,11 @@ class InboxJob extends \Federator\Api
$inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity); $inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
if ($inboxActivity === false) { if ($inboxActivity === false) {
error_log('InboxJob: Failed to create inboxActivity from JSON'); error_log("InboxJob: Failed to create inboxActivity from JSON");
return false; return false;
} }
\Federator\DIO\FedUser::inboxForUser( \Federator\Api\FedUsers\Inbox::postForUser($this->dbh, $this->connector, $this->cache, $user, $recipientId, $inboxActivity);
$this,
$this->dbh,
$this->connector,
$this->cache,
$user,
$recipientId,
$inboxActivity
);
return true; return true;
} }
} }

View file

@ -52,7 +52,7 @@ class NewContentJob extends \Federator\Api
*/ */
public function perform(): bool public function perform(): bool
{ {
error_log('NewContentJob: Starting job'); error_log("NewContentJob: Starting job");
$user = $this->args['user']; $user = $this->args['user'];
$recipientId = $this->args['recipientId']; $recipientId = $this->args['recipientId'];
$activity = $this->args['activity']; $activity = $this->args['activity'];
@ -61,23 +61,14 @@ class NewContentJob extends \Federator\Api
$activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity); $activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
if ($activity === false) { if ($activity === false) {
error_log('NewContentJob: Failed to create activity from JSON'); error_log("NewContentJob: Failed to create activity from JSON");
return false; return false;
} }
$domain = $this->config['generic']['externaldomain']; $domain = $this->config['generic']['externaldomain'];
$ourUrl = 'https://' . $domain; $ourUrl = 'https://' . $domain;
\Federator\Api\V1\NewContent::postForUser( \Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $ourUrl, $user, $recipientId, $activity, $articleId);
$this->dbh,
$this->connector,
$this->cache,
$ourUrl,
$user,
$recipientId,
$activity,
$articleId
);
return true; return true;
} }
} }

View file

@ -20,9 +20,9 @@ class Language
* @var array $validLanguages * @var array $validLanguages
*/ */
private $validLanguages = array( private $validLanguages = array(
'de' => true, "de" => true,
'en' => true, "en" => true,
'xy' => true "xy" => true
); );
/** /**
@ -92,12 +92,9 @@ class Language
} }
if (! isset($this->lang[$group])) { if (! isset($this->lang[$group])) {
$l = []; $l = [];
$root = $_SERVER['DOCUMENT_ROOT']; $root = PROJECT_ROOT;
if ($root === '') { if (@file_exists($root . '/lang/federator/' . $this->uselang . "/$group.inc")) {
$root = '.'; require($root . '/lang/federator/' . $this->uselang . "/$group.inc");
}
if (@file_exists($root . '../lang/federator/' . $this->uselang . '/' . $group . '.inc')) {
require($root . '../lang/federator/' . $this->uselang . '/' . $group . '.inc');
$this->lang[$group] = $l; $this->lang[$group] = $l;
} }
} }
@ -107,15 +104,15 @@ class Language
if (isset($values[$i])) { if (isset($values[$i])) {
$string = str_replace("\$$i", $values[$i], $string); $string = str_replace("\$$i", $values[$i], $string);
} else { } else {
$string = str_replace("\$$i", '', $string); $string = str_replace("\$$i", "", $string);
} }
} }
return $string; return $string;
} }
$basedir = $_SERVER['DOCUMENT_ROOT'] . '/../'; $basedir = PROJECT_ROOT;
$fh = @fopen($basedir . '/logs/missingtrans.txt', 'a'); $fh = @fopen("$basedir/logs/missingtrans.txt", 'a');
if ($fh !== false) { if ($fh !== false) {
fwrite($fh, $this->uselang . ':' . $group . ':' . "$key\n"); fwrite($fh, $this->uselang.":$group:$key\n");
fclose($fh); fclose($fh);
} }
return "&gt;&gt;$group:$key&lt;&lt;"; return "&gt;&gt;$group:$key&lt;&lt;";
@ -132,7 +129,7 @@ class Language
{ {
if (! isset($this->lang[$group])) { if (! isset($this->lang[$group])) {
$l = []; $l = [];
require_once($_SERVER['DOCUMENT_ROOT'] . '/../lang/' . $this->uselang . '/' . $group . '.inc'); require_once(PROJECT_ROOT . '/lang/' . $this->uselang . "/$group.inc");
$this->lang[$group] = $l; $this->lang[$group] = $l;
} }
// @phan-suppress-next-line PhanPartialTypeMismatchReturn // @phan-suppress-next-line PhanPartialTypeMismatchReturn
@ -288,7 +285,7 @@ function smarty_function_printlang($params, $template) : string
*/ */
function smarty_function_printjslang($params, $template) : string function smarty_function_printjslang($params, $template) : string
{ {
$lang = $template->getTemplateVars('language'); $lang = $template->getTemplateVars("language");
$prefix = 'window.translations.' . $params['group'] . '.' . $params['key'] . ' = \''; $prefix = 'window.translations.' . $params['group'] . '.' . $params['key'] . ' = \'';
$postfix = '\';'; $postfix = '\';';
if (isset($params['var'])) { if (isset($params['var'])) {

View file

@ -20,43 +20,37 @@ class Main
* *
* @var Cache\Cache $cache * @var Cache\Cache $cache
*/ */
protected $cache = null; protected $cache;
/** /**
* current config * current config
* *
* @var array<string,mixed> $config * @var array<string,mixed> $config
*/ */
protected $config; protected $config;
/** /**
* remote connector * remote connector
* *
* @var Connector\Connector $connector * @var Connector\Connector $connector
*/ */
protected $connector = null; protected $connector = null;
/** /**
* remote host (f.e. https://contentnation.net) * remote host (f.e. https://contentnation.net)
* *
* @var string $host * @var string $host
*/ */
protected $host = null; protected $host = null;
/** /**
* response content type * response content type
* *
* @var string $contentType * @var string $contentType
*/ */
protected $contentType = 'text/html'; protected $contentType = "text/html";
/** /**
* database instance * database instance
* *
* @var \Mysqli $dbh * @var \Mysqli $dbh
*/ */
protected $dbh; protected $dbh;
/** /**
* extra headers * extra headers
* *
@ -84,9 +78,9 @@ class Main
*/ */
public function __construct() public function __construct()
{ {
require_once($_SERVER['DOCUMENT_ROOT'] . '../vendor/autoload.php'); require_once(PROJECT_ROOT . '/vendor/autoload.php');
$this->responseCode = 200; $this->responseCode = 200;
$rootDir = $_SERVER['DOCUMENT_ROOT'] . '../'; $rootDir = PROJECT_ROOT . '/';
$config = parse_ini_file($rootDir . 'config.ini', true); $config = parse_ini_file($rootDir . 'config.ini', true);
if ($config !== false) { if ($config !== false) {
$this->config = $config; $this->config = $config;
@ -105,7 +99,7 @@ class Main
public static function extractFromURI($param, $fallback = '') public static function extractFromURI($param, $fallback = '')
{ {
$uri = $_SERVER['REQUEST_URI']; $uri = $_SERVER['REQUEST_URI'];
$params = substr($uri, (int)(strpos($uri, '?') + 1)); $params = substr($uri, (int) (strpos($uri, '?') + 1));
$params = explode('&', $params); $params = explode('&', $params);
foreach ($params as $p) { foreach ($params as $p) {
$tokens = explode('=', $p); $tokens = explode('=', $p);
@ -151,22 +145,20 @@ class Main
{ {
return $this->cache; return $this->cache;
} }
/** /**
* get connector * get connector
* *
* @return \Federator\Connector\Connector * @return \Federator\Connector\Connector
*/ */
public function getConnector() public function getConnector()
{ {
return $this->connector; return $this->connector;
} }
/** /**
* get host (f.e. https://contentnation.net) * get host (f.e. https://contentnation.net)
* *
* @return string * @return string
*/ */
public function getHost() public function getHost()
{ {
return $this->host; return $this->host;
@ -197,7 +189,7 @@ class Main
public function loadPlugins(): void public function loadPlugins(): void
{ {
if (array_key_exists('plugins', $this->config)) { if (array_key_exists('plugins', $this->config)) {
$basepath = $_SERVER['DOCUMENT_ROOT'] . '../plugins/federator/'; $basepath = PROJECT_ROOT . '/plugins/federator/';
$plugins = $this->config['plugins']; $plugins = $this->config['plugins'];
foreach ($plugins as $name => $file) { foreach ($plugins as $name => $file) {
require_once($basepath . $file); require_once($basepath . $file);
@ -218,7 +210,7 @@ class Main
*/ */
public function openDatabase($usernameOverride = null, $passwordOverride = null) public function openDatabase($usernameOverride = null, $passwordOverride = null)
{ {
$dbconf = $this->config['database']; $dbconf = $this->config["database"];
$this->dbh = new \mysqli( $this->dbh = new \mysqli(
$dbconf['host'], $dbconf['host'],
$usernameOverride ?? (string) $dbconf['username'], $usernameOverride ?? (string) $dbconf['username'],
@ -240,10 +232,10 @@ class Main
*/ */
public function renderTemplate($template, $data) public function renderTemplate($template, $data)
{ {
$rootDir = PROJECT_ROOT . '/';
$smarty = new \Smarty\Smarty(); $smarty = new \Smarty\Smarty();
$root = $_SERVER['DOCUMENT_ROOT']; $smarty->setCompileDir($rootDir . $this->config['templates']['compiledir']);
$smarty->setCompileDir($root . $this->config['templates']['compiledir']); $smarty->setTemplateDir((string) realpath($rootDir . $this->config['templates']['path']));
$smarty->setTemplateDir((string)realpath($root . $this->config['templates']['path']));
$smarty->assign('database', $this->dbh); $smarty->assign('database', $this->dbh);
$smarty->assign('maininstance', $this); $smarty->assign('maininstance', $this);
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
@ -269,6 +261,9 @@ class Main
*/ */
public function setConnector(Connector\Connector $connector) : void public function setConnector(Connector\Connector $connector) : void
{ {
if (isset($this->connector)) {
# echo "main::setConnector Setting new connector will override old one.\n"; // TODO CHANGE TO LOG WARNING
}
$this->connector = $connector; $this->connector = $connector;
} }
@ -279,6 +274,9 @@ class Main
*/ */
public function setHost(string $host) : void public function setHost(string $host) : void
{ {
if (isset($this->host)) {
# echo "main::setHost Setting new host will override old one.\n"; // TODO CHANGE TO LOG WARNING
}
$this->host = $host; $this->host = $host;
} }

View file

@ -22,16 +22,15 @@ class Maintenance
*/ */
public static function run($argc, $argv) public static function run($argc, $argv)
{ {
date_default_timezone_set('Europe/Berlin'); date_default_timezone_set("Europe/Berlin");
spl_autoload_register(static function (string $className) { spl_autoload_register(static function (string $className) {
$root = $_SERVER['DOCUMENT_ROOT']; include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php';
include $root . '../php/' . str_replace("\\", "/", strtolower($className)) . '.php';
}); });
if ($argc < 2) { if ($argc < 2) {
self::printUsage(); self::printUsage();
} }
// pretend that we are running from web directory // pretend that we are running from web directory
$_SERVER['DOCUMENT_ROOT'] = realpath('../../htdocs') . '/'; define('PROJECT_ROOT', dirname(__DIR__, 2));
$main = new \Federator\Main(); $main = new \Federator\Main();
switch ($argv[1]) { switch ($argv[1]) {
case 'dbupgrade': case 'dbupgrade':
@ -71,7 +70,7 @@ class Maintenance
} }
} }
echo "current version: $version\n"; echo "current version: $version\n";
$root = $_SERVER['DOCUMENT_ROOT'] . '../'; $root = PROJECT_ROOT . '/';
$updateFolder = opendir($root . 'sql'); $updateFolder = opendir($root . 'sql');
if ($updateFolder === false) { if ($updateFolder === false) {
die(); die();

View file

@ -81,7 +81,7 @@ class Test
*/ */
public static function run($argc, $argv) public static function run($argc, $argv)
{ {
date_default_timezone_set('Europe/Berlin'); date_default_timezone_set("Europe/Berlin");
spl_autoload_register(static function (string $className) { spl_autoload_register(static function (string $className) {
include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php'; include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php';
}); });
@ -143,8 +143,7 @@ class Test
$inboxActivity->setAActor('https://mastodon.local/users/admin'); $inboxActivity->setAActor('https://mastodon.local/users/admin');
$inboxActivity->setObject($_url); $inboxActivity->setObject($_url);
$inboxActivity->setID("https://mastodon.local/users/admin#like/" . md5($_url)); $inboxActivity->setID("https://mastodon.local/users/admin#like/" . md5($_url));
\Federator\DIO\FedUser::inboxForUser( \Federator\Api\FedUsers\Inbox::postForUser(
$api,
$dbh, $dbh,
$api->getConnector(), $api->getConnector(),
null, null,

View file

@ -1,18 +1,14 @@
<?php <?php
define('PROJECT_ROOT', dirname(__DIR__, 3)); define('PROJECT_ROOT', dirname(__DIR__, 3));
require_once PROJECT_ROOT . '/vendor/autoload.php';
$_SERVER['DOCUMENT_ROOT'] = PROJECT_ROOT . '/htdocs/';
spl_autoload_register(static function (string $className) { require_once PROJECT_ROOT . '/vendor/autoload.php';
include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php';
});
$config = parse_ini_file(PROJECT_ROOT . '/rediscache.ini'); $config = parse_ini_file(PROJECT_ROOT . '/rediscache.ini');
// Set the Redis backend for Resque // Set the Redis backend for Resque
$redisUrl = sprintf( $redisUrl = sprintf(
'redis://%s:%s@%s:%d?password-encoding=u', 'redis://%s:%s@%s:%d',
urlencode($config['username']), urlencode($config['username']),
urlencode($config['password']), urlencode($config['password']),
$config['host'], $config['host'],

View file

@ -41,7 +41,7 @@ class ContentNation implements Connector
*/ */
public function __construct($main) public function __construct($main)
{ {
$config = parse_ini_file($_SERVER['DOCUMENT_ROOT'] . '../contentnation.ini', true); $config = parse_ini_file(PROJECT_ROOT . '/contentnation.ini', true);
if ($config !== false) { if ($config !== false) {
$this->config = $config; $this->config = $config;
} }
@ -53,28 +53,26 @@ class ContentNation implements Connector
/** /**
* get followers of given user * get followers of given user
* *
* @param string $userId user id @unused-param * @param string $userId user id
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getFollowersByUser($userId) public function getRemoteFollowersOfUser($userId)
{ {
// ContentNation does not export followers // todo implement queue for this
/*
if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) { if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
$userId = $matches[1]; $userId = $matches[1];
} }
$remoteURL = $this->service . '/api/profile/' . urlencode($userId);# . '/followers'; $remoteURL = $this->service . '/api/profile/' . urlencode($userId) . '/followers';
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, []); [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
error_log("ContentNation::getFollowersByUser error retrieving followers for userId: $userId . Error: " error_log("ContentNation::getRemoteFollowersOfUser error retrieving followers for userId: $userId . Error: " . json_encode($info));
. json_encode($info));
return false; return false;
} }
$r = json_decode($response, true); $r = json_decode($response, true);
if ($r === false || $r === null || !is_array($r)) { if ($r === false || $r === null || !is_array($r)) {
return false; return false;
}*/ }
$followers = []; $followers = [];
return $followers; return $followers;
} }
@ -82,13 +80,13 @@ class ContentNation implements Connector
/** /**
* get following of given user * get following of given user
* *
* @param string $userId user id @unused-param * @param string $userId user id
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getFollowingByUser($userId) public function getRemoteFollowingForUser($userId)
{ {
// ContentNation does not export Following for user // todo implement queue for this
/*
if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) { if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
$userId = $matches[1]; $userId = $matches[1];
} }
@ -96,16 +94,15 @@ class ContentNation implements Connector
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, []); [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
error_log('ContentNation::getRemoteFollowingForUser error retrieving following for userId: ' . $userId error_log("ContentNation::getRemoteFollowingForUser error retrieving following for userId: $userId . Error: " . json_encode($info));
. '. Error: ' . json_encode($info));
return false; return false;
} }
$r = json_decode($response, true); $r = json_decode($response, true);
if ($r === false || $r === null || !is_array($r)) { if ($r === false || $r === null || !is_array($r)) {
return false; return false;
}*/ }
$following = []; $followers = [];
return $following; return $followers;
} }
/** /**
@ -132,8 +129,7 @@ class ContentNation implements Connector
} }
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, []); [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
error_log('ContentNation::getRemotePostsByUser error retrieving activities for userId: ' . $userId error_log("ContentNation::getRemotePostsByUser error retrieving activities for userId: $userId . Error: " . json_encode($info));
. '. Error: ' . json_encode($info));
return false; return false;
} }
$r = json_decode($response, true); $r = json_decode($response, true);
@ -215,8 +211,7 @@ class ContentNation implements Connector
$commentJson = $activity; $commentJson = $activity;
$commentJson['type'] = 'Note'; $commentJson['type'] = 'Note';
$commentJson['summary'] = $activity['subject']; $commentJson['summary'] = $activity['subject'];
$commentJson['id'] = $ourUrl . '/' . $activity['articleOwnerName'] . '/' $commentJson['id'] = $ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id'];
. $activity['articleName'] . '#' . $activity['id'];
$note = \Federator\Data\ActivityPub\Factory::newFromJson($commentJson, ""); $note = \Federator\Data\ActivityPub\Factory::newFromJson($commentJson, "");
if ($note === null) { if ($note === null) {
error_log("ContentNation::getRemotePostsByUser couldn't create comment"); error_log("ContentNation::getRemotePostsByUser couldn't create comment");
@ -226,14 +221,11 @@ class ContentNation implements Connector
} }
$note->setID($commentJson['id']); $note->setID($commentJson['id']);
if (!isset($commentJson['parent']) || $commentJson['parent'] === null) { if (!isset($commentJson['parent']) || $commentJson['parent'] === null) {
$note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/' $note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName']);
. $activity['articleName']);
} else { } else {
$note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/' $note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . "#" . $commentJson['parent']);
. $activity['articleName'] . "#" . $commentJson['parent']);
} }
$url = $ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] $url = $ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id'];
. '#' . $activity['id'];
$create->setURL($url); $create->setURL($url);
$create->setID($url); $create->setID($url);
$create->setObject($note); $create->setObject($note);
@ -301,10 +293,16 @@ class ContentNation implements Connector
public function getRemoteUserByName(string $_name) public function getRemoteUserByName(string $_name)
{ {
// validate name // validate name
if (preg_match("/^[a-zA-Z0-9\._\-]+$/", $_name) != 1) { if (preg_match("/^[a-zA-Z@0-9\._\-]+$/", $_name) != 1) {
return false; return false;
} }
$remoteURL = $this->service . '/api/users/info?user=' . urlencode($_name); // make sure we only get name part, without domain
if (preg_match("#^([^@]+)@([^/]+)#", $_name, $matches) == 1) {
$name = $matches[1];
} else {
$name = $_name;
}
$remoteURL = $this->service . '/api/users/info?user=' . urlencode($name);
$headers = ['Accept: application/json']; $headers = ['Accept: application/json'];
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers); [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
@ -341,7 +339,7 @@ class ContentNation implements Connector
if (preg_match("/^[a-z0-9]{16}$/", $_session) != 1) { if (preg_match("/^[a-z0-9]{16}$/", $_session) != 1) {
return false; return false;
} }
if (preg_match("/^[a-zA-Z0-9_\-]+$/", $_user) != 1) { if (preg_match("/^[a-zA-Z@0-9\._\-]+$/", $_user) != 1) {
return false; return false;
} }
$remoteURL = $this->service . '/api/users/permissions?profile=' . urlencode($_user); $remoteURL = $this->service . '/api/users/permissions?profile=' . urlencode($_user);
@ -430,8 +428,7 @@ class ContentNation implements Connector
} elseif ($jsonData['object']['vote']['value'] == 0) { } elseif ($jsonData['object']['vote']['value'] == 0) {
$ap['object']['type'] = 'Dislike'; $ap['object']['type'] = 'Dislike';
} else { } else {
error_log('ContentNation::jsonToActivity unknown vote value: ' error_log("ContentNation::jsonToActivity unknown vote value: {$jsonData['object']['vote']['value']}");
. $jsonData['object']['vote']['value']);
break; break;
} }
$ap['object']['object'] = self::generateObjectJson($ourUrl, $jsonData); $ap['object']['object'] = self::generateObjectJson($ourUrl, $jsonData);
@ -441,7 +438,7 @@ class ContentNation implements Connector
} }
$returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
if ($returnActivity === false) { if ($returnActivity === false) {
error_log('ContentNation::jsonToActivity couldn\'t create undo'); error_log("ContentNation::jsonToActivity couldn't create undo");
$returnActivity = new \Federator\Data\ActivityPub\Common\Undo(); $returnActivity = new \Federator\Data\ActivityPub\Common\Undo();
} else { } else {
$returnActivity->setID($ap['id']); $returnActivity->setID($ap['id']);
@ -450,8 +447,7 @@ class ContentNation implements Connector
break; break;
default: default:
// Handle unsupported types or fallback to default behavior // Handle unsupported types or fallback to default behavior
throw new \InvalidArgumentException('ContentNation::jsonToActivity Unsupported type: ' throw new \InvalidArgumentException("ContentNation::jsonToActivity Unsupported type: {$jsonData['type']}");
. $jsonData['type']);
} }
} else { } else {
// Handle specific fields based on the type // Handle specific fields based on the type
@ -484,7 +480,7 @@ class ContentNation implements Connector
$ap['object'] = self::generateObjectJson($ourUrl, $jsonData); $ap['object'] = self::generateObjectJson($ourUrl, $jsonData);
$returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
if ($returnActivity === false) { if ($returnActivity === false) {
error_log('ContentNation::jsonToActivity couldn\'t create article'); error_log("ContentNation::jsonToActivity couldn't create article");
$returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create'); $returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create');
} else { } else {
$returnActivity->setID($ap['id']); $returnActivity->setID($ap['id']);
@ -517,7 +513,7 @@ class ContentNation implements Connector
$ap['object'] = self::generateObjectJson($ourUrl, $jsonData); $ap['object'] = self::generateObjectJson($ourUrl, $jsonData);
$returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
if ($returnActivity === false) { if ($returnActivity === false) {
error_log('ContentNation::jsonToActivity couldn\'t create comment'); error_log("ContentNation::jsonToActivity couldn't create comment");
$returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create'); $returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create');
} else { } else {
$returnActivity->setID($ap['id']); $returnActivity->setID($ap['id']);
@ -539,18 +535,17 @@ class ContentNation implements Connector
} elseif ($jsonData['object']['vote']['value'] == 0) { } elseif ($jsonData['object']['vote']['value'] == 0) {
$ap['type'] = 'Dislike'; $ap['type'] = 'Dislike';
} else { } else {
error_log('ContentNation::jsonToActivity unknown vote value: ' error_log("ContentNation::jsonToActivity unknown vote value: {$jsonData['object']['vote']['value']}");
. $jsonData['object']['vote']['value']);
break; break;
} }
$ap['object'] = self::generateObjectJson($ourUrl, $jsonData); $ap['object'] = self::generateObjectJson($ourUrl, $jsonData);
$returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
if ($returnActivity === false) { if ($returnActivity === false) {
error_log('ContentNation::jsonToActivity couldn\'t create vote'); error_log("ContentNation::jsonToActivity couldn't create vote");
if ($ap['type'] === 'Like') { if ($ap['type'] === "Like") {
$returnActivity = new \Federator\Data\ActivityPub\Common\Like(); $returnActivity = new \Federator\Data\ActivityPub\Common\Like();
} elseif ($ap['type'] === 'Dislike') { } elseif ($ap['type'] === "Dislike") {
$returnActivity = new \Federator\Data\ActivityPub\Common\Dislike(); $returnActivity = new \Federator\Data\ActivityPub\Common\Dislike();
} else { } else {
$returnActivity = new \Federator\Data\ActivityPub\Common\Undo(); $returnActivity = new \Federator\Data\ActivityPub\Common\Undo();
@ -564,8 +559,7 @@ class ContentNation implements Connector
default: default:
// Handle unsupported types or fallback to default behavior // Handle unsupported types or fallback to default behavior
throw new \InvalidArgumentException('ContentNation::jsonToActivity Unsupported object type: ' throw new \InvalidArgumentException("ContentNation::jsonToActivity Unsupported object type: {$jsonData['type']}");
. $jsonData['type']);
} }
} }
@ -587,7 +581,7 @@ class ContentNation implements Connector
$actorUrl = $ourUrl . '/' . $actorName; $actorUrl = $ourUrl . '/' . $actorName;
if ($objectType === 'article') { if ($objectType === "article") {
$articleName = $jsonData['object']['name'] ?? null; $articleName = $jsonData['object']['name'] ?? null;
$articleOwnerName = $jsonData['object']['ownerName'] ?? null; $articleOwnerName = $jsonData['object']['ownerName'] ?? null;
$updatedOn = $jsonData['object']['modified'] ?? null; $updatedOn = $jsonData['object']['modified'] ?? null;
@ -595,13 +589,13 @@ class ContentNation implements Connector
$update = $updatedOn !== $originalPublished; $update = $updatedOn !== $originalPublished;
$returnJson = [ $returnJson = [
'type' => 'Article', 'type' => 'Article',
'id' => $ourUrl . '/' . $articleOwnerName . '/' . $articleName, 'id' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName,
'name' => $jsonData['object']['title'] ?? null, 'name' => $jsonData['object']['title'] ?? null,
'published' => $originalPublished, 'published' => $originalPublished,
'summary' => $jsonData['object']['summary'] ?? null, 'summary' => $jsonData['object']['summary'] ?? null,
'content' => $jsonData['object']['content'] ?? null, 'content' => $jsonData['object']['content'] ?? null,
'attributedTo' => $actorUrl, 'attributedTo' => $actorUrl,
'url' => $ourUrl . '/' . $articleOwnerName . '/' . $articleName, 'url' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName,
'cc' => ['https://www.w3.org/ns/activitystreams#Public'], 'cc' => ['https://www.w3.org/ns/activitystreams#Public'],
]; ];
if ($update) { if ($update) {
@ -625,14 +619,14 @@ class ContentNation implements Connector
} }
} }
} }
} elseif ($objectType === 'comment') { } elseif ($objectType === "comment") {
$commentId = $jsonData['object']['id'] ?? null; $commentId = $jsonData['object']['id'] ?? null;
$articleName = $jsonData['object']['articleName'] ?? null; $articleName = $jsonData['object']['articleName'] ?? null;
$articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null; $articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null;
$returnJson = [ $returnJson = [
'type' => 'Note', 'type' => 'Note',
'id' => $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $commentId, 'id' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId,
'url' => $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $commentId, 'url' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId,
'attributedTo' => $actorUrl, 'attributedTo' => $actorUrl,
'content' => $jsonData['object']['content'] ?? null, 'content' => $jsonData['object']['content'] ?? null,
'summary' => $jsonData['object']['summary'] ?? null, 'summary' => $jsonData['object']['summary'] ?? null,
@ -648,26 +642,24 @@ class ContentNation implements Connector
} }
$replyType = $jsonData['object']['inReplyTo']['type'] ?? null; $replyType = $jsonData['object']['inReplyTo']['type'] ?? null;
if ($replyType === "article") { if ($replyType === "article") {
$returnJson['inReplyTo'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName; $returnJson['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName;
} elseif ($replyType === "comment") { } elseif ($replyType === "comment") {
$returnJson['inReplyTo'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName $returnJson['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $jsonData['object']['inReplyTo']['id'];
. '#' . $jsonData['object']['inReplyTo']['id'];
} else { } else {
error_log('ContentNation::generateObjectJson for comment - unknown inReplyTo type: ' error_log("ContentNation::generateObjectJson for comment - unknown inReplyTo type: {$replyType}");
. $replyType);
} }
} elseif ($objectType === 'vote') { } elseif ($objectType === "vote") {
$votedOn = $jsonData['object']['type'] ?? null; $votedOn = $jsonData['object']['type'] ?? null;
$articleName = $jsonData['object']['articleName'] ?? null; $articleName = $jsonData['object']['articleName'] ?? null;
$articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null; $articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null;
$objectId = $ourUrl . '/' . $articleOwnerName . '/' . $articleName; $objectId = $ourUrl . '/' . $articleOwnerName . '/' . $articleName;
if ($votedOn === 'comment') { if ($votedOn === "comment") {
$objectId .= '#' . $jsonData['object']['commentId']; $objectId .= '#' . $jsonData['object']['commentId'];
} }
$returnJson = $objectId; $returnJson = $objectId;
} else { } else {
error_log('ContentNation::generateObjectJson unknown object type: ' . $objectType); error_log("ContentNation::generateObjectJson unknown object type: {$objectType}");
return false; return false;
} }
@ -686,16 +678,10 @@ class ContentNation implements Connector
$targetUrl = $this->service; $targetUrl = $this->service;
$targetRequestType = 'post'; // Default request type $targetRequestType = 'post'; // Default request type
// Convert ActivityPub activity to ContentNation JSON format and retrieve target url // Convert ActivityPub activity to ContentNation JSON format and retrieve target url
$jsonData = self::activityToJson( $jsonData = self::activityToJson($this->main->getDatabase(), $this->service, $activity, $targetUrl, $targetRequestType);
$this->main->getDatabase(),
$this->service,
$activity,
$targetUrl,
$targetRequestType
);
if ($jsonData === false) { if ($jsonData === false) {
error_log('ContentNation::sendActivity failed to convert activity to JSON'); error_log("ContentNation::sendActivity failed to convert activity to JSON");
return false; return false;
} }
@ -723,7 +709,7 @@ class ContentNation implements Connector
"date: {$date}\n" . "date: {$date}\n" .
"digest: {$digest}"; "digest: {$digest}";
$pKeyPath = $_SERVER['DOCUMENT_ROOT'] . '../' . $this->main->getConfig()['keys']['federatorPrivateKeyPath']; $pKeyPath = PROJECT_ROOT . '/' . $this->main->getConfig()['keys']['federatorPrivateKeyPath'];
$privateKeyPem = file_get_contents($pKeyPath); $privateKeyPem = file_get_contents($pKeyPath);
if ($privateKeyPem === false) { if ($privateKeyPem === false) {
http_response_code(500); http_response_code(500);
@ -739,8 +725,7 @@ class ContentNation implements Connector
openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256); openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
$signature_b64 = base64_encode($signature); $signature_b64 = base64_encode($signature);
$signatureHeader = 'algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' $signatureHeader = 'algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
. $signature_b64 . '"';
$ch = curl_init($targetUrl); $ch = curl_init($targetUrl);
if ($ch === false) { if ($ch === false) {
@ -765,8 +750,7 @@ class ContentNation implements Connector
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
break; break;
default: default:
throw new \Exception('ContentNation::sendActivity Unsupported target request type: ' throw new \Exception("ContentNation::sendActivity Unsupported target request type: $targetRequestType");
. $targetRequestType);
} }
curl_setopt($ch, CURLOPT_POSTFIELDS, $json); curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
@ -774,11 +758,11 @@ class ContentNation implements Connector
curl_close($ch); curl_close($ch);
if ($response === false) { if ($response === false) {
throw new \Exception('Failed to send activity: ' . curl_error($ch)); throw new \Exception("Failed to send activity: " . curl_error($ch));
} else { } else {
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpcode != 200 && $httpcode != 202) { if ($httpcode != 200 && $httpcode != 202) {
throw new \Exception('Unexpected HTTP code ' . $httpcode .':' . $response); throw new \Exception("Unexpected HTTP code $httpcode: $response");
} }
} }
@ -795,7 +779,7 @@ class ContentNation implements Connector
* @param string $targetRequestType the target request type (e.g., 'post', 'delete', etc.) * @param string $targetRequestType the target request type (e.g., 'post', 'delete', etc.)
* @return array<string, mixed>|false the json data or false on failure * @return array<string, mixed>|false the json data or false on failure
*/ */
private function activityToJson($dbh, $serviceUrl, $activity, &$targetUrl, &$targetRequestType) private function activityToJson($dbh, $serviceUrl, \Federator\Data\ActivityPub\Common\Activity $activity, string &$targetUrl, string &$targetRequestType)
{ {
$type = strtolower($activity->getType()); $type = strtolower($activity->getType());
$targetRequestType = 'post'; // Default request type $targetRequestType = 'post'; // Default request type
@ -807,14 +791,12 @@ class ContentNation implements Connector
$objType = strtolower($object->getType()); $objType = strtolower($object->getType());
$articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity); $articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
if ($articleId === null) { if ($articleId === null) {
error_log('ContentNation::activityToJson Failed to get original article ID' error_log("ContentNation::activityToJson Failed to get original article ID for create/update activity");
.' for create/update activity');
} }
switch ($objType) { switch ($objType) {
case 'article': case 'article':
// We don't support article create/update at this point in time // We don't support article create/update at this point in time
error_log('ContentNation::activityToJson Unsupported create/update object type: ' error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}");
.$objType);
break; break;
case 'note': case 'note':
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/comment'; $targetUrl = $serviceUrl . '/api/article/' . $articleId . '/comment';
@ -834,8 +816,7 @@ class ContentNation implements Connector
} }
} }
} else { } else {
error_log('ContentNation::activityToJson Unsupported target type for comment with id: ' error_log("ContentNation::activityToJson Unsupported target type for comment with id: " . $activity->getID() . " Type: " . gettype($target));
. $activity->getID() . ' Type: ' . gettype($target));
return false; return false;
} }
return [ return [
@ -846,8 +827,7 @@ class ContentNation implements Connector
'comment' => $object->getContent(), 'comment' => $object->getContent(),
]; ];
default: default:
error_log('ContentNation::activityToJson Unsupported create/update object type: ' error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}");
. $objType);
return false; return false;
} }
} }
@ -856,14 +836,13 @@ class ContentNation implements Connector
case 'follow': case 'follow':
$profileUrl = $activity->getObject(); $profileUrl = $activity->getObject();
if (!is_string($profileUrl)) { if (!is_string($profileUrl)) {
error_log('ContentNation::activityToJson Invalid profile URL: ' . json_encode($profileUrl)); error_log("ContentNation::activityToJson Invalid profile URL: " . json_encode($profileUrl));
return false; return false;
} }
$receiverName = basename((string) (parse_url($profileUrl, PHP_URL_PATH) ?? '')); $receiverName = basename((string) (parse_url($profileUrl, PHP_URL_PATH) ?? ''));
$ourDomain = parse_url($profileUrl, PHP_URL_HOST); $ourDomain = parse_url($profileUrl, PHP_URL_HOST);
if ($receiverName === '' || $ourDomain === '') { if ($receiverName === "" || $ourDomain === "") {
error_log('ContentNation::activityToJson no profileName or domain found for object url: ' error_log("ContentNation::activityToJson no profileName or domain found for object url: " . $profileUrl);
. $profileUrl);
return false; return false;
} }
$receiver = $receiverName; $receiver = $receiverName;
@ -875,12 +854,11 @@ class ContentNation implements Connector
null null
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log('ContentNation::activityToJson get user by name: ' . $receiver . '. Exception: ' error_log("ContentNation::activityToJson get user by name: " . $receiver . ". Exception: " . $e->getMessage());
. $e->getMessage());
return false; return false;
} }
if ($localUser === null || $localUser->id === null) { if ($localUser === null || $localUser->id === null) {
error_log('ContentNation::activityToJson couldn\'t find user: ' . $receiver); error_log("ContentNation::activityToJson couldn't find user: $receiver");
return false; return false;
} }
$targetUrl = $serviceUrl . '/api/profile/' . $localUser->id . '/fedfollow'; $targetUrl = $serviceUrl . '/api/profile/' . $localUser->id . '/fedfollow';
@ -903,7 +881,7 @@ class ContentNation implements Connector
case 'dislike': case 'dislike':
$articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity); $articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
if ($articleId === null) { if ($articleId === null) {
error_log('ContentNation::activityToJson Failed to get original article ID for vote activity'); error_log("ContentNation::activityToJson Failed to get original article ID for vote activity");
} }
$voteValue = $type === 'like' ? true : false; $voteValue = $type === 'like' ? true : false;
$activityType = 'vote'; $activityType = 'vote';
@ -922,8 +900,7 @@ class ContentNation implements Connector
} }
} }
} else { } else {
error_log('ContentNation::activityToJson Unsupported target type for vote with id: ' error_log("ContentNation::activityToJson Unsupported target type for vote with id: " . $activity->getID() . " Type: " . gettype($target));
. $activity->getID() . ' Type: ' . gettype($target));
return false; return false;
} }
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote'; $targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
@ -942,15 +919,13 @@ class ContentNation implements Connector
case 'follow': case 'follow':
$profileUrl = $object->getObject(); $profileUrl = $object->getObject();
if (!is_string($profileUrl)) { if (!is_string($profileUrl)) {
error_log('ContentNation::activityToJson Invalid profile URL: ' error_log("ContentNation::activityToJson Invalid profile URL: " . json_encode($profileUrl));
. json_encode($profileUrl));
return false; return false;
} }
$receiverName = basename((string) (parse_url($profileUrl, PHP_URL_PATH) ?? '')); $receiverName = basename((string) (parse_url($profileUrl, PHP_URL_PATH) ?? ''));
$ourDomain = parse_url($profileUrl, PHP_URL_HOST); $ourDomain = parse_url($profileUrl, PHP_URL_HOST);
if ($receiverName === '' || $ourDomain === '') { if ($receiverName === "" || $ourDomain === "") {
error_log('ContentNation::activityToJson no profileName or domain found for object' error_log("ContentNation::activityToJson no profileName or domain found for object url: " . $profileUrl);
. ' url: ' . $profileUrl);
return false; return false;
} }
$receiver = $receiverName; $receiver = $receiverName;
@ -962,12 +937,11 @@ class ContentNation implements Connector
null null
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log('ContentNation::activityToJson get user by name: ' . $receiver error_log("ContentNation::activityToJson get user by name: " . $receiver . ". Exception: " . $e->getMessage());
. '. Exception: ' . $e->getMessage());
return false; return false;
} }
if ($localUser === null || $localUser->id === null) { if ($localUser === null || $localUser->id === null) {
error_log('ContentNation::activityToJson couldn\'t find user: ' . $receiver); error_log("ContentNation::activityToJson couldn't find user: $receiver");
return false; return false;
} }
$targetUrl = $serviceUrl . '/api/profile/' . $localUser->id . '/fedfollow'; $targetUrl = $serviceUrl . '/api/profile/' . $localUser->id . '/fedfollow';
@ -996,8 +970,7 @@ class ContentNation implements Connector
case 'dislike': case 'dislike':
$articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity); $articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
if ($articleId === null) { if ($articleId === null) {
error_log('ContentNation::activityToJson Failed to get original article ID ' error_log("ContentNation::activityToJson Failed to get original article ID for undo vote activity");
. 'for undo vote activity');
} }
$activityType = 'vote'; $activityType = 'vote';
$inReplyTo = $object->getInReplyTo(); $inReplyTo = $object->getInReplyTo();
@ -1015,8 +988,7 @@ class ContentNation implements Connector
} }
} }
} else { } else {
error_log('ContentNation::activityToJson Unsupported target type for undo ' error_log("ContentNation::activityToJson Unsupported target type for undo vote with id: " . $activity->getID() . " Type: " . gettype($target));
. 'vote with id: ' . $activity->getID() . " Type: " . gettype($target));
return false; return false;
} }
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote'; $targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
@ -1028,17 +1000,16 @@ class ContentNation implements Connector
]; ];
case 'note': case 'note':
// We don't support comment deletions at this point in time // We don't support comment deletions at this point in time
error_log('ContentNation::activityToJson Unsupported undo object type: ' . $objType); error_log("ContentNation::activityToJson Unsupported undo object type: {$objType}");
break; break;
default: default:
error_log('ContentNation::activityToJson Unsupported create/update object type: ' error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}");
. $objType);
return false; return false;
} }
} }
break; break;
default: default:
error_log('ContentNation::activityToJson Unsupported activity type: ' . $type); error_log("ContentNation::activityToJson Unsupported activity type: {$type}");
return false; return false;
} }
@ -1057,11 +1028,11 @@ class ContentNation implements Connector
$signatureHeader = $headers['Signature'] ?? null; $signatureHeader = $headers['Signature'] ?? null;
if (!isset($signatureHeader)) { if (!isset($signatureHeader)) {
throw new \Federator\Exceptions\PermissionDenied('Missing Signature header'); throw new \Federator\Exceptions\PermissionDenied("Missing Signature header");
} }
if (!isset($headers['X-Sender']) || $headers['X-Sender'] !== $this->config['keys']['headerSenderName']) { if (!isset($headers['X-Sender']) || $headers['X-Sender'] !== $this->config['keys']['headerSenderName']) {
throw new \Federator\Exceptions\PermissionDenied('Invalid sender name'); throw new \Federator\Exceptions\PermissionDenied("Invalid sender name");
} }
// Parse Signature header // Parse Signature header
@ -1071,11 +1042,11 @@ class ContentNation implements Connector
$signature = base64_decode($signatureParts['signature']); $signature = base64_decode($signatureParts['signature']);
$signedHeaders = explode(' ', $signatureParts['headers']); $signedHeaders = explode(' ', $signatureParts['headers']);
$pKeyPath = $_SERVER['DOCUMENT_ROOT'] . '../' . $this->config['keys']['publicKeyPath']; $pKeyPath = PROJECT_ROOT . '/' . $this->config['keys']['publicKeyPath'];
$publicKeyPem = file_get_contents($pKeyPath); $publicKeyPem = file_get_contents($pKeyPath);
if ($publicKeyPem === false) { if ($publicKeyPem === false) {
http_response_code(500); http_response_code(500);
throw new \Federator\Exceptions\PermissionDenied('Public key couldn\'t be determined'); throw new \Federator\Exceptions\PermissionDenied("Public key couldn't be determined");
} }
// Reconstruct the signed string // Reconstruct the signed string
@ -1089,7 +1060,7 @@ class ContentNation implements Connector
$headerValue = $headers[ucwords($header, '-')] ?? ''; $headerValue = $headers[ucwords($header, '-')] ?? '';
} }
$signedString .= strtolower($header) . ': ' . $headerValue . "\n"; $signedString .= strtolower($header) . ": " . $headerValue . "\n";
} }
$signedString = rtrim($signedString); $signedString = rtrim($signedString);
@ -1102,9 +1073,9 @@ class ContentNation implements Connector
} }
if ($verified != 1) { if ($verified != 1) {
http_response_code(500); http_response_code(500);
throw new \Federator\Exceptions\PermissionDenied('Signature verification failed'); throw new \Federator\Exceptions\PermissionDenied("Signature verification failed");
} }
return 'Signature verified.'; return "Signature verified.";
} }
} }
@ -1119,5 +1090,6 @@ namespace Federator;
function contentnation_load($main) function contentnation_load($main)
{ {
$cn = new Connector\ContentNation($main); $cn = new Connector\ContentNation($main);
# echo "contentnation::contentnation_load Loaded new connector, adding to main\n"; // TODO change to proper log
$main->setConnector($cn); $main->setConnector($cn);
} }

View file

@ -25,7 +25,7 @@ class DummyConnector implements Connector
* @param string $userId user id @unused-param * @param string $userId user id @unused-param
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getFollowersByUser($userId) public function getRemoteFollowersOfUser($userId)
{ {
return false; return false;
} }
@ -34,9 +34,10 @@ class DummyConnector implements Connector
* get following of given user * get following of given user
* *
* @param string $id user id @unused-param * @param string $id user id @unused-param
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getFollowingByUser($id) public function getRemoteFollowingForUser($id)
{ {
return false; return false;
} }
@ -77,8 +78,7 @@ class DummyConnector implements Connector
* (used to identify the article in the remote system) @unused-param * (used to identify the article in the remote system) @unused-param
* @return \Federator\Data\ActivityPub\Common\Activity|false * @return \Federator\Data\ActivityPub\Common\Activity|false
*/ */
public function jsonToActivity(array $jsonData, &$articleId) public function jsonToActivity(array $jsonData, &$articleId) {
{
return false; return false;
} }
@ -147,5 +147,6 @@ namespace Federator;
function dummy_load($main) function dummy_load($main)
{ {
$dummy = new Connector\DummyConnector(); $dummy = new Connector\DummyConnector();
# echo "dummyconnector::dummy_load Loaded new connector, adding to main\n"; // TODO change to proper log
$main->setConnector($dummy); $main->setConnector($dummy);
} }

View file

@ -53,12 +53,11 @@ class RedisCache implements Cache
*/ */
public function __construct() public function __construct()
{ {
$config = parse_ini_file('../rediscache.ini'); $config = parse_ini_file(PROJECT_ROOT . '/rediscache.ini');
if ($config !== false) { if ($config !== false) {
$this->config = $config; $this->config = $config;
$this->userTTL = array_key_exists('userttl', $config) ? intval($config['userttl'], 10) : 60; $this->userTTL = array_key_exists('userttl', $config) ? intval($config['userttl'], 10) : 60;
$this->publicKeyPemTTL = array_key_exists('publickeypemttl', $config) $this->publicKeyPemTTL = array_key_exists('publickeypemttl', $config) ? intval($config['publickeypemttl'], 10) : 3600;
? intval($config['publickeypemttl'], 10) : 3600;
} }
} }
@ -103,9 +102,9 @@ class RedisCache implements Cache
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getFollowersByUser($id) public function getRemoteFollowersOfUser($id)
{ {
error_log("rediscache::getFollowersByUser not implemented"); error_log("rediscache::getRemoteFollowersOfUser not implemented");
return false; return false;
} }
@ -116,9 +115,9 @@ class RedisCache implements Cache
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getFollowingByUser($id) public function getRemoteFollowingForUser($id)
{ {
error_log("rediscache::getFollowingByUser not implemented"); error_log("rediscache::getRemoteFollowingForUser not implemented");
return false; return false;
} }
@ -254,9 +253,9 @@ class RedisCache implements Cache
* @param \Federator\Data\FedUser[]|false $followers user followers @unused-param * @param \Federator\Data\FedUser[]|false $followers user followers @unused-param
* @return void * @return void
*/ */
public function saveFollowersByUser($user, $followers) public function saveRemoteFollowersOfUser($user, $followers)
{ {
error_log("rediscache::saveFollowersByUser not implemented"); error_log("rediscache::saveRemoteFollowersOfUser not implemented");
} }
/** /**
@ -266,9 +265,9 @@ class RedisCache implements Cache
* @param \Federator\Data\FedUser[]|false $following user following @unused-param * @param \Federator\Data\FedUser[]|false $following user following @unused-param
* @return void * @return void
*/ */
public function saveFollowingByUser($user, $following) public function saveRemoteFollowingForUser($user, $following)
{ {
error_log("rediscache::saveFollowingByUser not implemented"); error_log("rediscache::saveRemoteFollowingForUser not implemented");
} }
/** /**
@ -301,7 +300,6 @@ class RedisCache implements Cache
$serialized = $stats->toJson(); $serialized = $stats->toJson();
$this->redis->setEx($key, $this->config['statsttl'], $serialized); $this->redis->setEx($key, $this->config['statsttl'], $serialized);
} }
/** /**
* save remote user by name * save remote user by name
* *

View file

@ -19,10 +19,10 @@ primary goal is to connect ContentNation via ActivityPub again.
- [X] full cache for users - [X] full cache for users
- [X] webfinger - [X] webfinger
- [X] discovery endpoints - [X] discovery endpoints
- [X] ap outbox - [ ] ap outbox
- [X] ap inbox - [ ] ap inbox
- [ ] support for AP profile in service - [ ] support for AP profile in service
- [ ] support for article - [ ] support for article
- [ ] support for comment - [ ] support for comment
- [ ] posting comments from ap to service - [ ] posting comments from ap to service
- [X] callback from service to add new input - [ ] callback from service to add new input

View file

@ -1,2 +0,0 @@
create table outbox (`id` char(32) unique primary key, `user` varchar(255), index(user), `timestamp` timestamp, `type` enum ("article", "note"), index(type), `externalid` varchar(255), index(externalid), `apjson` text);
update settings set `value`="2024-07-24" where `key`="database_version";

View file

@ -1,2 +0,0 @@
create table webfinger (`subject` varchar(255) unique primary key, `timestamp` timestamp, index(`timestamp`), `aliases` text, `links` text);
update settings set `value`="2024-07-22" where `key`="database_version";

View file

@ -1,3 +0,0 @@
create table settings(`key` varchar(255) unique primary key, `value` text);
create table users(`id` varchar(255) unique primary key, `externalid` varchar(255), index(`externalid`), `rsapublic` text, `rsaprivate` text);
insert into settings (`key`, `value`) value ("database_version", "2024-07-19");

View file

@ -64,26 +64,26 @@
"followers":"https://{$fqdn}/{$username}/followers", "followers":"https://{$fqdn}/{$username}/followers",
"inbox":"https://{$fqdn}/{$username}/inbox", "inbox":"https://{$fqdn}/{$username}/inbox",
"outbox":"https://{$fqdn}/{$username}/outbox", "outbox":"https://{$fqdn}/{$username}/outbox",
{*"featured":"https://{$fqdn}/{$username}/collections/featured", "featured":"https://{$fqdn}/{$username}/collections/featured",
"featuredTags":"https://{$fqdn}/{$username}/collections/tags",*} "featuredTags":"https://{$fqdn}/{$username}/collections/tags",
"preferredUsername":"{$username}", "preferredUsername":"{$username}",
"name":"{$name}", "name":"{$name}",
"summary":"{$summary}", "summary":"{$summary}",
"url":"https://{$sourcedomain}/@{$username}", "url":"https://{$fqdn}/@{$username}",
"manuallyApprovesFollowers":false, "manuallyApprovesFollowers":false,
"discoverable":true, "discoverable":true,
"published":"{$registered}", "published":"{$registered}",
"publicKey":{ldelim} "publicKey":{ldelim}
"id":"https://{$fqdn}/{$username}#main-key", "id":"https://{$fqdn}/{$username}#main-key",
"owner":"https://{$sourcedomain}/{$username}", "owner":"https://{$fqdn}/{$username}",
"publicKeyPem":"{$publickey}" "publicKeyPem":"{$publickey}"
{rdelim}, {rdelim},
"tag":[], "tag":[],
"attachment":[ "attachment":[
{if $type==='group'}{ldelim} {if $type==='group'}{ldelim}
"type":"PropertyValue", "type":"PropertyValue",
"name":"website", "name":"website",
"value":"\u003ca href=\"https://{$sourcedomain}/@{$username}\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003e{$fqdn}/@{$username}\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e" "value":"\u003ca href=\"https://{$fqdn}/@{$username}\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003e{$fqdn}/@{$username}\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
{rdelim}{/if} {rdelim}{/if}
], ],
"endpoints":{ldelim} "endpoints":{ldelim}

View file

@ -1,7 +1,7 @@
{ldelim} {ldelim}
"subject": "acct:{$username}@{$sourcedomain}", "subject": "acct:{$username}@{$domain}",
"aliases": [ "aliases": [
"https://{$sourcedomain}/@{$username}" "https://{$domain}/@{$username}"
], ],
"links": [ "links": [
{ldelim}"rel": "self", "type": "application/activity+json", "href": "https://{$domain}/{$username}"{rdelim}, {ldelim}"rel": "self", "type": "application/activity+json", "href": "https://{$domain}/{$username}"{rdelim},