Skip to content

Commit b88fdfc

Browse files
committed
collect_api_endpoints.py - refactor controller parser to ease further extensions and ease pipelining to different outputs
1 parent 83d9aa8 commit b88fdfc

File tree

2 files changed

+144
-118
lines changed

2 files changed

+144
-118
lines changed

collect_api_endpoints.py

Lines changed: 3 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/local/bin/python3
22
"""
3-
Copyright (c) 2020 Ad Schellevis <[email protected]>
3+
Copyright (c) 2020-2025 Ad Schellevis <[email protected]>
44
All rights reserved.
55
66
Redistribution and use in source and binary forms, with or without
@@ -26,127 +26,12 @@
2626
"""
2727
import os
2828
import argparse
29-
import re
3029
from jinja2 import Template
30+
from lib import ApiParser
3131

3232
EXCLUDE_CONTROLLERS = ['Core/Api/FirmwareController.php']
33-
DEFAULT_BASE_METHODS = {
34-
"ApiMutableModelControllerBase": [{
35-
"command": "set",
36-
"parameters": "",
37-
"method": "POST"
38-
}, {
39-
"command": "get",
40-
"parameters": "",
41-
"method": "GET"
42-
}],
43-
"ApiMutableServiceControllerBase": [{
44-
"command": "status",
45-
"parameters": "",
46-
"method": "GET"
47-
}, {
48-
"command": "start",
49-
"parameters": "",
50-
"method": "POST"
51-
}, {
52-
"command": "stop",
53-
"parameters": "",
54-
"method": "POST"
55-
}, {
56-
"command": "restart",
57-
"parameters": "",
58-
"method": "POST"
59-
}, {
60-
"command": "reconfigure",
61-
"parameters": "",
62-
"method": "POST"
63-
}]
64-
}
6533

6634

