From 4d2b9288d5403294fe6541358341986910e43c36 Mon Sep 17 00:00:00 2001 From: Christian Weiske Date: Thu, 14 May 2020 07:09:02 +0200 Subject: [PATCH] "Push to my OUYA" support --- .gitignore | 1 + README.rst | 1 + bin/build-html.php | 9 ++ config.php.dist | 1 + data/templates/game.tpl.php | 39 +++++++ src/push-to-my-ouya-helpers.php | 39 +++++++ www/.htaccess | 6 + www/api/v1/queued_downloads.php | 54 +++++++++ www/api/v1/queued_downloads_delete.php | 53 +++++++++ www/ouya-game.css | 38 +++++++ www/push-to-my-ouya.php | 145 +++++++++++++++++++++++++ www/push-to-my-ouya.png | Bin 0 -> 11908 bytes 12 files changed, 386 insertions(+) create mode 100644 src/push-to-my-ouya-helpers.php create mode 100644 www/api/v1/queued_downloads.php create mode 100644 www/api/v1/queued_downloads_delete.php create mode 100644 www/push-to-my-ouya.php create mode 100644 www/push-to-my-ouya.png diff --git a/.gitignore b/.gitignore index cd66da5..4827cbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /config.php +/data/push-to-my-ouya.sqlite3 /README.html www/api/v1/apps/ www/api/v1/details-data/ diff --git a/README.rst b/README.rst index 91d490f..d06a382 100644 --- a/README.rst +++ b/README.rst @@ -36,6 +36,7 @@ Apache setup Virtual host configuration:: Script PUT /empty-json.php + Script DELETE /api/v1/queued_downloads_delete.php ``mod_actions`` need to be enabled for apache 2.4. diff --git a/bin/build-html.php b/bin/build-html.php index b4561e6..f7f9154 100755 --- a/bin/build-html.php +++ b/bin/build-html.php @@ -7,6 +7,13 @@ */ require_once __DIR__ . '/functions.php'; +//default configuration values +$GLOBALS['pushToMyOuyaUrl'] = '../push-to-my-ouya.php'; +$cfgFile = __DIR__ . '/../config.php'; +if (file_exists($cfgFile)) { + include $cfgFile; +} + $wwwDir = __DIR__ . '/../www/'; $discoverDir = __DIR__ . '/../www/api/v1/discover-data/'; $wwwDiscoverDir = $wwwDir . 'discover/'; @@ -111,6 +118,8 @@ function renderGameFile($gameDataFile) ) ); $apkDownloadUrl = $downloadJson->app->downloadLink; + $pushUrl = $GLOBALS['pushToMyOuyaUrl'] + . '?game=' . urlencode($json->apk->package); $navLinks = []; foreach ($json->genres as $genreTitle) { diff --git a/config.php.dist b/config.php.dist index 8c23bd2..5087c22 100644 --- a/config.php.dist +++ b/config.php.dist @@ -11,3 +11,4 @@ $GLOBALS['packagelists']["cweiske's picks"] = [ 'com.cosmos.babyloniantwins', 'com.inverseblue.skyriders', ]; +$GLOBALS['pushToMyOuyaUrl'] = '../push-to-my-ouya.php'; diff --git a/data/templates/game.tpl.php b/data/templates/game.tpl.php index 36a1d09..531fb28 100644 --- a/data/templates/game.tpl.php +++ b/data/templates/game.tpl.php @@ -75,6 +75,15 @@ version->publishedAt) ?>

