diff --git a/lib/Controller/Implementation/ConfigImplementation.php b/lib/Controller/Implementation/ConfigImplementation.php index 49a0a2f63..8e995543a 100644 --- a/lib/Controller/Implementation/ConfigImplementation.php +++ b/lib/Controller/Implementation/ConfigImplementation.php @@ -32,6 +32,7 @@ public function __construct( } protected const KEY_VISIBLE_INFO_BLOCKS = 'visibleInfoBlocks'; + protected const KEY_BROWSERLESS_ADDRESS = 'browserless_address'; /** * Get the current configuration of the app @@ -46,6 +47,7 @@ public function list() { 'update_interval' => $this->dbCacheService->getSearchIndexUpdateInterval(), 'print_image' => $this->service->getPrintImage(), self::KEY_VISIBLE_INFO_BLOCKS => $this->service->getVisibleInfoBlocks(), + self::KEY_BROWSERLESS_ADDRESS => $this->service->getBrowserlessAddress(), ], Http::STATUS_OK); } @@ -75,6 +77,10 @@ public function config() { $this->service->setPrintImage((bool)$data['print_image']); } + if (isset($data['browserlessAddress'])) { + $this->service->setBrowserlessAddress($data['browserlessAddress']); + } + if (isset($data[self::KEY_VISIBLE_INFO_BLOCKS])) { $this->service->setVisibleInfoBlocks($data[self::KEY_VISIBLE_INFO_BLOCKS]); } diff --git a/lib/Helper/Filter/JSON/RecipeIdTypeFilter.php b/lib/Helper/Filter/JSON/RecipeIdTypeFilter.php index 14acfa2ea..850619cfe 100644 --- a/lib/Helper/Filter/JSON/RecipeIdTypeFilter.php +++ b/lib/Helper/Filter/JSON/RecipeIdTypeFilter.php @@ -8,11 +8,15 @@ * The id should be a string and no integer. */ class RecipeIdTypeFilter extends AbstractJSONFilter { - public function apply(array &$json): bool { - $copy = $json; - if (array_key_exists('id', $json)) { - $json['id'] = strval($json['id']); - } + public function apply(array &$json): bool { + $copy = $json; + if (array_key_exists('id', $json)) { + $json['id'] = strval($json['id']); + } + // Check and fix `id` under `author` + if (array_key_exists('author', $json) && is_array($json['author']) && array_key_exists('id', $json['author'])) { + $json['id'] = strval($json['author']['id']); + } return $json !== $copy; } diff --git a/lib/Helper/UserConfigHelper.php b/lib/Helper/UserConfigHelper.php index f78cc5f62..0333803fe 100644 --- a/lib/Helper/UserConfigHelper.php +++ b/lib/Helper/UserConfigHelper.php @@ -41,6 +41,7 @@ public function __construct( protected const KEY_PRINT_IMAGE = 'print_image'; protected const KEY_VISIBLE_INFO_BLOCKS = 'visible_info_blocks'; protected const KEY_FOLDER = 'folder'; + protected const KEY_BROWSERLESS_ADDRESS = 'browserless_address'; /** * Checks if the user is logged in and the configuration can be obtained at all @@ -155,6 +156,27 @@ public function setPrintImage(bool $value): void { $this->setRawValue(self::KEY_PRINT_IMAGE, '0'); } } + /** + * Gets the browserless address from the configuration + * + * @return string The browserless address + * @throws UserNotLoggedInException if no user is logged in + */ + public function getBrowserlessAddress(): string { + $rawValue = $this->getRawValue(self::KEY_BROWSERLESS_ADDRESS); + + return $rawValue; + } + + /** + * Sets the browserless address in the configuration + * + * @param string $address The browserless address to store + * @throws UserNotLoggedInException if no user is logged in + */ + public function setBrowserlessAddress(string $address): void { + $this->setRawValue(self::KEY_BROWSERLESS_ADDRESS, $address); + } /** * Determines which info blocks are displayed next to the recipe diff --git a/lib/Service/HtmlDownloadService.php b/lib/Service/HtmlDownloadService.php index 5ce2013ef..7fa5d8caa 100644 --- a/lib/Service/HtmlDownloadService.php +++ b/lib/Service/HtmlDownloadService.php @@ -13,6 +13,7 @@ use OCA\Cookbook\Helper\HTMLFilter\HtmlEncodingFilter; use OCA\Cookbook\Helper\HTMLFilter\HtmlEntityDecodeFilter; use OCA\Cookbook\Helper\HtmlToDomParser; +use OCA\Cookbook\Helper\UserConfigHelper; use OCP\IL10N; use Psr\Log\LoggerInterface; @@ -49,7 +50,11 @@ class HtmlDownloadService { */ private $dom; + /** @var userConfigHelper */ + private $userConfigHelper; + public function __construct( + userConfigHelper $userConfigHelper, HtmlEntityDecodeFilter $htmlEntityDecodeFilter, HtmlEncodingFilter $htmlEncodingFilter, IL10N $l10n, @@ -59,6 +64,7 @@ public function __construct( EncodingGuessingHelper $encodingGuesser, DownloadEncodingHelper $downloadEncodingHelper, ) { + $this->userConfigHelper = $userConfigHelper; $this->htmlFilters = [ $htmlEntityDecodeFilter, $htmlEncodingFilter, @@ -82,7 +88,16 @@ public function __construct( * @throws ImportException If obtaining of the URL was not possible */ public function downloadRecipe(string $url): int { - $html = $this->fetchHtmlPage($url); + $browserlessAddress = $this->userConfigHelper->getBrowserlessAddress(); + + // Check if a browserless address is available + if ($browserlessAddress) { + // Use Browserless API if the address is set + $html = $this->fetchHtmlPageUsingBrowserless($url); + } else { + // Otherwise, use the standard method + $html = $this->fetchHtmlPage($url); + } // Filter the HTML code /** @var AbstractHtmlFilter $filter */ @@ -113,7 +128,61 @@ public function getDom(): ?DOMDocument { * * @return string The content of the page as a plain string */ - private function fetchHtmlPage(string $url): string { + /** + * Fetch an HTML page from Browserless.io (rendered HTML) + * + * @param string $url The URL of the page to fetch + * + * @throws ImportException If the given URL was not fetched or parsed + * + * @return string The rendered HTML content as a plain string + */ + private function fetchHtmlPageUsingBrowserless(string $url): string { + + // Get the browserless address from configuration or setting + $browserlessAddress = $this->userConfigHelper->getBrowserlessAddress(); // Retrieve from userConfigHelper or wherever it's stored + + if (empty($browserlessAddress)) { + // Handle the case where Browserless address is not configured + $this->logger->error('Browserless address is not set.'); + throw new ImportException($this->l->t('Browserless address is not configured.')); + } + + // API endpoint for Browserless.io + $apiEndpoint = $browserlessAddress . '/content'; // Use the dynamic address + // Prepare the data to be sent in the POST request + $data = json_encode([ + 'url' => $url, + ]); + + // Define the HTTP context options for the request + $options = [ + 'http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'content' => $data, + ], + ]; + + // Create the stream context for the request + $context = stream_context_create($options); + + // Send the request to Browserless.io + $response = file_get_contents($apiEndpoint, false, $context); + + // Check if the response was successful + if ($response === false) { + $this->logger->error('Failed to fetch rendered HTML from Browserless.io'); + throw new ImportException($this->l->t('Failed to fetch rendered HTML.')); + } + + // Create a new DOMDocument to parse the HTML response + + // You can return the DOMDocument or the raw HTML string based on your needs + return $response; + } + + private function fetchHtmlPage(string $url): string { $host = parse_url($url); if (!$host) { diff --git a/lib/Service/RecipeService.php b/lib/Service/RecipeService.php index b7c0cf7cc..fb82355e1 100755 --- a/lib/Service/RecipeService.php +++ b/lib/Service/RecipeService.php @@ -564,6 +564,14 @@ public function getVisibleInfoBlocks(): array { return $this->userConfigHelper->getVisibleInfoBlocks(); } + public function getBrowserlessAddress() { + return $this->userConfigHelper->getBrowserlessAddress(); // Default to an empty string if not set + } + + public function setBrowserlessAddress(string $address) { + $this->userConfigHelper->setBrowserlessAddress($address); + } + /** * Get recipe file contents as an array * diff --git a/src/components/Modals/SettingsDialog.vue b/src/components/Modals/SettingsDialog.vue index 7d96821d0..78cfcff11 100644 --- a/src/components/Modals/SettingsDialog.vue +++ b/src/components/Modals/SettingsDialog.vue @@ -46,6 +46,25 @@ + +
+ +
+
} */ const writeChanges = ref(true); +/** + * @type {import('vue').Ref} + */ +const browserlessAddress = ref(''); // Watchers watch( @@ -383,6 +406,23 @@ const pickRecipeFolder = () => { ); }); }; +watch( + () => browserlessAddress.value, + async (newVal, oldVal) => { + if (!writeChanges.value) { + return; + } + try { + await api.config.browserlessAddress.update(newVal); + await store.dispatch('refreshConfig'); + } catch { + await showSimpleAlertModal( + t('cookbook', 'Could not save Browserless address'), + ); + browserlessAddress.value = oldVal; // Revert if save fails + } + } +); /** * Reindex all recipes @@ -434,6 +474,7 @@ const handleShowSettings = () => { showFiltersInRecipeList.value = store.state.localSettings.showFiltersInRecipeList; updateInterval.value = config.update_interval; + browserlessAddress.value = config.browserlessAddress; recipeFolder.value = config.folder; nextTick(() => { diff --git a/src/js/api-interface.js b/src/js/api-interface.js index 9150bd61f..0f0cece3c 100644 --- a/src/js/api-interface.js +++ b/src/js/api-interface.js @@ -112,6 +112,10 @@ function reindex() { return instance.post(`${baseUrl}/reindex`); } +function updateBrowserlessAddress(newAddress) { + return instance.post(`${baseUrl}/config`, { browserlessAddress: newAddress }); +} + export default { recipes: { create: createNewRecipe, @@ -146,5 +150,8 @@ export default { visibleInfoBlocks: { update: updateVisibleInfoBlocks, }, + browserlessAddress: { + update: updateBrowserlessAddress, + }, }, };