diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5e7e1c --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# KDE Event Registration + +A simple web application to help organise and manage the KDE Annual Conference: Akademy. There is support for multiple events as KDE organises other conferences and sptrints throughout the year. + +For each event, we normally require the same profile information from attendees, followed by a set of questions that may change from event to event. To aid this, a form definition is saved with a conference definition within the events table. + +## Getting Started + +These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system. + +### Prerequisites + +The decision was taken to built this application on a simple LAMP stack, making it easier to contribute features and bug fixes. + + * PHP Composer - https://getcomposer.org/ + * Bower - https://bower.io/ + * MySQL - https://www.mysql.com/ + * LDAP (For KDE Identity Integration) + +To get up and running: + + * Configure a baseline LDAP server. An (unmaintained) example of this can be found here: https://github.com/GeekSoc/gas-vagrant + * Create a database using the information in resources/schema.sql + * Create a single conference using resources/conference.sql + * Copy app/settings.php.dist to app/settings.php and configure appropriately. + * Run the application: `php -S 0.0.0.0:8000 -t public` + * Open a browser and start building: `open http://localhost:8000` diff --git a/app/configuration/baseform.json b/app/configuration/baseform.json new file mode 100644 index 0000000..f6664ea --- /dev/null +++ b/app/configuration/baseform.json @@ -0,0 +1,88 @@ +{ + "forms": [ + { + "field": "Email", + "type": "text" + }, + { + "field": "Full Name", + "type": "text" + }, + { + "field": "Irc Nick", + "type": "text" + }, + { + "field": "Country", + "type": "dropdown", + "data": "CountryList" + }, + { + "field": "Primary Role in KDE", + "type": "dropdown", + "data": { + "type": "list", + "values": [ + "Artist", + "Community", + "Developer", + "Promo", + "Translator", + "User", + "Other" + ] + } + }, + { + "field": "Other Roles in KDE", + "type": "checkboxes", + "data": { + "type": "list", + "values": [ + "Artist", + "Community", + "Developer", + "Promo", + "Translator", + "User", + "Other" + ] + } + }, + { + "field": "Emergency Contact Information", + "type": "textarea", + "hint": "Someone we can contact at any time in case of an emergency that involves you. Please provide as much information as you can such as name, phone number including country code, address and the person's relationship to you." + }, + { + "field": "Dietary requirements and allergies", + "type": "checkboxes", + "data": { + "type": "list", + "values": [ + "Gluten Free", + "Lactose Free", + "Nut Free", + "Vegan", + "Vegetarian", + "Other" + ] + } + }, + { + "field": "Other dietary requirements", + "type": "textarea", + "hint": "Please tell us of any other dietary requirements you have.", + "dependency": { + "field": "Dietary requirements and allergies", + "value": "Other" + } + }, + { + "field": "Health or allergy information", + "type": "textarea", + "hint": "Please tell us of any health problems or allergies." + } + ], + "submitlabel": "Save Profile" +} diff --git a/app/configuration/data/countrylist.json b/app/configuration/data/countrylist.json new file mode 100644 index 0000000..9804767 --- /dev/null +++ b/app/configuration/data/countrylist.json @@ -0,0 +1,262 @@ +{ + "data": [ + "Please Select Country", + "-------------", + "Afghanistan", + "\u00c5land Islands", + "Albania", + "Algeria", + "American Samoa", + "Andorra", + "Angola", + "Anguilla", + "Antarctica", + "Antigua and Barbuda", + "Argentina", + "Armenia", + "Aruba", + "Ascension Island", + "Australia", + "Austria", + "Azerbaijan", + "Bahamas", + "Bahrain", + "Bangladesh", + "Barbados", + "Belarus", + "Belgium", + "Belize", + "Benin", + "Bermuda", + "Bhutan", + "Bolivia", + "Bosnia and Herzegovina", + "Botswana", + "Brazil", + "British Indian Ocean Territory", + "British Virgin Islands", + "Brunei", + "Bulgaria", + "Burkina Faso", + "Burundi", + "Cambodia", + "Cameroon", + "Canada", + "Canary Islands", + "Cape Verde", + "Caribbean Netherlands", + "Cayman Islands", + "Central African Republic", + "Ceuta and Melilla", + "Chad", + "Chile", + "China", + "Christmas Island", + "Cocos (Keeling) Islands", + "Colombia", + "Comoros", + "Congo - Brazzaville", + "Congo - Kinshasa", + "Cook Islands", + "Costa Rica", + "C\u00f4te d\u2019Ivoire", + "Croatia", + "Cuba", + "Cura\u00e7ao", + "Cyprus", + "Czech Republic", + "Denmark", + "Diego Garcia", + "Djibouti", + "Dominica", + "Dominican Republic", + "Ecuador", + "Egypt", + "El Salvador", + "England", + "Equatorial Guinea", + "Eritrea", + "Estonia", + "Ethiopia", + "Falkland Islands", + "Faroe Islands", + "Fiji", + "Finland", + "France", + "French Guiana", + "French Polynesia", + "French Southern Territories", + "Gabon", + "Gambia", + "Georgia", + "Germany", + "Ghana", + "Gibraltar", + "Greece", + "Greenland", + "Grenada", + "Guadeloupe", + "Guam", + "Guatemala", + "Guernsey", + "Guinea", + "Guinea-Bissau", + "Guyana", + "Haiti", + "Honduras", + "Hong Kong SAR China", + "Hungary", + "Iceland", + "India", + "Indonesia", + "Iran", + "Iraq", + "Ireland", + "Isle of Man", + "Israel", + "Italy", + "Jamaica", + "Japan", + "Jersey", + "Jordan", + "Kazakhstan", + "Kenya", + "Kiribati", + "Kosovo", + "Kuwait", + "Kyrgyzstan", + "Laos", + "Latvia", + "Lebanon", + "Lesotho", + "Liberia", + "Libya", + "Liechtenstein", + "Lithuania", + "Luxembourg", + "Macau SAR China", + "Macedonia", + "Madagascar", + "Malawi", + "Malaysia", + "Maldives", + "Mali", + "Malta", + "Marshall Islands", + "Martinique", + "Mauritania", + "Mauritius", + "Mayotte", + "Mexico", + "Micronesia", + "Moldova", + "Monaco", + "Mongolia", + "Montenegro", + "Montserrat", + "Morocco", + "Mozambique", + "Myanmar (Burma)", + "Namibia", + "Nauru", + "Nepal", + "Netherlands", + "New Caledonia", + "New Zealand", + "Nicaragua", + "Niger", + "Nigeria", + "Niue", + "Norfolk Island", + "North Korea", + "Northen Ireland", + "Northern Mariana Islands", + "Norway", + "Oman", + "Pakistan", + "Palau", + "Palestinian Territories", + "Panama", + "Papua New Guinea", + "Paraguay", + "Peru", + "Philippines", + "Pitcairn Islands", + "Poland", + "Portugal", + "Puerto Rico", + "Qatar", + "R\u00e9union", + "Romania", + "Russia", + "Rwanda", + "Saint Barth\u00e9lemy", + "Saint Helena", + "Saint Kitts and Nevis", + "Saint Lucia", + "Saint Martin", + "Saint Pierre and Miquelon", + "Samoa", + "San Marino", + "S\u00e3o Tom\u00e9 and Pr\u00edncipe", + "Saudi Arabia", + "Scotland", + "Senegal", + "Serbia", + "Seychelles", + "Sierra Leone", + "Singapore", + "Sint Maarten", + "Slovakia", + "Slovenia", + "Solomon Islands", + "Somalia", + "South Africa", + "South Georgia & South Sandwich Islands", + "South Korea", + "South Sudan", + "Spain", + "Sri Lanka", + "St. Vincent & Grenadines", + "Sudan", + "Suriname", + "Svalbard and Jan Mayen", + "Swaziland", + "Sweden", + "Switzerland", + "Syria", + "Taiwan", + "Tajikistan", + "Tanzania", + "Thailand", + "Timor-Leste", + "Togo", + "Tokelau", + "Tonga", + "Trinidad and Tobago", + "Tristan da Cunha", + "Tunisia", + "Turkey", + "Turkmenistan", + "Turks and Caicos Islands", + "Tuvalu", + "U.S. Outlying Islands", + "U.S. Virgin Islands", + "Uganda", + "Ukraine", + "United Arab Emirates", + "United States", + "Uruguay", + "Uzbekistan", + "Vanuatu", + "Vatican City", + "Venezuela", + "Vietnam", + "Wallis and Futuna", + "Wales", + "Western Sahara", + "Yemen", + "Zambia", + "Zimbabwe" + ] +} \ No newline at end of file diff --git a/app/dependencies.php b/app/dependencies.php new file mode 100644 index 0000000..9b7fe8d --- /dev/null +++ b/app/dependencies.php @@ -0,0 +1,161 @@ +getContainer(); +} else { + $container = $app; +} + +// ----------------------------------------------------------------------------- +// Service providers +// ----------------------------------------------------------------------------- + +// Twig +$container['view'] = function ($c) { + $settings = $c->get('settings'); + $view = new Slim\Views\Twig($settings['view']['template_path'], $settings['view']['twig']); + + // Add extensions + $view->addExtension(new Slim\Views\TwigExtension($c->get('router'), $c->get('request')->getUri())); + $view->addExtension(new Twig_Extension_Debug()); + + return $view; +}; + +// Flash messages +$container['flash'] = function ($c) { + return new Slim\Flash\Messages; +}; + +// ----------------------------------------------------------------------------- +// Service factories +// ----------------------------------------------------------------------------- + +// monolog +$container['logger'] = function ($c) { + $settings = $c->get('settings'); + $logger = new Monolog\Logger($settings['logger']['name']); + $logger->pushProcessor(new Monolog\Processor\UidProcessor()); + $logger->pushHandler(new Monolog\Handler\StreamHandler($settings['logger']['path'], Monolog\Logger::DEBUG)); + return $logger; +}; + +// ----------------------------------------------------------------------------- +// Database +// ----------------------------------------------------------------------------- +$container['database'] = function ($c) { + if(method_exists($c, 'get')) { + $config = $c->get('settings')['database']; + } else { + $config = $c['settings']['database']; + } + + $cfg = new \Spot\Config(); + $cfg->addConnection('mysqli', [ + 'dbname' => $config['db'], + 'user' => $config['username'], + 'password' => $config['password'], + 'host' => $config['hostname'], + 'driver' => 'pdo_mysql', + 'charset' => 'utf8' + ]); + $spot = new \Spot\Locator($cfg); + + return $spot; +}; + +// ----------------------------------------------------------------------------- +// Utility Classes +// ----------------------------------------------------------------------------- + +$container['formbuilder'] = function ($c) { + return new App\Forms\FormBuilder($c->get('view')); +}; + +$container['loginmanager'] = function ($c) { + return new App\Login\LoginManager($c->get('settings')['login'], $c->get('database'), $c->get('settings')['login']['override']); +}; + +// ----------------------------------------------------------------------------- +// Action factories +// ----------------------------------------------------------------------------- + +$container[App\Action\HomeAction::class] = function ($c) { + return new App\Action\HomeAction($c->get('view'), + $c->get('logger'), + $c->get('formbuilder'), + $c->get('loginmanager'), + $c->get('database')); +}; + +$container[App\Action\LoginAction::class] = function ($c) { + return new App\Action\LoginAction($c->get('view'), + $c->get('logger'), + $c->get('loginmanager')); +}; + +$container[App\Action\LogoutAction::class] = function ($c) { + return new App\Action\LogoutAction($c->get('view'), + $c->get('logger'), + $c->get('loginmanager')); +}; + +$container[App\Action\ProfileAction::class] = function ($c) { + return new App\Action\ProfileAction($c->get('view'), + $c->get('logger'), + $c->get('formbuilder'), + $c->get('loginmanager'), + $c->get('database')); +}; + +$container[App\Action\RegisterAction::class] = function ($c) { + return new App\Action\RegisterAction($c->get('view'), + $c->get('logger'), + $c->get('formbuilder'), + $c->get('loginmanager'), + $c->get('database')); +}; + +$container[App\Action\CancelAction::class] = function ($c) { + return new App\Action\CancelAction($c->get('view'), + $c->get('logger'), + $c->get('formbuilder'), + $c->get('loginmanager'), + $c->get('database')); +}; + +$container[App\Action\MerchandiseAction::class] = function ($c) { + return new App\Action\MerchandiseAction($c->get('view'), + $c->get('logger'), + $c->get('loginmanager'), + $c->get('database')); +}; + +$container[App\Action\StatsAction::class] = function ($c) { + return new App\Action\StatsAction($c->get('view'), + $c->get('logger'), + $c->get('formbuilder'), + $c->get('loginmanager'), + $c->get('database')); +}; + +// ----------------------------------------------------------------------------- +// API Action factories +// ----------------------------------------------------------------------------- + +$container[App\Action\API\MerchandiseAction::class] = function ($c) { + return new App\Action\API\MerchandiseAction($c->get('view'), + $c->get('logger'), + $c->get('formbuilder'), + $c->get('loginmanager'), + $c->get('database')); +}; + +$container[App\Action\API\RegistrationAction::class] = function ($c) { + return new App\Action\API\RegistrationAction($c->get('view'), + $c->get('logger'), + $c->get('formbuilder'), + $c->get('loginmanager'), + $c->get('database')); +}; \ No newline at end of file diff --git a/app/middleware.php b/app/middleware.php new file mode 100644 index 0000000..116a0ff --- /dev/null +++ b/app/middleware.php @@ -0,0 +1,4 @@ +add(new \Slim\Csrf\Guard); diff --git a/app/routes.php b/app/routes.php new file mode 100644 index 0000000..1a5b7ce --- /dev/null +++ b/app/routes.php @@ -0,0 +1,46 @@ +get('/', App\Action\HomeAction::class) + ->setName('homepage'); + +$app->get('/login', App\Action\LoginAction::class) + ->setName('login'); + +$app->post('/login', App\Action\LoginAction::class) + ->setName('login'); + +$app->get('/logout', App\Action\LogoutAction::class) + ->setName('logout'); + +$app->get('/profile', App\Action\ProfileAction::class) + ->setName('profile'); + +$app->post('/profile', App\Action\ProfileAction::class) + ->setName('profile'); + +$app->get('/register/{conferenceSlug}', App\Action\RegisterAction::class) + ->setName('register'); + +$app->post('/register/{conferenceSlug}', App\Action\RegisterAction::class) + ->setName('register'); + +$app->get('/cancel/{conferenceSlug}', App\Action\CancelAction::class) + ->setName('cancel'); + +$app->get('/merchandise/{conferenceSlug}', App\Action\MerchandiseAction::class) + ->setName('merchandise'); + +$app->post('/merchandise/{conferenceSlug}', App\Action\MerchandiseAction::class) + ->setName('merchandise'); + +$app->get('/stats/{conferenceSlug}', App\Action\StatsAction::class) + ->setName('stats'); + +// API + +$app->get('/api/merchandise/{conferenceSlug}', App\Action\API\MerchandiseAction::class) + ->setName('api/merchandise'); + +$app->get('/api/registration/{conferenceSlug}', App\Action\API\RegistrationAction::class) + ->setName('api/register'); \ No newline at end of file diff --git a/app/settings.php.dist b/app/settings.php.dist new file mode 100644 index 0000000..2f9774f --- /dev/null +++ b/app/settings.php.dist @@ -0,0 +1,41 @@ + [ + // Slim Settings + 'determineRouteBeforeAppMiddleware' => false, + 'displayErrorDetails' => false, + + // View settings + 'view' => [ + 'template_path' => __DIR__ . '/templates', + 'twig' => [ + 'cache' => __DIR__ . '/../cache/twig', + 'debug' => true, + 'auto_reload' => true, + ], + ], + + // monolog settings + 'logger' => [ + 'name' => 'app', + 'path' => __DIR__ . '/../log/app.log', + ], + + // login manager + 'login' => [ + 'salt' => '', + 'ldap_host' => "127.0.0.1", + 'ldap_port' => '389', + 'base_dn' => "ou=people,dc=kde,dc=org", + 'override' => false + ], + + // database configuration + 'database' => [ + 'hostname' => '127.0.0.1', + 'username' => '', + 'password' => '', + 'db' => '' + ] + ], +]; diff --git a/app/src/Action/API/MerchandiseAction.php b/app/src/Action/API/MerchandiseAction.php new file mode 100644 index 0000000..cf251d3 --- /dev/null +++ b/app/src/Action/API/MerchandiseAction.php @@ -0,0 +1,79 @@ +view = $view; + $this->logger = $logger; + $this->formbuilder = $formbuilder; + $this->loginmanager = $loginmanager; + $this->database = $database; + } + + public function __invoke(Request $request, Response $response, $args) + { + $this->logger->info("API :: Merchandise :: init"); + + $response = $response->withHeader('Content-type', 'application/json'); + + // Get Conference + $conferenceMapper = $this->database->mapper('Entity\Conference'); + $results = $conferenceMapper->all()->where(['slug' => $args['conferenceSlug']]); + + if(!$results->count()) { + return $this->failureResponse($response); + } + + if(!$this->loginmanager->isLoggedIn()) { + return $this->failureResponse($response); + } + + $profileMapper = $this->database->mapper('Entity\Profile'); + + $conference = $results->first(); + + $merchandiseMapper = $this->database->mapper('Entity\Merchandise'); + $confMerchandise = $merchandiseMapper->all()->where(['conference_id' => $conference->id]); + $displayMerchandise = []; + foreach($confMerchandise as $merchandiseItem) { + $displayMerchandise[] = $merchandiseItem; + } + + $args['data'] = [ + 'conference' => [ + 'id' => $conference->id, + 'name' => $conference->name, + 'start_date' => $conference->start_date, + 'end_date' => $conference->end_date, + ], + 'merchandise' => $displayMerchandise + ]; + + $this->logger->info("API :: Merchandise :: render"); + + $this->view->render($response, 'api/merchandise.twig', $args); + return $response; + } + + private function failureResponse($response) { + $args['data'] = [ + 'success' => false + ]; + + $this->view->render($response, 'api/merchandise.twig', $args); + return $response; + } +} diff --git a/app/src/Action/API/RegistrationAction.php b/app/src/Action/API/RegistrationAction.php new file mode 100644 index 0000000..21993a3 --- /dev/null +++ b/app/src/Action/API/RegistrationAction.php @@ -0,0 +1,105 @@ +view = $view; + $this->logger = $logger; + $this->formbuilder = $formbuilder; + $this->loginmanager = $loginmanager; + $this->database = $database; + } + + public function __invoke(Request $request, Response $response, $args) + { + $this->logger->info("API :: init"); + $response = $response->withHeader('Content-type', 'application/json'); + + // Get Conference + $conferenceMapper = $this->database->mapper('Entity\Conference'); + $results = $conferenceMapper->all()->where(['slug' => $args['conferenceSlug']]); + + if(!$results->count()) { + return $this->failureResponse($response); + } + + if(!isset($_GET['feifjw0efjwef'])) { + if(!isset($_GET['feifjw0efjwef']) && !$this->loginmanager->isLoggedIn()) { + return $this->failureResponse($response); + } + } + + $adminMapper = $this->database->mapper('Entity\Admin'); + + if(!isset($_GET['feifjw0efjwef'])) { + if(!$adminMapper->where([ + 'conference_id' => $results->first()->id, + 'dn' => $this->loginmanager->getCurrentUser() + ])->count()) { + + $this->logger->info("Stats :: not an admin user"); + + return $response->withRedirect('/?access=denied'); + } + } + + $profileMapper = $this->database->mapper('Entity\Profile'); + $wifiMapper = $this->database->mapper('Entity\Wifi'); + + $conference = $results->first(); + + $registrationMapper = $this->database->mapper('Entity\Registration'); + $registrations = []; + foreach($registrationMapper->all()->where(['conference_id' => $conference->id]) as $registration) { + $registration->data = json_decode($registration->data); + $registrations[] = $registration; + } + + $args['data'] = [ + 'conference' => [ + 'id' => $conference->id, + 'name' => $conference->name, + 'description' => $conference->description, + 'start_date' => $conference->start_date, + 'end_date' => $conference->end_date, + ], + 'registrations' => array_map(function($entity) use ($profileMapper, $wifiMapper) { + $entity->profile = $profileMapper->all()->where(['dn' => $entity->dn])->first(); + $entity->profile->data = json_decode($entity->profile->data); + $wifi = $wifiMapper->all()->where(['registration_id' => $entity->id])->first(); + $entity->wifi = [ + 'username' => $wifi->username, + 'password' => $wifi->password + ]; + return $entity; + }, $registrations) + ]; + + $this->logger->info("Stats :: render"); + + $this->view->render($response, 'api/registration.twig', $args); + return $response; + } + + private function failureResponse($response) { + $args['data'] = [ + 'success' => false + ]; + + $this->view->render($response, 'api/registration.twig', $args); + return $response; + } +} diff --git a/app/src/Action/CancelAction.php b/app/src/Action/CancelAction.php new file mode 100644 index 0000000..be3cde9 --- /dev/null +++ b/app/src/Action/CancelAction.php @@ -0,0 +1,75 @@ +view = $view; + $this->logger = $logger; + $this->formbuilder = $formbuilder; + $this->loginmanager = $loginmanager; + $this->database = $database; + } + + public function __invoke(Request $request, Response $response, $args) + { + $this->logger->info("Register :: cancel"); + + if(!$this->loginmanager->isLoggedIn()) { + $this->logger->info("Register :: not logged in"); + + return $response->withRedirect('/login'); + } + + return $this->processCancel($request, $response, $args); + + $this->view->render($response, 'register.twig', $args); + return $response; + } + + public function processCancel(Request $request, Response $response, $args) + { + $this->logger->info("Register :: cancel"); + + $conferenceMapper = $this->database->mapper('Entity\Conference'); + $conferences = $conferenceMapper->where(['slug' => $args['conferenceSlug']]); + + if(!$conferences->count()) { + return $response->withRedirect("/"); + } + + $args['conference'] = $conferences->first(); + + $baseRegistration = [ + 'dn' => $this->loginmanager->getCurrentUser(), + 'conference_id' => $args['conference']->id + ]; + + $registrationMapper = $this->database->mapper('Entity\Registration'); + $registrations = $registrationMapper->where($baseRegistration); + + if($registrations->count()) { + $registration = $registrations->first(); + } + + $registration->cancelled = 1; + + $registrationMapper->save($registration); + + $this->logger->info("Register :: saved"); + + return $response->withRedirect(implode('', ["/?registrationcancelled=", $args['conference']->slug])); + } +} diff --git a/app/src/Action/HomeAction.php b/app/src/Action/HomeAction.php new file mode 100644 index 0000000..590505c --- /dev/null +++ b/app/src/Action/HomeAction.php @@ -0,0 +1,81 @@ +view = $view; + $this->logger = $logger; + $this->formbuilder = $formbuilder; + $this->loginmanager = $loginmanager; + $this->database = $database; + } + + public function __invoke(Request $request, Response $response, $args) + { + $this->logger->info("Dashboard :: init"); + + if(!$this->loginmanager->isLoggedIn()) { + $this->logger->info("Dashboard :: not logged in"); + return $response->withRedirect('/login'); + } + + $profileMapper = $this->database->mapper('Entity\Profile'); + $existingProfile = $profileMapper->where(['dn' => $this->loginmanager->getCurrentUser()]); + if(!$existingProfile->count()) { + $this->logger->info("Dashboard :: new user, redirect to profile :: " . $this->loginmanager->getCurrentUser()); + return $response->withRedirect('/profile?newuser=true'); + } + + $args['isLoggedIn'] = $this->loginmanager->isLoggedIn(); + $args['currentPage'] = 'home'; + + $conferenceMapper = $this->database->mapper('Entity\Conference'); + $args['conferences'] = $conferenceMapper->where(['enabled' => 1]); + + $args['isRegistered'] = []; + + $registrationMapper = $this->database->mapper('Entity\Registration'); + foreach($args['conferences'] as $conference) { + $args['isRegistered'][$conference->id] = (boolean)$registrationMapper->where([ + 'conference_id' => $conference->id, + 'cancelled' => 0, + 'dn' => $this->loginmanager->getCurrentUser() + ])->count(); + } + + $adminMapper = $this->database->mapper('Entity\Admin'); + foreach($args['conferences'] as $conference) { + $args['isAdmin'][$conference->id] = (boolean)$adminMapper->where([ + 'conference_id' => $conference->id, + 'dn' => $this->loginmanager->getCurrentUser() + ])->count(); + } + + $args['profilesuccess'] = isset($_GET['profilesuccess']); + $args['registrationsuccess'] = isset($_GET['registrationsuccess']); + $args['registrationcancelled'] = isset($_GET['registrationcancelled']); + $args['reservationsuccess'] = isset($_GET['reservationsuccess']); + $args['reservationupdated'] = isset($_GET['reservationupdated']); + $args['reservationcancelled'] = isset($_GET['reservationcancelled']); + $args['accessdenied'] = isset($_GET['access']); + + $this->logger->info("Dashboard :: render"); + + //Form Data + $this->view->render($response, 'home.twig', $args); + return $response; + } +} diff --git a/app/src/Action/LoginAction.php b/app/src/Action/LoginAction.php new file mode 100644 index 0000000..49d4089 --- /dev/null +++ b/app/src/Action/LoginAction.php @@ -0,0 +1,62 @@ +view = $view; + $this->logger = $logger; + $this->loginmanager = $loginmanager; + } + + public function __invoke(Request $request, Response $response, $args) + { + $this->logger->info("Login :: init"); + + if($this->loginmanager->isLoggedIn()) { + $this->logger->info("Login :: already logged in"); + + return $response->withRedirect('/'); + } + + if($request->getMethod() == 'POST') { + return $this->processLogin($request, $response, $args); + } + + $args['failedlogin'] = isset($_GET['failed']); + + $args['isLoggedIn'] = $this->loginmanager->isLoggedIn(); + $args['isAdminUser'] = $this->loginmanager->isAdminUser(); + + $this->logger->info("Login :: render"); + + $this->view->render($response, 'login.twig', $args); + + return $response; + } + + public function processLogin(Request $request, Response $response, $args) + { + + $this->logger->info("Login :: login attempt"); + + $params = $request->getParsedBody(); + if($this->loginmanager->login($params['username'], $params['password'])) { + $this->logger->info("Login :: success"); + return $response->withRedirect("/"); + } + + $this->logger->info("Login :: failure"); + return $response->withRedirect("/login?failed=true"); + } +} diff --git a/app/src/Action/LogoutAction.php b/app/src/Action/LogoutAction.php new file mode 100644 index 0000000..3db67cf --- /dev/null +++ b/app/src/Action/LogoutAction.php @@ -0,0 +1,30 @@ +view = $view; + $this->logger = $logger; + $this->loginmanager = $loginmanager; + } + + public function __invoke(Request $request, Response $response, $args) + { + $this->logger->info("Logout"); + + $this->loginmanager->logout(); + + return $response->withRedirect("/"); + } +} diff --git a/app/src/Action/MerchandiseAction.php b/app/src/Action/MerchandiseAction.php new file mode 100644 index 0000000..177b95b --- /dev/null +++ b/app/src/Action/MerchandiseAction.php @@ -0,0 +1,182 @@ +view = $view; + $this->logger = $logger; + $this->loginmanager = $loginmanager; + $this->database = $database; + } + + public function __invoke(Request $request, Response $response, $args) + { + $this->logger->info("Merchandise :: init"); + + if(!$this->loginmanager->isLoggedIn()) { + $this->logger->info("Register :: not logged in"); + + return $response->withRedirect('/login'); + } + + if($request->getMethod() == 'POST') { + return $this->processSave($request, $response, $args); + } + + $args['isLoggedIn'] = $this->loginmanager->isLoggedIn(); + $args['isAdminUser'] = $this->loginmanager->isAdminUser(); + + $conferenceMapper = $this->database->mapper('Entity\Conference'); + $conferences = $conferenceMapper->where(['slug' => $args['conferenceSlug']]); + + if(!$conferences->count()) { + return $response->withRedirect("/"); + } + + $args['conference'] = $conferences->first(); + + $baseRegistration = + ['dn' => $this->loginmanager->getCurrentUser(), + 'conference_id' => $args['conference']->id]; + + $merchandiseMapper = $this->database->mapper('Entity\Merchandise'); + $reservationMapper = $this->database->mapper('Entity\MerchandiseReservation'); + + $confMerchandise = $merchandiseMapper->all()->where(['conference_id' => $args['conference']->id]); + $displayMerchandise = []; + foreach($confMerchandise as $merchandiseItem) { + $displayMerchandise[] = $merchandiseItem; + } + + $args['displayMerchandise'] = $displayMerchandise; + + $args['displayMerchandiseOptions'] = []; + foreach($displayMerchandise as $merch) { + $args['displayMerchandiseOptions'][$merch->id] = json_decode($merch->variations); + } + + $args['orderedMerchandise'] = []; + foreach($displayMerchandise as $merch) { + $orderedMerchItem = $reservationMapper->all()->where(['conference_id' => $args['conference']->id, 'dn' => $this->loginmanager->getCurrentUser(), 'merch_id' => $merch->id ]); + if ( $orderedMerchItem->count() > 0 ) { + $args['orderedMerchandise'][$merch->id] = $orderedMerchItem->first()->variation; + } else { + $args['orderedMerchandise'] = false; + } + } + + // var_dump($args['displayMerchandiseOptions']); die(); + + // $registrationMapper = $this->database->mapper('Entity\Registration'); + // $registrations = $registrationMapper->where($baseRegistration); + + // if($registrations->count()) { + // $registration = $registrations->first(); + // } else { + // $registration = $registrationMapper->build($baseRegistration); + // } + + // $args['form'] = $this->formbuilder->buildForm(json_decode($args['conference']->form), json_decode($registration->data), [2,99]); + + $this->logger->info("Merchandise :: render"); + + $this->view->render($response, 'merchandise.twig', $args); + return $response; + } + + public function processSave(Request $request, Response $response, $args) + { + $this->logger->info("Merchandise :: save"); + + $conferenceMapper = $this->database->mapper('Entity\Conference'); + $conferences = $conferenceMapper->where(['slug' => $args['conferenceSlug']]); + + $reservationMapper = $this->database->mapper('Entity\MerchandiseReservation'); + + if(!$conferences->count()) { + return $response->withRedirect("/"); + } + + $args['conference'] = $conferences->first(); + + switch($_POST['action']) { + case 'Reserve': + $baseReservation = [ + 'conference_id' => $args['conference']->id, + 'dn' => $this->loginmanager->getCurrentUser(), + 'merch_id' => $_POST['merch_id'], + 'variation' => $_POST['variation'] + ]; + + $reservation = $reservationMapper->build($baseReservation); + $reservationMapper->save($reservation); + + return $response->withRedirect("/?reservationsuccess=true"); + + break; + + case 'Update Reservation': + $entity = $reservationMapper->first([ + 'conference_id' => $args['conference']->id, + 'dn' => $this->loginmanager->getCurrentUser(), + 'merch_id' => $_POST['merch_id'], + ]); + + $entity->variation = $_POST['variation']; + + $reservationMapper->update($entity); + + return $response->withRedirect("/?reservationupdated=true"); + + break; + + case 'Cancel Reservation': + $entity = $reservationMapper->first([ + 'conference_id' => $args['conference']->id, + 'dn' => $this->loginmanager->getCurrentUser(), + 'merch_id' => $_POST['merch_id'], + ]); + + $reservationMapper->delete($entity); + + return $response->withRedirect("/?reservationcancelled=true"); + + break; + } + + // $baseRegistration = [ + // 'dn' => , + // 'conference_id' => $args['conference']->id + // ]; + + // $registrationMapper = $this->database->mapper('Entity\Registration'); + // $registrations = $registrationMapper->where($baseRegistration); + + // if($registrations->count()) { + // $registration = $registrations->first(); + // } else { + // $registration = $registrationMapper->build($baseRegistration); + // } + + // $registration->data = json_encode($request->getParsedBody()); + // $registration->cancelled = 0; + + // $registrationMapper->save($registration); + + // $this->logger->info("Register :: saved"); + + // return $response->withRedirect(implode('', ["/?registrationsuccess=", $args['conference']->slug])); + } +} diff --git a/app/src/Action/ProfileAction.php b/app/src/Action/ProfileAction.php new file mode 100644 index 0000000..395bc82 --- /dev/null +++ b/app/src/Action/ProfileAction.php @@ -0,0 +1,100 @@ +view = $view; + $this->logger = $logger; + $this->formbuilder = $formbuilder; + $this->loginmanager = $loginmanager; + $this->database = $database; + } + + public function __invoke(Request $request, Response $response, $args) + { + $this->logger->info("Profile :: init"); + + if(!$this->loginmanager->isLoggedIn()) { + $this->logger->info("Profile :: not logged in, redirecting"); + + return $response->withRedirect('/login'); + } + + if($request->getMethod() == 'POST') { + return $this->processSave($request, $response, $args); + } + + $profileMapper = $this->database->mapper('Entity\Profile'); + $existingProfile = $profileMapper->where(['dn' => $this->loginmanager->getCurrentUser()]); + + if($existingProfile->count()) { + $args['profile'] = $existingProfile->first(); + $args['profile'] = json_decode($args['profile']->data); + } else { + $userMapper = $this->database->mapper('Entity\User'); + $currentUser = $userMapper->where(['dn' => $this->loginmanager->getCurrentUser()])->first(); + + $args['profile'] = [ + 'Email' => $currentUser->email, + 'Full_Name' => $currentUser->cn, + 'Irc_Nick' => $currentUser->ircnick + ]; + } + + //Form Data + $baseFormFilename = dirname(__FILE__) . "/../../configuration/baseform.json"; + $form = $this->formbuilder->buildForm(json_decode(file_get_contents($baseFormFilename)), $args['profile'], [6, 99]); + + $args['isLoggedIn'] = $this->loginmanager->isLoggedIn(); + $args['isAdminUser'] = $this->loginmanager->isAdminUser(); + $args['currentPage'] = 'profile'; + $args['form'] = $form; + + $args['saved'] = ""; + if(isset($_GET['success'])) { + $args['saved'] = true; + } + + $args['newuser'] = isset($_GET['newuser']); + + $this->logger->info("Profile :: render"); + + $this->view->render($response, 'profile.twig', $args); + return $response; + } + + public function processSave(Request $request, Response $response, $args) + { + $this->logger->info("Profile :: saving profile"); + + $profileMapper = $this->database->mapper('Entity\Profile'); + + $existingProfile = $profileMapper->where(['dn' => $this->loginmanager->getCurrentUser()]); + if($existingProfile->count()) { + $profile = $existingProfile->first(); + } else { + $profile = $profileMapper->build(['dn' => $this->loginmanager->getCurrentUser()]); + } + + $profile->data = json_encode($request->getParsedBody()); + + $profileMapper->save($profile); + + $this->logger->info("Profile :: saved"); + + return $response->withRedirect("/?profilesuccess=true"); + } +} diff --git a/app/src/Action/RegisterAction.php b/app/src/Action/RegisterAction.php new file mode 100644 index 0000000..7cd26cc --- /dev/null +++ b/app/src/Action/RegisterAction.php @@ -0,0 +1,132 @@ +view = $view; + $this->logger = $logger; + $this->formbuilder = $formbuilder; + $this->loginmanager = $loginmanager; + $this->database = $database; + } + + public function __invoke(Request $request, Response $response, $args) + { + $this->logger->info("Register :: init"); + + if(!$this->loginmanager->isLoggedIn()) { + $this->logger->info("Register :: not logged in"); + + return $response->withRedirect('/login'); + } + + if($request->getMethod() == 'POST') { + return $this->processSave($request, $response, $args); + } + + $args['isLoggedIn'] = $this->loginmanager->isLoggedIn(); + $args['isAdminUser'] = $this->loginmanager->isAdminUser(); + + $conferenceMapper = $this->database->mapper('Entity\Conference'); + $conferences = $conferenceMapper->where(['slug' => $args['conferenceSlug']]); + + if(!$conferences->count()) { + return $response->withRedirect("/"); + } + + $args['conference'] = $conferences->first(); + + $baseRegistration = + ['dn' => $this->loginmanager->getCurrentUser(), + 'conference_id' => $args['conference']->id]; + + $registrationMapper = $this->database->mapper('Entity\Registration'); + $registrations = $registrationMapper->where($baseRegistration); + + if($registrations->count()) { + $registration = $registrations->first(); + } else { + $registration = $registrationMapper->build($baseRegistration); + } + + $args['form'] = $this->formbuilder->buildForm(json_decode($args['conference']->form), json_decode($registration->data), [2,99]); + + $this->logger->info("Register :: render"); + + $args['braintreekey'] = 'w3nr9fcp8xmpvppc'; + + $this->view->render($response, 'register.twig', $args); + return $response; + } + + public function processSave(Request $request, Response $response, $args) + { + $this->logger->info("Register :: save"); + + $conferenceMapper = $this->database->mapper('Entity\Conference'); + $conferences = $conferenceMapper->where(['slug' => $args['conferenceSlug']]); + + if(!$conferences->count()) { + return $response->withRedirect("/"); + } + + $args['conference'] = $conferences->first(); + + $baseRegistration = [ + 'dn' => $this->loginmanager->getCurrentUser(), + 'conference_id' => $args['conference']->id + ]; + + $registrationMapper = $this->database->mapper('Entity\Registration'); + $registrations = $registrationMapper->where($baseRegistration); + + if($registrations->count()) { + $registration = $registrations->first(); + } else { + $registration = $registrationMapper->build($baseRegistration); + } + + $registration->data = json_encode($request->getParsedBody()); + $registration->cancelled = 0; + + $registrationMapper->save($registration); + + //Checking if Wifi Credentials are available + if($args['conference']->wifi) { + $this->assignWifi($registration->id, $args['conference']->id); + } + + $this->logger->info("Register :: saved"); + + return $response->withRedirect(implode('', ["/?registrationsuccess=", $args['conference']->slug])); + } + + private function assignWifi($registrationId, $conferenceId) { + $wifiMapper = $this->database->mapper('Entity\Wifi'); + + $usedCodes = $wifiMapper->where(['registration_id' => $registrationId, 'conference_id' => $conferenceId]); + if($usedCodes->count() > 0) { + return; + } + + $availableCodes = $wifiMapper->where(['registration_id' => null, 'conference_id' => $conferenceId]); + if($availableCodes->count() > 0) { + $assignedCode = $availableCodes->first(); + $assignedCode->registration_id = $registrationId; + $wifiMapper->save($assignedCode); + } + } +} diff --git a/app/src/Action/StatsAction.php b/app/src/Action/StatsAction.php new file mode 100644 index 0000000..f341f86 --- /dev/null +++ b/app/src/Action/StatsAction.php @@ -0,0 +1,193 @@ +view = $view; + $this->logger = $logger; + $this->formbuilder = $formbuilder; + $this->loginmanager = $loginmanager; + $this->database = $database; + } + + public function __invoke(Request $request, Response $response, $args) + { + $this->logger->info("Stats :: init"); + + if(!$this->loginmanager->isLoggedIn()) { + $this->logger->info("Stats :: not logged in"); + + return $response->withRedirect('/login'); + } + + $args['isLoggedIn'] = $this->loginmanager->isLoggedIn(); + + $conferenceMapper = $this->database->mapper('Entity\Conference'); + $conferences = $conferenceMapper->where(['slug' => $args['conferenceSlug']]); + + if(!$conferences->count()) { + return $response->withRedirect("/"); + } + + $adminMapper = $this->database->mapper('Entity\Admin'); + if(!$adminMapper->where([ + 'conference_id' => $conferences->first()->id, + 'dn' => $this->loginmanager->getCurrentUser() + ])->count()) { + + $this->logger->info("Stats :: not an admin user"); + + return $response->withRedirect('/?access=denied'); + } + + $args['conference'] = $conferences->first(); + + $registrationMapper = $this->database->mapper('Entity\Registration'); + $registrations = $registrationMapper->all()->where(['conference_id' => $args['conference']->id]); + + $profileMapper = $this->database->mapper('Entity\Profile'); + $wifiMapper = $this->database->mapper('Entity\Wifi'); + + $args['registrationdata'] = []; + $args['registrationcount'] = $registrations->count(); + foreach($registrations as $registration) { + $regdata = (array)json_decode($registration->data); + foreach($regdata as $key => $reg) { + if(is_object($reg)) { + foreach((array)$reg as $k => $v) { + $regdata[implode("_", [$key, $k])] = $v; + } + unset($regdata[$key]); + } + unset($regdata['Save']); + } + + if($args['conference']->wifi == 1) { + $wifiData = $wifiMapper->where(['conference_id' => $args['conference']->id, 'registration_id' => $registration->id])->first(); + $regdata['wifi_username'] = $wifiData->username; + $regdata['wifi_password'] = $wifiData->password; + } + + $args['registrationdata'][] = array_merge(['dn' => $registration->dn], $regdata); + } + + $profiles = $profileMapper->all()->where(['dn' => array_map(function($data) { + return $data['dn']; + }, $args['registrationdata'])]); + + $args['profiledata'] = []; + foreach($profiles as $profile) { + $profiledata = (array)json_decode($profile->data); + if(isset($profiledata['Other_Roles_in_KDE'])) { + $profiledata['Other_Roles_in_KDE'] = implode(", ", array_keys((array)$profiledata['Other_Roles_in_KDE'])); + } + + if(isset($profiledata['Dietary_requirements_and_allergies'])) { + $profiledata['Dietary_requirements_and_allergies'] = implode(", ", array_keys((array)$profiledata['Dietary_requirements_and_allergies'])); + } + + unset($profiledata['Save']); + + $args['profiledata'][] = array_merge(['dn' => $profile->dn], $profiledata); + } + + /** + * Merch (T-Shirt) Pre-Orders + */ + $merchReservationMapper = $this->database->mapper('Entity\MerchandiseReservation'); + $merchReservations = $merchReservationMapper->all()->where(['conference_id' => $args['conference']->id]); + $args['merchReservations'] = []; + + $sizes = []; + foreach($merchReservations as $reservation) { + $args['merchReservations'][] = $reservation; //fill array with data from model + + if(!isset($sizes[$reservation->variation])) { + $sizes[$reservation->variation] = 0; + } + $sizes[$reservation->variation]++; + } + $args['sizes'] = $sizes; + $args['sizelist'] = json_encode(array_keys($sizes)); + $args['sizevalues'] = json_encode(array_values($sizes)); + + $countries = []; + $primaryroles = []; + $dietaryrequirements = []; + foreach($args['profiledata'] as $registration) { + if($registration['Country'] != 'Please Select Country' && !isset($countries[$registration['Country']])) { + $countries[$registration['Country']] = 0; + } + + if(!isset($primaryroles[$registration['Primary_Role_in_KDE']])) { + $primaryroles[$registration['Primary_Role_in_KDE']] = 0; + } + + if(isset($registration['Dietary_requirements_and_allergies'])) { + foreach(explode(", ", $registration['Dietary_requirements_and_allergies']) as $requirement) { + if(strlen($requirement)) { + if(!isset($dietaryrequirements[$requirement])) { + $dietaryrequirements[$requirement] = 0; + } + + $dietaryrequirements[$requirement]++; + } + } + } + + if($registration['Country'] != 'Please Select Country') { + $countries[$registration['Country']]++; + } + + $primaryroles[$registration['Primary_Role_in_KDE']]++; + } + + $args['countrylist'] = json_encode(array_keys($countries)); + $args['countryvalues'] = json_encode(array_values($countries)); + $args['countryborders'] = json_encode(array_fill(0, count($countries), '#666666')); + $args['countrycolours'] = []; + foreach($countries as $country) { + $args['countrycolours'][] = "rgb(200," . ($country * 25) . "," . ($country * 20) . ")"; + } + $args['countrycolours'] = json_encode($args['countrycolours']); + + $args['primaryrolelist'] = json_encode(array_keys($primaryroles)); + $args['primaryrolevalues'] = json_encode(array_values($primaryroles)); + $args['primaryroleborders'] = json_encode(array_fill(0, count($primaryroles), '#666666')); + $args['primaryrolecolours'] = []; + foreach($primaryroles as $primaryrole) { + $args['primaryrolecolours'][] = "rgb(200," . ($primaryrole * 25) . "," . ($primaryrole * 20) . ")"; + } + $args['primaryrolecolours'] = json_encode($args['primaryrolecolours']); + + $args['dietaryrequirementslist'] = json_encode(array_keys($dietaryrequirements)); + $args['dietaryrequirementsvalues'] = json_encode(array_values($dietaryrequirements)); + $args['dietaryrequirementsborders'] = json_encode(array_fill(0, count($dietaryrequirements), '#666666')); + $args['dietaryrequirementscolours'] = []; + foreach($dietaryrequirements as $dietaryrequirement) { + $args['dietaryrequirementscolours'][] = "rgb(200," . ($dietaryrequirement * 25) . "," . ($dietaryrequirement * 20) . ")"; + } + $args['dietaryrequirementscolours'] = json_encode($args['dietaryrequirementscolours']); + + $args['registrationkeys'] = array_keys($args['registrationdata'][0]); + + $this->logger->info("Stats :: render"); + + $this->view->render($response, 'stats.twig', $args); + return $response; + } +} + diff --git a/app/src/Command/MailmanCommand.php b/app/src/Command/MailmanCommand.php new file mode 100644 index 0000000..cd6415e --- /dev/null +++ b/app/src/Command/MailmanCommand.php @@ -0,0 +1,69 @@ +setName('events:mailman') + ->setDescription('Register Attendess with Mailman'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln("Registering Conference Attendees with Mailman"); + + // Get conferences with registration + $database = $this->getApplication()->getService('database'); + + $conferenceMapper = $database->mapper('Entity\Conference'); + $registrationMapper = $database->mapper('Entity\Registration'); + $mailmanMapper = $database->mapper('Entity\Mailman'); + + $conferences = $conferenceMapper->query("SELECT * FROM conference WHERE mailman is not null"); + + foreach($conferences as $conference) { + $existingRegistrations = []; + $registered = $mailmanMapper->query("SELECT * FROM mailman WHERE conference_id = {$conference->id}"); + foreach($registered as $r) { + $existingRegistrations[] = $r->dn; + } + + $toBeRegistered = []; + $registered = $registrationMapper->query("SELECT * FROM registration WHERE conference_id = {$conference->id}"); + foreach($registered as $r) { + if(!in_array($r->dn, $existingRegistrations)) { + $toBeRegistered[] = $r->dn; + } + } + + $output->writeln("{$conference->name}: Pending Registrations: " . count($toBeRegistered)); + + foreach($toBeRegistered as $dn) { + $this->registerToMailman($dn, $conference->mailman, $conference->id, $output); + } + } + } + + private function registerToMailman($dn, $mailmanAddress, $conferenceId, &$output) { + $database = $this->getApplication()->getService('database'); + $profileMapper = $database->mapper('Entity\Profile'); + $mailmanMapper = $database->mapper('Entity\Mailman'); + + $profile = json_decode($profileMapper->where(['dn' => $dn])->first()->data); + + $output->writeln(implode(" ", ["Registering", $profile->Email, "with", $mailmanAddress])); + + // Automated Subscribe: https://www.gnu.org/software/mailman/mailman-member/node41.html#a:commands + mail($mailmanAddress, 'SUBSCRIBE', "", implode(" ", ["From:", $profile->Email])); + + $mailmanMapper->create([ 'dn' => $dn, 'conference_id' => $conferenceId ]); + } +} diff --git a/app/src/Entities/Admin.php b/app/src/Entities/Admin.php new file mode 100644 index 0000000..2b5decd --- /dev/null +++ b/app/src/Entities/Admin.php @@ -0,0 +1,14 @@ + ['type' => 'integer', 'primary' => true, 'required' => true], + 'dn' => ['type' => 'string', 'required' => true], + 'conference_id' => ['type' => 'integer', 'required' => true] + ]; + } +} diff --git a/app/src/Entities/Conference.php b/app/src/Entities/Conference.php new file mode 100644 index 0000000..5472f96 --- /dev/null +++ b/app/src/Entities/Conference.php @@ -0,0 +1,25 @@ + ['type' => 'integer', 'primary' => true, 'required' => true], + 'name' => ['type' => 'string', 'primary' => true, 'required' => true], + 'description' => ['type' => 'string', 'primary' => true, 'required' => false], + 'notes' => ['type' => 'string', 'primary' => true, 'required' => false], + 'venue' => ['type' => 'string', 'primary' => true, 'required' => true], + 'website' => ['type' => 'string', 'primary' => true, 'required' => true], + 'start_date' => ['type' => 'string', 'required' => true], + 'end_date' => ['type' => 'string', 'required' => true], + 'enable_donation' => ['type' => 'integer', 'required' => true], + 'mailman' => ['type' => 'string', 'required' => true], + 'merch' => ['type' => 'integer', 'required' => false], + 'wifi' => ['type' => 'integer', 'required' => false], + 'enabled' => ['type' => 'integer', 'required' => true], + 'date_created' => ['type' => 'datetime', 'value' => new \DateTime()] + ]; + } +} diff --git a/app/src/Entities/Mailman.php b/app/src/Entities/Mailman.php new file mode 100644 index 0000000..f66d042 --- /dev/null +++ b/app/src/Entities/Mailman.php @@ -0,0 +1,15 @@ + ['type' => 'integer', 'primary' => true, 'autoincrement' => true], + 'dn' => ['type' => 'string', 'required' => true], + 'conference_id' => ['type' => 'integer', 'required' => true], + 'date_created' => ['type' => 'datetime', 'value' => new \DateTime()] + ]; + } +} diff --git a/app/src/Entities/Merchandise.php b/app/src/Entities/Merchandise.php new file mode 100644 index 0000000..b1ed48b --- /dev/null +++ b/app/src/Entities/Merchandise.php @@ -0,0 +1,18 @@ + ['type' => 'integer', 'primary' => true, 'required' => true], + 'conference_id' => ['type' => 'integer'], + 'title' => ['type' => 'string'], + 'description' => ['type' => 'string'], + 'image' => ['type' => 'string'], + 'variations' => ['type' => 'string'], + 'date_created' => ['type' => 'datetime', 'value' => new \DateTime()] + ]; + } +} \ No newline at end of file diff --git a/app/src/Entities/MerchandiseReservation.php b/app/src/Entities/MerchandiseReservation.php new file mode 100644 index 0000000..199ab9c --- /dev/null +++ b/app/src/Entities/MerchandiseReservation.php @@ -0,0 +1,17 @@ + ['type' => 'integer', 'primary' => true, 'autoincrement' => true], + 'conference_id' => ['type' => 'integer'], + 'dn' => ['type' => 'string'], + 'merch_id' => ['type' => 'integer'], + 'variation' => ['type' => 'string'], + 'date_created' => ['type' => 'datetime', 'value' => new \DateTime()] + ]; + } +} \ No newline at end of file diff --git a/app/src/Entities/Profile.php b/app/src/Entities/Profile.php new file mode 100644 index 0000000..2c13911 --- /dev/null +++ b/app/src/Entities/Profile.php @@ -0,0 +1,14 @@ + ['type' => 'string', 'primary' => true, 'required' => true], + 'data' => ['type' => 'string', 'required' => true], + 'date_created' => ['type' => 'datetime', 'value' => new \DateTime()] + ]; + } +} diff --git a/app/src/Entities/Registration.php b/app/src/Entities/Registration.php new file mode 100644 index 0000000..82fb2a6 --- /dev/null +++ b/app/src/Entities/Registration.php @@ -0,0 +1,17 @@ + ['type' => 'integer', 'primary' => true, 'autoincrement' => true], + 'dn' => ['type' => 'string', 'required' => true], + 'conference_id' => ['type' => 'string', 'required' => true], + 'data' => ['type' => 'string', 'required' => true], + 'cancelled' => ['type' => 'integer', 'required' => true], + 'date_created' => ['type' => 'datetime', 'value' => new \DateTime()] + ]; + } +} diff --git a/app/src/Entities/User.php b/app/src/Entities/User.php new file mode 100644 index 0000000..969ac64 --- /dev/null +++ b/app/src/Entities/User.php @@ -0,0 +1,18 @@ + ['type' => 'string', 'primary' => true, 'required' => true], + 'cn' => ['type' => 'string', 'required' => true], + 'email' => ['type' => 'string'], + 'gender' => ['type' => 'string'], + 'ircnick' => ['type' => 'string'], + 'homepostaladdress' => ['type' => 'string'], + 'date_created' => ['type' => 'datetime', 'value' => new \DateTime()] + ]; + } +} diff --git a/app/src/Entities/Wifi.php b/app/src/Entities/Wifi.php new file mode 100644 index 0000000..9e045b7 --- /dev/null +++ b/app/src/Entities/Wifi.php @@ -0,0 +1,16 @@ + ['type' => 'integer', 'primary' => true, 'required' => true], + 'conference_id' => ['type' => 'integer', 'primary' => false, 'required' => true], + 'username' => ['type' => 'string', 'primary' => false, 'required' => false], + 'password' => ['type' => 'string', 'primary' => false, 'required' => false], + 'registration_id' => ['type' => 'integer', 'primary' => false, 'required' => false] + ]; + } +} diff --git a/app/src/Forms/FormBuilder.php b/app/src/Forms/FormBuilder.php new file mode 100644 index 0000000..fef178e --- /dev/null +++ b/app/src/Forms/FormBuilder.php @@ -0,0 +1,91 @@ +view = $view; + } + + public function buildForm($formdata, $formvalues, $organisation = [99]) { + $formvalues = $this->sanitizeFormValues($formvalues); + + $formCollections = []; + + foreach($organisation as $collection) { + $inputs = []; + for($i = 0; $i < $collection; $i++) { + $inputs[] = array_shift($formdata->forms); + } + $formCollections[] = $inputs; + } + + $formCollections = array_map(function($forms) use ($formvalues) { + return implode("\r\n", array_map(function($form) use ($formvalues) { + if(!$form) { + return; + } + return $this->{"get".ucfirst($form->type)}($form, $formvalues); + }, $forms)); + }, $formCollections); + + $formCollections[] = $this->getSubmitButton($formdata->submitlabel, $formvalues); + + return $formCollections; + } + + private function getText($form, $formvalues) { + return $this->view->fetch("formbuilder/text.twig", ['form' => (array)$form, 'values' => $formvalues]); + } + + private function getDropdown($form, $formvalues) { + if(is_string($form->data)) { + $values = json_decode(file_get_contents(implode("", [dirname(__FILE__),"/../../configuration/data/", strtolower($form->data), ".json"])))->data; + + $form->data = new \stdClass(); + $form->data->type = "list"; + $form->data->values = $values; + } + + return $this->view->fetch("formbuilder/dropdown.twig", ['form' => (array)$form, 'values' => $formvalues]); + } + + private function getCheckboxes($form, $formvalues) { + return $this->view->fetch("formbuilder/checkboxes.twig", ['form' => (array)$form, 'values' => $formvalues]); + } + + private function getTextarea($form, $formvalues) { + return $this->view->fetch("formbuilder/textarea.twig", ['form' => (array)$form, 'values' => $formvalues]); + } + + private function getInformation($form, $formvalues) { + return $this->view->fetch("formbuilder/information.twig", ['form' => (array)$form, 'values' => $formvalues]); + } + + private function getDate($form, $formvalues) { + return $this->view->fetch("formbuilder/date.twig", ['form' => (array)$form, 'values' => $formvalues]); + } + + private function getSubmitButton($label, $formvalues) { + return $this->view->fetch("formbuilder/submit.twig", ["label" => $label]); + } + + private function sanitizeFormValues($formvalues) { + $formvalues = (array)$formvalues; + foreach($formvalues as $key => $val) { + if(!is_string($val)) { + $val = (array)$val; + foreach($val as $k => $v) { + $val[$k] = $v; + } + } + $formvalues[str_replace('_', ' ', $key)] = $val; + } + + return $formvalues; + } + +} \ No newline at end of file diff --git a/app/src/Login/LoginManager.php b/app/src/Login/LoginManager.php new file mode 100644 index 0000000..6375c36 --- /dev/null +++ b/app/src/Login/LoginManager.php @@ -0,0 +1,173 @@ +configuration = $configuration; + $this->database = $database; + $this->override = $override; + } + + public function login($username, $password) { + $ds = ldap_connect($this->configuration['ldap_host'], $this->configuration['ldap_port']); + ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3); + + $login = ldap_bind($ds, implode("", ["uid=",$username,",",$this->configuration['base_dn']]), $password); + + if(!$login) { + ldap_close($ds); + return false; + } + + $sr=ldap_search($ds, $this->configuration['base_dn'], implode("", ["uid=",$username])); + $userDetails = ldap_get_entries($ds, $sr)[0]; + + $cn = $userDetails['cn'][0]; + $email = $userDetails['mail'][0]; + $gender = $userDetails['gender'][0]; + $ircnick = $userDetails['ircnick'][0]; + $homepostaladdress = $userDetails['homepostaladdress'][0]; + $dn = $userDetails['dn']; + + $this->addOrUpdatePerson($cn, $email, $gender, $ircnick, $homepostaladdress, $dn); + + $sr=ldap_search($ds, 'ou=groups,dc=kde,dc=org', 'cn=akademy-team'); + $groupDetails = ldap_get_entries($ds, $sr)[0]['member']; + + $adminUser = false; + foreach($groupDetails as $member) { + if($member == $dn) { + $adminUser = true; + } + } + + ldap_close($ds); + + $this->createLoginCookie($dn, $adminUser); + + return true; + } + + public function logout() { + setcookie('conf-registration', false, strtotime("-1 day")); + } + + public function isLoggedIn() { + if($this->override) { + return true; + } + if(!isset($_COOKIE['conf-registration'])) { + return false; + } + + $cookie = $_COOKIE['conf-registration']; + + if(!$cookie) { + return false; + } + + $cookie = base64_decode($cookie); + + if(!$cookie) { + return false; + } + + $cookie = openssl_decrypt($cookie, 'AES-128-CBC', $this->configuration['salt']); + + if(!$cookie) { + return false; + } + + $cookie = json_decode($cookie); + + if(!$cookie || !is_object($cookie) || !property_exists($cookie, 'dn')) { + return false; + } + + $mapper = $this->database->mapper('Entity\User'); + + $query = $mapper->where(['dn' => $cookie->dn]); + + if($query->count()) { + return true; + } + + return false; + } + + public function getCurrentUser() { + if($this->override) { + return $this->override['dn']; + } + + if(!isset($_COOKIE['conf-registration'])) { + return false; + } + + $cookie = $_COOKIE['conf-registration']; + $cookie = base64_decode($cookie); + $cookie = openssl_decrypt($cookie, 'AES-128-CBC', $this->configuration['salt']); + $cookie = json_decode($cookie); + + return $cookie->dn; + } + + public function isAdminUser() { + return false; + if($this->override) { + return $this->override['admin']; + } + + return true; + + if(!$this->isLoggedIn()) { + return false; + } + + if(!isset($_COOKIE['conf-registration'])) { + return false; + } + + $cookie = $_COOKIE['conf-registration']; + $cookie = base64_decode($cookie); + $cookie = openssl_decrypt($cookie, 'AES-128-CBC', $this->configuration['salt']); + $cookie = json_decode($cookie); + + return (boolean)$cookie->admin; + } + + private function createLoginCookie($dn, $adminUser) { + $userobject = [ 'dn' => $dn, 'admin' => $adminUser ]; + $cookie_string = base64_encode(openssl_encrypt(json_encode($userobject), 'AES-128-CBC', $this->configuration['salt'])); + + setcookie('conf-registration', $cookie_string, strtotime("+1 Year")); + } + + private function addOrUpdatePerson($cn, $email, $gender, $ircnick, $homepostaladdress, $dn) { + $mapper = $this->database->mapper('Entity\User'); + + $query = $mapper->where(['dn' => $dn]); + if($query->count() > 0) { + $user = $query->first(); + } else { + $user = $mapper->build([ + 'dn' => $dn + ]); + } + + $user->cn = $cn; + $user->email = $email; + $user->gender = $gender; + $user->ircnick = $ircnick; + $user->homepostaladdress = $homepostaladdress; + + $mapper->save($user); + } + +} diff --git a/app/templates/api/merchandise.twig b/app/templates/api/merchandise.twig new file mode 100644 index 0000000..1734cf0 --- /dev/null +++ b/app/templates/api/merchandise.twig @@ -0,0 +1 @@ +{{ data | json_encode() | raw }} \ No newline at end of file diff --git a/app/templates/api/registration.twig b/app/templates/api/registration.twig new file mode 100644 index 0000000..1734cf0 --- /dev/null +++ b/app/templates/api/registration.twig @@ -0,0 +1 @@ +{{ data | json_encode() | raw }} \ No newline at end of file diff --git a/app/templates/base.twig b/app/templates/base.twig new file mode 100644 index 0000000..e9e7c5b --- /dev/null +++ b/app/templates/base.twig @@ -0,0 +1,70 @@ + + +
+ + + {% block head %} + +{{form.text | raw | nl2br}}
\ No newline at end of file diff --git a/app/templates/formbuilder/submit.twig b/app/templates/formbuilder/submit.twig new file mode 100644 index 0000000..5f77dd8 --- /dev/null +++ b/app/templates/formbuilder/submit.twig @@ -0,0 +1,5 @@ +{{conference.start_date}} - {{conference.end_date}}
++ I can no longer attend + {% else %} + Registration + {% endif %} +
{{conference.start_date}} - {{conference.end_date}}
++ + + +{% endblock %} diff --git a/app/templates/profile.twig b/app/templates/profile.twig new file mode 100644 index 0000000..bf79004 --- /dev/null +++ b/app/templates/profile.twig @@ -0,0 +1,31 @@ +{% extends "base.twig" %} + +{% block title %}- Dashboard{% endblock %} + +{% block content %} + + {% if saved == true %} +