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 / Util.php

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

140 lines 4.1 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 class Util {
6 private static ?array $_settings = null;
7 private static ?array $_options = null;
8
9 /**
10 * Returns the full options array if an API key is configured, otherwise null.
11 */
12 public static function get_api_options(): ?array {
13 $options = get_option( 'pushly' );
14 if ( empty( $options['api_key'] ) ) {
15 self::log_to_event_stream( 'no_api_key', 'Settings does not contain `api_key`.' );
16 return null;
17 }
18
19 return $options;
20 }
21
22 /**
23 * Encrypts a value using AES-256-CTR with a random IV.
24 * Returns the original value unchanged if OpenSSL is unavailable.
25 */
26 public static function encrypt_api_key( string $salt, string $passphrase, string $value ): string {
27 if ( ! extension_loaded( 'openssl' ) ) {
28 return $value;
29 }
30
31 $method = 'aes-256-ctr';
32 $iv_len = openssl_cipher_iv_length( $method );
33 $iv = openssl_random_pseudo_bytes( $iv_len );
34
35 $raw_value = openssl_encrypt( $value . $salt, $method, $passphrase, 0, $iv );
36 if ( ! $raw_value ) {
37 self::log_to_event_stream( 'encrypt_api_key_failed', 'Failed to encrypt API key.' );
38 }
39
40 return base64_encode( $iv . $raw_value );
41 }
42
43 /**
44 * Decrypts an AES-256-CTR encrypted value and validates the salt.
45 * Returns false if decryption fails or the salt does not match.
46 *
47 * @return string|false
48 */
49 public static function decrypt_api_key( string $salt, string $passphrase, string $value ) {
50 if ( ! extension_loaded( 'openssl' ) ) {
51 return $value;
52 }
53
54 $raw_value = base64_decode( $value, true );
55 $method = 'aes-256-ctr';
56 $iv_len = openssl_cipher_iv_length( $method );
57 $iv = substr( $raw_value, 0, $iv_len );
58 $raw_value = substr( $raw_value, $iv_len );
59
60 $decrypted = openssl_decrypt( $raw_value, $method, $passphrase, 0, $iv );
61 if ( ! $decrypted || substr( $decrypted, -strlen( $salt ) ) !== $salt ) {
62 self::log_to_event_stream( 'decrypt_api_key_failed', 'Failed to decrypt API key.' );
63 return false;
64 }
65
66 return substr( $decrypted, 0, -strlen( $salt ) );
67 }
68
69 /**
70 * Sends a structured debug event to the Pushly event stream.
71 * Only fires when the WORDPRESS_DEBUG_EVENTS flag is enabled on the domain.
72 */
73 public static function log_to_event_stream( string $error_type, string $error_message, $data = null ): void {
74 try {
75 if ( self::$_options === null ) {
76 self::$_options = get_option( 'pushly' ) ?: [];
77 }
78
79 if ( empty( self::$_options['sdk_key'] ) || empty( self::$_options['domain_id'] ) ) {
80 return;
81 }
82
83 if ( self::$_settings === null ) {
84 // Cache domain settings for 5 minutes to avoid an HTTP call on every log
85 $cache_key = 'pushly_domain_settings_' . md5( self::$_options['sdk_key'] );
86 $cached = get_transient( $cache_key );
87
88 if ( $cached !== false ) {
89 self::$_settings = $cached;
90 } else {
91 $request = wp_remote_get( 'https://' . PUSHLY__CDN_DOMAIN . '/domain-settings/' . self::$_options['sdk_key'] );
92 if ( is_wp_error( $request ) ) {
93 return;
94 }
95
96 self::$_settings = json_decode( wp_remote_retrieve_body( $request ), true ) ?: [];
97 set_transient( $cache_key, self::$_settings, 5 * MINUTE_IN_SECONDS );
98 }
99 }
100
101 if ( empty( self::$_settings['domain']['flags'] ) || ! in_array( 'WORDPRESS_DEBUG_EVENTS', self::$_settings['domain']['flags'], true ) ) {
102 return;
103 }
104
105 global $wp_version;
106
107 if ( $data !== null ) {
108 $error_message .= ' (' . serialize( $data ) . ')';
109 }
110
111 $payload = [
112 'domain_id' => self::$_options['domain_id'],
113 'action' => 'error',
114 'data' => [
115 'error_type' => "wordpress_{$error_type}",
116 'error_message' => $error_message,
117 ],
118 'meta' => [
119 'application' => [
120 'identifier' => 'wordpress',
121 'version' => $wp_version,
122 ],
123 'sdk' => [
124 'name' => 'pushly-wordpress-plugin',
125 'version' => PUSHLY__PLUGIN_VERSION,
126 ],
127 'event' => [ 'version' => 3 ],
128 ],
129 ];
130
131 wp_remote_request( 'https://' . PUSHLY__K_DOMAIN . '/event-stream', [
132 'method' => 'POST',
133 'body' => wp_json_encode( $payload ),
134 ] );
135 } catch ( \Exception $e ) {
136 // Intentionally silent — logging must never cause a visible error
137 }
138 }
139 }
140