diff options
52 files changed, 703 insertions, 2740 deletions
diff --git a/bin/relay_mqtt_http_proxy.py b/bin/relay_mqtt_http_proxy.py index 23938e1..866ead3 100755 --- a/bin/relay_mqtt_http_proxy.py +++ b/bin/relay_mqtt_http_proxy.py @@ -2,6 +2,7 @@ import logging import __py_include +from aiohttp import web from homekit import http from homekit.config import config, AppConfigUnit from homekit.mqtt import MqttPayload, MqttWrapper, MqttNode, MqttModule, MqttNodesConfig @@ -15,7 +16,6 @@ mqtt: Optional[MqttWrapper] = None mqtt_nodes: dict[str, MqttNode] = {} relay_modules: dict[str, Union[MqttRelayModule, MqttModule]] = {} relay_states: dict[str, MqttRelayState] = {} - mqtt_nodes_config = MqttNodesConfig() @@ -67,41 +67,47 @@ def on_mqtt_message(node: MqttNode, relay_states[node.id].update(**kwargs) -class RelayMqttHttpProxy(http.HTTPServer): - def __init__(self, *args, **kwargs): - 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) +# -=-=-=-=-=-=- # +# Web interface # +# -=-=-=-=-=-=- # + +routes = web.RouteTableDef() + + +async def _relay_on_off(self, + enable: Optional[bool], + req: web.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: + 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_on_off(self, - enable: Optional[bool], - req: http.Request): - node_id = req.match_info['id'] - node_secret = req.query['secret'] + node.secret = node_secret + relay_module.switchpower(enable) + return self.ok() - node = mqtt_nodes[node_id] - relay_module = relay_modules[node_id] - if enable is None: - 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 +@routes.get('/relay/{id}/on') +async def relay_on(self, req: web.Request): + return await self._relay_on_off(True, req) - node.secret = node_secret - relay_module.switchpower(enable) - return self.ok() - async def relay_on(self, req: http.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) - async def relay_off(self, req: http.Request): - return await self._relay_on_off(False, req) - async def relay_toggle(self, req: http.Request): - return await self._relay_on_off(None, 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__': @@ -127,8 +133,7 @@ if __name__ == '__main__': mqtt.connect_and_loop(loop_forever=False) - proxy = RelayMqttHttpProxy(config.app_config['listen_addr']) try: - proxy.run() + http.serve(config.app_config['listen_addr'], routes=routes) except KeyboardInterrupt: mqtt.disconnect() diff --git a/bin/sound_node.py b/bin/sound_node.py index 90e6997..4d0172c 100755 --- a/bin/sound_node.py +++ b/bin/sound_node.py @@ -17,7 +17,7 @@ from homekit import http def _amixer_control_response(control): info = amixer.get(control) caps = amixer.get_caps(control) - return http.ok({ + return http.ajax_ok({ 'caps': caps, 'info': info }) diff --git a/bin/web_api.py b/bin/web_api.py index d221838..1804b30 100755 --- a/bin/web_api.py +++ b/bin/web_api.py @@ -51,7 +51,7 @@ class WebAPIServer(http.HTTPServer): @staticmethod @web.middleware - async def validate_auth(req: http.Request, handler): + async def validate_auth(req: web.Request, handler): def get_token() -> str: name = 'X-Token' if name in req.headers: @@ -70,13 +70,13 @@ class WebAPIServer(http.HTTPServer): return await handler(req) @staticmethod - async def get_index(req: http.Request): + async def get_index(req: web.Request): message = "nothing here, keep lurking" if is_development_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: hours = int(req.query['hours']) if hours < 1 or hours > 24: @@ -93,7 +93,7 @@ class WebAPIServer(http.HTTPServer): data = db.get_temperature_recordings(sensor, (dt_from, dt_to)) 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'])) after = int(req.query['after']) @@ -112,7 +112,7 @@ class WebAPIServer(http.HTTPServer): data = BotsDatabase().get_sound_hits(location, **kwargs) 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 = [] data = await req.post() for hit, count in json.loads(data['hits']): @@ -125,7 +125,7 @@ class WebAPIServer(http.HTTPServer): BotsDatabase().add_sound_hits(hits, datetime.now()) 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() try: @@ -154,7 +154,7 @@ class WebAPIServer(http.HTTPServer): BotsDatabase().add_openwrt_logs(lines, ap) 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() try: @@ -176,7 +176,7 @@ class WebAPIServer(http.HTTPServer): return self.ok(files) @staticmethod - def _get_inverter_from_to(req: http.Request): + def _get_inverter_from_to(req: web.Request): s_from = req.query['from'] s_to = req.query['to'] @@ -189,12 +189,12 @@ class WebAPIServer(http.HTTPServer): 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) wh = InverterDatabase().get_consumed_energy(dt_from, dt_to) 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) wh = InverterDatabase().get_grid_consumed_energy(dt_from, dt_to) return self.ok(wh) diff --git a/bin/web_kbn.py b/bin/web_kbn.py index bf17046..a0f12a6 100644 --- a/bin/web_kbn.py +++ b/bin/web_kbn.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +import __py_include import asyncio +import logging import jinja2 import aiohttp_jinja2 import json @@ -7,18 +9,18 @@ import re import inverterd import phonenumbers import time -import __py_include from io import StringIO -from aiohttp.web import HTTPFound, HTTPBadRequest +from aiohttp import web from typing import Optional, Union +from urllib.parse import quote_plus from homekit.config import config, AppConfigUnit, is_development_mode, Translation 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.inverter.config import InverterdConfig from homekit.relay.sunxi_h3_client import RelayClient -from homekit import http +from homekit import openwrt, http class WebKbnConfig(AppConfigUnit): @@ -36,13 +38,16 @@ class WebKbnConfig(AppConfigUnit): } -STATIC_FILES = [ +common_static_files = [ 'bootstrap.min.css', 'bootstrap.min.js', 'polyfills.js', 'app.js', 'app.css' ] +routes = web.RouteTableDef() +webkbn_strings = Translation('web_kbn') +logger = logging.getLogger(__name__) def get_js_link(file, version) -> str: @@ -65,7 +70,7 @@ def get_head_static(files=None) -> str: buf = StringIO() if files is None: files = [] - for file in STATIC_FILES+files: + for file in common_static_files + files: v = 2 try: q_ind = file.index('?') @@ -179,245 +184,356 @@ def get_inverter_data() -> tuple: return status, rated, html -class WebSite(http.HTTPServer): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) +def get_current_upstream() -> str: + r = openwrt.get_default_route() + 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 + + +def lang(key: str): + 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} - 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) + 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 + } + } - def filter_lang(key, unit='web_kbn'): - strings = Translation(unit) - return strings.get()[key] + return await render(req, 'cams', + title=lang('cams'), + assets=['hls.js'], + context=dict( + mode=mode, + js_config=js_config, + )) - env.filters['tojson'] = lambda obj: json.dumps(obj, separators=(',', ':'), default=json_serial) - env.filters['lang'] = filter_lang - self.app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets')) +@routes.get('/routing/main.cgi') +async def routing_main(req: web.Request): + upstream = get_current_upstream() - self.get('/main.cgi', self.index) + set_upstream_to = req.query.get('set-upstream-to', None) + mc = ModemsConfig() - self.get('/modems.cgi', self.modems) - self.get('/modems/info.ajx', self.modems_ajx) - self.get('/modems/verbose.cgi', self.modems_verbose) + 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') - 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) + context = dict( + upstream=upstream, + selected_tab='main', + modems=mc.keys() + ) + return await render(req, 'routing_main', title=lang('routing'), context=context) - 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} +@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: - 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 - } - } + 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 await self.render_page(req, 'cams', - title='Камеры', - assets=['hls.js'], - context=dict( - mode=mode, - js_config=js_config, - )) + env.filters['tojson'] = lambda obj: json.dumps(obj, separators=(',', ':'), default=json_serial) + env.filters['lang'] = filter_lang + + app.router.add_static('/assets/', path=homekit_path('web', 'kbn_assets')) if __name__ == '__main__': config.load_app(WebKbnConfig) - - server = WebSite(config.app_config['listen_addr']) - server.run() + http.serve(addr=config.app_config['listen_addr'], + routes=routes, + before_start=init_web_app) diff --git a/doc/localwebsite.md b/doc/localwebsite.md deleted file mode 100644 index b46f62e..0000000 --- a/doc/localwebsite.md +++ /dev/null @@ -1,7 +0,0 @@ -## Dependencies - -``` -apt install nginx-extras php-fpm php-mbstring php-sqlite3 php-curl php-simplexml php-gmp composer -``` - - diff --git a/include/py/homekit/config/config.py b/include/py/homekit/config/config.py index 3aa0e04..1eec97d 100644 --- a/include/py/homekit/config/config.py +++ b/include/py/homekit/config/config.py @@ -67,7 +67,7 @@ class BaseConfigUnit(ABC): return self._data cur = self._data - pts = key.split('.') + pts = str(key).split('.') for i in range(len(pts)): k = pts[i] if i < len(pts)-1: diff --git a/include/py/homekit/http/__init__.py b/include/py/homekit/http/__init__.py index d019e4c..f3721a4 100644 --- a/include/py/homekit/http/__init__.py +++ b/include/py/homekit/http/__init__.py @@ -1,2 +1 @@ -from .http import serve, ok, routes, HTTPServer, HTTPMethod -from aiohttp.web import FileResponse, StreamResponse, Request, Response
\ No newline at end of file +from .http import serve, ajax_ok, HTTPMethod diff --git a/include/py/homekit/http/http.py b/include/py/homekit/http/http.py index 8819c46..a8c7d82 100644 --- a/include/py/homekit/http/http.py +++ b/include/py/homekit/http/http.py @@ -1,17 +1,46 @@ import logging import asyncio +import html from enum import Enum from aiohttp import web -from aiohttp.web import Response, HTTPFound +from aiohttp.web import HTTPFound from aiohttp.web_exceptions import HTTPNotFound - from ..util import stringify, format_tb, Addr - _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 async def errors_handler_middleware(request, handler): try: @@ -19,97 +48,56 @@ async def errors_handler_middleware(request, handler): return response 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: raise exc except Exception as exc: _logger.exception(exc) - data = { - 'error': exc.__class__.__name__, - 'message': exc.message if hasattr(exc, 'message') else str(exc) - } - tb = format_tb(exc) - if tb: - data['stacktrace'] = tb + return _render_error( + error_type=exc.__class__.__name__, + error_message=exc.message if hasattr(exc, 'message') else str(exc), + traceback=format_tb(exc) + ) - 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.add_routes(route_table) app.middlewares.append(errors_handler_middleware) - host, port = addr + if routes is not None: + app.add_routes(routes) - web.run_app(app, - host=host, - port=port, - handle_signals=handle_signals) + if callable(before_start): + before_start(app) + if not event_loop: + event_loop = asyncio.get_event_loop() -def routes() -> web.RouteTableDef: - return web.RouteTableDef() + runner = web.AppRunner(app, handle_signals=handle_signals) + event_loop.run_until_complete(runner.setup()) + host, port = addr + site = web.TCPSite(runner, host=host, port=port) + event_loop.run_until_complete(site.start()) -def ok(data=None): + _logger.info(f'Server started at http://{host}:{port}') + + event_loop.run_forever() + + +def ajax_ok(data=None): if data is None: data = 1 response = {'response': data} 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): GET = 'GET' POST = 'POST' diff --git a/include/py/homekit/media/node_server.py b/include/py/homekit/media/node_server.py index 5d0803c..229b9f7 100644 --- a/include/py/homekit/media/node_server.py +++ b/include/py/homekit/media/node_server.py @@ -33,12 +33,12 @@ class MediaNodeServer(http.HTTPServer): raise ValueError(f'invalid duration: max duration is {max}') 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): record_id = int(request.match_info['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): 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}" self.recorder.forget(record_id) - return http.ok() + return http.ajax_ok() async def record_download(self, request: http.Request): record_id = int(request.match_info['id']) @@ -64,7 +64,7 @@ class MediaNodeServer(http.HTTPServer): if extended: files = list(map(lambda file: file.__dict__(), files)) - return http.ok({ + return http.ajax_ok({ 'files': files }) @@ -75,7 +75,7 @@ class MediaNodeServer(http.HTTPServer): raise ValueError(f'file {file} not found') self.storage.delete(file) - return http.ok() + return http.ajax_ok() async def storage_download(self, request): file_id = request.query['file_id'] diff --git a/include/py/homekit/openwrt/__init__.py b/include/py/homekit/openwrt/__init__.py new file mode 100644 index 0000000..b233b00 --- /dev/null +++ b/include/py/homekit/openwrt/__init__.py @@ -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 +) diff --git a/include/py/homekit/openwrt/config.py b/include/py/homekit/openwrt/config.py new file mode 100644 index 0000000..bd75d1c --- /dev/null +++ b/include/py/homekit/openwrt/config.py @@ -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} + }
\ No newline at end of file diff --git a/include/py/homekit/openwrt/openwrt.py b/include/py/homekit/openwrt/openwrt.py new file mode 100644 index 0000000..d5f949c --- /dev/null +++ b/include/py/homekit/openwrt/openwrt.py @@ -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)
\ No newline at end of file diff --git a/include/py/homekit/soundsensor/server.py b/include/py/homekit/soundsensor/server.py index a627390..d6320c1 100644 --- a/include/py/homekit/soundsensor/server.py +++ b/include/py/homekit/soundsensor/server.py @@ -3,6 +3,7 @@ import json import logging import threading +from aiohttp import web from ..database.sqlite import SQLiteBase from ..config import config from .. import http @@ -108,21 +109,21 @@ class SoundSensorServer: loop.run_forever() def run_guard_server(self): - routes = http.routes() + routes = web.RouteTableDef() @routes.post('/guard/enable') async def guard_enable(request): self.set_recording(True) - return http.ok() + return http.ajax_ok() @routes.post('/guard/disable') async def guard_disable(request): self.set_recording(False) - return http.ok() + return http.ajax_ok() @routes.get('/guard/status') 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 - 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 diff --git a/include/py/homekit/util.py b/include/py/homekit/util.py index 4410251..2d76968 100644 --- a/include/py/homekit/util.py +++ b/include/py/homekit/util.py @@ -105,6 +105,11 @@ class Addr: yield self.host 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 def chunks(lst, n): diff --git a/localwebsite/classes/MyOpenWrtUtils.php b/localwebsite/classes/MyOpenWrtUtils.php deleted file mode 100644 index c140fa1..0000000 --- a/localwebsite/classes/MyOpenWrtUtils.php +++ /dev/null @@ -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 - ]; - } - -} diff --git a/localwebsite/classes/MySimpleSocketClient.php b/localwebsite/classes/MySimpleSocketClient.php deleted file mode 100644 index e59efba..0000000 --- a/localwebsite/classes/MySimpleSocketClient.php +++ /dev/null @@ -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)); - } - -}
\ No newline at end of file diff --git a/localwebsite/classes/TemphumdClient.php b/localwebsite/classes/TemphumdClient.php deleted file mode 100644 index 07e5a3e..0000000 --- a/localwebsite/classes/TemphumdClient.php +++ /dev/null @@ -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; - } - -}
\ No newline at end of file diff --git a/localwebsite/classes/User.php b/localwebsite/classes/User.php deleted file mode 100644 index 9019082..0000000 --- a/localwebsite/classes/User.php +++ /dev/null @@ -1,11 +0,0 @@ -<?php - -class User extends model { - - const DB_TABLE = 'users'; - - public int $id; - public string $username; - public string $password; - -} diff --git a/localwebsite/classes/auth.php b/localwebsite/classes/auth.php deleted file mode 100644 index a13843b..0000000 --- a/localwebsite/classes/auth.php +++ /dev/null @@ -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; - } - -}
\ No newline at end of file diff --git a/localwebsite/classes/config.php b/localwebsite/classes/config.php deleted file mode 100644 index 927321e..0000000 --- a/localwebsite/classes/config.php +++ /dev/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]; - } - -}
\ No newline at end of file diff --git a/localwebsite/classes/users.php b/localwebsite/classes/users.php deleted file mode 100644 index 1160dba..0000000 --- a/localwebsite/classes/users.php +++ /dev/null @@ -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); - } - -}
\ No newline at end of file diff --git a/localwebsite/engine/debug.php b/localwebsite/engine/debug.php deleted file mode 100644 index b1b959f..0000000 --- a/localwebsite/engine/debug.php +++ /dev/null @@ -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)); - } -} diff --git a/localwebsite/engine/model.php b/localwebsite/engine/model.php deleted file mode 100644 index 4dd981c..0000000 --- a/localwebsite/engine/model.php +++ /dev/null @@ -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]; - } - -} diff --git a/localwebsite/engine/request_handler.php b/localwebsite/engine/request_handler.php deleted file mode 100644 index 535e850..0000000 --- a/localwebsite/engine/request_handler.php +++ /dev/null @@ -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]; - } - -} diff --git a/localwebsite/engine/router.php b/localwebsite/engine/router.php deleted file mode 100644 index 5e966a9..0000000 --- a/localwebsite/engine/router.php +++ /dev/null @@ -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]; -}
\ No newline at end of file diff --git a/localwebsite/engine/tpl.php b/localwebsite/engine/tpl.php deleted file mode 100644 index 3d18c9a..0000000 --- a/localwebsite/engine/tpl.php +++ /dev/null @@ -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; -}
\ No newline at end of file diff --git a/localwebsite/handlers/AuthHandler.php b/localwebsite/handlers/AuthHandler.php deleted file mode 100644 index 971f850..0000000 --- a/localwebsite/handlers/AuthHandler.php +++ /dev/null @@ -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('/'); - } - -} diff --git a/localwebsite/handlers/FakeRequestHandler.php b/localwebsite/handlers/FakeRequestHandler.php deleted file mode 100644 index 09e03b8..0000000 --- a/localwebsite/handlers/FakeRequestHandler.php +++ /dev/null @@ -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; - } - -}
\ No newline at end of file diff --git a/localwebsite/handlers/MiscHandler.php b/localwebsite/handlers/MiscHandler.php deleted file mode 100644 index 4e7291e..0000000 --- a/localwebsite/handlers/MiscHandler.php +++ /dev/null @@ -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"; - } - } - -} diff --git a/localwebsite/handlers/ModemHandler.php b/localwebsite/handlers/ModemHandler.php deleted file mode 100644 index 94ad75b..0000000 --- a/localwebsite/handlers/ModemHandler.php +++ /dev/null @@ -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)); - } - -} diff --git a/localwebsite/handlers/RequestHandler.php b/localwebsite/handlers/RequestHandler.php deleted file mode 100644 index 136a23e..0000000 --- a/localwebsite/handlers/RequestHandler.php +++ /dev/null @@ -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/'); - } -}
\ No newline at end of file diff --git a/localwebsite/htdocs/.htaccess b/localwebsite/htdocs/.htaccess deleted file mode 100644 index c18b135..0000000 --- a/localwebsite/htdocs/.htaccess +++ /dev/null @@ -1,6 +0,0 @@ -RewriteEngine on - -RewriteCond %{REQUEST_FILENAME} !-f -RewriteCond %{REQUEST_FILENAME} !-d -RewriteCond %{REQUEST_URI} !=/server-status -RewriteRule ^.*$ /index.php [L,QSA] diff --git a/localwebsite/htdocs/favicon.ico b/localwebsite/htdocs/favicon.ico Binary files differdeleted file mode 100644 index d5ff579..0000000 --- a/localwebsite/htdocs/favicon.ico +++ /dev/null diff --git a/localwebsite/htdocs/index.php b/localwebsite/htdocs/index.php deleted file mode 100644 index cd32132..0000000 --- a/localwebsite/htdocs/index.php +++ /dev/null @@ -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); diff --git a/localwebsite/templates-web/404.twig b/localwebsite/templates-web/404.twig deleted file mode 100644 index e28f5c5..0000000 --- a/localwebsite/templates-web/404.twig +++ /dev/null @@ -1 +0,0 @@ -Page Not Found
\ No newline at end of file diff --git a/localwebsite/templates-web/auth.twig b/localwebsite/templates-web/auth.twig deleted file mode 100644 index a0107b3..0000000 --- a/localwebsite/templates-web/auth.twig +++ /dev/null @@ -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>
\ No newline at end of file diff --git a/localwebsite/templates-web/bc.twig b/localwebsite/templates-web/bc.twig deleted file mode 100644 index b74ad40..0000000 --- a/localwebsite/templates-web/bc.twig +++ /dev/null @@ -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>
\ No newline at end of file diff --git a/localwebsite/templates-web/footer.twig b/localwebsite/templates-web/footer.twig deleted file mode 100644 index 8739f80..0000000 --- a/localwebsite/templates-web/footer.twig +++ /dev/null @@ -1,8 +0,0 @@ -{% if js %} - <script>{{ js|raw }}</script> -{% endif %} - -</div> -</body> -</html> -<!-- generated in {{ exec_time}} -->
\ No newline at end of file diff --git a/localwebsite/templates-web/header.twig b/localwebsite/templates-web/header.twig deleted file mode 100644 index 1a866e1..0000000 --- a/localwebsite/templates-web/header.twig +++ /dev/null @@ -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">
\ No newline at end of file diff --git a/localwebsite/templates-web/index.twig b/localwebsite/templates-web/index.twig deleted file mode 100644 index b28a078..0000000 --- a/localwebsite/templates-web/index.twig +++ /dev/null @@ -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> diff --git a/localwebsite/templates-web/routing_dhcp_page.twig b/localwebsite/templates-web/routing_dhcp_page.twig deleted file mode 100644 index 12b837e..0000000 --- a/localwebsite/templates-web/routing_dhcp_page.twig +++ /dev/null @@ -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 %}
\ No newline at end of file diff --git a/localwebsite/templates-web/routing_header.twig b/localwebsite/templates-web/routing_header.twig deleted file mode 100644 index 7d07d0a..0000000 --- a/localwebsite/templates-web/routing_header.twig +++ /dev/null @@ -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 %} diff --git a/localwebsite/templates-web/routing_ipsets_page.twig b/localwebsite/templates-web/routing_ipsets_page.twig deleted file mode 100644 index 5996e68..0000000 --- a/localwebsite/templates-web/routing_ipsets_page.twig +++ /dev/null @@ -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 %}
\ No newline at end of file diff --git a/localwebsite/templates-web/routing_page.twig b/localwebsite/templates-web/routing_page.twig deleted file mode 100644 index 90437fd..0000000 --- a/localwebsite/templates-web/routing_page.twig +++ /dev/null @@ -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 %}
\ No newline at end of file diff --git a/localwebsite/templates-web/sensors.twig b/localwebsite/templates-web/sensors.twig deleted file mode 100644 index 1005dc0..0000000 --- a/localwebsite/templates-web/sensors.twig +++ /dev/null @@ -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 %} diff --git a/web/kbn_assets/error_page.css b/web/kbn_assets/error_page.css new file mode 100644 index 0000000..c39f932 --- /dev/null +++ b/web/kbn_assets/error_page.css @@ -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; +}
\ No newline at end of file diff --git a/web/kbn_templates/base.j2 b/web/kbn_templates/base.j2 index dc1fd58..53a2984 100644 --- a/web/kbn_templates/base.j2 +++ b/web/kbn_templates/base.j2 @@ -1,7 +1,7 @@ {% macro breadcrumbs(history) %} <nav aria-label="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 %} <li class="breadcrumb-item"{% if loop.last %} aria-current="page"{% endif %}> {% if item.link %}<a href="{{ item.link }}">{% endif %} diff --git a/web/kbn_templates/index.j2 b/web/kbn_templates/index.j2 index e246dc5..cd40819 100644 --- a/web/kbn_templates/index.j2 +++ b/web/kbn_templates/index.j2 @@ -17,15 +17,15 @@ <h6>{{ "internet"|lang }}</h6> <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="/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> </ul> <h6 class="mt-4">{{ "misc"|lang }}</h6> <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="/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="/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>#} </ul> <nav class="mt-4"> diff --git a/web/kbn_templates/routing_dhcp.j2 b/web/kbn_templates/routing_dhcp.j2 new file mode 100644 index 0000000..c8a3e8e --- /dev/null +++ b/web/kbn_templates/routing_dhcp.j2 @@ -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 %}
\ No newline at end of file diff --git a/web/kbn_templates/routing_header.j2 b/web/kbn_templates/routing_header.j2 new file mode 100644 index 0000000..527f154 --- /dev/null +++ b/web/kbn_templates/routing_header.j2 @@ -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 %} diff --git a/web/kbn_templates/routing_main.j2 b/web/kbn_templates/routing_main.j2 new file mode 100644 index 0000000..856da78 --- /dev/null +++ b/web/kbn_templates/routing_main.j2 @@ -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 %}
\ No newline at end of file diff --git a/web/kbn_templates/routing_rules.j2 b/web/kbn_templates/routing_rules.j2 new file mode 100644 index 0000000..61b68c9 --- /dev/null +++ b/web/kbn_templates/routing_rules.j2 @@ -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 %}
\ No newline at end of file |