Added Twig and started templating the pages.

templating
Eirik Th S 2022-04-15 21:48:13 +02:00
parent ccd8557362
commit 2f394e530f
18 changed files with 1093 additions and 232 deletions

3
.gitignore vendored
View File

@ -3,3 +3,6 @@ www/webdata/config.php
www/temp/
www/css/archive/*
www/js/archive/*
vendor
config/config.ini
tmp/

56
Router.php Normal file
View File

@ -0,0 +1,56 @@
<?php
const PRODUCTION = false;
const DEBUG = true;
if(DEBUG){
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
}
// Composer autoload
require_once 'vendor/autoload.php';
// Application and Models autoload
spl_autoload_register(function ($class_name) {
if(file_exists(__DIR__."/application/".$class_name.'.php')){
include __DIR__."/application/".$class_name.'.php';
}
elseif(file_exists(__DIR__."/models/".$class_name.'.php')) {
include __DIR__."/models/".$class_name.'.php';
}
});
// Define Twig
$twigSettings = PRODUCTION?['cache' => __dir__.'/tmp/']:[];
$loader = new Twig\Loader\FilesystemLoader(__dir__ . '/templates/'.Config::get('system', 'template', 'default')."/");
$twig = new Twig\Environment($loader, $twigSettings);
class Config {
private static $instance = null;
private $config;
private function __construct($file = 'config/config.ini'){
$this->config = parse_ini_file($file, true);
}
public static function get(string $category, string $config, $default = false){
// Singleton method to avoid reading the same file (too) many times
if (self::$instance == null){
self::$instance = new Config();
}
$conf = self::$instance;
if(!empty($conf->config[$category]) && !empty($conf->config[$category][$config])){
return $conf->config[$category][$config];
}
return $default;
}
}

173
application/Auth.php Normal file
View File

@ -0,0 +1,173 @@
<?php
class Auth {
private static $instance = null;
private $isLoggedIn = false;
private function __construct(){
session_start();
}
public static function getInstance(): Auth {
if(self::$instance == null){
self::$instance = new Auth();
}
return self::$instance;
}
public static function loginWithCredentials($email, $pass, bool $stayLoggedIn = false){
$Auth = new Auth();
$err = [];
// get user from database
$getUserSQL = "SELECT pwd, user_id, user_email FROM user WHERE user_email = ? LIMIT 1;";
$getUserRes = DB::query($getUserSQL, $email);
$dbPass = "missingPassword";
$dbUserId = 0;
$dbUserName = "Unknown";
if($getUserRes->num_rows == 1){
// Verify password
list($dbPass, $dbUserId, $dbUserName) = $getUserRes->fetch_row();
}
if(password_verify(PwdGen($pass), $dbPass)){
session_regenerate_id();
$md5agent = md5($_SERVER['HTTP_USER_AGENT']);
$userKey = GenKey();
$shaUserKey = sha1($userKey);
$expire = $stayLoggedIn?30:0; // if "stay logged in", stay logged in for 30 days.
$updateUserSQL = "INSERT INTO user_login (user_id, ckey, ctime, expire, agent) VALUES (?, ?, ?, ?, ?);";
if(DB::query($updateUserSQL, $dbUserId, $userKey, time(), $expire, $md5agent)){
$_SESSION['user_id'] = $dbUserId;
$_SESSION['user_name'] = $dbUserName;
$_SESSION['user_agent'] = $md5agent;
$_SESSION['user_key'] = $shaUserKey;
if($stayLoggedIn){
setcookie("auth", $shaUserKey.".".$dbUserId, time() + (30 * 24*60*60), "/", $_SERVER['HTTP_HOST'], isset($_SERVER['HTTPS']), true );
}
return true;
}
else {
$err[] = "Failed to login.\n".$db->error;
}
}
else {
$err[] = "Username and/or Password is wrong.";
}
return $err;
}
public static function loginFromAuthKey(): bool {
if(isset($_COOKIE['auth'])){
$md5agent = md5($_SERVER['HTTP_USER_AGENT']);
$cookieSplode = explode('.', $_COOKIE['auth']);
if(count($cookieSplode) == 2){
$tkey = $cookieSplode[0];
$tuid = $cookieSplode[1];
$verifyLoginCookieRes = DB::query("
SELECT ul.user_id, ul.ckey, ul.ctime, ul.expire, ul.agent, u.full_name
FROM user_login ul
INNER JOIN user u ON ul.user_id = u.user_id
WHERE ul.user_id = ?
ORDER BY ctime DESC", $tuid);
while($row = $verifyLoginCookieRes->fetch_assoc()){
if($row['expire'] == 0){ $row['expire'] = 1; }
if(
sha1($row['ckey']) == $tkey &&
$row['agent'] == $md5agent &&
($row['ctime']+($row['expire']*24*60*60) > time())
){
session_regenerate_id();
$_SESSION['user_id'] = $row['user_id'];
$_SESSION['user_name'] = $row['full_name'];
$_SESSION['user_agent'] = $md5agent;
$_SESSION['user_key'] = sha1($row['ckey']);
return true;
}
}
}
}
return false;
}
public static function logout(){}
public static function checkLogin($strict = false): bool {
$Auth = Auth::getInstance();
if(
($Auth->isLoggedIn && !$strict) ||
$Auth->checkSessionLogin() ||
$Auth->loginFromAuthKey()
) {
return true;
}
unset($_SESSION['user_id']);
return false;
}
private function checkSessionLogin(): bool {
if(isset($_SESSION['user_id'])){
$md5agent = md5($_SERVER['HTTP_USER_AGENT']);
if($md5agent == @$_SESSION['user_agent']){
$verifyLoginSessionRes = DB::query("
SELECT expire, ctime, ckey
FROM `user_login`
WHERE user_id = ?
AND agent = ?",
$_SESSION['user_id'], $md5agent);
while ($row = $verifyLoginSessionRes->fetch_assoc()){
if(
(sha1($row['ckey']) == $_SESSION['user_key']) &&
($row['expire'] == 0 || $row['ctime']+($row['expire']*24*60*60) > time())
){
return true;
}
}
}
}
return false;
}
}
function PwdGen($pass, $returnHashed = false): string {
$pepper = Config::get('security', 'pepper', 'IAmBadAtSecurity');
$pwd_peppered = hash_hmac("sha256", $pass, $pepper);
if(!$returnHashed){
return $pwd_peppered;
}
return password_hash($pwd_peppered, PASSWORD_ARGON2ID, ['threads' => 2]);
}
function GenKey($length = 21): string{
$password = "";
$possible = "0123456789abcdefghijkmnopqrstuvwxyz";
$i = 0;
while($i < $length){
$char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
if(!strstr($password, $char)){
$password .= $char;
$i++;
}
}
return $password;
}

82
application/DB.php Normal file
View File

@ -0,0 +1,82 @@
<?php
class DB {
private static $instance = null;
private $db;
private function __construct(){
$this->db = new mysqli(
Config::get('database', 'host'),
Config::get('database', "user"),
Config::get('database', "pass"),
Config::get('database', "database")
);
if($this->db->connect_error){
die("Connection failed: " . $this->db->connect_error);
}
}
public static function getInstance(): DB {
if(self::$instance == null){
self::$instance = new DB();
}
return self::$instance;
}
public static function query($sql, ...$params){
return self::doQuery(0, $sql, ...$params);
}
public static function queryTest($sql, ...$params){
return self::doQuery(1, $sql, ...$params);
}
public static function queryPreview($sql, ...$params){
return self::doQuery(2, $sql, ...$params);
}
private static function doQuery($mode, $sql, ...$params){
/*
Modes:
0: Execute normally
1: Execute, but print query
2: Only print query
*/
// Check if query parameters comes as an array, or as individual arguments
if(count($params) == 1 && is_array($params[0])){
$params = $params[0];
}
// Create type-specification for query
$types = "";
foreach($params as &$value){
if(is_double($value)){
$types .= "d";
}
elseif(is_integer($value)){
$types .= "i";
}
else {
$types .= "s";
}
}
$db = DB::getInstance();
if($mode >= 1){
printf(str_replace('?', "'%s'", $sql)."\n", ...$params);
}
if($mode >= 2) {
return [];
}
$stmt = $db->db->prepare($sql);
$stmt->bind_param($types, ...$params);
$stmt->execute();
return $stmt->get_result();
}
}

