diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index b8155a3..f154bb5 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php @@ -108,4 +108,30 @@ class ApiUtils ]; return $link; } + + /** + * Update link fields using an updated link object. + * + * @param array $oldLink data + * @param array $newLink data + * + * @return array $oldLink updated with $newLink values + */ + public static function updateLink($oldLink, $newLink) + { + foreach (['title', 'url', 'description', 'tags', 'private'] as $field) { + $oldLink[$field] = $newLink[$field]; + } + $oldLink['updated'] = new \DateTime(); + + if (empty($oldLink['url'])) { + $oldLink['url'] = '?' . $oldLink['shorturl']; + } + + if (empty($oldLink['title'])) { + $oldLink['title'] = $oldLink['url']; + } + + return $oldLink; + } } diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index 0db10fd..1c68b06 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php @@ -146,4 +146,46 @@ class Links extends ApiController return $response->withAddedHeader('Location', $redirect) ->withJson($out, 201, $this->jsonStyle); } + + /** + * Updates an existing link from posted request body. + * + * @param Request $request Slim request. + * @param Response $response Slim response. + * @param array $args Path parameters. including the ID. + * + * @return Response response. + * + * @throws ApiLinkNotFoundException generating a 404 error. + */ + public function putLink($request, $response, $args) + { + if (! isset($this->linkDb[$args['id']])) { + throw new ApiLinkNotFoundException(); + } + + $index = index_url($this->ci['environment']); + $data = $request->getParsedBody(); + + $requestLink = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); + // duplicate URL on a different link, return 409 Conflict + if (! empty($requestLink['url']) + && ! empty($dup = $this->linkDb->getLinkFromUrl($requestLink['url'])) + && $dup['id'] != $args['id'] + ) { + return $response->withJson( + ApiUtils::formatLink($dup, $index), + 409, + $this->jsonStyle + ); + } + + $responseLink = $this->linkDb[$args['id']]; + $responseLink = ApiUtils::updateLink($responseLink, $requestLink); + $this->linkDb[$responseLink['id']] = $responseLink; + $this->linkDb->save($this->conf->get('resource.page_cache')); + + $out = ApiUtils::formatLink($responseLink, $index); + return $response->withJson($out, 200, $this->jsonStyle); + } } diff --git a/index.php b/index.php index 76aa1ae..b8d2005 100644 --- a/index.php +++ b/index.php @@ -2251,6 +2251,7 @@ $app->group('/api/v1', function() { $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks'); $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink'); $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink'); + $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink'); })->add('\Shaarli\Api\ApiMiddleware'); $response = $app->run(true); diff --git a/tests/api/ApiUtilsTest.php b/tests/api/ApiUtilsTest.php index b4431d1..62baf4c 100644 --- a/tests/api/ApiUtilsTest.php +++ b/tests/api/ApiUtilsTest.php @@ -271,4 +271,82 @@ class ApiUtilsTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, ApiUtils::formatLink($link, $indexUrl)); } + + /** + * Test updateLink with valid data, and also unnecessary fields. + */ + public function testUpdateLink() + { + $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102'); + $old = [ + 'id' => 12, + 'url' => '?abc', + 'shorturl' => 'abc', + 'title' => 'Note', + 'description' => '', + 'tags' => '', + 'private' => '', + 'created' => $created, + ]; + + $new = [ + 'id' => 13, + 'shorturl' => 'nope', + 'url' => 'http://somewhere.else', + 'title' => 'Le Cid', + 'description' => 'Percé jusques au fond du cœur [...]', + 'tags' => 'corneille rodrigue', + 'private' => true, + 'created' => 'creation', + 'updated' => 'updation', + ]; + + $result = ApiUtils::updateLink($old, $new); + $this->assertEquals(12, $result['id']); + $this->assertEquals('http://somewhere.else', $result['url']); + $this->assertEquals('abc', $result['shorturl']); + $this->assertEquals('Le Cid', $result['title']); + $this->assertEquals('Percé jusques au fond du cœur [...]', $result['description']); + $this->assertEquals('corneille rodrigue', $result['tags']); + $this->assertEquals(true, $result['private']); + $this->assertEquals($created, $result['created']); + $this->assertTrue(new \DateTime('5 seconds ago') < $result['updated']); + } + + /** + * Test updateLink with minimal data. + */ + public function testUpdateLinkMinimal() + { + $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102'); + $old = [ + 'id' => 12, + 'url' => '?abc', + 'shorturl' => 'abc', + 'title' => 'Note', + 'description' => 'Interesting description!', + 'tags' => 'doggo', + 'private' => true, + 'created' => $created, + ]; + + $new = [ + 'url' => '', + 'title' => '', + 'description' => '', + 'tags' => '', + 'private' => false, + ]; + + $result = ApiUtils::updateLink($old, $new); + $this->assertEquals(12, $result['id']); + $this->assertEquals('?abc', $result['url']); + $this->assertEquals('abc', $result['shorturl']); + $this->assertEquals('?abc', $result['title']); + $this->assertEquals('', $result['description']); + $this->assertEquals('', $result['tags']); + $this->assertEquals(false, $result['private']); + $this->assertEquals($created, $result['created']); + $this->assertTrue(new \DateTime('5 seconds ago') < $result['updated']); + } } diff --git a/tests/api/controllers/PutLinkTest.php b/tests/api/controllers/PutLinkTest.php new file mode 100644 index 0000000..4096c1a --- /dev/null +++ b/tests/api/controllers/PutLinkTest.php @@ -0,0 +1,199 @@ +conf = new ConfigManager('tests/utils/config/configJson.json.php'); + $this->refDB = new \ReferenceLinkDB(); + $this->refDB->write(self::$testDatastore); + + $this->container = new Container(); + $this->container['conf'] = $this->conf; + $this->container['db'] = new \LinkDB(self::$testDatastore, true, false); + + $this->controller = new Links($this->container); + + // Used by index_url(). + $this->controller->getCi()['environment'] = [ + 'SERVER_NAME' => 'domain.tld', + 'SERVER_PORT' => 80, + 'SCRIPT_NAME' => '/', + ]; + } + + /** + * After every test, remove the test datastore. + */ + public function tearDown() + { + @unlink(self::$testDatastore); + } + + /** + * Test link update without value: reset the link to default values + */ + public function testPutLinkMinimal() + { + $env = Environment::mock([ + 'REQUEST_METHOD' => 'PUT', + ]); + $id = '41'; + $request = Request::createFromEnvironment($env); + + $response = $this->controller->putLink($request, new Response(), ['id' => $id]); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_LINK, count($data)); + $this->assertEquals($id, $data['id']); + $this->assertEquals('WDWyig', $data['shorturl']); + $this->assertEquals('http://domain.tld/?WDWyig', $data['url']); + $this->assertEquals('?WDWyig', $data['title']); + $this->assertEquals('', $data['description']); + $this->assertEquals([], $data['tags']); + $this->assertEquals(false, $data['private']); + $this->assertEquals( + \DateTime::createFromFormat('Ymd_His', '20150310_114651'), + \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) + ); + $this->assertTrue(new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])); + } + + /** + * Test link update with new values + */ + public function testPutLinkWithValues() + { + $env = Environment::mock([ + 'REQUEST_METHOD' => 'PUT', + 'CONTENT_TYPE' => 'application/json' + ]); + $id = 41; + $update = [ + 'url' => 'http://somewhere.else', + 'title' => 'Le Cid', + 'description' => 'Percé jusques au fond du cœur [...]', + 'tags' => ['corneille', 'rodrigue'], + 'private' => true, + ]; + $request = Request::createFromEnvironment($env); + $request = $request->withParsedBody($update); + + $response = $this->controller->putLink($request, new Response(), ['id' => $id]); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_LINK, count($data)); + $this->assertEquals($id, $data['id']); + $this->assertEquals('WDWyig', $data['shorturl']); + $this->assertEquals('http://somewhere.else', $data['url']); + $this->assertEquals('Le Cid', $data['title']); + $this->assertEquals('Percé jusques au fond du cœur [...]', $data['description']); + $this->assertEquals(['corneille', 'rodrigue'], $data['tags']); + $this->assertEquals(true, $data['private']); + $this->assertEquals( + \DateTime::createFromFormat('Ymd_His', '20150310_114651'), + \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) + ); + $this->assertTrue(new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])); + } + + /** + * Test link update with an existing URL: 409 Conflict with the existing link as body + */ + public function testPutLinkDuplicate() + { + $link = [ + 'url' => 'mediagoblin.org/', + 'title' => 'new entry', + 'description' => 'shaare description', + 'tags' => ['one', 'two'], + 'private' => true, + ]; + $env = Environment::mock([ + 'REQUEST_METHOD' => 'PUT', + 'CONTENT_TYPE' => 'application/json' + ]); + + $request = Request::createFromEnvironment($env); + $request = $request->withParsedBody($link); + $response = $this->controller->putLink($request, new Response(), ['id' => 41]); + + $this->assertEquals(409, $response->getStatusCode()); + $data = json_decode((string) $response->getBody(), true); + $this->assertEquals(self::NB_FIELDS_LINK, count($data)); + $this->assertEquals(7, $data['id']); + $this->assertEquals('IuWvgA', $data['shorturl']); + $this->assertEquals('http://mediagoblin.org/', $data['url']); + $this->assertEquals('MediaGoblin', $data['title']); + $this->assertEquals('A free software media publishing platform #hashtagOther', $data['description']); + $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']); + $this->assertEquals(false, $data['private']); + $this->assertEquals( + \DateTime::createFromFormat(\LinkDB::LINK_DATE_FORMAT, '20130614_184135'), + \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) + ); + $this->assertEquals( + \DateTime::createFromFormat(\LinkDB::LINK_DATE_FORMAT, '20130615_184230'), + \DateTime::createFromFormat(\DateTime::ATOM, $data['updated']) + ); + } + + /** + * Test link update on non existent link => ApiLinkNotFoundException. + * + * @expectedException Shaarli\Api\Exceptions\ApiLinkNotFoundException + * @expectedExceptionMessage Link not found + */ + public function testGetLink404() + { + $env = Environment::mock([ + 'REQUEST_METHOD' => 'PUT', + ]); + $request = Request::createFromEnvironment($env); + + $this->controller->putLink($request, new Response(), ['id' => -1]); + } +}