1+ <?php
2+ /**
3+ * Frak Config Endpoint
4+ *
5+ * Handles dynamic JavaScript configuration delivery with proper caching
6+ */
7+ class Frak_Config_Endpoint {
8+
9+ private static $ instance = null ;
10+
11+ public static function instance () {
12+ if (null === self ::$ instance ) {
13+ self ::$ instance = new self ();
14+ }
15+ return self ::$ instance ;
16+ }
17+
18+ private function __construct () {
19+ // Register the endpoint
20+ add_action ('init ' , array ($ this , 'register_endpoint ' ));
21+ add_action ('template_redirect ' , array ($ this , 'handle_endpoint ' ));
22+
23+ // Flush rewrite rules on activation
24+ register_activation_hook (FRAK_PLUGIN_FILE , array ($ this , 'flush_rewrite_rules ' ));
25+ register_deactivation_hook (FRAK_PLUGIN_FILE , array ($ this , 'flush_rewrite_rules ' ));
26+ }
27+
28+ /**
29+ * Register the custom endpoint
30+ */
31+ public function register_endpoint () {
32+ add_rewrite_rule ('^frak-config\.js$ ' , 'index.php?frak_config=1 ' , 'top ' );
33+ add_rewrite_tag ('%frak_config% ' , '([^&]+) ' );
34+ }
35+
36+ /**
37+ * Handle the endpoint request
38+ */
39+ public function handle_endpoint () {
40+ global $ wp_query ;
41+
42+ if (!isset ($ wp_query ->query_vars ['frak_config ' ])) {
43+ return ;
44+ }
45+
46+ // Get the configuration
47+ $ config = get_option ('frak_custom_config ' , '' );
48+
49+ if (empty ($ config )) {
50+ // Generate default config if none exists
51+ $ admin = Frak_Admin::instance ();
52+ $ config = $ this ->get_compiled_config ();
53+ }
54+
55+ // Generate ETag based on config content
56+ $ etag = md5 ($ config );
57+ $ last_modified = get_option ('frak_config_last_modified ' , time ());
58+
59+ // Set proper headers
60+ $ this ->set_cache_headers ($ etag , $ last_modified );
61+
62+ // Check if client has valid cached version
63+ if ($ this ->is_not_modified ($ etag )) {
64+ status_header (304 );
65+ exit ;
66+ }
67+
68+ // Output the JavaScript
69+ header ('Content-Type: application/javascript; charset=utf-8 ' );
70+
71+ // Apply minification if enabled
72+ if (get_option ('frak_minify_config ' , 0 ) && !defined ('SCRIPT_DEBUG ' )) {
73+ $ config = $ this ->minify_javascript ($ config );
74+ } else {
75+ // Add header comment for non-minified version
76+ $ config = "/* Frak Configuration - Generated: " . date ('Y-m-d H:i:s ' ) . " */ \n" . $ config ;
77+ }
78+
79+ echo $ config ;
80+ exit ;
81+ }
82+
83+ /**
84+ * Get compiled configuration from individual options
85+ */
86+ private function get_compiled_config () {
87+ $ app_name = get_option ('frak_app_name ' , get_bloginfo ('name ' ));
88+ $ logo_url = get_option ('frak_logo_url ' , '' );
89+ $ modal_language = get_option ('frak_modal_language ' , 'default ' );
90+ $ floating_button_position = get_option ('frak_floating_button_position ' , 'right ' );
91+ $ modal_i18n = get_option ('frak_modal_i18n ' , '{} ' );
92+
93+ // Handle language setting
94+ $ lang_code = $ modal_language === 'default ' ? 'undefined ' : "' {$ modal_language }' " ;
95+
96+ return <<<JS
97+ let logoUrl = ' {$ logo_url }';
98+ const lang = {$ lang_code };
99+
100+ let i18n = {};
101+ try {
102+ i18n = JSON.parse(' {$ modal_i18n }'.replace(
103+ /&|<|>|'|"/g,
104+ tag =>
105+ ({
106+ '&': '&',
107+ '<': '<',
108+ '>': '>',
109+ ''': "'",
110+ '"': '"'
111+ }[tag] || tag)
112+ )) || {};
113+ } catch (error) {
114+ console.error('Error parsing i18n customizations:', error);
115+ }
116+
117+ window.FrakSetup = {
118+ config: {
119+ walletUrl: 'https://wallet.frak.id',
120+ metadata: {
121+ name: ' {$ app_name }',
122+ lang,
123+ logoUrl
124+ },
125+ customizations: { i18n },
126+ domain: window.location.host
127+ },
128+ modalConfig: {
129+ login: {
130+ allowSso: true,
131+ ssoMetadata: {
132+ logoUrl,
133+ homepageLink: window.location.host
134+ }
135+ }
136+ },
137+ modalShareConfig: {
138+ link: window.location.href
139+ },
140+ modalWalletConfig: {
141+ metadata: {
142+ position: ' {$ floating_button_position }'
143+ }
144+ },
145+ };
146+ JS ;
147+ }
148+
149+ /**
150+ * Set proper cache headers
151+ */
152+ private function set_cache_headers ($ etag , $ last_modified ) {
153+ // Allow caching for 1 hour, but validate with ETag
154+ header ('Cache-Control: public, max-age=3600, must-revalidate ' );
155+ header ('ETag: " ' . $ etag . '" ' );
156+ header ('Last-Modified: ' . gmdate ('D, d M Y H:i:s ' , $ last_modified ) . ' GMT ' );
157+ header ('X-Content-Type-Options: nosniff ' );
158+
159+ // Add CORS headers if needed
160+ $ allowed_origin = get_option ('frak_cors_origin ' , '* ' );
161+ header ('Access-Control-Allow-Origin: ' . $ allowed_origin );
162+ }
163+
164+ /**
165+ * Check if client has valid cached version
166+ */
167+ private function is_not_modified ($ etag ) {
168+ $ if_none_match = isset ($ _SERVER ['HTTP_IF_NONE_MATCH ' ]) ? $ _SERVER ['HTTP_IF_NONE_MATCH ' ] : false ;
169+
170+ if ($ if_none_match && $ if_none_match === '" ' . $ etag . '" ' ) {
171+ return true ;
172+ }
173+
174+ return false ;
175+ }
176+
177+ /**
178+ * Flush rewrite rules
179+ */
180+ public function flush_rewrite_rules () {
181+ $ this ->register_endpoint ();
182+ flush_rewrite_rules ();
183+ }
184+
185+ /**
186+ * Get config URL with version parameter
187+ */
188+ public static function get_config_url () {
189+ $ config = get_option ('frak_custom_config ' , '' );
190+ $ version = substr (md5 ($ config ), 0 , 8 );
191+
192+ return home_url ('/frak-config.js?v= ' . $ version );
193+ }
194+
195+ /**
196+ * Update last modified timestamp when config changes
197+ */
198+ public static function update_last_modified () {
199+ update_option ('frak_config_last_modified ' , time ());
200+
201+ // Clear caches from popular caching plugins
202+ self ::clear_external_caches ();
203+ }
204+
205+ /**
206+ * Simple JavaScript minification
207+ */
208+ private function minify_javascript ($ js ) {
209+ // Remove comments
210+ $ js = preg_replace ('/\/\*[\s\S]*?\*\/|\/\/.*$/m ' , '' , $ js );
211+
212+ // Remove unnecessary whitespace
213+ $ js = preg_replace ('/\s+/ ' , ' ' , $ js );
214+
215+ // Remove whitespace around operators
216+ $ js = preg_replace ('/\s*([{}:;,=+\-*\/])\s*/ ' , '$1 ' , $ js );
217+
218+ return trim ($ js );
219+ }
220+
221+ /**
222+ * Clear caches from popular caching plugins
223+ */
224+ private static function clear_external_caches () {
225+ // WP Rocket
226+ if (function_exists ('rocket_clean_domain ' )) {
227+ rocket_clean_domain ();
228+ }
229+
230+ // W3 Total Cache
231+ if (function_exists ('w3tc_flush_all ' )) {
232+ w3tc_flush_all ();
233+ }
234+
235+ // WP Super Cache
236+ if (function_exists ('wp_cache_clear_cache ' )) {
237+ wp_cache_clear_cache ();
238+ }
239+
240+ // WP Fastest Cache
241+ if (class_exists ('WpFastestCache ' ) && method_exists ('WpFastestCache ' , 'deleteCache ' )) {
242+ $ wpfc = new WpFastestCache ();
243+ $ wpfc ->deleteCache (true );
244+ }
245+
246+ // LiteSpeed Cache
247+ if (class_exists ('LiteSpeed\Purge ' )) {
248+ LiteSpeed \Purge::purge_all ();
249+ }
250+
251+ // Autoptimize
252+ if (class_exists ('autoptimizeCache ' ) && method_exists ('autoptimizeCache ' , 'clearall ' )) {
253+ autoptimizeCache::clearall ();
254+ }
255+
256+ // Clear WordPress object cache
257+ wp_cache_flush ();
258+ }
259+ }
0 commit comments