Completed
Push — master ( 527a8d...111bd3 )
by Mikael
02:27
created

CFileBasedContent::getViewDataForRoute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 13
ccs 0
cts 0
cp 0
rs 9.4285
cc 1
eloc 7
nc 1
nop 2
crap 2
1
<?php
2
3
namespace Anax\Content;
4
5
/**
6
 * Pages based on file content.
7
 */
8
class CFileBasedContent
9
{
10
    use \Anax\TConfigure,
11
        \Anax\DI\TInjectionAware;
12
13
14
15
    /**
16
     * Properties.
17
     */
18
    private $index = null;
19
    private $meta = null;
20
    private $ignoreCache = false;
21
    
22
    /**
23
     * File name pattern, all files must match this pattern and the first
24
     * numbered part is optional, the second part becomes the route.
25
     */
26
    private $filenamePattern = "#^(\d*)_*([^\.]+)\.md$#";
27
28
    /**
29
     * Internal routes that is marked as internal content routes and not
30
     * exposed as public routes.
31
     */
32
    private $internalRouteDirPattern = [
33
        "#block/#",
34
    ];
35
36
    private $internalRouteFilePattern = [
37
        "#^block[_-]{1}#",
38
        "#^_#",
39
    ];
40
41
    /**
42
     * Routes that should be used in toc.
43
     */
44
    private $allowedInTocPattern = "([\d]+_(\w)+)";
45
46
47
48
    /**
49
     * Create a breadcrumb, append slash / to all dirs.
50
     *
51
     * @param string $route      current route.
52
     *
53
     * @return array with values for the breadcrumb.
54
     */
55
    public function createBreadcrumb($route)
56
    {
57
        $breadcrumbs = [];
58
59
        while ($route !== "./" && $route !== "/") {
60
            $routeIndex = $this->mapRoute2IndexKey($route);
61
            $item["url"] = $route;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$item was never initialized. Although not strictly required by PHP, it is generally a good practice to add $item = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
62
            $item["text"] = $this->getBreadcrumbTitle($this->index[$routeIndex]["file"]);
0 ignored issues
show
Bug introduced by
The variable $item does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
63
            $breadcrumbs[] = $item;
64
            $route = dirname($route) . "/";
65
        }
66
67
        krsort($breadcrumbs);
68
        return $breadcrumbs;
69
    }
70
71
72
73
/**
74
 * Get time when the content was last updated.
75
 *
76
 * @return string with the time.
77
 */
78
/*public function PublishTime() {
79
  if(!empty($this['published'])) {
80
    return $this['published'];
81
  } else if(isset($this['updated'])) {
82
    return $this['updated'];
83
  } else {
84
    return $this['created'];
85
  } 
86
}
87
*/
88
/**
89
 * Get the action for latest updated of the content.
90
 *
91
 * @return string with the time.
92
 */
93
/*public function PublishAction() {
94
  if(!empty($this['published'])) {
95
    //return t('Published');
96
    return t('Last updated');
97
  } else if(isset($this['updated'])) {
98
    return t('Updated');
99
  } else {
100
    return t('Created');
101
  } 
102
}
103
*/
104
105
106
107
    /**
108
     * Set default values from configuration.
109
     *
110
     * @return this.
0 ignored issues
show
Documentation introduced by
The doc-type this. could not be parsed: Unknown type name "this." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
111
     */
112
    public function setDefaultsFromConfiguration()
113
    {
114
        $this->ignoreCache = isset($this->config["ignoreCache"])
115
            ? $this->config["ignoreCache"]
116
            : $this->ignoreCache;
117
118
        return $this;
119
    }
120
121
122
123
    /**
124
     * Should the cache be used or ignored.
125
     *
126
     * @param boolean $use true to use the cache or false to ignore the cache
127
     *
128
     * @return this.
0 ignored issues
show
Documentation introduced by
The doc-type this. could not be parsed: Unknown type name "this." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
129
     */
130
    public function useCache($use)
131
    {
132
        $this->ignoreCache = !$use;
133
134
        return $this;
135
    }
136
137
138
139
    /**
140
     * Get the index as an array.
141
     *
142
     * @return array as index.
143
     */
144
    public function getIndex()
145
    {
146
        return $this->loadIndex();
147
    }
148
149
150
151
    /**
152
     * Create the index of all content into an array.
153
     *
154
     * @return array as index.
155
     */
156 View Code Duplication
    private function loadIndex()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
157
    {
158
        if ($this->index) {
159
            return $this->index;
160
        }
161
162
        $key = $this->di->cache->createKey(__CLASS__, "index");
163
        $this->index = $this->di->cache->get($key);
164
165
        if (!$this->index || $this->ignoreCache) {
166
            $this->index = $this->createIndex();
167
            $this->di->cache->put($key, $this->index);
168
        }
169
170
        return $this->index;
171
    }
172
173
174
175
    /**
176
     * Check if a filename is to be marked as an internal route..
177
     *
178
     * @param string $filepath as the basepath (routepart) to the file.
179
     *
180
     * @return boolean true if the route content is internal, else false
181
     */
182
    private function isInternalRoute($filepath)
183
    {
184
        foreach ($this->internalRouteDirPattern as $pattern) {
185
            if (preg_match($pattern, $filepath)) {
186
                return true;
187
            }
188
        }
189
190
        $filename = basename($filepath);
191
        foreach ($this->internalRouteFilePattern as $pattern) {
192
            if (preg_match($pattern, $filename)) {
193
                return true;
194
            }
195
        }
196
197
        return false;
198
    }
199
200
201
202
    /**
203
     * Check if filepath should be used as part of toc.
204
     *
205
     * @param string $filepath as the basepath (routepart) to the file.
206
     *
207
     * @return boolean true if the route content shoul dbe in toc, else false
208
     */
209
    private function allowInToc($filepath)
210
    {
211
        return (boolean) preg_match($this->allowedInTocPattern, $filepath);
212
    }
213
214
215
216
    /**
217
     * Generate an index from the directory structure.
218
     *
219
     * @return array as index for all content files.
220
     */
221
    private function createIndex()
222
    {
223
        $basepath   = $this->config["basepath"];
224
        $pattern    = $this->config["pattern"];
225
        $path       = "$basepath/$pattern";
226
227
        $index = [];
228
        foreach (glob_recursive($path) as $file) {
229
            $filepath = substr($file, strlen($basepath) + 1);
230
231
            // Find content files
232
            $matches = [];
233
            preg_match($this->filenamePattern, basename($filepath), $matches);
234
            $dirpart = dirname($filepath) . "/";
235
            if ($dirpart === "./") {
236
                $dirpart = null;
237
            }
238
            $key = $dirpart . $matches[2];
239
            
240
            // Create level depending on the file id
241
            $id = $matches[1];
242
            $level = 2;
243 View Code Duplication
            if ($id % 100 === 0) {
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...
244
                $level = 0;
245
            } elseif ($id % 10 === 0) {
246
                $level = 1;
247
            }
248
249
            $index[$key] = [
250
                "file"     => $filepath,
251
                "section"  => $matches[1],
252
                "level"    => $level,
253
                "internal" => $this->isInternalRoute($filepath),
254
                "tocable"  => $this->allowInToc($filepath),
255
            ];
256
        }
257
258
        return $index;
259
    }
260
261
262
263
    /**
264
     * Create the index of all meta content into an array.
265
     *
266
     * @return array as index.
267
     */
268 View Code Duplication
    private function loadMetaIndex()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
269
    {
270
        if ($this->meta) {
271
            return $this->meta;
272
        }
273
274
        $key = $this->di->cache->createKey(__CLASS__, "meta");
275
        $this->meta = $this->di->cache->get($key);
276
277
        if (!$this->meta || $this->ignoreCache) {
278
            $this->meta = $this->createMetaIndex();
279
            $this->di->cache->put($key, $this->meta);
280
        }
281
282
        return $this->meta;
283
    }
284
285
286
287
    /**
288
     * Generate an index for meta files.
289
     *
290
     * @return array as table of content.
291
     */
292
    private function createMetaIndex()
293
    {
294
        $basepath = $this->config["basepath"];
295
        $filter   = $this->config["metafilter"];
296
        $meta     = $this->config["meta"];
297
        $path     = "$basepath/$meta";
298
299
        $meta = [];
300
        foreach (glob_recursive($path) as $file) {
301
            $filepath = substr($file, strlen($basepath) + 1);
302
            
303
            $src = file_get_contents($file);
304
            $filtered = $this->di->get("textFilter")->parse($src, $filter);
305
306
            $key = dirname($filepath);
307
            $meta[$key] = $filtered->frontmatter;
308
309
            // Add Toc to the data array
310
            $meta[$key]["__toc__"] = $this->createBaseRouteToc(dirname($filepath));
311
        }
312
313
        return $meta;
314
    }
315
316
317
318
    /**
319
     * Get a reference to meta data for specific route.
320
     *
321
     * @param string $route current route used to access page.
322
     *
323
     * @return array as table of content.
324
     */
325
    private function getMetaForRoute($route)
326
    {
327
        $base = dirname($route);
328
        return isset($this->meta[$base])
329
            ? $this->meta[$base]
330
            : [];
331
    }
332
333
334
335
    /**
336
     * Get the frontmatter of a document.
337
     *
338
     * @param string $file to get frontmatter from.
339
     *
340
     * @return array as frontmatter.
341
     */
342
    private function getFrontmatter($file)
343
    {
344
        $basepath = $this->config["basepath"];
345
        $filter   = $this->config["textfilter"];
346
347
        $path = $basepath . "/" . $file;
348
        $src = file_get_contents($path);
349
        $filtered = $this->di->textFilter->parse($src, $filter);
350
        return $filtered->frontmatter;
351
    }
352
353
354
355
    /**
356
     * Get the title of a document to use for breadcrumb.
357
     *
358
     * @param string $file to get title from.
359
     *
360
     * @return string as the breadcrumb title.
361
     */
362
    private function getBreadcrumbTitle($file)
363
    {
364
        $frontmatter = $this->getFrontmatter($file);
365
366
        $title = $frontmatter["title"];
367
        if (isset($frontmatter["titleBreadcrumb"])) {
368
            $title = $frontmatter["titleBreadcrumb"];
369
        }
370
371
        return $title;
372
    }
373
374
375
376
    /**
377
     * Create a table of content for routes at particular level.
378
     *
379
     * @param string $route base route to use.
380
     *
381
     * @return array as the toc.
382
     */
383
    private function createBaseRouteToc($route)
384
    {
385
        $toc = [];
386
        $len = strlen($route);
387
388
        foreach ($this->index as $key => $value) {
389
            if (substr($key, 0, $len) === $route) {
390
                if ($value["internal"] === false
391
                    && $value["tocable"] === true) {
392
                    $toc[$key] = $value;
393
                    
394
                    $frontm = $this->getFrontmatter($value["file"]);
395
                    $toc[$key]["title"] = $frontm["title"];
396
                    $toc[$key]["sectionHeader"] = isset($frontm["sectionHeader"]) ? $frontm["sectionHeader"] : null;
397
                    $toc[$key]["linkable"] = isset($frontm["linkable"]) ? $frontm["linkable"] : null;
398
                }
399
            }
400
        };
401
402
        return $toc;
403
    }
404
405
406
407
    /**
408
     * Map the route to the correct key in the index.
409
     *
410
     * @param string $route current route used to access page.
411
     *
412
     * @return string as key or false if no match.
413
     */
414
    private function mapRoute2IndexKey($route)
415
    {
416
        $route = rtrim($route, "/");
417
418
        if (key_exists($route, $this->index)) {
419
            return $route;
420
        } elseif (empty($route) && key_exists("index", $this->index)) {
421
            return "index";
422
        } elseif (key_exists($route . "/index", $this->index)) {
423
            return "$route/index";
424
        }
425
426
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Anax\Content\CFileBasedContent::mapRoute2IndexKey of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
427
    }
428
429
430
431
    /**
432
     * Map the route to the correct entry in the index.
433
     *
434
     * @param string $route current route used to access page.
435
     *
436
     * @return array as the matched route.
437
     */
438
    private function mapRoute2Index($route)
439
    {
440
        $routeIndex = $this->mapRoute2IndexKey($route);
441
442
        if ($routeIndex) {
443
            return [$routeIndex, $this->index[$routeIndex]];
444
        }
445
446
        throw new \Anax\Exception\NotFoundException(t("The route '!ROUTE' does not exists in the index.", ["!ROUTE" => $route]));
447
    }
448
449
450
451
    /**
452
     * Get view by mergin information from meta and frontmatter.
453
     *
454
     * @param string $route       current route used to access page.
455
     * @param array  $frontmatter for the content.
456
     * @param string $key         for the view to retrive.
457
     * @param string $distinct    how to merge the array.
458
     *
459
     * @return array with data to add as view.
460
     */
461
    private function getView($route, $frontmatter, $key, $distinct = true)
462
    {
463
        $view = [];
464
465
        // From meta frontmatter
466
        $meta = $this->getMetaForRoute($route);
467
        if (isset($meta[$key])) {
468
            $view = $meta[$key];
469
        }
470
471
        // From document frontmatter
472
        if (isset($frontmatter[$key])) {
473
            if ($distinct) {
474
                $view = array_merge_recursive_distinct($view, $frontmatter[$key]);
475
            } else {
476
                $view = array_merge($view, $frontmatter[$key]);
477
            }
478
        }
479
480
        return $view;
481
    }
482
483
484
485
    /**
486
     * Get details on extra views.
487
     *
488
     * @param string $route       current route used to access page.
489
     * @param array  $frontmatter for the content.
490
     *
491
     * @return array with page data to send to view.
492
     */
493
    private function getViews($route, $frontmatter)
494
    {
495
        // Arrange data into views
496
        $views = $this->getView($route, $frontmatter, "views", false);
497
498
        // Set defaults
499
        if (!isset($views["main"]["template"])) {
500
            $views["main"]["template"] = $this->config["template"];
501
        }
502
        if (!isset($views["main"]["data"])) {
503
            $views["main"]["data"] = [];
504
        }
505
506
        // Merge remaining frontmatter into view main data.
507
        $data = $this->getMetaForRoute($route);
508
        unset($data["__toc__"]);
509
        unset($data["views"]);
510
        unset($frontmatter["views"]);
511
512
        if ($frontmatter) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $frontmatter of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
513
            $data = array_merge_recursive_distinct($data, $frontmatter);
514
        }
515
        $views["main"]["data"] = array_merge_recursive_distinct($views["main"]["data"], $data);
516
517
        return $views;
518
    }
519
520
521
522
    /**
523
     * Load extra info into views based of meta information provided in each
524
     * view.
525
     *
526
     * @param string $view    with current settings.
527
     * @param string $route   to load view from.
528
     * @param string $baseurl to prepend relative urls.
529
     *
530
     * @return array with view details.
531
     */
532
    private function getAdditionalViewDataForRoute($view, $route, $baseurl)
533
    {
534
        // From configuration
535
         $filter = $this->config["textfilter"];
536
537
        // Get filtered content from route
538
        list(, , $filtered) =
539
            $this->mapRoute2Content($route);
540
541
        // Merge view data with document frontmatter
542
        if (!empty($filtered->frontmatter)) {
543
            $view["data"] = array_merge_recursive_distinct($view["data"], $filtered->frontmatter);
0 ignored issues
show
Documentation introduced by
$view['data'] is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
544
        }
545
546
        // Do phase 2 processing
547
        $new = $this->di->get("textFilter")->parse($filtered->text, $filter);
548
        $this->addBaseurl2AnchorUrls($new, $baseurl);
549
        $view["data"]["content"] = $new->text;
550
551
        return $view;
552
553
    }
554
555
556
557
    /**
558
     * Load extra info into views based of meta information provided in each
559
     * view.
560
     *
561
     * @param string $view    with current settings.
0 ignored issues
show
Bug introduced by
There is no parameter named $view. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
562
     * @param string $route   to load view from.
563
     * @param string $baseurl to prepend relative urls.
564
     *
565
     * @return array with view data details.
566
     */
