added on WebSTLs (webstls.com) at 2026-07-01 22:26:38
| 1 | package MODS::WebSTLs::Config; | |
| 2 | #====================================================================== | |
| 3 | # WebSTLs — central configuration. | |
| 4 | # | |
| 5 | # Lookup order for settings(): | |
| 6 | # 1. platform_settings table (DB-backed) -- editable via | |
| 7 | # /admin_config.cgi by a super-admin. Highest precedence so an | |
| 8 | # admin can override anything without a code deploy. | |
| 9 | # 2. /var/www/vhosts/webstls.com/private/stripe.conf -- legacy | |
| 10 | # bootstrap path for the Stripe secret + webhook secret on a | |
| 11 | # brand-new install (before the admin has opened the config page). | |
| 12 | # 3. The hardcoded %DEFAULTS hash below -- final fallback. These are | |
| 13 | # also the deploy-coupled keys that must stay in code so they're | |
| 14 | # readable before the DB is reachable (database_name, asset paths, | |
| 15 | # cookie/session, feature-flag fallbacks). | |
| 16 | # | |
| 17 | # Lookups are cached per request inside %_DB_CACHE so a page that | |
| 18 | # reads ten settings only hits the DB once. The DB probe is column- | |
| 19 | # existence-guarded so pre-migration installs degrade cleanly to the | |
| 20 | # file + code fallbacks. | |
| 21 | #====================================================================== | |
| 22 | ||
| 23 | use strict; | |
| 24 | use warnings; | |
| 25 | ||
| 26 | # Whitelist of keys the admin can override via the Software | |
| 27 | # Configuration page. Anything not in this list is read straight from | |
| 28 | # %DEFAULTS and is_secret marking controls UI masking. | |
| 29 | # | |
| 30 | # Note: this also defines what /admin_config.cgi can WRITE. Adding a | |
| 31 | # key here is the one-line "expose this to the admin" switch. | |
| 32 | my %EDITABLE = ( | |
| 33 | session_minutes => { | |
| 34 | label => 'Session length (minutes)', | |
| 35 | group => 'Defaults', secret => 0, | |
| 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 | }, | |
| 38 | # Stripe | |
| 39 | stripe_publishable => { | |
| 40 | label => 'Stripe publishable key', | |
| 41 | group => 'Stripe', secret => 0, | |
| 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 | }, | |
| 44 | stripe_secret => { | |
| 45 | label => 'Stripe secret key', | |
| 46 | group => 'Stripe', secret => 1, | |
| 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 | }, | |
| 49 | stripe_connect_client => { | |
| 50 | label => 'Stripe Connect client id', | |
| 51 | group => 'Stripe', secret => 0, | |
| 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 | }, | |
| 54 | stripe_webhook_secret => { | |
| 55 | label => 'Stripe webhook signing secret', | |
| 56 | group => 'Stripe', secret => 1, | |
| 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 | }, | |
| 59 | stripe_platform_fee_bps => { | |
| 60 | label => 'Platform fee (basis points)', | |
| 61 | group => 'Stripe', secret => 0, | |
| 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 | }, | |
| 64 | stripe_currency_default => { | |
| 65 | label => 'Default currency code', | |
| 66 | group => 'Stripe', secret => 0, | |
| 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 | }, | |
| 69 | ||
| 70 | # Branding | |
| 71 | website_title => { | |
| 72 | label => 'Site title', | |
| 73 | group => 'Branding', secret => 0, | |
| 74 | description => 'The HTML <title> tag value -- shown in browser tabs, search results, and link previews. HTML entities are allowed (e.g. &mdash;).', | |
| 75 | }, | |
| 76 | brand_name => { | |
| 77 | label => 'Brand name', | |
| 78 | group => 'Branding', secret => 0, | |
| 79 | description => 'Short product name shown in the wrapper header, profile dropdown, marketing pages, and the From-line on system emails.', | |
| 80 | }, | |
| 81 | brand_tagline => { | |
| 82 | label => 'Tagline', | |
| 83 | group => 'Branding', secret => 0, | |
| 84 | description => 'One-line tagline shown under the brand name in the sidebar header and a few marketing pages.', | |
| 85 | }, | |
| 86 | ||
| 87 | ||
| 88 | feedback_emails => { | |
| 89 | label => 'Support / feedback inbox', | |
| 90 | group => 'Email', secret => 0, | |
| 91 | description => 'Inbox that receives "Contact us" form submissions. Comma-separate for multiple recipients (e.g. "support@example.com, ops@example.com").', | |
| 92 | }, | |
| 93 | from_email => { | |
| 94 | label => 'Outbound "From" address', | |
| 95 | group => 'Email', secret => 0, | |
| 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 | }, | |
| 98 | smtp_host => { | |
| 99 | label => 'SMTP host', | |
| 100 | group => 'Email', secret => 0, | |
| 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 | }, | |
| 103 | smtp_port => { | |
| 104 | label => 'SMTP port', | |
| 105 | group => 'Email', secret => 0, | |
| 106 | description => 'Usually 587 for STARTTLS or 465 for implicit TLS. Only consulted when SMTP host is set.', | |
| 107 | }, | |
| 108 | smtp_user => { | |
| 109 | label => 'SMTP username', | |
| 110 | group => 'Email', secret => 0, | |
| 111 | description => 'Authentication username for the SMTP server. Often your full email address. Leave blank if your SMTP server allows unauthenticated relay (uncommon).', | |
| 112 | }, | |
| 113 | smtp_pass => { | |
| 114 | label => 'SMTP password', | |
| 115 | group => 'Email', secret => 1, | |
| 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 | }, | |
| 118 | ||
| 119 | # File storage (R2) | |
| 120 | r2_bucket => { | |
| 121 | label => 'R2 bucket name', | |
| 122 | group => 'Storage', secret => 0, | |
| 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 | }, | |
| 125 | r2_public_base => { | |
| 126 | label => 'R2 public base URL', | |
| 127 | group => 'Storage', secret => 0, | |
| 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 | }, | |
| 130 | r2_account_id => { | |
| 131 | label => 'R2 account ID', | |
| 132 | group => 'Storage', secret => 0, | |
| 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 | }, | |
| 135 | r2_access_key_id => { | |
| 136 | label => 'R2 access key ID', | |
| 137 | group => 'Storage', secret => 0, | |
| 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 | }, | |
| 140 | r2_secret_access_key => { | |
| 141 | label => 'R2 secret access key', | |
| 142 | group => 'Storage', secret => 1, | |
| 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 | }, | |
| 145 | r2_region => { | |
| 146 | label => 'R2 region', | |
| 147 | group => 'Storage', secret => 0, | |
| 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 | }, | |
| 150 | ||
| 151 | # Defaults | |
| 152 | timezone_default => { | |
| 153 | label => 'Default timezone', | |
| 154 | group => 'Defaults', secret => 0, | |
| 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 | }, | |
| 157 | currency_default => { | |
| 158 | label => 'Default currency', | |
| 159 | group => 'Defaults', secret => 0, | |
| 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 | }, | |
| 162 | ||
| 163 | # ----- Storage caps + compression --------------------------------- | |
| 164 | # Per-tier per-file upload caps (megabytes). Drives the upload gate | |
| 165 | # in /upload_model.cgi and the user dashboard storage gauge. | |
| 166 | upload_max_mb_free => { label => 'Free upload cap (MB)', group => 'Storage', secret => 0, | |
| 167 | description => 'Max size of a single model file a Free user can upload. Bigger files are rejected at /upload_model.cgi.' }, | |
| 168 | upload_max_mb_starter => { label => 'Starter upload cap (MB)', group => 'Storage', secret => 0, | |
| 169 | description => 'Max single-file upload size for Starter users.' }, | |
| 170 | upload_max_mb_pro => { label => 'Pro upload cap (MB)', group => 'Storage', secret => 0, | |
| 171 | description => 'Max single-file upload size for Pro users.' }, | |
| 172 | upload_max_mb_studio => { label => 'Studio upload cap (MB)', group => 'Storage', secret => 0, | |
| 173 | description => 'Max single-file upload size for Studio users.' }, | |
| 174 | ||
| 175 | # Per-tier total-storage caps (gigabytes). | |
| 176 | storage_hard_cap_gb_free => { label => 'Free total storage (GB)', group => 'Storage', secret => 0, | |
| 177 | description => 'Total bytes a Free user can store. Per-user override on /admin_user.cgi wins.' }, | |
| 178 | storage_hard_cap_gb_starter => { label => 'Starter total storage (GB)', group => 'Storage', secret => 0, | |
| 179 | description => 'Total bytes a Starter user can store.' }, | |
| 180 | storage_hard_cap_gb_pro => { label => 'Pro total storage (GB)', group => 'Storage', secret => 0, | |
| 181 | description => 'Total bytes a Pro user can store.' }, | |
| 182 | storage_hard_cap_gb_studio => { label => 'Studio total storage (GB)', group => 'Storage', secret => 0, | |
| 183 | description => 'Total bytes a Studio user can store.' }, | |
| 184 | ||
| 185 | # Allowed extensions -- per-tier (preferred) + legacy global fallback. | |
| 186 | # Resolution: user override > tier-specific > global > hardcoded. | |
| 187 | allowed_file_extensions_free => { label => 'Free allowed extensions', group => 'Storage', secret => 0, | |
| 188 | description => 'Comma-separated, lowercase, no dots. Shipped narrow (e.g. stl,obj) so .step / .iges / .fbx require an upgrade.' }, | |
| 189 | allowed_file_extensions_starter => { label => 'Starter allowed extensions', group => 'Storage', secret => 0, | |
| 190 | description => 'Comma-separated, lowercase, no dots. Ship default adds 3MF + PLY + glTF so hobbyists with Bambu/Prusa workflows are covered.' }, | |
| 191 | allowed_file_extensions_pro => { label => 'Pro allowed extensions', group => 'Storage', secret => 0, | |
| 192 | description => 'Comma-separated, lowercase, no dots. Ships with the full CAD set including STEP/IGES/FBX.' }, | |
| 193 | allowed_file_extensions_studio => { label => 'Studio allowed extensions', group => 'Storage', secret => 0, | |
| 194 | description => 'Comma-separated, lowercase, no dots. Ships identical to Pro by default; raise the floor here if you want to differentiate Studio further.' }, | |
| 195 | allowed_file_extensions => { label => 'Global allowed extensions (legacy fallback)', group => 'Storage', secret => 0, | |
| 196 | description => 'Comma-separated, lowercase, no dots. Used only when the per-tier key for the user\'s tier is unset. Kept for back-compat -- prefer the per-tier keys above.' }, | |
| 197 | ||
| 198 | storage_soft_alert_pct => { label => 'Soft-alert threshold (%)', group => 'Storage', secret => 0, | |
| 199 | description => 'Percent of total cap at which the dashboard gauge turns amber and the hourly worker queues an email nudge. Default 80.' }, | |
| 200 | ||
| 201 | # On-store compression toggles. | |
| 202 | storage_compression_enabled => { label => 'Compress uploads on store', group => 'Storage', secret => 0, | |
| 203 | description => '1 = compress STL/OBJ/etc. on upload (zstd if available, else gzip). 0 = store raw. Already-packed formats (3MF / GLB / ZIP) are always stored raw regardless.' }, | |
| 204 | storage_compression_algo => { label => 'Compression algorithm', group => 'Storage', secret => 0, | |
| 205 | description => '"auto" (recommended) picks zstd when /usr/bin/zstd is present, otherwise gzip. Force-pin to "zstd" or "gzip" only if you need a specific algo on this host.' }, | |
| 206 | ||
| 207 | # The actual root for paid model files. MUST be OUTSIDE httpdocs so | |
| 208 | # Apache can never serve the bytes directly -- everything flows | |
| 209 | # through /download.cgi which checks order + ownership before | |
| 210 | # streaming. The CGI user (stlsweb) needs read+write on this path. | |
| 211 | private_files_dir => { label => 'Private files directory', group => 'Storage', secret => 0, | |
| 212 | description => 'Absolute filesystem path OUTSIDE the web root where uploaded model files (.stl/.obj/etc) are stored. The CGI user must own it. NOTE: changing this does NOT move existing files -- run /_storage_backfill.pl on the server to migrate.' }, | |
| 213 | ); | |
| 214 | ||
| 215 | my %DEFAULTS = ( | |
| 216 | # ----- Database ---------------------------------------------- | |
| 217 | # NOT editable via UI -- the lookup itself depends on this. | |
| 218 | database_name => 'WEBSTLs', | |
| 219 | ||
| 220 | # Storage root for paid model files. OUTSIDE httpdocs by design -- | |
| 221 | # served only through /download.cgi after order-ownership check. | |
| 222 | private_files_dir => '/var/www/vhosts/webstls.com/private/files', | |
| 223 | ||
| 224 | # ----- Branding ---------------------------------------------- | |
| 225 | website_title => 'WebSTLs — 3D Printable Designs Platform', | |
| 226 | brand_name => 'WebSTLs', | |
| 227 | brand_tagline => 'The operating system for 3D creators', | |
| 228 | ||
| 229 | # ----- Sessions / cookies ------------------------------------ | |
| 230 | # NOT editable via UI -- deploy-coupled. Edit MODS/WebSTLs/Config.pm. | |
| 231 | cookie_name => 'webstls_session', | |
| 232 | session_minutes => 720, | |
| 233 | secure_cookies => 1, | |
| 234 | # Domain scope for the session cookie. Leading-dot form means the | |
| 235 | # cookie is sent to BOTH apex (webstls.com) and every subdomain | |
| 236 | # (e.g. 3dshawn.webstls.com), so when a seller is logged in on the | |
| 237 | # dashboard their session is recognized on their own storefront | |
| 238 | # subdomain too (needed for owner-only features like draft bundle | |
| 239 | # previews, edit-mode bypass, etc). Empty string keeps the cookie | |
| 240 | # host-only (legacy behavior). | |
| 241 | cookie_domain => '.webstls.com', | |
| 242 | ||
| 243 | # ----- Asset paths ------------------------------------------- | |
| 244 | # NOT editable via UI -- deploy-coupled. | |
| 245 | assets => '/assets', | |
| 246 | assets_css => '/assets/css', | |
| 247 | assets_js => '/assets/javascript', | |
| 248 | assets_images => '/assets/images', | |
| 249 | ||
| 250 | # ----- Email / mail ------------------------------------------ | |
| 251 | feedback_emails => 'support@webstls.com', | |
| 252 | from_email => 'noreply@webstls.com', | |
| 253 | smtp_host => '', # blank = use local sendmail | |
| 254 | smtp_port => 587, | |
| 255 | smtp_user => '', | |
| 256 | smtp_pass => '', | |
| 257 | ||
| 258 | # ----- Stripe ------------------------------------------------ | |
| 259 | stripe_publishable => 'pk_test_REPLACE_ME', | |
| 260 | stripe_secret => '', | |
| 261 | stripe_connect_client => 'ca_REPLACE_ME', | |
| 262 | stripe_webhook_secret => '', | |
| 263 | stripe_platform_fee_bps => 1000, | |
| 264 | stripe_currency_default => 'usd', | |
| 265 | ||
| 266 | # ----- File storage (R2) ------------------------------------- | |
| 267 | r2_bucket => 'webstls-files', | |
| 268 | r2_public_base => 'https://files.webstls.com', | |
| 269 | r2_account_id => '', | |
| 270 | r2_access_key_id => '', | |
| 271 | r2_secret_access_key => '', | |
| 272 | r2_region => 'auto', | |
| 273 | ||
| 274 | # ----- Plan limits ------------------------------------------- | |
| 275 | plan_free_models => 10, | |
| 276 | plan_starter_price => 19, | |
| 277 | plan_pro_price => 49, | |
| 278 | plan_studio_price => 129, | |
| 279 | ||
| 280 | # ----- Feature flags fallback (when DB unreachable) ---------- | |
| 281 | # NOT editable via UI -- read when DB is DOWN, so DB rows would | |
| 282 | # never apply anyway. Keep in code. | |
| 283 | flag_default__module_publishing => 1, | |
| 284 | flag_default__module_storefront => 1, | |
| 285 | flag_default__module_optimization => 1, | |
| 286 | flag_default__module_pooled_data => 0, | |
| 287 | flag_default__module_audience => 1, | |
| 288 | ||
| 289 | # ----- Misc -------------------------------------------------- | |
| 290 | timezone_default => 'UTC', | |
| 291 | currency_default => 'USD', | |
| 292 | ); | |
| 293 | ||
| 294 | # Per-process cache. Populated on first DB hit; survives for the | |
| 295 | # request and is cheap on subsequent settings() calls. | |
| 296 | my %_DB_CACHE; | |
| 297 | my $_DB_LOADED = 0; | |
| 298 | my $_DB_AVAILABLE; # undef = unknown, 0 = table missing, 1 = ready | |
| 299 | ||
| 300 | sub new { | |
| 301 | my ($class) = @_; | |
| 302 | return bless({}, $class); | |
| 303 | } | |
| 304 | ||
| 305 | # Public helper for the admin config page. Returns the editable | |
| 306 | # whitelist with current values + source ('db' | 'file' | 'default'). | |
| 307 | # is_secret rows have their value masked to a non-empty marker so the | |
| 308 | # UI can show "set" without leaking the actual secret. | |
| 309 | sub editable_settings_meta { | |
| 310 | my ($self) = @_; | |
| 311 | my @rows; | |
| 312 | foreach my $key (sort keys %EDITABLE) { | |
| 313 | my $meta = $EDITABLE{$key}; | |
| 314 | my ($val, $source) = $self->_resolve_with_source($key); | |
| 315 | my $masked = ($meta->{secret} && defined($val) && length $val) ? 1 : 0; | |
| 316 | push @rows, { | |
| 317 | key => $key, | |
| 318 | label => $meta->{label}, | |
| 319 | group => $meta->{group}, | |
| 320 | secret => $meta->{secret}, | |
| 321 | description => $meta->{description} || '', | |
| 322 | value => $masked ? '' : (defined $val ? $val : ''), | |
| 323 | is_set => (defined $val && length $val) ? 1 : 0, | |
| 324 | source => $source, | |
| 325 | }; | |
| 326 | } | |
| 327 | return @rows; | |
| 328 | } | |
| 329 | ||
| 330 | # Bool -- is this key editable via the admin config page? | |
| 331 | sub is_editable { | |
| 332 | my ($self, $key) = @_; | |
| 333 | return exists $EDITABLE{$key} ? 1 : 0; | |
| 334 | } | |
| 335 | ||
| 336 | sub editable_meta_for { | |
| 337 | my ($self, $key) = @_; | |
| 338 | return $EDITABLE{$key}; | |
| 339 | } | |
| 340 | ||
| 341 | sub settings { | |
| 342 | my ($self, $key) = @_; | |
| 343 | return undef unless defined $key; | |
| 344 | ||
| 345 | my ($val) = $self->_resolve_with_source($key); | |
| 346 | return $val; | |
| 347 | } | |
| 348 | ||
| 349 | # Core lookup. Returns ($value, $source) where source is one of | |
| 350 | # 'db' | 'file' | 'default'. Centralises the precedence so any caller | |
| 351 | # (settings() / editable_settings_meta) gets the same answer. | |
| 352 | sub _resolve_with_source { | |
| 353 | my ($self, $key) = @_; | |
| 354 | ||
| 355 | # 1) DB lookup (cached). Skip for the database_name itself -- we | |
| 356 | # can't ask the DB which DB to use. | |
| 357 | if ($key ne 'database_name') { | |
| 358 | $self->_load_db_cache; | |
| 359 | if (exists $_DB_CACHE{$key}) { | |
| 360 | return ($_DB_CACHE{$key}, 'db'); | |
| 361 | } | |
| 362 | } | |
| 363 | ||
| 364 | # 2) Private file fallback (legacy Stripe secret path). Keeps | |
| 365 | # brand-new installs working until the admin opens the config | |
| 366 | # page. | |
| 367 | if ($key eq 'stripe_secret' || $key eq 'stripe_webhook_secret') { | |
| 368 | my $val = _read_private('stripe.conf', $key); | |
| 369 | return ($val, 'file') if defined $val && length $val; | |
| 370 | } | |
| 371 | ||
| 372 | # 3) Hardcoded default. | |
| 373 | return (exists $DEFAULTS{$key} ? $DEFAULTS{$key} : undef, 'default'); | |
| 374 | } | |
| 375 | ||
| 376 | # Single-pass population of %_DB_CACHE. Probes for the table to avoid | |
| 377 | # crashing pre-migration installs. Silently bails on any error so | |
| 378 | # Config.pm never takes the site down. | |
| 379 | sub _load_db_cache { | |
| 380 | my ($self) = @_; | |
| 381 | return if $_DB_LOADED; | |
| 382 | $_DB_LOADED = 1; | |
| 383 | ||
| 384 | # We deliberately defer the require until called rather than at | |
| 385 | # module-load -- avoids a circular use between Config and DBConnect | |
| 386 | # in early-boot paths. | |
| 387 | my $db_pkg = 'MODS::DBConnect'; | |
| 388 | eval "require $db_pkg; 1" or return; | |
| 389 | my $db = $db_pkg->new; | |
| 390 | return unless $db; | |
| 391 | ||
| 392 | my $dbh = eval { $db->db_connect() }; | |
| 393 | return unless $dbh; | |
| 394 | ||
| 395 | my $DB = $DEFAULTS{database_name}; | |
| 396 | ||
| 397 | my $col_check = eval { | |
| 398 | $db->db_readwrite($dbh, qq~ | |
| 399 | SELECT COUNT(*) AS n FROM information_schema.tables | |
| 400 | WHERE table_schema='$DB' AND table_name='platform_settings' | |
| 401 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 402 | }; | |
| 403 | unless ($col_check && $col_check->{n}) { | |
| 404 | $_DB_AVAILABLE = 0; | |
| 405 | eval { $db->db_disconnect($dbh) }; | |
| 406 | return; | |
| 407 | } | |
| 408 | $_DB_AVAILABLE = 1; | |
| 409 | ||
| 410 | my @rows; | |
| 411 | eval { | |
| 412 | @rows = $db->db_readwrite_multiple($dbh, qq~ | |
| 413 | SELECT setting_key, setting_value | |
| 414 | FROM `${DB}`.platform_settings | |
| 415 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 416 | 1; | |
| 417 | }; | |
| 418 | foreach my $r (@rows) { | |
| 419 | next unless ref($r) eq 'HASH'; | |
| 420 | next unless defined $r->{setting_key}; | |
| 421 | my $val = defined $r->{setting_value} ? $r->{setting_value} : ''; | |
| 422 | next unless length $val; # empty rows defer to file/code so admins can "blank" a field | |
| 423 | $_DB_CACHE{ $r->{setting_key} } = $val; | |
| 424 | } | |
| 425 | eval { $db->db_disconnect($dbh) }; | |
| 426 | } | |
| 427 | ||
| 428 | # Reads /var/www/vhosts/webstls.com/private/<file> and returns the | |
| 429 | # value for the requested key. Returns undef if the file or key is | |
| 430 | # missing. Cached per-process. | |
| 431 | my %_PRIVATE_CACHE; | |
| 432 | sub _read_private { | |
| 433 | my ($file, $key) = @_; | |
| 434 | my $path = '/var/www/vhosts/webstls.com/private/' . $file; | |
| 435 | unless (exists $_PRIVATE_CACHE{$path}) { | |
| 436 | my %h; | |
| 437 | if (open(my $fh, '<', $path)) { | |
| 438 | while (my $line = <$fh>) { | |
| 439 | chomp $line; | |
| 440 | next if $line =~ /^\s*#/; | |
| 441 | next unless $line =~ /^([A-Za-z0-9_]+)\s*=\s*(.*)$/; | |
| 442 | $h{$1} = $2; | |
| 443 | } | |
| 444 | close $fh; | |
| 445 | } | |
| 446 | $_PRIVATE_CACHE{$path} = \%h; | |
| 447 | } | |
| 448 | return $_PRIVATE_CACHE{$path}->{$key}; | |
| 449 | } | |
| 450 | ||
| 451 | # Returns the {url_key => "/path?cachekey"} hash used by Template.pm's | |
| 452 | # [url:foo] tag. Mirrors MODS::Urls but kept WebSTLs-local so we never | |
| 453 | # collide with the existing portal URL registry. | |
| 454 | sub urls { | |
| 455 | my $self = shift; | |
| 456 | return { | |
| 457 | home => '/index.cgi', | |
| 458 | login => '/login.cgi', | |
| 459 | signup => '/signup.cgi', | |
| 460 | logout => '/logout.cgi', | |
| 461 | dashboard => '/dashboard.cgi', | |
| 462 | models => '/models.cgi', | |
| 463 | upload => '/upload_model.cgi', | |
| 464 | storefront => '/storefront.cgi', | |
| 465 | analytics => '/analytics.cgi', | |
| 466 | optimization => '/optimization.cgi', | |
| 467 | settings => '/profile.cgi', | |
| 468 | profile => '/profile.cgi', | |
| 469 | preferences => '/preferences.cgi', | |
| 470 | admin => '/admin.cgi', | |
| 471 | messages => '/messages.cgi', | |
| 472 | audience => '/audience.cgi', | |
| 473 | assets => '/assets', | |
| 474 | assets_css => '/assets/css', | |
| 475 | assets_js => '/assets/javascript', | |
| 476 | assets_images => '/assets/images', | |
| 477 | }; | |
| 478 | } | |
| 479 | ||
| 480 | 1; |