Browse Source

refactor response model to multi-protocol connection interface

PHP-GTK3
yggverse 5 months ago
parent
commit
f0024a0855
  1. 123
      src/Abstract/Model/Connection.php
  2. 27
      src/Entity/Browser/Container/Page.php
  3. 70
      src/Interface/Model/Connection.php
  4. 100
      src/Model/Connection.php
  5. 131
      src/Model/Connection/File.php
  6. 144
      src/Model/Connection/Gemini.php
  7. 69
      src/Model/Connection/Nex.php
  8. 338
      src/Model/Response.php

123
src/Abstract/Model/Connection.php

@ -0,0 +1,123 @@ @@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Abstract\Model;
abstract class Connection implements \Yggverse\Yoda\Interface\Model\Connection
{
// Status
protected bool $_completed = false;
// Response
protected ?string $_title = null;
protected ?string $_subtitle = null;
protected ?string $_tooltip = null;
protected ?string $_mime = null;
protected ?string $_data = null;
protected ?string $_redirect = null;
protected ?array $_request = null;
public function isCompleted(): bool
{
return $this->_completed;
}
public function setCompleted(
bool $completed
): void
{
$this->_completed = $completed;
}
public function getTitle(): ?string
{
return $this->_title;
}
public function setTitle(
?string $title = null
): void
{
$this->_title = $title;
}
public function getSubtitle(): ?string
{
return $this->_subtitle;
}
public function setSubtitle(
?string $subtitle = null
): void
{
$this->_subtitle = $subtitle;
}
public function getTooltip(): ?string
{
return $this->_tooltip;
}
public function setTooltip(
?string $tooltip = null
): void
{
$this->_tooltip = $tooltip;
}
public function getMime(): ?string
{
return $this->_mime;
}
public function setMime(
?string $mime = null
): void
{
$this->_mime = $mime;
}
public function getData(): ?string
{
return $this->_data;
}
public function setData(
?string $data = null
): void
{
$this->_data = $data;
}
public function getRedirect(): ?string
{
return $this->_redirect;
}
public function setRedirect(
?string $redirect = null
): void
{
$this->_redirect = $redirect;
}
public function getRequest(): ?array
{
return $this->_request;
}
public function setRequest(
?array $request = null
): void
{
$this->_request = $request; // @TODO
}
public function getLength(): ?int
{
return mb_strlen(
$this->_data
);
}
}

27
src/Entity/Browser/Container/Page.php

@ -135,8 +135,11 @@ class Page @@ -135,8 +135,11 @@ class Page
// Hide response form
$this->response->hide();
// Update content by multi-protocol responser
$response = new \Yggverse\Yoda\Model\Response(
// Update content using multi-protocol driver
$connection = new \Yggverse\Yoda\Model\Connection;
// Async request
$connection->request(
$this->navbar->request->getValue(),
$timeout
);
@ -147,10 +150,10 @@ class Page @@ -147,10 +150,10 @@ class Page
// Listen response
\Gtk::timeout_add(
$refresh,
function() use ($response, $expire)
function() use ($connection, $expire)
{
// Redirect requested
if ($location = $response->getRedirect())
if ($location = $connection->getRedirect())
{
$this->open(
$location
@ -163,7 +166,7 @@ class Page @@ -163,7 +166,7 @@ class Page
}
// Response form requested
if ($request = $response->getRequest())
if ($request = $connection->getRequest())
{
$this->response->show(
$request['placeholder'],
@ -178,20 +181,20 @@ class Page @@ -178,20 +181,20 @@ class Page
// Update title
$this->title->set(
$response->getTitle(),
$response->getSubtitle(),
$response->getTooltip()
$connection->getTitle(),
$connection->getSubtitle(),
$connection->getTooltip()
);
// Update content
switch ($response->getMime())
switch ($connection->getMime())
{
case 'text/gemini':
$title = null;
$this->content->setGemtext(
(string) $response->getData(),
(string) $connection->getData(),
$title
);
@ -207,7 +210,7 @@ class Page @@ -207,7 +210,7 @@ class Page
case 'text/plain':
$this->content->setPlain(
(string) $response->getData()
(string) $connection->getData()
);
break;
@ -220,7 +223,7 @@ class Page @@ -220,7 +223,7 @@ class Page
}
// Stop event loop on request completed
if ($response->isCompleted())
if ($connection->isCompleted())
{
// Hide progressbar
$this->progressbar->hide();

70
src/Interface/Model/Connection.php

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Yggverse\Yoda\Interface\Model;
/*
* Single API for multiple protocols
*
*/
interface Connection
{
public function request(
string $request,
int $timeout = 5
): void;
public const MIME_TEXT_GEMINI = 'text/gemini';
public const MIME_TEXT_PLAIN = 'text/plain';
public function isCompleted(): bool;
public function setCompleted(
bool $completed
): void;
public function getTitle(): ?string;
public function setTitle(
?string $title = null
): void;
public function getSubtitle(): ?string;
public function setSubtitle(
?string $subtitle = null
): void;
public function getTooltip(): ?string;
public function setTooltip(
?string $tooltip = null
): void;
public function getMime(): ?string;
public function setMime(
?string $mime = null
): void;
public function getData(): ?string;
public function setData(
?string $data = null
): void;
public function getRedirect(): ?string;
public function setRedirect(
?string $redirect = null
): void;
public function getRequest(): ?array;
public function setRequest(
?array $request = null
): void;
public function getLength(): ?int;
}

100
src/Model/Connection.php

@ -0,0 +1,100 @@ @@ -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

@ -0,0 +1,131 @@ @@ -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

@ -0,0 +1,144 @@ @@ -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

@ -0,0 +1,69 @@ @@ -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
);
}
}

338
src/Model/Response.php

@ -1,338 +0,0 @@ @@ -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…
Cancel
Save