diff --git a/lib/Alchemy/Phrasea/Application/Api.php b/lib/Alchemy/Phrasea/Application/Api.php index 87137cae78..faf5721fd0 100644 --- a/lib/Alchemy/Phrasea/Application/Api.php +++ b/lib/Alchemy/Phrasea/Application/Api.php @@ -21,6 +21,7 @@ use Alchemy\Phrasea\Core\Event\ApiLoadEndEvent; use Alchemy\Phrasea\Core\Event\ApiLoadStartEvent; use Alchemy\Phrasea\Core\Event\Subscriber\ApiOauth2ErrorsSubscriber; use Alchemy\Phrasea\Core\Event\Subscriber\ApiExceptionHandlerSubscriber; +use Alchemy\Phrasea\Core\Provider\JsonSchemaServiceProvider; use Monolog\Logger; use Monolog\Processor\WebProcessor; use Silex\Application as SilexApplication; @@ -86,6 +87,7 @@ return call_user_func(function ($environment = PhraseaApplication::ENV_PROD) { } }); + $app->register(new JsonSchemaServiceProvider()); $app->register(new \API_V1_Timer()); $app['dispatcher']->dispatch(PhraseaEvents::API_LOAD_START, new ApiLoadStartEvent()); diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1.php b/lib/Alchemy/Phrasea/Controller/Api/V1.php index a5eef20241..593117085b 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1.php @@ -480,6 +480,30 @@ class V1 implements ControllerProviderInterface return $app['api']->substitute_subdef($app, $request)->get_response(); }); + /** + * Route : /stories/add/ + * + * Method : POST + * + * Parameters : + * + */ + $controllers->post('/stories', function (SilexApplication $app, Request $request) { + return $app['api']->add_story($app, $request)->get_response(); + }); + + /** + * Route : /stories/{story_id}/records + * + * Method : POST + * + * Parameters : + * + */ + $controllers->post('/stories/{databox_id}/{story_id}/records', function (SilexApplication $app, Request $request, $databox_id, $story_id) { + return $app['api']->add_records_to_story($app, $request, $databox_id, $story_id)->get_response(); + }); + /** * Route : /search/ * diff --git a/lib/Alchemy/Phrasea/Core/Provider/JsonSchemaServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/JsonSchemaServiceProvider.php new file mode 100644 index 0000000000..92b6fe39d0 --- /dev/null +++ b/lib/Alchemy/Phrasea/Core/Provider/JsonSchemaServiceProvider.php @@ -0,0 +1,35 @@ +share(function (Application $app) { + return new UriRetriever(); + }); + + $app['json-schema.validator'] = $app->share(function (Application $app) { + return new Validator(); + }); + } + + public function boot(Application $app) + { + } +} diff --git a/lib/classes/API/V1/adapter.php b/lib/classes/API/V1/adapter.php index e23138f47e..b59d14c685 100644 --- a/lib/classes/API/V1/adapter.php +++ b/lib/classes/API/V1/adapter.php @@ -2182,6 +2182,135 @@ class API_V1_adapter extends API_V1_Abstract ); } + public function add_story(Application $app, Request $request) + { + $content = $request->getContent(); + + $data = @json_decode($content); + + if (JSON_ERROR_NONE !== json_last_error()) { + $app->abort(400, 'Json response cannot be decoded or the encoded data is deeper than the recursion limit'); + } + + if (!isset($data->{'stories'})) { + $app->abort(400, 'Missing "stories" property'); + } + + $schemaStory = $app['json-schema.retriever']->retrieve('file://'.$app['root.path'].'/lib/conf.d/json_schema/story.json'); + $schemaRecordStory = $app['json-schema.retriever']->retrieve('file://'.$app['root.path'].'/lib/conf.d/json_schema/story_record.json'); + + $storyData = $data->{'stories'}; + + if (!is_array($storyData)) { + $storyData = array($storyData); + } + + $stories = array(); + foreach ($storyData as $data) { + $stories[] = $this->create_story($app, $data, $schemaStory, $schemaRecordStory); + } + + $result = new API_V1_result($app, $request, $this); + + $result->set_datas(array('stories' => array_map(function($story) { + return sprintf('/stories/%s/%s/', $story->get_sbas_id(), $story->get_record_id()); + }, $stories))); + + return $result; + } + + public function add_records_to_story(Application $app, Request $request, $databox_id, $story_id) + { + $content = $request->getContent(); + + $data = @json_decode($content); + + if (JSON_ERROR_NONE !== json_last_error()) { + $app->abort(400, 'Json response cannot be decoded or the encoded data is deeper than the recursion limit'); + } + + if (!isset($data->{'story_records'})) { + $app->abort(400, 'Missing "story_records" property'); + } + + $recordsData = $data->{'story_records'}; + + if (!is_array($recordsData)) { + $recordsData = array($recordsData); + } + + $story = new \record_adapter($app, $databox_id, $story_id); + + $schema = $app['json-schema.retriever']->retrieve('file://'.$app['root.path'].'/lib/conf.d/json_schema/story_record.json'); + + $records = array(); + foreach ($recordsData as $data) { + $records[] = $this->add_record_to_story($app, $story, $data, $schema); + } + + $app['dispatcher']->dispatch(PhraseaEvents::RECORD_EDIT, new RecordEdit($story)); + + $result = new API_V1_result($this->app, $request, $this); + + $result->set_datas(array('records' => $records)); + + return $result; + } + + protected function create_story(Application $app, $data, $schemaStory, $schemaRecordStory) + { + $app['json-schema.validator']->check($data, $schemaStory); + + if (false === $app['json-schema.validator']->isValid()) { + $app->abort(400, 'Request body does not contains a valid "story" object'); + } + + $collection = \collection::get_from_base_id($app, $data->{'collection_id'}); + + if (!$app['authentication']->getUser()->ACL()->has_right_on_base($collection->get_base_id(), 'canaddrecord')) { + $app->abort(403, sprintf('You can not create a story on this collection %s', $collection->get_base_id())); + } + + $story = \record_adapter::createStory($app, $collection); + + if (isset($data->{'title'})) { + $story->set_original_name((string) $data->{'title'}); + } + + if (isset($data->{'story_records'})) { + $recordsData = (array) $data->{'story_records'}; + foreach ($recordsData as $data) { + $this->add_record_to_story($app, $story, $data, $schemaRecordStory); + } + } + + return $story; + } + + protected function add_record_to_story(Application $app, record_adapter $story, $data, $jsonSchema) + { + $app['json-schema.validator']->check($data, $jsonSchema); + + if (false === $app['json-schema.validator']->isValid()) { + $app->abort(400, 'Request body contains not a valid "record story" object'); + } + + $databox_id = $data->{'databox_id'}; + $record_id = $data->{'record_id'}; + + try { + $record = new \record_adapter($app, $databox_id, $record_id); + } catch (Exception_Record_AdapterNotFound $e) { + $app->abort(404, sprintf('Record identified by databox_is %s and record_id %s could not be found', $databox_id, $record_id)); + } + + if (!$story->hasChild($record)) { + $story->appendChild($record); + } + + return $record->get_serialize_key(); + } + /** * Provide phraseanet global values * diff --git a/lib/classes/record/adapter.php b/lib/classes/record/adapter.php index a8480f9aab..022512806e 100644 --- a/lib/classes/record/adapter.php +++ b/lib/classes/record/adapter.php @@ -1409,7 +1409,8 @@ class record_adapter implements record_Interface, cache_cacheableInterface $app['filesystem']->copy($file->getFile()->getRealPath(), $pathhd . $newname, true); $media = $app['mediavorus']->guess($pathhd . $newname); - $subdef = media_subdef::create($app, $record, 'document', $media); + + media_subdef::create($app, $record, 'document', $media); $record->delete_data_from_cache(\record_adapter::CACHE_SUBDEFS); diff --git a/lib/conf.d/json_schema/story.json b/lib/conf.d/json_schema/story.json new file mode 100644 index 0000000000..4fddba05ca --- /dev/null +++ b/lib/conf.d/json_schema/story.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Story", + "description": "A story from Phraseanet", + "type": "object", + "properties": { + "id": { + "description": "The unique identifier for a story", + "type": "string" + }, + "story_id": { + "description": "The identifier for a record contextualized by the databox", + "type": "integer" + }, + "databox_id": { + "description": "The databox identifier where the record is localized", + "type": "integer" + }, + "collection_id": { + "description": "The collection identifier where the record is localized", + "type": "integer" + } + }, + "required": ["collection_id"] +} \ No newline at end of file diff --git a/lib/conf.d/json_schema/story_record.json b/lib/conf.d/json_schema/story_record.json new file mode 100644 index 0000000000..bca79d7b47 --- /dev/null +++ b/lib/conf.d/json_schema/story_record.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Record", + "description": "A record in a story", + "type": "object", + "properties": { + "id": { + "description": "The unique identifier for a record", + "type": "string" + }, + "record_id": { + "description": "The identifier for a story contextualized by the databox", + "type": "integer" + }, + "databox_id": { + "description": "The databox identifier where the story is localized", + "type": "integer" + } + }, + "required": ["databox_id", "record_id"] +} \ No newline at end of file diff --git a/tests/Alchemy/Tests/Phrasea/Application/ApiAbstract.php b/tests/Alchemy/Tests/Phrasea/Application/ApiAbstract.php index 9eba7fc22f..da6e911f7e 100644 --- a/tests/Alchemy/Tests/Phrasea/Application/ApiAbstract.php +++ b/tests/Alchemy/Tests/Phrasea/Application/ApiAbstract.php @@ -1589,6 +1589,80 @@ abstract class ApiAbstract extends \PhraseanetWebTestCaseAbstract } } + public function testAddStory() + { + $this->setToken(self::$token); + $route = '/api/v1/stories'; + + $story['collection_id'] = self::$DI['collection']->get_base_id(); + $story['title'] = uniqid('story'); + + $file = new File(self::$DI['app'], self::$DI['app']['mediavorus']->guess(__DIR__ . '/../../../../files/p4logo.jpg'), self::$DI['collection']); + $record = \record_adapter::createFromFile($file, self::$DI['app']); + + $story['story_records'] = array(array( + 'databox_id' => $record->get_sbas_id(), + 'record_id' => $record->get_record_id() + )); + + self::$DI['client']->request( + 'POST', + $route, + $this->getParameters(), + $this->getAddRecordFile(), + array('HTTP_Accept' => $this->getAcceptMimeType()), + json_encode(array('stories' => array($story))) + ); + $content = $this->unserialize(self::$DI['client']->getResponse()->getContent()); + + $this->evaluateResponse200(self::$DI['client']->getResponse()); + $this->evaluateMeta200($content); + $data = $content['response']; + + $this->assertArrayHasKey('stories', $data); + $this->assertCount(1, $data['stories']); + list($empty, $path, $databox_id, $story_id) = explode('/', current($data['stories'])); + $databox = self::$DI['app']['phraseanet.appbox']->get_databox($databox_id); + $story = $databox->get_record($story_id); + $story->delete(); + $record->delete(); + } + + public function testAddRecordToStory() + { + $this->setToken(self::$token); + $story = \record_adapter::createStory(self::$DI['app'], self::$DI['collection']); + + $route = sprintf('/api/v1/stories/%s/%s/records', $story->get_sbas_id(), $story->get_record_id()); + + $file = new File(self::$DI['app'], self::$DI['app']['mediavorus']->guess(__DIR__ . '/../../../../files/extractfile.jpg'), self::$DI['collection']); + $record = \record_adapter::createFromFile($file, self::$DI['app']); + + $records = array( + 'databox_id' => $record->get_sbas_id(), + 'record_id' => $record->get_record_id() + ); + + self::$DI['client']->request( + 'POST', + $route, + $this->getParameters(), + $this->getAddRecordFile(), + array('HTTP_Accept' => $this->getAcceptMimeType()), + json_encode(array('story_records' => array($records))) + ); + $content = $this->unserialize(self::$DI['client']->getResponse()->getContent()); + + $this->evaluateResponse200(self::$DI['client']->getResponse()); + $this->evaluateMeta200($content); + $data = $content['response']; + + $this->assertArrayHasKey('records', $data); + $this->assertCount(1, $data['records']); + $story->delete(); + $record->delete(); + } + /** * @covers \API_V1_adapter::add_record * @covers \API_V1_adapter::list_record diff --git a/tests/Alchemy/Tests/Phrasea/Border/Checker/Sha256Test.php b/tests/Alchemy/Tests/Phrasea/Border/Checker/Sha256Test.php index 68f3a0f34d..80ca83cff4 100644 --- a/tests/Alchemy/Tests/Phrasea/Border/Checker/Sha256Test.php +++ b/tests/Alchemy/Tests/Phrasea/Border/Checker/Sha256Test.php @@ -41,7 +41,16 @@ class Sha256Test extends \PhraseanetPHPUnitAbstract $session = new \Entities\LazaretSession(); self::$DI['app']['EM']->persist($session); - self::$DI['app']['border-manager']->process($session, File::buildFromPathfile($this->media->getFile()->getPathname(), self::$DI['collection'], self::$DI['app']), null, Manager::FORCE_RECORD); + self::$DI['app']['border-manager']->process( + $session, + File::buildFromPathfile( + $this->media->getFile()->getPathname(), + self::$DI['collection'], + self::$DI['app'] + ), + null, + Manager::FORCE_RECORD + ); $mock = $this->getMock('\\Alchemy\\Phrasea\\Border\\File', array('getSha256'), array(self::$DI['app'], $this->media, self::$DI['collection']));