mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-04-01 00:55:28 +00:00
refactor response model to multi-protocol connection interface
This commit is contained in:
parent
3316a149a6
commit
f0024a0855
8 changed files with 652 additions and 350 deletions
100
src/Model/Connection.php
Normal file
100
src/Model/Connection.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Yggverse\Yoda\Model;
|
||||
|
||||
use \Yggverse\Net\Address;
|
||||
|
||||
use \Yggverse\Yoda\Model\Connection\File;
|
||||
use \Yggverse\Yoda\Model\Connection\Gemini;
|
||||
use \Yggverse\Yoda\Model\Connection\Nex;
|
||||
|
||||
class Connection extends \Yggverse\Yoda\Abstract\Model\Connection
|
||||
{
|
||||
public function request(
|
||||
string $request,
|
||||
int $timeout = 5
|
||||
): void
|
||||
{
|
||||
// Build address instance
|
||||
$address = new Address(
|
||||
$request
|
||||
);
|
||||
|
||||
// Detect protocol
|
||||
switch ($address->getScheme())
|
||||
{
|
||||
case 'file':
|
||||
|
||||
(new File($this))->sync(
|
||||
$address
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'gemini':
|
||||
|
||||
(new Gemini($this))->sync(
|
||||
$address,
|
||||
$timeout
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 'nex':
|
||||
|
||||
(new Nex($this))->sync(
|
||||
$address,
|
||||
$timeout
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case null: // no scheme provided
|
||||
|
||||
// Use gemini protocol by default
|
||||
$redirect = new Address(
|
||||
sprintf(
|
||||
'gemini://%s',
|
||||
$address->get()
|
||||
)
|
||||
);
|
||||
|
||||
// Hostname valid
|
||||
if (filter_var(
|
||||
$redirect->getHost(),
|
||||
FILTER_VALIDATE_DOMAIN,
|
||||
FILTER_FLAG_HOSTNAME
|
||||
)
|
||||
) {
|
||||
// Redirect
|
||||
$this->setRedirect(
|
||||
$redirect->get()
|
||||
);
|
||||
}
|
||||
|
||||
// Redirect to default search provider
|
||||
else
|
||||
{
|
||||
// @TODO custom providers
|
||||
$this->setRedirect(
|
||||
sprintf(
|
||||
'gemini://tlgs.one/search?%s',
|
||||
urlencode(
|
||||
$request
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
default:
|
||||
|
||||
throw new \Exception(
|
||||
_('Protocol not supported')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/Model/Connection/File.php
Normal file
131
src/Model/Connection/File.php
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Yggverse\Yoda\Model\Connection;
|
||||
|
||||
use \Yggverse\Net\Address;
|
||||
|
||||
use \Yggverse\Yoda\Model\Connection;
|
||||
use \Yggverse\Yoda\Model\Filesystem;
|
||||
|
||||
class File
|
||||
{
|
||||
private Connection $_connection;
|
||||
|
||||
public function __construct(
|
||||
Connection $connection
|
||||
) {
|
||||
$this->_connection = $connection;
|
||||
}
|
||||
|
||||
public function sync(
|
||||
Address $address
|
||||
): void
|
||||
{
|
||||
$this->_connection->setTitle(
|
||||
basename(
|
||||
$address->getPath()
|
||||
)
|
||||
);
|
||||
|
||||
$this->_connection->setSubtitle(
|
||||
$address->getPath()
|
||||
);
|
||||
|
||||
$this->_connection->setTooltip(
|
||||
$address->getPath()
|
||||
);
|
||||
|
||||
switch (true)
|
||||
{
|
||||
case ( // is directory
|
||||
$list = Filesystem::getList(
|
||||
$address->getPath()
|
||||
)
|
||||
):
|
||||
$tree = [];
|
||||
|
||||
foreach ($list as $item)
|
||||
{
|
||||
$tree[] = trim(
|
||||
sprintf(
|
||||
'=> file://%s %s',
|
||||
$item['path'],
|
||||
$item['name'] . (
|
||||
// append slash indicator
|
||||
$item['file'] ? null : '/'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->_connection->setMime(
|
||||
$this->_connection::MIME_TEXT_GEMINI
|
||||
);
|
||||
|
||||
$this->_connection->setData(
|
||||
implode(
|
||||
PHP_EOL,
|
||||
$tree
|
||||
) . PHP_EOL
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case file_exists( // is file
|
||||
$address->getPath()
|
||||
) && is_readable(
|
||||
$address->getPath()
|
||||
):
|
||||
$this->_connection->setData(
|
||||
strval(
|
||||
file_get_contents(
|
||||
$address->getPath()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$this->_connection->setMime(
|
||||
strval(
|
||||
mime_content_type(
|
||||
$address->getPath()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if ($this->_connection::MIME_TEXT_PLAIN == $this->_connection->getMime())
|
||||
{
|
||||
$extension = pathinfo(
|
||||
strval(
|
||||
$address->getPath()
|
||||
),
|
||||
PATHINFO_EXTENSION
|
||||
);
|
||||
|
||||
if (in_array($extension, ['gmi', 'gemini']))
|
||||
{
|
||||
$this->_connection->setMime(
|
||||
$this->_connection::MIME_TEXT_GEMINI
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
$this->_connection->setTitle(
|
||||
_('Failure')
|
||||
);
|
||||
|
||||
$this->_connection->setData(
|
||||
_('Could not open location')
|
||||
);
|
||||
}
|
||||
|
||||
$this->_connection->setCompleted(
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
144
src/Model/Connection/Gemini.php
Normal file
144
src/Model/Connection/Gemini.php
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Yggverse\Yoda\Model\Connection;
|
||||
|
||||
use \Yggverse\Gemini\Client\Request;
|
||||
use \Yggverse\Gemini\Client\Response;
|
||||
use \Yggverse\Net\Address;
|
||||
|
||||
use \Yggverse\Yoda\Model\Connection;
|
||||
|
||||
class Gemini
|
||||
{
|
||||
private Connection $_connection;
|
||||
|
||||
public function __construct(
|
||||
Connection $connection
|
||||
) {
|
||||
$this->_connection = $connection;
|
||||
}
|
||||
|
||||
public function sync(
|
||||
Address $address,
|
||||
int $timeout = 5
|
||||
): void
|
||||
{
|
||||
$request = new Request(
|
||||
$address->get()
|
||||
);
|
||||
|
||||
$response = new Response(
|
||||
$request->getResponse(
|
||||
$timeout
|
||||
)
|
||||
);
|
||||
|
||||
// Route status code
|
||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes
|
||||
switch ($response->getCode())
|
||||
{
|
||||
case 10: // response expected
|
||||
case 11: // sensitive input
|
||||
|
||||
$this->_connection->setMime(
|
||||
$this->_connection::MIME_TEXT_GEMINI
|
||||
);
|
||||
|
||||
$this->_connection->setRequest(
|
||||
[
|
||||
'placeholder' => $response->getMeta(),
|
||||
'visible' => 11 !== $response->getCode()
|
||||
]
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case 20: // ok
|
||||
|
||||
$this->_connection->setData(
|
||||
$response->getBody()
|
||||
);
|
||||
|
||||
switch (true)
|
||||
{
|
||||
case str_contains(
|
||||
$response->getMeta(),
|
||||
self::MIME_TEXT_GEMINI
|
||||
):
|
||||
|
||||
$this->_connection->setMime(
|
||||
$this->_connection::MIME_TEXT_GEMINI
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
case str_contains(
|
||||
$response->getMeta(),
|
||||
self::MIME_TEXT_PLAIN
|
||||
):
|
||||
|
||||
$this->_connection->setMime(
|
||||
$this->_connection::MIME_TEXT_PLAIN
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
throw new \Exception(
|
||||
sprintf(
|
||||
_('MIME type not implemented for %s'),
|
||||
$response->getMeta()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 31: // redirect
|
||||
// show link, no follow
|
||||
|
||||
$this->_connection->setTitle(
|
||||
_('Redirect!')
|
||||
);
|
||||
|
||||
$this->_connection->setData(
|
||||
sprintf(
|
||||
'=> %s',
|
||||
$response->getMeta()
|
||||
)
|
||||
);
|
||||
|
||||
$this->_connection->setMime(
|
||||
$this->_connection::MIME_TEXT_GEMINI
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
$this->_connection->setTitle(
|
||||
_('Oops!')
|
||||
);
|
||||
|
||||
$this->_connection->setData(
|
||||
sprintf(
|
||||
'Could not open request (code: %d)',
|
||||
intval(
|
||||
$response->getCode()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$this->_connection->setMime(
|
||||
$this->_connection::MIME_TEXT_GEMINI
|
||||
);
|
||||
}
|
||||
|
||||
$this->_connection->setCompleted(
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
69
src/Model/Connection/Nex.php
Normal file
69
src/Model/Connection/Nex.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Yggverse\Yoda\Model\Connection;
|
||||
|
||||
use \Yggverse\Net\Address;
|
||||
use \Yggverse\Nex\Client;
|
||||
|
||||
use \Yggverse\Yoda\Model\Connection;
|
||||
|
||||
class Nex
|
||||
{
|
||||
private Connection $_connection;
|
||||
|
||||
public function __construct(
|
||||
Connection $connection
|
||||
) {
|
||||
$this->_connection = $connection;
|
||||
}
|
||||
|
||||
// @TODO
|
||||
public function sync(
|
||||
Address $address,
|
||||
int $timeout = 5
|
||||
): void
|
||||
{
|
||||
$response = (new \Yggverse\Nex\Client)->request(
|
||||
$address->get(),
|
||||
$timeout
|
||||
);
|
||||
|
||||
if ($response)
|
||||
{
|
||||
$this->_connection->setTitle(
|
||||
strval(
|
||||
$address->getHost()
|
||||
)
|
||||
);
|
||||
|
||||
$this->_connection->setData(
|
||||
$response
|
||||
);
|
||||
|
||||
$this->_connection->setMime(
|
||||
$this->_connection::MIME_TEXT_PLAIN
|
||||
);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
$this->_connection->setTitle(
|
||||
_('Oops!')
|
||||
);
|
||||
|
||||
$this->_connection->setData(
|
||||
_('Could not open request')
|
||||
);
|
||||
|
||||
$this->_connection->setMime(
|
||||
$this->_connection::MIME_TEXT_GEMINI
|
||||
);
|
||||
}
|
||||
|
||||
$this->_connection->setCompleted(
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Yggverse\Yoda\Model;
|
||||
|
||||
/*
|
||||
* Single response API for multiple protocol providers
|
||||
*
|
||||
*/
|
||||
class Response
|
||||
{
|
||||
// Subject
|
||||
private \Yggverse\Net\Address $_address;
|
||||
|
||||
// Async status
|
||||
private bool $_completed = false;
|
||||
|
||||
// Response
|
||||
private ?string $_title = null;
|
||||
private ?string $_subtitle = null;
|
||||
private ?string $_tooltip = null;
|
||||
private ?string $_mime = null;
|
||||
private ?string $_data = null;
|
||||
private ?string $_redirect = null;
|
||||
private ?array $_request = null;
|
||||
|
||||
// Config
|
||||
public const MIME_TEXT_GEMINI = 'text/gemini';
|
||||
public const MIME_TEXT_PLAIN = 'text/plain';
|
||||
|
||||
public function __construct(
|
||||
string $request,
|
||||
int $timeout = 5
|
||||
) {
|
||||
// Build address instance
|
||||
$this->_address = new \Yggverse\Net\Address(
|
||||
$request
|
||||
);
|
||||
|
||||
// Detect protocol
|
||||
switch ($this->_address->getScheme())
|
||||
{
|
||||
case 'file':
|
||||
|
||||
$this->_title = basename(
|
||||
$this->_address->getPath()
|
||||
);
|
||||
|
||||
$this->_subtitle = $this->_address->getPath();
|
||||
$this->_tooltip = $this->_address->getPath();
|
||||
|
||||
switch (true)
|
||||
{
|
||||
case (
|
||||
$list = \Yggverse\Yoda\Model\Filesystem::getList(
|
||||
$this->_address->getPath()
|
||||
)
|
||||
): // directory
|
||||
$tree = [];
|
||||
|
||||
foreach ($list as $item)
|
||||
{
|
||||
$tree[] = trim(
|
||||
sprintf(
|
||||
'=> file://%s %s',
|
||||
$item['path'],
|
||||
$item['name'] . (
|
||||
$item['file'] ? null : '/'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->_mime = self::MIME_TEXT_GEMINI;
|
||||
|
||||
$this->_data = implode(
|
||||
PHP_EOL,
|
||||
$tree
|
||||
) . PHP_EOL;
|
||||
|
||||
break;
|
||||
|
||||
case file_exists(
|
||||
$this->_address->getPath()
|
||||
) && is_readable(
|
||||
$this->_address->getPath()
|
||||
):
|
||||
$this->_data = strval(
|
||||
file_get_contents(
|
||||
$this->_address->getPath()
|
||||
)
|
||||
);
|
||||
|
||||
$this->_mime = strval(
|
||||
mime_content_type(
|
||||
$this->_address->getPath()
|
||||
)
|
||||
);
|
||||
|
||||
if ($this->_mime == self::MIME_TEXT_PLAIN)
|
||||
{
|
||||
$extension = pathinfo(
|
||||
strval(
|
||||
$this->_address->getPath()
|
||||
),
|
||||
PATHINFO_EXTENSION
|
||||
);
|
||||
|
||||
if (in_array($extension, ['gmi', 'gemini']))
|
||||
{
|
||||
$this->_mime = self::MIME_TEXT_GEMINI;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
$this->_title = _(
|
||||
'Failure'
|
||||
);
|
||||
|
||||
$this->_data = _(
|
||||
'Could not open location'
|
||||
);
|
||||
}
|
||||
|
||||
$this->_completed = true;
|
||||
|
||||
break;
|
||||
|
||||
case 'gemini': // @TODO async
|
||||
|
||||
$request = new \Yggverse\Gemini\Client\Request(
|
||||
$this->_address->get()
|
||||
);
|
||||
|
||||
$response = new \Yggverse\Gemini\Client\Response(
|
||||
$request->getResponse(
|
||||
$timeout
|
||||
)
|
||||
);
|
||||
|
||||
// Route status code
|
||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes
|
||||
switch ($response->getCode())
|
||||
{
|
||||
case 10: // response expected
|
||||
case 11: // sensitive input
|
||||
|
||||
$this->_mime = self::MIME_TEXT_GEMINI;
|
||||
|
||||
$this->_request =
|
||||
[
|
||||
'placeholder' => $response->getMeta(),
|
||||
'visible' => 11 !== $response->getCode()
|
||||
];
|
||||
|
||||
break;
|
||||
|
||||
case 20: // ok
|
||||
|
||||
$this->_data = $response->getBody();
|
||||
|
||||
switch (true)
|
||||
{
|
||||
case str_contains(
|
||||
$response->getMeta(),
|
||||
self::MIME_TEXT_GEMINI
|
||||
):
|
||||
|
||||
$this->_mime = self::MIME_TEXT_GEMINI;
|
||||
|
||||
break;
|
||||
|
||||
case str_contains(
|
||||
$response->getMeta(),
|
||||
self::MIME_TEXT_PLAIN
|
||||
):
|
||||
|
||||
$this->_mime = self::MIME_TEXT_PLAIN;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
throw new \Exception(
|
||||
sprintf(
|
||||
_('MIME type not implemented for %s'),
|
||||
$response->getMeta()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->_completed = true;
|
||||
|
||||
break;
|
||||
|
||||
case 31: // redirect
|
||||
// show link, no follow
|
||||
|
||||
$this->_data = sprintf(
|
||||
'=> %s',
|
||||
$response->getMeta()
|
||||
);
|
||||
|
||||
$this->_mime = self::MIME_TEXT_GEMINI;
|
||||
|
||||
$this->_completed = true;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
$this->_title = _(
|
||||
'Oops!'
|
||||
);
|
||||
|
||||
$this->_data = sprintf(
|
||||
'Could not open request (code: %d)',
|
||||
intval(
|
||||
$response->getCode()
|
||||
)
|
||||
);
|
||||
|
||||
$this->_mime = self::MIME_TEXT_GEMINI;
|
||||
|
||||
$this->_completed = true;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'nex': // @TODO async
|
||||
|
||||
$this->_data = (
|
||||
new \Yggverse\Nex\Client
|
||||
)->request(
|
||||
$this->_address->get(),
|
||||
$timeout
|
||||
);
|
||||
|
||||
$this->_mime = self::MIME_TEXT_PLAIN; // @TODO
|
||||
|
||||
$this->_completed = true;
|
||||
|
||||
break;
|
||||
|
||||
case null:
|
||||
|
||||
// Build gemini protocol address
|
||||
$address = new \Yggverse\Net\Address(
|
||||
sprintf(
|
||||
'gemini://%s',
|
||||
$this->_address->get()
|
||||
)
|
||||
);
|
||||
|
||||
// Validate hostname
|
||||
if (filter_var(
|
||||
$address->getHost(),
|
||||
FILTER_VALIDATE_DOMAIN,
|
||||
FILTER_FLAG_HOSTNAME
|
||||
)
|
||||
) {
|
||||
// Request redirect
|
||||
$this->_redirect = $address->get();
|
||||
}
|
||||
|
||||
// Request redirect to search provider
|
||||
else
|
||||
{
|
||||
// @TODO custom providers
|
||||
$this->_redirect = sprintf(
|
||||
'gemini://tlgs.one/search?%s',
|
||||
urlencode(
|
||||
$request
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
default:
|
||||
|
||||
throw new \Exception(
|
||||
_('Protocol not supported')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function isCompleted(): bool
|
||||
{
|
||||
return $this->_completed;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->_title;
|
||||
}
|
||||
|
||||
public function getSubtitle(): ?string
|
||||
{
|
||||
return $this->_subtitle;
|
||||
}
|
||||
|
||||
public function getTooltip(): ?string
|
||||
{
|
||||
return $this->_tooltip;
|
||||
}
|
||||
|
||||
public function getMime(): ?string
|
||||
{
|
||||
return $this->_mime;
|
||||
}
|
||||
|
||||
public function getData(): ?string
|
||||
{
|
||||
return $this->_data;
|
||||
}
|
||||
|
||||
public function getRedirect(): ?string
|
||||
{
|
||||
return $this->_redirect;
|
||||
}
|
||||
|
||||
public function getRequest(): ?array
|
||||
{
|
||||
return $this->_request;
|
||||
}
|
||||
|
||||
public function getLength(): ?int
|
||||
{
|
||||
return mb_strlen(
|
||||
$this->_data
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue