thumb: append .jpg extension to match file type
[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->file = preg_replace('{^\.(?:/|$)}', '', $path);
11                 $this->link = preg_replace('{(?:(?:/|^)index)?\.html$}', '', $this->file);
12                 $this->raw($this->file);
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 index()
69         {
70                 $this->handler;
71                 if (empty($this->handler)) {
72                         return;
73                 }
74                 $User = NULL;
75                 $Page = $this;
76                 $res = include "./{$this->handler}/index.php";
77                 return $res;
78         }
79
80         function restricted()
81         {
82                 $this->handler;
83                 return $this->restricted;
84         }
85
86         function safetitle()
87         {
88                 return trim($this->meta['og:title'] ?? strip_tags($this->title));
89         }
90         function name()
91         {
92                 return $this->safetitle ?: htmlspecialchars($this->link);
93         }
94
95         function last()
96         {
97                 return filemtime($this->file);
98         }
99         function lastiso()
100         {
101                 return date(DATE_ATOM, $this->last);
102         }
103
104         function dateparts()
105         {
106                 preg_match('< / (\d{4}) [/-] (\d{2}) (?:- (\d{2}) )? - >x', $this->file, $ymd);
107                 array_shift($ymd);
108                 return $ymd;
109         }
110         function dateiso()
111         {
112                 return implode('-', $this->dateparts()) . 'T12:00:00+02:00';
113         }
114
115         function story()
116         {
117                 if ( preg_match('{
118                         (?: < (?: p | figure [^>]* ) >\s* )+ (<img\ [^>]*>) | \n <hr\ />
119                 }x', $this->body, $img, PREG_OFFSET_CAPTURE) ) {
120                         # strip part after matching divider (image)
121                         if (isset($img[1])) {
122                                 $this->img = $img[1][0];
123                         }
124                         return substr($this->body, 0, $img[0][1]);
125                 }
126                 return $this->body;
127         }
128
129         function teaser()
130         {
131                 if ($override = @$this->meta['og:description']) {
132                         # prefer specific page description if found in metadata
133                         return $override;
134                 }
135
136                 # paragraph contents following the page header if any
137                 if (preg_match('{
138                         \G (?> \s+ | <div [^>]*> | \[\[[^]]*\]\] )* <p> \s* (.*?) </p>
139                 }sx', $this->body, $bodyp, 0)) {
140                         return $bodyp[1];
141                 }
142         }
143
144         function img()
145         {
146                 $this->img = NULL;
147                 $this->story;
148                 return $this->img;
149         }
150         function image()
151         {
152                 if ($override = @$this->meta['og:image']) {
153                         # prefer specific page image if found in metadata
154                         return $override;
155                 }
156
157                 if ( preg_match('/\bsrc="([^"]*)"/', $this->img, $src) ) {
158                         return $src[1];
159                 }
160         }
161         function thumb($size = '300x')
162         {
163                 if (!$this->image or $this->image[0] !== '/') return;
164                 if (preg_match('{^/thumb/\D}', $this->image)) {
165                         return ltrim($this->image, '/');
166                 }
167                 return preg_replace(
168                         ['{^(?:/thumb/[^/]*)?}', '/\.groot(?=\.\w+$)/', '/(?:\.jpg)?$/'],
169                         [      "thumb/$size",    '',                         '.jpg'    ],
170                         $this->image, 1
171                 );
172         }
173
174         function widget($name, $params = [])
175         {
176                 $path = stream_resolve_include_path("widget/$name.php");
177                 if (!file_exists($path)) {
178                         return '<strong class="warn"><em>'.$name.'</em> ontbreekt</strong>';
179                 }
180
181                 ob_start();
182                 $Page = clone $this;
183                 if (is_array($params)) {
184                         $Page->place += $params;
185                 }
186                 else {
187                         foreach (explode(' ', $params) as $param) {
188                                 if ($set = strpos($param, '=')) {
189                                         $Page->place[ substr($param, 0, $set) ] = substr($param, $set + 1);
190                                 }
191                                 elseif (!empty($param)) {
192                                         $Page->place[] = $param;
193                                 }
194                         }
195                 }
196
197                 try {
198                         include "widget/$name.php";
199                         return ob_get_clean();
200                 }
201                 catch (Exception $e) {
202                         return sprintf('<strong class="warn">%s</strong>',
203                                 "fout in <em>$name</em>: {$e->getMessage()}"
204                         );
205                 }
206         }
207
208         function render()
209         {
210                 $doc = $this->raw;
211
212                 if (!empty($this->place['warn'])) {
213                         $warn = '<p class="warn">[[warn]]</p>';
214                         if ($offset = strpos($doc, '</h2>')) {
215                                 $doc = substr_replace($doc, "\n\n".$warn, $offset + 5, 0);
216                         }
217                         else {
218                                 $doc = $warn . "\n\n" . $doc;
219                         }
220                 }
221
222                 # keep either login or logout parts depending on user level
223                 global $User;
224                 $userexists = $User && property_exists($User, 'login') && $User->login;
225                 if (! ($userexists and $User->admin("edit {$this->link}")) ) {
226                         # remove matching elements until first corresponding closing tag
227                         $hideclass = $userexists ? 'logout' : 'login';
228                         $tagmatch = '<([a-z]+) class="'.$hideclass.'"[^>]*>';
229                         $doc = preg_replace("{\s*{$tagmatch}.*?</\\1>}s", '', $doc);
230                 }
231
232                 return preg_replace_callback(
233                         '{ \[\[ ([^] ]+) ([^]]*) \]\] }x',
234                         function ($sub) {
235                                 list ($placeholder, $name, $params) = $sub;
236                                 $html = $this->place[$name] ??
237                                         $this->widget($name, $params);
238                                 if (empty($html) or $html[0] != '<') {
239                                         $html = "<span>$html</span>";
240                                 }
241                                 $attr = sprintf(' data-dyn="%s"', is_numeric($name) ? '' : $name.$params);
242                                 # contents with identifier in first tag
243                                 return preg_replace( '/(?=>)/', $attr, $html, 1);
244                         },
245                         $doc
246                 );
247         }
248 }
249
250 class PageSearch
251 {
252         public $handlers = [];
253
254         function __construct($path = '.')
255         {
256                 $this->iterator = new RecursiveCallbackFilterIterator(
257                         new RecursiveDirectoryIterator($path),
258                         function ($current) {
259                                 if ($current->getFilename()[0] === '.') {
260                                         # skip hidden files and directories
261                                         return FALSE;
262                                 }
263                                 if (file_exists($current->getFilename() . '/index.php')) {
264                                         # contents better provided by handler code
265                                         $this->handlers[ $current->getPathname() ] = $current;
266                                         return FALSE;
267                                 }
268                                 if ($current->isLink()) {
269                                         # ignore symlinks, original contents only
270                                         return FALSE;
271                                 }
272                                 if ($current->isDir()) {
273                                         # traverse subdirectories unless untracked in any amount
274                                         return !file_exists("$current/.gitignore");
275                                 }
276                                 # match **/*.html
277                                 return preg_match('/(?<!\.inc)\.html$/', $current->getFilename());
278                         }
279                 );
280         }
281
282         function files()
283         {
284                 # order alphabetically by link
285                 $dir = [];
286                 foreach (new RecursiveIteratorIterator($this->iterator) as $name) {
287                         $article = new ArchiveArticle($name);
288                         $dir[$article->link] = $article;
289                 }
290                 ksort($dir);
291                 return $dir;
292         }
293 }