1
0
Fork 0
mirror of https://github.com/ThomasGsp/HyperProxmox.git synced 2025-02-14 12:12:16 +00:00

first commit

This commit is contained in:
thomas.guiseppin 2017-10-21 22:04:42 +02:00
commit 5352a2b94a
396 changed files with 10008 additions and 0 deletions

0
README.md Normal file
View file

View file

@ -0,0 +1,76 @@
### General
For all get request, there are a possibility to pass a payloads with differents param in json
```code
{'key1': 'value1', 'key2': 'value2'}
```
All actions need a valid ticket. This ticket is generate when the client valid the connexion on the backend php.
it's saved in the database with an expiration key.
Each tickets are uniq.
Example to dump database:
``` code
GET /api/v1/instance/<instanceid>/database/<databaseid> -d '{'userid': '1', 'ticket': 'SFQSF22dFF','action': 'dump'}'
```
##### Instance management
Actions limited to proxmox api (wrapper)
```code
GET /api/v1/instance
GET /api/v1/instance/<instanceid>
POST /api/v1/instance/<instanceid>
PUT /api/v1/instance/<instanceid>
DELETE /api/v1/instance/<instanceid>
```
##### Packages management
Actions in the instances for packages managements
```code
GET /api/v1/instance/<instanceid>/package
POST /api/v1/instance/<instanceid>/packages
PUT /api/v1/instance/<instanceid>/packages
DELETE /api/v1/instance/<instanceid>/package
```
##### vhosts management
Actions in the instances for packages managements
```code
GET /api/v1/instance/<instanceid>/vhost
GET /api/v1/instance/<instanceid>/vhost/<vhostid>
POST /api/v1/instance/<instanceid>/vhost/<vhostid>
PUT /api/v1/instance/<instanceid>/vhost/<vhostid>
DELETE /api/v1/instance/<instanceid>/vhost/<vhostid>
```
##### Databases
Actions in the instances for packages managements
```code
GET /api/v1/instance/<instanceid>/database
GET /api/v1/instance/<instanceid>/database/<databaseid>
POST /api/v1/instance/<instanceid>/database/<databaseid>
PUT /api/v1/instance/<instanceid>/database/<databaseid>
DELETE /api/v1/instance/<instanceid>/database/<databaseid>
```
##### Nodes management
Actions to proxmox nodes availables
```code
GET /api/v1/node
GET /api/v1/node/<nodeid>
```
##### services management
```code
GET /api/v1/service/ssl/instance/<instanceid>/vhost/<vhostid>
GET /api/v1/service/ssl/instance/<instanceid>/vhost/<vhostid>
POST api/v1/service/ssl/instance/<instanceid>/vhost/<vhostid>
PUT /api/v1/service/ssl/instance/<instanceid>/vhost/<vhostid>
DELETE /api/v1/service/ssl/instance/<instanceid>/vhost/<vhostid>
GET /api/v1/service/cache/instance/<instanceid>/vhost/<vhostid>
GET /api/v1/service/cache/instance/<instanceid>/vhost/<vhostid>
POST api/v1/service/cache/instance/<instanceid>/vhost/<vhostid>
PUT /api/v1/service/cache/instance/<instanceid>/vhost/<vhostid>
DELETE /api/v1/service/cache/instance/<instanceid>/vhost/<vhostid>
```

View file

@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

View file

@ -0,0 +1,129 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import threading
import web
from core.core import *
from core.modules.mod_analyst import *
import json
import time
import random
import ast
class Auth:
def POST(self):
# '{"username":"fff", "password":"azerty"}'
data = json.loads(web.data().decode('utf-8'))
print(data["username"])
# Test Login
# If true generate an ticket
return
class Cluster:
def GET(self, cluster=None):
if cluster:
return core.get_cluster(cluster)
else:
return core.get_cluster()
def POST(self):
data = json.loads(web.data().decode('utf-8'))
return core.insert_cluster(data)
def PUT(self, cluster):
data = json.loads(web.data().decode('utf-8'))
return core.change_cluster(cluster, data)
def DELETE(self, cluster):
return core.delete_cluste(cluster)
class Instance:
def GET(self, vmid=None, status=None):
if status:
""" GET INSTANCE STATUS """
return core.status_instance(vmid, status)
elif vmid:
""" GET INSTANCE INFORMATION """
return core.info_instance(vmid)
def POST(self, vmid=None, status=None):
if vmid:
""" GET INSTANCE INFORMATION """
return core.status_instance(vmid, status)
else:
""" CREATE NEWS INSTANCES"""
count = json.loads(web.data().decode('utf-8'))["count"]
""" GENERATE UNIQ COMMAND ID """
randtext = ''.join(random.choice('0123456789ABCDEF') for i in range(8))
command_id = "{0}_{1}_{2}".format(time.time(), count, randtext)
""" LOAD CLUSTER CONFIGURATIONS """
select = Analyse(core.clusters_conf, generalconf)
sorted_nodes = dict(select.set_attribution(count))
""" START ALL Thread """
for target, count in sorted_nodes.items():
# Limit to 5 instance per block
thci = threading.Thread(name="Insert Instance",
target=core.insert_instance,
args=(target, str(count), command_id,))
thci.start()
""" Wait all results of Thread from redis messages queue. Valid or not """
timeout = 2 * int(generalconf["deploy"]["concurrencydeploy"]) * int(generalconf["deploy"]["delayrounddeploy"])
for t in range(timeout):
time.sleep(1)
try:
if len(ast.literal_eval(Lredis.get_message(command_id))) == int(count):
break
except BaseException as err:
print("Value not found", err)
""" Return messages """
return ast.literal_eval(Lredis.get_message(command_id))
def PUT(self, vmid):
data = json.loads(web.data().decode('utf-8'))
return core.change_instance(vmid, data)
def DELETE(self, vmid):
return core.delete_instance(vmid)
class ThreadAPI(threading.Thread):
def __init__(self, threadid, name, urls, c, g, r):
""" Pass Global var in this theard."""
global core, generalconf, Lredis
core = c
generalconf = g
Lredis = r
""" RUN API """
threading.Thread.__init__(self)
self.threadID = threadid
self.threadName = name
self.app = HttpApi(urls, globals())
self.app.notfound = notfound
def run(self):
print("Start API server...")
self.app.run()
def stop(self):
print("Stop API server...")
self.app.stop()
def notfound():
return web.notfound({"value": "Bad request"})
class HttpApi(web.application):
def run(self, ip="127.0.0.1", port=8080, *middleware):
func = self.wsgifunc(*middleware)
return web.httpserver.runsimple(func, (ip, int(port)))

View file

View file

View file

@ -0,0 +1,3 @@
##### Core funtions
The core is divised manage load differents modules with the goal to manage then and orchestrate all function.

Binary file not shown.

View file

