Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
ut4 committed Feb 16, 2020
2 parents 0039e9b + 2f80fcb commit 67b7fdc
Show file tree
Hide file tree
Showing 16 changed files with 364 additions and 162 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ A tight set of classes that helps you write killer applications. Does not try to

- PHP ^7.1
- [altorouter/altorouter ^1.2](https://github.com/dannyvankooten/AltoRouter)
- [rdlowrey/auryn ^1.4](https://github.com/rdlowrey/auryn) (depencency injection)
- [phpmailer/phpmailer ^6.1](https://github.com/rdlowrey/auryn) (password reset)
- [rdlowrey/auryn ^1.4](https://github.com/rdlowrey/auryn) (dependency injection)
- [phpmailer/phpmailer ^6.1](https://github.com/PHPMailer/PHPMailer) (password reset)

# License

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ut4/pike",
"description": "A minimal framework.",
"description": "A minimalistic application framework.",
"type": "library",
"license": "Apache-2.0",
"require": {
Expand Down
25 changes: 1 addition & 24 deletions example/index.php
Original file line number Diff line number Diff line change
@@ -1,26 +1,3 @@
<?php

require dirname(__DIR__) . '/vendor/autoload.php';

class SomeModule {
public static function init(\stdClass $ctx) {
$ctx->router->map('GET', '/protected',
[SomeController::class, 'handleProtectdRequest', true]
);
$ctx->router->map('GET', '/[**:stuff]',
[SomeController::class, 'handleSomeRequest', false]
);
}
}

class SomeController {
public function handleProtectdRequest(\Pike\Response $res) {
$res->json(['message' => 'Hello <authenticateduser>']);
}
public function handleSomeRequest(\Pike\Request $req, \Pike\Response $res) {
$res->json(['hello' => $req->params->stuff]);
}
}

$app = \Pike\App::create([SomeModule::class], []);
$app->handleRequest(\Pike\Request::createFromGlobals('', $_GET['q'] ?? '/'));
// todo
137 changes: 90 additions & 47 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,78 +2,105 @@

namespace Pike;

use AltoRouter;
use Auryn\Injector;
use Pike\Auth\Authenticator;
use Pike\Auth\Crypto;
use Pike\Auth\Internal\CachingServicesFactory;

final class App {
public const VERSION = '0.1.0';
public const VERSION = '0.2.0';
public const SERVICE_DB = 'db';
public const SERVICE_AUTH = 'auth';
public const MAKE_AUTOMATICALLY = '@auto';
private $ctx;
private $moduleClsPaths;
private $makeInjector;
/**
* @param \stdClass $ctx
* @param string[] $modules
* @param null|fn(): \Auryn\Injector $makeInjector
*/
public function __construct(\stdClass $ctx, array $modules) {
private function __construct(\stdClass $ctx,
array $modules,
callable $makeInjector = null) {
$this->ctx = $ctx;
$this->moduleClsPaths = $modules;
$this->makeInjector = $makeInjector;
}
/**
* RadCMS:n entry-point.
*
* @param \Pike\Request|string $request
* @param string|\Auryn\Injector ...$args
* @param string $urlPath = null
*/
public function handleRequest($request, ...$args) {
public function handleRequest($request, $urlPath = null) {
if (is_string($request))
$request = Request::createFromGlobals($request, $args[0] ?? null);
$request = Request::createFromGlobals($request, $urlPath);
if (($match = $this->ctx->router->match($request->path, $request->method))) {
$request->params = (object)$match['params'];
$request->name = $match['name'];
// @allow \Pike\PikeException
[$ctrlClassPath, $ctrlMethodName, $requireAuth] =
[$ctrlClassPath, $ctrlMethodName, $usersRouteCtx] =
$this->validateRouteMatch($match, $request);
$request->user = $this->ctx->auth->getIdentity();
if ($requireAuth && !$request->user) {
(new Response(403))->json(['err' => 'Login required']);
} else {
$injector = $this->setupIocContainer(array_pop($args), $request);
$injector->execute($ctrlClassPath . '::' . $ctrlMethodName);
}
$middlewareLoopState = (object)['req' => $request,
'res' => $this->ctx->res ?? new Response()];
$middlewareLoopState->req->routeCtx = (object)[
'myData' => $usersRouteCtx,
'name' => $match['name'],
];
// @allow \Pike\PikeException
$this->execMiddlewareCallback(0, $middlewareLoopState);
if ($middlewareLoopState->res->sendIfReady())
return;
$injector = $this->setupIocContainer($middlewareLoopState);
$injector->execute($ctrlClassPath . '::' . $ctrlMethodName);
$middlewareLoopState->res->sendIfReady();
} else {
throw new PikeException("No route for {$request->method} {$request->path}");
}
}
/**
* @param int $index
* @param {req: \Pike\Request, res: \Pike\Response} $state
*/
private function execMiddlewareCallback($index, $state) {
$ware = $this->ctx->router->middleware[$index] ?? null;
if (!$ware || $state->res->isSent()) return;
// @allow \Pike\PikeException
call_user_func($ware->fn, $state->req, $state->res, function () use ($index, $state) {
$this->execMiddlewareCallback($index + 1, $state);
});
}
/**
* @param array $match
* @return array [string, string, bool]
* @param \Pike\Request $req
* @return array [string, string, <usersRouteCtx>|null]
* @throws \Pike\PikeException
*/
private function validateRouteMatch($match, $req) {
$routeInfo = $match['target'];
if (!is_array($routeInfo) ||
count($routeInfo) !== 3 ||
$numItems = is_array($routeInfo) ? count($routeInfo) : 0;
if ($numItems < 2 ||
!is_string($routeInfo[0]) ||
!is_string($routeInfo[1]) ||
!is_bool($routeInfo[2])) {
!is_string($routeInfo[1]))
throw new PikeException(
'A route (' . $req->method . ' ' . $req->path . ') must be an array ' .
'[\'Ctrl\\Class\\Path\', \'methodName\', \'requireAuth\' ? true : false].',
'A route (' . $req->method . ' ' . $req->path . ') must return an' .
' array [\'Ctrl\\Class\\Path\', \'methodName\', <yourCtxVarHere>].',
PikeException::BAD_INPUT);
}
if ($numItems < 3)
$routeInfo[] = null;
return $routeInfo;
}
/**
* @param \Auryn\Injector|string $candidate
* @param \Pike\Request $request
* @param {req: \Pike\Request, res: \Pike\Response} $http
* @return \Auryn\Injector
*/
private function setupIocContainer($candidate, $request) {
$container = !($candidate instanceof Injector) ? new Injector() : $candidate;
$container->share($this->ctx->db);
$container->share($this->ctx->auth);
$container->share($request);
private function setupIocContainer($http) {
$container = !$this->makeInjector
? new Injector()
: call_user_func($this->makeInjector);
$container->share($http->req);
$container->share($http->res);
if (isset($this->ctx->db)) $container->share($this->ctx->db);
if (isset($this->ctx->auth)) $container->share($this->ctx->auth);
$container->alias(FileSystemInterface::class, FileSystem::class);
$container->alias(SessionInterface::class, NativeSession::class);
foreach ($this->moduleClsPaths as $clsPath) {
Expand All @@ -86,41 +113,57 @@ private function setupIocContainer($candidate, $request) {
* @param callable[] $modules
* @param string|array $config = null
* @param object $ctx = null
* @param fn(): \Auryn\Injector $makeInjector = null
* @return \Pike\App
*/
public static function create(array $modules, $config = null, $ctx = null) {
[$config, $ctx] = self::normalizeConfig($config, $ctx);
public static function create(array $modules,
$config = null,
$ctx = null,
callable $makeInjector = null) {
[$config, $ctx] = self::getNormalizedSettings($config, $ctx);
//
if (!isset($ctx->db))
$ctx->db = new Db($config);
if (!isset($ctx->router)) {
$ctx->router = new AltoRouter();
$ctx->router = new Router();
$ctx->router->addMatchTypes(['w' => '[0-9A-Za-z_-]++']);
}
if (!isset($ctx->auth)) {
if (($ctx->{self::SERVICE_DB} ?? null) === self::MAKE_AUTOMATICALLY) {
if (!is_array($config))
throw new PikeException('cant orovide db without config',
PikeException::BAD_INPUT);
$ctx->db = new Db($config);
}
if (($ctx->{self::SERVICE_AUTH} ?? '') === self::MAKE_AUTOMATICALLY) {
if (!isset($ctx->db))
throw new PikeException('cant provide auth without db',
PikeException::BAD_INPUT);
$ctx->auth = new Authenticator(new CachingServicesFactory($ctx->db,
new Crypto()));
}
//
foreach ($modules as $clsPath) {
if (!method_exists($clsPath, 'init'))
throw new PikeException('Module must have init() -method',
throw new PikeException("Module ({$clsPath}) must have init()-method",
PikeException::BAD_INPUT);
call_user_func([$clsPath, 'init'], $ctx);
}
//
return new static($ctx, $modules);
return new static($ctx, $modules, $makeInjector);
}
/**
* @return array [array, object]
* @param array|string|null $confix
* @param object|array $ctx
* @return array [array|null, object]
*/
private static function normalizeConfig($config, $ctx) {
if (is_string($config))
private static function getNormalizedSettings($config, $ctx) {
if (is_string($config) && strlen($config))
$config = require $config;
if (!$ctx) {
if (!is_array($config))
throw new PikeException('Can\'t make db without config',
PikeException::BAD_INPUT);
$ctx = (object)['db' => null, 'router' => null, 'auth' => null];
if (!($ctx instanceof \stdClass)) {
if (is_array($ctx))
$ctx = $ctx ? (object)$ctx : new \stdClass;
elseif ($ctx === null)
$ctx = new \stdClass;
else
throw new PikeException('ctx must be object|array');
}
return [$config, $ctx];
}
Expand Down
15 changes: 12 additions & 3 deletions src/Auth/Authenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,18 @@ public function getIdentity() {
*
* @param string $username
* @param string $password
* @param fn(object $user): mixed $serializeUserForSession = null
* @return bool
* @throws \Pike\PikeException
*/
public function login($username, $password) {
return $this->services->makeUserManager()->login($username, $password);
public function login($username, $password, callable $serializeUserForSession = null) {
if (($user = $this->services->makeUserManager()->login($username, $password))) {
$this->services->makeSession()->put('user', $serializeUserForSession
? call_user_func($serializeUserForSession, $user)
: $user->id);
return true;
}
return false;
}
/**
* Kirjaa käyttäjän ulos poistamalla käyttäjän tiedot sessiosta.
Expand All @@ -61,7 +68,9 @@ public function logout() {
*/
public function requestPasswordReset($usernameOrEmail, callable $makeEmailSettings) {
return $this->services->makeUserManager()
->requestPasswordReset($usernameOrEmail, $makeEmailSettings);
->requestPasswordReset($usernameOrEmail,
$makeEmailSettings,
$this->services->makeMailer());
}
/**
* ...
Expand Down
23 changes: 10 additions & 13 deletions src/Auth/Internal/UserManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,14 @@
class UserManager {
private $persistence;
private $crypto;
private $services;
private $lastErrReason;
/**
* @param \Pike\Auth\Internal\UserRepository $persistence
* @param \Pike\Auth\Crypto $crypto
* @param \Pike\Auth\Internal\CachingServicesFactory $services
*/
public function __construct(UserRepository $persistence,
Crypto $crypto,
CachingServicesFactory $services) {
public function __construct(UserRepository $persistence, Crypto $crypto) {
$this->persistence = $persistence;
$this->crypto = $crypto;
$this->services = $services;
}
/**
* Asettaa käyttäjän $username kirjautuneeksi käyttäjäksi, tai heittää
Expand All @@ -30,7 +25,7 @@ public function __construct(UserRepository $persistence,
*
* @param string $username
* @param string $password
* @return bool
* @return object
* @throws \Pike\PikeException
*/
public function login($username, $password) {
Expand All @@ -41,18 +36,20 @@ public function login($username, $password) {
if (!$this->crypto->verifyPass($password, $user->passwordHash))
throw new PikeException('Invalid password',
Authenticator::INVALID_CREDENTIAL);
$this->services->makeSession()->put('user', $user->id);
return true;
return $user;
}
/**
* ...
*
* @param string $usernameOrEmail
* @param fn({id: string, username: string, email: string, passwordHash: string, resetKey: string, resetRequestedAt: int} $user, string $resetKey, {fromAddress: string, fromName?: string, toAddress: string, toName?: string, subject: string, body: string} $settingsOut): void $makeEmailSettings
* @param \Pike\Auth\Internal\PhpMailerMailer $mailer
* @return bool
* @throws \Pike\PikeException
*/
public function requestPasswordReset($usernameOrEmail, callable $makeEmailSettings) {
public function requestPasswordReset($usernameOrEmail,
callable $makeEmailSettings,
$mailer) {
$user = $this->persistence->getUser('username = ? OR email = ?',
[$usernameOrEmail, $usernameOrEmail]);
if (!$user)
Expand All @@ -69,15 +66,15 @@ public function requestPasswordReset($usernameOrEmail, callable $makeEmailSettin
$user, $key);
// @allow \Pike\PikeException
$this->persistence->runInTransaction(function () use ($key,
$emailSettings,
$user) {
$emailSettings,
$user,
$mailer) {
$data = new \stdClass;
$data->resetKey = $key;
$data->resetRequestedAt = time();
if (!$this->persistence->updateUser($data, 'id = ?', [$user->id]))
throw new PikeException('Failed to insert resetInfo',
PikeException::FAILED_DB_OP);
$mailer = $this->services->makeMailer();
if (!$mailer->sendMail($emailSettings))
throw new PikeException('Failed to send mail: ' .
$mailer->getLastError()->getMessage(),
Expand Down
7 changes: 4 additions & 3 deletions src/Auth/Internal/UserRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ public function putUser(object $user) {
/**
* @param string $wherePlaceholders
* @param array $whereVals
* @return {id: string, username: string, email: string, passwordHash: string, resetKey: string, resetRequestedAt: int}|null
* @return {id: string, username: string, email: string, passwordHash: string, role: string, resetKey: string, resetRequestedAt: int}|null
*/
public function getUser($wherePlaceholders, $whereVals) {
try {
$row = $this->db->fetchOne('SELECT `id`,`username`,`email`,`passwordHash`' .
',`resetKey`,`resetRequestedAt`' .
',`role`,`resetKey`,`resetRequestedAt`' .
' FROM ${p}users' .
' WHERE ' . $wherePlaceholders,
$whereVals);
Expand All @@ -38,7 +38,7 @@ public function getUser($wherePlaceholders, $whereVals) {
}
}
/**
* @param {username?: string, email?: string, passwordHash?: string, resetKey?: string, resetRequestedAt?: int} $data Olettaa että validi
* @param {username?: string, email?: string, passwordHash?: string, role?: string, resetKey?: string, resetRequestedAt?: int} $data Olettaa että validi
* @param string $wherePlaceholders
* @param array $whereVals
* @return int $numAffectedRows
Expand Down Expand Up @@ -69,6 +69,7 @@ function makeUser($row) {
'username' => $row['username'],
'email' => $row['email'],
'passwordHash' => $row['passwordHash'],
'role' => (int)$row['role'],
'resetKey' => $row['resetKey'] ?? null,
'resetRequestedAt' => isset($row['resetRequestedAt'])
? (int)$row['resetRequestedAt']
Expand Down
Loading

0 comments on commit 67b7fdc

Please sign in to comment.