92
application/WebPage.php Normal file
View File

@ -0,0 +1,92 @@
<?php
interface RequireAuth {}
class WebPage {
protected $httpStatus = 200;
protected $activePage;
public $status;
public $template;
public $title = "PaperBag";
public $lang = "EN";
public $navbar;
public $loggedIn = false;
public $pr = ""; // project root
public function __construct(){
$this->pr = Config::get('system', 'projectroot', '');
$this->loggedIn = Auth::checkLogin();
if($this instanceof RequireAuth && !$this->loggedIn) {
$returnTo = str_ireplace('index.php', '', $_SERVER['PHP_SELF']);
$_SESSION['pre-auth'] = $returnTo;
header("Location: ".$this->pr."/login.php");
return;
}
try {
$this->prepareNavbar();
$this->load();
if(!$this->template){
$this->template = str_replace('.php', '.html', $_SERVER['SCRIPT_NAME']);
}
if($this->httpStatus == 200){
global $twig;
if(DEBUG){
header("X-Template: ".$this->template);
header("X-Script-name: ".$_SERVER['SCRIPT_NAME']);
}
http_response_code(200);
echo $twig->render($this->template, $this->vars());
}
else {
http_response_code($this->httpStatus);
print_r( $this->vars() );
}
}
catch (\Twig\Error\LoaderError $e){
http_response_code(500);
if( $e->getCode() == 0){
print_r($e->getMessage());
}
else {
echo $e;
}
}
catch(\Twig\Error\RuntimeError | \Twig\Error\SyntaxError | Exception $e) {
http_response_code(500);
echo $e;
print_r( $this->vars() );
}
}
function load(){ throw new Exception("Incomplete implementation"); }
function vars(): array {
return (array) $this;
}
private function prepareNavbar(){
$this->activePage = str_replace('index.php', '', $_SERVER['REQUEST_URI']);
$this->navbar['links'] = array_merge([
["href"=>"/", "name"=>"Home", "active"=>$this->activePage=="/"?'active':''],
["href"=>"/plan/", "name"=>"Plan", "active"=>$this->activePage=="/plan/"?'active':''],
["href"=>"/review/", "name"=>"Review", "active"=>$this->activePage=="/review/"?'active':'']
], $this->navbar ?? []);
}
}
class WebPageAuth extends WebPage {
public function __construct(){
if(Auth::checklogin()){
parent::__construct();
}
}
}

