diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Platforms/Web/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Platforms/Web/Create.php index 3d8c98799a..293b655aef 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/Platforms/Web/Create.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Platforms/Web/Create.php @@ -10,6 +10,7 @@ use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\CustomId; +use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; @@ -20,7 +21,12 @@ use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Hostname; use Utopia\Validator\Text; +use Utopia\Validator\WhiteList; +/** + * WARNING: This kind of platform has most complex action, because it holds backwards compatibility too. + * If possible, refer to any other type of platform for APIs, for more simpler endpoint. + */ class Create extends Base { use HTTP; @@ -35,6 +41,7 @@ class Create extends Base $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) ->setHttpPath('/v1/project/platforms/web') + ->httpAlias('/v1/projects/:projectId/platforms') ->desc('Create project web platform') ->groups(['api', 'project']) ->label('scope', 'project.write') @@ -58,7 +65,9 @@ class Create extends Base )) ->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform']) ->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.') - ->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.') + ->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', true) // Optional for backwards compatibility + ->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', true) // Exists for backwards compatibility + ->inject('request') ->inject('response') ->inject('queueForEvents') ->inject('project') @@ -71,12 +80,65 @@ class Create extends Base string $platformId, string $name, string $hostname, + Request $request, Response $response, QueueEvent $queueForEvents, Document $project, Database $dbForPlatform, Authorization $authorization, ) { + $type = Platform::TYPE_WEB; + $key = ''; // App platform attribute + + // Backwards compatibility + // Used to have: type, name, key, hostname + if(!empty($request->getParam('type', ''))) { + // Validate deprecated type, and rename to new type + $deprecatedtypeMapping = [ + // Web + 'web' => Platform::TYPE_WEB, + 'flutter-web' => Platform::TYPE_WEB, + 'unity' => Platform::TYPE_WEB, // Was not officially supported anyway + + // Apple + 'flutter-macos' => Platform::TYPE_APPLE, + 'flutter-ios' => Platform::TYPE_APPLE, + 'react-native-ios' => Platform::TYPE_APPLE, + 'apple-ios' => Platform::TYPE_APPLE, + 'apple-macos' => Platform::TYPE_APPLE, + 'apple-watchos' => Platform::TYPE_APPLE, + 'apple-tvos' => Platform::TYPE_APPLE, + + // Android + 'flutter-android' => Platform::TYPE_ANDROID, + 'android' => Platform::TYPE_ANDROID, + 'react-native-android' => Platform::TYPE_ANDROID, + + 'flutter-windows' => Platform::TYPE_WINDOWS, + ]; + + $typeValidator = new WhiteList(\array_keys($deprecatedtypeMapping)); + if(!$typeValidator->isValid($request->getParam('type', ''))) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "type" is invalid: ' . $typeValidator->getDescription()); + } + + $type = $deprecatedtypeMapping[$request->getParam('type', '')] ?? Platform::TYPE_WEB; + + // Validate deprecated app id (key) + if (!empty($request->getParam('key', ''))) { + $keyValidator = new Text(256); + if(!$keyValidator->isValid($request->getParam('key', ''))) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "key" is invalid: ' . $keyValidator->getDescription()); + } + $key = $request->getParam('key', ''); + } + } else { + // Modern request, validate hostname + if (empty($hostname)) { + throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "hostname" is not optional.'); + } + } + $platformId = ($platformId == 'unique()') ? ID::unique() : $platformId; $platform = new Document([ @@ -84,9 +146,9 @@ class Create extends Base '$permissions' => [], 'projectInternalId' => $project->getSequence(), 'projectId' => $project->getId(), - 'type' => Platform::TYPE_WEB, + 'type' => $type, 'name' => $name, - 'key' => '', // App platform attribute + 'key' => $key, 'hostname' => $hostname ]); diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 9bb28feacf..04454a7ab3 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -3980,7 +3980,7 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertEquals($platformFultteriOSId, $response['body']['$id']); - $this->assertEquals('flutter-ios', $response['body']['type']); + $this->assertEquals('apple', $response['body']['type']); $this->assertEquals('Flutter App (iOS)', $response['body']['name']); $this->assertEquals('com.example.ios', $response['body']['key']); $this->assertEquals('', $response['body']['store']); @@ -3997,7 +3997,7 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertEquals($platformFultterAndroidId, $response['body']['$id']); - $this->assertEquals('flutter-android', $response['body']['type']); + $this->assertEquals('android', $response['body']['type']); $this->assertEquals('Flutter App (Android)', $response['body']['name']); $this->assertEquals('com.example.android', $response['body']['key']); $this->assertEquals('', $response['body']['store']); @@ -4014,7 +4014,7 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertEquals($platformFultterWebId, $response['body']['$id']); - $this->assertEquals('flutter-web', $response['body']['type']); + $this->assertEquals('web', $response['body']['type']); $this->assertEquals('Flutter App (Web)', $response['body']['name']); $this->assertEquals('', $response['body']['key']); $this->assertEquals('', $response['body']['store']); @@ -4031,7 +4031,7 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertEquals($platformAppleIosId, $response['body']['$id']); - $this->assertEquals('apple-ios', $response['body']['type']); + $this->assertEquals('apple', $response['body']['type']); $this->assertEquals('iOS App', $response['body']['name']); $this->assertEquals('com.example.ios', $response['body']['key']); $this->assertEquals('', $response['body']['store']); @@ -4048,7 +4048,7 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertEquals($platformAppleMacOsId, $response['body']['$id']); - $this->assertEquals('apple-macos', $response['body']['type']); + $this->assertEquals('apple', $response['body']['type']); $this->assertEquals('macOS App', $response['body']['name']); $this->assertEquals('com.example.macos', $response['body']['key']); $this->assertEquals('', $response['body']['store']); @@ -4065,7 +4065,7 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertEquals($platformAppleWatchOsId, $response['body']['$id']); - $this->assertEquals('apple-watchos', $response['body']['type']); + $this->assertEquals('apple', $response['body']['type']); $this->assertEquals('watchOS App', $response['body']['name']); $this->assertEquals('com.example.watchos', $response['body']['key']); $this->assertEquals('', $response['body']['store']); @@ -4082,7 +4082,7 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertEquals($platformAppleTvOsId, $response['body']['$id']); - $this->assertEquals('apple-tvos', $response['body']['type']); + $this->assertEquals('apple', $response['body']['type']); $this->assertEquals('tvOS App', $response['body']['name']); $this->assertEquals('com.example.tvos', $response['body']['key']); $this->assertEquals('', $response['body']['store']);