X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;ds=sidebyside;f=src%2FWallabag%2FCoreBundle%2FHelper%2FContentProxy.php;h=ddecd6f4839de3c5e53c9f2638646f1269a0f5d8;hb=f808b01692a835673f328d7221ba8c212caa9b61;hp=ddeffa77d090a6f23c3380f8156df34c87553d93;hpb=4dface66707688ea440a6a7569795a85631d1ff0;p=github%2Fwallabag%2Fwallabag.git diff --git a/src/Wallabag/CoreBundle/Helper/ContentProxy.php b/src/Wallabag/CoreBundle/Helper/ContentProxy.php index ddeffa77..ddecd6f4 100644 --- a/src/Wallabag/CoreBundle/Helper/ContentProxy.php +++ b/src/Wallabag/CoreBundle/Helper/ContentProxy.php @@ -3,11 +3,13 @@ namespace Wallabag\CoreBundle\Helper; use Graby\Graby; -use Psr\Log\LoggerInterface as Logger; +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser; +use Symfony\Component\Validator\Constraints\Locale as LocaleConstraint; +use Symfony\Component\Validator\Constraints\Url as UrlConstraint; +use Symfony\Component\Validator\Validator\ValidatorInterface; use Wallabag\CoreBundle\Entity\Entry; -use Wallabag\CoreBundle\Entity\Tag; use Wallabag\CoreBundle\Tools\Utils; -use Wallabag\CoreBundle\Repository\TagRepository; /** * This kind of proxy class take care of getting the content from an url @@ -17,131 +19,143 @@ class ContentProxy { protected $graby; protected $tagger; + protected $validator; protected $logger; - protected $tagRepository; + protected $mimeGuesser; + protected $fetchingErrorMessage; + protected $eventDispatcher; - public function __construct(Graby $graby, RuleBasedTagger $tagger, TagRepository $tagRepository, Logger $logger) + public function __construct(Graby $graby, RuleBasedTagger $tagger, ValidatorInterface $validator, LoggerInterface $logger, $fetchingErrorMessage) { $this->graby = $graby; $this->tagger = $tagger; + $this->validator = $validator; $this->logger = $logger; - $this->tagRepository = $tagRepository; + $this->mimeGuesser = new MimeTypeExtensionGuesser(); + $this->fetchingErrorMessage = $fetchingErrorMessage; } /** - * Fetch content using graby and hydrate given entry with results information. - * In case we couldn't find content, we'll try to use Open Graph data. + * Update entry using either fetched or provided content. * - * We can also force the content, in case of an import from the v1 for example, so the function won't - * fetch the content from the website but rather use information given with the $content parameter. - * - * @param Entry $entry Entry to update - * @param string $url Url to grab content for - * @param array $content An array with AT LEAST keys title, html, url, language & content_type to skip the fetchContent from the url - * - * @return Entry + * @param Entry $entry Entry to update + * @param string $url Url of the content + * @param array $content Array with content provided for import with AT LEAST keys title, html, url to skip the fetchContent from the url + * @param bool $disableContentUpdate Whether to skip trying to fetch content using Graby */ - public function updateEntry(Entry $entry, $url, array $content = []) + public function updateEntry(Entry $entry, $url, array $content = [], $disableContentUpdate = false) { - // do we have to fetch the content or the provided one is ok? - if (empty($content) || false === $this->validateContent($content)) { - $content = $this->graby->fetchContent($url); + 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); + + // 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 + if (empty($content) || $fetchedContent['html'] !== $this->fetchingErrorMessage) { + $content = $fetchedContent; + } } + // be sure to keep the url in case of error + // so we'll be able to refetch it in the future + $content['url'] = !empty($content['url']) ? $content['url'] : $url; + + $this->stockEntry($entry, $content); + } + + /** + * Stock entry with fetched or imported content. + * Will fall back to OpenGraph data if available. + * + * @param Entry $entry Entry to stock + * @param array $content Array with at least title, url & html + */ + private function stockEntry(Entry $entry, array $content) + { $title = $content['title']; - if (!$title && isset($content['open_graph']['og_title'])) { + if (!$title && !empty($content['open_graph']['og_title'])) { $title = $content['open_graph']['og_title']; } $html = $content['html']; if (false === $html) { - $html = '

Unable to retrieve readable content.

'; + $html = $this->fetchingErrorMessage; - if (isset($content['open_graph']['og_description'])) { + if (!empty($content['open_graph']['og_description'])) { $html .= '

But we found a short description:

'; $html .= $content['open_graph']['og_description']; } } - $entry->setUrl($content['url'] ?: $url); + $entry->setUrl($content['url']); $entry->setTitle($title); $entry->setContent($html); - $entry->setLanguage($content['language']); - $entry->setMimetype($content['content_type']); - $entry->setReadingTime(Utils::getReadingTime($html)); + $entry->setHttpStatus(isset($content['status']) ? $content['status'] : ''); - $domainName = parse_url($entry->getUrl(), PHP_URL_HOST); - if (false !== $domainName) { - $entry->setDomainName($domainName); - } + if (!empty($content['date'])) { + $date = $content['date']; - if (isset($content['open_graph']['og_image'])) { - $entry->setPreviewPicture($content['open_graph']['og_image']); - } + // is it a timestamp? + if (filter_var($date, FILTER_VALIDATE_INT) !== false) { + $date = '@' . $content['date']; + } - try { - $this->tagger->tag($entry); - } catch (\Exception $e) { - $this->logger->error('Error while trying to automatically tag an entry.', [ - 'entry_url' => $url, - 'error_msg' => $e->getMessage(), - ]); + try { + $entry->setPublishedAt(new \DateTime($date)); + } catch (\Exception $e) { + $this->logger->warning('Error while defining date', ['e' => $e, 'url' => $content['url'], 'date' => $content['date']]); + } } - return $entry; - } - - /** - * Assign some tags to an entry. - * - * @param Entry $entry - * @param array|string $tags An array of tag or a string coma separated of tag - * @param array $entitiesReady Entities from the EntityManager which are persisted but not yet flushed - * It is mostly to fix duplicate tag on import @see http://stackoverflow.com/a/7879164/569101 - */ - public function assignTagsToEntry(Entry $entry, $tags, array $entitiesReady = []) - { - if (!is_array($tags)) { - $tags = explode(',', $tags); + if (!empty($content['authors']) && is_array($content['authors'])) { + $entry->setPublishedBy($content['authors']); } - // keeps only Tag entity from the "not yet flushed entities" - $tagsNotYetFlushed = []; - foreach ($entitiesReady as $entity) { - if ($entity instanceof Tag) { - $tagsNotYetFlushed[$entity->getLabel()] = $entity; - } + if (!empty($content['all_headers'])) { + $entry->setHeaders($content['all_headers']); } - foreach ($tags as $label) { - $label = trim($label); + $this->validateAndSetLanguage( + $entry, + isset($content['language']) ? $content['language'] : null + ); - // avoid empty tag - if (0 === strlen($label)) { - continue; - } + $this->validateAndSetPreviewPicture( + $entry, + isset($content['open_graph']['og_image']) ? $content['open_graph']['og_image'] : null + ); + + // 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->validateAndSetPreviewPicture( + $entry, + $content['url'] + ); + } - if (isset($tagsNotYetFlushed[$label])) { - $tagEntity = $tagsNotYetFlushed[$label]; - } else { - $tagEntity = $this->tagRepository->findOneByLabel($label); + $entry->setMimetype(isset($content['content_type']) ? $content['content_type'] : ''); + $entry->setReadingTime(Utils::getReadingTime($html)); - if (is_null($tagEntity)) { - $tagEntity = new Tag(); - $tagEntity->setLabel($label); - } - } + $domainName = parse_url($entry->getUrl(), PHP_URL_HOST); + if (false !== $domainName) { + $entry->setDomainName($domainName); + } - // only add the tag on the entry if the relation doesn't exist - if (false === $entry->getTags()->contains($tagEntity)) { - $entry->addTag($tagEntity); - } + try { + $this->tagger->tag($entry); + } catch (\Exception $e) { + $this->logger->error('Error while trying to automatically tag an entry.', [ + 'entry_url' => $content['url'], + 'error_msg' => $e->getMessage(), + ]); } } /** - * Validate that the given content as enough value to be used - * instead of fetch the content from the url. + * Validate that the given content has at least a title, an html and a url. * * @param array $content * @@ -149,174 +163,54 @@ class ContentProxy */ private function validateContent(array $content) { - return isset($content['title']) && isset($content['html']) && isset($content['url']) && isset($content['language']) && isset($content['content_type']); - } - - /** - * Changing pictures URL in article content. - */ - public static function filterPicture($content, $url, $id) - { - $matches = array(); - $processing_pictures = array(); // list of processing image to avoid processing the same pictures twice - preg_match_all('#<\s*(img)[^>]+src="([^"]*)"[^>]*>#Si', $content, $matches, PREG_SET_ORDER); - foreach ($matches as $i => $link) { - $link[1] = trim($link[1]); - if (!preg_match('#^(([a-z]+://)|(\#))#', $link[1])) { - $absolute_path = self::_getAbsoluteLink($link[2], $url); - $filename = basename(parse_url($absolute_path, PHP_URL_PATH)); - $directory = self::_createAssetsDirectory($id); - $fullpath = $directory.'/'.$filename; - - if (in_array($absolute_path, $processing_pictures) === true) { - // replace picture's URL only if processing is OK : already processing -> go to next picture - continue; - } - - if (self::_downloadPictures($absolute_path, $fullpath) === true) { - $content = str_replace($matches[$i][2], Tools::getPocheUrl().$fullpath, $content); - } - - $processing_pictures[] = $absolute_path; - } - } - - return $content; - } - - /** - * Get absolute URL. - */ - private static function _getAbsoluteLink($relativeLink, $url) - { - /* return if already absolute URL */ - if (parse_url($relativeLink, PHP_URL_SCHEME) != '') { - return $relativeLink; - } - - /* queries and anchors */ - if ($relativeLink[0] == '#' || $relativeLink[0] == '?') { - return $url.$relativeLink; - } - - /* parse base URL and convert to local variables: - $scheme, $host, $path */ - extract(parse_url($url)); - - /* remove non-directory element from path */ - $path = preg_replace('#/[^/]*$#', '', $path); - - /* destroy path if relative url points to root */ - if ($relativeLink[0] == '/') { - $path = ''; - } - - /* dirty absolute URL */ - $abs = $host.$path.'/'.$relativeLink; - - /* replace '//' or '/./' or '/foo/../' with '/' */ - $re = array('#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#'); - for ($n = 1; $n > 0; $abs = preg_replace($re, '/', $abs, -1, $n)) { - } - - /* absolute URL is ready! */ - return $scheme.'://'.$abs; + return !empty($content['title']) && !empty($content['html']) && !empty($content['url']); } /** - * Downloading pictures. + * Use a Symfony validator to ensure the language is well formatted. * - * @return bool true if the download and processing is OK, false else + * @param Entry $entry + * @param string $value Language to validate */ - private static function _downloadPictures($absolute_path, $fullpath) + private function validateAndSetLanguage($entry, $value) { - $rawdata = Tools::getFile($absolute_path); - $fullpath = urldecode($fullpath); - - if (file_exists($fullpath)) { - unlink($fullpath); - } - - // check extension - $file_ext = strrchr($fullpath, '.'); - $whitelist = array('.jpg', '.jpeg', '.gif', '.png'); - if (!(in_array($file_ext, $whitelist))) { - Tools::logm('processed image with not allowed extension. Skipping '.$fullpath); - - return false; - } - - // check headers - $imageinfo = getimagesize($absolute_path); - if ($imageinfo['mime'] != 'image/gif' && $imageinfo['mime'] != 'image/jpeg' && $imageinfo['mime'] != 'image/jpg' && $imageinfo['mime'] != 'image/png') { - Tools::logm('processed image with bad header. Skipping '.$fullpath); - - return false; - } + // some lang are defined as fr-FR, es-ES. + // replacing - by _ might increase language support + $value = str_replace('-', '_', $value); - // regenerate image - $im = imagecreatefromstring($rawdata); - if ($im === false) { - Tools::logm('error while regenerating image '.$fullpath); + $errors = $this->validator->validate( + $value, + (new LocaleConstraint()) + ); - return false; - } + if (0 === count($errors)) { + $entry->setLanguage($value); - switch ($imageinfo['mime']) { - case 'image/gif': - $result = imagegif($im, $fullpath); - break; - case 'image/jpeg': - case 'image/jpg': - $result = imagejpeg($im, $fullpath, REGENERATE_PICTURES_QUALITY); - break; - case 'image/png': - $result = imagepng($im, $fullpath, ceil(REGENERATE_PICTURES_QUALITY / 100 * 9)); - break; + return; } - imagedestroy($im); - return $result; + $this->logger->warning('Language validation failed. ' . (string) $errors); } /** - * Create a directory for an article. - * - * @param $id ID of the article + * Use a Symfony validator to ensure the preview picture is a real url. * - * @return string + * @param Entry $entry + * @param string $value URL to validate */ - private static function _createAssetsDirectory($id) + private function validateAndSetPreviewPicture($entry, $value) { - $assets_path = ABS_PATH; - if (!is_dir($assets_path)) { - mkdir($assets_path, 0715); - } - - $article_directory = $assets_path.$id; - if (!is_dir($article_directory)) { - mkdir($article_directory, 0715); - } + $errors = $this->validator->validate( + $value, + (new UrlConstraint()) + ); - return $article_directory; - } - - /** - * Remove the directory. - * - * @param $directory - * - * @return bool - */ - public static function removeDirectory($directory) - { - if (is_dir($directory)) { - $files = array_diff(scandir($directory), array('.', '..')); - foreach ($files as $file) { - (is_dir("$directory/$file")) ? self::removeDirectory("$directory/$file") : unlink("$directory/$file"); - } + if (0 === count($errors)) { + $entry->setPreviewPicture($value); - return rmdir($directory); + return; } + + $this->logger->warning('PreviewPicture validation failed. ' . (string) $errors); } }