7
composer.json Normal file
View File

@ -0,0 +1,7 @@
{
"require": {
"twbs/bootstrap": "5.1.3",
"twig/twig": "^3.3",
"ext-mysqli": "*"
}
}

310
composer.lock generated Normal file
View File

@ -0,0 +1,310 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0daa3afffc94d70bd593d310cef916f8",
"packages": [
{
"name": "symfony/polyfill-ctype",
"version": "v1.25.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "30885182c981ab175d4d034db0f6f469898070ab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab",
"reference": "30885182c981ab175d4d034db0f6f469898070ab",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-10-20T20:35:02+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.25.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-11-30T18:21:41+00:00"
},
{
"name": "twbs/bootstrap",
"version": "v5.1.3",
"source": {
"type": "git",
"url": "https://github.com/twbs/bootstrap.git",
"reference": "1a6fdfae6be09b09eaced8f0e442ca6f7680a61e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twbs/bootstrap/zipball/1a6fdfae6be09b09eaced8f0e442ca6f7680a61e",
"reference": "1a6fdfae6be09b09eaced8f0e442ca6f7680a61e",
"shasum": ""
},
"replace": {
"twitter/bootstrap": "self.version"
},
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Otto",
"email": "markdotto@gmail.com"
},
{
"name": "Jacob Thornton",
"email": "jacobthornton@gmail.com"
}
],
"description": "The most popular front-end framework for developing responsive, mobile first projects on the web.",
"homepage": "https://getbootstrap.com/",
"keywords": [
"JS",
"css",
"framework",
"front-end",
"mobile-first",
"responsive",
"sass",
"web"
],
"support": {
"issues": "https://github.com/twbs/bootstrap/issues",
"source": "https://github.com/twbs/bootstrap/tree/v5.1.3"
},
"time": "2021-10-09T06:43:19+00:00"
},
{
"name": "twig/twig",
"version": "v3.3.10",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "8442df056c51b706793adf80a9fd363406dd3674"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/8442df056c51b706793adf80a9fd363406dd3674",
"reference": "8442df056c51b706793adf80a9fd363406dd3674",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3"
},
"require-dev": {
"psr/container": "^1.0",
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.3-dev"
}
},
"autoload": {
"psr-4": {
"Twig\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
},
{
"name": "Twig Team",
"role": "Contributors"
},
{
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"role": "Project Founder"
}
],
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "https://twig.symfony.com",
"keywords": [
"templating"
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.3.10"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2022-04-06T06:47:41+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.2.0"
}

