PluginProbe
Post Lockdown / trunk
Post Lockdown vtrunk
trunk 1.0.0 1.0.1 1.1 1.1.1 2.0 2.0.1 2.0.2 2.0.3 2.1 3.0 3.0.1 3.0.13 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 4.0 4.0.2 4.0.3 4.0.4 4.0.5 4.1.0 All 26 releases
post-lockdown / src / PostLockdown / PostLockdown.php

PostLockdown.php in Post Lockdown trunk, at src/PostLockdown/PostLockdown.php

435 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace PostLockdown;
4
5 class PostLockdown
6 {
7 /** Plugin key for options and the option page. */
8 public const KEY = 'postlockdown';
9 public const VERSION = '4.1.1';
10
11 /** @var array List of post IDs which cannot be edited, trashed or deleted. */
12 private $locked_post_ids = [];
13 /** @var array List of post IDs which cannot be trashed or deleted. */
14 private $protected_post_ids = [];
15 /** @var bool */
16 private $bulk_actions_enabled = false;
17 /** @var string */
18 public $plugin_path;
19 /** @var string */
20 public $plugin_url;
21 /** @var string */
22 public $db_version;
23
24 public $registry = [];
25
26 public function __construct($plugin_path, $plugin_url)
27 {
28 $this->plugin_path = $plugin_path;
29 $this->plugin_url = $plugin_url;
30
31 $this->load_registry();
32 $this->load_options();
33
34 add_action('delete_post', [$this, '_remove_deleted_post']);
35 add_filter('user_has_cap', [$this, '_filter_cap'], 10, 3);
36 add_filter('wp_insert_post_data', [$this, '_prevent_status_change'], 10, 2);
37 }
38
39 /**
40 * Returns an array of locked post IDs.
41 *
42 * @param bool $suppress_filters Whether to suppress filters and only return IDs
43 * selected on the Post Lockdown options page.
44 *
45 * @return array
46 */
47 public function get_locked_post_ids($suppress_filters = false)
48 {
49 if ($suppress_filters) {
50 return $this->locked_post_ids;
51 }
52
53 return apply_filters('postlockdown_locked_posts', $this->locked_post_ids);
54 }
55
56 /**
57 * Returns an array of protected post IDs.
58 *
59 * @param bool $suppress_filters Whether to suppress filters and only return IDs
60 * selected on the Post Lockdown options page.
61 *
62 * @return array
63 */
64 public function get_protected_post_ids($suppress_filters = false)
65 {
66 if ($suppress_filters) {
67 return $this->protected_post_ids;
68 }
69
70 return apply_filters('postlockdown_protected_posts', $this->protected_post_ids);
71 }
72
73 /**
74 * Returns whether there are any locked or protected posts set.
75 *
76 * @return bool
77 */
78 public function have_posts()
79 {
80 return (bool)($this->get_locked_post_ids() || $this->get_protected_post_ids());
81 }
82
83 /**
84 * Returns whether a post is locked.
85 *
86 * @param int $post_id The ID of the post to check.
87 * @param bool $suppress_filters
88 *
89 * @return bool
90 */
91 public function is_post_locked($post_id, $suppress_filters = false)
92 {
93 if ($suppress_filters) {
94 return isset($this->locked_post_ids[$post_id]);
95 }
96
97 $locked_post_ids = $this->get_locked_post_ids();
98
99 return isset($locked_post_ids[$post_id]);
100 }
101
102 /**
103 * Returns whether a post is protected.
104 *
105 * @param int $post_id The ID of the post to check.
106 * @param bool $suppress_filters
107 *
108 * @return bool
109 */
110 public function is_post_protected($post_id, $suppress_filters = false)
111 {
112 if ($suppress_filters) {
113 return isset($this->protected_post_ids[$post_id]);
114 }
115
116 $protected_post_ids = $this->get_protected_post_ids();
117
118 return isset($protected_post_ids[$post_id]);
119 }
120
121 /**
122 * Adds the given post ID or post IDs to the list of locked posts.
123 *
124 * @param int ...$post_ids
125 */
126 public function add_locked_post(...$post_ids)
127 {
128 foreach ($post_ids as $post_id) {
129 $this->locked_post_ids[$post_id] = $post_id;
130 }
131
132 $this->update_option();
133 }
134
135 /**
136 * Removes the given post ID or post IDs from the list of locked posts.
137 *
138 * @param int ...$post_ids
139 */
140 public function remove_locked_post(...$post_ids)
141 {
142 foreach ($post_ids as $post_id) {
143 unset($this->locked_post_ids[$post_id]);
144 }
145
146 $this->update_option();
147 }
148
149 /**
150 * Adds the given post ID or post IDs to the list of protected posts.
151 *
152 * @param int ...$post_ids
153 */
154 public function add_protected_post(...$post_ids)
155 {
156 foreach ($post_ids as $post_id) {
157 $this->protected_post_ids[$post_id] = $post_id;
158 }
159
160 $this->update_option();
161 }
162
163 /**
164 * Removes the given post ID or post IDs to the list of protected posts.
165 *
166 * @param int ...$post_ids
167 */
168 public function remove_protected_post(...$post_ids)
169 {
170 foreach ($post_ids as $post_id) {
171 unset($this->protected_post_ids[$post_id]);
172 }
173
174 $this->update_option();
175 }
176
177 /**
178 * Convenience wrapper for get_posts().
179 *
180 * @param array $args Array of args to merge with defaults passed to get_posts().
181 *
182 * @return \WP_Post[] Array of post objects.
183 */
184 public function get_posts($args = [])
185 {
186 $defaults = [
187 'post_type' => $this->get_post_types(),
188 'post_status' => ['publish', 'pending', 'draft', 'future', 'private', 'inherit'],
189 'update_post_meta_cache' => false,
190 'update_post_term_cache' => false,
191 'no_found_rows' => true,
192 'cache_results' => false,
193 'ignore_sticky_posts' => true,
194 ];
195
196 $args = wp_parse_args($args, $defaults);
197
198 $args = apply_filters('postlockdown_get_posts', $args);
199
200 $query = new \WP_Query($args);
201
202 return $query->posts;
203 }
204
205 /**
206 * @return array
207 */
208 public function get_post_types()
209 {
210 $excluded_post_types = [];
211
212 if (class_exists('WooCommerce')) {
213 array_push($excluded_post_types, 'shop_order', 'shop_coupon');
214 }
215
216 $excluded_post_types = apply_filters('postlockdown_excluded_post_types', $excluded_post_types);
217
218 $post_types = get_post_types([
219 'show_ui' => true,
220 ]);
221
222 $post_types = array_diff($post_types, $excluded_post_types);
223
224 return apply_filters('postlockdown_post_types', $post_types);
225 }
226
227 /**
228 * Returns the required capability a user must have to bypass all
229 * locked and protected post restrictions. Defaults to 'manage_options'.
230 *
231 * Also serves as a callback for the 'option_page_capability_{slug}' hook.
232 *
233 * @return string The required capability.
234 */
235 public function get_admin_cap()
236 {
237 return apply_filters('postlockdown_admin_capability', 'manage_options');
238 }
239
240 /**
241 * @return bool
242 */
243 public function is_bulk_actions_enabled()
244 {
245 return $this->bulk_actions_enabled;
246 }
247
248 /**
249 * Filter for the 'user_has_cap' hook.
250 *
251 * Sets the capability to false when current_user_can() has been called on
252 * one of the capabilities we're interested in on a locked or protected post.
253 *
254 * @param array $allcaps All capabilities of the user.
255 * @param array $cap [0] Required capability.
256 * @param array $args [0] Requested capability.
257 * [1] User ID.
258 * [2] Post ID.
259 *
260 * @return array
261 */
262 public function _filter_cap($allcaps, $cap, $args)
263 {
264 /* If the user doesn't have the required capabilities to begin with we can return early
265 * because we never want to give users more capabilities than they already have.
266 * We only want to restrict capabilities based on whether the post is locked or protected.
267 */
268 foreach ($cap as $requiredCap) {
269 if (empty($allcaps[$requiredCap])) {
270 return $allcaps;
271 }
272 }
273
274 /* If there are no locked or protected posts, or the user
275 * has the required capability to bypass restrictions get out of here.
276 */
277 if (!$this->have_posts() || !empty($allcaps[$this->get_admin_cap()])) {
278 return $allcaps;
279 }
280
281 $the_caps = apply_filters('postlockdown_capabilities', [
282 'delete_post' => true,
283 'edit_post' => true,
284 ]);
285
286 // If it's not a capability we're interested in get out of here.
287 if (!isset($the_caps[$args[0]])) {
288 return $allcaps;
289 }
290
291 $post_id = $args[2];
292
293 if (!$post_id) {
294 return $allcaps;
295 }
296
297 $has_cap = true;
298
299 if ($this->is_post_locked($post_id) || ('edit_post' !== $args[0] && $this->is_post_protected($post_id))) {
300 $has_cap = false;
301 }
302
303 foreach ($cap as $requiredCap) {
304 $allcaps[$requiredCap] = $has_cap;
305 }
306
307 return $allcaps;
308 }
309
310 /**
311 * Filter for the 'wp_insert_post_data' hook.
312 *
313 * Reverts any changes made by a non-admin to a published protected post's status, privacy and password.
314 * Also reverts any date changes if they're set to a future date. If anything is changed a filter for
315 * the 'redirect_post_location' hook is added to display an admin notice letting the user know we reverted it.
316 *
317 * @param array $data Sanitized post data.
318 * @param array $postarr Raw post data. Contains post ID.
319 *
320 * @return array
321 */
322 public function _prevent_status_change($data, $postarr)
323 {
324 $post_id = $postarr['ID'];
325
326 if ($post_id === 0) {
327 // New post
328 return $data;
329 }
330
331 $post = get_post($post_id);
332
333 /*
334 * Only continue if the current user is a non-admin
335 * and the post is both published and protected.
336 */
337 if (current_user_can($this->get_admin_cap()) || 'publish' !== $post->post_status || !$this->is_post_protected($post_id)) {
338 return $data;
339 }
340
341 $changed = false;
342
343 if ('publish' !== $data['post_status']) {
344 $changed = true;
345 $data['post_status'] = $post->post_status;
346 }
347
348 if ($data['post_password'] !== $post->post_password) {
349 $changed = true;
350 $data['post_password'] = $post->post_password;
351 }
352
353 // Revert the post date if it's set to a future date.
354 if ($data['post_date'] !== $post->post_date && strtotime($data['post_date']) > time()) {
355 $changed = true;
356 $data['post_date'] = $post->post_date;
357 $data['post_date_gmt'] = $post->post_date_gmt;
358 }
359
360 if ($changed) {
361 add_filter('redirect_post_location', [$this->registry['AdminNotice'], '_add_query_arg']);
362 $this->registry['BlockEditorNotice']->flag_reverted($post_id);
363 }
364
365 return $data;
366 }
367
368 /**
369 * Callback for the 'delete_post' hook.
370 *
371 * Removes the deleted post's ID from both locked and protected arrays.
372 *
373 * @param int $post_id Deleted post's ID.
374 */
375 public function _remove_deleted_post($post_id)
376 {
377 unset($this->locked_post_ids[$post_id], $this->protected_post_ids[$post_id]);
378
379 $this->update_option();
380 }
381
382 public function update_option()
383 {
384 update_option(self::KEY, [
385 'locked_post_ids' => $this->locked_post_ids,
386 'protected_post_ids' => $this->protected_post_ids,
387 'bulk_actions_enabled' => $this->bulk_actions_enabled,
388 ]);
389 }
390
391 /**
392 * Callback for register_uninstall_hook() function.
393 *
394 * Removes the plugin option from the database when it is uninstalled.
395 */
396 public static function _uninstall()
397 {
398 delete_option(self::KEY);
399 }
400
401 /**
402 * Initialises class instances and adds them to our registry array.
403 */
404 private function load_registry()
405 {
406 $this->registry = [
407 'AdminNotice' => new AdminNotice($this->plugin_path),
408 'BlockEditorNotice' => new BlockEditorNotice($this),
409 'OptionsPage' => new OptionsPage($this),
410 'StatusColumn' => new StatusColumn($this),
411 'BulkActions' => new BulkActions($this),
412 ];
413 }
414
415 /**
416 * Sets the arrays of locked and protected post IDs.
417 */
418 private function load_options()
419 {
420 $options = get_option(self::KEY, []);
421
422 if (!empty($options['locked_post_ids']) && \is_array($options['locked_post_ids'])) {
423 $this->locked_post_ids = $options['locked_post_ids'];
424 }
425
426 if (!empty($options['protected_post_ids']) && \is_array($options['protected_post_ids'])) {
427 $this->protected_post_ids = $options['protected_post_ids'];
428 }
429
430 if (!empty($options['bulk_actions_enabled'])) {
431 $this->bulk_actions_enabled = true;
432 }
433 }
434 }
435