65 changed files with 3114 additions and 471 deletions
@ -0,0 +1,20 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org |
||||
|
||||
# top-most EditorConfig file |
||||
root = true |
||||
|
||||
# Unix-style newlines with a newline ending every file |
||||
[*] |
||||
end_of_line = lf |
||||
insert_final_newline = false |
||||
charset = utf-8 |
||||
indent_style = tab |
||||
trim_trailing_whitespace = true |
||||
|
||||
[*.{cpp,c,h,hpp,cxx}] |
||||
insert_final_newline = true |
||||
|
||||
# Yaml files |
||||
[*.{yml,yaml}] |
||||
indent_style = space |
||||
indent_size = 4 |
@ -1,11 +1,17 @@
|
||||
{ |
||||
"name": "timw4mail/hummingbird-anime-client", |
||||
"description": "A self-hosted anime/manga client for hummingbird.", |
||||
"license":"MIT", |
||||
"require": { |
||||
"guzzlehttp/guzzle": "5.3.*", |
||||
"filp/whoops": "1.1.*", |
||||
"filp/whoops": "dev-php7#fe32a402b086b21360e82013e8a0355575c7c6f4", |
||||
"aura/router": "2.2.*", |
||||
"aura/web": "2.0.*", |
||||
"aviat4ion/query": "2.0.*", |
||||
"robmorgan/phinx": "*", |
||||
"abeautifulsite/simpleimage": "*" |
||||
"aura/html": "2.*", |
||||
"aura/session": "2.*", |
||||
"aviat4ion/query": "2.5.*", |
||||
"robmorgan/phinx": "0.4.*", |
||||
"abeautifulsite/simpleimage": "2.5.*", |
||||
"szymach/c-pchart": "1.*" |
||||
} |
||||
} |
@ -0,0 +1,164 @@
|
||||
<?php |
||||
/** |
||||
* Base Configuration class |
||||
*/ |
||||
|
||||
namespace AnimeClient\Base; |
||||
|
||||
/** |
||||
* Wrapper for configuration values |
||||
*/ |
||||
class Config { |
||||
|
||||
/** |
||||
* Config object |
||||
* |
||||
* @var array |
||||
*/ |
||||
protected $config = []; |
||||
|
||||
/** |
||||
* Constructor |
||||
* |
||||
* @param array $config_files |
||||
*/ |
||||
public function __construct(Array $config_files=[]) |
||||
{ |
||||
// @codeCoverageIgnoreStart |
||||
if (empty($config_files)) |
||||
{ |
||||
require_once \_dir(CONF_DIR, 'config.php'); // $config |
||||
require_once \_dir(CONF_DIR, 'base_config.php'); // $base_config |
||||
|
||||
$this->config = array_merge($config, $base_config); |
||||
} |
||||
else // @codeCoverageIgnoreEnd |
||||
{ |
||||
$this->config = $config_files; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Getter for config values |
||||
* |
||||
* @param string $key |
||||
* @return mixed |
||||
*/ |
||||
public function __get($key) |
||||
{ |
||||
if (isset($this->config[$key])) |
||||
{ |
||||
return $this->config[$key]; |
||||
} |
||||
|
||||
return NULL; |
||||
} |
||||
|
||||
/** |
||||
* Get the base url for css/js/images |
||||
* |
||||
* @return string |
||||
*/ |
||||
public function asset_url(/*...*/) |
||||
{ |
||||
$args = func_get_args(); |
||||
$base_url = rtrim($this->url(""), '/'); |
||||
|
||||
$routing_config = $this->__get("routing"); |
||||
|
||||
|
||||
$base_url = "{$base_url}" . $routing_config['asset_path']; |
||||
|
||||
array_unshift($args, $base_url); |
||||
|
||||
return implode("/", $args); |
||||
} |
||||
|
||||
/** |
||||
* Get the base url from the config |
||||
* |
||||
* @param string $type - (optional) The controller |
||||
* @return string |
||||
*/ |
||||
public function base_url($type="anime") |
||||
{ |
||||
$config_path = trim($this->__get("{$type}_path"), "/"); |
||||
|
||||
// Set the appropriate HTTP host |
||||
$host = $_SERVER['HTTP_HOST']; |
||||
$path = ($config_path !== '') ? $config_path : ""; |
||||
|
||||
return implode("/", ['/', $host, $path]); |
||||
} |
||||
|
||||
/** |
||||
* Generate a proper url from the path |
||||
* |
||||
* @param string $path |
||||
* @return string |
||||
*/ |
||||
public function url($path) |
||||
{ |
||||
$path = trim($path, '/'); |
||||
|
||||
// Remove any optional parameters from the route |
||||
$path = preg_replace('`{/.*?}`i', '', $path); |
||||
|
||||
// Set the appropriate HTTP host |
||||
$host = $_SERVER['HTTP_HOST']; |
||||
|
||||
return "//{$host}/{$path}"; |
||||
} |
||||
|
||||
public function default_url($type) |
||||
{ |
||||
$type = trim($type); |
||||
$default_path = $this->__get("default_{$type}_path"); |
||||
|
||||
if ( ! is_null($default_path)) |
||||
{ |
||||
return $this->url($default_path); |
||||
} |
||||
|
||||
return ""; |
||||
} |
||||
|
||||
/** |
||||
* Generate full url path from the route path based on config |
||||
* |
||||
* @param string $path - (optional) The route path |
||||
* @param string $type - (optional) The controller (anime or manga), defaults to anime |
||||
* @return string |
||||
*/ |
||||
public function full_url($path="", $type="anime") |
||||
{ |
||||
$config_path = trim($this->__get("{$type}_path"), "/"); |
||||
$config_default_route = $this->__get("default_{$type}_path"); |
||||
|
||||
// Remove beginning/trailing slashes |
||||
$config_path = trim($config_path, '/'); |
||||
$path = trim($path, '/'); |
||||
|
||||
// Remove any optional parameters from the route |
||||
$path = preg_replace('`{/.*?}`i', '', $path); |
||||
|
||||
// Set the appropriate HTTP host |
||||
$host = $_SERVER['HTTP_HOST']; |
||||
|
||||
// Set the default view |
||||
if ($path === '') |
||||
{ |
||||
$path .= trim($config_default_route, '/'); |
||||
if ($this->__get('default_to_list_view')) $path .= '/list'; |
||||
} |
||||
|
||||
// Set an leading folder |
||||
/*if ($config_path !== '') |
||||
{ |
||||
$path = "{$config_path}/{$path}"; |
||||
}*/ |
||||
|
||||
return "//{$host}/{$path}"; |
||||
} |
||||
} |
||||
// End of config.php |
@ -0,0 +1,50 @@
|
||||
<?php |
||||
|
||||
namespace Animeclient\Base; |
||||
|
||||
/** |
||||
* Wrapper of Aura container to be in the anime client namespace |
||||
*/ |
||||
class Container { |
||||
|
||||
/** |
||||
* @var array |
||||
*/ |
||||
protected $container = []; |
||||
|
||||
/** |
||||
* Constructor |
||||
*/ |
||||
public function __construct(array $values = []) |
||||
{ |
||||
$this->container = $values; |
||||
} |
||||
|
||||
/** |
||||
* Get a value |
||||
* |
||||
* @param string $key |
||||
* @retun mixed |
||||
*/ |
||||
public function get($key) |
||||
{ |
||||
if (array_key_exists($key, $this->container)) |
||||
{ |
||||
return $this->container[$key]; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Add a value to the container |
||||
* |
||||
* @param string $key |
||||
* @param mixed $value |
||||
* @return Container |
||||
*/ |
||||
public function set($key, $value) |
||||
{ |
||||
$this->container[$key] = $value; |
||||
return $this; |
||||
} |
||||
} |
||||
// End of Container.php |
@ -0,0 +1,283 @@
|
||||
<?php |
||||
/** |
||||
* Base Controller |
||||
*/ |
||||
namespace AnimeClient\Base; |
||||
|
||||
/** |
||||
* Base class for controllers, defines output methods |
||||
*/ |
||||
class Controller { |
||||
|
||||
/** |
||||
* The global configuration object |
||||
* @var object $config |
||||
*/ |
||||
protected $config; |
||||
|
||||
/** |
||||
* Request object |
||||
* @var object $request |
||||
*/ |
||||
protected $request; |
||||
|
||||
/** |
||||
* Response object |
||||
* @var object $response |
||||
*/ |
||||
protected $response; |
||||
|
||||
/** |
||||
* The api model for the current controller |
||||
* @var object |
||||
*/ |
||||
protected $model; |
||||
|
||||
/** |
||||
* Common data to be sent to views |
||||
* @var array |
||||
*/ |
||||
protected $base_data = [ |
||||
'url_type' => 'anime', |
||||
'other_type' => 'manga', |
||||
'nav_routes' => [] |
||||
]; |
||||
|
||||
/** |
||||
* Constructor |
||||
* |
||||
* @param Config $config |
||||
* @param array $web |
||||
*/ |
||||
public function __construct(Container $container) |
||||
{ |
||||
$this->config = $container->get('config'); |
||||
$this->base_data['config'] = $this->config; |
||||
|
||||
$this->request = $container->get('request'); |
||||
$this->response = $container->get('response'); |
||||
} |
||||
|
||||
/** |
||||
* Destructor |
||||
* |
||||
* @codeCoverageIgnore |
||||
*/ |
||||
public function __destruct() |
||||
{ |
||||
$this->output(); |
||||
} |
||||
|
||||
/** |
||||
* Get a class member |
||||
* |
||||
* @param string $key |
||||
* @return object |
||||
*/ |
||||
public function __get($key) |
||||
{ |
||||
$allowed = ['request', 'response', 'config']; |
||||
|
||||
if (in_array($key, $allowed)) |
||||
{ |
||||
return $this->$key; |
||||
} |
||||
|
||||
return NULL; |
||||
} |
||||
|
||||
/** |
||||
* Get the string output of a partial template |
||||
* |
||||
* @codeCoverageIgnore |
||||
* @param string $template |
||||
* @param array|object $data |
||||
* @return string |
||||
*/ |
||||
public function load_partial($template, $data=[]) |
||||
{ |
||||
if (isset($this->base_data)) |
||||
{ |
||||
$data = array_merge($this->base_data, $data); |
||||
} |
||||
|
||||
global $router, $defaultHandler; |
||||
$route = $router->get_route(); |
||||
$data['route_path'] = ($route) ? $router->get_route()->path : ""; |
||||
|
||||
$defaultHandler->addDataTable('Template Data', $data); |
||||
|
||||
$template_path = _dir(SRC_DIR, 'views', "{$template}.php"); |
||||
|
||||
if ( ! is_file($template_path)) |
||||
{ |
||||
throw new \InvalidArgumentException("Invalid template : {$path}"); |
||||
} |
||||
|
||||
ob_start(); |
||||
extract($data); |
||||
include _dir(SRC_DIR, 'views', 'header.php'); |
||||
include $template_path; |
||||
include _dir(SRC_DIR, 'views', 'footer.php'); |
||||
$buffer = ob_get_contents(); |
||||
ob_end_clean(); |
||||
|
||||
return $buffer; |
||||
} |
||||
|
||||
/** |
||||
* Output a template to HTML, using the provided data |
||||
* |
||||
* @codeCoverageIgnore |
||||
* @param string $template |
||||
* @param array|object $data |
||||
* @return void |
||||
*/ |
||||
public function outputHTML($template, $data=[]) |
||||
{ |
||||
$buffer = $this->load_partial($template, $data); |
||||
|
||||
$this->response->content->setType('text/html'); |
||||
$this->response->content->set($buffer); |
||||
} |
||||
|
||||
/** |
||||
* Output json with the proper content type |
||||
* |
||||
* @param mixed $data |
||||
* @return void |
||||
*/ |
||||
public function outputJSON($data) |
||||
{ |
||||
if ( ! is_string($data)) |
||||
{ |
||||
$data = json_encode($data); |
||||
} |
||||
|
||||
$this->response->content->setType('application/json'); |
||||
$this->response->content->set($data); |
||||
} |
||||
|
||||
/** |
||||
* Redirect to the selected page |
||||
* |
||||
* @codeCoverageIgnore |
||||
* @param string $url |
||||
* @param int $code |
||||
* @param string $type |
||||
* @return void |
||||
*/ |
||||
public function redirect($url, $code, $type="anime") |
||||
{ |
||||
$url = $this->config->full_url($url, $type); |
||||
|
||||
$this->response->redirect->to($url, $code); |
||||
} |
||||
|
||||
/** |
||||
* Add a message box to the page |
||||
* |
||||
* @codeCoverageIgnore |
||||
* @param string $type |
||||
* @param string $message |
||||
* @return string |
||||
*/ |
||||
public function show_message($type, $message) |
||||
{ |
||||
return $this->load_partial('message', [ |
||||
'stat_class' => $type, |
||||
'message' => $message |
||||
]); |
||||
} |
||||
|
||||
/** |
||||
* Clear the api session |
||||
* |
||||
* @codeCoverageIgnore |
||||
* @return void |
||||
*/ |
||||
public function logout() |
||||
{ |
||||
session_destroy(); |
||||
$this->response->redirect->seeOther($this->config->full_url('')); |
||||
} |
||||
|
||||
/** |
||||
* Show the login form |
||||
* |
||||
* @codeCoverageIgnore |
||||
* @param string $status |
||||
* @return void |
||||
*/ |
||||
public function login($status="") |
||||
{ |
||||
$message = ""; |
||||
|
||||
if ($status != "") |
||||
{ |
||||
$message = $this->show_message('error', $status); |
||||
} |
||||
|
||||
$this->outputHTML('login', [ |
||||
'title' => 'Api login', |
||||
'message' => $message |
||||
]); |
||||
} |
||||
|
||||
/** |
||||
* Attempt to log in with the api |
||||
* |
||||
* @return void |
||||
*/ |
||||
public function login_action() |
||||
{ |
||||
if ( |
||||
$this->model->authenticate( |
||||
$this->config->hummingbird_username, |
||||
$this->request->post->get('password') |
||||
) |
||||
) |
||||
{ |
||||
$this->response->redirect->afterPost($this->config->full_url('', $this->base_data['url_type'])); |
||||
return; |
||||
} |
||||
|
||||
$this->login("Invalid username or password."); |
||||
} |
||||
|
||||
/** |
||||
* Send the appropriate response |
||||
* |
||||
* @codeCoverageIgnore |
||||
* @return void |
||||
*/ |
||||
private function output() |
||||
{ |
||||
// send status |
||||
@header($this->response->status->get(), true, $this->response->status->getCode()); |
||||
|
||||
// headers |
||||
foreach($this->response->headers->get() as $label => $value) |
||||
{ |
||||
@header("{$label}: {$value}"); |
||||
} |
||||
|
||||
// cookies |
||||
foreach($this->response->cookies->get() as $name => $cookie) |
||||
{ |
||||
@setcookie( |
||||
$name, |
||||
$cookie['value'], |
||||
$cookie['expire'], |
||||
$cookie['path'], |
||||
$cookie['domain'], |
||||
$cookie['secure'], |
||||
$cookie['httponly'] |
||||
); |
||||
} |
||||
|
||||
// send the actual response |
||||
echo $this->response->content->get(); |
||||
} |
||||
} |
||||
// End of BaseController.php |
@ -0,0 +1,112 @@
|
||||
<?php |
||||
/** |
||||
* Base for base models |
||||
*/ |
||||
namespace AnimeClient\Base; |
||||
|
||||
use abeautifulsite\SimpleImage; |
||||
|
||||
/** |
||||
* Common base for all Models |
||||
*/ |
||||
class Model { |
||||
|
||||
/** |
||||
* The global configuration object |
||||
* @var Config |
||||
*/ |
||||
protected $config; |
||||
|
||||
/** |
||||
* The container object |
||||
* @var Container |
||||
*/ |
||||
protected $container; |
||||
|
||||
/** |
||||
* Constructor |
||||
*/ |
||||
public function __construct(Container $container) |
||||
{ |
||||
$this->container = $container; |
||||
$this->config = $container->get('config'); |
||||
} |
||||
|
||||
/** |
||||
* Get the path of the cached version of the image. Create the cached image |
||||
* if the file does not already exist |
||||
* |
||||
* @codeCoverageIgnore |
||||
* @param string $api_path - The original image url |
||||
* @param string $series_slug - The part of the url with the series name, becomes the image name |
||||
* @param string $type - Anime or Manga, controls cache path |
||||
* @return string - the frontend path for the cached image |
||||
*/ |
||||
public function get_cached_image($api_path, $series_slug, $type="anime") |
||||
{ |
||||
$api_path = str_replace("jjpg", "jpg", $api_path); |
||||
$path_parts = explode('?', basename($api_path)); |
||||
$path = current($path_parts); |
||||
$ext_parts = explode('.', $path); |
||||
$ext = end($ext_parts); |
||||
|
||||
// Workaround for some broken extensions |
||||
if ($ext == "jjpg") $ext = "jpg"; |
||||
|
||||
// Failsafe for weird urls |
||||
if (strlen($ext) > 3) return $api_path; |
||||
|
||||
$cached_image = "{$series_slug}.{$ext}"; |
||||
$cached_path = "{$this->config->img_cache_path}/{$type}/{$cached_image}"; |
||||
|
||||
// Cache the file if it doesn't already exist |
||||
if ( ! file_exists($cached_path)) |
||||
{ |
||||
if (ini_get('allow_url_fopen')) |
||||
{ |
||||
copy($api_path, $cached_path); |
||||
} |
||||
elseif (function_exists('curl_init')) |
||||
{ |
||||
$ch = curl_init($api_path); |
||||
$fp = fopen($cached_path, 'wb'); |
||||
curl_setopt_array($ch, [ |
||||
CURLOPT_FILE => $fp, |
||||
CURLOPT_HEADER => 0 |
||||
]); |
||||
curl_exec($ch); |
||||
curl_close($ch); |
||||
fclose($ch); |
||||
} |
||||
else |
||||
{ |
||||
throw new DomainException("Couldn't cache images because they couldn't be downloaded."); |
||||
} |
||||
|
||||
// Resize the image |
||||
if ($type == 'anime') |
||||
{ |
||||
$resize_width = 220; |
||||
$resize_height = 319; |
||||
$this->_resize($cached_path, $resize_width, $resize_height); |
||||
} |
||||
} |
||||
|
||||
return "/public/images/{$type}/{$cached_image}"; |
||||
} |
||||
|
||||
/** |
||||
* Resize an image |
||||
* |
||||
* @codeCoverageIgnore |
||||
* @param string $path |
||||
* @param string $width |
||||
* @param string $height |
||||
*/ |
||||
private function _resize($path, $width, $height) |
||||
{ |
||||
$img = new SimpleImage($path); |
||||
$img->resize($width,$height)->save(); |
||||
} |
||||
} |
||||
// End of BaseModel.php |
@ -0,0 +1,81 @@
|
||||
<?php |
||||
/** |
||||
* Base API Model |
||||
*/ |
||||
namespace AnimeClient\Base\Model; |
||||
|
||||
use \GuzzleHttp\Client; |
||||
use \GuzzleHttp\Cookie\CookieJar; |
||||
use \AnimeClient\Base\Container; |
||||
|
||||
/** |
||||
* Base model for api interaction |
||||
*/ |
||||
class API extends \AnimeClient\Base\Model { |
||||
|
||||
/** |
||||
* Base url for making api requests |
||||
* @var string |
||||
*/ |
||||
protected $base_url = ''; |
||||
|
||||
/** |
||||
* The Guzzle http client object |
||||
* @var object |
||||
*/ |
||||
protected $client; |
||||
|
||||
/** |
||||
* Cookie jar object for api requests |
||||
* @var object |
||||
*/ |
||||
protected $cookieJar; |
||||
|
||||
/** |
||||
* Constructor |
||||
*/ |
||||
public function __construct(Container $container) |
||||
{ |
||||
parent::__construct($container); |
||||
$this->cookieJar = new CookieJar(); |
||||
$this->client = new Client([ |
||||
'base_url' => $this->base_url, |
||||
'defaults' => [ |
||||
'cookies' => $this->cookieJar, |
||||
'headers' => [ |
||||
'User-Agent' => $_SERVER['HTTP_USER_AGENT'], |
||||
'Accept-Encoding' => 'application/json' |
||||
], |
||||
'timeout' => 5, |
||||
'connect_timeout' => 5 |
||||
] |
||||
]); |
||||
} |
||||
|
||||
/** |
||||
* Attempt login via the api |
||||
* |
||||
* @codeCoverageIgnore |
||||
* @param string $username |
||||
* @param string $password |
||||
* @return bool |
||||
*/ |
||||
public function authenticate($username, $password) |
||||
{ |
||||
$result = $this->client->post('https://hummingbird.me/api/v1/users/authenticate', [ |
||||
'body' => [ |
||||
'username' => $username, |
||||
'password' => $password |
||||
] |
||||
]); |
||||
|
||||
if ($result->getStatusCode() === 201) |
||||
{ |
||||
$_SESSION['hummingbird_anime_token'] = $result->json(); |
||||
return TRUE; |
||||
} |
||||
|
||||
return FALSE; |
||||
} |
||||
} |
||||
// End of BaseApiModel.php |
@ -0,0 +1,34 @@
|
||||
<?php |
||||
/** |
||||
* Base DB model |
||||
*/ |
||||
namespace AnimeClient\Base\Model; |
||||
|
||||
use AnimeClient\Base\Container; |
||||
|
||||
/** |
||||
* Base model for database interaction |
||||
*/ |
||||
class DB extends \AnimeClient\Base\Model { |
||||
/** |
||||
* The query builder object |
||||
* @var object $db |
||||
*/ |
||||
protected $db; |
||||
|
||||
/** |
||||
* The database connection information array |
||||
* @var array $db_config |
||||
*/ |
||||
protected $db_config; |
||||
|
||||
/** |
||||
* Constructor |
||||
*/ |
||||
public function __construct(Container $container) |
||||
{ |
||||
parent::__construct($container); |
||||
$this->db_config = $this->config->database; |
||||
} |
||||
} |
||||
// End of BaseDBModel.php |
@ -0,0 +1,230 @@
|
||||
<?php |
||||
/** |
||||
* Routing logic |
||||
*/ |
||||
namespace AnimeClient\Base; |
||||
|
||||
use \Aura\Web\Request; |
||||
use \Aura\Web\Response; |
||||
|
||||
/** |
||||
* Basic routing/ dispatch |
||||
*/ |
||||
class Router { |
||||
|
||||
/** |
||||
* The route-matching object |
||||
* @var object $router |
||||
*/ |
||||
protected $router; |
||||
|
||||
/** |
||||
* The global configuration object |
||||
* @var object $config |
||||
*/ |
||||
protected $config; |
||||
|
||||
/** |
||||
* Class wrapper for input superglobals |
||||
* @var object |
||||
*/ |
||||
protected $request; |
||||
|
||||
/** |
||||
* Array containing request and response objects |
||||
* @var array $web |
||||
*/ |
||||
protected $web; |
||||
|
||||
|