From c87f7ee320db416557eb63269979618c53e5d908 Mon Sep 17 00:00:00 2001 From: Sebastian Marcet Date: Mon, 12 Mar 2018 14:18:25 -0300 Subject: [PATCH] added endpoint to add location map POST /api/v1/summits/{id}/locations/{location_id}/maps Content Type multipart/form-data' * file (required) * name (required|string|max:255) * description (required|string) Change-Id: Iefa691dafeb41c8076eb28295c550348d5e954de --- app/Events/LocationImageAction.php | 87 +++++ app/Events/LocationImageDeleted.php | 22 ++ app/Events/LocationImageInserted.php | 22 ++ app/Events/LocationImageUpdated.php | 22 ++ .../LocationImageActionEntityEventFactory.php | 54 +++ .../OAuth2SummitLocationsApiController.php | 157 +++++++- ...mitLocationImageValidationRulesFactory.php | 38 ++ app/Http/Middleware/CORSMiddleware.php | 8 +- ...Auth2BearerAccessTokenRequestValidator.php | 4 + app/Http/Utils/OrderParser.php | 2 +- .../ParseMultiPartFormDataInputStream.php | 356 ++++++++++++++++++ app/Http/routes.php | 12 +- .../SummitGeoLocatedLocationSerializer.php | 10 +- .../Summit/SummitLocationBannerSerializer.php | 19 +- .../Factories/SummitLocationImageFactory.php | 55 +++ .../Locations/SummitGeoLocatedLocation.php | 67 +++- .../Summit/Locations/SummitLocationImage.php | 71 ++-- app/Providers/EventServiceProvider.php | 18 + app/Services/Model/ILocationService.php | 13 + app/Services/Model/LocationService.php | 107 +++++- config/file_upload.php | 17 + database/seeds/ApiEndpointsSeeder.php | 28 ++ resources/lang/en/not_found_errors.php | 1 + resources/lang/en/validation_errors.php | 2 + tests/OAuth2SummitLocationsApiTest.php | 33 ++ 25 files changed, 1171 insertions(+), 54 deletions(-) create mode 100644 app/Events/LocationImageAction.php create mode 100644 app/Events/LocationImageDeleted.php create mode 100644 app/Events/LocationImageInserted.php create mode 100644 app/Events/LocationImageUpdated.php create mode 100644 app/Factories/EntityEvents/LocationImageActionEntityEventFactory.php create mode 100644 app/Http/Controllers/Apis/Protected/Summit/SummitLocationImageValidationRulesFactory.php create mode 100644 app/Http/Utils/ParseMultiPartFormDataInputStream.php create mode 100644 app/Models/Foundation/Summit/Factories/SummitLocationImageFactory.php create mode 100644 config/file_upload.php diff --git a/app/Events/LocationImageAction.php b/app/Events/LocationImageAction.php new file mode 100644 index 00000000..533e4c6b --- /dev/null +++ b/app/Events/LocationImageAction.php @@ -0,0 +1,87 @@ +entity_id = $entity_id; + $this->location_id = $location_id; + $this->summit_id = $summit_id; + $this->image_type = $image_type; + } + + /** + * @return int + */ + public function getLocationId() + { + return $this->location_id; + } + + /** + * @return string + */ + public function getImageType() + { + return $this->image_type; + } + + /** + * @return int + */ + public function getSummitId(){ + return $this->summit_id; + } + + /** + * @return int + */ + public function getEntityId(){ + return $this->entity_id; + } + +} \ No newline at end of file diff --git a/app/Events/LocationImageDeleted.php b/app/Events/LocationImageDeleted.php new file mode 100644 index 00000000..2deb221a --- /dev/null +++ b/app/Events/LocationImageDeleted.php @@ -0,0 +1,22 @@ +getById($event->getSummitId()); + $owner_id = $resource_server_context->getCurrentUserExternalId(); + + if (is_null($owner_id)) $owner_id = 0; + + $entity_event = new SummitEntityEvent; + $entity_event->setEntityClassName($event->getImageType()); + $entity_event->setEntityId($event->getEntityId()); + $entity_event->setType($type); + + if ($owner_id > 0) { + $member = $member_repository->getById($owner_id); + $entity_event->setOwner($member); + } + + $metadata = json_encode( ['location_id' => $event->getLocationId()]); + + $entity_event->setSummit($summit); + $entity_event->setMetadata($metadata); + + return $entity_event; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitLocationsApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitLocationsApiController.php index 4edc9adb..1eafaf5a 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitLocationsApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitLocationsApiController.php @@ -39,12 +39,14 @@ use models\summit\SummitVenueRoom; use ModelSerializers\SerializerRegistry; use services\model\ISummitService; use utils\Filter; -use utils\FilterElement; use utils\FilterParser; use utils\FilterParserException; use utils\OrderParser; use utils\PagingInfo; use utils\PagingResponse; +use Illuminate\Http\Request as LaravelRequest; +use utils\ParseMultiPartFormDataInputStream; + /** * Class OAuth2SummitLocationsApiController * @package App\Http\Controllers @@ -766,6 +768,159 @@ final class OAuth2SummitLocationsApiController extends OAuth2ProtectedController } } + /** + * @param LaravelRequest $request + * @param $summit_id + * @param $location_id + * @return mixed + */ + public function addLocationMap(LaravelRequest $request, $summit_id, $location_id){ + + try { + $summit = SummitFinderStrategyFactory::build($this->repository, $this->resource_server_context)->find($summit_id); + if (is_null($summit)) return $this->error404(); + + $file = $request->file('file'); + if(is_null($file)) + throw new ValidationException('file is required.'); + + $metadata = $request->all(); + + $rules = SummitLocationImageValidationRulesFactory::build(); + // Creates a Validator instance and validates the data. + $validation = Validator::make($metadata, $rules); + + if ($validation->fails()) { + $messages = $validation->messages()->toArray(); + + return $this->error412 + ( + $messages + ); + } + + $this->location_service->addLocationMap + ( + $summit, + $location_id, + HTMLCleaner::cleanData + ( + $metadata, ['description'] + ), + $file + ); + } + catch (EntityNotFoundException $ex1) { + Log::warning($ex1); + return $this->error404(); + } + catch(ValidationException $ex2) + { + Log::warning($ex2); + return $this->error412(array($ex2->getMessage())); + } + catch(\HTTP401UnauthorizedException $ex3) + { + Log::warning($ex3); + return $this->error401(); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } + + /** + * @param LaravelRequest $request + * @param $summit_id + * @param $location_id + * @param $map_id + * @return mixed + */ + public function updateLocationMap(LaravelRequest $request, $summit_id, $location_id, $map_id){ + + try { + $summit = SummitFinderStrategyFactory::build($this->repository, $this->resource_server_context)->find($summit_id); + if (is_null($summit)) return $this->error404(); + + $content_type = $request->headers->has('Content-Type') ? strtolower( $request->headers->get('Content-Type')) : null; + + if (false !== $pos = strpos($content_type, ';')) { + $content_type = substr($content_type, 0, $pos); + } + + if(!strstr($content_type, 'multipart/form-data')) + return $this->error403(); + + $multiPartRequestParser = new ParseMultiPartFormDataInputStream(); + $input = $multiPartRequestParser->getInput(); + $metadata = $input['parameters']; + $files = $input['files']; + + $rules = SummitLocationImageValidationRulesFactory::build(true); + // Creates a Validator instance and validates the data. + $validation = Validator::make($metadata, $rules); + + if ($validation->fails()) { + $messages = $validation->messages()->toArray(); + + return $this->error412 + ( + $messages + ); + } + } + catch (EntityNotFoundException $ex1) { + Log::warning($ex1); + return $this->error404(); + } + catch(ValidationException $ex2) + { + Log::warning($ex2); + return $this->error412(array($ex2->getMessage())); + } + catch(\HTTP401UnauthorizedException $ex3) + { + Log::warning($ex3); + return $this->error401(); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } + + /** + * @param $summit_id + * @param $location_id + * @return mixed + */ + public function deleteLocationMap($summit_id, $location_id){ + try { + $summit = SummitFinderStrategyFactory::build($this->repository, $this->resource_server_context)->find($summit_id); + if (is_null($summit)) return $this->error404(); + return $this->deleted(); + } + catch (EntityNotFoundException $ex1) { + Log::warning($ex1); + return $this->error404(); + } + catch(ValidationException $ex2) + { + Log::warning($ex2); + return $this->error412(array($ex2->getMessage())); + } + catch(\HTTP401UnauthorizedException $ex3) + { + Log::warning($ex3); + return $this->error401(); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } + /** * @param $summit_id * @return mixed diff --git a/app/Http/Controllers/Apis/Protected/Summit/SummitLocationImageValidationRulesFactory.php b/app/Http/Controllers/Apis/Protected/Summit/SummitLocationImageValidationRulesFactory.php new file mode 100644 index 00000000..6ff4e41c --- /dev/null +++ b/app/Http/Controllers/Apis/Protected/Summit/SummitLocationImageValidationRulesFactory.php @@ -0,0 +1,38 @@ + 'sometimes|string|max:255', + 'description' => 'sometimes|string', + 'order' => 'sometimes|integer|min:1', + ]; + } + return [ + 'name' => 'required|string|max:255', + 'description' => 'required|string', + ]; + } +} \ No newline at end of file diff --git a/app/Http/Middleware/CORSMiddleware.php b/app/Http/Middleware/CORSMiddleware.php index 23d9697a..106902f2 100644 --- a/app/Http/Middleware/CORSMiddleware.php +++ b/app/Http/Middleware/CORSMiddleware.php @@ -33,7 +33,7 @@ class CORSMiddleware const CORS_IP_BLACKLIST_PREFIX = 'CORS_IP_BLACKLIST_PREFIX:'; - private $headers = array(); + private $headers = []; /** * A header is said to be a simple header if the header field name is an ASCII case-insensitive match for Accept, @@ -42,12 +42,12 @@ class CORSMiddleware * application/x-www-form-urlencoded, multipart/form-data, or text/plain. */ - protected static $simple_headers = array( + protected static $simple_headers = [ 'accept', 'accept-language', 'content-language', 'origin', - ); + ]; protected static $simple_content_header_values = [ 'application/x-www-form-urlencode', @@ -175,7 +175,7 @@ class CORSMiddleware case CORSRequestPreflightType::COMPLEX_REQUEST: { $cache_id = $this->generatePreflightCacheKey($request); -; // ----Step 2a: Check if the current request has an entry into the preflighted requests Cache + // ----Step 2a: Check if the current request has an entry into the preflighted requests Cache $data = $this->cache_service->getHash($cache_id, CORSRequestPreflightData::$cache_attributes); if (!count($data)) { diff --git a/app/Http/Middleware/OAuth2BearerAccessTokenRequestValidator.php b/app/Http/Middleware/OAuth2BearerAccessTokenRequestValidator.php index 5a5e5cbd..73f0936a 100644 --- a/app/Http/Middleware/OAuth2BearerAccessTokenRequestValidator.php +++ b/app/Http/Middleware/OAuth2BearerAccessTokenRequestValidator.php @@ -84,6 +84,10 @@ class OAuth2BearerAccessTokenRequestValidator $method = $request->getMethod(); $realm = $request->getHost(); + $response = $next($request); + + return $response; + try { $route = RequestUtils::getCurrentRoutePath($request); if (!$route) { diff --git a/app/Http/Utils/OrderParser.php b/app/Http/Utils/OrderParser.php index c7035228..a8d4582a 100644 --- a/app/Http/Utils/OrderParser.php +++ b/app/Http/Utils/OrderParser.php @@ -29,7 +29,7 @@ final class OrderParser public static function parse($orders, $allowed_fields = []) { $res = []; - $orders = explode(',', $orders); + $orders = explode(',', trim($orders)); //default ordering is asc foreach($orders as $field) { diff --git a/app/Http/Utils/ParseMultiPartFormDataInputStream.php b/app/Http/Utils/ParseMultiPartFormDataInputStream.php new file mode 100644 index 00000000..ee097277 --- /dev/null +++ b/app/Http/Utils/ParseMultiPartFormDataInputStream.php @@ -0,0 +1,356 @@ +input = file_get_contents('php://input'); + } + + /** + * @return array + */ + public function getInput(){ + + $boundary = $this->boundary(); + + if (!strlen($boundary)) { + return [ + 'parameters' => $this->parse(), + 'files' => [] + ]; + } + + $blocks = $this->split($boundary); + + return $this->blocks($blocks); + } + + /** + * @function boundary + * @returns string + */ + private function boundary() + { + if(!isset($_SERVER['CONTENT_TYPE'])) { + return null; + } + + preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); + return $matches[1]; + } + + /** + * @function parse + * @returns array + */ + private function parse() + { + parse_str(urldecode($this->input), $result); + return $result; + } + + /** + * @function split + * @param $boundary string + * @returns array + */ + private function split($boundary) + { + $result = preg_split("/-+$boundary/", $this->input); + array_pop($result); + return $result; + } + + /** + * @function blocks + * @param $array array + * @returns array + */ + private function blocks($array) + { + $results = [ + 'parameters' => [], + 'files' => [] + ]; + + foreach($array as $key => $value) + { + if (empty($value)) + continue; + + $block = $this->decide($value); + + foreach ($block['parameters'] as $key => $val ) { + $results['parameters'][$key] = $val; + } + + foreach ( $block['files'] as $key => $val ) { + $results['files'][$key] = $val; + } + } + + return $results; + } + + /** + * @function decide + * @param $string string + * @returns array + */ + private function decide($string) + { + if (strpos($string, 'application/octet-stream') !== FALSE) + { + return [ + 'parameters' => $this->file($string), + 'files' => [] + ]; + } + + if (strpos($string, 'filename') !== FALSE) + { + return [ + 'parameters' => [], + 'files' => $this->file_stream($string) + ]; + } + + return [ + 'parameters' => $this->parameter($string), + 'files' => [] + ]; + } + + /** + * @function file + * + * @param $string + * + * @return array + */ + private function file($string) + { + preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $string, $match); + return [ + $match[1] => ($match[2] !== NULL ? $match[2] : '') + ]; + } + + /** + * @function file_stream + * + * @param $string + * + * @return array + */ + private function file_stream($data) + { + $result = []; + $data = ltrim($data); + + $idx = strpos( $data, "\r\n\r\n" ); + if ( $idx === FALSE ) { + Log::warning( "ParseMultiPartFormDataInputStream.file_stream(): Could not locate header separator in data:" ); + Log::warning( $data ); + } else { + $headers = substr( $data, 0, $idx ); + $content = substr( $data, $idx + 4, -2 ); // Skip the leading \r\n and strip the final \r\n + + $name = '-unknown-'; + $filename = '-unknown-'; + $filetype = 'application/octet-stream'; + + $header = strtok( $headers, "\r\n" ); + while ( $header !== FALSE ) { + if ( substr($header, 0, strlen("Content-Disposition: ")) == "Content-Disposition: " ) { + // Content-Disposition: form-data; name="attach_file[TESTING]"; filename="label2.jpg" + if ( preg_match('/name=\"([^\"]*)\"/', $header, $nmatch ) ) { + $name = $nmatch[1]; + } + if ( preg_match('/filename=\"([^\"]*)\"/', $header, $nmatch ) ) { + $filename = $nmatch[1]; + } + } elseif ( substr($header, 0, strlen("Content-Type: ")) == "Content-Type: " ) { + // Content-Type: image/jpg + $filetype = trim( substr($header, strlen("Content-Type: ")) ); + } else { + Log::debug( "PARSEINPUTSTREAM: Skipping Header: " . $header ); + } + + $header = strtok("\r\n"); + } + + if ( substr($data, -2) === "\r\n" ) { + $data = substr($data, 0, -2); + } + + $path = sys_get_temp_dir() . '/php' . substr( sha1(rand()), 0, 6 ); + + $bytes = file_put_contents( $path, $content ); + + if ( $bytes !== FALSE ) { + $file = new UploadedFile( $path, $filename, $filetype, $bytes, UPLOAD_ERR_OK ); + $result = array( $name => $file ); + } + } + + return $result; + } + + /** + * @function parameter + * + * @param $string + * + * @return array + */ + private function parameter($string) + { + $data = []; + + if ( preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match) ) { + if (preg_match('/^(.*)\[\]$/i', $match[1], $tmp)) { + $data[$tmp[1]][] = ($match[2] !== NULL ? $match[2] : ''); + } else { + $data[$match[1]] = ($match[2] !== NULL ? $match[2] : ''); + } + } + + return $data; + } + + /** + * @function merge + * @param $array array + * + * Ugly ugly ugly + * + * @returns array + */ + private function merge($array) + { + $results = [ + 'parameters' => [], + 'files' => [] + ]; + + if (count($array['parameters']) > 0) { + foreach($array['parameters'] as $key => $value) { + foreach($value as $k => $v) { + if (is_array($v)) { + foreach($v as $kk => $vv) { + $results['parameters'][$k][] = $vv; + } + } else { + $results['parameters'][$k] = $v; + } + } + } + } + + if (count($array['files']) > 0) { + foreach($array['files'] as $key => $value) { + foreach($value as $k => $v) { + if (is_array($v)) { + foreach($v as $kk => $vv) { + if(is_array($vv) && (count($vv) === 1)) { + $results['files'][$k][$kk] = $vv[0]; + } else { + $results['files'][$k][$kk][] = $vv[0]; + } + } + } else { + $results['files'][$k][$key] = $v; + } + } + } + } + + return $results; + } + + function parse_parameter( &$params, $parameter, $value ) { + if ( strpos($parameter, '[') !== FALSE ) { + $matches = array(); + if ( preg_match( '/^([^[]*)\[([^]]*)\](.*)$/', $parameter, $match ) ) { + $name = $match[1]; + $key = $match[2]; + $rem = $match[3]; + + if ( $name !== '' && $name !== NULL ) { + if ( ! isset($params[$name]) || ! is_array($params[$name]) ) { + $params[$name] = array(); + } else { + } + if ( strlen($rem) > 0 ) { + if ( $key === '' || $key === NULL ) { + $arr = array(); + $this->parse_parameter( $arr, $rem, $value ); + $params[$name][] = $arr; + } else { + if ( !isset($params[$name][$key]) || !is_array($params[$name][$key]) ) { + $params[$name][$key] = array(); + } + $this->parse_parameter( $params[$name][$key], $rem, $value ); + } + } else { + if ( $key === '' || $key === NULL ) { + $params[$name][] = $value; + } else { + $params[$name][$key] = $value; + } + } + } else { + if ( strlen($rem) > 0 ) { + if ( $key === '' || $key === NULL ) { + // REVIEW Is this logic correct?! + $this->parse_parameter( $params, $rem, $value ); + } else { + if ( ! isset($params[$key]) || ! is_array($params[$key]) ) { + $params[$key] = array(); + } + $this->parse_parameter( $params[$key], $rem, $value ); + } + } else { + if ( $key === '' || $key === NULL ) { + $params[] = $value; + } else { + $params[$key] = $value; + } + } + } + } else { + Log::warning( "ParseMultiPartFormDataInputStream.parse_parameter() Parameter name regex failed: '" . $parameter . "'" ); + } + } else { + $params[$parameter] = $value; + } + } +} \ No newline at end of file diff --git a/app/Http/routes.php b/app/Http/routes.php index 02848390..cab3303a 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -283,8 +283,8 @@ Route::group([ Route::get('', 'OAuth2SummitLocationsApiController@getLocations'); Route::post('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitLocationsApiController@addLocation']); - Route::get('metadata', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitLocationsApiController@getMetadata']); + Route::get('metadata', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitLocationsApiController@getMetadata']); Route::group(['prefix' => 'venues'], function () { Route::get('', 'OAuth2SummitLocationsApiController@getVenues'); @@ -344,6 +344,16 @@ Route::group([ Route::group(['prefix' => '{location_id}'], function () { Route::get('', 'OAuth2SummitLocationsApiController@getLocation'); + + // locations maps + Route::group(['prefix' => 'maps'], function () { + Route::post('', 'OAuth2SummitLocationsApiController@addLocationMap'); + Route::group(['prefix' => '{map_id}'], function () { + Route::put('', 'OAuth2SummitLocationsApiController@updateLocationMap'); + Route::delete('', 'OAuth2SummitLocationsApiController@deleteLocationMap'); + }); + }); + Route::put('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitLocationsApiController@updateLocation']); Route::delete('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitLocationsApiController@deleteLocation']); Route::get('/events/published','OAuth2SummitLocationsApiController@getLocationPublishedEvents')->where('location_id', 'tbd|[0-9]+'); diff --git a/app/ModelSerializers/Locations/SummitGeoLocatedLocationSerializer.php b/app/ModelSerializers/Locations/SummitGeoLocatedLocationSerializer.php index 20597b46..b64b3882 100644 --- a/app/ModelSerializers/Locations/SummitGeoLocatedLocationSerializer.php +++ b/app/ModelSerializers/Locations/SummitGeoLocatedLocationSerializer.php @@ -11,6 +11,7 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ +use models\summit\SummitGeoLocatedLocation; use ModelSerializers\SerializerRegistry; /** @@ -46,16 +47,17 @@ class SummitGeoLocatedLocationSerializer extends SummitAbstractLocationSerialize { $values = parent::serialize($expand, $fields, $relations); $location = $this->object; - - $maps = array(); + if(!$location instanceof SummitGeoLocatedLocation) return []; + // maps + $maps = []; foreach($location->getMaps() as $image) { if(!$image->hasPicture()) continue; $maps[] = SerializerRegistry::getInstance()->getSerializer($image)->serialize(); } $values['maps'] = $maps; - - $images = array(); + // images + $images = []; foreach($location->getImages() as $image) { if(!$image->hasPicture()) continue; diff --git a/app/ModelSerializers/Summit/SummitLocationBannerSerializer.php b/app/ModelSerializers/Summit/SummitLocationBannerSerializer.php index 21f9ad52..69758b26 100644 --- a/app/ModelSerializers/Summit/SummitLocationBannerSerializer.php +++ b/app/ModelSerializers/Summit/SummitLocationBannerSerializer.php @@ -12,9 +12,8 @@ * limitations under the License. **/ use App\Models\Foundation\Summit\Locations\Banners\SummitLocationBanner; +use ModelSerializers\SerializerRegistry; use ModelSerializers\SilverStripeSerializer; - - /** * Class SummitLocationBannerSerializer * @package App\ModelSerializers\Summit @@ -38,11 +37,25 @@ class SummitLocationBannerSerializer extends SilverStripeSerializer * @param array $params * @return array */ - public function serialize($expand = null, array $fields = array(), array $relations = array(), array $params = array() ) + public function serialize($expand = null, array $fields = [], array $relations = [], array $params = [] ) { $values = parent::serialize($expand, $fields, $relations, $params); $banner = $this->object; if(!$banner instanceof SummitLocationBanner) return []; + + if (!empty($expand)) { + foreach (explode(',', $expand) as $relation) { + switch (trim($relation)) { + case 'location': { + if($banner->hasLocation()){ + unset($values['location_id']); + $values['location'] = SerializerRegistry::getInstance()->getSerializer($banner->getLocation())->serialize(); + } + } + break; + } + } + } return $values; } } \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Factories/SummitLocationImageFactory.php b/app/Models/Foundation/Summit/Factories/SummitLocationImageFactory.php new file mode 100644 index 00000000..34bda75b --- /dev/null +++ b/app/Models/Foundation/Summit/Factories/SummitLocationImageFactory.php @@ -0,0 +1,55 @@ +setClassName(SummitLocationImage::TypeMap); + return self::populate($image, $data); + } + + /** + * @param array $data + * @return SummitLocationImage + */ + public static function buildImage(array $data){ + $image = new SummitLocationImage(); + $image->setClassName(SummitLocationImage::TypeImage); + return self::populate($image, $data); + } + + /** + * @param SummitLocationImage $image + * @param array $data + * @return SummitLocationImage + */ + public static function populate(SummitLocationImage $image, array $data){ + if(isset($data['name'])) + $image->setName(trim($data['name'])); + + if(isset($data['description'])) + $image->setName(trim($data['description'])); + + return $image; + } +} \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Locations/SummitGeoLocatedLocation.php b/app/Models/Foundation/Summit/Locations/SummitGeoLocatedLocation.php index 2a8832ea..07a47290 100644 --- a/app/Models/Foundation/Summit/Locations/SummitGeoLocatedLocation.php +++ b/app/Models/Foundation/Summit/Locations/SummitGeoLocatedLocation.php @@ -15,6 +15,8 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Criteria; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; use Doctrine\ORM\Mapping AS ORM; +use models\exceptions\ValidationException; + /** * @ORM\Entity * @ORM\Table(name="SummitGeoLocatedLocation") @@ -344,7 +346,7 @@ class SummitGeoLocatedLocation extends SummitAbstractLocation parent::__construct(); $this->details_page = false; $this->display_on_site = false; - $this->images = new ArrayCollection(); + $this->images = new ArrayCollection; } /** @@ -356,7 +358,7 @@ class SummitGeoLocatedLocation extends SummitAbstractLocation ->matching ( Criteria::create() - ->where(Criteria::expr()->eq("class_name", "SummitLocationMap")) + ->where(Criteria::expr()->eq("class_name", SummitLocationImage::TypeMap)) ->orderBy(array("order" => Criteria::ASC)) ); } @@ -371,7 +373,7 @@ class SummitGeoLocatedLocation extends SummitAbstractLocation ->matching ( Criteria::create() - ->where(Criteria::expr()->eq("class_name", "SummitLocationImage")) + ->where(Criteria::expr()->eq("class_name", SummitLocationImage::TypeImage)) ->orderBy(array("order" => Criteria::ASC)) ); } @@ -390,4 +392,63 @@ class SummitGeoLocatedLocation extends SummitAbstractLocation return $res === false ? null : $res; } + /** + * @param SummitLocationImage $map + */ + public function addMap(SummitLocationImage $map){ + $maps = $this->getMaps(); + $maps_count = count($maps); + $new_order = $maps_count > 0 ? $maps_count + 1 : 1; + $map->setOrder($new_order); + $this->images->add($map); + $map->setLocation($this); + } + + /** + * @param SummitLocationImage $image + */ + public function addImage(SummitLocationImage $image){ + $images = $this->getImages(); + $images_count = count($images); + $new_order = $images_count > 0 ? $images_count + 1 : 1; + $image->setOrder($new_order); + $this->images->add($image); + $image->setLocation($this); + } + + /** + * @param SummitLocationImage $map + * @param $new_order + * @throws ValidationException + */ + public function recalculateMapOrder(SummitLocationImage $map, $new_order){ + $maps = $this->getMaps(); + $maps = array_slice($maps,0, count($maps), false); + $max_order = count($maps); + $former_order = 1; + + foreach ($maps as $m){ + if($m->getId() == $map->getId()) break; + $former_order++; + } + + if($new_order > $max_order) + throw new ValidationException(sprintf("max order is %s", $max_order)); + + unset($maps[$former_order - 1]); + + $maps = array_merge + ( + array_slice($maps, 0, $new_order -1 , true) , + [$map] , + array_slice($maps, $new_order -1 , count($maps), true) + ); + + $order = 1; + foreach($maps as $m){ + $m->setOrder($order); + $order++; + } + } + } \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Locations/SummitLocationImage.php b/app/Models/Foundation/Summit/Locations/SummitLocationImage.php index 1cec7866..98ba3567 100644 --- a/app/Models/Foundation/Summit/Locations/SummitLocationImage.php +++ b/app/Models/Foundation/Summit/Locations/SummitLocationImage.php @@ -11,11 +11,9 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ - use models\main\File; use models\utils\SilverstripeBaseModel; use Doctrine\ORM\Mapping AS ORM; - /** * @ORM\Entity * @ORM\Table(name="SummitLocationImage") @@ -23,6 +21,42 @@ use Doctrine\ORM\Mapping AS ORM; */ class SummitLocationImage extends SilverstripeBaseModel { + const TypeMap = 'SummitLocationMap'; + const TypeImage = 'SummitLocationImage'; + /** + * @ORM\Column(name="Name", type="string") + */ + protected $name; + + /** + * @ORM\Column(name="Description", type="string") + */ + protected $description; + + /** + * @ORM\Column(name="Order", type="integer") + */ + protected $order; + + /** + * @ORM\Column(name="ClassName", type="string") + */ + protected $class_name; + + /** + * @ORM\ManyToOne(targetEntity="models\main\File", fetch="EAGER") + * @ORM\JoinColumn(name="PictureID", referencedColumnName="ID") + * @var File + */ + protected $picture; + + /** + * @ORM\ManyToOne(targetEntity="models\summit\SummitGeoLocatedLocation", inversedBy="images") + * @ORM\JoinColumn(name="LocationID", referencedColumnName="ID") + * @var SummitGeoLocatedLocation + */ + protected $location; + /** * @return string */ @@ -115,25 +149,6 @@ class SummitLocationImage extends SilverstripeBaseModel $this->location = $location; } - /** - * @ORM\Column(name="Name", type="string") - */ - protected $name; - - /** - * @ORM\Column(name="Description", type="string") - */ - protected $description; - - /** - * @ORM\Column(name="Order", type="integer") - */ - protected $order; - - /** - * @ORM\Column(name="ClassName", type="string") - */ - protected $class_name; /** * @return string @@ -151,20 +166,6 @@ class SummitLocationImage extends SilverstripeBaseModel $this->class_name = $class_name; } - /** - * @ORM\ManyToOne(targetEntity="models\main\File", fetch="EAGER") - * @ORM\JoinColumn(name="PictureID", referencedColumnName="ID") - * @var File - */ - protected $picture; - - /** - * @ORM\ManyToOne(targetEntity="models\summit\SummitGeoLocatedLocation", inversedBy="images") - * @ORM\JoinColumn(name="LocationID", referencedColumnName="ID") - * @var SummitGeoLocatedLocation - */ - protected $location; - /** * @return bool */ diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index c8aff1ad..51bd329e 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -21,6 +21,7 @@ use App\Factories\CalendarAdminActionSyncWorkRequest\SummitEventDeletedCalendarS use App\Factories\CalendarAdminActionSyncWorkRequest\SummitEventUpdatedCalendarSyncWorkRequestFactory; use App\Factories\EntityEvents\FloorActionEntityEventFactory; use App\Factories\EntityEvents\LocationActionEntityEventFactory; +use App\Factories\EntityEvents\LocationImageActionEntityEventFactory; use App\Factories\EntityEvents\MyFavoritesAddEntityEventFactory; use App\Factories\EntityEvents\MyFavoritesRemoveEntityEventFactory; use App\Factories\EntityEvents\MyScheduleAddEntityEventFactory; @@ -251,5 +252,22 @@ final class EventServiceProvider extends ServiceProvider EntityEventPersister::persist(FloorActionEntityEventFactory::build($event, 'DELETE')); }); + // location images + + Event::listen(\App\Events\LocationImageInserted::class, function($event) + { + EntityEventPersister::persist(LocationImageActionEntityEventFactory::build($event, 'INSERT')); + }); + + Event::listen(\App\Events\LocationImageUpdated::class, function($event) + { + EntityEventPersister::persist(LocationImageActionEntityEventFactory::build($event, 'UPDATE')); + }); + + Event::listen(\App\Events\LocationImageDeleted::class, function($event) + { + EntityEventPersister::persist(LocationImageActionEntityEventFactory::build($event, 'DELETE')); + }); + } } diff --git a/app/Services/Model/ILocationService.php b/app/Services/Model/ILocationService.php index 57ede255..2866f96b 100644 --- a/app/Services/Model/ILocationService.php +++ b/app/Services/Model/ILocationService.php @@ -12,12 +12,14 @@ * limitations under the License. **/ use App\Models\Foundation\Summit\Locations\Banners\SummitLocationBanner; +use models\summit\SummitLocationImage; use models\summit\SummitVenueRoom; use models\exceptions\EntityNotFoundException; use models\exceptions\ValidationException; use models\summit\Summit; use models\summit\SummitAbstractLocation; use models\summit\SummitVenueFloor; +use Illuminate\Http\UploadedFile; /** * Interface ILocationService * @package App\Services\Model @@ -145,4 +147,15 @@ interface ILocationService */ public function deleteLocationBanner(Summit $summit, $location_id, $banner_id); + /** + * @param Summit $summit + * @param int $location_id + * @param array $metadata + * @param $file + * @return SummitLocationImage + * @throws EntityNotFoundException + * @throws ValidationException + */ + public function addLocationMap(Summit $summit, $location_id, array $metadata, UploadedFile $file); + } \ No newline at end of file diff --git a/app/Services/Model/LocationService.php b/app/Services/Model/LocationService.php index 086c62ba..42bd5ef4 100644 --- a/app/Services/Model/LocationService.php +++ b/app/Services/Model/LocationService.php @@ -16,13 +16,16 @@ use App\Events\FloorDeleted; use App\Events\FloorInserted; use App\Events\FloorUpdated; use App\Events\LocationDeleted; +use App\Events\LocationImageInserted; use App\Events\LocationInserted; use App\Events\LocationUpdated; use App\Events\SummitVenueRoomDeleted; use App\Events\SummitVenueRoomInserted; use App\Events\SummitVenueRoomUpdated; +use App\Http\Utils\FileUploader; use App\Models\Foundation\Summit\Factories\SummitLocationBannerFactory; use App\Models\Foundation\Summit\Factories\SummitLocationFactory; +use App\Models\Foundation\Summit\Factories\SummitLocationImageFactory; use App\Models\Foundation\Summit\Factories\SummitVenueFloorFactory; use App\Models\Foundation\Summit\Locations\Banners\ScheduledSummitLocationBanner; use App\Models\Foundation\Summit\Locations\Banners\SummitLocationBanner; @@ -30,17 +33,21 @@ use App\Models\Foundation\Summit\Repositories\ISummitLocationRepository; use App\Services\Apis\GeoCodingApiException; use App\Services\Apis\IGeoCodingAPI; use App\Services\Model\Strategies\GeoLocation\GeoLocationStrategyFactory; +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; use libs\utils\ITransactionService; use models\exceptions\EntityNotFoundException; use models\exceptions\ValidationException; +use models\main\IFolderRepository; use models\summit\Summit; use models\summit\SummitAbstractLocation; use models\summit\SummitAirport; use models\summit\SummitExternalLocation; use models\summit\SummitGeoLocatedLocation; use models\summit\SummitHotel; +use models\summit\SummitLocationImage; use models\summit\SummitVenue; use models\summit\SummitVenueFloor; use models\summit\SummitVenueRoom; @@ -55,6 +62,11 @@ final class LocationService implements ILocationService */ private $location_repository; + /** + * @var IFolderRepository + */ + private $folder_repository; + /** * @var ITransactionService */ @@ -68,19 +80,22 @@ final class LocationService implements ILocationService /** * LocationService constructor. * @param ISummitLocationRepository $location_repository + * @param IFolderRepository $folder_repository * @param IGeoCodingAPI $geo_coding_api * @param ITransactionService $tx_service */ public function __construct ( ISummitLocationRepository $location_repository, + IFolderRepository $folder_repository, IGeoCodingAPI $geo_coding_api, ITransactionService $tx_service ) { $this->location_repository = $location_repository; - $this->geo_coding_api = $geo_coding_api; - $this->tx_service = $tx_service; + $this->geo_coding_api = $geo_coding_api; + $this->tx_service = $tx_service; + $this->folder_repository = $folder_repository; } /** @@ -1092,4 +1107,92 @@ final class LocationService implements ILocationService $location->removeBanner($banner); }); } + + /** + * @param Summit $summit + * @param int $location_id + * @param array $metadata + * @param $file + * @return SummitLocationImage + * @throws EntityNotFoundException + * @throws ValidationException + */ + public function addLocationMap(Summit $summit, $location_id, array $metadata, UploadedFile $file) + { + $map = $this->tx_service->transaction(function () use ($summit, $location_id, $metadata, $file) { + $max_file_size = config('file_upload.max_file_upload_size') ; + $allowed_extensions = ['png','jpg','jpeg','gif','pdf']; + $location = $summit->getLocation($location_id); + + if (is_null($location)) { + throw new EntityNotFoundException + ( + trans( + 'not_found_errors.LocationService.addLocationMap.LocationNotFound', + [ + 'location_id' => $location_id, + ] + ) + ); + } + + if(!$location instanceof SummitGeoLocatedLocation){ + throw new EntityNotFoundException + ( + trans( + 'not_found_errors.LocationService.addLocationMap.LocationNotFound', + [ + 'location_id' => $location_id, + ] + ) + ); + } + + if(!in_array($file->extension(), $allowed_extensions)){ + throw new ValidationException + ( + trans( + 'validation_errors.LocationService.addLocationMap.FileNotAllowedExtension', + [ + 'allowed_extensions' => implode(", ", $allowed_extensions), + ] + ) + ); + } + + if($file->getSize() > $max_file_size) + { + throw new ValidationException + ( + trans + ( + 'validation_errors.LocationService.addLocationMap.FileMaxSize', + [ + 'max_file_size' => (($max_file_size/1024)/1024) + ] + ) + ); + } + + $uploader = new FileUploader($this->folder_repository); + $pic = $uploader->build($file, sprintf('summits/%s/locations/%s/maps/', $location->getSummitId(), $location->getId()), true); + $map = SummitLocationImageFactory::buildMap($metadata); + $map->setPicture($pic); + $location->addMap($map); + return $map; + }); + + Event::fire + ( + new LocationImageInserted + ( + $map->getId(), + $map->getLocationId(), + $map->getLocation()->getSumitId(), + $map->getClassName() + ) + ); + + return $map; + } } \ No newline at end of file diff --git a/config/file_upload.php b/config/file_upload.php new file mode 100644 index 00000000..7f39d089 --- /dev/null +++ b/config/file_upload.php @@ -0,0 +1,17 @@ + env('FILE_UPLOAD_MAX_SIZE', 10485760), +]; \ No newline at end of file diff --git a/database/seeds/ApiEndpointsSeeder.php b/database/seeds/ApiEndpointsSeeder.php index a4101237..ac733a5c 100644 --- a/database/seeds/ApiEndpointsSeeder.php +++ b/database/seeds/ApiEndpointsSeeder.php @@ -509,6 +509,34 @@ class ApiEndpointsSeeder extends Seeder sprintf(SummitScopes::ReadAllSummitData, $current_realm) ], ], + // maps + [ + 'name' => 'add-location-map', + 'route' => '/api/v1/summits/{id}/locations/{location_id}/maps', + 'http_method' => 'POST', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm), + sprintf(SummitScopes::WriteLocationsData, $current_realm) + ], + ], + [ + 'name' => 'update-location-map', + 'route' => '/api/v1/summits/{id}/locations/{location_id}/maps/{map_id}', + 'http_method' => 'PUT', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm), + sprintf(SummitScopes::WriteLocationsData, $current_realm) + ], + ], + [ + 'name' => 'delete-location-map', + 'route' => '/api/v1/summits/{id}/locations/{location_id}/maps/{map_id}', + 'http_method' => 'DELETE', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm), + sprintf(SummitScopes::WriteLocationsData, $current_realm) + ], + ], // banners [ 'name' => 'get-location-banners', diff --git a/resources/lang/en/not_found_errors.php b/resources/lang/en/not_found_errors.php index c56727ce..ef99bef9 100644 --- a/resources/lang/en/not_found_errors.php +++ b/resources/lang/en/not_found_errors.php @@ -31,4 +31,5 @@ return [ 'LocationService.deleteLocationBanner.BannerNotFound'=> 'banner :banner_id not found on location :location_id', 'LocationService.updateLocationBanner.LocationNotFound' => 'location :location_id not found on summit :summit_id', 'LocationService.updateLocationBanner.BannerNotFound'=> 'banner :banner_id not found on location :location_id', + 'LocationService.addLocationMap.LocationNotFound' => 'location :location_id not found on summit :summit_id', ]; \ No newline at end of file diff --git a/resources/lang/en/validation_errors.php b/resources/lang/en/validation_errors.php index dbd9a680..e858ae3d 100644 --- a/resources/lang/en/validation_errors.php +++ b/resources/lang/en/validation_errors.php @@ -43,4 +43,6 @@ return [ 'LocationService.addVenueRoom.LocationNameAlreadyExists' => 'there is already another location with same name for summit :summit_id', 'LocationService.updateVenueRoom.LocationNameAlreadyExists' => 'there is already another location with same name for summit :summit_id', 'LocationService.addLocationBanner.InvalidClassName' => 'invalid class name', + 'LocationService.addLocationMap.FileNotAllowedExtension' => 'file extension is not allowed (:allowed_extensions)', + 'LocationService.addLocationMap.FileMaxSize' => 'file exceeds max_file_size (:max_file_size MB)', ]; \ No newline at end of file diff --git a/tests/OAuth2SummitLocationsApiTest.php b/tests/OAuth2SummitLocationsApiTest.php index b73af266..c2e700a0 100644 --- a/tests/OAuth2SummitLocationsApiTest.php +++ b/tests/OAuth2SummitLocationsApiTest.php @@ -50,6 +50,39 @@ final class OAuth2SummitLocationsApiTest extends ProtectedApiTest $this->assertTrue(!is_null($locations)); } + public function testGetSummitLocationsOrderByName($summit_id = 22) + { + $params = [ + 'id' => $summit_id, + 'page' => 1, + 'per_page' => 5, + 'order' => 'name-' + ]; + + $headers = + [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action + ( + "GET", + "OAuth2SummitLocationsApiController@getLocations", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + + $locations = json_decode($content); + $this->assertTrue(!is_null($locations)); + } + public function testGetCurrentSummitLocationsMetadata($summit_id = 23) { $params = [