Skip to content

Commit 4f5138f

Browse files
Merge pull request #326 from keymanapp/auto/A19S20-merge-master-into-staging
auto: A19S20 merge master into staging
2 parents e4f36e4 + 5e8b571 commit 4f5138f

15 files changed

Lines changed: 534 additions & 32 deletions

File tree

.htaccess

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ RewriteRule "^keyboard/(.*)$" "/script/keyboard/keyboard.php?id=$1" [END]
3232

3333
RewriteRule "^increment-download/(.*)$" "/script/increment-download/increment-download.php?id=$1" [END]
3434

35+
#### Rewrites for /script folder: /app-downloads-increment
36+
37+
RewriteRule "^app-downloads-increment/([^/]+)/([^/]+)/(.*)$" "/script/app-downloads-increment/app-downloads-increment.php?product=$1&version=$2&tier=$3" [END]
38+
3539
#### Rewrites for /script folder: /model
3640

3741
RewriteRule "^model(/)?$" "/script/model-search/model-search.php" [END]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$ref": "#/definitions/app-downloads-increment",
4+
5+
"definitions": {
6+
"app-downloads-increment": {
7+
"type": "object",
8+
"required": [
9+
"product",
10+
"version",
11+
"tier",
12+
"count"
13+
],
14+
"additionalProperties": true,
15+
"properties": {
16+
"product": { "type": "string" },
17+
"version": { "type": "string" },
18+
"tier": { "type": "string" },
19+
"count": { "type": "integer" }
20+
}
21+
}
22+
}
23+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
{
2+
"msi": {
3+
"name": "Keyman for Windows MSI installer",
4+
"version": "18.0.245",
5+
"date": "2025-12-03",
6+
"platform": "win",
7+
"stability": "stable",
8+
"file": "keymandesktop.msi",
9+
"md5": "98EABC9C8F4803B11C5EF2EF64F0D802",
10+
"type": "msi",
11+
"build": "245",
12+
"size": 110870528,
13+
"url": "https://downloads.keyman.com/windows/stable/18.0.245/keymandesktop.msi"
14+
},
15+
"setup": {
16+
"name": "Keyman for Windows setup bootstrap",
17+
"version": "18.0.245",
18+
"date": "2025-12-03",
19+
"platform": "win",
20+
"stability": "stable",
21+
"file": "setup.exe",
22+
"md5": "C32FD8926909F1E447BBC72AF8B16380",
23+
"type": "exe",
24+
"build": "245",
25+
"size": 4032904,
26+
"url": "https://downloads.keyman.com/windows/stable/18.0.245/setup.exe"
27+
},
28+
"bundle": {
29+
"name": "Keyman for Windows",
30+
"version": "18.0.245",
31+
"date": "2025-12-03",
32+
"platform": "win",
33+
"stability": "stable",
34+
"file": "keyman-18.0.245.exe",
35+
"md5": "2BE0290AF6DF49BA736CEDCE50D1E763",
36+
"type": "exe",
37+
"build": "245",
38+
"size": 111668424,
39+
"url": "https://downloads.keyman.com/windows/stable/18.0.245/keyman-18.0.245.exe"
40+
},
41+
"keyboards": {
42+
"khmer_angkor": {
43+
"id": "khmer_angkor",
44+
"name": "Khmer Angkor",
45+
"license": "mit",
46+
"authorName": "Makara Sok",
47+
"authorEmail": "makara_sok@sil.org",
48+
"description": "<p>Khmer Unicode keyboard layout based on the NiDA keyboard layout.\nAutomatically corrects many common keying errors.</p>",
49+
"languages": {
50+
"km": {
51+
"examples": [
52+
{
53+
"keys": "x j m E r",
54+
"note": "Name of language",
55+
"text": "\u1781\u17d2\u1798\u17c2\u179a"
56+
}
57+
],
58+
"font": {
59+
"family": "Busra",
60+
"source": [
61+
"Busra-Regular.ttf"
62+
]
63+
},
64+
"oskFont": {
65+
"family": "KbdKhmr",
66+
"source": [
67+
"KbdKhmr.ttf"
68+
]
69+
},
70+
"languageName": "Khmer",
71+
"displayName": "Khmer"
72+
}
73+
},
74+
"lastModifiedDate": "2025-12-16T08:51:56.000Z",
75+
"packageFilename": "khmer_angkor.kmp",
76+
"packageFileSize": 2541924,
77+
"jsFilename": "khmer_angkor.js",
78+
"jsFileSize": 74154,
79+
"packageIncludes": [
80+
"visualKeyboard",
81+
"welcome",
82+
"documentation",
83+
"fonts"
84+
],
85+
"version": "2.4",
86+
"encodings": [
87+
"unicode"
88+
],
89+
"platformSupport": {
90+
"windows": "full",
91+
"macos": "full",
92+
"linux": "full",
93+
"desktopWeb": "full",
94+
"ios": "full",
95+
"android": "full",
96+
"mobileWeb": "full"
97+
},
98+
"minKeymanVersion": "10.0",
99+
"sourcePath": "release/k/khmer_angkor",
100+
"helpLink": "https://help.keyman.com/keyboard/khmer_angkor",
101+
"related": {
102+
"khmer10": {
103+
"deprecates": true
104+
},
105+
"basic_kbdkni": {
106+
"deprecates": false
107+
}
108+
},
109+
"url": "https://keyman.com/go/package/download/khmer_angkor?version=2.4&platform=windows&tier=stable&update=0"
110+
},
111+
"sil_ipa": {
112+
"id": "sil_ipa",
113+
"name": "IPA (SIL)",
114+
"license": "mit",
115+
"authorName": "Martin Hosken, Lorna Evans",
116+
"authorEmail": "fonts@sil.org",
117+
"description": "<p>The keyboard layout is described in terms of an IPA chart rather than a\nkeyboard. This is because many base characters are typed as a sequence\nof a letter followed by one of &lt;, &gt; or = which are characters used to\nchange a base character to another base character. Diacritics are typed\nas sequences of an appropriate key.</p>",
118+
"languages": {
119+
"und-latn": {
120+
"examples": [],
121+
"font": {
122+
"family": "Charis",
123+
"source": [
124+
"Charis-Regular.ttf"
125+
]
126+
},
127+
"oskFont": {
128+
"family": "Charis",
129+
"source": [
130+
"Charis-Regular.ttf"
131+
]
132+
},
133+
"languageName": "Undetermined",
134+
"scriptName": "Latin",
135+
"displayName": "Undetermined (Latin)"
136+
}
137+
},
138+
"lastModifiedDate": "2025-06-09T21:19:30.000Z",
139+
"packageFilename": "sil_ipa.kmp",
140+
"packageFileSize": 3626354,
141+
"jsFilename": "sil_ipa.js",
142+
"jsFileSize": 88661,
143+
"packageIncludes": [
144+
"documentation",
145+
"welcome",
146+
"fonts"
147+
],
148+
"version": "2.0.2",
149+
"encodings": [
150+
"unicode"
151+
],
152+
"platformSupport": {
153+
"windows": "full",
154+
"macos": "full",
155+
"linux": "full",
156+
"desktopWeb": "full",
157+
"ios": "full",
158+
"android": "full",
159+
"mobileWeb": "full"
160+
},
161+
"minKeymanVersion": "17.0",
162+
"sourcePath": "release/sil/sil_ipa",
163+
"helpLink": "https://help.keyman.com/keyboard/sil_ipa",
164+
"related": {
165+
"ipauni11": {
166+
"deprecates": true
167+
},
168+
"ipauni111": {
169+
"deprecates": true
170+
},
171+
"ipa93_km5": {
172+
"deprecates": false
173+
}
174+
},
175+
"url": "https://keyman.com/go/package/download/sil_ipa?version=2.0.2&platform=windows&tier=stable&update=0"
176+
}
177+
}
178+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
namespace Keyman\Site\com\keyman\api;
3+
4+
require_once(__DIR__ . '/../../tools/util.php');
5+
6+
class AppDownloads {
7+
static function increment($mssql, $product, $version, $tier) {
8+
9+
$stmt = $mssql->prepare('EXEC sp_app_downloads_increment :product, :version, :tier');
10+
$stmt->bindParam(":product", $product);
11+
$stmt->bindParam(":version", $version);
12+
$stmt->bindParam(":tier", $tier);
13+
$stmt->execute();
14+
$data = $stmt->fetchAll();
15+
if(count($data) == 0) {
16+
return NULL;
17+
}
18+
19+
$obj = [
20+
'product' => $data[0]['product'],
21+
'version' => $data[0]['version'],
22+
'tier' => $data[0]['tier'],
23+
'count' => intval($data[0]['count'])
24+
];
25+
26+
return $obj;
27+
}
28+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
require_once(__DIR__ . '/../../tools/util.php');
3+
4+
allow_cors();
5+
json_response();
6+
7+
require_once(__DIR__ . '/app-downloads-increment.inc.php');
8+
require_once(__DIR__ . '/../../tools/db/db.php');
9+
require_once __DIR__ . '/../../tools/autoload.php';
10+
11+
use Keyman\Site\Common\KeymanHosts;
12+
13+
$mssql = Keyman\Site\com\keyman\api\Tools\DB\DBConnect::Connect();
14+
$env = getenv();
15+
16+
header('Link: <' . KeymanHosts::Instance()->api_keyman_com .'/schemas/app-downloads-increment/1.0/app-downloads-increment.json#>; rel="describedby"');
17+
18+
$AllowGet = isset($_REQUEST['debug']);
19+
20+
if(!$AllowGet && $_SERVER['REQUEST_METHOD'] != 'POST') {
21+
fail('POST required');
22+
}
23+
24+
if(!isset($_REQUEST['key'])) {
25+
fail('key parameter must be set');
26+
}
27+
28+
// Note: we don't currently unit-test this one
29+
if(KeymanHosts::Instance()->Tier() === KeymanHosts::TIER_DEVELOPMENT)
30+
$key = 'local';
31+
else
32+
$key = $env['API_KEYMAN_COM_INCREMENT_DOWNLOAD_KEY'];
33+
34+
if($_REQUEST['key'] !== $key) {
35+
fail('Invalid key');
36+
}
37+
38+
if(!isset($_REQUEST['product']) ||
39+
!isset($_REQUEST['version']) ||
40+
!isset($_REQUEST['tier'])
41+
) {
42+
// We don't constrain what the product / version / tier may be here, because
43+
// we may add other products in the future
44+
fail('product, version, tier parameters must be set');
45+
}
46+
47+
$product = $_REQUEST['product'];
48+
$version = $_REQUEST['version'];
49+
$tier = $_REQUEST['tier'];
50+
51+
/**
52+
* POST https://api.keyman.com/app-downloads-increment/product/version/tier
53+
*
54+
* Increments the download counter for a single product identified by
55+
* `product`, `version`, and `tier`. Returns the new total count for the
56+
* product/version/tier for the day
57+
*
58+
* https://api.keyman.com/schemas/app-downloads-increment.json is JSON schema
59+
*
60+
* @param product the name of the product to increment ( "android", "ios",
61+
* "linux", "macos", "web", "windows", "developer"...)
62+
* @param version the version number ("1.2.3")
63+
* @param tier the tier of the product ("alpha", "beta", "stable")
64+
* @param key internal key to allow endpoint to run
65+
*/
66+
67+
$json = \Keyman\Site\com\keyman\api\AppDownloads::increment($mssql, $product, $version, $tier);
68+
if($json === NULL) {
69+
fail("Failed to increment stat, invalid parameters [$product, $version, $tier]?", 401);
70+
}
71+
72+
echo json_encode($json, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);

script/statistics/annual-statistics.inc.php

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,48 @@
77

88
namespace Keyman\Site\com\keyman\api;
99

10-
// strip out repeated columns with numeric keys (by default the results returned
11-
// give each column twice, once with a column name, and once with a column index)
12-
function filter_columns_by_name($data) {
13-
$result = [];
14-
foreach($data as $row) {
15-
$r = [];
16-
foreach($row as $id => $val) {
17-
if(!is_numeric($id)) {
18-
$r[$id] = intval($val);
19-
}
20-
}
21-
array_push($result, $r);
22-
}
23-
return $result;
24-
}
25-
2610
class AnnualStatistics {
2711

2812
function execute($mssql, $startDate, $endDate) {
13+
return $this->_execute('sp_annual_statistics', $mssql, $startDate, $endDate);
14+
}
2915

30-
$stmt = $mssql->prepare('EXEC sp_annual_statistics :prmStartDate, :prmEndDate');
16+
function executeDownloadsByMonth($mssql, $startDate, $endDate) {
17+
return $this->_execute('sp_keyboard_downloads_by_month_statistics', $mssql, $startDate, $endDate);
18+
}
3119

32-
$stmt->bindParam(":prmStartDate", $startDate);
33-
$stmt->bindParam(":prmEndDate", $endDate);
20+
function executeKeyboards($mssql, $startDate, $endDate) {
21+
return $this->_execute('sp_statistics_keyboard_downloads_by_id', $mssql, $startDate, $endDate);
22+
}
3423

35-
$stmt->execute();
36-
$data = $stmt->fetchAll();
37-
return filter_columns_by_name($data);
24+
function executeAppDownloadsByMonth($mssql, $startDate, $endDate) {
25+
return $this->_execute('sp_app_downloads_by_month_statistics', $mssql, $startDate, $endDate);
3826
}
3927

40-
function executeDownloadsByMonth($mssql, $startDate, $endDate) {
41-
$stmt = $mssql->prepare('EXEC sp_keyboard_downloads_by_month_statistics :prmStartDate, :prmEndDate');
28+
private function _execute($proc, $mssql, $startDate, $endDate) {
29+
$stmt = $mssql->prepare("EXEC $proc :prmStartDate, :prmEndDate");
4230

4331
$stmt->bindParam(":prmStartDate", $startDate);
4432
$stmt->bindParam(":prmEndDate", $endDate);
4533

4634
$stmt->execute();
4735
$data = $stmt->fetchAll();
48-
return filter_columns_by_name($data);
36+
return $this->filter_columns_by_name($data);
37+
}
38+
39+
// strip out repeated columns with numeric keys (by default the results returned
40+
// give each column twice, once with a column name, and once with a column index)
41+
private function filter_columns_by_name($data) {
42+
$result = [];
43+
foreach($data as $row) {
44+
$r = [];
45+
foreach($row as $id => $val) {
46+
if(!is_numeric($id)) {
47+
$r[$id] = is_numeric($val) ? intval($val) : $val;
48+
}
49+
}
50+
array_push($result, $r);
51+
}
52+
return $result;
4953
}
5054
}

0 commit comments

Comments
 (0)