13
models/Model.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace models;
abstract class Model {
// public function set($value, )
public function save(){
}
}

22
models/Recipe.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace models;
class Recipe extends Model {
public static function getRecipes() : array {
$db = $db ?? database();
$recipes = [];
$result = $db->query("SELECT * FROM recipe");
while($row = $result->fetch_assoc()){
$recipe = new Recipe();
$recipe->
$recipes[] = $recipe;
}
return $recipes;
}
}

5
models/products.php Normal file
View File

@ -0,0 +1,5 @@
<?php
require_once $_SERVER['DOCUMENT_ROOT'].'/webdata/init.php';
include $_SERVER['DOCUMENT_ROOT']."/api/v1/products.php";

View File

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="{{ language|default('en') }}">
<head>
<meta charset='UTF-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>{{ title|default('PaperBag') }}</title>
<link rel='stylesheet' href='{{ pr }}/css/bootstrap.min.css' type='text/css' />
<link rel='stylesheet' href='{{ pr }}/css/index.css' type='text/css' />
<link rel='apple-touch-icon' sizes='180x180' href='{{ pr }}/apple-touch-icon.png'>
<link rel='icon' type='image/png' sizes='32x32' href='{{ pr }}/favicon-32x32.png'>
<link rel='icon' type='image/png' sizes='16x16' href='{{ pr }}/favicon-16x16.png'>
<link rel='manifest' href='{{ pr }}/site.webmanifest'>
<link rel='mask-icon' href='{{ pr }}/favicon/safari-pinned-tab.svg' color='#5bbad5'>
<meta name='msapplication-TileColor' content='#da532c'>
<meta name='theme-color' content='#ffffff'>
<script src='{{ pr }}/js/jquery-3.5.1.min.js'></script>
<script src='{{ pr }}/js/popper.min.js'></script>
<script src='{{ pr }}/js/bootstrap.min.js'></script>
<!--<script src='//cdn.jsdelivr.net/npm/marked/marked.min.js'></script>-->
<script src='{{ pr }}/js/hammer.js'></script>
<script src='{{ pr }}/js/hammer.jquery.js'></script>
{% block head %}{% endblock %}
</head>
<body id="{{ pagekey|default('') }}">
<div id="page-container">
<div id="page-wrapper">
<nav class="navbar navbar-expand-sm navbar-light bg-light" style="z-index: 2;">
<div class="container-fluid" style="max-width: 900px;">
<a class="navbar-brand" href="{{ pr }}/"><img src='{{ pr }}/paperbag_small.svg' style="height: 24px;" alt='Logo' /> PaperBag</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-sm-0">
{% for link in navbar.links %}
<li class="nav-item">
<a class="nav-link {{ link.active }}" aria-current="page" href="{{ link.href }}">{{ link.name }}</a>
</li>
{% endfor %}
</ul>
<div class="navbar-nav" id="navLogin">
{% if loggedIn %}
<a class='nav-item nav-link' href='{{ pr }}/logout.php'>Log out</a>
{% else %}
<a class="nav-item nav-link" id='login' href='{{ pr }}/login.php'>Login</a>
{% endif %}
</div>
</div>
</div>
</nav>
{% if err or msg %}
<div class='container-sm' style="max-width: 540px;">
{% for e in err %}
<div class='alert alert-danger' role='alert'>{{ e }}</div>
{% endfor %}
{% for m in msg %}
<div class='alert alert-success' role='alert'>{{ m }}</div>
{% endfor %}
</div>
{% endif %}
{% block content %}{% endblock %}
</div>
<footer>
Any prices you find on this website is added by its users and are only a guideline.<br>
<strong>PaperBag</strong> is a <a href="//svagard.no/projects/paperbag" target="_blank" rel="nofollow">Svagård</a> project<br>
{#
<!--<a href="https://useg.it/eirik/GroceryAssist" target="_blank" rel="nofollow">View source</a> |
<a href="/cookies" rel="nofollow">Cookies</a> |
<a href="/tos" rel="nofollow">Terms of Use</a> |
<a href="/privacy" rel="nofollow">Privacy Policy</a>-->
#}
{% block footer %}{% endblock %}
</footer>
</div>
</body>
</html>

View File

@ -0,0 +1,40 @@
{% extends "assets/base.html.twig" %}
{% block content %}
<h1 class="headline text-center"><img src='paperbag_small.svg' style="width: 4rem;" alt='Logo' /> PaperBag</h1>
<div class="container-sm" style="padding-top: 5px;">
<div class="card-group" style="text-align: center;">
<div class="card h-100" style="">
<a href='./plan'>
<!-- <img src="#" class="card-img-top" alt="Plan">-->
<div class="card-body">
<h5 class="card-title">Plan shopping</h5>
<p class="card-text"></p>
</div>
</a>
</div>
<div class="card h-100" style="">
<!-- <img src="#" class="card-img-top" alt="Review">-->
<div class="card-body">
<h5 class="card-title">Review</h5>
<p class="card-text">Coming soon</p>
</div>
</div>
</div>
<div class="card-group" style="text-align: center;">
<div class="card" style="">
<div class="card-body">
<h5 class="card-title">Recent changes</h5>
<pre class="card-text" style="text-align: left; width: min-content; max-width: 100%; margin: auto; max-height: 150px; overflow-y: auto; padding: 0 15px;">{{ changes }}</pre>
</div>
<div class="card-footer">For more details see the <a href="https://useg.it/eirik/PaperBag/commits/branch/master">commits</a></div>
</div>
</div>
<?php } ?>
</div>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "assets/base.html.twig" %}
{% block content %}
<div class='container-sm' style="max-width: 540px;">
<h1 class="headline text-center">Login</h1>
<form action="login.php" method="POST">
<div class="form-group row mb-1">
<label for="loginEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="email" class="form-control" name="loginEmail" id="loginEmail">
</div>
</div>
<div class="form-group row">
<label for="loginPwd" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" class="form-control" name="loginPwd" id="loginPwd">
</div>
</div>
<div class="form-check" style="margin: 3px 0;">
<input class="form-check-input" type="checkbox" id="gridCheck">
<label class="form-check-label" for="gridCheck">
I agree to save a temporary cookie in my browser for the logged in functions to work.
</label>
</div>
<div class="form-check" style="margin: 3px 0;">
<input class="form-check-input" type="checkbox" id="rememberCheck" name="stayLoggedIn">
<label class="form-check-label" for="rememberCheck">
Remember me (for 30 days)
</label>
</div>
<button type="submit" class="btn btn-primary mb-3 w-100">Login</button>
</form>
<p>Do you not have an account? <a href="register.php">Register</a> </p>
</div>
{% endblock %}

