From e7ad87e640176bbf5d8efaba54160f77bdc64ebc Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 25 Jun 2026 10:08:51 +0200 Subject: [PATCH 1/2] feat: add keyman-version check to package-version API Add the `keyman-version` parameter to /package-version API, for example: http://api.keyman.com/package-version?platform=android&keyboard=sil_euro_latin&model=nrc.en.mtnt&keyman-version=11.0.0 This also makes a small tweak to the results, so that an error response will no longer give a `version` or `kmp` field. (Reason: the iOS app sees the presence of those two fields as indication of a success response and never checks for error in that case. Yes, we will address that, but we need to support existing versions of the app anyway.) And refactor the package-version.inc.php module to make it clearer and simpler. Relates-to: #325 Test-bot: skip --- .../package-version/package-version.inc.php | 137 ++++++++++-------- script/package-version/package-version.php | 40 +++-- tests/PackageVersionTest.php | 4 +- 3 files changed, 108 insertions(+), 73 deletions(-) diff --git a/script/package-version/package-version.inc.php b/script/package-version/package-version.inc.php index afb51fa..ee37875 100644 --- a/script/package-version/package-version.inc.php +++ b/script/package-version/package-version.inc.php @@ -9,81 +9,102 @@ class PackageVersion { - static function available_platforms() - { + private $mssql; + + static function available_platforms() { return ['android', 'ios', 'linux', 'mac', 'web', 'windows']; } - function execute($mssql, $params, $platform) - { - // TODO: params should be expanded to keyboards, models + function __construct($mssql) { + $this->mssql = $mssql; + } + function execute($keyboards, $models, $platform, $keymanVersion) { // Prepare results $json = []; - if (isset($params['keyboard'])) { + if (count($keyboards) > 0) { $json['keyboards'] = []; - foreach ($params['keyboard'] as $keyboard) { - $stmt = $mssql->prepare( - 'SELECT - k.version, k.package_filename, - k.platform_android, k.platform_linux, k.platform_macos, k.platform_ios, k.platform_web, k.platform_windows, - kr.keyboard_id deprecated_by_keyboard_id - FROM - t_keyboard k LEFT JOIN - t_keyboard_related kr ON k.keyboard_id = kr.related_keyboard_id AND kr.deprecates = 1 - WHERE - k.keyboard_id = ?' - ); - $stmt->bindParam(1, $keyboard); - $stmt->execute(); - $data = $stmt->fetchAll(); - if (count($data) == 0) { - $json["keyboards"][$keyboard] = ['error' => 'not found']; - } else { - - $json["keyboards"][$keyboard] = [ - 'version' => $data[0][0] - ]; - - if (!empty($platform) && !$data[0][array_search($platform, PackageVersion::available_platforms()) + 2]) { - $json["keyboards"][$keyboard]['error'] = 'not available for platform'; - } - - if(!empty($data[0][1])) { - $json["keyboards"][$keyboard]['kmp'] = KeymanUrls::keyboard_download_url($keyboard, $data[0][0], $data[0][1]); - } else { - $json["keyboards"][$keyboard]['error'] = 'not available as package'; - } - - if(!empty($data[0]['deprecated_by_keyboard_id'])) { - $json["keyboards"][$keyboard]['deprecatedBy'] = $data[0]['deprecated_by_keyboard_id']; - } - } + foreach ($keyboards as $keyboard) { + $json["keyboards"][$keyboard] = $this->getKeyboard($keyboard, $platform, $keymanVersion); } } - if (isset($params['model'])) { + if (count($models) > 0) { $json['models'] = []; - foreach ($params['model'] as $model) { - $stmt = $mssql->prepare('SELECT version, package_filename FROM t_model WHERE model_id = ?'); - $stmt->bindParam(1, $model); - $stmt->execute(); - $data = $stmt->fetchAll(); - if (count($data) == 0) { - $json["models"][$model] = ['error' => 'not found']; - } else { - // Note: we don't currently test platform for models - $json["models"][$model] = [ - 'version' => $data[0][0], - 'kmp' => KeymanUrls::model_download_url($model, $data[0][0], $data[0][1]) - ]; - } + foreach ($models as $model) { + $json["models"][$model] = $this->getModel($model, $platform, $keymanVersion); } } + return $json; } + + function getKeyboard($keyboard, $platform, $keymanVersion) { + $stmt = $this->mssql->prepare( + 'SELECT + k.version, k.package_filename, + k.platform_android, k.platform_linux, k.platform_macos, k.platform_ios, k.platform_web, k.platform_windows, + kr.keyboard_id deprecated_by_keyboard_id, k.min_keyman_version + FROM + t_keyboard k LEFT JOIN + t_keyboard_related kr ON k.keyboard_id = kr.related_keyboard_id AND kr.deprecates = 1 + WHERE + k.keyboard_id = ?' + ); + $stmt->bindParam(1, $keyboard); + $stmt->execute(); + $data = $stmt->fetchAll(); + + $jsonKeyboard = []; + + if (count($data) == 0) { + $jsonKeyboard['error'] = 'not found'; + } else { + $dataKeyboard = $data[0]; + + if (!empty($platform) && !$dataKeyboard[array_search($platform, PackageVersion::available_platforms()) + 2]) { + $jsonKeyboard['error'] = "Not available for platform $platform"; + } + else if(!empty($keymanVersion) && version_compare($keymanVersion, $dataKeyboard['min_keyman_version'], '<')) { + $jsonKeyboard['error'] = "Keyman version {$dataKeyboard['min_keyman_version']}+ required"; + } + else if(empty($dataKeyboard['package_filename'])) { + $jsonKeyboard['error'] = 'not available as package'; + } else { + $jsonKeyboard['version'] = $dataKeyboard['version']; + $jsonKeyboard['kmp'] = KeymanUrls::keyboard_download_url($keyboard, $dataKeyboard['version'], $dataKeyboard['package_filename']); + if(!empty($dataKeyboard['deprecated_by_keyboard_id'])) { + $jsonKeyboard['deprecatedBy'] = $dataKeyboard['deprecated_by_keyboard_id']; + } + } + } + return $jsonKeyboard; + } + + function getModel($model, $platform, $keymanVersion) { + $stmt = $this->mssql->prepare('SELECT version, package_filename, min_keyman_version FROM t_model WHERE model_id = ?'); + $stmt->bindParam(1, $model); + $stmt->execute(); + $data = $stmt->fetchAll(); + + $jsonModel = []; + + if (count($data) == 0) { + $jsonModel["error"] = 'not found'; + } else { + $dataModel = $data[0]; + // Note: we don't currently test platform for models + if(!empty($keymanVersion) && version_compare($keymanVersion, $dataModel['min_keyman_version'], '<')) { + $jsonModel['error'] = "Keyman version {$dataModel['min_keyman_version']}+ required"; + } else { + $jsonModel['version'] = $dataModel['version']; + $jsonModel['kmp'] = KeymanUrls::model_download_url($model, $dataModel['version'], $dataModel['package_filename']); + } + } + return $jsonModel; + } } diff --git a/script/package-version/package-version.php b/script/package-version/package-version.php index 1086e1e..56936a6 100644 --- a/script/package-version/package-version.php +++ b/script/package-version/package-version.php @@ -8,14 +8,16 @@ * * https://api.keyman.com/schemas/package-version.json is JSON schema for valid responses * - * @param keyboard Optional. keyboard id, can be repeated (either with comma or repeated param). - * @param model Optional. model id, can be repeated (either with comma or repeated param). - * @param platform Optional. Filter by platform support for keyboards: - * android, ios, linux, mac, [web], windows (web does not currently support .kmp) - * This stops the API returning keyboard packages that are invalid for the target - * platform. If not supplied, does not filter by platform support. - * @return JSON blob or HTTP/400 (with JSON error) on invalid parameters - * The valid blob will contain latest version and url for the keyboards/lexical models. + * @param keyboard Optional. keyboard id, can be repeated (either with comma or repeated param). + * @param model Optional. model id, can be repeated (either with comma or repeated param). + * @param platform Optional. Filter by platform support for keyboards: + * android, ios, linux, mac, [web], windows (web does not currently support .kmp) + * This stops the API returning keyboard packages that are invalid for the target + * platform. If not supplied, does not filter by platform support. + * @param keyman-version Optional. Version of Keyman requesting the keyboard, will filter out keyboards + * that depend on a newer version of Keyman. + * @return JSON blob or HTTP/400 (with JSON error) on invalid parameters + * The valid blob will contain latest version and url for the keyboards/lexical models. */ require_once('../../tools/util.php'); @@ -39,22 +41,34 @@ $available_platforms = PackageVersion::available_platforms(); foreach($params as $param => $value) { - if(!in_array($param, ['keyboard', 'model', 'platform'])) { - fail("Invalid parameter $param"); + if(!in_array($param, ['keyboard', 'model', 'platform', 'keyman-version'])) { + fail("Unrecognized parameter '$param'"); } } if(isset($params['platform'])) { $platform = $params['platform']; if(!in_array($platform, $available_platforms)) { - fail("Invalid platform $platform"); + fail("Invalid platform' $platform'"); } } else $platform = null; + // Prepare results - $PackageVersion = new Keyman\Site\com\keyman\api\PackageVersion(); - $json = $PackageVersion->execute($mssql, $params, $platform); + if(isset($params['keyman-version'])) { + $keymanVersion = $params['keyman-version']; + if(!preg_match('/^(\d+)\.(\d+)\.(\d+)$/', $keymanVersion)) { + fail("Invalid keyman-version '$keymanVersion', expected a.b.c format"); + } + } + else $keymanVersion = null; + + $keyboards = isset($params['keyboard']) ? $params['keyboard'] : []; + $models = isset($params['model']) ? $params['model'] : []; + + $PackageVersion = new Keyman\Site\com\keyman\api\PackageVersion($mssql); + $json = $PackageVersion->execute($keyboards, $models, $platform, $keymanVersion); echo json_encode($json, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); diff --git a/tests/PackageVersionTest.php b/tests/PackageVersionTest.php index 59b299d..455a24c 100644 --- a/tests/PackageVersionTest.php +++ b/tests/PackageVersionTest.php @@ -25,8 +25,8 @@ public function testSimpleResultValidatesAgainstSchema(): void $schema = TestUtils::LoadJSONSchema(PackageVersionTest::SchemaFilename); $mssql = \Keyman\Site\com\keyman\api\Tools\DB\DBConnect::Connect(); - $pv = new \Keyman\Site\com\keyman\api\PackageVersion(); - $json = $pv->execute($mssql, [ 'keyboard' => ['khmer_angkor', 'bar', 'us', 'european2'], ['model' => 'zoo','nrc.en.mtnt'] ], 'windows'); + $pv = new \Keyman\Site\com\keyman\api\PackageVersion($mssql); + $json = $pv->execute(['khmer_angkor', 'bar', 'us', 'european2'], ['zoo','nrc.en.mtnt'], 'windows', '1.0'); // TODO(lowpri): find a way to skip this by emitting clean JSON object from execute() $json = json_decode(json_encode($json)); From 1c61e7c92a798fdc6c2c58b540bdbc634686f210 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 29 Jun 2026 16:48:29 +0200 Subject: [PATCH 2/2] chore: address review comments Co-authored-by: Eberhard Beilharz --- script/package-version/package-version.inc.php | 2 +- script/package-version/package-version.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/script/package-version/package-version.inc.php b/script/package-version/package-version.inc.php index ee37875..4ae7f1b 100644 --- a/script/package-version/package-version.inc.php +++ b/script/package-version/package-version.inc.php @@ -47,7 +47,7 @@ function getKeyboard($keyboard, $platform, $keymanVersion) { $stmt = $this->mssql->prepare( 'SELECT k.version, k.package_filename, - k.platform_android, k.platform_linux, k.platform_macos, k.platform_ios, k.platform_web, k.platform_windows, + k.platform_android, k.platform_ios, k.platform_linux, k.platform_macos, k.platform_web, k.platform_windows, kr.keyboard_id deprecated_by_keyboard_id, k.min_keyman_version FROM t_keyboard k LEFT JOIN diff --git a/script/package-version/package-version.php b/script/package-version/package-version.php index 56936a6..2313247 100644 --- a/script/package-version/package-version.php +++ b/script/package-version/package-version.php @@ -49,7 +49,7 @@ if(isset($params['platform'])) { $platform = $params['platform']; if(!in_array($platform, $available_platforms)) { - fail("Invalid platform' $platform'"); + fail("Invalid platform '$platform'"); } } else $platform = null;