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
This commit is contained in:
Yannis Vogel 2025-05-15 17:39:12 +02:00
parent 49a4bee76a
commit 6cf9a030a4
No known key found for this signature in database
17 changed files with 150 additions and 70 deletions

View file

@ -359,6 +359,7 @@ return [
'directory_list' => [ 'directory_list' => [
'vendor/phan/phan/src/Phan', 'vendor/phan/phan/src/Phan',
'vendor/smarty/smarty/src', 'vendor/smarty/smarty/src',
'vendor/resque/php-resque/lib',
'php/', 'php/',
'plugins', 'plugins',
'htdocs', 'htdocs',

View file

@ -3,7 +3,8 @@
"description": "A federation service", "description": "A federation service",
"type": "project", "type": "project",
"require": { "require": {
"smarty/smarty": "^5.3" "smarty/smarty": "^5.3",
"resque/php-resque": "^1.3.6"
}, },
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"authors": [ "authors": [
@ -14,5 +15,10 @@
], ],
"require-dev": { "require-dev": {
"phan/phan": "^5.4" "phan/phan": "^5.4"
},
"autoload": {
"psr-4": {
"Federator\\": "php/federator/"
}
} }
} }

View file

@ -8,8 +8,8 @@ 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'
@ -21,5 +21,5 @@ username = 'federatoradmin'
password = '*change*me*as*well' password = '*change*me*as*well'
[keys] [keys]
federatorPrivateKeyPath = '../federator.key' federatorPrivateKeyPath = 'federator.key'
federatorPublicKeyPath = '../federator.pub' federatorPublicKeyPath = 'federator.pub'

View file

@ -7,4 +7,4 @@ url = 'https://userdata.contentnation.net'
[keys] [keys]
headerSenderName = 'contentnation' headerSenderName = 'contentnation'
publicKeyPath = '../contentnation.pub' 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();

View file

@ -54,21 +54,22 @@ 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("Inbox::post Signature check failed: " . $e->getMessage()); throw new \Federator\Exceptions\Unauthorized("Inbox::post Signature check failed: " . $e->getMessage());
http_response_code(401);
return false;
} }
$activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null; $activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
if (!is_array($activity)) { if (!is_array($activity)) {
error_log("Inbox::post Input wasn't of type array"); throw new \Federator\Exceptions\ServerError("Inbox::post Input wasn't of type array");
return false;
} }
$inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity); $inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
$rootDir = $_SERVER['DOCUMENT_ROOT'] . '../'; if ($inboxActivity === false) {
throw new \Federator\Exceptions\ServerError("Inbox::post couldn't create inboxActivity");
}
$rootDir = PROJECT_ROOT . '/';
// Shared inbox // Shared inbox
if (!isset($_user)) { if (!isset($_user)) {
@ -80,11 +81,6 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
); );
} }
if ($inboxActivity === false) {
error_log("Inbox::post couldn't create inboxActivity, aborting");
return false;
}
$sendTo = $inboxActivity->getCC(); $sendTo = $inboxActivity->getCC();
if ($inboxActivity->getType() === 'Undo') { // for undo the object holds the proper cc if ($inboxActivity->getType() === 'Undo') { // for undo the object holds the proper cc
$object = $inboxActivity->getObject(); $object = $inboxActivity->getObject();
@ -132,9 +128,13 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
continue; continue;
} }
$this->postForUser($dbh, $connector, $cache, $user, $inboxActivity); $token = \Resque::enqueue('inbox', 'Federator\\Jobs\\InboxJob', [
'user' => $user,
'activity' => $inboxActivity->toObject(),
]);
error_log("Inbox::post enqueued job for user: $user with token: $token");
} }
return json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); return "success";
} }
/** /**
@ -147,7 +147,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
* @param \Federator\Data\ActivityPub\Common\Activity $inboxActivity the activity that we received * @param \Federator\Data\ActivityPub\Common\Activity $inboxActivity the activity that we received
* @return boolean response * @return boolean response
*/ */
private static function postForUser($dbh, $connector, $cache, $_user, $inboxActivity) public static function postForUser($dbh, $connector, $cache, $_user, $inboxActivity)
{ {
if (isset($_user)) { if (isset($_user)) {
// get user // get user
@ -158,12 +158,11 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$cache $cache
); );
if ($user === null || $user->id === null) { if ($user === null || $user->id === null) {
error_log("Inbox::postForUser couldn't find user: $_user"); throw new \Federator\Exceptions\ServerError("Inbox::postForUser couldn't find user: $_user");
return false;
} }
} }
$rootDir = $_SERVER['DOCUMENT_ROOT'] . '../'; $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_' . $_user . '.log', $rootDir . 'logs/inbox_' . $_user . '.log',

View file

@ -131,7 +131,7 @@ class NewContent implements \Federator\Api\APIInterface
} }
if (empty($users)) { // todo remove after proper implementation, debugging for now if (empty($users)) { // todo remove after proper implementation, debugging for now
$rootDir = $_SERVER['DOCUMENT_ROOT'] . '../'; $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/newContent.log', $rootDir . 'logs/newContent.log',
@ -186,7 +186,7 @@ class NewContent implements \Federator\Api\APIInterface
return false; return false;
} }
$rootDir = $_SERVER['DOCUMENT_ROOT'] . '../'; $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/newcontent_' . $_user . '.log', $rootDir . 'logs/newcontent_' . $_user . '.log',