@ -0,0 +1,327 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Author: Tlams
Langage: Python
Minimum version require: 3.4
"""
from core.modules.mod_proxmox import *
from core.modules.mod_database import *
from core.modules.mod_analyst import *
from core.modules.mod_access import *
from core.libs.hcrypt import *
from netaddr import iter_iprange
import threading
import time
def RunAnalyse(clusters_conf, generalconf, delay=300):
play = Analyse(clusters_conf, generalconf)
while True:
play.run()
time.sleep(delay)
class Core:
def __init__(self, generalconf, Lredis):
self.generalconf = generalconf
self.Lredis = Lredis
""" LOAD MONGODB """
self.mongo = MongoDB(generalconf["mongodb"]["ip"])
self.mongo.client = self.mongo.connect()
""" LOAD REDIS """
self.redis_msg = Lredis
#self.redis_msg.co = self.redis_msg.connect()
if self.mongo.client and self.redis_msg.connect():
self.mongo.db = self.mongo.client.db
""" Others """
# value
self.concurrencydeploy = generalconf["deploy"]["concurrencydeploy"]
# in seconds
self.delayrounddeploy = generalconf["deploy"]["delayrounddeploy"]
""" RUN THE ANALYZER IN DEDICATED THEARD"""
self.clusters_conf = self.mongo.get_clusters_conf()
thc = threading.Thread(name="Update statistics",
target=RunAnalyse,
args=(self.clusters_conf, self.generalconf))
thc.start()
"""
#######################
# INSTANCE MANAGEMENT #
#######################
"""
def insert_instance(self, target, count=1, command_id=000000):
""" Find cluster informations from node """
lastkeyvalid = self.mongo.get_last_datekey()
node_informations = self.mongo.get_nodes_informations((int(lastkeyvalid["value"])), target)
cluster_informations = self.mongo.get_clusters_conf(node_informations["cluster"])
proxmox_cluster_url = cluster_informations["url"]
proxmox_cluster_port = cluster_informations["port"]
proxmox_cluster_user = pdecrypt(cluster_informations["user"],self.generalconf["keys"]["key_pvt"])
proxmox_cluster_pwd = pdecrypt(cluster_informations["password"], self.generalconf["keys"]["key_pvt"])
proxmox_template = cluster_informations["template"]
proxmox_storage_disk = cluster_informations["storage_disk"]
""" LOAD PROXMOX """
proxmox = Proxmox(target)
proxmox.get_ticket("{0}:{1}".format(proxmox_cluster_url,
int(proxmox_cluster_port)),
proxmox_cluster_user,
proxmox_cluster_pwd)
returnlistresult = []
currentcount = 0
for c in range(0, int(count)):
if currentcount == self.concurrencydeploy:
time.sleep(self.delayrounddeploy)
currentcount = 0
currentcount = currentcount + 1
get_info_system = self.mongo.get_system_info()
""" FIND NEXT INSTANCE ID AVAILABLE AND INCREMENT IT"""
next_instance_id = int(get_info_system["instances_number"]+1)
""" FIND LAST LAST IP USE AND INCREMENT IT"""
if not get_info_system["IP_free"]:
get_instance_ip = get_info_system["IP_current"]
next_ip = iter_iprange(get_instance_ip, '172.16.255.250', step=1)
# Revoir pour un truc plus clean ....
next(next_ip)
ip = str(next(next_ip))
else:
ip = str(get_info_system["IP_free"][0])
self.mongo.update_system_delete_ip(ip)
""" INSTANCE DEFINITION """
data = {
'ostemplate': proxmox_template,
'vmid': next_instance_id,
'storage': proxmox_storage_disk,
'cores': 1,
'cpulimit': 1,
'cpuunits': 512,
'arch': "amd64",
'memory': 256,
'description': command_id,
'onboot': 0,
'swap': 256,
'ostype': 'debian',
'net0': 'name=eth0,bridge=vmbr1,ip={0}/16,gw=172.16.1.254'.format(ip),
'ssh-public-keys': get_info_system["sshpublickey"]
}
""" INSTANCE INSERTION """
result_new = {}
#while not proxmox.retry_on_errorcode(result_new['result']):
result_new = proxmox.create_instance("{0}:{1}".format(proxmox_cluster_url,
int(proxmox_cluster_port)), target, "lxc",
data)
""" VERIFY THE RESULT BY PROXMOX STATUS REQUEST CODE """
if result_new['result'] == "OK":
""" INCREMENT INSTANCE ID IN DATABASE """
self.mongo.update_system_instance_id(next_instance_id)
""" INCREMENT INSTANCE IP IN DATABASE """
self.mongo.update_system_instance_ip(ip)
""" INSERT THIS NEW SERVER IN DATABASE """
data["commandid"] = command_id
data["cluster"] = node_informations["cluster"]
data["node"] = target
data["ip"] = ip
self.mongo.insert_instance(data)
""" BREAK the loop due to valid creation """
returnlistresult.append(result_new)
""" SEND MESSAGE IN REDIS """
self.redis_msg.insert_message(command_id, returnlistresult)
return
def delete_instance(self, vmid):
try:
""" Find node/cluster informations from vmid """
instance_informations = self.mongo.get_instance(vmid)
""" Find cluster informations from node """
cluster_informations = self.mongo.get_clusters_conf(instance_informations['cluster'])
proxmox_cluster_url = cluster_informations["url"]
proxmox_cluster_port = cluster_informations["port"]
proxmox_cluster_user = pdecrypt(cluster_informations["user"],self.generalconf["keys"]["key_pvt"])
proxmox_cluster_pwd = pdecrypt(cluster_informations["password"], self.generalconf["keys"]["key_pvt"])
""" LOAD PROXMOX """
proxmox = Proxmox(instance_informations['node'])
proxmox.get_ticket("{0}:{1}".format(proxmox_cluster_url,
int(proxmox_cluster_port)),
proxmox_cluster_user,
proxmox_cluster_pwd)
result = proxmox.delete_instance("{0}:{1}".format(proxmox_cluster_url,
int(proxmox_cluster_port)), instance_informations['node'], "lxc", vmid)
if result['result'] == "OK":
self.mongo.delete_instance(vmid)
self.mongo.update_system_free_ip(instance_informations['ip'])
except BaseException:
result = {"value": "{0} {1}".format(vmid, "is not a valid VMID")}
return result
def status_instance(self, vmid, action):
""" Find node/cluster informations from vmid """
try:
instance_informations = self.mongo.get_instance(vmid)
""" Find cluster informations from node """
cluster_informations = self.mongo.get_clusters_conf(instance_informations['cluster'])
proxmox_cluster_url = cluster_informations["url"]
proxmox_cluster_port = cluster_informations["port"]
proxmox_cluster_user = pdecrypt(cluster_informations["user"],self.generalconf["keys"]["key_pvt"])
proxmox_cluster_pwd = pdecrypt(cluster_informations["password"], self.generalconf["keys"]["key_pvt"])
""" LOAD PROXMOX """
proxmox = Proxmox(instance_informations['node'])
proxmox.get_ticket("{0}:{1}".format(proxmox_cluster_url,
int(proxmox_cluster_port)),
proxmox_cluster_user,
proxmox_cluster_pwd)
result = proxmox.status_instance("{0}:{1}".format(proxmox_cluster_url,
int(proxmox_cluster_port)),
instance_informations['node'],
"lxc",
vmid, action)
except IndexError:
result = {"value": "{0} {1}".format(vmid, "is not a valid VMID")}
return result
def info_instance(self, vmid):
""" Find node/cluster informations from vmid """
try:
instance_informations = self.mongo.get_instance(vmid)
""" Find cluster informations from node """
cluster_informations = self.mongo.get_clusters_conf(instance_informations['cluster'])
proxmox_cluster_url = cluster_informations["url"]
proxmox_cluster_port = cluster_informations["port"]
proxmox_cluster_user = pdecrypt(cluster_informations["user"],self.generalconf["keys"]["key_pvt"])
proxmox_cluster_pwd = pdecrypt(cluster_informations["password"], self.generalconf["keys"]["key_pvt"])
""" LOAD PROXMOX """
proxmox = Proxmox(instance_informations['node'])
proxmox.get_ticket("{0}:{1}".format(proxmox_cluster_url,
int(proxmox_cluster_port)),
proxmox_cluster_user,
proxmox_cluster_pwd)
result = proxmox.get_config("{0}:{1}".format(proxmox_cluster_url,
int(proxmox_cluster_port)),
instance_informations['node'],
"lxc",
vmid)
except IndexError:
result = {"value": "{0} {1}".format(vmid, "is not a valid VMID")}
return result
def change_instance(self, vmid, data):
""" Find node/cluster informations from vmid """
try:
instance_informations = self.mongo.get_instance(vmid)
""" Find cluster informations from node """
cluster_informations = self.mongo.get_clusters_conf(instance_informations['cluster'])
proxmox_cluster_url = cluster_informations["url"]
proxmox_cluster_port = cluster_informations["port"]
proxmox_cluster_user = pdecrypt(cluster_informations["user"],self.generalconf["keys"]["key_pvt"])
proxmox_cluster_pwd = pdecrypt(cluster_informations["password"], self.generalconf["keys"]["key_pvt"])
""" LOAD PROXMOX """
proxmox = Proxmox(instance_informations['node'])
proxmox.get_ticket("{0}:{1}".format(proxmox_cluster_url,
int(proxmox_cluster_port)),
proxmox_cluster_user,
proxmox_cluster_pwd)
result = proxmox.resize_instance("{0}:{1}".format(proxmox_cluster_url,
int(proxmox_cluster_port)),
instance_informations['node'],
"lxc",
vmid, data)
if result['result'] == "OK":
self.mongo.update_instance(vmid, data)
except IndexError:
result = {"value": "{0} {1}".format(vmid, "is not a valid VMID")}
return result
"""
#######################
# CLUSTERS MANAGEMENT #
#######################
"""
def get_cluster(self, cluster=None):
""" Find cluster informations from node """
cluster_informations = self.mongo.get_clusters_conf(cluster)
return cluster_informations
def insert_cluster(self, data):
testdata = valid_cluster_data(data)
if not testdata:
data["user"] = pcrypt(data["user"], self.generalconf["keys"]["key_pvt"])["data"]
data["password"] = pcrypt(data["password"], self.generalconf["keys"]["key_pvt"])["data"]
new_cluster = self.mongo.insert_new_cluster(data)
else:
new_cluster = {"error": "{1} {0}".format(testdata, "Invalid or miss paramettrer")}
return new_cluster
def change_cluster(self, cluster, data):
return
def delete_cluster(self, cluster):
return
def valid_cluster_data(data):
key_required = ["name", "url", "port", "user", "password", "template", "storage_disk", "weight", "exclude_nodes"]
result = []
for key in key_required:
if key not in data:
result.append(key)
return result

View file

@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

View file

@ -0,0 +1,11 @@
from core.modules.mod_access import *
def pcrypt(data, key):
CritConf = CryticalData()
data = CritConf.data_encryption(data, key)
return data
def pdecrypt(data, key):
CritConf = CryticalData()
data = CritConf.data_decryption(data, key)
return data

View file

View file

View file

@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

View file

@ -0,0 +1,133 @@
"""
Author: Tlams
Langage: Python
Minimum version require: 3.4
"""
import os
from Crypto.PublicKey import RSA
import hashlib
import codecs
def encodepassphrase(passphrase):
return hashlib.sha512(passphrase.encode("UTF-8")).hexdigest()
class CryticalData:
def __init__(self):
self.public_key = None
self.private_key = None
def generate_key(self, key_pvt, key_pub, passphrase, lgt=4096):
try:
private_key = RSA.generate(lgt)
file = open(key_pvt, "wb")
file.write(private_key.exportKey('PEM', passphrase, pkcs=1))
file.close()
public_key = private_key.publickey()
file = open(key_pub, "wb")
file.write(public_key.exportKey())
file.close()
os.chmod(key_pvt, 0o600)
os.chmod(key_pub, 0o600)
key_generation = {"result": "OK"}
except BaseException as e:
try:
print("Clean...")
os.remove(key_pvt)
os.remove(key_pub)
except OSError:
pass
key_generation = {
"result": "ERROR",
"type": "PYTHON",
"Error": "Key generation fail: {0}".format(e)
}
return key_generation
def read_public_key(self, key_pub):
try:
file_key_pub = open(key_pub, "rb")
self.public_key = RSA.importKey(file_key_pub.read())
file_key_pub.close()
result_public_key = {
"result": "OK",
"data": self.public_key
}
except BaseException as e:
result_public_key = {
"result": "ERROR",
"type": "PYTHON",
"error": "Your public key seem to invalid: {0}".format(e)
}
return result_public_key
def read_private_key(self, key_pvt, passphrase):
try:
file_key_pvt = open(key_pvt, "rb")
self.private_key = RSA.importKey(file_key_pvt.read(), passphrase)
file_key_pvt.close()
result_private_key = {
"result": "OK",
"data": self.private_key
}
except BaseException as e:
result_private_key = {
"result": "ERROR",
"type": "PYTHON",
"error": "Your private key seem to invalid: {0}".format(e)
}
return result_private_key
def data_encryption(self, data, key=None):
encfrypt = key.encrypt(data.encode("utf-8"), 32)
convert_str = str(encfrypt[0])[2:-1]
try:
if key:
result_encrypt = {
"result": "OK",
"data": convert_str
}
else:
result_encrypt = {
"result": "OK",
"data": codecs.encode(self.public_key.encrypt(mutable_bytes, 32)[0], 'base64')
}
except BaseException as e:
result_encrypt = {
"result": "ERROR",
"type": "PYTHON",
"error": "Data encryption failed: {0}".format(e)
}
return result_encrypt
def data_decryption(self, data, key=None):
try:
if key:
result_decryption = {
"result": "OK",
"data": key.decrypt(data)
}
else:
result_decryption = {
"result": "OK",
"data": self.private_key.decrypt(data)
}
except BaseException as e:
result_decryption = {
"result": "ERROR",
"type": "PYTHON",
"error": "Data decryption failed: {0}".format(e)
}
return result_decryption

View file

@ -0,0 +1,157 @@
"""
Author: Tlams
Langage: Python
Minimum version require: 3.4
Module function:
The goal of this module is to analyse the differents clusters and node
to allocate news instances.
"""
from core.modules.mod_proxmox import *
from core.modules.mod_database import *
from core.libs.hcrypt import *
import time
import operator
import random
import codecs
def add_token(tokens_in_slots, slot_distributions):
num_tokens = sum(tokens_in_slots)
if not num_tokens:
#first token can go anywhere
tokens_in_slots[random.randint(0, 2)] += 1
return
expected_tokens = [num_tokens*distr for distr in slot_distributions]
errors = [expected - actual
for expected, actual in zip(expected_tokens, tokens_in_slots)]
most_error = max(enumerate(errors), key=lambda i_e: i_e[1])
tokens_in_slots[most_error[0]] += 1
def distribution(n, tokens_in_slots, slot_distributions):
for i in range(n):
add_token(tokens_in_slots, slot_distributions)
return tokens_in_slots
class Analyse:
def __init__(self, clusters_conf, generalconf):
"""
:param clusters_conf: Proxmox configurations
:param generalconf : General configuration
"""
self.generalconf = generalconf
self.clusters_conf = clusters_conf
""" LOAD MONGODB """
self.mongo = MongoDB(generalconf["mongodb"]["ip"])
self.mongo.client = self.mongo.connect()
self.mongo.db = self.mongo.client.db
def run(self):
insert_time = time.time()
self.mongo.insert_datekey(insert_time, 'running')
for cluster in self.clusters_conf:
datamongo=cluster["user"].replace("\\\\x", "x")
convert_to_byte=datamongo.encode("utf-8")
print(convert_to_byte)
print(pdecrypt(datamongo.encode("utf-8"), self.generalconf["keys"]["key_pvt"]))
""" AUTH """
proxmox = Proxmox("Analyse")
proxmox.get_ticket("{0}:{1}".format(cluster["url"],
int(cluster["port"])),
pdecrypt(cluster["user"], self.generalconf["keys"]["key_pvt"]),
pdecrypt(cluster["password"], self.generalconf["keys"]["key_pvt"]))
""" Get excluded nodes """
exclude_nodes = cluster["exclude_nodes"]
""" UPDATE NODES LIST """
nodes_list = proxmox.get_nodes("{0}:{1}".format(cluster["url"], int(cluster["port"])))["value"]
for value_nodes_list in nodes_list["data"]:
if value_nodes_list["node"] not in exclude_nodes:
""" TOTAL COUNT CPU and RAM allocate"""
list_instances = proxmox.get_instance("{0}:{1}".format(cluster["url"], int(cluster["port"])),
value_nodes_list["node"], "lxc")["value"]
totalcpu = 0
totalram = 0
for key_list_instances, value_list_instances in list_instances.items():
for instances in value_list_instances:
totalcpu = totalcpu + instances["cpus"]
totalram = totalram + instances["maxmem"]
value_nodes_list["totalalloccpu"] = totalcpu
value_nodes_list["totalallocram"] = totalram
value_nodes_list["vmcount"] = len(list_instances.items())
percent_cpu_alloc = (totalcpu / value_nodes_list["maxcpu"]) * 100
percent_ram_alloc = (totalram / value_nodes_list["mem"]) * 100
"""
weight of node =
(((Percent Alloc CPU x coef) + ( Percent Alloc RAM x coef)) / Total coef ) * Cluster weight
"""
weight = (((percent_cpu_alloc * 2) + (percent_ram_alloc * 4)) / 6) * int(cluster["weight"])
value_nodes_list["weight"] = int(weight)
value_nodes_list["date"] = int(insert_time)
value_nodes_list["cluster"] = cluster["name"]
self.mongo.insert_node(value_nodes_list)
self.mongo.update_datekey(int(insert_time), "OK")
return
def set_attribution(self, count):
""" RETURN cluster and node"""
# Search the last valid key
lastkeyvalid = self.mongo.get_last_datekey()
# Get nodes weight
nodes_availables = self.mongo.get_nodes_informations(int(lastkeyvalid["value"]))
if len(nodes_availables) > 1:
# Select node name with weight
nodes_values = {}
for nodes in nodes_availables:
nodes_values[nodes["node"]] = nodes["weight"]
# Sort node by weight
sorted_nodes = sorted(nodes_values.items(), key=operator.itemgetter(1))
slot_distributions = []
sorted_nodes_name = []
# Divise dict sorted_node to two list [name1, name2] [value1, value2]
for nodes_sort in sorted_nodes:
slot_distributions.append(nodes_sort[1])
sorted_nodes_name.append((nodes_sort[0]))
# Calcul weight on a range of value 0-1 and convert in percent
slot_distributions_p = []
for s in slot_distributions:
slot_distributions_p.append(100-(s/sum(slot_distributions)*100)/100)
# Generate default list
tokens_in_slots = [0] * len(nodes_availables)
# use distribution algorithm to allocate
distrib_final = distribution(int(count), tokens_in_slots, slot_distributions_p)
# Regenerate final dict !!
final = {k: int(v) for k, v in zip(sorted_nodes_name, distrib_final)}
else:
final = {nodes_availables[0]['node']: count}
return final

View file

@ -0,0 +1,179 @@
from pymongo import MongoClient
from bson.json_util import dumps
import json
import redis
import time
class Redis_instance_queue:
def __init__(self, server="127.0.0.1", port=6379, db=3, password=None):
self.server = server
self.port = port
self.r = None
self.db = db
self.password = password
def connect(self):
try:
conn = self.r = redis.Redis(
host=self.server, port=self.port, db=self.db, password=self.password,
charset="utf-8", decode_responses=True)
self.r.client_list()
except BaseException as err:
print("Redis connexion error on {0}:{1} ({2})".format(self.server, self.port, err))
conn = False
return conn
def insert_instance_queue(self, logtext, expir=3000):
self.r.set(time.time(), logtext, expir)
class Redis_logger:
def __init__(self, server="127.0.0.1", port=6379, db=2, password=None):
self.server = server
self.port = port
self.r = None
self.db = db
self.password = password
def connect(self):
try:
conn = self.r = redis.Redis(
host=self.server, port=self.port, db=self.db, password=self.password,
charset="utf-8", decode_responses=True)
self.r.client_list()
except BaseException as err:
print("Redis connexion error on {0}:{1} ({2})".format(self.server, self.port, err))
conn = False
return conn
def insert_logs(self, logtext, expir=86400*4):
self.r.set(time.time(), logtext, expir)
class Redis_messages:
def __init__(self, server="127.0.0.1", port=6379, db=1, password=None):
self.server = server
self.port = port
self.r = None
self.db = db
self.password = password
def connect(self):
try:
conn = self.r = redis.Redis(
host=self.server, port=self.port, db=self.db, password=self.password,
charset="utf-8", decode_responses=True)
self.r.client_list()
except BaseException as err:
print("Redis connexion error on {0}:{1} ({2})".format(self.server, self.port, err))
conn = False
return conn
def insert_message(self, key, value, expir=86400):
self.r.set(key, value, expir)
def get_message(self, key):
return self.r.get(key)
class MongoDB:
def __init__(self, server="127.0.0.1", port=27017):
"""
:param server:
:param port:
"""
self.server = server
self.port = port
self.collection_system = "system"
self.collection_instance = "instances"
self.collection_nodes = "nodes"
self.collection_clusters = "clusters"
self.collection_datekey = "dates"
self.port = port
self.db = None
self.client = None
def connect(self):
try:
conn = MongoClient(self.server + ':' + str(self.port))
conn.server_info()
except BaseException as err:
print("MongoDB connexion error on {0}:{1} ({2})".format(self.server, self.port, err))
conn = False
return conn
def authenticate(self, user=None, password=None, mechanism='SCRAM-SHA-1'):
try:
self.client.db.authenticate(user, password, mechanism)
except (TypeError, ValueError) as e:
raise("MongoDB authentification error on {0}:{1} ({2})".format(self.server, self.port, e))
""" CLUSTER """
def get_clusters_conf(self, cluster=None):
if cluster:
return json.loads(dumps(self.db[self.collection_clusters].find_one({"name": cluster})))
else:
return json.loads(dumps(self.db[self.collection_clusters].find({})))
def insert_new_cluster(self, data):
return self.db[self.collection_clusters].insert(data)
def update_cluster(self, cluster, data):
return self.db[self.collection_clusters].update({"vmid": str(cluster)}, {'$set': data}, upsert=False)
def delete_cluster(self, cluster):
return self.db[self.collection_clusters].remove({"cluster": str(cluster)})
""" SYSTEM """
def get_system_info(self):
return self.db[self.collection_system].find_one({"_id": "0"})
def update_system_instance_id(self, value):
self.db[self.collection_system].update({'_id': "0"}, {'$set': {'instances_number': value}})
def update_system_instance_ip(self, value):
self.db[self.collection_system].update({'_id': "0"}, {'$set': {'IP_current': value}})
def update_system_free_ip(self, value):
self.db[self.collection_system].update({'_id': "0"}, {'$push': {'IP_free': value}}, upsert=False)
def update_system_delete_ip(self, value):
self.db[self.collection_system].update({'_id': "0"}, {'$pull': {'IP_free': value}}, upsert=False)
""" NODES MANAGEMENT"""
def insert_node(self, data):
return self.db[self.collection_nodes].insert(data)
def get_nodes_informations(self, time, node=None):
if node:
return json.loads(dumps(self.db[self.collection_nodes].find_one({'$and': [{'node': node, 'date': time}]})))
else:
return json.loads(dumps(self.db[self.collection_nodes].find({'date': time})))
""" KEY DATE MANAGEMENT"""
def insert_datekey(self, date, status):
return self.db[self.collection_datekey].insert({'date': int(date), 'status': status})
def update_datekey(self, date, status):
self.db[self.collection_datekey].update({'date': date}, {'$set': {'status': status}}, upsert=False)
def get_last_datekey(self):
last_id = self.db[self.collection_datekey].find({'status': 'OK'}).sort("date", -1).limit(1)
return {"value": int(json.loads(dumps(last_id))[0]['date'])}
""" INSTANCE MANAGEMENT"""
def insert_instance(self, data):
return self.db[self.collection_instance].insert(data)
def update_instance(self, vmid, data):
self.db[self.collection_instance].update({"vmid": int(vmid)}, {'$set': data}, upsert=False)
def delete_instance(self, vmid):
self.db[self.collection_instance].remove({"vmid": int(vmid)})
def get_instance(self, vmid):
try:
return json.loads(dumps(self.db[self.collection_instance].find_one({"vmid": int(vmid)})))
except BaseException as serr:
raise ("MongoDB error on {0}:{1} ({2})".format(self.server, self.port, serr))

View file

@ -0,0 +1,415 @@
import requests
import time
"""
Proxmox management API WRAPPER
"""
class NetworkError(RuntimeError):
pass
class Proxmox:
def __init__(self, name):
"""
:param name: Cluster Proxmox
"""
self.name = name
self.socket = None
self.ticket = None
self.PVEAuthCookie = None
self.csrf = None
self.nodes = None
self.status = None
self.storage = None
self.disks = None
self.qemu = None
self.config = None
def get_ticket(self, url, user, password):
"""
Get a new ticket from Proxmox api
:param url: Generic ticket url
:param user: Proxmox user API
:param password: Proxmox password user API
"""
request = "https://{0}/api2/json/access/ticket".format(url)
try:
self.socket = requests.session()
params = {'username': user, 'password': password}
self.ticket = self.socket.post(request, params=params, verify=False, timeout=5)
if self.ticket.status_code == 200:
result = {"result": "OK",
"value": self.ticket.json()
}
self.PVEAuthCookie = {'PVEAuthCookie': self.ticket.json()['data']['ticket']}
self.csrf = {'CSRFPreventionToken': self.ticket.json()['data']['CSRFPreventionToken']}
else:
result = {"result": "ERROR",
"target": "{0}".format(url),
"type": "PROXMOX - STATUS CODE",
"customerror": "Error nodes informations. Bad HTTP Status code : "
"{0} -- {1}".format(self.ticket.status_code, self.ticket.text)
}
except (TypeError, ValueError, requests.exceptions.RequestException) as e:
result = {"result": "ERROR",
"target": "{0}".format(url),
"type": "PYTHON",
"customerror": "Cannot get ticket session {0} ({1})".format(url, e)}
return result
def get_nodes(self, url):
"""
Get Nodes from cluster
:param url: Generic node url (node = physical hypervisor)
"""
request = "https://{0}/api2/json/nodes".format(url)
try:
nodes = self.nodes = self.socket.get(request,
cookies=self.PVEAuthCookie,
verify=False, timeout=5)
if nodes.status_code == 200:
result = {
"result": "OK",
"value": nodes.json()
}
else:
result = {
"result": "ERROR",
"target": "{0}".format(url),
"type": "PROXMOX - STATUS CODE",
"customerror": "Error nodes informations. Bad HTTP Status code : "
"{0} -- {1}".format(nodes.status_code, nodes.text)
}
except (TypeError, ValueError, requests.exceptions.RequestException) as e:
result = {
"result": "ERROR",
"target": "{0}".format(url),
"type": "PYTHON",
"customerror": "Cannot get node information for {0} ({1})".format(url, e)
}
return result
def get_status(self, url, nodename):
"""
Get node informations
:param url: Generic node url
:param nodename: Node name (not int id)
"""
request = "https://{0}/api2/json/nodes/{1}/status".format(url, nodename)
try:
self.status = self.socket.get(request, cookies=self.PVEAuthCookie, verify=False, timeout=5).json()
result = {"result": "OK"}
except (TypeError, ValueError, requests.exceptions.RequestException) as e:
result = {"result": "ERROR", "target": "[{3}]",
"customerror": "Cannot get node information for {0} ({1})".format(url, e, nodename)}
return result
def get_storages(self, url, nodename):
"""
Get Storage from nodes
:param url: Generic storage url
:param nodename: Node name (no int id)
"""
request = "https://{0}/api2/json/nodes/{1}/storage".format(url, nodename)
try:
self.storage = self.socket.get(request, cookies=self.PVEAuthCookie, verify=False, timeout=5).json()
result = {"result": "OK"}
except (TypeError, ValueError, requests.exceptions.RequestException) as e:
result = {"result": "ERROR", "target": "[{3}]",
"customerror": "Cannot get storage information for {0} ({1})".format(url, e, nodename)}
return result
def get_disks(self, url, nodename, sto_id):
"""
Get VMs disk from storages
:param url: Generic content url
:param nodename: Node name (no int id)
:param sto_id: Storage name (no int id)
"""
request = "https://{0}/api2/json/nodes/{1}/storage/{2}/content".format(url, nodename, sto_id)
try:
self.disks = self.socket.get(request, cookies=self.PVEAuthCookie, verify=False, timeout=5).json()
result = {"result": "OK"}
except (TypeError, ValueError, requests.exceptions.RequestException) as e:
result = {"result": "ERROR", "target": "[{3}]",
"customerror": "Cannot get disks information for {0} ({1})".format(url, e, nodename)}
return result
def get_instance(self, url, nodename, category):
"""
Get basic VMs informations from nodes
:param url: Generic qemu url
:param nodename: Node name (not int id)
:param category: lxc or qemu
"""
request = "https://{0}/api2/json/nodes/{1}/{2}".format(url, nodename, category)
try:
instances = self.socket.get(request,
cookies=self.PVEAuthCookie,
verify=False, timeout=5)
if instances.status_code == 200:
result = {
"result": "OK",
"value": instances.json()
}
else:
result = {
"result": "ERROR",
"target": "{0}".format(url),
"type": "PROXMOX - STATUS CODE",
"customerror": "Error nodes informations. Bad HTTP Status code : "
"{0} -- {1}".format(instances.status_code, instances.text)
}
except (TypeError, ValueError, requests.exceptions.RequestException) as e:
result = {
"result": "ERROR",
"target": "{0}".format(url),
"type": "PYTHON",
"customerror": "Cannot get VM information for {0} {1} ({2})".format(url, nodename, e)
}
return result
def get_config(self, url, nodename, category, instanceid):
"""
Get avanced VM information from nodes
:param url: Generic qemu config url
:param category: lxc or qemu
:param nodename: Node name (not int id)
:param instanceid: VM id (int id)
"""
request = "https://{0}/api2/json/nodes/{1}/{2}/{3}/config".format(url, nodename, category, instanceid)
try:
config = self.socket.get(request, cookies=self.PVEAuthCookie, verify=False, timeout=5)
if config.status_code == 200:
result = {
"result": "OK",
"value": config.json()
}
else:
result = {
"result": "ERROR",
"target": "{0}".format(url),
"type": "PROXMOX - STATUS CODE",
"customerror": "Error nodes informations. Bad HTTP Status code : "
"{0} -- {1}".format(config.status_code, config.text)
}
except (TypeError, ValueError, requests.exceptions.RequestException) as e:
result = {
"result": "ERROR",
"target": "{0}".format(url),
"type": "PYTHON",
"customerror": "Cannot get VM information for {0} {1} ({2})".format(url, nodename, e)
}
return result
def create_instance(self, url, nodename, category, data):
"""
:param url:
:param nodename:
:param category:
:param data:
:return:
"""
request = "https://{0}/api2/json/nodes/{1}/{2}".format(url, nodename, category)
try:
createvm = self.socket.post(request,
data=data,
headers=self.csrf,
cookies=self.PVEAuthCookie,
verify=False,
timeout=5)
if createvm.status_code == 200:
result = {
"result": "OK",
"value": createvm.json()
}
else:
result = {
"result": "ERROR",
"target": "{0}".format(nodename),
"type": "PROXMOX - STATUS CODE",
"customerror": "Error creating Container. Bad HTTP Status code : "
"{0} -- {1}".format(createvm.status_code, createvm.text)
}
except (TypeError, ValueError, requests.exceptions.RequestException) as e:
result = {"result": "ERROR",
"target": "{0}".format(nodename),
"type": "PYTHON",
"customerror": "Cannot create this instance on {0} : ({1})".format(url, e)}
return result
def delete_instance(self, url, nodename, category, vmid):
"""
:param url:
:param nodename:
:param category:
:param vmid:
:return:
"""
request = "https://{0}/api2/json/nodes/{1}/{2}/{3}".format(url, nodename, category, vmid)
try:
deletevm = self.socket.delete(request,
headers=self.csrf,
cookies=self.PVEAuthCookie,
verify=False,
timeout=5
)
if deletevm.status_code == 200:
result = {
"result": "OK",
"value": deletevm.json()
}
else:
result = {
"result": "ERROR",
"target": "{0}".format(nodename),
"type": "PROXMOX - STATUS CODE",
"customerror": "Error delete Container. Bad HTTP Status code : "
"{0} -- {1}".format(deletevm.status_code, deletevm.text)
}
except (TypeError, ValueError, requests.exceptions.RequestException) as e:
result = {"result": "ERROR",
"target": "{0}".format(nodename),
"type": "PYTHON",
"customerror": "Cannot delete Container ({2}) on {0} : ({1})".format(url, e, vmid)}
return result
def status_instance(self, url, nodename, category, vmid, action):
"""
:param url:
:param nodename:
:param category:
:param vmid:
:param action:
:return:
"""
request = "https://{0}/api2/json/nodes/{1}/{2}/{3}/status/{4}".format(url, nodename, category, vmid, action)
try:
if action == "current":
statusm = self.socket.get(request,
cookies=self.PVEAuthCookie,
verify=False,
timeout=5)
else:
statusm = self.socket.post(request,
headers=self.csrf,
cookies=self.PVEAuthCookie,
verify=False,
timeout=5)
if statusm.status_code == 200:
result = {
"result": "OK",
"value": statusm.json()
}
else:
result = {
"result": "ERROR",
"target": "{0}".format(nodename),
"type": "PROXMOX - STATUS CODE",
"customerror": "Error action Container. Bad HTTP Status code : "
"{0} -- {1}".format(statusm.status_code, statusm.text)
}
except (TypeError, ValueError, requests.exceptions.RequestException) as e:
result = {"result": "ERROR",
"target": "{0}".format(nodename),
"type": "PYTHON",
"customerror": "Cannot do this action this instance ({2}) on {0} : ({1})".format(url, e, vmid)}
return result
def resize_instance(self, url, nodename, category, instanceid, data):
"""
:param url:
:param nodename:
:param category:
:param instanceid:
:param data:
:return:
"""
request = "https://{0}/api2/json/nodes/{1}/{2}/{3}/config".format(url, nodename, category, instanceid)
try:
resizevm = self.socket.put(request,
data=data,
headers=self.csrf,
cookies=self.PVEAuthCookie,
verify=False,
timeout=5)
if resizevm.status_code == 200:
result = {
"result": "OK",
"value": resizevm.json()
}
else:
result = {
"result": "ERROR",
"target": "{0}".format(nodename),
"type": "PROXMOX - STATUS CODE",
"customerror": "Error resizing container. Bad HTTP Status code : "
"{0} -- {1}".format(resizevm.status_code, resizevm.text)
}
except (TypeError, ValueError, requests.exceptions.RequestException) as e:
result = {
"result": "ERROR",
"target": "{0}".format(nodename),
"type": "PYTHON",
"customerror": "Cannot resize this instance {2} on {0} : ({1})".format(url, e, instanceid)
}
return result
def stats_instance(self, url, nodename, category, instanceid):
"""
:param url:
:param nodename:
:param category:
:param instanceid:
:return:
"""
request = "https://{0}/api2/json/nodes/{1}/{2}/{3}/rrddata".format(url, nodename, category, instanceid)
try:
statsvm = self.socket.get(request, cookies=self.PVEAuthCookie, verify=False, timeout=5)
if statsvm.status_code == 200:
result = {
"result": "OK",
"value": statsvm.json()
}
else:
result = {
"result": "ERROR",
"target": "{0}".format(nodename),
"type": "PROXMOX - STATUS CODE",
"customerror": "Error to find statistic for instance {2}. Bad HTTP Status code : "
"{0} -- {1}".format(statsvm.status_code, statsvm.text, instanceid)
}
except (TypeError, ValueError, requests.exceptions.RequestException) as e:
result = {
"result": "ERROR",
"target": "{0}".format(nodename),
"type": "PYTHON",
"customerror": "Cannot find statistic for this instance {2} on {0} : ({1})".format(url, e, instanceid)
}
return result

View file

@ -0,0 +1,20 @@
"""
Queue Management
"""
class Queue():
def __init__(self):
self.name = "command"
def insert_command_queue(self):
return
def delete_command(self):
return
def serve_command(self):
return
def queue_stats(self):
return

View file

@ -0,0 +1,34 @@
import requests
"""
VHOST Management
"""
class Nginx:
def __init__(self, clientid):
"""
:param clientid:
"""
class Apache:
def __init__(self, siteid):
"""
:param name: Cluster Proxmox
"""
self.siteid = siteid
def get_vhost(self):
"""
List all vhost available
:return:
"""
return
def create_vhost(self):
return
def delete_vhost(self):
return
def update_vhost(self):
return

View file

@ -0,0 +1,36 @@
[system]
; System configurations
user: hosting
; If not exist on start, the key is auto-generate.
key_pvt: private/keys/Ragnarok.pvt.key
key_pub: private/keys/Ragnarok.pub.key
[web]
user: www-data
[api]:
user: hosting_api
[databases]
; Databases configurations
; NOSQL databases, should use a password
mongodb_user:
mongodb_password:
mongodb_ip: 192.168.66.1
mongodb_port: 27017
redis_user:
redis_password:
redis_ip: 192.168.66.1
redis_port: 6379
[options]
; Maximum deploy instance in same time
; A hight value my do overcharge on your physicals servers
concurrencydeploy: 2
; Delay between two deploy round
; If your infrastructure isn't very large, you should'nt reduce this delay.
; A low delay my do overcharge on your physicals servers
delayrounddeploy: 15

View file

@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0BzZj0KTIqPsAnxO0P89
3Q8u13jSchAPcT0B7EgJL6RCK5qFRvo4JhGvaG0R8L1YK6T5jS2bzoZR6TDIdze/
/a2XJ/0B2u9CvsP/goUMdIqnhkC98K+UxiFY2Rqs68TAJFbWVxAjDLjyYEnZ+faU
FWo5x9Nq9Py+VN66N+zRAfWLoUDouEwVQm6pQf5ksJ2qBxyQUxD2j+/0h3yRz+tY
UKy7F8cMXup9VYpiebC4O0XwAtE+QXwCzC15C1VaVQs/8rw4qYy6gzdwbaN0Dqim
wG/3+nO9R08stCBimEMynKbcOjsdktebTdbgWo+uMI+DauBgpsxqgTciI1yKWmzk
uKe0D+fZzfIAGTcSQpxwefqfOKBk2Uy6jhFz6kBa4pDWPNqvfmD3v2TzCkFNqbEA
ZGfQuBEZR4iEQoef2D6pA5KjNpdDFdWXC1HvmO9LsS71TlJig1u1/boWWyOTlIm4
1mGZumco35EhsyTdFFgf779isgley84Z1jLbmILf8z41l8icZt3Sz3ej4QeH2vdm
xpIGcw5RWCh2+ociRvqTw2JBrGU9GLXQibYGeI8xIDds8hJ8R2U38/R4Sru7a5NX
JDxPZXyfLZbTtCUihOtqWWke33BAGzHMn7+bPRJe27f/fl6gYiBzxaNhB4Ta3m4G
aNVCH4vCl37SGpASd2NYpWkCAwEAAQ==
-----END PUBLIC KEY-----

View file

@ -0,0 +1,54 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,B9C57D57F4BBC73D
kdOcbiVGrcGvjOM++I9NL2msXKy3meuO8q4+I21BGMizLyjhP+zw1wtBVrjTEBum
kWu4B6Ty0OQtz02Nh4V2FE/DOI6MiCQwuDCfYY0tEEED41y+ULlHsG9vAWJ9qpBp
dYLXUL6UUE+CZLbe+4P+zNEtRuKQ3TP1ZP2fnsvlJLNrJIn4bxXMZhCxrv4TFFXY
e/qifyNlj4vU8Ff+7po4d/rcuMoHXbNxVgLCwSJYiR12i882NzUKJYKDQdNz/WNe
IiyYIHhPAtK8ZwZdcixYjTxh2R+CzqA9lTg+SI1KfkQNb6ONoQKO4bjCv6Admp4l
PmrfrRwKSpUxvbHXKnoPAE+4KgorC1vnpLxIbok9XeFLZLXpTyRE3YV1w7giEQXD
5NUIs3/VseZD6dP2/y8b6pn2UlemoGHtirdP+ZP2O+kOO0egkypti2P3r68cyQ5e
XFntz674S2T89u+7YXZ3D11/pd94b8nZ7G8bhTVB4kkzPHp1mm4nBefRzoR0gyMX
aHrIDgV7TO3TFwRVFTNlCg5S71Ne8DAEeog0JDWA26xQd1fb0QNy/dPNkWAgsyNr
vjU5a9hey3JbwTGjBl4tkEFzh4Lz5XedbqGXvecCLy7KZZR+/+kD9hFhXSDazy1v
DZeyZwFfiZJcqA2quo8RWKjCo4low/SZVRyBdaNXCZ4P90RN7NDJ+frhZ1+3mzkj
79rnJseSN413IVwCyyIbbad8P1Y/a5Jsdcs0BJB2qd1G+phERvgcKGpNNR1apQ+3
XU64IVD+2gjX7s0J70LNFfSALKA/eXiv7GzivSwB7vh49Sz0St1yzzjZxgIcYjET
lMcAYb35qorLdAIVNDlmY+MN+SLYAuRbr+KawpTzoZmVKjSwCIY3fWaaVmgvWwMx
3WMYy8BlpXiw70T8PaBieHLkMYkRAlQfD2fZj27il8HulEyZlth0MHDZqZBHovVv
fUbQJi4MymjoQW9Woi7h4DVYTdscLpc161SxTe2sg1KgXEq1TTTJYmjA3emxMfO6
H4YPFXRdofP7rz9w2wsBIsGRcStVHMzc21AoM0WXlxd7zYwEjmjzV4GmEMycNz8t
FXOaC2pCLsLj+05UYjy13Ijt8FnO5WnJ7/ukK/SS9QSN0xS8Rr6aLleqjFEAhiSG
La06sfhxBakP5Dq9WbBW0o18/YsL3O50rRlVMma44WDUPYjQYaBRnN8fKfzsTlOW
hgFKrmZARVts7R52cD2UaZGWizv7D6T9GwrflM7BbWw0+/DxFzZ2uRqOq+1Oa1m0
t3JtV1tgxltI8TYgb5MtyD7w+uj8KxCmQ9QrNDioVV0WTMFBr2jUEgzMF5H3npwK
OMYn+HCHPc/4p4sOreH6tHiZIIY1H8Vf+j2TidgJXPmbaPb0pNtI/iuqwIOOFXK3
Lzw+pz6FN8S00AXULvsmZwN4Ku4vO1yk8CscbsLJOfNa2npCP2wB8IaHRM2LL55Z
wBvN1jdCiEoRkzoKArPCgrNigUPudyL/LZnqKd6oP1dx5G47+pAGHPm1Xw3hIwXX
I4p+Rcd5J8Svy/jTlkF68igH/Y5KqdtagepW3r0y/wZB+E+s1dmdX4bQRvUQXurR
UywVbbchkTG84hzhi2DHPQX49b/VwV484QQ6TJOjGMlyWotJln3GdaD1vu2EDTeJ
tA5qDBBA+UOTAkQ+KhvsZIDomWYqNT+TSktlmYmVP0iLJeQKvREUexkg24cSmfcR
mVj/qyzGsbr8zAW8Pmxd5k47c0h53F//YoohF4nXvUcm03ij9MnWfuK052l6OQg3
n+G2/9SViiozkH4z5C36nxdENB5avo0igLTlas5Mr1ud4re5qAggG0yJ8I8eP9Mp
pEthdvSGET4VhVT3JbWjGUBh56/brk+jWc55DL1T+uLrtozzGMOiC0XloVP8Xgd+
Q9kaDLTB7RvMWCTc2coSURnxUnv6zX9T+/+veUcGFnBn4VBlqdqVQJ7/bfuyy9ku
m6OafFFUKVS0O4U2VgoLv0aS+njTsyl00/8Tu0xdifgNTdqwI/EHprJ9Kv+czaIV
MCiXpMaYIHBlT5OGaY+YQVtwlcIvNSfr7rCscP+mRXLzR3cy/MR6Uxo02xK2Yxna
pbFwwXIRolafU989KjFNS7QeQVZ2emInICRluAyvOfOu9ahp9Sn4dRze+rMzfsQR
UV0LgC3rNbYZ9BZ465n1AmqIUWQ1h3ewxfw03AMYAda7kU0h5Tft2YXipOqhRBgZ
+fZR3ZG8HY0D2kqrYtXITVTJHvCk7rEBCq1bTtGUyzruBZWI6jb0KigkREMQlD6V
H+b03JQ5Oui5tIh3Vt5E+/iny7fxcCFcPrdpsc90V7Vm8AfllNESGj3q7o9rFrYX
c/8uxxY9N7xfmk131VnS01WlMJob+qOxSQOjePECBrvuPGd0PF5AxZFMhw0idUBZ
zcN5jnCtP1bbmCBdRmIo9uN3r8vhud8YI2JiJnLh18oImqqpuGp7nXm3NwUw6mXL
PdCccaxepuno9XHa9dVPZSHcJjRdTBdB1gRGhegEJd6dE9269F0NfHbX5C4HUbm5
2DGyNBx9gP9LOfFcb2ZNeI+i2qnIEN5E7E98jkg/aZ43ktKuwKfLm/xBTODlqevJ
M/ZeaiRpDURi0D3TNn92uCgzpTq251c4bXq1FAHd7mhk7oj+qCwGEHlL78riTHLH
Wm44BYJSq24QmGbw8LtHRH9A5G+hy5Ls3IPTPRJVhOok/39Lpmk1nAf+0UQaqZLN
7ASXZxVRlqax5R2C86nak19wti1eLz8QaMk6qmogddCbJxyE/pA391Q8hKbjrahF
fJ2F8KTa4YiTy9kiyJhndfLvKwny29nSOlXRfHxcdKZf8CnKif7BPYrf4X311h86
4VxkqQ/2O/9vvZo1/lSR21c7TU/4C0GhmkDjNozi1zBQxG1iDbSL31cZfrVSMZJs
0hrNkmTbG1V9oT9xpVkFroXzhkJyCu80JFDVqU03VIfXDai1N+DEDtm+lUVn9HZG
JkevPv08YVmz4ZWnpCeVO4++PirHJAjJc6zGa5Y9GRzkfmuzqF1B3exj0w5mHZ/Q
zYLFhDbF/DSs1+lIlX+KvND2maMEE8etjW/JqgxPKpsKiUcRMxml5ockMYchWZuQ
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1,115 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Author: Tlams
Langage: Python
Minimum version require: 3.4
"""
from pathlib import Path
from api.v1.api import *
from core.modules.mod_access import *
import configparser
import getpass
import os
import stat
import urllib3
global passhash
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
if __name__ == "__main__":
""" Read conf """
localconf = configparser.ConfigParser()
localconf.read('private/conf/config')
CritConf = CryticalData()
""" Step One: test private key or create it """
key_pvt = Path(localconf['system']['key_pvt'])
if not key_pvt.is_file():
print("No key found, auto-generation started ...")
passhash = encodepassphrase(getpass.getpass("Need a passphrase to start the generation:"))
print("This action can take some minutes, please wait.")
gen = CritConf.generate_key(localconf['system']['key_pvt'], localconf['system']['key_pub'], passhash)
if gen['result'] == "OK":
print("Your new key has been generate ! "
"\n - Private Key: {0} "
"\n - Public Key: {1}"
.format(localconf['system']['key_pvt'], localconf['system']['key_pvt']))
print("Passphrase HASH: {0}".format(passhash))
print("You must save your passphrase hash in a security place !")
else:
print(gen['Error'])
exit(1)
""" Test valid right for your private Key """
if oct(stat.S_IMODE(os.stat(localconf['system']['key_pvt']).st_mode)) != oct(0o600):
print("Your private key has not the good right({0})..."
"This problem can be very critical for your security.".
format(oct(stat.S_IMODE(os.stat(localconf['system']['key_pvt']).st_mode))))
os.chmod(localconf['system']['key_pvt'], 0o600)
print("Auto correction... done !")
""" Step two"""
if 'passhash' not in vars():
passhash = encodepassphrase(getpass.getpass("This system need a passphrase to start:"))
key_pvt = CritConf.read_private_key(localconf['system']['key_pvt'], passhash)
if key_pvt['result'] != "OK":
print("{0}: {1}"
"\n Please verify your passphrase".format(key_pvt['type'], key_pvt['error']))
exit(1)
key_pub = CritConf.read_public_key(localconf['system']['key_pub'])
"""
crypttest=CritConf.data_encryption("ploopp")
print(type(crypttest['data']))
print(CritConf.data_decryption(crypttest['data']))
exit(0)
"""
# URL MAPPING
urls = \
(
# MAPPING INSTANCES
'/api/v1/instance', 'Instance',
'/api/v1/instance/new', 'Instance',
'/api/v1/instance/([0-9]+)', 'Instance',
'/api/v1/instance/([0-9]+)/status/([a-z]+)', 'Instance',
'/api/v1/instance/([0-9]+)/package', 'package',
'/api/v1/instance/([0-9]+)/vhost(?:/([0-9]+))', 'vhost',
'/api/v1/instance/([0-9]+)/database(?:/([0-9]+))', 'database',
#  MAPPIN NODES
'/api/v1/node(?:/([0-9]+))', 'node',
# MAPPING SERVICES
'/api/v1/service/([a-z]+)/instance/([0-9]+)/vhost(?:/([0-9]+))', 'service',
# AUTH
'/api/v1/auth', 'Auth',
# MANAGEMENT
'/api/v1/administration/cluster', 'Cluster',
'/api/v1/administration/cluster/new', 'Cluster',
)
generalconf = {
"keys": {"key_pvt": key_pvt["data"], "key_pub": key_pub["data"]},
"mongodb": {"ip": localconf['databases']['mongodb_ip'], 'port': localconf['databases']['mongodb_port']},
"redis": {"ip": localconf['databases']['redis_ip'], 'port': localconf['databases']['redis_port']},
"deploy": {'concurrencydeploy': localconf['options']['concurrencydeploy'], 'delayrounddeploy': localconf['options']['delayrounddeploy']}
}
""" First redis connection """
Lredis = Redis_messages(generalconf["redis"]["ip"])
Lredis.connect()
""" Init Core thread """
core = Core(generalconf, Lredis)
""" Init API thread """
api_th = ThreadAPI(1, "ThreadAPI", urls, core, generalconf, Lredis)
api_th.start()