567
    private function getViewDataForRoute($route, $baseurl)
568
    {
569
        // Get filtered content from route
570
        list(, , $filtered) =
571
            $this->mapRoute2Content($route);
572
573
        $data = $filtered->frontmatter;
574
        $this->addBaseurl2AnchorUrls($filtered, $baseurl);
575
        $data["content"] = $filtered->text;
576
577
        return $data;
578
579
    }
580
581
582
583
    /**
584
     * Order and limit toc items.
585
     *
586
     * @param string &$toc  array with current toc.
587
     * @param string &$meta on how to order and limit toc.
588
     *
589
     * @return void.
0 ignored issues
show
Documentation introduced by
The doc-type void. could not be parsed: Unknown type name "void." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
590
     */
591
    private function orderAndlimitToc(&$toc, &$meta)
592
    {
593
        $defaults = [
594
            "items" => 7,
595
            "offset" => 0,
596
            "orderby" => "section",
597
            "orderorder" => "asc",
598
        ];
599
        $options = array_merge($defaults, $meta);
600
        $orderby = $options["orderby"];
601
        $order   = $options["orderorder"];
602
603
        $meta["totalItems"] = count($toc);
604
605
        // TODO support pagination by adding entries to $meta
606
607
        uksort($toc, function ($a, $b) use ($toc, $orderby, $order) {
608
            $a = $toc[$a][$orderby];
609
            $b = $toc[$b][$orderby];
610
611
            if ($order == "asc") {
612
                return strcmp($a, $b);
613
            }
614
            return strcmp($b, $a);
615
        });
616
617
        $toc = array_slice($toc, $options["offset"], $options["items"]);
618
        $meta["displayedItems"] = count($toc);
619
    }