67-
def parse_api_php(src_filename):
68-
base_filename = os.path.basename(src_filename)
69-
controller = re.sub('(?<!^)(?=[A-Z])', '_', os.path.basename(base_filename.split('Controller.php')[0])).lower()
70-
module_name = src_filename.replace('\\', '/').split('/')[-3].lower()
71-
72-
data = open(src_filename).read()
73-
m = re.findall(r"\n([\w]*).*class.*Controller.*extends\s([\w|\\]*)", data)
74-
base_class = m[0][1].split('\\')[-1] if len(m) > 0 else None
75-
is_abstract = len(m) > 0 and m[0][0] == 'abstract'
76-
77-
m = re.findall(r"\sprotected\sstatic\s\$internalModelClass\s=\s['|\"]([\w|\\]*)['|\"];", data)
78-
if len(m) == 0:
79-
m = re.findall(r"\sprotected\sstatic\s\$internalServiceClass\s=\s['|\"]([\w|\\]*)['|\"];", data)
80-
81-
model_filename = None
82-
if len(m) > 0:
83-
app_location = "/".join(src_filename.split('/')[:-5])
84-
model_xml = "%s/models/%s.xml" % (app_location, m[0].replace("\\", "/"))
85-
if os.path.isfile(model_xml):
86-
model_filename = model_xml.replace('//', '/')
87-
88-
function_callouts = re.findall(r"(\n\s+(private|public|protected)\s+function\s+(\w+)\((.*)\))", data)
89-
result = list()
90-
this_commands = []
91-
for idx, function in enumerate(function_callouts):
92-
begin_marker = data.find(function_callouts[idx][0])
93-
if idx+1 < len(function_callouts):
94-
end_marker = data.find(function_callouts[idx+1][0])
95-
else:
96-
end_marker = -1
97-
code_block = data[begin_marker+len(function[0]):end_marker]
98-
if function[2].endswith('Action'):
99-
cmd = "".join("_" + c.lower() if c.isupper() else c for c in function[2][:-6])
100-
this_commands.append(function[2][:-6])
101-
record = {
102-
'method': 'GET',
103-
'module': module_name,
104-
'controller': controller,
105-
'is_abstract': is_abstract,
106-
'base_class': base_class,
107-
'command': cmd,
108-
'parameters': function[3].replace(' ', '').replace('"', '""'),
109-
'filename': base_filename,
110-
'model_filename': model_filename
111-
}
112-
if is_abstract:
113-
record['type'] = 'Abstract [non-callable]'
114-
elif controller.find('service') > -1:
115-
record['type'] = 'Service'
116-
else:
117-
record['type'] = 'Resources'
118-
# find most likely method (default => GET)
119-
if code_block.find('request->isPost(') > -1:
120-
record['method'] = 'POST'
121-
elif code_block.find('$this->delBase') > -1:
122-
record['method'] = 'POST'
123-
elif code_block.find('$this->addBase') > -1:
124-
record['method'] = 'POST'
125-
elif code_block.find('$this->setBase') > -1:
126-
record['method'] = 'POST'
127-
elif code_block.find('$this->toggleBase') > -1:
128-
record['method'] = 'POST'
129-
elif code_block.find('$this->searchBase') > -1:
130-
record['method'] = '*'
131-
result.append(record)
132-
if base_class in DEFAULT_BASE_METHODS:
133-
for item in DEFAULT_BASE_METHODS[base_class]:
134-
if item not in this_commands:
135-
result.append({
136-
'type': 'Service',
137-
'method': item['method'],
138-
'module': module_name,
139-
'controller': controller,
140-
'is_abstract': False,
141-
'base_class': base_class,
142-
'command': item['command'],
143-
'parameters': item['parameters'],
144-
'filename': base_filename,
145-
'model_filename': model_filename
146-
})
147-
148-
return sorted(result, key=lambda i: i['command'])
149-
15035
def source_url(repo, src_filename):
15136
parts = src_filename.split('/')
15237
if repo == 'plugins':
@@ -172,7 +57,7 @@ def source_url(repo, src_filename):
17257
break
17358
if not skip and filename.lower().endswith('controller.php') and filename.find('mvc/app/controllers') > -1 \
17459
and root.endswith('Api'):
175-
payload = parse_api_php(filename)
60+
payload = ApiParser(filename).parse_api_php()
17661
if len(payload) > 0:
17762
if payload[0]['module'] not in all_modules:
17863
all_modules[payload[0]['module']] = list()

