DirectoryListing   C
last analyzed

Complexity

Total Complexity 57

Size/Duplication

Total Lines 441
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
dl 0
loc 441
rs 5.04
c 0
b 0
f 0
wmc 57
lcom 1
cbo 2

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getCurrentRealDirectory() 0 20 4
A getRootDirectory() 0 4 1
A getUrl() 0 10 2
A __construct() 0 5 1
A footer() 0 12 1
A header() 0 12 1
A getAssetPath() 0 18 4
A buildBreadcrumbHtml() 0 31 5
A fetchThemeDirectory() 0 19 6
B buildPreviews() 0 65 9
A loadConfig() 0 24 4
A buildFooterReadme() 0 16 3
A buildHeaderReadme() 0 20 4
A buildReadmeHtml() 0 18 4
A buildCssAssets() 0 28 5
A buildJavascriptAssets() 0 11 1
A sanitizeUrl() 0 10 2

How to fix   Complexity   

Complex Class

Complex classes like DirectoryListing often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DirectoryListing, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Potherca\Apache\Modules\AutoIndex;
4
5
use League\CommonMark\CommonMarkConverter;
6
7
/**
8
 * Note: The variable naming scheme used in this code is an adaption of
9
 * Systems Hungarian which is explained at http://pother.ca/VariableNamingConvention/
10
 */
