Skip to content

Instantly share code, notes, and snippets.

@FyiurAmron
Created April 18, 2018 12:11
Show Gist options
  • Save FyiurAmron/f9d6b86ece72e8473ff54ea180ebce46 to your computer and use it in GitHub Desktop.
Save FyiurAmron/f9d6b86ece72e8473ff54ea180ebce46 to your computer and use it in GitHub Desktop.
<?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