Completed
Push — master ( 37082b...b3f134 )
by Tyler
36:04
created

Pico::getPageUrl()   C

Complexity

Conditions 10
Paths 31

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 23
rs 5.6534
cc 10
eloc 16
nc 31
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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()
0 ignored issues
show
Coding Style introduced by
run uses the super-global variable $_SERVER which is generally not recommended.

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:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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 {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
406
                // TODO: breaks backward compatibility
407
                //throw new RuntimeException("Unable to load plugin '".$className."'");
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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']);
0 ignored issues
show
Bug introduced by
It seems like $this->config['content_dir'] can also be of type boolean; however, Pico::getAbsolutePath() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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()
0 ignored issues
show
Coding Style introduced by
evaluateRequestUrl uses the super-global variable $_SERVER which is generally not recommended.

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:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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();
0 ignored issues
show
Unused Code introduced by
$meta is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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()
0 ignored issues
show
Coding Style introduced by
getBaseUrl uses the super-global variable $_SERVER which is generally not recommended.

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:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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()
0 ignored issues
show
Coding Style introduced by
isUrlRewritingEnabled uses the super-global variable $_SERVER which is generally not recommended.

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:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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