1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace GeminiLabs\Pollux\Config; |
4
|
|
|
|
5
|
|
|
use GeminiLabs\Pollux\Application; |
6
|
|
|
use GeminiLabs\Pollux\Config\Config; |
7
|
|
|
use GeminiLabs\Pollux\MetaBox\SiteMetaManager; |
8
|
|
|
use Symfony\Component\Yaml\Exception\DumpException; |
9
|
|
|
use Symfony\Component\Yaml\Exception\ParseException; |
10
|
|
|
use Symfony\Component\Yaml\Yaml; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* @property int $updated |
14
|
|
|
*/ |
15
|
|
|
class ConfigManager extends SiteMetaManager |
16
|
|
|
{ |
17
|
|
|
const RAW_STRINGS = [ |
18
|
|
|
// '__', '_n', '_x', 'esc_attr__', 'esc_html__', 'sprintf', |
|
|
|
|
19
|
|
|
]; |
20
|
|
|
|
21
|
|
|
public $compiled; |
22
|
|
|
|
23
|
|
|
public $parseError = false; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var Application |
27
|
|
|
*/ |
28
|
|
|
protected $app; |
29
|
|
|
|
30
|
|
|
public function __construct( Application $app ) |
31
|
|
|
{ |
32
|
|
|
$this->app = $app; |
33
|
|
|
$this->options = $this->buildConfig(); |
34
|
|
|
$this->compiled = $this->compile(); |
35
|
|
|
} |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @return array |
39
|
|
|
*/ |
40
|
|
|
public function buildConfig() |
41
|
|
|
{ |
42
|
|
|
$yamlFile = $this->getYamlFile(); |
43
|
|
|
$yaml = $this->normalizeYamlValues( $this->normalize( |
44
|
|
|
$this->parseYaml( file_get_contents( $yamlFile ), $yamlFile ) |
45
|
|
|
)); |
46
|
|
|
if( !$yaml['disable_config'] ) { |
47
|
|
|
$config = array_filter( (array) get_option( Config::id(), [] )); |
48
|
|
|
} |
49
|
|
|
return empty( $config ) |
50
|
|
|
? $this->setTimestamp( $yaml, filemtime( $yamlFile )) |
51
|
|
|
: $this->normalizeYamlValues( $this->normalize( $config )); |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @return object |
56
|
|
|
*/ |
57
|
|
|
public function compile() |
58
|
|
|
{ |
59
|
|
|
$configFile = $this->getCompileDestination(); |
60
|
|
|
if( $this->shouldCompile( $configFile )) { |
61
|
|
|
$config = $this->normalizeArray( $this->options ); |
62
|
|
|
if( $this->parseError ) { |
63
|
|
|
return (object) $config; |
64
|
|
|
} |
65
|
|
|
file_put_contents( $configFile, sprintf( '<?php // DO NOT MODIFY THIS FILE DIRECTLY!%sreturn (object) %s;', |
66
|
|
|
PHP_EOL, |
67
|
|
|
$this->parseRawStrings( var_export( $this->setTimestamp( $config ), true )) |
68
|
|
|
)); |
69
|
|
|
} |
70
|
|
|
return include $configFile; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @return string |
75
|
|
|
*/ |
76
|
|
|
public function convertArrayToYaml( array $array ) |
77
|
|
|
{ |
78
|
|
|
return !empty( $array ) |
79
|
|
|
? trim( $this->parseRawStrings( $this->dumpYaml( $array ))) |
80
|
|
|
: ''; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* @return string |
85
|
|
|
*/ |
86
|
|
|
public function getCompileDestination( $filename = 'pollux-config.php' ) |
87
|
|
|
{ |
88
|
|
|
$filename = apply_filters( 'pollux/config/dist/file', $filename ); |
89
|
|
|
$storagePath = apply_filters( 'pollux/config/dist/location', WP_CONTENT_DIR ); |
90
|
|
|
wp_mkdir_p( $storagePath ); |
91
|
|
|
return sprintf( '%s%s', trailingslashit( $storagePath ), $filename ); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* @return string |
96
|
|
|
*/ |
97
|
|
|
public function getYamlFile() |
98
|
|
|
{ |
99
|
|
|
$theme = wp_get_theme(); |
100
|
|
|
$configYaml = apply_filters( 'pollux/config/src/file', 'pollux.yml' ); |
101
|
|
|
$configLocations = apply_filters( 'pollux/config/src/location', [ |
102
|
|
|
trailingslashit( trailingslashit( $theme->theme_root ) . $theme->stylesheet ), |
103
|
|
|
trailingslashit( trailingslashit( $theme->theme_root ) . $theme->template ), |
104
|
|
|
trailingslashit( WP_CONTENT_DIR ), |
105
|
|
|
trailingslashit( ABSPATH ), |
106
|
|
|
trailingslashit( dirname( ABSPATH )), |
107
|
|
|
trailingslashit( dirname( dirname( ABSPATH ))), |
108
|
|
|
]); |
109
|
|
|
foreach( (array) $configLocations as $location ) { |
110
|
|
|
if( !file_exists( $location . $configYaml ))continue; |
111
|
|
|
return $location . $configYaml; |
112
|
|
|
} |
113
|
|
|
return $this->app->path( 'defaults.yml' ); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* @return array |
118
|
|
|
*/ |
119
|
|
|
public function normalizeArray( array $array ) |
120
|
|
|
{ |
121
|
|
|
array_walk( $array, function( &$value, $key ) { |
122
|
|
|
if( !is_numeric( $value ) && is_string( $value )) { |
123
|
|
|
$value = $this->parseYaml( $value, $key ); |
124
|
|
|
if( $this->parseError == $key ) { |
125
|
|
|
$value = []; |
126
|
|
|
} |
127
|
|
|
} |
128
|
|
|
}); |
129
|
|
|
return $array; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* @return array |
134
|
|
|
*/ |
135
|
|
|
public function normalizeYamlValues( array $array ) |
136
|
|
|
{ |
137
|
|
|
return array_map( function( $value ) { |
138
|
|
|
return is_array( $value ) |
139
|
|
|
? $this->convertArrayToYaml( $value ) |
140
|
|
|
: $value; |
141
|
|
|
}, $array ); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* @return array |
146
|
|
|
*/ |
147
|
|
|
public function setTimestamp( array $config, $timestamp = null ) |
148
|
|
|
{ |
149
|
|
|
$timestamp || $timestamp = time(); |
150
|
|
|
$config['updated'] = $timestamp; |
151
|
|
|
return $config; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* @return string|null |
156
|
|
|
*/ |
157
|
|
|
protected function dumpYaml( array $array ) |
158
|
|
|
{ |
159
|
|
|
try { |
160
|
|
|
return Yaml::dump( $array, 13, 2 ); |
161
|
|
|
} |
162
|
|
|
catch( DumpException $e ) { |
163
|
|
|
$this->app->make( 'Notice' )->addError( $e->getMessage() ); |
164
|
|
|
} |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* @return array |
169
|
|
|
*/ |
170
|
|
|
protected function normalize( array $config ) |
171
|
|
|
{ |
172
|
|
|
return wp_parse_args( |
173
|
|
|
$config, |
174
|
|
|
$this->parseYaml( |
175
|
|
|
file_get_contents( $this->app->path( 'defaults.yml' )), |
176
|
|
|
$this->app->path( 'defaults.yml' ) |
177
|
|
|
) |
178
|
|
|
); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* @param string $configString |
183
|
|
|
* @return string |
184
|
|
|
* @todo only allow raw strings when we can parse them properly without using eval() |
|
|
|
|
185
|
|
|
*/ |
186
|
|
|
protected function parseRawStrings( $configString ) |
187
|
|
|
{ |
188
|
|
|
$strings = apply_filters( 'pollux/config/raw_strings', static::RAW_STRINGS ); |
189
|
|
|
if( empty( $strings )) { |
190
|
|
|
return $configString; |
191
|
|
|
} |
192
|
|
|
$pattern = '/(\')((' . implode( '|', $strings ) . ')\(?.+\))(\')/'; |
193
|
|
|
return stripslashes( |
194
|
|
|
preg_replace_callback( $pattern, function( $matches ) { |
195
|
|
|
return str_replace( "''", "'", $matches[2] ); |
196
|
|
|
}, $configString ) |
197
|
|
|
); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* @link http://api.symfony.com/3.2/Symfony/Component/Yaml/Exception/ParseException.html |
202
|
|
|
* @return array |
203
|
|
|
*/ |
204
|
|
|
protected function parseYaml( $value, $file = null ) |
205
|
|
|
{ |
206
|
|
|
try { |
207
|
|
|
return (array) Yaml::parse( $value ); |
208
|
|
|
} |
209
|
|
|
catch( ParseException $e ) { |
210
|
|
|
$this->parseError = $file; |
211
|
|
|
if( $file ) { |
212
|
|
|
$file = sprintf( '<code>%s</code>', $file ); |
213
|
|
|
} |
214
|
|
|
$this->app->make( 'Notice' )->addError([ |
215
|
|
|
sprintf( '<strong>Pollux Error:</strong> Unable to parse config at line %s (near "%s").', |
216
|
|
|
$e->getParsedLine(), |
217
|
|
|
$e->getSnippet() |
218
|
|
|
), |
219
|
|
|
$file |
220
|
|
|
]); |
221
|
|
|
return $value; |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* @param string $configFile |
227
|
|
|
* @return bool |
228
|
|
|
*/ |
229
|
|
|
protected function shouldCompile( $configFile ) |
230
|
|
|
{ |
231
|
|
|
if( !file_exists( $configFile )) { |
232
|
|
|
return true; |
233
|
|
|
} |
234
|
|
|
$config = include $configFile; |
235
|
|
|
if( $this->updated >= $config->updated ) { |
236
|
|
|
return true; |
237
|
|
|
} |
238
|
|
|
return filemtime( $this->getYamlFile() ) >= $config->updated; |
239
|
|
|
} |
240
|
|
|
} |
241
|
|
|
|
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.