PluginProbe
Noted! / 2.0
Noted! v2.0
2.0.2 2.0.1 trunk 1.0 2.0
noted / includes / PostType.php

PostType.php in Noted! 2.0, at includes/PostType.php

165 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Noted;
6
7 use WP_Post;
8
9 /**
10 * The noted_note custom post type and related helpers.
11 *
12 * Notes are private: not publicly queryable, hidden from search, and the
13 * single-post route is redirected to the home URL as a final guard.
14 */
15 final class PostType
16 {
17 public const SLUG = 'noted_note';
18
19 public const META_TIMESTAMP = '_noted_timestamp';
20 public const META_USERNAME = '_noted_username';
21 public const META_MARKDOWN = '_noted_markdown';
22 public const META_ATTACHED_POST_ID = '_noted_attached_post_id';
23 public const META_PINNED = '_noted_pinned';
24
25 /**
26 * Register WordPress hooks.
27 */
28 public function register(): void
29 {
30 add_action('init', [$this, 'registerPostType']);
31 add_action('template_redirect', [$this, 'redirectPrivateNotes']);
32 }
33
34 /**
35 * Register the custom post type with WordPress.
36 */
37 public function registerPostType(): void
38 {
39 register_post_type(self::SLUG, [
40 'public' => false,
41 'show_ui' => false,
42 'show_in_rest' => false,
43 'exclude_from_search' => true,
44 'publicly_queryable' => false,
45 'capability_type' => 'post',
46 'supports' => ['title', 'editor', 'custom-fields'],
47 ]);
48 }
49
50 /**
51 * Redirect any direct visit to a single note back to the home URL.
52 */
53 public function redirectPrivateNotes(): void
54 {
55 if (is_singular(self::SLUG)) {
56 wp_redirect(home_url());
57 exit;
58 }
59 }
60
61 /**
62 * Resolve the current post ID — on the admin edit screen or on a
63 * frontend singular view (single post / page / CPT).
64 *
65 * Returns 0 in every other context (list tables, archives, search,
66 * settings pages) so the "This Post" tab is hidden where there is
67 * no meaningful post to attach to.
68 */
69 public static function currentPostId(): int
70 {
71 if (! is_admin() && function_exists('is_singular') && is_singular()) {
72 return (int) get_queried_object_id();
73 }
74
75 if (! is_admin() || ! function_exists('get_current_screen')) {
76 return 0;
77 }
78
79 $screen = get_current_screen();
80 // List tables also expose $post globally, so we limit the lookup to
81 // the post-edit screen ("post" base) to avoid mistaking a row in a
82 // list view for the current edit target.
83 if (! $screen || $screen->base !== 'post') {
84 return 0;
85 }
86
87 global $post;
88 if ($post instanceof WP_Post) {
89 return (int) $post->ID;
90 }
91
92 return isset($_GET['post']) ? (int) $_GET['post'] : 0;
93 }
94
95 /**
96 * Filter out empty notes — those with no title AND no body.
97 *
98 * @param list<WP_Post> $posts
99 * @return list<WP_Post>
100 */
101 public static function filterRenderable(array $posts): array
102 {
103 $filtered = array_filter($posts, static function (WP_Post $note): bool {
104 return trim($note->post_title) !== '' || trim(wp_strip_all_tags($note->post_content)) !== '';
105 });
106 return array_values($filtered);
107 }
108
109 /**
110 * Stable sort that promotes pinned notes to the top, preserving the
111 * caller's existing order within each group.
112 *
113 * @param list<WP_Post> $posts
114 * @return list<WP_Post>
115 */
116 public static function sortPinnedFirst(array $posts): array
117 {
118 $pinned = [];
119 $rest = [];
120 foreach ($posts as $post) {
121 if (self::isPinned($post)) {
122 $pinned[] = $post;
123 } else {
124 $rest[] = $post;
125 }
126 }
127 return array_merge($pinned, $rest);
128 }
129
130 /**
131 * True if a note carries the pinned flag.
132 */
133 public static function isPinned(WP_Post $note): bool
134 {
135 return (string) get_post_meta($note->ID, self::META_PINNED, true) === '1';
136 }
137
138 /**
139 * Serialise a note post for JSON responses.
140 *
141 * Returns plain (sanitised but un-HTML-encoded) values for text fields;
142 * consumers (React `createElement`, jQuery `.text()`/`.attr()`) escape
143 * at render time. The HTML body is run through {@see wp_kses_post()}
144 * because it is consumed via `dangerouslySetInnerHTML` / `.html()`.
145 *
146 * @return array<string, mixed>
147 */
148 public static function format(WP_Post $note): array
149 {
150 $date = get_post_meta($note->ID, self::META_TIMESTAMP, true);
151 $markdown = (string) get_post_meta($note->ID, self::META_MARKDOWN, true);
152
153 return [
154 'id' => (int) $note->ID,
155 'title' => get_the_title($note),
156 'description' => wp_kses_post($note->post_content),
157 'markdown' => $markdown !== '' ? $markdown : Markdown::toMarkdown($note->post_content),
158 'timestamp' => $date ? date_i18n(get_option('date_format') . ' · ' . get_option('time_format'), strtotime((string) $date)) : '',
159 'username' => (string) get_post_meta($note->ID, self::META_USERNAME, true),
160 'attached_post_id' => (int) get_post_meta($note->ID, self::META_ATTACHED_POST_ID, true),
161 'pinned' => self::isPinned($note),
162 ];
163 }
164 }
165