Skip to content

Commit

Permalink
Add JSON storage class
Browse files Browse the repository at this point in the history
Add a generic JSON-based storage class and an extension of it that
will be used in the API to store client blobs. This is a similar
structure to the Settings class / user_settings table but enforces
a JSON value.
  • Loading branch information
cpeel committed Dec 22, 2024
1 parent 4ed1635 commit 33f354c
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 1 deletion.
1 change: 1 addition & 0 deletions SETUP/configuration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ _API_ENABLED=true
_API_RATE_LIMIT=false
_API_RATE_LIMIT_REQUESTS_PER_WINDOW=3600
_API_RATE_LIMIT_SECONDS_IN_WINDOW=3600
_API_CLIENT_STORAGE_KEYS='[]'

# XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Expand Down
12 changes: 12 additions & 0 deletions SETUP/db_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ CREATE TABLE `job_logs` (
KEY `timestamp` (`tracetime`, `succeeded`)
);

--
-- Table structure for table `json_storage`
--

CREATE TABLE `json_storage` (
`username` varchar(25) NOT NULL,
`setting` varchar(32) NOT NULL,
`value` json NOT NULL,
`timestamp` int NOT NULL DEFAULT '0',
PRIMARY KEY (`username`,`setting`)
);

--
-- Table structure for table `news_items`
--
Expand Down
1 change: 1 addition & 0 deletions SETUP/tests/ci_configuration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ _API_ENABLED=true
_API_RATE_LIMIT=false
_API_RATE_LIMIT_REQUESTS_PER_WINDOW=3600
_API_RATE_LIMIT_SECONDS_IN_WINDOW=3600
_API_CLIENT_STORAGE_KEYS='[]'

_EXTERNAL_CATALOG_LOCATOR='z3950.loc.gov:7090/Voyager'

Expand Down
50 changes: 50 additions & 0 deletions SETUP/tests/unittests/StorageTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

class StorageTest extends PHPUnit\Framework\TestCase
{
//------------------------------------------------------------------------
// Basic JSON storage test

public function test_valid_json(): void
{
$storage = new JsonStorage("username");
$storage->set("setting", "{}");
$value = $storage->get("setting");
$this->assertEquals("{}", $value);
$value = $storage->delete("setting");
$this->assertEquals(null, $value);
}

public function test_invalid_json(): void
{
$this->expectException(ValueError::class);
$storage = new JsonStorage("username");
$storage->set("setting", "blearg");
}

//------------------------------------------------------------------------
// API client storage test

public function test_valid_client(): void
{
global $api_client_storage_keys;
$api_client_storage_keys = ["valid"];

$storage = new ApiClientStorage("valid", "username");
$storage->set("{}");
$value = $storage->get();
$this->assertEquals("{}", $value);
$value = $storage->delete();
$this->assertEquals(null, $value);
}

public function test_invalid_client(): void
{
global $api_client_storage_keys;
$api_client_storage_keys = [];

$this->expectException(ValueError::class);

$storage = new ApiClientStorage("invalid", "username");
}
}
23 changes: 23 additions & 0 deletions SETUP/upgrade/22/20241219_create_json_storage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
$relPath = '../../../pinc/';
include_once($relPath.'base.inc');

// ------------------------------------------------------------

echo "Creating json_storage table...\n";

$sql = "
CREATE TABLE json_storage (
username varchar(25) NOT NULL,
setting varchar(32) NOT NULL,
value json NOT NULL,
timestamp int NOT NULL default 0,
PRIMARY KEY (username, setting)
)
";

mysqli_query(DPDatabase::get_connection(), $sql) or die(mysqli_error(DPDatabase::get_connection()));

// ------------------------------------------------------------

echo "\nDone!\n";
38 changes: 38 additions & 0 deletions pinc/ApiClientStorage.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

class ApiClientStorage
{
private JsonStorage $storage;
private string $setting;

public function __construct(string $client, string $username)
{
global $api_client_storage_keys;

if (!in_array($client, $api_client_storage_keys)) {
throw new ValueError("$client is not an accepted client");
}
$this->setting = "client:$client";
$this->storage = new JsonStorage($username);
}

// -------------------------------------------------------------------------

public function set(string $value): void
{
$this->storage->set($this->setting, $value);
}

public function get(): string
{
// if the setting isn't set, JsonStorage will return null but we want
// to always return valid JSON for the API so we return an empty JSON
// doc.
return $this->storage->get($this->setting) ?? "{}";
}

public function delete(): void
{
$this->storage->delete($this->setting);
}
}
2 changes: 1 addition & 1 deletion pinc/DPDatabase.inc
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ final class DPDatabase
$error = _("An error occurred during a database query.");
}
if ($throw_on_failure) {
throw new DBQueryError($error);
throw new DBQueryError($error, 0, $e);
}
return false;
}
Expand Down
91 changes: 91 additions & 0 deletions pinc/JsonStorage.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

class JsonStorage
{
private string $username;

public function __construct(string $username)
{
$this->username = $username;
}

// -------------------------------------------------------------------------

/**
* Set or update a json_storage object.
*/
public function set(string $setting, string $value): void
{
// It's possible $value could be very large and including it twice
// in the query will cause it to be too big. The default max query
// size is 64MB which means this will fail if the JSON blob is
// just shy of 32MB. That seems plenty big.
$sql = sprintf(
"
INSERT INTO json_storage
SET
username = '%s',
setting = '%s',
value = '%s',
timestamp = UNIX_TIMESTAMP()
ON DUPLICATE KEY UPDATE
value = '%s',
timestamp = UNIX_TIMESTAMP()
",
DPDatabase::escape($this->username),
DPDatabase::escape($setting),
DPDatabase::escape($value),
DPDatabase::escape($value)
);
// We rely on MySQL to validate the JSON is valid or throw an exception.
// It's going to do the check anyway and saves us from de-serializing
// it here.
try {
DPDatabase::query($sql, true, false);
} catch (DBQueryError $e) {
if (startswith($e->getPrevious()->getMessage(), "Invalid JSON")) {
throw new ValueError("Error persisting data, invalid JSON");
}
throw $e;
}
}

/**
* Get a json_storage object.
*/
public function get(string $setting): ?string
{
$sql = sprintf(
"
SELECT value
FROM json_storage
WHERE username = '%s' AND setting = '%s'
",
DPDatabase::escape($this->username),
DPDatabase::escape($setting)
);
$result = DPDatabase::query($sql);
$value = null;
while ($row = mysqli_fetch_assoc($result)) {
$value = $row['value'];
}
mysqli_free_result($result);
return $value;
}

/**
* Delete a json_storage object.
*/
public function delete(string $setting): void
{
$sql = sprintf(
"
DELETE FROM json_storage
WHERE username = '%s' AND setting = '%s'
",
DPDatabase::escape($this->username),
DPDatabase::escape($setting)
);
DPDatabase::query($sql);
}
}
1 change: 1 addition & 0 deletions pinc/site_vars.php.template
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ $api_enabled = <<API_ENABLED>>;
$api_rate_limit = <<API_RATE_LIMIT>>;
$api_rate_limit_requests_per_window = <<API_RATE_LIMIT_REQUESTS_PER_WINDOW>>;
$api_rate_limit_seconds_in_window = <<API_RATE_LIMIT_SECONDS_IN_WINDOW>>;
$api_client_storage_keys = <<API_CLIENT_STORAGE_KEYS>>;

// -----------------------------------------------------------------------------

Expand Down

0 comments on commit 33f354c

Please sign in to comment.