1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Pico |
5
|
|
|
* |
6
|
|
|
* Pico is a stupidly simple, blazing fast, flat file CMS. |
7
|
|
|
* - Stupidly Simple: Pico makes creating and maintaining a |
8
|
|
|
* website as simple as editing text files. |
9
|
|
|
* - Blazing Fast: Pico is seriously lightweight and doesn't |
10
|
|
|
* use a database, making it super fast. |
11
|
|
|
* - No Database: Pico is a "flat file" CMS, meaning no |
12
|
|
|
* database woes, no MySQL queries, nothing. |
13
|
|
|
* - Markdown Formatting: Edit your website in your favourite |
14
|
|
|
* text editor using simple Markdown formatting. |
15
|
|
|
* - Twig Templates: Pico uses the Twig templating engine, |
16
|
|
|
* for powerful and flexible themes. |
17
|
|
|
* - Open Source: Pico is completely free and open source, |
18
|
|
|
* released under the MIT license. |
19
|
|
|
* See <http://picocms.org/> for more info. |
20
|
|
|
* |
21
|
|
|
* @author Gilbert Pellegrom |
22
|
|
|
* @author Daniel Rudolf |
23
|
|
|
* @link <http://picocms.org> |
24
|
|
|
* @license The MIT License <http://opensource.org/licenses/MIT> |
25
|
|
|
* @version 1.0 |
26
|
|
|
*/ |
27
|
|
|
class Pico |
28
|
|
|
{ |
29
|
|
|
/** |
30
|
|
|
* Sort files in alphabetical ascending order |
31
|
|
|
* |
32
|
|
|
* @see Pico::getFiles() |
33
|
|
|
* @var int |
34
|
|
|
*/ |
35
|
|
|
const SORT_ASC = 0; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Sort files in alphabetical descending order |
39
|
|
|
* |
40
|
|
|
* @see Pico::getFiles() |
41
|
|
|
* @var int |
42
|
|
|
*/ |
43
|
|
|
const SORT_DESC = 1; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Don't sort files |
47
|
|
|
* |
48
|
|
|
* @see Pico::getFiles() |
49
|
|
|
* @var int |
50
|
|
|
*/ |
51
|
|
|
const SORT_NONE = 2; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Root directory of this Pico instance |
55
|
|
|
* |
56
|
|
|
* @see Pico::getRootDir() |
57
|
|
|
* @var string |
58
|
|
|
*/ |
59
|
|
|
protected $rootDir; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Config directory of this Pico instance |
63
|
|
|
* |
64
|
|
|
* @see Pico::getConfigDir() |
65
|
|
|
* @var string |
66
|
|
|
*/ |
67
|
|
|
protected $configDir; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Plugins directory of this Pico instance |
71
|
|
|
* |
72
|
|
|
* @see Pico::getPluginsDir() |
73
|
|
|
* @var string |
74
|
|
|
*/ |
75
|
|
|
protected $pluginsDir; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Themes directory of this Pico instance |
79
|
|
|
* |
80
|
|
|
* @see Pico::getThemesDir() |
81
|
|
|
* @var string |
82
|
|
|
*/ |
83
|
|
|
protected $themesDir; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Boolean indicating whether Pico started processing yet |
87
|
|
|
* |
88
|
|
|
* @var boolean |
89
|
|
|
*/ |
90
|
|
|
protected $locked = false; |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* List of loaded plugins |
94
|
|
|
* |
95
|
|
|
* @see Pico::getPlugins() |
96
|
|
|
* @var object[]|null |
97
|
|
|
*/ |
98
|
|
|
protected $plugins; |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Current configuration of this Pico instance |
102
|
|
|
* |
103
|
|
|
* @see Pico::getConfig() |
104
|
|
|
* @var array|null |
105
|
|
|
*/ |
106
|
|
|
protected $config; |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Part of the URL describing the requested contents |
110
|
|
|
* |
111
|
|
|
* @see Pico::getRequestUrl() |
112
|
|
|
* @var string|null |
113
|
|
|
*/ |
114
|
|
|
protected $requestUrl; |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* Absolute path to the content file being served |
118
|
|
|
* |
119
|
|
|
* @see Pico::getRequestFile() |
120
|
|
|
* @var string|null |
121
|
|
|
*/ |
122
|
|
|
protected $requestFile; |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Raw, not yet parsed contents to serve |
126
|
|
|
* |
127
|
|
|
* @see Pico::getRawContent() |
128
|
|
|
* @var string|null |
129
|
|
|
*/ |
130
|
|
|
protected $rawContent; |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Meta data of the page to serve |
134
|
|
|
* |
135
|
|
|
* @see Pico::getFileMeta() |
136
|
|
|
* @var array|null |
137
|
|
|
*/ |
138
|
|
|
protected $meta; |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Parsedown Extra instance used for markdown parsing |
142
|
|
|
* |
143
|
|
|
* @see Pico::getParsedown() |
144
|
|
|
* @var ParsedownExtra|null |
145
|
|
|
*/ |
146
|
|
|
protected $parsedown; |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Parsed content being served |
150
|
|
|
* |
151
|
|
|
* @see Pico::getFileContent() |
152
|
|
|
* @var string|null |
153
|
|
|
*/ |
154
|
|
|
protected $content; |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* List of known pages |
158
|
|
|
* |
159
|
|
|
* @see Pico::getPages() |
160
|
|
|
* @var array[]|null |
161
|
|
|
*/ |
162
|
|
|
protected $pages; |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Data of the page being served |
166
|
|
|
* |
167
|
|
|
* @see Pico::getCurrentPage() |
168
|
|
|
* @var array|null |
169
|
|
|
*/ |
170
|
|
|
protected $currentPage; |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Data of the previous page relative to the page being served |
174
|
|
|
* |
175
|
|
|
* @see Pico::getPreviousPage() |
176
|
|
|
* @var array|null |
177
|
|
|
*/ |
178
|
|
|
protected $previousPage; |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* Data of the next page relative to the page being served |
182
|
|
|
* |
183
|
|
|
* @see Pico::getNextPage() |
184
|
|
|
* @var array|null |
185
|
|
|
*/ |
186
|
|
|
protected $nextPage; |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Twig instance used for template parsing |
190
|
|
|
* |
191
|
|
|
* @see Pico::getTwig() |
192
|
|
|
* @var Twig_Environment|null |
193
|
|
|
*/ |
194
|
|
|
protected $twig; |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Variables passed to the twig template |
198
|
|
|
* |
199
|
|
|
* @see Pico::getTwigVariables |
200
|
|
|
* @var array|null |
201
|
|
|
*/ |
202
|
|
|
protected $twigVariables; |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Constructs a new Pico instance |
206
|
|
|
* |
207
|
|
|
* To carry out all the processing in Pico, call {@link Pico::run()}. |
208
|
|
|
* |
209
|
|
|
* @param string $rootDir root directory of this Pico instance |
210
|
|
|
* @param string $configDir config directory of this Pico instance |
211
|
|
|
* @param string $pluginsDir plugins directory of this Pico instance |
212
|
|
|
* @param string $themesDir themes directory of this Pico instance |
213
|
|
|
*/ |
214
|
|
|
public function __construct($rootDir, $configDir, $pluginsDir, $themesDir) |
215
|
|
|
{ |
216
|
|
|
$this->rootDir = rtrim($rootDir, '/\\') . '/'; |
217
|
|
|
$this->configDir = $this->getAbsolutePath($configDir); |
218
|
|
|
$this->pluginsDir = $this->getAbsolutePath($pluginsDir); |
219
|
|
|
$this->themesDir = $this->getAbsolutePath($themesDir); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* Returns the root directory of this Pico instance |
224
|
|
|
* |
225
|
|
|
* @return string root directory path |
226
|
|
|
*/ |
227
|
|
|
public function getRootDir() |
228
|
|
|
{ |
229
|
|
|
return $this->rootDir; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Returns the config directory of this Pico instance |
234
|
|
|
* |
235
|
|
|
* @return string config directory path |
236
|
|
|
*/ |
237
|
|
|
public function getConfigDir() |
238
|
|
|
{ |
239
|
|
|
return $this->configDir; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Returns the plugins directory of this Pico instance |
244
|
|
|
* |
245
|
|
|
* @return string plugins directory path |
246
|
|
|
*/ |
247
|
|
|
public function getPluginsDir() |
248
|
|
|
{ |
249
|
|
|
return $this->pluginsDir; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Returns the themes directory of this Pico instance |
254
|
|
|
* |
255
|
|
|
* @return string themes directory path |
256
|
|
|
*/ |
257
|
|
|
public function getThemesDir() |
258
|
|
|
{ |
259
|
|
|
return $this->themesDir; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Runs this Pico instance |
264
|
|
|
* |
265
|
|
|
* Loads plugins, evaluates the config file, does URL routing, parses |
266
|
|
|
* meta headers, processes Markdown, does Twig processing and returns |
267
|
|
|
* the rendered contents. |
268
|
|
|
* |
269
|
|
|
* @return string rendered Pico contents |
270
|
|
|
* @throws Exception thrown when a not recoverable error occurs |
271
|
|
|
*/ |
272
|
|
|
public function run() |
|
|
|
|
273
|
|
|
{ |
274
|
|
|
// lock Pico |
275
|
|
|
$this->locked = true; |
276
|
|
|
|
277
|
|
|
// load plugins |
278
|
|
|
$this->loadPlugins(); |
279
|
|
|
$this->triggerEvent('onPluginsLoaded', array(&$this->plugins)); |
280
|
|
|
|
281
|
|
|
// load config |
282
|
|
|
$this->loadConfig(); |
283
|
|
|
$this->triggerEvent('onConfigLoaded', array(&$this->config)); |
284
|
|
|
|
285
|
|
|
// check content dir |
286
|
|
|
if (!is_dir($this->getConfig('content_dir'))) { |
287
|
|
|
throw new RuntimeException('Invalid content directory "' . $this->getConfig('content_dir') . '"'); |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
// evaluate request url |
291
|
|
|
$this->evaluateRequestUrl(); |
292
|
|
|
$this->triggerEvent('onRequestUrl', array(&$this->requestUrl)); |
293
|
|
|
|
294
|
|
|
// discover requested file |
295
|
|
|
$this->discoverRequestFile(); |
296
|
|
|
$this->triggerEvent('onRequestFile', array(&$this->requestFile)); |
297
|
|
|
|
298
|
|
|
// load raw file content |
299
|
|
|
$this->triggerEvent('onContentLoading', array(&$this->requestFile)); |
300
|
|
|
|
301
|
|
|
if (file_exists($this->requestFile)) { |
302
|
|
|
$this->rawContent = $this->loadFileContent($this->requestFile); |
303
|
|
|
} else { |
304
|
|
|
$this->triggerEvent('on404ContentLoading', array(&$this->requestFile)); |
305
|
|
|
|
306
|
|
|
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); |
307
|
|
|
$this->rawContent = $this->load404Content($this->requestFile); |
308
|
|
|
|
309
|
|
|
$this->triggerEvent('on404ContentLoaded', array(&$this->rawContent)); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
$this->triggerEvent('onContentLoaded', array(&$this->rawContent)); |
313
|
|
|
|
314
|
|
|
// parse file meta |
315
|
|
|
$headers = $this->getMetaHeaders(); |
316
|
|
|
|
317
|
|
|
$this->triggerEvent('onMetaParsing', array(&$this->rawContent, &$headers)); |
318
|
|
|
$this->meta = $this->parseFileMeta($this->rawContent, $headers); |
319
|
|
|
$this->triggerEvent('onMetaParsed', array(&$this->meta)); |
320
|
|
|
|
321
|
|
|
// register parsedown |
322
|
|
|
$this->triggerEvent('onParsedownRegistration'); |
323
|
|
|
$this->registerParsedown(); |
324
|
|
|
|
325
|
|
|
// parse file content |
326
|
|
|
$this->triggerEvent('onContentParsing', array(&$this->rawContent)); |
327
|
|
|
|
328
|
|
|
$this->content = $this->prepareFileContent($this->rawContent, $this->meta); |
329
|
|
|
$this->triggerEvent('onContentPrepared', array(&$this->content)); |
330
|
|
|
|
331
|
|
|
$this->content = $this->parseFileContent($this->content); |
332
|
|
|
$this->triggerEvent('onContentParsed', array(&$this->content)); |
333
|
|
|
|
334
|
|
|
// read pages |
335
|
|
|
$this->triggerEvent('onPagesLoading'); |
336
|
|
|
|
337
|
|
|
$this->readPages(); |
338
|
|
|
$this->sortPages(); |
339
|
|
|
$this->discoverCurrentPage(); |
340
|
|
|
|
341
|
|
|
$this->triggerEvent('onPagesLoaded', array( |
342
|
|
|
&$this->pages, |
343
|
|
|
&$this->currentPage, |
344
|
|
|
&$this->previousPage, |
345
|
|
|
&$this->nextPage |
346
|
|
|
)); |
347
|
|
|
|
348
|
|
|
// register twig |
349
|
|
|
$this->triggerEvent('onTwigRegistration'); |
350
|
|
|
$this->registerTwig(); |
351
|
|
|
|
352
|
|
|
// render template |
353
|
|
|
$this->twigVariables = $this->getTwigVariables(); |
354
|
|
|
if (isset($this->meta['template']) && $this->meta['template']) { |
355
|
|
|
$templateName = $this->meta['template']; |
356
|
|
|
} else { |
357
|
|
|
$templateName = 'index'; |
358
|
|
|
} |
359
|
|
|
if (file_exists($this->getThemesDir() . $this->getConfig('theme') . '/' . $templateName . '.twig')) { |
360
|
|
|
$templateName .= '.twig'; |
361
|
|
|
} else { |
362
|
|
|
$templateName .= '.html'; |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
$this->triggerEvent('onPageRendering', array(&$this->twig, &$this->twigVariables, &$templateName)); |
366
|
|
|
|
367
|
|
|
$output = $this->twig->render($templateName, $this->twigVariables); |
368
|
|
|
$this->triggerEvent('onPageRendered', array(&$output)); |
369
|
|
|
|
370
|
|
|
return $output; |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* Loads plugins from Pico::$pluginsDir in alphabetical order |
375
|
|
|
* |
376
|
|
|
* Plugin files MAY be prefixed by a number (e.g. 00-PicoDeprecated.php) |
377
|
|
|
* to indicate their processing order. Plugins without a prefix will be |
378
|
|
|
* loaded last. If you want to use a prefix, you MUST consider the |
379
|
|
|
* following directives: |
380
|
|
|
* - 00 to 19: Reserved |
381
|
|
|
* - 20 to 39: Low level code helper plugins |
382
|
|
|
* - 40 to 59: Plugins manipulating routing or the pages array |
383
|
|
|
* - 60 to 79: Plugins hooking into template or markdown parsing |
384
|
|
|
* - 80 to 99: Plugins using the `onPageRendered` event |
385
|
|
|
* |
386
|
|
|
* @see Pico::getPlugin() |
387
|
|
|
* @see Pico::getPlugins() |
388
|
|
|
* @return void |
389
|
|
|
* @throws RuntimeException thrown when a plugin couldn't be loaded |
390
|
|
|
*/ |
391
|
|
|
protected function loadPlugins() |
392
|
|
|
{ |
393
|
|
|
$this->plugins = array(); |
394
|
|
|
$pluginFiles = $this->getFiles($this->getPluginsDir(), '.php'); |
395
|
|
|
foreach ($pluginFiles as $pluginFile) { |
396
|
|
|
require_once($pluginFile); |
397
|
|
|
|
398
|
|
|
$className = preg_replace('/^[0-9]+-/', '', basename($pluginFile, '.php')); |
399
|
|
|
if (class_exists($className)) { |
400
|
|
|
// class name and file name can differ regarding case sensitivity |
401
|
|
|
$plugin = new $className($this); |
402
|
|
|
$className = get_class($plugin); |
403
|
|
|
|
404
|
|
|
$this->plugins[$className] = $plugin; |
405
|
|
|
} else { |
|
|
|
|
406
|
|
|
// TODO: breaks backward compatibility |
407
|
|
|
//throw new RuntimeException("Unable to load plugin '".$className."'"); |
|
|
|
|
408
|
|
|
} |
409
|
|
|
} |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
/** |
413
|
|
|
* Returns the instance of a named plugin |
414
|
|
|
* |
415
|
|
|
* Plugins SHOULD implement {@link PicoPluginInterface}, but you MUST NOT |
416
|
|
|
* rely on it. For more information see {@link PicoPluginInterface}. |
417
|
|
|
* |
418
|
|
|
* @see Pico::loadPlugins() |
419
|
|
|
* @see Pico::getPlugins() |
420
|
|
|
* @param string $pluginName name of the plugin |
421
|
|
|
* @return object instance of the plugin |
422
|
|
|
* @throws RuntimeException thrown when the plugin wasn't found |
423
|
|
|
*/ |
424
|
|
|
public function getPlugin($pluginName) |
425
|
|
|
{ |
426
|
|
|
if (isset($this->plugins[$pluginName])) { |
427
|
|
|
return $this->plugins[$pluginName]; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
throw new RuntimeException("Missing plugin '" . $pluginName . "'"); |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* Returns all loaded plugins |
435
|
|
|
* |
436
|
|
|
* @see Pico::loadPlugins() |
437
|
|
|
* @see Pico::getPlugin() |
438
|
|
|
* @return object[]|null |
439
|
|
|
*/ |
440
|
|
|
public function getPlugins() |
441
|
|
|
{ |
442
|
|
|
return $this->plugins; |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
/** |
446
|
|
|
* Loads the config.php from Pico::$configDir |
447
|
|
|
* |
448
|
|
|
* @see Pico::setConfig() |
449
|
|
|
* @see Pico::getConfig() |
450
|
|
|
* @return void |
451
|
|
|
*/ |
452
|
|
|
protected function loadConfig() |
453
|
|
|
{ |
454
|
|
|
$config = null; |
455
|
|
|
if (file_exists($this->getConfigDir() . 'config.php')) { |
456
|
|
|
require($this->getConfigDir() . 'config.php'); |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
$defaultConfig = array( |
460
|
|
|
'site_title' => 'Pico', |
461
|
|
|
'base_url' => '', |
462
|
|
|
'rewrite_url' => null, |
463
|
|
|
'theme' => 'default', |
464
|
|
|
'date_format' => '%D %T', |
465
|
|
|
'twig_config' => array('cache' => false, 'autoescape' => false, 'debug' => false), |
466
|
|
|
'pages_order_by' => 'alpha', |
467
|
|
|
'pages_order' => 'asc', |
468
|
|
|
'content_dir' => null, |
469
|
|
|
'content_ext' => '.md', |
470
|
|
|
'timezone' => '' |
471
|
|
|
); |
472
|
|
|
|
473
|
|
|
$this->config = is_array($this->config) ? $this->config : array(); |
474
|
|
|
$this->config += is_array($config) ? $config + $defaultConfig : $defaultConfig; |
475
|
|
|
|
476
|
|
|
if (empty($this->config['base_url'])) { |
477
|
|
|
$this->config['base_url'] = $this->getBaseUrl(); |
478
|
|
|
} else { |
479
|
|
|
$this->config['base_url'] = rtrim($this->config['base_url'], '/') . '/'; |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
if ($this->config['rewrite_url'] === null) { |
483
|
|
|
$this->config['rewrite_url'] = $this->isUrlRewritingEnabled(); |
484
|
|
|
} |
485
|
|
|
|
486
|
|
|
if (empty($this->config['content_dir'])) { |
487
|
|
|
// try to guess the content directory |
488
|
|
|
if (is_dir($this->getRootDir() . 'content')) { |
489
|
|
|
$this->config['content_dir'] = $this->getRootDir() . 'content/'; |
490
|
|
|
} else { |
491
|
|
|
$this->config['content_dir'] = $this->getRootDir() . 'content-sample/'; |
492
|
|
|
} |
493
|
|
|
} else { |
494
|
|
|
$this->config['content_dir'] = $this->getAbsolutePath($this->config['content_dir']); |
|
|
|
|
495
|
|
|
} |
496
|
|
|
|
497
|
|
|
if (empty($this->config['timezone'])) { |
498
|
|
|
// explicitly set a default timezone to prevent a E_NOTICE |
499
|
|
|
// when no timezone is set; the `date_default_timezone_get()` |
500
|
|
|
// function always returns a timezone, at least UTC |
501
|
|
|
$this->config['timezone'] = date_default_timezone_get(); |
502
|
|
|
} |
503
|
|
|
date_default_timezone_set($this->config['timezone']); |
504
|
|
|
} |
505
|
|
|
|
506
|
|
|
/** |
507
|
|
|
* Sets Pico's config before calling Pico::run() |
508
|
|
|
* |
509
|
|
|
* This method allows you to modify Pico's config without creating a |
510
|
|
|
* {@path "config/config.php"} or changing some of its variables before |
511
|
|
|
* Pico starts processing. |
512
|
|
|
* |
513
|
|
|
* You can call this method between {@link Pico::__construct()} and |
514
|
|
|
* {@link Pico::run()} only. Options set with this method cannot be |
515
|
|
|
* overwritten by {@path "config/config.php"}. |
516
|
|
|
* |
517
|
|
|
* @see Pico::loadConfig() |
518
|
|
|
* @see Pico::getConfig() |
519
|
|
|
* @param array $config array with config variables |
520
|
|
|
* @return void |
521
|
|
|
* @throws LogicException thrown if Pico already started processing |
522
|
|
|
*/ |
523
|
|
|
public function setConfig(array $config) |
524
|
|
|
{ |
525
|
|
|
if ($this->locked) { |
526
|
|
|
throw new LogicException("You cannot modify Pico's config after processing has started"); |
527
|
|
|
} |
528
|
|
|
|
529
|
|
|
$this->config = $config; |
530
|
|
|
} |
531
|
|
|
|
532
|
|
|
/** |
533
|
|
|
* Returns either the value of the specified config variable or |
534
|
|
|
* the config array |
535
|
|
|
* |
536
|
|
|
* @see Pico::setConfig() |
537
|
|
|
* @see Pico::loadConfig() |
538
|
|
|
* @param string $configName optional name of a config variable |
539
|
|
|
* @return mixed returns either the value of the named config |
540
|
|
|
* variable, null if the config variable doesn't exist or the config |
541
|
|
|
* array if no config name was supplied |
542
|
|
|
*/ |
543
|
|
|
public function getConfig($configName = null) |
544
|
|
|
{ |
545
|
|
|
if ($configName !== null) { |
546
|
|
|
return isset($this->config[$configName]) ? $this->config[$configName] : null; |
547
|
|
|
} else { |
548
|
|
|
return $this->config; |
549
|
|
|
} |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
/** |
553
|
|
|
* Evaluates the requested URL |
554
|
|
|
* |
555
|
|
|
* Pico 1.0 uses the `QUERY_STRING` routing method (e.g. `/pico/?sub/page`) |
556
|
|
|
* to support SEO-like URLs out-of-the-box with any webserver. You can |
557
|
|
|
* still setup URL rewriting (e.g. using `mod_rewrite` on Apache) to |
558
|
|
|
* basically remove the `?` from URLs, but your rewritten URLs must follow |
559
|
|
|
* the new `QUERY_STRING` principles. URL rewriting requires some special |
560
|
|
|
* configuration on your webserver, but this should be "basic work" for |
561
|
|
|
* any webmaster... |
562
|
|
|
* |
563
|
|
|
* Pico 0.9 and older required Apache with `mod_rewrite` enabled, thus old |
564
|
|
|
* plugins, templates and contents may require you to enable URL rewriting |
565
|
|
|
* to work. If you're upgrading from Pico 0.9, you will probably have to |
566
|
|
|
* update your rewriting rules. |
567
|
|
|
* |
568
|
|
|
* We recommend you to use the `link` filter in templates to create |
569
|
|
|
* internal links, e.g. `{{ "sub/page"|link }}` is equivalent to |
570
|
|
|
* `{{ base_url }}/sub/page` and `{{ base_url }}?sub/page`, depending on |
571
|
|
|
* enabled URL rewriting. In content files you can use the `%base_url%` |
572
|
|
|
* variable; e.g. `%base_url%?sub/page` will be replaced accordingly. |
573
|
|
|
* |
574
|
|
|
* @see Pico::getRequestUrl() |
575
|
|
|
* @return void |
576
|
|
|
*/ |
577
|
|
|
protected function evaluateRequestUrl() |
|
|
|
|
578
|
|
|
{ |
579
|
|
|
// use QUERY_STRING; e.g. /pico/?sub/page |
580
|
|
|
// if you want to use rewriting, you MUST make your rules to |
581
|
|
|
// rewrite the URLs to follow the QUERY_STRING method |
582
|
|
|
// |
583
|
|
|
// Note: you MUST NOT call the index page with /pico/?someBooleanParameter; |
584
|
|
|
// use /pico/?someBooleanParameter= or /pico/?index&someBooleanParameter instead |
585
|
|
|
$pathComponent = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : ''; |
586
|
|
|
if (($pathComponentLength = strpos($pathComponent, '&')) !== false) { |
587
|
|
|
$pathComponent = substr($pathComponent, 0, $pathComponentLength); |
588
|
|
|
} |
589
|
|
|
$this->requestUrl = (strpos($pathComponent, '=') === false) ? rawurldecode($pathComponent) : ''; |
590
|
|
|
$this->requestUrl = trim($this->requestUrl, '/'); |
591
|
|
|
} |
592
|
|
|
|
593
|
|
|
/** |
594
|
|
|
* Returns the URL where a user requested the page |
595
|
|
|
* |
596
|
|
|
* @see Pico::evaluateRequestUrl() |
597
|
|
|
* @return string|null request URL |
598
|
|
|
*/ |
599
|
|
|
public function getRequestUrl() |
600
|
|
|
{ |
601
|
|
|
return $this->requestUrl; |
602
|
|
|
} |
603
|
|
|
|
604
|
|
|
/** |
605
|
|
|
* Uses the request URL to discover the content file to serve |
606
|
|
|
* |
607
|
|
|
* @see Pico::getRequestFile() |
608
|
|
|
* @return void |
609
|
|
|
*/ |
610
|
|
|
protected function discoverRequestFile() |
611
|
|
|
{ |
612
|
|
|
if (empty($this->requestUrl)) { |
613
|
|
|
$this->requestFile = $this->getConfig('content_dir') . 'index' . $this->getConfig('content_ext'); |
614
|
|
|
} else { |
615
|
|
|
// prevent content_dir breakouts using malicious request URLs |
616
|
|
|
// we don't use realpath() here because we neither want to check for file existance |
617
|
|
|
// nor prohibit symlinks which intentionally point to somewhere outside the content_dir |
618
|
|
|
// it is STRONGLY RECOMMENDED to use open_basedir - always, not just with Pico! |
619
|
|
|
$requestUrl = str_replace('\\', '/', $this->requestUrl); |
620
|
|
|
$requestUrlParts = explode('/', $requestUrl); |
621
|
|
|
|
622
|
|
|
$requestFileParts = array(); |
623
|
|
|
foreach ($requestUrlParts as $requestUrlPart) { |
624
|
|
|
if (($requestUrlPart === '') || ($requestUrlPart === '.')) { |
625
|
|
|
continue; |
626
|
|
|
} elseif ($requestUrlPart === '..') { |
627
|
|
|
array_pop($requestFileParts); |
628
|
|
|
continue; |
629
|
|
|
} |
630
|
|
|
|
631
|
|
|
$requestFileParts[] = $requestUrlPart; |
632
|
|
|
} |
633
|
|
|
|
634
|
|
|
if (empty($requestFileParts)) { |
635
|
|
|
$this->requestFile = $this->getConfig('content_dir') . 'index' . $this->getConfig('content_ext'); |
636
|
|
|
return; |
637
|
|
|
} |
638
|
|
|
|
639
|
|
|
// discover the content file to serve |
640
|
|
|
// Note: $requestFileParts neither contains a trailing nor a leading slash |
641
|
|
|
$this->requestFile = $this->getConfig('content_dir') . implode('/', $requestFileParts); |
642
|
|
|
if (is_dir($this->requestFile)) { |
643
|
|
|
// if no index file is found, try a accordingly named file in the previous dir |
644
|
|
|
// if this file doesn't exist either, show the 404 page, but assume the index |
645
|
|
|
// file as being requested (maintains backward compatibility to Pico < 1.0) |
646
|
|
|
$indexFile = $this->requestFile . '/index' . $this->getConfig('content_ext'); |
647
|
|
|
if (file_exists($indexFile) || !file_exists($this->requestFile . $this->getConfig('content_ext'))) { |
648
|
|
|
$this->requestFile = $indexFile; |
649
|
|
|
return; |
650
|
|
|
} |
651
|
|
|
} |
652
|
|
|
$this->requestFile .= $this->getConfig('content_ext'); |
653
|
|
|
} |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
/** |
657
|
|
|
* Returns the absolute path to the content file to serve |
658
|
|
|
* |
659
|
|
|
* @see Pico::discoverRequestFile() |
660
|
|
|
* @return string|null file path |
661
|
|
|
*/ |
662
|
|
|
public function getRequestFile() |
663
|
|
|
{ |
664
|
|
|
return $this->requestFile; |
665
|
|
|
} |
666
|
|
|
|
667
|
|
|
/** |
668
|
|
|
* Returns the raw contents of a file |
669
|
|
|
* |
670
|
|
|
* @see Pico::getRawContent() |
671
|
|
|
* @param string $file file path |
672
|
|
|
* @return string raw contents of the file |
673
|
|
|
*/ |
674
|
|
|
public function loadFileContent($file) |
675
|
|
|
{ |
676
|
|
|
return file_get_contents($file); |
677
|
|
|
} |
678
|
|
|
|
679
|
|
|
/** |
680
|
|
|
* Returns the raw contents of the first found 404 file when traversing |
681
|
|
|
* up from the directory the requested file is in |
682
|
|
|
* |
683
|
|
|
* @see Pico::getRawContent() |
684
|
|
|
* @param string $file path to requested (but not existing) file |
685
|
|
|
* @return string raw contents of the 404 file |
686
|
|
|
* @throws RuntimeException thrown when no suitable 404 file is found |
687
|
|
|
*/ |
688
|
|
|
public function load404Content($file) |
689
|
|
|
{ |
690
|
|
|
$errorFileDir = substr($file, strlen($this->getConfig('content_dir'))); |
691
|
|
|
do { |
692
|
|
|
$errorFileDir = dirname($errorFileDir); |
693
|
|
|
$errorFile = $errorFileDir . '/404' . $this->getConfig('content_ext'); |
694
|
|
|
} while (!file_exists($this->getConfig('content_dir') . $errorFile) && ($errorFileDir !== '.')); |
695
|
|
|
|
696
|
|
|
if (!file_exists($this->getConfig('content_dir') . $errorFile)) { |
697
|
|
|
$errorFile = ($errorFileDir === '.') ? '404' . $this->getConfig('content_ext') : $errorFile; |
698
|
|
|
throw new RuntimeException('Required "' . $this->getConfig('content_dir') . $errorFile . '" not found'); |
699
|
|
|
} |
700
|
|
|
|
701
|
|
|
return $this->loadFileContent($this->getConfig('content_dir') . $errorFile); |
702
|
|
|
} |
703
|
|
|
|
704
|
|
|
/** |
705
|
|
|
* Returns the raw contents, either of the requested or the 404 file |
706
|
|
|
* |
707
|
|
|
* @see Pico::loadFileContent() |
708
|
|
|
* @see Pico::load404Content() |
709
|
|
|
* @return string|null raw contents |
710
|
|
|
*/ |
711
|
|
|
public function getRawContent() |
712
|
|
|
{ |
713
|
|
|
return $this->rawContent; |
714
|
|
|
} |
715
|
|
|
|
716
|
|
|
/** |
717
|
|
|
* Returns known meta headers and triggers the onMetaHeaders event |
718
|
|
|
* |
719
|
|
|
* Heads up! Calling this method triggers the `onMetaHeaders` event. |
720
|
|
|
* Keep this in mind to prevent a infinite loop! |
721
|
|
|
* |
722
|
|
|
* @return string[] known meta headers; the array value specifies the |
723
|
|
|
* YAML key to search for, the array key is later used to access the |
724
|
|
|
* found value |
725
|
|
|
*/ |
726
|
|
|
public function getMetaHeaders() |
727
|
|
|
{ |
728
|
|
|
$headers = array( |
729
|
|
|
'title' => 'Title', |
730
|
|
|
'description' => 'Description', |
731
|
|
|
'author' => 'Author', |
732
|
|
|
'date' => 'Date', |
733
|
|
|
'robots' => 'Robots', |
734
|
|
|
'template' => 'Template' |
735
|
|
|
); |
736
|
|
|
|
737
|
|
|
$this->triggerEvent('onMetaHeaders', array(&$headers)); |
738
|
|
|
return $headers; |
739
|
|
|
} |
740
|
|
|
|
741
|
|
|
/** |
742
|
|
|
* Parses the file meta from raw file contents |
743
|
|
|
* |
744
|
|
|
* Meta data MUST start on the first line of the file, either opened and |
745
|
|
|
* closed by `---` or C-style block comments (deprecated). The headers are |
746
|
|
|
* parsed by the YAML component of the Symfony project, keys are lowered. |
747
|
|
|
* If you're a plugin developer, you MUST register new headers during the |
748
|
|
|
* `onMetaHeaders` event first. The implicit availability of headers is |
749
|
|
|
* for users and pure (!) theme developers ONLY. |
750
|
|
|
* |
751
|
|
|
* @see Pico::getFileMeta() |
752
|
|
|
* @see <http://symfony.com/doc/current/components/yaml/introduction.html> |
753
|
|
|
* @param string $rawContent the raw file contents |
754
|
|
|
* @param string[] $headers known meta headers |
755
|
|
|
* @return array parsed meta data |
756
|
|
|
* @throws \Symfony\Component\Yaml\Exception\ParseException thrown when the |
757
|
|
|
* meta data is invalid |
758
|
|
|
*/ |
759
|
|
|
public function parseFileMeta($rawContent, array $headers) |
760
|
|
|
{ |
761
|
|
|
$meta = array(); |
|
|
|
|
762
|
|
|
$pattern = "/^(\/(\*)|---)[[:blank:]]*(?:\r)?\n" |
763
|
|
|
. "(?:(.*?)(?:\r)?\n)?(?(2)\*\/|---)[[:blank:]]*(?:(?:\r)?\n|$)/s"; |
764
|
|
|
if (preg_match($pattern, $rawContent, $rawMetaMatches) && isset($rawMetaMatches[3])) { |
765
|
|
|
$yamlParser = new \Symfony\Component\Yaml\Parser(); |
766
|
|
|
$meta = $yamlParser->parse($rawMetaMatches[3]); |
767
|
|
|
$meta = ($meta !== null) ? array_change_key_case($meta, CASE_LOWER) : array(); |
768
|
|
|
|
769
|
|
|
foreach ($headers as $fieldId => $fieldName) { |
770
|
|
|
$fieldName = strtolower($fieldName); |
771
|
|
|
if (isset($meta[$fieldName])) { |
772
|
|
|
// rename field (e.g. remove whitespaces) |
773
|
|
|
if ($fieldId != $fieldName) { |
774
|
|
|
$meta[$fieldId] = $meta[$fieldName]; |
775
|
|
|
unset($meta[$fieldName]); |
776
|
|
|
} |
777
|
|
|
} elseif (!isset($meta[$fieldId])) { |
778
|
|
|
// guarantee array key existance |
779
|
|
|
$meta[$fieldId] = ''; |
780
|
|
|
} |
781
|
|
|
} |
782
|
|
|
|
783
|
|
|
if (!empty($meta['date'])) { |
784
|
|
|
$meta['time'] = strtotime($meta['date']); |
785
|
|
|
$meta['date_formatted'] = utf8_encode(strftime($this->getConfig('date_format'), $meta['time'])); |
786
|
|
|
} else { |
787
|
|
|
$meta['time'] = $meta['date_formatted'] = ''; |
788
|
|
|
} |
789
|
|
|
} else { |
790
|
|
|
// guarantee array key existance |
791
|
|
|
$meta = array_fill_keys(array_keys($headers), ''); |
792
|
|
|
$meta['time'] = $meta['date_formatted'] = ''; |
793
|
|
|
} |
794
|
|
|
|
795
|
|
|
return $meta; |
796
|
|
|
} |
797
|
|
|
|
798
|
|
|
/** |
799
|
|
|
* Returns the parsed meta data of the requested page |
800
|
|
|
* |
801
|
|
|
* @see Pico::parseFileMeta() |
802
|
|
|
* @return array|null parsed meta data |
803
|
|
|
*/ |
804
|
|
|
public function getFileMeta() |
805
|
|
|
{ |
806
|
|
|
return $this->meta; |
807
|
|
|
} |
808
|
|
|
|
809
|
|
|
/** |
810
|
|
|
* Registers the Parsedown Extra markdown parser |
811
|
|
|
* |
812
|
|
|
* @see Pico::getParsedown() |
813
|
|
|
* @return void |
814
|
|
|
*/ |
815
|
|
|
protected function registerParsedown() |
816
|
|
|
{ |
817
|
|
|
$this->parsedown = new ParsedownExtra(); |
818
|
|
|
} |
819
|
|
|
|
820
|
|
|
/** |
821
|
|
|
* Returns the Parsedown Extra markdown parser |
822
|
|
|
* |
823
|
|
|
* @see Pico::registerParsedown() |
824
|
|
|
* @return ParsedownExtra|null Parsedown Extra markdown parser |
825
|
|
|
*/ |
826
|
|
|
public function getParsedown() |
827
|
|
|
{ |
828
|
|
|
return $this->parsedown; |
829
|
|
|
} |
830
|
|
|
|
831
|
|
|
/** |
832
|
|
|
* Applies some static preparations to the raw contents of a page, |
833
|
|
|
* e.g. removing the meta header and replacing %base_url% |
834
|
|
|
* |
835
|
|
|
* @see Pico::parseFileContent() |
836
|
|
|
* @see Pico::getFileContent() |
837
|
|
|
* @param string $rawContent raw contents of a page |
838
|
|
|
* @param array $meta meta data to use for %meta.*% replacement |
839
|
|
|
* @return string contents prepared for parsing |
840
|
|
|
*/ |
841
|
|
|
public function prepareFileContent($rawContent, array $meta) |
842
|
|
|
{ |
843
|
|
|
// remove meta header |
844
|
|
|
$metaHeaderPattern = "/^(\/(\*)|---)[[:blank:]]*(?:\r)?\n" |
845
|
|
|
. "(?:(.*?)(?:\r)?\n)?(?(2)\*\/|---)[[:blank:]]*(?:(?:\r)?\n|$)/s"; |
846
|
|
|
$content = preg_replace($metaHeaderPattern, '', $rawContent, 1); |
847
|
|
|
|
848
|
|
|
// replace %site_title% |
849
|
|
|
$content = str_replace('%site_title%', $this->getConfig('site_title'), $content); |
850
|
|
|
|
851
|
|
|
// replace %base_url% |
852
|
|
|
if ($this->isUrlRewritingEnabled()) { |
853
|
|
|
// always use `%base_url%?sub/page` syntax for internal links |
854
|
|
|
// we'll replace the links accordingly, depending on enabled rewriting |
855
|
|
|
$content = str_replace('%base_url%?', $this->getBaseUrl(), $content); |
856
|
|
|
} else { |
857
|
|
|
// actually not necessary, but makes the URL look a little nicer |
858
|
|
|
$content = str_replace('%base_url%?', $this->getBaseUrl() . '?', $content); |
859
|
|
|
} |
860
|
|
|
$content = str_replace('%base_url%', rtrim($this->getBaseUrl(), '/'), $content); |
861
|
|
|
|
862
|
|
|
// replace %theme_url% |
863
|
|
|
$themeUrl = $this->getBaseUrl() . basename($this->getThemesDir()) . '/' . $this->getConfig('theme'); |
864
|
|
|
$content = str_replace('%theme_url%', $themeUrl, $content); |
865
|
|
|
|
866
|
|
|
// replace %meta.*% |
867
|
|
|
if (!empty($meta)) { |
868
|
|
|
$metaKeys = $metaValues = array(); |
869
|
|
|
foreach ($meta as $metaKey => $metaValue) { |
870
|
|
|
if (is_scalar($metaValue) || ($metaValue === null)) { |
871
|
|
|
$metaKeys[] = '%meta.' . $metaKey . '%'; |
872
|
|
|
$metaValues[] = strval($metaValue); |
873
|
|
|
} |
874
|
|
|
} |
875
|
|
|
$content = str_replace($metaKeys, $metaValues, $content); |
876
|
|
|
} |
877
|
|
|
|
878
|
|
|
return $content; |
879
|
|
|
} |
880
|
|
|
|
881
|
|
|
/** |
882
|
|
|
* Parses the contents of a page using ParsedownExtra |
883
|
|
|
* |
884
|
|
|
* @see Pico::prepareFileContent() |
885
|
|
|
* @see Pico::getFileContent() |
886
|
|
|
* @param string $content raw contents of a page (Markdown) |
887
|
|
|
* @return string parsed contents (HTML) |
888
|
|
|
*/ |
889
|
|
|
public function parseFileContent($content) |
890
|
|
|
{ |
891
|
|
|
if ($this->parsedown === null) { |
892
|
|
|
throw new LogicException("Unable to parse file contents: Parsedown instance wasn't registered yet"); |
893
|
|
|
} |
894
|
|
|
|
895
|
|
|
return $this->parsedown->text($content); |
896
|
|
|
} |
897
|
|
|
|
898
|
|
|
/** |
899
|
|
|
* Returns the cached contents of the requested page |
900
|
|
|
* |
901
|
|
|
* @see Pico::prepareFileContent() |
902
|
|
|
* @see Pico::parseFileContent() |
903
|
|
|
* @return string|null parsed contents |
904
|
|
|
*/ |
905
|
|
|
public function getFileContent() |
906
|
|
|
{ |
907
|
|
|
return $this->content; |
908
|
|
|
} |
909
|
|
|
|
910
|
|
|
/** |
911
|
|
|
* Reads the data of all pages known to Pico |
912
|
|
|
* |
913
|
|
|
* The page data will be an array containing the following values: |
914
|
|
|
* |
915
|
|
|
* | Array key | Type | Description | |
916
|
|
|
* | -------------- | ------ | ---------------------------------------- | |
917
|
|
|
* | id | string | relative path to the content file | |
918
|
|
|
* | url | string | URL to the page | |
919
|
|
|
* | title | string | title of the page (YAML header) | |
920
|
|
|
* | description | string | description of the page (YAML header) | |
921
|
|
|
* | author | string | author of the page (YAML header) | |
922
|
|
|
* | time | string | timestamp derived from the Date header | |
923
|
|
|
* | date | string | date of the page (YAML header) | |
924
|
|
|
* | date_formatted | string | formatted date of the page | |
925
|
|
|
* | raw_content | string | raw, not yet parsed contents of the page | |
926
|
|
|
* | meta | string | parsed meta data of the page | |
927
|
|
|
* |
928
|
|
|
* @see Pico::sortPages() |
929
|
|
|
* @see Pico::getPages() |
930
|
|
|
* @return void |
931
|
|
|
*/ |
932
|
|
|
protected function readPages() |
933
|
|
|
{ |
934
|
|
|
$this->pages = array(); |
935
|
|
|
$files = $this->getFiles($this->getConfig('content_dir'), $this->getConfig('content_ext'), Pico::SORT_NONE); |
936
|
|
|
foreach ($files as $i => $file) { |
937
|
|
|
// skip 404 page |
938
|
|
|
if (basename($file) === '404' . $this->getConfig('content_ext')) { |
939
|
|
|
unset($files[$i]); |
940
|
|
|
continue; |
941
|
|
|
} |
942
|
|
|
|
943
|
|
|
$id = substr($file, strlen($this->getConfig('content_dir')), -strlen($this->getConfig('content_ext'))); |
944
|
|
|
|
945
|
|
|
// drop inaccessible pages (e.g. drop "sub.md" if "sub/index.md" exists) |
946
|
|
|
$conflictFile = $this->getConfig('content_dir') . $id . '/index' . $this->getConfig('content_ext'); |
947
|
|
|
if (in_array($conflictFile, $files, true)) { |
948
|
|
|
continue; |
949
|
|
|
} |
950
|
|
|
|
951
|
|
|
$url = $this->getPageUrl($id); |
952
|
|
|
if ($file != $this->requestFile) { |
953
|
|
|
$rawContent = file_get_contents($file); |
954
|
|
|
|
955
|
|
|
$headers = $this->getMetaHeaders(); |
956
|
|
|
try { |
957
|
|
|
$meta = $this->parseFileMeta($rawContent, $headers); |
958
|
|
|
} catch (\Symfony\Component\Yaml\Exception\ParseException $e) { |
959
|
|
|
$meta = $this->parseFileMeta('', $headers); |
960
|
|
|
$meta['YAML_ParseError'] = $e->getMessage(); |
961
|
|
|
} |
962
|
|
|
} else { |
963
|
|
|
$rawContent = &$this->rawContent; |
964
|
|
|
$meta = &$this->meta; |
965
|
|
|
} |
966
|
|
|
|
967
|
|
|
// build page data |
968
|
|
|
// title, description, author and date are assumed to be pretty basic data |
969
|
|
|
// everything else is accessible through $page['meta'] |
970
|
|
|
$page = array( |
971
|
|
|
'id' => $id, |
972
|
|
|
'url' => $url, |
973
|
|
|
'title' => &$meta['title'], |
974
|
|
|
'description' => &$meta['description'], |
975
|
|
|
'author' => &$meta['author'], |
976
|
|
|
'time' => &$meta['time'], |
977
|
|
|
'date' => &$meta['date'], |
978
|
|
|
'date_formatted' => &$meta['date_formatted'], |
979
|
|
|
'raw_content' => &$rawContent, |
980
|
|
|
'meta' => &$meta |
981
|
|
|
); |
982
|
|
|
|
983
|
|
|
if ($file === $this->requestFile) { |
984
|
|
|
$page['content'] = &$this->content; |
985
|
|
|
} |
986
|
|
|
|
987
|
|
|
unset($rawContent, $meta); |
988
|
|
|
|
989
|
|
|
// trigger event |
990
|
|
|
$this->triggerEvent('onSinglePageLoaded', array(&$page)); |
991
|
|
|
|
992
|
|
|
$this->pages[$id] = $page; |
993
|
|
|
} |
994
|
|
|
} |
995
|
|
|
|
996
|
|
|
/** |
997
|
|
|
* Sorts all pages known to Pico |
998
|
|
|
* |
999
|
|
|
* @see Pico::readPages() |
1000
|
|
|
* @see Pico::getPages() |
1001
|
|
|
* @return void |
1002
|
|
|
*/ |
1003
|
|
|
protected function sortPages() |
1004
|
|
|
{ |
1005
|
|
|
// sort pages |
1006
|
|
|
$order = $this->getConfig('pages_order'); |
1007
|
|
|
$alphaSortClosure = function ($a, $b) use ($order) { |
1008
|
|
|
$aSortKey = (basename($a['id']) === 'index') ? dirname($a['id']) : $a['id']; |
1009
|
|
|
$bSortKey = (basename($b['id']) === 'index') ? dirname($b['id']) : $b['id']; |
1010
|
|
|
|
1011
|
|
|
$cmp = strcmp($aSortKey, $bSortKey); |
1012
|
|
|
return $cmp * (($order === 'desc') ? -1 : 1); |
1013
|
|
|
}; |
1014
|
|
|
|
1015
|
|
|
if ($this->getConfig('pages_order_by') === 'date') { |
1016
|
|
|
// sort by date |
1017
|
|
|
uasort($this->pages, function ($a, $b) use ($alphaSortClosure, $order) { |
1018
|
|
|
if (empty($a['time']) || empty($b['time'])) { |
1019
|
|
|
$cmp = (empty($a['time']) - empty($b['time'])); |
1020
|
|
|
} else { |
1021
|
|
|
$cmp = ($b['time'] - $a['time']); |
1022
|
|
|
} |
1023
|
|
|
|
1024
|
|
|
if ($cmp === 0) { |
1025
|
|
|
// never assume equality; fallback to alphabetical order |
1026
|
|
|
return $alphaSortClosure($a, $b); |
1027
|
|
|
} |
1028
|
|
|
|
1029
|
|
|
return $cmp * (($order === 'desc') ? 1 : -1); |
1030
|
|
|
}); |
1031
|
|
|
} else { |
1032
|
|
|
// sort alphabetically |
1033
|
|
|
uasort($this->pages, $alphaSortClosure); |
1034
|
|
|
} |
1035
|
|
|
} |
1036
|
|
|
|
1037
|
|
|
/** |
1038
|
|
|
* Returns the list of known pages |
1039
|
|
|
* |
1040
|
|
|
* @see Pico::readPages() |
1041
|
|
|
* @see Pico::sortPages() |
1042
|
|
|
* @return array[]|null the data of all pages |
1043
|
|
|
*/ |
1044
|
|
|
public function getPages() |
1045
|
|
|
{ |
1046
|
|
|
return $this->pages; |
1047
|
|
|
} |
1048
|
|
|
|
1049
|
|
|
/** |
1050
|
|
|
* Walks through the list of known pages and discovers the requested page |
1051
|
|
|
* as well as the previous and next page relative to it |
1052
|
|
|
* |
1053
|
|
|
* @see Pico::getCurrentPage() |
1054
|
|
|
* @see Pico::getPreviousPage() |
1055
|
|
|
* @see Pico::getNextPage() |
1056
|
|
|
* @return void |
1057
|
|
|
*/ |
1058
|
|
|
protected function discoverCurrentPage() |
1059
|
|
|
{ |
1060
|
|
|
$pageIds = array_keys($this->pages); |
1061
|
|
|
|
1062
|
|
|
$contentDir = $this->getConfig('content_dir'); |
1063
|
|
|
$contentExt = $this->getConfig('content_ext'); |
1064
|
|
|
$currentPageId = substr($this->requestFile, strlen($contentDir), -strlen($contentExt)); |
1065
|
|
|
$currentPageIndex = array_search($currentPageId, $pageIds); |
1066
|
|
|
if ($currentPageIndex !== false) { |
1067
|
|
|
$this->currentPage = &$this->pages[$currentPageId]; |
1068
|
|
|
|
1069
|
|
|
if (($this->getConfig('order_by') === 'date') && ($this->getConfig('order') === 'desc')) { |
1070
|
|
|
$previousPageOffset = 1; |
1071
|
|
|
$nextPageOffset = -1; |
1072
|
|
|
} else { |
1073
|
|
|
$previousPageOffset = -1; |
1074
|
|
|
$nextPageOffset = 1; |
1075
|
|
|
} |
1076
|
|
|
|
1077
|
|
|
if (isset($pageIds[$currentPageIndex + $previousPageOffset])) { |
1078
|
|
|
$previousPageId = $pageIds[$currentPageIndex + $previousPageOffset]; |
1079
|
|
|
$this->previousPage = &$this->pages[$previousPageId]; |
1080
|
|
|
} |
1081
|
|
|
|
1082
|
|
|
if (isset($pageIds[$currentPageIndex + $nextPageOffset])) { |
1083
|
|
|
$nextPageId = $pageIds[$currentPageIndex + $nextPageOffset]; |
1084
|
|
|
$this->nextPage = &$this->pages[$nextPageId]; |
1085
|
|
|
} |
1086
|
|
|
} |
1087
|
|
|
} |
1088
|
|
|
|
1089
|
|
|
/** |
1090
|
|
|
* Returns the data of the requested page |
1091
|
|
|
* |
1092
|
|
|
* @see Pico::discoverCurrentPage() |
1093
|
|
|
* @return array|null page data |
1094
|
|
|
*/ |
1095
|
|
|
public function getCurrentPage() |
1096
|
|
|
{ |
1097
|
|
|
return $this->currentPage; |
1098
|
|
|
} |
1099
|
|
|
|
1100
|
|
|
/** |
1101
|
|
|
* Returns the data of the previous page relative to the page being served |
1102
|
|
|
* |
1103
|
|
|
* @see Pico::discoverCurrentPage() |
1104
|
|
|
* @return array|null page data |
1105
|
|
|
*/ |
1106
|
|
|
public function getPreviousPage() |
1107
|
|
|
{ |
1108
|
|
|
return $this->previousPage; |
1109
|
|
|
} |
1110
|
|
|
|
1111
|
|
|
/** |
1112
|
|
|
* Returns the data of the next page relative to the page being served |
1113
|
|
|
* |
1114
|
|
|
* @see Pico::discoverCurrentPage() |
1115
|
|
|
* @return array|null page data |
1116
|
|
|
*/ |
1117
|
|
|
public function getNextPage() |
1118
|
|
|
{ |
1119
|
|
|
return $this->nextPage; |
1120
|
|
|
} |
1121
|
|
|
|
1122
|
|
|
/** |
1123
|
|
|
* Registers the twig template engine |
1124
|
|
|
* |
1125
|
|
|
* This method also registers Picos core Twig filters `link` and `content` |
1126
|
|
|
* as well as Picos {@link PicoTwigExtension} Twig extension. |
1127
|
|
|
* |
1128
|
|
|
* @see Pico::getTwig() |
1129
|
|
|
* @return void |
1130
|
|
|
*/ |
1131
|
|
|
protected function registerTwig() |
1132
|
|
|
{ |
1133
|
|
|
$twigLoader = new Twig_Loader_Filesystem($this->getThemesDir() . $this->getConfig('theme')); |
1134
|
|
|
$this->twig = new Twig_Environment($twigLoader, $this->getConfig('twig_config')); |
1135
|
|
|
$this->twig->addExtension(new Twig_Extension_Debug()); |
1136
|
|
|
$this->twig->addExtension(new PicoTwigExtension($this)); |
1137
|
|
|
|
1138
|
|
|
// register link filter |
1139
|
|
|
$this->twig->addFilter(new Twig_SimpleFilter('link', array($this, 'getPageUrl'))); |
1140
|
|
|
|
1141
|
|
|
// register content filter |
1142
|
|
|
// we pass the $pages array by reference to prevent multiple parser runs for the same page |
1143
|
|
|
// this is the reason why we can't register this filter as part of PicoTwigExtension |
1144
|
|
|
$pico = $this; |
1145
|
|
|
$pages = &$this->pages; |
1146
|
|
|
$this->twig->addFilter(new Twig_SimpleFilter('content', function ($page) use ($pico, &$pages) { |
1147
|
|
|
if (isset($pages[$page])) { |
1148
|
|
|
$pageData = &$pages[$page]; |
1149
|
|
View Code Duplication |
if (!isset($pageData['content'])) { |
|
|
|
|
1150
|
|
|
$pageData['content'] = $pico->prepareFileContent($pageData['raw_content'], $pageData['meta']); |
1151
|
|
|
$pageData['content'] = $pico->parseFileContent($pageData['content']); |
1152
|
|
|
} |
1153
|
|
|
return $pageData['content']; |
1154
|
|
|
} |
1155
|
|
|
return null; |
1156
|
|
|
})); |
1157
|
|
|
} |
1158
|
|
|
|
1159
|
|
|
/** |
1160
|
|
|
* Returns the twig template engine |
1161
|
|
|
* |
1162
|
|
|
* @see Pico::registerTwig() |
1163
|
|
|
* @return Twig_Environment|null Twig template engine |
1164
|
|
|
*/ |
1165
|
|
|
public function getTwig() |
1166
|
|
|
{ |
1167
|
|
|
return $this->twig; |
1168
|
|
|
} |
1169
|
|
|
|
1170
|
|
|
/** |
1171
|
|
|
* Returns the variables passed to the template |
1172
|
|
|
* |
1173
|
|
|
* URLs and paths (namely `base_dir`, `base_url`, `theme_dir` and |
1174
|
|
|
* `theme_url`) don't add a trailing slash for historic reasons. |
1175
|
|
|
* |
1176
|
|
|
* @return array template variables |
1177
|
|
|
*/ |
1178
|
|
|
protected function getTwigVariables() |
1179
|
|
|
{ |
1180
|
|
|
$frontPage = $this->getConfig('content_dir') . 'index' . $this->getConfig('content_ext'); |
1181
|
|
|
return array( |
1182
|
|
|
'config' => $this->getConfig(), |
1183
|
|
|
'base_dir' => rtrim($this->getRootDir(), '/'), |
1184
|
|
|
'base_url' => rtrim($this->getBaseUrl(), '/'), |
1185
|
|
|
'theme_dir' => $this->getThemesDir() . $this->getConfig('theme'), |
1186
|
|
|
'theme_url' => $this->getBaseUrl() . basename($this->getThemesDir()) . '/' . $this->getConfig('theme'), |
1187
|
|
|
'rewrite_url' => $this->isUrlRewritingEnabled(), |
1188
|
|
|
'site_title' => $this->getConfig('site_title'), |
1189
|
|
|
'meta' => $this->meta, |
1190
|
|
|
'content' => $this->content, |
1191
|
|
|
'pages' => $this->pages, |
1192
|
|
|
'prev_page' => $this->previousPage, |
1193
|
|
|
'current_page' => $this->currentPage, |
1194
|
|
|
'next_page' => $this->nextPage, |
1195
|
|
|
'is_front_page' => ($this->requestFile === $frontPage), |
1196
|
|
|
); |
1197
|
|
|
} |
1198
|
|
|
|
1199
|
|
|
/** |
1200
|
|
|
* Returns the base URL of this Pico instance |
1201
|
|
|
* |
1202
|
|
|
* @return string the base url |
1203
|
|
|
*/ |
1204
|
|
|
public function getBaseUrl() |
|
|
|
|
1205
|
|
|
{ |
1206
|
|
|
$baseUrl = $this->getConfig('base_url'); |
1207
|
|
|
if (!empty($baseUrl)) { |
1208
|
|
|
return $baseUrl; |
1209
|
|
|
} |
1210
|
|
|
|
1211
|
|
|
$protocol = 'http'; |
1212
|
|
|
if (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] !== 'off')) { |
1213
|
|
|
$protocol = 'https'; |
1214
|
|
|
} elseif ($_SERVER['SERVER_PORT'] == 443) { |
1215
|
|
|
$protocol = 'https'; |
1216
|
|
|
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')) { |
1217
|
|
|
$protocol = 'https'; |
1218
|
|
|
} |
1219
|
|
|
|
1220
|
|
|
$this->config['base_url'] = |
1221
|
|
|
$protocol . "://" . $_SERVER['HTTP_HOST'] |
1222
|
|
|
. rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\') . '/'; |
1223
|
|
|
|
1224
|
|
|
return $this->getConfig('base_url'); |
1225
|
|
|
} |
1226
|
|
|
|
1227
|
|
|
/** |
1228
|
|
|
* Returns true if URL rewriting is enabled |
1229
|
|
|
* |
1230
|
|
|
* @return boolean true if URL rewriting is enabled, false otherwise |
1231
|
|
|
*/ |
1232
|
|
|
public function isUrlRewritingEnabled() |
|
|
|
|
1233
|
|
|
{ |
1234
|
|
|
$urlRewritingEnabled = $this->getConfig('rewrite_url'); |
1235
|
|
|
if ($urlRewritingEnabled !== null) { |
1236
|
|
|
return $urlRewritingEnabled; |
1237
|
|
|
} |
1238
|
|
|
|
1239
|
|
|
$this->config['rewrite_url'] = (isset($_SERVER['PICO_URL_REWRITING']) && $_SERVER['PICO_URL_REWRITING']); |
1240
|
|
|
return $this->getConfig('rewrite_url'); |
1241
|
|
|
} |
1242
|
|
|
|
1243
|
|
|
/** |
1244
|
|
|
* Returns the URL to a given page |
1245
|
|
|
* |
1246
|
|
|
* @param string $page identifier of the page to link to |
1247
|
|
|
* @param array|string $queryData either an array containing properties to |
1248
|
|
|
* create a URL-encoded query string from, or a already encoded string |
1249
|
|
|
* @return string URL |
1250
|
|
|
*/ |
1251
|
|
|
public function getPageUrl($page, $queryData = null) |
1252
|
|
|
{ |
1253
|
|
|
if (is_array($queryData)) { |
1254
|
|
|
$queryData = http_build_query($queryData, '', '&'); |
1255
|
|
|
} elseif (($queryData !== null) && !is_string($queryData)) { |
1256
|
|
|
throw new InvalidArgumentException( |
1257
|
|
|
'Argument 2 passed to ' . get_called_class() . '::getPageUrl() must be of the type array or string, ' |
1258
|
|
|
. (is_object($queryData) ? get_class($queryData) : gettype($queryData)) . ' given' |
1259
|
|
|
); |
1260
|
|
|
} |
1261
|
|
|
if (!empty($queryData)) { |
1262
|
|
|
$page = !empty($page) ? $page : 'index'; |
1263
|
|
|
$queryData = $this->isUrlRewritingEnabled() ? '?' . $queryData : '&' . $queryData; |
1264
|
|
|
} |
1265
|
|
|
|
1266
|
|
|
if (empty($page)) { |
1267
|
|
|
return $this->getBaseUrl() . $queryData; |
1268
|
|
|
} elseif (!$this->isUrlRewritingEnabled()) { |
1269
|
|
|
return $this->getBaseUrl() . '?' . rawurlencode($page) . $queryData; |
1270
|
|
|
} else { |
1271
|
|
|
return $this->getBaseUrl() . implode('/', array_map('rawurlencode', explode('/', $page))) . $queryData; |
1272
|
|
|
} |
1273
|
|
|
} |
1274
|
|
|
|
1275
|
|
|
/** |
1276
|
|
|
* Recursively walks through a directory and returns all containing files |
1277
|
|
|
* matching the specified file extension |
1278
|
|
|
* |
1279
|
|
|
* @param string $directory start directory |
1280
|
|
|
* @param string $fileExtension return files with the given file extension |
1281
|
|
|
* only (optional) |
1282
|
|
|
* @param int $order specify whether and how files should be |
1283
|
|
|
* sorted; use Pico::SORT_ASC for a alphabetical ascending order (this |
1284
|
|
|
* is the default behaviour), Pico::SORT_DESC for a descending order |
1285
|
|
|
* or Pico::SORT_NONE to leave the result unsorted |
1286
|
|
|
* @return array list of found files |
1287
|
|
|
*/ |
1288
|
|
|
protected function getFiles($directory, $fileExtension = '', $order = self::SORT_ASC) |
1289
|
|
|
{ |
1290
|
|
|
$directory = rtrim($directory, '/'); |
1291
|
|
|
$result = array(); |
1292
|
|
|
|
1293
|
|
|
// scandir() reads files in alphabetical order |
1294
|
|
|
$files = scandir($directory, $order); |
1295
|
|
|
$fileExtensionLength = strlen($fileExtension); |
1296
|
|
|
if ($files !== false) { |
1297
|
|
|
foreach ($files as $file) { |
1298
|
|
|
// exclude hidden files/dirs starting with a .; this also excludes the special dirs . and .. |
1299
|
|
|
// exclude files ending with a ~ (vim/nano backup) or # (emacs backup) |
1300
|
|
|
if ((substr($file, 0, 1) === '.') || in_array(substr($file, -1), array('~', '#'))) { |
1301
|
|
|
continue; |
1302
|
|
|
} |
1303
|
|
|
|
1304
|
|
|
if (is_dir($directory . '/' . $file)) { |
1305
|
|
|
// get files recursively |
1306
|
|
|
$result = array_merge($result, $this->getFiles($directory . '/' . $file, $fileExtension, $order)); |
1307
|
|
|
} elseif (empty($fileExtension) || (substr($file, -$fileExtensionLength) === $fileExtension)) { |
1308
|
|
|
$result[] = $directory . '/' . $file; |
1309
|
|
|
} |
1310
|
|
|
} |
1311
|
|
|
} |
1312
|
|
|
|
1313
|
|
|
return $result; |
1314
|
|
|
} |
1315
|
|
|
|
1316
|
|
|
/** |
1317
|
|
|
* Makes a relative path absolute to Pico's root dir |
1318
|
|
|
* |
1319
|
|
|
* This method also guarantees a trailing slash. |
1320
|
|
|
* |
1321
|
|
|
* @param string $path relative or absolute path |
1322
|
|
|
* @return string absolute path |
1323
|
|
|
*/ |
1324
|
|
|
public function getAbsolutePath($path) |
1325
|
|
|
{ |
1326
|
|
|
if (strncasecmp(PHP_OS, 'WIN', 3) === 0) { |
1327
|
|
|
if (preg_match('/^([a-zA-Z]:\\\\|\\\\\\\\)/', $path) !== 1) { |
1328
|
|
|
$path = $this->getRootDir() . $path; |
1329
|
|
|
} |
1330
|
|
|
} else { |
1331
|
|
|
if (substr($path, 0, 1) !== '/') { |
1332
|
|
|
$path = $this->getRootDir() . $path; |
1333
|
|
|
} |
1334
|
|
|
} |
1335
|
|
|
return rtrim($path, '/\\') . '/'; |
1336
|
|
|
} |
1337
|
|
|
|
1338
|
|
|
/** |
1339
|
|
|
* Triggers events on plugins which implement PicoPluginInterface |
1340
|
|
|
* |
1341
|
|
|
* Deprecated events (as used by plugins not implementing |
1342
|
|
|
* {@link PicoPluginInterface}) are triggered by {@link PicoDeprecated}. |
1343
|
|
|
* |
1344
|
|
|
* @see PicoPluginInterface |
1345
|
|
|
* @see AbstractPicoPlugin |
1346
|
|
|
* @see DummyPlugin |
1347
|
|
|
* @param string $eventName name of the event to trigger |
1348
|
|
|
* @param array $params optional parameters to pass |
1349
|
|
|
* @return void |
1350
|
|
|
*/ |
1351
|
|
|
protected function triggerEvent($eventName, array $params = array()) |
1352
|
|
|
{ |
1353
|
|
|
if (!empty($this->plugins)) { |
1354
|
|
|
foreach ($this->plugins as $plugin) { |
1355
|
|
|
// only trigger events for plugins that implement PicoPluginInterface |
1356
|
|
|
// deprecated events (plugins for Pico 0.9 and older) will be triggered by `PicoDeprecated` |
1357
|
|
|
if (is_a($plugin, 'PicoPluginInterface')) { |
1358
|
|
|
$plugin->handleEvent($eventName, $params); |
1359
|
|
|
} |
1360
|
|
|
} |
1361
|
|
|
} |
1362
|
|
|
} |
1363
|
|
|
} |
1364
|
|
|
|
Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable: