/config.php
+/data/push-to-my-ouya.sqlite3
/README.html
www/api/v1/apps/
www/api/v1/details-data/
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.
*/
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/';
)
);
$apkDownloadUrl = $downloadJson->app->downloadLink;
+ $pushUrl = $GLOBALS['pushToMyOuyaUrl']
+ . '?game=' . urlencode($json->apk->package);
$navLinks = [];
foreach ($json->genres as $genreTitle) {
'com.cosmos.babyloniantwins',
'com.inverseblue.skyriders',
];
+$GLOBALS['pushToMyOuyaUrl'] = '../push-to-my-ouya.php';
<?= gmdate('Y-m-d', $json->version->publishedAt) ?>
</p>
</div>
+ <div>
+ <form method="post" action="<?= htmlspecialchars($pushUrl) ?>" id="push" onsubmit="pushToMyOuya();return false;">
+ <button name="push" type="submit" class="push-to-my-ouya">
+ <img src="../push-to-my-ouya.png" width="335" height="63"
+ alt="Push to my OUYA"
+ />
+ </button>
+ </form>
+ </div>
</section>
<nav>
<a rel="up" href="<?= htmlspecialchars($url) ?>"><?= htmlspecialchars($title) ?></a>
<?php endforeach ?>
</nav>
+
+ <div style="display: none" class="popup" id="push-success">
+ <a class="close" href="#" onclick="this.parentNode.style.display='none';return false;">⊗</a>
+ <strong><?= htmlspecialchars($json->title); ?></strong>
+ will start downloading to your OUYA within the next few minutes
+ </div>
+ <div style="display: none" class="popup" id="push-error">
+ <a class="close" href="#" onclick="this.parentNode.style.display='none';return false;">⊗</a>
+ <strong>Push error</strong>
+ <p>error message</p>
+ </div>
+
+ <script type="text/javascript">
+ function pushToMyOuya() {
+ var form = document.getElementById("push");
+ var req = new XMLHttpRequest();
+ req.addEventListener("load", pushToMyOuyaComplete);
+ req.open("POST", form.action);
+ req.send();
+ }
+ function pushToMyOuyaComplete() {
+ if (this.status / 100 == 2) {
+ document.getElementById('push-success').style.display = "";
+ } else {
+ var err = document.getElementById('push-error');
+ err.getElementsByTagName("p")[0].textContent = this.responseText;
+ err.style.display = "";
+ }
+ }
+ </script>
</body>
</html>
--- /dev/null
+<?php
+/**
+ * Helper methods for the push-to-my-ouya download queue
+ */
+
+/**
+ * Map local IPs to a single IP so that this the queue can be used
+ * in the home network.
+ *
+ * @see RFC 3330: Special-Use IPv4 Addresses
+ */
+function mapIp($ip)
+{
+ $private = substr($ip, 0, 3) == '10.'
+ || substr($ip, 0, 7) == '172.16.'
+ || substr($ip, 0, 7) == '172.17.'
+ || substr($ip, 0, 7) == '172.18.'
+ || substr($ip, 0, 7) == '172.19.'
+ || substr($ip, 0, 7) == '172.20.'
+ || substr($ip, 0, 7) == '172.21.'
+ || substr($ip, 0, 7) == '172.22.'
+ || substr($ip, 0, 7) == '172.23.'
+ || substr($ip, 0, 7) == '172.24.'
+ || substr($ip, 0, 7) == '172.25.'
+ || substr($ip, 0, 7) == '172.26.'
+ || substr($ip, 0, 7) == '172.27.'
+ || substr($ip, 0, 7) == '172.28.'
+ || substr($ip, 0, 7) == '172.29.'
+ || substr($ip, 0, 7) == '172.30.'
+ || substr($ip, 0, 7) == '172.31.'
+ || substr($ip, 0, 8) == '192.168.'
+ || substr($ip, 0, 8) == '169.254.';
+ $local = substr($ip, 0, 4) == '127.';
+
+ if ($private || $local) {
+ return 'local';
+ }
+ return $ip;
+}
#this one wants a 204 status code
RewriteRule ^api/v1/status$ - [R=204,L]
+
+#push-to-my-ouya needs php scripting support
+RewriteRule ^api/v1/queued_downloads?$ /api/v1/queued_downloads.php [END]
+
+RewriteCond %{REQUEST_METHOD} DELETE
+RewriteRule ^api/v1/queued_downloads/(.+)?$ /api/v1/queued_downloads_delete.php?game=$1 [END]
--- /dev/null
+<?php
+/**
+ * List games from the "push to my OUYA" list
+ *
+ * Pushes are stored in the sqlite3 database in push-to-my-ouya.php
+ *
+ */
+$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";
+?>
--- /dev/null
+<?php
+/**
+ * Delete a game from the "push to my OUYA" list
+ *
+ * Pushes are stored in the sqlite3 database in push-to-my-ouya.php
+ *
+ */
+$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');
+?>
.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;
.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;
+}
--- /dev/null
+<?php
+/**
+ * Click "push to my OUYA" in the browser, and the OUYA will install
+ * the game a few minutes later.
+ *
+ * Works without registration.
+ * We simply use the IP address as user identification.
+ * Pushed games are deleted after 24 hours.
+ * Maximal 30 games per IP to prevent flooding.
+ *
+ */
+$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(
+ <<<SQL
+ CREATE TABLE pushes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ game TEXT NOT NULL,
+ ip TEXT NOT NULL,
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
+ )
+SQL
+ );
+}
+
+//clean up old pushes
+$db->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);
+?>