View file

@ -0,0 +1,4 @@
# This file is - if you set up HUGE correctly - not needed.
# But, for fallback reasons (if you don't route your vhost to /public), it will stay here.
RewriteEngine on
RewriteRule ^(.*) public/$1 [L]

View file

@ -0,0 +1,5 @@
# This file just tells the wonderful code quality analyzer Scrutinizer (https://scrutinizer-ci.com/g/panique/huge/)
# that we are using external services (Travis) to generate code coverage stats
# TODO is this correct ?
tools:
external_code_coverage: true

View file

@ -0,0 +1,30 @@
language: php
php:
- 5.5
- 5.6
- hhvm
before_install:
- sudo apt-get update > /dev/null
before_script:
- sudo apt-get install apache2
- sudo a2enmod rewrite
# configure apache virtual hosts, create vhost via travis-ci-apache file template
- sudo cp -f travis-ci-apache /etc/apache2/sites-available/default
- sudo sed -e "s?%TRAVIS_BUILD_DIR%?$(pwd)?g" --in-place /etc/apache2/sites-available/default
- sudo service apache2 restart
# composer
- composer self-update
- composer install --prefer-source --no-interaction --dev
# go to tests folder
- cd tests
# run unit tests, create result file
script: phpunit --configuration phpunit.xml --coverage-text --coverage-clover=coverage.clover
# gets tools from Scrutinizer, uploads unit tests results to Scrutinizer (?)
after_script:
- wget https://scrutinizer-ci.com/ocular.phar
- php ocular.phar code-coverage:upload --format=php-clover coverage.clover

View file

