PluginProbe
AL Pack / 1.2.0
AL Pack v1.2.0
1.3.13 trunk 1.0.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.11 1.3.12
alpack / includes / api.php

api.php in AL Pack 1.2.0, at includes/api.php

395 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * PressLearn API
4 */
5
6 if (!defined('ABSPATH')) {
7 exit;
8 }
9
10 class PressLearn_API {
11
12 public static function init() {
13 add_action('rest_api_init', array(__CLASS__, 'register_endpoints'));
14 add_action('rest_api_init', array(__CLASS__, 'add_cors_support'));
15 add_filter('rest_authentication_errors', array(__CLASS__, 'disable_rest_authentication'), 999);
16 add_filter('rest_nonce_enabled', array(__CLASS__, 'disable_nonce_for_presslearn'), 999);
17 add_action('rest_api_init', array(__CLASS__, 'remove_cookie_check_for_presslearn'));
18 }
19
20 public static function disable_rest_authentication($errors) {
21 $current_route = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
22
23 if (strpos($current_route, '/wp-json/presslearn/v1/activate') !== false) {
24 return $errors;
25 }
26
27 if (strpos($current_route, '/wp-json/presslearn/') !== false) {
28 if (current_user_can('manage_options') || self::is_trusted_origin()) {
29 return true;
30 }
31 }
32
33 return $errors;
34 }
35
36 public static function disable_nonce_for_presslearn($enabled) {
37 $current_route = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
38
39 if (strpos($current_route, '/wp-json/presslearn/v1/activate') !== false) {
40 return $enabled;
41 }
42
43 if (strpos($current_route, '/wp-json/presslearn/') !== false) {
44 if (current_user_can('manage_options') || self::is_trusted_origin()) {
45 return false;
46 }
47 }
48
49 return $enabled;
50 }
51
52 public static function remove_cookie_check_for_presslearn() {
53 add_filter('rest_pre_dispatch', function($result, $server, $request) {
54 $route = $request->get_route();
55
56 if (strpos($route, '/presslearn/') === 0) {
57 remove_filter('rest_pre_dispatch', 'rest_cookie_check_errors', 10);
58 }
59
60 return $result;
61 }, 5, 3);
62 }
63
64 public static function add_cors_support() {
65 add_filter('rest_pre_serve_request', function($served, $result, $request) {
66 $route = $request->get_route();
67
68 if (strpos($route, '/presslearn/') === 0) {
69 $origin = get_http_origin();
70 if ($origin) {
71 header('Access-Control-Allow-Origin: ' . esc_url_raw($origin));
72 header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
73 header('Access-Control-Allow-Headers: Content-Type, Authorization');
74 header('Access-Control-Allow-Credentials: true');
75 }
76
77 if (isset($_SERVER['REQUEST_METHOD']) && sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) === 'OPTIONS') {
78 status_header(200);
79 return true;
80 }
81 }
82
83 return $served;
84 }, 10, 3);
85 }
86
87 public static function register_endpoints() {
88 register_rest_route('presslearn/v1', '/activate', array(
89 'methods' => 'POST',
90 'callback' => array(__CLASS__, 'activate_plugin'),
91 'permission_callback' => array(__CLASS__, 'check_activate_permission'),
92 ));
93
94 register_rest_route('presslearn/v1', '/status', array(
95 'methods' => 'GET',
96 'callback' => array(__CLASS__, 'check_status'),
97 'permission_callback' => array(__CLASS__, 'check_permission'),
98 ));
99
100 register_rest_route('presslearn/v1', '/banner', array(
101 'methods' => 'GET',
102 'callback' => array(__CLASS__, 'get_banner'),
103 'permission_callback' => array(__CLASS__, 'check_permission'),
104 ));
105 }
106
107 public static function check_permission($request) {
108 if (current_user_can('manage_options')) {
109 return true;
110 }
111
112 $site_host = wp_parse_url(site_url(), PHP_URL_HOST);
113 $origin = get_http_origin();
114 $origin_host = $origin ? wp_parse_url($origin, PHP_URL_HOST) : '';
115
116 if ($origin_host && $origin_host === $site_host) {
117 return true;
118 }
119
120 $current_host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : '';
121 if ($current_host === $site_host) {
122 return true;
123 }
124
125 return false;
126 }
127
128 public static function check_admin_permission($request) {
129 return self::check_permission($request);
130 }
131
132 private static function is_trusted_origin() {
133 $origin = get_http_origin();
134 $allowed_domains = array('qa.ledu.kr', 'api.qa.ledu.kr', 'presslearn.co.kr');
135
136 if (!empty($origin)) {
137 $origin_host = wp_parse_url($origin, PHP_URL_HOST);
138 if ($origin_host && in_array($origin_host, $allowed_domains, true)) {
139 return true;
140 }
141 }
142
143 $site_host = wp_parse_url(site_url(), PHP_URL_HOST);
144 $origin_host = $origin ? wp_parse_url($origin, PHP_URL_HOST) : '';
145
146 return $origin_host === $site_host;
147 }
148
149 private static function validate_trusted_domain_request($request, $origin_host) {
150
151 $key = $request->get_param('key');
152 if (empty($key)) {
153 error_log("PressLearn: Activation request from {$origin_host} without key");
154 return false;
155 }
156
157 if (strlen($key) < 32) {
158 error_log("PressLearn: Activation request from {$origin_host} with invalid key format");
159 return false;
160 }
161
162 $recent_attempts = get_transient('presslearn_activation_attempts_' . md5($origin_host));
163 if ($recent_attempts && $recent_attempts > 5) {
164 error_log("PressLearn: Too many activation attempts from {$origin_host}");
165 return false;
166 }
167
168 set_transient('presslearn_activation_attempts_' . md5($origin_host), ($recent_attempts + 1), 300); // 5분
169
170 $user_agent = $request->get_header('User-Agent');
171 if (empty($user_agent) || strlen($user_agent) < 10) {
172 error_log("PressLearn: Suspicious User-Agent from {$origin_host}: " . $user_agent);
173 return false;
174 }
175
176 error_log("PressLearn: Valid activation request from trusted domain: {$origin_host}");
177 return true;
178 }
179
180 public static function check_activate_permission($request) {
181 if (current_user_can('manage_options')) {
182 return true;
183 }
184
185 $origin = get_http_origin();
186 $allowed_domains = array('qa.ledu.kr', 'api.qa.ledu.kr', 'presslearn.co.kr');
187 $origin_host = $origin ? wp_parse_url($origin, PHP_URL_HOST) : '';
188
189 if (!empty($origin_host) && in_array($origin_host, $allowed_domains, true)) {
190 return self::validate_trusted_domain_request($request, $origin_host);
191 }
192
193 $site_host = wp_parse_url(site_url(), PHP_URL_HOST);
194 if ($origin_host === $site_host) {
195 $nonce = $request->get_header('X-WP-Nonce');
196 if ((!empty($nonce) && wp_verify_nonce($nonce, 'wp_rest')) || is_user_logged_in()) {
197 return true;
198 }
199 }
200
201 return false;
202 }
203
204 public static function activate_plugin($request) {
205 $key = $request->get_param('key');
206 $origin = get_http_origin();
207
208 error_log("PressLearn: Activation attempt - Origin: {$origin}, Key length: " . strlen($key ?: ''));
209
210 if (empty($key)) {
211 error_log("PressLearn: Activation failed - Empty key from origin: {$origin}");
212 return new WP_Error(
213 'invalid_key',
214 '유효하지 않은 키�
215 니다.',
216 array('status' => 400)
217 );
218 }
219
220 $is_valid = self::validate_key($key);
221
222 if (!$is_valid) {
223 error_log("PressLearn: Activation failed - Invalid key from origin: {$origin}, Key: " . substr($key, 0, 8) . '...');
224 return new WP_Error(
225 'invalid_key',
226 '키가 유효하지 않습니다.',
227 array('status' => 400)
228 );
229 }
230
231 update_option('presslearn_plugin_key', $key);
232
233 self::perform_activation_tasks($key);
234
235 error_log("PressLearn: Activation successful from origin: {$origin}");
236
237 return array(
238 'success' => true,
239 'message' => 'Successfully activated.',
240 'timestamp' => current_time('timestamp'),
241 'debug' => array(
242 'origin' => $origin,
243 'key_length' => strlen($key),
244 'is_admin' => current_user_can('manage_options'),
245 'user_logged_in' => is_user_logged_in()
246 )
247 );
248 }
249
250 public static function check_status($request) {
251 $key = get_option('presslearn_plugin_key', '');
252 $is_active = !empty($key);
253
254 $activated_time = get_option('presslearn_plugin_activated_time', 0);
255
256 return array(
257 'is_active' => $is_active,
258 'activated_time' => $activated_time,
259 'message' => $is_active ? '플러그인이 활성화되었습니다.' : '플러그인이 비활성화 상태�
260 니다.'
261 );
262 }
263
264 private static function validate_key($key) {
265 $start_time = microtime(true);
266
267 $is_empty = empty($key);
268 $is_short = strlen($key ?: '') < 32;
269
270 $server_valid = self::mock_validate_with_presslearn_server($key ?: '');
271
272 $is_valid = !$is_empty && !$is_short && $server_valid;
273
274 $min_execution_time = 0.1;
275 $elapsed = microtime(true) - $start_time;
276 if ($elapsed < $min_execution_time) {
277 usleep(($min_execution_time - $elapsed) * 1000000);
278 }
279
280 return $is_valid;
281 }
282
283 private static function mock_validate_with_presslearn_server($key) {
284 $origin = get_http_origin();
285 $origin_host = $origin ? wp_parse_url($origin, PHP_URL_HOST) : '';
286
287 if (empty($key) || strlen($key) < 32) {
288 error_log("PressLearn: Key validation failed - Invalid format from {$origin_host}");
289 return false;
290 }
291
292 if (strpos($key, 'PL_') === 0) {
293 error_log("PressLearn: Key validation success - Test key from {$origin_host}");
294 return true;
295 }
296
297 $trusted_domains = array('qa.ledu.kr', 'api.qa.ledu.kr', 'presslearn.co.kr');
298 if (in_array($origin_host, $trusted_domains, true)) {
299 error_log("PressLearn: Key validation success - Trusted domain {$origin_host}");
300 return true;
301 }
302
303 if (current_user_can('manage_options')) {
304 error_log('PressLearn: Key validation success - Admin activated from ' . $origin_host . ' with key: ' . substr($key, 0, 8) . '...');
305 return true;
306 }
307
308 error_log("PressLearn: Key validation failed - Untrusted source {$origin_host}");
309 return false;
310 }
311
312
313 private static function perform_activation_tasks($key) {
314 update_option('presslearn_plugin_activated_time', time());
315 self::log_activation_event($key);
316 }
317
318 private static function log_activation_event($key) {
319 $masked_key = substr($key, 0, 4) . '...' . substr($key, -4);
320
321 $remote_addr = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
322 $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
323 $origin = get_http_origin();
324
325 $user_info = 'anonymous';
326 if (is_user_logged_in()) {
327 $current_user = wp_get_current_user();
328 $user_info = $current_user->user_login . ' (ID: ' . $current_user->ID . ')';
329 }
330
331 $log_data = array(
332 'time' => current_time('mysql'),
333 'key' => $masked_key,
334 'ip' => $remote_addr,
335 'user_agent' => $user_agent,
336 'origin' => $origin,
337 'user' => $user_info,
338 'is_admin' => current_user_can('manage_options'),
339 'method' => 'REST_API'
340 );
341
342 $activation_logs = get_option('presslearn_activation_logs', array());
343 $activation_logs[] = $log_data;
344
345 if (count($activation_logs) > 20) {
346 $activation_logs = array_slice($activation_logs, -20);
347 }
348
349 update_option('presslearn_activation_logs', $activation_logs);
350
351 error_log('PressLearn Activation: ' . json_encode($log_data));
352 }
353
354 public static function get_banner($request) {
355 $supabase_url = 'https://odkponsvhcfajgoetubm.supabase.co';
356 $supabase_key = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9ka3BvbnN2aGNmYWpnb2V0dWJtIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDU4OTIxMjMsImV4cCI6MjA2MTQ2ODEyM30.yR08gaJeSy4kagAT3PZl1i8uAC6aEEnZSfQ4sSbqOYk';
357
358 $response = wp_remote_get(
359 $supabase_url . '/rest/v1/banner_table?id=eq.1',
360 array(
361 'headers' => array(
362 'apikey' => $supabase_key,
363 'Authorization' => 'Bearer ' . $supabase_key,
364 'Content-Type' => 'application/json'
365 )
366 )
367 );
368
369 if (is_wp_error($response)) {
370 return new WP_Error(
371 'banner_fetch_error',
372 '배너 데이터를 가져오는데 실패했습니다.',
373 array('status' => 500)
374 );
375 }
376
377 $body = wp_remote_retrieve_body($response);
378 $data = json_decode($body, true);
379
380 if (empty($data)) {
381 return new WP_Error(
382 'no_banner',
383 '배너 데이터가 없습니다.',
384 array('status' => 404)
385 );
386 }
387
388 return array(
389 'success' => true,
390 'data' => $data[0]
391 );
392 }
393 }
394
395 PressLearn_API::init();