Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
100.00% |
35 / 35 |
|
100.00% |
132 / 132 |
CRAP | |
100.00% |
1661 / 1661 |
picture | |
100.00% |
1 / 1 |
0 | |
100.00% |
51 / 51 |
|||
v | |
100.00% |
1 / 1 |
0 | |
100.00% |
6 / 6 |
|||
get | |
100.00% |
1 / 1 |
0 | |
100.00% |
30 / 30 |
|||
ts | |
100.00% |
1 / 1 |
0 | |
100.00% |
1 / 1 |
|||
dump | |
100.00% |
1 / 1 |
0 | n/a |
0 / 0 |
||||
{\L | |
100.00% |
1 / 1 |
0 | |
100.00% |
2 / 2 |
|||
{\url | |
100.00% |
1 / 1 |
0 | |
100.00% |
1 / 1 |
|||
Extension | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
__toString | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
AddOn | |
100.00% |
1 / 1 |
|
100.00% |
4 / 4 |
6 | |
100.00% |
9 / 9 |
__get | |
100.00% |
1 / 1 |
1 | n/a |
0 / 0 |
||||
__construct | |
100.00% |
1 / 1 |
3 | |
100.00% |
7 / 7 |
|||
show | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
__toString | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
Filter | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | n/a |
0 / 0 |
|||
Client | |
100.00% |
1 / 1 |
|
100.00% |
3 / 3 |
51 | |
100.00% |
62 / 62 |
__get | |
100.00% |
1 / 1 |
1 | n/a |
0 / 0 |
||||
__construct | |
100.00% |
1 / 1 |
7 | |
100.00% |
8 / 8 |
|||
init | |
100.00% |
1 / 1 |
43 | |
100.00% |
54 / 54 |
|||
Model | |
100.00% |
1 / 1 |
|
100.00% |
4 / 4 |
39 | |
100.00% |
39 / 39 |
__construct | |
100.00% |
1 / 1 |
6 | |
100.00% |
9 / 9 |
|||
find | |
100.00% |
1 / 1 |
6 | |
100.00% |
3 / 3 |
|||
load | |
100.00% |
1 / 1 |
12 | |
100.00% |
10 / 10 |
|||
save | |
100.00% |
1 / 1 |
15 | |
100.00% |
17 / 17 |
|||
User | |
100.00% |
1 / 1 |
|
100.00% |
5 / 5 |
42 | |
100.00% |
30 / 30 |
has | |
100.00% |
1 / 1 |
8 | |
100.00% |
8 / 8 |
|||
grant | |
100.00% |
1 / 1 |
5 | |
100.00% |
5 / 5 |
|||
clear | |
100.00% |
1 / 1 |
6 | |
100.00% |
9 / 9 |
|||
init | |
100.00% |
1 / 1 |
4 | |
100.00% |
8 / 8 |
|||
route | |
100.00% |
1 / 1 |
19 | n/a |
0 / 0 |
||||
Http | |
100.00% |
1 / 1 |
|
100.00% |
9 / 9 |
91 | |
100.00% |
103 / 103 |
url | |
100.00% |
1 / 1 |
14 | |
100.00% |
10 / 10 |
|||
redirect | |
100.00% |
1 / 1 |
9 | n/a |
0 / 0 |
||||
_r | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
mime | |
100.00% |
1 / 1 |
3 | |
100.00% |
8 / 8 |
|||
route | |
100.00% |
1 / 1 |
22 | |
100.00% |
23 / 23 |
|||
urlMatch | |
100.00% |
1 / 1 |
10 | |
100.00% |
4 / 4 |
|||
anonymous function | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
get | |
100.00% |
1 / 1 |
29 | |
100.00% |
38 / 38 |
|||
h | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
DS | |
100.00% |
1 / 1 |
|
100.00% |
13 / 13 |
89 | |
100.00% |
129 / 129 |
__get | |
100.00% |
1 / 1 |
1 | n/a |
0 / 0 |
||||
__construct | |
100.00% |
1 / 1 |
5 | |
100.00% |
8 / 8 |
|||
close | |
100.00% |
1 / 1 |
4 | |
100.00% |
6 / 6 |
|||
diag | |
100.00% |
1 / 1 |
6 | |
100.00% |
14 / 14 |
|||
db | |
100.00% |
1 / 1 |
14 | |
100.00% |
21 / 21 |
|||
ds | |
100.00% |
1 / 1 |
4 | |
100.00% |
3 / 3 |
|||
like | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
exec | |
100.00% |
1 / 1 |
35 | |
100.00% |
57 / 57 |
|||
query | |
100.00% |
1 / 1 |
8 | |
100.00% |
2 / 2 |
|||
fetch | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
field | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
tree | |
100.00% |
1 / 1 |
7 | |
100.00% |
10 / 10 |
|||
bill | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
Cache | |
100.00% |
1 / 1 |
|
100.00% |
6 / 6 |
34 | |
100.00% |
28 / 28 |
__get | |
100.00% |
1 / 1 |
1 | n/a |
0 / 0 |
||||
__construct | |
100.00% |
1 / 1 |
17 | |
100.00% |
13 / 13 |
|||
set | |
100.00% |
1 / 1 |
5 | |
100.00% |
3 / 3 |
|||
get | |
100.00% |
1 / 1 |
4 | |
100.00% |
3 / 3 |
|||
invalidate | |
100.00% |
1 / 1 |
6 | |
100.00% |
8 / 8 |
|||
init | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
Assets | |
100.00% |
1 / 1 |
|
100.00% |
3 / 3 |
102 | |
100.00% |
56 / 56 |
route | |
100.00% |
1 / 1 |
13 | n/a |
0 / 0 |
||||
b | |
100.00% |
1 / 1 |
4 | n/a |
0 / 0 |
||||
minify | |
100.00% |
1 / 1 |
85 | |
100.00% |
56 / 56 |
|||
Content | |
100.00% |
1 / 1 |
|
100.00% |
3 / 3 |
33 | |
100.00% |
52 / 52 |
__construct | |
100.00% |
1 / 1 |
15 | |
100.00% |
31 / 31 |
|||
action | |
100.00% |
1 / 1 |
3 | |
100.00% |
5 / 5 |
|||
getDDS | |
100.00% |
1 / 1 |
15 | |
100.00% |
16 / 16 |
|||
View | |
100.00% |
1 / 1 |
|
100.00% |
15 / 15 |
312 | |
100.00% |
503 / 503 |
init | |
100.00% |
1 / 1 |
6 | |
100.00% |
11 / 11 |
|||
setPath | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
assign | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
css | |
100.00% |
1 / 1 |
5 | |
100.00% |
8 / 8 |
|||
jslib | |
100.00% |
1 / 1 |
11 | |
100.00% |
12 / 12 |
|||
js | |
100.00% |
1 / 1 |
5 | |
100.00% |
8 / 8 |
|||
menu | |
100.00% |
1 / 1 |
4 | |
100.00% |
5 / 5 |
|||
fromCache | |
100.00% |
1 / 1 |
4 | |
100.00% |
7 / 7 |
|||
generate | |
100.00% |
1 / 1 |
12 | |
100.00% |
23 / 23 |
|||
template | |
100.00% |
1 / 1 |
7 | |
100.00% |
10 / 10 |
|||
getval | |
100.00% |
1 / 1 |
46 | |
100.00% |
80 / 80 |
|||
tc | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
e | |
100.00% |
1 / 1 |
8 | |
100.00% |
5 / 5 |
|||
_t | |
100.00% |
1 / 1 |
109 | |
100.00% |
185 / 185 |
|||
output | |
100.00% |
1 / 1 |
92 | |
100.00% |
144 / 144 |
|||
Tools | n/a |
0 / 0 |
|
100.00% |
6 / 6 |
68 | n/a |
0 / 0 |
||
rmdir | |
100.00% |
1 / 1 |
3 | n/a |
0 / 0 |
||||
untar | |
100.00% |
1 / 1 |
31 | n/a |
0 / 0 |
||||
ssh | |
100.00% |
1 / 1 |
13 | n/a |
0 / 0 |
||||
copy | |
100.00% |
1 / 1 |
6 | n/a |
0 / 0 |
||||
bg | |
100.00% |
1 / 1 |
10 | n/a |
0 / 0 |
||||
diag | |
100.00% |
1 / 1 |
5 | n/a |
0 / 0 |
||||
ClassMap | |
100.00% |
1 / 1 |
|
100.00% |
6 / 6 |
57 | |
100.00% |
84 / 84 |
__construct | |
100.00% |
1 / 1 |
9 | |
100.00% |
3 / 3 |
|||
anonymous function | |
100.00% |
1 / 1 |
3 | |
100.00% |
3 / 3 |
|||
has | |
100.00% |
1 / 1 |
6 | |
100.00% |
6 / 6 |
|||
map | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
ace | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
generate | |
100.00% |
1 / 1 |
37 | |
100.00% |
69 / 69 |
|||
Core | |
100.00% |
1 / 1 |
|
100.00% |
25 / 25 |
329 | |
100.00% |
270 / 270 |
__get | |
100.00% |
1 / 1 |
1 | n/a |
0 / 0 |
||||
__construct | |
100.00% |
1 / 1 |
87 | |
100.00% |
92 / 92 |
|||
anonymous function | |
100.00% |
1 / 1 |
4 | n/a |
0 / 0 |
||||
bootdiag | |
100.00% |
1 / 1 |
54 | n/a |
0 / 0 |
||||
i | |
100.00% |
1 / 1 |
6 | n/a |
0 / 0 |
||||
run | |
100.00% |
1 / 1 |
40 | n/a |
0 / 0 |
||||
lib | |
100.00% |
1 / 1 |
13 | |
100.00% |
15 / 15 |
|||
isInst | |
100.00% |
1 / 1 |
5 | |
100.00% |
3 / 3 |
|||
lang | |
100.00% |
1 / 1 |
6 | |
100.00% |
11 / 11 |
|||
log | |
100.00% |
1 / 1 |
19 | |
100.00% |
30 / 30 |
|||
isBtn | |
100.00% |
1 / 1 |
3 | |
100.00% |
1 / 1 |
|||
error | |
100.00% |
1 / 1 |
3 | |
100.00% |
6 / 6 |
|||
isError | |
100.00% |
1 / 1 |
2 | |
100.00% |
1 / 1 |
|||
event | |
100.00% |
1 / 1 |
6 | |
100.00% |
6 / 6 |
|||
validate | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
req2obj | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
req2arr | |
100.00% |
1 / 1 |
23 | |
100.00% |
31 / 31 |
|||
obj2str | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
arr2str | |
100.00% |
1 / 1 |
8 | |
100.00% |
12 / 12 |
|||
val2arr | |
100.00% |
1 / 1 |
4 | |
100.00% |
8 / 8 |
|||
tre2arr | |
100.00% |
1 / 1 |
25 | |
100.00% |
24 / 24 |
|||
bm | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
started | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
cf | |
100.00% |
1 / 1 |
9 | |
100.00% |
11 / 11 |
|||
toBytes | |
100.00% |
1 / 1 |
5 | |
100.00% |
9 / 9 |
|||
APC | |
100.00% |
1 / 1 |
|
100.00% |
4 / 4 |
15 | |
100.00% |
9 / 9 |
__construct | |
100.00% |
1 / 1 |
3 | |
100.00% |
3 / 3 |
|||
get | |
100.00% |
1 / 1 |
5 | |
100.00% |
4 / 4 |
|||
set | |
100.00% |
1 / 1 |
4 | |
100.00% |
1 / 1 |
|||
invalidate | |
100.00% |
1 / 1 |
3 | |
100.00% |
1 / 1 |
|||
Files | |
100.00% |
1 / 1 |
|
100.00% |
6 / 6 |
20 | |
100.00% |
33 / 33 |
__construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
fn | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
get | |
100.00% |
1 / 1 |
7 | |
100.00% |
9 / 9 |
|||
set | |
100.00% |
1 / 1 |
6 | |
100.00% |
10 / 10 |
|||
invalidate | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
cronMinute | |
100.00% |
1 / 1 |
4 | |
100.00% |
8 / 8 |
|||
get | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |
100.00% |
1 / 1 |
filter | |
100.00% |
1 / 1 |
2 | |
100.00% |
1 / 1 |
|||
post | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |
100.00% |
1 / 1 |
filter | |
100.00% |
1 / 1 |
2 | |
100.00% |
1 / 1 |
|||
loggedin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
filter | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
csrf | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
filter | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
hidden | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |
100.00% |
1 / 1 |
edit | |
100.00% |
1 / 1 |
2 | |
100.00% |
1 / 1 |
|||
button | |
100.00% |
1 / 1 |
|
100.00% |
2 / 2 |
7 | |
100.00% |
4 / 4 |
show | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
edit | |
100.00% |
1 / 1 |
6 | |
100.00% |
3 / 3 |
|||
update | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
6 | |
100.00% |
3 / 3 |
edit | |
100.00% |
1 / 1 |
6 | |
100.00% |
3 / 3 |
|||
text | |
100.00% |
1 / 1 |
|
100.00% |
2 / 2 |
25 | |
100.00% |
28 / 28 |
edit | |
100.00% |
1 / 1 |
22 | |
100.00% |
24 / 24 |
|||
validate | |
100.00% |
1 / 1 |
3 | |
100.00% |
4 / 4 |
|||
pass | |
100.00% |
1 / 1 |
|
100.00% |
3 / 3 |
9 | |
100.00% |
10 / 10 |
show | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
edit | |
100.00% |
1 / 1 |
2 | |
100.00% |
6 / 6 |
|||
validate | |
100.00% |
1 / 1 |
6 | |
100.00% |
3 / 3 |
|||
num | |
100.00% |
1 / 1 |
|
100.00% |
2 / 2 |
9 | |
100.00% |
20 / 20 |
edit | |
100.00% |
1 / 1 |
3 | |
100.00% |
7 / 7 |
|||
validate | |
100.00% |
1 / 1 |
6 | |
100.00% |
13 / 13 |
|||
select | |
100.00% |
1 / 1 |
|
100.00% |
2 / 2 |
31 | |
100.00% |
26 / 26 |
show | |
100.00% |
1 / 1 |
2 | |
100.00% |
1 / 1 |
|||
edit | |
100.00% |
1 / 1 |
29 | |
100.00% |
25 / 25 |
|||
check | |
100.00% |
1 / 1 |
|
100.00% |
3 / 3 |
16 | |
100.00% |
13 / 13 |
show | |
100.00% |
1 / 1 |
5 | |
100.00% |
4 / 4 |
|||
edit | |
100.00% |
1 / 1 |
7 | |
100.00% |
7 / 7 |
|||
validate | |
100.00% |
1 / 1 |
4 | |
100.00% |
2 / 2 |
|||
radio | |
100.00% |
1 / 1 |
|
100.00% |
2 / 2 |
11 | |
100.00% |
10 / 10 |
show | |
100.00% |
1 / 1 |
5 | |
100.00% |
4 / 4 |
|||
edit | |
100.00% |
1 / 1 |
6 | |
100.00% |
6 / 6 |
|||
phone | |
100.00% |
1 / 1 |
|
100.00% |
2 / 2 |
2 | |
100.00% |
6 / 6 |
edit | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
validate | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
|
100.00% |
1 / 1 |
|
100.00% |
3 / 3 |
10 | |
100.00% |
12 / 12 |
|
show | |
100.00% |
1 / 1 |
3 | |
100.00% |
3 / 3 |
|||
edit | |
100.00% |
1 / 1 |
3 | |
100.00% |
5 / 5 |
|||
validate | |
100.00% |
1 / 1 |
4 | |
100.00% |
4 / 4 |
|||
file | |
100.00% |
1 / 1 |
|
100.00% |
3 / 3 |
6 | |
100.00% |
8 / 8 |
show | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
edit | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
validate | |
100.00% |
1 / 1 |
4 | |
100.00% |
4 / 4 |
|||
color | |
100.00% |
1 / 1 |
|
100.00% |
2 / 2 |
3 | |
100.00% |
4 / 4 |
show | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
edit | |
100.00% |
1 / 1 |
2 | |
100.00% |
3 / 3 |
|||
date | |
100.00% |
1 / 1 |
|
100.00% |
2 / 2 |
2 | |
100.00% |
3 / 3 |
edit | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
validate | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
time | |
100.00% |
1 / 1 |
|
100.00% |
2 / 2 |
2 | |
100.00% |
3 / 3 |
edit | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
validate | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
label | |
100.00% |
1 / 1 |
|
100.00% |
2 / 2 |
3 | |
100.00% |
3 / 3 |
show | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
edit | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
<?php | |
/* | |
* PHP Portal Engine v3.0.0 | |
* https://github.com/bztsrc/phppe3/ | |
* | |
* Copyright LGPL 2016 bzt | |
* | |
* This program is free software; you can redistribute it and/or modify | |
* it under the terms of the GNU Lesser General Public License as published | |
* by the Free Software Foundation, either version 3 of the License, or | |
* (at your option) any later version. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU Lesser General Public License for more details. | |
* | |
* <http://www.gnu.org/licenses/> | |
* | |
* PHPPE Core - Use as is | |
*/ | |
/** | |
* @file public/source.php | |
* | |
* @author bzt | |
* @date 1 Jan 2016 | |
* @brief PHPPE micro-framework's Core | |
* | |
* @see https://raw.githubusercontent.com/bztsrc/phppe3/master/public/source.php | |
* | |
* This is the nicely formatted and commented source version of PHPPE Core | |
* | |
* NOTE on unit testing: redirects and methods with die() cannot be tested | |
* with PHPUnit. This does not mean they're untested, simply there are many | |
* cases which are checked through PHPPE\Http::get() and not directly called | |
* from test classes, see vendor/phppe/Developer/tests. Also note that | |
* Developer extension ships with it's own PHPUnit compatible unit tester | |
* class, so you can run the tests without phpunit.phar too. | |
*/ | |
namespace PHPPE { | |
define('VERSION', '3.0.0'); | |
/** | |
* Extension interface. We declare it as a class because | |
* implementing event handlers are optional. | |
*/ | |
class Extension | |
{ | |
//function diag(){} | |
//function init($cfg){} | |
//function route($app,$action){} | |
//function view($output){} | |
//function filter(){} | |
//function cronX($retCode){} | |
//function stat() | |
function __toString() | |
{ | |
return get_class($this); | |
} | |
} | |
/** | |
* Add-On prototype. | |
*/ | |
class AddOn | |
{ | |
protected $name; //!< instance name | |
public $args; //!< arguments in pharenthesis after type | |
public $fld; //!< field name | |
public $value; //!< object field's value | |
protected $attrs; //!< attributes, everything after the name in tag | |
protected $css; //!< css class to use, input or reqinput, occasionally errinput added | |
protected $conf; //!< configuration scheme | |
/** | |
* Magic getter to implement read-only properties | |
*/ | |
function __get($n) { return $this->$n; } | |
/** | |
* Constructor, do not try to override, use init() instead. | |
* | |
* @param array arguments, listed with pharenthesis after type in templates | |
* @param string name of the add-on | |
* @param mixed reference of object field | |
* @param array attributes, listed after field name in templates | |
* @param boolean required field flag | |
* | |
* @return PHPHE\AddOn instance | |
*/ | |
final public function __construct($a, $n, &$v, $t = [], $r = 0) | |
{ | |
//! save arguments, name and attributes | |
$this->args = $a; | |
$this->name = $n; | |
$this->fld = strtr($n, ['.' => '_']); | |
$this->value = $v; | |
$this->attrs = $t; | |
//! css class name reqinput for mandatory fields | |
$this->css = ((!empty($r) ? 'req' : '').'input').(Core::isError($n) ? ' errinput' : ''); | |
} | |
/** | |
* Init method is called when needed but only once per page generation | |
* constructor may be called several times depending on the template. | |
*/ | |
//! function init() | |
//! { | |
//! \PHPPE\Core::jslib() to load javascript libraries | |
//! \PHPPE\Core::js() to add javascript functions and | |
//! \PHPPE\Core::css() for style sheets here | |
//! } | |
/** | |
* Field input or widget configuration form. | |
* | |
* @return string output | |
*/ | |
//! function edit() {return "";} | |
/** | |
* Display a field's value or show widget face. | |
* | |
* @return string output | |
*/ | |
public function show() | |
{ | |
return htmlspecialchars($this->value); | |
} | |
/* | |
* Value validator, returns boolean and a failure reason | |
* | |
* @param string name of value to valudate, for error reporting | |
* @param mixed reference to value | |
* @param array arguments | |
* @param array attributes | |
* @return [boolean,error message] if the first value is true, it's valid | |
*/ | |
//! static function validate( $name, &$value,$args,$attrs ) | |
//! { | |
//! return [true, "Dummy validator that always pass"]; | |
//! } | |
function __toString() | |
{ | |
return get_class($this)."(".$this->name.")"; | |
} | |
} | |
/** | |
* Filter prototype. | |
*/ | |
class Filter | |
{ | |
//static function filter() | |
} | |
/** | |
* Client class, this is used to store client's information and session. | |
*/ | |
class Client extends Extension | |
{ | |
private $ip; //!< remote ip address. Also valid if behind proxy or load balancer | |
private $agent; //!< client program | |
private $user; //!< user account (unix user on CLI, http auth user on web) | |
private $tz; //!< client's timezone | |
public $lang; //!< client's prefered language | |
public $screen = []; //!< screen dimensions | |
public $geo = []; //!< geo location data (filled in by a third party extension) | |
/** | |
* Magic getter to implement read-only properties | |
*/ | |
function __get($n) { return $this->$n; } | |
/** | |
* Constructor. Starts user session | |
*/ | |
public function __construct($cfg=[]) | |
{ | |
Core::$client = $this; | |
//! start user session | |
if(empty($_SESSION)){ | |
// no way we could test this as session is already started when unit tests reach this | |
// @codeCoverageIgnoreStart | |
@session_name(!empty(Core::$core->sessionvar) ? Core::$core->sessionvar : 'pe_sid'); | |
@session_start(); | |
// @codeCoverageIgnoreEnd | |
} | |
//! refresh session cookie | |
if (ini_get('session.use_cookies')) { | |
@setcookie(session_name(), session_id(), Core::$core->now + Core::$core->timeout, | |
'/', Core::$core->base, !empty(Core::$core->secsession) ? 1 : 0, 1); | |
} | |
//! destroy user session if requested | |
if (isset($_REQUEST['clear'])) { | |
// @codeCoverageIgnoreStart | |
//! save logged in user if any | |
$d = 'pe_u'; | |
$u = @$_SESSION[$d]; | |
//! keep user object, but clear preferences | |
$u->data=[]; | |
$_SESSION = []; | |
$_SESSION[$d] = $u; | |
//! redirect user to reload everything | |
Http::redirect(); | |
// @codeCoverageIgnoreEnd | |
} | |
//! override autodetected timezone with a fixed one | |
if(!empty($cfg['tz'])) { | |
// @codeCoverageIgnoreStart | |
$this->tz = $cfg['tz']; | |
// @codeCoverageIgnoreEnd | |
} | |
} | |
/** | |
* Initialize event. Collects information on client (language, timezone, screen size etc.) | |
* | |
* @param array configuration | |
* | |
* @return boolean false if initialization failed | |
*/ | |
public function init($cfg = []) | |
{ | |
Core::$l = []; | |
View::assign('client', $this); | |
//! set up client's prefered language | |
$L = 'pe_l'; | |
$a = ''; | |
$d = []; | |
//! get prefered language from browser or from environment | |
if (empty($_SESSION[$L])) { | |
//! user preference | |
if(!empty($_SESSION['pe_u']->data['lang'])) | |
$d=[$_SESSION['pe_u']->data['lang']]; | |
else { | |
$i = 'HTTP_ACCEPT_LANGUAGE'; | |
$d = explode(',', strtr(!empty($_SERVER[$i]) ? $_SERVER[$i] : (getenv('LANG') || 'en'), ['/' => ''])); | |
} | |
} | |
//! this can be overriden from url | |
if (!empty($_REQUEST['lang'])) { | |
$d = [strtr(trim($_REQUEST['lang']), ['/' => ''])]; | |
} | |
//! look for valid language code | |
//! only allow if language is defined in core or in app | |
foreach ($d as $v) { | |
list($a) = explode(';', strtolower(str_replace('-', '_', $v))); | |
if (!empty($a) && | |
(file_exists("vendor/phppe/Core/lang/$a.php") || | |
file_exists("app/lang/$a.php") || $a == 'en')) { | |
$_SESSION[$L] = $a; | |
break; | |
} | |
} | |
//! failsafe | |
if (empty($_SESSION[$L])) { | |
$_SESSION[$L] = 'en'; | |
} | |
$this->lang = $v = $_SESSION[$L]; | |
$i = explode('_', $v); | |
//! set PHP locale for the language | |
setlocale(LC_ALL, strtolower($i[0]).'_'.strtoupper(!empty($i[1]) ? $i[1] : $i[0]).'.UTF8'); | |
//! load dictionary for core | |
Core::lang('Core'); | |
Core::lang('app'); | |
$L = 'pe_tz'; | |
if (Core::$w) { | |
//! Detect values for Web | |
// @codeCoverageIgnoreStart | |
$d = 'HTTP_USER_AGENT'; | |
$c = empty($_REQUEST['cache']) && !(empty($_SERVER[$d])||$_SERVER[$d]=="API"|| | |
strpos(strtolower($_SERVER[$d]),"wget")!==false|| | |
strpos(strtolower($_SERVER[$d]),"curl")!==false); | |
if (!isset($_REQUEST['nojs']) && empty($_SESSION[$L]) && $c) { | |
//! this is a small JavaScript page that shows up for the first time | |
//! after collecting information it redirects user so fast, he won't | |
//! notice a thing. | |
if (empty($_REQUEST['n'])) { | |
$_SESSION['pe_n'] = sha1(rand()); | |
//! save redirection url | |
Http::_r();$u=$_SESSION['pe_r'].(strpos($_SERVER['REQUEST_URI'], '?') === false ? '?' : '&'); | |
$g = 'getTimezoneOffset()'; | |
$d = "var d%=new Date();d%.setDate(1);d%.setMonth(@);d%=parseInt(d%.$g);"; | |
die("<html><script type='text/javascript' style='display:none;'>var now=new Date();".strtr($d, ['%' => '1', '@' => '1']). | |
strtr($d, ['%' => '2', '@' => '7']). | |
"txt=now.toString().replace(/[^\(]+\(([^\)]+)\)/,\"$1\");document.location.href=\"htt\"+\"".substr($u,3). | |
'n='.$_SESSION['pe_n']."&t=\"+(-now.$g*60)+\"&d=\"+(d1==d2||(d1<d2&&d1==parseInt(now.$g))||(d1>d2&&d2==parseInt(now.$g))?\"1\":\"0\")+\"&w=\"+screen.availWidth+\"&h=\"+screen.availHeight;</script><meta http-equiv='refresh' content='0;".$u."nojs'></html>"); | |
} elseif ($_REQUEST['n'] == $_SESSION['pe_n']) { | |
unset($_SESSION['pe_n']); | |
$_SESSION[$L] = timezone_name_from_abbr('', $_REQUEST['t'] + 0, $_REQUEST['d'] + 0); | |
$_SESSION['pe_w'] = floor($_REQUEST['w']); | |
$_SESSION['pe_h'] = floor($_REQUEST['h']); | |
$_SESSION['pe_j'] = true; | |
Http::redirect(); | |
} | |
} | |
if (isset($_REQUEST['nojs'])||isset($_REQUEST['n'])||!empty($_SESSION['pe_n'])) { | |
if($c && empty($_COOKIE)){$t=View::template("nocookies");die($t?$t:L("Please enable cookies"));} | |
unset($_SESSION['pe_n']); | |
$_SESSION['pe_j']=false; | |
$_SESSION[$L]="UTC"; | |
if(!isset($_REQUEST['nojs']))Http::redirect(); | |
} | |
// @codeCoverageIgnoreEnd | |
//! get client's real ip address | |
$d = 'HTTP_X_FORWARDED_FOR'; | |
$this->ip = $i = !empty($_SERVER[$d]) ? $_SERVER[$d] : $_SERVER['REMOTE_ADDR']; | |
$this->screen = !empty($_SESSION['pe_w']) ? [$_SESSION['pe_w'], $_SESSION['pe_h']] : [0, 0]; | |
//! agent is user agent | |
$d = 'HTTP_USER_AGENT'; | |
$this->agent = !empty($_SERVER[$d]) ? $_SERVER[$d] : 'browser'; | |
//! user is http auth user | |
$d = 'PHP_AUTH_USER'; | |
$this->user = !empty($_SERVER[$d]) ? $_SERVER[$d] : ''; | |
$this->js = intval(@$_SESSION['pe_j']); | |
} else { | |
//! detect values for CLI | |
$T = getenv('TZ'); | |
//! this should be a symlink to something | |
//! like /usr/share/zoneinfo/Europe/London | |
$d = explode('/', $T ? $T : @readlink('/etc/localtime')); | |
$c = count($d); | |
$_SESSION[$L] = $c > 1 ? $d[$c - 2].'/'.$d[$c - 1] : 'UTC'; | |
//! no IP for tty | |
$this->ip = 'CLI'; | |
//! query tty size. If you know a better, exec()-less way, let me know!!! | |
$c = intval(exec('tput cols 2>/dev/null')); | |
$d = intval(exec('tput lines 2>/dev/null')); | |
$this->screen = [$c < 1 ? 80 : $c, $d < 1 ? 25 : $d]; | |
//! agent is a terminal | |
$d = getenv('TERM'); | |
$this->agent = !empty($d) ? $d : 'term'; | |
//! user is standard unix user | |
$d = getenv('USER'); | |
$this->user = !empty($d) ? $d : ''; | |
$this->js = false; | |
Core::$core->noframe = 1; | |
} | |
//! set up client's timezone | |
if(empty($this->tz)) { | |
$this->tz = !empty($_SESSION[$L]) ? $_SESSION[$L] : 'UTC'; | |
} | |
date_default_timezone_set($this->tz); | |
} | |
} | |
/** | |
* Model that supports Object Relational Mapping. | |
*/ | |
class Model | |
{ | |
public $id; | |
public $name; | |
//! this breaks PSR-2, but required to exclude table name from sql | |
protected static $_table; | |
/** | |
* Default model constructor. Load object with data if id given. Id can be | |
* a property value list (associative array or object) as well. | |
* | |
* @param integer/mixed id/properties | |
*/ | |
function __construct($id="") | |
{ | |
if(!empty($id)) { | |
if(is_scalar($id)) { | |
$this->id = $id; | |
$this->load($id); | |
} else { | |
$d=get_object_vars($this); | |
foreach($id as $k=>$v) { | |
if($k[0]!="_" && array_key_exists($k,$d)) | |
$this->$k=$v; | |
} | |
} | |
} | |
} | |
/** | |
* Find objects of the same kind in database. | |
* | |
* @param string/array search phrase(s) | |
* @param string where clause with placeholders | |
* @param string order by | |
* @param string fields, comma separated list | |
* | |
* @return array of associative arrays | |
*/ | |
public static function find($s = [], $w = '', $o = '', $f = '*', $g = '') | |
{ | |
if (empty(static::$_table)) { | |
throw new \Exception('no _table'); | |
} | |
//get the records from current datasource | |
return DS::query($f, static::$_table, !empty($w) ? $w : (!empty($s)?'id=?':''), !empty($g) ? $g : '', $o, 0, 0, is_array($s) ? $s : [$s]); | |
} | |
/** | |
* Load, reload or find a record in database and load result into this object. | |
* | |
* @param integer id of the object to load, or (if second argument given) search phrase | |
* @param string where clause with placeholders | |
* @param string order by (optional) | |
* | |
* @return true on success | |
*/ | |
public function load($i = 0, $w = '', $o = '') | |
{ | |
if (empty(static::$_table)) { | |
throw new \Exception('no _table'); | |
} | |
//get the record from current datasource | |
$r = (array)DS::fetch('*', static::$_table, $w ? $w : 'id=?', '', $o, is_array($i) ? $i : [$i ? $i : $this->id]); | |
//update property values. FETCH_INTO not exactly what we want | |
if ($r) { | |
foreach (get_object_vars($this) as $k => $v) { | |
if ($k[0] != '_') { | |
$this->$k = is_string($r[$k]) && !empty($r[$k]) && | |
($r[$k][0] == '{' || $r[$k][0] == '[') ? json_decode($r[$k], true) : $r[$k]; | |
} | |
} | |
return true; | |
} | |
return false; | |
} | |
/** | |
* Save the current object into database. May also alter $id property (and that only). | |
* | |
* @param boolean force insert | |
* | |
* @return boolean true on success | |
*/ | |
public function save($f = 0) | |
{ | |
if (empty(static::$_table)) { | |
throw new \Exception('no _table'); | |
} | |
$d = DS::db(); | |
if (empty($d)) { | |
throw new \Exception('no ds'); | |
} | |
//build the arguments array | |
$a = []; | |
foreach (get_object_vars($this) as $k => $v) { | |
if ($k[0] != '_' && ($f || $k != 'id') && ($k != 'created'||!empty($v)) ) { | |
$a[$k] = is_scalar($v) ? $v : json_encode($v); | |
} | |
} | |
if (!DS::exec(($this->id && !$f ? | |
'UPDATE '.static::$_table.' SET '.implode('=?,', array_keys($a)).'=? WHERE id='.$d->quote($this->id) : | |
'INSERT INTO '.static::$_table.' ('.implode(',', array_keys($a)).') VALUES (?'.str_repeat(',?', count($a) - 1).')'), | |
array_values($a))) { | |
return false; | |
} | |
//save new id for inserts | |
if (!$this->id || $f) { | |
$this->id = $d->lastInsertId(); | |
} | |
//return id | |
return $this->id; | |
} | |
} | |
/** | |
* Default user class, will be extended by PHPPE Pack with Users class. | |
*/ | |
class User extends Model | |
{ | |
public $id = 0; //!< only for Anonymous. Otherwise user id can be a string as well | |
public $name = 'Anonymous'; //!< user real name | |
public $data = []; //!< user preferences | |
// protected static $_table = "users"; //! set table name. This should be in Users class! | |
protected $acl = []; //!< Access Control List | |
// private remote = []; //!< remote server configuration, added run-time | |
/** | |
* Check access for an access control entry. | |
* | |
* @param string/array access control entry or list (pipe separated string or array) | |
* | |
* @return boolean true or false | |
*/ | |
final public function has($l) | |
{ | |
//check if at least one of the ACE match | |
foreach (is_array($l) ? $l : explode('|', $l) as $a) { | |
$a = trim($a); | |
//is user logged in? | |
//is superadmin with bypass priviledge? | |
if (!empty($this->id) && ($this->id == -1 || | |
$a == 'loggedin' || | |
!empty($this->acl[$a]) || | |
!empty($this->acl["$a:".Core::$core->item]))) { | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* Grant priviledge for a user. | |
* | |
* @param string/array access control entry or list (pipe separated string or array) | |
*/ | |
public function grant($l) | |
{ | |
foreach (is_array($l) ? $l : explode('|', $l) as $a) { | |
$a = trim($a); | |
if (!empty($this->id) && !empty($a)) { | |
$this->acl[$a] = true; | |
} | |
} | |
} | |
/** | |
* Drop privileges, specific access control entry or the whole access control list. | |
* | |
* @param string/array access control entry or list (pipe separated string or array) or empty for dropping all | |
*/ | |
public function clear($l = '') | |
{ | |
if (empty($l)) { | |
$this->acl = []; | |
} else { | |
foreach (is_array($l) ? $l : explode('|', $l) as $a) { | |
$a = trim($a); | |
//! drop the ACE | |
unset($this->acl[$a]); | |
//! drop item specific ACEs as well | |
foreach ($this->acl as $k => $v) { | |
if (substr($k, 0, strlen($a) + 1) == $a.':') { | |
unset($this->acl[$k]); | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Initialize event. | |
* | |
* @param array configuration array | |
* | |
* @return boolean false if initialization failed | |
*/ | |
public function init($cfg) | |
{ | |
$L = 'pe_u'; | |
if (!empty($_SESSION[$L]) && is_object($_SESSION[$L])) { | |
Core::$user = $_SESSION[$L]; | |
foreach (['id', 'name', 'data'] as $k) { | |
$this->$k = Core::$user->$k; | |
} | |
} else { | |
Core::$user = $_SESSION[$L] = $this; | |
} | |
View::assign('user', Core::$user); | |
} | |
/** | |
* Route event. We handle login/logout actions here, before routing | |
* decision is made. | |
* | |
* @param current application | |
* @param current action | |
*/ | |
// @codeCoverageIgnoreStart | |
public function route($app, $action) | |
{ | |
//! operation modes | |
if (!empty(Core::$user->id)) { | |
//! edit for updating records and conf for widget configuration | |
foreach (['edit', 'conf'] as $v) { | |
if (isset($_REQUEST[$v]) && Core::$user->has($v)) { | |
$_SESSION['pe_'.substr($v, 0, 1)] = !empty($_REQUEST[$v]); | |
Http::redirect(); | |
} | |
} | |
} | |
//! handle hardwired admin login and logout before Users class get's a chance | |
if (Core::$core->app == 'login') { | |
//! if already logged in redirect to home page | |
if (Core::$user->id) { | |
Http::redirect('/'); | |
} | |
//! superuser's name | |
$A = 'admin'; | |
if (Core::isBtn() && !empty($_REQUEST['id'])) { | |
Core::req2arr("login"); | |
if(!Core::isError()) { | |
foreach($_SESSION as $k=>$v) if(substr($k,0,3)!="pe_") unset($_SESSION[$k]); | |
//don't accept password in GET parameter | |
if ($_REQUEST['id'] == $A && !empty(Core::$core->masterpasswd) && | |
password_verify($_POST['pass'], Core::$core->masterpasswd)) { | |
$_SESSION['pe_u']->id = -1; | |
$_SESSION['pe_u']->name = $A; | |
} else { | |
//! *** LOGIN Event *** | |
Core::event("login", [$_REQUEST['id'],$_POST['pass']]); | |
} | |
if(!empty($_SESSION['pe_u']->id)) { | |
Core::log('A', 'Login '.$_SESSION['pe_u']->name, 'users'); | |
Http::redirect(); | |
} else { | |
Core::error(L('Bad username or password'), 'id'); | |
} | |
} | |
} | |
} elseif (Core::$core->app == 'logout') { | |
$i = Core::$user->id; | |
if ($i) { | |
Core::log('A', 'Logout '.Core::$user->name, 'users'); | |
//! hook Users class' method for non-admin user logouts | |
if ($i != -1) { | |
//! *** LOGOUT Event *** | |
Core::event("logout"); | |
} | |
} | |
session_destroy(); | |
Http::redirect('/'); | |
} | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
/** | |
* HTTP helpers. | |
*/ | |
class Http | |
{ | |
private static $r; //!< url routes | |
/** | |
* Generate a permanent link (see also url()). | |
* | |
* @param string application | |
* @param string action | |
*/ | |
public static function url($m = '', $p = '') | |
{ | |
//! generate canonized permanent link | |
$c = Core::$core->base; | |
$A = Core::$core->app; | |
$f = basename(__FILE__); | |
if (empty($m) && !empty($A)) { | |
$m = $A; | |
} | |
if (empty($p) && !empty($A) && $A == $m) { | |
$p = Core::$core->action; | |
} | |
$a = ($m != '/' ? ($m.$p != 'indexaction' ? $m.'/' : '').(!empty($p) && $p != 'action' ? $p.'/' : '') : ''); | |
return 'http'.(Core::$core->sec ? 's' : '').'://'.$c.($c[strlen($c) - 1] != '/' ? '/' : ''). | |
($f != 'index.php' ? $f.($a?'/':'') : '').$a; | |
} | |
/** | |
* Redirect user. | |
* | |
* @param string url to redirect to | |
* @param boolean save current url before redirect so that it will be used next time | |
*/ | |
// @codeCoverageIgnoreStart | |
public static function redirect($u = '', $s = 0) | |
{ | |
//save current url | |
if ($s) { | |
self::_r(); | |
} | |
//get redirection url if exists | |
if (empty($u) && !empty($_SESSION['pe_r'])) { | |
$u = $_SESSION['pe_r']; | |
unset($_SESSION['pe_r']); | |
} | |
//redirect user | |
header('HTTP/1.1 302 Found'); | |
$f = basename(__FILE__); | |
header('Location:'.(!empty($u) ? (strpos($u, '://') ? $u : | |
'http'.(Core::$core->sec ? 's' : '').'://'. | |
Core::$core->base.($f != 'index.php' ? $f.'/' : '').($u != '/' ? $u : '')) : | |
self::url().Core::$core->item)); | |
exit(); | |
} | |
// @codeCoverageIgnoreEnd | |
/** | |
* Application allowed to call this in special cases, but normally won't need it. | |
* This function saves current request uri in session for later redirection. | |
*/ | |
public static function _r() | |
{ | |
//! save request uri, will be used later after successful login | |
//! called when redirect has true as second argument. | |
@$_SESSION['pe_r'] = 'http'.(Core::$core->sec ? 's' : '').'://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']; | |
} | |
/** | |
* Generate http header with mime info for content. | |
* | |
* @param string mime type of output | |
* @param boolean client side cache enabled | |
*/ | |
public static function mime($m, $c = true) | |
{ | |
//on cli this will most probably report an error | |
//as usually cli action handlers have already echoed output by now | |
if ($c && empty(Core::$core->nocache)) { | |
@header('Pragma:cache'); | |
@header('Cache-Control:cache,public,max-age='.Core::$core->cachettl); | |
@header('Connection:close'); | |
} else { | |
@header('Pragma:no-cache'); | |
@header('Cache-Control:no-cache,no-store,private,must-revalidate,max-age=0'); | |
} | |
@header("Content-Type:$m;charset=utf-8"); | |
} | |
/** | |
* Query routing table. | |
* @usage route() | |
* @return array of routing rules | |
* | |
* Register a new url route. This method can handle many different input formats | |
* @usage route(...) call it from your initialization code, extension/init.php | |
* @param regexp mask of url | |
* @param class in which the app resides | |
* @param method of the application action handler (if not given, default action routing applies) | |
* @param filters comma separated list or array (ACE has to be started with '@' or PHPPE\Filter\*::filter() will be used) | |
*/ | |
public static function route($u = '', $n = '', $a = '', $f = []) | |
{ | |
if (empty($u)) { | |
return self::$r; | |
} | |
$A = 'action'; | |
$F = 'filters'; | |
$U = 'url'; | |
$N = 'name'; | |
//! standard arguments | |
if (is_string($u) && !empty($n)) { | |
if (!is_array($f)) { | |
$f = str_getcsv($f, ','); | |
} | |
self::$r[self::h($u, $n, $a)] = [$u, $n, $a, $f]; | |
} | |
//! associative array | |
elseif (is_array($u) && !empty($u[$U]) && !empty($u[$N])) { | |
$f = !empty($u[$F]) ? $u[$F] : []; | |
$a = !empty($u[$A]) ? $u[$A] : ''; | |
self::$r[self::h($u[$U], $u[$N], $a)] = [$u[$U], $u[$N], $a, is_array($f) ? $f : explode(',', $f)]; | |
} | |
//! mass import from an array | |
elseif (is_array($u) && !empty(current($u)[0])) { | |
foreach ($u as $v) { | |
self::$r[self::h($v[0], $v[1], (!empty($v[2]) ? $v[2] : ''))] = $v; | |
} | |
} | |
//! from stdClass | |
elseif (is_object($u) && !empty($u->$U) && !empty($u->$N)) { | |
$f = !empty($u->$F) ? $u->$F : []; | |
$a = !empty($u->$A) ? $u->$A : ''; | |
self::$r[self::h($u->$U, $u->$N, $a)] = [$u->$U, $u->$N, $a, is_array($f) ? $f : explode(',', $f)]; | |
} else { | |
throw new \Exception('bad route: '.serialize($u)); | |
} | |
//! limit check | |
// @codeCoverageIgnoreStart | |
if (count(self::$r) >= 512) { | |
Core::log('C', 'too many routes'); | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
/** | |
* Get controller for an url. | |
* | |
* @param string application | |
* @param string action | |
* @param string url | |
* | |
* @return [application,action,arguments] | |
*/ | |
public static function urlMatch($app = '', $ac = '', $url = '') | |
{ | |
$X = []; | |
if (empty($app)) { | |
//! url routing | |
$w = 0; | |
if (!empty(self::$r)) { | |
//! check routes, best match policy | |
uasort(self::$r, function ($a, $b) { | |
return strcmp($b[0], $a[0]); | |
}); | |
//! check route patterns | |
foreach (self::$r as $v) { | |
//! if matches current url | |
if (preg_match('!^'.strtr($v[0], ['!' => '']).'!i', $url, $X)) { | |
//! check filter | |
if (!empty($v[3]) && !Core::cf($v[3])) { | |
$w = 1; | |
continue; | |
} | |
$w = 0; | |
//! chop off whole match (first index) from arguments | |
array_shift($X); | |
//! get class and method | |
$app = $v[1]; | |
$ac = $v[2]; | |
break; | |
} | |
} | |
} | |
//! if there was a match but failed due to filters, | |
//! set output to 403 Access Denied page | |
if ($w) { | |
$app = Core::$core->template = '403'; | |
} | |
} | |
//! load detected values if no application given | |
if (empty($app)) $app = Core::$core->app; | |
if (empty($ac)) $ac = Core::$core->action; | |
return [$app, $ac, $X]; | |
} | |
/** | |
* make a http request and return content. Unlike cURL, this will follow cookie changes during redirects. | |
* | |
* @param string url | |
* @param array post variables | |
* @param integer timeout in sec | |
* | |
* @return content | |
*/ | |
public static function get($u, $p = '', $T = 3, $l = 0) | |
{ | |
static $C; | |
//! check recursion maximum level | |
if ($l > 7) { | |
return; | |
} | |
//! parse url | |
if (preg_match("/^([^\:]+)\:\/\/([^\/\:]+)\:?([0-9]*)(.*)$/", $u, $m)) { | |
//! validation and default values | |
$s = 0; | |
if ($m[1] != 'http' && $m[1] != 'https') return; | |
if ($m[1] == 'https') { $s = 1; $m[2] = 'ssl://'.$m[2]; } | |
if ($m[3] == '') $m[3] = ($m[1] == 'http' ? 80 : 443); | |
if ($m[4] == '') $m[4] = '/'; | |
//! open socket | |
$f = fsockopen($m[2], $m[3], $n, $e, $T); | |
if (!$f) { | |
// @codeCoverageIgnoreStart | |
//log failure | |
Core::log('E', "$u #$n $e", 'http'); | |
//give it a fallback in case ssl transport not configured in php | |
return $s && strpos($e, '"ssl"') ? file_get_contents($u, false, is_array($p) ? stream_context_create([ | |
'http' => ['method' => 'POST', | |
'header' => 'Content-type: application/x-www-form-urlencoded', | |
'content' => http_build_query($p),],]) : null) : ''; | |
// @codeCoverageIgnoreEnd | |
} | |
//! construct POST | |
$P = is_array($p) ? http_build_query($p, '_') : ''; | |
//! send request | |
//! we are using HTTP/1.0 on purpose so that we don't have to mess with chunked response | |
$o = ($P ? 'POST' : 'GET').' '.$m[4]." HTTP/1.0\r\nHost: ".$m[2]."\r\nAccept-Language: ".Core::$client->lang.";q=0.8\r\n".($C ? 'Cookie: '.http_build_query($C, '', ';')."\r\n" : '').($P ? "Content-Type: application/x-www-form-urlencoded\r\nContent-Length: ".strlen($P)."\r\n" : '')."Connection: close;\r\n\r\n".$P; | |
fwrite($f, $o); | |
//! receive response | |
$d = $H = $n = ''; | |
$h = '-'; | |
$t = 0; | |
stream_set_timeout($f, $T); | |
while (!feof($f) && trim($h) != '') { | |
//! parse headers | |
$h = trim((fgets($f, 4096))); | |
if (!empty($h)) { | |
$H = strtolower($h); | |
if (substr($H, 0, 8) == 'location') { | |
$n = trim(substr($h, 9)); | |
} | |
if (substr($H, 0, 12) == 'content-type' && strpos($h, 'text/')) { | |
$t = 1; | |
} | |
//! follow cookie changes | |
if (substr($H, 0, 10) == 'set-cookie') { | |
$c = str_getcsv(str_getcsv(trim(substr($h, 11)),';')[0], '='); | |
//c[1] is undefined on nginx when clearing the cookie | |
@$C[$c[0]] = $c[1]; | |
} | |
} | |
} | |
//! handle redirections | |
if ($n && $n != $u) { | |
return self::get($n, $p, $T, $l + 1); | |
} | |
//! receive data if there was a header (not timed out) | |
if ($H) { | |
while (!feof($f)) { | |
$d .= fread($f, 65535); | |
} | |
Core::log('D', "$u ".strlen($d), 'http'); | |
} | |
// @codeCoverageIgnoreStart | |
else { | |
Core::log('E', "$u timed out $T", 'http'); | |
} | |
// @codeCoverageIgnoreEnd | |
fclose($f); | |
return $t ? strtr($d, ["\r" => '']) : $d; | |
} | |
} | |
/** | |
* Calculate hash for routes and others | |
*/ | |
private static function h($a, $b, $c = '') | |
{ | |
return sha1($a.'|'.$b.'|'.$c); | |
} | |
} | |
/** | |
* DataSource layer. It's called DS and not DB because class DB is | |
* the Sql Query Builder shipped with PHPPE Pack. | |
*/ | |
class DS extends Extension | |
{ | |
private $name = ''; | |
private static $db = []; //!< database layer | |
private static $s = 0; //!< data source selector | |
private static $b = 0; //!< time consumed by data source queries (bill for db) | |
/** | |
* Magic getter to implement read-only properties | |
*/ | |
function __get($n) { return $this->$n; } | |
/** | |
* Constructor. Initialize primary datasource if any. Called by core. | |
* | |
* @param string primary datasource uri | |
*/ | |
public function __construct($ds = null) | |
{ | |
//! initialize primary datasource if configured prior module initialization | |
if (!empty($ds)) { | |
//! replace string $this->db with an array of pdo objects | |
@self::db($ds); | |
//get primary datasource's name | |
if (!empty(self::$db[0])) { | |
$this->name = self::$db[0]->name; | |
//! get current timestamp from primary datasoure | |
//! this will override time() in $core->now with | |
//! a time in database server's timezone. This is | |
//! important if webserver and dbserver are on | |
//! separate hosts without time synchronization. | |
try { | |
$t = @strtotime(@self::field('CURRENT_TIMESTAMP')); | |
if ($t > 0) { | |
Core::$core->now = $t; | |
} | |
// @codeCoverageIgnoreStart | |
} catch (\Exception $e) { | |
} | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
} | |
/** | |
* Close database connections for all datasources. | |
*/ | |
public static function close() | |
{ | |
if (!empty(self::$db)) { | |
foreach (self::$db as $d) { | |
if (method_exists($d, 'close')) { | |
// @codeCoverageIgnoreStart | |
$d->close(); | |
// @codeCoverageIgnoreEnd | |
} | |
} | |
} | |
self::$db = []; | |
self::$s = 0; | |
} | |
/** | |
* Diag event handler. Look for sql updates. | |
*/ | |
public function diag() | |
{ | |
//! nothing to do without database | |
$d = @self::$db[0]; | |
if (empty($d)) { | |
return; | |
} | |
//! apply sql updates | |
$D = []; | |
foreach (['', '.'.$d->name] as $s) { | |
$D += array_fill_keys(@glob('vendor/phppe/*/sql/upd_*'.$s.'.sql',GLOB_NOSORT), 0); | |
} | |
if (!empty($D)) { | |
echo "DIAG-I: db update (".count($D).")\n"; | |
} | |
foreach ($D as $f => $v) { | |
//! get sql commands from file | |
$s = str_getcsv(file_get_contents($f), ';'); | |
@unlink($f); | |
//! execute one by one | |
foreach ($s as $q) { | |
self::exec($q); | |
} | |
} | |
} | |
/** | |
* return database instance for current selector | |
* @usage DS::db() | |
* | |
* initialize a database and make connection available as a data source | |
* @usage DS::db(pdodsn) | |
* | |
* @param string pdo dsn of new connection | |
* @param pdo optional: any PDO compatible class instance | |
* | |
* @return pdo/integer pdo instance for query or selector for this new data source | |
*/ | |
public static function db($u = null, $O = null) | |
{ | |
//! query PDO instance | |
if (empty($u)) { | |
return !empty(self::$db[self::$s]) ? self::$db[self::$s] : null; | |
} | |
//! initialize a database and make connection available as a data source | |
$S = microtime(1); | |
//! create instance | |
try { | |
//! get username and password if it's not part of dsn | |
if (!preg_match('/^(.*)@([^@:]+)?:?([^:]*)$/', $u, $d)) { | |
$d[1] = $u; | |
} | |
self::$s = count(self::$db); | |
self::$db[] = is_object($O) ? $O : new \PDO($d[1], | |
!empty($d[2]) ? $d[2] : '', | |
!empty($d[3]) ? $d[3] : '', | |
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_EMULATE_PREPARES => 0]); | |
if (!isset(self::$db[self::$s])) throw new \PDOException(); | |
//! housekeeping | |
$d = &self::$db[self::$s]; | |
$d->id = count(self::$db); | |
$d->name = is_object($O) ? get_class($O) : $d->getAttribute(\PDO::ATTR_DRIVER_NAME); | |
//! to maintain interoperability among different sql implementations, load replacements from | |
//! vendor/phppe/*/libs/ds_(driver).php | |
$d->s = @include @glob('vendor/phppe/*/libs/ds_'.$d->name.'.php')[0]; | |
if (!empty($d->s['_init'])) { | |
// @codeCoverageIgnoreStart | |
//! driver specific commands for connection | |
$c = str_getcsv($d->s['_init'], ';'); | |
foreach ($c as $n => $C) { | |
if (!empty(trim($C))) { | |
$d->exec(trim($C)); | |
} | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
} catch (\Exception $e) { | |
//! consider failure of first data source fatal | |
Core::log(self::$s ? 'E' : 'C', L('Unable to initialize').": $u, ".$e->getMessage(), 'db'); | |
throw $e; | |
} | |
self::$b += microtime(1) - $S; | |
//! return selector of newly created instance | |
return self::$s; | |
} | |
/** | |
* Set current data source to use with exec, fetch etc. if argument given. | |
* | |
* @param integer data source selector (returned by db()) | |
* | |
* @return integer returns current selector | |
*/ | |
public static function ds($s = -1) | |
{ | |
//! select a data source to use | |
if ($s >= 0 && $s < count(self::$db) && !empty(self::$db[$s])) { | |
self::$s = $s; | |
} | |
return self::$s; | |
} | |
/** | |
* Convert a string from user into a sql like phrase. | |
* | |
* @param string user profided string | |
* | |
* @return string sql safe, formatted string | |
*/ | |
public static function like($s) | |
{ | |
return | |
preg_replace('/[%_]+/', '%', '%'. | |
preg_replace("/[^a-z0-9\%]/i", '_', | |
preg_replace("/[\ \t]+/", '%', | |
strtr(trim($s), ['%' => '']))).'%'); | |
} | |
/** | |
* Common code for executing a query on current data source. All the other methods are wrappers only. | |
* | |
* @param string query string | |
* | |
* @return array/integer number of affected rows or data array | |
*/ | |
public static function exec($q, $a = []) | |
{ | |
//! log query in developer mode | |
Core::log('D', $q.' '.json_encode($a), 'db'); | |
//! check for valid datasource | |
if (!is_array($a)) { | |
$a = [$a]; | |
} | |
if (empty(self::$db[self::$s])) { | |
throw new \Exception(L('Invalid ds').' #'.self::$s); | |
} | |
//! skip comment lines and empty queries by | |
//! reporting 1 affected row to avoid errors on caller side | |
$q = trim($q); | |
if (empty($q) || $q[0] == '-' || $q[0] == '/') { | |
return 1; | |
} | |
//! do the thing | |
$t = microtime(1); | |
$r = null; | |
$h = self::$db[self::$s]; | |
try { | |
$i = strtolower(substr(trim($q), 0, 6)) == 'select' || strtolower(substr(trim($q), 0, 4)) == 'show'; | |
//! to maintain interoperability among different sql implementations, a replace | |
//! array is used with regexp pattern keys and replacement strings as value | |
//! see db() it's initialized there. The array is specified here: | |
//! vendor/phppe/*/libs/ds_(driver).php | |
if (is_array($h->s)) { | |
foreach ($h->s as $k => $v) { | |
if ($k[0] != '_') { | |
$q = preg_replace('/'.$k.'/ims', $v, $q); | |
} | |
} | |
if(!$i && strtolower(substr(trim($q), 0, 6)) == 'select') $i=1; | |
} | |
//! prepare and execute the statement with arguments | |
$s = $h->prepare($q); | |
@$s->execute($a); | |
//! get result, either an array or a number | |
$r = $i ? $s->fetchAll(\PDO::FETCH_ASSOC) : $s->rowCount(); | |
} catch (\Exception $e) { | |
//! try to load scheme for missing table | |
$E = $e->getMessage(); | |
$c = strtr($E, 'le or v', ''); | |
// @codeCoverageIgnoreStart | |
if ((/* other */(!empty($h->s['_tablename']) && preg_match($h->s['_tablename'], $E, $d)) || | |
/*Sqlite/MySQL/MariaDB*/ preg_match("/able:?\ [\'\"]?([a-z0-9_\.]+)/mi", $c, $d) || | |
/*Postgre*/ preg_match("/([a-z0-9_\.]+)[\'\"] does\ ?n/mi", $E, $d) || | |
/*MSSql*/ preg_match("/name:?\ [\'\"]?([a-z0-9_\.]+)/mi", $E, $d) | |
) && !empty($d[1])) { | |
// @codeCoverageIgnoreEnd | |
$c = ''; | |
$m = '.'.trim($h->name); | |
$d = explode('.', $d[1]); | |
$d = trim(!empty($d[1]) ? $d[1] : $d[0]); | |
list($D) = explode('_', $d); | |
//! look for engine specific scheme | |
$f = @glob('vendor/phppe/*/sql/'.$d.$m.'.sql',GLOB_NOSORT)[0]; | |
//! common scheme | |
if (empty($f)) { | |
$f = @glob('vendor/phppe/*/sql/'.$d.'.sql',GLOB_NOSORT)[0]; | |
} | |
if (!empty($f) && file_exists($f)) { | |
$c = file_get_contents($f); | |
} | |
//! if scheme not found. Only log pages and views missing in debug runlevel | |
if (empty($c)) { | |
if(($d!="pages"&&$d!="views") || Core::$core->runlevel > 1) | |
Core::log('E', $E, 'db'); | |
throw $e; | |
} | |
if (is_array($h->s)) { | |
foreach ($h->s as $k => $v) { | |
if ($k[0] != '_') { | |
$c = preg_replace('/'.$k.'/ims', $v, $c); | |
} | |
} | |
} | |
//! execute schema creation commands | |
$c = str_getcsv($c, ';'); | |
foreach ($c as $n => $C) { | |
try { | |
if (!empty(trim($C))) { | |
$h->exec(trim($C)); | |
} | |
} catch (\Exception $e) { | |
Core::log('E', "creating $d at line:".($n + 1).' '.$e->getMessage(), 'db'); | |
throw $e; | |
} | |
} | |
Core::log('A', "$d created.", 'db'); | |
//! repeat original command | |
$s = $h->prepare($q); | |
$s->execute($a); | |
$r = $i ? $s->fetchAll(\PDO::FETCH_ASSOC) : $s->rowCount(); | |
} else { | |
Core::log('E', $q.' '.json_encode($a).' '.$E, 'db'); | |
$r = null; | |
throw $e; | |
} | |
} | |
//! housekeeping | |
self::$b += microtime(1) - $t; | |
return $r; | |
} | |
/** | |
* Query records from current data source. | |
* | |
* @param string fields | |
* @param string table | |
* @param string where clause | |
* @param string group by | |
* @param string order by | |
* @param integer offset | |
* @param integer limit | |
* @param array arguments | |
* | |
* @return array/integer | |
*/ | |
public static function query($f, $t, $w = '', $g = '', $o = '', $s = 0, $l = 0, $a = []) | |
{ | |
//! execute a query that returns records of associative arrays | |
$q = 'SELECT '.(is_array($f)?implode(",",$f):$f).($t ? ' FROM '.$t : '').($w ? ' WHERE '.$w : '').($g ? ' GROUP BY '.$g : '').($o ? ' ORDER BY '.$o : '').($l ? (' LIMIT '.($s ? $s.',' : '').$l) : '').';'; | |
return self::exec($q, $a); | |
} | |
/** | |
* Query one record from current data source. | |
* | |
* @param string fields | |
* @param string table | |
* @param string where clause | |
* @param string group by | |
* @param string order by | |
* @param array arguments | |
* | |
* @return array record | |
*/ | |
public static function fetch($f, $t = '', $w = '', $g = '', $o = '', $a = []) | |
{ | |
//! return the first record | |
$r = self::query($f, $t, $w, $g, $o, 0, 1, $a); | |
return (object)(empty($r[0]) ? [] : $r[0]); | |
} | |
/** | |
* Query one field from current data source. | |
* | |
* @param string field | |
* @param string table | |
* @param string where clause | |
* @param string group by | |
* @param string order by | |
* @param array arguments | |
* | |
* @return string value | |
*/ | |
public static function field($f, $t = '', $w = '', $g = '', $o = '', $a = []) | |
{ | |
//! return the first field | |
return @reset(self::fetch($f, $t, $w, $g, $o, $a)); | |
} | |
/** | |
* Query a recursive tree from current data source. | |
* | |
* @param string query string, use '?' placeholder to mark place of parent id | |
* @param integer root id of the tree, 0 for all | |
* | |
* @return array of data | |
*/ | |
public static function tree($q, $p = 0) | |
{ | |
//! return a tree array (childs in _) | |
$r = self::exec($q, [$p]); | |
if (empty($r)) { | |
return[]; | |
} | |
foreach ($r as $k => $v) { | |
$i = isset($v['id']) ? $v['id'] : -1; | |
if (!empty($i) && $i != $p) { | |
$c = self::tree($q, $i); | |
if (!empty($c)) { | |
$r[$k]['_'] = $c; | |
} | |
} | |
} | |
return $r; | |
} | |
/** | |
* Return time consumed by database calls. | |
* | |
* @return integer secs | |
*/ | |
public static function bill() | |
{ | |
return self::$b; | |
} | |
} | |
/** | |
* Cache wrapper. Allow multiple options | |
* and fallbacks to php memcache. | |
*/ | |
class Cache extends Extension | |
{ | |
private $name; //!< implementation | |
private static $uri; //!< cache uri | |
public static $mc; //!< memcache instance | |
/** | |
* Magic getter to implement read-only properties | |
*/ | |
function __get($n) { return $this->$n; } | |
/** | |
* Constructor. Called by core. | |
* | |
* @usage configure it in vendor/phppe/Core/config.php | |
* | |
* @param string cache uri | |
*/ | |
public function __construct($cfg = null) | |
{ | |
if (!empty($cfg)) { | |
self::$uri = $cfg; | |
$m = explode(':', $cfg); | |
$d = '\\PHPPE\\Cache\\'.$m[0]; | |
if (ClassMap::has($d)) { | |
self::$mc = new $d($cfg); | |
} | |
//! if none, fallback to memcache | |
if (empty(self::$mc)) { | |
// @codeCoverageIgnoreStart | |
$d = '\\Memcache'; | |
if (!class_exists($d)) { | |
\PHPPE\Core::log('C', L("no php-memcache"), 'cache'); | |
} | |
//! unix file: "unix:/tmp/fifo", "host" or "host:port" otherwise | |
if ($m[0] == 'unix') { | |
$p = 0; | |
$h = $m[1]; | |
} else { | |
$p = !empty($m[1]) ? $m[1] + 0 : 11211; | |
$h = $m[0]; | |
} | |
// @codeCoverageIgnoreEnd | |
self::$mc = new $d(); | |
//Core::$mc->addServer( $h, $p ); | |
//$s = @Core::$mc->getExtendedStats( ); | |
if (/*empty( $s[$h . ( $p > 0 ? ":" . $p : "" )] ) || */ !@self::$mc->pconnect($h, $p, 1)) { | |
// @codeCoverageIgnoreStart | |
usleep(100); | |
if (!@self::$mc->pconnect($h, $p, 1)) { | |
self::$mc = null; | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
} | |
//! let rest of the world know about us | |
if (is_object(self::$mc)) { | |
$this->name = $d; | |
} else { | |
self::$mc = null; | |
} | |
} | |
//! built-in blobs - referenced as cached objects | |
//! this should go to init(), but we serve them as soon | |
//! as possible to speed up page load | |
// } | |
// @codeCoverageIgnoreStart | |
// function init($cfg) | |
// { | |
if (!empty($_GET['cache'])) { | |
$d = trim($_GET['cache']); | |
switch ($d) { | |
//! inline PHPPE logo | |
case 'logo' : | |
Http::mime('image/png'); | |
$c = 'vendor/phppe/Core/images/.phppe'; | |
die(file_exists($c) ? file_get_contents($c) : | |
base64_decode('R0lGODlhKgAYAMIHAAACAAcAABYAAygBDD4BEFwAGGoBGwWYISH5BAEKAAcALAAAAAAqABgAAAOxeLrcCsDJSSkIoertYOSgBmXh5p3MiT4qJGIw9h3BFZP0LVceU0c91sy1uMwkwQfmYEzhiCwc8sh0QQ+FrMFQIAgY2cIWuUx9LoutWsxNs9udaxDKDb+7Wzth+huRRmlcJANrW148NjJDdF2Db2t7EzUUkwpqAx8EaoWRUyCXgVx5L1QUeQQDBGwFhIYDAxNNHJubBQqPBiWmeWqdWG+6EmrBxJZwxbqjyMnHy87P0BMJADs=')); | |
//! Stylesheet for PHPPE Panel | |
case 'css' : | |
Http::mime('text/css'); | |
$p = 'position:fixed;top:'; | |
$s = 'text-shadow:2px 2px 2px #FFF;'; | |
$c = 'rgba(136,146,191'; | |
die('#pe_p{'.$p."0;z-index:1998;left:0;width:100%;padding:0 2px 0 32px;background-color:$c,0.9);background:linear-gradient($c,0.4),$c,0.6),$c,0.8),$c,0.9),$c,1) 90%,rgba(0,0,0,1));height:31px !important;font-family:helvetica;font-size:14px !important;line-height:20px !important;}#pe_p SPAN{margin:0 5px 0 0;cursor:pointer;}#pe_p UL{list-style-type:none;margin:3px;padding:0;}#pe_p IMG{border:0;vertical-align:middle;padding-right:4px;}#pe_p A{text-decoration:none;color:#000;".$s.'}#pe_p .menu {position:fixed;top:8px;left:90px;}#pe_p .stat SPAN{display:inline-block;'.$s.'}#pe_p LI{cursor:pointer;}#pe_p LI:hover{background:#F0F0F0;}#pe_p .stat{'.$p.'6px;right:48px;}#pe_p .sub{'.$p.'28px;display:inline;background:#FFF;border:solid 1px #808080;box-shadow:2px 2px 6px #000;z-index:1999;}#pe_p .menu_i{padding:5px 6px 5px 6px;'.$s.'}#pe_p .menu_a{padding:4px 5px 5px 5px;border-top:solid #000 1px;border-left:solid #000 1px;border-right:solid #000 1px;background:#FFF;}@media print{#pe_p{display:none;}}'); | |
//! serve real cache requests | |
default : | |
$c = self::get("c_$d"); | |
if (is_array($c) && !empty($c['d'])) { | |
Http::mime((!empty($c['m']) ? $c['m'] : 'text/plain')); | |
die($c['d']); | |
} | |
die('CACHE-E: '.$d); | |
} | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
/** | |
* Set a value in cache. | |
* | |
* @param string key | |
* @param mixed value | |
* @param integer ttl, optional | |
* @param boolean force use of cache, optional | |
*/ | |
public static function set($k, $v, $ttl = 0, $force=false) | |
{ | |
if (!empty(self::$mc) && (empty(Core::$core->nocache)||$force)) { | |
return @self::$mc->set($k, $v, MEMCACHE_COMPRESSED, $ttl > 0 ? $ttl : Core::$core->cachettl); | |
} | |
return false; | |
} | |
/** | |
* Get a value from cache. | |
* | |
* @param string key | |
* @param boolean force use of cache | |
*/ | |
public static function get($k, $force=false) | |
{ | |
if (!empty(self::$mc) && (empty(Core::$core->nocache)||$force)) { | |
return self::$mc->get($k); | |
} | |
return; | |
} | |
/** | |
* Clear cache. | |
* | |
*/ | |
public static function invalidate() | |
{ | |
if (!empty(self::$mc) && method_exists(self::$mc,"invalidate")) { | |
return self::$mc->invalidate(); | |
} else { | |
// fallback | |
$c = !empty(Core::$core->cache) ? Core::$core->cache : self::$uri; | |
if (preg_match("/^([^:]+):?([0-9]*)$/",$c,$m)) { | |
$f = fsockopen($m[1], $m[2]>0?$m[2]:11211, $n, $e, 1); | |
fwrite($f,"flush_all\n"); | |
fclose($f); | |
} | |
} | |
return; | |
} | |
/** | |
* Initialize event. | |
* | |
* @param array configuration array | |
* | |
* @return boolean false if initialization failed | |
*/ | |
public function init($cfg) | |
{ | |
//! remove Cache from extensions if there's no instance | |
return !empty(self::$mc); | |
} | |
} | |
/** | |
* Assets proxy. It will use memcache if configured | |
* Also takes care of dynamic assets and saves their output. | |
*/ | |
class Assets extends Extension | |
{ | |
/** | |
* Route event handler. Will look for images, css, js application. | |
* | |
* @param string current application | |
* @param string current action | |
*/ | |
// @codeCoverageIgnoreStart | |
public function route($app, $action) | |
{ | |
//! proxy dynamic assets (vendor directory is not accessable by the webserver, only public dir) | |
if (in_array($app, ['css', 'js', 'images', 'fonts'])) { | |
//! helper function to specify mime header and minify assets | |
function b($a, $b) | |
{ | |
Http::mime($a == 'css' ? 'text/css' : ($a == 'js' ? 'text/javascript' : ($a == 'images'?'image/png':'application/octet-stream'))); | |
die($b); | |
} | |
//! let's try to get it from cache | |
$N = 'a_'.sha1(Core::$core->base.Core::$core->url.'/'.Core::$user->id.'/'.Core::$client->lang); | |
$d = Cache::get($N); | |
if (!empty($d)) { | |
b($app, $d); | |
} else { | |
//! cache miss, we'll have to generate the asset | |
//! remove language code from core.js url. This "alias" allows per language cache | |
foreach ([Core::$core->url, | |
preg_replace("/^js\/core\.[^\.]+\.js/", 'js/core.js', Core::$core->url).'.php',] as $p) { | |
$A = 'vendor/phppe/*/'.strtr($p, ['*' => '', '..' => '']); | |
$c = @glob($A, GLOB_NOSORT)[0]; | |
if (empty($c)) { | |
$A = 'public/'.strtr($p, ['*' => '', '..' => '']); | |
$c = @glob($A, GLOB_NOSORT)[0]; | |
} | |
if ($c) { | |
if (substr($c, -4) != '.php') { | |
//! use file_get_contents and 10 times longer cache ttl for static files | |
$d = file_get_contents($c); | |
Core::$core->cachettl *= 10; | |
} else { | |
//! use include_once for php with normal cache ttl | |
ob_start(); | |
include_once $c; | |
$d = ob_get_clean(); | |
} | |
} | |
if ($d) { | |
$d = self::minify($d, $app); | |
//! save it to the cache for later | |
Cache::set($N, $d); | |
//! output result | |
b($app, $d); | |
} | |
} | |
} | |
//! no asset found by that url | |
header('HTTP/1.1 404 Not Found'); | |
die; | |
} | |
//! not a real asset, but no better place | |
if($app=="passwd"&&Core::$client->ip=="CLI") { | |
echo(chr(27)."[96m".L("Password")."? ".chr(27)."[0m"); | |
system('stty -echo'); | |
$p = rtrim(fgets(STDIN)); | |
system('stty echo'); | |
die("\n".password_hash($p, PASSWORD_BCRYPT, ['cost'=>12])."\n"); | |
} | |
} | |
// @codeCoverageIgnoreEnd | |
/** | |
* Asset minifier. | |
* | |
* @param string data | |
* @param string type, 'css' or 'js' | |
* | |
* @return string minified data | |
*/ | |
public static function minify($d, $t = 'js') | |
{ | |
//! check input, return output just as is if type unknown | |
if (!empty(Core::$core->nominify) || ($t != 'css' && $t != 'js' && $t != 'php')) { | |
return $d; | |
} | |
$d = trim($d); | |
//! allow use of third party vendor code | |
if ($t == 'css' && class_exists('CSSMin')) return \CSSMin::minify($d); | |
if ($t == 'js' && class_exists('JSMin')) return \JSMin::minify($d); | |
//! do the stuff ourself (fastest, safest, simpliest, and no dependency required at all... and only 70 SLoC) | |
$n = ''; | |
$i = 0; | |
$l = strlen($d); | |
while ($i < $l) { | |
if ($t == 'php' && ($d[$i] == '?' && $d[$i + 1] == '>')) { | |
$j = $i; | |
$i += 2; | |
while ($i < $l && ($d[$i-1] != '<' || $d[$i] != '?')) { | |
$i++; | |
} | |
$i++; | |
$n .= substr($d, $j, $i - $j); | |
continue; | |
} | |
$c = @substr($n, -1); | |
//! string literals | |
if (($d[$i] == "'" || $d[$i] == '"') && $c != '\\') { | |
$s = $d[$i]; | |
$j = $i; | |
++$i; | |
while ($i < $l && $d[$i] != $s) { | |
if ($d[$i] == '\\') $i++; | |
++$i; | |
} | |
++$i; | |
$n .= substr($d, $j, $i - $j); | |
continue; | |
} | |
//! remove comments | |
if ($t != 'css' && ($d[$i] == '/' && $d[$i + 1] == '/')) { | |
$i += 2; | |
while ($i < $l && $d[$i] != "\n") { | |
$i++; | |
} | |
continue; | |
} | |
if ($d[$i] == '/' && $d[$i + 1] == '*') { | |
$i += 2; | |
while ($i + 1 < $l && ($d[$i] != '*' || $d[$i + 1] != '/')) { | |
$i++; | |
} | |
$i += 2; | |
continue; | |
} | |
//! remove tabs and line endings | |
if ($d[$i] == "\t" || $d[$i] == "\r" || $d[$i] == "\n") { | |
//! add a space to separate words if necessary | |
if ( | |
(($c >= 'a' && $c <= 'z') || ($c >= 'A' && $c <= 'Z') || ($c >= '0' && $c <= '9')) && | |
($d[$i + 1] == '\\' || $d[$i + 1] == '/' || $d[$i + 1] == '_' || $d[$i + 1] == '*' || ($d[$i + 1] >= 'a' && $d[$i + 1] <= 'z') || ($d[$i + 1] >= 'A' && $d[$i + 1] <= 'Z') || ($d[$i + 1] >= '0' && $d[$i + 1] <= '9') || $d[$i + 1] == '#' || ($t == 'css' && $d[$i+1]=='.')) | |
) { | |
$n .= ' '; | |
} | |
$i++; | |
continue; | |
} | |
//! remove extra spaces | |
if ($d[$i] == ' ' && $c!='\\' && | |
(!(($c >= 'a' && $c <= 'z') || ($c >= 'A' && $c <= 'Z') || ($c >= '0' && $c <= '9') || $c=='%' || $c=='-') || | |
!($d[$i + 1] == '\\' || $d[$i + 1] == '/' || $d[$i + 1] == '_' || ($d[$i + 1] >= 'a' && $d[$i + 1] <= 'z') || | |
($d[$i + 1] >= 'A' && $d[$i + 1] <= 'Z') || ($d[$i + 1] >= '0' && $d[$i + 1] <= '9') || $d[$i + 1] == '#' || $d[$i + 1] == '*' || | |
($t == 'css' && ($d[$i+1]=='.'||$d[$i+1]=='-'||$c=='-')) || | |
($t == 'js' && $d[$i + 1] == '$')))) { | |
++$i; | |
continue; | |
} | |
if($t=="css" && $d[$i]=='(' && (substr($n,-3)=="and"||substr($n,-2)=="or")) $n.=' '; | |
//! copy character to new string | |
$n .= $d[$i++]; | |
} | |
return $n; | |
} | |
} | |
/** | |
* Content Server. This is the default fallback application if | |
* url route failed. | |
*/ | |
class Content extends Extension | |
{ | |
private static $dds = []; //!< dynamic data sets | |
/** | |
* Constructor. Common code for all Content actions. | |
* | |
* @param string url | |
*/ | |
public function __construct($u="") | |
{ | |
//! check cache | |
$C = 'd_'.sha1(url().'/'.Core::$user->id.'/'.Core::$client->lang); | |
$data = Cache::get($C); | |
try { | |
//! cache miss, look it up in database - only primary datasource | |
if (empty($data['id'])) { | |
DS::ds(0); | |
if(empty($u)) $u=Core::$core->url; | |
$data = (array)DS::fetch("a.*,b.ctrl", "pages a LEFT JOIN views b ON a.template=b.id", | |
"(a.id=? OR ? LIKE a.id||'/%') AND ". | |
"(a.lang='' OR a.lang=?) AND ".(ClassMap::has("PHPPE\\CMS") && | |
@get_class(View::getval('app'))=="PHPPE\\Content" && | |
Core::$user->has("siteadm|webadm")?"":"a.publishid!=0 AND "). | |
"a.pubd<=CURRENT_TIMESTAMP AND (a.expd='' OR a.expd=0 OR a.expd>CURRENT_TIMESTAMP)", | |
"", "a.id DESC,a.created DESC", | |
[$u, $u, Core::$client->lang] | |
); | |
if (!empty($data['id'])) { | |
Cache::set($C, $data); | |
} else { | |
return; | |
} | |
} | |
//! check filters | |
if (!empty($data['filter']) && !Core::cf($data['filter'])) { | |
//! not allowed, fallback to 403 | |
Core::$core->template = '403'; | |
return; | |
} | |
//! set view for page | |
Core::$core->template = $data['template']; | |
//! load site title | |
Core::$core->title = $data['name']; | |
//! load application property overrides | |
$o = json_decode($data['data'], true); | |
if (is_array($o)) { | |
foreach ($o as $k => $v) { | |
if(substr($k,0,4)=="app.") $k=substr($k,4); | |
$this->$k = $v; | |
} | |
} | |
foreach (['id', 'name', 'lang', 'modifyd', 'ctrl', 'publishid'] as $k) { | |
$this->$k = $data[$k]; | |
} | |
//! get page specific DDS | |
$E = json_decode($data['dds'], true); | |
if (is_array($E)) { | |
self::$dds += $E; | |
} | |
// @codeCoverageIgnoreStart | |
} catch (\Exception $e) { | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
/** | |
* Default action. | |
* | |
* @param not used. | |
*/ | |
public function action($item = '') | |
{ | |
//! as this could be considered as a security risk, this feature can be turned off globally | |
if (!empty(Core::$core->noctrl) || empty($this->ctrl)) { | |
return; | |
} | |
ob_start(); | |
//FIXME: sanitize php code | |
eval("namespace PHPPE;\n".$this->ctrl); | |
return ob_get_clean(); | |
} | |
/** | |
* Get dynamic data sets into application properties. | |
* | |
* @param object application instance | |
*/ | |
public static function getDDS(&$app) | |
{ | |
try { | |
//! special page holds global page parameters and dds' | |
$F = (array)DS::fetch('data,dds', 'pages', "id='frame' AND ".(ClassMap::has("PHPPE\\CMS") && | |
get_class(View::getval('app'))=="PHPPE\\Content" && | |
// bug in php-code-coverage, marks unchecked in middle of an AND expression... | |
// @codeCoverageIgnoreStart | |
Core::$user->has("siteadm|webadm")?"":"publishid!=0 AND "). | |
"(lang='' OR lang=?)", '', 'id DESC,created DESC',[Core::$client->lang]); | |
$E = $F?json_decode($F['data'], true):null; | |
View::assign('frame', $E); | |
//! load global dds | |
$D = $F?json_decode($F['dds'], true):null; | |
if (is_array($D)) { | |
self::$dds += $D; | |
} | |
} catch (\Exception $e) { | |
} | |
// @codeCoverageIgnoreEnd | |
$o = []; | |
foreach (self::$dds as $k => $c) { | |
//! don't allow to set these, as they cannot be arrays | |
if (!in_array($k, ['dds', 'id', 'title', 'mimetype'])) { | |
try { | |
$o[$k] = @DS::query($c[0], @$c[1], strtr(@$c[2], ['@ID' => $k,'@SHA' => sha1($k), '@URL' => Core::$core->url]), @$c[3], @$c[4], @$c[5], View::getval(@$c[6])); | |
foreach ($o[$k] as $i => $v) { | |
$d = @json_decode($v['data'], true); | |
if (is_array($d)) $o[$k][$i] += $d; | |
unset($o[$k][$i]['data']); | |
} | |
} catch (\Exception $e) { | |
Core::log('W', $k.' '.$e->getMessage().' '.implode(' ', $c), 'dds'); | |
} | |
} | |
} | |
//! set application properties | |
if (!empty($o)) { | |
foreach ($o as $k => $v) { | |
$app->$k = $v; | |
} | |
} | |
} | |
} | |
/** | |
* View layer. | |
*/ | |
class View extends Extension | |
{ | |
private static $hdr = [ | |
'meta' => [], | |
'link' => [], | |
'css' => [], | |
'js' => [], | |
'jslib' => [], | |
]; //!< header items and js libraries | |
private static $menu; //!< system menu, populated by initialized modules | |
private static $n; //!< templater nested level | |
private static $c; //!< templater control structures context | |
private static $o = []; //!< templater objects | |
private static $tc; //!< try button counter | |
private static $p; //!< templater default path for views | |
private static $addons = []; //!< list of initialized widgets | |
public static $e = ''; //!< last expression to evaluate | |
public static $C; //!< php expression cache | |
/** | |
* Initialize event handler. Register basic object in templater | |
* also copy meta and link tags from configuration | |
*/ | |
public static function init() | |
{ | |
//! register core, user and client to templater | |
self::$o['core'] = Core::$core; | |
//! register default meta keywords | |
if (!empty(Core::$core->meta) && is_array(Core::$core->meta)) { | |
self::$hdr['meta'] = Core::$core->meta; | |
} | |
self::$hdr['meta']['viewport'] = 'width=device-width,initial-scale=1.0'; | |
if (!empty(Core::$core->link) && is_array(Core::$core->link)) { | |
self::$hdr['link'] = Core::$core->link; | |
} | |
//! add core.js with language code in name. This allows separate client side caches | |
//! also give it a high priortity; jQuery has 00, so core.js should have 01 | |
$js = 'vendor/phppe/Core/js/core.js.php'; | |
if (file_exists($js)) { | |
self::$hdr['jslib']['core.'.Core::$client->lang.'.js'] = "01$js"; | |
} | |
//! try button counter | |
self::$tc = 0; | |
} | |
/** | |
* Set default path for templater. Used by Core::run() after appliction class determined | |
* | |
* @param string path | |
*/ | |
public static function setPath(&$p) | |
{ | |
self::$p = &$p; | |
} | |
/** | |
* Register an object in templater. | |
* | |
* @param string name | |
* @param mixed instance reference | |
*/ | |
public static function assign($n, &$o) | |
{ | |
self::$o[$n] = &$o; | |
} | |
/** | |
* Register a new stylesheet. | |
* | |
* @param string name of the stylesheet | |
*/ | |
public static function css($c = '') | |
{ | |
if (empty($c)) { | |
return self::$hdr['css']; | |
} | |
//! add cdn stylesheet | |
if (substr($c, 0, 4) == 'http') { | |
self::$hdr['link'][$c] = 'stylesheet'; | |
} else { | |
//! add a new stylesheet to output | |
$a=@glob("vendor/phppe/*/css/".@explode('?', $c)[0]."*")[0]; | |
if (!isset(self::$hdr['css'][$c]) && !empty($a)) { | |
self::$hdr['css'][$c] = realpath($a); | |
} | |
} | |
} | |
/** | |
* Register a new javascript library. | |
* | |
* @param string name of the js library | |
* @param string if it needs to be initialized, the code to do that | |
* @param integer priority (0=jQuery, 1-9=frameworks, 10-=libraries) | |
*/ | |
public static function jslib($l = '', $i = '', $p = 10) | |
{ | |
if (empty($l)) { | |
return self::$hdr['jslib']; | |
} | |
if ($p < 0 || $p > 99) $p = 99; | |
//! add cdn javascript | |
if (substr($l, 0, 4) == 'http') { | |
self::$hdr['jslib'][$l] = sprintf('%02d', $p).$l; | |
} else { | |
//! add a new javascript library to output | |
$a=@glob("vendor/phppe/*/js/".@explode('?', $l)[0]."*")[0]; | |
if (!isset(self::$hdr['jslib'][$l]) && !empty($a)) { | |
self::$hdr['jslib'][$l] = sprintf('%02d', $p).realpath($a); | |
} | |
} | |
//! also register init hook and call it on domcomplete event | |
$i = trim($i); | |
if (!empty($i) && (empty(self::$hdr['js']['init()']) || strpos(self::$hdr['js']['init()'], $i) === false)) { | |
self::js('init()', $i.($i[strlen($i) - 1] != ';' ? ';' : ''), true); | |
} | |
} | |
/** | |
* Register a new javascript function. | |
* | |
* @param string name of the js function with arguments | |
* @param string code | |
* @param boolean if code should be appended to existing code, true. Replace otherwise | |
*/ | |
public static function js($f = '', $c = '', $a = 0) | |
{ | |
if ($c) { | |
//! add a javascript function to output | |
$C = Assets::minify($c, 'js'); | |
$C .= ($C[strlen($C) - 1] != ';' ? ';' : ''); | |
if ($a) { | |
if (strpos(@self::$hdr['js'][$f], $C) === false) { | |
@self::$hdr['js'][$f] .= $C; | |
} | |
} else { | |
self::$hdr['js'][$f] = $C; | |
} | |
} | |
} | |
/** | |
* Register a new menu item or submenu in PHPPE panel. | |
* | |
* @param string title of the link | |
* @param string/array url or array of title=>url | |
*/ | |
public static function menu($t = '', $l = '') | |
{ | |
if (empty($t)) { | |
return self::$menu; | |
} | |
//! add a new menuitem or submenu | |
if (is_string($l) || is_array($l)) { | |
self::$menu[$t] = $l; | |
} | |
} | |
/** | |
* Load view from cache. Called by Core::run() | |
* | |
* @param string cache key | |
* | |
* @return string cached content or null | |
*/ | |
public static function fromCache($N) | |
{ | |
$d = Cache::get($N); | |
if (is_array($d)) { | |
//! cache hit, we are happy! | |
foreach (['m' => 'meta', 'c' => 'css', 'j' => 'js', 'J' => 'jslib'] as $k => $v) { | |
if (is_array($d[$k])) { | |
self::$hdr[$v] = array_merge(self::$hdr[$v], $d[$k]); | |
} | |
} | |
return $d['d']; | |
} | |
return ''; | |
} | |
/** | |
* Generate the main part of the view (that is, without html header and footer). | |
* Called by Core::run() once. | |
* | |
* @param string template to use | |
* @param string cache key | |
* | |
* @return string generated content | |
*/ | |
public static function generate($template, $N = '') | |
{ | |
//! we should check cache here, but it's already handled | |
//! by Core::run() because this code never reached when cached | |
//! clear validators | |
$_SESSION['pe_v'] = []; | |
//! if controller cleared template name, return empty string | |
$T = ""; | |
if (!empty($template)) { | |
$T = self::template($template); | |
//! if action specific template not found, fallback to application's | |
if (empty($T)) $T = self::template(Core::$core->app.'_'.Core::$core->action); | |
if (empty($T)) $T = self::template(Core::$core->app); | |
if (empty($T)) $T = self::template('404'); | |
//! fallback index page if even 404 template missing | |
if (empty($T) && Core::$core->app == 'index') { | |
// @codeCoverageIgnoreStart | |
$T = '<h1>PHPPE works!</h1>Next step: <samp>php public/'.basename(__FILE__).' --diag</samp>'; | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
//! wrap generated output in a frame | |
if (empty(Core::$core->noframe)) { | |
$d = self::template('frame'); | |
//! failsafe frame | |
if (!$d) $T = "<div id='content'>".$T."</div>"; | |
//! replace application marker in frame with output | |
elseif (preg_match('/<!app>/ims', $d, $m, PREG_OFFSET_CAPTURE)) { | |
$T = substr($d, 0, $m[0][1]).$T.substr($d, $m[0][1] + 6); | |
} | |
} | |
//! sort jslibs in priority order before stored in cache | |
asort(self::$hdr['jslib'], SORT_FLAG_CASE | SORT_STRING); | |
//! save to cache | |
if (!empty($T) && !empty($N)) { | |
Cache::set($N, [ | |
'm' => self::$hdr['meta'], | |
'l' => self::$hdr['link'], | |
'c' => self::$hdr['css'], | |
'j' => self::$hdr['js'], | |
'J' => self::$hdr['jslib'], | |
'd' => $T,]); | |
} | |
return $T; | |
} | |
/** | |
* Load, parse and evaluate a template. Called several times | |
* | |
* @param string name of the template | |
* @param array view variables (see assign) | |
* | |
* @return string parsed output | |
*/ | |
public static function template($n,$v=null) | |
{ | |
//! merge extra parameters | |
if (is_array($v)&&!empty($v)) { | |
self::$o = array_merge(self::$o,$v); | |
} | |
//! set http response header as well for special templates | |
if ($n == '403' || $n == '404') { | |
@header("HTTP/1.1 $n ".($n == '403' ? 'Access Denied' : 'Not Found')); | |
} | |
//! get template content | |
$d = self::get($n); | |
//! skip parser if it's empty | |
if (empty($d)) { | |
return ''; | |
} | |
self::$n = -1; | |
self::$c = []; | |
//! parse tags | |
return self::_t($d); | |
} | |
/** | |
* Get value of a templater expression. | |
* | |
* @param string expression | |
* | |
* @return mixed value | |
*/ | |
public static function getval($x) | |
{ | |
if (!is_string($x)) { | |
return $x; | |
} | |
if (isset(self::$o[$x])) | |
return self::$o[$x]; | |
if(empty(self::$C[$x])){ | |
//! evaluate an expression for templater and return it's value | |
//! security check: look for variables and let operator | |
if (strpos($x, '$') !== false || preg_match('/[^!=]=[^=]/', $x)) { | |
return self::e('W', $x, 'BADINP'); | |
} | |
$l = $r = ''; | |
$d = $x; | |
//convert expression to php commands | |
$L = strlen($d); | |
for ($i = 0; $i < $L; ++$i) { | |
$c = $d[$i]; | |
//! string literals | |
if ($c == '"' || $c == "'") { | |
$r .= $c; | |
++$i; | |
$b = ''; | |
while ($i < $L && $b != $c) { | |
if ($b == '\\') { | |
$b .= $d[$i++]; | |
} | |
$r .= $b; | |
$b = $d[$i++]; | |
} | |
$r .= $c; | |
$l = ''; | |
} | |
//! variable and function names | |
if ((ctype_alpha($c) || $c == '_') && !ctype_alnum($l) && $l != '.' && $l != '_') { | |
$Y = 0; | |
while (substr($d, $i, 7) == 'parent.') { | |
$i += 7; | |
$Y++; | |
} | |
$j = $i; | |
$b = $d[$j]; | |
while (($b||$b=='0') && (ctype_alnum($b) || $b == '_')) { | |
$j++; | |
$b = isset($d[$j]) ? $d[$j] : ''; | |
} | |
if ($b != '(' && $b != ':') { | |
//! special variables in foreach structures | |
$v = substr($d, $i, $j - $i); | |
switch ($v) { | |
case 'KEY' : //! the current key of the array/field name of object | |
case 'IDX' : //! index in interation | |
$r .= '\PHPPE\View::$c[\PHPPE\View::$n-'.$Y.']->'.$v; | |
$i = $j; | |
break; | |
case 'ODD' : //! for striped output | |
$r .= '(\PHPPE\View::$c[\PHPPE\View::$n-'.$Y.']->IDX%2)'; | |
$i = $j; | |
break; | |
case 'true' : //! for convenience | |
case 'false' : | |
case 'null' : | |
$r .= $v; | |
$i = $j; | |
break; | |
case 'VALUE' : //! the current value of the array element/object field | |
$r .= '(isset(\PHPPE\View::$c[\PHPPE\View::$n-'.$Y.'])&&isset(\PHPPE\View::$c[\PHPPE\View::$n-'.$Y.']->VALUE)?(!is_object(\PHPPE\View::$c[\PHPPE\View::$n-'.$Y.']->VALUE)?\PHPPE\View::$c[\PHPPE\View::$n-'.$Y.']->VALUE:get_class(\PHPPE\View::$c[\PHPPE\View::$n-'.$Y.']->VALUE)):"")'; | |
$i = $j; @$c=$d[$i]; | |
break; | |
default : //! get the value | |
if(@$d[$j]=="."&&isset(self::$o[$v])) | |
$r.='\PHPPE\View::$o["'.$v.'"]'; | |
else | |
$r .= '(isset(\PHPPE\View::$c[\PHPPE\View::$n-'.$Y.'])&&isset(\PHPPE\View::$c[\PHPPE\View::$n-'.$Y.']->VALUE)?(is_array(\PHPPE\View::$c[\PHPPE\View::$n-'.$Y.']->VALUE)?\PHPPE\View::$c[\PHPPE\View::$n-'.$Y.']->VALUE["'.$v.'"]:\PHPPE\View::$c[\PHPPE\View::$n-'.$Y.']->VALUE->'.$v.'):(isset(\PHPPE\View::$o["app"]->'.$v.')?\PHPPE\View::$o["app"]->'.$v.':$'.$v.'))'; | |
$i = $j; @$c=$d[$i]; | |
} | |
} | |
//! check function names against allowed config. Always allow translations and core methods | |
elseif ($b == '(' && ($j - $i != 1 || $d[$i] != 'L') && substr($d, $i, 5) != 'core.' && | |
substr($d, $i, $j - $i) != 'array' && !empty(Core::$core->allowed) && !in_array(substr($d, $i, $j - $i), | |
Core::$core->allowed)) { | |
return self::e('E', $x, 'BADFNC'); | |
} | |
} | |
$r .= ($c == '.' ? '->' : (isset($d[$i]) ? $d[$i] : '')); | |
$l = $c; | |
} | |
//! for string operators | |
$r = strtr($r, ["+'" => ".'", '+"' => '."', "'+" => "'.", '"+' => '".']); | |
self::$C[$x] = $r; | |
} else | |
$r = self::$C[$x]; | |
//! evaluate php | |
ob_start(); | |
//! save expression for Exception handler | |
self::$e = $x.' => '.$r; | |
$e=error_reporting(); | |
error_reporting($e&~E_NOTICE); | |
try { | |
$R = eval('return '.$r.';'); | |
} catch(\ParseError $e) { | |
$R = $r; | |
} | |
error_reporting($e); | |
$o = ob_get_clean(); | |
self::$e = ''; | |
//! log expressions in debug mode | |
if (Core::$core->runlevel > 2) { | |
// @codeCoverageIgnoreStart | |
Core::log('D', $x.' => '.$r.' = '.serialize($R).$o, 'view'); | |
//! on error stop | |
if(!empty($o)) | |
die($o); | |
} | |
// @codeCoverageIgnoreEnd | |
return $R; | |
} | |
/** | |
* Return and increase try button counter. | |
*/ | |
public static function tc() | |
{ | |
return ++self::$tc; | |
} | |
/** | |
* Format an error message. | |
* | |
* @param string weight (see log()) | |
* @param string module | |
* @param string message | |
* | |
* @return string formated message | |
*/ | |
public static function e($w, $m, $c="") | |
{ | |
if (!is_string($m)) { | |
$m = json_encode($m); | |
} | |
return !empty(Core::$core->output) && Core::$core->output == 'html' ? | |
"<span style='background:#F00000;color:#FEA0A0;padding:3px;'>".($w ? "$w-" : '').($c ? "$c: " : '').htmlspecialchars($m).'</span>' : | |
($w=='E'||$w=='C'?chr(27)."[91;05m":"")."$c-$w: ".strtr($m, ["\r" => '', "\n" => '\\n']).chr(27)."[0m\n"; | |
} | |
/** | |
* Evaluate view template. Reentrant, use View::template() instead | |
*/ | |
public static function _t($x, $re = 0) | |
{ | |
//! parse a template string | |
//check recursion limit | |
$L = self::e('W', L('recursion limit exceeded').'!', 'TOOMNY'); | |
if ($re >= 64) { | |
return $L; | |
} | |
//check if we're in cms edit mode | |
$J = ClassMap::has("PHPPE\\CMS") && | |
@get_class(View::getval('app'))=="PHPPE\\Content" && | |
Core::$user->has("siteadm|webadm"); | |
//get tags | |
if (preg_match_all("/<!([^\[\-][^>]+)>[\r\n]*/ms", $x, $T, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { | |
//get opening/closing pairs | |
$o = []; | |
$I = 0; | |
foreach ($T as $k => $v) { | |
$T[$k][0][2] = strlen($v[0][0]); | |
$t = strtolower(substr($v[1][0], 0, 4)); | |
if (substr($t, 0, 2) == 'if' || $t == 'fore' || $t == 'temp') { | |
$o[$I++] = $k; | |
} elseif ($t == 'else') { | |
@$T[$o[$I - 1]][4] = $k; | |
} elseif (substr($t, 0, 3) == '/if' || $t == '/for' || $t == '/tem') { | |
@$T[$o[--$I]][3] = $k; | |
} | |
} | |
if ($I) { | |
return self::e('W', L('unclosed tag'), 'UNCLS'); | |
} | |
unset($o); | |
//parse tags | |
$C = 0; | |
for ($k = 0; $k < count($T) && $m = $T[$k]; ++$k) { | |
$H = trim($m[1][0]); | |
$w = ''; | |
$a = ''; | |
if ($H[0] == '=') { | |
$t = $g = '='; | |
} else { | |
$g = trim(strstr($H, ' ', true)); | |
$t = trim(strstr($g, '(', true)); | |
if(empty($t)) $t = $g; | |
} | |
if (empty($t)) { | |
$t = $g = $H; | |
} else { | |
$a = trim(substr($H, strlen($g))); | |
} | |
$A = str_getcsv($a, ' '); | |
$N = $m[0][1]; | |
$M = $m[0][2]; | |
//interpret tags | |
switch ($t) { | |
//multilanguage support | |
case 'l' : | |
case 'L' : | |
$w = L($a); | |
break; | |
//application output marker in frame. It's not our job to parse it | |
case 'app' : | |
$w = '<!app>'; | |
break; | |
//include another template | |
case 'include' : | |
$c = self::get($a); | |
if (!$c) { | |
$c = self::get(self::getval($a)); | |
} | |
$w = self::_t($c, $re + 1); | |
break; | |
//expression | |
case '=' : | |
$w = self::getval($a); | |
break; | |
//re-entrant parsing | |
case 'template' : | |
$w = self::_t(strtr(self::_t(substr($x, $m[0][1] + $m[0][2] + $C, | |
$T[$m[3]][0][1] - $m[0][1] - $m[0][2]), $re), '<%', '<!'), $re + 1); | |
$k = $m[3]; | |
$M = $T[$m[3]][0][1] - $m[0][1] + $T[$m[3]][0][2]; | |
break; | |
//iteration | |
case 'foreach' : | |
$d = self::getval($a); | |
self::$n++; | |
self::$c[self::$n] = (object) ['IDX' => 1]; | |
$t = substr($x, $m[0][1] + $m[0][2] + $C, $T[$m[3]][0][1] - $m[0][1] - $m[0][2]); | |
if ((is_array($d) && count($d) > 0) || is_object($d)) { | |
foreach ($d as $k => $v) { | |
self::$c[self::$n]->KEY = $k; | |
self::$c[self::$n]->VALUE = $v; | |
$w .= @self::_t($t, $re + 1); | |
self::$c[self::$n]->IDX++; | |
} | |
} | |
$k = $m[3]; | |
$M = $T[$m[3]][0][1] - $m[0][1] + $T[$m[3]][0][2]; | |
unset(self::$c[self::$n]); | |
self::$n--; | |
break; | |
//conditional | |
case 'if' : | |
$M = $T[$m[3]][0][1] + $T[$m[3]][0][2] - $m[0][1]; | |
$w = self::_t(($a != 'cms' && !empty(self::getval($a))) || ($a == 'cms' && $J) ? substr($x, $N + $C + $m[0][2], !empty($m[4]) ? $T[$m[4]][0][1] - $m[0][1] - $m[0][2] : $M - $m[0][2] - $T[$m[3]][0][2]) : (!empty($m[4]) ? substr($x, $T[$m[4]][0][1] + $C + $T[$m[4]][0][2], $T[$m[3]][0][1] - $T[$m[4]][0][1] - $T[$m[4]][0][2]) : ''), $re + 1); | |
$k = $m[3]; | |
break; | |
//user form | |
case 'form' : | |
self::$tc = 0; | |
$c = sha1(url()); | |
$n = !empty($A[0]) && $A[0] != '-' ? urlencode($A[0]) : 'form'; | |
$w = '<form'.(!empty($A[5]) ? " role='form'" : '')." name='".$n."' action='".url(!empty($A[2]) && $A[2] != '-' ? $A[2] : ''). | |
"' class='".(!empty($A[1]) && $A[1] != '-' ? $A[1] : 'form-vertical'). | |
"' method='post' enctype='multipart/form-data'". | |
(!empty($A[3]) && $A[3] != '-' ? ' onsubmit="'.strtr($A[3], ['"' => '\\"']).'"' : '').(!empty($A[4]) && $A[4] != '-'?' '.$A[4]:''). | |
"><input type='hidden' name='MAX_FILE_SIZE' value='".Core::$core->fm. | |
"'><input type='hidden' name='pe_s' value='".@$_SESSION['pe_s'][$c]. | |
"'><input type='hidden' name='pe_f' value='".$n."'>".(!empty(Core::$core->item) ? | |
"<input type='hidden' name='item' value='".htmlspecialchars(Core::$core->item)."'>" : ''); | |
break; | |
//date and time formating | |
case 'time' : | |
case 'date' : | |
$v = self::getval($A[0]); | |
$w = !empty($v) ? date((!empty(Core::$l['dateformat']) ? | |
Core::$l['dateformat'] : 'Y-m-d').($t == 'time' ? ' H:i:s' : ''), self::ts($v)) : (!empty($A[1]) ? '' : L('Epoch')); | |
break; | |
case 'difftime' : | |
$w = ''; $l='%s'; | |
$v = self::getval($A[0]); | |
if (!empty($A[1])) { | |
if (!$v) { | |
$w = '-'; | |
break; | |
} | |
$v -= self::ts(self::getval($A[1])); | |
} | |
if ($v < 0) { | |
$l=!empty(Core::$l['%s ago'])?Core::$l['%s ago']:'- %s'; | |
$v = -$v; | |
} | |
$c = floor($v / 86400); | |
$b = floor(($v - $c * 86400) / 3600); | |
$a = floor(($v - $c * 86400 - $b * 3600) / 60); | |
$w = sprintf($l,$c ? "$c ".L('day'.($c > 1 ? 's' : '')) : ($b ? "$b ".L('hour'.($b > 1 ? 's' : '')) : '').($a || !$b ? ($b ? ', ' : '')."$a ".L('min'.($a > 1 ? 's' : '')) : '')); | |
break; | |
//dump object - this only works if runlevel is at least testing (1) | |
case 'dump' : | |
$l = Core::$core->runlevel; | |
if ($l < 1) { | |
$w = ''; | |
} else { | |
ob_start(); | |
$s = null; | |
if ($A[0] == '_SESSION') { | |
$s = $_SESSION; | |
unset($s['pe_u']); | |
unset($s['pe_s']); | |
} else { | |
$s = self::getval($A[0]); | |
} | |
//use print_r in verbose, var_dump on developer and debug runlevels | |
if ($l > 1) { | |
var_dump($s); | |
$n = ob_get_clean(); | |
if ($n[0] != '<') { | |
$n = '<pre>'.$n.'</pre>'; | |
} | |
} else { | |
print_r($s); | |
$n = '<pre>'.htmlspecialchars(ob_get_clean()).'</pre>'; | |
} | |
$w = "<div class='dump'><b style='font:monospace;'>".$A[0].':</b>'. | |
preg_replace("/<small>.*?<\/small>\n?/", '', preg_replace("/PRIVATE KEY.*?PRIVATE KEY\n?/ims", '', $n)).'</div>'; | |
} | |
break; | |
//hook for cms editor icons | |
case 'cms' : | |
//add-on support | |
case 'widget' : | |
case 'var' : | |
case 'field' : | |
$Z = $R = $m = false; | |
$w=""; | |
$G = $t == 'cms'; | |
$V = $t == 'var'; | |
//if first attribute starts with an at sign, it's an ace definition | |
if ($A[0][0] == '@') { | |
$Z = substr($A[0], 1); | |
array_shift($A); | |
} | |
$Z = empty($Z) || Core::$user->has($Z); | |
//if type starts with an asterix, it's a mandatory field | |
//or with cms tag it displays value | |
if ($A[0][0] == '*') { | |
$R = true; | |
$A[0] = substr($A[0], 1); | |
} | |
$f = $A[0]; | |
//get add-on type and arguments | |
if (preg_match("/^([^\(]+)[\(]?([^\)]*)/", $A[0], $B) && !empty($B[1])) { | |
//submit is just an alias of update | |
$f = $B[1] == 'submit' ? 'update' : $B[1]; | |
//get arguments array | |
$a = self::getval('['.$B[2].']'); | |
array_shift($A); | |
//name | |
$n = !empty($A[0]) ? $A[0] : ''; | |
array_shift($A); | |
//value (if applicable) | |
$v = self::getval($n); | |
//find appropriate class for AddOn | |
$d = '\\PHPPE\\Addon\\'.$f; | |
if (ClassMap::has($d)) { | |
//ok, got it | |
$F = new $d($a, $n, $v, $A, !$G&&$R); | |
//if it has an init() method, and not called yet, call it | |
if (empty(self::$addons[$f])) { | |
if (method_exists($F, 'init')) | |
$F->init(); | |
self::$addons[$f]=1; | |
} | |
//! cms icon | |
if ($G) { | |
if ($J && $Z) | |
$w = CMS::icon($g, $f, $F); | |
//! we use the (otherwise here useless) required marker | |
//! for showing value in non-edit mode | |
$m = $R ? "show" : ""; | |
} else { | |
//add validators and check for required fields | |
if ($R || method_exists($d, 'validate')) { | |
$_SESSION['pe_v'][$n][$f] = [$R, $a, $A]; | |
} | |
//find out method to use to draw AddOn | |
$m = $t == 'field' || !empty($_SESSION[$V ? 'pe_e' : 'pe_c']) && method_exists($F, 'edit') ? 'edit' : 'show'; | |
} | |
//get output | |
$w .= $m && method_exists($F, $m) && $Z ? $F->$m() : ""; | |
unset($F); | |
break; | |
} | |
else | |
$w .= !$G||$J?self::e('W', $f, 'UNKADDON'):""; | |
} | |
break; | |
default : $w = self::e('W', $t, 'UNKTAG'); | |
} | |
//replace templater tag with output, not using any search-and-replace algorithms | |
$D = $N + $C && $x[$N + $C - 1] == "\n" ? 1 : 0; | |
$E = isset($x[$N + $M + $C]) && $x[$N + $M + $C] == "\n" ? 1 : 0; | |
$x = substr($x, 0, $N + $C - $D).$w.substr($x, $N + $M + $C + $E); | |
$C += @strlen($w) - $M - $D - $E; | |
} | |
} | |
return $x; | |
} | |
/** | |
* Generate html head and script tags at the end, and also flush output to client. | |
* Note that this generates what's outside of body tag. Use frame template | |
* for menus, navbars, footer line etc. | |
* | |
* @param string pre-generated main part | |
*/ | |
public static function output(&$txt, $ap="") | |
{ | |
//! get output format | |
$o = Core::$core->output; | |
//! application may override mime type of output | |
Http::mime((!empty(self::$o['app']->mimetype) ? self::$o['app']->mimetype : 'text/'.($o ? $o : 'html')), false); | |
//! output header | |
if ($o) { | |
//! look for extension | |
$c = @glob('vendor/phppe/*/out/'.$o.'_header.php'); | |
if (!empty($c[0])) include_once $c[0]; | |
//! if not found, fallback to built-in version for html | |
elseif ($o == 'html') { | |
$P = empty(Core::$core->nopanel) && Core::$user->has('panel'); | |
$I = basename(__FILE__).'/'; | |
if ($I == 'index.php/') $I = ''; | |
$d = 'http'.(Core::$core->sec ? 's' : '').'://'.Core::$core->base; | |
//! HTML5 header and title | |
echo "<!DOCTYPE HTML><html lang='".Core::$client->lang."'".(!empty(Core::$l['rtl']) ? " dir='rtl'" : '').'>'. | |
'<head><title>'.(!empty(self::$o['app']->title) ? self::$o['app']->title : ( | |
Core::$core->title ? Core::$core->title : 'PHPPE'.VERSION)).'</title>'. | |
"<base href='$d'/><meta charset='utf-8'/><meta name='Generator' content='PHPPE".VERSION."'/>"; | |
//! meta tags | |
foreach (array_merge(self::$hdr['meta'], !empty(self::$o['app']->meta) ? self::$o['app']->meta : []) as $k => $m) { | |
if (!is_array($m)) { | |
$m=[$m,"name"]; | |
} | |
if ($k && !empty($m[0]) && !empty($m[1])) { | |
echo "<meta ".$m[1]."='$k' content='".htmlspecialchars($m[0])."'/>"; | |
} | |
} | |
//! favicon | |
self::$hdr['link'][ | |
!empty(self::$o['app']->favicon) ? self::$o['app']->favicon : 'favicon.ico' | |
] = 'shortcut icon'; | |
//! link tags | |
foreach (self::$hdr['link'] as $k => $m) { | |
if (!empty($m) && $m != 'js') { | |
echo "<link rel='$m' href='".$k."'/>"; | |
} | |
} | |
//! add style sheets (async) | |
$O = "<style media='all'>"; | |
$d = "@import url('%s');"; | |
$N = Core::$core->base.Core::$core->url.'/'.Core::$user->id.'/'.Core::$client->lang; | |
$e = 'css'; | |
//! admin css if user logged in and has access | |
if ($P) { | |
$O .= sprintf($d, $I."?cache=$e"); | |
} | |
//! user stylesheets | |
if (!empty(self::$hdr['css'])) { | |
//! if aggregation allowed | |
if (!empty(Cache::$mc) && empty(Core::$core->noaggr)) { | |
$n = sha1($N."_$e"); | |
if (empty(Cache::get("c_$n", true))) { | |
$da = ''; | |
//! skip dynamic assets (they use a different caching mechanism) | |
foreach (self::$hdr['css'] as $u => $v) { | |
if ($v && substr($v, -3) != 'php' && $u[0] != '?') { | |
$da .= Assets::minify(file_get_contents($v), $e)."\n"; | |
} | |
} | |
//! save result to cache | |
Cache::set("c_$n", ['m' => "text/$e", 'd' => $da], 0, true); | |
} | |
$O .= sprintf($d, $I."?cache=$n"); | |
//! add dynamic stylesheets, they were left out from aggregated cache above | |
foreach (self::$hdr['css'] as $u => $v) { | |
if ($v && ($u[0] == '?' || substr($v, -3) == 'php')) { | |
$O .= sprintf($d, ($u[0] == '?' ? '' : $I."$e/").$u); | |
} | |
} | |
} else { | |
foreach (self::$hdr['css'] as $u => $v) { | |
if ($v) { | |
$O .= sprintf($d, ($u[0] == '?' ? '' : $I."$e/").$u); | |
} | |
} | |
} | |
} | |
$O .= "</style>"; | |
echo "$O</head><body>\n"; | |
//! display PHPPE panel | |
if ($P) { | |
$H = " class='sub' style='visibility:hidden;' onmousemove='return pe_w();'"; | |
$O = "<div id='pe_p'><a href='".url('/')."'><img src='$I?cache=logo' alt='PHPPE".VERSION."' style='margin:3px 10px -3px 10px;float:left;'></a><div class='menu'>"; | |
//! menu items and submenus | |
$x = 0; | |
if (!empty(self::$menu)) { | |
foreach (self::$menu as $e => $L) { | |
//! access check | |
@list($ti, $a) = explode('@', $e); | |
if ($a && !Core::$user->has($a)) continue; | |
$a = 0; | |
if (is_array($L)) { | |
$l = $L[array_keys($L)[0]]; | |
} else { | |
$l = $L; | |
} | |
$U = Core::$core->url; | |
//! if url starts with menu link | |
if (substr($l, 0, strlen($U)) == $U) $a = 1; | |
else { | |
$d = explode('/', $l); | |
if ((!empty($ap)?$ap:Core::$core->app) == (!empty($d[0]) ? $d[0] : 'index')) { | |
$a = 1; | |
} | |
unset($d); | |
} | |
if (is_array($L)) { | |
$O .= "<div id='pe_m$x'$H><ul>"; | |
foreach ($L as $t => $l) { | |
if ($t) { | |
@list($Y, $A) = explode('@', $t); | |
if (empty($A) || Core::$user->has($A)) { | |
$O .= "<li onclick=\"document.location.href='".$I."$l';\"><a href='".$I."$l'>".htmlspecialchars(L($Y)).'</a></li>'; | |
} | |
} | |
} | |
$O .= "</ul></div><span class='menu_".($a ? 'a' : 'i')."' onclick='return pe_p(\"pe_m$x\");'>".htmlspecialchars(L($ti)).'</span>'; | |
++$x; | |
} else { | |
$O .= "<span class='menu_".($a ? 'a' : 'i')."'><a href='".$I."$L'>".htmlspecialchars(L($ti)).'</a></span>'; | |
} | |
} | |
} | |
//! call extensions status hooks | |
$O .= "</div><div class='stat'>"; | |
//! *** STAT Event *** | |
foreach (Core::lib() as $d) { | |
if (method_exists($d, 'stat')) { | |
$O .= '<span>'.$d->stat().'</span>'; | |
} | |
} | |
//! language selector box | |
$O .= "<div id='pe_l'$H><ul>"; | |
if (!empty($_SESSION['pe_ls']) && count($_SESSION['pe_ls'])>1) { | |
$d = $_SESSION['pe_ls']; | |
} else { | |
//if application has translations, use that list | |
//if not, fallback to core's translations | |
$D = @scandir('app/lang'); | |
if (!is_array($D)) $D = []; | |
$d = @scandir('vendor/phppe/Core/lang'); | |
if (is_array($d)) { | |
$D = array_unique($D + $d); | |
} | |
$d = []; | |
foreach ($D as $f) { | |
if (substr($f, -4) == '.php') { | |
$d[substr($f, 0, strlen($f) - 4)] = 1; | |
} | |
} | |
$_SESSION['pe_ls'] = $d; | |
} | |
foreach ($d as $k => $v) { | |
if ($k) { | |
$O .= "<li><a href='".url()."?lang=$k'><img src='images/lang_$k.png' alt='$k' title='$k'>".($k != L($k) ? ' '.L($k) : '').'</a></li>'; | |
} | |
} | |
$O .= '</ul></div>'; | |
//! current language and user menu | |
$k = Core::$client->lang; | |
$f = "images/lang_$k.png"; | |
$c = !empty($_SESSION['pe_c']); | |
$O .= "<span onclick='return pe_p(\"pe_l\");'>". | |
(file_exists('vendor/phppe/Core/'.$f) ? "<img src='$f' height='10' alt='$k' title='$k'>" : $k).'</span>'. | |
"<div id='pe_u'$H><ul><li onclick='pe_p(\"\");if(typeof(users.profile)==\"function\")users.profile(this);else alert(\"".L('Install PHPPE Pack')."\");'>".L('Profile').'</li>'. | |
(Core::$user->has('conf') ? "<li><a href='".url().'?conf='.(1 - $c)."'>".($c ? L('Lock') : L('Unlock')).'</a></li>' : ''). | |
"<li><a href='".url('logout')."'>".L('Logout').'</a></li></ul></div>'. | |
"<span onclick='return pe_p(\"pe_u\");'>".(!empty(Core::$user->name) ? Core::$user->name : '#'.Core::$user->id).'</span></div></div>'. | |
"<div style='height:32px !important;'></div>\n"; | |
echo $O; | |
} | |
} | |
} | |
Core::bm("header"); | |
//! output main content (generated earlier by View::generate()) | |
echo $txt; | |
Core::bm("content"); | |
//! output footer | |
if ($o) { | |
//! look for extension | |
$c = @glob('vendor/phppe/*/out/'.$o.'_footer.php'); | |
if (!empty($c[0])) include_once $c[0]; | |
//! fallback to built-in version for html | |
elseif ($o == 'html') { | |
//! add javascript libraries (async) | |
$d = '<script'; | |
$e = "</script>"; | |
$a = " src='".$I.'js/'; | |
$O = $d.">var pe={};".$e; | |
if (!empty(self::$hdr['jslib'])) { | |
//! if aggregation allowed | |
if (!empty(Cache::$mc) && empty(Core::$core->noaggr)) { | |
$n = sha1($N.'_js'); | |
if (empty(Cache::get("c_$n", true))) { | |
$da = ''; | |
//! skip dynamic assets and cdn links (they use a different caching mechanism) | |
foreach (self::$hdr['jslib'] as $u => $v) { | |
if ($v && substr($v, -3) != 'php' && substr($u, 0, 4) != 'http') { | |
$da .= Assets::minify(file_get_contents(substr($v, 2)), 'js')."\n"; | |
} | |
} | |
Cache::set("c_$n", ['m' => 'text/javascript', 'd' => $da], 0, true); | |
} | |
$O .= "$d src='${I}js/?cache=$n'>$e"; | |
//! add dynamic javascripts, they were left out from aggregated cache above | |
foreach (self::$hdr['jslib'] as $u => $v) { | |
if (substr($u, 0, 4) == 'http') { | |
$O .= "$d src='$u'>$e"; | |
} elseif ($u[0] == '?' || substr($v, -3) == 'php') { | |
$O .= "$d$a$u'>$e"; | |
} | |
} | |
} else { | |
foreach (self::$hdr['jslib'] as $u => $v) { | |
if (substr($u, 0, 4) == 'http') { | |
$O .= "$d src='$u'>$e"; | |
} else { | |
$O .= "$d$a$u'>$e"; | |
} | |
} | |
} | |
} | |
//load PHPPE\Users' JS library if it's not aggregated already and PHPPE panel is shown | |
$c = 'users.js'; | |
$i = file_exists('vendor/phppe/Core/js/'.$c.'.php'); | |
if ($P && !isset(self::$hdr['jslib'][$c]) && $i) { | |
$O .= "$d$a$c'>$e"; | |
} | |
//! add javascript functions | |
$c = self::$hdr['js']; | |
$a = ''; | |
//! built-in stuff if core.js is not installed | |
if (!$i) { | |
// @codeCoverageIgnoreStart | |
$x = 'document.getElementById('; | |
$y = '.style.visibility'; | |
$a = "pe_t=setTimeout(function(){pe_p('');},2000)"; | |
$c['L(t)'] = "var i=0,a=Array.prototype.slice.call(arguments,1);return t.replace(/_/g,' ').replace(/%[sd]/g,function(){return a[i++];});"; | |
$c['pe_p(i)'] = "var o=i?${x}i):i;if(pe_t!=null)clearTimeout(pe_t);if(pe_c&&pe_c!=i)${x}pe_c)$y='hidden';pe_t=pe_c=null;if(o!=null&&o.style!=null){if(o$y=='visible')o$y='hidden';else{o$y='visible';pe_c=i;$a;}}return false;"; | |
$c['pe_w()'] = "if(pe_t!=null)clearTimeout(pe_t);$a;return false;"; | |
$a = ',pe_t,pe_c,pe_h=0'; | |
// @codeCoverageIgnoreEnd | |
} | |
if (!empty($c)) { | |
//! Js variables: pe_i=init executed, pe_ot=offset top | |
$O .= $d.">var pe_i=0,pe_ot=".($P ? 31 : 0)."$a;"; | |
foreach ($c as $fu => $co) { | |
//! make sure init only gets called once | |
$O .= "function ".$fu."{".($fu=="init()"?"if(pe_i)return;pe_i=1;".(Core::$core->runlevel>2?"console.log('PE Plugins',pe);":""):"").$co."}\n"; | |
} | |
//! add event listeners to call init() on page load | |
if(!empty(self::$hdr['js']['init()'])) { | |
$O .= "document.addEventListener('DOMContentLoaded', init);window.addEventListener('load', init);setTimeout(init,100);"; | |
} | |
$O .= $e; | |
} | |
$s = Core::started(); | |
$d = 'REQUEST_TIME_FLOAT'; | |
$T = !empty($_SERVER[$d]) ? $_SERVER[$d] : $s; | |
echo "\n$O<!-- MONITORING: ".(Core::isError() ? 'ERROR' : (Core::$core->runlevel > 0 ? 'WARNING' : 'OK')). | |
', page '.sprintf("%.4f sec, db %.4f sec, server %.4f sec, mem %.4f mb%s --></body></html>\n", | |
microtime(1) - $T, | |
DS::bill(), $s - $T, memory_get_peak_usage() / 1024 / 1024, !empty(Cache::$mc) && empty(Core::$core->nocache) ? ', mc' : ''); | |
} | |
} | |
Core::bm("footer"); | |
flush(); | |
} | |
/** | |
* Picture manipulation. | |
* | |
* @param string original image file | |
* @param string new image file | |
* @param integer maximum width | |
* @param integer maximum height | |
* @param boolean crop image | |
* @param boolean use lossless compression (png), defaults to jpeg | |
* @param string watermark image, must be a semi-transparent png | |
* @param integer maximum file size for output. Will reduce quality to fit | |
* @param integer minimum quality (1-10) | |
* | |
* @return boolean true or false, success | |
*/ | |
public static function picture($o, $n, $w, $h, $c = 0, $l = 1, $W = '', $s = 8192, $m = 5) | |
{ | |
//! try to load image, fallback to plain copy if failed | |
$d = @file_get_contents($o); | |
if (!function_exists('gd_info') || empty($d) || !($i = @imagecreatefromstring($d))) { | |
Core::log('W', L("no php-libgd or bad image").": $o", 'picture'); | |
if (file_exists($o)) { | |
@copy($o, $n); | |
} | |
return false; | |
} | |
//! get original image dimensions | |
$x = imagesx($i); | |
$y = imagesy($i); | |
//! limit checks and output format | |
$q = 9; | |
$m = ($m > 0 && $m < 10 ? $m : 5); | |
$s = ($s > 64 ? $s : 64) * 1024; | |
$j = 'imagepng'; | |
if (!$l) { | |
$j = 'imagejpeg'; | |
$m *= 10; | |
$q = 99; | |
} | |
Core::log('D', $o."($x,$y) -> ".$n."($w,$h,".($c ? 'crop' : 'resize').",$j,$s,$q) $W", 'picture'); | |
//! calculate new picture dimensions | |
if (!$c) { | |
//! resize keeping aspect ratio | |
$X = $x < $y ? floor(($h / $y) * $x) : $w; | |
$Y = $x < $y ? $h : floor(($w / $x) * $y); | |
$c = $d = 0; | |
$e = $x; | |
$f = $y; | |
} else { | |
//! crop from the middle | |
$X = $w; | |
$Y = $h; | |
$e = $x / $y <= $w / $h ? $x : floor($w * $y / $h); | |
$f = $x / $y <= $w / $h ? floor($h * $x / $w) : $y; | |
$c = $x / $y <= $w / $h ? 0 : floor(($x - $e) / 2); | |
$d = $x / $y <= $w / $h ? floor(($y - $f) / 2) : 0; | |
} | |
//! create output image | |
$N = imagecreatetruecolor($X, $Y); | |
//! don't loose transparent background | |
if ($l) { | |
imagealphablending($N, 0); | |
$a = imagecolorallocatealpha($N, 255, 255, 255, 255); | |
imagesavealpha($N, 1); | |
} else { | |
$a = imagecolorallocate($N, 255, 255, 255); | |
} | |
imagefill($N, 0, 0, $a); | |
imagecopyresampled($N, $i, 0, 0, $c, $d, $X, $Y, $e, $f); | |
//! tile watermark logo on image | |
if (!empty($W)) { | |
$g = @imagecreatefrompng($W); | |
if (!empty($g)) { | |
$a = imagesx($g); | |
$b = imagesy($g); | |
for ($y = 0; $y < $Y; $y += $a) { | |
for ($x = 0; $x < $X; $x += $a) { | |
imagecopyresampled($N, $g, $x, $y, 0, 0, $a, $b, $a, $b); | |
} | |
} | |
} else { | |
Core::log('W', "bad watermark image: $W", 'picture'); | |
} | |
} | |
//! reduce quality to match file maximum byte size requirement | |
@unlink($n); | |
while (!file_exists($n) || (filesize($n) > $s && $q >= $m)) { | |
@unlink($n); | |
if (!$j($N, $n, $q--)) { | |
// @codeCoverageIgnoreStart | |
@copy($o, $n); | |
return false; | |
// @codeCoverageIgnoreEnd | |
} | |
} | |
return true; | |
} | |
/** | |
* Private helper function to generate html for built-in fields. | |
*/ | |
public static function v($a, $b, $e = '', $f = [], $p = '', $i = '', $n = '') | |
{ | |
return (@$a->css[0] == 'r' ? ' required' : ''). | |
($p ? " pattern='".$p."'" : ''). | |
" class='".$a->css.' '.(!empty($b) && $b != '-' ? $b : 'form-control')."'". | |
($a->fld ? " id='".$a->fld.$i."' name='".$a->fld.$n."'" : ''). | |
($e && $e != '-' ? " onchange='".$e."'" : ''). | |
(!empty($f[0]) && $f[0] > 0 ? " maxlength='".$f[0]."'" : ''); | |
} | |
/** | |
* Load a raw template. | |
* | |
* @param string name of the template | |
* | |
* @return string template string, cached if available | |
*/ | |
public static function get($n) | |
{ | |
//! get a template in raw format | |
$t = ''; | |
$m = []; | |
$V = 'views'; | |
$e = '.tpl'; | |
//! from cache if possible | |
$C = 't_'.sha1(Core::$core->base.'_'.$n); | |
if ($p = Cache::get($C) && !empty($p) && is_array($p) && !empty($p['d'])) $t = $p['d']; | |
//! on cache miss | |
if (empty($t)) { | |
//! from database | |
if (!empty(DS::db())/* && file_exists("vendor/phppe/Core/sql/$V.sql")*/) { | |
try { | |
foreach ([Core::$core->app.'/'.$n, $n] as $v) { | |
$p = (array)DS::fetch('*', $V, 'id=?', '', '', [$v]); | |
if (!empty($p['data'])) { | |
foreach (['css', 'jslib'] as $c) { | |
$t = json_decode($p[$c], true); | |
if (is_array($t)) { | |
foreach ($t as $v) { | |
self::$hdr[$c][basename($v)] = ($c == 'jslib' ? '99' : '').$v; | |
} | |
} | |
} | |
$t = $p['data']; | |
break; | |
} | |
} | |
// @codeCoverageIgnoreStart | |
} catch (\Exception $F) { | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
//! from file - fallback if not found in database | |
if (!$t) { | |
foreach (["app/$V/$n$e", | |
self::$p ? self::$p."/$V/$n$e" : '', | |
'vendor/phppe/'.Core::$core->app."/$V/$n$e", | |
"vendor/phppe/Core/$V/$n$e",] as $F) { | |
if ($F && file_exists($F)) { | |
$t = file_get_contents($F); | |
break; | |
} | |
} | |
} | |
//! failsafe: remove comments and php tags | |
$t = preg_replace("/<!-.*?->[\r\n]*/ms", '', preg_replace("/<\?.*?\?\>[\r\n]*/ms", '', $t)); | |
//! save to cache | |
if (!empty($t)) { | |
Cache::set($C, ['d' => $t]); | |
} | |
} | |
//! return raw template | |
return $t; | |
} | |
/** | |
* Return a timestamp. | |
* | |
* @param string timestamp or date | |
* | |
* @return integer timestamp | |
*/ | |
private static function ts($v) | |
{ | |
return preg_match('|^[0-9]+$|', $v) ? $v : strtotime($v); | |
} | |
/** | |
* Dump view objects. | |
*/ | |
// @codeCoverageIgnoreStart | |
public static function dump() | |
{ | |
Http::mime('text/plain', false); | |
print_r(self::$o); | |
print_r($_SERVER); | |
print_r(Http::route()); | |
die; | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
/** | |
* Some useful tools for file manipulations. | |
*/ | |
// @codeCoverageIgnoreStart | |
class Tools extends Extension | |
{ | |
/** | |
* Recursive directory delete | |
* | |
* @param string directory | |
* | |
* @return boolean true on success | |
*/ | |
public static function rmdir($dir) | |
{ | |
if (is_dir($dir)) { | |
$d = array_diff(scandir($dir),[".",".."]); | |
foreach ($d as $v) { | |
self::rmdir($dir."/".$v); | |
} | |
return rmdir($dir); | |
} else { | |
return unlink($dir); | |
} | |
} | |
/** | |
* Archive extractor (file can be pkzip,gz,bz2,tar,cpio,pax) | |
* | |
* @param string archive file | |
* @param string/array/callable filename to get or callback [class,method] or callable | |
* | |
* @return string content of the file in archive | |
*/ | |
public static function untar($file, $fn = '') | |
{ | |
//! detect format | |
$body = ''; | |
$f = gzopen($file, 'rb'); | |
if ($f) { | |
$read = 'gzread'; | |
$close = 'gzclose'; | |
$close = 'gzclose'; | |
$open = 'gzopen'; | |
} else { | |
$f = bzopen($file, 'rb'); | |
if ($f) { | |
$read = 'bzread'; | |
$close = 'bzclose'; | |
$close = 'bzclose'; | |
$open = 'bzopen'; | |
} else { | |
throw new \Exception(L('Unable to open').': '.$file); | |
} | |
} | |
//! read archive | |
$data = $read($f, 512); | |
$close($f); | |
if ($data[0] == 'P' && $data[1] == 'K') { | |
$zip = zip_open($file); | |
if (!$zip) { | |
throw new \Exception(L('Unable to open').': '.$file); | |
} | |
while ($zip_entry = zip_read($zip)) { | |
$zname = zip_entry_name($zip_entry); | |
if (!zip_entry_open($zip, $zip_entry, 'r')) { | |
continue; | |
} | |
$zip_fs = zip_entry_filesize($zip_entry); | |
if (empty($zip_fs)) { | |
continue; | |
} | |
$body = zip_entry_read($zip_entry, $zip_fs); | |
if (!empty($fn) && is_string($fn)) { | |
zip_entry_close($zip_entry); | |
zip_close($zip); | |
return $body; | |
} | |
if (is_array($fn) && method_exists($fn[0], $fn[1])) { | |
call_user_func($fn, $zname, $body); | |
} elseif(is_callable($fn)) $fn($zname, $body); | |
zip_entry_close($zip_entry); | |
} | |
zip_close($zip); | |
return; | |
} | |
$f = $open($file, 'rb'); | |
$ustar = substr($data, 257, 5) == 'ustar' ? 1 : 0; | |
while (!feof($f) && $data) { | |
$name = ''; | |
if ($ustar) { | |
$data = $read($f, 512); | |
$size = octdec(substr($data, 124, 12)); | |
$body = $size > 0 ? $read($f, floor(($size + 511) / 512) * 512) : ''; | |
$i = 0; | |
while (isset($data[$i]) && ord($data[$i]) != 0 && $i < 512) { | |
$i++; | |
} | |
$name = substr($data, 0, $i); | |
} else { | |
$data = $read($f, 110); | |
if (substr($data, 0, 6) != '070701') { | |
throw new \Exception(L('Bad format')); | |
} | |
$size = floor((hexdec(substr($data, 54, 8)) + 3) / 4) * 4; | |
$len = hexdec(substr($data, 94, 8)); | |
$len += floor((110 + $len + 3) / 4) * 4 - 110 - $len; | |
$name = trim($read($f, $len)); | |
$body = ''; | |
if ($name == 'TRAILER!!!') { | |
break; | |
} | |
$body = $read($f, $size); | |
} | |
if (empty($name)) { | |
$close($f); | |
return ''; | |
} | |
//! if argument was a filename, return it's contents | |
if (!empty($fn) && is_string($fn) && $name == $fn) { | |
$close($f); | |
return substr($body, 0, $size); | |
} | |
//! if argument was an array with class and method name, call it on every file in the archive | |
if (is_array($fn) && method_exists($fn[0], $fn[1])) { | |
call_user_func($fn, $name, substr($body, 0, $size)); | |
} elseif(is_callable($fn)) $fn($name, substr($body, 0, $size)); | |
} | |
$close($f); | |
} | |
/** | |
* Execute a shell command on remote server | |
* | |
* @param string command | |
* @param string input string | |
* @param string command to generate input string | |
* | |
* @return string command output | |
*/ | |
public static function ssh($cmd, $input="", $precmd="") | |
{ | |
//! check for remote configuration | |
if (empty(Core::$user->data['remote']['identity']) || empty(Core::$user->data['remote']['user']) || empty(Core::$user->data['remote']['host']) || empty(Core::$user->data['remote']['path'])) { | |
throw new \Exception(L('configure remote access')); | |
} | |
//! we cannot install localy, that would use webserver's user, forbidden to write. | |
//! So we must use remote user's identity even when host is localhost. | |
$idfile = tempnam('.tmp', '.id_'); | |
file_put_contents($idfile, trim(Core::$user->data['remote']['identity'])."\n", LOCK_EX); | |
chmod($idfile, 0400); | |
//! a special case | |
if ($cmd=="rsync"){ | |
$ssh = "rsync -an -e \'ssh -i ".escapeshellarg($idfile)."\' --include-from=/dev/stdin ". | |
escapeshellarg(Core::$user->data['remote']['user']."@".Core::$user->data['remote']['host'].":".$precmd); | |
} else { | |
$ssh = ($precmd?$precmd."|":""). | |
"ssh -i ".escapeshellarg($idfile)." -l ".escapeshellarg(Core::$user->data['remote']['user']). | |
(!empty(Core::$user->data['remote']['port'])&&Core::$user->data['remote']['port']>0?" -p ".intval(Core::$user->data['remote']['port']):"")." ".escapeshellarg(Core::$user->data['remote']['host']). | |
" sh -c \\\" ".$cmd." \\\" 2>&1"; | |
} | |
Core::log('A', $ssh, "remote"); | |
$d=[0=>["pipe", "r"], 1=>["pipe", "w"]]; | |
$pr=proc_open($ssh, $d, $p); | |
if($pr!==false && is_array($p)) { | |
if( !empty($input) ) | |
fwrite($p[0],is_array($input)?implode("\n",$input):$input); | |
fclose($p[0]); | |
$r=trim(stream_get_contents($p[1])); | |
fclose($p[1]); | |
proc_close($pr); | |
} else { | |
$r="ssh: unable to execute"; | |
} | |
unlink($idfile); | |
return $r; | |
} | |
/** | |
* Copy files to a remote server over a secure channel. | |
* This one works even if ssh("rsync") fails but does a full copy. | |
* | |
* @param string/array source files | |
* @param string destination directory on remote server | |
*/ | |
public static function copy($files, $dest = '', $opt = '') | |
{ | |
if (is_string($files)) { | |
$files = [$files]; | |
} | |
foreach ($files as $k => $v) { | |
$files[$k] = escapeshellarg($v); | |
} | |
$d=Core::$user->data['remote']['path']; | |
if(substr($d,-1)!='/') $d.='/'; $d.=$dest; | |
$r = self::ssh( | |
"tar -xvz -C ".escapeshellarg($d)." ".$opt, | |
"", | |
"tar -cz ".implode(' ', $files)); | |
if (in_array(substr($r, 0, 4), ['ssh:', 'tar:']) || substr($r, 0, 3) == 'sh:') { | |
throw new \Exception(sprintf(L('failed to copy %d files to %s'), count($files), | |
Core::$user->data['remote']['user'].'@'.Core::$user->data['remote']['host'].':'.$d) | |
.': '.explode("\n", $r)[0]); | |
} | |
return $r; | |
} | |
/** | |
* Start a background job | |
* | |
* @param string class | |
* @param string method | |
* @param mixed optional argument | |
* @param int run interval in secs | |
*/ | |
public static function bg($class,$method,$arg=[],$interval=60) | |
{ | |
$f=[".."=>"","/"=>""]; | |
$class=strtr($class, $f); | |
$method=strtr($method, $f); | |
@mkdir(".tmp/bg"); | |
$pidfile=".tmp/bg/".strtr($class[0]=="\\"?substr($class,1):$class,["\\"=>"_"])."_".$method.".pid"; | |
$pid=@file_get_contents($pidfile); | |
// server supervisor | |
if(empty($pid) || !posix_kill($pid,SIGCONT)) { | |
// server service is not running! | |
if ($pid = pcntl_fork()) { | |
file_put_contents($pidfile, $pid); | |
return; // Parent | |
} if (posix_setsid() < 0) | |
return; | |
@ob_end_clean(); // Discard the output buffer and close | |
set_time_limit(0); | |
fclose(STDIN); // Close all of the standard | |
fclose(STDOUT); // file descriptors as we | |
fclose(STDERR); // are running as a daemon. | |
$ctrl = new $class; | |
while(1){ | |
call_user_func_array([$ctrl, $method], is_array($arg)?$arg:[]); | |
if($interval==0) die(); | |
sleep($interval>1?$interval:1); | |
} | |
} | |
} | |
/** | |
* Diag event handler. Cleans up pid files after dead jobs. | |
*/ | |
public function diag() | |
{ | |
@mkdir(".tmp/bg"); | |
$P=glob(".tmp/bg/*.pid"); | |
if (!empty($P)) | |
foreach($P as $p) { | |
$v=@file_get_contents($p); | |
if (empty($v) || !posix_kill($v,SIGCONT)) | |
@unlink($p); | |
} | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
/** | |
* ClassMap autoloader. | |
*/ | |
class ClassMap extends Extension | |
{ | |
public static $file= '.tmp/.classmap'; //!< classes map file | |
public static $ace = '.tmp/.acemap'; //!< access control entries | |
public static $map = []; //!< loaded class map | |
/** | |
* Contructor. Loads the class map and regenerates it if necessary | |
*/ | |
public function __construct() | |
{ | |
//! generate classmap file if it's not exists, | |
//! it's older than extensions directory or forced | |
if (!file_exists(self::$file) || filemtime(self::$file)<@filemtime("vendor/phppe") || | |
isset($_REQUEST['clear']) || @$_SERVER['argv'][1] == '--diag') { | |
self::$map = $this->generate(); | |
} | |
//! load it | |
// @codeCoverageIgnoreStart | |
elseif (empty(self::$map)) { | |
self::$map = json_decode(@file_get_contents(self::$file), true); | |
} | |
//! force regeneration if file corrupted and decoding failed | |
if (!is_array(self::$map)) { | |
self::$map = $this->generate(); | |
} | |
// @codeCoverageIgnoreEnd | |
//! register class loader | |
spl_autoload_register(function ($c) { | |
if (!class_exists($c) && !empty(\PHPPE\ClassMap::$map[strtolower($c)])) { | |
include_once \PHPPE\ClassMap::$map[strtolower($c)]; | |
} | |
}); | |
} | |
/** | |
* Check if a class or method exists. | |
* | |
* @param string classname | |
* @param string methodname optional | |
* | |
* @return boolean true if it's exists or at least loadable | |
*/ | |
public static function has($c, $m="") | |
{ | |
$c=strtolower($c[0]=="\\"?substr($c,1):$c); | |
$i = isset(self::$map[$c]); | |
if(empty($m)) | |
return class_exists($c) || $i; | |
if(!class_exists($c) && $i) include_once(self::$map[$c]); | |
return method_exists($c, $m); | |
} | |
/** | |
* Return class map. | |
* | |
* @return array classes | |
*/ | |
public static function map() | |
{ | |
return self::$map; | |
} | |
/** | |
* Return access control entries map. | |
* | |
* @return array access control entries | |
*/ | |
public static function ace() | |
{ | |
return json_decode(@file_get_contents(self::$ace)); | |
} | |
/** | |
* Generate classmap cache for autoloading. | |
* | |
* @return array new map | |
*/ | |
public function generate() | |
{ | |
//! get list of php files | |
$D = []; | |
$R = []; | |
$A = ["loggedin"=>1]; | |
foreach (['*/*', '*/*/*', '*/*/*/*', '*/*/*/*/*', '*/*/*/*/*/*'] as $v) { | |
$D += array_fill_keys(@glob('vendor/'.$v.'.php', GLOB_NOSORT), 0); | |
} | |
//iterate on list | |
foreach ($D as $fn => $v) { | |
//! load php code | |
$d = @file_get_contents($fn); | |
//! get access control entries | |
if (strpos($fn, '/tests/') === false && | |
preg_match_all("/user\-\>has\([\'\"]([^\'\"]+)/ms", $d, $a)) { | |
foreach($a[1] as $l) { | |
foreach(explode("|",$l) as $r) { | |
$A[@explode(":",$r)[0]]=1; | |
} | |
} | |
} | |
//! skip directories marked | |
if (file_exists(dirname($fn).'/.skipautoload')) { | |
continue; | |
} | |
//! skip if file marked | |
if (strpos($d, '/*!SKIPAUTOLOAD!*/') !== false) { | |
continue; | |
} | |
//! look for namespace and class definitions | |
$i = 0; | |
$l = strlen($d); | |
$ns = ''; | |
//! find first php block | |
while ($i < $l && substr($d, $i, 2) != '<'.'?') { | |
$i++; | |
} | |
while ($i < $l) { | |
//! on php block end, skip to the next | |
if (substr($d, $i, 2) == '?'.'>') { | |
while ($i < $l && substr($d, $i, 2) != '<'.'?') { | |
$i++; | |
} | |
continue; | |
} | |
//! skip over string literals | |
if (($d[$i] == "'" || $d[$i] == '"')) { | |
$s = $d[$i]; | |
$j = $i; | |
++$i; | |
while ($i < $l && $d[$i] != $s) { | |
if ($d[$i] == '\\') { | |
$i++; | |
} | |
$i++; | |
} | |
$i++; | |
continue; | |
} | |
//! don't take comments into account | |
if ($d[$i] == '/' && $d[$i + 1] == '/') { | |
$s = $i; | |
$i += 2; | |
while ($i < $l && $d[$i] != "\n") { | |
$i++; | |
} | |
continue; | |
} | |
if ($d[$i] == '/' && $d[$i + 1] == '*') { | |
$s = $i; | |
$i += 2; | |
while ($i + 1 < $l && ($d[$i] != '*' || $d[$i + 1] != '/')) { | |
$i++; | |
} | |
$i += 2; | |
continue; | |
} | |
//! check for declarations | |
if (($d[$i] == 'n' || $d[$i] == 'c') && | |
preg_match("/^(namespace|class)[\ \t\n]+([^\ \t\n;{\[\(\]\)\$]+)/ims", substr($d, $i), $m) && | |
ctype_alpha(trim($m[2])[0])) { | |
$c = trim($m[2]); | |
if (strtolower($m[1][0]) == 'n') { | |
$ns = $c; | |
} else { | |
$R[strtolower($ns.($ns ? '\\' : '').$c)] = $fn; | |
} | |
$i += strlen($m[0]); | |
} | |
$i++; | |
} | |
} | |
//! sort list of classes alphabetically | |
ksort($R); | |
ksort($A); | |
//! save new classmap cache | |
@mkdir(dirname(self::$file), 0750, true); | |
$ret = ""; | |
foreach ($R as $k => $v) { | |
$ret .= ($ret?",\n":""). ' "'.addslashes($k).'": "'.addslashes($v)."\""; | |
} | |
//! when running in an unitiliazed environment, there'll be no .tmp directory | |
@file_put_contents(self::$file, "{\n".$ret."\n}", LOCK_EX); | |
@chmod(self::$file,0664); | |
@file_put_contents(self::$ace, "[\n \"".implode("\",\n \"",array_keys($A))."\"\n]", LOCK_EX); | |
@chmod(self::$ace,0664); | |
return $R; | |
} | |
} | |
/****** PHPPE Core ******/ | |
/** | |
* this is the heart of PHPPE, the class of \PHPPE\Core::$core. | |
*/ | |
class Core | |
{ | |
//generated properties | |
private $id; //!< magic, 'PHPPE'+VERSION | |
private $base; //!< base url | |
private $url; //!< whole url after script name | |
private $app; //!< main page generator controller | |
private $action; //!< main subpage generator action (extends page) | |
public $item; //!< item to work with, usually an id | |
public $template; //!< templater app's template to use | |
public $now; //!< current server timestamp, from primary datasource if available | |
//configurable properties | |
public $title; //!< title of the site | |
public $runlevel = 1; //!< 0-production,1-test,2-development,3-debug | |
public $syslog = false; //!< send logs to syslog | |
public $trace = false; //!< save trace to log messages | |
public $timeout; //!< session timeout | |
public $mailer; //!< mailer backend (smtp relay url) | |
public $nocache = false; //!< skip cache | |
public $cache; //!< memcache url | |
public $cachettl = 600; //!< whole output cache ttl in sec | |
public $db = ''; //!< primary datasource | |
public $noctrl = true; //!< do not execute Content Controller code | |
public $output; //!< templater output header and footer selector | |
public $meta; //!< meta tags | |
public $link; //!< link tags | |
//end of configurable properties | |
public static $core; //!< self reference, phppe system | |
public static $user; //!< user layer | |
public static $client; //!< client data | |
public static $l = []; //!< language translations | |
private $fm; //!< file max size | |
private $form; //!< name of the submitted form | |
private $try; //!< none-zero for update transaction starts, 1 up to 9 | |
private $error; //!< error messages array | |
private $libs = []; //!< list of initialized services and libraries | |
private $disabled = []; //!< list of disabled extensions | |
private static $started; //!< script start time in msec, float | |
private static $v; //!< validator data | |
private static $paths; //!< direcories of extensions | |
public static $w; //!< boolean, true if called via web (REQUEST_METHOD not empty) | |
public static $g; //!< posix group | |
/*! BENCHMARK START */ | |
public static $bm; //!< benchmarking data | |
/*! BENCHMARK END */ | |
/** | |
* Magic getter to implement read-only properties | |
*/ | |
function __get($n) { return $this->$n; } | |
/** | |
* Constructor. If you pass true as argument, it will build up PHPPE environment, | |
* but won't run your application. For that you'll need to call \PHPPE\Core::$core->run() manually | |
* step 1: check and patch php | |
* step 2: self check | |
* step 3: load framework configuration | |
* step 4: autoload classes | |
* step 5: determine bootstrap type. | |
* | |
* @param boolean true if called as a library | |
* | |
* @return \PHPPE\Core instance | |
*/ | |
public function __construct($islib = true) | |
{ | |
//! server time is calculated with (this - http request arrive time) | |
self::$started = microtime(1); | |
/*! BENCHMARK START */ | |
self::$bm["baseline"]=[0,0]; | |
/*! BENCHMARK END */ | |
//! set self reference for singleton | |
self::$core = &$this; | |
//! patch php, set defaults | |
set_exception_handler(function ($e) { | |
// @codeCoverageIgnoreStart | |
self::log('C', get_class($e).' '.$e->getFile().'('.$e->getLine().'): '.$e->getMessage().(\PHPPE\View::$e ? "\n".\PHPPE\View::$e : '').(empty(Core::$core->trace) ? '' : "\n\t".strtr($e->getTraceAsString(), ["\n" => "\n\t"])), $e->getTrace()[0]['function'] == 'getval' ? 'view' : ''); | |
// @codeCoverageIgnoreEnd | |
}); | |
ini_set('error_log', dirname(__DIR__).'/data/log/php.log'); | |
ini_set('log_errors', 1); | |
//! php version check | |
if (version_compare(PHP_VERSION, '7.0') < 0) { | |
// @codeCoverageIgnoreStart | |
self::log('C', 'PHP 7.0.0 required, found '.PHP_VERSION); | |
} | |
if(!function_exists('mb_internal_encoding')) { | |
self::log('W', L("no php-mbstring")); | |
} else { | |
mb_internal_encoding('utf-8'); | |
} | |
// @codeCoverageIgnoreEnd | |
ini_set('precision',30); | |
ini_set('file_uploads', 1); | |
ini_set('upload_tmp_dir', dirname(__DIR__).'/.tmp'); | |
ini_set('uploadprogress.file.filename_template', dirname(__DIR__).'/.tmp/upd_%s.txt'); | |
//! self check | |
//! this will be updated by the Developer extension's | |
//! Repository::compress() when called with mkrepo or deploy | |
//$c=__FILE__;if(filesize($c)!=99999||'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'!=sha1(preg_replace("/\'([^\']+)\'\!\=sha1/","''!=sha1",file_get_contents($c))))self::log("C","Corrupted ".basename($c)); | |
//! | |
//! set default working directory to ProjectRoot | |
chdir(dirname(__DIR__)); | |
//! add becnhmark point | |
self::bm("phppatch"); | |
//! initialize PHPPE environment | |
//! load framework configuration | |
$c = 'vendor/phppe/Core/config.php'; | |
// @codeCoverageIgnoreStart | |
if (file_exists($c)) { | |
$cfg = @require_once $c; | |
if (is_array($cfg)) { | |
foreach ($cfg as $k => $v) { | |
$this->$k = $v; | |
} | |
} | |
} else { | |
$this->syslog = true; | |
} | |
//! range checks and defaults | |
$this->id = 'PHPPE'.VERSION; | |
if ($this->runlevel < 0 || $this->runlevel > 3) { | |
$this->runlevel = 0; | |
} | |
if ($this->timeout < 60) { | |
$this->timeout = 7 * 24 * 3600; | |
} | |
if ($this->cachettl < 10) { | |
$this->cachettl = 10; | |
} | |
//! functions allowed in view expressions | |
if (!empty($this->allowed) && !is_array($this->allowed)) { | |
$this->allowed = explode(',', $this->allowed); | |
} | |
//! blacklisted domains | |
if (!empty($this->blacklist) && !is_array($this->blacklist)) { | |
$this->blacklist = explode(',', $this->blacklist); | |
} | |
//! disabled extensions | |
if (!is_array($this->disabled)) { | |
$this->disabled = explode(',', $this->disabled); | |
} | |
// @codeCoverageIgnoreEnd | |
//! patch php. has to be done *after* config loaded | |
ini_set('display_errors', $this->runlevel > 1 ? 1 : 0); | |
//! generated values, not configurable from config.php | |
$this->now = time(); | |
$this->error = []; | |
$this->sec = strtolower(getenv('HTTPS')) == 'on' ? 1 : 0; | |
//! set up some default values | |
self::$w = isset($_SERVER['REQUEST_METHOD']) ? 1 : 0; | |
$this->output = self::$w ? 'html' : 'ncurses'; | |
$this->try = 0; | |
//! calculate upload max file size | |
$c = self::toBytes('post_max_size'); | |
$d = self::toBytes('upload_max_filesize'); | |
$v = self::toBytes('memory_limit'); | |
if ($c > $d && $d) $c = $d; | |
$this->fm = ($c > $v && $v ? $v : $c); | |
//! construct base href | |
$c = $_SERVER['SCRIPT_NAME']; | |
//dirty hack required when run through phpunit | |
//as it does not call run(), and SCRIPT_FILENAME | |
//won't be public/index.php, | |
//but /usr/local/bin/phpunit.phar | |
if(strpos($c,"phpunit")!==false) $c=""; | |
$C = dirname($c); | |
//! eliminate ./ in some nginx configurations | |
// @codeCoverageIgnoreStart | |
if ($C == '.') { | |
$C = ''; | |
} elseif ($C != '/') { | |
$C .= '/'; | |
} | |
// @codeCoverageIgnoreEnd | |
$d = 'SERVER_NAME'; | |
$this->base = !empty($this->base) ? $this->base : | |
(!empty($_SERVER[$d]) ? $_SERVER[$d] : 'localhost'). | |
(@$C[0] != '/' ? '/' : '').$C; | |
//! fix slashes in request | |
if (get_magic_quotes_gpc()) { | |
// @codeCoverageIgnoreStart | |
foreach ($_REQUEST as $k => $v) { | |
if (is_string($v)) { | |
$_REQUEST[$k] = stripslashes($v); | |
} elseif (is_array($v)) { | |
foreach ($v as $K => $V) { | |
if (is_string($V)) { | |
$_REQUEST[$k][$K] = stripslashes($V); | |
} | |
} | |
} | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
//! get current requested url | |
list($d) = explode('?', @$_SERVER['REQUEST_URI']); | |
foreach ([$c, dirname($c)] as $C) { | |
if ($C != '/' && substr($d, 0, strlen($C)) == $C) { | |
//! cut leading directory | |
// @codeCoverageIgnoreStart | |
$d = substr($d, strlen($C)); | |
// @codeCoverageIgnoreEnd | |
break; | |
} | |
} | |
if (@$d[0] == '/') { | |
$d = substr($d, 1); | |
} | |
$D = explode('/', !empty($d) ? '/'.$d : '//'); | |
//! get current application, action and item. | |
//! these can be overriden by url route as well as route events | |
foreach ([1 => 'app', 2 => 'action', 3 => 'item'] as $c => $v) { | |
$this->$v = !empty($D[$c]) ? urldecode($D[$c]) : | |
(!empty($_REQUEST[$v]) ? trim($_REQUEST[$v]) : | |
(!self::$w && !empty($_SERVER['argv'][$c]) && | |
$_SERVER['argv'][$c] != '--dump' ? trim($_SERVER['argv'][$c]) : | |
($c < 3 ? ($c == 1 ? 'index' : 'action') : ''))); | |
} | |
if (empty($d)) { | |
$d = $this->app.'/'.$this->action.(!empty($this->item) ? '/'.$this->item : ''); | |
} | |
if (substr($d,-1)=='/') | |
$d=substr($d,0,strlen($d)-1); | |
$this->url = $d; | |
self::bm("getconfig"); | |
//! session restore may require models, so we have to | |
//! load all classes *before* session_start() | |
//! PHP Composer autoload support (if exists) | |
@include_once 'vendor/autoload.php'; | |
//! PHP ClassMap autoload | |
$this->libs['ClassMap'] = new ClassMap(); | |
//! register built-in modules (middleware classes) | |
$this->libs['DS'] = new DS($this->db); | |
$cls = '\\PHPPE\\User'; | |
//! this code is tricky. Core defines PHPPE\User, while Pack ships PHPPE\Users. | |
//! we'll use the later if found, and fallback to the former. | |
if (ClassMap::has($cls.'s')) { | |
$cls .= 's'; | |
} | |
$this->libs['Users'] = new $cls(); | |
$this->libs['Client'] = new Client(['tz'=>!empty($this->tz)?$this->tz: | |
(!empty($_SESSION['pe_u']->data['tz'])?$_SESSION['pe_u']->data['tz']:'')]); | |
$this->libs['Cache'] = new Cache($this->cache); | |
$this->libs['Assets'] = new Assets(); | |
$this->libs['Tools'] = new Tools(); | |
//! autoload extensions | |
$d = @glob('vendor/phppe/*', GLOB_NOSORT | GLOB_ONLYDIR); | |
foreach ($d as $f) { | |
//! save extension path | |
$c = basename($f); | |
self::$paths[strtolower($c)] = $f; | |
//! look for init code. This file should | |
//! 1. set routes if any | |
//! 2. return a service instance if any | |
//! an empty init.php will also load the extension | |
if (!in_array($c, $this->disabled) && | |
file_exists($f.'/init.php')) { | |
$cls = '\\PHPPE\\'.$c; | |
$o = include_once $f.'/init.php'; | |
if (!is_object($o) && $c != 'Core' && ClassMap::has($cls)) { | |
$o = new $cls(); | |
} | |
if (empty($this->libs[$c])) { | |
$this->libs[$c] = is_object($o) ? $o : new Extension(); | |
} | |
} | |
} | |
self::bm("autoload"); | |
//! check arguments | |
if (!self::$w && !$islib) { | |
// @codeCoverageIgnoreStart | |
if (in_array('--version', $_SERVER['argv'])) { | |
die(VERSION."\n"); | |
} | |
if (empty($_SERVER['argv'][1]) || in_array('--help', $_SERVER['argv'])) { | |
$c = chr(27)."[90mphp ".$_SERVER['argv'][0].chr(27)."[0m"; | |
echo(chr(27).'[96mPHP Portal Engine '.VERSION.", LGPL 2016 bzt".chr(27)."[0m\n $c --help\n $c --version\n $c --diag [--gid=x]\n $c --self-update\n $c [application [action [item]]] [--dump]\n $c passwd\n"); | |
foreach(ClassMap::$map as $C=>$v) | |
if(!empty($C::$cli)) | |
foreach(is_array($C::$cli)?$C::$cli:[$C::$cli] as $d) | |
echo(" $c $d\n"); | |
die("\n"); | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
//! detect bootstrap type | |
$s = @$_SERVER['argv'][1] == '--self-update'; | |
if (!self::$w && !$islib && ($s||@$_SERVER['argv'][1] == '--diag')) { | |
// @codeCoverageIgnoreStart | |
$this->bootdiag($s); | |
// @codeCoverageIgnoreEnd | |
} else { | |
//! normal bootsrap | |
//! Cache hit, not in debug and developer mode | |
$d = 'HTTP_IF_MODIFIED_SINCE'; | |
// @codeCoverageIgnoreStart | |
if ($this->runlevel < 2 && isset($_SERVER[$d]) && strtotime($_SERVER[$d]) + $this->cachettl < $this->now) { | |
self::bm("notmodified"); | |
header('HTTP/1.1 304 Not Modified'); | |
die; | |
} | |
// @codeCoverageIgnoreEnd | |
//! load autoloaded classes' dictionaries and initialize the extensions one by one | |
//! *** INIT Event *** | |
foreach ($this->libs as $k => $v) { | |
self::lang($k); | |
$c = @include_once self::$paths[strtolower($k)].'/config.php'; | |
if (method_exists($v, 'init') && $v->init(is_array($c) ? $c : []) === false) { | |
unset($this->libs[$k]); | |
} | |
/*! BENCHMARK START */ | |
//! if benchmark>0 also measure individual modules | |
if(!empty($_REQUEST['benchmark'])) self::bm("init.".$k); | |
/*! BENCHMARK END */ | |
} | |
//! load application dictionary overrides | |
self::lang('app'); | |
//! overall init | |
self::bm("init"); | |
//! if not included as a library, run application | |
if (!$islib) { | |
// @codeCoverageIgnoreStart | |
$this->run(); | |
// @codeCoverageIgnoreEnd | |
} | |
} | |
} | |
/** | |
* Run diagnostics and try to fix errors. | |
*/ | |
// @codeCoverageIgnoreStart | |
private function bootdiag($s) | |
{ | |
//! we'll need some information from the client | |
self::$client->init(); | |
ini_set('display_errors', 1); | |
//! extensions checks and webserver group id | |
if (!empty($_SERVER['argv'][2]) && z($_SERVER['argv'][2], 0, 6) == '--gid=') { | |
$g['g'] = intval(substr($_SERVER['argv'][2], 6)); | |
} elseif (function_exists('posix_getpwnam')) { | |
foreach (['www', '_www', 'www-data', 'http', 'httpd', 'apache', 'nginx'] as $n) { | |
$g = posix_getpwnam($n); | |
if (!empty($g['gid'])) { | |
$g['g'] = $g['gid']; | |
break; | |
} | |
} | |
} | |
//! fallback to 33 if not found or configured | |
if (empty($g['g'])) { | |
self::$g = 33; | |
} else { | |
self::$g = $g['g']; | |
} | |
//! output UID and GID | |
$U = fileowner(__FILE__); | |
if ($this->runlevel) { | |
echo "DIAG-I: uid $U gid ".self::$g."\n"; | |
} | |
$R = 'http://bztsrc.github.io/phppe3/'; | |
//! if called with --self-update | |
if(!empty($s)){ | |
//! update the core | |
$C=file_get_contents("https://raw.githubusercontent.com/bztsrc/phppe3/3.0/public/index.php"); | |
if(!empty($C)) { | |
$c="public/index.php"; | |
echo "DIAG-U: $c\n"; | |
file_put_contents($c,$C); | |
} | |
//! update base extensions | |
@mkdir(".tmp"); | |
$c=".tmp/archive"; | |
foreach(["Core","Extensions"] as $r) { | |
self::$w=$r; | |
$C=file_get_contents($R."phppe3_".strtolower($r).".tgz"); | |
if(!empty($C)){ | |
echo "DIAG-U: vendor/phppe/$r\n"; | |
file_put_contents($c,$C); | |
Tools::untar($c,function($n,$b){ | |
if(substr($n,-1)!="/") { | |
$d="vendor/phppe/".\PHPPE\Core::$w."/".$n; | |
@mkdir(dirname($d), 0770, true); | |
if(substr($d,-10)!="config.php"||!file_exists($d)) | |
file_put_contents($d,$b); | |
} | |
}); | |
@unlink($c); | |
} | |
} | |
//! create self reference in remote config | |
$c="vendor/phppe/Extensions/config.php"; | |
if(!file_exists($c)) { | |
echo "DIAG-U: $c\n"; | |
$r=getenv("HOME")."/.ssh/id_rsa"; | |
if(!file_exists($r)) { | |
system("ssh-keygen -t rsa -f ".escapeshellarg($r)); | |
$d=@file_get_contents($r.".pub"); | |
@file_put_contents(dirname($r)."/authorized_keys", | |
$d, FILE_APPEND); | |
$d=@explode(" ",$d); | |
@file_put_contents(dirname($r)."/known_hosts", | |
"127.0.0.1 ".$d[0]." ".$d[1]."\n", FILE_APPEND); | |
} | |
file_put_contents($c,"<"."?ph"."p return [\n". | |
"\t\"host\"=>\"localhost\",\n\t\"port\"=>22,\n". | |
"\t\"user\"=>\"".getenv("USER")."\",\n". | |
"\t\"path\"=>\"".dirname(__DIR__)."\",\n". | |
"\t\"identity\"=>\"".@file_get_contents($r). | |
"\"\n];\n"); | |
} | |
} | |
//! helper function to create files | |
function i($c, $r, $f = 0, $a = 0640) | |
{ | |
//! if not exists yet or creation is forced | |
if (!file_exists($c) || $f) { | |
echo "DIAG-A: $c\n"; | |
file_put_contents($c, $r, LOCK_EX); | |
} | |
//! change owner and group | |
if (file_exists($c) && (!@chgrp($c, \PHPPE\Core::$g) || !@chown($c, fileowner(__FILE__)))) { | |
echo "DIAG-E: chown/chgrp $c\n"; | |
} | |
//! change access rights | |
return !@chmod($c, $a); | |
} | |
//! fix missing files and access rights | |
$E = ''; | |
$C = 0750; | |
$W = 0775; | |
if (function_exists('posix_getuid') && posix_getuid() != 0) { | |
echo "DIAG-W: not root or no php-posix, chown/chgrp may fail!\n"; | |
} | |
//! create directory structure | |
$o = umask(0); | |
//! directory skeleton | |
$D = ['.tmp' => $W, | |
'data' => $W, | |
'data/log' => $W, | |
'app' => 0, | |
'vendor' => 0, | |
'vendor/bin' => 0, | |
'vendor/phppe' => 0, | |
'vendor/phppe/Core' => 0, | |
'vendor/phppe/Core/views' => 0, | |
'public/fonts' => 0, | |
'public/images' => 0, | |
'public/css' => 0, | |
'public/js' => 0, | |
'app/addons' => 0, | |
'app/sql' => 0, | |
'app/ctrl' => 0, | |
'app/lang' => 0, | |
'app/libs' => 0, | |
'app/views' => 0,]; | |
$A = ['*', '*/*', '*/*/*', '*/*/*/*', '*/*/*/*/*']; | |
foreach (['data/', 'vendor/'] as $d) { | |
foreach ($A as $v) { | |
$D += array_fill_keys(@glob($d.$v,GLOB_NOSORT), 0); | |
} | |
} | |
// set .tmp access rights | |
foreach ($A as $v) { | |
$D += array_fill_keys(@glob(".tmp/".$v,GLOB_NOSORT), $W); | |
} | |
//! $D now has all installed files | |
foreach ($D as $d => $p) { | |
if (!$p) { | |
$p = $C; | |
} | |
//! exceptions, dirs that needs to be writeable | |
$x = in_array(substr($d, 0, 4), ['.tmp', 'data']); | |
if (is_file($d)) { | |
$P = fileperms($d) & 0777; | |
$p = $x ? ($d==ClassMap::$file ? 0664 : 0660) : (substr($d,0,10)=="vendor/bin"?$C:0640); | |
if(substr($d,0,6)=="vendor" && basename(dirname($d))=="bin"){ | |
$p = $C; $c=substr($d,7); $e="vendor/bin/".basename($d); | |
if (!file_exists($e)) { | |
symlink("../$c", $e); | |
echo "DIAG-A: symlink $e -> $c\n"; | |
} | |
} | |
} else { | |
if ($x) { | |
$p = $W; | |
} | |
if (!is_dir($d) && !is_file($d)) { | |
echo "DIAG-A: $d\n"; | |
if (!mkdir($d, $p)) { | |
self::log('C', "creating $d", 'diag'); | |
} | |
} | |
$P = fileperms($d) & 0777; | |
} | |
//! if detected and calculated access rights differ | |
if ($P != $p) { | |
$E .= sprintf("\t%03o?\t%03o ", $P, $p)."$d\n"; | |
@chmod($d, $p); | |
} | |
if (!@chgrp($d, self::$g) || !@chown($d, $U)) { | |
echo "DIAG-E: chown/chgrp $d\n"; | |
} | |
} | |
//! hide errors here, symlink may be already there | |
$c = 'vendor/phppe/app'; | |
@symlink('../../app', $c); | |
if (!@chgrp($c, self::$g) || !@chown($c, $U)) { | |
echo "DIAG-E: chown/chgrp $c\n"; | |
} | |
foreach (['images', 'css', 'js', 'fonts'] as $v) { | |
$c = "app/$v"; | |
if (!file_exists($c)) { | |
echo "DIAG-A: $c\n"; | |
} | |
@symlink("../public/$v", $c); | |
if (!@chgrp($c, self::$g) || !@chown($c, $U)) { | |
echo "DIAG-E: chown/chgrp $c\n"; | |
} | |
} | |
//! hide errors here, target may not exists or the symlink may be already there | |
$c = 'phppe'; | |
@symlink('vendor/phppe/Core', $c); | |
if (!@chgrp($c, self::$g) || !@chown($c, $U)) { | |
echo "DIAG-E: chown/chgrp $c\n"; | |
} | |
//! create files | |
umask(0027); | |
i('app/config.php', ''); | |
i('app/init.php', '<'."?php\n//! set your routes here (if any)\n//\\PHPPE\\Http::route('myurl','myClass','myMethod');\n\n//! return service instance (if any)\n//return new myService;\n"); | |
i('public/.htaccess', "RewriteEngine On\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteRule ^(.*)\$ index.php/\$1\n"); | |
i('public/favicon.ico', ''); | |
$D = 'vendor/phppe/Core/views/'; | |
$e = '.tpl'; | |
$c = "<!dump core.req2arr('obj')>"; | |
i($D."403$e", "<h1>403</h1><!L Access denied>\n<!-- <!L hacker> -->"); | |
i($D."404$e", "<h1>404</h1><!L Not found>: <b><!=core.url></b>"); | |
i($D."frame$e", "<div id='content'><!app></div>"); | |
i($D."index$e", "<h1>PHPPE works!</h1>Next step: <samp>php public/".basename(__FILE__)." --self-update</samp><br/><br/><!if core.isBtn()><div style='display:none;'>$c</div><!/if><div style='background:#F0F0F0;padding:3px;'><b>Test form</b></div><!form obj>Text<!field text obj.f0 - - - Example [a-z0-9]+> Pass<!field pass obj.f1> Num(100..999)<!field *num(100,999) obj.f2> Phone<!field phone obj.f3><!field check obj.f4 Check> File<!field file obj.f5> <!field submit></form><table width='100%'><tr><td valign='top' width='50%'><!dump _REQUEST><!dump _FILES></td><td> </td><td valign='top'>$c</td></tr></table>\n"); | |
i($D."login$e", "<!form login><div style='color:red;'><!foreach core.error()><!foreach VALUE><!=VALUE><br/><!/foreach><!/foreach></div><!field text id - - - Username><!field pass pass - Password><!field submit></form>"); | |
i($D."maintenance$e", "<h1><!L Site is temporarily down for maintenance></h1>"); | |
i($D."errorbox$e", "<!if core.isError()><div class='alert alert-danger'><!foreach core.error()><!foreach VALUE> <!=VALUE><br/><!/foreach><!/foreach></div><!/if>"); | |
i('composer.json', "{\n\t\"name\":\"phppe3\",\n\t\"version\":\"1.0.0\",\n\t\"keywords\":[\"phppe3\",\"\"],\n\t\"license\":[\"LGPL-3.0-or-later\"],\n\n\t\"type\":\"project\",\n\t\"repositories\":[\n\t\t{\"type\":\"composer\",\"url\":\"$R\"}\n\t],\n\t\"require\":{\"phppe/Core\":\"3.*\"},\n\n\t\"scripts\":{\"post-update-cmd\":\"sudo php public/index.php --diag\"}\n}\n"); | |
i('.gitignore', ".tmp\nphppe\nvendor\n"); | |
if ($E) { | |
self::log('E', "Wrong permissions:\n$E", 'diag'); | |
} | |
//! *** DIAG Event *** | |
if (!function_exists('posix_getuid') || posix_getuid() != 0) { | |
self::event('diag'); | |
} | |
umask($o); | |
die("DIAG-I: OK\n"); | |
} | |
/** | |
* execute a PHPPE application. | |
* | |
* @param string application name, if not specified, url routing will choose | |
* @param string action name, if not specified, default action routing will apply | |
*/ | |
public function run($app = '', $ac = '') | |
{ | |
//! rotate security tokens for form validation, save form name | |
$c = sha1(url()); | |
$S = !empty($_SESSION['pe_s'][$c]) ? $_SESSION['pe_s'][$c] : ''; | |
for ($i = 1; $i <= 9; ++$i) { | |
if (isset($_REQUEST['pe_try'.$i]) && !empty($_REQUEST['pe_s']) && $_REQUEST['pe_s'] == $S) { | |
$this->try = $i; | |
$this->form = !empty($_REQUEST['pe_f']) ? trim($_REQUEST['pe_f']) : ''; | |
$_SESSION['pe_s'][$c] = 0; | |
break; | |
} | |
} | |
if (empty($_SESSION['pe_s'][$c])) { | |
$_SESSION['pe_s'][$c] = sha1(uniqid().'PHPPE'.VERSION); | |
} | |
//! get validators from previous view generation | |
if (!empty($_SESSION['pe_v'])) { | |
self::$v = $_SESSION['pe_v']; | |
} | |
self::bm("tokens"); | |
//! initialize view layer | |
View::init(); | |
self::bm("viewinit"); | |
if (empty($this->maintenance)) { | |
//! get application and action | |
list($app, $ac, $args) = HTTP::urlMatch($app, $ac, $this->url); | |
//! *** ROUTE Event *** | |
list($app, $ac) = self::event('route', [$app, $ac]); | |
//! a few basic security checks | |
$c = $app.'_'.$ac; | |
if (strpos($c, '..') !== false || strpos($c, '/') !== false || | |
substr($app, -4) == '.php' || substr($ac, -4) == '.php') { | |
$app = $this->template = '403'; | |
} | |
//! default template, it's empty on CLI | |
else { | |
$this->template = self::$w ? $app.'_'.$ac : ''; | |
} | |
//! canonize application's class' name | |
$appCls = $app; | |
foreach (['PHPPE\\Ctrl\\'.$app.ucfirst($ac), | |
'PHPPE\\Ctrl\\'.$app, | |
'PHPPE\\'.$app, | |
$app,] as $a) { | |
if (ClassMap::has($a)) { | |
$appCls = $a; | |
if($a[0]=="\\") $a=substr($a,1); | |
//! add it's path | |
$p = dirname(ClassMap::$map[strtolower($a)]); | |
if(basename($p)=="ctrl"||basename($p)=="libs") | |
$p=dirname($p); | |
self::$paths[strtolower($app)]=$p; | |
break; | |
} | |
} | |
//! if still no application found | |
if (!ClassMap::has($appCls)) { | |
//! for CLI check if it's a cron job, or fail | |
if (!self::$w) { | |
if (@in_array('--dump', $_SERVER['argv']) && $this->runlevel > 0) { | |
View::dump(); | |
} | |
//! *** CRON Event *** | |
die($this->app == 'cron' ? | |
self::event('cron'.ucfirst($this->action), 0) : | |
'PHPPE-C: '.L($this->app.'_'.$this->action.' not found!')."\n"); | |
} | |
//! for CGI fallback to Content Server | |
$appCls = 'PHPPE\\Content'; | |
$ac = 'action'; | |
} | |
self::bm("routing"); | |
//! instantiate application | |
$c="vendor/phppe/".$app."/config.php"; | |
if(!file_exists($c)) $c="app/config.php"; | |
if(file_exists($c)) $c=@include($c); | |
$appObj = new $appCls(is_array($c)?$c:[]); | |
View::setPath(self::$paths[strtolower($app)]); | |
View::assign('app', $appObj); | |
//! Application constructor may altered template, so we have to log this after "new App" | |
self::log('D', $this->url." ->$app::$ac ".$this->template, 'routes'); | |
//! Check if page found in cache | |
//! note that controller constructor may have turned cache off | |
//! so Cache::get() will return a null | |
$N = 'p_'.sha1($this->base.$this->url.'/'.self::$user->id.'/'.self::$client->lang); | |
if (empty($this->nocache) && !self::isBtn()) { | |
$T = View::fromCache($N); | |
self::bm("cache"); | |
} | |
if (empty($T)) { | |
self::bm("controller"); | |
//! get frame meta data | |
Content::getDDS($appObj); | |
//! *** CTRL Event (Controller action) *** | |
self::event('ctrl', [$app, $ac]); | |
//! call action method | |
self::bm("action"); | |
if (!method_exists($appObj, $ac)) { | |
$ac = 'action'; | |
} | |
if (method_exists($appObj, $ac)) { | |
if(empty($args)) $args=array_slice(explode("/",$this->url),2); | |
if(empty($args)) $args=[$this->item]; | |
call_user_func_array([$appObj, $ac], $args); | |
} | |
self::bm("template"); | |
$T = View::generate($this->template, $N); | |
} | |
} else { | |
session_destroy(); | |
//! site is down message | |
$T = View::template('maintenance'); | |
//! if no template found | |
if (empty($T)) { | |
$T = L('Site is temporarily down for maintenance'); | |
} | |
} | |
//! check dump argument here, by now all core properties are populated | |
if ((@in_array('--dump', $_SERVER['argv']) || isset($_REQUEST['--dump'])) && $this->runlevel > 0) { | |
View::dump(); | |
} | |
//! close all database connections before output | |
DS::close(); | |
self::bm("view"); | |
//! *** VIEW Event *** | |
$T = self::event('view', $T); | |
View::output($T); | |
//! make sure to flush session | |
session_write_close(); | |
/*! BENCHMARK START */ | |
if(isset($_REQUEST['benchmark'])) | |
@file_put_contents(".tmp/benchmarks",json_encode([url()." ".sha1(implode(",",array_keys(Core::$bm)))=>Core::$bm])."\n",FILE_APPEND | LOCK_EX); | |
/*! BENCHMARK END */ | |
} | |
// @codeCoverageIgnoreEnd | |
/*** Core library ***/ | |
/** | |
* List all registered libraries (services). | |
* @usage lib() | |
* @return array array of library instances | |
* | |
* Query a library instance | |
* @usage lib(n) | |
* @param string name | |
* @return object library instance or null | |
* | |
* Define a new library (service) | |
* @usage lib(n,o,d) | |
* @param string name | |
* @param string/object object instance or class name | |
* @param string/array dependency, optional | |
*/ | |
public static function lib($n = '', $o = null, $d = '') | |
{ | |
$L = &self::$core->libs; | |
if (empty($o)) { | |
//! return list of lib or a specific service instance | |
return empty($n) ? $L : (empty($L[$n]) ? null : $L[$n]); | |
} else { | |
//! check dependencies | |
$f = ''; | |
if ($d) { | |
if (!is_array($d)) { | |
$d = str_getcsv($d, ','); | |
} | |
foreach ($d as $v) { | |
if (!self::isInst($v)) { | |
$f .= ($f ? ',' : '').$v; | |
} | |
} | |
if ($f) { | |
throw new \Exception("$n ".L("depends on").": $f"); | |
} | |
} | |
//! add to list | |
if (empty(self::$core->libs[$n])) { | |
self::$core->libs[$n] = is_object($o) ? $o : (class_exists($o) ? new $o() : new Extension()); | |
} | |
} | |
} | |
/** | |
* Checks if an extension or an Add-On is installed or not. | |
* | |
* @param string name | |
* | |
* @return bool true or false | |
*/ | |
public static function isInst($n) | |
{ | |
//! check for installed library or.. | |
return isset(self::$core->libs[$n]) || ClassMap::has('\\PHPPE\\'.$n) || | |
//! ...available addon... | |
ClassMap::has('\\PHPPE\\AddOn\\'.$n) || | |
//! ...or any other JS only extension | |
ClassMap::has($n) || is_dir('vendor/phppe/'.$n); | |
} | |
/** | |
* Load a new language dictionary into memory. | |
* | |
* @param string class name (extension name) | |
*/ | |
public static function lang($c = '') | |
{ | |
//! failsafe | |
$L = empty(self::$client->lang) ? 'en' : self::$client->lang; | |
//! expand language dictionary | |
$i = explode('_', $L); | |
//! get translations | |
$la = null; | |
//! workaround if symlink does not exists | |
$c = $c=="app"? "$c/lang/" : "vendor/phppe/$c/lang/"; | |
//! first check as is, then first part, finally English | |
//! eg.: hu_HU, hu, en; en_US, en | |
foreach (array_unique([$L, $i[0], 'en']) as $l) { | |
if (file_exists($c.$l.'.php')) { | |
$la = include_once $c.$l.'.php'; | |
break; | |
} | |
} | |
//! merge into dictionary | |
if (is_array($la)) { | |
self::$l = array_merge(self::$l, $la); | |
} | |
} | |
/** | |
* Log a message. | |
* | |
* @param string weight, Debug | Info | Audit | Warning | Error | Critical | |
* @param string message in English (don't translate it for compatibility) | |
* @param string extension name, guessed if not given | |
*/ | |
public static function log($w, $m, $n = null) | |
{ | |
//! log a message of weight for a module | |
$w = strtoupper($w); | |
if (!in_array($w, ['D', 'I', 'A', 'W', 'E', 'C'])) { | |
$w = 'A'; | |
} | |
if (empty($n)) { | |
$n = !empty(self::$core->app) ? self::$core->app : 'core'; | |
} | |
if (!is_string($m) || self::$core->runlevel < 3 && $w == 'D') { | |
return; | |
} | |
$n = str_replace("--", "", $n); | |
//! remove sensitive information from message | |
$m = trim(strtr($m, [dirname(dirname(__FILE__)).'/' => ''])); | |
$g = !empty(self::$l[$m]) ? L($m) : $m; | |
//! debug trace | |
$t = ''; | |
if (!empty(self::$core->trace)) { | |
$s = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); | |
foreach ($s as $d) { | |
$t .= "\t".substr(@$d['file'], strlen(dirname(__DIR__)) + 1).':'. | |
@$d['line'].':'.$d['function']."\n"; | |
} | |
} | |
//! log always stores dates in UTC to be comparable among servers | |
date_default_timezone_set('UTC'); | |
$p = date('Y-m-d').'T'.date('H:i:s')."Z-$w-".strtoupper($n).': '; | |
//! restore timezone | |
@date_default_timezone_set(self::$client->tz); | |
//! send message syslog | |
if (!empty(self::$core->syslog)) { | |
syslog($w == 'C' ? LOG_ERR : LOG_NOTICE, self::$core->base.":PHPPE-$w-".strtoupper($n).': ' | |
.strtr($m, ["\n" => '\\n', "\r" => '\\r']).strtr($t, ["\n" => ''])); | |
} else { | |
//! save message to file | |
$l = dirname(__DIR__).'/data/log/'.$n.'.log'; | |
if (!@file_put_contents($l, $p. | |
strtr($m, ["\n" => '\\n', "\r" => '\\r'])."\n".$t, FILE_APPEND | LOCK_EX)) { | |
// @codeCoverageIgnoreStart | |
$g .= (!self::$w ? "\nLOG-C" : "<br/>\n".date('Y-m-d').'T'.date('H:i:s').'Z-C-LOG').': '.L('unable to write')." $l"; | |
// @codeCoverageIgnoreEnd | |
} | |
@chmod($l, 0660); | |
//! on critical message, bail out | |
} | |
if ($w == 'C') { | |
// @codeCoverageIgnoreStart | |
if (@self::$core->output != 'html') { | |
die(strtoupper("$n-C").': '.$g."\n".$t); | |
} | |
die("\n<html><body style='margin:8px;background:#000000;color:#A00000;'><div style='text-align:center;font-size:28px;color:#ff0000;'>PHPPE".VERSION.' - '.L('Developer Console')."</div><br/><br/>\n$p".nl2br(strtr("$g\n$t", ["\t" => ' ']))."</body></html>\n"); | |
// @codeCoverageIgnoreEnd | |
} elseif (!self::$w && $w != 'D' && $w != 'I') { | |
fwrite(STDERR, strtoupper("$n-$w").': '.$g."\n"); | |
} | |
return true; | |
} | |
/** | |
* Return button number when user tries to save a form. | |
* | |
* @param string form name | |
* | |
* @return integer button number | |
*/ | |
public static function isBtn($f = '') | |
{ | |
//! we just return the previously calculated value here | |
//! this speeds up things | |
return empty($f) || $f == self::$core->form ? self::$core->try : 0; | |
} | |
/** | |
* Query all error messages. | |
* @usage Core::error() | |
* @return array array of messages groupped by fields | |
* | |
* Add an error message to output | |
* @param string message | |
* @param string if message is related to a field, it's name | |
*/ | |
public static function error($m = '', $f = '') | |
{ | |
if (empty($m)) { | |
return self::$core->error; | |
} | |
//! register an error message | |
if (!isset(self::$core->error[$f])) { | |
self::$core->error[$f] = []; | |
} | |
self::$core->error[$f][trim($m)] = trim($m); | |
//log validation error in developer and debug mode | |
// if(self::$core->runlevel > 1) | |
// self::log("E", $f . "@" . $_SERVER['REQUEST_URI'] . " " . $m, "validate"); | |
} | |
/** | |
* Check for errors. | |
* | |
* @param string if interested in errors for a specific field, it's name | |
* | |
* @return boolean true if there's an error | |
*/ | |
public static function isError($f = '') | |
{ | |
//! check for error | |
return !empty($f) ? isset(self::$core->error[$f]) : !empty(self::$core->error); | |
} | |
/** | |
* Trigger an event. | |
* | |
* @param string event name | |
* @param array context | |
* | |
* @return array modified context or null if not modified | |
*/ | |
public static function event($e, $c = []) | |
{ | |
foreach (self::$core->libs as $k => $v) { | |
if (method_exists($v, $e)) { | |
$d = call_user_func_array([$v, $e], is_array($c) ? $c : [$c]); | |
if (!empty($d) && is_array($d)) { | |
$c = $d; | |
} | |
} | |
} | |
return $c; | |
} | |
/*** Data layer ***/ | |
/** | |
* Add a validator on a field value. | |
* | |
* @usage call it *BEFORE* req2obj or req2arr | |
* | |
* @param string field name | |
* @param string validator name (will use \PHPPE\AddOn\(validator)::validate) | |
* @param boolean is value required | |
* @param array arguments | |
* @param array attributes | |
*/ | |
public static function validate($f, $v, $r = 0, $a = [], $t = []) | |
{ | |
if (ClassMap::has('\\PHPPE\\Addon\\'.$v, 'validate')) { | |
self::$v[$f][$v] = [!empty($r), $a, $t]; | |
} | |
} | |
/** | |
* Render user request to object. Validates user input and returns an stdClass. | |
* | |
* @param string form prefix (request name) | |
* @param array validator data (if given, overrides templater's validator list) | |
* | |
* @return stdClass form fields | |
*/ | |
public static function req2obj($p, $V = []) | |
{ | |
return self::req2arr($p, $V, 0); | |
} | |
/** | |
* Render user request to array. Validates user input and returns an array. | |
* | |
* @param string form prefix (request name) | |
* @param array validator data (if given, overrides templater's validator list) | |
* | |
* @return array form fields | |
*/ | |
public static function req2arr($p, $V = [], $a = 1) | |
{ | |
//! output format | |
if ($a) { | |
$o = array(); | |
} else { | |
$o = new \stdClass(); | |
} | |
//! php dropping a warning here is an indicator of hack | |
if (!empty(self::$v)) { | |
$V += self::$v; | |
} | |
$R = $_REQUEST; | |
//! patch missing elements | |
foreach ($V as $K => $v) { | |
if (substr($K, 0, strlen($p) + 1) == $p.'.') { | |
$r = $p.'_'.substr($K, strlen($p) + 1); | |
if(empty($R[$r])) | |
$R[$r]=''; | |
} | |
} | |
// ksort($R); | |
//! iterate through form elements with validation | |
foreach ($R as $k => $v) { | |
if (substr($k, 0, strlen($p) + 1) == $p.'_' && $k[strlen($k) - 2] != ':') { | |
$d = substr($k, strlen($p) + 1); | |
$K = $p.'.'.$d; | |
if (isset($V[$K])) { | |
//iterate on validators for this key | |
foreach ($V[$K] as $T => $C) { | |
$t = '\\PHPPE\\AddOn\\'.$T; | |
if ((!empty($v)||$T=="check"||$T=="file") && ClassMap::has($t, 'validate')) { | |
list($r, $m) = $t::validate($K, $v, $C[1], $C[2]); | |
if (!$r && $m) { | |
$O = explode('.', $K); | |
//field name and translated error message for user | |
self::error(L(ucfirst(!empty($O[1]) ? $O[1] : $O[0])).' '.L($m), $K); | |
} | |
} | |
if (!empty($C[0]) && empty($v)) { | |
$v = null; | |
self::error(L(ucfirst($d)).' '.L('is a required field.'), $K); | |
} | |
} | |
} | |
$v = $v == 'true' ? true : ($v == 'false' ? false : $v); | |
if ($a) { | |
$o[$d] = $v; | |
} else { | |
$o->$d = $v; | |
} | |
} | |
} | |
return $o; | |
} | |
/** | |
* Convert a an object (associative array or stdClass) to string attributes. | |
* | |
* @param object object | |
* @param string/array skip list (array or comma spearated values in string) | |
* @param string separator (defaults to space) | |
* | |
* @return string xml attributes or sql query expression | |
*/ | |
public static function obj2str($o, $s = '', $c = ' ') | |
{ | |
return self::arr2str($o, $s, $c); | |
} | |
public static function arr2str($o, $s = '', $c = ' ') | |
{ | |
//! get skip list | |
if (!is_array($s)) { | |
$s = str_getcsv($s, ','); | |
} | |
//! iterate on fields | |
$r = ''; | |
$d = DS::db(); | |
if (is_string($o)) { | |
$o = [$o]; | |
} | |
foreach ($o as $k => $v) { | |
if (!in_array($k, $s)) { | |
$r .= ($r ? $c : '').$k.'='. | |
($c == ',' && !empty($d) ? $d->quote($v) : | |
"'".str_replace(["\r", "\n", "\t", "\x1a"], ['\\r', '\\n', '\\t', '\\x1a'], addslashes($v))."'"); | |
} | |
} | |
return $r; | |
} | |
/** | |
* Convert a templater expression into an array by splitting at separator. | |
* | |
* @param string input string | |
* @param string separator (defaults to comma) | |
* | |
* @return array parts | |
*/ | |
public static function val2arr($s, $c = ',') | |
{ | |
//! if input is already an array | |
if (is_array($s)) { | |
return $s; | |
} elseif ($s != '') { | |
//! get value of variable | |
$v = View::getval($s); | |
//! if returned value is already an array | |
if (is_array($v)) { | |
return $v; | |
} | |
//! if not, explode string | |
return str_getcsv($v, $c); | |
} | |
return []; | |
} | |
/** | |
* Flat a recursive array (sub-levels in "_") into simple one level array | |
* this is useful if you want to use an option list on trees. | |
* | |
* @param array input tree array | |
* @param string prefix to use in names | |
* @param string suffix to use in names (if given, prefix and suffix only appended on nesting) | |
* | |
* @return array flat array | |
*/ | |
public static function tre2arr($t, $p = ' ', $s = '', $P = '', $S = '', &$d = null) | |
{ | |
//! iterate through array | |
foreach ($t as $v) { | |
//! get output size | |
$i = @count($d); | |
//! look for sub arrays | |
foreach ($v as $k => $w) { | |
if ($k != '_') { | |
$c = strtolower($k) == 'name' && !$s ? $P.$w.$S : $w; | |
if (is_array($v)) { | |
$d[$i][$k] = $c; | |
} elseif (is_object($v)) { | |
@$d[$i]->$k = $c; | |
} | |
} | |
} | |
//! get child items | |
if (is_array($v) && !empty($v['_']) || is_object($v) && !empty($v->_)) { | |
//! prefix | |
if ($s) { | |
$c = "\n".sprintf($p, $i); | |
if (is_array($d[$i]) && isset($d[$i]['name'])) { | |
$d[$i]['name'] .= $c; | |
} elseif (is_object($d[$i]) && isset($d[$i]->name)) { | |
$d[$i]->name .= $c; | |
} | |
} | |
//! recursive call to walk through children elements too | |
$d = self::tre2arr(is_array($v) ? $v['_'] : $v->_, $p, $s, $P.($s ? '' : $p), ($s ? '' : $s).$S, $d); | |
//! suffix | |
if ($s) { | |
$c = "\n".$s; | |
if (is_array($d[count($d) - 1]) && isset($d[count($d) - 1]['name'])) { | |
$d[count($d) - 1]['name'] .= $c; | |
} elseif (is_object($d[count($d) - 1]) && isset($d[count($d) - 1]->name)) { | |
$d[count($d) - 1]->name .= $c; | |
} | |
} | |
} | |
} | |
return $d; | |
} | |
/** | |
* Create a benchmark point | |
* | |
* @param string name | |
*/ | |
public static function bm($name) | |
{ | |
/*! BENCHMARK START */ | |
$d=microtime(1)-self::$started; | |
setlocale(LC_NUMERIC,"C"); | |
self::$bm[$name]=[sprintf("%.6f",$d-end(self::$bm)[1]),sprintf("%.6f",$d)]; | |
/*! BENCHMARK END */ | |
} | |
/*** private helper functions for Core classes ***/ | |
/** | |
* Return constructor started time | |
* | |
* @return float timestamp | |
*/ | |
public static function started() | |
{ | |
return self::$started; | |
} | |
/** | |
* Check url against given filters. | |
* | |
* @param string/array filters or @ACEs | |
* | |
* @return boolean true if access granted | |
*/ | |
public static function cf($c) | |
{ | |
//! check filters | |
if (!is_array($c)) { | |
$c = explode(',', $c); | |
} | |
if (!empty($c)) { | |
foreach ($c as $F) { | |
$F = trim($F); | |
$G = "PHPPE\\Filter\\$F"; | |
//! for each filter do... | |
if (!empty($F) && | |
//! is starts with a '@', it's an ACE | |
(($F[0] == '@' && !self::$user->has(substr($F, 1))) || | |
//! otherwise run filter method | |
($F[0] != '@' && !@$G::filter()))) { | |
return false; | |
} | |
} | |
} | |
return true; | |
} | |
/** | |
* Convert human readble php ini value to bytes. | |
* | |
* @param string php ini variable name (value of units) | |
* | |
* @return integer in bytes | |
*/ | |
private static function toBytes($i) | |
{ | |
$v = trim(ini_get($i)); | |
$l = strtolower($v[strlen($v) - 1]); | |
$v = intval($v); | |
switch ($l) { | |
case 't' : $v *= 1024; | |
case 'g' : $v *= 1024; | |
case 'm' : $v *= 1024; | |
case 'k' : $v *= 1024; | |
} | |
return $v; | |
} | |
}//class | |
/******* Bootstrap PHPPE *******/ | |
if (empty(\PHPPE\Core::$core)) { | |
new \PHPPE\Core(!empty(debug_backtrace())); | |
} | |
return \PHPPE\Core::$core; | |
}//namespace | |
/*** Alternative cache implementations ***/ | |
namespace PHPPE\Cache { | |
//! PHP APC support | |
class APC | |
{ | |
/** | |
* Constructor. | |
* | |
* @param string url (constant "apc") | |
*/ | |
public function __construct($c = '') | |
{ | |
ini_set('apc.enabled', 1); | |
if (!function_exists('apc_fetch') && !function_exists('apcu_fetch')) { | |
// @codeCoverageIgnoreStart | |
\PHPPE\Core::log('C', L("no php-apc"), 'cache'); | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
public function get($key) | |
{ | |
// @codeCoverageIgnoreStart | |
if (function_exists('apc_fetch')) { | |
$e = 'apc_exists'; | |
$f = 'apc_fetch'; | |
} else { | |
$e = 'apcu_exists'; | |
$f = 'apcu_fetch'; | |
} | |
// @codeCoverageIgnoreEnd | |
$v = $e($key) ? $f($key) : null; | |
if (function_exists('gzinflate')) { | |
$d = json_decode(@gzinflate($v)); | |
} | |
return !empty($d) ? $d : $v; | |
} | |
public function set($key, $value, $compress = false, $ttl = 0) | |
{ | |
// @codeCoverageIgnoreStart | |
if (function_exists('apc_store')) { | |
$s = 'apc_store'; | |
} else { | |
$s = 'apcu_store'; | |
} | |
// @codeCoverageIgnoreEnd | |
return $s($key, $compress && function_exists('gzdeflate') ? gzdeflate(json_encode($value)) : $value, $ttl); | |
} | |
public function invalidate() | |
{ | |
// @codeCoverageIgnoreStart | |
if (function_exists('apcu_clear_cache')) { | |
apcu_clear_cache(); | |
} else if (function_exists('apc_clear_cache')) { | |
apc_clear_cache("user"); | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
} | |
//! Plain file cache support | |
class Files | |
{ | |
static $cli="cron minute"; | |
/** | |
* Constructor. | |
* | |
* @param string url (constant "files") | |
*/ | |
public function __construct($c = '') | |
{ | |
@mkdir('.tmp/cache', 0770); | |
@chmod('.tmp/cache', 0775); | |
} | |
private function fn($key) | |
{ | |
return '.tmp/cache/'.substr($key, 0, 2).'/'.substr($key, 2, 2).'/'.substr($key, 4); | |
} | |
public function get($key) | |
{ | |
if (strlen($key) < 5) { | |
$key = '0000'.$key; | |
} | |
$ttl = intval(@file_get_contents($this->fn($key).'.ttl')); | |
if (!file_exists($this->fn($key)) || ($ttl > 0 && time() - filemtime($this->fn($key)) > $ttl)) { | |
return; | |
} | |
$v = @file_get_contents($this->fn($key)); | |
if (function_exists('gzinflate')) { | |
$d = @gzinflate($v); | |
} | |
return json_decode(!empty($d) ? $d : $v, true); | |
} | |
public function set($key, $value, $compress = false, $ttl = 0) | |
{ | |
if (strlen($key) < 5) { | |
$key = '0000'.$key; | |
} | |
//! unfortunately there's a bug in PHP 7, mkdir(fn(),0755,true) | |
//! won't set the right mode for all directories along the path | |
@mkdir('.tmp/cache/'.substr($key, 0, 2), 0775); | |
@chmod('.tmp/cache/'.substr($key, 0, 2), 0775); | |
@mkdir('.tmp/cache/'.substr($key, 0, 2).'/'.substr($key, 2, 2), 0775); | |
@chmod('.tmp/cache/'.substr($key, 0, 2).'/'.substr($key, 2, 2), 0775); | |
if ($ttl > 0) { | |
@file_put_contents($this->fn($key).'.ttl', $ttl, LOCK_EX); | |
} | |
$v = json_encode($value); | |
return file_put_contents($this->fn($key), $compress && function_exists('gzdeflate') ? gzdeflate($v) : $v, LOCK_EX) > 0 ? true : false; | |
} | |
public function invalidate() | |
{ | |
\PHPPE\Tools::rmdir('.tmp/cache'); | |
} | |
public function cronMinute($args) | |
{ | |
$files = glob('.tmp/cache/*/*/*.ttl',GLOB_NOSORT); | |
foreach ($files as $f) { | |
$ttl = intval(@file_get_contents($f)); | |
$cf = substr($f, 0, strlen($f) - 4); | |
if ($ttl < 1 || time() - @filemtime($cf) >= $ttl) { | |
@unlink($f); | |
@unlink($cf); | |
} | |
} | |
} | |
} | |
}//namespace | |
/*** Common routing filters ***/ | |
namespace PHPPE\Filter { | |
class get extends \PHPPE\Filter | |
{ | |
public static function filter() | |
{ | |
return @$_SERVER['REQUEST_METHOD'] == 'GET' ? true : false; | |
} | |
} | |
class post extends \PHPPE\Filter | |
{ | |
public static function filter() | |
{ | |
return @$_SERVER['REQUEST_METHOD'] == 'POST' ? true : false; | |
} | |
} | |
class loggedin extends \PHPPE\Filter | |
{ | |
public static function filter() | |
{ | |
if (\PHPPE\Core::$user->id) { | |
return true; | |
} | |
/* save request uri for returning after successful login */ | |
// @codeCoverageIgnoreStart | |
\PHPPE\Http::redirect('login', 1); | |
} | |
// @codeCoverageIgnoreEnd | |
} | |
class csrf extends \PHPPE\Filter | |
{ | |
public static function filter() | |
{ | |
return \PHPPE\Core::isBtn(); | |
} | |
} | |
}//namespace | |
/*** Built-in fields ***/ | |
namespace PHPPE\AddOn { | |
use PHPPE\Core as Core; | |
use PHPPE\View as View; | |
/** | |
* hidden field element. | |
*/ | |
//L("Hidden value") | |
class hidden extends \PHPPE\AddOn | |
{ | |
public $conf = "*obj.field [value]"; | |
public function edit() | |
{ | |
return "<input type='hidden' name='".$this->fld."' value='".htmlspecialchars(trim(!empty($this->attrs[0])?View::getval($this->attrs[0]):$this->value))."'>"; | |
} | |
} | |
/** | |
* javascript button element. | |
*/ | |
//L("Button") | |
class button extends \PHPPE\AddOn | |
{ | |
public $conf = "*label onclickjs [cssclass]"; | |
public function show() | |
{ | |
return $this->edit(); | |
} | |
public function edit() | |
{ | |
$t = $this; | |
$a = $t->attrs; | |
return "<button class='".(!empty($a[1]) && $a[1] != '-' ? $a[1] : 'btn btn-default')."' onclick=\"".strtr(!empty($a[0]) && $a[0] != '-' ? $a[0] : "alert('".L('No action')."');", ['"' => '\\"']).'">'.L(!empty($t->name) ? $t->name : 'Press me').'</button>'; | |
} | |
} | |
/** | |
* form submit button element. | |
*/ | |
//L("Update") | |
class update extends \PHPPE\AddOn | |
{ | |
public $conf = "*[label [onclickjs [cssclass]]]"; | |
public function edit() | |
{ | |
$t = $this; | |
$a = $t->attrs; | |
return "<button class='".(!empty($a[1]) && $a[1] != '-' ? $a[1] : 'btn btn-default')."' name='pe_try".View::tc()."' type='submit'".(!empty($a[0]) && $a[0] != '-' ? ' onclick="return '.strtr($a[0], ['"' => '\\"']).'"' : '').'>'.L(!empty($t->name) ? $t->name : 'Okay').'</button>'; | |
} | |
} | |
/** | |
* text field element. | |
*/ | |
//L("Text") | |
class text extends \PHPPE\AddOn | |
{ | |
public $conf = "*([maxlen[,rows[,listopts[,isltr]]]]) obj.field [onchangejs [cssclass [onkeyupjs [placeholder [pattern]]]]]"; | |
public function edit() | |
{ | |
$t = $this; | |
$a = $t->args; | |
$b = $t->attrs; | |
$v = trim($t->value); | |
$D = (!empty($a[3]) ? " dir='ltr'" : "").(!empty($a[2])&&is_array($a[2]) ? " list='".$this->fld.":list'" : ""); | |
if ($v == 'null') { | |
$v = ''; | |
} | |
if (!empty($a[1]) && $a[1] > 0) { | |
if ($a[0] > 0) { | |
View::js('pe_mt(e,m)', 'var c,o;if(!e)e=window.event;o=e.target;c=e.keyCode?e.keyCode:e.which;return(c==8||c==46||o.value.length<m);'); | |
} | |
return '<textarea'.@View::v($t, $b[1], $b[0],$a)." rows='".$a[1]."'".($a[0] > 0 ? " onkeypress='return pe_mt(event,".$a[0].");'" : ''). | |
(!empty($b[2])&&$b[2]!="-"?" onkeyup='return ".$b[2].";'":'').(!empty($b[3]) && $b[3] != '-' ? ' placeholder="'.htmlspecialchars(L(trim($b[3]))).'"' : '')."$D wrap='soft' onfocus='this.className=this.className.replace(\" errinput\",\"\")'>".$v.'</textarea>'; | |
} | |
if (!empty($a[2])&&is_array($a[2])) { | |
$o="<datalist id='".$this->fld.":list'>"; | |
foreach($a[2] as $d) | |
$o.="<option value=\"".htmlspecialchars($d)."\">".L($d[0]=='@'?substr($d,1):$d)."</option>\n"; | |
$o.="</datalist>"; | |
} else { | |
$o=""; | |
} | |
return $o.'<input'.@View::v($t, $b[1], $b[0], $a)." type='text'".$D. | |
(!empty($b[2]) && $b[2] != '-' ? " onkepup='".$b[2]."'" : ''). | |
" onfocus='this.className=this.className.replace(\" errinput\",\"\")'". | |
(!empty($b[3]) && $b[3] != '-' ? ' placeholder="'.htmlspecialchars(L(trim($b[3]))).'"' : ''). | |
(!empty($b[4]) ? ' pattern="'.trim($b[4]).'"' : ''). | |
"$D value=\"".htmlspecialchars($v).'">'; | |
} | |
public static function validate($n, &$v, $a, $t) | |
{ | |
if (@$a[0] > 0) { | |
$v = substr($v, 0, $a[0]); | |
} | |
return [ | |
empty($t[4]) || preg_match('/'.$t[4].'/', $v), | |
'not matches the requested format', | |
]; | |
} | |
} | |
/** | |
* password field element. | |
*/ | |
//L("Password") | |
class pass extends \PHPPE\AddOn | |
{ | |
public $conf = "*([maxlen]) obj.field [cssclass [placeholder]]"; | |
public function show() | |
{ | |
return '******'; | |
} | |
public function edit() | |
{ | |
$t = $this; | |
return '<input'. | |
@View::v($t, $t->attrs[0], '', $t->args). | |
" type='password' value=\"".htmlspecialchars(trim($t->value)). | |
"\" onfocus='this.className=this.className.replace(\" errinput\",\"\")'". | |
(!empty($t->attrs[1]) ? ' placeholder="'.htmlspecialchars(L(trim($t->attrs[1]))).'"' : ''). | |
'>'; | |
} | |
public static function validate($n, &$v, $a, $t) | |
{ | |
if (function_exists('\\PHPPE\\pass')) { | |
// @codeCoverageIgnoreStart | |
return \PHPPE\pass($n, $v, $a, $t); | |
// @codeCoverageIgnoreEnd | |
} | |
return [ | |
preg_match('/[0-9]/', $v) && preg_match('/[a-z]/i', $v) && strtoupper($v) != $v && strtolower($v) != $v && strlen($v) >= 6, | |
'not a valid password! [a-zA-Z0-9]*6', | |
]; | |
} | |
} | |
/** | |
* number element. Note you have to specify both min and max values | |
*/ | |
//L("Decimal number") | |
class num extends \PHPPE\AddOn | |
{ | |
public $conf = "*([min,max]) obj.field [cssclass]"; | |
public function edit() | |
{ | |
$a = $this->args; | |
$t = 'this.value'; | |
$b = 'o.className=o.className.replace(" errinput","")'; | |
$C = isset($a[1]) ? "if($t<".$a[0].")$t=".$a[0].";if($t>".$a[1].")$t=".$a[1].';' : ''; | |
$r = 'return'; | |
View::js('pe_on(e)', "var c,o;if(!e)e=window.event;o=e.target;c=e.keyCode?e.keyCode:e.which;$b;if(c==8||c==37||c==39||c==46)$r true;c=String.fromCharCode(c);if(c.match(/[0-9\\b\\t\\r\\-\\.\\,]/)!=null)$r true;else{o.className+=' errinput';setTimeout(function(){{$b};},200);$r false;}"); | |
return '<input'.@View::v($this, $this->attrs[1], $this->attrs[0])." style='text-align:right;' type='number' onkeypress='$r pe_on(event);' onkeyup='$t=$t.replace(\",\",\".\");' onfocus='".$C."if($t==\"\")$t=0;".strtr($b, ['o.' => 'this.']).";this.select();'".(isset($a[1]) ? " onblur='$C' min='".$a[0]."' max='".$a[1]."'" : '').' value="'.htmlspecialchars(trim($this->value)).'">'; | |
} | |
public static function validate($n, &$v, $a, $t) | |
{ | |
$r = floatval($v).'' == $v.'' ? true : false; | |
//p("/^[0-9\-][0-9\.]+$/", $v); | |
$m = 'not a valid number!'; | |
if ($r && isset($a[1])) { | |
if ($v < $a[0]) { | |
$r = false; | |
$m = 'not enough!'; | |
$v = $a[0]; | |
} | |
if ($v > $a[1]) { | |
$r = false; | |
$m = 'too much!'; | |
$v = $a[1]; | |
} | |
} | |
$v = floatval($v); | |
return[$r, $m]; | |
} | |
} | |
/** | |
* option list element. | |
*/ | |
//L("Option list") | |
class select extends \PHPPE\AddOn | |
{ | |
public $conf = "*(size[,ismultiple]) obj.field dataset [skipids [onchangejs [cssclass]]]"; | |
public function show() | |
{ | |
return htmlspecialchars(is_array($this->value) ? implode(', ', $this->value) : $this->value); | |
} | |
public function edit() | |
{ | |
$t = $this; | |
$a = $t->attrs; | |
$b = $t->args; | |
$opts = !empty($a[0]) && $a[0] != '-' ? View::getval($a[0]) : []; | |
if (is_string($opts)) { | |
$opts = str_getcsv($opts, ','); | |
} | |
$skip = !empty($a[1]) && $a[1] != '-' ? View::getval($a[1]) : []; | |
if (is_string($skip)) { | |
$skip = str_getcsv($skip, ','); | |
} | |
if (!is_array($skip)) { | |
$skip = []; | |
} else { | |
$skip = array_flip($skip); | |
} | |
if (!empty($b[1])) { | |
$t->name .= '[]'; | |
} | |
$r = '<select'.@View::v($t, $a[3], $a[2], [], '', '', !empty($b[1]) ? '[]' : '').(!empty($b[1]) ? ' multiple' : ''). | |
(!empty($b[0]) && $b[0] > 0 ? " size='".intval($b[0])."'" : ''). | |
" onfocus='this.className=this.className.replace(\" errinput\",\"\")'>"; | |
if (is_array($opts)) { | |
foreach ($opts as $k => $v) { | |
$o = is_array($v) && isset($v['id']) ? $v['id'] : (is_object($v) && isset($v->id) ? $v->id : $k); | |
$n = is_array($v) && !empty($v['name']) ? $v['name'] : (is_object($v) && !empty($v->name) ? $v->name : (is_string($v) ? $v : $k)); | |
if (!isset($skip[$o]) && !empty($n)) { | |
$r .= '<option value="'.htmlspecialchars($o).'"'.((is_array($t->value) && in_array($o.'', $t->value)) || $o == $t->value ? ' selected' : '').'>'.$n.'</option>'; | |
} | |
} | |
} | |
$r .= '</select>'; | |
return $r; | |
} | |
} | |
/** | |
* checkbox element. | |
*/ | |
//L("Checkbox") | |
class check extends \PHPPE\AddOn | |
{ | |
public $conf = "*(truevalue) obj.field [label [cssclass]]"; | |
public function show() | |
{ | |
$t = $this; | |
return empty(Core::$core->output) || Core::$core->output != 'html' ? | |
('['.(!empty($t->value) ? 'X' : ' ').'] '.(!empty($t->attrs[0]) ? L($t->attrs[0]) : $t->value)) : | |
htmlspecialchars($t->value); | |
} | |
public function edit() | |
{ | |
$t = $this; | |
$a = $t->attrs; | |
$e = Core::isError($t->name); | |
return($e ? "<span class='errinput'>" : ''). | |
'<label><input'.@View::v($t, empty($a[2]) ? 'checkbox' : $a[2], $a[1])." type='checkbox'".(!empty($t->value) ? ' checked' : '').' value="'.htmlspecialchars(trim(!empty($t->args[0]) ? $t->args[0] : '1')).'"> '. | |
(!empty($a[0]) ? L($a[0]) : '').'</label>'. | |
($e ? '</span>' : ''); | |
} | |
public static function validate($n, &$v, $a, $t) | |
{ | |
$v = !empty($v) ? ($v == 1 || $v == '1' ? true : $v) : false; | |
return [ true, "OK" ]; | |
} | |
} | |
/** | |
* radiobutton elements. | |
*/ | |
//L("Radiobutton") | |
class radio extends \PHPPE\AddOn | |
{ | |
public $conf = "*(value) obj.field [label [cssclass]]"; | |
public function show() | |
{ | |
$t = $this; | |
return empty(Core::$core->output) || Core::$core->output != 'html' ? | |
('('.($t->value == $t->args[0] ? 'X' : ' ').') '.(!empty($t->attrs[0]) ? L($t->attrs[0]) : $t->args[0])) : | |
htmlspecialchars($t->value); | |
} | |
public function edit() | |
{ | |
$t = $this; | |
$a = $t->args; | |
$b = $t->attrs; | |
return '<label><input'.@View::v($t, empty($b[2]) ? 'radiobutton' : $b[2], $b[1], [], '', '_'.(is_scalar($a[0])?$a[0]:sha1($a[0])))." type='radio'". | |
($t->value == $a[0] ? ' checked' : '').' value="'.htmlspecialchars(trim(isset($a[0]) ? $a[0] : '1')).'"> '. | |
(!empty($b[0]) ? L($b[0]) : '').'</label>'; | |
} | |
} | |
/** | |
* phone number field element. | |
*/ | |
//L("Phone") | |
class phone extends \PHPPE\AddOn | |
{ | |
public $conf = "*([maxlen]) obj.field [onchangejs [cssclass]]"; | |
public function edit() | |
{ | |
$t = $this; | |
$b = 'o.className=o.className.replace(" errinput","")'; | |
View::js('pe_op(e)', "var c,o;if(!e)e=window.event;o=e.target;c=e.keyCode?e.keyCode:e.which;$b;if(c==8||c==37||c==39||c==46)return true;c=String.fromCharCode(c);if(c.match(/[0-9\\b\\t\\r\\-\\ \\+\\(\\)\\/]/)!=null)return true;else{o.className+=' errinput';setTimeout(function(){{$b};},200);return false;}"); | |
return '<input'.@View::v($t, $t->attrs[1], $t->attrs[0], $t->args, "[0-9\+][0-9\ \(\)\-]+")." type='tel' onfocus='".strtr($b, ['o.' => 'this.'])."' onkeypress='return pe_op(event);' value=\"".htmlspecialchars(trim($t->value)).'">'; | |
} | |
public static function validate($n, &$v, $a, $t) | |
{ | |
return [ | |
preg_match("/^[0-9\+][0-9\ \(\)\-]+$/", $v), | |
'invalid phone number', | |
]; | |
} | |
} | |
/** | |
* email address field element. | |
*/ | |
//L("Email") | |
class email extends \PHPPE\AddOn | |
{ | |
public $conf = "*([maxlen]) obj.field [onchangejs [cssclass]]"; | |
public function show() | |
{ | |
if (empty(Core::$core->output) || Core::$core->output != 'html') { | |
return $this->value; | |
} | |
return strtr(htmlspecialchars($this->value), ['@' => '@', '.' => '.']); | |
} | |
public function edit() | |
{ | |
$t = $this; | |
$a = $t->attrs; | |
$b = 'o.className=o.className.replace(" errinput","")'; | |
View::js('pe_oe(o)', "$b;if(o.value!=''&&o.value.match(/^.+\@(\[?)[a-z0-9\-\.]+\.([a-z]+|[0-9]{1,3})(\]?)$/i)==null)o.className+=' errinput';"); | |
return '<input'.@View::v($t, $a[1], '', $t->args)." type='email' onfocus='".strtr($b, ['o.' => 'this.'])."' onchange='pe_oe(this);".(!empty($a[0]) && $a[0] != '-' ? $a[0] : '')."' value=\"".htmlspecialchars(trim($t->value)).'">'; | |
} | |
public static function validate($n, &$v, $a, $t) | |
{ | |
$r=preg_match("/^.+\@((\[?)[a-z0-9\-\.]+\.([a-z]+|[0-9]{1,3})(\]?))$/i", $v, $m); | |
if($r && !empty(Core::$core->blacklist) && in_array($m[1],Core::$core->blacklist)) { $r=0; $v=""; } | |
return [ | |
$r, | |
'invalid email address', | |
]; | |
} | |
} | |
/** | |
* file upload input box. | |
*/ | |
//L("File") | |
class file extends \PHPPE\AddOn | |
{ | |
public $conf = "*obj.field [onchangejs [cssclass]]"; | |
public function show() | |
{ | |
return ''; | |
} | |
public function edit() | |
{ | |
$t = $this; | |
$e = Core::isError($t->name); | |
return | |
'<input'.@View::v($t, $t->attrs[1], $t->attrs[0], $t->args)." type='file' style='display:inline;' title='".round(Core::$core->fm / 1048576)."Mb'>"; | |
} | |
public static function validate($n, &$v, $a, $t) | |
{ | |
//! get field name | |
$n = strtr($n,["."=>"_"]); | |
$ok = !empty($_FILES[$n]) && (@$_FILES[$n]['error']==0||@$_FILES[$n]['error']==4); | |
//! copy data from $_FILES | |
$v = $ok? $_FILES[$n] : []; | |
return [$ok, 'failed to upload file.']; | |
} | |
} | |
/** | |
* colorpicker element. | |
*/ | |
//L("Color picker") | |
class color extends \PHPPE\AddOn | |
{ | |
public $conf = "*obj.field [onchangejs [cssclass]]"; | |
public function show() | |
{ | |
return "<span style='width:10px;height:10px;background-color:".$this->value.";'></span> ".$this->value; | |
} | |
public function edit() | |
{ | |
$t = $this; | |
$a = $t->attrs; | |
return '<input'.@View::v($t, $a[1], $a[0], $t->args)." type='color' value=\"".htmlspecialchars(empty($t->value)?"#000000":trim($t->value)).'">'; | |
} | |
} | |
/** | |
* Date element. Note you have to specify both min and max values | |
*/ | |
//L("Date") | |
class date extends \PHPPE\AddOn | |
{ | |
public $conf = "*obj.field [cssclass]"; | |
public function edit() | |
{ | |
return '<input'.@View::v($this, $this->attrs[1], $this->attrs[0])." type='date' value=\"".htmlspecialchars(trim($this->value)).'">'; | |
} | |
public static function validate($n, &$v, $a, $t) | |
{ | |
$v = strtotime($v); | |
return[$v!=0, 'not a valid date time!']; | |
} | |
} | |
/** | |
* Date time element. Note you have to specify both min and max values | |
*/ | |
//L("Time") | |
class time extends \PHPPE\AddOn | |
{ | |
public $conf = "*obj.field [cssclass]"; | |
public function edit() | |
{ | |
return '<input'.@View::v($this, $this->attrs[1], $this->attrs[0])." type='datetime-local' value=\"".htmlspecialchars(trim($this->value)).'">'; | |
} | |
public static function validate($n, &$v, $a, $t) | |
{ | |
$v = strtotime($v); | |
return[$v!=0, 'not a valid date time!']; | |
} | |
} | |
/** | |
* field label. | |
*/ | |
//L("Label") | |
class label extends \PHPPE\AddOn | |
{ | |
public $conf = "*obj.field label [cssclass]"; | |
public function show() | |
{ | |
return $this->edit(); | |
} | |
public function edit() | |
{ | |
return '<label'.(!empty($this->attrs[1]) ? " class='".$this->attrs[1]."'" : '')." for='".$this->fld."'>". | |
L($this->attrs[0]).':</label>'; | |
} | |
} | |
}//namespace | |
namespace { | |
/*** I18N ***/ | |
/** | |
* Translate a string or code to user's language. | |
* | |
* @param string text or code, with optional arguments | |
* | |
* @return string translated text | |
*/ | |
function L() | |
{ | |
$a=func_get_args();$s=array_shift($a); | |
return vsprintf(isset(\PHPPE\Core::$l[$s]) ? \PHPPE\Core::$l[$s] : strtr($s, ['_' => ' ']),$a); | |
} | |
/** | |
* Returns permanent link with canonical, absolute path. | |
* | |
* @param string application | |
* @param string action | |
* | |
* @return string url | |
*/ | |
function url($m = null, $p = null) | |
{ | |
//return permalink for an action | |
return \PHPPE\Http::url($m, $p); | |
} | |
} | |
/* to make lang utility happy | |
L("sure") | |
*/ |