View file

@ -12,7 +12,7 @@ class Accept extends Activity
{ {
public function __construct() public function __construct()
{ {
parent::__construct('Accept'); parent::__construct('Accept');
parent::addContext('https://www.w3.org/ns/activitystreams'); parent::addContext('https://www.w3.org/ns/activitystreams');
} }
} }

View file

@ -15,29 +15,29 @@ class Delete extends Activity
* @var string * @var string
*/ */
private $object = ""; private $object = "";
public function setFObject(string $object): void public function setFObject(string $object): void
{ {
$this->object = $object; $this->object = $object;
} }
public function __construct() public function __construct()
{ {
parent::__construct('Delete'); parent::__construct('Delete');
parent::addContext('https://www.w3.org/ns/activitystreams'); parent::addContext('https://www.w3.org/ns/activitystreams');
} }
/** /**
* create from json/array * create from json/array
* @param mixed $json * @param mixed $json
*/ */
public function fromJson($json): bool public function fromJson($json): bool
{ {
if (array_key_exists('object', $json)) { if (array_key_exists('object', $json)) {
$this->object = $json['object']; $this->object = $json['object'];
unset($json['object']); unset($json['object']);
} }
return parent::fromJson($json); return parent::fromJson($json);
} }
/** /**
* convert internal state to php array * convert internal state to php array
@ -45,10 +45,10 @@ class Delete extends Activity
*/ */
public function toObject() public function toObject()
{ {
$return = parent::toObject(); $return = parent::toObject();
if ($this->object !== "") { if ($this->object !== "") {
$return['object'] = $this->object; $return['object'] = $this->object;
} }
return $return; return $return;
} }
} }

View file

@ -12,7 +12,7 @@ class Reject extends Activity
{ {
public function __construct() public function __construct()
{ {
parent::__construct('Reject'); parent::__construct('Reject');
parent::addContext('https://www.w3.org/ns/activitystreams'); parent::addContext('https://www.w3.org/ns/activitystreams');
} }
} }

View file

@ -0,0 +1,67 @@
<?php
namespace Federator\Jobs;
class InboxJob
{
/** @var array<string, mixed> $args Arguments for the job */
public $args = [];
/**
* cache instance
*
* @var \Federator\Cache\Cache $cache
*/
protected $cache;
/**
* remote connector
*
* @var \Federator\Connector\Connector $connector
*/
protected $connector = null;
/**
* database instance
*
* @var \Mysqli $dbh
*/
protected $dbh;
/**
* Set up environment for this job
*/
public function setUp(): void
{
$contentnation = new \Federator\Api();
$contentnation->openDatabase();
$contentnation->loadPlugins();
// Recreate dependencies as needed
$this->dbh = $contentnation->getDatabase(); // get DB connection
$this->connector = $contentnation->getConnector(); // get connector
$this->cache = $contentnation->getCache(); // get cache
}
/**
* Perform the inbox job.
*
* @return bool true on success, false on failure
*/
public function perform(): bool
{
error_log("InboxJob: Starting inbox job");
$user = $this->args['user'];
$activity = $this->args['activity'];
$inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
if ($inboxActivity === false) {
error_log("InboxJob: Failed to create inboxActivity from JSON");
return false;
}
\Federator\Api\FedUsers\Inbox::postForUser($this->dbh, $this->connector, $this->cache, $user, $inboxActivity);
return true;
}
}

View file

@ -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;
} }
} }
@ -112,7 +109,7 @@ class Language
} }
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");
@ -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

View file

@ -78,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;
@ -189,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);
@ -233,9 +233,8 @@ class Main
public function renderTemplate($template, $data) public function renderTemplate($template, $data)
{ {
$smarty = new \Smarty\Smarty(); $smarty = new \Smarty\Smarty();
$root = $_SERVER['DOCUMENT_ROOT']; $smarty->setCompileDir(PROJECT_ROOT . $this->config['templates']['compiledir']);
$smarty->setCompileDir($root . $this->config['templates']['compiledir']); $smarty->setTemplateDir((string) realpath(PROJECT_ROOT . $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) {

View file

@ -24,14 +24,13 @@ class Maintenance
{ {
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

@ -0,0 +1,11 @@
<?php
define('PROJECT_ROOT', dirname(__DIR__, 3));
require_once PROJECT_ROOT . '/vendor/autoload.php';
// Start the worker
$worker = new \Resque_Worker(['inbox']);
fwrite(STDOUT, "*** Starting worker for inbox queue\n");
$worker->work(10); // 10 seconds interval

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;
} }
@ -424,7 +424,7 @@ 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);

View file

@ -53,7 +53,7 @@ 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;
@ -67,10 +67,10 @@ class RedisCache implements Cache
*/ */
private function connect() private function connect()
{ {
$this->redis = new \Redis(); $this->redis = new \Redis();
$this->redis->pconnect($this->config['host'], intval($this->config['port'], 10)); $this->redis->pconnect($this->config['host'], intval($this->config['port'], 10));
// @phan-suppress-next-line PhanTypeMismatchArgumentInternalProbablyReal // @phan-suppress-next-line PhanTypeMismatchArgumentInternalProbablyReal
$this->redis->auth([$this->config['username'], $this->config['password']]); $this->redis->auth([$this->config['username'], $this->config['password']]);
} }
/** /**