Created
February 3, 2023 19:32
-
-
Save JC5/3084d91c424b5162ea93049fc62aa946 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* autosave.php | |
* Copyright (c) 2020 [email protected] | |
* | |
* This program is free software: you can redistribute it and/or modify | |
* it under the terms of the GNU Affero General Public License as | |
* published by the Free Software Foundation, either version 3 of the | |
* License, or (at your option) any later version. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU Affero General Public License for more details. | |
* | |
* You should have received a copy of the GNU Affero General Public License | |
* along with this program. If not, see <https://www.gnu.org/licenses/>. | |
*/ | |
declare(strict_types=1); | |
bcscale(12); | |
/* | |
* INSTRUCTIONS FOR USE. | |
* | |
* Feel free to edit the code, read it and play with it. If you have questions feel free to ask them. | |
* Keep in mind that running this script is entirely AT YOUR OWN RISK with ZERO GUARANTEES. | |
*/ | |
const FIREFLY_III_URL = 'https://firefly.example.com'; | |
const FIREFLY_III_TOKEN = 'ey...'; | |
/* | |
* HERE BE MONSTERS | |
* | |
* BELOW THIS LINE IS ACTUAL CODE (TM). | |
*/ | |
// get and validate arguments | |
$arguments = getArguments($argv); | |
// action 1: download all user's rules: | |
if ('download-rules' === $arguments['action']) { | |
// if no file, exit: | |
if (!array_key_exists('file', $arguments)) { | |
messageAndExit('No file specified. Use --file=filename.json to specify a file.'); | |
} | |
if ('' === $arguments['file']) { | |
messageAndExit('No file specified. Use --file=filename.json to specify a file.'); | |
} | |
// download rules: | |
$rules = downloadRules(); | |
// save to JSON file. | |
if (file_exists($arguments['file']) && array_key_exists('overwrite', $arguments)) { | |
message(sprintf('Will overwrite file "%s"', $arguments['file'])); | |
unlink($arguments['file']); | |
} | |
if (file_exists($arguments['file']) && !array_key_exists('overwrite', $arguments)) { | |
messageAndExit(sprintf('Cowardly refuse to overwrite file "%s". Use "--overwrite".', $arguments['file'])); | |
} | |
file_put_contents($arguments['file'], json_encode($rules, JSON_PRETTY_PRINT)); | |
messageAndExit(sprintf('Saved all rules to "%s".', $arguments['file'])); | |
} | |
// action 2: clear all existing rules | |
if ('clear-rules' === $arguments['action']) { | |
$isDryRun = array_key_exists('dry-run', $arguments); | |
$keepGroups = array_key_exists('keep-groups', $arguments); | |
clearRules($isDryRun, $keepGroups); | |
exit; | |
} | |
// action 3: upload new rule set | |
if ('upload-rules' === $arguments['action']) { | |
// overwrite or skip? | |
$overwrite = array_key_exists('overwrite', $arguments); | |
$skip = array_key_exists('skip', $arguments); | |
$clear = array_key_exists('clear', $arguments); | |
if ($overwrite && $skip) { | |
messageAndExit('You cannot use both --overwrite and --skip.'); | |
} | |
if ($clear && $skip) { | |
messageAndExit('You cannot use both --clear and --skip.'); | |
} | |
if ($clear && $overwrite) { | |
messageAndExit('You cannot use both --clear and --overwrite.'); | |
} | |
// if no file, exit: | |
if (!array_key_exists('file', $arguments)) { | |
messageAndExit('No file specified. Use --file=filename.json to specify a file.'); | |
} | |
if ('' === $arguments['file']) { | |
messageAndExit('No file specified. Use --file=filename.json to specify a file.'); | |
} | |
if (!file_exists($arguments['file'])) { | |
messageAndExit(sprintf('File "%s" does not exist.', $arguments['file'])); | |
} | |
// parse rules in file: | |
$content = file_get_contents($arguments['file']); | |
$json = json_decode($content, true, JSON_THROW_ON_ERROR); | |
if (false === $json || null === $json) { | |
messageAndExit('No rules found in file.'); | |
} | |
if (0 === count($json)) { | |
messageAndExit('No rules found in file.'); | |
} | |
$isDryRun = array_key_exists('dry-run', $arguments); | |
if (array_key_exists('clear', $arguments)) { | |
if ($isDryRun) { | |
message('Would have cleared all existing rules and rule groups first.'); | |
} | |
if (!$isDryRun) { | |
message('Clearing all existing rules and rule groups first.'); | |
} | |
clearRules($isDryRun, false); | |
} | |
// download rules | |
$existingRules = downloadRules(); | |
$existingRuleGroups = downloadRuleGroups(); | |
// loop json and action! | |
/** @var array $rule */ | |
foreach ($json as $rule) { | |
// try to make a valid uploadable JSON string from the content in $rule | |
// code ignores problems, will just dump this in the user's lap. | |
$ruleExists = ruleExists($existingRules, $rule['title']); | |
$rule['rule_group_title'] = $rule['rule_group'] ?? 'Default group'; | |
if ($isDryRun && $ruleExists && $overwrite) { | |
message(sprintf('Would have overwritten existing rule "%s".', $rule['title'])); | |
continue; | |
} | |
if ($isDryRun && $ruleExists && $skip) { | |
message(sprintf('Would have skipped existing rule "%s".', $rule['title'])); | |
continue; | |
} | |
if ($isDryRun && !$ruleExists) { | |
message(sprintf('Would have created new rule "%s".', $rule['title'])); | |
continue; | |
} | |
if ($overwrite && $ruleExists) { | |
$existingRule = getExistingRule($existingRules, $rule['title']); | |
message(sprintf('Will overwrite existing rule #%d "%s".', $existingRule['id'], $rule['title'])); | |
// delete rule | |
// submit rule | |
die('dont know how'); | |
continue; | |
} | |
if ($skip && $ruleExists) { | |
$existingRule = getExistingRule($existingRules, $rule['title']); | |
message(sprintf('Cowardly refuse to overwrite existing rule #%d "%s".', $existingRule['id'], $rule['title'])); | |
continue; | |
} | |
// see if rule group exists, create if it doesn't. | |
$ruleGroupExists = ruleGroupExists($existingRuleGroups, $rule['rule_group_title']); | |
if (!$ruleGroupExists) { | |
$group = [ | |
'title' => $rule['rule_group_title'], | |
]; | |
$result = postCurlRequest('/api/v1/rule_groups', $group); | |
message(sprintf('Stored new rule group "%s" under ID #%d', $group['title'], $result['data']['id'])); | |
} | |
$result = postCurlRequest('/api/v1/rules', $rule); | |
message(sprintf('Stored new rule "%s" under ID #%d', $rule['title'], $result['data']['id'])); | |
} | |
} | |
/** | |
* @param array $existingRules | |
* @param string $title | |
* @return array|null | |
*/ | |
function getExistingRule(array $existingRules, string $title): ?array | |
{ | |
foreach ($existingRules as $existingRule) { | |
if ($existingRule['title'] === $title) { | |
return $existingRule; | |
} | |
} | |
return null; | |
} | |
/** | |
* @param array $existingRules | |
* @param string $title | |
* @return bool | |
*/ | |
function ruleExists(array $existingRules, string $title): bool | |
{ | |
foreach ($existingRules as $existingRule) { | |
if ($existingRule['title'] === $title) { | |
message(sprintf('Rule "%s" already exists under id #%d in rule group "%s".', $title, $existingRule['id'], $existingRule['rule_group'])); | |
return true; | |
} | |
} | |
message(sprintf('Rule "%s" does not exist yet', $title)); | |
return false; | |
} | |
/** | |
* @param array $existing | |
* @param string $title | |
* @return bool | |
*/ | |
function ruleGroupExists(array $existing, string $title): bool | |
{ | |
foreach ($existing as $item) { | |
if ($existing['title'] === $title) { | |
message(sprintf('Rule group "%s" already exists under id #%d.', $title, $existing['id'])); | |
return true; | |
} | |
} | |
message(sprintf('Rule group "%s" does not exist yet', $title)); | |
return false; | |
} | |
/** | |
* @return array | |
*/ | |
function downloadRuleGroups(): array | |
{ | |
// loop over the rules: | |
$currentPage = 1; | |
$return = []; | |
$hasNextPage = true; | |
while ($hasNextPage) { | |
$current = getCurlRequest(sprintf('/api/v1/rule_groups?page=%d', $currentPage)); | |
$totalPages = $current['meta']['pagination']['total_pages']; | |
$return = array_merge($return, $current['data']); | |
// set up the next run: | |
$hasNextPage = false; | |
if ($totalPages > $currentPage) { | |
$currentPage++; | |
$hasNextPage = true; | |
} | |
} | |
return $return; | |
} | |
/** | |
* Download all rules from server. | |
* @return array | |
*/ | |
function downloadRules(): array | |
{ | |
$result = []; | |
$currentPage = 1; | |
$hasNextPage = true; | |
$rules = []; | |
while ($hasNextPage) { | |
$current = getCurlRequest(sprintf('/api/v1/rules?page=%d', $currentPage)); | |
$totalPages = $current['meta']['pagination']['total_pages']; | |
$rules = array_merge($rules, $current['data']); | |
// set up the next run: | |
$hasNextPage = false; | |
if ($totalPages > $currentPage) { | |
$currentPage++; | |
$hasNextPage = true; | |
} | |
} | |
// parse rules into a simpler array: | |
foreach ($rules as $rule) { | |
$newRule = [ | |
'id' => $rule['id'], | |
'title' => $rule['attributes']['title'], | |
'rule_group' => $rule['attributes']['rule_group_title'], | |
'order' => (int)$rule['attributes']['order'], | |
'active' => $rule['attributes']['active'], | |
'strict' => $rule['attributes']['strict'], | |
'stop_processing' => $rule['attributes']['stop_processing'], | |
'trigger' => $rule['attributes']['trigger'], | |
'triggers' => [], | |
'actions' => [], | |
]; | |
$index = 1; | |
foreach ($rule['attributes']['triggers'] as $trigger) { | |
$item = [ | |
'type' => $trigger['type'], | |
'value' => $trigger['value'], | |
'order' => $index, | |
'active' => $trigger['active'], | |
'stop_processing' => $trigger['stop_processing'], | |
]; | |
if ('' === $trigger['value'] || null === $trigger['value']) { | |
$item['value'] = null; | |
} | |
$newRule['triggers'][] = $item; | |
$index++; | |
} | |
$index = 1; | |
foreach ($rule['attributes']['actions'] as $action) { | |
$item = [ | |
'type' => $action['type'], | |
'value' => $action['value'], | |
'order' => $index, | |
'active' => $action['active'], | |
'stop_processing' => $action['stop_processing'], | |
]; | |
if ('' === $action['value'] || null === $action['value']) { | |
$item['value'] = null; | |
} | |
$index++; | |
$newRule['actions'][] = $item; | |
} | |
$result[] = $newRule; | |
} | |
return $result; | |
} | |
/** | |
* Clear all rules. Optionally keep rule groups. | |
* | |
* @param bool $isDryRun | |
* @param bool $keepGroups | |
* @return void | |
*/ | |
function clearRules(bool $isDryRun, bool $keepGroups): void | |
{ | |
$rules = downloadRules(); | |
foreach ($rules as $rule) { | |
if ($isDryRun) { | |
message(sprintf('Would have deleted rule #%d, "%s"', $rule['id'], $rule['title'])); | |
} | |
if (!$isDryRun) { | |
deleteCurlRequest(sprintf('/api/v1/rules/%d', $rule['id'])); | |
message(sprintf('Deleted rule #%d, "%s"', $rule['id'], $rule['title'])); | |
} | |
} | |
if (!$isDryRun && $keepGroups) { | |
message('Skipped deleting (now empty) rule groups.'); | |
} | |
if ($isDryRun && $keepGroups) { | |
message('Would have skipped deleting (now empty) rule groups.'); | |
} | |
if (!$keepGroups) { | |
// loop over rule groups: | |
$ruleGroups = downloadRuleGroups(); | |
foreach ($ruleGroups as $ruleGroup) { | |
if ($isDryRun) { | |
message(sprintf('Would have deleted rule group #%d, "%s"', $ruleGroup['id'], $ruleGroup['attributes']['title'])); | |
} | |
if (!$isDryRun) { | |
deleteCurlRequest(sprintf('/api/v1/rule_groups/%d', $ruleGroup['id'])); | |
message(sprintf('Deleted rule group #%d, "%s"', $ruleGroup['id'], $ruleGroup['attributes']['title'])); | |
} | |
} | |
} | |
} | |
/** | |
* @param array $arguments | |
* @return array | |
*/ | |
function getArguments(array $arguments): array | |
{ | |
$return = []; | |
$validActions = ['download-rules', 'clear-rules', 'upload-rules']; | |
if (count($arguments) < 2) { | |
infoAndExit(); | |
} | |
$action = $arguments[1] ?? 'invalid'; | |
if (!in_array($action, $validActions, true)) { | |
infoAndExit(); | |
} | |
$return['action'] = $action; | |
foreach ($arguments as $arg) { | |
if (str_starts_with($arg, '--')) { | |
$parts = explode('=', $arg); | |
$return[substr($parts[0], 2)] = $parts[1] ?? ''; | |
} | |
} | |
return $return; | |
} | |
/** | |
* @param string $message | |
*/ | |
function message(string $message): void | |
{ | |
echo $message.PHP_EOL; | |
} | |
function messageAndExit(string $message): never | |
{ | |
message($message); | |
exit; | |
} | |
/** | |
* @return void | |
*/ | |
function infoAndExit(): never | |
{ | |
message('To use this application:'); | |
message(''); | |
message('php json-rules.php download-rules --file=some-file.json --overwrite'); | |
message('php json-rules.php clear-rules --keep-groups --dry-run'); | |
message('php json-rules.php upload-rules --file=some-file.json --overwrite --clear --dry-run'); | |
message('php json-rules.php upload-rules --file=some-file.json --skip --clear --dry-run'); | |
messageAndExit(''); | |
} | |
/** | |
* @param string $url | |
* | |
* @return array | |
*/ | |
function getCurlRequest(string $url): array | |
{ | |
$ch = curl_init(); | |
curl_setopt( | |
$ch, CURLOPT_HTTPHEADER, | |
[ | |
'Content-Type: application/json', | |
'Accept: application/json', | |
sprintf('Authorization: Bearer %s', FIREFLY_III_TOKEN), | |
] | |
); | |
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
curl_setopt($ch, CURLOPT_URL, sprintf('%s%s', FIREFLY_III_URL, $url)); | |
curl_setopt($ch, CURLOPT_TIMEOUT, 6); | |
// Execute | |
$result = curl_exec($ch); | |
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); | |
if (200 !== $httpCode) { | |
$error = curl_error($ch); | |
message(sprintf('Request %s returned with HTTP code %d.', $url, $httpCode)); | |
message($error); | |
message((string)$result); | |
messageAndExit(''); | |
} | |
$body = []; | |
try { | |
$body = json_decode($result, true, 512, JSON_THROW_ON_ERROR); | |
} catch (JsonException $e) { | |
messageAndExit($e->getMessage()); | |
} | |
return $body; | |
} | |
/** | |
* @param string $url | |
* @return void | |
*/ | |
function deleteCurlRequest(string $url): void | |
{ | |
$ch = curl_init(); | |
curl_setopt( | |
$ch, CURLOPT_HTTPHEADER, | |
[ | |
'Content-Type: application/json', | |
'Accept: application/json', | |
sprintf('Authorization: Bearer %s', FIREFLY_III_TOKEN), | |
] | |
); | |
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
curl_setopt($ch, CURLOPT_URL, sprintf('%s%s', FIREFLY_III_URL, $url)); | |
curl_setopt($ch, CURLOPT_TIMEOUT, 6); | |
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); | |
// Execute | |
$result = curl_exec($ch); | |
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); | |
if (204 !== $httpCode) { | |
$error = curl_error($ch); | |
message(sprintf('Request %s returned with HTTP code %d.', $url, $httpCode)); | |
message($error); | |
message((string)$result); | |
messageAndExit(''); | |
} | |
} | |
/** | |
* @param string $url | |
* @param array $body | |
* @return array | |
* @throws JsonException | |
*/ | |
function postCurlRequest(string $url, array $body): array | |
{ | |
//message(sprintf('Going to POST %s', $url)); | |
$ch = curl_init(); | |
curl_setopt( | |
$ch, CURLOPT_HTTPHEADER, | |
[ | |
'Content-Type: application/json', | |
'Accept: application/json', | |
sprintf('Authorization: Bearer %s', FIREFLY_III_TOKEN), | |
] | |
); | |
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
curl_setopt($ch, CURLOPT_URL, sprintf('%s%s', FIREFLY_III_URL, $url)); | |
curl_setopt($ch, CURLOPT_TIMEOUT, 3); | |
curl_setopt($ch, CURLOPT_POST, 1); | |
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_THROW_ON_ERROR)); | |
// Execute | |
$result = curl_exec($ch); | |
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); | |
if (422 === $httpCode) { | |
$json = json_decode($result, true, 512, JSON_THROW_ON_ERROR); | |
message(sprintf('Validation error: %s', $json['message'])); | |
foreach ($json['errors'] as $field => $errors) { | |
message(sprintf('Field "%s": ', $field)); | |
foreach ($errors as $message) { | |
message(sprintf(' - %s', $message)); | |
} | |
} | |
message(''); | |
message('Please fix this in your JSON and try again.'); | |
messageAndExit(''); | |
} | |
if (200 !== $httpCode) { | |
$error = curl_error($ch); | |
message(sprintf('Request %s returned with HTTP code %d.', $url, $httpCode)); | |
message($error); | |
message((string)$result); | |
messageAndExit(''); | |
} | |
$body = []; | |
try { | |
$body = json_decode($result, true, 512, JSON_THROW_ON_ERROR); | |
} catch (JsonException $e) { | |
messageAndExit($e->getMessage()); | |
} | |
return $body; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment