diff --git a/src/Entity/Browser/Container/Page.php b/src/Entity/Browser/Container/Page.php index 90bf877..a0a67a9 100644 --- a/src/Entity/Browser/Container/Page.php +++ b/src/Entity/Browser/Container/Page.php @@ -104,18 +104,117 @@ class Page } public function update( - bool $history = true + bool $history = true, + int $refresh = 100 ): void { + // Update history + if ($history) + { + // Save request in memory + $this->navbar->history->add( + $this->navbar->request->getValue() + ); + + // Save request in database + $this->container->browser->database->renewHistory( + $this->navbar->request->getValue(), + // @TODO title + ); + } + + // Update title + $this->title->set( + _('Loading...') + ); + // Show progressbar $this->progressbar->infinitive(); - // Update content entity - $this->content->update( - $history + // Update content by multi-protocol responser + $response = new \Yggverse\Yoda\Model\Response( + $this->navbar->request->getValue() ); - // Hide progressbar - $this->progressbar->hide(); + // Listen response + \Gtk::timeout_add( + $refresh, + function() use ($response) + { + // Redirect requested + if ($location = $response->getRedirect()) + { + $this->open( + $location + ); + + return false; // stop + } + + // Update title + $this->title->set( + $response->getTitle(), + $response->getSubtitle(), + $response->getTooltip() + ); + + // Update content + switch ($response->getMime()) + { + case 'text/gemini': + + $title = null; + + $this->content->setGemtext( + (string) $response->getData(), + $title + ); + + if ($title) + { + $this->title->setValue( + $title + ); + } + + break; + + case 'text/plain': + + $this->content->setPlain( + (string) $response->getData() + ); + + break; + + default: + + throw new \Exception( + _('MIME type not supported') + ); + } + + // Response form requested + if ($request = $response->getRequest()) + { + $this->response->show( + $request['placeholder'], + $request['visible'] + ); + } + + else $this->response->hide(); + + // Stop event loop on request expired or completed + if ($response->isExpired() || $response->isCompleted()) + { + // Hide progressbar + $this->progressbar->hide(); + + // Stop + return false; + } + } + ); } } \ No newline at end of file diff --git a/src/Entity/Browser/Container/Page/Content.php b/src/Entity/Browser/Container/Page/Content.php index c52e1a1..560ee9f 100644 --- a/src/Entity/Browser/Container/Page/Content.php +++ b/src/Entity/Browser/Container/Page/Content.php @@ -69,379 +69,23 @@ class Content // @TODO } - public function update( - bool $history = true + public function setGemtext( + ?string $data = null, + ?string &$title = null ): void { - // Parse address - $address = new \Yggverse\Net\Address( - $this->page->navbar->request->getValue() + $this->data->setGemtext( + $data, + $title ); + } - // Init new title - $this->page->title->set( - $address->getHost(), - 'loading...' - ); - - if ($history) - { - // Remember address in the navigation memory - $this->page->navbar->history->add( - $address->get() - ); - - // Update history in database - $this->page->container->browser->database->renewHistory( - $address->get(), - // @TODO title - ); - } - - // Detect protocol - switch ($address->getScheme()) - { - case 'file': - - switch (true) - { - // Try directory - case ( - $list = \Yggverse\Yoda\Model\Filesystem::getList( - $address->getPath() - ) - ): - - $map = []; - - foreach ($list as $item) - { - $map[] = trim( - sprintf( - '=> file://%s %s', - $item['path'], - $item['name'] . ( - $item['file'] ? null : '/' - ) - ) - ); - } - - $this->data->setGemtext( - implode( - PHP_EOL, - $map - ) . PHP_EOL - ); - - $this->page->title->set( - basename( - $address->getPath() - ), - 'localhost' - ); - - break; - - // Try open file by extension supported - case str_ends_with( - $address->getPath(), - '.gmi' - ): - - $title = null; - - $this->data->setGemtext( - file_get_contents( // @TODO format relative links - $address->getPath() - ), - $title - ); - - if ($title) // detect title by document h1 - { - $this->page->title->set( - $title, - 'localhost' - ); - } - - else - { - $this->page->title->set( - basename( - $address->getPath() - ), - 'localhost' - ); - } - - break; - - default: - - $this->page->title->set( - 'Failure', - 'resource not found or not readable' - ); - - $this->data->setPlain( - 'Could not open location' - ); - } - - break; - - case 'nex': - - $client = new \Yggverse\Nex\Client; - - if ($response = $client->request($address->get())) - { - // Detect content type - switch (true) - { - case in_array( - pathinfo( - strval( - $address->getPath() - ), - PATHINFO_EXTENSION - ), - [ - 'gmi', - 'gemini' - ] - ): - - $title = null; - - $this->data->setGemtext( - $response, - $title - ); - - $this->page->title->set( - $title ? sprintf( - '%s - %s', - $title, - $address->getHost() - ) : $address->getHost() - ); - - break; - - default: - - $this->data->setMono( - $response - ); - - $this->page->title->set( - $address->getHost() - ); - } - } - - else - { - $this->page->title->set( - 'Failure', - 'could not open resource' - ); - - $this->data->setPlain( - 'Requested resource not available!' - ); - } - - break; - - case 'gemini': - - $request = new \Yggverse\Gemini\Client\Request( - $address->get() - ); - - $response = new \Yggverse\Gemini\Client\Response( - $request->getResponse() - ); - - // Route status code - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes - switch ($response->getCode()) - { - case 10: // response expected - case 11: // sensitive input - - $this->page->title->set( - $address->getHost(), - $response->getMeta() ? $response->getMeta() : 'response expected' - ); - - $this->page->response->show( - $response->getMeta(), // placeholder - boolval(11 !== $response->getCode()) // input visibility - ); - - break; - - case 20: // ok - - // Detect content type - switch (true) - { - case str_contains( - $response->getMeta(), - 'text/gemini' - ): - - case in_array( - pathinfo( - strval( - $address->getPath() - ), - PATHINFO_EXTENSION - ), - [ - 'gmi', - 'gemini' - ] - ): - - $title = null; - - $this->data->setGemtext( - $response->getBody(), - $title - ); - - $this->page->title->set( - $title ? sprintf( - '%s - %s', - $title, - $address->getHost() - ) : $address->getHost(), // detect title by document h1 - $response->getMeta() - ); - - break; - - default: - - $this->data->setPlain( - $response->getBody() - ); - - $this->page->title->set( - $address->getHost() - ); - } - - break; - - case 31: // redirect @TODO - - $this->data->setGemtext( - sprintf( - '=> %s', - $response->getMeta() - ) - ); - - $this->page->title->set( - $address->getHost(), - sprintf( - 'redirect (code %d)', - intval( - $response->getCode() - ) - ) - ); - - break; - - default: - - $this->page->title->set( - 'Failure', - sprintf( - '%s (code %d)', - $response->getMeta() ? $response->getMeta() : 'could not open resource', - intval( - $response->getCode() - ) - ) - ); - - $this->data->setPlain( - 'Requested resource not available!' - ); - } - - break; - - case null: - - // Try gemini protocol - $address = new \Yggverse\Net\Address( - sprintf( - 'gemini://%s', - $this->page->navbar->request->getValue() - ) - ); - - // Is hostname request - if (filter_var( - $address->getHost(), - FILTER_VALIDATE_DOMAIN, - FILTER_FLAG_HOSTNAME - ) - ) { - $this->page->navbar->request->setValue( - $address->get() - ); - } - - // Is search request - else - { - $this->page->navbar->request->setValue( - sprintf( - 'gemini://tlgs.one/search?%s', // @TODO custom provider - urlencode( - $this->page->navbar->request->getValue() - ) - ) - ); - } - - $this->update(); - - return; - - default: - - $this->page->title->set( - 'Oops!', - 'protocol not supported!' - ); - - $this->data->setPlain( - 'Protocol not supported!' - ); - } - - // Render content - $this->gtk->show_all(); - - // Refresh page components - $this->page->refresh(); - - // Update window header - $this->page->container->browser->header->setTitle( - $this->page->title->getValue(), - $this->page->title->getSubtitle(), + public function setPlain( + ?string $data = null + ): void + { + $this->data->setPlain( + $data ); } } \ No newline at end of file diff --git a/src/Model/Response.php b/src/Model/Response.php new file mode 100644 index 0000000..ee068f6 --- /dev/null +++ b/src/Model/Response.php @@ -0,0 +1,339 @@ +_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() + ); + + // Route status code + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes + switch ($response->getCode()) + { + case 10: // response expected + case 11: // sensitive input + + $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() + ); + + $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 isExpired(): bool + { + return $this->_expired; + } + + 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