nextpress-core 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/cli/index.js +11 -0
  2. package/cli/meta.js +5 -0
  3. package/cli/np-down.js +10 -0
  4. package/cli/np-install.js +35 -0
  5. package/cli/np-log.js +12 -0
  6. package/cli/np-run-dev.js +34 -0
  7. package/cli/np-run-start.js +33 -0
  8. package/cli/np-run.js +10 -0
  9. package/cli/run-command.js +23 -0
  10. package/cli/start-composer.js +39 -0
  11. package/lib/container-lib/NextJS.Dockerfile +47 -0
  12. package/lib/container-lib/docker-compose.dev.yml +69 -0
  13. package/lib/container-lib/docker-compose.prod.yml +23 -0
  14. package/lib/container-lib/docker-compose.yml +161 -0
  15. package/lib/container-lib/mail.dev.ini +3 -0
  16. package/lib/container-lib/mail.prod.ini +3 -0
  17. package/lib/container-lib/nginx.conf +71 -0
  18. package/lib/container-lib/uploads.ini +3 -0
  19. package/lib/{ambient.d.ts → nextjs-lib/ambient.d.ts} +2 -2
  20. package/lib/{router → nextjs-lib/router}/nextpress-layout.tsx +1 -1
  21. package/lib/{router → nextjs-lib/router}/types.ts +3 -0
  22. package/lib/{ui → nextjs-lib/ui}/render-the-logo.tsx +1 -1
  23. package/lib/wp-lib/src/acf/acf.php +10 -0
  24. package/lib/wp-lib/src/acf/acf_add_options.php +18 -0
  25. package/lib/wp-lib/src/acf/load_acf_fields.php +34 -0
  26. package/lib/wp-lib/src/api/nextpress_get_admin_bar.php +260 -0
  27. package/lib/wp-lib/src/api/nextpress_validate_user_session.php +114 -0
  28. package/lib/wp-lib/src/index.php +26 -0
  29. package/lib/wp-lib/src/nextpress_dev_mailer_setup.php +17 -0
  30. package/lib/wp-lib/src/nextpress_disable_core_depency_updates.php +49 -0
  31. package/lib/wp-lib/src/nextpress_remove_comments.php +17 -0
  32. package/lib/wp-lib/src/nextpress_remove_menu_pages.php +32 -0
  33. package/lib/wp-lib/src/nextpress_revalidate_frontend.php +57 -0
  34. package/lib/wp-lib/src/nextpress_save_path.php +150 -0
  35. package/lib/wp-lib/src/theme.php +31 -0
  36. package/package.json +15 -10
  37. package/cli/install.js +0 -27
  38. /package/lib/{acf-functions → nextjs-lib/acf-functions}/core/acf-builder.ts +0 -0
  39. /package/lib/{acf-functions → nextjs-lib/acf-functions}/core/acf-component-autoloader.ts +0 -0
  40. /package/lib/{acf-functions → nextjs-lib/acf-functions}/core/acf-field-group-autoloader.ts +0 -0
  41. /package/lib/{acf-functions → nextjs-lib/acf-functions}/services/define-field-group.ts +0 -0
  42. /package/lib/{acf-functions → nextjs-lib/acf-functions}/services/define-layout.ts +0 -0
  43. /package/lib/{acf-functions → nextjs-lib/acf-functions}/services/map-fields/helpers/map-choice-object.ts +0 -0
  44. /package/lib/{acf-functions → nextjs-lib/acf-functions}/services/map-fields/map-fields.ts +0 -0
  45. /package/lib/{acf-functions → nextjs-lib/acf-functions}/types/acf-field-group.ts +0 -0
  46. /package/lib/{acf-functions → nextjs-lib/acf-functions}/types/acf-field.ts +0 -0
  47. /package/lib/{acf-functions → nextjs-lib/acf-functions}/types/acf-layout.ts +0 -0
  48. /package/lib/{acf-functions → nextjs-lib/acf-functions}/types/acf-values.ts +0 -0
  49. /package/lib/{acf-functions → nextjs-lib/acf-functions}/types/components/field-props.ts +0 -0
  50. /package/lib/{acf-functions → nextjs-lib/acf-functions}/types/components/nextpress-component.ts +0 -0
  51. /package/lib/{entities → nextjs-lib/entities}/common.ts +0 -0
  52. /package/lib/{entities → nextjs-lib/entities}/option/option.interface.ts +0 -0
  53. /package/lib/{entities → nextjs-lib/entities}/option/option.ts +0 -0
  54. /package/lib/{entities → nextjs-lib/entities}/post/post.interface.ts +0 -0
  55. /package/lib/{entities → nextjs-lib/entities}/post/post.ts +0 -0
  56. /package/lib/{entities → nextjs-lib/entities}/term/term.interface.ts +0 -0
  57. /package/lib/{entities → nextjs-lib/entities}/term/term.ts +0 -0
  58. /package/lib/{entities → nextjs-lib/entities}/user/user.interface.ts +0 -0
  59. /package/lib/{entities → nextjs-lib/entities}/user/user.ts +0 -0
  60. /package/lib/{globals → nextjs-lib/globals}/entity-loader/entity-loader-base.ts +0 -0
  61. /package/lib/{globals → nextjs-lib/globals}/entity-loader/entity-loader.ts +0 -0
  62. /package/lib/{globals → nextjs-lib/globals}/entity-loader/option-loader.ts +0 -0
  63. /package/lib/{globals → nextjs-lib/globals}/entity-loader/post-loader.ts +0 -0
  64. /package/lib/{globals → nextjs-lib/globals}/entity-loader/term-loader.ts +0 -0
  65. /package/lib/{globals → nextjs-lib/globals}/entity-loader/user-loader.ts +0 -0
  66. /package/lib/{globals → nextjs-lib/globals}/get-field/get-field.ts +0 -0
  67. /package/lib/{globals → nextjs-lib/globals}/globals.ts +0 -0
  68. /package/lib/{globals → nextjs-lib/globals}/nextpress-config/nextpress-config.interface.ts +0 -0
  69. /package/lib/{globals → nextjs-lib/globals}/nextpress-config/nextpress-config.ts +0 -0
  70. /package/lib/{globals → nextjs-lib/globals}/queried-object/queried-object.ts +0 -0
  71. /package/lib/{repository → nextjs-lib/repository}/optionquery/option-query-args.ts +0 -0
  72. /package/lib/{repository → nextjs-lib/repository}/optionquery/option-query.ts +0 -0
  73. /package/lib/{repository → nextjs-lib/repository}/postquery/post-query-args.ts +0 -0
  74. /package/lib/{repository → nextjs-lib/repository}/postquery/post-query.ts +0 -0
  75. /package/lib/{repository → nextjs-lib/repository}/termquery/term-query-args.ts +0 -0
  76. /package/lib/{repository → nextjs-lib/repository}/termquery/term-query.ts +0 -0
  77. /package/lib/{repository → nextjs-lib/repository}/userquery/user-query-args.ts +0 -0
  78. /package/lib/{repository → nextjs-lib/repository}/userquery/user-query.ts +0 -0
  79. /package/lib/{router → nextjs-lib/router}/helpers.ts +0 -0
  80. /package/lib/{router → nextjs-lib/router}/nextpress-not-found-route.tsx +0 -0
  81. /package/lib/{router → nextjs-lib/router}/nextpress-proxy.ts +0 -0
  82. /package/lib/{router → nextjs-lib/router}/nextpress-static-params.ts +0 -0
  83. /package/lib/{router → nextjs-lib/router}/router.tsx +0 -0
  84. /package/lib/{router → nextjs-lib/router}/routes/api/api-get-admin-bar.ts +0 -0
  85. /package/lib/{router → nextjs-lib/router}/routes/api/api-get-draft-mode.ts +0 -0
  86. /package/lib/{router → nextjs-lib/router}/routes/api/api-get-field-groups.ts +0 -0
  87. /package/lib/{router → nextjs-lib/router}/routes/api/api-post-revalidate.ts +0 -0
  88. /package/lib/{router → nextjs-lib/router}/routes/api/helpers.ts +0 -0
  89. /package/lib/{router → nextjs-lib/router}/routes/author-archive.tsx +0 -0
  90. /package/lib/{router → nextjs-lib/router}/routes/post-index-page.tsx +0 -0
  91. /package/lib/{router → nextjs-lib/router}/routes/singular-page.tsx +0 -0
  92. /package/lib/{router → nextjs-lib/router}/routes/site-front-page.tsx +0 -0
  93. /package/lib/{router → nextjs-lib/router}/routes/term-archive.tsx +0 -0
  94. /package/lib/{services → nextjs-lib/services}/get-menu.ts +0 -0
  95. /package/lib/{services → nextjs-lib/services}/get-theme-mods.ts +0 -0
  96. /package/lib/{services → nextjs-lib/services}/metadata/get-blogname.ts +0 -0
  97. /package/lib/{services → nextjs-lib/services}/metadata/get-favicon-url.ts +0 -0
  98. /package/lib/{services → nextjs-lib/services}/metadata/get-language-attribute.ts +0 -0
  99. /package/lib/{services → nextjs-lib/services}/utilities/capitalise-first-letter.ts +0 -0
  100. /package/lib/{services → nextjs-lib/services}/utilities/esc-html.ts +0 -0
  101. /package/lib/{services → nextjs-lib/services}/utilities/get-date-time-formatter.ts +0 -0
  102. /package/lib/{services → nextjs-lib/services}/utilities/kses-post.ts +0 -0
  103. /package/lib/{services → nextjs-lib/services}/utilities/process-url.ts +0 -0
  104. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/_autoloader/template-autoloader.ts +0 -0
  105. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/archive/archive.tsx +0 -0
  106. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/archive/author.tsx +0 -0
  107. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/archive/category.tsx +0 -0
  108. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/archive/posttypearchive.tsx +0 -0
  109. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/archive/tag.tsx +0 -0
  110. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/archive/taxonomy.tsx +0 -0
  111. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/home/home.tsx +0 -0
  112. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/index.tsx +0 -0
  113. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/not-found.tsx/not-found.tsx +0 -0
  114. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/page/page.tsx +0 -0
  115. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/page/posttype.tsx +0 -0
  116. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/page/single.tsx +0 -0
  117. /package/lib/{template-heirarchy → nextjs-lib/template-heirarchy}/page/singular.tsx +0 -0
  118. /package/lib/{ui → nextjs-lib/ui}/render-attachment-image.tsx +0 -0
  119. /package/lib/{ui → nextjs-lib/ui}/render-components.tsx +0 -0
  120. /package/lib/{ui → nextjs-lib/ui}/render-the-admin-bar.tsx +0 -0
  121. /package/lib/{wpdb → nextjs-lib/wpdb}/wpdb.interface.ts +0 -0
  122. /package/lib/{wpdb → nextjs-lib/wpdb}/wpdb.ts +0 -0
