mirror of
https://github.com/daledavies/jump.git
synced 2026-01-10 09:22:06 +01:00
309 lines
12 KiB
PHP
309 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* ██ ██ ██ ███ ███ ██████
|
|
* ██ ██ ██ ████ ████ ██ ██
|
|
* ██ ██ ██ ██ ████ ██ ██████
|
|
* ██ ██ ██ ██ ██ ██ ██ ██
|
|
* █████ ██████ ██ ██ ██
|
|
*
|
|
* @author Dale Davies <dale@daledavies.co.uk>
|
|
* @copyright Copyright (c) 2022, Dale Davies
|
|
* @license MIT
|
|
*/
|
|
|
|
namespace Jump;
|
|
|
|
use \divineomega\array_undot;
|
|
use \Jump\Exceptions\ConfigException;
|
|
use \Jump\Exceptions\SiteNotFoundException;
|
|
use \Jump\Exceptions\TagNotFoundException;
|
|
|
|
/**
|
|
* Loads, validates and caches the site data defined in sites.json
|
|
* into an array of Site objects.
|
|
*/
|
|
class Sites {
|
|
|
|
private array $default;
|
|
private string $sitesfilelocation;
|
|
private array $loadedsites;
|
|
public array $tags;
|
|
|
|
/**
|
|
* Automatically load sites.json on instantiation.
|
|
*/
|
|
public function __construct(private Config $config, private Cache $cache) {
|
|
$this->config = $config;
|
|
$this->loadedsites = [];
|
|
$this->sitesfilelocation = $this->config->get('sitesfile');
|
|
$this->cache = $cache;
|
|
$this->default = [
|
|
'icon' => null,
|
|
'nofollow' => false,
|
|
'newtab' => false,
|
|
];
|
|
|
|
// Retrieve sites from cache. Load all sites from json file and docker if not
|
|
// cached or the cache has expired.
|
|
$this->loadedsites = $this->cache->load(cachename: 'sites', callback: function() {
|
|
// Load json file first to set defaults.
|
|
return array_merge($this->load_sites_from_json(), $this->load_sites_from_docker());
|
|
});
|
|
|
|
// Enumerate a list of unique tags from loaded sites. Again will retrieve from
|
|
// cache if available.
|
|
$this->tags = $this->cache->load(cachename: 'tags', callback: function() {
|
|
$uniquetags = [];
|
|
foreach (array_column($this->get_sites(), 'tags') as $tags) {
|
|
foreach ($tags as $tag) {
|
|
$uniquetags[] = $tag;
|
|
}
|
|
}
|
|
return array_values(array_unique($uniquetags));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Try to find a list of sites from correctly labelled docker containers.
|
|
*
|
|
* Throws an exception if the json response from docker cannot be
|
|
* decoded.
|
|
*
|
|
* @return array Array of Site objects sites identified from docker.
|
|
* @throws ConfigException If invalid response from docker.
|
|
*/
|
|
private function load_sites_from_docker(): array {
|
|
// Get either dockerproxy or dockersocket config and return early if
|
|
// neihter have been set.
|
|
$dockerproxy = $this->config->get('dockerproxyurl');
|
|
$dockersocket = $this->config->get('dockersocket');
|
|
if (!$dockerproxy && !$dockersocket) {
|
|
return [];
|
|
}
|
|
|
|
// Determine correct guzzle client and request options to use
|
|
// for either a docker proxy or connecting directly to the socket,
|
|
// prefer to use the proxy if both seem to have been given.
|
|
$clientopts = ['timeout' => 2.0];
|
|
$requestopts = [];
|
|
if ($dockerproxy) {
|
|
$clientopts['base_uri'] = 'http://'.rtrim($dockerproxy, '/');
|
|
} else if (file_exists($dockersocket)) {
|
|
$clientopts['base_uri'] = 'http://localhost';
|
|
$requestopts = [
|
|
'curl' => [CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock']
|
|
];
|
|
}
|
|
|
|
// Make a request to docker for all containers.
|
|
try {
|
|
$response = (new \GuzzleHttp\Client($clientopts))->request('GET', '/containers/json', $requestopts);
|
|
} catch (\GuzzleHttp\Exception\ConnectException $e) {
|
|
throw new ConfigException('Did not get a response from Docker API endpoint');
|
|
}
|
|
$containers = json_decode($response->getBody());
|
|
if (is_null($containers)) {
|
|
throw new ConfigException('Docker returned an invalid json response for containers');
|
|
}
|
|
|
|
// Build a new array of Site() objects based on labels that have been added to
|
|
// containers returned by docker.
|
|
$sites = [];
|
|
foreach ($containers as $container) {
|
|
$labels = (array) $container->Labels;
|
|
// We can't build a Site() without at least a name and url.
|
|
if (!isset($labels['jump.name'], $labels['jump.url'])) {
|
|
continue;
|
|
}
|
|
// Convert dot-syntax labels into a proper multidimensional array
|
|
// and just use the top-level key "jump" as our site array.
|
|
$site = array_undot($labels)['jump'];
|
|
// jump.tags will have been given as a comma separated string so make this
|
|
// into an array.
|
|
if (isset($site['tags'])) {
|
|
// Explode the comma separated string into an array and trim any elements.
|
|
$site['tags'] = array_map('trim', explode(',', $site['tags']));
|
|
}
|
|
// Convert status array to an object and also explode list of allowed status codes to array.
|
|
if (isset($site['status'])) {
|
|
$site['status'] = (object) $site['status'];
|
|
if (isset($site['status']->allowed_status_codes)) {
|
|
$site['status']->allowed_status_codes = array_map('trim', explode(',', $site['status']->allowed_status_codes));
|
|
}
|
|
}
|
|
// Finally add this to the list of sites we will return.
|
|
$sites[] = new Site($this->config, $this->cache, (array) $site, $this->default);
|
|
}
|
|
return $sites;
|
|
}
|
|
|
|
/**
|
|
* Try to load the list of sites from sites.json. Sets defaults if any
|
|
* are found, which will then apply to any docker sites.
|
|
*
|
|
* Throws an exception if the file cannot be loaded, is empty, or cannot
|
|
* be decoded to an array,
|
|
*
|
|
* @return array Array of Site objects sites loaded from sites.json
|
|
* @throws ConfigException If sites.json cannot be found.
|
|
*/
|
|
private function load_sites_from_json(): array {
|
|
|
|
$docker = function() {
|
|
$dockerproxy = $this->config->get('dockerproxyurl');
|
|
$dockersocket = $this->config->get('dockersocket');
|
|
if ($dockerproxy || $dockersocket) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
$allsites = [];
|
|
// If we have been instructed to only look for sites via docker then
|
|
// don't worry about loading a local sites.json file and just
|
|
// return an empty $allsites array.
|
|
if ($this->config->get('dockeronlysites')) {
|
|
if (!$docker()) {
|
|
throw new ConfigException('DOCKERONLYSITES is specified but no Docker endpoint has been provided');
|
|
}
|
|
return $allsites;
|
|
}
|
|
// Try to load the sites.json file.
|
|
$rawjson = @file_get_contents($this->sitesfilelocation);
|
|
if ($rawjson === false) {
|
|
throw new ConfigException('There was a problem loading the sites.json file');
|
|
}
|
|
if ($rawjson === '') {
|
|
throw new ConfigException('The sites.json file is empty, if this is intentional please delete it');
|
|
}
|
|
// Do some checks to see if the JSON decodes into something
|
|
// like what we expect to see...
|
|
$decodedjson = json_decode($rawjson);
|
|
// First we'll assume maybe the old format for sites.json.
|
|
if (is_array($decodedjson)) {
|
|
$allsites = $decodedjson;
|
|
}
|
|
// Handle not having any sites in sites.json or having docker integration set up.
|
|
if (!isset($decodedjson->sites)) {
|
|
if (!$docker()) {
|
|
throw new ConfigException('The sites.json file is empty and docker integration is not set up either');
|
|
}
|
|
}
|
|
// Now check for the newer sites format from sites.json.
|
|
if (is_array($decodedjson->sites)) {
|
|
$allsites = $decodedjson->sites;
|
|
}
|
|
// Extract default site params into an array.
|
|
if (isset($decodedjson->default)) {
|
|
$this->default = (array) $decodedjson->default;
|
|
}
|
|
|
|
// Instantiate an actual Site() object for each element.
|
|
foreach ($allsites as $key => $item) {
|
|
$allsites[$key] = new Site($this->config, $this->cache, (array) $item, $this->default);
|
|
}
|
|
|
|
// Return the array of Site() objects, note we are in a callback
|
|
// so the return is not from the outer function.
|
|
return $allsites;
|
|
}
|
|
|
|
/**
|
|
* Returns an array of all loaded Site objects.
|
|
*
|
|
* @return array Array of all loaded Site objects.
|
|
*/
|
|
public function get_sites(): array {
|
|
return $this->loadedsites;
|
|
}
|
|
|
|
/**
|
|
* Return array of tags sorted alphabetically, minus the home tag.
|
|
*
|
|
* @return array Array of tag names.
|
|
*/
|
|
public function get_tags_for_template(): array {
|
|
$template_tags = [];
|
|
foreach ($this->tags as $tag) {
|
|
if ($tag === 'home') {
|
|
continue;
|
|
}
|
|
$template_tags[] = $tag;
|
|
}
|
|
sort($template_tags);
|
|
return $template_tags;
|
|
}
|
|
|
|
/**
|
|
* Given a URL, does that site exist in our list of sites?
|
|
*
|
|
* @param string $url The URL to search for.
|
|
* @return Site A matching Site object if found.
|
|
* @throws SiteNotFoundException If a site with given URL does not exist.
|
|
*/
|
|
public function get_site_by_url(string $url): Site {
|
|
$found = array_search($url, array_column($this->get_sites(), 'url'));
|
|
if ($found === false) {
|
|
throw new SiteNotFoundException($url);
|
|
}
|
|
return $this->loadedsites[$found];
|
|
}
|
|
|
|
/**
|
|
* Given a Site ID, does that site exist in our list of sites?
|
|
*
|
|
* @param string $id The Site ID to search for.
|
|
* @return Site A matching Site object if found.
|
|
* @throws SiteNotFoundException If a site with given Site ID does not exist.
|
|
*/
|
|
public function get_site_by_id(string $id): Site {
|
|
$found = array_search($id, array_column($this->get_sites(), 'id'));
|
|
if ($found === false) {
|
|
throw new SiteNotFoundException($id);
|
|
}
|
|
return $this->loadedsites[$found];
|
|
}
|
|
|
|
/**
|
|
* Returns an array of Site objects with a given tag.
|
|
*
|
|
* @param string $tagname The tag to look look up sites.
|
|
* @return array Array of Site objects with the given tag.
|
|
* @throws TagNotFoundException If there are no sites tagged with $tagname.
|
|
*/
|
|
public function get_sites_by_tag(string $tagname): array {
|
|
if (!in_array($tagname, $this->tags)) {
|
|
throw new TagNotFoundException($tagname);
|
|
}
|
|
$found = [];
|
|
foreach ($this->get_sites() as $site) {
|
|
if (in_array($tagname, $site->tags)) {
|
|
$found[] = $site;
|
|
}
|
|
}
|
|
return $found;
|
|
}
|
|
|
|
/**
|
|
* Get a list of cached sites from for use in the front end via JS. Some extra details
|
|
* added to each site in the list may not be already saved in the main sites cache.
|
|
*
|
|
* @return array Array of stdClass objects containing required site details.
|
|
*/
|
|
public function get_sites_for_frontend(): array {
|
|
$searchlist = [];
|
|
foreach ($this->loadedsites as $loadedsite) {
|
|
$site = new \stdClass();
|
|
$site->id = $loadedsite->id;
|
|
$site->name = $loadedsite->name;
|
|
$site->url = $loadedsite->url;
|
|
$site->tags = $loadedsite->tags;
|
|
$site->iconurl = '/api/icon?siteid='.$loadedsite->id;
|
|
$site->status = $this->cache->load(cachename: 'sites/status', key: $site->url) ?? null;
|
|
$searchlist[] = $site;
|
|
}
|
|
return $searchlist;
|
|
}
|
|
|
|
}
|