This give us ability to use Entry ID to determine where to store images and it’s then more easy to remove them when we remove the entry.
use Wallabag\CoreBundle\Form\Type\EditEntryType;
use Wallabag\CoreBundle\Form\Type\NewEntryType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
+use Wallabag\CoreBundle\Event\EntrySavedEvent;
+use Wallabag\CoreBundle\Event\EntryDeletedEvent;
class EntryController extends Controller
{
$em->persist($entry);
$em->flush();
+ // entry saved, dispatch event about it!
+ $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
+
return $this->redirect($this->generateUrl('homepage'));
}
$em = $this->getDoctrine()->getManager();
$em->persist($entry);
$em->flush();
+
+ // entry saved, dispatch event about it!
+ $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
}
return $this->redirect($this->generateUrl('homepage'));
$em->persist($entry);
$em->flush();
+ // entry saved, dispatch event about it!
+ $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
+
return $this->redirect($this->generateUrl('view', ['id' => $entry->getId()]));
}
UrlGeneratorInterface::ABSOLUTE_PATH
);
+ // entry deleted, dispatch event about it!
+ $this->get('event_dispatcher')->dispatch(EntryDeletedEvent::NAME, new EntryDeletedEvent($entry));
+
$em = $this->getDoctrine()->getManager();
$em->remove($entry);
$em->flush();
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event;
+
+use Symfony\Component\EventDispatcher\Event;
+use Wallabag\CoreBundle\Entity\Entry;
+
+/**
+ * This event is fired as soon as an entry is deleted.
+ */
+class EntryDeletedEvent extends Event
+{
+ const NAME = 'entry.deleted';
+
+ protected $entry;
+
+ public function __construct(Entry $entry)
+ {
+ $this->entry = $entry;
+ }
+
+ public function getEntry()
+ {
+ return $this->entry;
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event;
+
+use Symfony\Component\EventDispatcher\Event;
+use Wallabag\CoreBundle\Entity\Entry;
+
+/**
+ * This event is fired as soon as an entry was saved.
+ */
+class EntrySavedEvent extends Event
+{
+ const NAME = 'entry.saved';
+
+ protected $entry;
+
+ public function __construct(Entry $entry)
+ {
+ $this->entry = $entry;
+ }
+
+ public function getEntry()
+ {
+ return $this->entry;
+ }
+}
namespace Wallabag\CoreBundle\Event\Subscriber;
-use Doctrine\Common\EventSubscriber;
-use Doctrine\ORM\Event\LifecycleEventArgs;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Psr\Log\LoggerInterface;
use Wallabag\CoreBundle\Helper\DownloadImages;
use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\CoreBundle\Event\EntrySavedEvent;
+use Wallabag\CoreBundle\Event\EntryDeletedEvent;
use Doctrine\ORM\EntityManager;
-use Craue\ConfigBundle\Util\Config;
-class DownloadImagesSubscriber implements EventSubscriber
+class DownloadImagesSubscriber implements EventSubscriberInterface
{
- private $configClass;
+ private $em;
private $downloadImages;
+ private $enabled;
private $logger;
- /**
- * We inject the class instead of the service otherwise it generates a circular reference with the EntityManager.
- * So we build the service ourself when we got the EntityManager (in downloadImages).
- */
- public function __construct(DownloadImages $downloadImages, $configClass, LoggerInterface $logger)
+ public function __construct(EntityManager $em, DownloadImages $downloadImages, $enabled, LoggerInterface $logger)
{
+ $this->em = $em;
$this->downloadImages = $downloadImages;
- $this->configClass = $configClass;
+ $this->enabled = $enabled;
$this->logger = $logger;
}
- public function getSubscribedEvents()
+ public static function getSubscribedEvents()
{
- return array(
- 'prePersist',
- 'preUpdate',
- );
+ return [
+ EntrySavedEvent::NAME => 'onEntrySaved',
+ EntryDeletedEvent::NAME => 'onEntryDeleted',
+ ];
}
/**
- * In case of an entry has been updated.
- * We won't update the content field if it wasn't updated.
+ * Download images and updated the data into the entry.
*
- * @param LifecycleEventArgs $args
+ * @param EntrySavedEvent $event
*/
- public function preUpdate(LifecycleEventArgs $args)
+ public function onEntrySaved(EntrySavedEvent $event)
{
- $entity = $args->getEntity();
+ if (!$this->enabled) {
+ $this->logger->debug('DownloadImagesSubscriber: disabled.');
- if (!$entity instanceof Entry) {
return;
}
- $config = new $this->configClass();
- $config->setEntityManager($args->getEntityManager());
-
- if (!$config->get('download_images_enabled')) {
- return;
- }
+ $entry = $event->getEntry();
- // field content has been updated
- if ($args->hasChangedField('content')) {
- $html = $this->downloadImages($config, $entity);
+ $html = $this->downloadImages($entry);
+ if (false !== $html) {
+ $this->logger->debug('DownloadImagesSubscriber: updated html.');
- if (false !== $html) {
- $args->setNewValue('content', $html);
- }
+ $entry->setContent($html);
}
- // field preview picture has been updated
- if ($args->hasChangedField('previewPicture')) {
- $previewPicture = $this->downloadPreviewImage($config, $entity);
+ // update preview picture
+ $previewPicture = $this->downloadPreviewImage($entry);
+ if (false !== $previewPicture) {
+ $this->logger->debug('DownloadImagesSubscriber: update preview picture.');
- if (false !== $previewPicture) {
- $entity->setPreviewPicture($previewPicture);
- }
+ $entry->setPreviewPicture($previewPicture);
}
+
+ $this->em->persist($entry);
+ $this->em->flush();
}
/**
- * When a new entry is saved.
+ * Remove images related to the entry.
*
- * @param LifecycleEventArgs $args
+ * @param EntryDeletedEvent $event
*/
- public function prePersist(LifecycleEventArgs $args)
+ public function onEntryDeleted(EntryDeletedEvent $event)
{
- $entity = $args->getEntity();
-
- if (!$entity instanceof Entry) {
- return;
- }
-
- $config = new $this->configClass();
- $config->setEntityManager($args->getEntityManager());
+ if (!$this->enabled) {
+ $this->logger->debug('DownloadImagesSubscriber: disabled.');
- if (!$config->get('download_images_enabled')) {
return;
}
- // update all images inside the html
- $html = $this->downloadImages($config, $entity);
- if (false !== $html) {
- $entity->setContent($html);
- }
-
- // update preview picture
- $previewPicture = $this->downloadPreviewImage($config, $entity);
- if (false !== $previewPicture) {
- $entity->setPreviewPicture($previewPicture);
- }
+ $this->downloadImages->removeImages($event->getEntry()->getId());
}
/**
*
* @todo If we want to add async download, it should be done in that method
*
- * @param Config $config
- * @param Entry $entry
+ * @param Entry $entry
*
* @return string|false False in case of async
*/
- public function downloadImages(Config $config, Entry $entry)
+ private function downloadImages(Entry $entry)
{
- $this->downloadImages->setWallabagUrl($config->get('wallabag_url'));
-
return $this->downloadImages->processHtml(
+ $entry->getId(),
$entry->getContent(),
$entry->getUrl()
);
*
* @todo If we want to add async download, it should be done in that method
*
- * @param Config $config
- * @param Entry $entry
+ * @param Entry $entry
*
* @return string|false False in case of async
*/
- public function downloadPreviewImage(Config $config, Entry $entry)
+ private function downloadPreviewImage(Entry $entry)
{
- $this->downloadImages->setWallabagUrl($config->get('wallabag_url'));
-
return $this->downloadImages->processSingleImage(
+ $entry->getId(),
$entry->getPreviewPicture(),
$entry->getUrl()
);
use Symfony\Component\DomCrawler\Crawler;
use GuzzleHttp\Client;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser;
+use Symfony\Component\Finder\Finder;
class DownloadImages
{
private $mimeGuesser;
private $wallabagUrl;
- public function __construct(Client $client, $baseFolder, LoggerInterface $logger)
+ public function __construct(Client $client, $baseFolder, $wallabagUrl, LoggerInterface $logger)
{
$this->client = $client;
$this->baseFolder = $baseFolder;
+ $this->wallabagUrl = rtrim($wallabagUrl, '/');
$this->logger = $logger;
$this->mimeGuesser = new MimeTypeExtensionGuesser();
$this->setFolder();
}
- /**
- * Since we can't inject CraueConfig service because it'll generate a circular reference when injected in the subscriber
- * we use a different way to inject the current wallabag url.
- *
- * @param string $url Usually from `$config->get('wallabag_url')`
- */
- public function setWallabagUrl($url)
- {
- $this->wallabagUrl = rtrim($url, '/');
- }
-
/**
* Setup base folder where all images are going to be saved.
*/
/**
* Process the html and extract image from it, save them to local and return the updated html.
*
+ * @param int $entryId ID of the entry
* @param string $html
- * @param string $url Used as a base path for relative image and folder
+ * @param string $url Used as a base path for relative image and folder
*
* @return string
*/
- public function processHtml($html, $url)
+ public function processHtml($entryId, $html, $url)
{
$crawler = new Crawler($html);
$result = $crawler
->filterXpath('//img')
->extract(array('src'));
- $relativePath = $this->getRelativePath($url);
+ $relativePath = $this->getRelativePath($entryId);
// download and save the image to the folder
foreach ($result as $image) {
- $imagePath = $this->processSingleImage($image, $url, $relativePath);
+ $imagePath = $this->processSingleImage($entryId, $image, $url, $relativePath);
if (false === $imagePath) {
continue;
* - re-saved it (for security reason)
* - return the new local path.
*
+ * @param int $entryId ID of the entry
* @param string $imagePath Path to the image to retrieve
* @param string $url Url from where the image were found
* @param string $relativePath Relative local path to saved the image
*
* @return string Relative url to access the image from the web
*/
- public function processSingleImage($imagePath, $url, $relativePath = null)
+ public function processSingleImage($entryId, $imagePath, $url, $relativePath = null)
{
- if (null == $relativePath) {
- $relativePath = $this->getRelativePath($url);
+ if (null === $relativePath) {
+ $relativePath = $this->getRelativePath($entryId);
}
+ $this->logger->debug('DownloadImages: working on image: '.$imagePath);
+
$folderPath = $this->baseFolder.'/'.$relativePath;
// build image path
$absolutePath = $this->getAbsoluteLink($url, $imagePath);
if (false === $absolutePath) {
- $this->logger->log('error', 'Can not determine the absolute path for that image, skipping.');
+ $this->logger->error('DownloadImages: Can not determine the absolute path for that image, skipping.');
return false;
}
try {
$res = $this->client->get($absolutePath);
} catch (\Exception $e) {
- $this->logger->log('error', 'Can not retrieve image, skipping.', ['exception' => $e]);
+ $this->logger->error('DownloadImages: Can not retrieve image, skipping.', ['exception' => $e]);
return false;
}
$ext = $this->mimeGuesser->guess($res->getHeader('content-type'));
- $this->logger->log('debug', 'Checking extension', ['ext' => $ext, 'header' => $res->getHeader('content-type')]);
+ $this->logger->debug('DownloadImages: Checking extension', ['ext' => $ext, 'header' => $res->getHeader('content-type')]);
if (!in_array($ext, ['jpeg', 'jpg', 'gif', 'png'], true)) {
- $this->logger->log('error', 'Processed image with not allowed extension. Skipping '.$imagePath);
+ $this->logger->error('DownloadImages: Processed image with not allowed extension. Skipping '.$imagePath);
return false;
}
}
if (false === $im) {
- $this->logger->log('error', 'Error while regenerating image', ['path' => $localPath]);
+ $this->logger->error('DownloadImages: Error while regenerating image', ['path' => $localPath]);
return false;
}
switch ($ext) {
case 'gif':
$result = imagegif($im, $localPath);
- $this->logger->log('debug', 'Re-creating gif');
+ $this->logger->debug('DownloadImages: Re-creating gif');
break;
case 'jpeg':
case 'jpg':
$result = imagejpeg($im, $localPath, self::REGENERATE_PICTURES_QUALITY);
- $this->logger->log('debug', 'Re-creating jpg');
+ $this->logger->debug('DownloadImages: Re-creating jpg');
break;
case 'png':
$result = imagepng($im, $localPath, ceil(self::REGENERATE_PICTURES_QUALITY / 100 * 9));
- $this->logger->log('debug', 'Re-creating png');
+ $this->logger->debug('DownloadImages: Re-creating png');
}
imagedestroy($im);
return $this->wallabagUrl.'/assets/images/'.$relativePath.'/'.$hashImage.'.'.$ext;
}
+ /**
+ * Remove all images for the given entry id.
+ *
+ * @param int $entryId ID of the entry
+ */
+ public function removeImages($entryId)
+ {
+ $relativePath = $this->getRelativePath($entryId);
+ $folderPath = $this->baseFolder.'/'.$relativePath;
+
+ $finder = new Finder();
+ $finder
+ ->files()
+ ->ignoreDotFiles(true)
+ ->in($folderPath);
+
+ foreach ($finder as $file) {
+ @unlink($file->getRealPath());
+ }
+
+ @rmdir($folderPath);
+ }
+
/**
* Generate the folder where we are going to save images based on the entry url.
*
- * @param string $url
+ * @param int $entryId ID of the entry
*
* @return string
*/
- private function getRelativePath($url)
+ private function getRelativePath($entryId)
{
- $hashUrl = hash('crc32', $url);
- $relativePath = $hashUrl[0].'/'.$hashUrl[1].'/'.$hashUrl;
+ $hashId = hash('crc32', $entryId);
+ $relativePath = $hashId[0].'/'.$hashId[1].'/'.$hashId;
$folderPath = $this->baseFolder.'/'.$relativePath;
if (!file_exists($folderPath)) {
mkdir($folderPath, 0777, true);
}
- $this->logger->log('debug', 'Folder used for that url', ['folder' => $folderPath, 'url' => $url]);
+ $this->logger->debug('DownloadImages: Folder used for that Entry id', ['folder' => $folderPath, 'entryId' => $entryId]);
return $relativePath;
}
return $absolute->get_uri();
}
- $this->logger->log('error', 'Can not make an absolute link', ['base' => $base, 'url' => $url]);
+ $this->logger->error('DownloadImages: Can not make an absolute link', ['base' => $base, 'url' => $url]);
return false;
}
wallabag_core.subscriber.download_images:
class: Wallabag\CoreBundle\Event\Subscriber\DownloadImagesSubscriber
arguments:
+ - "@doctrine.orm.default_entity_manager"
- "@wallabag_core.entry.download_images"
- - "%craue_config.config.class%"
+ - '@=service(''craue_config'').get(''download_images_enabled'')'
- "@logger"
tags:
- - { name: doctrine.event_subscriber }
+ - { name: kernel.event_subscriber }
wallabag_core.entry.download_images:
class: Wallabag\CoreBundle\Helper\DownloadImages
arguments:
- "@wallabag_core.entry.download_images.client"
- "%kernel.root_dir%/../web/assets/images"
+ - '@=service(''craue_config'').get(''wallabag_url'')'
- "@logger"
wallabag_core.entry.download_images.client:
$this->assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $entry);
$this->assertEquals($url, $entry->getUrl());
$this->assertContains('Perpignan', $entry->getTitle());
- $this->assertContains('assets/images/8/e/8ec9229a/d9bc0fcd.jpeg', $entry->getContent());
+ $this->assertContains('/d9bc0fcd.jpeg', $entry->getContent());
- $em->remove($entry);
- $em->flush();
+ $client->getContainer()->get('craue_config')->set('download_images_enabled', 0);
+ }
+
+ /**
+ * @depends testNewEntryWithDownloadImagesEnabled
+ */
+ public function testRemoveEntryWithDownloadImagesEnabled()
+ {
+ $this->logInAs('admin');
+ $client = $this->getClient();
+
+ $url = 'http://www.20minutes.fr/montpellier/1952003-20161030-video-car-tombe-panne-rugbymen-perpignan-improvisent-melee-route';
+ $client->getContainer()->get('craue_config')->set('download_images_enabled', 1);
+
+ $content = $client->getContainer()
+ ->get('doctrine.orm.entity_manager')
+ ->getRepository('WallabagCoreBundle:Entry')
+ ->findByUrlAndUserId($url, $this->getLoggedInUserId());
+
+ $client->request('GET', '/delete/'.$content->getId());
+
+ $this->assertEquals(302, $client->getResponse()->getStatusCode());
$client->getContainer()->get('craue_config')->set('download_images_enabled', 0);
}
$logHandler = new TestHandler();
$logger = new Logger('test', array($logHandler));
- $download = new DownloadImages($client, sys_get_temp_dir().'/wallabag_test', $logger);
- $download->setWallabagUrl('http://wallabag.io/');
+ $download = new DownloadImages($client, sys_get_temp_dir().'/wallabag_test', 'http://wallabag.io/', $logger);
- $res = $download->processHtml('<div><img src="http://i.imgur.com/T9qgcHc.jpg" /></div>', 'http://imgur.com/gallery/WxtWY');
+ $res = $download->processHtml(123, '<div><img src="http://i.imgur.com/T9qgcHc.jpg" /></div>', 'http://imgur.com/gallery/WxtWY');
- $this->assertContains('http://wallabag.io/assets/images/4/2/4258f71e/c638b4c2.png', $res);
+ $this->assertContains('http://wallabag.io/assets/images/9/b/9b0ead26/c638b4c2.png', $res);
}
public function testProcessHtmlWithBadImage()
$logHandler = new TestHandler();
$logger = new Logger('test', array($logHandler));
- $download = new DownloadImages($client, sys_get_temp_dir().'/wallabag_test', $logger);
- $res = $download->processHtml('<div><img src="http://i.imgur.com/T9qgcHc.jpg" /></div>', 'http://imgur.com/gallery/WxtWY');
+ $download = new DownloadImages($client, sys_get_temp_dir().'/wallabag_test', 'http://wallabag.io/', $logger);
+ $res = $download->processHtml(123, '<div><img src="http://i.imgur.com/T9qgcHc.jpg" /></div>', 'http://imgur.com/gallery/WxtWY');
$this->assertContains('http://i.imgur.com/T9qgcHc.jpg', $res, 'Image were not replace because of content-type');
}
$logHandler = new TestHandler();
$logger = new Logger('test', array($logHandler));
- $download = new DownloadImages($client, sys_get_temp_dir().'/wallabag_test', $logger);
- $res = $download->processSingleImage('T9qgcHc.jpg', 'http://imgur.com/gallery/WxtWY');
+ $download = new DownloadImages($client, sys_get_temp_dir().'/wallabag_test', 'http://wallabag.io/', $logger);
+ $res = $download->processSingleImage(123, 'T9qgcHc.jpg', 'http://imgur.com/gallery/WxtWY');
- $this->assertContains('/assets/images/4/2/4258f71e/ebe60399.'.$extension, $res);
+ $this->assertContains('/assets/images/9/b/9b0ead26/ebe60399.'.$extension, $res);
}
public function testProcessSingleImageWithBadUrl()
$logHandler = new TestHandler();
$logger = new Logger('test', array($logHandler));
- $download = new DownloadImages($client, sys_get_temp_dir().'/wallabag_test', $logger);
- $res = $download->processSingleImage('T9qgcHc.jpg', 'http://imgur.com/gallery/WxtWY');
+ $download = new DownloadImages($client, sys_get_temp_dir().'/wallabag_test', 'http://wallabag.io/', $logger);
+ $res = $download->processSingleImage(123, 'T9qgcHc.jpg', 'http://imgur.com/gallery/WxtWY');
$this->assertFalse($res, 'Image can not be found, so it will not be replaced');
}
$logHandler = new TestHandler();
$logger = new Logger('test', array($logHandler));
- $download = new DownloadImages($client, sys_get_temp_dir().'/wallabag_test', $logger);
- $res = $download->processSingleImage('http://i.imgur.com/T9qgcHc.jpg', 'http://imgur.com/gallery/WxtWY');
+ $download = new DownloadImages($client, sys_get_temp_dir().'/wallabag_test', 'http://wallabag.io/', $logger);
+ $res = $download->processSingleImage(123, 'http://i.imgur.com/T9qgcHc.jpg', 'http://imgur.com/gallery/WxtWY');
$this->assertFalse($res, 'Image can not be loaded, so it will not be replaced');
}
$logHandler = new TestHandler();
$logger = new Logger('test', array($logHandler));
- $download = new DownloadImages($client, sys_get_temp_dir().'/wallabag_test', $logger);
- $res = $download->processSingleImage('/i.imgur.com/T9qgcHc.jpg', 'imgur.com/gallery/WxtWY');
+ $download = new DownloadImages($client, sys_get_temp_dir().'/wallabag_test', 'http://wallabag.io/', $logger);
+ $res = $download->processSingleImage(123, '/i.imgur.com/T9qgcHc.jpg', 'imgur.com/gallery/WxtWY');
$this->assertFalse($res, 'Absolute image can not be determined, so it will not be replaced');
}