@@ -0,0 +1,18 @@
1
+ <?php
2
+
3
+ namespace Nextpress\ACF;
4
+
5
+ /**
6
+ * Registers a custom global ACF options page for App Options.
7
+ *
8
+ * @return void
9
+ */
10
+ function acf_add_options(): void {
11
+ \acf_add_options_page([
12
+ 'page_title' => "App Options",
13
+ 'menu_title' => "App Options",
14
+ 'menu_slug' => "app_options",
15
+ 'capability' => "manage_options",
16
+ 'redirect' => false,
17
+ ]);
18
+ }
@@ -0,0 +1,34 @@
1
+ <?php
2
+
3
+ namespace Nextpress\ACF;
4
+
5
+ /**
6
+ * Loads ACF field groups remotely from the fronend API service.
7
+ *
8
+ * @return void
9
+ */
10
+
11
+ function load_acf_fields_from_api(): void {
12
+ $nextjs_service_url = getenv_docker('NEXTJS_SERVICE_URL', '');
13
+ $api_key = getenv_docker('CROSS_CONTAINER_API_KEY', '');
14
+ if (!$nextjs_service_url || !\is_scalar($nextjs_service_url) || !\is_scalar($api_key)) return;
15
+
16
+ $response = wp_remote_get($nextjs_service_url . '/api/field-groups', [
17
+ 'headers' => [
18
+ 'Authorization' => 'api-key ' . $api_key,
19
+ 'Content-Type' => 'application/json'
20
+ ]
21
+ ]);
22
+
23
+ if (!is_wp_error($response)) {
24
+ $body = wp_remote_retrieve_body($response);
25
+ $field_groups_data = json_decode($body, true);
26
+ if (!\is_array($field_groups_data)) return;
27
+
28
+ foreach ($field_groups_data as $field_group) {
29
+ if (!\is_array($field_group)) continue;
30
+
31
+ acf_add_local_field_group($field_group);
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,260 @@
1
+ <?php
2
+
3
+ namespace Nextpress;
4
+
5
+ use WP_Admin_Bar;
6
+ use WP_Post;
7
+ use WP_Error;
8
+ use WP_Query;
9
+ use WP_REST_Request;
10
+ use WP_REST_Response;
11
+ use WP_Rewrite;
12
+ use WP_Scripts;
13
+ use WP_Styles;
14
+
15
+ add_action('rest_api_init', function() {
16
+ register_rest_route(
17
+ 'nextpress/v1',
18
+ '/get-admin-bar/',
19
+ [
20
+ 'methods' => 'GET',
21
+ 'callback' => __NAMESPACE__ . '\handle_get_admin_bar_response',
22
+ 'permission_callback' => __NAMESPACE__ . '\validate_get_admin_bar',
23
+ 'args' => [
24
+ 'user_id' => [
25
+ 'type' => 'integer',
26
+ 'required' => true,
27
+ 'sanitize_callback' => 'absint',
28
+ ],
29
+ 'path' => [
30
+ 'type' => 'string',
31
+ 'required' => false,
32
+ 'sanitize_callback' => 'sanitize_text_field',
33
+ ]
34
+ ]
35
+ ]
36
+ );
37
+ });
38
+
39
+ /**
40
+ * Permission callback to validate the API key for the get-admin-bar REST route.
41
+ *
42
+ * This function ensures that requests to the admin bar endpoint are securely authenticated using server-to-server authentication. It verifies that the `Authorization: api-key <key>`
43
+ * header matches the expected `CROSS_CONTAINER_API_KEY` defined in the Docker environment.
44
+ *
45
+ * @since 1.0.0
46
+ * @access public
47
+ *
48
+ * @param WP_REST_Request $request The incoming REST API request object.
49
+ * @return bool|WP_Error Returns `true` if the API key is valid. Returns a `WP_Error` (401 status) if authorization fails.
50
+ */
51
+ function validate_get_admin_bar(WP_REST_Request $request): bool | WP_Error {
52
+ $api_key = getenv_docker('CROSS_CONTAINER_API_KEY', '');
53
+ $auth_header = $request->get_header('Authorization');
54
+
55
+ $passed_key = '';
56
+ if (!empty($auth_header) && preg_match('/api-key\s(\S+)/', $auth_header, $matches)) {
57
+ $passed_key = $matches[1];
58
+ }
59
+
60
+ if (!\is_string($api_key) || empty($api_key) || !hash_equals($api_key, $passed_key)) {
61
+ return new WP_Error('rest_forbidden', __('Unauthorized API Key.'), ['status' => 401]);
62
+ }
63
+
64
+ return true;
65
+ }
66
+
67
+ /**
68
+ * Handles the GET request to generate and return the WordPress admin bar HTML and assets.
69
+ *
70
+ * This function acts as the primary callback for the `/nextpress/v1/get-admin-bar/` endpoint.
71
+ * Because REST API requests do not run the standard WordPress frontend loop, this function artificially constructs the main query (`WP_Query`), global objects, and context based
72
+ * on the provided `path`. This ensures that context-sensitive admin bar items (like "Edit Post") point to the correct URLs.
73
+ *
74
+ * ### API Endpoint Information
75
+ * - URL: `/wp-json/nextpress/v1/get-admin-bar/`
76
+ * - Method: `GET`
77
+ * - Auth Required: Yes (via `Authorization` header)
78
+ *
79
+ * ### Request Headers
80
+ * - `Authorization`: `api-key <CROSS_CONTAINER_API_KEY>`
81
+ *
82
+ * ### Request Parameters
83
+ * - `user_id` (integer|required): The ID of the authenticated user to generate the admin bar for.
84
+ * - `path` (string|optional): The frontend path/URL being viewed. Used to figure out the current queried object.
85
+ *
86
+ * ### Success Response
87
+ * - Code: 200 OK
88
+ * - Content:
89
+ * ```json
90
+ * {
91
+ * "success": true,
92
+ * "html": "<div id=\"wpadminbar\" class=\"...\">...</div>",
93
+ * "assets": {
94
+ * "css": {
95
+ * "admin_bar": "[https://yoursite.com/wp-includes/css/admin-bar.min.css](https://yoursite.com/wp-includes/css/admin-bar.min.css)",
96
+ * "dashicons": "[https://yoursite.com/wp-includes/css/dashicons.min.css](https://yoursite.com/wp-includes/css/dashicons.min.css)"
97
+ * },
98
+ * "js": {
99
+ * "admin_bar": "[https://yoursite.com/wp-includes/js/admin-bar.min.js](https://yoursite.com/wp-includes/js/admin-bar.min.js)"
100
+ * }
101
+ * }
102
+ * }
103
+ * ```
104
+ *
105
+ * ### Error Responses
106
+ * - Code: 400 Bad Request (`Empty user`) - Missing or invalid `user_id` parameter.
107
+ * - Code: 401 Unauthorized (`rest_forbidden`) - Missing or invalid API key (handled by permission callback).
108
+ *
109
+ * @since 1.0.0
110
+ * @access public
111
+ *
112
+ * @param WP_REST_Request $request The incoming REST API request object containing the path and user_id.
113
+ * @return WP_REST_Response Response object containing success status, generated HTML, and required CSS/JS assets.
114
+ */
115
+ function handle_get_admin_bar_response(WP_REST_Request $request): WP_REST_Response {
116
+
117
+ // STEP 1: Set Up Global Scope and Require Classes
118
+ // Because REST API requests do not run the standard frontend WordPress loop, many variables
119
+ // and classes needed to render the admin bar are missing. We bring in essential global variables.
120
+ global $post, $wp_admin_bar, $wp_query, $wp_the_query, $wp_styles, $wp_scripts, $wp_rewrite;
121
+
122
+ // The WP_Admin_Bar class might not be loaded in a REST API context, so we manually require it.
123
+ if (!class_exists('WP_Admin_Bar')) {
124
+ if (file_exists(ABSPATH . WPINC . '/class-wp-admin-bar.php')) {
125
+ require_once ABSPATH . WPINC . '/class-wp-admin-bar.php';
126
+ }
127
+ }
128
+
129
+ // STEP 2: Initialize Core WordPress Objects
130
+ // Check if the core globals exist. If they don't, create fresh instances.
131
+ // This establishes the foundation needed to simulate a normal page load.
132
+ $wp_admin_bar = new WP_Admin_Bar();
133
+
134
+ if (!($wp_query instanceof WP_Query)) {
135
+ $wp_query = new WP_Query();
136
+ }
137
+ if (!($wp_rewrite instanceof WP_Rewrite)) {
138
+ $wp_rewrite = new WP_Rewrite();
139
+ }
140
+ if (!($wp_styles instanceof WP_Styles)) {
141
+ $wp_styles = new WP_Styles();
142
+ }
143
+ if (!($wp_scripts instanceof WP_Scripts)) {
144
+ $wp_scripts = new WP_Scripts();
145
+ }
146
+
147
+ // STEP 3: Authenticate the User Context
148
+ /** @var string */
149
+ $user_id = $request->get_param('user_id');
150
+
151
+ if (!$user_id) {
152
+ return new WP_REST_Response(['success' => false, 'error' => 'Empty user'], 400);
153
+ }
154
+
155
+ // Force WordPress to recognize the request as coming from this specific user.
156
+ // This ensures the admin bar shows the correct personalized details and proper permission-based nodes.
157
+ wp_set_current_user(absint($user_id));
158
+
159
+ // STEP 4: Resolve the Requested Path
160
+ /** @var string */
161
+ $path = $request->get_param('path');
162
+ $full_url = home_url((string)$path);
163
+ $cleaned_path = trim((string)$path, '/');
164
+
165
+ // Try to map the provided URL path to a specific WordPress post or page ID.
166
+ $post_id = url_to_postid($full_url);
167
+ $query_vars = [];
168
+
169
+ if ($post_id) {
170
+ // If it finds a match, set up the internal query variables ('p' or 'page_id').
171
+ $target_post = get_post($post_id);
172
+ if ($target_post) {
173
+ $query_vars[$target_post->post_type === 'page' ? 'page_id' : 'p'] = $post_id;
174
+ }
175
+ } elseif ($cleaned_path === '') {
176
+ // If the path is empty, check the WordPress reading settings to determine if the homepage is a static page or the blog archive, and set the query variables accordingly.
177
+ $front_page_id = get_option('page_on_front');
178
+ if ($front_page_id > 0) {
179
+ $query_vars['page_id'] = $front_page_id;
180
+ } else {
181
+ $query_vars['is_home'] = 1;
182
+ }
183
+ }
184
+
185
+ // STEP 5: Run the Simulated Query
186
+ // @phpstan-ignore argument.type
187
+ $wp_query->parse_query($query_vars);
188
+ $posts = $wp_query->get_posts();
189
+
190
+ // STEP 6: Establish the "Queried Object" Context
191
+ // The WordPress admin bar relies heavily on knowing *what* is currently being viewed so it can
192
+ // generate contextual links like "Edit Page". We manually inspect the query results and populate context.
193
+ if ($wp_query->is_singular && !empty($posts)) {
194
+ $post = $posts[0];
195
+
196
+ $wp_query->queried_object = $post instanceof WP_Post ? $post : get_post($post);
197
+ $wp_query->queried_object_id = $post instanceof WP_post ? $post->ID : $post;
198
+ } elseif ($wp_query->is_archive || $wp_query->is_home) {
199
+ $wp_query->queried_object = $wp_query->get_queried_object();
200
+
201
+ if (isset($wp_query->queried_object->term_id)) {
202
+ $wp_query->queried_object_id = is_numeric($wp_query->queried_object->term_id) ? absint($wp_query->queried_object->term_id) : 0;
203
+ }
204
+ }
205
+
206
+ // STEP 7: Render and Capture the HTML
207
+ $admin_bar_html = '';
208
+
209
+ // Force the admin bar to display by adding a filter.
210
+ add_filter('show_admin_bar', '__return_true', 999);
211
+
212
+ // Initialize the standard admin bar hooks.
213
+ _wp_admin_bar_init();
214
+ do_action('admin_bar_init');
215
+
216
+ // Use output buffering to intercept and capture the HTML that wp_admin_bar_render() echoes directly.
217
+ ob_start();
218
+ wp_admin_bar_render();
219
+ $admin_bar_html = ob_get_clean();
220
+
221
+ // STEP 8: Extract Required Assets
222
+ // Load the default styles and scripts into the simulated queues so we can extract their direct URLs.
223
+ wp_default_styles($wp_styles);
224
+ wp_default_scripts($wp_scripts);
225
+
226
+ // Set fallback paths for the required CSS and JS assets.
227
+ $admin_bar_css = '/wp-includes/css/admin-bar.min.css';
228
+ $dashicons_css = '/wp-includes/css/dashicons.min.css';
229
+ $admin_bar_js = '/wp-includes/js/admin-bar.min.js';
230
+
231
+ // If WordPress has dynamically registered these assets (e.g., they have a version string),
232
+ // extract their exact source URLs.
233
+ if (isset($wp_styles->registered['admin-bar']->src)) {
234
+ $admin_bar_css = $wp_styles->registered['admin-bar']->src;
235
+ }
236
+ if (isset($wp_styles->registered['dashicons']->src)) {
237
+ $dashicons_css = $wp_styles->registered['dashicons']->src;
238
+ }
239
+ if (isset($wp_scripts->registered['admin-bar']->src)) {
240
+ $admin_bar_js = $wp_scripts->registered['admin-bar']->src;
241
+ }
242
+
243
+ // Build an array of the absolute URLs to the required assets.
244
+ $assets = [
245
+ 'css' => [
246
+ 'admin_bar' => site_url((string)$admin_bar_css),
247
+ 'dashicons' => site_url((string)$dashicons_css),
248
+ ],
249
+ 'js' => [
250
+ 'admin_bar' => site_url((string)$admin_bar_js),
251
+ ]
252
+ ];
253
+
254
+ // STEP 9: Return the Payload
255
+ return new WP_REST_Response([
256
+ 'success' => true,
257
+ 'html' => $admin_bar_html,
258
+ 'assets' => $assets,
259
+ ], 200);
260
+ }
@@ -0,0 +1,114 @@
1
+ <?php
2
+
3
+ namespace Nextpress;
4
+
5
+ use WP_Error;
6
+ use WP_REST_Request;
7
+ use WP_REST_Response;
8
+
9
+ add_action('rest_api_init', function() {
10
+ register_rest_route(
11
+ 'nextpress/v1',
12
+ '/validate-user-session/',
13
+ [
14
+ 'methods' => 'GET',
15
+ 'callback' => __NAMESPACE__ . '\handle_validate_user_session_response',
16
+ 'permission_callback' => __NAMESPACE__ . '\validate_validate_user_session',
17
+ 'args' => [
18
+ 'user_hash' => [
19
+ 'type' => 'string',
20
+ 'required' => true,
21
+ 'sanitize_callback' => 'sanitize_text_field',
22
+ ]
23
+ ]
24
+ ]
25
+ );
26
+ });
27
+
28
+ /**
29
+ * Permission callback to validate the API key and the user session hash.
30
+ *
31
+ * This function intercepts the request before it reaches the main callback. It performs two critical security checks:
32
+ * 1. Server-to-Server Authentication: Verifies that the `Authorization: api-key <key>` header matches the expected `CROSS_CONTAINER_API_KEY` Docker environment variable.
33
+ * 2. Session Validation: Uses WordPress's native `wp_validate_auth_cookie()` to verify the provided `user_hash` parameter.
34
+ *
35
+ * If validation succeeds, it injects the `validated_user_id` parameter into the request object so it can be consumed by the main callback without re-querying the database.
36
+ *
37
+ * @since 1.0.0
38
+ * @access public
39
+ *
40
+ * @param WP_REST_Request $request The incoming REST API request object.
41
+ * @return bool|WP_Error Returns `true` if the API key and session hash are valid. Returns a `WP_Error` (401 status) if either check fails.
42
+ */
43
+ function validate_validate_user_session(WP_REST_Request $request): bool | WP_Error {
44
+ $api_key = getenv_docker('CROSS_CONTAINER_API_KEY', '');
45
+ $auth_header = $request->get_header('Authorization');
46
+
47
+ $passed_key = '';
48
+ if (!empty($auth_header) && preg_match('/api-key\s(\S+)/', $auth_header, $matches)) {
49
+ $passed_key = $matches[1];
50
+ }
51
+
52
+ if (!\is_string($api_key) || empty($api_key) || !hash_equals($api_key, $passed_key)) {
53
+ return new WP_Error('rest_forbidden', __('Unauthorized API Key.'), ['status' => 401]);
54
+ }
55
+
56
+ $passed_hash = (string) (\is_scalar($request->get_param('user_hash')) ? $request->get_param('user_hash') : '');
57
+
58
+ $user_id = !empty($passed_hash)
59
+ ? wp_validate_auth_cookie($passed_hash, 'logged_in')
60
+ : wp_validate_auth_cookie('', 'logged_in');
61
+
62
+ if (!$user_id) {
63
+ return new WP_Error('rest_cookie_invalid', __('Invalid session hash.'), ['status' => 401]);
64
+ }
65
+
66
+ $request->set_param('validated_user_id', $user_id);
67
+
68
+ return true;
69
+ }
70
+
71
+ /**
72
+ * Handles the GET request for validating a user session hash.
73
+ *
74
+ * This function serves as the primary callback for the `/nextpress/v1/validate-user-session/` endpoint.
75
+ * It relies on the `validated_user_id` parameter, which is set by the `validate_validate_user_session` permission callback upon successful authentication.
76
+ *
77
+ * ### API Endpoint Information
78
+ * - URL: `/wp-json/nextpress/v1/validate-user-session/`
79
+ * - Method: `GET`
80
+ * - Auth Required: Yes (via `Authorization` header)
81
+ *
82
+ * ### Request Headers
83
+ * - `Authorization`: `api-key <CROSS_CONTAINER_API_KEY>`
84
+ *
85
+ * ### Request Parameters
86
+ * - `user_hash` (string): The hashed session cookie value to validate.
87
+ *
88
+ * ### Success Response
89
+ * - Code: 200 OK
90
+ * - Content: * ```json
91
+ * {
92
+ * "success": true,
93
+ * "user_id": 123
94
+ * }
95
+ * ```
96
+ *
97
+ * ### Error Responses (Handled by permission callback)
98
+ * - Code: 401 Unauthorized (`rest_forbidden`) - Missing or invalid API key.
99
+ * - Code: 401 Unauthorized (`rest_cookie_invalid`) - Invalid session hash.
100
+ *
101
+ * @since 1.0.0
102
+ * @access public
103
+ *
104
+ * @param WP_REST_Request $request The incoming REST API request object containing the `validated_user_id`.
105
+ * @return WP_REST_Response Response object containing the success status and the validated user ID.
106
+ */
107
+ function handle_validate_user_session_response(WP_REST_Request $request): WP_REST_Response {
108
+ $user_id = $request->get_param('validated_user_id');
109
+
110
+ return new WP_REST_Response([
111
+ 'success' => true,
112
+ 'user_id' => $user_id
113
+ ], 200);
114
+ }
@@ -0,0 +1,26 @@
1
+ <?php
2
+
3
+ namespace Nextpress;
4
+
5
+ \defined('ABSPATH') or die;
6
+
7
+ require_once __DIR__ . '/api/nextpress_validate_user_session.php';
8
+ require_once __DIR__ . '/api/nextpress_get_admin_bar.php';
9
+ require_once __DIR__ . '/nextpress_save_path.php';
10
+ require_once __DIR__ . '/nextpress_revalidate_frontend.php';
11
+ require_once __DIR__ . '/nextpress_remove_menu_pages.php';
12
+ require_once __DIR__ . '/nextpress_remove_comments.php';
13
+ require_once __DIR__ . '/nextpress_disable_core_depency_updates.php';
14
+ require_once __DIR__ . '/nextpress_dev_mailer_setup.php';
15
+ require_once __DIR__ . '/theme.php';
16
+
17
+ require_once __DIR__ . '/acf/acf.php';
18
+
19
+ add_action('after_setup_theme', fn() => nextpress_setup_theme());
20
+
21
+ add_action('after_switch_theme', fn() => nextpress_switch_theme());
22
+
23
+ add_action('save_post', fn(int $_post_id, \WP_Post $post, bool $_update) => nextpress_save_post_path($post), 10, 3 );
24
+ add_action('saved_term', fn(int $term_id, int $_tt_id, string $taxonomy) => nextpress_save_term_path($term_id, $taxonomy), 10, 3);
25
+
26
+ add_action('admin_init', fn() => nextpress_revalidate_frontend());
@@ -0,0 +1,17 @@
1
+ <?php
2
+
3
+ add_action('phpmailer_init', function($phpmailer) {
4
+ if (WP_DEBUG) {
5
+ $phpmailer->isSMTP();
6
+ $phpmailer->Host = 'mailpit';
7
+ $phpmailer->Port = 1025;
8
+ $phpmailer->SMTPAuth = false;
9
+ $phpmailer->From = 'wordpress@test.local';
10
+ $phpmailer->FromName = 'WordPress';
11
+ }
12
+ });
13
+
14
+ add_filter('wp_mail_from', function($_original_email_address) {
15
+ return 'wordpress@test.local';
16
+ });
17
+
@@ -0,0 +1,49 @@
1
+ <?php
2
+
3
+ namespace Nextpress;
4
+
5
+ /**
6
+ * Block updates and notifications ONLY for WordPress Core and Secure Custom Fields.
7
+ * Allows translations, other plugins, and themes to update normally.
8
+ */
9
+
10
+ add_filter('pre_site_transient_update_core', function($transient) {
11
+ if (!\is_object($transient)) {
12
+ $transient = new \stdClass();
13
+ }
14
+
15
+ if (property_exists($transient, 'updates')) {
16
+ $transient->updates = [];
17
+ }
18
+ if (property_exists($transient, 'response')) {
19
+ $transient->response = [];
20
+ }
21
+
22
+ return $transient;
23
+ });
24
+
25
+ // 2. Intercept Plugin update checks specifically for Secure Custom Fields
26
+ add_filter('site_transient_update_plugins', function($transient) {
27
+ if (!\is_object($transient) || !isset($transient->response)) {
28
+ return $transient;
29
+ }
30
+
31
+ // The target slug for Secure Custom Fields
32
+ $scf_slug = 'secure-custom-fields/secure-custom-fields.php';
33
+
34
+ // If an update is waiting for SCF, strip it out of the notification loop
35
+ if (property_exists($transient, 'response') && is_array($transient->response) && isset($transient->response[$scf_slug])) {
36
+ unset($transient->response[$scf_slug]);
37
+ }
38
+
39
+ return $transient;
40
+ });
41
+
42
+ // 3. Block automatic background updates for Core and SCF
43
+ add_filter('auto_update_core', '__return_false');
44
+ add_filter('auto_update_plugin', function($update, $item) {
45
+ if (\is_object($item) && isset($item->plugin) && $item->plugin === 'advanced-custom-fields/acf.php') {
46
+ return false;
47
+ }
48
+ return $update;
49
+ }, 10, 2);
@@ -0,0 +1,17 @@
1
+ <?php
2
+
3
+ add_action('init', 'nextpress_disable_comments_post_types_support');
4
+ function nextpress_disable_comments_post_types_support(): void {
5
+ $post_types = get_post_types();
6
+ foreach ($post_types as $post_type) {
7
+ if (post_type_supports($post_type, 'comments')) {
8
+ remove_post_type_support($post_type, 'comments');
9
+ remove_post_type_support($post_type, 'trackbacks');
10
+ }
11
+ }
12
+ }
13
+
14
+ add_filter('comments_open', '__return_false', 20);
15
+ add_filter('pings_open', '__return_false', 20);
16
+
17
+ add_filter('comments_array', '__return_empty_array', 10);
@@ -0,0 +1,32 @@
1
+ <?php
2
+
3
+ add_action('admin_menu', function() {
4
+ remove_menu_page('edit-comments.php');
5
+ remove_submenu_page('options-general.php', 'options-permalink.php');
6
+ remove_menu_page('themes.php');
7
+
8
+ // Create a brand new top-level menu item for Customize
9
+ add_menu_page(
10
+ 'Customize', // Page title
11
+ 'Customize', // Menu title
12
+ 'edit_theme_options', // Capability required to see it
13
+ 'customize.php', // Direct link to the Customizer
14
+ '', // No function needed for an existing file
15
+ 'dashicons-admin-appearance', // Keeps the original brush icon from image_d5e315.png
16
+ 60 // Positions it right where Appearance used to be
17
+ );
18
+ });
19
+
20
+ /* Hides the entire active theme control row in the Customizer */
21
+ add_action('customize_controls_print_styles', function() {
22
+ ?>
23
+ <style>
24
+ #accordion-section-themes, #accordion-section-custom_css {
25
+ display: none !important;
26
+ }
27
+ </style>
28
+ <script>
29
+ document.title = "<?php echo esc_js(get_bloginfo('name')); ?>";
30
+ </script>
31
+ <?php
32
+ });
@@ -0,0 +1,57 @@
1
+ <?php
2
+
3
+ namespace Nextpress;
4
+
5
+ /**
6
+ * Revalidates the Next.js frontend by pinging its API.
7
+ *
8
+ * * @return true|\WP_Error Returns true on success, or a WP_Error object on failure.
9
+ */
10
+ function revalidate_frontend(): true|\WP_Error {
11
+ $nextjs_service_url = getenv_docker('NEXTJS_SERVICE_URL', '');
12
+ $api_key = getenv_docker('CROSS_CONTAINER_API_KEY', '');
13
+
14
+ if (!$nextjs_service_url || !\is_scalar($nextjs_service_url) || !\is_scalar($api_key)) {
15
+ return new \WP_Error(
16
+ 'missing_credentials',
17
+ 'Next.js service URL or API key is missing or invalid.'
18
+ );
19
+ }
20
+
21
+ $response = wp_remote_post($nextjs_service_url . '/api/revalidate', [
22
+ 'headers' => [
23
+ 'Authorization' => 'api-key ' . $api_key,
24
+ 'Content-Type' => 'application/json'
25
+ ]
26
+ ]);
27
+
28
+ if (is_wp_error($response)) {
29
+ return $response;
30
+ }
31
+
32
+ $status_code = wp_remote_retrieve_response_code($response);
33
+
34
+ if ($status_code !== 200) {
35
+ $error_message = wp_remote_retrieve_response_message($response);
36
+ return new \WP_Error(
37
+ 'http_response_error',
38
+ sprintf('Next.js API returned status code %d: %s', $status_code, $error_message),
39
+ ['status_code' => $status_code]
40
+ );
41
+ }
42
+
43
+ return true;
44
+ }
45
+
46
+ /**
47
+ * Triggers the frontend revalidation and logs any errors that occur.
48
+ *
49
+ * @return void
50
+ */
51
+ function nextpress_revalidate_frontend(): void {
52
+ $revalidated = revalidate_frontend();
53
+
54
+ if (is_wp_error($revalidated)) {
55
+ error_log($revalidated->get_error_message());
56
+ }
57
+ }