PluginProbe
WindPress – Tailwind CSS integration for WordPress / 3.2.89
WindPress – Tailwind CSS integration for WordPress v3.2.89
3.2.89 3.2.88 3.2.87 3.2.86 3.2.85 3.2.84 3.2.83 3.2.82 3.2.81 trunk 3.0.0 3.0.1 3.0.10 3.0.11 3.0.12 3.0.13 3.0.14 3.0.15 3.0.16 3.0.17 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 All 143 releases
← All changes | src/Core/Volume.php +78 -17 3.2.813.2.89 View file →
@@ -35,8 +35,13 @@
35 35 continue;
36 36 }
37 37 $entries[] = ['name' => $file->getFilename(), 'relative_path' => $file->getRelativePathname(), 'content' => $file->getContents(), 'handler' => 'internal', 'signature' => wp_create_nonce(sprintf('%s:%s', WIND_PRESS::WP_OPTION, $file->getRelativePathname())), 'readonly' => strpos(wp_normalize_path($file->getPathname()), wp_normalize_path($data_dir)) === \false, 'path_on_disk' => $file->getPathname()];
38 38 }
39 + $directory_finder = new Finder();
40 + $directory_finder->ignoreUnreadableDirs()->in($data_dir)->directories()->followLinks();
41 + foreach ($directory_finder as $directory) {
42 + $entries[] = ['name' => $directory->getBasename(), 'relative_path' => $directory->getRelativePathname(), 'content' => '', 'handler' => 'internal', 'directory' => \true, 'readonly' => \false, 'path_on_disk' => $directory->getPathname()];
43 + }
39 44 $tailwindcss_version = \WindPress\WindPress\Core\Runtime::tailwindcss_version();
40 45 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local file
41 46 $stubs_main_css = file_get_contents(sprintf('%s/stubs/tailwindcss-v%d/main.css', dirname(WIND_PRESS::FILE), $tailwindcss_version));
42 47 // check if 'main.css' already exists and content is not empty, else use the stubs
@@ -77,31 +82,77 @@
77 82 * @param array $entries The list of volume's entries. Each volume have `name`, `relative_path`, `content`, `handler`, and `signature` keys.
78 83 */
79 84 return apply_filters('f!windpress/core/volume:get_entries.entries', $entries);
80 85 }
81 - public static function save_entries($entries)
86 + public static function save_entries($entries): array
82 87 {
88 + $result = ['saved' => [], 'deleted' => [], 'handled' => [], 'skipped' => [], 'errors' => []];
83 89 if (!is_array($entries)) {
84 - return;
90 + $result['errors'][] = ['code' => 'invalid_entries', 'message' => __('Entries must be an array.', 'windpress')];
91 + return $result;
85 92 }
86 93 $data_dir = static::data_dir_path();
87 94 foreach ($entries as $entry) {
95 + if (!is_array($entry)) {
96 + $result['skipped'][] = ['relative_path' => '', 'reason' => 'invalid_entry', 'message' => __('Entry must be an array.', 'windpress')];
97 + continue;
98 + }
99 + $relative_path = isset($entry['relative_path']) && is_scalar($entry['relative_path']) ? (string) $entry['relative_path'] : '';
88 100 // if doesn't have any of the following keys, skip: name, relative_path, content, handler
89 101 if (!isset($entry['name'], $entry['relative_path'], $entry['content'], $entry['handler'])) {
102 + $result['skipped'][] = ['relative_path' => $relative_path, 'reason' => 'missing_required_fields', 'message' => __('Entry is missing required fields.', 'windpress')];
90 103 continue;
91 104 }
105 + if (!is_string($entry['name']) || !is_string($entry['relative_path']) || !is_string($entry['handler'])) {
106 + $result['skipped'][] = ['relative_path' => $relative_path, 'reason' => 'invalid_entry', 'message' => __('Entry name, relative path, and handler must be strings.', 'windpress')];
107 + continue;
108 + }
92 109 // skip the readonly entries
93 110 if (isset($entry['readonly']) && $entry['readonly']) {
111 + $result['skipped'][] = ['relative_path' => $relative_path, 'reason' => 'readonly_entry', 'message' => __('Read-only entries cannot be saved.', 'windpress')];
94 112 continue;
95 113 }
114 + if (!empty($entry['directory'])) {
115 + if ($entry['handler'] !== 'internal') {
116 + $result['skipped'][] = ['relative_path' => $relative_path, 'reason' => 'invalid_directory_handler', 'message' => __('Directories must use the internal handler.', 'windpress')];
117 + continue;
118 + }
119 + try {
120 + $safe_directory_path = static::sanitize_relative_path($entry['relative_path'], $data_dir);
121 + if (!empty($entry['hidden'])) {
122 + if (is_dir($safe_directory_path) && !rmdir($safe_directory_path)) {
123 + throw new \RuntimeException(__('Directory is not empty.', 'windpress'));
124 + }
125 + $result['deleted'][] = ['relative_path' => $relative_path];
126 + } else {
127 + if (file_exists($safe_directory_path) && !is_dir($safe_directory_path)) {
128 + throw new \RuntimeException(__('A file already exists at the directory path.', 'windpress'));
129 + }
130 + if (!is_dir($safe_directory_path) && !wp_mkdir_p($safe_directory_path)) {
131 + throw new \RuntimeException(__('The directory could not be created.', 'windpress'));
132 + }
133 + $result['saved'][] = ['relative_path' => $relative_path];
134 + }
135 + } catch (\InvalidArgumentException $th) {
136 + $result['skipped'][] = ['relative_path' => $relative_path, 'reason' => 'invalid_path', 'message' => __('Directory path is invalid.', 'windpress')];
137 + } catch (\Throwable $th) {
138 + if (\WP_DEBUG_LOG) {
139 + error_log($th->__toString());
140 + }
141 + $result['errors'][] = ['relative_path' => $relative_path, 'code' => 'filesystem_error', 'message' => $th->getMessage()];
142 + }
143 + continue;
144 + }
96 145 if ($entry['handler'] !== 'internal') {
97 146 // the handler only accept alphanumeric, hyphens, and underscores
98 - if (!preg_match('/^[a-zA-Z0-9_-]+$/', $entry['handler'])) {
147 + if (!is_string($entry['handler']) || !preg_match('/^[a-zA-Z0-9_-]+$/', $entry['handler'])) {
148 + $result['skipped'][] = ['relative_path' => $relative_path, 'reason' => 'invalid_handler', 'message' => __('Entry handler is invalid.', 'windpress')];
99 149 continue;
100 150 }
101 151 do_action('a!windpress/core/volume:save_entries.entry', $entry);
102 152 // use specific handler instead for efficient handling
103 153 do_action('a!windpress/core/volume:save_entries.entry.' . $entry['handler'], $entry);
154 + $result['handled'][] = ['relative_path' => $relative_path, 'handler' => $entry['handler']];
104 155 continue;
105 156 }
106 157 // if the signature is not set, it is a new entry.
107 158 if (!isset($entry['signature'])) {
@@ -113,8 +164,9 @@
113 164 remove_filter('sanitize_file_name_chars', [static::class, 'sanitize_file_name_chars'], 10);
114 165 $entry['name'] = pathinfo($entry['relative_path'], \PATHINFO_BASENAME);
115 166 // only handle a css and js files.
116 167 if (!in_array(pathinfo($entry['name'], \PATHINFO_EXTENSION), ['css', 'js'], \true)) {
168 + $result['skipped'][] = ['relative_path' => $entry['relative_path'], 'reason' => 'unsupported_file_type', 'message' => __('Only CSS and JavaScript files can be saved.', 'windpress')];
117 169 continue;
118 170 }
119 171 $entry['signature'] = wp_create_nonce(sprintf('%s:%s', WIND_PRESS::WP_OPTION, $entry['relative_path']));
120 172 }
@@ -119,8 +171,9 @@
119 171 $entry['signature'] = wp_create_nonce(sprintf('%s:%s', WIND_PRESS::WP_OPTION, $entry['relative_path']));
120 172 }
121 173 // verify the signature
122 174 if (!wp_verify_nonce($entry['signature'], sprintf('%s:%s', WIND_PRESS::WP_OPTION, $entry['relative_path']))) {
175 + $result['skipped'][] = ['relative_path' => $entry['relative_path'], 'reason' => 'invalid_signature', 'message' => __('Entry signature is invalid.', 'windpress')];
123 176 continue;
124 177 }
125 178 try {
126 179 // Sanitize and validate the path to prevent directory traversal
@@ -125,19 +178,27 @@
125 178 try {
126 179 // Sanitize and validate the path to prevent directory traversal
127 180 $safe_file_path = static::sanitize_relative_path($entry['relative_path'], $data_dir);
128 181 // if the content is empty, delete the file.
129 - if (empty($entry['content'])) {
130 - Common::delete_file($safe_file_path);
182 + if ($entry['content'] === '') {
183 + if (file_exists($safe_file_path)) {
184 + Common::delete_file($safe_file_path);
185 + }
186 + $result['deleted'][] = ['relative_path' => $entry['relative_path']];
131 187 } else {
132 188 Common::save_file($entry['content'], $safe_file_path);
189 + $result['saved'][] = ['relative_path' => $entry['relative_path']];
133 190 }
191 + } catch (\InvalidArgumentException $th) {
192 + $result['skipped'][] = ['relative_path' => $entry['relative_path'], 'reason' => 'invalid_path', 'message' => __('Entry path is invalid.', 'windpress')];
134 193 } catch (\Throwable $th) {
135 194 if (\WP_DEBUG_LOG) {
136 195 error_log($th->__toString());
137 196 }
197 + $result['errors'][] = ['relative_path' => $entry['relative_path'], 'code' => 'filesystem_error', 'message' => $th->getMessage()];
138 198 }
139 199 }
200 + return $result;
140 201 }
141 202 public static function sanitize_file_name_chars(array $special_chars, $filename_raw)
142 203 {
143 204 // allow dir
@@ -142,8 +203,20 @@
142 203 {
143 204 // allow dir
144 205 return array_diff($special_chars, ['/']);
145 206 }
207 + public static function data_dir_url(): string
208 + {
209 + return wp_upload_dir()['baseurl'] . WIND_PRESS::DATA_DIR;
210 + }
211 + public static function data_dir_path(): string
212 + {
213 + return wp_upload_dir()['basedir'] . WIND_PRESS::DATA_DIR;
214 + }
215 + public static function get_available_handlers(): array
216 + {
217 + return apply_filters('f!windpress/core/volume:get_available_handlers', []);
218 + }
146 219 /**
147 220 * Sanitize and validate a relative path to prevent directory traversal attacks.
148 221 *
149 222 * @param string $relative_path The relative path to sanitize
@@ -167,18 +240,6 @@
167 240 if (!Path::isBasePath($base_dir, $full_path)) {
168 241 throw new \InvalidArgumentException('Path traversal attempt detected: ' . $relative_path);
169 242 }
170 243 return $full_path;
171 - }
172 - public static function data_dir_url(): string
173 - {
174 - return wp_upload_dir()['baseurl'] . WIND_PRESS::DATA_DIR;
175 - }
176 - public static function data_dir_path(): string
177 - {
178 - return wp_upload_dir()['basedir'] . WIND_PRESS::DATA_DIR;
179 - }
180 - public static function get_available_handlers(): array
181 - {
182 - return apply_filters('f!windpress/core/volume:get_available_handlers', []);
183 244 }
184 245 }