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") | |
| */ |