View File

@ -0,0 +1,82 @@
{% extends "assets/base.html.twig" %}
{% block head %}
<!-- jsDelivr :: Sortable :: Latest (https://www.jsdelivr.com/package/npm/sortablejs) -->
<script src="/js/sortable/Sortable.min.js"></script> <!-- CDN: https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js -->
<script src="/js/sortable/jquery-sortable.js"></script> <!-- CDN: https://cdn.jsdelivr.net/npm/jquery-sortablejs@latest/jquery-sortable.js -->
{% endblock %}
{% block content %}
<h1 class="headline text-center">PaperBag</h1>
<div class="container" style="padding-top: 5px; padding-bottom: 15px; text-align: center;">
<div id="spaceSelectWrapper" style="max-width: 900px; margin: auto;">
<div class="input-group" style="width: 200px;">
<label class="input-group-text" for="spaceSelect">Space:</label>
<select class="form-select" id='spaceSelect' aria-label="Select space">
<option>...</option>
</select>
</div>
</div>
<div class="card-columns" id="stores" style="text-align: center;">
<noscript>
<hr>
<h5>Please enable javascript for this page to work</h5>
<hr>
</noscript>
</div>
<div id='totalPriceWrapper'>Space subtotal: <span id="totalPrice" class="priceWrapper price">00.00</span></div>
<br>
<button class="btn btn-primary mb-2" id="addStore">Add store</button><br>
<button class="btn btn-secondary" id="refreshAll">Refresh all</button>
</div>
<script src='htmlElements.js'></script>
<script src='plan.js'></script>
<script src='recipe.js'></script>
<script src='product.js'></script>
<div class="modal" tabindex="-1" id="addStoreModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add store</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body align-content-center">
<button class="btn btn-primary" id="addEmptyStore">Empty store</button>
<hr>
<div class="accordion mb-2" id="addStoreRecipe"></div>
</div>
</div>
</div>
</div>
<script>
let addStoreModal = new bootstrap.Modal( document.getElementById('addStoreModal') );
$("#addStore").on('click', ev=>{
ev.stopPropagation();
addStoreModal.show();
});
$("#addStoreModal").on('show.bs.modal', ev=> {
$("#addStoreRecipe").html('');
$.getJSON('/api/v1/recipe', {}, resp => {
let recipes = new Recipe(resp);
recipes.getAccordionHtml('#addStoreRecipe');
});
});
$("#addEmptyStore").on('click', ev => {
stores.push(new Store());
addStoreModal.hide();
});
</script>
{% endblock %}

View File

@ -1,56 +1,17 @@
<?php require 'webdata/init.php'; ?><!DOCTYPE html>
<html lang="en">
<head>
<?=getHtmlHeaders();?>
<title>PaperBag - Plan & Execute Your Shopping</title>
</head>
<body id="home">
<div id="page-container">
<div id="page-wrapper">
<?php
require_once '../Router.php';
<?php include 'webdata/navbar.php'; ?>
class HomePage extends WebPage {
public $pagekey = "home";
public $title = "PaperBag - Plan & Execute Your Shopping";
<h1 class="headline text-center"><img src='paperbag_small.svg' style="height: 4rem;" alt='Logo' /> PaperBag</h1>
<div class="container-sm" style="padding-top: 5px;">
function load(){
<div class="card-group" style="text-align: center;">
if (file_exists('changes.txt')) {
$this->changes = file_get_contents('changes.txt');
}
<div class="card h-100" style="">
<a href='./plan'>
<!-- <img src="#" class="card-img-top" alt="Plan">-->
<div class="card-body">
<h5 class="card-title">Plan shopping</h5>
<p class="card-text"></p>
</div>
</a>
</div>
}
}
<div class="card h-100" style="">
<!-- <img src="#" class="card-img-top" alt="Review">-->
<div class="card-body">
<h5 class="card-title">Review</h5>
<p class="card-text">Coming soon</p>
</div>
</div>
</div>
<?php if(file_exists('changes.txt')){
$changes = file_get_contents('changes.txt');
?>
<div class="card-group" style="text-align: center;">
<div class="card" style="">
<div class="card-body">
<h5 class="card-title">Recent changes</h5>
<pre class="card-text" style="text-align: left; width: min-content; max-width: 100%; margin: auto; max-height: 150px; overflow-y: auto; padding: 0 15px;"><?=$changes;?></pre>
</div>
<div class="card-footer">For more details see the <a href="https://useg.it/eirik/PaperBag/commits/branch/master">commits</a></div>
</div>
</div>
<?php } ?>
</div>
</div>
<?php include 'webdata/footer.html'; ?>
</div>
</body>
</html>
$a = new HomePage();

View File

@ -1,7 +1,55 @@
<?php
require 'webdata/init.php';
$returnToPage = "./";
require_once '../Router.php';
class LoginPage extends WebPage {
public $pagekey = "login";
public $title = "PaperBag - Plan & Execute Your Shopping";
function load(){
$returnToPage = $_POST['referrerPage'] ?? $_SESSION['pre-auth'] ?? explode($_SERVER['HTTP_HOST'], $_SERVER['HTTP_REFERER'])[1];
// TODO: check if returntopage is an auth-changing page (e.g: login / register, and if so don't redirect there!
$this->msg[] = $returnToPage;
if(Auth::checkLogin(true)){
header("Location: ".$returnToPage);
}
if(isset($_GET['thank'])){
$this->msg[] = "Thank you for registering. Please log in to continue!";
}
if(isset($_POST) && !empty($_POST)){
$data = [];
foreach($_POST as $key => $value){
if(($data[$key] = filter($value)) === false){
print_r($value);
echo "Failed to sanitize: `".$key."`: ".$value." \t-\t type: ".gettype($value)."\n";
}
}
$stayLoggedIn = isset($_POST['stayLoggedIn']);
$err = Auth::loginWithCredentials($data['loginEmail'], $data['loginPwd'], $stayLoggedIn);
if($err === true){
header("Location: ".$returnToPage);
die();
}
}
}
}
$a = new LoginPage();
//require 'webdata/init.php';
/*$returnToPage = "./";
if(isset($_GET['return'])){
$returnToPage = $_GET['return'];
}
@ -10,103 +58,17 @@ elseif(isset($_POST['referrerPage'])){
}
elseif(isset($_SERVER['HTTP_REFERER'])){
$returnToPage = explode($_SERVER['HTTP_HOST'], $_SERVER['HTTP_REFERER'])[1];
}
}*/
if(stristr($returnToPage, "login.php") || stristr($returnToPage, "register.php")){
/*if(stristr($returnToPage, "login.php") || stristr($returnToPage, "register.php")){
$returnToPage = "./";
} else {
$returnToPage = str_ireplace('index.php', '', $returnToPage);
}
}*/
if( checkLogin() ){
/*if( checkLogin() ){
header("Location: ".$returnToPage);
}
$msg = [];
if(isset($_GET['thank'])){
$msg[] = "Thank you for registering. Please log in to continue!";
}
if(isset($_POST) && !empty($_POST)){
$db = database();
$data = [];
foreach($_POST as $key => $value){
if(($data[$key] = filter($value)) === false){
print_r($value);
echo "Failed to sanitize: `".$key."`: ".$value." \t-\t type: ".gettype($value)."\n";
}
}
$stayLoggedIn = isset($_POST['stayLoggedIn']);
$err = loginUser($data['loginEmail'], $data['loginPwd'], $stayLoggedIn);
if($err === true){
header("Location: ".$returnToPage);
die();
}
}
}*/
?><!DOCTYPE html>
<html lang="en">
<head>
<?=getHtmlHeaders();?>
<title>Login - PaperBag</title>
</head>
<body id='plan'>
<div id="page-container">
<div id="page-wrapper">
<?php include 'webdata/navbar.php'; ?>
<div class='container-sm' style="max-width: 540px;">
<h1 class="headline text-center">Login</h1>
<?php if(!empty($err)){
foreach($err as $e){
echo "<div class='alert alert-danger' role='alert'>$e</div>";
}
}
if(!empty($msg)){
foreach($msg as $m){
echo "<div class='alert alert-success' role='alert'>$m</div>";
}
}
?>
<form action="login.php" method="POST">
<input type="hidden" name="referrerPage" value="<?=$returnToPage;?>">
<div class="form-group row mb-1">
<label for="loginEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="email" class="form-control" name="loginEmail" id="loginEmail">
</div>
</div>
<div class="form-group row">
<label for="loginPwd" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" class="form-control" name="loginPwd" id="loginPwd">
</div>
</div>
<div class="form-check" style="margin: 3px 0;">
<input class="form-check-input" type="checkbox" id="gridCheck">
<label class="form-check-label" for="gridCheck">
I agree to save a temporary cookie in my browser for the logged in functions to work.
</label>
</div>
<div class="form-check" style="margin: 3px 0;">
<input class="form-check-input" type="checkbox" id="rememberCheck" name="stayLoggedIn">
<label class="form-check-label" for="rememberCheck">
Remember me (for 30 days)
</label>
</div>
<button type="submit" class="btn btn-primary mb-3 w-100">Login</button>
</form>
<p>Do you not have an account? <a href="register.php">Register</a> </p>
</div>
</div>
<?php include 'webdata/footer.html'; ?>
</div>
</body>
</html>

View File

@ -1,88 +1,12 @@
<?php $rPath = "../"; require $rPath.'webdata/init.php'; requireLogin(); ?><!DOCTYPE html>
<html lang="en">
<head>
<?=getHtmlHeaders($rPath);?>
<title>Plan - PaperBag - Plan Your Shopping</title>
<?php
require_once '../../Router.php';
<!-- jsDelivr :: Sortable :: Latest (https://www.jsdelivr.com/package/npm/sortablejs) -->
<script src="/js/sortable/Sortable.min.js"></script> <!-- CDN: https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js -->
<script src="/js/sortable/jquery-sortable.js"></script> <!-- CDN: https://cdn.jsdelivr.net/npm/jquery-sortablejs@latest/jquery-sortable.js -->
</head>
<body id='plan'>
<div id="page-container">
<div id="page-wrapper">
<?php include $rPath.'webdata/navbar.php'; ?>
<h1 class="headline text-center">PaperBag</h1>
<div class="container" style="padding-top: 5px; padding-bottom: 15px; text-align: center;">
class PlanPage extends WebPage implements RequireAuth {
public $pagekey = "plan";
public $title = "Plan - PaperBag - Plan & Execute Your Shopping";
<div id="spaceSelectWrapper" style="max-width: 900px; margin: auto;">
<div class="input-group" style="width: 200px;">
<label class="input-group-text" for="spaceSelect">Space:</label>
<select class="form-select" id='spaceSelect' aria-label="Select space">
<option>...</option>
</select>
</div>
</div>
function load(){
}
}
<div class="card-columns" id="stores" style="text-align: center;">
<hr>
<h5>Please enable javascript for this page to work</h5>
<hr>
</div>
<div id='totalPriceWrapper'>Space subtotal: <span id="totalPrice" class="priceWrapper price">00.00</span></div>
<br>
<button class="btn btn-primary mb-2" id="addStore">Add store</button><br>
<button class="btn btn-secondary" id="refreshAll">Refresh all</button>
</div>
<script src='htmlElements.js'></script>
<script src='plan.js'></script>
<script src='recipe.js'></script>
<script src='product.js'></script>
<!-- <script src='draggingClass.js'></script>-->
</div>
<?php include $rPath.'webdata/footer.html'; ?>
</div>
<div class="modal" tabindex="-1" id="addStoreModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add store</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body align-content-center">
<button class="btn btn-primary" id="addEmptyStore">Empty store</button>
<hr>
<div class="accordion mb-2" id="addStoreRecipe"></div>
</div>
</div>
</div>
</div>
<script>
let addStoreModal = new bootstrap.Modal( document.getElementById('addStoreModal') );
$("#addStore").on('click', ev=>{
ev.stopPropagation();
addStoreModal.show();
});
$("#addStoreModal").on('show.bs.modal', ev=> {
$("#addStoreRecipe").html('');
$.getJSON('/api/v1/recipe', {}, resp => {
let recipes = new Recipe(resp);
recipes.getAccordionHtml('#addStoreRecipe');
});
});
$("#addEmptyStore").on('click', ev => {
stores.push(new Store());
addStoreModal.hide();
});
</script>
</body>
</html>
$a = new PlanPage();

View File

@ -186,7 +186,7 @@ function logoutUser($everywhere = false){
session_destroy();
}
function PwdGen($pass, $returnHashed = false): string {
/*function PwdGen($pass, $returnHashed = false): string {
global $config;
$pepper = $config['general']['pepper'] ?? 'IAmBadAtSecurity';
@ -196,7 +196,7 @@ function PwdGen($pass, $returnHashed = false): string {
}
return password_hash($pwd_peppered, PASSWORD_ARGON2ID, ['threads' => 2]);
}
}* /
function GenKey($length = 21): string{
$password = "";
@ -215,7 +215,7 @@ function GenKey($length = 21): string{
}
return $password;
}
}*/
function checkLogin(): bool {
global $db, $_SESSION, $_COOKIE;