Skip to content

Commit bbe5e39

Browse files
committed
New plugin: IGDB driver
1 parent d356091 commit bbe5e39

4 files changed

Lines changed: 354 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- ClickHouse: Fix offset (bug #1188)
1414
- ClickHouse: Fix list of tables (bug #1176)
1515
- Plugins: Methods showVariables() and showStatus() (bug #1157)
16+
- New plugin: IGDB driver
1617

1718
## Adminer 5.4.1 (released 2025-09-26)
1819
- SQL command: Unlink NULL primary keys

adminer/file.inc.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
../externals/jush/modules/jush-sqlite.js;
3535
../externals/jush/modules/jush-mssql.js;
3636
../externals/jush/modules/jush-oracle.js;
37-
../externals/jush/modules/jush-simpledb.js', 'minify_js'));
37+
../externals/jush/modules/jush-simpledb.js;
38+
../externals/jush/modules/jush-igdb.js', 'minify_js'));
3839
} elseif ($_GET["file"] == "logo.png") {
3940
header("Content-Type: image/png");
4041
echo compile_file('../adminer/static/logo.png');

externals/jush

plugins/drivers/igdb.php

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
<?php
2+
/** Driver for https://api-docs.igdb.com/
3+
* @link https://demo.adminer.org/igdb/?igdb=IGDB&db=api
4+
* username: your Client-ID
5+
* password: your access token from https://id.twitch.tv/oauth2/token
6+
* @link https://www.adminer.org/static/plugins/igdb.png
7+
*/
8+
9+
namespace Adminer;
10+
11+
add_driver("igdb", "APICalypse");
12+
13+
if (isset($_GET["igdb"])) {
14+
define('Adminer\DRIVER', "igdb");
15+
16+
class Db extends SqlDb {
17+
public $extension = "json";
18+
public $server_info = "v4";
19+
private $username;
20+
private $password;
21+
22+
function attach($server, $username, $password): string {
23+
$this->username = $username;
24+
$this->password = $password;
25+
return '';
26+
}
27+
28+
function select_db($database) {
29+
return ($database == "api");
30+
}
31+
32+
function request($endpoint, $query) {
33+
$context = stream_context_create(array('http' => array(
34+
'method' => 'POST',
35+
'header' => array(
36+
"Content-Type: text/plain",
37+
"Client-ID: $this->username",
38+
"Authorization: Bearer $this->password",
39+
),
40+
'content' => $query,
41+
'ignore_errors' => true,
42+
)));
43+
$response = file_get_contents("https://api.igdb.com/v4/$endpoint", false, $context);
44+
$return = json_decode($response, true);
45+
if ($http_response_header[0] != 'HTTP/1.1 200 OK') {
46+
if (is_array($return)) {
47+
foreach (is_array($return[0]) ? $return : array($return) as $rows) {
48+
foreach ($rows as $key => $val) {
49+
$this->error .= '<b>' . h($key) . ':</b> ' . (is_url($val) ? '<a href="' . h($val) . '"' . target_blank() . '>' . h($val) . '</a>' : h($val)) . '<br>';
50+
}
51+
}
52+
} else {
53+
$this->error = h($response);
54+
}
55+
return false;
56+
}
57+
return $return;
58+
}
59+
60+
function query($query, $unbuffered = false) {
61+
if (preg_match('~^SELECT COUNT\(\*\) FROM (\w+)( WHERE ((MATCH \(search\) AGAINST \((.+)\))|.+))?$~', $query, $match)) {
62+
return new Result(array($this->request("$match[1]/count", ($match[5] ? 'search "' . addcslashes($match[5], '\\"') . '";'
63+
: ($match[3] ? 'where ' . str_replace(' AND ', ' & ', $match[3]) . ';'
64+
: ''
65+
)))));
66+
}
67+
if (preg_match('~^\s*endpoint\s+(\w+(/count)?)\s*;\s*(.*)$~s', $query, $match)) {
68+
$endpoint = $match[1];
69+
$response = $this->request($endpoint, $match[3]);
70+
if ($response === false) {
71+
return $response;
72+
}
73+
$return = new Result($match[2] ? array($response) : $response);
74+
$return->table = $endpoint;
75+
if ($endpoint == 'multiquery') {
76+
$return->results = $response;
77+
}
78+
return $return;
79+
}
80+
$this->error = "Syntax:<br>DELIMITER ;;<br>endpoint &lt;endpoint>; fields ...;";
81+
return false;
82+
}
83+
84+
function store_result() {
85+
if ($this->multi && ($result = current($this->multi->results))) {
86+
echo "<h3>" . h($result['name']) . "</h3>\n";
87+
$this->multi->__construct($result['count'] ? array(array('count' => $result['count'])) : $result['result']);
88+
}
89+
return $this->multi;
90+
}
91+
92+
function next_result(): bool {
93+
return $this->multi && next($this->multi->results);
94+
}
95+
96+
function quote($string): string {
97+
return $string;
98+
}
99+
}
100+
101+
class Result {
102+
public $num_rows;
103+
public $table;
104+
public $results = array();
105+
private $result;
106+
private $fields;
107+
108+
function __construct($result) {
109+
$keys = array();
110+
foreach ($result as $i => $row) {
111+
foreach ($row as $key => $val) {
112+
$keys[$key] = null;
113+
if (is_array($val) && is_int($val[0])) {
114+
$result[$i][$key] = "(" . implode(",", $val) . ")";
115+
}
116+
}
117+
}
118+
foreach ($result as $i => $row) {
119+
$result[$i] = array_merge($keys, $row);
120+
}
121+
$this->result = $result;
122+
$this->num_rows = count($result);
123+
$this->fields = array_keys(idx($result, 0, array()));
124+
}
125+
126+
function fetch_assoc() {
127+
$row = current($this->result);
128+
next($this->result);
129+
return $row;
130+
}
131+
132+
function fetch_row() {
133+
$row = $this->fetch_assoc();
134+
return ($row ? array_values($row) : false);
135+
}
136+
137+
function fetch_field(): \stdClass {
138+
$field = current($this->fields);
139+
next($this->fields);
140+
return ($field != '' ? (object) array('name' => $field, 'type' => 15, 'charsetnr' => 0, 'orgtable' => $this->table) : false);
141+
}
142+
}
143+
144+
class Driver extends SqlDriver {
145+
static $extensions = array("json");
146+
static $jush = "igdb";
147+
private static $docsFilename = __DIR__ . DIRECTORY_SEPARATOR . 'igdb-api.html';
148+
149+
public $operators = array("=", "<", ">", "<=", ">=", "!=", "~");
150+
151+
public $tables = array();
152+
public $links = array();
153+
public $fields = array();
154+
public $foreignKeys = array();
155+
public $foundRows = null;
156+
157+
static function connect(string $server, string $username, string $password) {
158+
if (!file_exists(self::$docsFilename)) {
159+
return "Download https://api-docs.igdb.com/ and save it as " . self::$docsFilename; // copy() doesn't work - bot protection
160+
}
161+
return parent::connect($server, $username, $password);
162+
}
163+
164+
function __construct($connection) {
165+
parent::__construct($connection);
166+
libxml_use_internal_errors(true);
167+
$dom = new \DOMDocument();
168+
$dom->loadHTMLFile(self::$docsFilename);
169+
$xpath = new \DOMXPath($dom);
170+
$els = $xpath->query('//div[@class="content"]/*');
171+
$link = '';
172+
foreach ($els as $i => $el) {
173+
if ($el->tagName == 'h2') {
174+
$link = $el->getAttribute('id');
175+
}
176+
if ($el->nodeValue == 'Request Path') {
177+
$table = preg_replace('~^https://api.igdb.com/v4/~', '', $els[$i+1]->firstElementChild->nodeValue);
178+
$this->fields[$table]['id'] = array('full_type' => 'bigserial', 'comment' => '');
179+
$this->links[$link] = $table;
180+
$this->tables[$table] = array(
181+
'Name' => $table,
182+
'Comment' => $els[$i-1]->tagName == 'p' ? $els[$i-1]->nodeValue : '',
183+
);
184+
foreach ($xpath->query('tbody/tr', $els[$i+2]) as $tr) {
185+
$tds = $xpath->query('td', $tr);
186+
$comment = $tds[2]->nodeValue;
187+
if (!preg_match('~^DEPRECATED!~', $comment)) {
188+
$field = $tds[0]->nodeValue;
189+
$this->fields[$table][$field] = array(
190+
'full_type' => str_replace(' ', ' ', $tds[1]->nodeValue),
191+
'comment' => str_replace(' ', ' ', $comment),
192+
);
193+
$ref = $xpath->query('a/@href', $tds[1]);
194+
if (count($ref) && !in_array($ref[0]->value, array('#game-version-feature-enums', '#tag-numbers'))) {
195+
$this->foreignKeys[$table][$field] = substr($ref[0]->value, 1);
196+
}
197+
}
198+
}
199+
}
200+
}
201+
}
202+
203+
function select($table, $select, $where, $group, $order = array(), $limit = 1, $page = 0, $print = false) {
204+
$query = '';
205+
$search = preg_match('~^MATCH \(search\) AGAINST \((.+)\)$~', $where[0], $match);
206+
if ($search) {
207+
$query = 'search "' . addcslashes($match[1], '\\"') . "\";\n";
208+
unset($where[0]);
209+
}
210+
foreach ($where as $i => $val) {
211+
$where[$i] = str_replace(' OR ', ' | ', $val);
212+
}
213+
$common = ($where ? "where " . implode(" & ", $where) . ";" : "");
214+
$query .= "fields " . implode(",", $select) . ";"
215+
. ($select == array('*') ? "\nexclude checksum;" : "")
216+
. ($where ? "\n$common" : "")
217+
. ($order ? "\nsort " . strtolower(implode(",", $order)) . ";" : "")
218+
. "\nlimit $limit;"
219+
. ($page ? "\noffset " . ($page * $limit) . ";" : "")
220+
;
221+
$start = microtime(true);
222+
$return = ($search || !array_key_exists($table, driver()->tables)
223+
? $this->conn->request($table, $query)
224+
: $this->conn->request('multiquery', "query $table \"result\" { $query };\nquery $table/count \"count\" { $common };")
225+
);
226+
if ($print) {
227+
echo adminer()->selectQuery("DELIMITER ;;\nendpoint $table;\n$query", $start);
228+
}
229+
if ($return === false) {
230+
return $return;
231+
}
232+
$this->foundRows = ($search ? null : $return[1]['count']);
233+
$return = ($search ? $return : $return[0]['result']);
234+
if ($return) {
235+
$return[0] = array_merge(array_fill_keys(($select != array('*') ? $select : array_keys($this->fields[$table])), null), $return[0]);
236+
}
237+
return new Result($return);
238+
}
239+
240+
function value($val, $field): ?string {
241+
return ($val && in_array($field['full_type'], array('Unix Time Stamp', 'datetime')) ? str_replace(' 00:00:00', '', gmdate('Y-m-d H:i:s', $val)) : $val);
242+
}
243+
244+
function tableHelp($name, $is_view = false) {
245+
return strtolower("https://api-docs.igdb.com/#" . array_search($name, $this->links));
246+
}
247+
}
248+
249+
function logged_user() {
250+
return $_GET["username"];
251+
}
252+
253+
function get_databases($flush) {
254+
return array("api");
255+
}
256+
257+
function collations() {
258+
return array();
259+
}
260+
261+
function db_collation($db, $collations) {
262+
}
263+
264+
function information_schema($db) {
265+
}
266+
267+
function indexes($table, $connection2 = null) {
268+
$return = array(array("type" => "PRIMARY", "columns" => array("id")));
269+
if (in_array($table, array('characters', 'collections', 'games', 'platforms', 'themes'))) { // https://api-docs.igdb.com/#search-1
270+
$return[] = array("type" => "FULLTEXT", "columns" => array("search"));
271+
}
272+
return $return;
273+
}
274+
275+
function fields($table) {
276+
$return = array();
277+
foreach (driver()->fields[$table] ?: array() as $key => $val) {
278+
$return[$key] = $val + array(
279+
"field" => $key,
280+
"type" => (preg_match('~^int|bool~i', $val['full_type']) ? $val['full_type'] : 'varchar'), // shorten reference columns
281+
"privileges" => array("select" => 1, "update" => 1, "where" => 1, "order" => 1),
282+
);
283+
}
284+
return $return;
285+
}
286+
287+
function convert_field($field) {
288+
}
289+
290+
function unconvert_field($field, $return) {
291+
return $return;
292+
}
293+
294+
function limit($query, $where, $limit, $offset = 0, $separator = " ") {
295+
return $query;
296+
}
297+
298+
function idf_escape($idf) {
299+
return $idf;
300+
}
301+
302+
function table($idf) {
303+
return idf_escape($idf);
304+
}
305+
306+
function foreign_keys($table) {
307+
$return = array();
308+
foreach (driver()->foreignKeys[$table] ?: array() as $key => $val) {
309+
$return[] = array(
310+
'table' => driver()->links[$val],
311+
'source' => array($key),
312+
'target' => array('id'),
313+
);
314+
}
315+
return $return;
316+
}
317+
318+
function tables_list() {
319+
return array_fill_keys(array_keys(table_status()), 'table');
320+
}
321+
322+
function table_status($name = "", $fast = false) {
323+
$tables = driver()->tables;
324+
return ($name != '' ? ($tables[$name] ? array($name => $tables[$name]) : array()) : $tables);
325+
}
326+
327+
function count_tables($databases) {
328+
return array(reset($databases) => count(tables_list()));
329+
}
330+
331+
function error() {
332+
return connection()->error;
333+
}
334+
335+
function is_view($table_status) {
336+
return false;
337+
}
338+
339+
function found_rows($table_status, $where) {
340+
return driver()->foundRows;
341+
}
342+
343+
function fk_support($table_status) {
344+
return true;
345+
}
346+
347+
function support($feature) {
348+
return in_array($feature, array('columns', 'comment', 'sql', 'table'));
349+
}
350+
}

0 commit comments

Comments
 (0)