620
621
622
623
    /**
624
     * Find next and previous links of current content.
625
     *
626
     * @param string $routeIndex target route to find next and previous for.
627
     *
628
     * @return array with next and previous if found.
629
     */
630
    private function findNextAndPrevious($routeIndex)
631
    {
632
        $key = dirname($routeIndex);
633
        if (!isset($this->meta[$key]["__toc__"])) {
634
            return [null, null];
635
        }
636
637
        $toc = $this->meta[$key]["__toc__"];
638
        if (!isset($toc[$routeIndex])) {
639
            return [null, null];
640
        }
641
642
        $index2Key = array_keys($toc);
643
        $keys = array_flip($index2Key);
644
        $values = array_values($toc);
645
        $count = count($keys);
646
647
        $current = $keys[$routeIndex];
648
        $previous = null;
649 View Code Duplication
        for ($i = $current - 1; $i >= 0; $i--) {
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...
650
            $isSectionHeader = $values[$i]["sectionHeader"];
651
            $isInternal = $values[$i]["internal"];
652
            if ($isSectionHeader || $isInternal) {
653
                continue;
654
            }
655
            $previous = $values[$i];
656
            $previous["route"] = $index2Key[$i];
657
            break;
658
        }
659
        
660
        $next = null;
661 View Code Duplication
        for ($i = $current + 1; $i < $count; $i++) {
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...
662
            $isSectionHeader = $values[$i]["sectionHeader"];
663
            $isInternal = $values[$i]["internal"];
664
            if ($isSectionHeader || $isInternal) {
665
                continue;
666
            }
667
            $next = $values[$i];
668
            $next["route"] = $index2Key[$i];
669
            break;
670
        }
671
672
        return [$next, $previous];
673
    }
