Issues (25)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

Features/Acf/OptionPages.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
// TODO adjust readme
4
// TODO [minor] Overview Page + setting for redirect
5
// TODO [minor] add custom post type label
6
// TODO [minor] add notice for meta keys that are too long (unsolved ACF / WordPress issue)
7
// TODO [minor] remove / don't create empty (parent) option pages
8
9
namespace Flynt\Features\Acf;
10
11
use ACFComposer;
12
use Flynt\ComponentManager;
13
use Flynt\Features\AdminNotices\AdminNoticeManager;
14
use Flynt\Features\CustomPostTypes\CustomPostTypeRegister;
15
use Flynt\Utils\Feature;
16
use Flynt\Utils\FileLoader;
17
use Flynt\Utils\StringHelpers;
18
19
class OptionPages
20
{
21
    const FIELD_GROUPS_DIR = '/config/fieldGroups';
22
23
    const OPTION_TYPES = [
24
        'translatableOptions' => [
25
            'title' => 'Translatable Options',
26
            'icon' => 'dashicons-translation',
27
            'translatable' => true
28
        ],
29
        'globalOptions' => [
30
            'title' => 'Global Options',
31
            'icon' => 'dashicons-admin-site',
32
            'translatable' => false
33
        ]
34
    ];
35
36
    const OPTION_CATEGORIES = [
37
        'component' => [
38
            'title' => 'Component',
39
            'icon' => 'dashicons-editor-table',
40
            'showType' => true
41
        ],
42
        'customPostType' => [
43
            'title' => 'Custom Post Type',
44
            'icon' => 'dashicons-palmtree',
45
            'showType' => true
46
            // 'label' => [ 'labels', 'menu_item' ], // TODO add this functionality
47
        ],
48
        'feature' => [
49
            'title' => 'Feature',
50
            'icon' => 'dashicons-carrot',
51
            'showType' => true
52
        ]
53
    ];
54
55
    const FILTER_NAMESPACES = [
56
        'component' => 'Flynt/Components',
57
        'customPostType' => 'Flynt/CustomPostTypes',
58
        'feature' => 'Flynt/Features'
59
    ];
60
61
    protected static $optionPages = [];
62
    protected static $optionTypes = [];
63
    protected static $optionCategories = [];
64
65
    public static function setup()
66
    {
67
68
        self::$optionTypes = self::OPTION_TYPES;
69
        self::$optionCategories = self::OPTION_CATEGORIES;
70
71
        self::createOptionPages();
72
73
        // Register Categories
74
        add_action('acf/init', function () {
75
            self::addComponentSubPages();
76
            self::addFeatureSubPages();
77
            self::addCustomPostTypeSubPages();
78
        });
79
80
        add_filter(
81
            'Flynt/addComponentData',
82
            ['Flynt\Features\Acf\OptionPages', 'addComponentData'],
83
            10,
84
            3
85
        );
86
87
        // Setup Flynt Non Persistent Cache
88
        wp_cache_add_non_persistent_groups('flynt');
89
    }
90
91
    public static function init()
92
    {
93
        // show (dismissible) Admin Notice if required feature is missing
94
        if (class_exists('Flynt\Features\AdminNotices\AdminNoticeManager')) {
95
            self::checkFeature('customPostType', 'flynt-custom-post-types');
96
            self::checkFeature('component', 'flynt-components');
97
        }
98
    }
99
100
    // ============
101
    // PUBLIC API
102
    // ============
103
104
    /**
105
     * Get option(s) from a sub page.
106
     *
107
     * Returns an option of a sub page. If no field name is provided it will get all option of that sub page.
108
     * Parameters are expected to be camelCase.
109
     *
110
     * @since 0.2.0 introduced as a replacement for OptionPages::getOption and OptionPages::getOptions
111
     * @since 0.2.2 added check for required hooks to have run to alert of timing issues when used incorrectly
112
     *
113
     * @param string $optionType Type of option page. Either globalOptions or translatableOptions.
114
     * @param string $optionCategory Category of option page. One of these three values: component, feature, customPostType.
115
     * @param string $subPageName Name of the sub page.
116
     * @param string $fieldName (optional) Name of the field to get.
117
     * @return mixed The value of the option or array of options. False if subpage doesn't exist or no option was found.
118
     **/
119
    public static function get($optionType, $optionCategory, $subPageName, $fieldName = null)
120
    {
121
        if (!self::checkRequiredHooks($optionType, $optionCategory, $subPageName, $fieldName)) {
122
            return false;
123
        }
124
125
        // convert parameters
126
        $optionType = lcfirst($optionType);
127
        $optionCategory = ucfirst($optionCategory);
128
        $subPageName = ucfirst($subPageName);
129
130
        if (!isset(self::$optionTypes[$optionType])) {
131
            return false;
132
        }
133
134
        $prefix = implode('', [$optionType, $optionCategory, $subPageName, '_']);
135
        $options = self::getOptionFields(self::$optionTypes[$optionType]['translatable']);
136
        $options = self::collectOptionsWithPrefix($options, $prefix);
137
138
        if (isset($fieldName)) {
139
            $fieldName = lcfirst($fieldName);
140
            return array_key_exists($fieldName, $options) ? $options[$fieldName] : false;
141
        }
142
        return $options;
143
    }
144
145
    // ============
146
    // COMPONENTS
147
    // ============
148
149
    public static function addComponentData($data, $parentData, $config)
150
    {
151
        // get fields for this component
152
        $options = array_reduce(array_keys(self::$optionTypes), function ($carry, $optionType) use ($config) {
153
            return array_merge($carry, self::get($optionType, 'Component', $config['name']));
154
        }, []);
155
156
        // don't overwrite existing data
157
        return array_merge($options, $data);
158
    }
159
160
    public static function addComponentSubPages()
161
    {
162
        // load fields.json if it exists
163
        $componentManager = ComponentManager::getInstance();
164
        $components = $componentManager->getAll();
165
166
        foreach ($components as $name => $path) {
167
            self::createSubPage('component', $name);
168
        }
169
    }
170
171
    // ==================
172
    // CUSTOM POST TYPES
173
    // ==================
174
175
    public static function addCustomPostTypeSubPages()
176
    {
177
        $customPostTypes = CustomPostTypeRegister::getAll();
178
179
        foreach ($customPostTypes as $name => $config) {
180
            self::createSubPage('customPostType', $name);
181
        }
182
    }
183
184
    // ========
185
    // FEATURES
186
    // ========
187
188
    public static function addFeatureSubPages()
189
    {
190
        foreach (Feature::getFeatures() as $handle => $config) {
191
            self::createSubPage('feature', $config['name']);
192
        }
193
    }
194
195
    // ========
196
    // GENERAL
197
    // ========
198
199
    protected static function createOptionPages()
200
    {
201
202
        foreach (self::$optionTypes as $optionType => $option) {
203
            $title = _x($option['title'], 'title', 'flynt-starter-theme');
204
            $slug = ucfirst($optionType);
205
206
            acf_add_options_page([
207
                'page_title'  => $title,
208
                'menu_title'  => $title,
209
                'redirect'    => true,
210
                'menu_slug'   => $slug,
211
                'icon_url'    => $option['icon']
212
            ]);
213
214
            self::$optionPages[$optionType] = [
215
                'menu_slug' => $slug,
216
                'menu_title' => $title
217
            ];
218
        }
219
220
        add_action('current_screen', function ($currentScreen) {
221
            foreach (self::$optionTypes as $optionType => $option) {
222
                $isTranslatable = $option['translatable'];
223
                $toplevelPageId = 'toplevel_page_' . $optionType;
224
                $menuTitle = self::$optionPages[$optionType]['menu_title'];
225
                $subPageId = sanitize_title($menuTitle) . '_page_' . $optionType;
226
                $isCurrentPage = StringHelpers::startsWith($toplevelPageId, $currentScreen->id)
227
                || StringHelpers::startsWith($subPageId, $currentScreen->id);
228
229
                if (!$isTranslatable && $isCurrentPage) {
230
                    // set acf field values to default language
231
                    add_filter('acf/settings/current_language', 'Flynt\Features\Acf\OptionPages::getDefaultAcfLanguage', 101);
232
233
                    // hide language selector in admin bar
234
                    add_action('wp_before_admin_bar_render', function () {
235
                        $adminBar = $GLOBALS['wp_admin_bar'];
236
                        $adminBar->remove_menu('WPML_ALS');
237
                    });
238
                }
239
            }
240
        });
241
    }
242
243
    protected static function createSubPage($type, $name)
244
    {
245
        $namespace = self::FILTER_NAMESPACES[$type];
246
        $name = ucfirst($name);
247
        foreach (self::$optionTypes as $optionType => $option) {
248
            $filterName = "{$namespace}/{$name}/Fields/" . ucfirst($optionType);
249
            $fields = apply_filters($filterName, []);
250
            if (!empty($fields)) {
251
                self::addOptionSubPage(
252
                    $type,
253
                    $name,
254
                    $optionType,
255
                    $fields
256
                );
257
            }
258
        }
259
    }
260
261
    protected static function addOptionSubPage($optionCategoryName, $subPageName, $optionType, $fields)
262
    {
263
        $prettySubPageName = StringHelpers::splitCamelCase($subPageName);
264
        $optionCategorySettings = self::$optionCategories[$optionCategoryName];
265
        $iconClasses = 'flynt-submenu-item dashicons-before ' . $optionCategorySettings['icon'];
266
267
        $appendCategory = '';
268
        if (isset($optionCategorySettings['showType']) && true === $optionCategorySettings['showType']) {
269
            $appendCategory = ' (' . $optionCategorySettings['title'] . ')';
270
        }
271
272
        $subPageConfig = [
273
            'page_title'  => $prettySubPageName . $appendCategory,
274
            'menu_title'  => "<span class='{$iconClasses}'>{$prettySubPageName}</span>",
275
            'parent_slug' => self::$optionPages[$optionType]['menu_slug'],
276
            'menu_slug'   => $optionType . ucfirst($optionCategoryName) . $subPageName
277
        ];
278
279
        acf_add_options_sub_page($subPageConfig);
280
281
        self::addFieldGroupToSubPage(
282
            $subPageConfig['parent_slug'],
283
            $subPageConfig['menu_slug'],
284
            $subPageConfig['menu_title'],
285
            $fields
286
        );
287
    }
288
289
    protected static function addFieldGroupToSubPage($parentMenuSlug, $menuSlug, $prettySubPageName, $fields)
0 ignored issues
show
The parameter $parentMenuSlug is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
290
    {
291
        $fieldGroup = ACFComposer\ResolveConfig::forFieldGroup(
292
            [
293
                'name' => $menuSlug,
294
                'title' => $prettySubPageName,
295
                'fields' => $fields,
296
                'style' => 'seamless',
297
                'location' => [
298
                    [
299
                        [
300
                            'param' => 'options_page',
301
                            'operator' => '==',
302
                            'value' => $menuSlug
303
                        ]
304
                    ]
305
                ]
306
            ]
307
        );
308
        $fieldGroup['fields'] = self::prefixFields($fieldGroup['fields'], $menuSlug);
309
        acf_add_local_field_group($fieldGroup);
310
    }
311
312
    protected static function checkFeature($optionCategory, $feature)
313
    {
314
        if (array_key_exists($optionCategory, self::$optionCategories) && !Feature::isRegistered($feature)) {
315
            $title = self::$optionCategories[$optionCategory]['title'];
316
            $noticeManager = AdminNoticeManager::getInstance();
317
            $noticeManager->addNotice(
318
                [
319
                    "Could not add Option Pages for {$title} because the \"{$feature}\" feature is missing."
320
                ],
321
                [
322
                    'title' => 'Acf Option Pages Feature',
323
                    'dismissible' => true,
324
                    'type' => 'info'
325
                ]
326
            );
327
        }
328
    }
329
330
    protected static function prefixFields($fields, $prefix)
331
    {
332
        return array_map(function ($field) use ($prefix) {
333
            $field['name'] = $prefix . '_' . $field['name'];
334
            return $field;
335
        }, $fields);
336
    }
337
338
    protected static function checkRequiredHooks($optionType, $optionCategory, $subPageName, $fieldName)
339
    {
340
        if (did_action('acf/init') < 1) {
341
            $parameters = "${optionType}, ${optionCategory}, ${subPageName}, ";
342
            $parameters .= isset($fieldName) ? $fieldName : 'NULL';
343
            trigger_error("Could not get option/s for [${parameters}]. Required hooks have not yet been executed! Please make sure to run `OptionPages::get()` after the `acf/init` action is finished.", E_USER_WARNING);
344
            return false;
345
        }
346
        return true;
347
    }
348
349
    protected static function getOptionFields($translatable)
350
    {
351
        global $sitepress;
352
353
        if (!isset($sitepress)) {
354
            $options = self::getCachedOptionFields();
355
        } else if ($translatable) {
356
            // get options from cache with language namespace
357
            $options = self::getCachedOptionFields(ICL_LANGUAGE_CODE);
358
        } else {
359
            // switch to default language to get global options
360
            $sitepress->switch_lang(acf_get_setting('default_language'));
361
362
            add_filter('acf/settings/current_language', 'Flynt\Features\Acf\OptionPages::getDefaultAcfLanguage', 100);
363
364
            // get optios from cache with global namespace
365
            $options = self::getCachedOptionFields('global');
366
367
            remove_filter('acf/settings/current_language', 'Flynt\Features\Acf\OptionPages::getDefaultAcfLanguage', 100);
368
369
            $sitepress->switch_lang(ICL_LANGUAGE_CODE);
370
        }
371
372
        return $options;
373
    }
374
375
    protected static function getCachedOptionFields($namespace = '')
376
    {
377
        // get cached options
378
        $found = false;
379
        $suffix = !empty($namespace) ? "_${namespace}" : '';
380
        $cacheKey = "Features/Acf/OptionPages/ID=options${suffix}";
381
382
        $options = wp_cache_get($cacheKey, 'flynt', null, $found);
383
384
        if (!$found) {
385
            $options = get_fields('options');
386
            wp_cache_set($cacheKey, $options, 'flynt');
387
        }
388
389
        return $options;
390
    }
391
392
    // find and replace relevant keys, then return an array of all options for this Sub-Page
393
    protected static function collectOptionsWithPrefix($options, $prefix)
394
    {
395
        $optionKeys = is_array($options) ? array_keys($options) : [];
396
        return array_reduce($optionKeys, function ($carry, $key) use ($options, $prefix) {
397
            $count = 0;
398
            $option = $options[$key];
399
            $key = str_replace($prefix, '', $key, $count);
400
            if ($count > 0) {
401
                $carry[$key] = $option;
402
            }
403
            return $carry;
404
        }, []);
405
    }
406
407
    protected static function combineArrayDefaults(array $array, array $defaults)
408
    {
409
        return array_map(function ($value) use ($defaults) {
410
            return is_array($value) ? array_merge($defaults, $value) : [];
411
        }, $array);
412
    }
413
414
    public static function getDefaultAcfLanguage()
415
    {
416
        return acf_get_setting('default_language');
417
    }
418
}
419