Diff -- /var/www/vhosts/3dshawn.com/crm.3dshawn.com/MODS/ContactForge/Config.pm
Diff
/var/www/vhosts/3dshawn.com/crm.3dshawn.com/MODS/ContactForge/Config.pm
added on local at 2026-07-01 15:02:52
Added
+0
lines
Removed
-0
lines
Context
412
unchanged
Blobs
from 1cd2199fe9ad
to 1cd2199fe9ad
to 1cd2199fe9ad
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.| 1 | 1 | package MODS::ContactForge::Config; |
| 2 | 2 | #====================================================================== |
| 3 | 3 | # WebSTLs — central configuration. |
| 4 | 4 | # |
| 5 | 5 | # Lookup order for settings(): |
| 6 | 6 | # 1. platform_settings table (DB-backed) -- editable via |
| 7 | 7 | # /admin_config.cgi by a super-admin. Highest precedence so an |
| 8 | 8 | # admin can override anything without a code deploy. |
| 9 | 9 | # 2. /var/www/vhosts/webstls.com/private/stripe.conf -- legacy |
| 10 | 10 | # bootstrap path for the Stripe secret + webhook secret on a |
| 11 | 11 | # brand-new install (before the admin has opened the config page). |
| 12 | 12 | # 3. The hardcoded %DEFAULTS hash below -- final fallback. These are |
| 13 | 13 | # also the deploy-coupled keys that must stay in code so they're |
| 14 | 14 | # readable before the DB is reachable (database_name, asset paths, |
| 15 | 15 | # cookie/session, feature-flag fallbacks). |
| 16 | 16 | # |
| 17 | 17 | # Lookups are cached per request inside %_DB_CACHE so a page that |
| 18 | 18 | # reads ten settings only hits the DB once. The DB probe is column- |
| 19 | 19 | # existence-guarded so pre-migration installs degrade cleanly to the |
| 20 | 20 | # file + code fallbacks. |
| 21 | 21 | #====================================================================== |
| 22 | 22 | |
| 23 | 23 | use strict; |
| 24 | 24 | use warnings; |
| 25 | 25 | |
| 26 | 26 | # Whitelist of keys the admin can override via the Software |
| 27 | 27 | # Configuration page. Anything not in this list is read straight from |
| 28 | 28 | # %DEFAULTS and is_secret marking controls UI masking. |
| 29 | 29 | # |
| 30 | 30 | # Note: this also defines what /admin_config.cgi can WRITE. Adding a |
| 31 | 31 | # key here is the one-line "expose this to the admin" switch. |
| 32 | 32 | my %EDITABLE = ( |
| 33 | 33 | session_minutes => { |
| 34 | 34 | label => 'Session length (minutes)', |
| 35 | 35 | group => 'Defaults', secret => 0, |
| 36 | 36 | description => 'How long a user stays signed in after login. Default 720 = 12 hours. Affects both server-side user_sessions.expires_at AND the browser cookie Max-Age. Per-account override at users.session_minutes_override takes precedence.', |
| 37 | 37 | }, |
| 38 | 38 | # Stripe |
| 39 | 39 | stripe_publishable => { |
| 40 | 40 | label => 'Stripe publishable key', |
| 41 | 41 | group => 'Stripe', secret => 0, |
| 42 | 42 | description => 'From Stripe Dashboard > Developers > API keys. Starts with pk_live_ for production or pk_test_ for test mode. Safe to expose to browsers -- it is embedded in the card-entry form on /billing_payment.cgi so Stripe Elements can tokenize card numbers without the PAN ever touching our server.', |
| 43 | 43 | }, |
| 44 | 44 | stripe_secret => { |
| 45 | 45 | label => 'Stripe secret key', |
| 46 | 46 | group => 'Stripe', secret => 1, |
| 47 | 47 | description => 'From Stripe Dashboard > Developers > API keys. Starts with sk_live_ or sk_test_. NEVER paste this anywhere outside this field -- anyone with this key can charge cards on your Stripe account. Server-side code uses it to create customers, SetupIntents, and PaymentIntents.', |
| 48 | 48 | }, |
| 49 | 49 | stripe_connect_client => { |
| 50 | 50 | label => 'Stripe Connect client id', |
| 51 | 51 | group => 'Stripe', secret => 0, |
| 52 | 52 | description => 'From Stripe Dashboard > Settings > Connect > "Client ID". Starts with ca_. Identifies your platform when sellers onboard their connected accounts via Stripe Express. Required for buyer-to-seller destination charges; not used for subscription billing.', |
| 53 | 53 | }, |
| 54 | 54 | stripe_webhook_secret => { |
| 55 | 55 | label => 'Stripe webhook signing secret', |
| 56 | 56 | group => 'Stripe', secret => 1, |
| 57 | 57 | description => 'From Stripe Dashboard > Developers > Webhooks > your endpoint > "Signing secret". Starts with whsec_. Used to verify incoming webhook calls really came from Stripe. Set this AFTER pointing the webhook at https://webstls.com/stripe_webhook.cgi -- Stripe generates the secret when you create the endpoint.', |
| 58 | 58 | }, |
| 59 | 59 | stripe_platform_fee_bps => { |
| 60 | 60 | label => 'Platform fee (basis points)', |
| 61 | 61 | group => 'Stripe', secret => 0, |
| 62 | 62 | description => 'WebSTLs cut of each buyer-to-seller transaction, in basis points: 100 = 1%, 1000 = 10%, 250 = 2.5%. Stripe takes this as application_fee_amount from the seller payout. Does NOT apply to subscription invoices (those are direct platform charges, not destination charges).', |
| 63 | 63 | }, |
| 64 | 64 | stripe_currency_default => { |
| 65 | 65 | label => 'Default currency code', |
| 66 | 66 | group => 'Stripe', secret => 0, |
| 67 | 67 | description => 'Three-letter ISO 4217 code, lowercase: usd, eur, gbp, cad. Used when a transaction does not specify its own currency. Only affects Stripe API calls -- the storefront display currency is set per-order.', |
| 68 | 68 | }, |
| 69 | 69 | |
| 70 | 70 | # Branding |
| 71 | 71 | website_title => { |
| 72 | 72 | label => 'Site title', |
| 73 | 73 | group => 'Branding', secret => 0, |
| 74 | 74 | description => 'The HTML <title> tag value -- shown in browser tabs, search results, and link previews. HTML entities are allowed (e.g. —).', |
| 75 | 75 | }, |
| 76 | 76 | brand_name => { |
| 77 | 77 | label => 'Brand name', |
| 78 | 78 | group => 'Branding', secret => 0, |
| 79 | 79 | description => 'Short product name shown in the wrapper header, profile dropdown, marketing pages, and the From-line on system emails.', |
| 80 | 80 | }, |
| 81 | 81 | brand_tagline => { |
| 82 | 82 | label => 'Tagline', |
| 83 | 83 | group => 'Branding', secret => 0, |
| 84 | 84 | description => 'One-line tagline shown under the brand name in the sidebar header and a few marketing pages.', |
| 85 | 85 | }, |
| 86 | 86 | |
| 87 | 87 | |
| 88 | 88 | feedback_emails => { |
| 89 | 89 | label => 'Support / feedback inbox', |
| 90 | 90 | group => 'Email', secret => 0, |
| 91 | 91 | description => 'Inbox that receives "Contact us" form submissions. Comma-separate for multiple recipients (e.g. "support@example.com, ops@example.com").', |
| 92 | 92 | }, |
| 93 | 93 | from_email => { |
| 94 | 94 | label => 'Outbound "From" address', |
| 95 | 95 | group => 'Email', secret => 0, |
| 96 | 96 | description => 'Sender address on system-generated emails (password resets, receipts, invoices). Must be on a domain whose SPF and DKIM records you control -- otherwise messages land in spam.', |
| 97 | 97 | }, |
| 98 | 98 | smtp_host => { |
| 99 | 99 | label => 'SMTP host', |
| 100 | 100 | group => 'Email', secret => 0, |
| 101 | 101 | description => 'SMTP server hostname for outbound mail (e.g. smtp.gmail.com, smtp.sendgrid.net, smtp.postmarkapp.com). Leave blank to use the server\'s local sendmail / Plesk MTA.', |
| 102 | 102 | }, |
| 103 | 103 | smtp_port => { |
| 104 | 104 | label => 'SMTP port', |
| 105 | 105 | group => 'Email', secret => 0, |
| 106 | 106 | description => 'Usually 587 for STARTTLS or 465 for implicit TLS. Only consulted when SMTP host is set.', |
| 107 | 107 | }, |
| 108 | 108 | smtp_user => { |
| 109 | 109 | label => 'SMTP username', |
| 110 | 110 | group => 'Email', secret => 0, |
| 111 | 111 | description => 'Authentication username for the SMTP server. Often your full email address. Leave blank if your SMTP server allows unauthenticated relay (uncommon).', |
| 112 | 112 | }, |
| 113 | 113 | smtp_pass => { |
| 114 | 114 | label => 'SMTP password', |
| 115 | 115 | group => 'Email', secret => 1, |
| 116 | 116 | description => 'Authentication password (or app-specific password / API token) for the SMTP server. Stored encrypted at rest; never echoed back to the browser after save.', |
| 117 | 117 | }, |
| 118 | 118 | |
| 119 | 119 | # File storage (R2) |
| 120 | 120 | r2_bucket => { |
| 121 | 121 | label => 'R2 bucket name', |
| 122 | 122 | group => 'Storage', secret => 0, |
| 123 | 123 | description => 'Cloudflare R2 bucket where uploaded model files and storefront images are stored. The CGIs use this when generating signed upload and download URLs.', |
| 124 | 124 | }, |
| 125 | 125 | r2_public_base => { |
| 126 | 126 | label => 'R2 public base URL', |
| 127 | 127 | group => 'Storage', secret => 0, |
| 128 | 128 | description => 'Public-facing base URL for the R2 bucket (typically a custom CNAME like https://files.webstls.com). Prepended to object keys when building <img> and <a href> URLs visitors see. No trailing slash. Use only for images / public assets -- paid model files must stay in the bucket and be served via the signed-URL path below.', |
| 129 | 129 | }, |
| 130 | 130 | r2_account_id => { |
| 131 | 131 | label => 'R2 account ID', |
| 132 | 132 | group => 'Storage', secret => 0, |
| 133 | 133 | description => 'Cloudflare account ID for R2. Find it in Cloudflare Dashboard > R2 > "Account ID" on the right sidebar. Used to construct the signed-URL endpoint as <account_id>.r2.cloudflarestorage.com. Required for serving paid model files directly from R2.', |
| 134 | 134 | }, |
| 135 | 135 | r2_access_key_id => { |
| 136 | 136 | label => 'R2 access key ID', |
| 137 | 137 | group => 'Storage', secret => 0, |
| 138 | 138 | description => 'Cloudflare R2 API token "Access Key ID". Generate at Cloudflare Dashboard > R2 > Manage R2 API tokens > "Create API token" (Object Read access is sufficient for the download path).', |
| 139 | 139 | }, |
| 140 | 140 | r2_secret_access_key => { |
| 141 | 141 | label => 'R2 secret access key', |
| 142 | 142 | group => 'Storage', secret => 1, |
| 143 | 143 | description => 'Secret half of the R2 API token (shown only once when the token is created in Cloudflare). Used to HMAC-SHA256-sign download URLs server-side. NEVER share or commit -- the signing key is your bucket\'s access control.', |
| 144 | 144 | }, |
| 145 | 145 | r2_region => { |
| 146 | 146 | label => 'R2 region', |
| 147 | 147 | group => 'Storage', secret => 0, |
| 148 | 148 | description => 'R2 is single-region for signing purposes -- leave as "auto" unless Cloudflare instructs otherwise. Used as the region segment in the AWS sigv4 credential scope.', |
| 149 | 149 | }, |
| 150 | 150 | |
| 151 | 151 | # Defaults |
| 152 | 152 | timezone_default => { |
| 153 | 153 | label => 'Default timezone', |
| 154 | 154 | group => 'Defaults', secret => 0, |
| 155 | 155 | description => 'IANA timezone name -- UTC, America/New_York, Europe/Berlin, Asia/Tokyo. New users inherit this until they pick their own in Preferences. Affects display only; the database stores timestamps in server time.', |
| 156 | 156 | }, |
| 157 | 157 | currency_default => { |
| 158 | 158 | label => 'Default currency', |
| 159 | 159 | group => 'Defaults', secret => 0, |
| 160 | 160 | description => 'Three-letter ISO 4217 code (USD, EUR, GBP, CAD). New users inherit this for their storefront display currency. Affects display only -- order amounts are stored in the currency they were transacted in.', |
| 161 | 161 | }, |
| 162 | 162 | ); |
| 163 | 163 | |
| 164 | 164 | my %DEFAULTS = ( |
| 165 | 165 | # ----- Database ---------------------------------------------- |
| 166 | 166 | database_name => 'crm3dshawn', |
| 167 | 167 | |
| 168 | 168 | # ----- Branding ---------------------------------------------- |
| 169 | 169 | website_title => 'ContactForge — Customer Relationship Platform', |
| 170 | 170 | brand_name => 'ContactForge', |
| 171 | 171 | brand_tagline => 'One place for contacts, deals, activities, and customer chat.', |
| 172 | 172 | |
| 173 | 173 | # ----- Brand colors (read by Wrapper.pm + admin pages) ------- |
| 174 | 174 | # Electric cyan -- distinct from every sibling site (ABForge sunset, |
| 175 | 175 | # WebSTLs/AffSoft indigo, ShopCart olive, RePricer green, TaskForge |
| 176 | 176 | # crimson). Pops on dark bg. |
| 177 | 177 | color_brand => '#06b6d4', |
| 178 | 178 | color_brand_2 => '#67e8f9', |
| 179 | 179 | color_accent => '#5aa9ff', |
| 180 | 180 | |
| 181 | 181 | # ----- Sessions / cookies ------------------------------------ |
| 182 | 182 | cookie_name => 'cf_session', |
| 183 | 183 | session_minutes => 720, |
| 184 | 184 | secure_cookies => 1, |
| 185 | 185 | cookie_domain => '.crm.3dshawn.com', |
| 186 | 186 | |
| 187 | 187 | # ----- Asset paths ------------------------------------------- |
| 188 | 188 | assets => '/assets', |
| 189 | 189 | assets_css => '/assets/css', |
| 190 | 190 | assets_js => '/assets/javascript', |
| 191 | 191 | assets_images => '/assets/images', |
| 192 | 192 | |
| 193 | 193 | # ----- Email / mail ------------------------------------------ |
| 194 | 194 | # PLATFORM (our own) transactional email goes through these creds -- |
| 195 | 195 | # signup confirms, password resets, billing receipts, admin alerts. |
| 196 | 196 | # Set these in admin Settings; leave smtp_host blank to fall back to |
| 197 | 197 | # the local Plesk MTA (fine for low volume). |
| 198 | 198 | feedback_emails => 'support@crm.3dshawn.com', |
| 199 | 199 | from_email => 'noreply@crm.3dshawn.com', |
| 200 | 200 | smtp_host => '', |
| 201 | 201 | smtp_port => 587, |
| 202 | 202 | smtp_user => '', |
| 203 | 203 | smtp_pass => '', |
| 204 | 204 | # BYO SMTP encryption key -- AES key used by AES_ENCRYPT/AES_DECRYPT |
| 205 | 205 | # to encrypt tenant-supplied SMTP passwords at rest in email_providers. |
| 206 | 206 | # Long random secret. Rotating this invalidates every saved tenant |
| 207 | 207 | # password (they must re-enter), so only rotate if it's leaked. |
| 208 | 208 | byo_smtp_enc_key => 'k7QmZ3RvY9JxL8nWfP2dT5sH4bU6gA1eC0iE-NbX_VqMjOiPyDhRoSlFwUaKuTzM', |
| 209 | 209 | |
| 210 | 210 | # ----- Stripe ------------------------------------------------ |
| 211 | 211 | stripe_publishable => 'pk_test_REPLACE_ME', |
| 212 | 212 | stripe_secret => '', |
| 213 | 213 | stripe_webhook_secret => '', |
| 214 | 214 | stripe_currency_default => 'usd', |
| 215 | 215 | |
| 216 | 216 | # ----- Plan defaults ---------------------------------------- |
| 217 | 217 | # Post pricing-v3 (2026-06): 2 paid tiers (Pro $29, Business $99), |
| 218 | 218 | # 14-day free trial on every signup, contact-count capped during trial. |
| 219 | 219 | plan_pro_price => 29, |
| 220 | 220 | plan_business_price => 99, |
| 221 | 221 | plan_trial_days => 14, |
| 222 | 222 | plan_trial_contacts_cap => 250, |
| 223 | 223 | |
| 224 | 224 | # ----- Misc -------------------------------------------------- |
| 225 | 225 | timezone_default => 'UTC', |
| 226 | 226 | currency_default => 'USD', |
| 227 | 227 | ); |
| 228 | 228 | |
| 229 | 229 | # Per-process cache. Populated on first DB hit; survives for the |
| 230 | 230 | # request and is cheap on subsequent settings() calls. |
| 231 | 231 | my %_DB_CACHE; |
| 232 | 232 | my $_DB_LOADED = 0; |
| 233 | 233 | my $_DB_AVAILABLE; # undef = unknown, 0 = table missing, 1 = ready |
| 234 | 234 | |
| 235 | 235 | sub new { |
| 236 | 236 | my ($class) = @_; |
| 237 | 237 | return bless({}, $class); |
| 238 | 238 | } |
| 239 | 239 | |
| 240 | 240 | # Public helper for the admin config page. Returns the editable |
| 241 | 241 | # whitelist with current values + source ('db' | 'file' | 'default'). |
| 242 | 242 | # is_secret rows have their value masked to a non-empty marker so the |
| 243 | 243 | # UI can show "set" without leaking the actual secret. |
| 244 | 244 | sub editable_settings_meta { |
| 245 | 245 | my ($self) = @_; |
| 246 | 246 | my @rows; |
| 247 | 247 | foreach my $key (sort keys %EDITABLE) { |
| 248 | 248 | my $meta = $EDITABLE{$key}; |
| 249 | 249 | my ($val, $source) = $self->_resolve_with_source($key); |
| 250 | 250 | my $masked = ($meta->{secret} && defined($val) && length $val) ? 1 : 0; |
| 251 | 251 | push @rows, { |
| 252 | 252 | key => $key, |
| 253 | 253 | label => $meta->{label}, |
| 254 | 254 | group => $meta->{group}, |
| 255 | 255 | secret => $meta->{secret}, |
| 256 | 256 | description => $meta->{description} || '', |
| 257 | 257 | value => $masked ? '' : (defined $val ? $val : ''), |
| 258 | 258 | is_set => (defined $val && length $val) ? 1 : 0, |
| 259 | 259 | source => $source, |
| 260 | 260 | }; |
| 261 | 261 | } |
| 262 | 262 | return @rows; |
| 263 | 263 | } |
| 264 | 264 | |
| 265 | 265 | # Bool -- is this key editable via the admin config page? |
| 266 | 266 | sub is_editable { |
| 267 | 267 | my ($self, $key) = @_; |
| 268 | 268 | return exists $EDITABLE{$key} ? 1 : 0; |
| 269 | 269 | } |
| 270 | 270 | |
| 271 | 271 | sub editable_meta_for { |
| 272 | 272 | my ($self, $key) = @_; |
| 273 | 273 | return $EDITABLE{$key}; |
| 274 | 274 | } |
| 275 | 275 | |
| 276 | 276 | sub settings { |
| 277 | 277 | my ($self, $key) = @_; |
| 278 | 278 | return undef unless defined $key; |
| 279 | 279 | |
| 280 | 280 | my ($val) = $self->_resolve_with_source($key); |
| 281 | 281 | return $val; |
| 282 | 282 | } |
| 283 | 283 | |
| 284 | 284 | # Core lookup. Returns ($value, $source) where source is one of |
| 285 | 285 | # 'db' | 'file' | 'default'. Centralises the precedence so any caller |
| 286 | 286 | # (settings() / editable_settings_meta) gets the same answer. |
| 287 | 287 | sub _resolve_with_source { |
| 288 | 288 | my ($self, $key) = @_; |
| 289 | 289 | |
| 290 | 290 | # 1) DB lookup (cached). Skip for the database_name itself -- we |
| 291 | 291 | # can't ask the DB which DB to use. |
| 292 | 292 | if ($key ne 'database_name') { |
| 293 | 293 | $self->_load_db_cache; |
| 294 | 294 | if (exists $_DB_CACHE{$key}) { |
| 295 | 295 | return ($_DB_CACHE{$key}, 'db'); |
| 296 | 296 | } |
| 297 | 297 | } |
| 298 | 298 | |
| 299 | 299 | # 2) Private file fallback (legacy Stripe secret path). Keeps |
| 300 | 300 | # brand-new installs working until the admin opens the config |
| 301 | 301 | # page. |
| 302 | 302 | if ($key eq 'stripe_secret' || $key eq 'stripe_webhook_secret') { |
| 303 | 303 | my $val = _read_private('stripe.conf', $key); |
| 304 | 304 | return ($val, 'file') if defined $val && length $val; |
| 305 | 305 | } |
| 306 | 306 | |
| 307 | 307 | # 3) Hardcoded default. |
| 308 | 308 | return (exists $DEFAULTS{$key} ? $DEFAULTS{$key} : undef, 'default'); |
| 309 | 309 | } |
| 310 | 310 | |
| 311 | 311 | # Single-pass population of %_DB_CACHE. Probes for the table to avoid |
| 312 | 312 | # crashing pre-migration installs. Silently bails on any error so |
| 313 | 313 | # Config.pm never takes the site down. |
| 314 | 314 | sub _load_db_cache { |
| 315 | 315 | my ($self) = @_; |
| 316 | 316 | return if $_DB_LOADED; |
| 317 | 317 | $_DB_LOADED = 1; |
| 318 | 318 | |
| 319 | 319 | # We deliberately defer the require until called rather than at |
| 320 | 320 | # module-load -- avoids a circular use between Config and DBConnect |
| 321 | 321 | # in early-boot paths. |
| 322 | 322 | my $db_pkg = 'MODS::DBConnect'; |
| 323 | 323 | eval "require $db_pkg; 1" or return; |
| 324 | 324 | my $db = $db_pkg->new; |
| 325 | 325 | return unless $db; |
| 326 | 326 | |
| 327 | 327 | my $dbh = eval { $db->db_connect() }; |
| 328 | 328 | return unless $dbh; |
| 329 | 329 | |
| 330 | 330 | my $DB = $DEFAULTS{database_name}; |
| 331 | 331 | |
| 332 | 332 | my $col_check = eval { |
| 333 | 333 | $db->db_readwrite($dbh, qq~ |
| 334 | 334 | SELECT COUNT(*) AS n FROM information_schema.tables |
| 335 | 335 | WHERE table_schema='$DB' AND table_name='platform_settings' |
| 336 | 336 | ~, $ENV{SCRIPT_NAME}, __LINE__); |
| 337 | 337 | }; |
| 338 | 338 | unless ($col_check && $col_check->{n}) { |
| 339 | 339 | $_DB_AVAILABLE = 0; |
| 340 | 340 | eval { $db->db_disconnect($dbh) }; |
| 341 | 341 | return; |
| 342 | 342 | } |
| 343 | 343 | $_DB_AVAILABLE = 1; |
| 344 | 344 | |
| 345 | 345 | my @rows; |
| 346 | 346 | eval { |
| 347 | 347 | @rows = $db->db_readwrite_multiple($dbh, qq~ |
| 348 | 348 | SELECT setting_key, setting_value |
| 349 | 349 | FROM `${DB}`.platform_settings |
| 350 | 350 | ~, $ENV{SCRIPT_NAME}, __LINE__); |
| 351 | 351 | 1; |
| 352 | 352 | }; |
| 353 | 353 | foreach my $r (@rows) { |
| 354 | 354 | next unless ref($r) eq 'HASH'; |
| 355 | 355 | next unless defined $r->{setting_key}; |
| 356 | 356 | my $val = defined $r->{setting_value} ? $r->{setting_value} : ''; |
| 357 | 357 | next unless length $val; # empty rows defer to file/code so admins can "blank" a field |
| 358 | 358 | $_DB_CACHE{ $r->{setting_key} } = $val; |
| 359 | 359 | } |
| 360 | 360 | eval { $db->db_disconnect($dbh) }; |
| 361 | 361 | } |
| 362 | 362 | |
| 363 | 363 | # Reads /var/www/vhosts/webstls.com/private/<file> and returns the |
| 364 | 364 | # value for the requested key. Returns undef if the file or key is |
| 365 | 365 | # missing. Cached per-process. |
| 366 | 366 | my %_PRIVATE_CACHE; |
| 367 | 367 | sub _read_private { |
| 368 | 368 | my ($file, $key) = @_; |
| 369 | 369 | my $path = '/var/www/vhosts/webstls.com/private/' . $file; |
| 370 | 370 | unless (exists $_PRIVATE_CACHE{$path}) { |
| 371 | 371 | my %h; |
| 372 | 372 | if (open(my $fh, '<', $path)) { |
| 373 | 373 | while (my $line = <$fh>) { |
| 374 | 374 | chomp $line; |
| 375 | 375 | next if $line =~ /^\s*#/; |
| 376 | 376 | next unless $line =~ /^([A-Za-z0-9_]+)\s*=\s*(.*)$/; |
| 377 | 377 | $h{$1} = $2; |
| 378 | 378 | } |
| 379 | 379 | close $fh; |
| 380 | 380 | } |
| 381 | 381 | $_PRIVATE_CACHE{$path} = \%h; |
| 382 | 382 | } |
| 383 | 383 | return $_PRIVATE_CACHE{$path}->{$key}; |
| 384 | 384 | } |
| 385 | 385 | |
| 386 | 386 | # Returns the {url_key => "/path?cachekey"} hash used by Template.pm's |
| 387 | 387 | # [url:foo] tag. Mirrors MODS::Urls but kept WebSTLs-local so we never |
| 388 | 388 | # collide with the existing portal URL registry. |
| 389 | 389 | sub urls { |
| 390 | 390 | my $self = shift; |
| 391 | 391 | return { |
| 392 | 392 | home => '/index.cgi', |
| 393 | 393 | login => '/login.cgi', |
| 394 | 394 | signup => '/signup.cgi', |
| 395 | 395 | logout => '/logout.cgi', |
| 396 | 396 | dashboard => '/dashboard.cgi', |
| 397 | 397 | models => '/models.cgi', |
| 398 | 398 | upload => '/upload_model.cgi', |
| 399 | 399 | storefront => '/storefront.cgi', |
| 400 | 400 | analytics => '/analytics.cgi', |
| 401 | 401 | optimization => '/optimization.cgi', |
| 402 | 402 | settings => '/profile.cgi', |
| 403 | 403 | admin => '/admin.cgi', |
| 404 | 404 | audience => '/audience.cgi', |
| 405 | 405 | assets => '/assets', |
| 406 | 406 | assets_css => '/assets/css', |
| 407 | 407 | assets_js => '/assets/javascript', |
| 408 | 408 | assets_images => '/assets/images', |
| 409 | 409 | }; |
| 410 | 410 | } |
| 411 | 411 | |
| 412 | 412 | 1; |