web_kbn: almost completely ported lws to python
This commit is contained in:
parent
d79309e498
commit
3741f7cf78
@ -2,6 +2,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import __py_include
|
import __py_include
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
from homekit import http
|
from homekit import http
|
||||||
from homekit.config import config, AppConfigUnit
|
from homekit.config import config, AppConfigUnit
|
||||||
from homekit.mqtt import MqttPayload, MqttWrapper, MqttNode, MqttModule, MqttNodesConfig
|
from homekit.mqtt import MqttPayload, MqttWrapper, MqttNode, MqttModule, MqttNodesConfig
|
||||||
@ -15,7 +16,6 @@ mqtt: Optional[MqttWrapper] = None
|
|||||||
mqtt_nodes: dict[str, MqttNode] = {}
|
mqtt_nodes: dict[str, MqttNode] = {}
|
||||||
relay_modules: dict[str, Union[MqttRelayModule, MqttModule]] = {}
|
relay_modules: dict[str, Union[MqttRelayModule, MqttModule]] = {}
|
||||||
relay_states: dict[str, MqttRelayState] = {}
|
relay_states: dict[str, MqttRelayState] = {}
|
||||||
|
|
||||||
mqtt_nodes_config = MqttNodesConfig()
|
mqtt_nodes_config = MqttNodesConfig()
|
||||||
|
|
||||||
|
|
||||||
@ -67,41 +67,47 @@ def on_mqtt_message(node: MqttNode,
|
|||||||
relay_states[node.id].update(**kwargs)
|
relay_states[node.id].update(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class RelayMqttHttpProxy(http.HTTPServer):
|
# -=-=-=-=-=-=- #
|
||||||
def __init__(self, *args, **kwargs):
|
# Web interface #
|
||||||
super().__init__(*args, **kwargs)
|
# -=-=-=-=-=-=- #
|
||||||
self.get('/relay/{id}/on', self.relay_on)
|
|
||||||
self.get('/relay/{id}/off', self.relay_off)
|
|
||||||
self.get('/relay/{id}/toggle', self.relay_toggle)
|
|
||||||
|
|
||||||
async def _relay_on_off(self,
|
routes = web.RouteTableDef()
|
||||||
enable: Optional[bool],
|
|
||||||
req: http.Request):
|
|
||||||
node_id = req.match_info['id']
|
|
||||||
node_secret = req.query['secret']
|
|
||||||
|
|
||||||
node = mqtt_nodes[node_id]
|
|
||||||
relay_module = relay_modules[node_id]
|
|
||||||
|
|
||||||
if enable is None:
|
async def _relay_on_off(self,
|
||||||
if node_id in relay_states and relay_states[node_id].ever_updated:
|
enable: Optional[bool],
|
||||||
cur_state = relay_states[node_id].enabled
|
req: web.Request):
|
||||||
else:
|
node_id = req.match_info['id']
|
||||||
cur_state = False
|
node_secret = req.query['secret']
|
||||||
enable = not cur_state
|
|
||||||
|
|
||||||
node.secret = node_secret
|
node = mqtt_nodes[node_id]
|
||||||
relay_module.switchpower(enable)
|
relay_module = relay_modules[node_id]
|
||||||
return self.ok()
|
|
||||||
|
|
||||||
async def relay_on(self, req: http.Request):
|
if enable is None:
|
||||||
return await self._relay_on_off(True, req)
|
if node_id in relay_states and relay_states[node_id].ever_updated:
|
||||||
|
cur_state = relay_states[node_id].enabled
|
||||||
|
else:
|
||||||
|
cur_state = False
|
||||||
|
enable = not cur_state
|
||||||
|
|
||||||
async def relay_off(self, req: http.Request):
|
node.secret = node_secret
|
||||||
return await self._relay_on_off(False, req)
|
relay_module.switchpower(enable)
|
||||||
|
return self.ok()
|
||||||
|
|
||||||
async def relay_toggle(self, req: http.Request):
|
|
||||||
return await self._relay_on_off(None, req)
|
@routes.get('/relay/{id}/on')
|
||||||
|
async def relay_on(self, req: web.Request):
|
||||||
|
return await self._relay_on_off(True, req)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/relay/{id}/off')
|
||||||
|
async def relay_off(self, req: web.Request):
|
||||||
|
return await self._relay_on_off(False, req)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/relay/{id}/toggle')
|
||||||
|
async def relay_toggle(self, req: web.Request):
|
||||||
|
return await self._relay_on_off(None, req)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@ -127,8 +133,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
mqtt.connect_and_loop(loop_forever=False)
|
mqtt.connect_and_loop(loop_forever=False)
|
||||||
|
|
||||||
proxy = RelayMqttHttpProxy(config.app_config['listen_addr'])
|
|
||||||
try:
|
try:
|
||||||
proxy.run()
|
http.serve(config.app_config['listen_addr'], routes=routes)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
mqtt.disconnect()
|
mqtt.disconnect()
|
||||||
|
@ -17,7 +17,7 @@ from homekit import http
|
|||||||
def _amixer_control_response(control):
|
def _amixer_control_response(control):
|
||||||
info = amixer.get(control)
|
info = amixer.get(control)
|
||||||
caps = amixer.get_caps(control)
|
caps = amixer.get_caps(control)
|
||||||
return http.ok({
|
return http.ajax_ok({
|
||||||
'caps': caps,
|
'caps': caps,
|
||||||
'info': info
|
'info': info
|
||||||
})
|
})
|
||||||
|
@ -51,7 +51,7 @@ class WebAPIServer(http.HTTPServer):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def validate_auth(req: http.Request, handler):
|
async def validate_auth(req: web.Request, handler):
|
||||||
def get_token() -> str:
|
def get_token() -> str:
|
||||||
name = 'X-Token'
|
name = 'X-Token'
|
||||||
if name in req.headers:
|
if name in req.headers:
|
||||||
@ -70,13 +70,13 @@ class WebAPIServer(http.HTTPServer):
|
|||||||
return await handler(req)
|
return await handler(req)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_index(req: http.Request):
|
async def get_index(req: web.Request):
|
||||||
message = "nothing here, keep lurking"
|
message = "nothing here, keep lurking"
|
||||||
if is_development_mode():
|
if is_development_mode():
|
||||||
message += ' (dev mode)'
|
message += ' (dev mode)'
|
||||||
return http.Response(text=message, content_type='text/plain')
|
return web.Response(text=message, content_type='text/plain')
|
||||||
|
|
||||||
async def GET_sensors_data(self, req: http.Request):
|
async def GET_sensors_data(self, req: web.Request):
|
||||||
try:
|
try:
|
||||||
hours = int(req.query['hours'])
|
hours = int(req.query['hours'])
|
||||||
if hours < 1 or hours > 24:
|
if hours < 1 or hours > 24:
|
||||||
@ -93,7 +93,7 @@ class WebAPIServer(http.HTTPServer):
|
|||||||
data = db.get_temperature_recordings(sensor, (dt_from, dt_to))
|
data = db.get_temperature_recordings(sensor, (dt_from, dt_to))
|
||||||
return self.ok(data)
|
return self.ok(data)
|
||||||
|
|
||||||
async def GET_sound_sensors_hits(self, req: http.Request):
|
async def GET_sound_sensors_hits(self, req: web.Request):
|
||||||
location = SoundSensorLocation(int(req.query['location']))
|
location = SoundSensorLocation(int(req.query['location']))
|
||||||
|
|
||||||
after = int(req.query['after'])
|
after = int(req.query['after'])
|
||||||
@ -112,7 +112,7 @@ class WebAPIServer(http.HTTPServer):
|
|||||||
data = BotsDatabase().get_sound_hits(location, **kwargs)
|
data = BotsDatabase().get_sound_hits(location, **kwargs)
|
||||||
return self.ok(data)
|
return self.ok(data)
|
||||||
|
|
||||||
async def POST_sound_sensors_hits(self, req: http.Request):
|
async def POST_sound_sensors_hits(self, req: web.Request):
|
||||||
hits = []
|
hits = []
|
||||||
data = await req.post()
|
data = await req.post()
|
||||||
for hit, count in json.loads(data['hits']):
|
for hit, count in json.loads(data['hits']):
|
||||||
@ -125,7 +125,7 @@ class WebAPIServer(http.HTTPServer):
|
|||||||
BotsDatabase().add_sound_hits(hits, datetime.now())
|
BotsDatabase().add_sound_hits(hits, datetime.now())
|
||||||
return self.ok()
|
return self.ok()
|
||||||
|
|
||||||
async def POST_openwrt_log(self, req: http.Request):
|
async def POST_openwrt_log(self, req: web.Request):
|
||||||
data = await req.post()
|
data = await req.post()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -154,7 +154,7 @@ class WebAPIServer(http.HTTPServer):
|
|||||||
BotsDatabase().add_openwrt_logs(lines, ap)
|
BotsDatabase().add_openwrt_logs(lines, ap)
|
||||||
return self.ok()
|
return self.ok()
|
||||||
|
|
||||||
async def GET_recordings_list(self, req: http.Request):
|
async def GET_recordings_list(self, req: web.Request):
|
||||||
data = await req.post()
|
data = await req.post()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -176,7 +176,7 @@ class WebAPIServer(http.HTTPServer):
|
|||||||
return self.ok(files)
|
return self.ok(files)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_inverter_from_to(req: http.Request):
|
def _get_inverter_from_to(req: web.Request):
|
||||||
s_from = req.query['from']
|
s_from = req.query['from']
|
||||||
s_to = req.query['to']
|
s_to = req.query['to']
|
||||||
|
|
||||||
@ -189,12 +189,12 @@ class WebAPIServer(http.HTTPServer):
|
|||||||
|
|
||||||
return dt_from, dt_to
|
return dt_from, dt_to
|
||||||
|
|
||||||
async def GET_consumed_energy(self, req: http.Request):
|
async def GET_consumed_energy(self, req: web.Request):
|
||||||
dt_from, dt_to = self._get_inverter_from_to(req)
|
dt_from, dt_to = self._get_inverter_from_to(req)
|
||||||
wh = InverterDatabase().get_consumed_energy(dt_from, dt_to)
|
wh = InverterDatabase().get_consumed_energy(dt_from, dt_to)
|
||||||
return self.ok(wh)
|
return self.ok(wh)
|
||||||
|
|
||||||
async def GET_grid_consumed_energy(self, req: http.Request):
|
async def GET_grid_consumed_energy(self, req: web.Request):
|
||||||
dt_from, dt_to = self._get_inverter_from_to(req)
|
dt_from, dt_to = self._get_inverter_from_to(req)
|
||||||
wh = InverterDatabase().get_grid_consumed_energy(dt_from, dt_to)
|
wh = InverterDatabase().get_grid_consumed_energy(dt_from, dt_to)
|
||||||
return self.ok(wh)
|
return self.ok(wh)
|
||||||
|
594
bin/web_kbn.py
594
bin/web_kbn.py
@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
import __py_include
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import jinja2
|
import jinja2
|
||||||
import aiohttp_jinja2
|
import aiohttp_jinja2
|
||||||
import json
|
import json
|
||||||
@ -7,18 +9,18 @@ import re
|
|||||||
import inverterd
|
import inverterd
|
||||||
import phonenumbers
|
import phonenumbers
|
||||||
import time
|
import time
|
||||||
import __py_include
|
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from aiohttp.web import HTTPFound, HTTPBadRequest
|
from aiohttp import web
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
from urllib.parse import quote_plus
|
||||||
from homekit.config import config, AppConfigUnit, is_development_mode, Translation
|
from homekit.config import config, AppConfigUnit, is_development_mode, Translation
|
||||||
from homekit.camera import IpcamConfig
|
from homekit.camera import IpcamConfig
|
||||||
from homekit.util import homekit_path, filesize_fmt, seconds_to_human_readable_string, json_serial
|
from homekit.util import homekit_path, filesize_fmt, seconds_to_human_readable_string, json_serial, validate_ipv4
|
||||||
from homekit.modem import E3372, ModemsConfig, MacroNetWorkType
|
from homekit.modem import E3372, ModemsConfig, MacroNetWorkType
|
||||||
from homekit.inverter.config import InverterdConfig
|
from homekit.inverter.config import InverterdConfig
|
||||||
from homekit.relay.sunxi_h3_client import RelayClient
|
from homekit.relay.sunxi_h3_client import RelayClient
|
||||||
from homekit import http
|
from homekit import openwrt, http
|
||||||
|
|
||||||
|
|
||||||
class WebKbnConfig(AppConfigUnit):
|
class WebKbnConfig(AppConfigUnit):
|
||||||
@ -36,13 +38,16 @@ class WebKbnConfig(AppConfigUnit):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
STATIC_FILES = [
|
common_static_files = [
|
||||||
'bootstrap.min.css',
|
'bootstrap.min.css',
|
||||||
'bootstrap.min.js',
|
'bootstrap.min.js',
|
||||||
'polyfills.js',
|
'polyfills.js',
|
||||||
'app.js',
|
'app.js',
|
||||||
'app.css'
|
'app.css'
|
||||||
]
|
]
|
||||||
|
routes = web.RouteTableDef()
|
||||||
|
webkbn_strings = Translation('web_kbn')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_js_link(file, version) -> str:
|
def get_js_link(file, version) -> str:
|
||||||
@ -65,7 +70,7 @@ def get_head_static(files=None) -> str:
|
|||||||
buf = StringIO()
|
buf = StringIO()
|
||||||
if files is None:
|
if files is None:
|
||||||
files = []
|
files = []
|
||||||
for file in STATIC_FILES+files:
|
for file in common_static_files + files:
|
||||||
v = 2
|
v = 2
|
||||||
try:
|
try:
|
||||||
q_ind = file.index('?')
|
q_ind = file.index('?')
|
||||||
@ -179,245 +184,356 @@ def get_inverter_data() -> tuple:
|
|||||||
return status, rated, html
|
return status, rated, html
|
||||||
|
|
||||||
|
|
||||||
class WebSite(http.HTTPServer):
|
def get_current_upstream() -> str:
|
||||||
def __init__(self, *args, **kwargs):
|
r = openwrt.get_default_route()
|
||||||
super().__init__(*args, **kwargs)
|
logger.info(f'default route: {r}')
|
||||||
|
mc = ModemsConfig()
|
||||||
|
for k, v in mc.items():
|
||||||
|
if 'gateway_ip' in v and v['gateway_ip'] == r:
|
||||||
|
r = v['ip']
|
||||||
|
break
|
||||||
|
upstream = None
|
||||||
|
for k, v in mc.items():
|
||||||
|
if r == v['ip']:
|
||||||
|
upstream = k
|
||||||
|
if not upstream:
|
||||||
|
raise RuntimeError('failed to determine current upstream!')
|
||||||
|
return upstream
|
||||||
|
|
||||||
aiohttp_jinja2.setup(
|
|
||||||
self.app,
|
|
||||||
loader=jinja2.FileSystemLoader(homekit_path('web', 'kbn_templates')),
|
|
||||||
autoescape=jinja2.select_autoescape(['html', 'xml']),
|
|
||||||
)
|
|
||||||
env = aiohttp_jinja2.get_env(self.app)
|
|
||||||
|
|
||||||
def filter_lang(key, unit='web_kbn'):
|
def lang(key: str):
|
||||||
strings = Translation(unit)
|
return webkbn_strings.get()[key]
|
||||||
|
|
||||||
|
|
||||||
|
async def render(req: web.Request,
|
||||||
|
template_name: str,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
context: Optional[dict] = None,
|
||||||
|
assets: Optional[list] = None):
|
||||||
|
if context is None:
|
||||||
|
context = {}
|
||||||
|
context = {
|
||||||
|
**context,
|
||||||
|
'head_static': get_head_static(assets)
|
||||||
|
}
|
||||||
|
if title is not None:
|
||||||
|
context['title'] = title
|
||||||
|
response = aiohttp_jinja2.render_template(template_name+'.j2', req, context=context)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/main.cgi')
|
||||||
|
async def index(req: web.Request):
|
||||||
|
ctx = {}
|
||||||
|
for k in 'inverter', 'sensors':
|
||||||
|
ctx[f'{k}_grafana_url'] = config.app_config[f'{k}_grafana_url']
|
||||||
|
|
||||||
|
cc = IpcamConfig()
|
||||||
|
ctx['camzones'] = cc['zones'].keys()
|
||||||
|
ctx['allcams'] = cc.get_all_cam_names()
|
||||||
|
|
||||||
|
return await render(req, 'index',
|
||||||
|
title=lang('sitename'),
|
||||||
|
context=ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/modems.cgi')
|
||||||
|
async def modems(req: web.Request):
|
||||||
|
return await render(req, 'modems',
|
||||||
|
title='Состояние модемов',
|
||||||
|
context=dict(modems=ModemsConfig()))
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/modems/info.ajx')
|
||||||
|
async def modems_ajx(req: web.Request):
|
||||||
|
mc = ModemsConfig()
|
||||||
|
modem = req.query.get('id', None)
|
||||||
|
if modem not in mc.keys():
|
||||||
|
raise ValueError('invalid modem id')
|
||||||
|
|
||||||
|
modem_cfg = mc.get(modem)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
modem_data = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg))
|
||||||
|
|
||||||
|
html = aiohttp_jinja2.render_string('modem_data.j2', req, context=dict(
|
||||||
|
modem_data=modem_data,
|
||||||
|
modem=modem
|
||||||
|
))
|
||||||
|
|
||||||
|
return http.ajax_ok({'html': html})
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/modems/verbose.cgi')
|
||||||
|
async def modems_verbose(req: web.Request):
|
||||||
|
modem = req.query.get('id', None)
|
||||||
|
if modem not in ModemsConfig().keys():
|
||||||
|
raise ValueError('invalid modem id')
|
||||||
|
|
||||||
|
modem_cfg = ModemsConfig().get(modem)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
signal, status, traffic, device, dialup_conn = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg, True))
|
||||||
|
data = [
|
||||||
|
['Signal', signal],
|
||||||
|
['Connection', status],
|
||||||
|
['Traffic', traffic],
|
||||||
|
['Device info', device],
|
||||||
|
['Dialup connection', dialup_conn]
|
||||||
|
]
|
||||||
|
|
||||||
|
modem_name = ModemsConfig().getfullname(modem)
|
||||||
|
return await render(req, 'modem_verbose',
|
||||||
|
title=f'Подробная информация о модеме "{modem_name}"',
|
||||||
|
context=dict(data=data, modem_name=modem_name))
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/sms.cgi')
|
||||||
|
async def sms(req: web.Request):
|
||||||
|
modem = req.query.get('id', list(ModemsConfig().keys())[0])
|
||||||
|
is_outbox = int(req.query.get('outbox', 0)) == 1
|
||||||
|
error = req.query.get('error', None)
|
||||||
|
sent = int(req.query.get('sent', 0)) == 1
|
||||||
|
|
||||||
|
cl = get_modem_client(ModemsConfig()[modem])
|
||||||
|
messages = cl.sms_list(1, 20, is_outbox)
|
||||||
|
return await render(req, 'sms',
|
||||||
|
title=f"SMS-сообщения ({'исходящие' if is_outbox else 'входящие'}, {modem})",
|
||||||
|
context=dict(
|
||||||
|
modems=ModemsConfig(),
|
||||||
|
selected_modem=modem,
|
||||||
|
is_outbox=is_outbox,
|
||||||
|
error=error,
|
||||||
|
is_sent=sent,
|
||||||
|
messages=messages
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@routes.post('/sms.cgi')
|
||||||
|
async def sms_post(req: web.Request):
|
||||||
|
modem = req.query.get('id', list(ModemsConfig().keys())[0])
|
||||||
|
is_outbox = int(req.query.get('outbox', 0)) == 1
|
||||||
|
|
||||||
|
fd = await req.post()
|
||||||
|
phone = fd.get('phone', None)
|
||||||
|
text = fd.get('text', None)
|
||||||
|
|
||||||
|
return_url = f'/sms.cgi?id={modem}&outbox={int(is_outbox)}'
|
||||||
|
phone = re.sub('\s+', '', phone)
|
||||||
|
|
||||||
|
if len(phone) > 4:
|
||||||
|
country = None
|
||||||
|
if not phone.startswith('+'):
|
||||||
|
country = 'RU'
|
||||||
|
number = phonenumbers.parse(phone, country)
|
||||||
|
if not phonenumbers.is_valid_number(number):
|
||||||
|
raise web.HTTPFound(f'{return_url}&error=Неверный+номер')
|
||||||
|
phone = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
|
||||||
|
|
||||||
|
cl = get_modem_client(ModemsConfig()[modem])
|
||||||
|
cl.sms_send(phone, text)
|
||||||
|
raise web.HTTPFound(return_url)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/inverter.cgi')
|
||||||
|
async def inverter(req: web.Request):
|
||||||
|
action = req.query.get('do', None)
|
||||||
|
if action == 'set-osp':
|
||||||
|
val = req.query.get('value')
|
||||||
|
if val not in ('sub', 'sbu'):
|
||||||
|
raise ValueError('invalid osp value')
|
||||||
|
cl = get_inverter_client()
|
||||||
|
cl.exec('set-output-source-priority',
|
||||||
|
arguments=(val.upper(),))
|
||||||
|
raise web.HTTPFound('/inverter.cgi')
|
||||||
|
|
||||||
|
status, rated, html = await asyncio.get_event_loop().run_in_executor(None, get_inverter_data)
|
||||||
|
return await render(req, 'inverter',
|
||||||
|
title=lang('inverter'),
|
||||||
|
context=dict(status=status, rated=rated, html=html))
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/inverter.ajx')
|
||||||
|
async def inverter_ajx(req: web.Request):
|
||||||
|
status, rated, html = await asyncio.get_event_loop().run_in_executor(None, get_inverter_data)
|
||||||
|
return http.ajax_ok({'html': html})
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/pump.cgi')
|
||||||
|
async def pump(req: web.Request):
|
||||||
|
# TODO
|
||||||
|
# these are blocking calls
|
||||||
|
# should be rewritten using aio
|
||||||
|
|
||||||
|
cl = get_pump_client()
|
||||||
|
|
||||||
|
action = req.query.get('set', None)
|
||||||
|
if action in ('on', 'off'):
|
||||||
|
getattr(cl, action)()
|
||||||
|
raise web.HTTPFound('/pump.cgi')
|
||||||
|
|
||||||
|
status = cl.status()
|
||||||
|
return await render(req, 'pump',
|
||||||
|
title=lang('pump'),
|
||||||
|
context=dict(status=status))
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/cams.cgi')
|
||||||
|
async def cams(req: web.Request):
|
||||||
|
cc = IpcamConfig()
|
||||||
|
|
||||||
|
cam = req.query.get('id', None)
|
||||||
|
zone = req.query.get('zone', None)
|
||||||
|
debug_hls = bool(req.query.get('debug_hls', False))
|
||||||
|
debug_video_events = bool(req.query.get('debug_video_events', False))
|
||||||
|
|
||||||
|
if cam is not None:
|
||||||
|
if not cc.has_camera(int(cam)):
|
||||||
|
raise ValueError('invalid camera id')
|
||||||
|
cams = [int(cam)]
|
||||||
|
mode = {'type': 'single', 'cam': cam}
|
||||||
|
|
||||||
|
elif zone is not None:
|
||||||
|
if not cc.has_zone(zone):
|
||||||
|
raise ValueError('invalid zone')
|
||||||
|
cams = cc['zones'][zone]
|
||||||
|
mode = {'type': 'zone', 'zone': zone}
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise web.HTTPBadRequest(text='no camera id or zone found')
|
||||||
|
|
||||||
|
js_config = {
|
||||||
|
'host': config.app_config['cam_hls_host'],
|
||||||
|
'proto': 'http',
|
||||||
|
'cams': cams,
|
||||||
|
'hlsConfig': {
|
||||||
|
'opts': {
|
||||||
|
'startPosition': -1,
|
||||||
|
# https://github.com/video-dev/hls.js/issues/3884#issuecomment-842380784
|
||||||
|
'liveSyncDuration': 2,
|
||||||
|
'liveMaxLatencyDuration': 3,
|
||||||
|
'maxLiveSyncPlaybackRate': 2,
|
||||||
|
'liveDurationInfinity': True
|
||||||
|
},
|
||||||
|
'debugVideoEvents': debug_video_events,
|
||||||
|
'debug': debug_hls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await render(req, 'cams',
|
||||||
|
title=lang('cams'),
|
||||||
|
assets=['hls.js'],
|
||||||
|
context=dict(
|
||||||
|
mode=mode,
|
||||||
|
js_config=js_config,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/routing/main.cgi')
|
||||||
|
async def routing_main(req: web.Request):
|
||||||
|
upstream = get_current_upstream()
|
||||||
|
|
||||||
|
set_upstream_to = req.query.get('set-upstream-to', None)
|
||||||
|
mc = ModemsConfig()
|
||||||
|
|
||||||
|
if set_upstream_to and set_upstream_to in mc and set_upstream_to != upstream:
|
||||||
|
modem = mc[set_upstream_to]
|
||||||
|
new_upstream = str(modem['gateway_ip'] if 'gateway_ip' in modem else modem['ip'])
|
||||||
|
openwrt.set_upstream(new_upstream)
|
||||||
|
raise web.HTTPFound('/routing/main.cgi')
|
||||||
|
|
||||||
|
context = dict(
|
||||||
|
upstream=upstream,
|
||||||
|
selected_tab='main',
|
||||||
|
modems=mc.keys()
|
||||||
|
)
|
||||||
|
return await render(req, 'routing_main', title=lang('routing'), context=context)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/routing/rules.cgi')
|
||||||
|
async def routing_rules(req: web.Request):
|
||||||
|
mc = ModemsConfig()
|
||||||
|
|
||||||
|
action = req.query.get('action', None)
|
||||||
|
error = req.query.get('error', None)
|
||||||
|
set_name = req.query.get('set', None)
|
||||||
|
ip = req.query.get('ip', None)
|
||||||
|
|
||||||
|
def validate_input():
|
||||||
|
# validate set
|
||||||
|
if not set_name or set_name not in mc:
|
||||||
|
raise ValueError(f'invalid set \'{set_name}\'')
|
||||||
|
|
||||||
|
# validate ip
|
||||||
|
if not isinstance(ip, str):
|
||||||
|
raise ValueError('invalid ip')
|
||||||
|
|
||||||
|
slash_pos = None
|
||||||
|
try:
|
||||||
|
slash_pos = ip.index('/')
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if slash_pos is not None:
|
||||||
|
ip_without_mask = ip[0:slash_pos]
|
||||||
|
else:
|
||||||
|
ip_without_mask = ip
|
||||||
|
if not validate_ipv4(ip_without_mask):
|
||||||
|
raise ValueError(f'invalid ip \'{ip}\'')
|
||||||
|
|
||||||
|
base_url = '/routing/rules.cgi'
|
||||||
|
if action in ('add', 'del'):
|
||||||
|
try:
|
||||||
|
validate_input()
|
||||||
|
except ValueError as e:
|
||||||
|
raise web.HTTPFound(f'{base_url}?error='+quote_plus(str(e)))
|
||||||
|
f = getattr(openwrt, f'ipset_{action}')
|
||||||
|
output = f(set_name, ip)
|
||||||
|
url = base_url
|
||||||
|
if output != '':
|
||||||
|
url += '?error='+quote_plus(output)
|
||||||
|
raise web.HTTPFound(url)
|
||||||
|
|
||||||
|
ipsets = openwrt.ipset_list_all()
|
||||||
|
context = dict(
|
||||||
|
sets=ipsets,
|
||||||
|
selected_tab='rules',
|
||||||
|
error=error
|
||||||
|
)
|
||||||
|
return await render(req, 'routing_rules',
|
||||||
|
title=lang('routing') + ' // ' + lang('routing_rules'),
|
||||||
|
context=context)
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get('/routing/dhcp.cgi')
|
||||||
|
async def routing_dhcp(req: web.Request):
|
||||||
|
leases = openwrt.get_dhcp_leases()
|
||||||
|
return await render(req, 'routing_dhcp',
|
||||||
|
title=lang('routing') + ' // DHCP',
|
||||||
|
context=dict(leases=leases, selected_tab='dhcp'))
|
||||||
|
|
||||||
|
|
||||||
|
def init_web_app(app: web.Application):
|
||||||
|
aiohttp_jinja2.setup(
|
||||||
|
app,
|
||||||
|
loader=jinja2.FileSystemLoader(homekit_path('web', 'kbn_templates')),
|
||||||
|
autoescape=jinja2.select_autoescape(['html', 'xml']),
|
||||||
|
)
|
||||||
|
env = aiohttp_jinja2.get_env(app)
|
||||||
|
|
||||||
|
def filter_lang(key, unit='web_kbn'):
|
||||||
|
strings = Translation(unit)
|
||||||
|
if isinstance(key, str) and '.' in key:
|
||||||
|
return strings.get().get(key)
|
||||||
|
else:
|
||||||
return strings.get()[key]
|
return strings.get()[key]
|
||||||
|
|
||||||
env.filters['tojson'] = lambda obj: json.dumps(obj, separators=(',', ':'), default=json_serial)
|
env.filters['tojson'] = lambda obj: json.dumps(obj, separators=(',', ':'), default=json_serial)
|
||||||
env.filters['lang'] = filter_lang
|
env.filters['lang'] = filter_lang
|
||||||
|
|
||||||
self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets'))
|
app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets'))
|
||||||
|
|
||||||
self.get('/main.cgi', self.index)
|
|
||||||
|
|
||||||
self.get('/modems.cgi', self.modems)
|
|
||||||
self.get('/modems/info.ajx', self.modems_ajx)
|
|
||||||
self.get('/modems/verbose.cgi', self.modems_verbose)
|
|
||||||
|
|
||||||
self.get('/inverter.cgi', self.inverter)
|
|
||||||
self.get('/inverter.ajx', self.inverter_ajx)
|
|
||||||
self.get('/pump.cgi', self.pump)
|
|
||||||
self.get('/sms.cgi', self.sms)
|
|
||||||
self.post('/sms.cgi', self.sms_post)
|
|
||||||
|
|
||||||
self.get('/cams.cgi', self.cams)
|
|
||||||
|
|
||||||
async def render_page(self,
|
|
||||||
req: http.Request,
|
|
||||||
template_name: str,
|
|
||||||
title: Optional[str] = None,
|
|
||||||
context: Optional[dict] = None,
|
|
||||||
assets: Optional[list] = None):
|
|
||||||
if context is None:
|
|
||||||
context = {}
|
|
||||||
context = {
|
|
||||||
**context,
|
|
||||||
'head_static': get_head_static(assets)
|
|
||||||
}
|
|
||||||
if title is not None:
|
|
||||||
context['title'] = title
|
|
||||||
response = aiohttp_jinja2.render_template(template_name+'.j2', req, context=context)
|
|
||||||
return response
|
|
||||||
|
|
||||||
async def index(self, req: http.Request):
|
|
||||||
ctx = {}
|
|
||||||
for k in 'inverter', 'sensors':
|
|
||||||
ctx[f'{k}_grafana_url'] = config.app_config[f'{k}_grafana_url']
|
|
||||||
|
|
||||||
cc = IpcamConfig()
|
|
||||||
ctx['camzones'] = cc['zones'].keys()
|
|
||||||
ctx['allcams'] = cc.get_all_cam_names()
|
|
||||||
|
|
||||||
return await self.render_page(req, 'index',
|
|
||||||
title="Home web site",
|
|
||||||
context=ctx)
|
|
||||||
|
|
||||||
async def modems(self, req: http.Request):
|
|
||||||
return await self.render_page(req, 'modems',
|
|
||||||
title='Состояние модемов',
|
|
||||||
context=dict(modems=ModemsConfig()))
|
|
||||||
|
|
||||||
async def modems_ajx(self, req: http.Request):
|
|
||||||
mc = ModemsConfig()
|
|
||||||
modem = req.query.get('id', None)
|
|
||||||
if modem not in mc.keys():
|
|
||||||
raise ValueError('invalid modem id')
|
|
||||||
|
|
||||||
modem_cfg = mc.get(modem)
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
modem_data = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg))
|
|
||||||
|
|
||||||
html = aiohttp_jinja2.render_string('modem_data.j2', req, context=dict(
|
|
||||||
modem_data=modem_data,
|
|
||||||
modem=modem
|
|
||||||
))
|
|
||||||
|
|
||||||
return self.ok({'html': html})
|
|
||||||
|
|
||||||
async def modems_verbose(self, req: http.Request):
|
|
||||||
modem = req.query.get('id', None)
|
|
||||||
if modem not in ModemsConfig().keys():
|
|
||||||
raise ValueError('invalid modem id')
|
|
||||||
|
|
||||||
modem_cfg = ModemsConfig().get(modem)
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
signal, status, traffic, device, dialup_conn = await loop.run_in_executor(None, lambda: get_modem_data(modem_cfg, True))
|
|
||||||
data = [
|
|
||||||
['Signal', signal],
|
|
||||||
['Connection', status],
|
|
||||||
['Traffic', traffic],
|
|
||||||
['Device info', device],
|
|
||||||
['Dialup connection', dialup_conn]
|
|
||||||
]
|
|
||||||
|
|
||||||
modem_name = ModemsConfig().getfullname(modem)
|
|
||||||
return await self.render_page(req, 'modem_verbose',
|
|
||||||
title=f'Подробная информация о модеме "{modem_name}"',
|
|
||||||
context=dict(data=data, modem_name=modem_name))
|
|
||||||
|
|
||||||
async def sms(self, req: http.Request):
|
|
||||||
modem = req.query.get('id', list(ModemsConfig().keys())[0])
|
|
||||||
is_outbox = int(req.query.get('outbox', 0)) == 1
|
|
||||||
error = req.query.get('error', None)
|
|
||||||
sent = int(req.query.get('sent', 0)) == 1
|
|
||||||
|
|
||||||
cl = get_modem_client(ModemsConfig()[modem])
|
|
||||||
messages = cl.sms_list(1, 20, is_outbox)
|
|
||||||
return await self.render_page(req, 'sms',
|
|
||||||
title=f"SMS-сообщения ({'исходящие' if is_outbox else 'входящие'}, {modem})",
|
|
||||||
context=dict(
|
|
||||||
modems=ModemsConfig(),
|
|
||||||
selected_modem=modem,
|
|
||||||
is_outbox=is_outbox,
|
|
||||||
error=error,
|
|
||||||
is_sent=sent,
|
|
||||||
messages=messages
|
|
||||||
))
|
|
||||||
|
|
||||||
async def sms_post(self, req: http.Request):
|
|
||||||
modem = req.query.get('id', list(ModemsConfig().keys())[0])
|
|
||||||
is_outbox = int(req.query.get('outbox', 0)) == 1
|
|
||||||
|
|
||||||
fd = await req.post()
|
|
||||||
phone = fd.get('phone', None)
|
|
||||||
text = fd.get('text', None)
|
|
||||||
|
|
||||||
return_url = f'/sms.cgi?id={modem}&outbox={int(is_outbox)}'
|
|
||||||
phone = re.sub('\s+', '', phone)
|
|
||||||
|
|
||||||
if len(phone) > 4:
|
|
||||||
country = None
|
|
||||||
if not phone.startswith('+'):
|
|
||||||
country = 'RU'
|
|
||||||
number = phonenumbers.parse(phone, country)
|
|
||||||
if not phonenumbers.is_valid_number(number):
|
|
||||||
raise HTTPFound(f'{return_url}&error=Неверный+номер')
|
|
||||||
phone = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
|
|
||||||
|
|
||||||
cl = get_modem_client(ModemsConfig()[modem])
|
|
||||||
cl.sms_send(phone, text)
|
|
||||||
raise HTTPFound(return_url)
|
|
||||||
|
|
||||||
async def inverter(self, req: http.Request):
|
|
||||||
action = req.query.get('do', None)
|
|
||||||
if action == 'set-osp':
|
|
||||||
val = req.query.get('value')
|
|
||||||
if val not in ('sub', 'sbu'):
|
|
||||||
raise ValueError('invalid osp value')
|
|
||||||
cl = get_inverter_client()
|
|
||||||
cl.exec('set-output-source-priority',
|
|
||||||
arguments=(val.upper(),))
|
|
||||||
raise HTTPFound('/inverter.cgi')
|
|
||||||
|
|
||||||
status, rated, html = await asyncio.get_event_loop().run_in_executor(None, get_inverter_data)
|
|
||||||
return await self.render_page(req, 'inverter',
|
|
||||||
title='Инвертор',
|
|
||||||
context=dict(status=status, rated=rated, html=html))
|
|
||||||
|
|
||||||
async def inverter_ajx(self, req: http.Request):
|
|
||||||
status, rated, html = await asyncio.get_event_loop().run_in_executor(None, get_inverter_data)
|
|
||||||
return self.ok({'html': html})
|
|
||||||
|
|
||||||
async def pump(self, req: http.Request):
|
|
||||||
# TODO
|
|
||||||
# these are blocking calls
|
|
||||||
# should be rewritten using aio
|
|
||||||
|
|
||||||
cl = get_pump_client()
|
|
||||||
|
|
||||||
action = req.query.get('set', None)
|
|
||||||
if action in ('on', 'off'):
|
|
||||||
getattr(cl, action)()
|
|
||||||
raise HTTPFound('/pump.cgi')
|
|
||||||
|
|
||||||
status = cl.status()
|
|
||||||
return await self.render_page(req, 'pump',
|
|
||||||
title='Насос',
|
|
||||||
context=dict(status=status))
|
|
||||||
|
|
||||||
async def cams(self, req: http.Request):
|
|
||||||
cc = IpcamConfig()
|
|
||||||
|
|
||||||
cam = req.query.get('id', None)
|
|
||||||
zone = req.query.get('zone', None)
|
|
||||||
debug_hls = bool(req.query.get('debug_hls', False))
|
|
||||||
debug_video_events = bool(req.query.get('debug_video_events', False))
|
|
||||||
|
|
||||||
if cam is not None:
|
|
||||||
if not cc.has_camera(int(cam)):
|
|
||||||
raise ValueError('invalid camera id')
|
|
||||||
cams = [int(cam)]
|
|
||||||
mode = {'type': 'single', 'cam': cam}
|
|
||||||
|
|
||||||
elif zone is not None:
|
|
||||||
if not cc.has_zone(zone):
|
|
||||||
raise ValueError('invalid zone')
|
|
||||||
cams = cc['zones'][zone]
|
|
||||||
mode = {'type': 'zone', 'zone': zone}
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise HTTPBadRequest(text='no camera id or zone found')
|
|
||||||
|
|
||||||
js_config = {
|
|
||||||
'host': config.app_config['cam_hls_host'],
|
|
||||||
'proto': 'http',
|
|
||||||
'cams': cams,
|
|
||||||
'hlsConfig': {
|
|
||||||
'opts': {
|
|
||||||
'startPosition': -1,
|
|
||||||
# https://github.com/video-dev/hls.js/issues/3884#issuecomment-842380784
|
|
||||||
'liveSyncDuration': 2,
|
|
||||||
'liveMaxLatencyDuration': 3,
|
|
||||||
'maxLiveSyncPlaybackRate': 2,
|
|
||||||
'liveDurationInfinity': True
|
|
||||||
},
|
|
||||||
'debugVideoEvents': debug_video_events,
|
|
||||||
'debug': debug_hls
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await self.render_page(req, 'cams',
|
|
||||||
title='Камеры',
|
|
||||||
assets=['hls.js'],
|
|
||||||
context=dict(
|
|
||||||
mode=mode,
|
|
||||||
js_config=js_config,
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
config.load_app(WebKbnConfig)
|
config.load_app(WebKbnConfig)
|
||||||
|
http.serve(addr=config.app_config['listen_addr'],
|
||||||
server = WebSite(config.app_config['listen_addr'])
|
routes=routes,
|
||||||
server.run()
|
before_start=init_web_app)
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
## Dependencies
|
|
||||||
|
|
||||||
```
|
|
||||||
apt install nginx-extras php-fpm php-mbstring php-sqlite3 php-curl php-simplexml php-gmp composer
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ class BaseConfigUnit(ABC):
|
|||||||
return self._data
|
return self._data
|
||||||
|
|
||||||
cur = self._data
|
cur = self._data
|
||||||
pts = key.split('.')
|
pts = str(key).split('.')
|
||||||
for i in range(len(pts)):
|
for i in range(len(pts)):
|
||||||
k = pts[i]
|
k = pts[i]
|
||||||
if i < len(pts)-1:
|
if i < len(pts)-1:
|
||||||
|
@ -1,2 +1 @@
|
|||||||
from .http import serve, ok, routes, HTTPServer, HTTPMethod
|
from .http import serve, ajax_ok, HTTPMethod
|
||||||
from aiohttp.web import FileResponse, StreamResponse, Request, Response
|
|
||||||
|
@ -1,17 +1,46 @@
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import html
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.web import Response, HTTPFound
|
from aiohttp.web import HTTPFound
|
||||||
from aiohttp.web_exceptions import HTTPNotFound
|
from aiohttp.web_exceptions import HTTPNotFound
|
||||||
|
|
||||||
from ..util import stringify, format_tb, Addr
|
from ..util import stringify, format_tb, Addr
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_error(error_type, error_message, traceback=None, code=500):
|
||||||
|
traceback_html = ''
|
||||||
|
if traceback:
|
||||||
|
traceback = '\n\n'.join(traceback)
|
||||||
|
traceback_html = f"""
|
||||||
|
<div class="error_traceback">
|
||||||
|
<div class="error_title">Traceback</div>
|
||||||
|
<div class="error_traceback_content">{html.escape(traceback)}</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
buf = f"""
|
||||||
|
<!doctype html>
|
||||||
|
<html lang=en>
|
||||||
|
<head>
|
||||||
|
<title>Error: {html.escape(error_type)}</title>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/assets/error_page.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="error_title">{html.escape(error_type)}</div>
|
||||||
|
<div class="error_message">{html.escape(error_message)}</div>
|
||||||
|
{traceback_html}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
return web.Response(text=buf, status=code, content_type='text/html')
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def errors_handler_middleware(request, handler):
|
async def errors_handler_middleware(request, handler):
|
||||||
try:
|
try:
|
||||||
@ -19,97 +48,56 @@ async def errors_handler_middleware(request, handler):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
except HTTPNotFound:
|
except HTTPNotFound:
|
||||||
return web.json_response({'error': 'not found'}, status=404)
|
return _render_error(
|
||||||
|
error_type='Not Found',
|
||||||
|
error_message='The page you requested has not been found.',
|
||||||
|
code=404
|
||||||
|
)
|
||||||
|
|
||||||
except HTTPFound as exc:
|
except HTTPFound as exc:
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_logger.exception(exc)
|
_logger.exception(exc)
|
||||||
data = {
|
return _render_error(
|
||||||
'error': exc.__class__.__name__,
|
error_type=exc.__class__.__name__,
|
||||||
'message': exc.message if hasattr(exc, 'message') else str(exc)
|
error_message=exc.message if hasattr(exc, 'message') else str(exc),
|
||||||
}
|
traceback=format_tb(exc)
|
||||||
tb = format_tb(exc)
|
)
|
||||||
if tb:
|
|
||||||
data['stacktrace'] = tb
|
|
||||||
|
|
||||||
return web.json_response(data, status=500)
|
|
||||||
|
|
||||||
|
|
||||||
def serve(addr: Addr, route_table: web.RouteTableDef, handle_signals: bool = True):
|
def serve(addr: Addr, before_start=None, handle_signals=True, routes=None, event_loop=None):
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.add_routes(route_table)
|
|
||||||
app.middlewares.append(errors_handler_middleware)
|
app.middlewares.append(errors_handler_middleware)
|
||||||
|
|
||||||
|
if routes is not None:
|
||||||
|
app.add_routes(routes)
|
||||||
|
|
||||||
|
if callable(before_start):
|
||||||
|
before_start(app)
|
||||||
|
|
||||||
|
if not event_loop:
|
||||||
|
event_loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
runner = web.AppRunner(app, handle_signals=handle_signals)
|
||||||
|
event_loop.run_until_complete(runner.setup())
|
||||||
|
|
||||||
host, port = addr
|
host, port = addr
|
||||||
|
site = web.TCPSite(runner, host=host, port=port)
|
||||||
|
event_loop.run_until_complete(site.start())
|
||||||
|
|
||||||
web.run_app(app,
|
_logger.info(f'Server started at http://{host}:{port}')
|
||||||
host=host,
|
|
||||||
port=port,
|
event_loop.run_forever()
|
||||||
handle_signals=handle_signals)
|
|
||||||
|
|
||||||
|
|
||||||
def routes() -> web.RouteTableDef:
|
def ajax_ok(data=None):
|
||||||
return web.RouteTableDef()
|
|
||||||
|
|
||||||
|
|
||||||
def ok(data=None):
|
|
||||||
if data is None:
|
if data is None:
|
||||||
data = 1
|
data = 1
|
||||||
response = {'response': data}
|
response = {'response': data}
|
||||||
return web.json_response(response, dumps=stringify)
|
return web.json_response(response, dumps=stringify)
|
||||||
|
|
||||||
|
|
||||||
class HTTPServer:
|
|
||||||
def __init__(self, addr: Addr, handle_errors=True):
|
|
||||||
self.addr = addr
|
|
||||||
self.app = web.Application()
|
|
||||||
self.logger = logging.getLogger(self.__class__.__name__)
|
|
||||||
|
|
||||||
if handle_errors:
|
|
||||||
self.app.middlewares.append(errors_handler_middleware)
|
|
||||||
|
|
||||||
def _add_route(self,
|
|
||||||
method: str,
|
|
||||||
path: str,
|
|
||||||
handler: callable):
|
|
||||||
self.app.router.add_routes([getattr(web, method)(path, handler)])
|
|
||||||
|
|
||||||
def get(self, path, handler):
|
|
||||||
self._add_route('get', path, handler)
|
|
||||||
|
|
||||||
def post(self, path, handler):
|
|
||||||
self._add_route('post', path, handler)
|
|
||||||
|
|
||||||
def put(self, path, handler):
|
|
||||||
self._add_route('put', path, handler)
|
|
||||||
|
|
||||||
def delete(self, path, handler):
|
|
||||||
self._add_route('delete', path, handler)
|
|
||||||
|
|
||||||
def run(self, event_loop=None, handle_signals=True):
|
|
||||||
if not event_loop:
|
|
||||||
event_loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
runner = web.AppRunner(self.app, handle_signals=handle_signals)
|
|
||||||
event_loop.run_until_complete(runner.setup())
|
|
||||||
|
|
||||||
host, port = self.addr
|
|
||||||
site = web.TCPSite(runner, host=host, port=port)
|
|
||||||
event_loop.run_until_complete(site.start())
|
|
||||||
|
|
||||||
self.logger.info(f'Server started at http://{host}:{port}')
|
|
||||||
|
|
||||||
event_loop.run_forever()
|
|
||||||
|
|
||||||
def ok(self, data=None):
|
|
||||||
return ok(data)
|
|
||||||
|
|
||||||
def plain(self, text: str):
|
|
||||||
return Response(text=text, content_type='text/plain')
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPMethod(Enum):
|
class HTTPMethod(Enum):
|
||||||
GET = 'GET'
|
GET = 'GET'
|
||||||
POST = 'POST'
|
POST = 'POST'
|
||||||
|
@ -33,12 +33,12 @@ class MediaNodeServer(http.HTTPServer):
|
|||||||
raise ValueError(f'invalid duration: max duration is {max}')
|
raise ValueError(f'invalid duration: max duration is {max}')
|
||||||
|
|
||||||
record_id = self.recorder.record(duration)
|
record_id = self.recorder.record(duration)
|
||||||
return http.ok({'id': record_id})
|
return http.ajax_ok({'id': record_id})
|
||||||
|
|
||||||
async def record_info(self, request: http.Request):
|
async def record_info(self, request: http.Request):
|
||||||
record_id = int(request.match_info['id'])
|
record_id = int(request.match_info['id'])
|
||||||
info = self.recorder.get_info(record_id)
|
info = self.recorder.get_info(record_id)
|
||||||
return http.ok(info.as_dict())
|
return http.ajax_ok(info.as_dict())
|
||||||
|
|
||||||
async def record_forget(self, request: http.Request):
|
async def record_forget(self, request: http.Request):
|
||||||
record_id = int(request.match_info['id'])
|
record_id = int(request.match_info['id'])
|
||||||
@ -47,7 +47,7 @@ class MediaNodeServer(http.HTTPServer):
|
|||||||
assert info.status in (RecordStatus.FINISHED, RecordStatus.ERROR), f"can't forget: record status is {info.status}"
|
assert info.status in (RecordStatus.FINISHED, RecordStatus.ERROR), f"can't forget: record status is {info.status}"
|
||||||
|
|
||||||
self.recorder.forget(record_id)
|
self.recorder.forget(record_id)
|
||||||
return http.ok()
|
return http.ajax_ok()
|
||||||
|
|
||||||
async def record_download(self, request: http.Request):
|
async def record_download(self, request: http.Request):
|
||||||
record_id = int(request.match_info['id'])
|
record_id = int(request.match_info['id'])
|
||||||
@ -64,7 +64,7 @@ class MediaNodeServer(http.HTTPServer):
|
|||||||
if extended:
|
if extended:
|
||||||
files = list(map(lambda file: file.__dict__(), files))
|
files = list(map(lambda file: file.__dict__(), files))
|
||||||
|
|
||||||
return http.ok({
|
return http.ajax_ok({
|
||||||
'files': files
|
'files': files
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ class MediaNodeServer(http.HTTPServer):
|
|||||||
raise ValueError(f'file {file} not found')
|
raise ValueError(f'file {file} not found')
|
||||||
|
|
||||||
self.storage.delete(file)
|
self.storage.delete(file)
|
||||||
return http.ok()
|
return http.ajax_ok()
|
||||||
|
|
||||||
async def storage_download(self, request):
|
async def storage_download(self, request):
|
||||||
file_id = request.query['file_id']
|
file_id = request.query['file_id']
|
||||||
|
9
include/py/homekit/openwrt/__init__.py
Normal file
9
include/py/homekit/openwrt/__init__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from .config import OpenwrtConfig
|
||||||
|
from .openwrt import (
|
||||||
|
ipset_list_all,
|
||||||
|
ipset_add,
|
||||||
|
ipset_del,
|
||||||
|
set_upstream,
|
||||||
|
get_default_route,
|
||||||
|
get_dhcp_leases
|
||||||
|
)
|
14
include/py/homekit/openwrt/config.py
Normal file
14
include/py/homekit/openwrt/config.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from homekit.config import ConfigUnit
|
||||||
|
|
||||||
|
|
||||||
|
class OpenwrtConfig(ConfigUnit):
|
||||||
|
NAME = 'openwrt'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def schema(cls) -> Optional[dict]:
|
||||||
|
return {
|
||||||
|
'ip': cls._addr_schema(only_ip=True, required=True),
|
||||||
|
'command_id': {'type': 'string', 'required': True}
|
||||||
|
}
|
90
include/py/homekit/openwrt/openwrt.py
Normal file
90
include/py/homekit/openwrt/openwrt.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from collections import namedtuple
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
from .config import OpenwrtConfig
|
||||||
|
from ..modem.config import ModemsConfig
|
||||||
|
|
||||||
|
DHCPLease = namedtuple('DHCPLease', 'time, time_s, mac, ip, hostname')
|
||||||
|
_config = OpenwrtConfig()
|
||||||
|
_modems_config = ModemsConfig()
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def ipset_list_all() -> list:
|
||||||
|
args = ['ipset-list-all']
|
||||||
|
args += _modems_config.keys()
|
||||||
|
lines = _to_list(_call(args))
|
||||||
|
sets = {}
|
||||||
|
cur_set = None
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith('>'):
|
||||||
|
cur_set = line[1:]
|
||||||
|
if cur_set not in sets:
|
||||||
|
sets[cur_set] = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
if cur_set is None:
|
||||||
|
_logger.error('ipset_list_all: cur_set is not set')
|
||||||
|
continue
|
||||||
|
|
||||||
|
sets[cur_set].append(line)
|
||||||
|
|
||||||
|
return sets
|
||||||
|
|
||||||
|
|
||||||
|
def ipset_add(set_name: str, ip: str):
|
||||||
|
return _call(['ipset-add', set_name, ip])
|
||||||
|
|
||||||
|
|
||||||
|
def ipset_del(set_name: str, ip: str):
|
||||||
|
return _call(['ipset-del', set_name, ip])
|
||||||
|
|
||||||
|
|
||||||
|
def set_upstream(ip: str):
|
||||||
|
return _call(['homekit-set-default-upstream', ip])
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_route():
|
||||||
|
return _call(['get-default-route'])
|
||||||
|
|
||||||
|
|
||||||
|
def get_dhcp_leases() -> list[DHCPLease]:
|
||||||
|
return list(map(lambda item: _to_dhcp_lease(item), _to_list(_call(['dhcp-leases']))))
|
||||||
|
|
||||||
|
|
||||||
|
def _call(arguments: list[str]) -> str:
|
||||||
|
url = _get_link(arguments)
|
||||||
|
r = requests.get(url)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_link(arguments: list[str]) -> str:
|
||||||
|
url = f'http://{_config["ip"]}/cgi-bin/luci/command/{_config["command_id"]}'
|
||||||
|
if arguments:
|
||||||
|
url += '/'
|
||||||
|
url += '%20'.join(list(map(lambda arg: quote_plus(arg.replace('/', '_')), arguments)))
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def _to_list(s: str) -> list:
|
||||||
|
return [] if s == '' else s.split('\n')
|
||||||
|
|
||||||
|
|
||||||
|
def _to_dhcp_lease(s: str) -> DHCPLease:
|
||||||
|
words = s.split(' ')
|
||||||
|
time = int(words.pop(0))
|
||||||
|
mac = words.pop(0)
|
||||||
|
ip = words.pop(0)
|
||||||
|
words.pop()
|
||||||
|
hostname = (' '.join(words)).strip()
|
||||||
|
if not hostname or hostname == '*':
|
||||||
|
hostname = '?'
|
||||||
|
return DHCPLease(time=time,
|
||||||
|
time_s=datetime.fromtimestamp(time).strftime('%d %b, %H:%M:%S'),
|
||||||
|
mac=mac,
|
||||||
|
ip=ip,
|
||||||
|
hostname=hostname)
|
@ -3,6 +3,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
from ..database.sqlite import SQLiteBase
|
from ..database.sqlite import SQLiteBase
|
||||||
from ..config import config
|
from ..config import config
|
||||||
from .. import http
|
from .. import http
|
||||||
@ -108,21 +109,21 @@ class SoundSensorServer:
|
|||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
|
|
||||||
def run_guard_server(self):
|
def run_guard_server(self):
|
||||||
routes = http.routes()
|
routes = web.RouteTableDef()
|
||||||
|
|
||||||
@routes.post('/guard/enable')
|
@routes.post('/guard/enable')
|
||||||
async def guard_enable(request):
|
async def guard_enable(request):
|
||||||
self.set_recording(True)
|
self.set_recording(True)
|
||||||
return http.ok()
|
return http.ajax_ok()
|
||||||
|
|
||||||
@routes.post('/guard/disable')
|
@routes.post('/guard/disable')
|
||||||
async def guard_disable(request):
|
async def guard_disable(request):
|
||||||
self.set_recording(False)
|
self.set_recording(False)
|
||||||
return http.ok()
|
return http.ajax_ok()
|
||||||
|
|
||||||
@routes.get('/guard/status')
|
@routes.get('/guard/status')
|
||||||
async def guard_status(request):
|
async def guard_status(request):
|
||||||
return http.ok({'enabled': self.is_recording_enabled()})
|
return http.ajax_ok({'enabled': self.is_recording_enabled()})
|
||||||
|
|
||||||
asyncio.set_event_loop(asyncio.new_event_loop()) # need to create new event loop in new thread
|
asyncio.set_event_loop(asyncio.new_event_loop()) # need to create new event loop in new thread
|
||||||
http.serve(self.addr, routes, handle_signals=False) # handle_signals=True doesn't work in separate thread
|
http.serve(self.addr, handle_signals=False) # handle_signals=True doesn't work in separate thread
|
||||||
|
@ -105,6 +105,11 @@ class Addr:
|
|||||||
yield self.host
|
yield self.host
|
||||||
yield self.port
|
yield self.port
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, str):
|
||||||
|
return self.__str__() == other
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/312443/how-do-you-split-a-list-into-evenly-sized-chunks
|
# https://stackoverflow.com/questions/312443/how-do-you-split-a-list-into-evenly-sized-chunks
|
||||||
def chunks(lst, n):
|
def chunks(lst, n):
|
||||||
|
@ -1,139 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class MyOpenWrtUtils {
|
|
||||||
|
|
||||||
// public static function getRoutingTable(?string $table = null): array {
|
|
||||||
// $arguments = ['route-show'];
|
|
||||||
// if ($table)
|
|
||||||
// $arguments[] = $table;
|
|
||||||
//
|
|
||||||
// return self::toList(self::run($arguments));
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public static function getRoutingRules(): array {
|
|
||||||
// return self::toList(self::run(['rule-show']));
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public static function ipsetList(string $set_name): array {
|
|
||||||
// return self::toList(self::run(['ipset-list', $set_name]));
|
|
||||||
// }
|
|
||||||
|
|
||||||
public static function ipsetListAll(): array {
|
|
||||||
global $config;
|
|
||||||
|
|
||||||
$args = ['ipset-list-all'];
|
|
||||||
$args = array_merge($args, array_keys($config['modems']));
|
|
||||||
|
|
||||||
$lines = self::toList(self::run($args));
|
|
||||||
|
|
||||||
$sets = [];
|
|
||||||
$cur_set = null;
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
if (startsWith($line, '>')) {
|
|
||||||
$cur_set = substr($line, 1);
|
|
||||||
if (!isset($sets[$cur_set]))
|
|
||||||
$sets[$cur_set] = [];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_null($cur_set)) {
|
|
||||||
debugError(__METHOD__.': cur_set is not set');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sets[$cur_set][] = $line;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $sets;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function ipsetAdd(string $set_name, string $ip) {
|
|
||||||
return self::run(['ipset-add', $set_name, $ip]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function ipsetDel(string $set_name, string $ip) {
|
|
||||||
return self::run(['ipset-del', $set_name, $ip]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getDHCPLeases(): array {
|
|
||||||
$list = self::toList(self::run(['dhcp-leases']));
|
|
||||||
$list = array_map('self::toDHCPLease', $list);
|
|
||||||
return $list;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function setUpstream(string $ip) {
|
|
||||||
return self::run(['homekit-set-default-upstream', $ip]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getDefaultRoute() {
|
|
||||||
return self::run(['get-default-route']);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// http functions
|
|
||||||
//
|
|
||||||
|
|
||||||
private static function run(array $arguments) {
|
|
||||||
$url = self::getLink($arguments);
|
|
||||||
|
|
||||||
$ch = curl_init();
|
|
||||||
curl_setopt($ch, CURLOPT_URL, $url);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
$body = curl_exec($ch);
|
|
||||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
if ($code != 200)
|
|
||||||
throw new Exception(__METHOD__.': http code '.$code);
|
|
||||||
|
|
||||||
curl_close($ch);
|
|
||||||
return trim($body);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function getLink($arguments) {
|
|
||||||
global $config;
|
|
||||||
|
|
||||||
$url = 'http://'.$config['openwrt_ip'].'/cgi-bin/luci/command/cfg099944';
|
|
||||||
if (!empty($arguments)) {
|
|
||||||
$arguments = array_map(function($arg) {
|
|
||||||
$arg = str_replace('/', '_', $arg);
|
|
||||||
return urlencode($arg);
|
|
||||||
}, $arguments);
|
|
||||||
$arguments = implode('%20', $arguments);
|
|
||||||
|
|
||||||
$url .= '/';
|
|
||||||
$url .= $arguments;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $url;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// parsing functions
|
|
||||||
//
|
|
||||||
|
|
||||||
private static function toList(string $s): array {
|
|
||||||
if ($s == '')
|
|
||||||
return [];
|
|
||||||
return explode("\n", $s);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function toDHCPLease(string $s): array {
|
|
||||||
$words = explode(' ', $s);
|
|
||||||
$time = array_shift($words);
|
|
||||||
$mac = array_shift($words);
|
|
||||||
$ip = array_shift($words);
|
|
||||||
array_pop($words);
|
|
||||||
$hostname = trim(implode(' ', $words));
|
|
||||||
if (!$hostname || $hostname == '*')
|
|
||||||
$hostname = '?';
|
|
||||||
return [
|
|
||||||
'time' => $time,
|
|
||||||
'time_s' => date('d M, H:i:s', $time),
|
|
||||||
'mac' => $mac,
|
|
||||||
'ip' => $ip,
|
|
||||||
'hostname' => $hostname
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class MySimpleSocketClient {
|
|
||||||
|
|
||||||
protected $sock;
|
|
||||||
|
|
||||||
public function __construct(string $host, int $port)
|
|
||||||
{
|
|
||||||
if (($socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false)
|
|
||||||
throw new Exception("socket_create() failed: ".$this->getSocketError());
|
|
||||||
|
|
||||||
$this->sock = $socket;
|
|
||||||
|
|
||||||
if ((socket_connect($socket, $host, $port)) === false)
|
|
||||||
throw new Exception("socket_connect() failed: ".$this->getSocketError());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __destruct()
|
|
||||||
{
|
|
||||||
$this->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function send(string $data)
|
|
||||||
{
|
|
||||||
$data .= "\r\n";
|
|
||||||
$remained = strlen($data);
|
|
||||||
|
|
||||||
while ($remained > 0) {
|
|
||||||
$result = socket_write($this->sock, $data);
|
|
||||||
if ($result === false)
|
|
||||||
throw new Exception(__METHOD__ . ": socket_write() failed: ".$this->getSocketError());
|
|
||||||
|
|
||||||
$remained -= $result;
|
|
||||||
if ($remained > 0)
|
|
||||||
$data = substr($data, $result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function recv()
|
|
||||||
{
|
|
||||||
$recv_buf = '';
|
|
||||||
$buf = '';
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
$result = socket_recv($this->sock, $recv_buf, 1024, 0);
|
|
||||||
if ($result === false)
|
|
||||||
throw new Exception(__METHOD__ . ": socket_recv() failed: " . $this->getSocketError());
|
|
||||||
|
|
||||||
// peer disconnected
|
|
||||||
if ($result === 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
$buf .= $recv_buf;
|
|
||||||
if (endsWith($buf, "\r\n"))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return trim($buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close connection.
|
|
||||||
*/
|
|
||||||
public function close()
|
|
||||||
{
|
|
||||||
if (!$this->sock)
|
|
||||||
return;
|
|
||||||
|
|
||||||
socket_close($this->sock);
|
|
||||||
$this->sock = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
protected function getSocketError(): string
|
|
||||||
{
|
|
||||||
$sle_args = [];
|
|
||||||
if ($this->sock !== null)
|
|
||||||
$sle_args[] = $this->sock;
|
|
||||||
return socket_strerror(socket_last_error(...$sle_args));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class TemphumdClient extends MySimpleSocketClient {
|
|
||||||
|
|
||||||
public string $name;
|
|
||||||
public float $temp;
|
|
||||||
public float $humidity;
|
|
||||||
public ?int $flags;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function __construct(string $host, int $port, string $name, ?int $flags = null) {
|
|
||||||
parent::__construct($host, $port);
|
|
||||||
$this->name = $name;
|
|
||||||
$this->flags = $flags;
|
|
||||||
|
|
||||||
socket_set_timeout($this->sock, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function readSensor(): void {
|
|
||||||
$this->send('read');
|
|
||||||
|
|
||||||
$data = jsonDecode($this->recv());
|
|
||||||
|
|
||||||
$temp = round((float)$data['temp'], 3);
|
|
||||||
$hum = round((float)$data['humidity'], 3);
|
|
||||||
|
|
||||||
$this->temp = $temp;
|
|
||||||
$this->humidity = $hum;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hasTemperature(): bool {
|
|
||||||
return ($this->flags & config::TEMPHUMD_NO_TEMP) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hasHumidity(): bool {
|
|
||||||
return ($this->flags & config::TEMPHUMD_NO_HUM) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class User extends model {
|
|
||||||
|
|
||||||
const DB_TABLE = 'users';
|
|
||||||
|
|
||||||
public int $id;
|
|
||||||
public string $username;
|
|
||||||
public string $password;
|
|
||||||
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class auth {
|
|
||||||
|
|
||||||
public static ?User $authorizedUser = null;
|
|
||||||
|
|
||||||
const COOKIE_NAME = 'lws-auth';
|
|
||||||
|
|
||||||
public static function getToken(): ?string {
|
|
||||||
return $_COOKIE[self::COOKIE_NAME] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function setToken(string $token) {
|
|
||||||
setcookie_safe(self::COOKIE_NAME, $token);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function resetToken() {
|
|
||||||
if (!headers_sent())
|
|
||||||
unsetcookie(self::COOKIE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function id(bool $do_check = true): int {
|
|
||||||
if ($do_check)
|
|
||||||
self::check();
|
|
||||||
|
|
||||||
if (!self::$authorizedUser)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return self::$authorizedUser->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function check(?string $pwhash = null): bool {
|
|
||||||
if (self::$authorizedUser !== null)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// get auth token
|
|
||||||
if (!$pwhash)
|
|
||||||
$pwhash = self::getToken();
|
|
||||||
|
|
||||||
if (!is_string($pwhash))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// find session by given token
|
|
||||||
$user = users::getUserByPwhash($pwhash);
|
|
||||||
if (is_null($user)) {
|
|
||||||
self::resetToken();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
self::$authorizedUser = $user;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function logout() {
|
|
||||||
self::resetToken();
|
|
||||||
self::$authorizedUser = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class config {
|
|
||||||
|
|
||||||
const TEMPHUMD_NO_TEMP = 1 << 0;
|
|
||||||
const TEMPHUMD_NO_HUM = 1 << 1;
|
|
||||||
|
|
||||||
public static function get(string $key) {
|
|
||||||
global $config;
|
|
||||||
return is_callable($config[$key]) ? $config[$key]() : $config[$key];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class users {
|
|
||||||
|
|
||||||
public static function add(string $username, string $password): int {
|
|
||||||
$db = getDB();
|
|
||||||
$db->insert('users', [
|
|
||||||
'username' => $username,
|
|
||||||
'password' => pwhash($password)
|
|
||||||
]);
|
|
||||||
return $db->insertId();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function exists(string $username): bool {
|
|
||||||
$db = getDB();
|
|
||||||
$count = (int)$db->querySingle("SELECT COUNT(*) FROM users WHERE username=?", $username);
|
|
||||||
return $count > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function validatePassword(string $username, string $password): bool {
|
|
||||||
$db = getDB();
|
|
||||||
$row = $db->querySingleRow("SELECT * FROM users WHERE username=?", $username);
|
|
||||||
if (!$row)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return $row['password'] == pwhash($password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getUserByPwhash(string $pwhash): ?User {
|
|
||||||
$db = getDB();
|
|
||||||
$data = $db->querySingleRow("SELECT * FROM users WHERE password=?", $pwhash);
|
|
||||||
return $data ? new User($data) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function setPassword(int $id, string $new_password) {
|
|
||||||
getDB()->exec("UPDATE users SET password=? WHERE id=?", pwhash($new_password), $id);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,355 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// require_once 'engine/mysql.php';
|
|
||||||
|
|
||||||
class debug {
|
|
||||||
|
|
||||||
protected static $Types = [
|
|
||||||
1 => 'E_ERROR',
|
|
||||||
2 => 'E_WARNING',
|
|
||||||
4 => 'E_PARSE',
|
|
||||||
8 => 'E_NOTICE',
|
|
||||||
16 => 'E_CORE_ERROR',
|
|
||||||
32 => 'E_CORE_WARNING',
|
|
||||||
64 => 'E_COMPILE_ERROR',
|
|
||||||
128 => 'E_COMPILE_WARNING',
|
|
||||||
256 => 'E_USER_ERROR',
|
|
||||||
512 => 'E_USER_WARNING',
|
|
||||||
1024 => 'E_USER_NOTICE',
|
|
||||||
2048 => 'E_STRICT',
|
|
||||||
4096 => 'E_RECOVERABLE_ERROR',
|
|
||||||
8192 => 'E_DEPRECATED',
|
|
||||||
16384 => 'E_USER_DEPRECATED',
|
|
||||||
32767 => 'E_ALL'
|
|
||||||
];
|
|
||||||
|
|
||||||
const STORE_NONE = -1;
|
|
||||||
const STORE_MYSQL = 0;
|
|
||||||
const STORE_FILE = 1;
|
|
||||||
const STORE_BOTH = 2;
|
|
||||||
|
|
||||||
private static $instance = null;
|
|
||||||
|
|
||||||
protected $enabled = false;
|
|
||||||
protected $errCounter = 0;
|
|
||||||
protected $logCounter = 0;
|
|
||||||
protected $messagesStoreType = self::STORE_NONE;
|
|
||||||
protected $errorsStoreType = self::STORE_NONE;
|
|
||||||
protected $filter;
|
|
||||||
protected $reportRecursionLevel = 0;
|
|
||||||
protected $overridenDebugFile = null;
|
|
||||||
protected $silent = false;
|
|
||||||
protected $prefix;
|
|
||||||
|
|
||||||
private function __construct($filter) {
|
|
||||||
$this->filter = $filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getInstance($filter = null) {
|
|
||||||
if (is_null(self::$instance)) {
|
|
||||||
self::$instance = new self($filter);
|
|
||||||
}
|
|
||||||
return self::$instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function enable() {
|
|
||||||
$self = $this;
|
|
||||||
|
|
||||||
set_error_handler(function($no, $str, $file, $line) use ($self) {
|
|
||||||
if ($self->silent || !$self->enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((is_callable($this->filter) && !($this->filter)($no, $file, $line, $str)) || !$self->canReport()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$self->report(true, $str, $no, $file, $line);
|
|
||||||
});
|
|
||||||
|
|
||||||
append_shutdown_function(function() use ($self) {
|
|
||||||
if (!$self->enabled || !($error = error_get_last())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (is_callable($this->filter)
|
|
||||||
&& !($this->filter)($error['type'], $error['file'], $error['line'], $error['message'])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!$self->canReport()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$self->report(true, $error['message'], $error['type'], $error['file'], $error['line']);
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->enabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function disable() {
|
|
||||||
restore_error_handler();
|
|
||||||
$this->enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function report($is_error, $text, $errno = 0, $errfile = '', $errline = '') {
|
|
||||||
global $config;
|
|
||||||
|
|
||||||
$this->reportRecursionLevel++;
|
|
||||||
|
|
||||||
$logstarted = $this->errCounter > 0 || $this->logCounter > 0;
|
|
||||||
$num = $is_error ? $this->errCounter++ : $this->logCounter++;
|
|
||||||
$custom = $is_error && !$errno;
|
|
||||||
$ts = time();
|
|
||||||
$exectime = exectime();
|
|
||||||
$bt = backtrace(2);
|
|
||||||
|
|
||||||
$store_file = (!$is_error && $this->checkMessagesStoreType(self::STORE_FILE))
|
|
||||||
|| ($is_error && $this->checkErrorsStoreType(self::STORE_FILE));
|
|
||||||
|
|
||||||
$store_mysql = (!$is_error && $this->checkMessagesStoreType(self::STORE_MYSQL))
|
|
||||||
|| ($is_error && $this->checkErrorsStoreType(self::STORE_MYSQL));
|
|
||||||
|
|
||||||
if ($this->prefix)
|
|
||||||
$text = $this->prefix.$text;
|
|
||||||
|
|
||||||
// if ($store_mysql) {
|
|
||||||
// $db = getMySQL('local_logs', true);
|
|
||||||
// $data = [
|
|
||||||
// 'ts' => $ts,
|
|
||||||
// 'num' => $num,
|
|
||||||
// 'time' => $exectime,
|
|
||||||
// 'custom' => intval($custom),
|
|
||||||
// 'errno' => $errno,
|
|
||||||
// 'file' => $errfile,
|
|
||||||
// 'line' => $errline,
|
|
||||||
// 'text' => $text,
|
|
||||||
// 'stacktrace' => $bt,
|
|
||||||
// 'is_cli' => PHP_SAPI == 'cli' ? 1 : 0,
|
|
||||||
// ];
|
|
||||||
// if (PHP_SAPI == 'cli') {
|
|
||||||
// $data += [
|
|
||||||
// 'ip' => '',
|
|
||||||
// 'ua' => '',
|
|
||||||
// 'url' => '',
|
|
||||||
// ];
|
|
||||||
// } else {
|
|
||||||
// $data += [
|
|
||||||
// 'ip' => ip2ulong($_SERVER['REMOTE_ADDR']),
|
|
||||||
// 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
|
||||||
// 'url' => $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']
|
|
||||||
// ];
|
|
||||||
// }
|
|
||||||
// $db->insert('backend_errors', $data);
|
|
||||||
// }
|
|
||||||
|
|
||||||
if ($store_file) {
|
|
||||||
$title = PHP_SAPI == 'cli' ? 'cli' : $_SERVER['REQUEST_URI'];
|
|
||||||
$date = date('d/m/y H:i:s', $ts);
|
|
||||||
$exectime = (string)$exectime;
|
|
||||||
if (strlen($exectime) < 6)
|
|
||||||
$exectime .= str_repeat('0', 6 - strlen($exectime));
|
|
||||||
|
|
||||||
$buf = "";
|
|
||||||
if (!$logstarted) {
|
|
||||||
$buf .= "\n<e fg=white bg=magenta style=fgbright,bold> {$title} </e><e fg=white bg=blue style=fgbright> {$date} </e>\n";
|
|
||||||
}
|
|
||||||
$buf .= "<e fg=".($is_error ? 'red' : 'white').">".($is_error ? 'E' : 'I')."=<e style=bold>${num}</e> <e fg=cyan>{$exectime}</e> ";
|
|
||||||
if ($is_error && !$custom) {
|
|
||||||
$buf .= "<e fg=green>{$errfile}<e fg=white>:<e fg=green style=fgbright>{$errline}</e> (".self::errname($errno).") ";
|
|
||||||
}
|
|
||||||
$buf = stransi($buf);
|
|
||||||
|
|
||||||
$buf .= $text;
|
|
||||||
$buf .= "\n";
|
|
||||||
if ($is_error && $config['debug_backtrace']) {
|
|
||||||
$buf .= $bt."\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$debug_file = $this->getDebugFile();
|
|
||||||
|
|
||||||
$logdir = dirname($debug_file);
|
|
||||||
if (!file_exists($logdir)) {
|
|
||||||
mkdir($logdir);
|
|
||||||
setperm($logdir);
|
|
||||||
}
|
|
||||||
|
|
||||||
$f = fopen($debug_file, 'a');
|
|
||||||
if ($f) {
|
|
||||||
fwrite($f, $buf);
|
|
||||||
fclose($f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->reportRecursionLevel--;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function canReport() {
|
|
||||||
return $this->reportRecursionLevel < 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setErrorsStoreType($errorsStoreType) {
|
|
||||||
$this->errorsStoreType = $errorsStoreType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setMessagesStoreType($messagesStoreType) {
|
|
||||||
$this->messagesStoreType = $messagesStoreType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function checkMessagesStoreType($store_type) {
|
|
||||||
return $this->messagesStoreType == $store_type || $this->messagesStoreType == self::STORE_BOTH;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function checkErrorsStoreType($store_type) {
|
|
||||||
return $this->errorsStoreType == $store_type || $this->errorsStoreType == self::STORE_BOTH;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function overrideDebugFile($file) {
|
|
||||||
$this->overridenDebugFile = $file;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getDebugFile() {
|
|
||||||
global $config;
|
|
||||||
return is_null($this->overridenDebugFile) ? ROOT.'/'.$config['debug_file'] : $this->overridenDebugFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setSilence($silent) {
|
|
||||||
$this->silent = $silent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setPrefix($prefix) {
|
|
||||||
$this->prefix = $prefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function errname($errno) {
|
|
||||||
static $errors = null;
|
|
||||||
if (is_null($errors)) {
|
|
||||||
$errors = array_flip(array_slice(get_defined_constants(true)['Core'], 0, 15, true));
|
|
||||||
}
|
|
||||||
return $errors[$errno];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getTypes() {
|
|
||||||
return self::$Types;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class debug_measure {
|
|
||||||
|
|
||||||
private $name;
|
|
||||||
private $time;
|
|
||||||
private $started = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function start($name = null) {
|
|
||||||
if (is_null($name)) {
|
|
||||||
$name = strgen(3);
|
|
||||||
}
|
|
||||||
$this->name = $name;
|
|
||||||
$this->time = microtime(true);
|
|
||||||
$this->started = true;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return float|string|null
|
|
||||||
*/
|
|
||||||
public function finish() {
|
|
||||||
if (!$this->started) {
|
|
||||||
debugLog("debug_measure::finish(): not started, name=".$this->name);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$time = (microtime(true) - $this->time);
|
|
||||||
debugLog("MEASURE".($this->name != '' ? ' '.$this->name : '').": ".$time);
|
|
||||||
|
|
||||||
$this->started = false;
|
|
||||||
return $time;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $var
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function str_print_r($var) {
|
|
||||||
ob_start();
|
|
||||||
print_r($var);
|
|
||||||
return trim(ob_get_clean());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $var
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function str_var_dump($var) {
|
|
||||||
ob_start();
|
|
||||||
var_dump($var);
|
|
||||||
return trim(ob_get_clean());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $args
|
|
||||||
* @param bool $all_dump
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function str_vars($args, $all_dump = false) {
|
|
||||||
return implode(' ', array_map(function($a) use ($all_dump) {
|
|
||||||
if ($all_dump) {
|
|
||||||
return str_var_dump($a);
|
|
||||||
}
|
|
||||||
$type = gettype($a);
|
|
||||||
if ($type == 'string' || $type == 'integer' || $type == 'double') {
|
|
||||||
return $a;
|
|
||||||
} else if ($type == 'array' || $type == 'object') {
|
|
||||||
return str_print_r($a);
|
|
||||||
} else {
|
|
||||||
return str_var_dump($a);
|
|
||||||
}
|
|
||||||
}, $args));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $shift
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function backtrace($shift = 0) {
|
|
||||||
$bt = debug_backtrace();
|
|
||||||
$lines = [];
|
|
||||||
foreach ($bt as $i => $t) {
|
|
||||||
if ($i < $shift) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!isset($t['file'])) {
|
|
||||||
$lines[] = 'from ?';
|
|
||||||
} else {
|
|
||||||
$lines[] = 'from '.$t['file'].':'.$t['line'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return implode("\n", $lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mixed ...$args
|
|
||||||
*/
|
|
||||||
function debugLog(...$args) {
|
|
||||||
global $config;
|
|
||||||
if (!$config['is_dev'])
|
|
||||||
return;
|
|
||||||
|
|
||||||
debug::getInstance()->report(false, str_vars($args));
|
|
||||||
}
|
|
||||||
|
|
||||||
function debugLogOnProd(...$args) {
|
|
||||||
debug::getInstance()->report(false, str_vars($args));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mixed ...$args
|
|
||||||
*/
|
|
||||||
function debugError(...$args) {
|
|
||||||
$debug = debug::getInstance();
|
|
||||||
if ($debug->canReport()) {
|
|
||||||
$debug->report(true, str_vars($args));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,243 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
abstract class model {
|
|
||||||
|
|
||||||
const DB_TABLE = null;
|
|
||||||
const DB_KEY = 'id';
|
|
||||||
|
|
||||||
const STRING = 0;
|
|
||||||
const INTEGER = 1;
|
|
||||||
const FLOAT = 2;
|
|
||||||
const ARRAY = 3;
|
|
||||||
const BOOLEAN = 4;
|
|
||||||
const JSON = 5;
|
|
||||||
const SERIALIZED = 6;
|
|
||||||
|
|
||||||
protected static array $SpecCache = [];
|
|
||||||
|
|
||||||
public static function create_instance(...$args) {
|
|
||||||
$cl = get_called_class();
|
|
||||||
return new $cl(...$args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __construct(array $raw) {
|
|
||||||
if (!isset(self::$SpecCache[static::class])) {
|
|
||||||
list($fields, $model_name_map, $db_name_map) = static::get_spec();
|
|
||||||
self::$SpecCache[static::class] = [
|
|
||||||
'fields' => $fields,
|
|
||||||
'model_name_map' => $model_name_map,
|
|
||||||
'db_name_map' => $db_name_map
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (self::$SpecCache[static::class]['fields'] as $field)
|
|
||||||
$this->{$field['model_name']} = self::cast_to_type($field['type'], $raw[$field['db_name']]);
|
|
||||||
|
|
||||||
if (is_null(static::DB_TABLE))
|
|
||||||
trigger_error('class '.get_class($this).' doesn\'t have DB_TABLE defined');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $fields
|
|
||||||
*
|
|
||||||
* TODO: support adding or subtracting (SET value=value+1)
|
|
||||||
*/
|
|
||||||
public function edit($fields) {
|
|
||||||
$db = getDB();
|
|
||||||
|
|
||||||
$model_upd = [];
|
|
||||||
$db_upd = [];
|
|
||||||
|
|
||||||
foreach ($fields as $name => $value) {
|
|
||||||
$index = self::$SpecCache[static::class]['db_name_map'][$name] ?? null;
|
|
||||||
if (is_null($index)) {
|
|
||||||
debugError(__METHOD__.': field `'.$name.'` not found in '.static::class);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$field = self::$SpecCache[static::class]['fields'][$index];
|
|
||||||
switch ($field['type']) {
|
|
||||||
case self::ARRAY:
|
|
||||||
if (is_array($value)) {
|
|
||||||
$db_upd[$name] = implode(',', $value);
|
|
||||||
$model_upd[$field['model_name']] = $value;
|
|
||||||
} else {
|
|
||||||
debugError(__METHOD__.': field `'.$name.'` is expected to be array. skipping.');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case self::INTEGER:
|
|
||||||
$value = (int)$value;
|
|
||||||
$db_upd[$name] = $value;
|
|
||||||
$model_upd[$field['model_name']] = $value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case self::FLOAT:
|
|
||||||
$value = (float)$value;
|
|
||||||
$db_upd[$name] = $value;
|
|
||||||
$model_upd[$field['model_name']] = $value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case self::BOOLEAN:
|
|
||||||
$db_upd[$name] = $value ? 1 : 0;
|
|
||||||
$model_upd[$field['model_name']] = $value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case self::JSON:
|
|
||||||
$db_upd[$name] = jsonEncode($value);
|
|
||||||
$model_upd[$field['model_name']] = $value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case self::SERIALIZED:
|
|
||||||
$db_upd[$name] = serialize($value);
|
|
||||||
$model_upd[$field['model_name']] = $value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
$value = (string)$value;
|
|
||||||
$db_upd[$name] = $value;
|
|
||||||
$model_upd[$field['model_name']] = $value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($db_upd) && !$db->update(static::DB_TABLE, $db_upd, static::DB_KEY."=?", $this->get_id())) {
|
|
||||||
debugError(__METHOD__.': failed to update database');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($model_upd)) {
|
|
||||||
foreach ($model_upd as $name => $value)
|
|
||||||
$this->{$name} = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_id() {
|
|
||||||
return $this->{to_camel_case(static::DB_KEY)};
|
|
||||||
}
|
|
||||||
|
|
||||||
public function as_array(array $fields = [], array $custom_getters = []): array {
|
|
||||||
if (empty($fields))
|
|
||||||
$fields = array_keys(static::$SpecCache[static::class]['db_name_map']);
|
|
||||||
|
|
||||||
$array = [];
|
|
||||||
foreach ($fields as $field) {
|
|
||||||
if (isset($custom_getters[$field]) && is_callable($custom_getters[$field])) {
|
|
||||||
$array[$field] = $custom_getters[$field]();
|
|
||||||
} else {
|
|
||||||
$array[$field] = $this->{to_camel_case($field)};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $array;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static function cast_to_type(int $type, $value) {
|
|
||||||
switch ($type) {
|
|
||||||
case self::BOOLEAN:
|
|
||||||
return (bool)$value;
|
|
||||||
|
|
||||||
case self::INTEGER:
|
|
||||||
return (int)$value;
|
|
||||||
|
|
||||||
case self::FLOAT:
|
|
||||||
return (float)$value;
|
|
||||||
|
|
||||||
case self::ARRAY:
|
|
||||||
return array_filter(explode(',', $value));
|
|
||||||
|
|
||||||
case self::JSON:
|
|
||||||
$val = jsonDecode($value);
|
|
||||||
if (!$val)
|
|
||||||
$val = null;
|
|
||||||
return $val;
|
|
||||||
|
|
||||||
case self::SERIALIZED:
|
|
||||||
$val = unserialize($value);
|
|
||||||
if ($val === false)
|
|
||||||
$val = null;
|
|
||||||
return $val;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return (string)$value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static function get_spec(): array {
|
|
||||||
$rc = new ReflectionClass(static::class);
|
|
||||||
$props = $rc->getProperties(ReflectionProperty::IS_PUBLIC);
|
|
||||||
|
|
||||||
$list = [];
|
|
||||||
$index = 0;
|
|
||||||
|
|
||||||
$model_name_map = [];
|
|
||||||
$db_name_map = [];
|
|
||||||
|
|
||||||
foreach ($props as $prop) {
|
|
||||||
if ($prop->isStatic())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
$name = $prop->getName();
|
|
||||||
if (startsWith($name, '_'))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
$type = $prop->getType();
|
|
||||||
$phpdoc = $prop->getDocComment();
|
|
||||||
|
|
||||||
$mytype = null;
|
|
||||||
if (!$prop->hasType() && !$phpdoc)
|
|
||||||
$mytype = self::STRING;
|
|
||||||
else {
|
|
||||||
$typename = $type->getName();
|
|
||||||
switch ($typename) {
|
|
||||||
case 'string':
|
|
||||||
$mytype = self::STRING;
|
|
||||||
break;
|
|
||||||
case 'int':
|
|
||||||
$mytype = self::INTEGER;
|
|
||||||
break;
|
|
||||||
case 'float':
|
|
||||||
$mytype = self::FLOAT;
|
|
||||||
break;
|
|
||||||
case 'array':
|
|
||||||
$mytype = self::ARRAY;
|
|
||||||
break;
|
|
||||||
case 'bool':
|
|
||||||
$mytype = self::BOOLEAN;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($phpdoc != '') {
|
|
||||||
$pos = strpos($phpdoc, '@');
|
|
||||||
if ($pos === false)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (substr($phpdoc, $pos+1, 4) == 'json')
|
|
||||||
$mytype = self::JSON;
|
|
||||||
else if (substr($phpdoc, $pos+1, 5) == 'array')
|
|
||||||
$mytype = self::ARRAY;
|
|
||||||
else if (substr($phpdoc, $pos+1, 10) == 'serialized')
|
|
||||||
$mytype = self::SERIALIZED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_null($mytype))
|
|
||||||
debugError(__METHOD__.": ".$name." is still null in ".static::class);
|
|
||||||
|
|
||||||
$dbname = from_camel_case($name);
|
|
||||||
$list[] = [
|
|
||||||
'type' => $mytype,
|
|
||||||
'model_name' => $name,
|
|
||||||
'db_name' => $dbname
|
|
||||||
];
|
|
||||||
|
|
||||||
$model_name_map[$name] = $index;
|
|
||||||
$db_name_map[$dbname] = $index;
|
|
||||||
|
|
||||||
$index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [$list, $model_name_map, $db_name_map];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,142 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
abstract class request_handler {
|
|
||||||
|
|
||||||
const GET = 'GET';
|
|
||||||
const POST = 'POST';
|
|
||||||
|
|
||||||
private static array $AllowedInputTypes = ['i', 'f', 'b', 'e' /* enum */];
|
|
||||||
|
|
||||||
public function dispatch(string $act) {
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'] == 'POST' ? 'POST' : 'GET';
|
|
||||||
return $this->call_act($method, $act);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function before_dispatch(string $method, string $act)/*: ?array*/ {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function call_act(string $method, string $act, array $input = []) {
|
|
||||||
global $RouterInput;
|
|
||||||
|
|
||||||
$notfound = !method_exists($this, $method.'_'.$act) || !((new ReflectionMethod($this, $method.'_'.$act))->isPublic());
|
|
||||||
if ($notfound)
|
|
||||||
$this->method_not_found($method, $act);
|
|
||||||
|
|
||||||
if (!empty($input)) {
|
|
||||||
foreach ($input as $k => $v)
|
|
||||||
$RouterInput[$k] = $v;
|
|
||||||
}
|
|
||||||
|
|
||||||
$args = $this->before_dispatch($method, $act);
|
|
||||||
return call_user_func_array([$this, $method.'_'.$act], is_array($args) ? [$args] : []);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract protected function method_not_found(string $method, string $act);
|
|
||||||
|
|
||||||
protected function input(string $input, bool $as_assoc = false): array {
|
|
||||||
$input = preg_split('/,\s+?/', $input, null, PREG_SPLIT_NO_EMPTY);
|
|
||||||
|
|
||||||
$ret = [];
|
|
||||||
foreach ($input as $var) {
|
|
||||||
list($type, $name, $enum_values, $enum_default) = self::parse_input_var($var);
|
|
||||||
|
|
||||||
$value = param($name);
|
|
||||||
|
|
||||||
switch ($type) {
|
|
||||||
case 'i':
|
|
||||||
if (is_null($value) && !is_null($enum_default)) {
|
|
||||||
$value = (int)$enum_default;
|
|
||||||
} else {
|
|
||||||
$value = (int)$value;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'f':
|
|
||||||
if (is_null($value) && !is_null($enum_default)) {
|
|
||||||
$value = (float)$enum_default;
|
|
||||||
} else {
|
|
||||||
$value = (float)$value;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'b':
|
|
||||||
if (is_null($value) && !is_null($enum_default)) {
|
|
||||||
$value = (bool)$enum_default;
|
|
||||||
} else {
|
|
||||||
$value = (bool)$value;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'e':
|
|
||||||
if (!in_array($value, $enum_values)) {
|
|
||||||
$value = !is_null($enum_default) ? $enum_default : '';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$as_assoc) {
|
|
||||||
$ret[] = $value;
|
|
||||||
} else {
|
|
||||||
$ret[$name] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
protected static function parse_input_var(string $var): array {
|
|
||||||
$type = null;
|
|
||||||
$name = null;
|
|
||||||
$enum_values = null;
|
|
||||||
$enum_default = null;
|
|
||||||
|
|
||||||
$pos = strpos($var, ':');
|
|
||||||
if ($pos !== false) {
|
|
||||||
$type = substr($var, 0, $pos);
|
|
||||||
$rest = substr($var, $pos+1);
|
|
||||||
|
|
||||||
if (!in_array($type, self::$AllowedInputTypes)) {
|
|
||||||
trigger_error('request_handler::parse_input_var('.$var.'): unknown type '.$type);
|
|
||||||
$type = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($type) {
|
|
||||||
case 'e':
|
|
||||||
$br_from = strpos($rest, '(');
|
|
||||||
$br_to = strpos($rest, ')');
|
|
||||||
|
|
||||||
if ($br_from === false || $br_to === false) {
|
|
||||||
trigger_error('request_handler::parse_input_var('.$var.'): failed to parse enum values');
|
|
||||||
$type = null;
|
|
||||||
$name = $rest;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$enum_values = array_map('trim', explode('|', trim(substr($rest, $br_from+1, $br_to-$br_from-1))));
|
|
||||||
$name = trim(substr($rest, 0, $br_from));
|
|
||||||
|
|
||||||
if (!empty($enum_values)) foreach ($enum_values as $key => $val) {
|
|
||||||
if (substr($val, 0, 1) == '=') {
|
|
||||||
$enum_values[$key] = substr($val, 1);
|
|
||||||
$enum_default = $enum_values[$key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
if (($eq_pos = strpos($rest, '=')) !== false) {
|
|
||||||
$enum_default = substr($rest, $eq_pos+1);
|
|
||||||
$rest = substr($rest, 0, $eq_pos);
|
|
||||||
}
|
|
||||||
$name = trim($rest);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$type = 's';
|
|
||||||
$name = $var;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [$type, $name, $enum_values, $enum_default];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,199 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class router {
|
|
||||||
|
|
||||||
protected array $routes = [
|
|
||||||
'children' => [],
|
|
||||||
're_children' => []
|
|
||||||
];
|
|
||||||
|
|
||||||
public function add($template, $value) {
|
|
||||||
if ($template == '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// expand {enum,erat,ions}
|
|
||||||
$templates = [[$template, $value]];
|
|
||||||
if (preg_match_all('/\{([\w\d_\-,]+)\}/', $template, $matches)) {
|
|
||||||
foreach ($matches[1] as $match_index => $variants) {
|
|
||||||
$variants = explode(',', $variants);
|
|
||||||
$variants = array_map('trim', $variants);
|
|
||||||
$variants = array_filter($variants, function($s) { return $s != ''; });
|
|
||||||
|
|
||||||
for ($i = 0; $i < count($templates); ) {
|
|
||||||
list($template, $value) = $templates[$i];
|
|
||||||
$new_templates = [];
|
|
||||||
foreach ($variants as $variant_index => $variant) {
|
|
||||||
$new_templates[] = [
|
|
||||||
str_replace_once($matches[0][$match_index], $variant, $template),
|
|
||||||
str_replace('${'.($match_index+1).'}', $variant, $value)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
array_splice($templates, $i, 1, $new_templates);
|
|
||||||
$i += count($new_templates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// process all generated routes
|
|
||||||
foreach ($templates as $template) {
|
|
||||||
list($template, $value) = $template;
|
|
||||||
|
|
||||||
$start_pos = 0;
|
|
||||||
$parent = &$this->routes;
|
|
||||||
$template_len = strlen($template);
|
|
||||||
|
|
||||||
while ($start_pos < $template_len) {
|
|
||||||
$slash_pos = strpos($template, '/', $start_pos);
|
|
||||||
if ($slash_pos !== false) {
|
|
||||||
$part = substr($template, $start_pos, $slash_pos-$start_pos+1);
|
|
||||||
$start_pos = $slash_pos+1;
|
|
||||||
} else {
|
|
||||||
$part = substr($template, $start_pos);
|
|
||||||
$start_pos = $template_len;
|
|
||||||
}
|
|
||||||
|
|
||||||
$parent = &$this->_addRoute($parent, $part,
|
|
||||||
$start_pos < $template_len ? null : $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function &_addRoute(&$parent, $part, $value = null) {
|
|
||||||
$par_pos = strpos($part, '(');
|
|
||||||
$is_regex = $par_pos !== false && ($par_pos == 0 || $part[$par_pos-1] != '\\');
|
|
||||||
|
|
||||||
$children_key = !$is_regex ? 'children' : 're_children';
|
|
||||||
|
|
||||||
if (isset($parent[$children_key][$part])) {
|
|
||||||
if (is_null($value)) {
|
|
||||||
$parent = &$parent[$children_key][$part];
|
|
||||||
} else {
|
|
||||||
if (!isset($parent[$children_key][$part]['value'])) {
|
|
||||||
$parent[$children_key][$part]['value'] = $value;
|
|
||||||
} else {
|
|
||||||
trigger_error(__METHOD__.': route is already defined');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
$child = [
|
|
||||||
'children' => [],
|
|
||||||
're_children' => []
|
|
||||||
];
|
|
||||||
if (!is_null($value)) {
|
|
||||||
$child['value'] = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
$parent[$children_key][$part] = $child;
|
|
||||||
return $parent[$children_key][$part];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function find($uri) {
|
|
||||||
if ($uri != '/' && $uri[0] == '/') {
|
|
||||||
$uri = substr($uri, 1);
|
|
||||||
}
|
|
||||||
$start_pos = 0;
|
|
||||||
$parent = &$this->routes;
|
|
||||||
$uri_len = strlen($uri);
|
|
||||||
$matches = [];
|
|
||||||
|
|
||||||
while ($start_pos < $uri_len) {
|
|
||||||
$slash_pos = strpos($uri, '/', $start_pos);
|
|
||||||
if ($slash_pos !== false) {
|
|
||||||
$part = substr($uri, $start_pos, $slash_pos-$start_pos+1);
|
|
||||||
$start_pos = $slash_pos+1;
|
|
||||||
} else {
|
|
||||||
$part = substr($uri, $start_pos);
|
|
||||||
$start_pos = $uri_len;
|
|
||||||
}
|
|
||||||
|
|
||||||
$found = false;
|
|
||||||
if (isset($parent['children'][$part])) {
|
|
||||||
$parent = &$parent['children'][$part];
|
|
||||||
$found = true;
|
|
||||||
} else if (!empty($parent['re_children'])) {
|
|
||||||
foreach ($parent['re_children'] as $re => &$child) {
|
|
||||||
$exp = '#^'.$re.'$#';
|
|
||||||
$re_result = preg_match($exp, $part, $match);
|
|
||||||
if ($re_result === false) {
|
|
||||||
debugError(__METHOD__.": regex $exp failed");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($re_result) {
|
|
||||||
if (count($match) > 1) {
|
|
||||||
$matches = array_merge($matches, array_slice($match, 1));
|
|
||||||
}
|
|
||||||
$parent = &$child;
|
|
||||||
$found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$found) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($parent['value'])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$value = $parent['value'];
|
|
||||||
if (!empty($matches)) {
|
|
||||||
foreach ($matches as $i => $match) {
|
|
||||||
$needle = '$('.($i+1).')';
|
|
||||||
$pos = strpos($value, $needle);
|
|
||||||
if ($pos !== false) {
|
|
||||||
$value = substr_replace($value, $match, $pos, strlen($needle));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function load($routes) {
|
|
||||||
$this->routes = $routes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function dump() {
|
|
||||||
return $this->routes;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function routerFind(router $router) {
|
|
||||||
$document_uri = $_SERVER['REQUEST_URI'];
|
|
||||||
if (($pos = strpos($document_uri, '?')) !== false)
|
|
||||||
$document_uri = substr($document_uri, 0, $pos);
|
|
||||||
$document_uri = urldecode($document_uri);
|
|
||||||
|
|
||||||
$fixed_document_uri = preg_replace('#/+#', '/', $document_uri);
|
|
||||||
if ($fixed_document_uri != $document_uri && !is_xhr_request()) {
|
|
||||||
redirect($fixed_document_uri);
|
|
||||||
} else {
|
|
||||||
$document_uri = $fixed_document_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
$route = $router->find($document_uri);
|
|
||||||
if ($route === false)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
$route = preg_split('/ +/', $route);
|
|
||||||
$handler = $route[0];
|
|
||||||
$act = $route[1];
|
|
||||||
$input = [];
|
|
||||||
if (count($route) > 2) {
|
|
||||||
for ($i = 2; $i < count($route); $i++) {
|
|
||||||
$var = $route[$i];
|
|
||||||
list($k, $v) = explode('=', $var);
|
|
||||||
$input[trim($k)] = trim($v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [$handler, $act, $input];
|
|
||||||
}
|
|
@ -1,520 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
abstract class base_tpl {
|
|
||||||
|
|
||||||
public $twig;
|
|
||||||
protected $vars = [];
|
|
||||||
protected $global_vars = [];
|
|
||||||
protected $title = '';
|
|
||||||
protected $title_modifiers = [];
|
|
||||||
protected $keywords = '';
|
|
||||||
protected $description = '';
|
|
||||||
protected $js = [];
|
|
||||||
protected $lang_keys = [];
|
|
||||||
protected $static = [];
|
|
||||||
protected $external_static = [];
|
|
||||||
protected $head = [];
|
|
||||||
protected $globals_applied = false;
|
|
||||||
protected $static_time;
|
|
||||||
|
|
||||||
public function __construct($templates_dir, $cache_dir) {
|
|
||||||
global $config;
|
|
||||||
|
|
||||||
// $cl = get_called_class();
|
|
||||||
|
|
||||||
$this->twig = self::twig_instance($templates_dir, $cache_dir, $config['is_dev']);
|
|
||||||
$this->static_time = time();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function twig_instance($templates_dir, $cache_dir, $auto_reload) {
|
|
||||||
// must specify a second argument ($rootPath) here
|
|
||||||
// otherwise it will be getcwd() and it's www-prod/htdocs/ for apache and www-prod/ for cli code
|
|
||||||
// this is bad for templates rebuilding
|
|
||||||
$twig_loader = new \Twig\Loader\FilesystemLoader($templates_dir, ROOT);
|
|
||||||
|
|
||||||
$env_options = [];
|
|
||||||
if (!is_null($cache_dir)) {
|
|
||||||
$env_options += [
|
|
||||||
'cache' => $cache_dir,
|
|
||||||
'auto_reload' => $auto_reload
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$twig = new \Twig\Environment($twig_loader, $env_options);
|
|
||||||
$twig->addExtension(new Twig_MyExtension);
|
|
||||||
|
|
||||||
return $twig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render($template, array $vars = []) {
|
|
||||||
$this->apply_globals();
|
|
||||||
return $this->do_render($template, array_merge($this->vars, $vars));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function do_render($template, $vars) {
|
|
||||||
global $config;
|
|
||||||
$s = '';
|
|
||||||
try {
|
|
||||||
$s = $this->twig->render($template, $vars);
|
|
||||||
} catch (\Twig\Error\Error $e) {
|
|
||||||
$error = get_class($e).": failed to render";
|
|
||||||
$source_ctx = $e->getSourceContext();
|
|
||||||
if ($source_ctx) {
|
|
||||||
$path = $source_ctx->getPath();
|
|
||||||
if (startsWith($path, ROOT))
|
|
||||||
$path = substr($path, strlen(ROOT)+1);
|
|
||||||
$error .= " ".$source_ctx->getName()." (".$path.") at line ".$e->getTemplateLine();
|
|
||||||
}
|
|
||||||
$error .= ": ";
|
|
||||||
$error .= $e->getMessage();
|
|
||||||
debugError($error);
|
|
||||||
if ($config['is_dev'])
|
|
||||||
$s = $error."\n";
|
|
||||||
}
|
|
||||||
return $s;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function set($arg1, $arg2 = null) {
|
|
||||||
if (is_array($arg1)) {
|
|
||||||
foreach ($arg1 as $key => $value) {
|
|
||||||
$this->vars[$key] = $value;
|
|
||||||
}
|
|
||||||
} elseif ($arg2 !== null) {
|
|
||||||
$this->vars[$arg1] = $arg2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function is_set($key): bool {
|
|
||||||
return isset($this->vars[$key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function set_global($arg1, $arg2 = null) {
|
|
||||||
if (is_array($arg1)) {
|
|
||||||
foreach ($arg1 as $key => $value) {
|
|
||||||
$this->global_vars[$key] = $value;
|
|
||||||
}
|
|
||||||
} elseif ($arg2 !== null) {
|
|
||||||
$this->global_vars[$arg1] = $arg2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function is_global_set($key): bool {
|
|
||||||
return isset($this->global_vars[$key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_global($key) {
|
|
||||||
return $this->is_global_set($key) ? $this->global_vars[$key] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function apply_globals() {
|
|
||||||
if (!empty($this->global_vars) && !$this->globals_applied) {
|
|
||||||
foreach ($this->global_vars as $key => $value)
|
|
||||||
$this->twig->addGlobal($key, $value);
|
|
||||||
$this->globals_applied = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $title
|
|
||||||
*/
|
|
||||||
public function set_title($title) {
|
|
||||||
$this->title = $title;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function get_title() {
|
|
||||||
$title = $this->title != '' ? $this->title : 'Домашний сайт';
|
|
||||||
if (!empty($this->title_modifiers)) {
|
|
||||||
foreach ($this->title_modifiers as $modifier) {
|
|
||||||
$title = $modifier($title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $title;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param callable $callable
|
|
||||||
*/
|
|
||||||
public function add_page_title_modifier(callable $callable) {
|
|
||||||
if (!is_callable($callable)) {
|
|
||||||
trigger_error(__METHOD__.': argument is not callable');
|
|
||||||
} else {
|
|
||||||
$this->title_modifiers[] = $callable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $css_name
|
|
||||||
* @param null $extra
|
|
||||||
*/
|
|
||||||
public function add_static(string $name, $extra = null) {
|
|
||||||
global $config;
|
|
||||||
// $is_css = endsWith($name, '.css');
|
|
||||||
$this->static[] = [$name, $extra];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function add_external_static($type, $url) {
|
|
||||||
$this->external_static[] = ['type' => $type, 'url' => $url];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function add_js($js) {
|
|
||||||
$this->js[] = $js;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function add_lang_keys(array $keys) {
|
|
||||||
$this->lang_keys = array_merge($this->lang_keys, $keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function add_head($html) {
|
|
||||||
$this->head[] = $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_head_html() {
|
|
||||||
global $config;
|
|
||||||
$lines = [];
|
|
||||||
$public_path = $config['static_public_path'];
|
|
||||||
foreach ($this->static as $val) {
|
|
||||||
list($name, $extra) = $val;
|
|
||||||
if (endsWith($name, '.js'))
|
|
||||||
$lines[] = self::js_link($public_path.'/'.$name, $config['static'][$name] ?? 1);
|
|
||||||
else
|
|
||||||
$lines[] = self::css_link($public_path.'/'.$name, $config['static'][$name] ?? 1, $extra);
|
|
||||||
}
|
|
||||||
if (!empty($this->external_static)) {
|
|
||||||
foreach ($this->external_static as $ext) {
|
|
||||||
if ($ext['type'] == 'js')
|
|
||||||
$lines[] = self::js_link($ext['url']);
|
|
||||||
else if ($ext['type'] == 'css')
|
|
||||||
$lines[] = self::css_link($ext['url']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!empty($this->head)) {
|
|
||||||
$lines = array_merge($lines, $this->head);
|
|
||||||
}
|
|
||||||
return implode("\n", $lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function js_link($name, $version = null): string {
|
|
||||||
if ($version !== null)
|
|
||||||
$name .= '?'.$version;
|
|
||||||
return '<script src="'.$name.'" type="text/javascript"></script>';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function css_link($name, $version = null, $extra = null) {
|
|
||||||
if ($version !== null)
|
|
||||||
$name .= '?'.$version;
|
|
||||||
$s = '<link';
|
|
||||||
if (is_array($extra)) {
|
|
||||||
if (!empty($extra['id']))
|
|
||||||
$s .= ' id="'.$extra['id'].'"';
|
|
||||||
}
|
|
||||||
$s .= ' rel="stylesheet" type="text/css"';
|
|
||||||
if (is_array($extra) && !empty($extra['media']))
|
|
||||||
$s .= ' media="'.$extra['media'].'"';
|
|
||||||
$s .= ' href="'.$name.'"';
|
|
||||||
$s .= '>';
|
|
||||||
return $s;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get_lang_keys() {
|
|
||||||
global $lang;
|
|
||||||
$keys = [];
|
|
||||||
if (!empty($this->lang_keys)) {
|
|
||||||
foreach ($this->lang_keys as $key)
|
|
||||||
$keys[$key] = $lang[$key];
|
|
||||||
}
|
|
||||||
return $keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render_not_found() {
|
|
||||||
http_response_code(404);
|
|
||||||
if (!is_xhr_request()) {
|
|
||||||
$this->render_page('404.twig');
|
|
||||||
} else {
|
|
||||||
ajax_error(['code' => 404]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param null|string $reason
|
|
||||||
*/
|
|
||||||
public function render_forbidden($reason = null) {
|
|
||||||
http_response_code(403);
|
|
||||||
if (!is_xhr_request()) {
|
|
||||||
$this->set(['reason' => $reason]);
|
|
||||||
$this->render_page('403.twig');
|
|
||||||
} else {
|
|
||||||
$data = ['code' => 403];
|
|
||||||
if (!is_null($reason))
|
|
||||||
$data['reason'] = $reason;
|
|
||||||
ajax_error($data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function must_revalidate() {
|
|
||||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract public function render_page($template);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class web_tpl extends base_tpl {
|
|
||||||
|
|
||||||
protected $alternate = false;
|
|
||||||
|
|
||||||
public function __construct() {
|
|
||||||
global $config;
|
|
||||||
$templates = $config['templates']['web'];
|
|
||||||
parent::__construct(
|
|
||||||
ROOT.'/'. $templates['root'],
|
|
||||||
$config['twig_cache']
|
|
||||||
? ROOT.'/'.$templates['cache']
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function set_alternate($alt) {
|
|
||||||
$this->alternate = $alt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render_page($template) {
|
|
||||||
echo $this->_render_header();
|
|
||||||
echo $this->_render_body($template);
|
|
||||||
echo $this->_render_footer();
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function _render_header() {
|
|
||||||
global $config;
|
|
||||||
$this->apply_globals();
|
|
||||||
|
|
||||||
$vars = [
|
|
||||||
'title' => $this->get_title(),
|
|
||||||
'keywords' => $this->keywords,
|
|
||||||
'description' => $this->description,
|
|
||||||
'alternate' => $this->alternate,
|
|
||||||
'static' => $this->get_head_html(),
|
|
||||||
];
|
|
||||||
return $this->do_render('header.twig', $vars);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function _render_body($template) {
|
|
||||||
return $this->do_render($template, $this->vars);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function _render_footer() {
|
|
||||||
$exec_time = microtime(true) - START_TIME;
|
|
||||||
$exec_time = round($exec_time, 4);
|
|
||||||
|
|
||||||
$footer_vars = [
|
|
||||||
'exec_time' => $exec_time,
|
|
||||||
'js' => !empty($this->js) ? implode("\n", $this->js) : '',
|
|
||||||
];
|
|
||||||
return $this->do_render('footer.twig', $footer_vars);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Twig_MyExtension extends \Twig\Extension\AbstractExtension {
|
|
||||||
|
|
||||||
public function getFilters() {
|
|
||||||
global $lang;
|
|
||||||
|
|
||||||
return array(
|
|
||||||
new \Twig\TwigFilter('lang', 'lang'),
|
|
||||||
|
|
||||||
new \Twig\TwigFilter('lang', function($key, array $args = []) use (&$lang) {
|
|
||||||
array_walk($args, function(&$item, $key) {
|
|
||||||
$item = htmlescape($item);
|
|
||||||
});
|
|
||||||
array_unshift($args, $key);
|
|
||||||
return call_user_func_array([$lang, 'get'], $args);
|
|
||||||
}, ['is_variadic' => true]),
|
|
||||||
|
|
||||||
new \Twig\TwigFilter('plural', function($text, array $args = []) use (&$lang) {
|
|
||||||
array_unshift($args, $text);
|
|
||||||
return call_user_func_array([$lang, 'num'], $args);
|
|
||||||
}, ['is_variadic' => true]),
|
|
||||||
|
|
||||||
new \Twig\TwigFilter('format_number', function($number, array $args = []) {
|
|
||||||
array_unshift($args, $number);
|
|
||||||
return call_user_func_array('formatNumber', $args);
|
|
||||||
}, ['is_variadic' => true]),
|
|
||||||
|
|
||||||
new \Twig\TwigFilter('short_number', function($number, array $args = []) {
|
|
||||||
array_unshift($args, $number);
|
|
||||||
return call_user_func_array('shortNumber', $args);
|
|
||||||
}, ['is_variadic']),
|
|
||||||
|
|
||||||
new \Twig\TwigFilter('format_time', function($ts, array $args = []) {
|
|
||||||
array_unshift($args, $ts);
|
|
||||||
return call_user_func_array('formatTime', $args);
|
|
||||||
}, ['is_variadic' => true]),
|
|
||||||
|
|
||||||
new \Twig\TwigFilter('format_duration', function($seconds, array $args = []) {
|
|
||||||
array_unshift($args, $seconds);
|
|
||||||
return call_user_func_array('formatDuration', $args);
|
|
||||||
}, ['is_variadic' => true]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTokenParsers() {
|
|
||||||
return [new JsTagTokenParser()];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getName() {
|
|
||||||
return 'lang';
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Based on https://stackoverflow.com/questions/26170727/how-to-create-a-twig-custom-tag-that-executes-a-callback
|
|
||||||
class JsTagTokenParser extends \Twig\TokenParser\AbstractTokenParser {
|
|
||||||
|
|
||||||
public function parse(\Twig\Token $token) {
|
|
||||||
$lineno = $token->getLine();
|
|
||||||
$stream = $this->parser->getStream();
|
|
||||||
|
|
||||||
// recovers all inline parameters close to your tag name
|
|
||||||
$params = array_merge([], $this->getInlineParams($token));
|
|
||||||
|
|
||||||
$continue = true;
|
|
||||||
while ($continue) {
|
|
||||||
// create subtree until the decideJsTagFork() callback returns true
|
|
||||||
$body = $this->parser->subparse(array ($this, 'decideJsTagFork'));
|
|
||||||
|
|
||||||
// I like to put a switch here, in case you need to add middle tags, such
|
|
||||||
// as: {% js %}, {% nextjs %}, {% endjs %}.
|
|
||||||
$tag = $stream->next()->getValue();
|
|
||||||
switch ($tag) {
|
|
||||||
case 'endjs':
|
|
||||||
$continue = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new \Twig\Error\SyntaxError(sprintf('Unexpected end of template. Twig was looking for the following tags "endjs" to close the "mytag" block started at line %d)', $lineno), -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// you want $body at the beginning of your arguments
|
|
||||||
array_unshift($params, $body);
|
|
||||||
|
|
||||||
// if your endjs can also contains params, you can uncomment this line:
|
|
||||||
// $params = array_merge($params, $this->getInlineParams($token));
|
|
||||||
// and comment this one:
|
|
||||||
$stream->expect(\Twig\Token::BLOCK_END_TYPE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JsTagNode(new \Twig\Node\Node($params), $lineno, $this->getTag());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recovers all tag parameters until we find a BLOCK_END_TYPE ( %} )
|
|
||||||
*
|
|
||||||
* @param \Twig\Token $token
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function getInlineParams(\Twig\Token $token) {
|
|
||||||
$stream = $this->parser->getStream();
|
|
||||||
$params = array ();
|
|
||||||
while (!$stream->test(\Twig\Token::BLOCK_END_TYPE)) {
|
|
||||||
$params[] = $this->parser->getExpressionParser()->parseExpression();
|
|
||||||
}
|
|
||||||
$stream->expect(\Twig\Token::BLOCK_END_TYPE);
|
|
||||||
return $params;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback called at each tag name when subparsing, must return
|
|
||||||
* true when the expected end tag is reached.
|
|
||||||
*
|
|
||||||
* @param \Twig\Token $token
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function decideJsTagFork(\Twig\Token $token) {
|
|
||||||
return $token->test(['endjs']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Your tag name: if the parsed tag match the one you put here, your parse()
|
|
||||||
* method will be called.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getTag() {
|
|
||||||
return 'js';
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class JsTagNode extends \Twig\Node\Node {
|
|
||||||
|
|
||||||
public function __construct($params, $lineno = 0, $tag = null) {
|
|
||||||
parent::__construct(['params' => $params], [], $lineno, $tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function compile(\Twig\Compiler $compiler) {
|
|
||||||
$count = count($this->getNode('params'));
|
|
||||||
|
|
||||||
$compiler->addDebugInfo($this);
|
|
||||||
$compiler
|
|
||||||
->write('global $__tpl;')
|
|
||||||
->raw(PHP_EOL);
|
|
||||||
|
|
||||||
for ($i = 0; ($i < $count); $i++) {
|
|
||||||
// argument is not an expression (such as, a \Twig\Node\Textbody)
|
|
||||||
// we should trick with output buffering to get a valid argument to pass
|
|
||||||
// to the functionToCall() function.
|
|
||||||
if (!($this->getNode('params')->getNode($i) instanceof \Twig\Node\Expression\AbstractExpression)) {
|
|
||||||
$compiler
|
|
||||||
->write('ob_start();')
|
|
||||||
->raw(PHP_EOL);
|
|
||||||
|
|
||||||
$compiler
|
|
||||||
->subcompile($this->getNode('params')->getNode($i));
|
|
||||||
|
|
||||||
$compiler
|
|
||||||
->write('$js = ob_get_clean();')
|
|
||||||
->raw(PHP_EOL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$compiler
|
|
||||||
->write('$__tpl->add_js($js);')
|
|
||||||
->raw(PHP_EOL)
|
|
||||||
->write('unset($js);')
|
|
||||||
->raw(PHP_EOL);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $data
|
|
||||||
*/
|
|
||||||
function ajax_ok($data) {
|
|
||||||
ajax_response(['response' => $data]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $error
|
|
||||||
* @param int $code
|
|
||||||
*/
|
|
||||||
function ajax_error($error, $code = 200) {
|
|
||||||
ajax_response(['error' => $error], $code);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $data
|
|
||||||
* @param int $code
|
|
||||||
*/
|
|
||||||
function ajax_response($data, $code = 200) {
|
|
||||||
header('Cache-Control: no-cache, must-revalidate');
|
|
||||||
header('Pragma: no-cache');
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
http_response_code($code);
|
|
||||||
echo jsonEncode($data);
|
|
||||||
exit;
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class AuthHandler extends RequestHandler {
|
|
||||||
|
|
||||||
protected function before_dispatch(string $method, string $act) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function GET_auth() {
|
|
||||||
list($error) = $this->input('error');
|
|
||||||
$this->tpl->set(['error' => $error]);
|
|
||||||
$this->tpl->set_title('Авторизация');
|
|
||||||
$this->tpl->render_page('auth.twig');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function POST_auth() {
|
|
||||||
list($username, $password) = $this->input('username, password');
|
|
||||||
|
|
||||||
$result = users::validatePassword($username, $password);
|
|
||||||
if (!$result) {
|
|
||||||
debugError('invalid login attempt: '.$_SERVER['REMOTE_ADDR'].', '.$_SERVER['HTTP_USER_AGENT'].", username=$username, password=$password");
|
|
||||||
redirect('/auth/?error='.urlencode('неверный логин или пароль'));
|
|
||||||
}
|
|
||||||
|
|
||||||
auth::setToken(pwhash($password));
|
|
||||||
redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function GET_deauth() {
|
|
||||||
if (auth::id())
|
|
||||||
auth::logout();
|
|
||||||
|
|
||||||
redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class FakeRequestHandler extends RequestHandler {
|
|
||||||
|
|
||||||
public function apacheNotFound() {
|
|
||||||
http_response_code(404);
|
|
||||||
$uri = htmlspecialchars($_SERVER['REQUEST_URI']);
|
|
||||||
echo <<<EOF
|
|
||||||
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
|
||||||
<html><head>
|
|
||||||
<title>404 Not Found</title>
|
|
||||||
</head><body>
|
|
||||||
<h1>Not Found</h1>
|
|
||||||
<p>The requested URL {$uri} was not found on this server.</p>
|
|
||||||
</body></html>
|
|
||||||
EOF;
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class MiscHandler extends RequestHandler
|
|
||||||
{
|
|
||||||
|
|
||||||
public function GET_sensors_page() {
|
|
||||||
global $config;
|
|
||||||
|
|
||||||
$clients = [];
|
|
||||||
foreach ($config['temphumd_servers'] as $key => $params) {
|
|
||||||
$cl = new TemphumdClient(...$params);
|
|
||||||
$clients[$key] = $cl;
|
|
||||||
|
|
||||||
$cl->readSensor();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->tpl->set(['sensors' => $clients]);
|
|
||||||
$this->tpl->set_title('Датчики');
|
|
||||||
$this->tpl->render_page('sensors.twig');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function GET_cams_stat() {
|
|
||||||
global $config;
|
|
||||||
list($ip, $port) = explode(':', $config['ipcam_server_api_addr']);
|
|
||||||
$body = jsonDecode(file_get_contents('http://'.$ip.':'.$port.'/api/timestamp/all'));
|
|
||||||
|
|
||||||
header('Content-Type: text/plain');
|
|
||||||
$date_fmt = 'd.m.Y H:i:s';
|
|
||||||
|
|
||||||
foreach ($body['response'] as $cam => $data) {
|
|
||||||
$fix = date($date_fmt, $data['fix']);
|
|
||||||
$start = date($date_fmt, $data['motion_start']);
|
|
||||||
$motion = date($date_fmt, $data['motion']);
|
|
||||||
echo "$cam:\n motion: $motion\n";
|
|
||||||
echo " motion_start: $start\n";
|
|
||||||
echo " fix: $fix\n\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,130 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use libphonenumber\NumberParseException;
|
|
||||||
use libphonenumber\PhoneNumberFormat;
|
|
||||||
use libphonenumber\PhoneNumberUtil;
|
|
||||||
|
|
||||||
class ModemHandler extends RequestHandler
|
|
||||||
{
|
|
||||||
|
|
||||||
public function GET_routing_smallhome_page() {
|
|
||||||
global $config;
|
|
||||||
|
|
||||||
list($error) = $this->input('error');
|
|
||||||
$upstream = self::getCurrentUpstream();
|
|
||||||
|
|
||||||
$current_upstream = [
|
|
||||||
'key' => $upstream,
|
|
||||||
'label' => $config['modems'][$upstream]['label']
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->tpl->set([
|
|
||||||
'error' => $error,
|
|
||||||
'current' => $current_upstream,
|
|
||||||
'modems' => $config['modems'],
|
|
||||||
]);
|
|
||||||
$this->tpl->set_title('Маршрутизация');
|
|
||||||
$this->tpl->render_page('routing_page.twig');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function GET_routing_smallhome_switch() {
|
|
||||||
global $config;
|
|
||||||
list($new_upstream) = $this->input('upstream');
|
|
||||||
if (!isset($config['modems'][$new_upstream]))
|
|
||||||
redirect('/routing/?error='.urlencode('invalid upstream'));
|
|
||||||
|
|
||||||
$current_upstream = self::getCurrentUpstream();
|
|
||||||
if ($current_upstream != $new_upstream) {
|
|
||||||
if ($new_upstream == 'mts-il')
|
|
||||||
$new_upstream_ip = '192.168.88.1';
|
|
||||||
else
|
|
||||||
$new_upstream_ip = $config['modems'][$new_upstream]['ip'];
|
|
||||||
MyOpenWrtUtils::setUpstream($new_upstream_ip);
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect('/routing/');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function GET_routing_ipsets_page() {
|
|
||||||
list($error) = $this->input('error');
|
|
||||||
|
|
||||||
$ip_sets = MyOpenWrtUtils::ipsetListAll();
|
|
||||||
$this->tpl->set([
|
|
||||||
'sets' => $ip_sets,
|
|
||||||
'error' => $error
|
|
||||||
]);
|
|
||||||
$this->tpl->set_title('Маршрутизация: IP sets');
|
|
||||||
$this->tpl->render_page('routing_ipsets_page.twig');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function GET_routing_ipsets_del() {
|
|
||||||
list($set, $ip) = $this->input('set, ip');
|
|
||||||
self::validateIpsetsInput($set, $ip);
|
|
||||||
|
|
||||||
$output = MyOpenWrtUtils::ipsetDel($set, $ip);
|
|
||||||
|
|
||||||
$url = '/routing/ipsets/';
|
|
||||||
if ($output != '')
|
|
||||||
$url .= '?error='.urlencode($output);
|
|
||||||
redirect($url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function POST_routing_ipsets_add() {
|
|
||||||
list($set, $ip) = $this->input('set, ip');
|
|
||||||
self::validateIpsetsInput($set, $ip);
|
|
||||||
|
|
||||||
$output = MyOpenWrtUtils::ipsetAdd($set, $ip);
|
|
||||||
|
|
||||||
$url = '/routing/ipsets/';
|
|
||||||
if ($output != '')
|
|
||||||
$url .= '?error='.urlencode($output);
|
|
||||||
redirect($url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function GET_routing_dhcp_page() {
|
|
||||||
$overrides = config::get('dhcp_hostname_overrides');
|
|
||||||
$leases = MyOpenWrtUtils::getDHCPLeases();
|
|
||||||
foreach ($leases as &$lease) {
|
|
||||||
if ($lease['hostname'] == '?' && array_key_exists($lease['mac'], $overrides))
|
|
||||||
$lease['hostname'] = $overrides[$lease['mac']];
|
|
||||||
}
|
|
||||||
$this->tpl->set([
|
|
||||||
'leases' => $leases
|
|
||||||
]);
|
|
||||||
$this->tpl->set_title('Маршрутизация: DHCP');
|
|
||||||
$this->tpl->render_page('routing_dhcp_page.twig');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static function getCurrentUpstream() {
|
|
||||||
global $config;
|
|
||||||
|
|
||||||
$default_route = MyOpenWrtUtils::getDefaultRoute();
|
|
||||||
if ($default_route == '192.168.88.1')
|
|
||||||
$default_route = $config['modems']['mts-il']['ip'];
|
|
||||||
$upstream = null;
|
|
||||||
foreach ($config['modems'] as $modem_name => $modem_data) {
|
|
||||||
if ($default_route == $modem_data['ip']) {
|
|
||||||
$upstream = $modem_name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (is_null($upstream))
|
|
||||||
$upstream = $config['routing_default'];
|
|
||||||
|
|
||||||
return $upstream;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static function validateIpsetsInput($set, $ip) {
|
|
||||||
global $config;
|
|
||||||
|
|
||||||
if (!isset($config['modems'][$set]))
|
|
||||||
redirect('/routing/ipsets/?error='.urlencode('invalid set: '.$set));
|
|
||||||
|
|
||||||
if (($slashpos = strpos($ip, '/')) !== false)
|
|
||||||
$ip = substr($ip, 0, $slashpos);
|
|
||||||
|
|
||||||
if (!filter_var($ip, FILTER_VALIDATE_IP))
|
|
||||||
redirect('/routing/ipsets/?error='.urlencode('invalid ip/network: '.$ip));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class RequestHandler extends request_handler {
|
|
||||||
|
|
||||||
/** @var web_tpl*/
|
|
||||||
protected $tpl;
|
|
||||||
|
|
||||||
public function __construct() {
|
|
||||||
global $__tpl;
|
|
||||||
$__tpl = new web_tpl();
|
|
||||||
$this->tpl = $__tpl;
|
|
||||||
|
|
||||||
$this->tpl->add_static('bootstrap.min.css');
|
|
||||||
$this->tpl->add_static('bootstrap.min.js');
|
|
||||||
$this->tpl->add_static('polyfills.js');
|
|
||||||
$this->tpl->add_static('app.js');
|
|
||||||
$this->tpl->add_static('app.css');
|
|
||||||
|
|
||||||
if (auth::id()) {
|
|
||||||
$this->tpl->set_global([
|
|
||||||
'auth_user' => auth::$authorizedUser
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function dispatch(string $act) {
|
|
||||||
global $config;
|
|
||||||
$this->tpl->set_global([
|
|
||||||
'__dev' => $config['is_dev'],
|
|
||||||
]);
|
|
||||||
return parent::dispatch($act);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function method_not_found(string $method, string $act)
|
|
||||||
{
|
|
||||||
global $config;
|
|
||||||
|
|
||||||
if ($act != '404' && $config['is_dev'])
|
|
||||||
debugError(get_called_class() . ": act {$method}_{$act} not found.");
|
|
||||||
|
|
||||||
if (!is_xhr_request())
|
|
||||||
$this->tpl->render_not_found();
|
|
||||||
else
|
|
||||||
ajax_error('unknown act "'.$act.'"', 404);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function before_dispatch(string $method, string $act) {
|
|
||||||
if (config::get('auth_need') && !auth::id())
|
|
||||||
redirect('/auth/');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
RewriteEngine on
|
|
||||||
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteCond %{REQUEST_URI} !=/server-status
|
|
||||||
RewriteRule ^.*$ /index.php [L,QSA]
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once __DIR__.'/../init.php';
|
|
||||||
|
|
||||||
$router = new router;
|
|
||||||
|
|
||||||
$router->add('routing/', 'Modem routing_smallhome_page');
|
|
||||||
$router->add('routing/switch-small-home/', 'Modem routing_smallhome_switch');
|
|
||||||
$router->add('routing/{ipsets,dhcp}/', 'Modem routing_${1}_page');
|
|
||||||
$router->add('routing/ipsets/{add,del}/', 'Modem routing_ipsets_${1}');
|
|
||||||
|
|
||||||
$router->add('sms/', 'Modem sms');
|
|
||||||
// $router->add('modem/set.ajax', 'Modem ctl_set_ajax');
|
|
||||||
|
|
||||||
// inverter
|
|
||||||
$router->add('inverter/set-osp/', 'Inverter set_osp');
|
|
||||||
|
|
||||||
// misc
|
|
||||||
$router->add('/', 'Misc main');
|
|
||||||
$router->add('sensors/', 'Misc sensors_page');
|
|
||||||
$router->add('cams/', 'Misc cams');
|
|
||||||
$router->add('cams/([\d,]+)/', 'Misc cams id=$(1)');
|
|
||||||
$router->add('cams/stat/', 'Misc cams_stat');
|
|
||||||
$router->add('debug/', 'Misc debug');
|
|
||||||
|
|
||||||
// auth
|
|
||||||
$router->add('auth/', 'Auth auth');
|
|
||||||
$router->add('deauth/', 'Auth deauth');
|
|
||||||
|
|
||||||
|
|
||||||
$route = routerFind($router);
|
|
||||||
if ($route === false)
|
|
||||||
(new FakeRequestHandler)->dispatch('404');
|
|
||||||
|
|
||||||
list($handler, $act, $RouterInput) = $route;
|
|
||||||
|
|
||||||
$handler_class = $handler.'Handler';
|
|
||||||
if (!class_exists($handler_class)) {
|
|
||||||
debugError('index.php: class '.$handler_class.' not found');
|
|
||||||
(new FakeRequestHandler)->dispatch('404');
|
|
||||||
}
|
|
||||||
|
|
||||||
(new $handler_class)->dispatch($act);
|
|
@ -1 +0,0 @@
|
|||||||
Page Not Found
|
|
@ -1,24 +0,0 @@
|
|||||||
{% include 'bc.twig' with {
|
|
||||||
history: [
|
|
||||||
{text: "Авторизация" }
|
|
||||||
]
|
|
||||||
} %}
|
|
||||||
|
|
||||||
{% if error %}
|
|
||||||
<div class="mt-4 alert alert-danger"><b>Ошибка:</b> {{ error }}</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
<form method="post" action="/auth/">
|
|
||||||
<div class="mt-2">
|
|
||||||
<input type="text" name="username" placeholder="Логин" class="form-control">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-2">
|
|
||||||
<input type="password" name="password" placeholder="Пароль" class="form-control">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-2">
|
|
||||||
<button type="submit" class="btn btn-outline-primary">Войти</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
@ -1,12 +0,0 @@
|
|||||||
<nav aria-label="breadcrumb">
|
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li class="breadcrumb-item"><a href="/">Главная</a></li>
|
|
||||||
{% for item in history %}
|
|
||||||
<li class="breadcrumb-item"{% if loop.last %} aria-current="page"{% endif %}>
|
|
||||||
{% if item.link %}<a href="{{ item.link }}">{% endif %}
|
|
||||||
{{ item.html ? item.html|raw : item.text }}
|
|
||||||
{% if item.link %}</a>{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
@ -1,8 +0,0 @@
|
|||||||
{% if js %}
|
|
||||||
<script>{{ js|raw }}</script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
<!-- generated in {{ exec_time}} -->
|
|
@ -1,15 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>{{ title }}</title>
|
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
||||||
<script>
|
|
||||||
window.onerror = function(error) {
|
|
||||||
window.console && console.error(error);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{{ static|raw }}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container py-3">
|
|
@ -1,35 +0,0 @@
|
|||||||
<div class="container py-4">
|
|
||||||
<nav aria-label="breadcrumb">
|
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">Главная</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{% if auth_user %}
|
|
||||||
<div class="mb-4 alert alert-secondary">
|
|
||||||
Вы авторизованы как <b>{{ auth_user.username }}</b>. <a href="/deauth/">Выйти</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h6>Интернет</h6>
|
|
||||||
<ul class="list-group list-group-flush">
|
|
||||||
<li class="list-group-item"><a href="/modem/">Модемы</a></li>
|
|
||||||
<li class="list-group-item"><a href="/routing/">Маршрутизация</a></li>
|
|
||||||
<li class="list-group-item"><a href="/sms/">SMS-сообщения</a></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h6 class="mt-4">Другое</h6>
|
|
||||||
<ul class="list-group list-group-flush">
|
|
||||||
<li class="list-group-item"><a href="/inverter/">Инвертор</a> (<a href="/inverter/?alt=1">alt</a>, <a href="{{ grafana_inverter_url }}">Grafana</a>)</li>
|
|
||||||
<li class="list-group-item"><a href="/pump/">Насос</a> (<a href="/pump/?alt=1">alt</a>)</li>
|
|
||||||
<li class="list-group-item"><a href="/sensors/">Датчики</a> (<a href="{{ grafana_sensors_url }}">Grafana</a>)</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h6 class="mt-4"><a href="/cams/"><b>Все камеры</b></a> (<a href="/cams/?high=1">HQ</a>)</h6>
|
|
||||||
<ul class="list-group list-group-flush">
|
|
||||||
{% for id, name in cameras %}
|
|
||||||
<li class="list-group-item"><a href="/cams/{{ id }}/">{{ name }}</a> (<a href="/cams/{{ id }}/?high=1">HQ</a>)</li>
|
|
||||||
{% endfor %}
|
|
||||||
<li class="list-group-item"><a href="/cams/stat/">Статистика</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
@ -1,11 +0,0 @@
|
|||||||
{% include 'routing_header.twig' with {
|
|
||||||
selected_tab: 'dhcp'
|
|
||||||
} %}
|
|
||||||
|
|
||||||
{% for lease in leases %}
|
|
||||||
<div class="mt-3">
|
|
||||||
<b>{{ lease.hostname }}</b> <span class="text-secondary">(exp: {{ lease.time_s }})</span><br/>
|
|
||||||
{{ lease.ip }}<br>
|
|
||||||
<span class="text-secondary">{{ lease.mac }}</span>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
@ -1,23 +0,0 @@
|
|||||||
{% include 'bc.twig' with {
|
|
||||||
history: [
|
|
||||||
{text: "Маршрутизация" }
|
|
||||||
]
|
|
||||||
} %}
|
|
||||||
|
|
||||||
{% set routing_tabs = [
|
|
||||||
{tab: 'smallhome', url: '/routing/', label: 'Интернет'},
|
|
||||||
{tab: 'ipsets', url: '/routing/ipsets/', label: 'Правила'},
|
|
||||||
{tab: 'dhcp', url: '/routing/dhcp/', label: 'DHCP'}
|
|
||||||
] %}
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
<div class="nav nav-tabs" id="nav-tab">
|
|
||||||
{% for tab in routing_tabs %}
|
|
||||||
<a href="{{ tab.url }}" class="text-decoration-none"><button class="nav-link{% if tab.tab == selected_tab %} active{% endif %}" type="button">{{ tab.label }}</button></a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{% if error %}
|
|
||||||
<div class="mt-4 alert alert-danger"><b>Ошибка:</b> {{ error }}</div>
|
|
||||||
{% endif %}
|
|
@ -1,29 +0,0 @@
|
|||||||
{% include 'routing_header.twig' with {
|
|
||||||
selected_tab: 'ipsets'
|
|
||||||
} %}
|
|
||||||
|
|
||||||
<div class="mt-2 text-secondary">
|
|
||||||
Таблицы расположены в порядке применения правил iptables.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% for set, ips in sets %}
|
|
||||||
<h6 class="text-primary mt-4">{{ set }}</h6>
|
|
||||||
|
|
||||||
{% if ips %}
|
|
||||||
{% for ip in ips %}
|
|
||||||
<div>{{ ip }} (<a href="/routing/ipsets/del/?set={{ set }}&ip={{ ip }}" onclick="return confirm('Подтвердите удаление {{ ip }} из {{ set }}.')">удалить</a>)</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-secondary">Нет записей.</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div style="max-width: 300px">
|
|
||||||
<form method="post" action="/routing/ipsets/add/">
|
|
||||||
<input type="hidden" name="set" value="{{ set }}">
|
|
||||||
<div class="input-group mt-2">
|
|
||||||
<input type="text" name="ip" placeholder="x.x.x.x/y" class="form-control">
|
|
||||||
<button type="submit" class="btn btn-outline-primary">Добавить</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
@ -1,17 +0,0 @@
|
|||||||
{% include 'routing_header.twig' with {
|
|
||||||
selected_tab: 'smallhome'
|
|
||||||
} %}
|
|
||||||
|
|
||||||
<div class="mt-3 mb-3">
|
|
||||||
Текущий апстрим: <b>{{ current.label }}</b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% for key, modem in modems %}
|
|
||||||
{% if key != current.key %}
|
|
||||||
<div class="pt-1 pb-2">
|
|
||||||
<a href="/routing/switch-small-home/?upstream={{ key }}">
|
|
||||||
<button type="button" class="btn btn-primary">Переключить на <b>{{ modem.label }}</b></button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
@ -1,15 +0,0 @@
|
|||||||
{% include 'bc.twig' with {
|
|
||||||
history: [
|
|
||||||
{text: "Датчики" }
|
|
||||||
]
|
|
||||||
} %}
|
|
||||||
|
|
||||||
{% for key, sensor in sensors %}
|
|
||||||
<h6 class="text-primary{% if not loop.first %} mt-4{% endif %}">{{ sensor.name }}</h6>
|
|
||||||
{% if sensor.hasTemperature() %}
|
|
||||||
<span class="text-secondary">Температура:</span> <b>{{ sensor.temp }}</b> °C<br>
|
|
||||||
{% endif %}
|
|
||||||
{% if sensor.hasHumidity() %}
|
|
||||||
<span class="text-secondary">Влажность:</span> <b>{{ sensor.humidity }}</b>%
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
30
web/kbn_assets/error_page.css
Normal file
30
web/kbn_assets/error_page.css
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #f9cfcf;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.error_title {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #5d1b1b;
|
||||||
|
}
|
||||||
|
.error_message {
|
||||||
|
color: #000;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.error_traceback {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.error_traceback .error_title {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.error_traceback_content {
|
||||||
|
font-family: monospace;
|
||||||
|
display: block;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
{% macro breadcrumbs(history) %}
|
{% macro breadcrumbs(history) %}
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="main.cgi">{{ "main"|lang }}</a></li>
|
<li class="breadcrumb-item"><a href="/main.cgi">{{ "main"|lang }}</a></li>
|
||||||
{% for item in history %}
|
{% for item in history %}
|
||||||
<li class="breadcrumb-item"{% if loop.last %} aria-current="page"{% endif %}>
|
<li class="breadcrumb-item"{% if loop.last %} aria-current="page"{% endif %}>
|
||||||
{% if item.link %}<a href="{{ item.link }}">{% endif %}
|
{% if item.link %}<a href="{{ item.link }}">{% endif %}
|
||||||
|
@ -17,15 +17,15 @@
|
|||||||
<h6>{{ "internet"|lang }}</h6>
|
<h6>{{ "internet"|lang }}</h6>
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
<li class="list-group-item"><a href="/modems.cgi">{{ "modems"|lang }}</a></li>
|
<li class="list-group-item"><a href="/modems.cgi">{{ "modems"|lang }}</a></li>
|
||||||
<li class="list-group-item"><a href="/routing.cgi">{{ "routing"|lang }}</a></li>
|
<li class="list-group-item"><a href="/routing/main.cgi">{{ "routing"|lang }}</a></li>
|
||||||
<li class="list-group-item"><a href="/sms.cgi">{{ "sms"|lang }}</a></li>
|
<li class="list-group-item"><a href="/sms.cgi">{{ "sms"|lang }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="mt-4">{{ "misc"|lang }}</h6>
|
<h6 class="mt-4">{{ "misc"|lang }}</h6>
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
<li class="list-group-item"><a href="/inverter.cgi">{{ "inverter"|lang }}</a> (<a href="{{ inverter_grafana_url }}">Grafana</a>)</li>
|
<li class="list-group-item"><a href="/inverter.cgi">{{ "inverter"|lang }}</a> (<a href="{{ inverter_grafana_url }}">Grafana</a>)</li>
|
||||||
<li class="list-group-item"><a href="/pump.cgi">{{ "pump"|lang }}</a></li>
|
{# <li class="list-group-item"><a href="/pump.cgi">{{ "pump"|lang }}</a></li>#}
|
||||||
<li class="list-group-item"><a href="/sensors.cgi">{{ "sensors"|lang }}</a> (<a href="{{ sensors_grafana_url }}">Grafana</a>)</li>
|
{# <li class="list-group-item"><a href="/sensors.cgi">{{ "sensors"|lang }}</a> (<a href="{{ sensors_grafana_url }}">Grafana</a>)</li>#}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<nav class="mt-4">
|
<nav class="mt-4">
|
||||||
|
14
web/kbn_templates/routing_dhcp.j2
Normal file
14
web/kbn_templates/routing_dhcp.j2
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.j2" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'routing_header.j2' %}
|
||||||
|
|
||||||
|
{% for lease in leases %}
|
||||||
|
<div class="mt-3">
|
||||||
|
<b>{{ lease.hostname }}</b> <span class="text-secondary">(exp: {{ lease.time_s }})</span><br/>
|
||||||
|
{{ lease.ip }}<br>
|
||||||
|
<span class="text-secondary">{{ lease.mac }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endblock %}
|
19
web/kbn_templates/routing_header.j2
Normal file
19
web/kbn_templates/routing_header.j2
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{{ breadcrumbs([{'text': 'routing'|lang}]) }}
|
||||||
|
|
||||||
|
{% set routing_tabs = [
|
||||||
|
{'tab': 'main', 'url': '/routing/main.cgi', 'label': 'routing_main'|lang},
|
||||||
|
{'tab': 'rules', 'url': '/routing/rules.cgi', 'label': 'routing_rules'|lang},
|
||||||
|
{'tab': 'dhcp', 'url': '/routing/dhcp.cgi', 'label': 'DHCP'}
|
||||||
|
] %}
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<div class="nav nav-tabs" id="nav-tab">
|
||||||
|
{% for tab in routing_tabs %}
|
||||||
|
<a href="{{ tab.url }}" class="text-decoration-none"><button class="nav-link{% if tab.tab == selected_tab %} active{% endif %}" type="button">{{ tab.label }}</button></a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="mt-4 alert alert-danger"><b>{{ "error"|lang }}:</b> {{ error }}</div>
|
||||||
|
{% endif %}
|
19
web/kbn_templates/routing_main.j2
Normal file
19
web/kbn_templates/routing_main.j2
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% extends "base.j2" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'routing_header.j2' %}
|
||||||
|
|
||||||
|
<div class="mt-3 mb-3">
|
||||||
|
{{ "routing_current_upstream"|lang }}: <b>{{ (upstream|lang('modems'))['full'] }}</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for modem in modems %}
|
||||||
|
{% if modem != upstream %}
|
||||||
|
<div class="pt-1 pb-2">
|
||||||
|
<a href="/routing/main.cgi?set-upstream-to={{ modem }}">
|
||||||
|
<button type="button" class="btn btn-primary">{{ "routing_switch_to"|lang }} <b>{{ (modem|lang('modems'))['full'] }}</b></button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
30
web/kbn_templates/routing_rules.j2
Normal file
30
web/kbn_templates/routing_rules.j2
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{% extends "base.j2" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'routing_header.j2' %}
|
||||||
|
|
||||||
|
<div class="mt-2 text-secondary">{{ "routing_iptables_note"|lang }}</div>
|
||||||
|
|
||||||
|
{% for set, ips in sets.items() %}
|
||||||
|
<h6 class="text-primary mt-4">{{ set }}</h6>
|
||||||
|
|
||||||
|
{% if ips %}
|
||||||
|
{% for ip in ips %}
|
||||||
|
<div>{{ ip }} (<a href="/routing/rules.cgi?action=del&set={{ set }}&ip={{ ip }}" onclick="return confirm('{{ 'routing_deleting_confirmation'|lang|format(ip, set) }}')">{{ "routing_del"|lang }}</a>)</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-secondary">{{ "routing_no_records"|lang }}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div style="max-width: 300px">
|
||||||
|
<form method="get" action="/routing/rules.cgi">
|
||||||
|
<input type="hidden" name="action" value="add">
|
||||||
|
<input type="hidden" name="set" value="{{ set }}">
|
||||||
|
<div class="input-group mt-2">
|
||||||
|
<input type="text" name="ip" placeholder="x.x.x.x/y" class="form-control">
|
||||||
|
<button type="submit" class="btn btn-outline-primary">{{ "routing_add"|lang }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user