diff --git a/src/Abstract/Model/Connection.php b/src/Abstract/Model/Connection.php new file mode 100644 index 00000000..baec4faa --- /dev/null +++ b/src/Abstract/Model/Connection.php @@ -0,0 +1,123 @@ +_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 + ); + } +} \ No newline at end of file diff --git a/src/Entity/Browser/Container/Page.php b/src/Entity/Browser/Container/Page.php index 5dabeda4..1725bfe9 100644 --- a/src/Entity/Browser/Container/Page.php +++ b/src/Entity/Browser/Container/Page.php @@ -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 // 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 } // Response form requested - if ($request = $response->getRequest()) + if ($request = $connection->getRequest()) { $this->response->show( $request['placeholder'], @@ -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 case 'text/plain': $this->content->setPlain( - (string) $response->getData() + (string) $connection->getData() ); break; @@ -220,7 +223,7 @@ class Page } // Stop event loop on request completed - if ($response->isCompleted()) + if ($connection->isCompleted()) { // Hide progressbar $this->progressbar->hide(); diff --git a/src/Interface/Model/Connection.php b/src/Interface/Model/Connection.php new file mode 100644 index 00000000..96100a0a --- /dev/null +++ b/src/Interface/Model/Connection.php @@ -0,0 +1,70 @@ +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') + ); + } + } +} \ No newline at end of file diff --git a/src/Model/Connection/File.php b/src/Model/Connection/File.php new file mode 100644 index 00000000..1e9ebcc9 --- /dev/null +++ b/src/Model/Connection/File.php @@ -0,0 +1,131 @@ +_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 + ); + } +} \ No newline at end of file diff --git a/src/Model/Connection/Gemini.php b/src/Model/Connection/Gemini.php new file mode 100644 index 00000000..9b6eb424 --- /dev/null +++ b/src/Model/Connection/Gemini.php @@ -0,0 +1,144 @@ +_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 + ); + } +} \ No newline at end of file diff --git a/src/Model/Connection/Nex.php b/src/Model/Connection/Nex.php new file mode 100644 index 00000000..84d4714f --- /dev/null +++ b/src/Model/Connection/Nex.php @@ -0,0 +1,69 @@ +_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 + ); + } +} \ No newline at end of file diff --git a/src/Model/Response.php b/src/Model/Response.php deleted file mode 100644 index bdf899df..00000000 --- a/src/Model/Response.php +++ /dev/null @@ -1,338 +0,0 @@ -_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 - ); - } -} \ No newline at end of file