The PATCH method for the entry should only update what user sent to us and not the whole entry as it was before.
Also, sending tags when patching an entry will now remove all current tags & assocatied new ones.
* {"name"="url", "dataType"="string", "required"=true, "format"="http://www.test.com/article.html", "description"="Url for the entry."},
* {"name"="title", "dataType"="string", "required"=false, "description"="Optional, we'll get the title from the page."},
* {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."},
- * {"name"="starred", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="entry already starred"},
* {"name"="archive", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="entry already archived"},
+ * {"name"="starred", "dataType"="integer", "required"=false, "format"="1 or 0", "description"="entry already starred"},
* {"name"="content", "dataType"="string", "required"=false, "description"="Content of the entry"},
* {"name"="language", "dataType"="string", "required"=false, "description"="Language of the entry"},
* {"name"="preview_picture", "dataType"="string", "required"=false, "description"="Preview picture of the entry"},
$entry->setUrl($url);
}
- $this->upsertEntry($entry, $request);
+ $data = $this->retrieveValueFromRequest($request);
+
+ try {
+ $this->get('wallabag_core.content_proxy')->updateEntry(
+ $entry,
+ $entry->getUrl(),
+ [
+ 'title' => !empty($data['title']) ? $data['title'] : $entry->getTitle(),
+ 'html' => !empty($data['content']) ? $data['content'] : $entry->getContent(),
+ 'url' => $entry->getUrl(),
+ 'language' => !empty($data['language']) ? $data['language'] : $entry->getLanguage(),
+ 'date' => !empty($data['publishedAt']) ? $data['publishedAt'] : $entry->getPublishedAt(),
+ // faking the open graph preview picture
+ 'open_graph' => [
+ 'og_image' => !empty($data['picture']) ? $data['picture'] : $entry->getPreviewPicture(),
+ ],
+ 'authors' => is_string($data['authors']) ? explode(',', $data['authors']) : $entry->getPublishedBy(),
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->get('logger')->error('Error while saving an entry', [
+ 'exception' => $e,
+ 'entry' => $entry,
+ ]);
+ }
+
+ if (!is_null($data['isArchived'])) {
+ $entry->setArchived((bool) $data['isArchived']);
+ }
+
+ if (!is_null($data['isStarred'])) {
+ $entry->setStarred((bool) $data['isStarred']);
+ }
+
+ if (!empty($data['tags'])) {
+ $this->get('wallabag_core.tags_assigner')->assignTagsToEntry($entry, $data['tags']);
+ }
+
+ if (!is_null($data['isPublic'])) {
+ if (true === (bool) $data['isPublic'] && null === $entry->getUid()) {
+ $entry->generateUid();
+ } elseif (false === (bool) $data['isPublic']) {
+ $entry->cleanUid();
+ }
+ }
+
+ $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->sendResponse($entry);
}
$this->validateAuthentication();
$this->validateUserAccess($entry->getUser()->getId());
- $this->upsertEntry($entry, $request, true);
+ $contentProxy = $this->get('wallabag_core.content_proxy');
+
+ $data = $this->retrieveValueFromRequest($request);
+
+ // this is a special case where user want to manually update the entry content
+ // the ContentProxy will only cleanup the html
+ // and also we force to not re-fetch the content in case of error
+ if (!empty($data['content'])) {
+ try {
+ $contentProxy->updateEntry(
+ $entry,
+ $entry->getUrl(),
+ [
+ 'html' => $data['content'],
+ ],
+ true
+ );
+ } catch (\Exception $e) {
+ $this->get('logger')->error('Error while saving an entry', [
+ 'exception' => $e,
+ 'entry' => $entry,
+ ]);
+ }
+ }
+
+ if (!empty($data['title'])) {
+ $entry->setTitle($data['title']);
+ }
+
+ if (!empty($data['language'])) {
+ $contentProxy->updateLanguage($entry, $data['language']);
+ }
+
+ if (!empty($data['authors']) && is_string($data['authors'])) {
+ $entry->setPublishedBy(explode(',', $data['authors']));
+ }
+
+ if (!empty($data['picture'])) {
+ $contentProxy->updatePreviewPicture($entry, $data['picture']);
+ }
+
+ if (!empty($data['publishedAt'])) {
+ $contentProxy->updatePublishedAt($entry, $data['publishedAt']);
+ }
+
+ if (!is_null($data['isArchived'])) {
+ $entry->setArchived((bool) $data['isArchived']);
+ }
+
+ if (!is_null($data['isStarred'])) {
+ $entry->setStarred((bool) $data['isStarred']);
+ }
+
+ if (!empty($data['tags'])) {
+ $entry->removeAllTags();
+ $this->get('wallabag_core.tags_assigner')->assignTagsToEntry($entry, $data['tags']);
+ }
+
+ if (!is_null($data['isPublic'])) {
+ if (true === (bool) $data['isPublic'] && null === $entry->getUid()) {
+ $entry->generateUid();
+ } elseif (false === (bool) $data['isPublic']) {
+ $entry->cleanUid();
+ }
+ }
+
+ $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->sendResponse($entry);
}
}
/**
- * Update or Insert a new entry.
+ * Retrieve value from the request.
+ * Used for POST & PATCH on a an entry.
*
- * @param Entry $entry
* @param Request $request
- * @param bool $disableContentUpdate If we don't want the content to be update by fetching the url (used when patching instead of posting)
+ *
+ * @return array
*/
- private function upsertEntry(Entry $entry, Request $request, $disableContentUpdate = false)
+ private function retrieveValueFromRequest(Request $request)
{
- $title = $request->request->get('title');
- $tags = $request->request->get('tags', []);
- $isArchived = $request->request->get('archive');
- $isStarred = $request->request->get('starred');
- $isPublic = $request->request->get('public');
- $content = $request->request->get('content');
- $language = $request->request->get('language');
- $picture = $request->request->get('preview_picture');
- $publishedAt = $request->request->get('published_at');
- $authors = $request->request->get('authors', '');
-
- try {
- $this->get('wallabag_core.content_proxy')->updateEntry(
- $entry,
- $entry->getUrl(),
- [
- 'title' => !empty($title) ? $title : $entry->getTitle(),
- 'html' => !empty($content) ? $content : $entry->getContent(),
- 'url' => $entry->getUrl(),
- 'language' => !empty($language) ? $language : $entry->getLanguage(),
- 'date' => !empty($publishedAt) ? $publishedAt : $entry->getPublishedAt(),
- // faking the open graph preview picture
- 'open_graph' => [
- 'og_image' => !empty($picture) ? $picture : $entry->getPreviewPicture(),
- ],
- 'authors' => is_string($authors) ? explode(',', $authors) : $entry->getPublishedBy(),
- ],
- $disableContentUpdate
- );
- } catch (\Exception $e) {
- $this->get('logger')->error('Error while saving an entry', [
- 'exception' => $e,
- 'entry' => $entry,
- ]);
- }
-
- if (null !== $isArchived) {
- $entry->setArchived((bool) $isArchived);
- }
-
- if (null !== $isStarred) {
- $entry->setStarred((bool) $isStarred);
- }
-
- if (!empty($tags)) {
- $this->get('wallabag_core.tags_assigner')->assignTagsToEntry($entry, $tags);
- }
-
- if (null !== $isPublic) {
- if (true === (bool) $isPublic && null === $entry->getUid()) {
- $entry->generateUid();
- } elseif (false === (bool) $isPublic) {
- $entry->cleanUid();
- }
- }
-
- $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 [
+ 'title' => $request->request->get('title'),
+ 'tags' => $request->request->get('tags', []),
+ 'isArchived' => $request->request->get('archive'),
+ 'isStarred' => $request->request->get('starred'),
+ 'isPublic' => $request->request->get('public'),
+ 'content' => $request->request->get('content'),
+ 'language' => $request->request->get('language'),
+ 'picture' => $request->request->get('preview_picture'),
+ 'publishedAt' => $request->request->get('published_at'),
+ 'authors' => $request->request->get('authors', ''),
+ ];
}
/**
$tag->addEntry($this);
}
+ /**
+ * Remove the given tag from the entry (if the tag is associated).
+ *
+ * @param Tag $tag
+ */
public function removeTag(Tag $tag)
{
if (!$this->tags->contains($tag)) {
$tag->removeEntry($this);
}
+ /**
+ * Remove all assigned tags from the entry.
+ */
+ public function removeAllTags()
+ {
+ foreach ($this->tags as $tag) {
+ $this->tags->removeElement($tag);
+ $tag->removeEntry($this);
+ }
+ }
+
/**
* Set previewPicture.
*
*/
private function stockEntry(Entry $entry, array $content)
{
- $title = $content['title'];
- if (!$title && !empty($content['open_graph']['og_title'])) {
- $title = $content['open_graph']['og_title'];
+ $entry->setUrl($content['url']);
+
+ $domainName = parse_url($entry->getUrl(), PHP_URL_HOST);
+ if (false !== $domainName) {
+ $entry->setDomainName($domainName);
+ }
+
+ 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'];
}
}
- $entry->setUrl($content['url']);
- $entry->setTitle($title);
$entry->setContent($html);
- $entry->setHttpStatus(isset($content['status']) ? $content['status'] : '');
-
- if (!empty($content['date'])) {
- $date = $content['date'];
-
- // is it a timestamp?
- if (filter_var($date, FILTER_VALIDATE_INT) !== false) {
- $date = '@' . $content['date'];
- }
+ $entry->setReadingTime(Utils::getReadingTime($html));
- try {
- $entry->setPublishedAt(new \DateTime($date));
- } catch (\Exception $e) {
- $this->logger->warning('Error while defining date', ['e' => $e, 'url' => $content['url'], 'date' => $content['date']]);
- }
+ if (!empty($content['status'])) {
+ $entry->setHttpStatus($content['status']);
}
if (!empty($content['authors']) && is_array($content['authors'])) {
$entry->setHeaders($content['all_headers']);
}
- $this->validateAndSetLanguage(
- $entry,
- isset($content['language']) ? $content['language'] : null
- );
+ if (!empty($content['date'])) {
+ $this->updatePublishedAt($entry, $content['date']);
+ }
- $this->validateAndSetPreviewPicture(
- $entry,
- isset($content['open_graph']['og_image']) ? $content['open_graph']['og_image'] : null
- );
+ if (!empty($content['language'])) {
+ $this->updateLanguage($entry, $content['language']);
+ }
+
+ if (!empty($content['open_graph']['og_image'])) {
+ $this->updatePreviewPicture($entry, $content['open_graph']['og_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)) {
);
}
- $entry->setMimetype(isset($content['content_type']) ? $content['content_type'] : '');
- $entry->setReadingTime(Utils::getReadingTime($html));
-
- $domainName = parse_url($entry->getUrl(), PHP_URL_HOST);
- if (false !== $domainName) {
- $entry->setDomainName($domainName);
+ if (!empty($content['content_type'])) {
+ $entry->setMimetype($content['content_type']);
}
try {
* Use a Symfony validator to ensure the language is well formatted.
*
* @param Entry $entry
- * @param string $value Language to validate
+ * @param string $value Language to validate and save
*/
- private function validateAndSetLanguage($entry, $value)
+ public function updateLanguage(Entry $entry, $value)
{
// some lang are defined as fr-FR, es-ES.
// replacing - by _ might increase language support
* Use a Symfony validator to ensure the preview picture is a real url.
*
* @param Entry $entry
- * @param string $value URL to validate
+ * @param string $value URL to validate and save
*/
- private function validateAndSetPreviewPicture($entry, $value)
+ public function updatePreviewPicture(Entry $entry, $value)
{
$errors = $this->validator->validate(
$value,
$this->logger->warning('PreviewPicture validation failed. ' . (string) $errors);
}
+
+ /**
+ * Update date.
+ *
+ * @param Entry $entry
+ * @param string $value Date to validate and save
+ */
+ public function updatePublishedAt(Entry $entry, $value)
+ {
+ $date = $value;
+
+ // is it a timestamp?
+ if (filter_var($date, FILTER_VALIDATE_INT) !== false) {
+ $date = '@'.$value;
+ }
+
+ try {
+ $entry->setPublishedAt(new \DateTime($date));
+ } catch (\Exception $e) {
+ $this->logger->warning('Error while defining date', ['e' => $e, 'url' => $entry->getUrl(), 'date' => $value]);
+ }
+ }
}
$this->markTestSkipped('No content found in db.');
}
- // hydrate the tags relations
- $nbTags = count($entry->getTags());
-
- $this->client->request('PATCH', '/api/entries/' . $entry->getId() . '.json', [
+ $this->client->request('PATCH', '/api/entries/'.$entry->getId().'.json', [
'title' => 'New awesome title',
'tags' => 'new tag ' . uniqid(),
'starred' => '1',
'authors' => 'bob,sponge',
'content' => 'awesome',
'public' => 0,
+ 'published_at' => 1488833381,
]);
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
$this->assertSame($entry->getId(), $content['id']);
$this->assertSame($entry->getUrl(), $content['url']);
$this->assertSame('New awesome title', $content['title']);
- $this->assertGreaterThan($nbTags, count($content['tags']));
+ $this->assertGreaterThanOrEqual(1, count($content['tags']), 'We force only one tag');
$this->assertSame(1, $content['user_id']);
$this->assertSame('de_AT', $content['language']);
$this->assertSame('http://preview.io/picture.jpg', $content['preview_picture']);
$this->assertContains('bob', $content['published_by']);
$this->assertSame('awesome', $content['content']);
$this->assertFalse($content['is_public'], 'Entry is no more shared');
+ $this->assertContains('2017-03-06', $content['published_at']);
}
public function testPatchEntryWithoutQuotes()
$this->markTestSkipped('No content found in db.');
}
- // hydrate the tags relations
- $nbTags = count($entry->getTags());
+ $previousContent = $entry->getContent();
+ $previousLanguage = $entry->getLanguage();
$this->client->request('PATCH', '/api/entries/' . $entry->getId() . '.json', [
'title' => 'New awesome title',
$this->assertSame($entry->getId(), $content['id']);
$this->assertSame($entry->getUrl(), $content['url']);
- $this->assertSame('New awesome title', $content['title']);
+ $this->assertGreaterThanOrEqual(1, count($content['tags']), 'We force only one tag');
$this->assertGreaterThan($nbTags, count($content['tags']));
$this->assertEmpty($content['published_by'], 'Authors were not saved because of an array instead of a string');
+ $this->assertEquals($previousContent, $content['content'], 'Ensure content has not moved');
+ $this->assertEquals($previousLanguage, $content['language'], 'Ensure language has not moved');
}
public function testGetTagsEntry()
$this->markTestSkipped('No content found in db.');
}
- $this->client->request('PATCH', '/api/entries/' . $entry->getId() . '.json', [
- 'title' => $entry->getTitle() . '++',
+ $previousTitle = $entry->getTitle();
+
+ $this->client->request('PATCH', '/api/entries/'.$entry->getId().'.json', [
+ 'title' => $entry->getTitle().'++',
]);
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
$content = json_decode($this->client->getResponse()->getContent(), true);
$this->assertSame(1, $content['is_archived']);
+ $this->assertEquals($previousTitle.'++', $content['title']);
}
public function testSaveIsStarredAfterPatch()
$this->assertCount(4, $tags);
}
+ public function testPostEntriesTagsListActionNoList()
+ {
+ $this->client->request('POST', '/api/entries/tags/lists?list='.json_encode([]));
+
+ $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
+
+ $content = json_decode($this->client->getResponse()->getContent(), true);
+
+ $this->assertEmpty($content);
+ }
+
public function testDeleteEntriesTagsListAction()
{
$em = $this->client->getContainer()->get('doctrine.orm.entity_manager');
$this->assertCount(0, $entry->getTags());
}
+ public function testDeleteEntriesTagsListActionNoList()
+ {
+ $this->client->request('DELETE', '/api/entries/tags/list?list='.json_encode([]));
+
+ $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
+
+ $content = json_decode($this->client->getResponse()->getContent(), true);
+
+ $this->assertEmpty($content);
+ }
+
public function testPostEntriesListAction()
{
$list = [
$this->assertSame('http://0.0.0.0/entry2', $content[1]['url']);
}
+ public function testPostEntriesListActionWithNoUrls()
+ {
+ $this->client->request('POST', '/api/entries/lists?urls='.json_encode([]));
+
+ $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
+
+ $content = json_decode($this->client->getResponse()->getContent(), true);
+
+ $this->assertEmpty($content);
+ }
+
public function testDeleteEntriesListAction()
{
$em = $this->client->getContainer()->get('doctrine.orm.entity_manager');
$this->assertSame('http://0.0.0.0/test-entry-not-exist', $content[1]['url']);
}
+ public function testDeleteEntriesListActionWithNoUrls()
+ {
+ $this->client->request('DELETE', '/api/entries/list?urls='.json_encode([]));
+
+ $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
+
+ $content = json_decode($this->client->getResponse()->getContent(), true);
+
+ $this->assertEmpty($content);
+ }
+
public function testLimitBulkAction()
{
$list = [
->method('tag');
$validator = $this->getValidator();
- $validator->expects($this->exactly(2))
+ $validator->expects($this->once())
->method('validate')
- ->will($this->onConsecutiveCalls(
- new ConstraintViolationList([new ConstraintViolation('oops', 'oops', [], 'oops', 'language', 'dontexist')]),
- new ConstraintViolationList()
- ));
+ ->willReturn(new ConstraintViolationList([new ConstraintViolation('oops', 'oops', [], 'oops', 'language', 'dontexist')]));
$graby = $this->getMockBuilder('Graby\Graby')
->setMethods(['fetchContent'])