PluginProbe
Pushly / trunk
Pushly vtrunk
2.4.0 2.3.0 trunk 1.0 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 2.0.0 2.1.0 2.1.1 2.1.10 2.1.11 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1
pushly / src / php / Admin / Settings.php

Settings.php in Pushly trunk, at src/php/Admin/Settings.php

227 lines 9.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Pushly\Admin;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 class Settings {
10 private const MENU_ICON = 'data:image/svg+xml;base64,ICAgIDxzdmcKICAgICAgICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgICAgICAgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIKICAgICAgICBkYXRhLWljb249InB1c2hseSIKICAgICAgICBoZWlnaHQ9IjEwMjQiCiAgICAgICAgd2lkdGg9IjEwMjQiCiAgICAgICAgZmlsbD0iIzRkYmNhMiIKICAgICAgICBhcmlhLWhpZGRlbj0idHJ1ZSIKICAgID4KICAgICAgICA8cGF0aAogICAgICAgICAgICBkPSJNOTc0LjYwMiwxNTUuNjJjLTE5LjQ1Ni0yOC40NjctNDQuNzI2LTUwLjgyMy03NS4xMzEtNjYuNTA0Qzg2OC4zNyw3My4xMDEsODMzLjg5LDY1LDc5Ni45NCw2NUg2MjcuNTcKCQljLTEzMy40MzgsMC0xOTEuMzEyLDE1MC41MDMtMTk1Ljc1MywxNzIuMTAxbC0xMzcuMzQsNjMzLjU1N2MtNC43NDUsMjMuMTA1LTAuNDA4LDQ0LjQ3LDEyLjU4Miw2MS43NjJsMC41ODMsMC43NTQKCQljMTMuODUxLDE3LjAxNiwzMy42ODEsMjUuOTk5LDU3LjM1MiwyNS45OTljMjEuNTE2LDAsNDEuODg0LTcuNDk5LDYwLjUxNS0yMi4yOThsMC42MjYtMC40OTYKCQljMTguMDMxLTE1LjMwMywyOS40NjctMzMuOTc1LDMzLjg5OC01NS42MTFMNTA5LjE5OSw2NjFoMC4wOTZsMC4xNTYtMUg2ODMuMzVjMTUuNTgsMCwzMS4zMTctMS41ODUsNDcuMTE1LTQuNDcKCQljMS45NTUtMC4zMTIsMy44OTMtMC43MjYsNS44MzEtMS4wODNjMC4yOTUtMC4wNjEsMC41ODItMC4xMDMsMC44NzctMC4xNzJjMTYuOTQ1LTMuMjQxLDMzLjYxMi04LjU3MSw1My41MjEtMTYuOTMxCgkJYzM1LjE0OS0xNC44NzYsNjcuOTk2LTM2LjA4MSw5Ny40ODktNjIuODljNjMuMjg3LTU2LjU5NiwxMDQuMjA2LTEyNi4wNjIsMTIxLjY0Ni0yMDYuNTYzCgkJQzEwMjcuMTU2LDI4Ni4yNTcsMTAxNS4zMDQsMjE0LjgxOCw5NzQuNjAyLDE1NS42MnogTTg0NC4zMzQsMzU3LjU5Yy04LjI2NCwzNy41NTctMjYuMTksNjkuMzkxLTU0LjgzMSw5Ny4zMTEKCQljLTQuODE0LDQuNTQ5LTkuNjAzLDguNTYzLTE0LjM5OSwxMi4zMDhDNzUzLjA0MSw0ODQuMDUsNzMxLjA1Niw0OTIsNzA4LjM3Niw0OTJoLTE2My4yMWwyNi4zMDQtMTIzLjcxOWwxNS44MTUtNzQuMzU5CgkJYzYuMTA4LTE2LjE0MSwxNi41NTQtMzAuMzQ5LDMxLjE5NS00Mi4yNTRsMC42LTAuMjY5QzYzMi42NzEsMjQxLjA0MSw2NDcuMjE4LDIzNCw2NjIuNDQyLDIzMmgxMDkuMjkKCQljMjYuNDQyLDAsNDUuNjgyLDEwLjczLDYwLjU1OCwzNC4yNEM4NDcuOTIzLDI5Mi4wMzYsODUxLjgzNCwzMjEuODU4LDg0NC4zMzQsMzU3LjU5eiIKICAgICAgICAvPgogICAgICAgIDxwYXRoCiAgICAgICAgICAgIGQ9Ik0zNTUuMzEzLDE4Mi42MzZsLTAuMzgyLTAuNDg2Yy04LjgzOC0xMC44NDUtMjEuNDczLTE2LjU4LTM2LjU1OC0xNi41OGMtMTMuNzM4LDAtMjYuNzIxLDQuNzg0LTM4LjYxNywxNC4yMjEKCQlsLTAuMzgyLDAuMzE3Yy0xMS41MTUsOS43NjMtMTguODA1LDIxLjc3NC0yMS42MiwzNS41ODJMMjI2Ljc4NSwzNjJoLTAuMjA5bC04Mi4yMywzODYuODY3CgkJYy0zLjAzMiwxNC43MzYtMC4yNjEsMjguMzUzLDguMDIxLDM5LjM3MWwwLjM4MywwLjQ5MWM4LjgyOCwxMC44NTMsMjEuNDcyLDE2LjU2NywzNi41NTcsMTYuNTY3YzEzLjczOSwwLDI2LjcyOS00Ljc4LDM4LjYtMTQuMjE4CgkJbDAuMzkyLTAuMzJjMTEuNTE0LTkuNzYsMTguNzg3LTIxLjcwNiwyMS42MzctMzUuNTE0TDI4MC45MDQsNjA5aDAuMTk5bDgyLjIzOS0zODYuOTI4CgkJQzM2Ni4zNjcsMjA3LjMzLDM2My41ODYsMTkzLjY2NywzNTUuMzEzLDE4Mi42MzZ6IgogICAgICAgIC8+CiAgICAgICAgPHBhdGgKICAgICAgICAgICAgZD0iTTE2OC40OTQsMjQwLjVsLTAuMzM5LTAuNDNjLTcuNzI1LTkuNTA2LTE4LjgyMS0xNC41MzMtMzIuMDM5LTE0LjUzM2MtMTIuMDM1LDAtMjMuNDE4LDQuMTkyLTMzLjgzNywxMi40NjFsLTAuMzQ4LDAuMjgyCgkJYy0xMC4wOCw4LjU2LTE2LjQ3NiwxOC44NDItMTguOTUyLDMwLjk0Mkw3OC4zMjEsMjg3aC0wLjE3NEw2LjA3NSw2MjYuMzI5Yy0yLjY1OSwxMi45MzEtMC4yMjYsMjQuOTc2LDcuMDIxLDM0LjY0N2wwLjMzOSwwLjQ4MgoJCWM3LjczNCw5LjUwNiwxOC44MzEsMTQuNTU4LDMyLjA0OCwxNC41NThjMTIuMDQ0LDAsMjMuNDI3LTQuMTc1LDMzLjgzOC0xMi40NTZsMC4zMzktMC4yNjJjMTAuMDk3LTguNTYsMTYuNDc2LTE5LjAyLDE4Ljk2LTMxLjEzMwoJCUwxMjUuNzY4LDUwNGgwLjE4Mmw0OS41ODQtMjI4LjkzN0MxNzguMTg0LDI2Mi4xNDEsMTc1Ljc1OSwyNTAuMTY0LDE2OC40OTQsMjQwLjV6IgogICAgICAgIC8+CiAgICA8L3N2Zz4=';
11
12 public function register_hooks(): void {
13 add_action( 'admin_menu', [ $this, 'add_menu_page' ] );
14 add_action( 'rest_api_init', [ $this, 'register_settings' ] );
15 add_action( 'admin_init', [ $this, 'maybe_migrate_options' ] );
16 add_action( 'admin_init', [ $this, 'register_settings' ] );
17 add_action( 'admin_notices', [ $this, 'register_settings_errors' ] );
18 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
19 add_action(
20 'plugin_action_links_' . plugin_basename( PUSHLY__DIR . '/pushly.php' ),
21 [ $this, 'add_links_to_plugin' ]
22 );
23 }
24
25 public function add_menu_page(): void {
26 add_menu_page(
27 'Pushly',
28 'Pushly',
29 'manage_options',
30 'pushly',
31 [ $this, 'menu_page_html' ],
32 self::MENU_ICON
33 );
34 }
35
36 public function menu_page_html(): void {
37 if ( ! current_user_can( 'manage_options' ) ) {
38 return;
39 }
40
41 printf(
42 '<div class="wrap" id="pushly-settings">%s</div>',
43 esc_html__( 'Loading…', 'pushly' )
44 );
45 }
46
47 public function enqueue_scripts( string $admin_page ): void {
48 if ( 'toplevel_page_pushly' !== $admin_page ) {
49 return;
50 }
51
52 $asset_file = PUSHLY__DIR_BUILD . '/settings.asset.php';
53 if ( ! file_exists( $asset_file ) ) {
54 return;
55 }
56
57 $asset = include $asset_file;
58
59 wp_enqueue_script(
60 'pushly-script',
61 plugins_url( 'build/settings.js', PUSHLY__DIR_BUILD ),
62 $asset['dependencies'],
63 $asset['version'],
64 [ 'in_footer' => true ]
65 );
66
67 $options = get_option( 'pushly', [] );
68
69 wp_add_inline_script(
70 'pushly-script',
71 'const pushly_env = ' . wp_json_encode( [
72 'CDN_DOMAIN' => PUSHLY__CDN_DOMAIN,
73 'API_NONCE' => wp_create_nonce( 'wp_rest' ),
74 'debug_logging_enabled' => ! empty( $options['debug_logging_enabled'] ),
75 ] ),
76 'before'
77 );
78
79 wp_enqueue_style(
80 'pushly-style',
81 plugins_url( 'build/settings.css', PUSHLY__DIR_BUILD ),
82 array_filter( $asset['dependencies'], fn( $style ) => wp_style_is( $style, 'registered' ) ),
83 $asset['version']
84 );
85 }
86
87 /**
88 * @param string[] $actions
89 * @return string[]
90 */
91 public function add_links_to_plugin( array $actions ): array {
92 return array_merge( $actions, [
93 'settings' => '<a href="' . admin_url( 'admin.php?page=pushly' ) . '">' . __( 'Settings', 'pushly' ) . '</a>',
94 ] );
95 }
96
97 /**
98 * One-time option migrations. Hooked to admin_init only so DB writes
99 * never happen on REST requests.
100 */
101 public function maybe_migrate_options(): void {
102 $new_options = [];
103
104 $legacy_options = get_option( 'pushly_options' );
105 if ( ! empty( $legacy_options ) ) {
106 delete_option( 'pushly_options' );
107 if ( ! empty( $legacy_options['pushly_domain_key'] ) ) {
108 $new_options['sdk_key'] = $legacy_options['pushly_domain_key'];
109 }
110 }
111
112 $current_options = get_option( 'pushly', [] );
113 if ( empty( $current_options['enabled_post_types'] ) ) {
114 $new_options['enabled_post_types'] = [ 'post' ];
115 }
116
117 if ( ! empty( $new_options ) ) {
118 update_option( 'pushly', array_merge( $new_options, $current_options ) );
119 }
120 }
121
122 public function register_settings(): void {
123 $schema = [
124 'type' => 'object',
125 'properties' => [
126 'domain_id' => [ 'type' => 'integer' ],
127 'sdk_key' => [ 'type' => 'string' ],
128 'api_key' => [ 'type' => [ 'string', 'null' ] ],
129 'sending_enabled' => [ 'type' => 'boolean' ],
130 'auto_send_enabled' => [ 'type' => 'boolean' ],
131 'enabled_post_types' => [ 'type' => 'array' ],
132 'initialization_disabled' => [ 'type' => 'boolean' ],
133 'debug_logging_enabled' => [ 'type' => 'boolean' ],
134 ],
135 ];
136
137 register_setting(
138 'options',
139 'pushly',
140 [
141 'type' => 'object',
142 'show_in_rest' => [ 'schema' => $schema ],
143 'sanitize_callback' => [ $this, 'sanitize_settings' ],
144 ]
145 );
146 }
147
148 /**
149 * @param array $input
150 * @return array|\WP_Error
151 */
152 public function sanitize_settings( array $input ) {
153 $current_options = get_option( 'pushly', [] );
154 $input['domain_id'] = (int) filter_var( $input['domain_id'], FILTER_SANITIZE_NUMBER_INT );
155 $input['sdk_key'] = sanitize_text_field( $input['sdk_key'] );
156
157 // Detect debug_logging_enabled transition from false → true and write environment snapshot.
158 $was_enabled = ! empty( $current_options['debug_logging_enabled'] );
159 $now_enabled = ! empty( $input['debug_logging_enabled'] );
160
161 if ( $now_enabled && ! $was_enabled ) {
162 try {
163 $sdk_key = $input['sdk_key'] ?? '';
164 if ( strlen( $sdk_key ) >= 4 ) {
165 $masked = str_repeat( '*', strlen( $sdk_key ) - 4 ) . substr( $sdk_key, -4 );
166 } else {
167 $masked = str_repeat( '*', strlen( $sdk_key ) );
168 }
169
170 $snapshot_context = [
171 'plugin_version' => PUSHLY__PLUGIN_VERSION,
172 'wordpress_version' => get_bloginfo( 'version' ),
173 'php_version' => phpversion(),
174 'sdk_key' => $masked,
175 'api_key_configured' => ! empty( $input['api_key'] ),
176 'sending_enabled' => ! empty( $input['sending_enabled'] ),
177 'auto_send_enabled' => ! empty( $input['auto_send_enabled'] ),
178 'enabled_post_types' => $input['enabled_post_types'] ?? [],
179 'debug_logging_enabled_at' => gmdate( 'c' ),
180 'active_theme' => wp_get_theme()->get( 'Name' ),
181 'multisite' => is_multisite(),
182 ];
183
184 $log_store = new LogStore();
185 $log_writer = new LogWriter( $log_store, true );
186 $log_writer->log_immediate(
187 'info',
188 'environment_snapshot',
189 'Environment snapshot: Captured — Debug logging enabled at ' . gmdate( 'c' ),
190 null,
191 $snapshot_context
192 );
193 } catch ( \Exception $e ) {
194 // Silently suppress — settings save must proceed even if snapshot fails.
195 }
196 }
197
198 if ( ! empty( $input['sending_enabled'] ) ) {
199 if ( empty( $input['api_key'] ) ) {
200 return new \WP_Error(
201 'missing_required_property',
202 'API key must be provided when sending is enabled.',
203 [ 'status' => 400 ]
204 );
205 }
206
207 // Encrypt the API key if it's new or has changed from the stored value.
208 // On first save, current_options['api_key'] is empty so we always encrypt.
209 // On subsequent saves, we only re-encrypt if the user entered a different key.
210 $stored_key = $current_options['api_key'] ?? '';
211 if ( empty( $stored_key ) || $stored_key !== $input['api_key'] ) {
212 $input['api_key'] = Util::encrypt_api_key(
213 $input['sdk_key'],
214 $input['sdk_key'],
215 $input['api_key']
216 );
217 }
218 }
219
220 return $input;
221 }
222
223 public function register_settings_errors(): void {
224 settings_errors( 'pushly' );
225 }
226 }
227