X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;ds=sidebyside;f=src%2FWallabag%2FCoreBundle%2FHelper%2FContentProxy.php;h=7e93249ddd8dd8d708ea3a868065b0556a487c96;hb=HEAD;hp=fab052685ccef748c6aba14591e05fc7866780cb;hpb=f80f16dfc858ec90da76daacd405b0cfdaa32f74;p=github%2Fwallabag%2Fwallabag.git diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index fab05268..7e93249d 100644 --- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php +++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php @@ -12,13 +12,14 @@ use Wallabag\CoreBundle\Entity\Entry; use Wallabag\CoreBundle\Tools\Utils; /** - * This kind of proxy class take care of getting the content from an url - * and update the entry with what it found. + * This kind of proxy class takes care of getting the content from an url + * and updates the entry with what it found. */ class ContentProxy { protected $graby; protected $tagger; + protected $ignoreOriginProcessor; protected $validator; protected $logger; protected $mimeGuesser; @@ -26,10 +27,11 @@ class ContentProxy protected $eventDispatcher; protected $storeArticleHeaders; - public function __construct(Graby $graby, RuleBasedTagger $tagger, ValidatorInterface $validator, LoggerInterface $logger, $fetchingErrorMessage, $storeArticleHeaders = false) + public function __construct(Graby $graby, RuleBasedTagger $tagger, RuleBasedIgnoreOriginProcessor $ignoreOriginProcessor, ValidatorInterface $validator, LoggerInterface $logger, $fetchingErrorMessage, $storeArticleHeaders = false) { $this->graby = $graby; $this->tagger = $tagger; + $this->ignoreOriginProcessor = $ignoreOriginProcessor; $this->validator = $validator; $this->logger = $logger; $this->mimeGuesser = new MimeTypeExtensionGuesser(); @@ -47,13 +49,18 @@ class ContentProxy */ public function updateEntry(Entry $entry, $url, array $content = [], $disableContentUpdate = false) { + $this->graby->toggleImgNoReferrer(true); if (!empty($content['html'])) { $content['html'] = $this->graby->cleanupHtml($content['html'], $url); } if ((empty($content) || false === $this->validateContent($content)) && false === $disableContentUpdate) { $fetchedContent = $this->graby->fetchContent($url); - $fetchedContent['title'] = $this->sanitizeContentTitle($fetchedContent['title'], $fetchedContent['content_type']); + + $fetchedContent['title'] = $this->sanitizeContentTitle( + $fetchedContent['title'], + isset($fetchedContent['headers']['content-type']) ? $fetchedContent['headers']['content-type'] : '' + ); // when content is imported, we have information in $content // in case fetching content goes bad, we'll keep the imported information instead of overriding them @@ -66,55 +73,21 @@ class ContentProxy // so we'll be able to refetch it in the future $content['url'] = !empty($content['url']) ? $content['url'] : $url; - $this->stockEntry($entry, $content); - } - - /** - * Try to sanitize the title of the fetched content from wrong character encodings and invalid UTF-8 character. - * @param $title - * @param $contentType - * @return string - */ - private function sanitizeContentTitle($title, $contentType) { - if ('application/pdf' === $contentType) { - $convertedTitle = $this->convertPdfEncodingToUTF8($title); - return $this->sanitizeUTF8Text($convertedTitle); + // In one case (at least in tests), url is empty here + // so we set it using $url provided in the updateEntry call. + // Not sure what are the other possible cases where this property is empty + if (empty($entry->getUrl()) && !empty($url)) { + $entry->setUrl($url); } - return $this->sanitizeUTF8Text($title); - } - /** - * If the title from the fetched content comes from a PDF, then its very possible that the character encoding is not - * UTF-8. This methods tries to identify the character encoding and translate the title to UTF-8. - * @param $title - * @return string (maybe contains invalid UTF-8 character) - */ - private function convertPdfEncodingToUTF8($title) { - // first try UTF-16 (then UTF-8) because its easier to detect its present/absence - foreach (array('UTF-16BE', 'UTF-16LE', 'UTF-8', 'WINDOWS-1252') as $encoding) { - if (mb_check_encoding($title, $encoding)) { - return mb_convert_encoding($title, 'UTF-8', $encoding); - } - } - return $title; - } + $entry->setGivenUrl($url); - /** - * Remove invalid UTF-8 characters from the given string. - * @param String $rawText - * @return string - */ - private function sanitizeUTF8Text($rawText) { - if (mb_check_encoding($rawText, 'UTF-8')) { - return $rawText; - } - return iconv("UTF-8", "UTF-8//IGNORE", $rawText); + $this->stockEntry($entry, $content); } /** * Use a Symfony validator to ensure the language is well formatted. * - * @param Entry $entry * @param string $value Language to validate and save */ public function updateLanguage(Entry $entry, $value) @@ -140,7 +113,6 @@ class ContentProxy /** * Use a Symfony validator to ensure the preview picture is a real url. * - * @param Entry $entry * @param string $value URL to validate and save */ public function updatePreviewPicture(Entry $entry, $value) @@ -162,7 +134,6 @@ class ContentProxy /** * Update date. * - * @param Entry $entry * @param string $value Date to validate and save */ public function updatePublishedAt(Entry $entry, $value) @@ -189,8 +160,6 @@ class ContentProxy /** * Helper to extract and save host from entry url. - * - * @param Entry $entry */ public function setEntryDomainName(Entry $entry) { @@ -204,8 +173,6 @@ class ContentProxy * Helper to set a default title using: * - url basename, if applicable * - hostname. - * - * @param Entry $entry */ public function setDefaultEntryTitle(Entry $entry) { @@ -219,6 +186,59 @@ class ContentProxy $entry->setTitle($path); } + /** + * Try to sanitize the title of the fetched content from wrong character encodings and invalid UTF-8 character. + * + * @param string $title + * @param string $contentType + * + * @return string + */ + private function sanitizeContentTitle($title, $contentType) + { + if ('application/pdf' === $contentType) { + $title = $this->convertPdfEncodingToUTF8($title); + } + + return $this->sanitizeUTF8Text($title); + } + + /** + * If the title from the fetched content comes from a PDF, then its very possible that the character encoding is not + * UTF-8. This methods tries to identify the character encoding and translate the title to UTF-8. + * + * @param $title + * + * @return string (maybe contains invalid UTF-8 character) + */ + private function convertPdfEncodingToUTF8($title) + { + // first try UTF-8 because its easier to detect its present/absence + foreach (['UTF-8', 'UTF-16BE', 'WINDOWS-1252'] as $encoding) { + if (mb_check_encoding($title, $encoding)) { + return mb_convert_encoding($title, 'UTF-8', $encoding); + } + } + + return $title; + } + + /** + * Remove invalid UTF-8 characters from the given string. + * + * @param string $rawText + * + * @return string + */ + private function sanitizeUTF8Text($rawText) + { + if (mb_check_encoding($rawText, 'UTF-8')) { + return $rawText; + } + + return iconv('UTF-8', 'UTF-8//IGNORE', $rawText); + } + /** * Stock entry with fetched or imported content. * Will fall back to OpenGraph data if available. @@ -228,28 +248,25 @@ class ContentProxy */ private function stockEntry(Entry $entry, array $content) { - $entry->setUrl($content['url']); + $this->updateOriginUrl($entry, $content['url']); $this->setEntryDomainName($entry); if (!empty($content['title'])) { $entry->setTitle($content['title']); - } elseif (!empty($content['open_graph']['og_title'])) { - $entry->setTitle($content['open_graph']['og_title']); } - $html = $content['html']; - if (false === $html) { - $html = $this->fetchingErrorMessage; + if (empty($content['html'])) { + $content['html'] = $this->fetchingErrorMessage; - if (!empty($content['open_graph']['og_description'])) { - $html .= '

But we found a short description:

'; - $html .= $content['open_graph']['og_description']; + if (!empty($content['description'])) { + $content['html'] .= '

But we found a short description:

'; + $content['html'] .= $content['description']; } } - $entry->setContent($html); - $entry->setReadingTime(Utils::getReadingTime($html)); + $entry->setContent($content['html']); + $entry->setReadingTime(Utils::getReadingTime($content['html'])); if (!empty($content['status'])) { $entry->setHttpStatus($content['status']); @@ -259,8 +276,8 @@ class ContentProxy $entry->setPublishedBy($content['authors']); } - if (!empty($content['all_headers']) && $this->storeArticleHeaders) { - $entry->setHeaders($content['all_headers']); + if (!empty($content['headers'])) { + $entry->setHeaders($content['headers']); } if (!empty($content['date'])) { @@ -271,17 +288,30 @@ class ContentProxy $this->updateLanguage($entry, $content['language']); } - if (!empty($content['open_graph']['og_image'])) { - $this->updatePreviewPicture($entry, $content['open_graph']['og_image']); + $previewPictureUrl = ''; + if (!empty($content['image'])) { + $previewPictureUrl = $content['image']; } // if content is an image, define it as a preview too - if (!empty($content['content_type']) && \in_array($this->mimeGuesser->guess($content['content_type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { - $this->updatePreviewPicture($entry, $content['url']); + if (!empty($content['headers']['content-type']) && \in_array($this->mimeGuesser->guess($content['headers']['content-type']), ['jpeg', 'jpg', 'gif', 'png'], true)) { + $previewPictureUrl = $content['url']; + } elseif (empty($previewPictureUrl)) { + $this->logger->debug('Extracting images from content to provide a default preview picture'); + $imagesUrls = DownloadImages::extractImagesUrlsFromHtml($content['html']); + $this->logger->debug(\count($imagesUrls) . ' pictures found'); + + if (!empty($imagesUrls)) { + $previewPictureUrl = $imagesUrls[0]; + } } - if (!empty($content['content_type'])) { - $entry->setMimetype($content['content_type']); + if (!empty($content['headers']['content-type'])) { + $entry->setMimetype($content['headers']['content-type']); + } + + if (!empty($previewPictureUrl)) { + $this->updatePreviewPicture($entry, $previewPictureUrl); } try { @@ -295,9 +325,80 @@ class ContentProxy } /** - * Validate that the given content has at least a title, an html and a url. + * Update the origin_url field when a redirection occurs + * This field is set if it is empty and new url does not match ignore list. * - * @param array $content + * @param string $url + */ + private function updateOriginUrl(Entry $entry, $url) + { + if (empty($url) || $entry->getUrl() === $url) { + return false; + } + + $parsed_entry_url = parse_url($entry->getUrl()); + $parsed_content_url = parse_url($url); + + /** + * The following part computes the list of part changes between two + * parse_url arrays. + * + * As array_diff_assoc only computes changes to go from the left array + * to the right one, we make two differents arrays to have both + * directions. We merge these two arrays and sort keys before passing + * the result to the switch. + * + * The resulting array gives us all changing parts between the two + * urls: scheme, host, path, query and/or fragment. + */ + $diff_ec = array_diff_assoc($parsed_entry_url, $parsed_content_url); + $diff_ce = array_diff_assoc($parsed_content_url, $parsed_entry_url); + + $diff = array_merge($diff_ec, $diff_ce); + $diff_keys = array_keys($diff); + sort($diff_keys); + + if ($this->ignoreOriginProcessor->process($entry)) { + $entry->setUrl($url); + + return false; + } + + /** + * This switch case lets us apply different behaviors according to + * changing parts of urls. + * + * As $diff_keys is an array, we provide arrays as cases. ['path'] means + * 'only the path is different between the two urls' whereas + * ['fragment', 'query'] means 'only fragment and query string parts are + * different between the two urls'. + * + * Note that values in $diff_keys are sorted. + */ + switch ($diff_keys) { + case ['path']: + if (($parsed_entry_url['path'] . '/' === $parsed_content_url['path']) // diff is trailing slash, we only replace the url of the entry + || ($url === urldecode($entry->getUrl()))) { // we update entry url if new url is a decoded version of it, see EntryRepository#findByUrlAndUserId + $entry->setUrl($url); + } + break; + case ['scheme']: + $entry->setUrl($url); + break; + case ['fragment']: + // noop + break; + default: + if (empty($entry->getOriginUrl())) { + $entry->setOriginUrl($entry->getUrl()); + } + $entry->setUrl($url); + break; + } + } + + /** + * Validate that the given content has at least a title, an html and a url. * * @return bool true if valid otherwise false */