+
+
+ +
+
+ + + + + diff --git a/src/push-to-my-ouya-helpers.php b/src/push-to-my-ouya-helpers.php new file mode 100644 index 0000000..a020988 --- /dev/null +++ b/src/push-to-my-ouya-helpers.php @@ -0,0 +1,39 @@ + + */ +$dbFile = __DIR__ . '/../../../data/push-to-my-ouya.sqlite3'; +$apiGameDir = __DIR__ . '/details-data/'; + +require_once __DIR__ . '/../../../src/push-to-my-ouya-helpers.php'; + +$ip = $_SERVER['REMOTE_ADDR']; +if ($ip == '' || strpos($ip, ':') !== false) { + //empty or IPv6 + header('Content-type: application/json'); + echo file_get_contents('queued_downloads'); + exit(1); +} +$ip = mapIp($ip); + +try { + $db = new SQLite3($dbFile, SQLITE3_OPEN_READONLY); +} catch (Exception $e) { + //db file not found + header('Content-type: application/json'); + echo file_get_contents('queued_downloads'); + exit(1); +} + +$res = $db->query( + 'SELECT * FROM pushes' + . ' WHERE ip = \'' . SQLite3::escapeString($ip) . '\'' +); +$queue = []; +while ($row = $res->fetchArray(SQLITE3_ASSOC)) { + $apiGameFile = $apiGameDir . $row['game'] . '.json'; + if (!file_exists($apiGameFile)) { + //game deleted? + continue; + } + $json = json_decode(file_get_contents($apiGameFile)); + $queue[] = [ + 'versionUuid' => '', + 'title' => $json->title, + 'source' => 'gamer', + 'uuid' => $row['game'], + ]; +} + +header('Content-type: application/json'); +echo json_encode(['queue' => $queue]) . "\n"; +?> diff --git a/www/api/v1/queued_downloads_delete.php b/www/api/v1/queued_downloads_delete.php new file mode 100644 index 0000000..3861f93 --- /dev/null +++ b/www/api/v1/queued_downloads_delete.php @@ -0,0 +1,53 @@ + + */ +$dbFile = __DIR__ . '/../../../data/push-to-my-ouya.sqlite3'; +$apiGameDir = __DIR__ . '/details-data/'; + +require_once __DIR__ . '/../../../src/push-to-my-ouya-helpers.php'; + +$ip = $_SERVER['REMOTE_ADDR']; +if ($ip == '' || strpos($ip, ':') !== false) { + //empty or IPv6 + header('HTTP/1.0 204 No Content'); + exit(1); +} +$ip = mapIp($ip); + +$game = $_GET['game']; +$cleanGame = preg_replace('#[^a-zA-Z0-9.]#', '', $game); +if ($game != $cleanGame || $game == '') { + header('HTTP/1.0 400 Bad Request'); + header('Content-type: text/plain'); + echo 'Invalid game' . "\n"; + exit(1); +} + +try { + $db = new SQLite3($dbFile, SQLITE3_OPEN_READWRITE); +} catch (Exception $e) { + //db file not found + header('HTTP/1.0 204 No Content'); + exit(1); +} + +$rowId = $db->querySingle( + 'SELECT id FROM pushes' + . ' WHERE ip = \'' . SQLite3::escapeString($ip) . '\'' + . ' AND game =\'' . SQLite3::escapeString($game) . '\'' +); +if ($rowId === null) { + header('HTTP/1.0 404 Not Found'); + header('Content-type: text/plain'); + echo 'Game not queued' . "\n"; + exit(1); +} + +$db->exec('DELETE FROM pushes WHERE id = ' . intval($rowId)); +header('HTTP/1.0 204 No Content'); +?> diff --git a/www/ouya-game.css b/www/ouya-game.css index b9886e7..ce23fdd 100644 --- a/www/ouya-game.css +++ b/www/ouya-game.css @@ -97,10 +97,20 @@ nav { .buttons h2 { display: none; } +.buttons { + display: flex; + justify-content: space-between; +} .buttons a { font-size: 1.5rem; color: #CCC; } +button.push-to-my-ouya { + cursor: pointer; + border: none; + padding: 0; + background-color: transparent; +} nav { text-align: center; @@ -156,3 +166,31 @@ nav a { .average-5:before { content: "★★★★★"; } + + +.popup { + position: fixed; + top: 2rem; + right: 2rem; + width: 20rem; + padding: 1rem; + background-color: black; + border: 1px solid #AAA; + border-radius: 0.5rem; +} +.popup a.close { + color: white; + font-size: 2rem; + text-decoration: none; + position: absolute; + top: 0; + right: 0.5rem; +} +.popup a.close:hover { + color: #fc4422; +} +.popup strong { + display: block; + color: #fc4422; + margin-bottom: 0.5rem; +} diff --git a/www/push-to-my-ouya.php b/www/push-to-my-ouya.php new file mode 100644 index 0000000..fd39043 --- /dev/null +++ b/www/push-to-my-ouya.php @@ -0,0 +1,145 @@ + + */ +$dbFile = __DIR__ . '/../data/push-to-my-ouya.sqlite3'; +$apiGameDir = __DIR__ . '/api/v1/details-data/'; + +require_once __DIR__ . '/../src/push-to-my-ouya-helpers.php'; + +//support different ipv4-only domain +header('Access-Control-Allow-Origin: *'); + +if ($_SERVER['REQUEST_METHOD'] != 'POST') { + header('HTTP/1.0 400 Bad Request'); + header('Content-type: text/plain'); + echo 'POST only, please' . "\n"; + exit(1); +} + +if (!isset($_GET['game'])) { + header('HTTP/1.0 400 Bad Request'); + header('Content-type: text/plain'); + echo '"game" parameter missing' . "\n"; + exit(1); +} + +$game = $_GET['game']; +$cleanGame = preg_replace('#[^a-zA-Z0-9.]#', '', $game); +if ($game != $cleanGame) { + header('HTTP/1.0 400 Bad Request'); + header('Content-type: text/plain'); + echo 'Invalid game' . "\n"; + exit(1); +} + +$apiGameFile = $apiGameDir . $game . '.json'; +if (!file_exists($apiGameFile)) { + header('HTTP/1.0 404 Not Found'); + header('Content-type: text/plain'); + echo 'Game does not exist' . "\n"; + exit(1); +} + +$ip = $_SERVER['REMOTE_ADDR']; +if ($ip == '') { + header('HTTP/1.0 400 Bad Request'); + header('Content-type: text/plain'); + echo 'Cannot detect your IP address' . "\n"; + exit(1); +} +if (strpos($ip, ':') !== false) { + header('HTTP/1.0 400 Bad Request'); + header('Content-type: text/plain'); + echo 'Sorry, IPv6 is not supported' . "\n"; + echo 'This here only works if the OUYA and your PC have the same IP address,' + . "\n"; + echo 'and this is definitely not the case when using IPv6' . "\n"; + exit(1); +} +$ip = mapIp($ip); + +try { + $db = new SQLite3($dbFile, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE); +} catch (Exception $e) { + header('HTTP/1.0 500 Internal server error'); + header('Content-type: text/plain'); + echo 'Cannot open database' . "\n"; + echo $e->getMessage() . "\n"; + exit(2); +} + +$res = $db->querySingle( + 'SELECT name FROM sqlite_master WHERE type = "table" AND name = "pushes"' +); +if ($res === null) { + //table does not exist yet + $db->exec( + <<exec( + 'DELETE FROM pushes' + . ' WHERE created_at < \'' . gmdate('Y-m-d H:i:s', time() - 86400) . '\'' +); + +//check if this IP already pushed this game +$numThisGame = $db->querySingle( + 'SELECT COUNT(*) FROM pushes' + . ' WHERE ip = \'' . SQLite3::escapeString($ip) . '\'' + . ' AND game = \'' . SQLite3::escapeString($game) . '\'' +); +if ($numThisGame >= 1) { + header('HTTP/1.0 400 Bad Request'); + header('Content-type: text/plain'); + echo 'Already pushed.' . "\n"; + exit(1); +} + +//check number of pushes for this IP +$numPushes = $db->querySingle( + 'SELECT COUNT(*) FROM pushes' + . ' WHERE ip = \'' . SQLite3::escapeString($ip) . '\'' +); +if ($numPushes >= 30) { + header('HTTP/1.0 400 Bad Request'); + header('Content-type: text/plain'); + echo 'Too many pushes. Come back tomorrow.' . "\n"; + exit(1); +} + +//store the push +$stmt = $db->prepare('INSERT INTO pushes (game, ip) VALUES(:game, :ip)'); +$stmt->bindValue(':game', $game); +$stmt->bindValue(':ip', $ip); +$res = $stmt->execute(); +if ($res === false) { + header('HTTP/1.0 500 Internal server error'); + header('Content-type: text/plain'); + echo 'Cannot store push' . "\n"; + exit(3); +} +$res->finalize(); + +header('HTTP/1.0 200 OK'); +header('Content-type: text/plain'); +echo 'Push accepted' . "\n"; +exit(3); +?> diff --git a/www/push-to-my-ouya.png b/www/push-to-my-ouya.png new file mode 100644 index 0000000000000000000000000000000000000000..50bd0e22ce33325fdc9ef66bc2bd1f6615ae1895 GIT binary patch literal 11908 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 literal 0 HcmV?d00001 -- 2.30.2