mirror of
https://github.com/ThomasGsp/HyperProxmox.git
synced 2025-02-14 12:12:16 +00:00
first commit
This commit is contained in:
commit
5352a2b94a
396 changed files with 10008 additions and 0 deletions
0
README.md
Normal file
0
README.md
Normal file
76
code/scripts/main/api/README.md
Normal file
76
code/scripts/main/api/README.md
Normal 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>
|
||||
```
|
2
code/scripts/main/api/v1/__init__.py
Normal file
2
code/scripts/main/api/v1/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
BIN
code/scripts/main/api/v1/__pycache__/__init__.cpython-35.pyc
Normal file
BIN
code/scripts/main/api/v1/__pycache__/__init__.cpython-35.pyc
Normal file
Binary file not shown.
BIN
code/scripts/main/api/v1/__pycache__/api.cpython-35.pyc
Normal file
BIN
code/scripts/main/api/v1/__pycache__/api.cpython-35.pyc
Normal file
Binary file not shown.
129
code/scripts/main/api/v1/api.py
Normal file
129
code/scripts/main/api/v1/api.py
Normal 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)))
|
0
code/scripts/main/cli/README.md
Normal file
0
code/scripts/main/cli/README.md
Normal file
0
code/scripts/main/cli/command.py
Normal file
0
code/scripts/main/cli/command.py
Normal file
3
code/scripts/main/core/README.md
Normal file
3
code/scripts/main/core/README.md
Normal 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.
|
BIN
code/scripts/main/core/__pycache__/core.cpython-35.pyc
Normal file
BIN
code/scripts/main/core/__pycache__/core.cpython-35.pyc
Normal file
Binary file not shown.
327
code/scripts/main/core/core.py
Normal file
327
code/scripts/main/core/core.py
Normal 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
|
2
code/scripts/main/core/libs/__init__.py
Normal file
2
code/scripts/main/core/libs/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
BIN
code/scripts/main/core/libs/__pycache__/__init__.cpython-35.pyc
Normal file
BIN
code/scripts/main/core/libs/__pycache__/__init__.cpython-35.pyc
Normal file
Binary file not shown.
BIN
code/scripts/main/core/libs/__pycache__/hcrypt.cpython-35.pyc
Normal file
BIN
code/scripts/main/core/libs/__pycache__/hcrypt.cpython-35.pyc
Normal file
Binary file not shown.
11
code/scripts/main/core/libs/hcrypt.py
Normal file
11
code/scripts/main/core/libs/hcrypt.py
Normal 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
|
0
code/scripts/main/core/libs/logs.py
Normal file
0
code/scripts/main/core/libs/logs.py
Normal file
0
code/scripts/main/core/libs/write.py
Normal file
0
code/scripts/main/core/libs/write.py
Normal file
2
code/scripts/main/core/modules/__init__.py
Normal file
2
code/scripts/main/core/modules/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
133
code/scripts/main/core/modules/mod_access.py
Normal file
133
code/scripts/main/core/modules/mod_access.py
Normal 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
|
||||
|
157
code/scripts/main/core/modules/mod_analyst.py
Normal file
157
code/scripts/main/core/modules/mod_analyst.py
Normal 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
|
179
code/scripts/main/core/modules/mod_database.py
Normal file
179
code/scripts/main/core/modules/mod_database.py
Normal 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))
|
0
code/scripts/main/core/modules/mod_instance.py
Normal file
0
code/scripts/main/core/modules/mod_instance.py
Normal file
0
code/scripts/main/core/modules/mod_package.py
Normal file
0
code/scripts/main/core/modules/mod_package.py
Normal file
415
code/scripts/main/core/modules/mod_proxmox.py
Normal file
415
code/scripts/main/core/modules/mod_proxmox.py
Normal 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
|
20
code/scripts/main/core/modules/mod_queue.py
Normal file
20
code/scripts/main/core/modules/mod_queue.py
Normal 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
|
0
code/scripts/main/core/modules/mod_ssl.py
Normal file
0
code/scripts/main/core/modules/mod_ssl.py
Normal file
0
code/scripts/main/core/modules/mod_varnish.py
Normal file
0
code/scripts/main/core/modules/mod_varnish.py
Normal file
34
code/scripts/main/core/modules/mod_vhost.py
Normal file
34
code/scripts/main/core/modules/mod_vhost.py
Normal 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
|
36
code/scripts/main/private/conf/config
Normal file
36
code/scripts/main/private/conf/config
Normal 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
|
14
code/scripts/main/private/keys/Ragnarok.pub.key
Normal file
14
code/scripts/main/private/keys/Ragnarok.pub.key
Normal 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-----
|
54
code/scripts/main/private/keys/Ragnarok.pvt.key
Normal file
54
code/scripts/main/private/keys/Ragnarok.pvt.key
Normal 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-----
|
115
code/scripts/main/startup.py
Normal file
115
code/scripts/main/startup.py
Normal 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()
|
||||
|
4
code/web/backend/.htaccess
Normal file
4
code/web/backend/.htaccess
Normal 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]
|
5
code/web/backend/.scrutinizer.yml
Normal file
5
code/web/backend/.scrutinizer.yml
Normal 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
|
30
code/web/backend/.travis.yml
Normal file
30
code/web/backend/.travis.yml
Normal 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
|
72
code/web/backend/CHANGELOG.md
Normal file
72
code/web/backend/CHANGELOG.md
Normal 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
353
code/web/backend/README.md
Normal 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.
|
22
code/web/backend/_one-click-installation/Vagrantfile
vendored
Normal file
22
code/web/backend/_one-click-installation/Vagrantfile
vendored
Normal 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
|
83
code/web/backend/_one-click-installation/bootstrap.sh
Normal file
83
code/web/backend/_one-click-installation/bootstrap.sh
Normal 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!"
|
BIN
code/web/backend/_pictures/huge-logo.png
Normal file
BIN
code/web/backend/_pictures/huge-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
BIN
code/web/backend/_pictures/support-via-a2hosting.png
Normal file
BIN
code/web/backend/_pictures/support-via-a2hosting.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.4 KiB |
BIN
code/web/backend/_pictures/support-via-paypal.png
Normal file
BIN
code/web/backend/_pictures/support-via-paypal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1 @@
|
|||
CREATE DATABASE IF NOT EXISTS `huge`;
|
|
@ -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');
|
|
@ -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';
|
129
code/web/backend/application/config/config.development.php
Normal file
129
code/web/backend/application/config/config.development.php
Normal 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: ',
|
||||
);
|
73
code/web/backend/application/config/texts.php
Normal file
73
code/web/backend/application/config/texts.php
Normal 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).",
|
||||
);
|
|
@ -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');
|
||||
}
|
||||
}
|
25
code/web/backend/application/controller/ErrorController.php
Normal file
25
code/web/backend/application/controller/ErrorController.php
Normal 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');
|
||||
}
|
||||
}
|
21
code/web/backend/application/controller/IndexController.php
Normal file
21
code/web/backend/application/controller/IndexController.php
Normal 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');
|
||||
}
|
||||
}
|
313
code/web/backend/application/controller/LoginController.php
Normal file
313
code/web/backend/application/controller/LoginController.php
Normal 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();
|
||||
}
|
||||
}
|
77
code/web/backend/application/controller/NoteController.php
Normal file
77
code/web/backend/application/controller/NoteController.php
Normal 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');
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
100
code/web/backend/application/core/Application.php
Normal file
100
code/web/backend/application/core/Application.php
Normal 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';
|
||||
}
|
||||
}
|
28
code/web/backend/application/core/Auth.php
Normal file
28
code/web/backend/application/core/Auth.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
23
code/web/backend/application/core/Config.php
Normal file
23
code/web/backend/application/core/Config.php
Normal 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];
|
||||
}
|
||||
}
|
31
code/web/backend/application/core/Controller.php
Normal file
31
code/web/backend/application/core/Controller.php
Normal 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();
|
||||
}
|
||||
}
|
46
code/web/backend/application/core/DatabaseFactory.php
Normal file
46
code/web/backend/application/core/DatabaseFactory.php
Normal 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;
|
||||
}
|
||||
}
|
18
code/web/backend/application/core/Environment.php
Normal file
18
code/web/backend/application/core/Environment.php
Normal 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");
|
||||
}
|
||||
}
|
115
code/web/backend/application/core/Mail.php
Normal file
115
code/web/backend/application/core/Mail.php
Normal 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;
|
||||
}
|
||||
}
|
27
code/web/backend/application/core/Redirect.php
Normal file
27
code/web/backend/application/core/Redirect.php
Normal 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);
|
||||
}
|
||||
}
|
53
code/web/backend/application/core/Request.php
Normal file
53
code/web/backend/application/core/Request.php
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
75
code/web/backend/application/core/Session.php
Normal file
75
code/web/backend/application/core/Session.php
Normal 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);
|
||||
}
|
||||
}
|
27
code/web/backend/application/core/Text.php
Normal file
27
code/web/backend/application/core/Text.php
Normal 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];
|
||||
}
|
||||
|
||||
}
|
164
code/web/backend/application/core/View.php
Normal file
164
code/web/backend/application/core/View.php
Normal 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;
|
||||
}
|
||||
}
|
254
code/web/backend/application/model/AvatarModel.php
Normal file
254
code/web/backend/application/model/AvatarModel.php
Normal 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;
|
||||
}
|
||||
}
|
46
code/web/backend/application/model/CaptchaModel.php
Normal file
46
code/web/backend/application/model/CaptchaModel.php
Normal 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;
|
||||
}
|
||||
}
|
270
code/web/backend/application/model/LoginModel.php
Normal file
270
code/web/backend/application/model/LoginModel.php
Normal 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();
|
||||
}
|
||||
}
|
120
code/web/backend/application/model/NoteModel.php
Normal file
120
code/web/backend/application/model/NoteModel.php
Normal 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;
|
||||
}
|
||||
}
|
251
code/web/backend/application/model/PasswordResetModel.php
Normal file
251
code/web/backend/application/model/PasswordResetModel.php
Normal 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;
|
||||
}
|
||||
}
|
278
code/web/backend/application/model/RegistrationModel.php
Normal file
278
code/web/backend/application/model/RegistrationModel.php
Normal 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;
|
||||
}
|
||||
}
|
331
code/web/backend/application/model/UserModel.php
Normal file
331
code/web/backend/application/model/UserModel.php
Normal 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();
|
||||
}
|
||||
}
|
65
code/web/backend/application/model/UserRoleModel.php
Normal file
65
code/web/backend/application/model/UserRoleModel.php
Normal 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;
|
||||
}
|
||||
}
|
19
code/web/backend/application/view/_templates/feedback.php
Normal file
19
code/web/backend/application/view/_templates/feedback.php
Normal 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>';
|
||||
}
|
||||
}
|
7
code/web/backend/application/view/_templates/footer.php
Normal file
7
code/web/backend/application/view/_templates/footer.php
Normal 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>
|
66
code/web/backend/application/view/_templates/header.php
Normal file
66
code/web/backend/application/view/_templates/header.php
Normal 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>
|
15
code/web/backend/application/view/dashboard/index.php
Normal file
15
code/web/backend/application/view/dashboard/index.php
Normal 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>
|
6
code/web/backend/application/view/error/index.php
Normal file
6
code/web/backend/application/view/error/index.php
Normal 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>
|
18
code/web/backend/application/view/index/index.php
Normal file
18
code/web/backend/application/view/index/index.php
Normal 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>
|
27
code/web/backend/application/view/login/changePassword.php
Normal file
27
code/web/backend/application/view/login/changePassword.php
Normal 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>
|
31
code/web/backend/application/view/login/changeUserRole.php
Normal file
31
code/web/backend/application/view/login/changeUserRole.php
Normal 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>
|
28
code/web/backend/application/view/login/editAvatar.php
Normal file
28
code/web/backend/application/view/login/editAvatar.php
Normal 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>
|
17
code/web/backend/application/view/login/editUserEmail.php
Normal file
17
code/web/backend/application/view/login/editUserEmail.php
Normal 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>
|
18
code/web/backend/application/view/login/editUsername.php
Normal file
18
code/web/backend/application/view/login/editUsername.php
Normal 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>
|
34
code/web/backend/application/view/login/index.php
Normal file
34
code/web/backend/application/view/login/index.php
Normal 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>
|
37
code/web/backend/application/view/login/register.php
Normal file
37
code/web/backend/application/view/login/register.php
Normal 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>
|
|
@ -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>
|
21
code/web/backend/application/view/login/showProfile.php
Normal file
21
code/web/backend/application/view/login/showProfile.php
Normal 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>
|
12
code/web/backend/application/view/login/verify.php
Normal file
12
code/web/backend/application/view/login/verify.php
Normal 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>
|
22
code/web/backend/application/view/note/edit.php
Normal file
22
code/web/backend/application/view/note/edit.php
Normal 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>
|
43
code/web/backend/application/view/note/index.php
Normal file
43
code/web/backend/application/view/note/index.php
Normal 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>
|
44
code/web/backend/application/view/profile/index.php
Normal file
44
code/web/backend/application/view/profile/index.php
Normal 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>
|
41
code/web/backend/application/view/profile/showProfile.php
Normal file
41
code/web/backend/application/view/profile/showProfile.php
Normal 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>
|
17
code/web/backend/composer.json
Normal file
17
code/web/backend/composer.json
Normal 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/"] }
|
||||
}
|
||||
}
|
23
code/web/backend/public/.htaccess
Normal file
23
code/web/backend/public/.htaccess
Normal 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
Loading…
Reference in a new issue