lib/__init__.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import os
2+
import re
3+
4+
DEFAULT_BASE_METHODS = {
5+
"ApiMutableModelControllerBase": [{
6+
"command": "set",
7+
"parameters": "",
8+
"method": "POST"
9+
}, {
10+
"command": "get",
11+
"parameters": "",
12+
"method": "GET"
13+
}],
14+
"ApiMutableServiceControllerBase": [{
15+
"command": "status",
16+
"parameters": "",
17+
"method": "GET"
18+
}, {
19+
"command": "start",
20+
"parameters": "",
21+
"method": "POST"
22+
}, {
23+
"command": "stop",
24+
"parameters": "",
25+
"method": "POST"
26+
}, {
27+
"command": "restart",
28+
"parameters": "",
29+
"method": "POST"
30+
}, {
31+
"command": "reconfigure",
32+
"parameters": "",
33+
"method": "POST"
34+
}]
35+
}
36+
37+
38+
class ApiParser:
39+
def __init__(self, filename):
40+
self._filename = filename
41+
self.base_filename = os.path.basename(filename)
42+
self.controller = re.sub('(?<!^)(?=[A-Z])', '_', self.base_filename.split('Controller.php')[0]).lower()
43+
self.module_name = filename.replace('\\', '/').split('/')[-3].lower()
44+
self._data = open(filename).read()
45+
46+
def _get_model_filename(self):
47+
m = re.findall(r"\sprotected\sstatic\s\$internalModelClass\s=\s['|\"]([\w|\\]*)['|\"];", self._data)
48+
if len(m) == 0:
49+
m = re.findall(r"\sprotected\sstatic\s\$internalServiceClass\s=\s['|\"]([\w|\\]*)['|\"];", self._data)
50+
51+
if len(m) > 0:
52+
app_location = "/".join(self._filename.split('/')[:-5])
53+
model_xml = "%s/models/%s.xml" % (app_location, m[0].replace("\\", "/"))
54+
if os.path.isfile(model_xml):
55+
return model_xml.replace('//', '/')
56+
57+
def _parse_action_content(self, code_block):
58+
record = {}
59+
boilerplates = {
60+
'get': {'pattern': '$this->getBase', 'method': 'GET'},
61+
'del': {'pattern': '$this->delBase', 'method': 'POST'},
62+
'add': {'pattern': '$this->addBase', 'method': 'POST'},
63+
'set': {'pattern': '$this->setBase', 'method': 'POST'},
64+
'toggle': {'pattern': '$this->toggleBase', 'method': 'POST'},
65+
'search': {'pattern': '$this->searchBase', 'method': 'GET,POST'},
66+
}
67+
for action, boilerplate in boilerplates.items():
68+
pos = code_block.find(boilerplate['pattern'])
69+
if pos > -1:
70+
record['method'] = boilerplate['method']
71+
72+
if 'method' not in record:
73+
methods = []
74+
if code_block.find('request->isPost(') > -1:
75+
methods.append('POST')
76+
if code_block.find('request->isGet(') > -1:
77+
methods.append('GET')
78+
if len(methods) > 1:
79+
record['method'] = ','.join(methods)
80+
81+
return record
82+
83+
84+
def parse_api_php(self):
85+
data = self._data
86+
m = re.findall(r"\n([\w]*).*class.*Controller.*extends\s([\w|\\]*)", data)
87+
base_class = m[0][1].split('\\')[-1] if len(m) > 0 else None
88+
is_abstract = len(m) > 0 and m[0][0] == 'abstract'
89+
90+
model_filename = self._get_model_filename()
91+
92+
function_callouts = re.findall(r"(\n\s+(private|public|protected)\s+function\s+(\w+)\((.*)\))", data)
93+
result = list()
94+
this_commands = []
95+
for idx, function in enumerate(function_callouts):
96+
begin_marker = data.find(function_callouts[idx][0])
97+
if idx+1 < len(function_callouts):
98+
end_marker = data.find(function_callouts[idx+1][0])
99+
else:
100+
end_marker = -1
101+
code_block = data[begin_marker+len(function[0]):end_marker]
102+
if function[2].endswith('Action'):
103+
cmd = "".join("_" + c.lower() if c.isupper() else c for c in function[2][:-6])
104+
this_commands.append(cmd)
105+
record = {
106+
'method': 'GET',
107+
'module': self.module_name,
108+
'controller': self.controller,
109+
'is_abstract': is_abstract,
110+
'base_class': base_class,
111+
'command': cmd,
112+
'parameters': function[3].replace(' ', '').replace('"', '""'),
113+
'filename': self.base_filename,
114+
'model_filename': model_filename
115+
}
116+
if is_abstract:
117+
record['type'] = 'Abstract [non-callable]'
118+
elif self.controller.find('service') > -1:
119+
record['type'] = 'Service'
120+
else:
121+
record['type'] = 'Resources'
122+
record.update(self._parse_action_content(code_block))
123+
# find most likely method (default => GET)
124+
result.append(record)
125+
if base_class in DEFAULT_BASE_METHODS:
126+
for item in DEFAULT_BASE_METHODS[base_class]:
127+
if item not in this_commands:
128+
result.append({
129+
'type': 'Service',
130+
'method': item['method'],
131+
'module': self.module_name,
132+
'controller': self.controller,
133+
'is_abstract': False,
134+
'base_class': base_class,
135+
'command': item['command'],
136+
'parameters': item['parameters'],
137+
'filename': self.base_filename,
138+
'model_filename': model_filename
139+
})
140+
141+
return sorted(result, key=lambda i: i['command'])

0 commit comments

Comments
 (0)