11
class DirectoryListing
12
{
13
    ////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\
14
    const DIRECTORY_NAME = 'Directory_Listing_Theme';
15
16
    private $m_aEnvironment = [];
17
18
    // @FIXME: Instead of a hard-coded list of themes, the contents of ./vendor/bower-asset/bootswatch should be used
19
    private $m_aBootswatchThemes = [
20
        'Cerulean',
21
        'Cosmo',
22
        'Cyborg',
23
        'Darkly',
24
        'Flatly',
25
        'Journal',
26
        'Lumen',
27
        'Paper',
28
        'Readable',
29
        'Sandstone',
30
        'Simplex',
31
        'Slate',
32
        'Spacelab',
33
        'Superhero',
34
        'United',
35
        'Yeti',
36
        /* "backward compatible" look from screenshot-01.png */
37
        'Foo',
38
    ];
39
40
    private $m_aConfig = [
41
        "theme" => "default",
42
        "readmePrefixes" => ["readme", "README", "ReadMe"],
43
        "readmeExtensions" => [".html", ".md", ".txt"],
44
        "assets" => []
45
    ];
46
47
    /**
48
     * @var array
49
     */
50
    private $m_aUserInput = [];
51
52
    private $m_bUseBootstrap = false;
53
54
    private $m_sConfigFile = 'config.json';
55
56
    //////////////////////////// SETTERS AND GETTERS \\\\\\\\\\\\\\\\\\\\\\\\\\\
57
    /**
58
     * @return array
59
     */
60
    private function getCurrentRealDirectory()
61
    {
62
        static $sCurrentRealDir;
63
64
        if ($sCurrentRealDir === null) {
65
66
            if (isset($this->m_aEnvironment['WEB_ROOT'])) {
67
                $sRoot = $this->m_aEnvironment['WEB_ROOT'];
68
            } elseif (is_dir($this->m_aEnvironment['DOCUMENT_ROOT'])) {
69
                $sRoot = $this->m_aEnvironment['DOCUMENT_ROOT'];
70
            } else {
71
                $sRoot = dirname(dirname($this->m_aEnvironment['SCRIPT_FILENAME']));
72
            }
73
74
            $sCurrentWebDir = $this->m_aEnvironment['REQUEST_URI'];
75
            $sCurrentRealDir = $this->sanitizeUrl($sRoot . $sCurrentWebDir);
76
        }
77
78
        return $sCurrentRealDir;
79
    }
80
81
    private function getRootDirectory()
82
    {
83
        return realpath(__DIR__ . '/../');
84
    }
85
86
    /**
87
     * @return string
88
     */
89
    private function getUrl()
90
    {
91
        static $sUrl;
92
93
        if ($sUrl === null) {
94
            $sUrl = $this->sanitizeUrl($this->m_aEnvironment['REQUEST_URI']);
95
        }
96
97
        return $sUrl;
98
    }
99
100
    //////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
101
    final public function  __construct(array $p_aEnvironment, array $p_aUserInput)
102
    {
103
        $this->m_aEnvironment = $p_aEnvironment;
104
        $this->m_aUserInput = $p_aUserInput;
105
    }
106
107
    final public function footer(TemplateInterface $p_oTemplate)
108
    {
109
        $aConfig = $this->loadConfig();
110
111
        $aContext = [];
112
        $aContext['aJsAssets'] = $this->buildJavascriptAssets($aConfig);
113
        $aContext['aPreviews'] = $this->buildPreviews();
114
        $aContext['sFooterReadme'] = $this->buildFooterReadme($aConfig);
115
        $aContext['sSignature'] = $this->m_aEnvironment['SERVER_SIGNATURE'];
116
117
        return $p_oTemplate->buildBottom($aContext);
118
    }
119
120
    final public function header(TemplateInterface $p_oTemplate)
121
    {
122
        $aConfig = $this->loadConfig();
123
124
        $aContext = [];
125
        $aContext['aCssAssets'] = $this->buildCssAssets($aConfig);
126
        $aContext['sIndex'] = 'Index of ' . $this->getUrl();
127
        $aContext['sIndexHtml'] = $this->buildBreadcrumbHtml();
128
        $aContext['sReadmeHtml'] = $this->buildHeaderReadme($aConfig);
129
130
        return $p_oTemplate->buildTop($aContext);
131
    }
132
133
    ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
134
    private function getAssetPath($p_sFile, $p_sThemeDir)
135
    {
136
137
        $aParts = explode('.', $p_sFile);
138
        array_splice($aParts, -1, 0, array('min'));
139
140
        if (is_file($this->getRootDirectory() . '/' . $p_sThemeDir . $p_sFile)) {
141
            $sPath = $p_sThemeDir . $p_sFile;
142
        } elseif (is_file($this->getRootDirectory() . '/' . $p_sThemeDir . implode('.', $aParts))) {
143
            $sPath = $p_sThemeDir . implode('.', $aParts);
144
        } elseif (is_file($this->getRootDirectory() . '/' . '/themes/default/' . $p_sFile)) {
145
            $sPath = '/themes/default/' . $p_sFile;
146
        } else {
147
            throw new \Exception('Could not find asset "' . $p_sFile . '"');
148
        }
149
150
        return '/' . self::DIRECTORY_NAME . '/' . $sPath;
151
    }
152
153
    /**
154
     * @TODO: Move breadcrumb HTML to template
155
     *
156
     * @return string
157
     */
158
    private function buildBreadcrumbHtml()
159
    {
160
        $sIndexHtml = sprintf(
161
            '<li><a href="http://%1$s">%1$s</a></li>',
162
            $this->m_aEnvironment['SERVER_NAME']
163
        );
164
165
        $sUrl = $this->getUrl();
166
167
        if ($this->m_aEnvironment['REQUEST_URI'] !== '/') {
168
            $aParts = explode('/', trim($sUrl, '/'));
169
            $iCount = count($aParts) - 1;
170
            $sUrl = 'http://' . $this->m_aEnvironment['SERVER_NAME'];
171
172
            foreach ($aParts as $t_iIndex => $t_sPart) {
173
                if (!empty($t_sPart)) {
174
175
                    $sUrl .= '/' . urlencode($t_sPart);
176
                    $sIndexHtml .= '<li><a';
177
                    if ($t_iIndex === $iCount) {
178
                        $sIndexHtml .= ' class="active"';
179
                    } else {
180
                        $sIndexHtml .= ' class="text-muted"';
181
                    }
182
                    $sIndexHtml .= ' href="' . $sUrl . '">' . $t_sPart . '</a></li>';
183
                }
184
            }
185
        }
186
187
        return $sIndexHtml;
188
    }
189
190
    /**
191
     * @param array $p_aConfig
192
     *
193
     * @return string
194
     *
195
     * @throws \Exception
196
     */
197
    private function fetchThemeDirectory($p_aConfig)
198
    {
199
        if (isset($this->m_aUserInput['theme'])
200
            && in_array(ucfirst($this->m_aUserInput['theme']), $this->m_aBootswatchThemes)
201
        ) {
202
            $this->m_bUseBootstrap = true;
203
            $sThemeDir = 'vendor/bower-asset/bootswatch/' . $this->m_aUserInput['theme'] . '/';
204
        } elseif ($this->m_bUseBootstrap === true
205
            && is_dir($this->getRootDirectory() . '/vendor/bower-asset/bootswatch/' . $p_aConfig['theme'])
206
        ) {
207
            $sThemeDir = 'vendor/bower-asset/bootswatch/' . $p_aConfig['theme'] . '/';
208
        } elseif (is_dir($this->getRootDirectory() . '/themes/' . $p_aConfig['theme'])) {
209
            $sThemeDir = 'themes/' . $p_aConfig['theme'] . '/';
210
        } else {
211
            throw new \Exception('Could not find theme directory "' . $p_aConfig['theme'] . '"');
212
        }
213
214
        return $sThemeDir;
215
    }
216
217
    /**
218
     * @CHECKME: Instead of using an external script to create thumbnails, images could be in-lined
219
     *        using `sprintf('<img src="data:%s;base64,%s">', $sMimeType, base64_encode(file_get_content($sFileName)));`
220
     *        This would increase page-load time (because each image would need to be opened for reading)
221
     *        but it would save thumbnails being written. The question is whether
222
     *        this is a scenario support should be added for...
223
     * @return string
224
     */
225
    private function buildPreviews()
226
    {
227
        $aPreviews = [];
228
        $sCurrentRealDir = $this->getCurrentRealDirectory();
229
230
        foreach (scandir($sCurrentRealDir) as $t_sFileName) {
231
            $aInfo = [];
232
233
            $rFileInfo = finfo_open(FILEINFO_MIME_TYPE | FILEINFO_PRESERVE_ATIME /*| FILEINFO_CONTINUE | FILEINFO_SYMLINK*/);
234
            $sMimeType = finfo_file($rFileInfo, $sCurrentRealDir . $t_sFileName);
235
            finfo_close($rFileInfo);
236
237
            $aInfo['name'] = basename($t_sFileName);
238
            $aInfo['link'] = $this->getUrl() . $t_sFileName;
239
            $aInfo['mime-type'] = $sMimeType;
240
241
            if (strpos($sMimeType, '/')) {
242
                list($aInfo['type'], $aInfo['subtype']) = explode('/', $sMimeType);
243
            } else {
244
                $aInfo['type'] = $sMimeType;
245
                $aInfo['subtype'] = '';
246
            }
247
248
            switch ($aInfo['type']) {
249
                case 'video':
250
                    $aInfo['tag'] = sprintf(
251
                        '<video src="%s" preload="%s" controls></video>', // @CHECKME: Add poster="%s" ?
252
                        $aInfo['link'],
253
                        'metadata'
254
                    );
255
                break;
256
257
                case 'audio':
258
                    $aInfo['tag'] = sprintf(
259
                        '<audio src="%s" preload="%s" controls></audio>',
260
                        $aInfo['link'],
261
                        'metadata'
262
                    );
263
                break;
264
265
                case 'image':
266
                   $aInfo['tag'] = sprintf(
267
                        '<img src="/%s/thumbnail.php?file=%s" alt="%s" />',
268
                        self::DIRECTORY_NAME,
269
                        urlencode($aInfo['link']),
270
                        $aInfo['name']
271
                    );
272
                break;
273
274
                case 'directory':
275
                case 'application':
276
                case 'text':
277
                default:
278
                    $aInfo['tag'] = sprintf(
279
                        '<p class="no-preview">No preview for %s</p>',
280
                        $aInfo['type']
281
                    );
282
                break;
283
            }
284
285
            array_push($aPreviews, $aInfo);
286
        }
287
288
        return $aPreviews;
289
    }
290
291
    private function loadConfig()
292
    {
293
        $aConfig = array_merge([], $this->m_aConfig);
294
295
        if (is_file($this->m_sConfigFile)) {
296
297
            $this->m_bUseBootstrap = true;
298
299
            if (!is_readable($this->m_sConfigFile)) {
300
                throw new \Exception("Could not read configuration file");
301
            } else {
302
                $sFileContent = file_get_contents($this->m_sConfigFile);
303
                $aJsonConfig = json_decode($sFileContent, true);
304
                if (is_array($aJsonConfig)) {
305
                    $aConfig = array_merge(
306
                        $aConfig,
307
                        $aJsonConfig
308
                    );
309
                }
310
            }
311
        }
312
313
        return $aConfig;
314
    }
315
316
    private function buildFooterReadme($aConfig)
317
    {
318
        $sReadme = '';
319
        foreach ($aConfig['readmeExtensions'] as $t_sExtension) {
320
            $sReadMeFileName = 'readme-footer' . $t_sExtension;
321
            $sReadMeFilePath = urldecode($this->m_aEnvironment['DOCUMENT_ROOT'] . $this->m_aEnvironment['REQUEST_URI'] . $sReadMeFileName);
322
323
            $sReadmeHtml = $this->buildReadmeHtml($sReadMeFilePath, $t_sExtension);
324
325
            if (!empty($sReadmeHtml)) {
326
                break;
327
            }
328
        }
329
330
        return $sReadme;
331
    }
332
333
    /**
334
     * @param array $aConfig
335
     *
336
     * @return array
337
     */
338
    private function buildHeaderReadme($aConfig)
339
    {
340
        $sReadmeHtml = '';
341
342
        $sCurrentRealDir = $this->getCurrentRealDirectory();
343
344
        foreach ($aConfig['readmePrefixes'] as $t_sPrefix) {
345
            foreach ($aConfig['readmeExtensions'] as $t_sExtension) {
346
                $sReadMeFileName = $t_sPrefix . $t_sExtension;
347
                $sReadMeFilePath = $sCurrentRealDir . urldecode($sReadMeFileName);
348
349
                $sReadmeHtml = $this->buildReadmeHtml($sReadMeFilePath, $t_sExtension);
350
                if (empty($sReadmeHtml) === false) {
351
                    break;
352
                }
353
            }
354
        }
355
356
        return $sReadmeHtml;
357
    }
358
359
    /**
360
     * @param $sReadMeFilePath
361
     * @param $t_sExtension
362
     *
363
     * @return string
364
     */
365
    private function buildReadmeHtml($sReadMeFilePath, $t_sExtension)
366
    {
367
        $sReadmeHtml = '';
368
369
        if (file_exists($sReadMeFilePath)) {
370
            $sReadmeContent = file_get_contents($sReadMeFilePath);
371
            if ($t_sExtension === '.md') {
372
                $converter = new CommonMarkConverter();
373
                $sReadmeHtml .= $converter->convertToHtml($sReadmeContent);
374
            } elseif ($t_sExtension === '.txt') {
375
                $sReadmeHtml .= '<div style="white-space: pre-wrap;">' . $sReadmeContent . '</div>';
376
            } else {
377
                $sReadmeHtml .= $sReadmeContent;
378
            }
379
380
        }
381
        return $sReadmeHtml;
382
    }
383
384
    /**
385
     * @param array $p_aConfig
386
     *
387
     * @return array
388
     */
389
    private function buildCssAssets(array $p_aConfig)
390
    {
391
        $sThemeDir = $this->fetchThemeDirectory($p_aConfig);
392
393
        $aAssets = [
394
            $this->getAssetPath('table.css', $sThemeDir),
395
            $this->getAssetPath('thumbnails.css', $sThemeDir),
396
        ];
397
398
        if ($this->m_bUseBootstrap === false
399
            || ($this->m_bUseBootstrap === true && $p_aConfig['theme'] !== 'default')
400
        ) {
401
            array_unshift(
402
                $aAssets,
403
                $this->getAssetPath('bootstrap.css', $sThemeDir)
404
            );
405
        }
406
407
        if ($this->m_bUseBootstrap === true) {
408
            array_unshift(
409
                $aAssets,
410
                '/' . self::DIRECTORY_NAME . '/vendor/bower-asset/bootstrap/dist/css/bootstrap.min.css',
411
                '/' . self::DIRECTORY_NAME . '/vendor/bower-asset/bootstrap/dist/css/bootstrap-theme.min.css'
412
            );
413
        }
414
415
        return $aAssets;
416
    }
417
418
    /**
419
     * @param array $p_aConfig
420
     *
421
     * @return array
422
     */
423
    private function buildJavascriptAssets($p_aConfig)
424
    {
425
        $sThemeDir = $this->fetchThemeDirectory($p_aConfig);
426
427
        $aAssets = [
428
            '/' . self::DIRECTORY_NAME . '/vendor/bower-asset/jquery/dist/jquery.js',
429
            $this->getAssetPath('functions.js', $sThemeDir),
430
        ];
431
432
        return $aAssets;
433
    }
434
435
    /**
436
     * @param $sUrl
437
     *
438
     * @return string
439
     */
440
    private function sanitizeUrl($sUrl)
441
    {
442
        $sUrl = urldecode($sUrl);
443
444
        if (strpos($sUrl, '?') !== false) {
445
            $sUrl = substr($sUrl, 0, strpos($sUrl, '?'));
446
        }
447
448
        return $sUrl;
449
    }
450
451
}
452
/*EOF*/
453