674
675
676
677
    /**
678
     * Load extra info into views based of meta information provided in each
679
     * view.
680
     *
681
     * @param array  &$views     with all views.
682
     * @param string $route      current route
683
     * @param string $routeIndex route with appended /index
684
     *
685
     * @throws NotFoundException when mapping can not be done.
686
     *
687
     * @return void.
0 ignored issues
show
Documentation introduced by
The doc-type void. could not be parsed: Unknown type name "void." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
688
     */
689
    private function loadAdditionalContent(&$views, $route, $routeIndex)
690
    {
691
        foreach ($views as $id => $view) {
692
            $meta = isset($view["data"]["meta"])
693
                ? $view["data"]["meta"]
694
                : null;
695
696
            if (is_array($meta)) {
697
                switch ($meta["type"]) {
698
                    case "article-toc":
699
                        $content = $views["main"]["data"]["content"];
700
                        $views[$id]["data"]["articleToc"] = $this->di->textFilter->createToc($content);
701
                        break;
702
703
                    case "breadcrumb":
704
                        $views[$id]["data"]["breadcrumb"] = $this->createBreadcrumb($route);
705
                        break;
706
707 View Code Duplication
                    case "next-previous":
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...
708
                        $baseRoute = dirname($routeIndex);
0 ignored issues
show
Unused Code introduced by
$baseRoute 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...
709
                        list($next, $previous) = $this->findNextAndPrevious($routeIndex);
710
                        $views[$id]["data"]["next"] = $next;
711
                        $views[$id]["data"]["previous"] = $previous;
712
                        break;
713
714
                    case "single": // OBSOLETE
715
                    case "content":
716
                        $baseurl = $this->getBaseurl($views, $id);
717
                        $views[$id] = $this->getAdditionalViewDataForRoute($view, $meta["route"], $baseurl);
718
                        break;
719
720
                    case "columns":
721
                        $baseurl = $this->getBaseurl($views, $id);
722
                        $columns = $meta["columns"];
723
                        foreach ($columns as $key => $value) {
724
                            $data = $this->getViewDataForRoute($value["route"], $baseurl);
725
                            $columns[$key] = $data;
726
                        }
727
                        $views[$id]["data"]["columns"] = $columns;
728
                        break;
729
730 View Code Duplication
                    case "toc":
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...
731
                        $baseRoute = dirname($routeIndex);
732
                        $toc = $this->meta[$baseRoute]["__toc__"];
733
                        $this->orderAndlimitToc($toc, $meta);
734
                        $views[$id]["data"]["toc"] = $toc;
735
                        $views[$id]["data"]["meta"] = $meta;
736
                        break;
737
738
                    default:
739
                        throw new Exception(t("Unsupported data/meta/type for additional content."));
740
                }
741
            }
742
        }
743
    }