@ -0,0 +1,72 @@
# CHANGE LOG
For the newest (und unstable) version always check the develop branch.
## 3.1
Code Quality at Scrutinizer 9.7/10, at Code Climate 3.9/4
**February 2015**
- [panique] several code quality improvements (and line reductions :) ) all over the project
- [PR](https://github.com/panique/huge/pull/620) [owenr88] view rending now possible with multiple view files
- [panique] lots of code refactorings and simplifications all over the project
- [PR](https://github.com/panique/huge/pull/615) [Dominic28] Avatar can now be deleted by the user
- [panique] First Unit tests :)
- [panique] several code quality improvements all over the project
- [panique] avatarModel code improvements
- [panique] renamed AccountType stuff to UserRole, minor changes
## 3.0
Code Quality at Scrutinizer 9.3/10, at Code Climate 3.9/4
**February 2015**
- [panique] removed duplicate code in AccountTypeModel
- [PR](https://github.com/panique/huge/pull/587) [upperwood] Facebook stuff completely removed from SQL
- [panique] tiny text changes
**January 2015**
- [panique] added static Text class (gets the messages etc)
- [panique] added static Environment class (get the environment)
- [panique] added static Config class (gets config easily and according to environment)
- [panique] new styling of the entire project: login/index has new look now
- [panique] massive refactoring of all model classes: lots of methods have been organized into other model classes
- [panique] massive refactoring of all model classes: all methods are static now
- [panique] EXPERIMENTAL: added static database call / DatabaseFactory, rebuild NoteModel with static methods
- [panique] massive refactoring of mail sending, (chose between PHPMailer, SwiftMailer, native / SMTP or no SMTP)
**December 2014**
- [panique] lots of refactorings
- [panique] refactored LoginModel'S login() method / LoginController's login() method
- [panique] removed COOKIE_DOMAIN (cookie is now valid on the domain/IP it has been created on)
- [panique] Abstracting super-globals like $_POST['x'] into Request::post('x')
- [panique] entirely removed all the Facebook stuff [will be replaced by new proper Oauth2 solution soon]
- [panique] lots of code refactorings and cleaning, deletions of duplicate code
- [panique] moving nearly all hardcoded values to config
- [panique] new View handling: you'll have to pass vars to the view renderer now
- [panique] completely removed Facebook login process from controller (incomplete) [will be replaced by new solution]
- [panique] less config, URL/IP is auto-detected now
- [panique] added loadConfig() to load a specific config according to environment setting (fallback: development)
- [panique] added getEnvironment() to fetch (potential) environment setting
- [panique] replaced native super-globals access by wrapper access (Session:get instead of $_SESSION)
- [panique] complete frontend rebuilding (incomplete yet)
- [panique] massive cleaning of all controllers
- [panique] added Session::add() to allow stacking of elements (useful for collecting feedback, errors etc)
- [panique] complete rebuild of model handling
- [panique] View can now render(), renderWithoutHeaderFooter() and renderJSON
- [panique] using Composer's PSR-4 autoloader (in a very basic way currently)
- [panique] DB construction needs now port by default
- [panique] removed (semi-optional) hashing cost factor (as it's redundant usually)
- [panique] email max limit increased to 254/255 (official number)
- [panique] simpler and improved core
- [panique] improved architecture, controllers are now named like "IndexController"
- [panique] moved index.php to /public folder, new .htaccess, new installation guideline
- [panique] MVC naming fixes
- [nerdalertdk] betters paths, automatic paths
- [panique] removed legacy PHP stuff: 5.5.x is now the minimum
- [PR](https://github.com/panique/php-login/pull/503) [Malkleth] allow users to request password reset by inputting email as well as user names
- [PR](https://github.com/panique/php-login/pull/516) [pein0119] cookie runtime calculation fix

353
code/web/backend/README.md Normal file
View file

@ -0,0 +1,353 @@
[![HUGE, formerly "php-login" logo](_pictures/huge-logo.png)](http://www.php-login.net)
# HUGE
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/panique/huge/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/panique/huge/?branch=master)
[![Code Climate](https://codeclimate.com/github/panique/huge/badges/gpa.svg)](https://codeclimate.com/github/panique/huge)
[![Travis CI](https://travis-ci.org/panique/huge.svg?branch=master)](https://travis-ci.org/panique/huge)
[![Dependency Status](https://www.versioneye.com/user/projects/54ca11fbde7924f81a000010/badge.svg?style=flat)](https://www.versioneye.com/user/projects/54ca11fbde7924f81a000010)
Just a simple user authentication solution inside a super-simple framework skeleton that works out-of-the-box
(and comes with an auto-installer), using the future-proof official bcrypt password hashing/salting implementation of
PHP 5.5+, plus some nice features that will speed up the time from idea to first usable prototype application
dramatically. Nothing more. This project has its focus on hardcore simplicity. Everything is as simple as possible,
made for smaller projects, typical agency work and quick pitch drafts. If you want to build massive corporate
applications with all the features modern frameworks have, then have a look at [Laravel](http://laravel.com),
[Symfony](http://symfony.com) or [Yii](http://www.yiiframework.com), but if you just want to quickly create something
that just works, then this script might be interesting for you.
HUGE's simple-as-possible architecture was inspired by several conference talks, slides and articles about huge
applications that - surprisingly and intentionally - go back to the basics of programming, using procedural programming,
static classes, extremely simple constructs, not-totally-DRY code etc. while keeping the code extremely readable
([StackOverflow](http://www.dev-metal.com/architecture-stackoverflow/), Wikipedia, SoundCloud).
Buzzwords: [KISS](http://en.wikipedia.org/wiki/KISS_principle), [YASNI](http://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it).
#### Quick-Index
+ [Features](#features)
+ [Live-Demo](#live-demo)
+ [Support](#support)
+ [Follow the project](#follow)
+ [License](#license)
+ [Requirements](#requirements)
+ [Auto-Installation](#auto-installation)
- [Auto-Installation in Vagrant](#auto-installation-vagrant)
- [Auto-Installation in Ubuntu 14.04 LTS server](#auto-installation-ubuntu)
+ [Installation (Ubuntu 14.04 LTS)](#installation)
- [Quick Installation](#quick-installation)
- [Detailed Installation](#detailed-installation)
+ [Documentation](#documentation)
+ [Why is there no support forum anymore ?](#why-no-support-forum)
+ [Zero tolerance for idiots, trolls and vandals](#zero-tolerance)
+ [Contribute](#contribute)
+ [Report a bug](#bug-report)
### The History of HUGE
This script was formerly named "php-login" and by far the most popular version of the 4 simple PHP user auth
scripts of [The PHP Login Project](http://www.php-login.net) (a collection of simple login scripts, made to prevent
people from using totally outdated and insecure MD5 password hashing, which was still very popular in the PHP world
back in 2012).
Why the name "HUGE" ? It's a nice combination to
[TINY](https://github.com/panique/tiny),
[MINI](https://github.com/panique/mini) and
[MINI2](https://github.com/panique/mini2), my other projects :)
### Features <a name="features"></a>
* built with the official PHP password hashing functions, fitting the most modern password hashing/salting web standards
* users can register, login, logout (with username, email, password)
* [planned: OAuth2 implementation for proper future-proof 3rd party auth]
* password-forget / reset
* remember-me (login via cookie)
* account verification via mail
* captcha
* failed-login-throttling
* user profiles
* account upgrade / downgrade
* supports local avatars and remote Gravatars
* supports native mail and SMTP sending (via PHPMailer and other tools)
* uses PDO for database access for sure, has nice DatabaseFactory (in case your project goes big)
* uses URL rewriting ("beautiful URLs")
* proper split of application and public files (requests only go into /public)
* uses Composer to load external dependencies (PHPMailer, Captcha-Generator, etc.)
* fits PSR-0/1/2/4 coding guidelines
* masses of comments
* is actively developed, maintained and bug-fixed
### Live-Demo <a name="live-demo"></a>
See a [live demo here](http://demo-huge.php-login.net) and [the server's phpinfo() here](http://demo-huge.php-login.net/info.php).
### Support the project <a name="support"></a>
There a lot of work behind this project. I might save you hundreds, maybe thousands of hours of work (calculate that
in developer costs). So when you are earning money by using HUGE, be fair and give something back to open-source.
HUGE is totally free to private and commercial use.
TODO new banners
[![Donate with PayPal banner](_pictures/support-via-paypal.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=P5YLUK4MW3LDG)
[![Donate by server affiliate sale](_pictures/support-via-a2hosting.png)](https://affiliates.a2hosting.com/idevaffiliate.php?id=4471&url=579)
You can also rent your next $5 server at [Virpus](http://my.virpus.com/aff.php?aff=1836) or [DigitalOcean](https://www.digitalocean.com/?refcode=40d978532a20)
or donate via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=P5YLUK4MW3LDG).
Also feel free to contribute to this project.
### Follow the project <a name="follow"></a>
Here on **[Twitter](https://twitter.com/simplephplogin)** or **[Facebook](https://www.facebook.com/pages/PHP-Login-Script/461306677235868)**.
I'm also blogging at **[Dev Metal](http://www.dev-metal.com)**.
### License <a name="license"></a>
Licensed under [MIT](http://www.opensource.org/licenses/mit-license.php).
Totally free for private or commercial projects.
### Requirements <a name="requirements"></a>
Make sure you know the basics of object-oriented programming and MVC, are able to use the command line and have
used Composer before. This script is not for beginners.
* **PHP 5.5+**
* **MySQL 5** database (better use versions 5.5+ as very old versions have a [PDO injection bug](http://stackoverflow.com/q/134099/1114320)
* installed PHP extensions: pdo, gd, openssl (the install guideline shows how to do)
* installed tools on your server: git, curl, composer (the install guideline shows how to do)
* for professional mail sending: an SMTP account (I use [SMTP2GO](http://www.smtp2go.com/?s=devmetal))
* activated mod_rewrite on your server (the install guideline shows how to do)
### Auto-Installations <a name="auto-installation"></a>
Yo, fully automatic. Why ? Because I always hated it to spend days trying to find out how to install a thing.
This will save you masses of time and nerves. Donate a coffee if you like it.
#### Auto-Installation (in Vagrant) <a name="auto-installation-vagrant"></a>
If you are using Vagrant for your development, then simply
1. Add the official Ubuntu 14.04 LTS box to your Vagrant: `vagrant box add ubuntu/trusty64`
2. Move *Vagrantfile* and *bootstrap.sh* (from *_one-click-installation* folder) to a folder where you want to initialize your project.
3. Do `vagrant up` in that folder.
5 minutes later you'll have a fully installed HUGE inside Ubuntu 14.04 LTS. The full code will be auto-synced with
the current folder. MySQL root password and the PHPMyAdmin root password are set to *12345678*. By default
192.168.33.111 is the IP of your new box.
#### Auto-Installation in a naked Ubuntu 14.04 LTS server <a name="auto-installation-ubuntu"></a>
Extremely simple installation in a fresh and naked typical Ubuntu 14.04 LTS server:
Download the installer script
```bash
wget https://raw.githubusercontent.com/panique/huge/master/_one-click-installation/bootstrap.sh
```
Make it executable
```bash
chmod +x bootstrap.sh
```
Run it! Give it some minutes to perform all the tasks. And yes, you can thank me later :)
```bash
sudo ./bootstrap.sh
```
### Installation <a name="installation"></a>
This script is very fresh, so the install guidelines are not perfect yet.
#### Quick guide: <a name="quick-installation"></a>
0. Make sure you have Apache, PHP, MySQL installed. [Tutorial](http://www.dev-metal.com/installsetup-basic-lamp-stack-linux-apache-mysql-php-ubuntu-14-04-lts/).
1. Clone the repo to a folder on your server
2. Activate mod_rewrite, route all traffic to application's /public folder. [Tutorial](http://www.dev-metal.com/enable-mod_rewrite-ubuntu-14-04-lts/).
3. Edit application/config: Set your database credentials
4. Execute SQL statements from application/_installation to setup database tables
5. [Install Composer](http://www.dev-metal.com/install-update-composer-windows-7-ubuntu-debian-centos/),
run `Composer install` on application's root folder to install dependencies
6. Make avatar folder (application/public/avatars) writable
7. For proper email usage: Set SMTP credentials in config file, set EMAIL_USE_SMTP to true
"Email does not work" ? See the troubleshooting below. TODO
#### Detailed guide (Ubuntu 14.04 LTS): <a name="detailed-installation"></a>
This is just a quick guideline for easy setup of a development environment!
Make sure you have Apache, PHP 5.5+ and MySQL installed. [Tutorial here](http://www.dev-metal.com/installsetup-basic-lamp-stack-linux-apache-mysql-php-ubuntu-14-04-lts/).
Nginx will work for sure too, but no install guidelines are available yet.
Edit vhost to make clean URLs possible and route all traffic to /public folder of your project:
```bash
sudo nano /etc/apache2/sites-available/000-default.conf
```
and make the file look like
```
<VirtualHost *:80>
DocumentRoot "/var/www/html/public"
<Directory "/var/www/html/public">
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
```
Enable mod_rewrite and restart apache.
```bash
sudo a2enmod rewrite
service apache2 restart
```
Install curl (needed to use git), openssl (needed to clone from GitHub, as github is https only),
PHP GD, the graphic lib (we create captchas and avatars), and git.
```bash
sudo apt-get -y install curl
sudo apt-get -y install php5-curl
sudo apt-get -y install openssl
sudo apt-get -y install php5-gd
sudo apt-get -y install git
```
git clone HUGE
```bash
sudo git clone https://github.com/panique/huge "/var/www/html"
```
Install Composer
```bash
curl -s https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
```
Go to project folder, load Composer packages (--dev is optional, you know the deal)
```bash
cd /var/www/html
composer install --dev
```
Execute the SQL statements. Via phpmyadmin or via the command line for example. 12345678 is the example password.
Note that this is written without a space.
```bash
sudo mysql -h "localhost" -u "root" "-p12345678" < "/var/www/html/application/_installation/01-create-database.sql"
sudo mysql -h "localhost" -u "root" "-p12345678" < "/var/www/html/application/_installation/02-create-table-users.sql"
sudo mysql -h "localhost" -u "root" "-p12345678" < "/var/www/html/application/_installation/03-create-table-notes.sql"
```
Make avatar folder writable
```bash
sudo chmod 0777 -R "/var/www/html/public/avatars"
```
Remove Apache's default demo file
```bash
sudo rm "/var/www/html/index.html"
```
Edit the application's config in application/config.development.php and put in your database credentials.
Last part (not needed for a first test): Set your SMTP credentials in the same file and set EMAIL_USE_SMTP to true, so
you can send proper emails. It's highly recommended to use SMTP for mail sending! Native sending via PHP's mail() will
not work in nearly every case (spam blocking). I use [SMTP2GO](http://www.smtp2go.com/?s=devmetal).
Then check your server's IP / domain. Everything should work fine.
#### Testing with demo user
By default HUGE has a demo-user: username is `demo`, password is `12345678`. The user is already activated.
### What the hell are .travis.yml, .scrutinizer.yml etc. ?
There are several files in the root folder of the project that might be irritating:
- *.htaccess* (optionally) routes all traffic to /public/index.php! If you installed this project correctly, then this
file is not necessary, but as lots of people have problems setting up the vhost correctly, .htaccess it still there
to increase security, even on partly-broken-installations.
- *.scrutinizer.yml* (can be deleted): Configs for the external code quality analyzer Scrutinizer, just used here on
GitHub, you don't need this for your project.
- *.travis.yml* (can be deleted): Same like above. Travis is an external service that creates installations of this
repo after each code change to make sure everything runs fine. Also runs the unit tests. You don't need this inside
your project.
- *composer.json* (important): You should know what this does. ;) This file says what external dependencies are used.
- *travis-ci-apache* (can be deleted): Config file for Travis, see above, so Travis knows how to setup the Apache.
*README* and *CHANGELOG* are self-explaining.
#### Documentation <a name="documentation"></a>
A real documentation is in the making. Until then, please have a look at the code and use your IDE's code completion
features to get an idea how things work, it's quite obvious when you look at the controller files, the model files and
how data is shown in the view files. A big sorry that there's no documentation yet, but time is rare :)
TODO: Full documentation
TODO: Basic examples on how to do things
### Why is there no support forum (anymore) ? <a name="why-no-support-forum"></a>
There were two (!) support forums for v1 and v2 of this project (HUGE is v3), and both were vandalized by people who
didn't even read the readme and / or the install guidelines. Most asked question was "script does not work plz help"
without giving any useful information (like code or server setup or even the version used). While I'm writing these
lines somebody just asked via Twitter "how to install without Composer". You know what I mean :) ... Beside, 140
characters on Twitter are not a clever way to ask for / describe a complex development situation. 99% of the questions
were not necessary if the people would had read the guidelines, do a minimal research on their own or would stop making
things so unnecessarily complicated. And even when writing detailed answers most of them still messed it up, resulting
in rants and complaints (for free support for a free software!). It was just frustrating to deal with this every day,
especially when people take it for totally granted that *it's the duty* of open-source developers to give detailed,
free and personal support for every "plz help"-request.
So I decided to completely stop any free support. For serious questions about real problems inside the script please
use the GitHub issues feature.
### Zero tolerance for idiots, trolls and vandals! <a name="zero-tolerance"></a>
Harsh words, but as basically every public internet project gets harassed, vandalized and trolled these days by very
strange people it's necessary: Some simple rules.
1. Respect that this is just a simple script written by unpaid volunteers in their free-time.
This is NOT business-software you've bought for $10.000.
There's no reason to complain (!) about free open-source software. The attitude against free software
is really frustrating these days, people take everything for granted without realizing the work behind it, and the
fact they they get serious software totally for free, saving thousands of dollars. If you don't like it, then don't
use it. If you want a feature, try to take part in the process, maybe even build it by yourself and add it to the
project! Be nice and respectful. Constructive criticism is for sure always welcome!
2. Don't bash, don't hate, don't spam, don't vandalize. Don't ask for personal free support, don't ask if somebody
could do your work for you. Before you ask something, make sure you've read the README, followed every tutorial,
double-checked the code and tried to solve the problem by yourself.
Trolls and very annoying people will get a permanent ban / block. GitHub has a very powerful anti-abuse team.
### Contribute <a name="contribute"></a>
Please commit only in *develop* branch. The *master* branch will always contain the stable version.
### Found a bug (Responsible Disclosure) ? <a name="bug-report"></a>
Due to the possible consequences when publishing a bug on a public open-source project I'd kindly ask you to send really
big bugs to my email address, not posting this here. If the bug is not interesting for attackers: Feel free to create
an normal GitHub issue.
### Current and further development
See active issues and requested features here:
https://github.com/panique/huge/issues?state=open
### Useful links
- [How to use PDO](http://wiki.hashphp.org/PDO_Tutorial_for_MySQL_Developers)
- [A short guideline on how to use the PHP 5.5 password hashing functions and its PHP 5.3 & 5.4 implementations](http://www.dev-metal.com/use-php-5-5-password-hashing-functions/)
- [How to setup latest version of PHP 5.5 on Ubuntu 12.04 LTS](http://www.dev-metal.com/how-to-setup-latest-version-of-php-5-5-on-ubuntu-12-04-lts/)
- [How to setup latest version of PHP 5.5 on Debian Wheezy 7.0/7.1 (and how to fix the GPG key error)](http://www.dev-metal.com/setup-latest-version-php-5-5-debian-wheezy-7-07-1-fix-gpg-key-error/)
- [Notes on password & hashing salting in upcoming PHP versions (PHP 5.5.x & 5.6 etc.)](https://github.com/panique/huge/wiki/Notes-on-password-&-hashing-salting-in-upcoming-PHP-versions-%28PHP-5.5.x-&-5.6-etc.%29)
- [Some basic "benchmarks" of all PHP hash/salt algorithms](https://github.com/panique/huge/wiki/Which-hashing-&-salting-algorithm-should-be-used-%3F)
- [How to prevent PHP sessions being shared between different apache vhosts / different applications](http://www.dev-metal.com/prevent-php-sessions-shared-different-apache-vhosts-different-applications/)
### Side-facts
1. Weird! When I renamed php-login to HUGE (to get rid off the too generic project name and to make it fitting nicely
to MINI, TINY and MINI2, my other projects) I had a research if the word "huge" is already used in the php world for
sure. Nothing came up. Then, weeks later, I stumbled upon this: https://github.com/ffremont/HugeRest
I nice little framework in PHP, but it has only 1 star on Github, so it's obviously not so widely used. Looks very
professional, too. Hmm.... The guy behind published the entire readme etc. in pure french (!), so it's hard to use
for non-french-speaking people. However, I'm not related to him in any way, this is pure coincidence.

View file

@ -0,0 +1,22 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# Every Vagrant virtual environment requires a box to build off of.
config.vm.box = "ubuntu/trusty64"
# Create a private network, which allows host-only access to the machine using a specific IP.
config.vm.network "private_network", ip: "192.168.33.111"
# Share an additional folder to the guest VM. The first argument is the path on the host to the actual folder.
# The second argument is the path on the guest to mount the folder.
config.vm.synced_folder "./", "/var/www/html"
# Define the bootstrap file: A (shell) script that runs after first setup of your box (= provisioning)
config.vm.provision :shell, path: "bootstrap.sh"
end

View file

@ -0,0 +1,83 @@
#!/usr/bin/env bash
# Use single quotes instead of double quotes to make it work with special-character passwords
PASSWORD='12345678'
PROJECTFOLDER='myproject'
# create project folder
sudo mkdir "/var/www/html/${PROJECTFOLDER}"
sudo apt-get update
sudo apt-get -y upgrade
sudo apt-get install -y apache2
sudo apt-get install -y php5
sudo debconf-set-selections <<< "mysql-server mysql-server/root_password password $PASSWORD"
sudo debconf-set-selections <<< "mysql-server mysql-server/root_password_again password $PASSWORD"
sudo apt-get -y install mysql-server
sudo apt-get install php5-mysql
sudo debconf-set-selections <<< "phpmyadmin phpmyadmin/dbconfig-install boolean true"
sudo debconf-set-selections <<< "phpmyadmin phpmyadmin/app-password-confirm password $PASSWORD"
sudo debconf-set-selections <<< "phpmyadmin phpmyadmin/mysql/admin-pass password $PASSWORD"
sudo debconf-set-selections <<< "phpmyadmin phpmyadmin/mysql/app-pass password $PASSWORD"
sudo debconf-set-selections <<< "phpmyadmin phpmyadmin/reconfigure-webserver multiselect apache2"
sudo apt-get -y install phpmyadmin
# setup hosts file
VHOST=$(cat <<EOF
<VirtualHost *:80>
DocumentRoot "/var/www/html/${PROJECTFOLDER}/public"
<Directory "/var/www/html/${PROJECTFOLDER}/public">
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
EOF
)
echo "${VHOST}" > /etc/apache2/sites-available/000-default.conf
# enable mod_rewrite
sudo a2enmod rewrite
# restart apache
service apache2 restart
# install curl (needed to use git afaik)
sudo apt-get -y install curl
sudo apt-get -y install php5-curl
# install openssl (needed to clone from GitHub, as github is https only)
sudo apt-get -y install openssl
# install PHP GD, the graphic lib (we create captchas and avatars)
sudo apt-get -y install php5-gd
# install git
sudo apt-get -y install git
# git clone HUGE
sudo git clone https://github.com/panique/huge "/var/www/html/${PROJECTFOLDER}"
# install Composer
curl -s https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
# go to project folder, load Composer packages
cd "/var/www/html/${PROJECTFOLDER}"
composer install --dev
# run SQL statements from install folder
sudo mysql -h "localhost" -u "root" "-p${PASSWORD}" < "/var/www/html/${PROJECTFOLDER}/application/_installation/01-create-database.sql"
sudo mysql -h "localhost" -u "root" "-p${PASSWORD}" < "/var/www/html/${PROJECTFOLDER}/application/_installation/02-create-table-users.sql"
sudo mysql -h "localhost" -u "root" "-p${PASSWORD}" < "/var/www/html/${PROJECTFOLDER}/application/_installation/03-create-table-notes.sql"
# writing rights to avatar folder
sudo chmod 0777 -R "/var/www/html/${PROJECTFOLDER}/public/avatars"
# remove Apache's default demo file
sudo rm "/var/www/html/index.html"
# final feedback
echo "Voila!"

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1 @@
CREATE DATABASE IF NOT EXISTS `huge`;

View file

@ -0,0 +1,28 @@
CREATE TABLE IF NOT EXISTS `huge`.`users` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'auto incrementing user_id of each user, unique index',
`user_name` varchar(64) COLLATE utf8_unicode_ci NOT NULL COMMENT 'user''s name, unique',
`user_password_hash` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'user''s password in salted and hashed format',
`user_email` varchar(64) COLLATE utf8_unicode_ci NOT NULL COMMENT 'user''s email, unique',
`user_active` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'user''s activation status',
`user_account_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'user''s account type (basic, premium, etc)',
`user_has_avatar` tinyint(1) NOT NULL DEFAULT '0' COMMENT '1 if user has a local avatar, 0 if not',
`user_remember_me_token` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'user''s remember-me cookie token',
`user_creation_timestamp` bigint(20) DEFAULT NULL COMMENT 'timestamp of the creation of user''s account',
`user_last_login_timestamp` bigint(20) DEFAULT NULL COMMENT 'timestamp of user''s last login',
`user_failed_logins` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'user''s failed login attempts',
`user_last_failed_login` int(10) DEFAULT NULL COMMENT 'unix timestamp of last failed login attempt',
`user_activation_hash` varchar(40) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'user''s email verification hash string',
`user_password_reset_hash` char(40) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'user''s password reset code',
`user_password_reset_timestamp` bigint(20) DEFAULT NULL COMMENT 'timestamp of the password reset request',
`user_provider_type` text COLLATE utf8_unicode_ci,
PRIMARY KEY (`user_id`),
UNIQUE KEY `user_name` (`user_name`),
UNIQUE KEY `user_email` (`user_email`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='user data';
INSERT INTO `huge`.`users` (`user_id`, `user_name`, `user_password_hash`, `user_email`, `user_active`, `user_account_type`,
`user_has_avatar`, `user_remember_me_token`, `user_creation_timestamp`, `user_last_login_timestamp`,
`user_failed_logins`, `user_last_failed_login`, `user_activation_hash`, `user_password_reset_hash`,
`user_password_reset_timestamp`, `user_provider_type`) VALUES
(1, 'demo', '$2y$10$OvprunjvKOOhM1h9bzMPs.vuwGIsOqZbw88rzSyGCTJTcE61g5WXi', 'demo@demo.com', 1, 1, 0, NULL, 1422205178,
1422209189, 0, NULL, NULL, NULL, NULL, 'DEFAULT');

View file

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS `huge`.`notes` (
`note_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`note_text` text NOT NULL,
`user_id` int(11) unsigned NOT NULL,
PRIMARY KEY (`note_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='user notes';

View file

@ -0,0 +1,129 @@
<?php
/**
* Configuration for DEVELOPMENT environment
* To create another configuration set just copy this file to config.production.php etc. You get the idea :)
*/
/**
* Configuration for: Error reporting
* Useful to show every little problem during development, but only show hard / no errors in production.
* It's a little bit dirty to put this here, but who cares. For development purposes it's totally okay.
*/
error_reporting(E_ALL);
ini_set("display_errors", 1);
/**
* Returns the full configuration.
* This is used by the core/Config class.
*/
return array(
/**
* Configuration for: Base URL
* This detects your URL/IP incl. sub-folder automatically. You can also deactivate auto-detection and provide the
* URL manually. This should then look like 'http://192.168.33.44/' ! Note the slash in the end.
*/
'URL' => 'http://' . $_SERVER['HTTP_HOST'] . str_replace('public', '', dirname($_SERVER['SCRIPT_NAME'])),
/**
* Configuration for: Folders
* Usually there's no reason to change this.
*/
'PATH_CONTROLLER' => realpath(dirname(__FILE__).'/../../') . '/application/controller/',
'PATH_VIEW' => realpath(dirname(__FILE__).'/../../') . '/application/view/',
/**
* Configuration for: Avatar paths
* Internal path to save avatars. Make sure this folder is writable. The slash at the end is VERY important!
*/
'PATH_AVATARS' => realpath(dirname(__FILE__).'/../../') . '/public/avatars/',
'PATH_AVATARS_PUBLIC' => 'avatars/',
/**
* Configuration for: Default controller and action
*/
'DEFAULT_CONTROLLER' => 'index',
'DEFAULT_ACTION' => 'index',
/**
* Configuration for: Database
* DB_TYPE The used database type. Note that other types than "mysql" might break the db construction currently.
* DB_HOST The mysql hostname, usually localhost or 127.0.0.1
* DB_NAME The database name
* DB_USER The username
* DB_PASS The password
* DB_PORT The mysql port, 3306 by default (?), find out via phpinfo() and look for mysqli.default_port.
* DB_CHARSET The charset, necessary for security reasons. Check Database.php class for more info.
*/
'DB_TYPE' => 'mysql',
'DB_HOST' => '127.0.0.1',
'DB_NAME' => 'huge',
'DB_USER' => 'root',
'DB_PASS' => '12345678',
'DB_PORT' => '3306',
'DB_CHARSET' => 'utf8',
/**
* Configuration for: Additional login providers: Facebook
* CURRENTLY REMOVED (as Facebook has removed support for the used API version).
* Another, better and up-to-date implementation might come soon.
*/
'FACEBOOK_LOGIN' => false,
/**
* Configuration for: Captcha size
* The currently used Captcha generator (https://github.com/Gregwar/Captcha) also runs without giving a size,
* so feel free to use ->build(); inside CaptchaModel.
*/
'CAPTCHA_WIDTH' => 359,
'CAPTCHA_HEIGHT' => 100,
/**
* Configuration for: Cookies
* 1209600 seconds = 2 weeks
* COOKIE_PATH is the path the cookie is valid on, usually "/" to make it valid on the whole domain.
* @see http://stackoverflow.com/q/9618217/1114320
* @see php.net/manual/en/function.setcookie.php
*/
'COOKIE_RUNTIME' => 1209600,
'COOKIE_PATH' => '/',
/**
* Configuration for: Avatars/Gravatar support
* Set to true if you want to use "Gravatar(s)", a service that automatically gets avatar pictures via using email
* addresses of users by requesting images from the gravatar.com API. Set to false to use own locally saved avatars.
* AVATAR_SIZE set the pixel size of avatars/gravatars (will be 44x44 by default). Avatars are always squares.
* AVATAR_DEFAULT_IMAGE is the default image in public/avatars/
*/
'USE_GRAVATAR' => false,
'GRAVATAR_DEFAULT_IMAGESET' => 'mm',
'GRAVATAR_RATING' => 'pg',
'AVATAR_SIZE' => 44,
'AVATAR_JPEG_QUALITY' => 85,
'AVATAR_DEFAULT_IMAGE' => 'default.jpg',
/**
* Configuration for: Email server credentials
*
* Here you can define how you want to send emails.
* If you have successfully set up a mail server on your linux server and you know
* what you do, then you can skip this section. Otherwise please set EMAIL_USE_SMTP to true
* and fill in your SMTP provider account data.
*
* EMAIL_USED_MAILER: Check Mail class for alternatives
* EMAIL_USE_SMTP: Use SMTP or not
* EMAIL_SMTP_AUTH: leave this true unless your SMTP service does not need authentication
*/
'EMAIL_USED_MAILER' => 'phpmailer',
'EMAIL_USE_SMTP' => false,
'EMAIL_SMTP_HOST' => 'yourhost',
'EMAIL_SMTP_AUTH' => true,
'EMAIL_SMTP_USERNAME' => 'yourusername',
'EMAIL_SMTP_PASSWORD' => 'yourpassword',
'EMAIL_SMTP_PORT' => 465,
'EMAIL_SMTP_ENCRYPTION' => 'ssl',
/**
* Configuration for: Email content data
*/
'EMAIL_PASSWORD_RESET_URL' => 'login/verifypasswordreset',
'EMAIL_PASSWORD_RESET_FROM_EMAIL' => 'no-reply@example.com',
'EMAIL_PASSWORD_RESET_FROM_NAME' => 'My Project',
'EMAIL_PASSWORD_RESET_SUBJECT' => 'Password reset for PROJECT XY',
'EMAIL_PASSWORD_RESET_CONTENT' => 'Please click on this link to reset your password: ',
'EMAIL_VERIFICATION_URL' => 'login/verify',
'EMAIL_VERIFICATION_FROM_EMAIL' => 'no-reply@example.com',
'EMAIL_VERIFICATION_FROM_NAME' => 'My Project',
'EMAIL_VERIFICATION_SUBJECT' => 'Account activation for PROJECT XY',
'EMAIL_VERIFICATION_CONTENT' => 'Please click on this link to activate your account: ',
);

View file

@ -0,0 +1,73 @@
<?php
/**
* Texts used in the application.
* These texts are used via Text::get('FEEDBACK_USERNAME_ALREADY_TAKEN').
* Could be extended to i18n etc.
*/
return array(
"FEEDBACK_UNKNOWN_ERROR" => "Unknown error occurred!",
"FEEDBACK_PASSWORD_WRONG_3_TIMES" => "You have typed in a wrong password 3 or more times already. Please wait 30 seconds to try again.",
"FEEDBACK_ACCOUNT_NOT_ACTIVATED_YET" => "Your account is not activated yet. Please click on the confirm link in the mail.",
"FEEDBACK_PASSWORD_WRONG" => "Password was wrong.",
"FEEDBACK_USER_DOES_NOT_EXIST" => "This user does not exist.",
"FEEDBACK_LOGIN_FAILED" => "Login failed.",
"FEEDBACK_USERNAME_FIELD_EMPTY" => "Username field was empty.",
"FEEDBACK_PASSWORD_FIELD_EMPTY" => "Password field was empty.",
"FEEDBACK_USERNAME_OR_PASSWORD_FIELD_EMPTY" => "Username or password field was empty.",
"FEEDBACK_USERNAME_EMAIL_FIELD_EMPTY" => "Username / email field was empty.",
"FEEDBACK_EMAIL_FIELD_EMPTY" => "Email field was empty.",
"FEEDBACK_EMAIL_AND_PASSWORD_FIELDS_EMPTY" => "Email and password fields were empty.",
"FEEDBACK_USERNAME_SAME_AS_OLD_ONE" => "Sorry, that username is the same as your current one. Please choose another one.",
"FEEDBACK_USERNAME_ALREADY_TAKEN" => "Sorry, that username is already taken. Please choose another one.",
"FEEDBACK_USER_EMAIL_ALREADY_TAKEN" => "Sorry, that email is already in use. Please choose another one.",
"FEEDBACK_USERNAME_CHANGE_SUCCESSFUL" => "Your username has been changed successfully.",
"FEEDBACK_USERNAME_AND_PASSWORD_FIELD_EMPTY" => "Username and password fields were empty.",
"FEEDBACK_USERNAME_DOES_NOT_FIT_PATTERN" => "Username does not fit the name pattern: only a-Z and numbers are allowed, 2 to 64 characters.",
"FEEDBACK_EMAIL_DOES_NOT_FIT_PATTERN" => "Sorry, your chosen email does not fit into the email naming pattern.",
"FEEDBACK_EMAIL_SAME_AS_OLD_ONE" => "Sorry, that email address is the same as your current one. Please choose another one.",
"FEEDBACK_EMAIL_CHANGE_SUCCESSFUL" => "Your email address has been changed successfully.",
"FEEDBACK_CAPTCHA_WRONG" => "The entered captcha security characters were wrong.",
"FEEDBACK_PASSWORD_REPEAT_WRONG" => "Password and password repeat are not the same.",
"FEEDBACK_PASSWORD_TOO_SHORT" => "Password has a minimum length of 6 characters.",
"FEEDBACK_USERNAME_TOO_SHORT_OR_TOO_LONG" => "Username cannot be shorter than 2 or longer than 64 characters.",
"FEEDBACK_ACCOUNT_SUCCESSFULLY_CREATED" => "Your account has been created successfully and we have sent you an email. Please click the VERIFICATION LINK within that mail.",
"FEEDBACK_VERIFICATION_MAIL_SENDING_FAILED" => "Sorry, we could not send you an verification mail. Your account has NOT been created.",
"FEEDBACK_ACCOUNT_CREATION_FAILED" => "Sorry, your registration failed. Please go back and try again.",
"FEEDBACK_VERIFICATION_MAIL_SENDING_ERROR" => "Verification mail could not be sent due to: ",
"FEEDBACK_VERIFICATION_MAIL_SENDING_SUCCESSFUL" => "A verification mail has been sent successfully.",
"FEEDBACK_ACCOUNT_ACTIVATION_SUCCESSFUL" => "Activation was successful! You can now log in.",
"FEEDBACK_ACCOUNT_ACTIVATION_FAILED" => "Sorry, no such id/verification code combination here...",
"FEEDBACK_AVATAR_UPLOAD_SUCCESSFUL" => "Avatar upload was successful.",
"FEEDBACK_AVATAR_UPLOAD_WRONG_TYPE" => "Only JPEG and PNG files are supported.",
"FEEDBACK_AVATAR_UPLOAD_TOO_SMALL" => "Avatar source file's width/height is too small. Needs to be 100x100 pixel minimum.",
"FEEDBACK_AVATAR_UPLOAD_TOO_BIG" => "Avatar source file is too big. 5 Megabyte is the maximum.",
"FEEDBACK_AVATAR_FOLDER_DOES_NOT_EXIST_OR_NOT_WRITABLE" => "Avatar folder does not exist or is not writable. Please change this via chmod 775 or 777.",
"FEEDBACK_AVATAR_IMAGE_UPLOAD_FAILED" => "Something went wrong with the image upload.",
"FEEDBACK_AVATAR_IMAGE_DELETE_SUCCESSFUL" => "You successfully deleted your avatar.",
"FEEDBACK_AVATAR_IMAGE_DELETE_NO_FILE" => "You don't have a custom avatar.",
"FEEDBACK_AVATAR_IMAGE_DELETE_FAILED" => "Something went wrong while deleting your avatar.",
"FEEDBACK_PASSWORD_RESET_TOKEN_FAIL" => "Could not write token to database.",
"FEEDBACK_PASSWORD_RESET_TOKEN_MISSING" => "No password reset token.",
"FEEDBACK_PASSWORD_RESET_MAIL_SENDING_ERROR" => "Password reset mail could not be sent due to: ",
"FEEDBACK_PASSWORD_RESET_MAIL_SENDING_SUCCESSFUL" => "A password reset mail has been sent successfully.",
"FEEDBACK_PASSWORD_RESET_LINK_EXPIRED" => "Your reset link has expired. Please use the reset link within one hour.",
"FEEDBACK_PASSWORD_RESET_COMBINATION_DOES_NOT_EXIST" => "Username/Verification code combination does not exist.",
"FEEDBACK_PASSWORD_RESET_LINK_VALID" => "Password reset validation link is valid. Please change the password now.",
"FEEDBACK_PASSWORD_CHANGE_SUCCESSFUL" => "Password successfully changed.",
"FEEDBACK_PASSWORD_CHANGE_FAILED" => "Sorry, your password changing failed.",
"FEEDBACK_ACCOUNT_TYPE_CHANGE_SUCCESSFUL" => "Account type change successful",
"FEEDBACK_ACCOUNT_TYPE_CHANGE_FAILED" => "Account type change failed",
"FEEDBACK_NOTE_CREATION_FAILED" => "Note creation failed.",
"FEEDBACK_NOTE_EDITING_FAILED" => "Note editing failed.",
"FEEDBACK_NOTE_DELETION_FAILED" => "Note deletion failed.",
"FEEDBACK_COOKIE_INVALID" => "Your remember-me-cookie is invalid.",
"FEEDBACK_COOKIE_LOGIN_SUCCESSFUL" => "You were successfully logged in via the remember-me-cookie.",
"FEEDBACK_FACEBOOK_LOGIN_NOT_REGISTERED" => "Sorry, you don't have an account here. Please register first.",
"FEEDBACK_FACEBOOK_EMAIL_NEEDED" => "Sorry, but you need to allow us to see your email address to register.",
"FEEDBACK_FACEBOOK_UID_ALREADY_EXISTS" => "Sorry, but you have already registered here (your Facebook ID exists in our database).",
"FEEDBACK_FACEBOOK_EMAIL_ALREADY_EXISTS" => "Sorry, but you have already registered here (your Facebook email exists in our database).",
"FEEDBACK_FACEBOOK_USERNAME_ALREADY_EXISTS" => "Sorry, but you have already registered here (your Facebook username exists in our database).",
"FEEDBACK_FACEBOOK_REGISTER_SUCCESSFUL" => "You have been successfully registered with Facebook.",
"FEEDBACK_FACEBOOK_OFFLINE" => "We could not reach the Facebook servers. Maybe Facebook is offline (that really happens sometimes).",
);

View file

@ -0,0 +1,26 @@
<?php
/**
* This controller shows an area that's only visible for logged in users (because of Auth::checkAuthentication(); in line 16)
*/
class DashboardController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
// this entire controller should only be visible/usable by logged in users, so we put authentication-check here
Auth::checkAuthentication();
}
/**
* This method controls what happens when you move to /dashboard/index in your app.
*/
public function index()
{
$this->View->render('dashboard/index');
}
}

View file

@ -0,0 +1,25 @@
<?php
/**
* Class Error
* This controller simply shows a page that will be displayed when a controller/method is not found. Simple 404.
*/
class ErrorController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
}
/**
* This method controls what happens / what the user sees when a page does not exist (404)
*/
public function index()
{
header('HTTP/1.0 404 Not Found');
$this->View->render('error/index');
}
}

View file

@ -0,0 +1,21 @@
<?php
class IndexController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
}
/**
* Handles what happens when user moves to URL/index/index - or - as this is the default controller, also
* when user moves to /index or enter your application at base level
*/
public function index()
{
$this->View->render('index/index');
}
}

View file

@ -0,0 +1,313 @@
<?php
/**
* LoginController
* Controls everything that is authentication-related
*/
class LoginController extends Controller
{
/**
* Construct this object by extending the basic Controller class. The parent::__construct thing is necessary to
* put checkAuthentication in here to make an entire controller only usable for logged-in users (for sure not
* needed in the LoginController).
*/
public function __construct()
{
parent::__construct();
}
/**
* Index, default action (shows the login form), when you do login/index
*/
public function index()
{
// if user is logged in redirect to main-page, if not show the view
if (LoginModel::isUserLoggedIn()) {
Redirect::home();
} else {
$this->View->render('login/index');
}
}
/**
* The login action, when you do login/login
*/
public function login()
{
// perform the login method, put result (true or false) into $login_successful
$login_successful = LoginModel::login(
Request::post('user_name'), Request::post('user_password'), Request::post('set_remember_me_cookie')
);
// check login status: if true, then redirect user login/showProfile, if false, then to login form again
if ($login_successful) {
Redirect::to('login/showProfile');
} else {
Redirect::to('login/index');
}
}
/**
* The logout action
* Perform logout, redirect user to main-page
*/
public function logout()
{
LoginModel::logout();
Redirect::home();
}
/**
* Login with cookie
*/
public function loginWithCookie()
{
// run the loginWithCookie() method in the login-model, put the result in $login_successful (true or false)
$login_successful = LoginModel::loginWithCookie(Request::cookie('remember_me'));
// if login successful, redirect to dashboard/index ...
if ($login_successful) {
Redirect::to('dashboard/index');
} else {
// if not, delete cookie (outdated? attack?) and route user to login form to prevent infinite login loops
LoginModel::deleteCookie();
Redirect::to('login/index');
}
}
/**
* Show user's PRIVATE profile
* Auth::checkAuthentication() makes sure that only logged in users can use this action and see this page
*/
public function showProfile()
{
Auth::checkAuthentication();
$this->View->render('login/showProfile', array(
'user_name' => Session::get('user_name'),
'user_email' => Session::get('user_email'),
'user_gravatar_image_url' => Session::get('user_gravatar_image_url'),
'user_avatar_file' => Session::get('user_avatar_file'),
'user_account_type' => Session::get('user_account_type')
));
}
/**
* Show edit-my-username page
* Auth::checkAuthentication() makes sure that only logged in users can use this action and see this page
*/
public function editUsername()
{
Auth::checkAuthentication();
$this->View->render('login/editUsername');
}
/**
* Edit user name (perform the real action after form has been submitted)
* Auth::checkAuthentication() makes sure that only logged in users can use this action
*/
public function editUsername_action()
{
Auth::checkAuthentication();
UserModel::editUserName(Request::post('user_name'));
Redirect::to('login/index');
}
/**
* Show edit-my-user-email page
* Auth::checkAuthentication() makes sure that only logged in users can use this action and see this page
*/
public function editUserEmail()
{
Auth::checkAuthentication();
$this->View->render('login/editUserEmail');
}
/**
* Edit user email (perform the real action after form has been submitted)
* Auth::checkAuthentication() makes sure that only logged in users can use this action and see this page
*/
// make this POST
public function editUserEmail_action()
{
Auth::checkAuthentication();
UserModel::editUserEmail(Request::post('user_email'));
Redirect::to('login/editUserEmail');
}
/**
* Edit avatar
* Auth::checkAuthentication() makes sure that only logged in users can use this action and see this page
*/
public function editAvatar()
{
Auth::checkAuthentication();
$this->View->render('login/editAvatar', array(
'avatar_file_path' => AvatarModel::getPublicUserAvatarFilePathByUserId(Session::get('user_id'))
));
}
/**
* Perform the upload of the avatar
* Auth::checkAuthentication() makes sure that only logged in users can use this action and see this page
* POST-request
*/
public function uploadAvatar_action()
{
Auth::checkAuthentication();
AvatarModel::createAvatar();
Redirect::to('login/editAvatar');
}
/**
* Delete the current user's avatar
* Auth::checkAuthentication() makes sure that only logged in users can use this action and see this page
*/
public function deleteAvatar_action()
{
Auth::checkAuthentication();
AvatarModel::deleteAvatar(Session::get("user_id"));
Redirect::to('login/editAvatar');
}
/**
* Show the change-account-type page
* Auth::checkAuthentication() makes sure that only logged in users can use this action and see this page
*/
public function changeUserRole()
{
Auth::checkAuthentication();
$this->View->render('login/changeUserRole');
}
/**
* Perform the account-type changing
* Auth::checkAuthentication() makes sure that only logged in users can use this action
* POST-request
*/
public function changeUserRole_action()
{
Auth::checkAuthentication();
if (Request::post('user_account_upgrade')) {
// "2" is quick & dirty account type 2, something like "premium user" maybe. you got the idea :)
UserRoleModel::changeUserRole(2);
}
if (Request::post('user_account_downgrade')) {
// "1" is quick & dirty account type 1, something like "basic user" maybe.
UserRoleModel::changeUserRole(1);
}
Redirect::to('login/changeUserRole');
}
/**
* Register page
* Show the register form, but redirect to main-page if user is already logged-in
*/
public function register()
{
if (LoginModel::isUserLoggedIn()) {
Redirect::home();
} else {
$this->View->render('login/register');
}
}
/**
* Register page action
* POST-request after form submit
*/
public function register_action()
{
$registration_successful = RegistrationModel::registerNewUser();
if ($registration_successful) {
Redirect::to('login/index');
} else {
Redirect::to('login/register');
}
}
/**
* Verify user after activation mail link opened
* @param int $user_id user's id
* @param string $user_activation_verification_code user's verification token
*/
public function verify($user_id, $user_activation_verification_code)
{
if (isset($user_id) && isset($user_activation_verification_code)) {
RegistrationModel::verifyNewUser($user_id, $user_activation_verification_code);
$this->View->render('login/verify');
} else {
Redirect::to('login/index');
}
}
/**
* Show the request-password-reset page
*/
public function requestPasswordReset()
{
$this->View->render('login/requestPasswordReset');
}
/**
* The request-password-reset action
* POST-request after form submit
*/
public function requestPasswordReset_action()
{
PasswordResetModel::requestPasswordReset(Request::post('user_name_or_email'));
Redirect::to('login/index');
}
/**
* Verify the verification token of that user (to show the user the password editing view or not)
* @param string $user_name username
* @param string $verification_code password reset verification token
*/
public function verifyPasswordReset($user_name, $verification_code)
{
// check if this the provided verification code fits the user's verification code
if (PasswordResetModel::verifyPasswordReset($user_name, $verification_code)) {
// pass URL-provided variable to view to display them
$this->View->render('login/changePassword', array(
'user_name' => $user_name,
'user_password_reset_hash' => $verification_code
));
} else {
Redirect::to('login/index');
}
}
/**
* Set the new password
* Please note that this happens while the user is not logged in. The user identifies via the data provided by the
* password reset link from the email, automatically filled into the <form> fields. See verifyPasswordReset()
* for more. Then (regardless of result) route user to index page (user will get success/error via feedback message)
* POST request !
* TODO this is an _action
*/
public function setNewPassword()
{
PasswordResetModel::setNewPassword(
Request::post('user_name'), Request::post('user_password_reset_hash'),
Request::post('user_password_new'), Request::post('user_password_repeat')
);
Redirect::to('login/index');
}
/**
* Generate a captcha, write the characters into $_SESSION['captcha'] and returns a real image which will be used
* like this: <img src="......./login/showCaptcha" />
* IMPORTANT: As this action is called via <img ...> AFTER the real application has finished executing (!), the
* SESSION["captcha"] has no content when the application is loaded. The SESSION["captcha"] gets filled at the
* moment the end-user requests the <img .. >
* Maybe refactor this sometime.
*/
public function showCaptcha()
{
CaptchaModel::generateAndShowCaptcha();
}
}

View file

@ -0,0 +1,77 @@
<?php
/**
* The note controller: Just an example of simple create, read, update and delete (CRUD) actions.
*/
class NoteController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
// VERY IMPORTANT: All controllers/areas that should only be usable by logged-in users
// need this line! Otherwise not-logged in users could do actions. If all of your pages should only
// be usable by logged-in users: Put this line into libs/Controller->__construct
Auth::checkAuthentication();
}
/**
* This method controls what happens when you move to /note/index in your app.
* Gets all notes (of the user).
*/
public function index()
{
$this->View->render('note/index', array(
'notes' => NoteModel::getAllNotes()
));
}
/**
* This method controls what happens when you move to /dashboard/create in your app.
* Creates a new note. This is usually the target of form submit actions.
* POST request.
*/
public function create()
{
NoteModel::createNote(Request::post('note_text'));
Redirect::to('note');
}
/**
* This method controls what happens when you move to /note/edit(/XX) in your app.
* Shows the current content of the note and an editing form.
* @param $note_id int id of the note
*/
public function edit($note_id)
{
$this->View->render('note/edit', array(
'note' => NoteModel::getNote($note_id)
));
}
/**
* This method controls what happens when you move to /note/editSave in your app.
* Edits a note (performs the editing after form submit).
* POST request.
*/
public function editSave()
{
NoteModel::updateNote(Request::post('note_id'), Request::post('note_text'));
Redirect::to('note');
}
/**
* This method controls what happens when you move to /note/delete(/XX) in your app.
* Deletes a note. In a real application a deletion via GET/URL is not recommended, but for demo purposes it's
* totally okay.
* @param int $note_id id of the note
*/
public function delete($note_id)
{
NoteModel::deleteNote($note_id);
Redirect::to('note');
}
}

View file

@ -0,0 +1,39 @@
<?php
class ProfileController extends Controller
{
/**
* Construct this object by extending the basic Controller class
*/
public function __construct()
{
parent::__construct();
}
/**
* This method controls what happens when you move to /overview/index in your app.
* Shows a list of all users.
*/
public function index()
{
$this->View->render('profile/index', array(
'users' => UserModel::getPublicProfilesOfAllUsers())
);
}
/**
* This method controls what happens when you move to /overview/showProfile in your app.
* Shows the (public) details of the selected user.
* @param $user_id int id the the user
*/
public function showProfile($user_id)
{
if (isset($user_id)) {
$this->View->render('profile/showProfile', array(
'user' => UserModel::getPublicProfileOfUser($user_id))
);
} else {
Redirect::home();
}
}
}

View file

@ -0,0 +1,100 @@
<?php
/**
* Class Application
* The heart of the application
*/
class Application
{
/** @var mixed Instance of the controller */
private $controller;
/** @var array URL parameters, will be passed to used controller-method */
private $parameters = array();
/** @var string Just the name of the controller, useful for checks inside the view ("where am I ?") */
private $controller_name;
/** @var string Just the name of the controller's method, useful for checks inside the view ("where am I ?") */
private $action_name;
/**
* Start the application, analyze URL elements, call according controller/method or relocate to fallback location
*/
public function __construct()
{
// create array with URL parts in $url
$this->splitUrl();
// creates controller and action names (from URL input)
$this->createControllerAndActionNames();
// does such a controller exist ?
if (file_exists(Config::get('PATH_CONTROLLER') . $this->controller_name . '.php')) {
// load this file and create this controller
// example: if controller would be "car", then this line would translate into: $this->car = new car();
require Config::get('PATH_CONTROLLER') . $this->controller_name . '.php';
$this->controller = new $this->controller_name();
// check for method: does such a method exist in the controller ?
if (method_exists($this->controller, $this->action_name)) {
if (!empty($this->parameters)) {
// call the method and pass arguments to it
call_user_func_array(array($this->controller, $this->action_name), $this->parameters);
} else {
// if no parameters are given, just call the method without parameters, like $this->index->index();
$this->controller->{$this->action_name}();
}
} else {
header('location: ' . Config::get('URL') . 'error');
}
} else {
header('location: ' . Config::get('URL') . 'error');
}
}
/**
* Get and split the URL
*/
private function splitUrl()
{
if (Request::get('url')) {
// split URL
$url = trim(Request::get('url'), '/');
$url = filter_var($url, FILTER_SANITIZE_URL);
$url = explode('/', $url);
// put URL parts into according properties
$this->controller_name = isset($url[0]) ? $url[0] : null;
$this->action_name = isset($url[1]) ? $url[1] : null;
// remove controller name and action name from the split URL
unset($url[0], $url[1]);
// rebase array keys and store the URL parameters
$this->parameters = array_values($url);
}
}
/**
* Checks if controller and action names are given. If not, default values are put into the properties.
* Also renames controller to usable name.
*/
private function createControllerAndActionNames()
{
// check for controller: no controller given ? then make controller = default controller (from config)
if (!$this->controller_name) {
$this->controller_name = Config::get('DEFAULT_CONTROLLER');
}
// check for action: no action given ? then make action = default action (from config)
if (!$this->action_name OR (strlen($this->action_name) == 0)) {
$this->action_name = Config::get('DEFAULT_ACTION');
}
// rename controller name to real controller class/file name ("index" to "IndexController")
$this->controller_name = ucwords($this->controller_name) . 'Controller';
}
}

View file

@ -0,0 +1,28 @@
<?php
/**
* Class Auth
* Checks if user is logged in, if not then sends the user to "yourdomain.com/login".
* Auth::checkAuthentication() can be used in the constructor of a controller (to make the
* entire controller only visible for logged-in users) or inside a controller-method to make only this part of the
* application available for logged-in users.
*/
class Auth
{
public static function checkAuthentication()
{
// initialize the session (if not initialized yet)
Session::init();
// if user is not logged in...
if (!Session::userIsLoggedIn()) {
// ... then treat user as "not logged in", destroy session, redirect to login page
Session::destroy();
header('location: ' . Config::get('URL') . 'login');
// to prevent fetching views via cURL (which "ignores" the header-redirect above) we leave the application
// the hard way, via exit(). @see https://github.com/panique/php-login/issues/453
// this is not optimal and will be fixed in future releases
exit();
}
}
}

View file

@ -0,0 +1,23 @@
<?php
class Config
{
// this is public to allow better Unit Testing
public static $config;
public static function get($key)
{
if (!self::$config) {
$config_file = '../application/config/config.' . Environment::get() . '.php';
if (!file_exists($config_file)) {
return false;
}
self::$config = require $config_file;
}
return self::$config[$key];
}
}

View file

@ -0,0 +1,31 @@
<?php
/**
* This is the "base controller class". All other "real" controllers extend this class.
* Whenever a controller is created, we also
* 1. initialize a session
* 2. check if the user is not logged in anymore (session timeout) but has a cookie
*/
class Controller
{
/** @var View View The view object */
public $View;
/**
* Construct the (base) controller. This happens when a real controller is constructed, like in
* the constructor of IndexController when it says: parent::__construct();
*/
function __construct()
{
// always initialize a session
Session::init();
// user is not logged in but has remember-me-cookie ? then try to login with cookie ("remember me" feature)
if (!Session::userIsLoggedIn() AND Request::cookie('remember_me')) {
header('location: ' . Config::get('URL') . 'login/loginWithCookie');
}
// create a view object to be able to use it inside a controller, like $this->View->render();
$this->View = new View();
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* Class DatabaseFactory
*
* Use it like this:
* $database = DatabaseFactory::getFactory()->getConnection();
*
* That's my personal favourite when creating a database connection.
* It's a slightly modified version of Jon Raphaelson's excellent answer on StackOverflow:
* http://stackoverflow.com/questions/130878/global-or-singleton-for-database-connection
*
* Full quote from the answer:
*
* "Then, in 6 months when your app is super famous and getting dugg and slashdotted and you decide you need more than
* a single connection, all you have to do is implement some pooling in the getConnection() method. Or if you decide
* that you want a wrapper that implements SQL logging, you can pass a PDO subclass. Or if you decide you want a new
* connection on every invocation, you can do do that. It's flexible, instead of rigid."
*
* Thanks! Big up, mate!
*/
class DatabaseFactory
{
private static $factory;
private $database;
public static function getFactory()
{
if (!self::$factory) {
self::$factory = new DatabaseFactory();
}
return self::$factory;
}
public function getConnection() {
if (!$this->database) {
$options = array(PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ, PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING);
$this->database = new PDO(
Config::get('DB_TYPE') . ':host=' . Config::get('DB_HOST') . ';dbname=' .
Config::get('DB_NAME') . ';port=' . Config::get('DB_PORT') . ';charset=' . Config::get('DB_CHARSET'),
Config::get('DB_USER'), Config::get('DB_PASS'), $options
);
}
return $this->database;
}
}

View file

@ -0,0 +1,18 @@
<?php
/**
* Class Environment
*
* Extremely simple way to get the environment, everywhere inside your application.
* Extend this the way you want.
*/
class Environment
{
public static function get()
{
// if APPLICATION_ENV constant exists (set in Apache configs)
// then return content of APPLICATION_ENV
// else return "development"
return (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : "development");
}
}

View file

@ -0,0 +1,115 @@
<?php
class Mail
{
/** @var mixed variable to collect errors */
private $error;
/**
* Try to send a mail by using PHP's native mail() function.
* Please note that not PHP itself will send a mail, it's just a wrapper for Linux's sendmail or other mail tools
*
* Good guideline on how to send mails natively with mail():
* @see http://stackoverflow.com/a/24644450/1114320
* @see http://www.php.net/manual/en/function.mail.php
*/
public function sendMailWithNativeMailFunction()
{
// no code yet, so we just return something to make IDEs and code analyzer tools happy
return false;
}
/**
* Try to send a mail by using SwiftMailer.
* Make sure you have loaded SwiftMailer via Composer.
*
* @return bool
*/
public function sendMailWithSwiftMailer()
{
// no code yet, so we just return something to make IDEs and code analyzer tools happy
return false;
}
/**
* Try to send a mail by using PHPMailer.
* Make sure you have loaded PHPMailer via Composer.
* Depending on your EMAIL_USE_SMTP setting this will work via SMTP credentials or via native mail()
*
* @param $user_email
* @param $from_email
* @param $from_name
* @param $subject
* @param $body
*
* @return bool
* @throws Exception
* @throws phpmailerException
*/
public function sendMailWithPHPMailer($user_email, $from_email, $from_name, $subject, $body)
{
$mail = new PHPMailer;
// if you want to send mail via PHPMailer using SMTP credentials
if (Config::get('EMAIL_USE_SMTP')) {
// set PHPMailer to use SMTP
$mail->IsSMTP();
// 0 = off, 1 = commands, 2 = commands and data, perfect to see SMTP errors
$mail->SMTPDebug = 0;
// enable SMTP authentication
$mail->SMTPAuth = Config::get('EMAIL_SMTP_AUTH');
// encryption
if (Config::get('EMAIL_SMTP_ENCRYPTION')) {
$mail->SMTPSecure = Config::get('EMAIL_SMTP_ENCRYPTION');
}
// set SMTP provider's credentials
$mail->Host = Config::get('EMAIL_SMTP_HOST');
$mail->Username = Config::get('EMAIL_SMTP_USERNAME');
$mail->Password = Config::get('EMAIL_SMTP_PASSWORD');
$mail->Port = Config::get('EMAIL_SMTP_PORT');
} else {
$mail->IsMail();
}
// fill mail with data
$mail->From = $from_email;
$mail->FromName = $from_name;
$mail->AddAddress($user_email);
$mail->Subject = $subject;
$mail->Body = $body;
// try to send mail
$mail->Send();
if ($mail) {
return true;
} else {
// if not successful, copy errors into Mail's error property
$this->error = $mail->ErrorInfo;
return false;
}
}
public function sendMail($user_email, $from_email, $from_name, $subject, $body)
{
if (Config::get('EMAIL_USED_MAILER') == "phpmailer") {
// returns true if successful, false if not
return $this->sendMailWithPHPMailer(
$user_email, $from_email, $from_name, $subject, $body
);
}
if (Config::get('EMAIL_USED_MAILER') == "swiftmailer") {
return $this->sendMailWithSwiftMailer();
}
if (Config::get('EMAIL_USED_MAILER') == "native") {
return $this->sendMailWithNativeMailFunction();
}
}
public function getError()
{
return $this->error;
}
}

View file

@ -0,0 +1,27 @@
<?php
/**
* Class Redirect
*
* Simple abstraction for redirecting the user to a certain page
*/
class Redirect
{
/**
* To the homepage
*/
public static function home()
{
header("location: " . Config::get('URL'));
}
/**
* To the defined page
*
* @param $path
*/
public static function to($path)
{
header("location: " . Config::get('URL') . $path);
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* This is under development. Expect changes!
* Class Request
* Abstracts the access to $_GET, $_POST and $_COOKIE, preventing direct access to these super-globals.
* This makes PHP code quality analyzer tools very happy.
* @see http://php.net/manual/en/reserved.variables.request.php
*/
class Request
{
/**
* Gets/returns the value of a specific key of the POST super-global.
* When using just Request::post('x') it will return the raw and untouched $_POST['x'], when using it like
* Request::post('x', true) then it will return a trimmed and stripped $_POST['x'] !
*
* @param mixed $key key
* @param bool $clean marker for optional cleaning of the var
* @return mixed the key's value or nothing
*/
public static function post($key, $clean = false)
{
if (isset($_POST[$key])) {
// we use the Ternary Operator here which saves the if/else block
// @see http://davidwalsh.name/php-shorthand-if-else-ternary-operators
return ($clean) ? trim(strip_tags($_POST[$key])) : $_POST[$key];
}
}
/**
* gets/returns the value of a specific key of the GET super-global
* @param mixed $key key
* @return mixed the key's value or nothing
*/
public static function get($key)
{
if (isset($_GET[$key])) {
return $_GET[$key];
}
}
/**
* gets/returns the value of a specific key of the COOKIE super-global
* @param mixed $key key
* @return mixed the key's value or nothing
*/
public static function cookie($key)
{
if (isset($_COOKIE[$key])) {
return $_COOKIE[$key];
}
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* Session class
*
* handles the session stuff. creates session when no one exists, sets and gets values, and closes the session
* properly (=logout). Not to forget the check if the user is logged in or not.
*/
class Session
{
/**
* starts the session
*/
public static function init()
{
// if no session exist, start the session
if (session_id() == '') {
session_start();
}
}
/**
* sets a specific value to a specific key of the session
*
* @param mixed $key key
* @param mixed $value value
*/
public static function set($key, $value)
{
$_SESSION[$key] = $value;
}
/**
* gets/returns the value of a specific key of the session
*
* @param mixed $key Usually a string, right ?
* @return mixed the key's value or nothing
*/
public static function get($key)
{
if (isset($_SESSION[$key])) {
return $_SESSION[$key];
}
}
/**
* adds a value as a new array element to the key.
* useful for collecting error messages etc
*
* @param mixed $key
* @param mixed $value
*/
public static function add($key, $value)
{
$_SESSION[$key][] = $value;
}
/**
* deletes the session (= logs the user out)
*/
public static function destroy()
{
session_destroy();
}
/**
* Checks if the user is logged in or not
*
* @return bool user's login status
*/
public static function userIsLoggedIn()
{
return (Session::get('user_logged_in') ? true : false);
}
}

View file

@ -0,0 +1,27 @@
<?php
class Text
{
private static $texts;
public static function get($key)
{
// if not $key
if (!$key) {
return null;
}
// load config file (this is only done once per application lifecycle)
if (!self::$texts) {
self::$texts = require('../application/config/texts.php');
}
// check if array key exists
if (!array_key_exists($key, self::$texts)) {
return null;
}
return self::$texts[$key];
}
}

View file

@ -0,0 +1,164 @@
<?php
/**
* Class View
* The part that handles all the output
*/
class View
{
/**
* simply includes (=shows) the view. this is done from the controller. In the controller, you usually say
* $this->view->render('help/index'); to show (in this example) the view index.php in the folder help.
* Usually the Class and the method are the same like the view, but sometimes you need to show different views.
* @param string $filename Path of the to-be-rendered view, usually folder/file(.php)
* @param array $data Data to be used in the view
*/
public function render($filename, $data = null)
{
if ($data) {
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
require Config::get('PATH_VIEW') . '_templates/header.php';
require Config::get('PATH_VIEW') . $filename . '.php';
require Config::get('PATH_VIEW') . '_templates/footer.php';
}
/**
* Similar to render, but accepts an array of separate views to render between the header and footer. Use like
* the following: $this->view->renderMulti(array('help/index', 'help/banner'));
* @param array $filenames Array of the paths of the to-be-rendered view, usually folder/file(.php) for each
* @param array $data Data to be used in the view
* @return bool
*/
public function renderMulti($filenames, $data = null)
{
if (!is_array($filenames)) {
self::render($filenames, $data);
return false;
}
if ($data) {
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
require Config::get('PATH_VIEW') . '_templates/header.php';
foreach($filenames as $filename) {
require Config::get('PATH_VIEW') . $filename . '.php';
}
require Config::get('PATH_VIEW') . '_templates/footer.php';
}
/**
* Same like render(), but does not include header and footer
* @param string $filename Path of the to-be-rendered view, usually folder/file(.php)
* @param mixed $data Data to be used in the view
*/
public function renderWithoutHeaderAndFooter($filename, $data = null)
{
if ($data) {
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
require Config::get('PATH_VIEW') . $filename . '.php';
}
/**
* Renders pure JSON to the browser, useful for API construction
* @param $data
*/
public function renderJSON($data)
{
echo json_encode($data);
}
/**
* renders the feedback messages into the view
*/
public function renderFeedbackMessages()
{
// echo out the feedback messages (errors and success messages etc.),
// they are in $_SESSION["feedback_positive"] and $_SESSION["feedback_negative"]
require Config::get('PATH_VIEW') . '_templates/feedback.php';
// delete these messages (as they are not needed anymore and we want to avoid to show them twice
Session::set('feedback_positive', null);
Session::set('feedback_negative', null);
}
/**
* Checks if the passed string is the currently active controller.
* Useful for handling the navigation's active/non-active link.
*
* @param string $filename
* @param string $navigation_controller
*
* @return bool Shows if the controller is used or not
*/
public static function checkForActiveController($filename, $navigation_controller)
{
$split_filename = explode("/", $filename);
$active_controller = $split_filename[0];
if ($active_controller == $navigation_controller) {
return true;
}
return false;
}
/**
* Checks if the passed string is the currently active controller-action (=method).
* Useful for handling the navigation's active/non-active link.
*
* @param string $filename
* @param string $navigation_action
*
* @return bool Shows if the action/method is used or not
*/
public static function checkForActiveAction($filename, $navigation_action)
{
$split_filename = explode("/", $filename);
$active_action = $split_filename[1];
if ($active_action == $navigation_action) {
return true;
}
return false;
}
/**
* Checks if the passed string is the currently active controller and controller-action.
* Useful for handling the navigation's active/non-active link.
*
* @param string $filename
* @param string $navigation_controller_and_action
*
* @return bool
*/
public static function checkForActiveControllerAndAction($filename, $navigation_controller_and_action)
{
$split_filename = explode("/", $filename);
$active_controller = $split_filename[0];
$active_action = $split_filename[1];
$split_filename = explode("/", $navigation_controller_and_action);
$navigation_controller = $split_filename[0];
$navigation_action = $split_filename[1];
if ($active_controller == $navigation_controller AND $active_action == $navigation_action) {
return true;
}
return false;
}
}

View file

@ -0,0 +1,254 @@
<?php
class AvatarModel
{
/**
* Gets a gravatar image link from given email address
*
* Gravatar is the #1 (free) provider for email address based global avatar hosting.
* The URL (or image) returns always a .jpg file ! For deeper info on the different parameter possibilities:
* @see http://gravatar.com/site/implement/images/
* @source http://gravatar.com/site/implement/images/php/
*
* This method will return something like http://www.gravatar.com/avatar/79e2e5b48aec07710c08d50?s=80&d=mm&r=g
* Note: the url does NOT have something like ".jpg" ! It works without.
*
* Set the configs inside the application/config/ files.
*
* @param string $email The email address
* @return string
*/
public static function getGravatarLinkByEmail($email)
{
return 'http://www.gravatar.com/avatar/' .
md5( strtolower( trim( $email ) ) ) .
'?s=' . Config::get('AVATAR_SIZE') . '&d=' . Config::get('GRAVATAR_DEFAULT_IMAGESET') . '&r=' . Config::get('GRAVATAR_RATING');
}
/**
* Gets the user's avatar file path
* @param int $user_has_avatar Marker from database
* @param int $user_id User's id
* @return string Avatar file path
*/
public static function getPublicAvatarFilePathOfUser($user_has_avatar, $user_id)
{
if ($user_has_avatar) {
return Config::get('URL') . Config::get('PATH_AVATARS_PUBLIC') . $user_id . '.jpg';
}
return Config::get('URL') . Config::get('PATH_AVATARS_PUBLIC') . Config::get('AVATAR_DEFAULT_IMAGE');
}
/**
* Gets the user's avatar file path
* @param $user_id integer The user's id
* @return string avatar picture path
*/
public static function getPublicUserAvatarFilePathByUserId($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("SELECT user_has_avatar FROM users WHERE user_id = :user_id LIMIT 1");
$query->execute(array(':user_id' => $user_id));
if ($query->fetch()->user_has_avatar) {
return Config::get('URL') . Config::get('PATH_AVATARS_PUBLIC') . $user_id . '.jpg';
}
return Config::get('URL') . Config::get('PATH_AVATARS_PUBLIC') . Config::get('AVATAR_DEFAULT_IMAGE');
}
/**
* Create an avatar picture (and checks all necessary things too)
* TODO decouple
* TODO total rebuild
*/
public static function createAvatar()
{
// check avatar folder writing rights, check if upload fits all rules
if (AvatarModel::isAvatarFolderWritable() AND AvatarModel::validateImageFile()) {
// create a jpg file in the avatar folder, write marker to database
$target_file_path = Config::get('PATH_AVATARS') . Session::get('user_id');
AvatarModel::resizeAvatarImage($_FILES['avatar_file']['tmp_name'], $target_file_path, Config::get('AVATAR_SIZE'), Config::get('AVATAR_SIZE'), Config::get('AVATAR_JPEG_QUALITY'));
AvatarModel::writeAvatarToDatabase(Session::get('user_id'));
Session::set('user_avatar_file', AvatarModel::getPublicUserAvatarFilePathByUserId(Session::get('user_id')));
Session::add('feedback_positive', Text::get('FEEDBACK_AVATAR_UPLOAD_SUCCESSFUL'));
}
}
/**
* Checks if the avatar folder exists and is writable
*
* @return bool success status
*/
public static function isAvatarFolderWritable()
{
if (is_dir(Config::get('PATH_AVATARS')) AND is_writable(Config::get('PATH_AVATARS'))) {
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_FOLDER_DOES_NOT_EXIST_OR_NOT_WRITABLE'));
return false;
}
/**
* Validates the image
* TODO totally decouple
*
* @return bool
*/
public static function validateImageFile()
{
if (!isset($_FILES['avatar_file'])) {
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_IMAGE_UPLOAD_FAILED'));
return false;
}
if ($_FILES['avatar_file']['size'] > 5000000) {
// if input file too big (>5MB)
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_UPLOAD_TOO_BIG'));
return false;
}
// get the image width, height and mime type
$image_proportions = getimagesize($_FILES['avatar_file']['tmp_name']);
// if input file too small
if ($image_proportions[0] < Config::get('AVATAR_SIZE') OR $image_proportions[1] < Config::get('AVATAR_SIZE')) {
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_UPLOAD_TOO_SMALL'));
return false;
}
if (!($image_proportions['mime'] == 'image/jpeg')) {
Session::add('feedback_negative', Text::get('FEEDBACK_AVATAR_UPLOAD_WRONG_TYPE'));
return false;
}
return true;
}
/**
* Writes marker to database, saying user has an avatar now
*
* @param $user_id
*/
public static function writeAvatarToDatabase($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_has_avatar = TRUE WHERE user_id = :user_id LIMIT 1");
$query->execute(array(':user_id' => $user_id));
}
/**
* Resize avatar image (while keeping aspect ratio and cropping it off sexy)
*
* TROUBLESHOOTING: You don't see the new image ? Press F5 or CTRL-F5 to refresh browser cache.
*
* @param string $source_image The location to the original raw image.
* @param string $destination The location to save the new image.
* @param int $final_width The desired width of the new image
* @param int $final_height The desired height of the new image.
* @param int $quality The quality of the JPG to produce 1 - 100
*
* TODO currently we just allow .jpg
*
* @return bool success state
*/
public static function resizeAvatarImage($source_image, $destination, $final_width = 44, $final_height = 44, $quality = 85)
{
list($width, $height) = getimagesize($source_image);
if (!$width || !$height) {
return false;
}
//saving the image into memory (for manipulation with GD Library)
$myImage = imagecreatefromjpeg($source_image);
// calculating the part of the image to use for thumbnail
if ($width > $height) {
$y = 0;
$x = ($width - $height) / 2;
$smallestSide = $height;
} else {
$x = 0;
$y = ($height - $width) / 2;
$smallestSide = $width;
}
// copying the part into thumbnail, maybe edit this for square avatars
$thumb = imagecreatetruecolor($final_width, $final_height);
imagecopyresampled($thumb, $myImage, 0, 0, $x, $y, $final_width, $final_height, $smallestSide, $smallestSide);
// add '.jpg' to file path, save it as a .jpg file with our $destination_filename parameter
$destination .= '.jpg';
imagejpeg($thumb, $destination, $quality);
// delete "working copy"
imagedestroy($thumb);
if (file_exists($destination)) {
return true;
}
// default return
return false;
}
/**
* Delete a user's avatar
*
* @param int $userId
* @return bool success
*/
public static function deleteAvatar($userId)
{
if (!ctype_digit($userId)) {
Session::add("feedback_negative", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_FAILED"));
return false;
}
// try to delete image, but still go on regardless of file deletion result
self::deleteAvatarImageFile($userId);
$database = DatabaseFactory::getFactory()->getConnection();
$sth = $database->prepare("UPDATE users SET user_has_avatar = 0 WHERE user_id = :user_id LIMIT 1");
$sth->bindValue(":user_id", (int)$userId, PDO::PARAM_INT);
$sth->execute();
if ($sth->rowCount() == 1) {
Session::set('user_avatar_file', self::getPublicUserAvatarFilePathByUserId($userId));
Session::add("feedback_positive", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_SUCCESSFUL"));
return true;
} else {
Session::add("feedback_negative", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_FAILED"));
return false;
}
}
/**
* Removes the avatar image file from the filesystem
*
* @param $userId
* @return bool
*/
public static function deleteAvatarImageFile($userId)
{
// Check if file exists
if (!file_exists(Config::get('PATH_AVATARS') . $userId . ".jpg")) {
Session::add("feedback_negative", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_NO_FILE"));
return false;
}
// Delete avatar file
if (!unlink(Config::get('PATH_AVATARS') . $userId . ".jpg")) {
Session::add("feedback_negative", Text::get("FEEDBACK_AVATAR_IMAGE_DELETE_FAILED"));
return false;
}
return true;
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* Class CaptchaModel
*
* This model class handles all the captcha stuff.
* Currently this uses the excellent Captcha generator lib from https://github.com/Gregwar/Captcha
* Have a look there for more options etc.
*/
class CaptchaModel
{
/**
* Generates the captcha, "returns" a real image, this is why there is header('Content-type: image/jpeg')
* Note: This is a very special method, as this is echoes out binary data.
*/
public static function generateAndShowCaptcha()
{
// create a captcha with the CaptchaBuilder lib (loaded via Composer)
$captcha = new Gregwar\Captcha\CaptchaBuilder;
$captcha->build(
Config::get('CAPTCHA_WIDTH'),
Config::get('CAPTCHA_HEIGHT')
);
// write the captcha character into session
Session::set('captcha', $captcha->getPhrase());
// render an image showing the characters (=the captcha)
header('Content-type: image/jpeg');
$captcha->output();
}
/**
* Checks if the entered captcha is the same like the one from the rendered image which has been saved in session
* @param $captcha string The captcha characters
* @return bool success of captcha check
*/
public static function checkCaptcha($captcha)
{
if ($captcha == Session::get('captcha')) {
return true;
}
return false;
}
}

View file

@ -0,0 +1,270 @@
<?php
/**
* LoginModel
*
* The login part of the model: Handles the login / logout stuff
*/
class LoginModel
{
/**
* Login process (for DEFAULT user accounts).
*
* @param $user_name string The user's name
* @param $user_password string The user's password
* @param $set_remember_me_cookie mixed Marker for usage of remember-me cookie feature
*
* @return bool success state
*/
public static function login($user_name, $user_password, $set_remember_me_cookie = null)
{
// we do negative-first checks here, for simplicity empty username and empty password in one line
if (empty($user_name) OR empty($user_password)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_OR_PASSWORD_FIELD_EMPTY'));
return false;
}
// checks if user exists, if login is not blocked (due to failed logins) and if password fits the hash
$result = self::validateAndGetUser($user_name, $user_password);
if (!$result) {
return false;
}
// reset the failed login counter for that user (if necessary)
if ($result->user_last_failed_login > 0) {
self::resetFailedLoginCounterOfUser($result->user_name);
}
// save timestamp of this login in the database line of that user
self::saveTimestampOfLoginOfUser($result->user_name);
// if user has checked the "remember me" checkbox, then write token into database and into cookie
if ($set_remember_me_cookie) {
self::setRememberMeInDatabaseAndCookie($result->user_id);
}
// successfully logged in, so we write all necessary data into the session and set "user_logged_in" to true
self::setSuccessfulLoginIntoSession(
$result->user_id, $result->user_name, $result->user_email, $result->user_account_type
);
// return true to make clear the login was successful
// maybe do this in dependence of setSuccessfulLoginIntoSession ?
return true;
}
/**
* Validates the inputs of the users, checks if password is correct etc.
* If successful, user is returned
*
* @param $user_name
* @param $user_password
*
* @return bool|mixed
*/
private static function validateAndGetUser($user_name, $user_password)
{
// get all data of that user (to later check if password and password_hash fit)
$result = UserModel::getUserDataByUsername($user_name);
// Check if that user exists. We don't give back a cause in the feedback to avoid giving an attacker details.
if (!$result) {
Session::add('feedback_negative', Text::get('FEEDBACK_LOGIN_FAILED'));
return false;
}
// block login attempt if somebody has already failed 3 times and the last login attempt is less than 30sec ago
if (($result->user_failed_logins >= 3) AND ($result->user_last_failed_login > (time() - 30))) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_WRONG_3_TIMES'));
return false;
}
// if hash of provided password does NOT match the hash in the database: +1 failed-login counter
if (!password_verify($user_password, $result->user_password_hash)) {
self::incrementFailedLoginCounterOfUser($result->user_name);
// we say "password wrong" here, but less details like "login failed" would be better (= less information)
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_WRONG'));
return false;
}
// if user is not active (= has not verified account by verification mail)
if ($result->user_active != 1) {
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_NOT_ACTIVATED_YET'));
return false;
}
return $result;
}
/**
* performs the login via cookie (for DEFAULT user account, FACEBOOK-accounts are handled differently)
* TODO add throttling here ?
*
* @param $cookie string The cookie "remember_me"
*
* @return bool success state
*/
public static function loginWithCookie($cookie)
{
if (!$cookie) {
Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
return false;
}
// check cookie's contents, check if cookie contents belong together or token is empty
list ($user_id, $token, $hash) = explode(':', $cookie);
if ($hash !== hash('sha256', $user_id . ':' . $token) OR empty($token)) {
Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
return false;
}
// get data of user that has this id and this token
$result = UserModel::getUserDataByUserIdAndToken($user_id, $token);
if ($result) {
// successfully logged in, so we write all necessary data into the session and set "user_logged_in" to true
self::setSuccessfulLoginIntoSession($result->user_id, $result->user_name, $result->user_email, $result->user_account_type);
// save timestamp of this login in the database line of that user
self::saveTimestampOfLoginOfUser($result->user_name);
Session::add('feedback_positive', Text::get('FEEDBACK_COOKIE_LOGIN_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
return false;
}
}
/**
* Log out process: delete cookie, delete session
*/
public static function logout()
{
self::deleteCookie();
Session::destroy();
}
/**
* The real login process: The user's data is written into the session.
* Cheesy name, maybe rename. Also maybe refactoring this, using an array.
*
* @param $user_id
* @param $user_name
* @param $user_email
* @param $user_account_type
*/
public static function setSuccessfulLoginIntoSession($user_id, $user_name, $user_email, $user_account_type)
{
Session::init();
Session::set('user_id', $user_id);
Session::set('user_name', $user_name);
Session::set('user_email', $user_email);
Session::set('user_account_type', $user_account_type);
Session::set('user_provider_type', 'DEFAULT');
// get and set avatars
Session::set('user_avatar_file', AvatarModel::getPublicUserAvatarFilePathByUserId($user_id));
Session::set('user_gravatar_image_url', AvatarModel::getGravatarLinkByEmail($user_email));
// finally, set user as logged-in
Session::set('user_logged_in', true);
}
/**
* Increments the failed-login counter of a user
*
* @param $user_name
*/
public static function incrementFailedLoginCounterOfUser($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users
SET user_failed_logins = user_failed_logins+1, user_last_failed_login = :user_last_failed_login
WHERE user_name = :user_name OR user_email = :user_name
LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_name' => $user_name, ':user_last_failed_login' => time() ));
}
/**
* Resets the failed-login counter of a user back to 0
*
* @param $user_name
*/
public static function resetFailedLoginCounterOfUser($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users
SET user_failed_logins = 0, user_last_failed_login = NULL
WHERE user_name = :user_name AND user_failed_logins != 0
LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_name' => $user_name));
}
/**
* Write timestamp of this login into database (we only write a "real" login via login form into the database,
* not the session-login on every page request
*
* @param $user_name
*/
public static function saveTimestampOfLoginOfUser($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET user_last_login_timestamp = :user_last_login_timestamp
WHERE user_name = :user_name LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_name' => $user_name, ':user_last_login_timestamp' => time()));
}
/**
* Write remember-me token into database and into cookie
* Maybe splitting this into database and cookie part ?
*
* @param $user_id
*/
public static function setRememberMeInDatabaseAndCookie($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
// generate 64 char random string
$random_token_string = hash('sha256', mt_rand());
// write that token into database
$sql = "UPDATE users SET user_remember_me_token = :user_remember_me_token WHERE user_id = :user_id LIMIT 1";
$sth = $database->prepare($sql);
$sth->execute(array(':user_remember_me_token' => $random_token_string, ':user_id' => $user_id));
// generate cookie string that consists of user id, random string and combined hash of both
$cookie_string_first_part = $user_id . ':' . $random_token_string;
$cookie_string_hash = hash('sha256', $cookie_string_first_part);
$cookie_string = $cookie_string_first_part . ':' . $cookie_string_hash;
// set cookie
setcookie('remember_me', $cookie_string, time() + Config::get('COOKIE_RUNTIME'), Config::get('COOKIE_PATH'));
}
/**
* Deletes the cookie
* It's necessary to split deleteCookie() and logout() as cookies are deleted without logging out too!
* Sets the remember-me-cookie to ten years ago (3600sec * 24 hours * 365 days * 10).
* that's obviously the best practice to kill a cookie @see http://stackoverflow.com/a/686166/1114320
*/
public static function deleteCookie()
{
setcookie('remember_me', false, time() - (3600 * 24 * 3650), Config::get('COOKIE_PATH'));
}
/**
* Returns the current state of the user's login
*
* @return bool user's login status
*/
public static function isUserLoggedIn()
{
return Session::userIsLoggedIn();
}
}

View file

@ -0,0 +1,120 @@
<?php
/**
* NoteModel
* This is basically a simple CRUD (Create/Read/Update/Delete) demonstration.
*/
class NoteModel
{
/**
* Get all notes (notes are just example data that the user has created)
* @return array an array with several objects (the results)
*/
public static function getAllNotes()
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, note_id, note_text FROM notes WHERE user_id = :user_id";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => Session::get('user_id')));
// fetchAll() is the PDO method that gets all result rows
return $query->fetchAll();
}
/**
* Get a single note
* @param int $note_id id of the specific note
* @return object a single object (the result)
*/
public static function getNote($note_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, note_id, note_text FROM notes WHERE user_id = :user_id AND note_id = :note_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => Session::get('user_id'), ':note_id' => $note_id));
// fetch() is the PDO method that gets a single result
return $query->fetch();
}
/**
* Set a note (create a new one)
* @param string $note_text note text that will be created
* @return bool feedback (was the note created properly ?)
*/
public static function createNote($note_text)
{
if (!$note_text || strlen($note_text) == 0) {
Session::add('feedback_negative', Text::get('FEEDBACK_NOTE_CREATION_FAILED'));
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "INSERT INTO notes (note_text, user_id) VALUES (:note_text, :user_id)";
$query = $database->prepare($sql);
$query->execute(array(':note_text' => $note_text, ':user_id' => Session::get('user_id')));
if ($query->rowCount() == 1) {
return true;
}
// default return
Session::add('feedback_negative', Text::get('FEEDBACK_NOTE_CREATION_FAILED'));
return false;
}
/**
* Update an existing note
* @param int $note_id id of the specific note
* @param string $note_text new text of the specific note
* @return bool feedback (was the update successful ?)
*/
public static function updateNote($note_id, $note_text)
{
if (!$note_id || !$note_text) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE notes SET note_text = :note_text WHERE note_id = :note_id AND user_id = :user_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':note_id' => $note_id, ':note_text' => $note_text, ':user_id' => Session::get('user_id')));
if ($query->rowCount() == 1) {
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_NOTE_EDITING_FAILED'));
return false;
}
/**
* Delete a specific note
* @param int $note_id id of the note
* @return bool feedback (was the note deleted properly ?)
*/
public static function deleteNote($note_id)
{
if (!$note_id) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "DELETE FROM notes WHERE note_id = :note_id AND user_id = :user_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':note_id' => $note_id, ':user_id' => Session::get('user_id')));
if ($query->rowCount() == 1) {
return true;
}
// default return
Session::add('feedback_negative', Text::get('FEEDBACK_NOTE_DELETION_FAILED'));
return false;
}
}

View file

@ -0,0 +1,251 @@
<?php
/**
* Class PasswordResetModel
*
* Handles all the stuff that is related to the password-reset process
*/
class PasswordResetModel
{
/**
* Perform the necessary actions to send a password reset mail
*
* @param $user_name_or_email string Username or user's email
*
* @return bool success status
*/
public static function requestPasswordReset($user_name_or_email)
{
if (empty($user_name_or_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_EMAIL_FIELD_EMPTY'));
return false;
}
// check if that username exists
$result = UserModel::getUserDataByUserNameOrEmail($user_name_or_email);
if (!$result) {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_DOES_NOT_EXIST'));
return false;
}
// generate integer-timestamp (to see when exactly the user (or an attacker) requested the password reset mail)
// generate random hash for email password reset verification (40 char string)
$temporary_timestamp = time();
$user_password_reset_hash = sha1(uniqid(mt_rand(), true));
// set token (= a random hash string and a timestamp) into database ...
$token_set = PasswordResetModel::setPasswordResetDatabaseToken($result->user_name, $user_password_reset_hash, $temporary_timestamp);
if (!$token_set) {
return false;
}
// ... and send a mail to the user, containing a link with username and token hash string
$mail_sent = PasswordResetModel::sendPasswordResetMail($result->user_name, $user_password_reset_hash, $result->user_email);
if ($mail_sent) {
return true;
}
// default return
return false;
}
/**
* Set password reset token in database (for DEFAULT user accounts)
*
* @param string $user_name username
* @param string $user_password_reset_hash password reset hash
* @param int $temporary_timestamp timestamp
*
* @return bool success status
*/
public static function setPasswordResetDatabaseToken($user_name, $user_password_reset_hash, $temporary_timestamp)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users
SET user_password_reset_hash = :user_password_reset_hash, user_password_reset_timestamp = :user_password_reset_timestamp
WHERE user_name = :user_name AND user_provider_type = :provider_type LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(
':user_password_reset_hash' => $user_password_reset_hash, ':user_name' => $user_name,
':user_password_reset_timestamp' => $temporary_timestamp, ':provider_type' => 'DEFAULT'
));
// check if exactly one row was successfully changed
if ($query->rowCount() == 1) {
return true;
}
// fallback
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_TOKEN_FAIL'));
return false;
}
/**
* Send the password reset mail
*
* @param string $user_name username
* @param string $user_password_reset_hash password reset hash
* @param string $user_email user email
*
* @return bool success status
*/
public static function sendPasswordResetMail($user_name, $user_password_reset_hash, $user_email)
{
// create email body
$body = Config::get('EMAIL_PASSWORD_RESET_CONTENT') . ' ' . Config::get('URL') .
Config::get('EMAIL_PASSWORD_RESET_URL') . '/' . urlencode($user_name) . '/' . urlencode($user_password_reset_hash);
// create instance of Mail class, try sending and check
$mail = new Mail;
$mail_sent = $mail->sendMail($user_email, Config::get('EMAIL_PASSWORD_RESET_FROM_EMAIL'),
Config::get('EMAIL_PASSWORD_RESET_FROM_NAME'), Config::get('EMAIL_PASSWORD_RESET_SUBJECT'), $body
);
if ($mail_sent) {
Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_RESET_MAIL_SENDING_SUCCESSFUL'));
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_MAIL_SENDING_ERROR') . $mail->getError() );
return false;
}
/**
* Verifies the password reset request via the verification hash token (that's only valid for one hour)
* @param string $user_name Username
* @param string $verification_code Hash token
* @return bool Success status
*/
public static function verifyPasswordReset($user_name, $verification_code)
{
$database = DatabaseFactory::getFactory()->getConnection();
// check if user-provided username + verification code combination exists
$sql = "SELECT user_id, user_password_reset_timestamp
FROM users
WHERE user_name = :user_name
AND user_password_reset_hash = :user_password_reset_hash
AND user_provider_type = :user_provider_type
LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(
':user_password_reset_hash' => $verification_code, ':user_name' => $user_name,
':user_provider_type' => 'DEFAULT'
));
// if this user with exactly this verification hash code does NOT exist
if ($query->rowCount() != 1) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_COMBINATION_DOES_NOT_EXIST'));
return false;
}
// get result row (as an object)
$result_user_row = $query->fetch();
// 3600 seconds are 1 hour
$timestamp_one_hour_ago = time() - 3600;
// if password reset request was sent within the last hour (this timeout is for security reasons)
if ($result_user_row->user_password_reset_timestamp > $timestamp_one_hour_ago) {
// verification was successful
Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_RESET_LINK_VALID'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_LINK_EXPIRED'));
return false;
}
}
/**
* Writes the new password to the database
*
* @param string $user_name username
* @param string $user_password_hash
* @param string $user_password_reset_hash
*
* @return bool
*/
public static function saveNewUserPassword($user_name, $user_password_hash, $user_password_reset_hash)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET user_password_hash = :user_password_hash, user_password_reset_hash = NULL,
user_password_reset_timestamp = NULL
WHERE user_name = :user_name AND user_password_reset_hash = :user_password_reset_hash
AND user_provider_type = :user_provider_type LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(
':user_password_hash' => $user_password_hash, ':user_name' => $user_name,
':user_password_reset_hash' => $user_password_reset_hash, ':user_provider_type' => 'DEFAULT'
));
// if one result exists, return true, else false. Could be written even shorter btw.
return ($query->rowCount() == 1 ? true : false);
}
/**
* Set the new password (for DEFAULT user, FACEBOOK-users don't have a password)
* Please note: At this point the user has already pre-verified via verifyPasswordReset() (within one hour),
* so we don't need to check again for the 60min-limit here. In this method we authenticate
* via username & password-reset-hash from (hidden) form fields.
*
* @param string $user_name
* @param string $user_password_reset_hash
* @param string $user_password_new
* @param string $user_password_repeat
*
* @return bool success state of the password reset
*/
public static function setNewPassword($user_name, $user_password_reset_hash, $user_password_new, $user_password_repeat)
{
// validate the password
if (!self::validateNewPassword($user_name, $user_password_reset_hash, $user_password_new, $user_password_repeat)) {
return false;
}
// crypt the password (with the PHP 5.5+'s password_hash() function, result is a 60 character hash string)
$user_password_hash = password_hash($user_password_new, PASSWORD_DEFAULT);
// write the password to database (as hashed and salted string), reset user_password_reset_hash
if (PasswordResetModel::saveNewUserPassword($user_name, $user_password_hash, $user_password_reset_hash)) {
Session::add('feedback_positive', Text::get('FEEDBACK_PASSWORD_CHANGE_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_CHANGE_FAILED'));
return false;
}
}
/**
* Validate the password submission
*
* @param $user_name
* @param $user_password_reset_hash
* @param $user_password_new
* @param $user_password_repeat
*
* @return bool
*/
public static function validateNewPassword($user_name, $user_password_reset_hash, $user_password_new, $user_password_repeat)
{
if (empty($user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_FIELD_EMPTY'));
return false;
} else if (empty($user_password_reset_hash)) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_TOKEN_MISSING'));
return false;
} else if (empty($user_password_new) || empty($user_password_repeat)) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_FIELD_EMPTY'));
return false;
} else if ($user_password_new !== $user_password_repeat) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_REPEAT_WRONG'));
return false;
} else if (strlen($user_password_new) < 6) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_TOO_SHORT'));
return false;
}
return true;
}
}

View file

@ -0,0 +1,278 @@
<?php
/**
* Class RegistrationModel
*
* Everything registration-related happens here.
*/
class RegistrationModel
{
/**
* Handles the entire registration process for DEFAULT users (not for people who register with
* 3rd party services, like facebook) and creates a new user in the database if everything is fine
*
* @return boolean Gives back the success status of the registration
*/
public static function registerNewUser()
{
// TODO this could be written simpler and cleaner
// clean the input
$user_name = strip_tags(Request::post('user_name'));
$user_email = strip_tags(Request::post('user_email'));
$user_password_new = Request::post('user_password_new');
$user_password_repeat = Request::post('user_password_repeat');
// stop registration flow if registrationInputValidation() returns false (= anything breaks the input check rules)
$validation_result = RegistrationModel::registrationInputValidation(Request::post('captcha'), $user_name, $user_password_new, $user_password_repeat, $user_email);
if (!$validation_result) {
return false;
}
// crypt the password with the PHP 5.5's password_hash() function, results in a 60 character hash string.
// @see php.net/manual/en/function.password-hash.php for more, especially for potential options
$user_password_hash = password_hash($user_password_new, PASSWORD_DEFAULT);
// check if username already exists
if (UserModel::doesUsernameAlreadyExist($user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_ALREADY_TAKEN'));
return false;
}
// check if email already exists
if (UserModel::doesEmailAlreadyExist($user_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_EMAIL_ALREADY_TAKEN'));
return false;
}
// generate random hash for email verification (40 char string)
$user_activation_hash = sha1(uniqid(mt_rand(), true));
// write user data to database
if (!RegistrationModel::writeNewUserToDatabase($user_name, $user_password_hash, $user_email, time(), $user_activation_hash)) {
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_CREATION_FAILED'));
}
// get user_id of the user that has been created, to keep things clean we DON'T use lastInsertId() here
$user_id = UserModel::getUserIdByUsername($user_name);
if (!$user_id) {
Session::add('feedback_negative', Text::get('FEEDBACK_UNKNOWN_ERROR'));
return false;
}
// send verification email
if (RegistrationModel::sendVerificationEmail($user_id, $user_email, $user_activation_hash)) {
Session::add('feedback_positive', Text::get('FEEDBACK_ACCOUNT_SUCCESSFULLY_CREATED'));
return true;
}
// if verification email sending failed: instantly delete the user
RegistrationModel::rollbackRegistrationByUserId($user_id);
Session::add('feedback_negative', Text::get('FEEDBACK_VERIFICATION_MAIL_SENDING_FAILED'));
return false;
}
/**
* Validates the registration input
*
* @param $captcha
* @param $user_name
* @param $user_password_new
* @param $user_password_repeat
* @param $user_email
*
* @return bool
*/
public static function registrationInputValidation($captcha, $user_name, $user_password_new, $user_password_repeat, $user_email)
{
// perform all necessary checks
if (!CaptchaModel::checkCaptcha($captcha)) {
Session::add('feedback_negative', Text::get('FEEDBACK_CAPTCHA_WRONG'));
return false;
}
// if username, email and password are all correctly validated
if (self::validateUserName($user_name) AND self::validateUserEmail($user_email) AND self::validateUserPassword($user_password_new, $user_password_repeat)) {
return true;
}
// otherwise, return false
return false;
}
/**
* Validates the username
*
* @param $user_name
* @return bool
*/
public static function validateUserName($user_name)
{
if (empty($user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_FIELD_EMPTY'));
return false;
}
// if username is too short (2), too long (64) or does not fit the pattern (aZ09)
if (!preg_match('/^[a-zA-Z0-9]{2,64}$/', $user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_DOES_NOT_FIT_PATTERN'));
return false;
}
return true;
}
/**
* Validates the email
*
* @param $user_email
* @return bool
*/
public static function validateUserEmail($user_email)
{
if (empty($user_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_FIELD_EMPTY'));
return false;
}
// validate the email with PHP's internal filter
// side-fact: Max length seems to be 254 chars
// @see http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
if (!filter_var($user_email, FILTER_VALIDATE_EMAIL)) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_DOES_NOT_FIT_PATTERN'));
return false;
}
return true;
}
/**
* Validates the password
*
* @param $user_password_new
* @param $user_password_repeat
* @return bool
*/
public static function validateUserPassword($user_password_new, $user_password_repeat)
{
if (empty($user_password_new) OR empty($user_password_repeat)) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_FIELD_EMPTY'));
return false;
}
if ($user_password_new !== $user_password_repeat) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_REPEAT_WRONG'));
return false;
}
if (strlen($user_password_new) < 6) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_TOO_SHORT'));
return false;
}
return true;
}
/**
* Writes the new user's data to the database
*
* @param $user_name
* @param $user_password_hash
* @param $user_email
* @param $user_creation_timestamp
* @param $user_activation_hash
*
* @return bool
*/
public static function writeNewUserToDatabase($user_name, $user_password_hash, $user_email, $user_creation_timestamp, $user_activation_hash)
{
$database = DatabaseFactory::getFactory()->getConnection();
// write new users data into database
$sql = "INSERT INTO users (user_name, user_password_hash, user_email, user_creation_timestamp, user_activation_hash, user_provider_type)
VALUES (:user_name, :user_password_hash, :user_email, :user_creation_timestamp, :user_activation_hash, :user_provider_type)";
$query = $database->prepare($sql);
$query->execute(array(':user_name' => $user_name,
':user_password_hash' => $user_password_hash,
':user_email' => $user_email,
':user_creation_timestamp' => $user_creation_timestamp,
':user_activation_hash' => $user_activation_hash,
':user_provider_type' => 'DEFAULT'));
$count = $query->rowCount();
if ($count == 1) {
return true;
}
return false;
}
/**
* Deletes the user from users table. Currently used to rollback a registration when verification mail sending
* was not successful.
*
* @param $user_id
*/
public static function rollbackRegistrationByUserId($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("DELETE FROM users WHERE user_id = :user_id");
$query->execute(array(':user_id' => $user_id));
}
/**
* Sends the verification email (to confirm the account).
* The construction of the mail $body looks weird at first, but it's really just a simple string.
*
* @param int $user_id user's id
* @param string $user_email user's email
* @param string $user_activation_hash user's mail verification hash string
*
* @return boolean gives back true if mail has been sent, gives back false if no mail could been sent
*/
public static function sendVerificationEmail($user_id, $user_email, $user_activation_hash)
{
$body = Config::get('EMAIL_VERIFICATION_CONTENT') . Config::get('URL') . Config::get('EMAIL_VERIFICATION_URL')
. '/' . urlencode($user_id) . '/' . urlencode($user_activation_hash);
$mail = new Mail;
$mail_sent = $mail->sendMail($user_email, Config::get('EMAIL_VERIFICATION_FROM_EMAIL'),
Config::get('EMAIL_VERIFICATION_FROM_NAME'), Config::get('EMAIL_VERIFICATION_SUBJECT'), $body
);
if ($mail_sent) {
Session::add('feedback_positive', Text::get('FEEDBACK_VERIFICATION_MAIL_SENDING_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_VERIFICATION_MAIL_SENDING_ERROR') . $mail->getError() );
return false;
}
}
/**
* checks the email/verification code combination and set the user's activation status to true in the database
*
* @param int $user_id user id
* @param string $user_activation_verification_code verification token
*
* @return bool success status
*/
public static function verifyNewUser($user_id, $user_activation_verification_code)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "UPDATE users SET user_active = 1, user_activation_hash = NULL
WHERE user_id = :user_id AND user_activation_hash = :user_activation_hash LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => $user_id, ':user_activation_hash' => $user_activation_verification_code));
if ($query->rowCount() == 1) {
Session::add('feedback_positive', Text::get('FEEDBACK_ACCOUNT_ACTIVATION_SUCCESSFUL'));
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_ACTIVATION_FAILED'));
return false;
}
}

View file

@ -0,0 +1,331 @@
<?php
/**
* UserModel
* Handles all the PUBLIC profile stuff. This is not for getting data of the logged in user, it's more for handling
* data of all the other users. Useful for display profile information, creating user lists etc.
*/
class UserModel
{
/**
* Gets an array that contains all the users in the database. The array's keys are the user ids.
* Each array element is an object, containing a specific user's data.
* The avatar line is built using Ternary Operators, have a look here for more:
* @see http://davidwalsh.name/php-shorthand-if-else-ternary-operators
*
* @return array The profiles of all users
*/
public static function getPublicProfilesOfAllUsers()
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, user_name, user_email, user_active, user_has_avatar FROM users";
$query = $database->prepare($sql);
$query->execute();
$all_users_profiles = array();
foreach ($query->fetchAll() as $user) {
$all_users_profiles[$user->user_id] = new stdClass();
$all_users_profiles[$user->user_id]->user_id = $user->user_id;
$all_users_profiles[$user->user_id]->user_name = $user->user_name;
$all_users_profiles[$user->user_id]->user_email = $user->user_email;
$all_users_profiles[$user->user_id]->user_active = $user->user_active;
$all_users_profiles[$user->user_id]->user_avatar_link = (Config::get('USE_GRAVATAR') ? AvatarModel::getGravatarLinkByEmail($user->user_email) : AvatarModel::getPublicAvatarFilePathOfUser($user->user_has_avatar, $user->user_id));
}
return $all_users_profiles;
}
/**
* Gets a user's profile data, according to the given $user_id
* @param int $user_id The user's id
* @return mixed The selected user's profile
*/
public static function getPublicProfileOfUser($user_id)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, user_name, user_email, user_active, user_has_avatar
FROM users WHERE user_id = :user_id LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(':user_id' => $user_id));
$user = $query->fetch();
if ($query->rowCount() == 1) {
if (Config::get('USE_GRAVATAR')) {
$user->user_avatar_link = AvatarModel::getGravatarLinkByEmail($user->user_email);
} else {
$user->user_avatar_link = AvatarModel::getPublicAvatarFilePathOfUser($user->user_has_avatar, $user->user_id);
}
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_DOES_NOT_EXIST'));
}
return $user;
}
/**
* @param $user_name_or_email
*
* @return mixed
*/
public static function getUserDataByUserNameOrEmail($user_name_or_email)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("SELECT user_id, user_name, user_email FROM users
WHERE (user_name = :user_name_or_email OR user_email = :user_name_or_email)
AND user_provider_type = :provider_type LIMIT 1");
$query->execute(array(':user_name_or_email' => $user_name_or_email, ':provider_type' => 'DEFAULT'));
return $query->fetch();
}
/**
* Checks if a username is already taken
*
* @param $user_name string username
*
* @return bool
*/
public static function doesUsernameAlreadyExist($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("SELECT user_id FROM users WHERE user_name = :user_name LIMIT 1");
$query->execute(array(':user_name' => $user_name));
if ($query->rowCount() == 0) {
return false;
}
return true;
}
/**
* Checks if a email is already used
*
* @param $user_email string email
*
* @return bool
*/
public static function doesEmailAlreadyExist($user_email)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("SELECT user_id FROM users WHERE user_email = :user_email LIMIT 1");
$query->execute(array(':user_email' => $user_email));
if ($query->rowCount() == 0) {
return false;
}
return true;
}
/**
* Writes new username to database
*
* @param $user_id int user id
* @param $new_user_name string new username
*
* @return bool
*/
public static function saveNewUserName($user_id, $new_user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_name = :user_name WHERE user_id = :user_id LIMIT 1");
$query->execute(array(':user_name' => $new_user_name, ':user_id' => $user_id));
if ($query->rowCount() == 1) {
return true;
}
return false;
}
/**
* Writes new email address to database
*
* @param $user_id int user id
* @param $new_user_email string new email address
*
* @return bool
*/
public static function saveNewEmailAddress($user_id, $new_user_email)
{
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_email = :user_email WHERE user_id = :user_id LIMIT 1");
$query->execute(array(':user_email' => $new_user_email, ':user_id' => $user_id));
$count = $query->rowCount();
if ($count == 1) {
return true;
}
return false;
}
/**
* Edit the user's name, provided in the editing form
*
* @param $new_user_name string The new username
*
* @return bool success status
*/
public static function editUserName($new_user_name)
{
// new username same as old one ?
if ($new_user_name == Session::get('user_name')) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_SAME_AS_OLD_ONE'));
return false;
}
// username cannot be empty and must be azAZ09 and 2-64 characters
if (!preg_match("/^[a-zA-Z0-9]{2,64}$/", $new_user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_DOES_NOT_FIT_PATTERN'));
return false;
}
// clean the input, strip usernames longer than 64 chars (maybe fix this ?)
$new_user_name = substr(strip_tags($new_user_name), 0, 64);
// check if new username already exists
if (UserModel::doesUsernameAlreadyExist($new_user_name)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_ALREADY_TAKEN'));
return false;
}
$status_of_action = UserModel::saveNewUserName(Session::get('user_id'), $new_user_name);
if ($status_of_action) {
Session::set('user_name', $new_user_name);
Session::add('feedback_positive', Text::get('FEEDBACK_USERNAME_CHANGE_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_UNKNOWN_ERROR'));
return false;
}
}
/**
* Edit the user's email
*
* @param $new_user_email
*
* @return bool success status
*/
public static function editUserEmail($new_user_email)
{
// email provided ?
if (empty($new_user_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_FIELD_EMPTY'));
return false;
}
// check if new email is same like the old one
if ($new_user_email == Session::get('user_email')) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_SAME_AS_OLD_ONE'));
return false;
}
// user's email must be in valid email format, also checks the length
// @see http://stackoverflow.com/questions/21631366/php-filter-validate-email-max-length
// @see http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
if (!filter_var($new_user_email, FILTER_VALIDATE_EMAIL)) {
Session::add('feedback_negative', Text::get('FEEDBACK_EMAIL_DOES_NOT_FIT_PATTERN'));
return false;
}
// strip tags, just to be sure
$new_user_email = substr(strip_tags($new_user_email), 0, 254);
// check if user's email already exists
if (UserModel::doesEmailAlreadyExist($new_user_email)) {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_EMAIL_ALREADY_TAKEN'));
return false;
}
// write to database, if successful ...
// ... then write new email to session, Gravatar too (as this relies to the user's email address)
if (UserModel::saveNewEmailAddress(Session::get('user_id'), $new_user_email)) {
Session::set('user_email', $new_user_email);
Session::set('user_gravatar_image_url', AvatarModel::getGravatarLinkByEmail($new_user_email));
Session::add('feedback_positive', Text::get('FEEDBACK_EMAIL_CHANGE_SUCCESSFUL'));
return true;
}
Session::add('feedback_negative', Text::get('FEEDBACK_UNKNOWN_ERROR'));
return false;
}
/**
* Gets the user's id
*
* @param $user_name
*
* @return mixed
*/
public static function getUserIdByUsername($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id FROM users WHERE user_name = :user_name AND user_provider_type = :provider_type LIMIT 1";
$query = $database->prepare($sql);
// DEFAULT is the marker for "normal" accounts (that have a password etc.)
// There are other types of accounts that don't have passwords etc. (FACEBOOK)
$query->execute(array(':user_name' => $user_name, ':provider_type' => 'DEFAULT'));
// return one row (we only have one result or nothing)
return $query->fetch()->user_id;
}
/**
* Gets the user's data
*
* @param $user_name string User's name
*
* @return mixed Returns false if user does not exist, returns object with user's data when user exists
*/
public static function getUserDataByUsername($user_name)
{
$database = DatabaseFactory::getFactory()->getConnection();
$sql = "SELECT user_id, user_name, user_email, user_password_hash, user_active, user_account_type,
user_failed_logins, user_last_failed_login
FROM users
WHERE (user_name = :user_name OR user_email = :user_name)
AND user_provider_type = :provider_type
LIMIT 1";
$query = $database->prepare($sql);
// DEFAULT is the marker for "normal" accounts (that have a password etc.)
// There are other types of accounts that don't have passwords etc. (FACEBOOK)
$query->execute(array(':user_name' => $user_name, ':provider_type' => 'DEFAULT'));
// return one row (we only have one result or nothing)
return $query->fetch();
}
/**
* Gets the user's data by user's id and a token (used by login-via-cookie process)
*
* @param $user_id
* @param $token
*
* @return mixed Returns false if user does not exist, returns object with user's data when user exists
*/
public static function getUserDataByUserIdAndToken($user_id, $token)
{
$database = DatabaseFactory::getFactory()->getConnection();
// get real token from database (and all other data)
$query = $database->prepare("SELECT user_id, user_name, user_email, user_password_hash, user_active,
user_account_type, user_has_avatar, user_failed_logins, user_last_failed_login
FROM users
WHERE user_id = :user_id
AND user_remember_me_token = :user_remember_me_token
AND user_remember_me_token IS NOT NULL
AND user_provider_type = :provider_type LIMIT 1");
$query->execute(array(':user_id' => $user_id, ':user_remember_me_token' => $token, ':provider_type' => 'DEFAULT'));
// return one row (we only have one result or nothing)
return $query->fetch();
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* Class UserRoleModel
*
* This class contains everything that is related to up- and downgrading accounts.
*/
class UserRoleModel
{
/**
* Upgrades / downgrades the user's account. Currently it's just the field user_account_type in the database that
* can be 1 or 2 (maybe "basic" or "premium"). Put some more complex stuff in here, maybe a pay-process or whatever
* you like.
*
* @param $type
*
* @return bool
*/
public static function changeUserRole($type)
{
if (!$type) {
return false;
}
// save new role to database
if (self::saveRoleToDatabase($type)) {
Session::add('feedback_positive', Text::get('FEEDBACK_ACCOUNT_TYPE_CHANGE_SUCCESSFUL'));
return true;
} else {
Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_TYPE_CHANGE_FAILED'));
return false;
}
}
/**
* Writes the new account type marker to the database and to the session
*
* @param $type
*
* @return bool
*/
public static function saveRoleToDatabase($type)
{
// if $type is not 1 or 2
if (!in_array($type, [1, 2])) {
return false;
}
$database = DatabaseFactory::getFactory()->getConnection();
$query = $database->prepare("UPDATE users SET user_account_type = :new_type WHERE user_id = :user_id LIMIT 1");
$query->execute(array(
':new_type' => $type,
':user_id' => Session::get('user_id')
));
if ($query->rowCount() == 1) {
// set account type in session
Session::set('user_account_type', $type);
return true;
}
return false;
}
}

View file

@ -0,0 +1,19 @@
<?php
// get the feedback (they are arrays, to make multiple positive/negative messages possible)
$feedback_positive = Session::get('feedback_positive');
$feedback_negative = Session::get('feedback_negative');
// echo out positive messages
if (isset($feedback_positive)) {
foreach ($feedback_positive as $feedback) {
echo '<div class="feedback success">'.$feedback.'</div>';
}
}
// echo out negative messages
if (isset($feedback_negative)) {
foreach ($feedback_negative as $feedback) {
echo '<div class="feedback error">'.$feedback.'</div>';
}
}

View file

@ -0,0 +1,7 @@
<div class="footer"></div>
</div><!-- close class="wrapper" -->
<!-- the support button on the top right -->
<a class="support-button" href="https://affiliates.a2hosting.com/idevaffiliate.php?id=4471&url=579" target="_blank"></a>
</body>
</html>

View file

@ -0,0 +1,66 @@
<!doctype html>
<html>
<head>
<!-- META -->
<meta charset="utf-8">
<!-- CSS -->
<link rel="stylesheet" href="<?php echo Config::get('URL'); ?>css/style.css" />
</head>
<body>
<!-- wrapper, to center website -->
<div class="wrapper">
<!-- logo -->
<div class="logo"></div>
<!-- navigation -->
<ul class="navigation">
<li <?php if (View::checkForActiveController($filename, "index")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>index/index">Index</a>
</li>
<li <?php if (View::checkForActiveController($filename, "overview")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>profile/index">Profiles</a>
</li>
<?php if (Session::userIsLoggedIn()) { ?>
<li <?php if (View::checkForActiveController($filename, "dashboard")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>dashboard/index">Dashboard</a>
</li>
<li <?php if (View::checkForActiveController($filename, "note")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>note/index">My Notes</a>
</li>
<?php } else { ?>
<!-- for not logged in users -->
<li <?php if (View::checkForActiveControllerAndAction($filename, "login/index")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>login/index">Login</a>
</li>
<li <?php if (View::checkForActiveControllerAndAction($filename, "login/register")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>login/register">Register</a>
</li>
<?php } ?>
</ul>
<!-- my account -->
<ul class="navigation right">
<?php if (Session::userIsLoggedIn()) : ?>
<li <?php if (View::checkForActiveController($filename, "login")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>login/showprofile">My Account</a>
<ul class="navigation-submenu">
<li <?php if (View::checkForActiveController($filename, "login")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>login/changeUserRole">Change account type</a>
</li>
<li <?php if (View::checkForActiveController($filename, "login")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>login/editAvatar">Edit your avatar</a>
</li>
<li <?php if (View::checkForActiveController($filename, "login")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>login/editusername">Edit my username</a>
</li>
<li <?php if (View::checkForActiveController($filename, "login")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>login/edituseremail">Edit my email</a>
</li>
<li <?php if (View::checkForActiveController($filename, "login")) { echo ' class="active" '; } ?> >
<a href="<?php echo Config::get('URL'); ?>login/logout">Logout</a>
</li>
</ul>
</li>
<?php endif; ?>
</ul>

View file

@ -0,0 +1,15 @@
<div class="container">
<h1>DashboardController/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<p>
This is an area that's only visible for logged in users. Try to log out, an go to /dashboard/ again. You'll
be redirected to /index/ as you are not logged in. You can protect a whole section in your app within the
according controller by placing <i>Auth::handleLogin();</i> into the constructor.
<p>
</div>
</div>

View file

@ -0,0 +1,6 @@
<div class="container">
<h1>Page not found</h1>
<div class="box">
<p class="red-text">This page does not exist.</p>
</div>
</div>

View file

@ -0,0 +1,18 @@
<div class="container">
<h1>IndexController/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<p>
This is the homepage. As no real URL-route (like /login/register) is provided, the app uses the default
controller and the default action, defined in application/config/config.php, by default it's
IndexController and index()-method. So, the app will load application/controller/IndexController.php and
run index() from that file. Easy. That index()-method (= the action) has just one line of code inside
($this->view->render('index/index');) that loads application/view/index/index.php, which is basically
this text you are reading right now.
</p>
</div>
</div>

View file

@ -0,0 +1,27 @@
<div class="container">
<h1>LoginController/changePassword</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Set new password</h2>
<p>FYI: ... Idenfitication process works via password-reset-token (hidden input field)</p>
<!-- new password form box -->
<form method="post" action="<?php echo Config::get('URL'); ?>login/setNewPassword" name="new_password_form">
<input type='hidden' name='user_name' value='<?php echo $this->user_name; ?>' />
<input type='hidden' name='user_password_reset_hash' value='<?php echo $this->user_password_reset_hash; ?>' />
<label for="reset_input_password_new">New password (min. 6 characters)</label>
<input id="reset_input_password_new" class="reset_input" type="password"
name="user_password_new" pattern=".{6,}" required autocomplete="off" />
<label for="reset_input_password_repeat">Repeat new password</label>
<input id="reset_input_password_repeat" class="reset_input" type="password"
name="user_password_repeat" pattern=".{6,}" required autocomplete="off" />
<input type="submit" name="submit_new_password" value="Submit new password" />
</form>
<a href="<?php echo Config::get('URL'); ?>login/index">Back to Login Page</a>
</div>
</div>

View file

@ -0,0 +1,31 @@
<div class="container">
<h1>LoginController/changeUserRole</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Change account type</h2>
<p>
This page is a basic implementation of the upgrade-process.
User can click on that button to upgrade their accounts from
"basic account" to "premium account". This script simple offers
a click-able button that will upgrade/downgrade the account instantly.
In a real world application you would implement something like a
pay-process.
</p>
<p>
Please note: This whole process has been renamed from AccountType (v3.0) to UserRole (v3.1).
</p>
<h2>Currently your account type is: <?php echo Session::get('user_account_type'); ?></h2>
<!-- basic implementation for two account types: type 1 and type 2 -->
<form action="<?php echo Config::get('URL'); ?>login/changeUserRole_action" method="post">
<?php if (Session::get('user_account_type') == 1) { ?>
<input type="submit" name="user_account_upgrade" value="Upgrade my account (to Premium User)" />
<?php } else if (Session::get('user_account_type') == 2) { ?>
<input type="submit" name="user_account_downgrade" value="Downgrade my account (to Basic User)" />
<?php } ?>
</form>
</div>
</div>

View file

@ -0,0 +1,28 @@
<div class="container">
<h1>Edit your avatar</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h3>Upload an Avatar</h3>
<div class="feedback info">
If you still see the old picture after uploading a new one: Hard-Reload the page with F5! Your browser doesn't
realize there's a new image as new and old one have the same filename.
</div>
<form action="<?php echo Config::get('URL'); ?>login/uploadAvatar_action" method="post" enctype="multipart/form-data">
<label for="avatar_file">Select an avatar image from your hard-disk (will be scaled to 44x44 px, only .jpg currently):</label>
<input type="file" name="avatar_file" required />
<!-- max size 5 MB (as many people directly upload high res pictures from their digital cameras) -->
<input type="hidden" name="MAX_FILE_SIZE" value="5000000" />
<input type="submit" value="Upload image" />
</form>
</div>
<div class="box">
<h3>Delete your avatar</h3>
<p>Click this link to delete your (local) avatar: <a href="<?php echo Config::get('URL'); ?>login/deleteAvatar_action">Delete your avatar</a>
</div>
</div>

View file

@ -0,0 +1,17 @@
<div class="container">
<h1>LoginController/editUserEmail</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Change your email address</h2>
<form action="<?php echo Config::get('URL'); ?>login/editUserEmail_action" method="post">
<label>
New email address: <input type="text" name="user_email" required />
</label>
<input type="submit" value="Submit" />
</form>
</div>
</div>

View file

@ -0,0 +1,18 @@
<div class="container">
<h1>LoginController/editUsername</h1>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="box">
<h2>Change your username</h2>
<form action="<?php echo Config::get('URL'); ?>login/editUserName_action" method="post">
<!-- btw http://stackoverflow.com/questions/774054/should-i-put-input-tag-inside-label-tag -->
<label>
New username: <input type="text" name="user_name" required />
</label>
<input type="submit" value="Submit" />
</form>
</div>
</div>

View file

@ -0,0 +1,34 @@
<div class="container">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div class="login-page-box">
<div class="table-wrapper">
<!-- login box on left side -->
<div class="login-box">
<h2>Login here</h2>
<form action="<?php echo Config::get('URL'); ?>login/login" method="post">
<input type="text" name="user_name" placeholder="Username or email" required />
<input type="password" name="user_password" placeholder="Password" required />
<label for="set_remember_me_cookie" class="remember-me-label">
<input type="checkbox" name="set_remember_me_cookie" class="remember-me-checkbox" />
Remember me for 2 weeks
</label>
<input type="submit" class="login-submit-button" value="Log in"/>
</form>
<div class="link-forgot-my-password">
<a href="<?php echo Config::get('URL'); ?>login/requestPasswordReset">I forgot my password</a>
</div>
</div>
<!-- register box on right side -->
<div class="register-box">
<h2>No account yet ?</h2>
<a href="<?php echo Config::get('URL'); ?>login/register">Register</a>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,37 @@
<div class="container">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<!-- login box on left side -->
<div class="login-box" style="width: 50%; display: block;">
<h2>Register a new account</h2>
<!-- register form -->
<form method="post" action="<?php echo Config::get('URL'); ?>login/register_action">
<!-- the user name input field uses a HTML5 pattern check -->
<input type="text" pattern="[a-zA-Z0-9]{2,64}" name="user_name" placeholder="Username (letters/numbers, 2-64 chars)" required />
<input type="text" name="user_email" placeholder="email address (a real address)" required />
<input type="password" name="user_password_new" pattern=".{6,}" placeholder="Password (6+ characters)" required autocomplete="off" />
<input type="password" name="user_password_repeat" pattern=".{6,}" required placeholder="Repeat your password" autocomplete="off" />
<!-- show the captcha by calling the login/showCaptcha-method in the src attribute of the img tag -->
<img id="captcha" src="<?php echo Config::get('URL'); ?>login/showCaptcha" />
<input type="text" name="captcha" placeholder="Please enter above characters" required />
<!-- quick & dirty captcha reloader -->
<a href="#" style="display: block; font-size: 11px; margin: 5px 0 15px 0; text-align: center"
onclick="document.getElementById('captcha').src = '<?php echo Config::get('URL'); ?>login/showCaptcha?' + Math.random(); return false">Reload Captcha</a>
<input type="submit" value="Register" />
</form>
</div>
</div>
<div class="container">
<p style="display: block; font-size: 11px; color: #999;">
Please note: This captcha will be generated when the img tag requests the captcha-generation
(= a real image) from YOURURL/login/showcaptcha. As this is a client-side triggered request, a
$_SESSION["captcha"] dump will not show the captcha characters. The captcha generation
happens AFTER the request that generates THIS page has been finished.
</p>
</div>

View file

@ -0,0 +1,18 @@
<div class="container">
<h1>Request a password reset</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<!-- request password reset form box -->
<form method="post" action="<?php echo Config::get('URL'); ?>login/requestPasswordReset_action">
<label>
Enter your username or email and you'll get a mail with instructions:
<input type="text" name="user_name_or_email" required />
</label>
<input type="submit" value="Send me a password-reset mail" />
</form>
</div>
</div>

View file

@ -0,0 +1,21 @@
<div class="container">
<h1>LoginController/showProfile</h1>
<div class="box">
<h2>Your profile</h2>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<div>Your username: <?= $this->user_name; ?></div>
<div>Your email: <?= $this->user_email; ?></div>
<div>Your avatar image:
<?php if (Config::get('USE_GRAVATAR')) { ?>
Your gravatar pic (on gravatar.com): <img src='<?= $this->user_gravatar_image_url; ?>' />
<?php } else { ?>
Your avatar pic (saved locally): <img src='<?= $this->user_avatar_file; ?>' />
<?php } ?>
</div>
<div>Your account type is: <?= $this->user_account_type; ?></div>
</div>
</div>

View file

@ -0,0 +1,12 @@
<div class="container">
<h1>Verification</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<a href="<?php echo Config::get('URL'); ?>">Go back to home page</a>
</div>
</div>

View file

@ -0,0 +1,22 @@
<div class="container">
<h1>NoteController/edit/:note_id</h1>
<div class="box">
<h2>Edit a note</h2>
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<?php if ($this->note) { ?>
<form method="post" action="<?php echo Config::get('URL'); ?>note/editSave">
<label>Change text of note: </label>
<!-- we use htmlentities() here to prevent user input with " etc. break the HTML -->
<input type="hidden" name="note_id" value="<?php echo htmlentities($this->note->note_id); ?>" />
<input type="text" name="note_text" value="<?php echo htmlentities($this->note->note_text); ?>" />
<input type="submit" value='Change' />
</form>
<?php } else { ?>
<p>This note does not exist.</p>
<?php } ?>
</div>
</div>

View file

@ -0,0 +1,43 @@
<div class="container">
<h1>NoteController/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<p>
This is just a simple CRUD implementation. Creating, reading, updating and deleting things.
</p>
<p>
<form method="post" action="<?php echo Config::get('URL');?>note/create">
<label>Text of new note: </label><input type="text" name="note_text" />
<input type="submit" value='Create this note' autocomplete="off" />
</form>
</p>
<?php if ($this->notes) { ?>
<table class="note-table">
<thead>
<tr>
<td>Id</td>
<td>Note</td>
<td>EDIT</td>
<td>DELETE</td>
</tr>
</thead>
<tbody>
<?php foreach($this->notes as $key => $value) { ?>
<tr>
<td><?= htmlentities($value->note_text); ?></td>
<td><a href="<?= Config::get('URL') . 'note/edit/' . $value->note_id; ?>">Edit</a></td>
<td><a href="<?= Config::get('URL') . 'note/delete/' . $value->note_id; ?>">Delete</a></td>
</tr>
<?php } ?>
</tbody>
</table>
<?php } else { ?>
<div>No notes yet. Create some !</div>
<?php } ?>
</div>
</div>

View file

@ -0,0 +1,44 @@
<div class="container">
<h1>ProfileController/index</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<div>
This controller/action/view shows a list of all users in the system. You could use the underlying code to
build things that use profile information of one or multiple/all users.
</div>
<div>
<table class="overview-table">
<thead>
<tr>
<td>Id</td>
<td>Avatar</td>
<td>Username</td>
<td>User's email</td>
<td>Activated ?</td>
<td>Link to user's profile</td>
</tr>
</thead>
<?php foreach ($this->users as $user) { ?>
<tr class="<?= ($user->user_active == 0 ? 'inactive' : 'active'); ?>">
<td><?= $user->user_id; ?></td>
<td class="avatar">
<?php if (isset($user->user_avatar_link)) { ?>
<img src="<?= $user->user_avatar_link; ?>" />
<?php } ?>
</td>
<td><?= $user->user_name; ?></td>
<td><?= $user->user_email; ?></td>
<td><?= ($user->user_active == 0 ? 'No' : 'Yes'); ?></td>
<td>
<a href="<?= Config::get('URL') . 'profile/showProfile/' . $user->user_id; ?>">Profile</a>
</td>
</tr>
<?php } ?>
</table>
</div>
</div>
</div>

View file

@ -0,0 +1,41 @@
<div class="container">
<h1>ProfileController/showProfile/:id</h1>
<div class="box">
<!-- echo out the system feedback (error and success messages) -->
<?php $this->renderFeedbackMessages(); ?>
<h3>What happens here ?</h3>
<div>This controller/action/view shows all public information about a certain user.</div>
<?php if ($this->user) { ?>
<div>
<table class="overview-table">
<thead>
<tr>
<td>Id</td>
<td>Avatar</td>
<td>Username</td>
<td>User's email</td>
<td>Activated ?</td>
</tr>
</thead>
<tbody>
<tr class="<?= ($this->user->user_active == 0 ? 'inactive' : 'active'); ?>">
<td><?= $this->user->user_id; ?></td>
<td class="avatar">
<?php if (isset($this->user->user_avatar_link)) { ?>
<img src="<?= $this->user->user_avatar_link; ?>" />
<?php } ?>
</td>
<td><?= $this->user->user_name; ?></td>
<td><?= $this->user->user_email; ?></td>
<td><?= ($this->user->user_active == 0 ? 'No' : 'Yes'); ?></td>
</tr>
</tbody>
</table>
</div>
<?php } ?>
</div>
</div>

View file

@ -0,0 +1,17 @@
{
"name": "panique/huge",
"type": "project",
"description": "A full-feature user authentication / login system embedded into a simple but powerful MVC framework structure",
"keywords": ["login", "auth", "user", "authentication", "mvc", "membership"],
"homepage": "https://github.com/panique/huge",
"license": "MIT",
"require-dev": {
"php": ">=5.5.0",
"phpmailer/phpmailer": "~5.2",
"gregwar/captcha": "~1.0.12",
"phpunit/phpunit": "~4.5"
},
"autoload": {
"psr-4": { "": ["application/core/", "application/model/"] }
}
}

View file

@ -0,0 +1,23 @@
# Necessary to prevent problems when using a controller named "index" and having a root index.php
# more here: http://httpd.apache.org/docs/2.2/content-negotiation.html
Options -MultiViews
# Activates URL rewriting (like myproject.com/controller/action/1/2/3)
RewriteEngine On
# Prevent people from looking directly into folders
Options -Indexes
# If the following conditions are true, then rewrite the URL:
# If the requested filename is not a directory,
RewriteCond %{REQUEST_FILENAME} !-d
# and if the requested filename is not a regular file that exists,
RewriteCond %{REQUEST_FILENAME} !-f
# and if the requested filename is not a symbolic link,
RewriteCond %{REQUEST_FILENAME} !-l
# then rewrite the URL in the following way:
# Take the whole request filename and provide it as the value of a
# "url" query parameter to index.php. Append any query string from
# the original URL as further query parameters (QSA), and stop
# processing this .htaccess file (L).
RewriteRule ^(.+)$ index.php?url=$1 [QSA,L]

Some files were not shown because too many files have changed in this diff Show more