page: skip directory handlers in search
[minimedit.git] / article.inc.php
1 <?php
2 class ArchiveArticle
3 {
4         public $raw, $title, $body; # file contents
5         public $meta = [];  # head metadata properties
6         public $place = []; # template variables replaced in render()
7
8         function __construct($path)
9         {
10                 $this->page = preg_replace('{^\.(?:/|$)}', '', $path);
11                 $this->link = preg_replace('{(?:(?:/|^)index)?\.html$}', '', $this->page);
12                 $this->raw($this->page);
13         }
14
15         function raw($page)
16         {
17                 if (!file_exists($page)) {
18                         return;
19                 }
20                 $this->raw = file_get_contents($page);
21
22                 if (preg_match_all('{
23                         \G <meta \s+ property="( [^"]+ )" \s+ content="( [^"]* )" > \s*
24                 }x', $this->raw, $meta)) {
25                         $matchlen = array_sum(array_map('strlen', $meta[0]));
26                         $this->raw = substr($this->raw, $matchlen); # delete matched contents
27                         $this->meta = array_combine($meta[1], $meta[2]); # [property => content]
28                 }
29
30                 // find significant contents
31                 $this->body = preg_replace('{<aside\b.*?</aside>}s', '', $this->raw);
32                 if (preg_match('{<h2>(.*?)</h2>\s*(.*)}s', $this->body, $titlematch)) {
33                         list (, $this->title, $this->body) = $titlematch;
34                 }
35
36                 return $this->raw;
37         }
38
39         function __get($col)
40         {
41                 return $this->$col = $this->$col();  # run method and cache
42         }
43
44         function handler()
45         {
46                 $path = $this->link;
47                 $this->path = '';
48                 $this->restricted = FALSE;
49                 while (TRUE) {
50                         if (file_exists("$path/.private")) {
51                                 $this->restricted = $path;
52                         }
53
54                         if (file_exists("$path/index.php")) {
55                                 return $path;
56                         }
57
58                         $up = strrpos($path, '/');
59                         $this->path = substr($path, $up) . $this->path;
60                         $path = substr($path, 0, $up);
61                         if ($up === FALSE) {
62                                 break;
63                         }
64                 }
65                 return;
66         }
67
68         function restricted()
69         {
70                 $this->handler;
71                 return $this->restricted;
72         }
73
74         function safetitle()
75         {
76                 return trim($this->meta['og:title'] ?? strip_tags($this->title));
77         }
78         function name()
79         {
80                 return $this->safetitle ?: $this->link;
81         }
82
83         function last()
84         {
85                 return filemtime($this->page);
86         }
87         function lastiso()
88         {
89                 return date(DATE_ATOM, $this->last);
90         }
91
92         function dateparts()
93         {
94                 preg_match('< / (\d{4}) [/-] (\d{2}) (?:- (\d{2}) )? - >x', $this->page, $ymd);
95                 array_shift($ymd);
96                 return $ymd;
97         }
98         function dateiso()
99         {
100                 return implode('-', $this->dateparts()) . 'T12:00:00+02:00';
101         }
102
103         function story()
104         {
105                 if ( preg_match('{
106                         (?: < (?: p | figure [^>]* ) >\s* )+ (<img\ [^>]*>) | \n <hr\ />
107                 }x', $this->body, $img, PREG_OFFSET_CAPTURE) ) {
108                         # strip part after matching divider (image)
109                         if (isset($img[1])) {
110                                 $this->img = $img[1][0];
111                         }
112                         return substr($this->body, 0, $img[0][1]);
113                 }
114                 return $this->body;
115         }
116
117         function teaser()
118         {
119                 if ($override = @$this->meta['og:description']) {
120                         # prefer specific page description if found in metadata
121                         return $override;
122                 }
123
124                 # paragraph contents following the page header if any
125                 if (preg_match('{
126                         \G (?> \s+ | <div [^>]*> | \[\[[^]]*\]\] )* <p> \s* (.*?) </p>
127                 }sx', $this->body, $bodyp, 0)) {
128                         return $bodyp[1];
129                 }
130         }
131
132         function img()
133         {
134                 $this->img = NULL;
135                 $this->story;
136                 return $this->img;
137         }
138         function image()
139         {
140                 if ($override = @$this->meta['og:image']) {
141                         # prefer specific page image if found in metadata
142                         return $override;
143                 }
144
145                 if ( preg_match('/\bsrc="([^"]*)"/', $this->img, $src) ) {
146                         return $src[1];
147                 }
148         }
149         function thumb($size = '300x')
150         {
151                 if (!$this->image or $this->image[0] !== '/') return;
152                 if (preg_match('{^/thumb/\D}', $this->image)) {
153                         return ltrim($this->image, '/');
154                 }
155                 return preg_replace(
156                         ['{^(?:/thumb/[^/]*)?}', '/\.groot(?=\.\w+$)/'], ["thumb/$size", ''],
157                         $this->image
158                 );
159         }
160
161         function widget($name, $params = [])
162         {
163                 $path = stream_resolve_include_path("widget/$name.php");
164                 if (!file_exists($path)) {
165                         return '<strong class="warn"><em>'.$name.'</em> ontbreekt</strong>';
166                 }
167
168                 ob_start();
169                 $Page = clone $this;
170                 if (is_array($params)) {
171                         $Page->place += $params;
172                 }
173                 else {
174                         foreach (explode(' ', $params) as $param) {
175                                 if ($set = strpos($param, '=')) {
176                                         $Page->place[ substr($param, 0, $set) ] = substr($param, $set + 1);
177                                 }
178                                 elseif (!empty($param)) {
179                                         $Page->place[] = $param;
180                                 }
181                         }
182                 }
183
184                 try {
185                         include "widget/$name.php";
186                         return ob_get_clean();
187                 }
188                 catch (Exception $e) {
189                         return sprintf('<strong class="warn">%s</strong>',
190                                 "fout in <em>$name</em>: {$e->getMessage()}"
191                         );
192                 }
193         }
194
195         function render()
196         {
197                 $doc = $this->raw;
198
199                 if (!empty($this->place['warn'])) {
200                         $warn = '<p class="warn">[[warn]]</p>';
201                         if ($offset = strpos($doc, '</h2>')) {
202                                 $doc = substr_replace($doc, "\n\n".$warn, $offset + 5, 0);
203                         }
204                         else {
205                                 $doc = $warn . "\n\n" . $doc;
206                         }
207                 }
208
209                 # keep either login or logout parts depending on user level
210                 global $User;
211                 $hideclass = $User && property_exists($User, 'login') && $User->login ? 'logout' : 'login';
212                 $doc = preg_replace('{\s*<([a-z]+) class="'.$hideclass.'">.*?</\1>}s', '', $doc);
213
214                 return preg_replace_callback(
215                         '{ \[\[ ([^] ]+) ([^]]*) \]\] }x',
216                         function ($sub) {
217                                 list ($placeholder, $name, $params) = $sub;
218                                 $html = $this->place[$name] ??
219                                         $this->widget($name, $params);
220                                 if (empty($html) or $html[0] != '<') {
221                                         $html = "<span>$html</span>";
222                                 }
223                                 $attr = sprintf(' data-dyn="%s"', is_numeric($name) ? '' : $name.$params);
224                                 # contents with identifier in first tag
225                                 return preg_replace( '/(?=>)/', $attr, $html, 1);
226                         },
227                         $doc
228                 );
229         }
230 }
231
232 class PageSearch
233 {
234         public $handlers = [];
235
236         function __construct($path = '.')
237         {
238                 $this->iterator = new RecursiveCallbackFilterIterator(
239                         new RecursiveDirectoryIterator($path),
240                         function ($current) {
241                                 if ($current->getFilename()[0] === '.') {
242                                         # skip hidden files and directories
243                                         return FALSE;
244                                 }
245                                 if (file_exists($current->getFilename() . '/index.php')) {
246                                         # contents better provided by handler code
247                                         $this->handlers[ $current->getPathname() ] = $current;
248                                         return FALSE;
249                                 }
250                                 if ($current->isLink()) {
251                                         # ignore symlinks, original contents only
252                                         return FALSE;
253                                 }
254                                 if ($current->isDir()) {
255                                         # traverse subdirectories unless untracked in any amount
256                                         return !file_exists("$current/.gitignore");
257                                 }
258                                 # match **/*.html
259                                 return preg_match('/(?<!\.inc)\.html$/', $current->getFilename());
260                         }
261                 );
262         }
263
264         function files()
265         {
266                 # order alphabetically by link
267                 $dir = iterator_to_array(new RecursiveIteratorIterator($this->iterator));
268                 array_walk($dir, function (&$row, $name) {
269                         # prepare values for sorting (directory index first)
270                         $row = preg_replace('{/index\.html$}', '', $name);
271                 });
272                 asort($dir);
273                 return $dir;
274         }
275 }