744
745
746
747
    /**
748
     * Get basurl from view, if it is defined.
749
     *
750
     * @param array  $views   data for all views.
751
     * @param string $current for current view if any.
752
     *
753
     * @return string | null as baseurl.
754
     */
755
    private function getBaseurl($views, $current = null)
756
    {
757
        $baseurl = isset($views["main"]["data"]["baseurl"])
758
            ? $views["main"]["data"]["baseurl"]
759
            : null;
760
761
        if ($current) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $current of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
762
            $baseurl = isset($views[$current]["data"]["baseurl"])
763
                ? $views[$current]["data"]["baseurl"]
764
                : $baseurl;
765
        }
766
767
        return $baseurl;
768
    }
769
770
771
772
    /**
773
     * Parse text, find and update all a href to use baseurl.
774
     *
775
     * @param object &$filtered with text and excerpt to process.
776
     * @param string $baseurl   add as baseurl for all relative urls.
777
     *
778
     * @return void.
0 ignored issues
show
Documentation introduced by
The doc-type void. could not be parsed: Unknown type name "void." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
779
     */
780
    private function addBaseurl2AnchorUrls(&$filtered, $baseurl)
781
    {
782
        $textf  = $this->di->get("textFilter");
783
        $url    = $this->di->get("url");
784
785
        // Use callback to url->create() instead of string concat
786
        $callback = function ($route) use ($url, $baseurl) {
787
            return $url->create($route, $baseurl);
788
        };
789
790
        $filtered->text =
791
            $textf->addBaseurlToRelativeLinks($filtered->text, $baseurl, $callback);
792
    }
793
794
795
796
    /**
797
     * Load content file and frontmatter, this is the first time we process
798
     * the content.
799
     *
800
     * @param string $key     array with all views.
801
     * @param string $content array with all views.
802
     *
803
     * @throws NotFoundException when mapping can not be done.
804
     *
805
     * @return void.
0 ignored issues
show
Documentation introduced by
The doc-type void. could not be parsed: Unknown type name "void." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
806
     */
807
    private function loadFileContent($key, $content)
808
    {
809
        // Settings from config
810
        $basepath = $this->config["basepath"];
811
        $filter   = $this->config["textfilter-frontmatter"];
812
813
        // Whole path to file
814
        $path = $basepath . "/" . $content["file"];
815
        $content["path"] = $path;
816
817
        // Load content from file
818
        if (!is_file($path)) {
819
            $msg = t("The content '!ROUTE' does not exists as a file '!FILE'.", ["!ROUTE" => $key, "!FILE" => $path]);
820
            throw new \Anax\Exception\NotFoundException($msg);
821
        }
822
823
        // Get filtered content
824
        $src = file_get_contents($path);
825
        $filtered = $this->di->get("textFilter")->parse($src, $filter);
826
827
        return [$content, $filtered];
828
    }
829
830
831
832
    /**
833
     * Look up the route in the index and use that to retrieve the filtered
834
     * content.
835
     *
836
     * @param string $route to look up.
837
     *
838
     * @return array with content and filtered version.
839
     */
840
    public function mapRoute2Content($route)
841
    {
842
        // Look it up in the index
843
        list($keyIndex, $content) = $this->mapRoute2Index($route);
844
        list($content, $filtered) = $this->loadFileContent($keyIndex, $content);
845
846
        return [$keyIndex, $content, $filtered];
847
    }
848
849
850
851
    /**
852
     * Look up the route in the index and use that to retrieve the filtered
853
     * content.
854
     *
855
     * @param array  $content   to process.
856
     * @param object &$filtered to use for settings.
857
     *
858
     * @return array with content and filtered version.
859
     */
860
     public function processContentPhaseTwo($content, &$filtered)
861
     {
862
        // From configuration
863
         $filter = $this->config["textfilter"];
864
         $revisionStart = $this->config["revision-history"]["start"];
865
         $revisionEnd   = $this->config["revision-history"]["end"];
866
         $revisionClass = $this->config["revision-history"]["class"];
867
         
868
         $textFilter = $this->di->get("textFilter");
869
         $text = $filtered->text;
870
871
         // Check if revision history is to be included
872
         if (isset($content["views"]["main"]["data"]["revision"])) {
873
             $text = $textFilter->addRevisionHistory(
874
                 $text,
875
                 $content["views"]["main"]["data"]["revision"],
876
                 $revisionStart,
877
                 $revisionEnd,
878
                 $revisionClass
879
             );
880
         }
881
882
         // Get new filtered content
883
         $new = $textFilter->parse($text, $filter);
884
         $filtered->text = $new->text;
885
         if ($filtered->frontmatter) {
886
             $filtered->frontmatter = array_merge_recursive_distinct($filtered->frontmatter, $new->frontmatter);
887
         } else {
888
             $filtered->frontmatter = $new->frontmatter;
889
         }
890
891
         // Update all anchor urls to use baseurl, needs info about baseurl
892
         // from merged frontmatter
893
         $baseurl = $this->getBaseurl($content["views"]);
894
         $this->addBaseurl2AnchorUrls($filtered, $baseurl);
895
896
         // Add excerpt and hasMore, if available
897
         $textFilter->addExcerpt($filtered);
898
    }
899
900
901
902
    /**
903
     * Map url to content if such mapping can be done, exclude internal routes.
904
     *
905
     * @param string $route optional route to look up.
906
     *
907
     * @return object with content and filtered version.
908
     */
909
    public function contentForRoute($route = null)
910
    {
911
        $content = $this->contentForInternalRoute($route);
912
        if ($content->internal === true) {
913
            $msg = t("The content '!ROUTE' does not exists as a public route.", ["!ROUTE" => $route]);
914
            throw new \Anax\Exception\NotFoundException($msg);
915
        }
916
917
        return $content;
918
    }
919
920
921
922
    /**
923
     * Map url to content, even internal content, if such mapping can be done.
924
     *
925
     * @param string $route route to look up.
926
     *
927
     * @return object with content and filtered version.
928
     */
929
    public function createContentForInternalRoute($route)
930
    {
931
        // Load index and map route to content
932
        $this->loadIndex();
933
        $this->loadMetaIndex();
934
        list($routeIndex, $content, $filtered) = $this->mapRoute2Content($route);
935
936
        // Create and arrange the content as views, merge with .meta,
937
        // frontmatter is complete.
938
        $content["views"] = $this->getViews($routeIndex, $filtered->frontmatter);
939
940
        // Do process content step two when all frontmatter is included.
941
        $this->processContentPhaseTwo($content, $filtered);
942
        
943
        // Set details of content
944
        $content["views"]["main"]["data"]["content"] = $filtered->text;
945
        $content["views"]["main"]["data"]["excerpt"] = $filtered->excerpt;
946
        $this->loadAdditionalContent($content["views"], $route, $routeIndex);
947
948
        // TODO Should not supply all frontmatter to theme, only the
949
        // parts valid to the index template. Separate that data into own
950
        // holder in frontmatter. Do not include whole frontmatter? Only
951
        // on debg?
952
        $content["frontmatter"] = $filtered->frontmatter;
953
954
        return (object) $content;
955
    }
956
957
958
959
    /**
960
     * Map url to content, even internal content, if such mapping can be done.
961
     *
962
     * @param string $route optional route to look up.
963
     *
964
     * @return object with content and filtered version.
965
     */
966
    public function contentForInternalRoute($route = null)
967
    {
968
        // Get the route
969
        if (is_null($route)) {
970
            $route = $this->di->get("request")->getRoute();
971
        }
972
973
        // Check cache for content or create cached version of content
974
        $slug = $this->di->get("url")->slugify($route);
975
        $key = $this->di->cache->createKey(__CLASS__, "route-$slug");
976
        $content = $this->di->cache->get($key);
977
978
        if (!$content || $this->ignoreCache) {
979
            $content = $this->createContentForInternalRoute($route);
980
            $this->di->cache->put($key, $content);
981
        }
982
983
        return $content;
984
    }
985
}
986