@ -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; | |||
/** | |||
* Routes added to router | |||
* @var array $output_routes | |||
*/ | |||
protected $output_routes; | |||
/** | |||
* Injection Container | |||
* @var Container $container | |||
*/ | |||
protected $container; | |||
/** | |||
* Constructor | |||
* | |||
* @param Config $config | |||
* @param Router $router | |||
* @param Request $request | |||
* @param Response $response | |||
*/ | |||
public function __construct(Container $container) | |||
{ | |||
$this->config = $container->get('config'); | |||
$this->router = $container->get('aura-router'); | |||
$this->request = $container->get('request'); | |||
$this->web = [$this->request, $container->get('response')]; | |||
$this->output_routes = $this->_setup_routes(); | |||
$this->container = $container; | |||
} | |||
/** | |||
* Get the current route object, if one matches | |||
* | |||
* @return object | |||
*/ | |||
public function get_route() | |||
{ | |||
$error_handler = $this->container->get('error-handler'); | |||
$raw_route = $this->request->server->get('PATH_INFO'); | |||
$route_path = "/" . trim($raw_route, '/'); | |||
$error_handler->addDataTable('Route Info', [ | |||
'route_path' => $route_path | |||
]); | |||
$route = $this->router->match($route_path, $_SERVER); | |||
return $route; | |||
} | |||
/** | |||
* Get list of routes applied | |||
* | |||
* @return array | |||
*/ | |||
public function get_output_routes() | |||
{ | |||
return $this->output_routes; | |||
} | |||
/** | |||
* Handle the current route | |||
* | |||
* @codeCoverageIgnore | |||
* @param [object] $route | |||
* @return void | |||
*/ | |||
public function dispatch($route = NULL) | |||
{ | |||
$error_handler = $this->container->get('error-handler'); | |||
if (is_null($route)) | |||
{ | |||
$route = $this->get_route(); | |||
$error_handler->addDataTable('route_args', (array)$route); | |||
} | |||
if ( ! $route) | |||
{ | |||
$failure = $this->router->getFailedRoute(); | |||
$error_handler->addDataTable('failed_route', (array)$failure); | |||
} | |||
else | |||
{ | |||
list($controller_name, $action_method) = $route->params['action']; | |||
$params = (isset($route->params['params'])) ? $route->params['params'] : []; | |||
if ( ! empty($route->tokens)) | |||
{ | |||
foreach($route->tokens as $key => $v) | |||
{ | |||
if (array_key_exists($key, $route->params)) | |||
{ | |||
$params[$key] = $route->params[$key]; | |||
} | |||
} | |||
} | |||
} | |||