Created
April 18, 2018 12:11
-
-
Save FyiurAmron/f9d6b86ece72e8473ff54ea180ebce46 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 | |
namespace AppBundle\Controller\HttpApi; | |
use Symfony\Bundle\FrameworkBundle\Controller\Controller; | |
use Symfony\Component\HttpFoundation\{ | |
Request, | |
Response | |
}; | |
use AppBundle\RmSchema; | |
use AppBundle\Utils\VaxUtils; | |
use AppBundle\Entity\{ | |
User, | |
UserContext, | |
Campaign, | |
City, | |
Unit, | |
Clazz | |
}; | |
abstract class HttpController extends Controller { | |
// static part | |
const META_SUFFIX = '.meta.json'; | |
const CONTEXT_NOT_SET = -1; | |
public static $debug = true; | |
private static $init = false; | |
private static $accessMap; | |
public static function init() { | |
if ( self::$init ) { | |
return; | |
} | |
VaxUtils::init(); | |
$accessMap = new class{}; | |
$accessMap->ROLE_CAMPAIGN_COORD = [ | |
'mailTo' => true, | |
'addCampaign' => true, | |
'manageCampaign' => true, | |
'progressThroughCampaign' => true, | |
'processFinishedCampaign' => true, | |
'removeCampaign' => true, | |
'addCity' => true, | |
'manageCity' => true, | |
'putFile' => true, | |
'deleteFile' => true, | |
'statsCities' => true, | |
'aggregateAsDefaultForPublicApi' => true, | |
'deleteUser' => true, | |
]; | |
$accessMap->ROLE_CITY_COORD = [ | |
'manageCity' => true, | |
'manageUnit' => true, | |
'addUnit' => true, | |
'mailTo' => true, | |
'putFile' => true, | |
'deleteFile' => true, | |
'invite' => true, | |
'requests' => true, | |
'removeUnit' => true, | |
'changeUnitContext' => true, | |
'reqPrints' => true, | |
'statsUnits' => true, | |
'statsClazzes' => true, | |
'addGroupReward' => true, | |
'addPersonalReward' => true, | |
'editGroupReward'=> true, | |
'editPersonalReward'=> true, | |
'removeGroupReward' => true, | |
'removePersonalReward' => true, | |
'assignGroupReward' => true, | |
'assignAllGroupRewards' => true, | |
'assignAllPersonalRewards' => true, | |
'resetPersonalRewardsAssignment' => true, | |
'confirmPersonalRewardsAssignment' => true, | |
'checkRewardsCity' => true, | |
'journalView' => true, | |
'deleteUser' => true, | |
]; | |
$accessMap->ROLE_UNIT_PRINCIPAL = [ | |
'addUnit' => true, | |
]; | |
$accessMap->ROLE_UNIT_COORD = [ | |
'manageUnit' => true, | |
'mailTo' => true, | |
'putFile' => true, | |
'deleteFile' => true, | |
'invite' => true, | |
'defineClazzes' => true, | |
'journalView' => true, | |
'journalEdit' => true, | |
'addParticipants' => true, | |
'removeParticipants' => true, | |
'editParticipant' => true, | |
'statsClazzes' => true, | |
'changeClazzContext' => true, // on stats->journal nav | |
'restrictToCurrentClazz' => true, | |
'checkRewardsUnit' => true, | |
'deleteUser' => true, | |
]; | |
$accessMap->ROLE_JOURNAL_KEEPER = [ | |
'journalView' => true, | |
'journalEdit' => true, | |
'addParticipants' => true, | |
'removeParticipants' => true, // NOTE #52 vs #53 | |
'editParticipant' => true, | |
'restrictToCurrentClazz' => true, | |
]; | |
$accessMap->ROLE_NOBODY = []; | |
$accessMap->ROLE_USER = [ | |
'view' => true, | |
'userRole' => true, | |
'possibleContexts' => true, | |
'currentContext' => true, | |
'changeContext' => true, | |
'file' => true, | |
'fileList' => true, | |
// 'changeUsername' => true, | |
'changeEmail' => true, | |
'changePassword' => true, | |
'changePhone' => true, | |
'lockAccount' => true, | |
]; // everyone has access to those automatically | |
self::$accessMap = $accessMap; | |
self::$init = true; | |
} | |
static function getMetafileName( $filename ) { | |
return '.'.$filename.self::META_SUFFIX; | |
} | |
// instance part | |
protected $expectedMethod; | |
protected $obj; | |
protected $request; | |
protected $method; | |
protected $actionData; | |
protected $userRole; | |
protected $error; | |
protected $exception; | |
protected $jsonError; | |
protected $status; | |
public function __construct ( $expectedMethod ) { | |
//parent::__construct(); | |
self::init(); | |
$this->expectedMethod = $expectedMethod; | |
} | |
abstract function process ( Request $request ); | |
protected function getRootDir () { | |
return $this->container->getParameter( 'kernel.root_dir' ); | |
} | |
protected function getUserfilesDir () { | |
return $this->getRootDir().'/../userfiles'; | |
} | |
protected function getOrmManager () { | |
return $this->getDoctrine()->getManager(); | |
} | |
protected function getOrmRepository ( $c ) { | |
return $this->getOrmManager()->getRepository( $c ); | |
} | |
protected function getPasswordEncoder () { | |
return $this->container->get( 'security.password_encoder' ); | |
} | |
protected function flush () { | |
$this->getOrmManager()->flush(); | |
} | |
protected function persist ( $o ) { | |
$em = $this->getOrmManager(); | |
$em->persist( $o ); | |
$em->flush(); | |
} | |
protected function getUserById ( $id ) { | |
return $this->getOrmRepository( User::class )->findOneById( $id ); | |
} | |
protected function getUserDb ( $username = null ) { | |
if ( $username === null ) { | |
$user = $this->getUser(); | |
if ( $user === null ) { | |
return new User(); | |
} | |
$username = $this->getUser()->getUsername(); | |
} | |
return RmSchema::getUserByName( $this->getOrmManager(), $username ); | |
} | |
/* | |
protected function getUserForQuery( $e ) { | |
return isset( $e->id ) ? $this->getUserById( $e->id ) : $this->getUserDb(); | |
} | |
*/ | |
protected function copyUserData ( $name, $src, $dest ) { | |
if ( $src->$name !== null ) { | |
$dest->$name = $src->$name->__getSimpleFields(); | |
$ton = $this->obj->$name; | |
$ton->password = null; | |
$ton->salt = null; | |
} else { | |
$p = new class{}; // structural placeholder | |
$p->isPlaceholder = true; | |
$dest->$name = $p; | |
} | |
} | |
protected function isAdmin () { | |
return $this->userRole === 'ROLE_ADMIN'; | |
} | |
protected function isCoord () { | |
switch ( $this->userRole ) { | |
case 'ROLE_CAMPAIGN_COORD': | |
case 'ROLE_CITY_COORD': | |
case 'ROLE_UNIT_COORD': | |
return true; | |
} | |
return false; | |
} | |
protected function hasAccess ( $action ) { | |
return $this->isAdmin() | |
|| isset( self::$accessMap->ROLE_USER[$action] ) | |
|| isset( self::$accessMap->{$this->userRole}[$action] ); | |
} | |
protected function checkAccess ( $action ) { | |
$hasAccess = $this->hasAccess( $action ); | |
if ( !$hasAccess ) { | |
$this->error = VAX_ERROR_FORBIDDEN; | |
} | |
return $hasAccess; | |
} | |
// 2 => RW (shared from below) | |
// 1 => RW (accessible as own or semi-own) | |
// 0 => RO (admin-public or shared as RO from above) | |
// -1 => N/A (out-of-context) | |
// -2 => N/A (forbidden) | |
protected function hasObjectAccess ( $meta ) { | |
if ( !isset( $meta->accessControl ) ) { | |
return $this->isAdmin() ? 1 : -2; // no access control data == admin-only resource | |
} | |
$ac = $meta->accessControl; | |
if ( $this->getUserDb()->id === $ac->createdById ) { | |
return 1; // can always RW own files | |
} | |
if ( !isset( $ac->context ) ) { | |
return $this->isAdmin() ? 1 : 0; // no schema data == admin-created resource for all | |
} | |
$ctx = $ac->context; | |
$posCtx = $this->getPossibleContexts(); | |
foreach( RmSchema::MAIN_SCHEMA as $x ) { | |
if ( isset( $ctx->$x ) | |
&& array_search( (int) $ctx->$x->id, $posCtx->ids->$x, true ) === false ) { | |
return -1; // out-of-context files - even for admin (to sort them) | |
} | |
} | |
$urthis = RmSchema::ROLE_TO_ACCESS_LEVEL[$this->userRole]; | |
$urby = RmSchema::ROLE_TO_ACCESS_LEVEL[$ac->createdByRole]; | |
if ( $urthis > $urby ) { | |
return 2; // shared from below | |
} | |
if ( $urthis === $urby ) { // semi-own (same user role, in valid context) | |
return 1; | |
} | |
// otherwise not admin nor (semi-)own; either RO allowed or not | |
if ( isset( $ac->requiredRole ) | |
&& $urthis !== RmSchema::ROLE_TO_ACCESS_LEVEL[$ac->requiredRole] ) { | |
return -2; | |
} | |
return 0; // no role required or valid role provided | |
} | |
protected function canManageUser ( $usr, $ousr ) { | |
if ( $usr === $ousr ) { | |
return true; // just in case | |
} | |
if ( $usr === null || $ousr === null ) { | |
return false; // also just in case | |
} | |
$ctxr = $usr->contextRestrictions; | |
$octxr = $ousr->contextRestrictions; | |
$usrole = $usr->roles[0]; | |
$ousrole = $ousr->roles[0]; | |
return ( $usrole === 'ROLE_ADMIN' ) | |
|| ( $usrole === 'ROLE_CAMPAIGN_COORD' | |
&& $ousrole === 'ROLE_CITY_COORD' ) | |
|| ( $usrole === 'ROLE_CITY_COORD' | |
&& ( $ousrole === 'ROLE_UNIT_COORD' || $ousrole === 'ROLE_UNIT_PRINCIPAL' ) | |
&& $ctxr->city === $octxr->city ) | |
|| ( $usrole === 'ROLE_UNIT_COORD' | |
&& $ousrole === 'ROLE_JOURNAL_KEEPER' | |
&& $ctxr->unit === $octxr->unit ); | |
} | |
protected function getUserForQuery ( $e ) { | |
$tusrdb = $this->getUserDb(); | |
if ( !isset( $e->id ) || $e->id === "" ) { | |
return $tusrdb; | |
} | |
$usrdb = $this->getUserById( $e->id ); | |
if ( !$this->canManageUser( $tusrdb, $usrdb ) ) { | |
$this->error = VAX_ERROR_FORBIDDEN; | |
return null; | |
} | |
return $usrdb; | |
} | |
protected function getRestrictToCurrentClazzUser ( $usr, $ousr ) { | |
$ctxr = $usr->contextRestrictions; | |
$usrole = $usr->roles[0]; | |
if ( $ousr === null ) { | |
return ( $usrole !== 'ROLE_JOURNAL_KEEPER' ) ? null : $usr; | |
} | |
$octxr = $ousr->contextRestrictions; | |
// $ousrole = $ousr->roles[0]; | |
return ( ( $usrole === 'ROLE_ADMIN' ) | |
|| ( $usrole === 'ROLE_UNIT_COORD' && $ctxr->unit === $octxr->unit ) ) | |
? $ousr | |
: null; | |
} | |
// | |
// Context Management | |
// | |
protected function getCurrentContextDb () { | |
return $this->getUserDb()->lastContext; | |
} | |
protected function getContextRestrictionsDb () { | |
return $this->getUserDb()->contextRestrictions; | |
} | |
protected function getCurrentContext ( $dbContext = null ) { | |
if ( $dbContext === null ) { | |
$dbContext = $this->getCurrentContextDb(); | |
} | |
if ( $dbContext === null ) { // not authorized | |
return null; | |
} | |
$c = new class{}; | |
$c->names = new class{}; | |
$c->ids = new class{}; | |
foreach( RmSchema::MAIN_SCHEMA as $x ) { | |
$v = $dbContext->$x; | |
if ( $v === null ) { | |
$c->names->$x = ''; | |
$c->ids->$x = self::CONTEXT_NOT_SET; | |
} else { | |
$c->names->$x = $v->{$x.'Name'}; | |
$c->ids->$x = $v->id; | |
} | |
} | |
$camp = $dbContext->campaign; | |
if ( $camp !== null ) { | |
$c->campaignStage = $camp->stage; | |
} | |
return $c; | |
} | |
protected function getPossibleContextPart( $ctxDb, $rrs, $c, $q, $cl ) { | |
if ( $this->isAdmin() || !( $rrs->{$cl.RmSchema::RESTRICTED_SUFFIX} ) ) { | |
$c->names->$cl[] = ''; | |
$c->ids->$cl[] = self::CONTEXT_NOT_SET; | |
foreach ( $q as $v ) { | |
$c->names->$cl[] = $v->{$cl.'Name'}; | |
$c->ids->$cl[] = $v->id; | |
} | |
} else { | |
$rcl = $rrs->$cl; | |
if ( $rcl !== null ) { | |
$c->names->$cl[] = $rcl->{$cl.'Name'}; | |
$c->ids->$cl[] = $rcl->id; | |
} else { | |
$c->names->$cl[] = ''; | |
$c->ids->$cl[] = self::CONTEXT_NOT_SET; | |
} | |
} | |
} | |
protected function getPossibleContexts () { | |
$ctxDb = $this->getCurrentContextDb(); | |
$ctx = $this->getCurrentContext( $ctxDb ); | |
$rrs = $this->getContextRestrictionsDb(); | |
$c = new class{}; | |
$c->names = new class{}; | |
$c->ids = new class{}; | |
if ( $rrs === null ) { // no session ATM | |
return $c; | |
} | |
$em = $this->getOrmManager(); | |
$this->getPossibleContextPart( $ctxDb, $rrs, $c, | |
$em->getRepository( Campaign::class )->findAll(), 'campaign' ); | |
$this->getPossibleContextPart( $ctxDb, $rrs, $c, | |
$em->getRepository( City::class )->findByCampaign( $ctxDb->campaign, [ 'cityName' => 'ASC' ] ), 'city' ); | |
$this->getPossibleContextPart( $ctxDb, $rrs, $c, | |
$em->getRepository( Unit::class )->findByCity( $ctxDb->city, [ 'unitName' => 'ASC' ] ), 'unit' ); | |
$this->getPossibleContextPart( $ctxDb, $rrs, $c, | |
$em->getRepository( Clazz::class )->findByUnit( $ctxDb->unit, [ 'clazzName' => 'ASC' ] ), 'clazz' ); | |
return $c; | |
} | |
// | |
// Other methods | |
// | |
protected function setMeta () { | |
$err = $this->error; | |
$meta = new class{}; | |
$meta->actionMethod = $this->method; | |
$u = $this->getUser(); | |
$meta->userName = ( $u === null ) ? 'NONE' : $u->getUsername(); | |
$meta->userRole = $this->userRole; | |
$meta->internalError = $err; | |
$meta->exception = $this->exception; | |
$meta->jsonError = $this->jsonError; | |
$meta->status = $this->status; | |
$dt = new \DateTime(); | |
$ts = 'ts_'.$dt->getTimestamp(); | |
$meta->$ts = $dt->format( \DateTime::W3C ); | |
if ( $err !== VAX_ERROR_NONE || self::$debug ) { | |
$meta->actionData = $this->actionData; | |
} | |
$this->obj->meta = $meta; | |
} | |
// debug/mock/test method | |
/* | |
protected function getMockJson ( $name ) { | |
$o = $this->getJson( './mock/json/'.$name.'.json' ); | |
if ( $o !== null ) { | |
$this->obj = $o; | |
} | |
} | |
*/ | |
protected function getJson ( $url ) { | |
if ( !file_exists( $url ) ) { | |
$this->error = VAX_ERROR_INVALID_ARGUMENT; | |
return; | |
} | |
$obj = json_decode( file_get_contents( $url ) ); | |
$this->jsonError = json_last_error(); | |
if ( $this->jsonError !== JSON_ERROR_NONE ) { | |
$obj = new class{}; | |
$this->error = VAX_ERROR_JSON_DECODE; | |
} | |
return $obj; | |
} | |
public function action ( Request $request ) { | |
$this->request = $request; | |
$this->obj = new class{}; | |
$this->jsonError = JSON_ERROR_NONE; | |
$user = $this->getUser(); | |
$this->userRole = $user ? $user->getRoles()[0] : 'ROLE_NOBODY'; | |
$this->method = $request->getMethod(); | |
$this->error = ( $this->method === $this->expectedMethod ) | |
? VAX_ERROR_NONE | |
: VAX_ERROR_WRONG_METHOD; | |
$response = null; | |
if ( $this->error === VAX_ERROR_NONE ) { | |
if ( $user !== null && !$user->isAccountNonLocked() ) { | |
// $this->error = VAX_ERROR_FORBIDDEN; | |
// Fail silently. Don't feed the troll! | |
} else { | |
try { | |
$response = $this->process( $request ); | |
} catch ( \Exception $ex ) { | |
$xmsg = $ex->getMessage(); | |
$this->error = $xmsg; | |
$exloc = basename( $ex->getFile() ).' @ '.$ex->getLine(); | |
$this->exception = $exloc; | |
if ( !in_array( $xmsg, VaxUtils::ERRORS ) ) { | |
VaxUtils::rdump( 'Unhandled exception: '.$xmsg. ' in '.$exloc."\n", | |
true, VaxUtils::$logPath.VaxUtils::ERROR_LOG_NAME ); | |
if ( self::$debug ) { | |
throw $ex; | |
} | |
} | |
} | |
} | |
} | |
if ( $response !== null && $this->error === VAX_ERROR_NONE ) { | |
return $response; | |
} | |
if ( !isset( $this->obj->meta ) ) { | |
$this->setMeta(); | |
} | |
$json = VaxUtils::jsonEncode( $this->obj ); | |
if ( $this->jsonError === JSON_ERROR_NONE ) { | |
$this->jsonError = json_last_error(); | |
} | |
if ( $this->jsonError !== JSON_ERROR_NONE ) { | |
$this->obj = new class{}; | |
if ( $this->error === VAX_ERROR_NONE ) { | |
$this->error = VAX_ERROR_JSON_ENCODE; | |
} | |
$this->setMeta(); | |
$json = VaxUtils::jsonEncode( $this->obj ); | |
} | |
if ( !isset( $this->status ) ) { | |
switch ( $this->error ) { | |
case VAX_ERROR_NONE: | |
break; | |
case VAX_ERROR_FORBIDDEN: | |
$this->status = Response::HTTP_FORBIDDEN; // 403 | |
break; | |
default: | |
$this->status = Response::HTTP_BAD_REQUEST; // 400 | |
break; | |
} | |
} | |
$err = $this->error; | |
if ( $err !== VAX_ERROR_NONE ) { | |
VaxUtils::rdump( 'Error: '.$err."\n", true, VaxUtils::$logPath.VaxUtils::ERROR_LOG_NAME ); | |
VaxUtils::rdump( $json."\n\n", true, VaxUtils::$logPath.VaxUtils::ERROR_LOG_NAME ); | |
} | |
return $this->status | |
? new Response( $json, $this->status ) | |
: new Response( $json ); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment